From 5258cff5cf967d8fa06b3b29d29195a63d64b0f6 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 29 Sep 2023 18:50:57 +0200 Subject: [PATCH 001/245] update openReport --- src/components/ReportActionItem/MoneyRequestAction.js | 4 ++-- src/libs/actions/Report.js | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestAction.js b/src/components/ReportActionItem/MoneyRequestAction.js index b4a5e010b7a8..5ed47e4a244d 100644 --- a/src/components/ReportActionItem/MoneyRequestAction.js +++ b/src/components/ReportActionItem/MoneyRequestAction.js @@ -107,11 +107,11 @@ function MoneyRequestAction({ if (!childReportID) { const thread = ReportUtils.buildTransactionThread(action, requestReportID); const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs); - Report.openReport(thread.reportID, userLogins, thread, action.reportActionID); + Report.openReport({reportID: thread.reportID}, userLogins, thread, action.reportActionID); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(thread.reportID)); return; } - Report.openReport(childReportID); + Report.openReport({reportID: childReportID}); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID)); }; diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index a6e115fe5d9c..808bec8faee4 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -443,13 +443,14 @@ function reportActionsExist(reportID) { * If a chat with the passed reportID is not found, we will create a chat based on the passed participantList * * @param {String} reportID + * @param {String} reportActionID * @param {Array} participantLoginList The list of users that are included in a new chat, not including the user creating it * @param {Object} newReportObject The optimistic report object created when making a new chat, saved as optimistic data * @param {String} parentReportActionID The parent report action that a thread was created from (only passed for new threads) * @param {Boolean} isFromDeepLink Whether or not this report is being opened from a deep link * @param {Array} participantAccountIDList The list of accountIDs that are included in a new chat, not including the user creating it */ -function openReport(reportID, participantLoginList = [], newReportObject = {}, parentReportActionID = '0', isFromDeepLink = false, participantAccountIDList = []) { +function openReport({reportID, reportActionID = ''}, participantLoginList = [], newReportObject = {}, parentReportActionID = '0', isFromDeepLink = false, participantAccountIDList = []) { const optimisticReportData = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -512,6 +513,7 @@ function openReport(reportID, participantLoginList = [], newReportObject = {}, p const params = { reportID, + reportActionID, emailList: participantLoginList ? participantLoginList.join(',') : '', accountIDList: participantAccountIDList ? participantAccountIDList.join(',') : '', parentReportActionID, From 5cd7e4c36cb1951901dcdf1fb16b8d907466a17a Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 29 Sep 2023 18:52:25 +0200 Subject: [PATCH 002/245] update the rest openReport calls --- src/pages/home/report/ContextMenu/ContextMenuActions.js | 4 ++-- .../home/report/withReportAndReportActionOrNotFound.js | 2 +- tests/actions/IOUTest.js | 8 ++++---- tests/actions/ReportTest.js | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.js index 157ae66dc918..e19927bfb33b 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.js +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.js @@ -287,11 +287,11 @@ export default [ if (!childReportID) { const thread = ReportUtils.buildTransactionThread(reportAction, reportID); const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs); - Report.openReport(thread.reportID, userLogins, thread, reportAction.reportActionID); + Report.openReport({reportID: thread.reportID}, userLogins, thread, reportAction.reportActionID); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(thread.reportID)); return; } - Report.openReport(childReportID); + Report.openReport({reportID: childReportID}); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID)); return; } diff --git a/src/pages/home/report/withReportAndReportActionOrNotFound.js b/src/pages/home/report/withReportAndReportActionOrNotFound.js index 47f499423d28..19bbf5b36e32 100644 --- a/src/pages/home/report/withReportAndReportActionOrNotFound.js +++ b/src/pages/home/report/withReportAndReportActionOrNotFound.js @@ -97,7 +97,7 @@ export default function (WrappedComponent) { if (!props.isSmallScreenWidth || (!_.isEmpty(props.report) && !_.isEmpty(reportAction))) { return; } - Report.openReport(props.route.params.reportID); + Report.openReport({reportID: props.route.params.reportID}); // eslint-disable-next-line react-hooks/exhaustive-deps }, [props.isSmallScreenWidth, props.route.params.reportID]); diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index 3df3b137bab3..ef613c26020a 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -1714,7 +1714,7 @@ describe('actions/IOU', () => { const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs); // When Opening a thread report with the given details - Report.openReport(thread.reportID, userLogins, thread, createIOUAction.reportActionID); + Report.openReport({reportID: thread.reportID}, userLogins, thread, createIOUAction.reportActionID); await waitForBatchedUpdates(); // Then The iou action has the transaction report id as a child report ID @@ -1780,7 +1780,7 @@ describe('actions/IOU', () => { thread = ReportUtils.buildTransactionThread(createIOUAction); const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs); jest.advanceTimersByTime(10); - Report.openReport(thread.reportID, userLogins, thread, createIOUAction.reportActionID); + Report.openReport({reportID: thread.reportID}, userLogins, thread, createIOUAction.reportActionID); await waitForBatchedUpdates(); Onyx.connect({ @@ -1870,7 +1870,7 @@ describe('actions/IOU', () => { jest.advanceTimersByTime(10); const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs); - Report.openReport(thread.reportID, userLogins, thread, createIOUAction.reportActionID); + Report.openReport({reportID: thread.reportID}, userLogins, thread, createIOUAction.reportActionID); await waitForBatchedUpdates(); @@ -2083,7 +2083,7 @@ describe('actions/IOU', () => { jest.advanceTimersByTime(10); const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs); - Report.openReport(thread.reportID, userLogins, thread, createIOUAction.reportActionID); + Report.openReport({reportID: thread.reportID}, userLogins, thread, createIOUAction.reportActionID); await waitForBatchedUpdates(); const allReportActions = await new Promise((resolve) => { diff --git a/tests/actions/ReportTest.js b/tests/actions/ReportTest.js index 06d8304111cb..1968b4983e70 100644 --- a/tests/actions/ReportTest.js +++ b/tests/actions/ReportTest.js @@ -267,7 +267,7 @@ describe('actions/Report', () => { // When the user visits the report jest.advanceTimersByTime(10); currentTime = DateUtils.getDBTime(); - Report.openReport(REPORT_ID); + Report.openReport({reportID: REPORT_ID}); Report.readNewestAction(REPORT_ID); waitForBatchedUpdates(); return waitForBatchedUpdates(); From 65edb743531fa9c662388afd6f5db486375080a8 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Sat, 30 Sep 2023 10:18:59 +0200 Subject: [PATCH 003/245] WIP linking utils --- src/libs/ReportActionsUtils.js | 160 +++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js index 67c44784eeb2..29cd6cd9cc54 100644 --- a/src/libs/ReportActionsUtils.js +++ b/src/libs/ReportActionsUtils.js @@ -212,6 +212,163 @@ function getSortedReportActions(reportActions, shouldSortInDescendingOrder = fal .value(); } +// /** +// * Given an object of reportActions, sorts them, and then adds the previousReportActionID to each item except the first. +// * @param {Object} reportActions +// * @returns {Array} +// */ +// function processReportActions(reportActions) { +// // TODO: +// // Separate new and sorted reportActions +// const newReportActions = _.filter(reportActions, (action) => !action.previousReportActionID); +// const sortedReportActions = _.filter(reportActions, (action) => action.previousReportActionID); + +// // console.log( +// // 'getChat.Sort.N.0', +// // newReportActions.length, +// // newReportActions.map(({message}) => message[0].text), +// // ); +// // console.log( +// // 'getChat.Sort.N.0.0', +// // newReportActions.length, +// // newReportActions.map(({previousReportActionID}) => previousReportActionID), +// // ); +// // console.log( +// // 'getChat.Sort.S.0', +// // sortedReportActions.length, +// // sortedReportActions.map(({message}) => message[0].text), +// // ); +// // console.log( +// // 'getChat.Sort.S.0.0', +// // sortedReportActions.length, +// // sortedReportActions.map(({previousReportActionID}) => previousReportActionID), +// // ); +// // Sort the new reportActions +// const sortedNewReportActions = getSortedReportActionsForDisplay(newReportActions); + +// // console.log( +// // 'getChat.SORT.SS', +// // JSON.stringify( +// // sortedNewReportActions.map(({message, reportActionID, previousReportActionID}) => ({ +// // message: message[0].text, +// // reportActionID, +// // previousReportActionID, +// // })), +// // ), +// // ); + +// // Then, iterate through the sorted new reportActions and add the previousReportActionID to each item except the first +// const processedReportActions = sortedNewReportActions.map((action, index) => { +// if (index === sortedNewReportActions.length - 1) { +// return action; // Return the first item as is +// } +// return { +// ...action, +// previousReportActionID: sortedNewReportActions[index + 1].reportActionID, +// }; +// }); + +// // console.log( +// // 'getChat.SORT.BEFORE', +// // JSON.stringify( +// // processedReportActions.map(({message, reportActionID, previousReportActionID}) => ({ +// // message: message[0].text, +// // reportActionID, +// // previousReportActionID, +// // })), +// // ), +// // ); +// if (processedReportActions[processedReportActions.length - 1]?.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED) { +// processedReportActions.pop(); +// } +// // console.log( +// // 'getChat.SORT.AFTER', +// // JSON.stringify( +// // processedReportActions.map(({message, reportActionID, previousReportActionID}) => ({ +// // message: message[0].text, +// // reportActionID, +// // previousReportActionID, +// // })), +// // ), +// // ); + +// // Determine the order of merging based on reportActionID values +// const lastSortedReportActionID = _.last(sortedReportActions)?.reportActionTimestamp || 0; +// const firstProcessedReportActionID = _.first(processedReportActions)?.reportActionTimestamp || Infinity; + +// // console.log('getChat.Sort.1', getSortedReportActionsForDisplay(reportActions).length, [...sortedReportActions, ...processedReportActions].length); +// // console.log('getChat.Sort.1.1', _.last(sortedReportActions), _.first(processedReportActions)); +// if (firstProcessedReportActionID > lastSortedReportActionID) { +// // console.log( +// // 'getChat.Sort.2', +// // [...sortedReportActions, ...processedReportActions].map(({message}) => message[0].text), +// // ); +// return [...sortedReportActions, ...processedReportActions]; +// } else { +// // console.log( +// // 'getChat.Sort.3', +// // [...processedReportActions, ...sortedReportActions].map(({message}) => message[0].text), +// // ); +// return [...processedReportActions, ...sortedReportActions]; +// } +// } + +// Usage: +// const reportActions = [ +// { reportActionID: '1' }, +// { reportActionID: '2', previousReportActionID: '1' }, +// { reportActionID: '3' } +// ]; +// const updatedActions = processReportActions(reportActions); +// console.log(updatedActions); + +function getRangeFromArrayByID(array, id) { + // without gaps + let index; + + if (id) { + index = array.findIndex((obj) => obj.reportActionID === id); + } else { + index = 0; + } + + if (index === -1) { + return []; + } + + let startIndex = index; + let endIndex = index; + + // Move down the list and compare reportActionID with previousReportActionID + while (endIndex < array.length - 1 && array[endIndex].previousReportActionID === array[endIndex + 1].reportActionID) { + endIndex++; + } + + // Move up the list and compare previousReportActionID with reportActionID + while (startIndex > 0 && array[startIndex].reportActionID === array[startIndex - 1].previousReportActionID) { + startIndex--; + } + + return array.slice(startIndex, endIndex + 1); + // return array.slice(startIndex, endIndex); +} + +function getSlicedRangeFromArrayByID(array, id) { + let index; + if (id) { + index = array.findIndex((obj) => obj.reportActionID === id); + } else { + index = array.length - 1; + } + + if (index === -1) { + return []; + } + + // return array.slice(0, index+1); + return array.slice(index, array.length); +} + /** * Finds most recent IOU request action ID. * @@ -696,4 +853,7 @@ export { getAllReportActions, isReportActionAttachment, isNotifiableReportAction, + // processReportActions, + getRangeFromArrayByID, + getSlicedRangeFromArrayByID, }; From 03804dbf5e3a4ef33c1496ec185cdf69f5e3e697 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 2 Oct 2023 16:22:58 +0200 Subject: [PATCH 004/245] WIP navigation, useRouteChangeHandler --- src/libs/ReportUtils.js | 26 +++++ src/pages/home/ReportScreen.js | 105 +++++++++++++++-- src/pages/home/report/ReportActionsView.js | 125 ++++++++++++++++++--- 3 files changed, 232 insertions(+), 24 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index c03858cb15f3..2be9e68ae451 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1,4 +1,5 @@ /* eslint-disable rulesdir/prefer-underscore-method */ +import {useEffect, useRef} from 'react'; import _ from 'underscore'; import {format, parseISO} from 'date-fns'; import Str from 'expensify-common/lib/str'; @@ -3654,6 +3655,30 @@ function getIOUReportActionDisplayMessage(reportAction) { return displayMessage; } +/** + * Hook that triggers a fetch when reportActionID appears while reportID stays the same. + * + * @param {string} reportID - The current reportID from the route. + * @param {string|null} reportActionID - The current reportActionID from the route or null. + * @param {function} triggerFetch - The function to be triggered on the condition. + */ +function useRouteChangeHandler(reportID = '', reportActionID = '', triggerFetch = () => {}) { + // Store the previous reportID and reportActionID + const previousReportIDRef = useRef(null); + const previousReportActionIDRef = useRef(null); + + useEffect(() => { + // Check if reportID is the same and reportActionID just appeared + if (reportID === previousReportIDRef.current && reportActionID && !previousReportActionIDRef.current) { + triggerFetch(); + } + + // Update refs for the next render + previousReportIDRef.current = reportID; + previousReportActionIDRef.current = reportActionID; + }, [reportID, reportActionID, triggerFetch]); +} + export { getReportParticipantsTitle, isReportMessageAttachment, @@ -3795,4 +3820,5 @@ export { hasMissingSmartscanFields, getIOUReportActionDisplayMessage, isWaitingForTaskCompleteFromAssignee, + useRouteChangeHandler, }; diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index aedc9247a21f..29cc51fab73a 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -1,3 +1,4 @@ +/* eslint-disable rulesdir/prefer-underscore-method */ import React, {useRef, useState, useEffect, useMemo, useCallback} from 'react'; import {withOnyx} from 'react-native-onyx'; import {useFocusEffect} from '@react-navigation/native'; @@ -63,7 +64,7 @@ const propTypes = { reportMetadata: reportMetadataPropTypes, /** Array of report actions for this report */ - reportActions: PropTypes.arrayOf(PropTypes.shape(reportActionPropTypes)), + sortedReportActions: PropTypes.arrayOf(PropTypes.shape(reportActionPropTypes)), /** Whether the composer is full size */ isComposerFullSize: PropTypes.bool, @@ -100,7 +101,7 @@ const propTypes = { const defaultProps = { isSidebarLoaded: false, - reportActions: [], + sortedReportActions: [], report: { hasOutstandingIOU: false, }, @@ -139,13 +140,26 @@ const checkDefaultReport = (report) => report === defaultProps.report; function getReportID(route) { return String(lodashGet(route, 'params.reportID', null)); } +/** + * Get the currently viewed report ID as number + * + * @param {Object} route + * @param {Object} route.params + * @param {String} route.params.reportID + * @returns {String} + */ +function getReportActionID(route) { + console.log('get.ROUTE', route?.params); + return {reportActionID: lodashGet(route, 'params.reportActionID', null), reportID: lodashGet(route, 'params.reportID', null)}; +} function ReportScreen({ betas, route, report, reportMetadata, - reportActions, + // reportActions, + sortedReportActions, accountManagerReportID, personalDetails, markReadyForHydration, @@ -156,6 +170,7 @@ function ReportScreen({ errors, userLeavingStatus, currentReportID, + updateCurrentReportID, }) { const {translate} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); @@ -165,9 +180,53 @@ function ReportScreen({ const reactionListRef = useRef(); const prevReport = usePrevious(report); const prevUserLeavingStatus = usePrevious(userLeavingStatus); + const {reportActionID, reportID} = getReportActionID(route); + const [isLinkingToMessage, setLinkingToMessageTrigger] = useState(false); + + const reportActions = useMemo(() => { + const val = ReportActionsUtils.getRangeFromArrayByID(sortedReportActions, reportActionID); + console.log('get.ROOT.reportActions', reportActionID, sortedReportActions.length, val.length); + return val; + }, [sortedReportActions, reportActionID]); + // const isReportActionArrayCatted = useMemo(() => sortedReportActions.length !== withoutGaps.length, [withoutGaps, sortedReportActions]); + + // const reportActions = useMemo(() => { + // //TODO: OpenReport, it means that we clicked on the link in current chat and we need to get a proper range of reportActions + // // console.log( + // // 'get.reportActions.initialSorted', + // // // sortedReportActions.length, + // // sortedReportActions.length, + // // !!reportActionID, + // // sortedReportActions.map((item) => { + // // return {message: item.message[0].text, previousReportActionID: item?.previousReportActionID, reportActionID: item?.reportActionID}; + // // }), + // // ); + // // // // console.log('get.reportActions.initialSorted', sortedReportActions.map((item) =>item?.previousReportActionID)); + // // console.log( + // // 'get.reportActions.withoutGaps', + // // // withoutGaps.length, + // // withoutGaps.length, + // // !!reportActionID, + // // withoutGaps.map((item) => { + // // return {message: item.message[0].text, previousReportActionID: item?.previousReportActionID, reportActionID: item?.reportActionID}; + // // }), + // // ); + // return withoutGaps; + // // return sortedReportActions; + // }, [sortedReportActions, reportActionID]); + + // const reportActionsBeforeAndIncludingLinked = useMemo(() => { + // if (reportActionID) { + // firstRenderRefI.current = false; + // return ReportActionsUtils.getSlicedRangeFromArrayByID(reportActions, reportActionID); + // } + // return null; + // }, [reportActions, reportActionID]); + + // const [skeletonViewContainerHeight, setSkeletonViewContainerHeight] = useState(0); + // >>>>>>> Stashed changes const [isBannerVisible, setIsBannerVisible] = useState(true); - const reportID = getReportID(route); const {addWorkspaceRoomOrChatPendingAction, addWorkspaceRoomOrChatErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report); const screenWrapperStyle = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}]; @@ -237,6 +296,8 @@ function ReportScreen({ return reportIDFromPath !== '' && report.reportID && !isTransitioning; }, [route, report]); + // ReportUtils.useRouteChangeHandler(reportID, reportActionID, () => Report.openReport({reportID: reportIDFromPath, reportActionID: reportActionID || ''})) + const fetchReportIfNeeded = useCallback(() => { const reportIDFromPath = getReportID(route); @@ -249,11 +310,26 @@ function ReportScreen({ // It possible that we may not have the report object yet in Onyx yet e.g. we navigated to a URL for an accessible report that // is not stored locally yet. If report.reportID exists, then the report has been stored locally and nothing more needs to be done. // If it doesn't exist, then we fetch the report from the API. - if (report.reportID && report.reportID === getReportID(route)) { + if (report.reportID && report.reportID === getReportID(route) && !reportActionID) { return; } - Report.openReport(reportIDFromPath); - }, [report.reportID, route]); + console.log('getChat.OPEN_REPORT', reportActionID); + Report.openReport({reportID: reportIDFromPath, reportActionID: reportActionID || ''}); + }, [report.reportID, route, reportActionID]); + + // useEffect(() => { + // console.log('get.ROUTE.0', route); + // const {reportActionID} = getReportActionID(route); + // if (!reportActionID) return; + // fetchReportIfNeeded(); + // console.log('get.ROUTE.+++++++++', route); + // setLinkingToMessageTrigger(true); + // }, [route, fetchReportIfNeeded]); + // ReportUtils.useRouteChangeHandler(reportID, reportActionID, () => { + // console.log('getChat.OPEN_REPORT.000', reportActionID); + // fetchReportIfNeeded(); + // // updateCurrentReportID(getReportID(route)); + // }); const dismissBanner = useCallback(() => { setIsBannerVisible(false); @@ -283,7 +359,7 @@ function ReportScreen({ return; } - Report.openReport(report.reportID); + Report.openReport({reportID: report.reportID}); }); return () => unsubscribeVisibilityListener(); @@ -426,10 +502,15 @@ function ReportScreen({ style={[styles.flex1, styles.justifyContentEnd, styles.overflowHidden]} onLayout={onListLayout} > - {isReportReadyForDisplay && !isFirstlyLoadingReportActions && !isLoading && ( + {/* {isReportReadyForDisplay && !isFirstlyLoadingReportActions && !isLoading && ( */} + {isReportReadyForDisplay && !isLoading && ( `${ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM}${getReportID(route)}`, initialValue: false, }, + sortedReportActions: { + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getReportID(route)}`, + canEvict: false, + selector: ReportActionsUtils.getSortedReportActionsForDisplay, + // selector: ReportActionsUtils.processReportActions, + }, }, true, ), diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index a0dee8abdb71..88e1dcef3e91 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -1,8 +1,8 @@ -import React, {useRef, useEffect, useContext, useMemo} from 'react'; +import React, {useRef, useEffect, useContext, useMemo, useState} from 'react'; import PropTypes from 'prop-types'; import _ from 'underscore'; import lodashGet from 'lodash/get'; -import {useIsFocused} from '@react-navigation/native'; +import {useIsFocused, useRoute} from '@react-navigation/native'; import * as Report from '../../../libs/actions/Report'; import reportActionPropTypes from './reportActionPropTypes'; import Timing from '../../../libs/actions/Timing'; @@ -65,15 +65,98 @@ const defaultProps = { isLoadingNewerReportActions: false, }; -function ReportActionsView(props) { +/** + * Get the currently viewed report ID as number + * + * @param {Object} route + * @param {Object} route.params + * @param {String} route.params.reportID + * @returns {String} + */ +function getReportActionID(route) { + return {reportActionID: lodashGet(route, 'params.reportActionID', null), reportID: lodashGet(route, 'params.reportID', null)}; +} + +function ReportActionsView({reportActions: allReportActions, ...props}) { useCopySelectionHelper(); const reactionListRef = useContext(ReactionListContext); + const route = useRoute(); + const {reportActionID} = getReportActionID(route); + // const reportActionID = '' + + // const [canShowAllReports, setShowAllReports] = useState(false); + const testRef = useRef(false); const didLayout = useRef(false); + // const isFirstRender = useRef(true); const didSubscribeToReportTypingEvents = useRef(false); - const isFetchNewerWasCalled = useRef(false); - const hasCachedActions = useRef(_.size(props.reportActions) > 0); + const [isFetchNewerWasCalled, setFetchNewerWasCalled] = useState(false); + const [isLinkingToMessage, setLinkingToMessageTrigger] = useState(false); + + const reportActionsBeforeAndIncludingLinked = useMemo(() => { + if (reportActionID && allReportActions?.length) { + return ReportActionsUtils.getSlicedRangeFromArrayByID(allReportActions, reportActionID); + } + return []; + }, [allReportActions, reportActionID]); + + const reportActions = useMemo(() => { + console.log( + 'get.reportActions.info|||', + '| reportActionID:', + reportActionID, + '| isFetchNewerWasCalled:', + isFetchNewerWasCalled, + '| isLinkingToMessage:', + isLinkingToMessage, + '| testRef.current:', + testRef.current, + '| reportActionsBeforeAndIncludingLinked:', + reportActionsBeforeAndIncludingLinked?.length, + '| allReportActions:', + allReportActions?.length, + ); + + if (!reportActionID || (!testRef.current && !isLinkingToMessage && !props.isLoadingInitialReportActions && isFetchNewerWasCalled)) { + console.log('get.reportActions.ALL||||', allReportActions.length); + return allReportActions; + } + console.log( + 'get.reportActions.CUT||||', + reportActionsBeforeAndIncludingLinked[0]?.message?.text || reportActionsBeforeAndIncludingLinked[0]?.message + ); + return reportActionsBeforeAndIncludingLinked; + }, [isFetchNewerWasCalled, allReportActions, reportActionsBeforeAndIncludingLinked, reportActionID, isLinkingToMessage, props.isLoadingInitialReportActions]); - const mostRecentIOUReportActionID = useRef(ReportActionsUtils.getMostRecentIOURequestActionID(props.reportActions)); + useEffect(() => { + console.log('get.ROUTE_CHANGED.triggered', route); + if (!reportActionID) { + return; + } + testRef.current = true; + setLinkingToMessageTrigger(true); + props.fetchReportIfNeeded(); + console.log('get.ROUTE_CHANGED.+++++++++'); + setTimeout(() => { + testRef.current = false; + setLinkingToMessageTrigger(false); + console.log('get.ROUTE_CHANGED.+++++++++FINISH', route); + }, 7000); + // setLinkingToMessageTrigger(true); + }, [route, props.fetchReportIfNeeded, reportActionID]); + + const isReportActionArrayCatted = useMemo(() => { + if (reportActions?.length !== allReportActions?.length && reportActionID) { + console.log('get.isReportActionArrayCatted.+++++++++'); + return true; + } + + console.log('get.isReportActionArrayCatted.----------'); + return false; + }, [reportActions, allReportActions, reportActionID, isFetchNewerWasCalled]); + + const hasCachedActions = useRef(_.size(reportActions) > 0); + + const mostRecentIOUReportActionID = useRef(ReportActionsUtils.getMostRecentIOURequestActionID(reportActions)); const prevNetworkRef = useRef(props.network); const prevIsSmallScreenWidthRef = useRef(props.isSmallScreenWidth); @@ -90,13 +173,13 @@ function ReportActionsView(props) { if (props.report.isOptimisticReport) { return; } - - Report.openReport(reportID); + Report.openReport({reportID, reportActionID}); }; useEffect(() => { openReportIfNecessary(); // eslint-disable-next-line react-hooks/exhaustive-deps + // isFirstRender.current = false; }, []); useEffect(() => { @@ -129,7 +212,7 @@ function ReportActionsView(props) { // update ref with current state prevIsSmallScreenWidthRef.current = props.isSmallScreenWidth; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props.isSmallScreenWidth, props.report, props.reportActions, isReportFullyVisible]); + }, [props.isSmallScreenWidth, props.report, reportActions, isReportFullyVisible]); useEffect(() => { // Ensures subscription event succeeds when the report/workspace room is created optimistically. @@ -153,7 +236,7 @@ function ReportActionsView(props) { return; } - const oldestReportAction = _.last(props.reportActions); + const oldestReportAction = _.last(reportActions); // Don't load more chats if we're already at the beginning of the chat history if (oldestReportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) { @@ -161,6 +244,7 @@ function ReportActionsView(props) { } // Retrieve the next REPORT.ACTIONS.LIMIT sized page of comments Report.getOlderActions(reportID, oldestReportAction.reportActionID); + setShowAllReports(true); }; /** @@ -182,11 +266,15 @@ function ReportActionsView(props) { // Additionally, we use throttling on the 'onStartReached' callback to further reduce the frequency of its invocation. // This should be removed once the issue of frequent re-renders is resolved. - if (!isFetchNewerWasCalled.current || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { - isFetchNewerWasCalled.current = true; + if (isReportActionArrayCatted || reportActionID) { + setFetchNewerWasCalled(true); return; } - const newestReportAction = _.first(props.reportActions); + if (!isFetchNewerWasCalled || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { + return; + } + + const newestReportAction = reportActions[0]; Report.getNewerActions(reportID, newestReportAction.reportActionID); }, 700); @@ -211,7 +299,7 @@ function ReportActionsView(props) { }; // Comments have not loaded at all yet do nothing - if (!_.size(props.reportActions)) { + if (!_.size(reportActions)) { return null; } @@ -220,7 +308,7 @@ function ReportActionsView(props) { Date: Tue, 3 Oct 2023 15:25:21 +0200 Subject: [PATCH 005/245] WIP linking --- src/libs/ReportActionsUtils.js | 3 +- src/pages/home/report/ReportActionsList.js | 10 +++++- src/pages/home/report/ReportActionsView.js | 37 ++++++---------------- 3 files changed, 21 insertions(+), 29 deletions(-) diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js index cc9efcf6c9cb..4665575f263f 100644 --- a/src/libs/ReportActionsUtils.js +++ b/src/libs/ReportActionsUtils.js @@ -355,7 +355,8 @@ function getSlicedRangeFromArrayByID(array, id) { } // return array.slice(0, index+1); - return array.slice(index, array.length); + // return array.slice(index, array.length); + return array.slice(index, array.length - index > 50 ? index + 50 : array.length); } /** diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 31e48dc0e46b..38f3ab5e6763 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -68,6 +68,7 @@ const defaultProps = { isLoadingInitialReportActions: false, isLoadingOlderReportActions: false, isLoadingNewerReportActions: false, + isLinkingLoader: false, ...withCurrentUserPersonalDetailsDefaultProps, }; @@ -117,6 +118,7 @@ function ReportActionsList({ loadOlderChats, onLayout, isComposerFullSize, + isLinkingLoader, }) { const reportScrollManager = useReportScrollManager(); const {translate} = useLocalize(); @@ -328,7 +330,7 @@ function ReportActionsList({ const shouldShowReportRecipientLocalTime = ReportUtils.canShowReportRecipientLocalTime(personalDetailsList, report, currentUserPersonalDetails.accountID) && !isComposerFullSize; const contentContainerStyle = useMemo( - () => [styles.chatContentScrollView, isLoadingNewerReportActions ? styles.chatContentScrollViewWithHeaderLoader : {}], + () => [styles.chatContentScrollView, isLoadingNewerReportActions ? styles.chatContentScrollViewWithHeaderLoader : {}], [isLoadingNewerReportActions], ); @@ -368,6 +370,12 @@ function ReportActionsList({ ); }, [isLoadingNewerReportActions]); + useEffect(() => { + if (!isLinkingLoader) { + return; + } + reportScrollManager.scrollToBottom(); + }, [isLinkingLoader, reportScrollManager]); return ( <> { - console.log('get.ROUTE_CHANGED.triggered', route); if (!reportActionID) { return; } - testRef.current = true; + setFetchNewerWasCalled(false); setLinkingToMessageTrigger(true); props.fetchReportIfNeeded(); - console.log('get.ROUTE_CHANGED.+++++++++'); setTimeout(() => { - testRef.current = false; setLinkingToMessageTrigger(false); - console.log('get.ROUTE_CHANGED.+++++++++FINISH', route); - }, 7000); + }, 700); // setLinkingToMessageTrigger(true); }, [route, props.fetchReportIfNeeded, reportActionID]); const isReportActionArrayCatted = useMemo(() => { if (reportActions?.length !== allReportActions?.length && reportActionID) { - console.log('get.isReportActionArrayCatted.+++++++++'); return true; } - - console.log('get.isReportActionArrayCatted.----------'); return false; - }, [reportActions, allReportActions, reportActionID, isFetchNewerWasCalled]); + }, [reportActions, allReportActions, reportActionID]); const hasCachedActions = useRef(_.size(reportActions) > 0); @@ -244,7 +229,6 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { } // Retrieve the next REPORT.ACTIONS.LIMIT sized page of comments Report.getOlderActions(reportID, oldestReportAction.reportActionID); - setShowAllReports(true); }; /** @@ -312,6 +296,8 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { mostRecentIOUReportActionID={mostRecentIOUReportActionID.current} loadOlderChats={loadOlderChats} loadNewerChats={loadNewerChats} + isLinkingLoader={!!reportActionID && props.isLoadingInitialReportActions} + isReportActionArrayCatted={isReportActionArrayCatted} isLoadingInitialReportActions={props.isLoadingInitialReportActions} isLoadingOlderReportActions={props.isLoadingOlderReportActions} isLoadingNewerReportActions={props.isLoadingNewerReportActions} @@ -351,12 +337,9 @@ function arePropsEqual(oldProps, newProps) { return false; } - // if (oldProps.isLinkingToMessage !== newProps.isLinkingToMessage) { - // return false; - // } - // if (oldisReportActionArrayCatted !== newisReportActionArrayCatted) { - // return false; - // } + if (oldProps.isLoadingNewerReportActions !== newProps.isLoadingNewerReportActions) { + return false; + } if (oldProps.report.lastReadTime !== newProps.report.lastReadTime) { return false; From 241a2994fd542a2ea3c930931465c313b042f70b Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 5 Oct 2023 16:26:13 +0200 Subject: [PATCH 006/245] still WIP --- src/pages/home/ReportScreen.js | 72 +++------------------- src/pages/home/report/ReportActionsView.js | 30 +++------ 2 files changed, 15 insertions(+), 87 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index a01f95e4f4fa..ddccb6d5fafe 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -149,7 +149,6 @@ function ReportScreen({ route, report, reportMetadata, - // reportActions, sortedReportActions, accountManagerReportID, personalDetails, @@ -176,46 +175,8 @@ function ReportScreen({ const reportActions = useMemo(() => { const val = ReportActionsUtils.getRangeFromArrayByID(sortedReportActions, reportActionID); - console.log('get.ROOT.reportActions', reportActionID, sortedReportActions.length, val.length); return val; }, [sortedReportActions, reportActionID]); - // const isReportActionArrayCatted = useMemo(() => sortedReportActions.length !== withoutGaps.length, [withoutGaps, sortedReportActions]); - - // const reportActions = useMemo(() => { - // //TODO: OpenReport, it means that we clicked on the link in current chat and we need to get a proper range of reportActions - // // console.log( - // // 'get.reportActions.initialSorted', - // // // sortedReportActions.length, - // // sortedReportActions.length, - // // !!reportActionID, - // // sortedReportActions.map((item) => { - // // return {message: item.message[0].text, previousReportActionID: item?.previousReportActionID, reportActionID: item?.reportActionID}; - // // }), - // // ); - // // // // console.log('get.reportActions.initialSorted', sortedReportActions.map((item) =>item?.previousReportActionID)); - // // console.log( - // // 'get.reportActions.withoutGaps', - // // // withoutGaps.length, - // // withoutGaps.length, - // // !!reportActionID, - // // withoutGaps.map((item) => { - // // return {message: item.message[0].text, previousReportActionID: item?.previousReportActionID, reportActionID: item?.reportActionID}; - // // }), - // // ); - // return withoutGaps; - // // return sortedReportActions; - // }, [sortedReportActions, reportActionID]); - - // const reportActionsBeforeAndIncludingLinked = useMemo(() => { - // if (reportActionID) { - // firstRenderRefI.current = false; - // return ReportActionsUtils.getSlicedRangeFromArrayByID(reportActions, reportActionID); - // } - // return null; - // }, [reportActions, reportActionID]); - - // const [skeletonViewContainerHeight, setSkeletonViewContainerHeight] = useState(0); - // >>>>>>> Stashed changes const [isBannerVisible, setIsBannerVisible] = useState(true); const {addWorkspaceRoomOrChatPendingAction, addWorkspaceRoomOrChatErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report); @@ -285,7 +246,9 @@ function ReportScreen({ return reportIDFromPath !== '' && report.reportID && !isTransitioning; }, [route, report]); - // ReportUtils.useRouteChangeHandler(reportID, reportActionID, () => Report.openReport({reportID: reportIDFromPath, reportActionID: reportActionID || ''})) + const fetchReport = useCallback(() => { + Report.openReport({reportID, reportActionID: reportActionID || ''}); + }, [reportID, reportActionID]); const fetchReportIfNeeded = useCallback(() => { const reportIDFromPath = getReportID(route); @@ -299,28 +262,12 @@ function ReportScreen({ // It possible that we may not have the report object yet in Onyx yet e.g. we navigated to a URL for an accessible report that // is not stored locally yet. If report.reportID exists, then the report has been stored locally and nothing more needs to be done. // If it doesn't exist, then we fetch the report from the API. - - // useEffect(() => { - // console.log('get.ROUTE.0', route); - // const {reportActionID} = getReportActionID(route); - // if (!reportActionID) return; - // fetchReportIfNeeded(); - // console.log('get.ROUTE.+++++++++', route); - // setLinkingToMessageTrigger(true); - // }, [route, fetchReportIfNeeded]); - // ReportUtils.useRouteChangeHandler(reportID, reportActionID, () => { - // console.log('getChat.OPEN_REPORT.000', reportActionID); - // fetchReportIfNeeded(); - // // updateCurrentReportID(getReportID(route)); - // }); - - // if (report.reportID && report.reportID === getReportID(route) && !reportActionID) { if (report.reportID && report.reportID === getReportID(route) && !reportMetadata.isLoadingInitialReportActions) { return; } - Report.openReport({reportID: reportIDFromPath, reportActionID: reportActionID || ''}); - }, [report.reportID, route, reportMetadata.isLoadingInitialReportActions]); + fetchReport(); + }, [report.reportID, route, reportMetadata.isLoadingInitialReportActions, fetchReport]); const dismissBanner = useCallback(() => { setIsBannerVisible(false); @@ -432,12 +379,7 @@ function ReportScreen({ // eslint-disable-next-line rulesdir/no-negated-variables const shouldShowNotFoundPage = useMemo( - () => (!firstRenderRef.current && - !report.reportID && - !isOptimisticDelete && - !reportMetadata.isLoadingInitialReportActions && - !isLoading && - !userLeavingStatus) || shouldHideReport, + () => (!firstRenderRef.current && !report.reportID && !isOptimisticDelete && !reportMetadata.isLoadingInitialReportActions && !isLoading && !userLeavingStatus) || shouldHideReport, [report, reportMetadata, isLoading, shouldHideReport, isOptimisticDelete, userLeavingStatus], ); @@ -496,7 +438,7 @@ function ReportScreen({ report={report} isLinkingToMessage={isLinkingToMessage} setLinkingToMessageTrigger={setLinkingToMessageTrigger} - fetchReportIfNeeded={fetchReportIfNeeded} + fetchReport={fetchReport} reportActionID={reportActionID} isLoadingInitialReportActions={reportMetadata.isLoadingInitialReportActions} isLoadingNewerReportActions={reportMetadata.isLoadingNewerReportActions} diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 3cfda35cd66c..c3ca835d7812 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -20,7 +20,6 @@ import reportPropTypes from '../../reportPropTypes'; import PopoverReactionList from './ReactionList/PopoverReactionList'; import getIsReportFullyVisible from '../../../libs/getIsReportFullyVisible'; import {ReactionListContext} from '../ReportScreenContext'; -import useReportScrollManager from '../../../hooks/useReportScrollManager'; const propTypes = { /** The report currently being looked at */ @@ -83,7 +82,6 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { const reactionListRef = useContext(ReactionListContext); const route = useRoute(); const {reportActionID} = getReportActionID(route); - const testRef = useRef(false); const didLayout = useRef(false); const didSubscribeToReportTypingEvents = useRef(false); const [isFetchNewerWasCalled, setFetchNewerWasCalled] = useState(false); @@ -97,22 +95,6 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { }, [allReportActions, reportActionID]); const reportActions = useMemo(() => { - console.log( - 'get.reportActions.info|||', - '| reportActionID:', - reportActionID, - '| isFetchNewerWasCalled:', - isFetchNewerWasCalled, - '| isLinkingToMessage:', - isLinkingToMessage, - '| testRef.current:', - testRef.current, - '| reportActionsBeforeAndIncludingLinked:', - reportActionsBeforeAndIncludingLinked?.length, - '| allReportActions:', - allReportActions?.length, - ); - if (!reportActionID || (!isLinkingToMessage && !props.isLoadingInitialReportActions && isFetchNewerWasCalled)) { return allReportActions; } @@ -124,13 +106,14 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { return; } setFetchNewerWasCalled(false); + setFetchNewerWasCalled(false); setLinkingToMessageTrigger(true); - props.fetchReportIfNeeded(); + props.fetchReport(); setTimeout(() => { setLinkingToMessageTrigger(false); - }, 700); + }, 300); // setLinkingToMessageTrigger(true); - }, [route, props.fetchReportIfNeeded, reportActionID]); + }, [route, reportActionID]); const isReportActionArrayCatted = useMemo(() => { if (reportActions?.length !== allReportActions?.length && reportActionID) { @@ -250,7 +233,7 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { // Additionally, we use throttling on the 'onStartReached' callback to further reduce the frequency of its invocation. // This should be removed once the issue of frequent re-renders is resolved. - if (isReportActionArrayCatted || reportActionID) { + if (!isFetchNewerWasCalled) { setFetchNewerWasCalled(true); return; } @@ -260,6 +243,9 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { const newestReportAction = reportActions[0]; Report.getNewerActions(reportID, newestReportAction.reportActionID); + // Report.getNewerActions(reportID, '2420805078232802130'); + // Report.getNewerActions(reportID, '569204055949619736'); + // Report.getNewerActions(reportID, '1134531619271003224'); }, 500); /** From 7ca764369daa8cb53154eb0b9d4d7be3250ea92d Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 9 Oct 2023 16:25:44 +0200 Subject: [PATCH 007/245] remove timer --- src/pages/home/report/ReportActionsView.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 8c0f5eb77c57..046c062d70be 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -108,9 +108,14 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { setFetchNewerWasCalled(false); setLinkingToMessageTrigger(true); props.fetchReport(); - setTimeout(() => { + const timeoutId = setTimeout(() => { setLinkingToMessageTrigger(false); - }, 300); + }, 500); + + // Return a cleanup function + return () => { + clearTimeout(timeoutId); + }; // setLinkingToMessageTrigger(true); }, [route, reportActionID]); @@ -233,8 +238,6 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { // This should be removed once the issue of frequent re-renders is resolved. if (!isFetchNewerWasCalled) { setFetchNewerWasCalled(true); - // if (!isFetchNewerWasCalled.current || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { - // isFetchNewerWasCalled.current = true; return; } if (!isFetchNewerWasCalled || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { From 8ce9ca0e65027ae31cc172d2150b314c6bd90ff0 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 9 Oct 2023 17:55:07 +0200 Subject: [PATCH 008/245] scroll to --- src/pages/home/ReportScreen.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index df85fcca52f0..4a29a05421fc 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -281,11 +281,17 @@ function ReportScreen({ * @param {String} text */ const onSubmitComment = useCallback( - (text) => { - Report.addComment(getReportID(route), text); - }, - [route], - ); + (text) => { + Report.addComment(getReportID(route), text); + // we need to scroll to the bottom of the list after the comment was added + const refID = setTimeout(() => { + flatListRef.current.scrollToOffset({animated: false, offset: 0}); + }, 10); + + return () => clearTimeout(refID); + }, + [route], + ); useFocusEffect( useCallback(() => { From 8efb8fd896b8d6347b334945acea3afa9bba22e1 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 10 Oct 2023 12:00:15 +0200 Subject: [PATCH 009/245] fix getSlicedRangeFromArrayByID --- src/libs/ReportActionsUtils.js | 58 ++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js index 373732172992..3d13394c133c 100644 --- a/src/libs/ReportActionsUtils.js +++ b/src/libs/ReportActionsUtils.js @@ -311,6 +311,14 @@ function getSortedReportActions(reportActions, shouldSortInDescendingOrder = fal // const updatedActions = processReportActions(reportActions); // console.log(updatedActions); +/** + * Returns the range of report actions from the given array which include current id + * the range is consistent + * + * @param {Array} array + * @param {String} id + * @returns {Array} + */ function getRangeFromArrayByID(array, id) { // without gaps let index; @@ -342,23 +350,45 @@ function getRangeFromArrayByID(array, id) { // return array.slice(startIndex, endIndex); } -function getSlicedRangeFromArrayByID(array, id) { - let index; - if (id) { - index = array.findIndex((obj) => obj.reportActionID === id); - } else { - index = array.length - 1; - } - - if (index === -1) { - return []; - } - // return array.slice(0, index+1); - // return array.slice(index, array.length); - return array.slice(index, array.length - index > 50 ? index + 50 : array.length); +/** + * Returns the sliced range of report actions from the given array. + * + * @param {Array} array + * @param {String} id + * @returns {Object} + * getSlicedRangeFromArrayByID([{id:1}, ..., {id: 100}], 50) => { catted: [{id:1}, ..., {id: 50}], expanded: [{id: 45}, ..., {id: 55}] } + */ +function getSlicedRangeFromArrayByID(array, id) { + let index; + if (id) { + index = array.findIndex((obj) => obj.reportActionID === id); + } else { + index = array.length - 1; + } + + if (index === -1) { + return { catted: [], expanded: [] }; + } + + const cattedEnd = array.length - index > 50 ? index + 50 : array.length; + const expandedStart = Math.max(0, index - 5); + + const catted = []; + const expanded = []; + + for (let i = expandedStart; i < cattedEnd; i++) { + if (i >= index) { + catted.push(array[i]); + } + expanded.push(array[i]); + } + // We need the expanded version to prevent jittering of list. So when user navigate to linked message we show to him the catted version. After that we show the expanded version. + // Then we can show all reports. + return { catted, expanded }; } + /** * Finds most recent IOU request action ID. * From cfda4c07ff3830e989ed99ed7a122212b7637106 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 10 Oct 2023 12:39:58 +0200 Subject: [PATCH 010/245] update cutting method --- src/pages/home/report/ReportActionsView.js | 79 ++++++++++++++-------- 1 file changed, 49 insertions(+), 30 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index f8ed94618f70..38174a867b80 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -77,47 +77,74 @@ function getReportActionID(route) { return {reportActionID: lodashGet(route, 'params.reportActionID', null), reportID: lodashGet(route, 'params.reportID', null)}; } -function ReportActionsView({reportActions: allReportActions, ...props}) { +function ReportActionsView({reportActions: allReportActions, fetchReport, ...props}) { useCopySelectionHelper(); const reactionListRef = useContext(ReactionListContext); const route = useRoute(); const {reportActionID} = getReportActionID(route); const didLayout = useRef(false); const didSubscribeToReportTypingEvents = useRef(false); - const [isFetchNewerWasCalled, setFetchNewerWasCalled] = useState(false); - const [isLinkingToMessage, setLinkingToMessageTrigger] = useState(false); + const [isLinkingToCattedMessage, setLinkingToCattedMessage] = useState(false); + const [isLinkingToExtendedMessage, setLinkingToExtendedMessage] = useState(false); + const isLoadingLinkedMessage = !!reportActionID && props.isLoadingInitialReportActions; - const reportActionsBeforeAndIncludingLinked = useMemo(() => { + useEffect(() => { + let timeoutIdCatted; + let timeoutIdExtended; + if (!isLoadingLinkedMessage) { + timeoutIdCatted = setTimeout(() => { + setLinkingToCattedMessage(false); + }, 100); + timeoutIdExtended = setTimeout(() => { + setLinkingToExtendedMessage(false); + }, 200); + } + return () => { + clearTimeout(timeoutIdCatted); + clearTimeout(timeoutIdExtended); + }; + }, [isLoadingLinkedMessage]); + + const {catted: reportActionsBeforeAndIncludingLinked, expanded: reportActionsBeforeAndIncludingLinkedExpanded} = useMemo(() => { if (reportActionID && allReportActions?.length) { return ReportActionsUtils.getSlicedRangeFromArrayByID(allReportActions, reportActionID); } - return []; + // catted means the reportActions before and including the linked message + // expanded means the reportActions before and including the linked message plus the next 5 + return {catted: [], expanded: []}; }, [allReportActions, reportActionID]); const reportActions = useMemo(() => { - if (!reportActionID || (!isLinkingToMessage && !props.isLoadingInitialReportActions && isFetchNewerWasCalled)) { + if (!reportActionID || (!isLinkingToCattedMessage && !isLoadingLinkedMessage && !isLinkingToExtendedMessage)) { return allReportActions; } + if ( + reportActionID && + !isLinkingToCattedMessage && + isLinkingToExtendedMessage && + reportActionsBeforeAndIncludingLinkedExpanded.length !== reportActionsBeforeAndIncludingLinked.length + ) { + return reportActionsBeforeAndIncludingLinkedExpanded; + } return reportActionsBeforeAndIncludingLinked; - }, [isFetchNewerWasCalled, allReportActions, reportActionsBeforeAndIncludingLinked, reportActionID, isLinkingToMessage, props.isLoadingInitialReportActions]); + }, [ + allReportActions, + reportActionsBeforeAndIncludingLinked, + reportActionID, + isLinkingToCattedMessage, + isLoadingLinkedMessage, + isLinkingToExtendedMessage, + reportActionsBeforeAndIncludingLinkedExpanded, + ]); useEffect(() => { if (!reportActionID) { return; } - setFetchNewerWasCalled(false); - setLinkingToMessageTrigger(true); - props.fetchReport(); - const timeoutId = setTimeout(() => { - setLinkingToMessageTrigger(false); - }, 500); - - // Return a cleanup function - return () => { - clearTimeout(timeoutId); - }; - // setLinkingToMessageTrigger(true); - }, [route, reportActionID]); + setLinkingToCattedMessage(true); + setLinkingToExtendedMessage(true); + fetchReport(); + }, [route, reportActionID, fetchReport]); const isReportActionArrayCatted = useMemo(() => { if (reportActions?.length !== allReportActions?.length && reportActionID) { @@ -237,26 +264,18 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { // // Additionally, we use throttling on the 'onStartReached' callback to further reduce the frequency of its invocation. // This should be removed once the issue of frequent re-renders is resolved. - if (!isFetchNewerWasCalled) { - setFetchNewerWasCalled(true); - return; - } - if (!isFetchNewerWasCalled || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { + if (!isLinkingToExtendedMessage || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { return; } const newestReportAction = reportActions[0]; Report.getNewerActions(reportID, newestReportAction.reportActionID); - // if (!isFetchNewerWasCalled.current || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { - // isFetchNewerWasCalled.current = true; - // return; - // } // const newestReportAction = _.first(props.reportActions); // Report.getNewerActions(reportID, newestReportAction.reportActionID); // Report.getNewerActions(reportID, '1134531619271003224'); }, 500), - [props.isLoadingNewerReportActions, props.isLoadingInitialReportActions, props.reportActions, reportID], + [props.isLoadingNewerReportActions, props.isLoadingInitialReportActions, isLinkingToExtendedMessage, reportID, reportActions], ); /** From 16c4231733589e88d66e0d98606aacdfe971f8e3 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 11 Oct 2023 18:16:50 +0200 Subject: [PATCH 011/245] remove comments --- src/libs/ReportActionsUtils.js | 75 +--------------------- src/libs/ReportUtils.js | 24 ------- src/pages/home/report/ReportActionsView.js | 13 +--- 3 files changed, 4 insertions(+), 108 deletions(-) diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js index 3d13394c133c..363ea4567f40 100644 --- a/src/libs/ReportActionsUtils.js +++ b/src/libs/ReportActionsUtils.js @@ -206,45 +206,14 @@ function getSortedReportActions(reportActions, shouldSortInDescendingOrder = fal // * @param {Object} reportActions // * @returns {Array} // */ -// function processReportActions(reportActions) { -// // TODO: +// function processReportActions(reportActions) { //TODO: remove after previousReportActionID is stable // // Separate new and sorted reportActions // const newReportActions = _.filter(reportActions, (action) => !action.previousReportActionID); // const sortedReportActions = _.filter(reportActions, (action) => action.previousReportActionID); -// // console.log( -// // 'getChat.Sort.N.0', -// // newReportActions.length, -// // newReportActions.map(({message}) => message[0].text), -// // ); -// // console.log( -// // 'getChat.Sort.N.0.0', -// // newReportActions.length, -// // newReportActions.map(({previousReportActionID}) => previousReportActionID), -// // ); -// // console.log( -// // 'getChat.Sort.S.0', -// // sortedReportActions.length, -// // sortedReportActions.map(({message}) => message[0].text), -// // ); -// // console.log( -// // 'getChat.Sort.S.0.0', -// // sortedReportActions.length, -// // sortedReportActions.map(({previousReportActionID}) => previousReportActionID), -// // ); // // Sort the new reportActions // const sortedNewReportActions = getSortedReportActionsForDisplay(newReportActions); -// // console.log( -// // 'getChat.SORT.SS', -// // JSON.stringify( -// // sortedNewReportActions.map(({message, reportActionID, previousReportActionID}) => ({ -// // message: message[0].text, -// // reportActionID, -// // previousReportActionID, -// // })), -// // ), -// // ); // // Then, iterate through the sorted new reportActions and add the previousReportActionID to each item except the first // const processedReportActions = sortedNewReportActions.map((action, index) => { @@ -257,59 +226,22 @@ function getSortedReportActions(reportActions, shouldSortInDescendingOrder = fal // }; // }); -// // console.log( -// // 'getChat.SORT.BEFORE', -// // JSON.stringify( -// // processedReportActions.map(({message, reportActionID, previousReportActionID}) => ({ -// // message: message[0].text, -// // reportActionID, -// // previousReportActionID, -// // })), -// // ), -// // ); // if (processedReportActions[processedReportActions.length - 1]?.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED) { // processedReportActions.pop(); // } -// // console.log( -// // 'getChat.SORT.AFTER', -// // JSON.stringify( -// // processedReportActions.map(({message, reportActionID, previousReportActionID}) => ({ -// // message: message[0].text, -// // reportActionID, -// // previousReportActionID, -// // })), -// // ), -// // ); // // Determine the order of merging based on reportActionID values // const lastSortedReportActionID = _.last(sortedReportActions)?.reportActionTimestamp || 0; // const firstProcessedReportActionID = _.first(processedReportActions)?.reportActionTimestamp || Infinity; -// // console.log('getChat.Sort.1', getSortedReportActionsForDisplay(reportActions).length, [...sortedReportActions, ...processedReportActions].length); -// // console.log('getChat.Sort.1.1', _.last(sortedReportActions), _.first(processedReportActions)); // if (firstProcessedReportActionID > lastSortedReportActionID) { -// // console.log( -// // 'getChat.Sort.2', -// // [...sortedReportActions, ...processedReportActions].map(({message}) => message[0].text), -// // ); // return [...sortedReportActions, ...processedReportActions]; // } else { -// // console.log( -// // 'getChat.Sort.3', -// // [...processedReportActions, ...sortedReportActions].map(({message}) => message[0].text), -// // ); // return [...processedReportActions, ...sortedReportActions]; // } // } -// Usage: -// const reportActions = [ -// { reportActionID: '1' }, -// { reportActionID: '2', previousReportActionID: '1' }, -// { reportActionID: '3' } -// ]; -// const updatedActions = processReportActions(reportActions); -// console.log(updatedActions); + /** * Returns the range of report actions from the given array which include current id @@ -320,7 +252,6 @@ function getSortedReportActions(reportActions, shouldSortInDescendingOrder = fal * @returns {Array} */ function getRangeFromArrayByID(array, id) { - // without gaps let index; if (id) { @@ -347,7 +278,6 @@ function getRangeFromArrayByID(array, id) { } return array.slice(startIndex, endIndex + 1); - // return array.slice(startIndex, endIndex); } @@ -874,7 +804,6 @@ export { getAllReportActions, isReportActionAttachment, isNotifiableReportAction, - // processReportActions, getRangeFromArrayByID, getSlicedRangeFromArrayByID, }; diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 79f052bd21b4..4a4876132220 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -3807,29 +3807,6 @@ function getIOUReportActionDisplayMessage(reportAction) { return displayMessage; } -/** - * Hook that triggers a fetch when reportActionID appears while reportID stays the same. - * - * @param {string} reportID - The current reportID from the route. - * @param {string|null} reportActionID - The current reportActionID from the route or null. - * @param {function} triggerFetch - The function to be triggered on the condition. - */ -function useRouteChangeHandler(reportID = '', reportActionID = '', triggerFetch = () => {}) { - // Store the previous reportID and reportActionID - const previousReportIDRef = useRef(null); - const previousReportActionIDRef = useRef(null); - - useEffect(() => { - // Check if reportID is the same and reportActionID just appeared - if (reportID === previousReportIDRef.current && reportActionID && !previousReportActionIDRef.current) { - triggerFetch(); - } - - // Update refs for the next render - previousReportIDRef.current = reportID; - previousReportActionIDRef.current = reportActionID; - }, [reportID, reportActionID, triggerFetch]); - } /** * @param {Object} report * @returns {Boolean} @@ -3982,6 +3959,5 @@ export { hasMissingSmartscanFields, getIOUReportActionDisplayMessage, isWaitingForTaskCompleteFromAssignee, - useRouteChangeHandler, isReportDraft, }; diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 06e0b4d865fc..eaa4d9bf3a24 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -84,13 +84,11 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro const {reportActionID} = getReportActionID(route); const didLayout = useRef(false); const didSubscribeToReportTypingEvents = useRef(false); - + const isFirstRender = useRef(true); const [isLinkingToCattedMessage, setLinkingToCattedMessage] = useState(false); const [isLinkingToExtendedMessage, setLinkingToExtendedMessage] = useState(false); const isLoadingLinkedMessage = !!reportActionID && props.isLoadingInitialReportActions; - const isFirstRender = useRef(true); //TODO: - // const hasCachedActions = useRef(_.size(props.reportActions) > 0); //TODO: useEffect(() => { @@ -183,7 +181,6 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro useEffect(() => { openReportIfNecessary(); // eslint-disable-next-line react-hooks/exhaustive-deps - // isFirstRender.current = false; }, []); useEffect(() => { @@ -269,21 +266,15 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro // // Additionally, we use throttling on the 'onStartReached' callback to further reduce the frequency of its invocation. // This should be removed once the issue of frequent re-renders is resolved. - - // if (!isLinkingToExtendedMessage || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { // TODO: // // onStartReached is triggered during the first render. Since we use OpenReport on the first render and are confident about the message ordering, we can safely skip this call - if (isFirstRender.current || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { + if (isFirstRender.current || isLinkingToExtendedMessage || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { isFirstRender.current = false; return; } const newestReportAction = reportActions[0]; Report.getNewerActions(reportID, newestReportAction.reportActionID); - - // const newestReportAction = _.first(props.reportActions); - // Report.getNewerActions(reportID, newestReportAction.reportActionID); - // Report.getNewerActions(reportID, '1134531619271003224'); }, 500), [props.isLoadingNewerReportActions, props.isLoadingInitialReportActions, isLinkingToExtendedMessage, reportID, reportActions], ); From adb4162f36d17c3eec13e3cf44d2a77a75769a1a Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 24 Oct 2023 13:43:48 +0200 Subject: [PATCH 012/245] include deleted message in getRangeFromArrayByID --- src/libs/ReportActionsUtils.ts | 16 +++++++++++---- src/pages/home/ReportScreen.js | 36 ++++++++++++++++++---------------- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index b329281a3078..071505d1067e 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -19,8 +19,8 @@ type LastVisibleMessage = { }; type SlicedResult = { - catted: ReportAction[]; - expanded: ReportAction[]; + catted: ReportAction[]; + expanded: ReportAction[]; }; const allReports: OnyxCollection = {}; @@ -218,7 +218,7 @@ function getSortedReportActions(reportActions: ReportAction[] | null, shouldSort * param {String} id * returns {ReportAction} */ -function getRangeFromArrayByID(array: ReportAction[], id: string): ReportAction[] { +function getRangeFromArrayByID(array: ReportAction[], id?: string): ReportAction[] { let index; if (id) { @@ -543,12 +543,19 @@ function filterOutDeprecatedReportActions(reportActions: ReportActions | null): */ function getSortedReportActionsForDisplay(reportActions: ReportActions | null): ReportAction[] { const filteredReportActions = Object.entries(reportActions ?? {}) - .filter(([key, reportAction]) => shouldReportActionBeVisible(reportAction, key)) + // .filter(([key, reportAction]) => shouldReportActionBeVisible(reportAction, key)) .map((entry) => entry[1]); const baseURLAdjustedReportActions = filteredReportActions.map((reportAction) => replaceBaseURL(reportAction)); return getSortedReportActions(baseURLAdjustedReportActions, true); } +function getReportActionsWithoutRemoved(reportActions: ReportAction[] | null): ReportAction[] { + if (!reportActions) { + return []; + } + return reportActions.filter((item) => shouldReportActionBeVisible(item, item.reportActionID)); +} + /** * In some cases, there can be multiple closed report actions in a chat report. * This method returns the last closed report action so we can always show the correct archived report reason. @@ -734,6 +741,7 @@ export { getReportPreviewAction, getSortedReportActions, getSortedReportActionsForDisplay, + getReportActionsWithoutRemoved, isConsecutiveActionMadeByPreviousActor, isCreatedAction, isCreatedTaskReportAction, diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index c1acb0d3de40..0a73be16297c 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -150,7 +150,8 @@ function ReportScreen({ route, report, reportMetadata, - sortedReportActions, + // sortedReportActions, + allReportActions, accountManagerReportID, personalDetails, markReadyForHydration, @@ -175,9 +176,12 @@ function ReportScreen({ const [isLinkingToMessage, setLinkingToMessageTrigger] = useState(false); const reportActions = useMemo(() => { - const val = ReportActionsUtils.getRangeFromArrayByID(sortedReportActions, reportActionID); - return val; - }, [sortedReportActions, reportActionID]); + if (allReportActions?.length === 0) return []; + const sorterReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions); + const cattedRangeOfReportActions = ReportActionsUtils.getRangeFromArrayByID(sorterReportActions, reportActionID); + const reportActionsWithoutDeleted = ReportActionsUtils.getReportActionsWithoutRemoved(cattedRangeOfReportActions); + return reportActionsWithoutDeleted; + }, [reportActionID, allReportActions]); const [isBannerVisible, setIsBannerVisible] = useState(true); const [listHeight, setListHeight] = useState(0); @@ -289,10 +293,10 @@ function ReportScreen({ flatListRef.current.scrollToOffset({animated: false, offset: 0}); }, 10); - return () => clearTimeout(refID); - }, - [route], - ); + return () => clearTimeout(refID); + }, + [route], + ); useFocusEffect( useCallback(() => { @@ -455,7 +459,6 @@ function ReportScreen({ onLayout={onListLayout} > {isReportReadyForDisplay && !isLoading && ( - `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getReportID(route)}`, canEvict: false, - selector: ReportActionsUtils.getSortedReportActionsForDisplay, }, report: { key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${getReportID(route)}`, @@ -550,12 +552,12 @@ export default compose( key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM}${getReportID(route)}`, initialValue: false, }, - sortedReportActions: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getReportID(route)}`, - canEvict: false, - selector: ReportActionsUtils.getSortedReportActionsForDisplay, - // selector: ReportActionsUtils.processReportActions, - }, + // sortedReportActions: { + // key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getReportID(route)}`, + // canEvict: false, + // selector: ReportActionsUtils.getSortedReportActionsForDisplay, + // // selector: ReportActionsUtils.processReportActions, + // }, }, true, ), From 1f073b03dc0192086ab3c215dd535a271dfb1e85 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 7 Nov 2023 19:59:23 +0100 Subject: [PATCH 013/245] fix sliding --- .../InvertedFlatList/BaseInvertedFlatList.js | 2 +- src/pages/home/report/ReportActionsList.js | 11 +---- src/pages/home/report/ReportActionsView.js | 40 +++++++++++-------- 3 files changed, 26 insertions(+), 27 deletions(-) diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.js b/src/components/InvertedFlatList/BaseInvertedFlatList.js index 802ae373d22a..b71311b0a173 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.js +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.js @@ -136,7 +136,7 @@ function BaseInvertedFlatList(props) { windowSize={15} maintainVisibleContentPosition={{ minIndexForVisible: 0, - autoscrollToTopThreshold: variables.listItemHeightNormal, + // autoscrollToTopThreshold: variables.listItemHeightNormal, }} inverted /> diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 5520221c3b56..b0e62281845b 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -130,9 +130,8 @@ function ReportActionsList({ loadOlderChats, onLayout, isComposerFullSize, - isLinkingLoader, + reportScrollManager, }) { - const reportScrollManager = useReportScrollManager(); const {translate} = useLocalize(); const {isOffline} = useNetwork(); const route = useRoute(); @@ -360,7 +359,7 @@ function ReportActionsList({ const shouldShowReportRecipientLocalTime = ReportUtils.canShowReportRecipientLocalTime(personalDetailsList, report, currentUserPersonalDetails.accountID) && !isComposerFullSize; const contentContainerStyle = useMemo( - () => [styles.chatContentScrollView, isLoadingNewerReportActions ? styles.chatContentScrollViewWithHeaderLoader : {}], + () => [styles.chatContentScrollView, isLoadingNewerReportActions ? styles.chatContentScrollViewWithHeaderLoader : {}], [isLoadingNewerReportActions], ); @@ -404,12 +403,6 @@ function ReportActionsList({ ); }, [isLoadingNewerReportActions]); - useEffect(() => { - if (!isLinkingLoader) { - return; - } - reportScrollManager.scrollToBottom(); - }, [isLinkingLoader, reportScrollManager]); return ( <> { - let timeoutIdCatted; - let timeoutIdExtended; - if (!isLoadingLinkedMessage) { - timeoutIdCatted = setTimeout(() => { - setLinkingToCattedMessage(false); - }, 100); - timeoutIdExtended = setTimeout(() => { - setLinkingToExtendedMessage(false); - }, 200); - } - return () => { - clearTimeout(timeoutIdCatted); - clearTimeout(timeoutIdExtended); - }; - }, [isLoadingLinkedMessage]); const {catted: reportActionsBeforeAndIncludingLinked, expanded: reportActionsBeforeAndIncludingLinkedExpanded} = useMemo(() => { if (reportActionID && allReportActions?.length) { @@ -158,10 +145,28 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro if (!reportActionID) { return; } + if (scrollToBottom) { + scrollToBottom(); + } setLinkingToCattedMessage(true); setLinkingToExtendedMessage(true); fetchReport(); - }, [route, reportActionID, fetchReport]); + + const timeoutIdCatted = setTimeout(() => { + setLinkingToCattedMessage(false); + }, 100); + const timeoutIdExtended = setTimeout(() => { + setLinkingToExtendedMessage(false); + }, 200); + + return () => { + if (!timeoutIdCatted && !timeoutIdExtended) { + return; + } + clearTimeout(timeoutIdCatted); + clearTimeout(timeoutIdExtended); + }; + }, [route, reportActionID, fetchReport, scrollToBottom]); const isReportActionArrayCatted = useMemo(() => { if (reportActions?.length !== allReportActions?.length && reportActionID) { @@ -346,6 +351,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro isLoadingInitialReportActions={props.isLoadingInitialReportActions} isLoadingOlderReportActions={props.isLoadingOlderReportActions} isLoadingNewerReportActions={props.isLoadingNewerReportActions} + reportScrollManager={reportScrollManager} policy={props.policy} /> From 2b7a7355d25c7207cccd723b5322ef90884777b6 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Sun, 12 Nov 2023 18:06:39 +0100 Subject: [PATCH 014/245] fix getSlicedRangeFromArrayByID --- src/libs/ReportActionsUtils.ts | 17 +++------ src/pages/home/report/ReportActionsView.js | 42 +++++++++++++--------- 2 files changed, 31 insertions(+), 28 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index b927febb8c13..a1a236201d5b 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -259,7 +259,7 @@ function getRangeFromArrayByID(array: ReportAction[], id?: string): ReportAction * returns {Object} * getSlicedRangeFromArrayByID([{id:1}, ..., {id: 100}], 50) => { catted: [{id:1}, ..., {id: 50}], expanded: [{id: 45}, ..., {id: 55}] } */ -function getSlicedRangeFromArrayByID(array: ReportAction[], id: string):SlicedResult { +function getSlicedRangeFromArrayByID(array: ReportAction[], id: string): SlicedResult { let index; if (id) { index = array.findIndex((obj) => obj.reportActionID === id); @@ -271,18 +271,11 @@ function getSlicedRangeFromArrayByID(array: ReportAction[], id: string):SlicedRe return {catted: [], expanded: []}; } - const cattedEnd = array.length - index > 50 ? index + 50 : array.length; - const expandedStart = Math.max(0, index - 5); + const amountOfItemsBeforeLinkedOne = 25; + const expandedStart = index >= amountOfItemsBeforeLinkedOne ? index - amountOfItemsBeforeLinkedOne : 0; - const catted: ReportAction[] = []; - const expanded: ReportAction[] = []; - - for (let i = expandedStart; i < cattedEnd; i++) { - if (i >= index) { - catted.push(array[i]); - } - expanded.push(array[i]); - } + const catted: ReportAction[] = array.slice(index, array.length); + const expanded: ReportAction[] = array.slice(expandedStart, array.length); // We need the expanded version to prevent jittering of list. So when user navigate to linked message we show to him the catted version. After that we show the expanded version. // Then we can show all reports. return {catted, expanded}; diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 6ee9ae789b8c..fc3a341a3935 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -103,12 +103,13 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro const didLayout = useRef(false); const didSubscribeToReportTypingEvents = useRef(false); const isFirstRender = useRef(true); + const timeoutIdCatted = useRef(null); + const timeoutIdExtended = useRef(null); const [isLinkingToCattedMessage, setLinkingToCattedMessage] = useState(false); const [isLinkingToExtendedMessage, setLinkingToExtendedMessage] = useState(false); const isLoadingLinkedMessage = !!reportActionID && props.isLoadingInitialReportActions; - const {catted: reportActionsBeforeAndIncludingLinked, expanded: reportActionsBeforeAndIncludingLinkedExpanded} = useMemo(() => { if (reportActionID && allReportActions?.length) { return ReportActionsUtils.getSlicedRangeFromArrayByID(allReportActions, reportActionID); @@ -125,8 +126,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro if ( reportActionID && !isLinkingToCattedMessage && - isLinkingToExtendedMessage && - reportActionsBeforeAndIncludingLinkedExpanded.length !== reportActionsBeforeAndIncludingLinked.length + isLinkingToExtendedMessage ) { return reportActionsBeforeAndIncludingLinkedExpanded; } @@ -142,30 +142,32 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro ]); useEffect(() => { - if (!reportActionID) { + if (isLoadingLinkedMessage) { return; } if (scrollToBottom) { scrollToBottom(); } - setLinkingToCattedMessage(true); - setLinkingToExtendedMessage(true); - fetchReport(); - const timeoutIdCatted = setTimeout(() => { + timeoutIdCatted.current = setTimeout(() => { setLinkingToCattedMessage(false); }, 100); - const timeoutIdExtended = setTimeout(() => { + timeoutIdExtended.current = setTimeout(() => { setLinkingToExtendedMessage(false); }, 200); + }, [isLoadingLinkedMessage, scrollToBottom]); + + useEffect(() => { + if (!reportActionID) { + return; + } + if (scrollToBottom) { + scrollToBottom(); + } + setLinkingToCattedMessage(true); + setLinkingToExtendedMessage(true); + fetchReport(); - return () => { - if (!timeoutIdCatted && !timeoutIdExtended) { - return; - } - clearTimeout(timeoutIdCatted); - clearTimeout(timeoutIdExtended); - }; }, [route, reportActionID, fetchReport, scrollToBottom]); const isReportActionArrayCatted = useMemo(() => { @@ -202,6 +204,14 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro useEffect(() => { openReportIfNecessary(); // eslint-disable-next-line react-hooks/exhaustive-deps + + return () => { + if (!timeoutIdCatted && !timeoutIdExtended) { + return; + } + clearTimeout(timeoutIdCatted.current); + clearTimeout(timeoutIdExtended.current); + }; }, []); useEffect(() => { From 9f8621852748158ed52ddef21dec575a31b33312 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 28 Nov 2023 09:55:12 +0100 Subject: [PATCH 015/245] lint --- src/components/InvertedFlatList/BaseInvertedFlatList.js | 1 - src/pages/home/ReportScreen.js | 6 +++--- src/pages/home/report/ReportActionsView.js | 7 +------ 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.js b/src/components/InvertedFlatList/BaseInvertedFlatList.js index 18dc09b9feb1..2862236daa07 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.js +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.js @@ -17,7 +17,6 @@ const defaultProps = { data: [], }; - const BaseInvertedFlatList = forwardRef((props, ref) => ( { From 2d083d329e4f5e0b2677bff3dc55d7b0dd9497e5 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 18 Dec 2023 13:16:23 +0100 Subject: [PATCH 016/245] WIP pagination --- src/pages/home/report/ReportActionsView.js | 290 ++++++++++++++------- 1 file changed, 192 insertions(+), 98 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 3abb59329b14..fccc6c8d7a66 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -1,7 +1,10 @@ +/* eslint-disable no-else-return */ + +/* eslint-disable rulesdir/prefer-underscore-method */ import {useIsFocused, useRoute} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useContext, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import networkPropTypes from '@components/networkPropTypes'; @@ -93,6 +96,95 @@ function getReportActionID(route) { return {reportActionID: lodashGet(route, 'params.reportActionID', null), reportID: lodashGet(route, 'params.reportID', null)}; } +const useHandleList = (linkedID, messageArray, fetchFn, route, isLoadingLinkedMessage, cb) => { + const [edgeID, setEdgeID] = useState(linkedID); + const prevLinkedID = useRef(linkedID); + const test = useRef(true); + // Function to calculate the current slice of the message array + + // const listID = useMemo(() => { + // // return reportActionID || 'list' + // console.log('route.key', route); + // return route.key + Math.random().toString; + // }, [route]); + + const index = useMemo(() => { + if (!linkedID) { + return -1; + } + + return messageArray.findIndex((obj) => String(obj.reportActionID) === String(edgeID || linkedID)); + }, [messageArray, linkedID, edgeID, isLoadingLinkedMessage]); + + useEffect(() => { + console.log('get.useHandleList.setEdgeID_EMPTY', linkedID !== prevLinkedID.current, linkedID, prevLinkedID.current); + setEdgeID(''); + // if (linkedID !== prevLinkedID.current) { + // setEdgeID(''); + // prevLinkedID.current = linkedID; + // } + test.current = false; + }, [route, linkedID]); + + console.log('get.useHandleList.INFO.index', index); + console.log('get.useHandleList.INFO.linkedID', linkedID); + console.log('get.useHandleList.INFO.messageArray', messageArray.length); + + const cattedArray = useMemo(() => { + if (!linkedID) { + return messageArray; + } + + if (index === -1) { + return messageArray; + } + console.log('get.useHandleList.calculateSlice.0.index', index); + if (linkedID && !edgeID) { + console.log('get.useHandleList.calculateSlice.1.linkedID', linkedID); + cb(); + return messageArray.slice(index, messageArray.length); + } else if (linkedID && edgeID) { + console.log('get.useHandleList.calculateSlice.2.linkedID_edgeID', linkedID, edgeID); + const amountOfItemsBeforeLinkedOne = 49; + const newStartIndex = index >= amountOfItemsBeforeLinkedOne ? index - amountOfItemsBeforeLinkedOne : 0; + console.log('get.useHandleList.calculateSlice.2.index_newStartIndex', index, newStartIndex); + if (index) { + return messageArray.slice(newStartIndex, messageArray.length); + } + return messageArray; + } + return messageArray; + }, [linkedID, messageArray, edgeID, index, isLoadingLinkedMessage]); + + // const cattedArray = calculateSlice(); + + const hasMoreCashed = cattedArray.length < messageArray.length; + + // Function to handle pagination (dummy in this case, as actual slicing is done in calculateSlice) + const paginate = useCallback( + ({firstReportActionID, distanceFromStart}) => { + // This function is a placeholder as the actual pagination is handled by calculateSlice + // It's here if you need to trigger any side effects during pagination + if (!hasMoreCashed) { + console.log('get.useHandleList.paginate.0.NO_CACHE'); + fetchFn({distanceFromStart}); + } + console.log('get.useHandleList.paginate.1.firstReportActionID'); + setEdgeID(firstReportActionID); + }, + [setEdgeID, fetchFn, hasMoreCashed], + ); + + return { + cattedArray, + fetchFunc: paginate, + linkedIdIndex: index, + setNull: () => { + setEdgeID(''); + }, + }; +}; + function ReportActionsView({reportActions: allReportActions, fetchReport, ...props}) { useCopySelectionHelper(); const reactionListRef = useContext(ReactionListContext); @@ -102,76 +194,88 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro const {reportActionID} = getReportActionID(route); const didLayout = useRef(false); const didSubscribeToReportTypingEvents = useRef(false); - const isFirstRender = useRef(true); - const timeoutIdCatted = useRef(null); - const timeoutIdExtended = useRef(null); + // const isFirstRender = useRef(true); - const [isLinkingToCattedMessage, setLinkingToCattedMessage] = useState(false); - const [isLinkingToExtendedMessage, setLinkingToExtendedMessage] = useState(false); + const [listID, setListID] = useState('1'); + const [isLinkingLoading, setLinkingLoading] = useState(!!reportActionID); const isLoadingLinkedMessage = !!reportActionID && props.isLoadingInitialReportActions; - const {catted: reportActionsBeforeAndIncludingLinked, expanded: reportActionsBeforeAndIncludingLinkedExpanded} = useMemo(() => { - if (reportActionID && allReportActions?.length) { - return ReportActionsUtils.getSlicedRangeFromArrayByID(allReportActions, reportActionID); - } - // catted means the reportActions before and including the linked message - // expanded means the reportActions before and including the linked message plus the next 5 - return {catted: [], expanded: []}; - }, [allReportActions, reportActionID]); - - const reportActions = useMemo(() => { - if (!reportActionID || (!isLinkingToCattedMessage && !isLoadingLinkedMessage && !isLinkingToExtendedMessage)) { - return allReportActions; - } - if (reportActionID && !isLinkingToCattedMessage && isLinkingToExtendedMessage) { - return reportActionsBeforeAndIncludingLinkedExpanded; - } - return reportActionsBeforeAndIncludingLinked; - }, [ - allReportActions, - reportActionsBeforeAndIncludingLinked, + console.log('get.isLoadingLinkedMessage', isLoadingLinkedMessage); + // const {catted: reportActionsBeforeAndIncludingLinked, expanded: reportActionsBeforeAndIncludingLinkedExpanded} = useMemo(() => { + // if (reportActionID && allReportActions?.length) { + // return ReportActionsUtils.getSlicedRangeFromArrayByID(allReportActions, reportActionID); + // } + // // catted means the reportActions before and including the linked message + // // expanded means the reportActions before and including the linked message plus the next 5 + // return {catted: [], expanded: []}; + // }, [allReportActions, reportActionID]); + + /** + * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently + * displaying. + */ + const throttledLoadNewerChats = useCallback( + ({distanceFromStart}) => { + console.log('get.throttledLoadNewerChats.0'); + // return null; + if (props.isLoadingNewerReportActions || props.isLoadingInitialReportActions) { + return; + } + console.log('get.throttledLoadNewerChats.1'); + + // Ideally, we wouldn't need to use the 'distanceFromStart' variable. However, due to the low value set for 'maxToRenderPerBatch', + // the component undergoes frequent re-renders. This frequent re-rendering triggers the 'onStartReached' callback multiple times. + // + // To mitigate this issue, we use 'CONST.CHAT_HEADER_LOADER_HEIGHT' as a threshold. This ensures that 'onStartReached' is not + // triggered unnecessarily when the chat is initially opened or when the user has reached the end of the list but hasn't scrolled further. + // + // Additionally, we use throttling on the 'onStartReached' callback to further reduce the frequency of its invocation. + // This should be removed once the issue of frequent re-renders is resolved. + // + // onStartReached is triggered during the first render. Since we use OpenReport on the first render and are confident about the message ordering, we can safely skip this call + // if (isFirstRender.current || isLinkingToExtendedMessage || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { + // if (isLinkingToExtendedMessage) { + // console.log('get.throttledLoadNewerChats.2', isFirstRender.current, isLinkingToExtendedMessage, distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT, distanceFromStart); + // // isFirstRender.current = false; + // return; + // } + + console.log('get.throttledLoadNewerChats.3'); + const newestReportAction = reportActions[0]; + Report.getNewerActions(reportID, newestReportAction.reportActionID); + }, + // [props.isLoadingNewerReportActions, props.isLoadingInitialReportActions, reportID, reportActions, hasNewestReportAction], + [props.isLoadingNewerReportActions, props.isLoadingInitialReportActions, reportID, reportActions], + ); + const triggerList = useCallback(() => { + setListID(id => id + 1) + },[setListID]) + + const { + cattedArray: reportActions, + fetchFunc, + linkedIdIndex, + setNull, + } = useHandleList( reportActionID, - isLinkingToCattedMessage, + allReportActions, + throttledLoadNewerChats, + route, isLoadingLinkedMessage, - isLinkingToExtendedMessage, - reportActionsBeforeAndIncludingLinkedExpanded, - ]); - - useEffect(() => { - if (isLoadingLinkedMessage) { - return; - } - if (scrollToBottom) { - scrollToBottom(); - } - - timeoutIdCatted.current = setTimeout(() => { - setLinkingToCattedMessage(false); - }, 100); - timeoutIdExtended.current = setTimeout(() => { - setLinkingToExtendedMessage(false); - }, 200); - }, [isLoadingLinkedMessage, scrollToBottom]); + triggerList + ); useEffect(() => { + console.log('get.useEffect.reportActionID', reportActionID); if (!reportActionID) { return; } - if (scrollToBottom) { - scrollToBottom(); - } - setLinkingToCattedMessage(true); - setLinkingToExtendedMessage(true); + console.log('get.useEffect.route', JSON.stringify(route)); fetchReport(); + // setNull() + // setListID(`${route.key}_${reportActionID}` ) }, [route, reportActionID, fetchReport, scrollToBottom]); - const isReportActionArrayCatted = useMemo(() => { - if (reportActions?.length !== allReportActions?.length && reportActionID) { - return true; - } - return false; - }, [reportActions, allReportActions, reportActionID]); - const hasCachedActions = useInitialValue(() => _.size(props.reportActions) > 0); const mostRecentIOUReportActionID = useInitialValue(() => ReportActionsUtils.getMostRecentIOURequestActionID(props.reportActions)); @@ -184,6 +288,25 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro const reportID = props.report.reportID; const hasNewestReportAction = lodashGet(reportActions[0], 'isNewestReportAction'); + // const listID = useMemo( + // () => + // // return reportActionID || 'list'; + // // console.log('route.key', route); + // `${route.key}_${reportActionID}`, + // // `${route.key}`, + // [reportActionID, route, isLinkingLoading], + // ); + // useEffect(() => { + // scrollToBottom(); + // }, [listID, scrollToBottom]); + + useEffect(() => { + if (!reportActionID) { + return; + } + setLinkingLoading(!!reportActionID); + }, [route, reportActionID]); + /** * @returns {Boolean} */ @@ -201,14 +324,6 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro useEffect(() => { openReportIfNecessary(); // eslint-disable-next-line react-hooks/exhaustive-deps - - return () => { - if (!timeoutIdCatted && !timeoutIdExtended) { - return; - } - clearTimeout(timeoutIdCatted.current); - clearTimeout(timeoutIdExtended.current); - }; }, []); useEffect(() => { @@ -287,36 +402,15 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro Report.getOlderActions(reportID, oldestReportAction.reportActionID); }; - /** - * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently - * displaying. - */ - const loadNewerChats = useMemo( - () => - _.throttle(({distanceFromStart}) => { - if (props.isLoadingNewerReportActions || props.isLoadingInitialReportActions || hasNewestReportAction) { - return; - } - - // Ideally, we wouldn't need to use the 'distanceFromStart' variable. However, due to the low value set for 'maxToRenderPerBatch', - // the component undergoes frequent re-renders. This frequent re-rendering triggers the 'onStartReached' callback multiple times. - // - // To mitigate this issue, we use 'CONST.CHAT_HEADER_LOADER_HEIGHT' as a threshold. This ensures that 'onStartReached' is not - // triggered unnecessarily when the chat is initially opened or when the user has reached the end of the list but hasn't scrolled further. - // - // Additionally, we use throttling on the 'onStartReached' callback to further reduce the frequency of its invocation. - // This should be removed once the issue of frequent re-renders is resolved. - // - // onStartReached is triggered during the first render. Since we use OpenReport on the first render and are confident about the message ordering, we can safely skip this call - if (isFirstRender.current || isLinkingToExtendedMessage || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { - isFirstRender.current = false; - return; - } - - const newestReportAction = reportActions[0]; - Report.getNewerActions(reportID, newestReportAction.reportActionID); - }, 500), - [props.isLoadingNewerReportActions, props.isLoadingInitialReportActions, isLinkingToExtendedMessage, reportID, reportActions, hasNewestReportAction], + const firstReportActionID = useMemo(() => reportActions[0]?.reportActionID, [reportActions]); + const handleLoadNewerChats = useCallback( + ({distanceFromStart}) => { + if ((reportActionID && linkedIdIndex > -1) || (!reportActionID && !hasNewestReportAction)) { + setLinkingLoading(false); + fetchFunc({firstReportActionID, distanceFromStart}); + } + }, + [hasNewestReportAction, linkedIdIndex, firstReportActionID, fetchFunc, reportActionID], ); /** @@ -352,14 +446,14 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro sortedReportActions={reportActions} mostRecentIOUReportActionID={mostRecentIOUReportActionID} loadOlderChats={loadOlderChats} - loadNewerChats={loadNewerChats} + loadNewerChats={handleLoadNewerChats} isLinkingLoader={!!reportActionID && props.isLoadingInitialReportActions} - isReportActionArrayCatted={isReportActionArrayCatted} isLoadingInitialReportActions={props.isLoadingInitialReportActions} isLoadingOlderReportActions={props.isLoadingOlderReportActions} isLoadingNewerReportActions={props.isLoadingNewerReportActions} reportScrollManager={reportScrollManager} policy={props.policy} + listID={listID} /> From 37b41c17263850a1367ed9bdd4111b3129cb7d45 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 19 Dec 2023 15:04:47 +0100 Subject: [PATCH 017/245] fix linking --- src/pages/home/report/ReportActionsView.js | 166 ++++----------------- 1 file changed, 27 insertions(+), 139 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index fccc6c8d7a66..41bdff7a74a3 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -96,17 +96,9 @@ function getReportActionID(route) { return {reportActionID: lodashGet(route, 'params.reportActionID', null), reportID: lodashGet(route, 'params.reportID', null)}; } -const useHandleList = (linkedID, messageArray, fetchFn, route, isLoadingLinkedMessage, cb) => { +const useHandleList = (linkedID, messageArray, fetchFn, route, isLoadingLinkedMessage) => { const [edgeID, setEdgeID] = useState(linkedID); - const prevLinkedID = useRef(linkedID); - const test = useRef(true); - // Function to calculate the current slice of the message array - - // const listID = useMemo(() => { - // // return reportActionID || 'list' - // console.log('route.key', route); - // return route.key + Math.random().toString; - // }, [route]); + const [listID, setListID] = useState(1); const index = useMemo(() => { if (!linkedID) { @@ -114,40 +106,23 @@ const useHandleList = (linkedID, messageArray, fetchFn, route, isLoadingLinkedMe } return messageArray.findIndex((obj) => String(obj.reportActionID) === String(edgeID || linkedID)); - }, [messageArray, linkedID, edgeID, isLoadingLinkedMessage]); + }, [messageArray, linkedID, edgeID]); - useEffect(() => { - console.log('get.useHandleList.setEdgeID_EMPTY', linkedID !== prevLinkedID.current, linkedID, prevLinkedID.current); + useMemo(() => { setEdgeID(''); - // if (linkedID !== prevLinkedID.current) { - // setEdgeID(''); - // prevLinkedID.current = linkedID; - // } - test.current = false; }, [route, linkedID]); - console.log('get.useHandleList.INFO.index', index); - console.log('get.useHandleList.INFO.linkedID', linkedID); - console.log('get.useHandleList.INFO.messageArray', messageArray.length); - const cattedArray = useMemo(() => { - if (!linkedID) { + if (!linkedID || index === -1) { return messageArray; } - if (index === -1) { - return messageArray; - } - console.log('get.useHandleList.calculateSlice.0.index', index); if (linkedID && !edgeID) { - console.log('get.useHandleList.calculateSlice.1.linkedID', linkedID); - cb(); + setListID((i) => i + 1); return messageArray.slice(index, messageArray.length); } else if (linkedID && edgeID) { - console.log('get.useHandleList.calculateSlice.2.linkedID_edgeID', linkedID, edgeID); - const amountOfItemsBeforeLinkedOne = 49; + const amountOfItemsBeforeLinkedOne = 10; const newStartIndex = index >= amountOfItemsBeforeLinkedOne ? index - amountOfItemsBeforeLinkedOne : 0; - console.log('get.useHandleList.calculateSlice.2.index_newStartIndex', index, newStartIndex); if (index) { return messageArray.slice(newStartIndex, messageArray.length); } @@ -156,20 +131,16 @@ const useHandleList = (linkedID, messageArray, fetchFn, route, isLoadingLinkedMe return messageArray; }, [linkedID, messageArray, edgeID, index, isLoadingLinkedMessage]); - // const cattedArray = calculateSlice(); - const hasMoreCashed = cattedArray.length < messageArray.length; - // Function to handle pagination (dummy in this case, as actual slicing is done in calculateSlice) const paginate = useCallback( ({firstReportActionID, distanceFromStart}) => { - // This function is a placeholder as the actual pagination is handled by calculateSlice + // This function is a placeholder as the actual pagination is handled by cattedArray // It's here if you need to trigger any side effects during pagination if (!hasMoreCashed) { - console.log('get.useHandleList.paginate.0.NO_CACHE'); fetchFn({distanceFromStart}); } - console.log('get.useHandleList.paginate.1.firstReportActionID'); + setEdgeID(firstReportActionID); }, [setEdgeID, fetchFn, hasMoreCashed], @@ -179,9 +150,7 @@ const useHandleList = (linkedID, messageArray, fetchFn, route, isLoadingLinkedMe cattedArray, fetchFunc: paginate, linkedIdIndex: index, - setNull: () => { - setEdgeID(''); - }, + listID, }; }; @@ -189,123 +158,42 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro useCopySelectionHelper(); const reactionListRef = useContext(ReactionListContext); const reportScrollManager = useReportScrollManager(); - const {scrollToBottom} = reportScrollManager; const route = useRoute(); const {reportActionID} = getReportActionID(route); const didLayout = useRef(false); const didSubscribeToReportTypingEvents = useRef(false); - // const isFirstRender = useRef(true); - - const [listID, setListID] = useState('1'); - const [isLinkingLoading, setLinkingLoading] = useState(!!reportActionID); const isLoadingLinkedMessage = !!reportActionID && props.isLoadingInitialReportActions; + const hasCachedActions = useInitialValue(() => _.size(props.reportActions) > 0); + const mostRecentIOUReportActionID = useInitialValue(() => ReportActionsUtils.getMostRecentIOURequestActionID(props.reportActions)); + + const prevNetworkRef = useRef(props.network); + const prevAuthTokenType = usePrevious(props.session.authTokenType); - console.log('get.isLoadingLinkedMessage', isLoadingLinkedMessage); - // const {catted: reportActionsBeforeAndIncludingLinked, expanded: reportActionsBeforeAndIncludingLinkedExpanded} = useMemo(() => { - // if (reportActionID && allReportActions?.length) { - // return ReportActionsUtils.getSlicedRangeFromArrayByID(allReportActions, reportActionID); - // } - // // catted means the reportActions before and including the linked message - // // expanded means the reportActions before and including the linked message plus the next 5 - // return {catted: [], expanded: []}; - // }, [allReportActions, reportActionID]); + const prevIsSmallScreenWidthRef = useRef(props.isSmallScreenWidth); + const isFocused = useIsFocused(); + const reportID = props.report.reportID; /** * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently * displaying. */ const throttledLoadNewerChats = useCallback( - ({distanceFromStart}) => { - console.log('get.throttledLoadNewerChats.0'); - // return null; + () => { if (props.isLoadingNewerReportActions || props.isLoadingInitialReportActions) { return; } - console.log('get.throttledLoadNewerChats.1'); - - // Ideally, we wouldn't need to use the 'distanceFromStart' variable. However, due to the low value set for 'maxToRenderPerBatch', - // the component undergoes frequent re-renders. This frequent re-rendering triggers the 'onStartReached' callback multiple times. - // - // To mitigate this issue, we use 'CONST.CHAT_HEADER_LOADER_HEIGHT' as a threshold. This ensures that 'onStartReached' is not - // triggered unnecessarily when the chat is initially opened or when the user has reached the end of the list but hasn't scrolled further. - // - // Additionally, we use throttling on the 'onStartReached' callback to further reduce the frequency of its invocation. - // This should be removed once the issue of frequent re-renders is resolved. - // - // onStartReached is triggered during the first render. Since we use OpenReport on the first render and are confident about the message ordering, we can safely skip this call - // if (isFirstRender.current || isLinkingToExtendedMessage || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { - // if (isLinkingToExtendedMessage) { - // console.log('get.throttledLoadNewerChats.2', isFirstRender.current, isLinkingToExtendedMessage, distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT, distanceFromStart); - // // isFirstRender.current = false; - // return; - // } - - console.log('get.throttledLoadNewerChats.3'); - const newestReportAction = reportActions[0]; + + // eslint-disable-next-line no-use-before-define Report.getNewerActions(reportID, newestReportAction.reportActionID); }, - // [props.isLoadingNewerReportActions, props.isLoadingInitialReportActions, reportID, reportActions, hasNewestReportAction], - [props.isLoadingNewerReportActions, props.isLoadingInitialReportActions, reportID, reportActions], + // eslint-disable-next-line no-use-before-define + [props.isLoadingNewerReportActions, props.isLoadingInitialReportActions, reportID, newestReportAction], ); - const triggerList = useCallback(() => { - setListID(id => id + 1) - },[setListID]) - - const { - cattedArray: reportActions, - fetchFunc, - linkedIdIndex, - setNull, - } = useHandleList( - reportActionID, - allReportActions, - throttledLoadNewerChats, - route, - isLoadingLinkedMessage, - triggerList - ); - - useEffect(() => { - console.log('get.useEffect.reportActionID', reportActionID); - if (!reportActionID) { - return; - } - console.log('get.useEffect.route', JSON.stringify(route)); - fetchReport(); - // setNull() - // setListID(`${route.key}_${reportActionID}` ) - }, [route, reportActionID, fetchReport, scrollToBottom]); - const hasCachedActions = useInitialValue(() => _.size(props.reportActions) > 0); - const mostRecentIOUReportActionID = useInitialValue(() => ReportActionsUtils.getMostRecentIOURequestActionID(props.reportActions)); - - const prevNetworkRef = useRef(props.network); - const prevAuthTokenType = usePrevious(props.session.authTokenType); + const {cattedArray: reportActions, fetchFunc, linkedIdIndex, listID} = useHandleList(reportActionID, allReportActions, throttledLoadNewerChats, route, isLoadingLinkedMessage); - const prevIsSmallScreenWidthRef = useRef(props.isSmallScreenWidth); - - const isFocused = useIsFocused(); - const reportID = props.report.reportID; const hasNewestReportAction = lodashGet(reportActions[0], 'isNewestReportAction'); - - // const listID = useMemo( - // () => - // // return reportActionID || 'list'; - // // console.log('route.key', route); - // `${route.key}_${reportActionID}`, - // // `${route.key}`, - // [reportActionID, route, isLinkingLoading], - // ); - // useEffect(() => { - // scrollToBottom(); - // }, [listID, scrollToBottom]); - - useEffect(() => { - if (!reportActionID) { - return; - } - setLinkingLoading(!!reportActionID); - }, [route, reportActionID]); + const newestReportAction = lodashGet(reportActions, '[0]'); /** * @returns {Boolean} @@ -404,9 +292,9 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro const firstReportActionID = useMemo(() => reportActions[0]?.reportActionID, [reportActions]); const handleLoadNewerChats = useCallback( + // eslint-disable-next-line rulesdir/prefer-early-return ({distanceFromStart}) => { - if ((reportActionID && linkedIdIndex > -1) || (!reportActionID && !hasNewestReportAction)) { - setLinkingLoading(false); + if ((reportActionID && linkedIdIndex > -1 && !hasNewestReportAction) || (!reportActionID && !hasNewestReportAction)) { fetchFunc({firstReportActionID, distanceFromStart}); } }, From a42d360757c0f95e0aaebb066e395f1f0564234e Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 19 Dec 2023 15:05:26 +0100 Subject: [PATCH 018/245] remove autoscrollToTopThreshold --- src/components/InvertedFlatList/BaseInvertedFlatList.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.js b/src/components/InvertedFlatList/BaseInvertedFlatList.js index 2862236daa07..abfad0f04be1 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.js +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.js @@ -2,8 +2,6 @@ import PropTypes from 'prop-types'; import React, {forwardRef} from 'react'; import FlatList from '@components/FlatList'; -const AUTOSCROLL_TO_TOP_THRESHOLD = 128; - const propTypes = { /** Same as FlatList can be any array of anything */ // eslint-disable-next-line react/forbid-prop-types @@ -25,7 +23,6 @@ const BaseInvertedFlatList = forwardRef((props, ref) => ( windowSize={15} maintainVisibleContentPosition={{ minIndexForVisible: 0, - // autoscrollToTopThreshold: AUTOSCROLL_TO_TOP_THRESHOLD, }} inverted /> From 67d530e8b8419b38ac673f546a277cc0b79f15e8 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 19 Dec 2023 15:05:59 +0100 Subject: [PATCH 019/245] clean ReportActionsUtils --- src/libs/ReportActionsUtils.ts | 35 ---------------------------------- 1 file changed, 35 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 4959c586a5fa..7eae4ce4d9e4 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -23,10 +23,6 @@ type LastVisibleMessage = { lastMessageHtml?: string; }; -type SlicedResult = { - catted: ReportAction[]; - expanded: ReportAction[]; -}; type MemberChangeMessageUserMentionElement = { readonly kind: 'userMention'; readonly accountID: number; @@ -302,36 +298,6 @@ function getRangeFromArrayByID(array: ReportAction[], id?: string): ReportAction return array.slice(startIndex, endIndex + 1); } -/** - * Returns the sliced range of report actions from the given array. - * - * param {ReportAction[]} array - * param {String} id - * returns {Object} - * getSlicedRangeFromArrayByID([{id:1}, ..., {id: 100}], 50) => { catted: [{id:1}, ..., {id: 50}], expanded: [{id: 45}, ..., {id: 55}] } - */ -function getSlicedRangeFromArrayByID(array: ReportAction[], id: string): SlicedResult { - let index; - if (id) { - index = array.findIndex((obj) => obj.reportActionID === id); - } else { - index = array.length - 1; - } - - if (index === -1) { - return {catted: [], expanded: []}; - } - - const amountOfItemsBeforeLinkedOne = 25; - const expandedStart = index >= amountOfItemsBeforeLinkedOne ? index - amountOfItemsBeforeLinkedOne : 0; - - const catted: ReportAction[] = array.slice(index, array.length); - const expanded: ReportAction[] = array.slice(expandedStart, array.length); - // We need the expanded version to prevent jittering of list. So when user navigate to linked message we show to him the catted version. After that we show the expanded version. - // Then we can show all reports. - return {catted, expanded}; -} - /** * Finds most recent IOU request action ID. */ @@ -936,7 +902,6 @@ export { shouldReportActionBeVisible, shouldReportActionBeVisibleAsLastAction, getRangeFromArrayByID, - getSlicedRangeFromArrayByID, hasRequestFromCurrentAccount, getFirstVisibleReportActionID, isMemberChangeAction, From aaf0377fb3bacf2f825d844d497b483d5fa4a2f8 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 19 Dec 2023 15:10:57 +0100 Subject: [PATCH 020/245] update initialNumToRender for web and desktop --- src/pages/home/report/ReportActionsList.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 36203cdd87a7..ac02c2974560 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -15,6 +15,7 @@ import useReportScrollManager from '@hooks/useReportScrollManager'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import DateUtils from '@libs/DateUtils'; +import getPlatform from '@libs/getPlatform'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import Visibility from '@libs/Visibility'; @@ -152,6 +153,8 @@ function ReportActionsList({ } return cacheUnreadMarkers.get(report.reportID); }; + const platform = getPlatform(); + const isNative = platform === CONST.PLATFORM.IOS || platform === CONST.PLATFORM.ANDROID; const [currentUnreadMarker, setCurrentUnreadMarker] = useState(markerInit); const scrollingVerticalOffset = useRef(0); const readActionSkipped = useRef(false); @@ -314,8 +317,14 @@ function ReportActionsList({ const initialNumToRender = useMemo(() => { const minimumReportActionHeight = styles.chatItem.paddingTop + styles.chatItem.paddingBottom + variables.fontSizeNormalHeight; const availableHeight = windowHeight - (CONST.CHAT_FOOTER_MIN_HEIGHT + variables.contentHeaderHeight); - return Math.ceil(availableHeight / minimumReportActionHeight); - }, [styles.chatItem.paddingBottom, styles.chatItem.paddingTop, windowHeight]); + const numToRender = Math.ceil(availableHeight / minimumReportActionHeight); + if (linkedReportActionID && !isNative) { + // For web and desktop environments, it's crucial to set this value equal to or higher than the 'batch per render' setting. If it's set lower, the 'onStartReached' event will be triggered excessively, every time an additional item enters the virtualized list. + + return Math.max(numToRender, 50); + } + return numToRender; + }, [styles.chatItem.paddingBottom, styles.chatItem.paddingTop, windowHeight, linkedReportActionID, isNative]); /** * Thread's divider line should hide when the first chat in the thread is marked as unread. From d6a9f584b304fca5d08766b25594a74d961a2498 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 19 Dec 2023 15:11:30 +0100 Subject: [PATCH 021/245] add listID for resetting --- src/pages/home/report/ReportActionsList.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index ac02c2974560..082f1437b5ed 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -139,6 +139,7 @@ function ReportActionsList({ onLayout, isComposerFullSize, reportScrollManager, + listID, }) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -483,6 +484,7 @@ function ReportActionsList({ onScroll={trackVerticalScrolling} onScrollToIndexFailed={() => {}} extraData={extraData} + key={listID} /> From 45fbbf0b52930ed4684f84a87c60cc56037d23e3 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 19 Dec 2023 15:12:24 +0100 Subject: [PATCH 022/245] WIP: fix MVCPFlatList --- src/components/FlatList/MVCPFlatList.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/FlatList/MVCPFlatList.js b/src/components/FlatList/MVCPFlatList.js index c9ec3c6a95c1..b6199e9174dd 100644 --- a/src/components/FlatList/MVCPFlatList.js +++ b/src/components/FlatList/MVCPFlatList.js @@ -1,7 +1,7 @@ /* eslint-disable es/no-optional-chaining, es/no-nullish-coalescing-operators, react/prop-types */ import PropTypes from 'prop-types'; import React from 'react'; -import {FlatList} from 'react-native'; +import {FlatList, InteractionManager} from 'react-native'; function mergeRefs(...args) { return function forwardRef(node) { @@ -137,8 +137,10 @@ const MVCPFlatList = React.forwardRef(({maintainVisibleContentPosition, horizont }, [adjustForMaintainVisibleContentPosition, getContentView, getScrollOffset, scrollToOffset]); React.useEffect(() => { - prepareForMaintainVisibleContentPosition(); - setupMutationObserver(); + InteractionManager.runAfterInteractions(() => { + prepareForMaintainVisibleContentPosition(); + setupMutationObserver(); + }); }, [prepareForMaintainVisibleContentPosition, setupMutationObserver]); const setMergedRef = useMergeRefs(scrollRef, forwardedRef); From 736a1e6e0fbbec21709067e39e8d41075d4770e4 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 20 Dec 2023 16:54:45 +0100 Subject: [PATCH 023/245] fix after merge --- src/pages/home/ReportScreen.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 75fb165ec475..6af7b7c2f724 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -154,7 +154,6 @@ function ReportScreen({ route, report, reportMetadata, - // sortedReportActions, allReportActions, accountManagerReportID, personalDetails, @@ -182,11 +181,11 @@ function ReportScreen({ const reportActions = useMemo(() => { if (allReportActions?.length === 0) return []; - const sorterReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions); + const sorterReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, true); const cattedRangeOfReportActions = ReportActionsUtils.getRangeFromArrayByID(sorterReportActions, reportActionID); const reportActionsWithoutDeleted = ReportActionsUtils.getReportActionsWithoutRemoved(cattedRangeOfReportActions); return reportActionsWithoutDeleted; - }, [reportActionID, allReportActions]); + }, [reportActionID, allReportActions, isOffline]); const [isBannerVisible, setIsBannerVisible] = useState(true); const [listHeight, setListHeight] = useState(0); const [scrollPosition, setScrollPosition] = useState({}); @@ -199,11 +198,9 @@ function ReportScreen({ const {addWorkspaceRoomOrChatPendingAction, addWorkspaceRoomOrChatErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report); const screenWrapperStyle = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}]; - // eslint-disable-next-line react-hooks/exhaustive-deps -- need to re-filter the report actions when network status changes - const filteredReportActions = useMemo(() => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), [isOffline, reportActions]); // There are no reportActions at all to display and we are still in the process of loading the next set of actions. - const isLoadingInitialReportActions = _.isEmpty(filteredReportActions) && reportMetadata.isLoadingInitialReportActions; + const isLoadingInitialReportActions = _.isEmpty(reportActions) && reportMetadata.isLoadingInitialReportActions; const isOptimisticDelete = lodashGet(report, 'statusNum') === CONST.REPORT.STATUS.CLOSED; @@ -470,7 +467,7 @@ function ReportScreen({ > {isReportReadyForDisplay && !isLoading && ( Date: Wed, 20 Dec 2023 16:55:22 +0100 Subject: [PATCH 024/245] another fix after merge --- src/pages/home/report/ReportActionsView.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 5be82b410b5b..3a6dd00c0699 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -219,6 +219,13 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + useEffect(() => { + if (!reportActionID) { + return; + } + Report.openReport({reportID, reportActionID}); + }, [route]); + useEffect(() => { const prevNetwork = prevNetworkRef.current; // When returning from offline to online state we want to trigger a request to OpenReport which From d94d8d6be3a2ddad450674afd6aadf724672afb0 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 21 Dec 2023 09:38:01 +0100 Subject: [PATCH 025/245] add pending reportAction to sorted list --- src/libs/ReportActionsUtils.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 9cea9f1100da..b3f46792fff3 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -275,7 +275,7 @@ function getRangeFromArrayByID(array: ReportAction[], id?: string): ReportAction if (id) { index = array.findIndex((obj) => obj.reportActionID === id); } else { - index = 0; + index = array.findIndex((obj) => obj.pendingAction !== 'add'); } if (index === -1) { @@ -285,13 +285,13 @@ function getRangeFromArrayByID(array: ReportAction[], id?: string): ReportAction let startIndex = index; let endIndex = index; - // Move down the list and compare reportActionID with previousReportActionID + // Move up the list and compare reportActionID with previousReportActionID while (endIndex < array.length - 1 && array[endIndex].previousReportActionID === array[endIndex + 1].reportActionID) { endIndex++; } // Move up the list and compare previousReportActionID with reportActionID - while (startIndex > 0 && array[startIndex].reportActionID === array[startIndex - 1].previousReportActionID) { + while (startIndex > 0 && array[startIndex].reportActionID === array[startIndex - 1].previousReportActionID || array[startIndex - 1]?.pendingAction === 'add' ) { startIndex--; } @@ -910,4 +910,4 @@ export { isReimbursementDeQueuedAction, }; -export type {LastVisibleMessage}; \ No newline at end of file +export type {LastVisibleMessage}; From 338fe4a6eebfd8d12b7c9d914859af31ef5ce1ee Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 21 Dec 2023 09:38:37 +0100 Subject: [PATCH 026/245] explaining --- src/libs/ReportActionsUtils.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index b3f46792fff3..fad712e19fbd 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -275,7 +275,7 @@ function getRangeFromArrayByID(array: ReportAction[], id?: string): ReportAction if (id) { index = array.findIndex((obj) => obj.reportActionID === id); } else { - index = array.findIndex((obj) => obj.pendingAction !== 'add'); + index = array.findIndex((obj) => obj.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); } if (index === -1) { @@ -285,13 +285,22 @@ function getRangeFromArrayByID(array: ReportAction[], id?: string): ReportAction let startIndex = index; let endIndex = index; - // Move up the list and compare reportActionID with previousReportActionID + // Iterate forwards through the array, starting from endIndex. This loop checks the continuity of actions by: + // 1. Comparing the current item's previousReportActionID with the next item's reportActionID. + // This ensures that we are moving in a sequence of related actions from newer to older. while (endIndex < array.length - 1 && array[endIndex].previousReportActionID === array[endIndex + 1].reportActionID) { endIndex++; } - // Move up the list and compare previousReportActionID with reportActionID - while (startIndex > 0 && array[startIndex].reportActionID === array[startIndex - 1].previousReportActionID || array[startIndex - 1]?.pendingAction === 'add' ) { + // Iterate backwards through the array, starting from startIndex. This loop has two main checks: + // 1. It compares the current item's reportActionID with the previous item's previousReportActionID. + // This is to ensure continuity in a sequence of actions. + // 2. If the first condition fails, it then checks if the previous item has a pendingAction of 'add'. + // This additional check is to include recently sent messages that might not yet be part of the established sequence. + while ( + (startIndex > 0 && array[startIndex].reportActionID === array[startIndex - 1].previousReportActionID) || + array[startIndex - 1]?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD + ) { startIndex--; } From 6cf2220c0e9034b81430b565ad3f75a6b17a2de8 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 21 Dec 2023 17:36:07 +0100 Subject: [PATCH 027/245] WIP: temporary clean onyx button --- src/components/FloatingActionButton.js | 5 ++- src/libs/Permissions.ts | 2 +- .../CheckForPreviousReportActionIDClean.ts | 32 +++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 src/libs/migrations/CheckForPreviousReportActionIDClean.ts diff --git a/src/components/FloatingActionButton.js b/src/components/FloatingActionButton.js index 791eb150f8c9..8e5f567d8e9d 100644 --- a/src/components/FloatingActionButton.js +++ b/src/components/FloatingActionButton.js @@ -2,6 +2,7 @@ import PropTypes from 'prop-types'; import React, {PureComponent} from 'react'; import {Animated, Easing, View} from 'react-native'; import compose from '@libs/compose'; +import CheckForPreviousReportActionIDClean from '@libs/migrations/CheckForPreviousReportActionIDClean'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import PressableWithFeedback from './Pressable/PressableWithFeedback'; @@ -100,7 +101,9 @@ class FloatingActionButton extends PureComponent { this.fabPressable.blur(); this.props.onPress(e); }} - onLongPress={() => {}} + onLongPress={() => { + CheckForPreviousReportActionIDClean(); + }} style={[this.props.themeStyles.floatingActionButton, this.props.StyleUtils.getAnimatedFABStyle(rotate, backgroundColor)]} > ): boolean { } function canUseCommentLinking(betas: OnyxEntry): boolean { - return !!betas?.includes(CONST.BETAS.BETA_COMMENT_LINKING) || canUseAllBetas(betas); + return '!!betas?.includes(CONST.BETAS.BETA_COMMENT_LINKING) || canUseAllBetas(betas)'; } function canUseReportFields(betas: OnyxEntry): boolean { diff --git a/src/libs/migrations/CheckForPreviousReportActionIDClean.ts b/src/libs/migrations/CheckForPreviousReportActionIDClean.ts new file mode 100644 index 000000000000..7ee7a498d1a6 --- /dev/null +++ b/src/libs/migrations/CheckForPreviousReportActionIDClean.ts @@ -0,0 +1,32 @@ +import Onyx, {OnyxCollection} from 'react-native-onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; +import * as OnyxTypes from '@src/types/onyx'; + +function getReportActionsFromOnyxClean(): Promise> { + return new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + waitForCollectionCallback: true, + callback: (allReportActions) => { + Onyx.disconnect(connectionID); + return resolve(allReportActions); + }, + }); + }); +} + +/** + * This migration checks for the 'previousReportActionID' key in the first valid reportAction of a report in Onyx. + * If the key is not found then all reportActions for all reports are removed from Onyx. + */ +export default function (): Promise { + return getReportActionsFromOnyx().then((allReportActions) => { + const onyxData: OnyxCollection = {}; + + Object.keys(allReportActions ?? {}).forEach((onyxKey) => { + onyxData[onyxKey] = {}; + }); + + return Onyx.multiSet(onyxData as Record<`${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS}`, Record>); + }); +} From ee2b7794aae4533a72f124f180925b814819cadc Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 22 Dec 2023 15:22:51 +0100 Subject: [PATCH 028/245] return patches --- ...eact-native+virtualized-lists+0.72.8.patch | 34 - .../react-native-web+0.19.9+001+initial.patch | 872 +++++------------- ...react-native-web+0.19.9+002+fix-mvcp.patch | 687 ++++++++++++++ ...tive-web+0.19.9+003+measureInWindow.patch} | 0 ...e-web+0.19.9+004+fix-pointer-events.patch} | 0 ...-native-web+0.19.9+005+fixLastSpacer.patch | 2 +- 6 files changed, 943 insertions(+), 652 deletions(-) delete mode 100644 patches/@react-native+virtualized-lists+0.72.8.patch create mode 100644 patches/react-native-web+0.19.9+002+fix-mvcp.patch rename patches/{react-native-web+0.19.9+002+measureInWindow.patch => react-native-web+0.19.9+003+measureInWindow.patch} (100%) rename patches/{react-native-web+0.19.9+003+fix-pointer-events.patch => react-native-web+0.19.9+004+fix-pointer-events.patch} (100%) diff --git a/patches/@react-native+virtualized-lists+0.72.8.patch b/patches/@react-native+virtualized-lists+0.72.8.patch deleted file mode 100644 index a3bef95f1618..000000000000 --- a/patches/@react-native+virtualized-lists+0.72.8.patch +++ /dev/null @@ -1,34 +0,0 @@ -diff --git a/node_modules/@react-native/virtualized-lists/Lists/VirtualizedList.js b/node_modules/@react-native/virtualized-lists/Lists/VirtualizedList.js -index ef5a3f0..2590edd 100644 ---- a/node_modules/@react-native/virtualized-lists/Lists/VirtualizedList.js -+++ b/node_modules/@react-native/virtualized-lists/Lists/VirtualizedList.js -@@ -125,19 +125,6 @@ function windowSizeOrDefault(windowSize: ?number) { - return windowSize ?? 21; - } - --function findLastWhere( -- arr: $ReadOnlyArray, -- predicate: (element: T) => boolean, --): T | null { -- for (let i = arr.length - 1; i >= 0; i--) { -- if (predicate(arr[i])) { -- return arr[i]; -- } -- } -- -- return null; --} -- - /** - * Base implementation for the more convenient [``](https://reactnative.dev/docs/flatlist) - * and [``](https://reactnative.dev/docs/sectionlist) components, which are also better -@@ -1019,7 +1006,8 @@ class VirtualizedList extends StateSafePureComponent { - const spacerKey = this._getSpacerKey(!horizontal); - - const renderRegions = this.state.renderMask.enumerateRegions(); -- const lastSpacer = findLastWhere(renderRegions, r => r.isSpacer); -+ const lastRegion = renderRegions[renderRegions.length - 1]; -+ const lastSpacer = lastRegion?.isSpacer ? lastRegion : null; - - for (const section of renderRegions) { - if (section.isSpacer) { diff --git a/patches/react-native-web+0.19.9+001+initial.patch b/patches/react-native-web+0.19.9+001+initial.patch index 91ba6bfd59c0..d88ef83d4bcd 100644 --- a/patches/react-native-web+0.19.9+001+initial.patch +++ b/patches/react-native-web+0.19.9+001+initial.patch @@ -1,648 +1,286 @@ diff --git a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js -index c879838..0c9dfcb 100644 +index c879838..288316c 100644 --- a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js +++ b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js -@@ -285,7 +285,7 @@ class VirtualizedList extends StateSafePureComponent { - // $FlowFixMe[missing-local-annot] - - constructor(_props) { -- var _this$props$updateCel; -+ var _this$props$updateCel, _this$props$maintainV, _this$props$maintainV2; - super(_props); - this._getScrollMetrics = () => { - return this._scrollMetrics; -@@ -520,6 +520,11 @@ class VirtualizedList extends StateSafePureComponent { - visibleLength, - zoomScale - }; -+ if (this.state.pendingScrollUpdateCount > 0) { -+ this.setState(state => ({ -+ pendingScrollUpdateCount: state.pendingScrollUpdateCount - 1 -+ })); -+ } - this._updateViewableItems(this.props, this.state.cellsAroundViewport); - if (!this.props) { - return; -@@ -569,7 +574,7 @@ class VirtualizedList extends StateSafePureComponent { - this._updateCellsToRender = () => { - this._updateViewableItems(this.props, this.state.cellsAroundViewport); - this.setState((state, props) => { -- var cellsAroundViewport = this._adjustCellsAroundViewport(props, state.cellsAroundViewport); -+ var cellsAroundViewport = this._adjustCellsAroundViewport(props, state.cellsAroundViewport, state.pendingScrollUpdateCount); - var renderMask = VirtualizedList._createRenderMask(props, cellsAroundViewport, this._getNonViewportRenderRegions(props)); - if (cellsAroundViewport.first === state.cellsAroundViewport.first && cellsAroundViewport.last === state.cellsAroundViewport.last && renderMask.equals(state.renderMask)) { - return null; -@@ -589,7 +594,7 @@ class VirtualizedList extends StateSafePureComponent { - return { - index, - item, -- key: this._keyExtractor(item, index, props), -+ key: VirtualizedList._keyExtractor(item, index, props), - isViewable - }; - }; -@@ -621,12 +626,10 @@ class VirtualizedList extends StateSafePureComponent { - }; - this._getFrameMetrics = (index, props) => { - var data = props.data, -- getItem = props.getItem, - getItemCount = props.getItemCount, - getItemLayout = props.getItemLayout; - invariant(index >= 0 && index < getItemCount(data), 'Tried to get frame for out of range index ' + index); -- var item = getItem(data, index); -- var frame = this._frames[this._keyExtractor(item, index, props)]; -+ var frame = this._frames[VirtualizedList._getItemKey(props, index)]; - if (!frame || frame.index !== index) { - if (getItemLayout) { - /* $FlowFixMe[prop-missing] (>=0.63.0 site=react_native_fb) This comment -@@ -650,7 +653,7 @@ class VirtualizedList extends StateSafePureComponent { - - // The last cell we rendered may be at a new index. Bail if we don't know - // where it is. -- if (focusedCellIndex >= itemCount || this._keyExtractor(props.getItem(props.data, focusedCellIndex), focusedCellIndex, props) !== this._lastFocusedCellKey) { -+ if (focusedCellIndex >= itemCount || VirtualizedList._getItemKey(props, focusedCellIndex) !== this._lastFocusedCellKey) { - return []; - } - var first = focusedCellIndex; -@@ -690,9 +693,15 @@ class VirtualizedList extends StateSafePureComponent { - } - } - var initialRenderRegion = VirtualizedList._initialRenderRegion(_props); -+ var minIndexForVisible = (_this$props$maintainV = (_this$props$maintainV2 = this.props.maintainVisibleContentPosition) == null ? void 0 : _this$props$maintainV2.minIndexForVisible) !== null && _this$props$maintainV !== void 0 ? _this$props$maintainV : 0; - this.state = { - cellsAroundViewport: initialRenderRegion, -- renderMask: VirtualizedList._createRenderMask(_props, initialRenderRegion) -+ renderMask: VirtualizedList._createRenderMask(_props, initialRenderRegion), -+ firstVisibleItemKey: this.props.getItemCount(this.props.data) > minIndexForVisible ? VirtualizedList._getItemKey(this.props, minIndexForVisible) : null, -+ // When we have a non-zero initialScrollIndex, we will receive a -+ // scroll event later so this will prevent the window from updating -+ // until we get a valid offset. -+ pendingScrollUpdateCount: this.props.initialScrollIndex != null && this.props.initialScrollIndex > 0 ? 1 : 0 - }; - - // REACT-NATIVE-WEB patch to preserve during future RN merges: Support inverted wheel scroller. -@@ -748,6 +757,26 @@ class VirtualizedList extends StateSafePureComponent { - } - } - } -+ static _findItemIndexWithKey(props, key, hint) { -+ var itemCount = props.getItemCount(props.data); -+ if (hint != null && hint >= 0 && hint < itemCount) { -+ var curKey = VirtualizedList._getItemKey(props, hint); -+ if (curKey === key) { -+ return hint; -+ } -+ } -+ for (var ii = 0; ii < itemCount; ii++) { -+ var _curKey = VirtualizedList._getItemKey(props, ii); -+ if (_curKey === key) { -+ return ii; -+ } +@@ -117,6 +117,14 @@ function findLastWhere(arr, predicate) { + * + */ + class VirtualizedList extends StateSafePureComponent { ++ pushOrUnshift(input, item) { ++ if (this.props.inverted) { ++ input.unshift(item); ++ } else { ++ input.push(item); + } -+ return null; + } -+ static _getItemKey(props, index) { -+ var item = props.getItem(props.data, index); -+ return VirtualizedList._keyExtractor(item, index, props); -+ } - static _createRenderMask(props, cellsAroundViewport, additionalRegions) { - var itemCount = props.getItemCount(props.data); - invariant(cellsAroundViewport.first >= 0 && cellsAroundViewport.last >= cellsAroundViewport.first - 1 && cellsAroundViewport.last < itemCount, "Invalid cells around viewport \"[" + cellsAroundViewport.first + ", " + cellsAroundViewport.last + "]\" was passed to VirtualizedList._createRenderMask"); -@@ -796,7 +825,7 @@ class VirtualizedList extends StateSafePureComponent { - } - } - } -- _adjustCellsAroundViewport(props, cellsAroundViewport) { -+ _adjustCellsAroundViewport(props, cellsAroundViewport, pendingScrollUpdateCount) { - var data = props.data, - getItemCount = props.getItemCount; - var onEndReachedThreshold = onEndReachedThresholdOrDefault(props.onEndReachedThreshold); -@@ -819,17 +848,9 @@ class VirtualizedList extends StateSafePureComponent { - last: Math.min(cellsAroundViewport.last + renderAhead, getItemCount(data) - 1) - }; - } else { -- // If we have a non-zero initialScrollIndex and run this before we've scrolled, -- // we'll wipe out the initialNumToRender rendered elements starting at initialScrollIndex. -- // So let's wait until we've scrolled the view to the right place. And until then, -- // we will trust the initialScrollIndex suggestion. -- -- // Thus, we want to recalculate the windowed render limits if any of the following hold: -- // - initialScrollIndex is undefined or is 0 -- // - initialScrollIndex > 0 AND scrolling is complete -- // - initialScrollIndex > 0 AND the end of the list is visible (this handles the case -- // where the list is shorter than the visible area) -- if (props.initialScrollIndex && !this._scrollMetrics.offset && Math.abs(distanceFromEnd) >= Number.EPSILON) { -+ // If we have a pending scroll update, we should not adjust the render window as it -+ // might override the correct window. -+ if (pendingScrollUpdateCount > 0) { - return cellsAroundViewport.last >= getItemCount(data) ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) : cellsAroundViewport; ++ + // scrollToEnd may be janky without getItemLayout prop + scrollToEnd(params) { + var animated = params ? params.animated : true; +@@ -350,6 +358,7 @@ class VirtualizedList extends StateSafePureComponent { + }; + this._defaultRenderScrollComponent = props => { + var onRefresh = props.onRefresh; ++ var inversionStyle = this.props.inverted ? this.props.horizontal ? styles.rowReverse : styles.columnReverse : null; + if (this._isNestedWithSameOrientation()) { + // $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors + return /*#__PURE__*/React.createElement(View, props); +@@ -367,13 +376,16 @@ class VirtualizedList extends StateSafePureComponent { + refreshing: props.refreshing, + onRefresh: onRefresh, + progressViewOffset: props.progressViewOffset +- }) : props.refreshControl ++ }) : props.refreshControl, ++ contentContainerStyle: [inversionStyle, this.props.contentContainerStyle] + })) + ); + } else { + // $FlowFixMe[prop-missing] Invalid prop usage + // $FlowFixMe[incompatible-use] +- return /*#__PURE__*/React.createElement(ScrollView, props); ++ return /*#__PURE__*/React.createElement(ScrollView, _extends({}, props, { ++ contentContainerStyle: [inversionStyle, this.props.contentContainerStyle] ++ })); } - newCellsAroundViewport = computeWindowedRenderLimits(props, maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch), windowSizeOrDefault(props.windowSize), cellsAroundViewport, this.__getFrameMetricsApprox, this._scrollMetrics); -@@ -902,16 +923,36 @@ class VirtualizedList extends StateSafePureComponent { - } - } - static getDerivedStateFromProps(newProps, prevState) { -+ var _newProps$maintainVis, _newProps$maintainVis2; - // first and last could be stale (e.g. if a new, shorter items props is passed in), so we make - // sure we're rendering a reasonable range here. - var itemCount = newProps.getItemCount(newProps.data); - if (itemCount === prevState.renderMask.numCells()) { - return prevState; - } -- var constrainedCells = VirtualizedList._constrainToItemCount(prevState.cellsAroundViewport, newProps); -+ var maintainVisibleContentPositionAdjustment = null; -+ var prevFirstVisibleItemKey = prevState.firstVisibleItemKey; -+ var minIndexForVisible = (_newProps$maintainVis = (_newProps$maintainVis2 = newProps.maintainVisibleContentPosition) == null ? void 0 : _newProps$maintainVis2.minIndexForVisible) !== null && _newProps$maintainVis !== void 0 ? _newProps$maintainVis : 0; -+ var newFirstVisibleItemKey = newProps.getItemCount(newProps.data) > minIndexForVisible ? VirtualizedList._getItemKey(newProps, minIndexForVisible) : null; -+ if (newProps.maintainVisibleContentPosition != null && prevFirstVisibleItemKey != null && newFirstVisibleItemKey != null) { -+ if (newFirstVisibleItemKey !== prevFirstVisibleItemKey) { -+ // Fast path if items were added at the start of the list. -+ var hint = itemCount - prevState.renderMask.numCells() + minIndexForVisible; -+ var firstVisibleItemIndex = VirtualizedList._findItemIndexWithKey(newProps, prevFirstVisibleItemKey, hint); -+ maintainVisibleContentPositionAdjustment = firstVisibleItemIndex != null ? firstVisibleItemIndex - minIndexForVisible : null; -+ } else { -+ maintainVisibleContentPositionAdjustment = null; -+ } -+ } -+ var constrainedCells = VirtualizedList._constrainToItemCount(maintainVisibleContentPositionAdjustment != null ? { -+ first: prevState.cellsAroundViewport.first + maintainVisibleContentPositionAdjustment, -+ last: prevState.cellsAroundViewport.last + maintainVisibleContentPositionAdjustment -+ } : prevState.cellsAroundViewport, newProps); - return { - cellsAroundViewport: constrainedCells, -- renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells) -+ renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells), -+ firstVisibleItemKey: newFirstVisibleItemKey, -+ pendingScrollUpdateCount: maintainVisibleContentPositionAdjustment != null ? prevState.pendingScrollUpdateCount + 1 : prevState.pendingScrollUpdateCount }; - } - _pushCells(cells, stickyHeaderIndices, stickyIndicesFromProps, first, last, inversionStyle) { -@@ -934,7 +975,7 @@ class VirtualizedList extends StateSafePureComponent { - last = Math.min(end, last); - var _loop = function _loop() { - var item = getItem(data, ii); -- var key = _this._keyExtractor(item, ii, _this.props); -+ var key = VirtualizedList._keyExtractor(item, ii, _this.props); + this._onCellLayout = (e, cellKey, index) => { +@@ -683,7 +695,7 @@ class VirtualizedList extends StateSafePureComponent { + onViewableItemsChanged = _this$props3.onViewableItemsChanged, + viewabilityConfig = _this$props3.viewabilityConfig; + if (onViewableItemsChanged) { +- this._viewabilityTuples.push({ ++ this.pushOrUnshift(this._viewabilityTuples, { + viewabilityHelper: new ViewabilityHelper(viewabilityConfig), + onViewableItemsChanged: onViewableItemsChanged + }); +@@ -937,10 +949,10 @@ class VirtualizedList extends StateSafePureComponent { + var key = _this._keyExtractor(item, ii, _this.props); _this._indicesToKeys.set(ii, key); if (stickyIndicesFromProps.has(ii + stickyOffset)) { - stickyHeaderIndices.push(cells.length); -@@ -969,20 +1010,23 @@ class VirtualizedList extends StateSafePureComponent { - } - static _constrainToItemCount(cells, props) { - var itemCount = props.getItemCount(props.data); -- var last = Math.min(itemCount - 1, cells.last); -+ var lastPossibleCellIndex = itemCount - 1; -+ -+ // Constraining `last` may significantly shrink the window. Adjust `first` -+ // to expand the window if the new `last` results in a new window smaller -+ // than the number of cells rendered per batch. - var maxToRenderPerBatch = maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch); -+ var maxFirst = Math.max(0, lastPossibleCellIndex - maxToRenderPerBatch); - return { -- first: clamp(0, itemCount - 1 - maxToRenderPerBatch, cells.first), -- last -+ first: clamp(0, cells.first, maxFirst), -+ last: Math.min(lastPossibleCellIndex, cells.last) - }; - } - _isNestedWithSameOrientation() { - var nestedContext = this.context; - return !!(nestedContext && !!nestedContext.horizontal === horizontalOrDefault(this.props.horizontal)); - } -- _keyExtractor(item, index, props -- // $FlowFixMe[missing-local-annot] -- ) { -+ static _keyExtractor(item, index, props) { - if (props.keyExtractor != null) { - return props.keyExtractor(item, index); - } -@@ -1022,7 +1066,12 @@ class VirtualizedList extends StateSafePureComponent { - cells.push( /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { +- stickyHeaderIndices.push(cells.length); ++ _this.pushOrUnshift(stickyHeaderIndices, cells.length); + } + var shouldListenForLayout = getItemLayout == null || debug || _this._fillRateHelper.enabled(); +- cells.push( /*#__PURE__*/React.createElement(CellRenderer, _extends({ ++ _this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(CellRenderer, _extends({ + CellRendererComponent: CellRendererComponent, + ItemSeparatorComponent: ii < end ? ItemSeparatorComponent : undefined, + ListItemComponent: ListItemComponent, +@@ -1012,14 +1024,14 @@ class VirtualizedList extends StateSafePureComponent { + // 1. Add cell for ListHeaderComponent + if (ListHeaderComponent) { + if (stickyIndicesFromProps.has(0)) { +- stickyHeaderIndices.push(0); ++ this.pushOrUnshift(stickyHeaderIndices, 0); + } + var _element = /*#__PURE__*/React.isValidElement(ListHeaderComponent) ? ListHeaderComponent : + /*#__PURE__*/ + // $FlowFixMe[not-a-component] + // $FlowFixMe[incompatible-type-arg] + React.createElement(ListHeaderComponent, null); +- cells.push( /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { ++ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { cellKey: this._getCellKey() + '-header', key: "$header" -- }, /*#__PURE__*/React.createElement(View, { -+ }, /*#__PURE__*/React.createElement(View -+ // We expect that header component will be a single native view so make it -+ // not collapsable to avoid this view being flattened and make this assumption -+ // no longer true. -+ , { -+ collapsable: false, - onLayout: this._onLayoutHeader, - style: [inversionStyle, this.props.ListHeaderComponentStyle] - }, -@@ -1124,7 +1173,11 @@ class VirtualizedList extends StateSafePureComponent { - // TODO: Android support - invertStickyHeaders: this.props.invertStickyHeaders !== undefined ? this.props.invertStickyHeaders : this.props.inverted, - stickyHeaderIndices, -- style: inversionStyle ? [inversionStyle, this.props.style] : this.props.style -+ style: inversionStyle ? [inversionStyle, this.props.style] : this.props.style, -+ maintainVisibleContentPosition: this.props.maintainVisibleContentPosition != null ? _objectSpread(_objectSpread({}, this.props.maintainVisibleContentPosition), {}, { -+ // Adjust index to account for ListHeaderComponent. -+ minIndexForVisible: this.props.maintainVisibleContentPosition.minIndexForVisible + (this.props.ListHeaderComponent ? 1 : 0) -+ }) : undefined - }); - this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1; - var innerRet = /*#__PURE__*/React.createElement(VirtualizedListContextProvider, { -@@ -1307,8 +1360,12 @@ class VirtualizedList extends StateSafePureComponent { - onStartReached = _this$props8.onStartReached, - onStartReachedThreshold = _this$props8.onStartReachedThreshold, - onEndReached = _this$props8.onEndReached, -- onEndReachedThreshold = _this$props8.onEndReachedThreshold, -- initialScrollIndex = _this$props8.initialScrollIndex; -+ onEndReachedThreshold = _this$props8.onEndReachedThreshold; -+ // If we have any pending scroll updates it means that the scroll metrics -+ // are out of date and we should not call any of the edge reached callbacks. -+ if (this.state.pendingScrollUpdateCount > 0) { -+ return; -+ } - var _this$_scrollMetrics2 = this._scrollMetrics, - contentLength = _this$_scrollMetrics2.contentLength, - visibleLength = _this$_scrollMetrics2.visibleLength, -@@ -1348,16 +1405,10 @@ class VirtualizedList extends StateSafePureComponent { - // and call onStartReached only once for a given content length, - // and only if onEndReached is not being executed - else if (onStartReached != null && this.state.cellsAroundViewport.first === 0 && isWithinStartThreshold && this._scrollMetrics.contentLength !== this._sentStartForContentLength) { -- // On initial mount when using initialScrollIndex the offset will be 0 initially -- // and will trigger an unexpected onStartReached. To avoid this we can use -- // timestamp to differentiate between the initial scroll metrics and when we actually -- // received the first scroll event. -- if (!initialScrollIndex || this._scrollMetrics.timestamp !== 0) { -- this._sentStartForContentLength = this._scrollMetrics.contentLength; -- onStartReached({ -- distanceFromStart -- }); -- } -+ this._sentStartForContentLength = this._scrollMetrics.contentLength; -+ onStartReached({ -+ distanceFromStart -+ }); - } - - // If the user scrolls away from the start or end and back again, -@@ -1412,6 +1463,11 @@ class VirtualizedList extends StateSafePureComponent { + }, /*#__PURE__*/React.createElement(View, { +@@ -1038,7 +1050,7 @@ class VirtualizedList extends StateSafePureComponent { + // $FlowFixMe[not-a-component] + // $FlowFixMe[incompatible-type-arg] + React.createElement(ListEmptyComponent, null); +- cells.push( /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { ++ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { + cellKey: this._getCellKey() + '-empty', + key: "$empty" + }, /*#__PURE__*/React.cloneElement(_element2, { +@@ -1077,7 +1089,7 @@ class VirtualizedList extends StateSafePureComponent { + var firstMetrics = this.__getFrameMetricsApprox(section.first, this.props); + var lastMetrics = this.__getFrameMetricsApprox(last, this.props); + var spacerSize = lastMetrics.offset + lastMetrics.length - firstMetrics.offset; +- cells.push( /*#__PURE__*/React.createElement(View, { ++ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(View, { + key: "$spacer-" + section.first, + style: { + [spacerKey]: spacerSize +@@ -1100,7 +1112,7 @@ class VirtualizedList extends StateSafePureComponent { + // $FlowFixMe[not-a-component] + // $FlowFixMe[incompatible-type-arg] + React.createElement(ListFooterComponent, null); +- cells.push( /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { ++ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { + cellKey: this._getFooterCellKey(), + key: "$footer" + }, /*#__PURE__*/React.createElement(View, { +@@ -1266,7 +1278,7 @@ class VirtualizedList extends StateSafePureComponent { + * suppresses an error found when Flow v0.68 was deployed. To see the + * error delete this comment and run Flow. */ + if (frame.inLayout) { +- framesInLayout.push(frame); ++ this.pushOrUnshift(framesInLayout, frame); + } } + var windowTop = this.__getFrameMetricsApprox(this.state.cellsAroundViewport.first, this.props).offset; +@@ -1452,6 +1464,12 @@ var styles = StyleSheet.create({ + left: 0, + borderColor: 'red', + borderWidth: 2 ++ }, ++ rowReverse: { ++ flexDirection: 'row-reverse' ++ }, ++ columnReverse: { ++ flexDirection: 'column-reverse' } - _updateViewableItems(props, cellsAroundViewport) { -+ // If we have any pending scroll updates it means that the scroll metrics -+ // are out of date and we should not call any of the visibility callbacks. -+ if (this.state.pendingScrollUpdateCount > 0) { -+ return; -+ } - this._viewabilityTuples.forEach(tuple => { - tuple.viewabilityHelper.onUpdate(props, this._scrollMetrics.offset, this._scrollMetrics.visibleLength, this._getFrameMetrics, this._createViewToken, tuple.onViewableItemsChanged, cellsAroundViewport); - }); + }); + export default VirtualizedList; +\ No newline at end of file diff --git a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js -index c7d68bb..43f9653 100644 +index c7d68bb..46b3fc9 100644 --- a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js +++ b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js -@@ -75,6 +75,10 @@ type ViewabilityHelperCallbackTuple = { - type State = { - renderMask: CellRenderMask, - cellsAroundViewport: {first: number, last: number}, -+ // Used to track items added at the start of the list for maintainVisibleContentPosition. -+ firstVisibleItemKey: ?string, -+ // When > 0 the scroll position available in JS is considered stale and should not be used. -+ pendingScrollUpdateCount: number, - }; - - /** -@@ -447,9 +451,24 @@ class VirtualizedList extends StateSafePureComponent { - - const initialRenderRegion = VirtualizedList._initialRenderRegion(props); +@@ -167,6 +167,14 @@ function findLastWhere( + class VirtualizedList extends StateSafePureComponent { + static contextType: typeof VirtualizedListContext = VirtualizedListContext; -+ const minIndexForVisible = -+ this.props.maintainVisibleContentPosition?.minIndexForVisible ?? 0; -+ - this.state = { - cellsAroundViewport: initialRenderRegion, - renderMask: VirtualizedList._createRenderMask(props, initialRenderRegion), -+ firstVisibleItemKey: -+ this.props.getItemCount(this.props.data) > minIndexForVisible -+ ? VirtualizedList._getItemKey(this.props, minIndexForVisible) -+ : null, -+ // When we have a non-zero initialScrollIndex, we will receive a -+ // scroll event later so this will prevent the window from updating -+ // until we get a valid offset. -+ pendingScrollUpdateCount: -+ this.props.initialScrollIndex != null && -+ this.props.initialScrollIndex > 0 -+ ? 1 -+ : 0, - }; - - // REACT-NATIVE-WEB patch to preserve during future RN merges: Support inverted wheel scroller. -@@ -534,6 +553,40 @@ class VirtualizedList extends StateSafePureComponent { - } - } - -+ static _findItemIndexWithKey( -+ props: Props, -+ key: string, -+ hint: ?number, -+ ): ?number { -+ const itemCount = props.getItemCount(props.data); -+ if (hint != null && hint >= 0 && hint < itemCount) { -+ const curKey = VirtualizedList._getItemKey(props, hint); -+ if (curKey === key) { -+ return hint; -+ } ++ pushOrUnshift(input: Array, item: Item) { ++ if (this.props.inverted) { ++ input.unshift(item) ++ } else { ++ input.push(item) + } -+ for (let ii = 0; ii < itemCount; ii++) { -+ const curKey = VirtualizedList._getItemKey(props, ii); -+ if (curKey === key) { -+ return ii; -+ } -+ } -+ return null; -+ } -+ -+ static _getItemKey( -+ props: { -+ data: Props['data'], -+ getItem: Props['getItem'], -+ keyExtractor: Props['keyExtractor'], -+ ... -+ }, -+ index: number, -+ ): string { -+ const item = props.getItem(props.data, index); -+ return VirtualizedList._keyExtractor(item, index, props); + } + - static _createRenderMask( - props: Props, - cellsAroundViewport: {first: number, last: number}, -@@ -617,6 +670,7 @@ class VirtualizedList extends StateSafePureComponent { - _adjustCellsAroundViewport( - props: Props, - cellsAroundViewport: {first: number, last: number}, -+ pendingScrollUpdateCount: number, - ): {first: number, last: number} { - const {data, getItemCount} = props; - const onEndReachedThreshold = onEndReachedThresholdOrDefault( -@@ -648,21 +702,9 @@ class VirtualizedList extends StateSafePureComponent { - ), - }; + // scrollToEnd may be janky without getItemLayout prop + scrollToEnd(params?: ?{animated?: ?boolean, ...}) { + const animated = params ? params.animated : true; +@@ -438,7 +446,7 @@ class VirtualizedList extends StateSafePureComponent { } else { -- // If we have a non-zero initialScrollIndex and run this before we've scrolled, -- // we'll wipe out the initialNumToRender rendered elements starting at initialScrollIndex. -- // So let's wait until we've scrolled the view to the right place. And until then, -- // we will trust the initialScrollIndex suggestion. -- -- // Thus, we want to recalculate the windowed render limits if any of the following hold: -- // - initialScrollIndex is undefined or is 0 -- // - initialScrollIndex > 0 AND scrolling is complete -- // - initialScrollIndex > 0 AND the end of the list is visible (this handles the case -- // where the list is shorter than the visible area) -- if ( -- props.initialScrollIndex && -- !this._scrollMetrics.offset && -- Math.abs(distanceFromEnd) >= Number.EPSILON -- ) { -+ // If we have a pending scroll update, we should not adjust the render window as it -+ // might override the correct window. -+ if (pendingScrollUpdateCount > 0) { - return cellsAroundViewport.last >= getItemCount(data) - ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) - : cellsAroundViewport; -@@ -771,14 +813,59 @@ class VirtualizedList extends StateSafePureComponent { - return prevState; - } - -+ let maintainVisibleContentPositionAdjustment: ?number = null; -+ const prevFirstVisibleItemKey = prevState.firstVisibleItemKey; -+ const minIndexForVisible = -+ newProps.maintainVisibleContentPosition?.minIndexForVisible ?? 0; -+ const newFirstVisibleItemKey = -+ newProps.getItemCount(newProps.data) > minIndexForVisible -+ ? VirtualizedList._getItemKey(newProps, minIndexForVisible) -+ : null; -+ if ( -+ newProps.maintainVisibleContentPosition != null && -+ prevFirstVisibleItemKey != null && -+ newFirstVisibleItemKey != null -+ ) { -+ if (newFirstVisibleItemKey !== prevFirstVisibleItemKey) { -+ // Fast path if items were added at the start of the list. -+ const hint = -+ itemCount - prevState.renderMask.numCells() + minIndexForVisible; -+ const firstVisibleItemIndex = VirtualizedList._findItemIndexWithKey( -+ newProps, -+ prevFirstVisibleItemKey, -+ hint, -+ ); -+ maintainVisibleContentPositionAdjustment = -+ firstVisibleItemIndex != null -+ ? firstVisibleItemIndex - minIndexForVisible -+ : null; -+ } else { -+ maintainVisibleContentPositionAdjustment = null; -+ } -+ } -+ - const constrainedCells = VirtualizedList._constrainToItemCount( -- prevState.cellsAroundViewport, -+ maintainVisibleContentPositionAdjustment != null -+ ? { -+ first: -+ prevState.cellsAroundViewport.first + -+ maintainVisibleContentPositionAdjustment, -+ last: -+ prevState.cellsAroundViewport.last + -+ maintainVisibleContentPositionAdjustment, -+ } -+ : prevState.cellsAroundViewport, - newProps, - ); - - return { - cellsAroundViewport: constrainedCells, - renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells), -+ firstVisibleItemKey: newFirstVisibleItemKey, -+ pendingScrollUpdateCount: -+ maintainVisibleContentPositionAdjustment != null -+ ? prevState.pendingScrollUpdateCount + 1 -+ : prevState.pendingScrollUpdateCount, - }; - } - -@@ -810,7 +897,7 @@ class VirtualizedList extends StateSafePureComponent { - - for (let ii = first; ii <= last; ii++) { - const item = getItem(data, ii); -- const key = this._keyExtractor(item, ii, this.props); -+ const key = VirtualizedList._keyExtractor(item, ii, this.props); + const {onViewableItemsChanged, viewabilityConfig} = this.props; + if (onViewableItemsChanged) { +- this._viewabilityTuples.push({ ++ this.pushOrUnshift(this._viewabilityTuples, { + viewabilityHelper: new ViewabilityHelper(viewabilityConfig), + onViewableItemsChanged: onViewableItemsChanged, + }); +@@ -814,13 +822,13 @@ class VirtualizedList extends StateSafePureComponent { this._indicesToKeys.set(ii, key); if (stickyIndicesFromProps.has(ii + stickyOffset)) { -@@ -853,15 +940,19 @@ class VirtualizedList extends StateSafePureComponent { - props: Props, - ): {first: number, last: number} { - const itemCount = props.getItemCount(props.data); -- const last = Math.min(itemCount - 1, cells.last); -+ const lastPossibleCellIndex = itemCount - 1; - -+ // Constraining `last` may significantly shrink the window. Adjust `first` -+ // to expand the window if the new `last` results in a new window smaller -+ // than the number of cells rendered per batch. - const maxToRenderPerBatch = maxToRenderPerBatchOrDefault( - props.maxToRenderPerBatch, - ); -+ const maxFirst = Math.max(0, lastPossibleCellIndex - maxToRenderPerBatch); - - return { -- first: clamp(0, itemCount - 1 - maxToRenderPerBatch, cells.first), -- last, -+ first: clamp(0, cells.first, maxFirst), -+ last: Math.min(lastPossibleCellIndex, cells.last), - }; - } +- stickyHeaderIndices.push(cells.length); ++ this.pushOrUnshift(stickyHeaderIndices, (cells.length)); + } -@@ -883,15 +974,14 @@ class VirtualizedList extends StateSafePureComponent { - _getSpacerKey = (isVertical: boolean): string => - isVertical ? 'height' : 'width'; + const shouldListenForLayout = + getItemLayout == null || debug || this._fillRateHelper.enabled(); -- _keyExtractor( -+ static _keyExtractor( - item: Item, - index: number, - props: { - keyExtractor?: ?(item: Item, index: number) => string, - ... - }, -- // $FlowFixMe[missing-local-annot] -- ) { -+ ): string { - if (props.keyExtractor != null) { - return props.keyExtractor(item, index); - } -@@ -937,6 +1027,10 @@ class VirtualizedList extends StateSafePureComponent { +- cells.push( ++ this.pushOrUnshift(cells, + { + // 1. Add cell for ListHeaderComponent + if (ListHeaderComponent) { + if (stickyIndicesFromProps.has(0)) { +- stickyHeaderIndices.push(0); ++ this.pushOrUnshift(stickyHeaderIndices, 0); + } + const element = React.isValidElement(ListHeaderComponent) ? ( + ListHeaderComponent +@@ -932,7 +940,7 @@ class VirtualizedList extends StateSafePureComponent { + // $FlowFixMe[incompatible-type-arg] + + ); +- cells.push( ++ this.pushOrUnshift(cells, + - { - style: inversionStyle - ? [inversionStyle, this.props.style] - : this.props.style, -+ maintainVisibleContentPosition: -+ this.props.maintainVisibleContentPosition != null -+ ? { -+ ...this.props.maintainVisibleContentPosition, -+ // Adjust index to account for ListHeaderComponent. -+ minIndexForVisible: -+ this.props.maintainVisibleContentPosition.minIndexForVisible + -+ (this.props.ListHeaderComponent ? 1 : 0), -+ } -+ : undefined, - }; - - this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1; -@@ -1516,8 +1620,12 @@ class VirtualizedList extends StateSafePureComponent { - onStartReachedThreshold, - onEndReached, - onEndReachedThreshold, -- initialScrollIndex, - } = this.props; -+ // If we have any pending scroll updates it means that the scroll metrics -+ // are out of date and we should not call any of the edge reached callbacks. -+ if (this.state.pendingScrollUpdateCount > 0) { -+ return; -+ } - const {contentLength, visibleLength, offset} = this._scrollMetrics; - let distanceFromStart = offset; - let distanceFromEnd = contentLength - visibleLength - offset; -@@ -1569,14 +1677,8 @@ class VirtualizedList extends StateSafePureComponent { - isWithinStartThreshold && - this._scrollMetrics.contentLength !== this._sentStartForContentLength - ) { -- // On initial mount when using initialScrollIndex the offset will be 0 initially -- // and will trigger an unexpected onStartReached. To avoid this we can use -- // timestamp to differentiate between the initial scroll metrics and when we actually -- // received the first scroll event. -- if (!initialScrollIndex || this._scrollMetrics.timestamp !== 0) { -- this._sentStartForContentLength = this._scrollMetrics.contentLength; -- onStartReached({distanceFromStart}); -- } -+ this._sentStartForContentLength = this._scrollMetrics.contentLength; -+ onStartReached({distanceFromStart}); - } - - // If the user scrolls away from the start or end and back again, -@@ -1703,6 +1805,11 @@ class VirtualizedList extends StateSafePureComponent { - visibleLength, - zoomScale, - }; -+ if (this.state.pendingScrollUpdateCount > 0) { -+ this.setState(state => ({ -+ pendingScrollUpdateCount: state.pendingScrollUpdateCount - 1, -+ })); -+ } - this._updateViewableItems(this.props, this.state.cellsAroundViewport); - if (!this.props) { - return; -@@ -1818,6 +1925,7 @@ class VirtualizedList extends StateSafePureComponent { - const cellsAroundViewport = this._adjustCellsAroundViewport( - props, - state.cellsAroundViewport, -+ state.pendingScrollUpdateCount, +@@ -963,7 +971,7 @@ class VirtualizedList extends StateSafePureComponent { + // $FlowFixMe[incompatible-type-arg] + + )): any); +- cells.push( ++ this.pushOrUnshift(cells, + +@@ -1017,7 +1025,7 @@ class VirtualizedList extends StateSafePureComponent { + const lastMetrics = this.__getFrameMetricsApprox(last, this.props); + const spacerSize = + lastMetrics.offset + lastMetrics.length - firstMetrics.offset; +- cells.push( ++ this.pushOrUnshift(cells, + { + // $FlowFixMe[incompatible-type-arg] + ); - const renderMask = VirtualizedList._createRenderMask( - props, -@@ -1848,7 +1956,7 @@ class VirtualizedList extends StateSafePureComponent { - return { - index, - item, -- key: this._keyExtractor(item, index, props), -+ key: VirtualizedList._keyExtractor(item, index, props), - isViewable, - }; +- cells.push( ++ this.pushOrUnshift(cells, + +@@ -1246,6 +1254,12 @@ class VirtualizedList extends StateSafePureComponent { + * LTI update could not be added via codemod */ + _defaultRenderScrollComponent = props => { + const onRefresh = props.onRefresh; ++ const inversionStyle = this.props.inverted ++ ? this.props.horizontal ++ ? styles.rowReverse ++ : styles.columnReverse ++ : null; ++ + if (this._isNestedWithSameOrientation()) { + // $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors + return ; +@@ -1273,12 +1287,24 @@ class VirtualizedList extends StateSafePureComponent { + props.refreshControl + ) + } ++ contentContainerStyle={[ ++ inversionStyle, ++ this.props.contentContainerStyle, ++ ]} + /> + ); + } else { + // $FlowFixMe[prop-missing] Invalid prop usage + // $FlowFixMe[incompatible-use] +- return ; ++ return ( ++ ++ ); + } }; -@@ -1909,13 +2017,12 @@ class VirtualizedList extends StateSafePureComponent { - inLayout?: boolean, - ... - } => { -- const {data, getItem, getItemCount, getItemLayout} = props; -+ const {data, getItemCount, getItemLayout} = props; - invariant( - index >= 0 && index < getItemCount(data), - 'Tried to get frame for out of range index ' + index, - ); -- const item = getItem(data, index); -- const frame = this._frames[this._keyExtractor(item, index, props)]; -+ const frame = this._frames[VirtualizedList._getItemKey(props, index)]; - if (!frame || frame.index !== index) { - if (getItemLayout) { - /* $FlowFixMe[prop-missing] (>=0.63.0 site=react_native_fb) This comment -@@ -1950,11 +2057,8 @@ class VirtualizedList extends StateSafePureComponent { - // where it is. - if ( - focusedCellIndex >= itemCount || -- this._keyExtractor( -- props.getItem(props.data, focusedCellIndex), -- focusedCellIndex, -- props, -- ) !== this._lastFocusedCellKey -+ VirtualizedList._getItemKey(props, focusedCellIndex) !== -+ this._lastFocusedCellKey - ) { - return []; + +@@ -1432,7 +1458,7 @@ class VirtualizedList extends StateSafePureComponent { + * suppresses an error found when Flow v0.68 was deployed. To see the + * error delete this comment and run Flow. */ + if (frame.inLayout) { +- framesInLayout.push(frame); ++ this.pushOrUnshift(framesInLayout, frame); + } } -@@ -1995,6 +2099,11 @@ class VirtualizedList extends StateSafePureComponent { - props: FrameMetricProps, - cellsAroundViewport: {first: number, last: number}, - ) { -+ // If we have any pending scroll updates it means that the scroll metrics -+ // are out of date and we should not call any of the visibility callbacks. -+ if (this.state.pendingScrollUpdateCount > 0) { -+ return; -+ } - this._viewabilityTuples.forEach(tuple => { - tuple.viewabilityHelper.onUpdate( - props, + const windowTop = this.__getFrameMetricsApprox( +@@ -2044,6 +2070,12 @@ const styles = StyleSheet.create({ + borderColor: 'red', + borderWidth: 2, + }, ++ rowReverse: { ++ flexDirection: 'row-reverse', ++ }, ++ columnReverse: { ++ flexDirection: 'column-reverse', ++ }, + }); + + export default VirtualizedList; +\ No newline at end of file diff --git a/patches/react-native-web+0.19.9+002+fix-mvcp.patch b/patches/react-native-web+0.19.9+002+fix-mvcp.patch new file mode 100644 index 000000000000..afd681bba3b0 --- /dev/null +++ b/patches/react-native-web+0.19.9+002+fix-mvcp.patch @@ -0,0 +1,687 @@ +diff --git a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js +index a6fe142..faeb323 100644 +--- a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js ++++ b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js +@@ -293,7 +293,7 @@ class VirtualizedList extends StateSafePureComponent { + // $FlowFixMe[missing-local-annot] + + constructor(_props) { +- var _this$props$updateCel; ++ var _this$props$updateCel, _this$props$maintainV, _this$props$maintainV2; + super(_props); + this._getScrollMetrics = () => { + return this._scrollMetrics; +@@ -532,6 +532,11 @@ class VirtualizedList extends StateSafePureComponent { + visibleLength, + zoomScale + }; ++ if (this.state.pendingScrollUpdateCount > 0) { ++ this.setState(state => ({ ++ pendingScrollUpdateCount: state.pendingScrollUpdateCount - 1 ++ })); ++ } + this._updateViewableItems(this.props, this.state.cellsAroundViewport); + if (!this.props) { + return; +@@ -581,7 +586,7 @@ class VirtualizedList extends StateSafePureComponent { + this._updateCellsToRender = () => { + this._updateViewableItems(this.props, this.state.cellsAroundViewport); + this.setState((state, props) => { +- var cellsAroundViewport = this._adjustCellsAroundViewport(props, state.cellsAroundViewport); ++ var cellsAroundViewport = this._adjustCellsAroundViewport(props, state.cellsAroundViewport, state.pendingScrollUpdateCount); + var renderMask = VirtualizedList._createRenderMask(props, cellsAroundViewport, this._getNonViewportRenderRegions(props)); + if (cellsAroundViewport.first === state.cellsAroundViewport.first && cellsAroundViewport.last === state.cellsAroundViewport.last && renderMask.equals(state.renderMask)) { + return null; +@@ -601,7 +606,7 @@ class VirtualizedList extends StateSafePureComponent { + return { + index, + item, +- key: this._keyExtractor(item, index, props), ++ key: VirtualizedList._keyExtractor(item, index, props), + isViewable + }; + }; +@@ -633,12 +638,10 @@ class VirtualizedList extends StateSafePureComponent { + }; + this._getFrameMetrics = (index, props) => { + var data = props.data, +- getItem = props.getItem, + getItemCount = props.getItemCount, + getItemLayout = props.getItemLayout; + invariant(index >= 0 && index < getItemCount(data), 'Tried to get frame for out of range index ' + index); +- var item = getItem(data, index); +- var frame = this._frames[this._keyExtractor(item, index, props)]; ++ var frame = this._frames[VirtualizedList._getItemKey(props, index)]; + if (!frame || frame.index !== index) { + if (getItemLayout) { + /* $FlowFixMe[prop-missing] (>=0.63.0 site=react_native_fb) This comment +@@ -662,7 +665,7 @@ class VirtualizedList extends StateSafePureComponent { + + // The last cell we rendered may be at a new index. Bail if we don't know + // where it is. +- if (focusedCellIndex >= itemCount || this._keyExtractor(props.getItem(props.data, focusedCellIndex), focusedCellIndex, props) !== this._lastFocusedCellKey) { ++ if (focusedCellIndex >= itemCount || VirtualizedList._getItemKey(props, focusedCellIndex) !== this._lastFocusedCellKey) { + return []; + } + var first = focusedCellIndex; +@@ -702,9 +705,15 @@ class VirtualizedList extends StateSafePureComponent { + } + } + var initialRenderRegion = VirtualizedList._initialRenderRegion(_props); ++ var minIndexForVisible = (_this$props$maintainV = (_this$props$maintainV2 = this.props.maintainVisibleContentPosition) == null ? void 0 : _this$props$maintainV2.minIndexForVisible) !== null && _this$props$maintainV !== void 0 ? _this$props$maintainV : 0; + this.state = { + cellsAroundViewport: initialRenderRegion, +- renderMask: VirtualizedList._createRenderMask(_props, initialRenderRegion) ++ renderMask: VirtualizedList._createRenderMask(_props, initialRenderRegion), ++ firstVisibleItemKey: this.props.getItemCount(this.props.data) > minIndexForVisible ? VirtualizedList._getItemKey(this.props, minIndexForVisible) : null, ++ // When we have a non-zero initialScrollIndex, we will receive a ++ // scroll event later so this will prevent the window from updating ++ // until we get a valid offset. ++ pendingScrollUpdateCount: this.props.initialScrollIndex != null && this.props.initialScrollIndex > 0 ? 1 : 0 + }; + + // REACT-NATIVE-WEB patch to preserve during future RN merges: Support inverted wheel scroller. +@@ -715,7 +724,7 @@ class VirtualizedList extends StateSafePureComponent { + var clientLength = this.props.horizontal ? ev.target.clientWidth : ev.target.clientHeight; + var isEventTargetScrollable = scrollLength > clientLength; + var delta = this.props.horizontal ? ev.deltaX || ev.wheelDeltaX : ev.deltaY || ev.wheelDeltaY; +- var leftoverDelta = delta; ++ var leftoverDelta = delta * 0.5; + if (isEventTargetScrollable) { + leftoverDelta = delta < 0 ? Math.min(delta + scrollOffset, 0) : Math.max(delta - (scrollLength - clientLength - scrollOffset), 0); + } +@@ -760,6 +769,26 @@ class VirtualizedList extends StateSafePureComponent { + } + } + } ++ static _findItemIndexWithKey(props, key, hint) { ++ var itemCount = props.getItemCount(props.data); ++ if (hint != null && hint >= 0 && hint < itemCount) { ++ var curKey = VirtualizedList._getItemKey(props, hint); ++ if (curKey === key) { ++ return hint; ++ } ++ } ++ for (var ii = 0; ii < itemCount; ii++) { ++ var _curKey = VirtualizedList._getItemKey(props, ii); ++ if (_curKey === key) { ++ return ii; ++ } ++ } ++ return null; ++ } ++ static _getItemKey(props, index) { ++ var item = props.getItem(props.data, index); ++ return VirtualizedList._keyExtractor(item, index, props); ++ } + static _createRenderMask(props, cellsAroundViewport, additionalRegions) { + var itemCount = props.getItemCount(props.data); + invariant(cellsAroundViewport.first >= 0 && cellsAroundViewport.last >= cellsAroundViewport.first - 1 && cellsAroundViewport.last < itemCount, "Invalid cells around viewport \"[" + cellsAroundViewport.first + ", " + cellsAroundViewport.last + "]\" was passed to VirtualizedList._createRenderMask"); +@@ -808,7 +837,7 @@ class VirtualizedList extends StateSafePureComponent { + } + } + } +- _adjustCellsAroundViewport(props, cellsAroundViewport) { ++ _adjustCellsAroundViewport(props, cellsAroundViewport, pendingScrollUpdateCount) { + var data = props.data, + getItemCount = props.getItemCount; + var onEndReachedThreshold = onEndReachedThresholdOrDefault(props.onEndReachedThreshold); +@@ -831,17 +860,9 @@ class VirtualizedList extends StateSafePureComponent { + last: Math.min(cellsAroundViewport.last + renderAhead, getItemCount(data) - 1) + }; + } else { +- // If we have a non-zero initialScrollIndex and run this before we've scrolled, +- // we'll wipe out the initialNumToRender rendered elements starting at initialScrollIndex. +- // So let's wait until we've scrolled the view to the right place. And until then, +- // we will trust the initialScrollIndex suggestion. +- +- // Thus, we want to recalculate the windowed render limits if any of the following hold: +- // - initialScrollIndex is undefined or is 0 +- // - initialScrollIndex > 0 AND scrolling is complete +- // - initialScrollIndex > 0 AND the end of the list is visible (this handles the case +- // where the list is shorter than the visible area) +- if (props.initialScrollIndex && !this._scrollMetrics.offset && Math.abs(distanceFromEnd) >= Number.EPSILON) { ++ // If we have a pending scroll update, we should not adjust the render window as it ++ // might override the correct window. ++ if (pendingScrollUpdateCount > 0) { + return cellsAroundViewport.last >= getItemCount(data) ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) : cellsAroundViewport; + } + newCellsAroundViewport = computeWindowedRenderLimits(props, maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch), windowSizeOrDefault(props.windowSize), cellsAroundViewport, this.__getFrameMetricsApprox, this._scrollMetrics); +@@ -914,16 +935,36 @@ class VirtualizedList extends StateSafePureComponent { + } + } + static getDerivedStateFromProps(newProps, prevState) { ++ var _newProps$maintainVis, _newProps$maintainVis2; + // first and last could be stale (e.g. if a new, shorter items props is passed in), so we make + // sure we're rendering a reasonable range here. + var itemCount = newProps.getItemCount(newProps.data); + if (itemCount === prevState.renderMask.numCells()) { + return prevState; + } +- var constrainedCells = VirtualizedList._constrainToItemCount(prevState.cellsAroundViewport, newProps); ++ var maintainVisibleContentPositionAdjustment = null; ++ var prevFirstVisibleItemKey = prevState.firstVisibleItemKey; ++ var minIndexForVisible = (_newProps$maintainVis = (_newProps$maintainVis2 = newProps.maintainVisibleContentPosition) == null ? void 0 : _newProps$maintainVis2.minIndexForVisible) !== null && _newProps$maintainVis !== void 0 ? _newProps$maintainVis : 0; ++ var newFirstVisibleItemKey = newProps.getItemCount(newProps.data) > minIndexForVisible ? VirtualizedList._getItemKey(newProps, minIndexForVisible) : null; ++ if (newProps.maintainVisibleContentPosition != null && prevFirstVisibleItemKey != null && newFirstVisibleItemKey != null) { ++ if (newFirstVisibleItemKey !== prevFirstVisibleItemKey) { ++ // Fast path if items were added at the start of the list. ++ var hint = itemCount - prevState.renderMask.numCells() + minIndexForVisible; ++ var firstVisibleItemIndex = VirtualizedList._findItemIndexWithKey(newProps, prevFirstVisibleItemKey, hint); ++ maintainVisibleContentPositionAdjustment = firstVisibleItemIndex != null ? firstVisibleItemIndex - minIndexForVisible : null; ++ } else { ++ maintainVisibleContentPositionAdjustment = null; ++ } ++ } ++ var constrainedCells = VirtualizedList._constrainToItemCount(maintainVisibleContentPositionAdjustment != null ? { ++ first: prevState.cellsAroundViewport.first + maintainVisibleContentPositionAdjustment, ++ last: prevState.cellsAroundViewport.last + maintainVisibleContentPositionAdjustment ++ } : prevState.cellsAroundViewport, newProps); + return { + cellsAroundViewport: constrainedCells, +- renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells) ++ renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells), ++ firstVisibleItemKey: newFirstVisibleItemKey, ++ pendingScrollUpdateCount: maintainVisibleContentPositionAdjustment != null ? prevState.pendingScrollUpdateCount + 1 : prevState.pendingScrollUpdateCount + }; + } + _pushCells(cells, stickyHeaderIndices, stickyIndicesFromProps, first, last, inversionStyle) { +@@ -946,7 +987,7 @@ class VirtualizedList extends StateSafePureComponent { + last = Math.min(end, last); + var _loop = function _loop() { + var item = getItem(data, ii); +- var key = _this._keyExtractor(item, ii, _this.props); ++ var key = VirtualizedList._keyExtractor(item, ii, _this.props); + _this._indicesToKeys.set(ii, key); + if (stickyIndicesFromProps.has(ii + stickyOffset)) { + _this.pushOrUnshift(stickyHeaderIndices, cells.length); +@@ -981,20 +1022,23 @@ class VirtualizedList extends StateSafePureComponent { + } + static _constrainToItemCount(cells, props) { + var itemCount = props.getItemCount(props.data); +- var last = Math.min(itemCount - 1, cells.last); ++ var lastPossibleCellIndex = itemCount - 1; ++ ++ // Constraining `last` may significantly shrink the window. Adjust `first` ++ // to expand the window if the new `last` results in a new window smaller ++ // than the number of cells rendered per batch. + var maxToRenderPerBatch = maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch); ++ var maxFirst = Math.max(0, lastPossibleCellIndex - maxToRenderPerBatch); + return { +- first: clamp(0, itemCount - 1 - maxToRenderPerBatch, cells.first), +- last ++ first: clamp(0, cells.first, maxFirst), ++ last: Math.min(lastPossibleCellIndex, cells.last) + }; + } + _isNestedWithSameOrientation() { + var nestedContext = this.context; + return !!(nestedContext && !!nestedContext.horizontal === horizontalOrDefault(this.props.horizontal)); + } +- _keyExtractor(item, index, props +- // $FlowFixMe[missing-local-annot] +- ) { ++ static _keyExtractor(item, index, props) { + if (props.keyExtractor != null) { + return props.keyExtractor(item, index); + } +@@ -1034,7 +1078,12 @@ class VirtualizedList extends StateSafePureComponent { + this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { + cellKey: this._getCellKey() + '-header', + key: "$header" +- }, /*#__PURE__*/React.createElement(View, { ++ }, /*#__PURE__*/React.createElement(View ++ // We expect that header component will be a single native view so make it ++ // not collapsable to avoid this view being flattened and make this assumption ++ // no longer true. ++ , { ++ collapsable: false, + onLayout: this._onLayoutHeader, + style: [inversionStyle, this.props.ListHeaderComponentStyle] + }, +@@ -1136,7 +1185,11 @@ class VirtualizedList extends StateSafePureComponent { + // TODO: Android support + invertStickyHeaders: this.props.invertStickyHeaders !== undefined ? this.props.invertStickyHeaders : this.props.inverted, + stickyHeaderIndices, +- style: inversionStyle ? [inversionStyle, this.props.style] : this.props.style ++ style: inversionStyle ? [inversionStyle, this.props.style] : this.props.style, ++ maintainVisibleContentPosition: this.props.maintainVisibleContentPosition != null ? _objectSpread(_objectSpread({}, this.props.maintainVisibleContentPosition), {}, { ++ // Adjust index to account for ListHeaderComponent. ++ minIndexForVisible: this.props.maintainVisibleContentPosition.minIndexForVisible + (this.props.ListHeaderComponent ? 1 : 0) ++ }) : undefined + }); + this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1; + var innerRet = /*#__PURE__*/React.createElement(VirtualizedListContextProvider, { +@@ -1319,8 +1372,12 @@ class VirtualizedList extends StateSafePureComponent { + onStartReached = _this$props8.onStartReached, + onStartReachedThreshold = _this$props8.onStartReachedThreshold, + onEndReached = _this$props8.onEndReached, +- onEndReachedThreshold = _this$props8.onEndReachedThreshold, +- initialScrollIndex = _this$props8.initialScrollIndex; ++ onEndReachedThreshold = _this$props8.onEndReachedThreshold; ++ // If we have any pending scroll updates it means that the scroll metrics ++ // are out of date and we should not call any of the edge reached callbacks. ++ if (this.state.pendingScrollUpdateCount > 0) { ++ return; ++ } + var _this$_scrollMetrics2 = this._scrollMetrics, + contentLength = _this$_scrollMetrics2.contentLength, + visibleLength = _this$_scrollMetrics2.visibleLength, +@@ -1360,16 +1417,10 @@ class VirtualizedList extends StateSafePureComponent { + // and call onStartReached only once for a given content length, + // and only if onEndReached is not being executed + else if (onStartReached != null && this.state.cellsAroundViewport.first === 0 && isWithinStartThreshold && this._scrollMetrics.contentLength !== this._sentStartForContentLength) { +- // On initial mount when using initialScrollIndex the offset will be 0 initially +- // and will trigger an unexpected onStartReached. To avoid this we can use +- // timestamp to differentiate between the initial scroll metrics and when we actually +- // received the first scroll event. +- if (!initialScrollIndex || this._scrollMetrics.timestamp !== 0) { +- this._sentStartForContentLength = this._scrollMetrics.contentLength; +- onStartReached({ +- distanceFromStart +- }); +- } ++ this._sentStartForContentLength = this._scrollMetrics.contentLength; ++ onStartReached({ ++ distanceFromStart ++ }); + } + + // If the user scrolls away from the start or end and back again, +@@ -1424,6 +1475,11 @@ class VirtualizedList extends StateSafePureComponent { + } + } + _updateViewableItems(props, cellsAroundViewport) { ++ // If we have any pending scroll updates it means that the scroll metrics ++ // are out of date and we should not call any of the visibility callbacks. ++ if (this.state.pendingScrollUpdateCount > 0) { ++ return; ++ } + this._viewabilityTuples.forEach(tuple => { + tuple.viewabilityHelper.onUpdate(props, this._scrollMetrics.offset, this._scrollMetrics.visibleLength, this._getFrameMetrics, this._createViewToken, tuple.onViewableItemsChanged, cellsAroundViewport); + }); +diff --git a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js +index d896fb1..f303b31 100644 +--- a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js ++++ b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js +@@ -75,6 +75,10 @@ type ViewabilityHelperCallbackTuple = { + type State = { + renderMask: CellRenderMask, + cellsAroundViewport: {first: number, last: number}, ++ // Used to track items added at the start of the list for maintainVisibleContentPosition. ++ firstVisibleItemKey: ?string, ++ // When > 0 the scroll position available in JS is considered stale and should not be used. ++ pendingScrollUpdateCount: number, + }; + + /** +@@ -455,9 +459,24 @@ class VirtualizedList extends StateSafePureComponent { + + const initialRenderRegion = VirtualizedList._initialRenderRegion(props); + ++ const minIndexForVisible = ++ this.props.maintainVisibleContentPosition?.minIndexForVisible ?? 0; ++ + this.state = { + cellsAroundViewport: initialRenderRegion, + renderMask: VirtualizedList._createRenderMask(props, initialRenderRegion), ++ firstVisibleItemKey: ++ this.props.getItemCount(this.props.data) > minIndexForVisible ++ ? VirtualizedList._getItemKey(this.props, minIndexForVisible) ++ : null, ++ // When we have a non-zero initialScrollIndex, we will receive a ++ // scroll event later so this will prevent the window from updating ++ // until we get a valid offset. ++ pendingScrollUpdateCount: ++ this.props.initialScrollIndex != null && ++ this.props.initialScrollIndex > 0 ++ ? 1 ++ : 0, + }; + + // REACT-NATIVE-WEB patch to preserve during future RN merges: Support inverted wheel scroller. +@@ -470,7 +489,7 @@ class VirtualizedList extends StateSafePureComponent { + const delta = this.props.horizontal + ? ev.deltaX || ev.wheelDeltaX + : ev.deltaY || ev.wheelDeltaY; +- let leftoverDelta = delta; ++ let leftoverDelta = delta * 5; + if (isEventTargetScrollable) { + leftoverDelta = delta < 0 + ? Math.min(delta + scrollOffset, 0) +@@ -542,6 +561,40 @@ class VirtualizedList extends StateSafePureComponent { + } + } + ++ static _findItemIndexWithKey( ++ props: Props, ++ key: string, ++ hint: ?number, ++ ): ?number { ++ const itemCount = props.getItemCount(props.data); ++ if (hint != null && hint >= 0 && hint < itemCount) { ++ const curKey = VirtualizedList._getItemKey(props, hint); ++ if (curKey === key) { ++ return hint; ++ } ++ } ++ for (let ii = 0; ii < itemCount; ii++) { ++ const curKey = VirtualizedList._getItemKey(props, ii); ++ if (curKey === key) { ++ return ii; ++ } ++ } ++ return null; ++ } ++ ++ static _getItemKey( ++ props: { ++ data: Props['data'], ++ getItem: Props['getItem'], ++ keyExtractor: Props['keyExtractor'], ++ ... ++ }, ++ index: number, ++ ): string { ++ const item = props.getItem(props.data, index); ++ return VirtualizedList._keyExtractor(item, index, props); ++ } ++ + static _createRenderMask( + props: Props, + cellsAroundViewport: {first: number, last: number}, +@@ -625,6 +678,7 @@ class VirtualizedList extends StateSafePureComponent { + _adjustCellsAroundViewport( + props: Props, + cellsAroundViewport: {first: number, last: number}, ++ pendingScrollUpdateCount: number, + ): {first: number, last: number} { + const {data, getItemCount} = props; + const onEndReachedThreshold = onEndReachedThresholdOrDefault( +@@ -656,21 +710,9 @@ class VirtualizedList extends StateSafePureComponent { + ), + }; + } else { +- // If we have a non-zero initialScrollIndex and run this before we've scrolled, +- // we'll wipe out the initialNumToRender rendered elements starting at initialScrollIndex. +- // So let's wait until we've scrolled the view to the right place. And until then, +- // we will trust the initialScrollIndex suggestion. +- +- // Thus, we want to recalculate the windowed render limits if any of the following hold: +- // - initialScrollIndex is undefined or is 0 +- // - initialScrollIndex > 0 AND scrolling is complete +- // - initialScrollIndex > 0 AND the end of the list is visible (this handles the case +- // where the list is shorter than the visible area) +- if ( +- props.initialScrollIndex && +- !this._scrollMetrics.offset && +- Math.abs(distanceFromEnd) >= Number.EPSILON +- ) { ++ // If we have a pending scroll update, we should not adjust the render window as it ++ // might override the correct window. ++ if (pendingScrollUpdateCount > 0) { + return cellsAroundViewport.last >= getItemCount(data) + ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) + : cellsAroundViewport; +@@ -779,14 +821,59 @@ class VirtualizedList extends StateSafePureComponent { + return prevState; + } + ++ let maintainVisibleContentPositionAdjustment: ?number = null; ++ const prevFirstVisibleItemKey = prevState.firstVisibleItemKey; ++ const minIndexForVisible = ++ newProps.maintainVisibleContentPosition?.minIndexForVisible ?? 0; ++ const newFirstVisibleItemKey = ++ newProps.getItemCount(newProps.data) > minIndexForVisible ++ ? VirtualizedList._getItemKey(newProps, minIndexForVisible) ++ : null; ++ if ( ++ newProps.maintainVisibleContentPosition != null && ++ prevFirstVisibleItemKey != null && ++ newFirstVisibleItemKey != null ++ ) { ++ if (newFirstVisibleItemKey !== prevFirstVisibleItemKey) { ++ // Fast path if items were added at the start of the list. ++ const hint = ++ itemCount - prevState.renderMask.numCells() + minIndexForVisible; ++ const firstVisibleItemIndex = VirtualizedList._findItemIndexWithKey( ++ newProps, ++ prevFirstVisibleItemKey, ++ hint, ++ ); ++ maintainVisibleContentPositionAdjustment = ++ firstVisibleItemIndex != null ++ ? firstVisibleItemIndex - minIndexForVisible ++ : null; ++ } else { ++ maintainVisibleContentPositionAdjustment = null; ++ } ++ } ++ + const constrainedCells = VirtualizedList._constrainToItemCount( +- prevState.cellsAroundViewport, ++ maintainVisibleContentPositionAdjustment != null ++ ? { ++ first: ++ prevState.cellsAroundViewport.first + ++ maintainVisibleContentPositionAdjustment, ++ last: ++ prevState.cellsAroundViewport.last + ++ maintainVisibleContentPositionAdjustment, ++ } ++ : prevState.cellsAroundViewport, + newProps, + ); + + return { + cellsAroundViewport: constrainedCells, + renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells), ++ firstVisibleItemKey: newFirstVisibleItemKey, ++ pendingScrollUpdateCount: ++ maintainVisibleContentPositionAdjustment != null ++ ? prevState.pendingScrollUpdateCount + 1 ++ : prevState.pendingScrollUpdateCount, + }; + } + +@@ -818,11 +905,11 @@ class VirtualizedList extends StateSafePureComponent { + + for (let ii = first; ii <= last; ii++) { + const item = getItem(data, ii); +- const key = this._keyExtractor(item, ii, this.props); ++ const key = VirtualizedList._keyExtractor(item, ii, this.props); + + this._indicesToKeys.set(ii, key); + if (stickyIndicesFromProps.has(ii + stickyOffset)) { +- this.pushOrUnshift(stickyHeaderIndices, (cells.length)); ++ this.pushOrUnshift(stickyHeaderIndices, cells.length); + } + + const shouldListenForLayout = +@@ -861,15 +948,19 @@ class VirtualizedList extends StateSafePureComponent { + props: Props, + ): {first: number, last: number} { + const itemCount = props.getItemCount(props.data); +- const last = Math.min(itemCount - 1, cells.last); ++ const lastPossibleCellIndex = itemCount - 1; + ++ // Constraining `last` may significantly shrink the window. Adjust `first` ++ // to expand the window if the new `last` results in a new window smaller ++ // than the number of cells rendered per batch. + const maxToRenderPerBatch = maxToRenderPerBatchOrDefault( + props.maxToRenderPerBatch, + ); ++ const maxFirst = Math.max(0, lastPossibleCellIndex - maxToRenderPerBatch); + + return { +- first: clamp(0, itemCount - 1 - maxToRenderPerBatch, cells.first), +- last, ++ first: clamp(0, cells.first, maxFirst), ++ last: Math.min(lastPossibleCellIndex, cells.last), + }; + } + +@@ -891,15 +982,14 @@ class VirtualizedList extends StateSafePureComponent { + _getSpacerKey = (isVertical: boolean): string => + isVertical ? 'height' : 'width'; + +- _keyExtractor( ++ static _keyExtractor( + item: Item, + index: number, + props: { + keyExtractor?: ?(item: Item, index: number) => string, + ... + }, +- // $FlowFixMe[missing-local-annot] +- ) { ++ ): string { + if (props.keyExtractor != null) { + return props.keyExtractor(item, index); + } +@@ -945,6 +1035,10 @@ class VirtualizedList extends StateSafePureComponent { + cellKey={this._getCellKey() + '-header'} + key="$header"> + { + style: inversionStyle + ? [inversionStyle, this.props.style] + : this.props.style, ++ maintainVisibleContentPosition: ++ this.props.maintainVisibleContentPosition != null ++ ? { ++ ...this.props.maintainVisibleContentPosition, ++ // Adjust index to account for ListHeaderComponent. ++ minIndexForVisible: ++ this.props.maintainVisibleContentPosition.minIndexForVisible + ++ (this.props.ListHeaderComponent ? 1 : 0), ++ } ++ : undefined, + }; + + this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1; +@@ -1255,11 +1359,10 @@ class VirtualizedList extends StateSafePureComponent { + _defaultRenderScrollComponent = props => { + const onRefresh = props.onRefresh; + const inversionStyle = this.props.inverted +- ? this.props.horizontal +- ? styles.rowReverse +- : styles.columnReverse +- : null; +- ++ ? this.props.horizontal ++ ? styles.rowReverse ++ : styles.columnReverse ++ : null; + if (this._isNestedWithSameOrientation()) { + // $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors + return ; +@@ -1542,8 +1645,12 @@ class VirtualizedList extends StateSafePureComponent { + onStartReachedThreshold, + onEndReached, + onEndReachedThreshold, +- initialScrollIndex, + } = this.props; ++ // If we have any pending scroll updates it means that the scroll metrics ++ // are out of date and we should not call any of the edge reached callbacks. ++ if (this.state.pendingScrollUpdateCount > 0) { ++ return; ++ } + const {contentLength, visibleLength, offset} = this._scrollMetrics; + let distanceFromStart = offset; + let distanceFromEnd = contentLength - visibleLength - offset; +@@ -1595,14 +1702,8 @@ class VirtualizedList extends StateSafePureComponent { + isWithinStartThreshold && + this._scrollMetrics.contentLength !== this._sentStartForContentLength + ) { +- // On initial mount when using initialScrollIndex the offset will be 0 initially +- // and will trigger an unexpected onStartReached. To avoid this we can use +- // timestamp to differentiate between the initial scroll metrics and when we actually +- // received the first scroll event. +- if (!initialScrollIndex || this._scrollMetrics.timestamp !== 0) { +- this._sentStartForContentLength = this._scrollMetrics.contentLength; +- onStartReached({distanceFromStart}); +- } ++ this._sentStartForContentLength = this._scrollMetrics.contentLength; ++ onStartReached({distanceFromStart}); + } + + // If the user scrolls away from the start or end and back again, +@@ -1729,6 +1830,11 @@ class VirtualizedList extends StateSafePureComponent { + visibleLength, + zoomScale, + }; ++ if (this.state.pendingScrollUpdateCount > 0) { ++ this.setState(state => ({ ++ pendingScrollUpdateCount: state.pendingScrollUpdateCount - 1, ++ })); ++ } + this._updateViewableItems(this.props, this.state.cellsAroundViewport); + if (!this.props) { + return; +@@ -1844,6 +1950,7 @@ class VirtualizedList extends StateSafePureComponent { + const cellsAroundViewport = this._adjustCellsAroundViewport( + props, + state.cellsAroundViewport, ++ state.pendingScrollUpdateCount, + ); + const renderMask = VirtualizedList._createRenderMask( + props, +@@ -1874,7 +1981,7 @@ class VirtualizedList extends StateSafePureComponent { + return { + index, + item, +- key: this._keyExtractor(item, index, props), ++ key: VirtualizedList._keyExtractor(item, index, props), + isViewable, + }; + }; +@@ -1935,13 +2042,12 @@ class VirtualizedList extends StateSafePureComponent { + inLayout?: boolean, + ... + } => { +- const {data, getItem, getItemCount, getItemLayout} = props; ++ const {data, getItemCount, getItemLayout} = props; + invariant( + index >= 0 && index < getItemCount(data), + 'Tried to get frame for out of range index ' + index, + ); +- const item = getItem(data, index); +- const frame = this._frames[this._keyExtractor(item, index, props)]; ++ const frame = this._frames[VirtualizedList._getItemKey(props, index)]; + if (!frame || frame.index !== index) { + if (getItemLayout) { + /* $FlowFixMe[prop-missing] (>=0.63.0 site=react_native_fb) This comment +@@ -1976,11 +2082,8 @@ class VirtualizedList extends StateSafePureComponent { + // where it is. + if ( + focusedCellIndex >= itemCount || +- this._keyExtractor( +- props.getItem(props.data, focusedCellIndex), +- focusedCellIndex, +- props, +- ) !== this._lastFocusedCellKey ++ VirtualizedList._getItemKey(props, focusedCellIndex) !== ++ this._lastFocusedCellKey + ) { + return []; + } +@@ -2021,6 +2124,11 @@ class VirtualizedList extends StateSafePureComponent { + props: FrameMetricProps, + cellsAroundViewport: {first: number, last: number}, + ) { ++ // If we have any pending scroll updates it means that the scroll metrics ++ // are out of date and we should not call any of the visibility callbacks. ++ if (this.state.pendingScrollUpdateCount > 0) { ++ return; ++ } + this._viewabilityTuples.forEach(tuple => { + tuple.viewabilityHelper.onUpdate( + props, diff --git a/patches/react-native-web+0.19.9+002+measureInWindow.patch b/patches/react-native-web+0.19.9+003+measureInWindow.patch similarity index 100% rename from patches/react-native-web+0.19.9+002+measureInWindow.patch rename to patches/react-native-web+0.19.9+003+measureInWindow.patch diff --git a/patches/react-native-web+0.19.9+003+fix-pointer-events.patch b/patches/react-native-web+0.19.9+004+fix-pointer-events.patch similarity index 100% rename from patches/react-native-web+0.19.9+003+fix-pointer-events.patch rename to patches/react-native-web+0.19.9+004+fix-pointer-events.patch diff --git a/patches/react-native-web+0.19.9+005+fixLastSpacer.patch b/patches/react-native-web+0.19.9+005+fixLastSpacer.patch index fc48c00094dc..0ca5ac778e0b 100644 --- a/patches/react-native-web+0.19.9+005+fixLastSpacer.patch +++ b/patches/react-native-web+0.19.9+005+fixLastSpacer.patch @@ -26,4 +26,4 @@ index faeb323..68d740a 100644 + var lastSpacer = lastRegion?.isSpacer ? lastRegion : null; for (var _iterator = _createForOfIteratorHelperLoose(renderRegions), _step; !(_step = _iterator()).done;) { var section = _step.value; - if (section.isSpacer) { + if (section.isSpacer) { \ No newline at end of file From 8196979763809e16bda6f198275b75737047aed9 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 22 Dec 2023 15:24:33 +0100 Subject: [PATCH 029/245] block fetching newer actions if the screen size is too large --- .../CheckForPreviousReportActionIDClean.ts | 2 +- src/pages/home/report/ReportActionsList.js | 12 ++- src/pages/home/report/ReportActionsView.js | 75 +++++++++++++------ 3 files changed, 65 insertions(+), 24 deletions(-) diff --git a/src/libs/migrations/CheckForPreviousReportActionIDClean.ts b/src/libs/migrations/CheckForPreviousReportActionIDClean.ts index 7ee7a498d1a6..4362ae79114b 100644 --- a/src/libs/migrations/CheckForPreviousReportActionIDClean.ts +++ b/src/libs/migrations/CheckForPreviousReportActionIDClean.ts @@ -2,7 +2,7 @@ import Onyx, {OnyxCollection} from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; import * as OnyxTypes from '@src/types/onyx'; -function getReportActionsFromOnyxClean(): Promise> { +function getReportActionsFromOnyx(): Promise> { return new Promise((resolve) => { const connectionID = Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 3df1ce98855c..24ac03ce1f87 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -12,6 +12,7 @@ import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withW import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import compose from '@libs/compose'; import DateUtils from '@libs/DateUtils'; import getPlatform from '@libs/getPlatform'; @@ -139,6 +140,7 @@ function ReportActionsList({ isComposerFullSize, reportScrollManager, listID, + onContentSizeChange, }) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -440,6 +442,12 @@ function ReportActionsList({ }, [onLayout], ); + const onContentSizeChangeInner = useCallback( + (w, h) => { + onContentSizeChange(w,h) + }, + [onContentSizeChange], + ); const listHeaderComponent = useCallback(() => { if (!isOffline && !hasHeaderRendered.current) { @@ -471,7 +479,8 @@ function ReportActionsList({ renderItem={renderItem} contentContainerStyle={contentContainerStyle} keyExtractor={keyExtractor} - initialNumToRender={initialNumToRender} + // initialNumToRender={initialNumToRender} + initialNumToRender={50} onEndReached={loadOlderChats} onEndReachedThreshold={0.75} onStartReached={loadNewerChats} @@ -480,6 +489,7 @@ function ReportActionsList({ ListHeaderComponent={listHeaderComponent} keyboardShouldPersistTaps="handled" onLayout={onLayoutInner} + onContentSizeChange={onContentSizeChangeInner} onScroll={trackVerticalScrolling} onScrollToIndexFailed={() => {}} extraData={extraData} diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 3a6dd00c0699..6b5b86a1950b 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -16,6 +16,7 @@ import useCopySelectionHelper from '@hooks/useCopySelectionHelper'; import useInitialValue from '@hooks/useInitialValue'; import usePrevious from '@hooks/usePrevious'; import useReportScrollManager from '@hooks/useReportScrollManager'; +// import useWindowDimensions from '@hooks/useWindowDimensions'; import compose from '@libs/compose'; import getIsReportFullyVisible from '@libs/getIsReportFullyVisible'; import Performance from '@libs/Performance'; @@ -100,6 +101,7 @@ function getReportActionID(route) { const useHandleList = (linkedID, messageArray, fetchFn, route, isLoadingLinkedMessage) => { const [edgeID, setEdgeID] = useState(linkedID); const [listID, setListID] = useState(1); + const isFirstRender = useRef(true); const index = useMemo(() => { if (!linkedID) { @@ -110,6 +112,7 @@ const useHandleList = (linkedID, messageArray, fetchFn, route, isLoadingLinkedMe }, [messageArray, linkedID, edgeID]); useMemo(() => { + isFirstRender.current = true; setEdgeID(''); }, [route, linkedID]); @@ -117,9 +120,9 @@ const useHandleList = (linkedID, messageArray, fetchFn, route, isLoadingLinkedMe if (!linkedID || index === -1) { return messageArray; } - - if (linkedID && !edgeID) { + if ((linkedID && !edgeID) || (linkedID && isFirstRender.current)) { setListID((i) => i + 1); + isFirstRender.current = false; return messageArray.slice(index, messageArray.length); } else if (linkedID && edgeID) { const amountOfItemsBeforeLinkedOne = 10; @@ -163,9 +166,13 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro const {reportActionID} = getReportActionID(route); const didLayout = useRef(false); const didSubscribeToReportTypingEvents = useRef(false); - const isLoadingLinkedMessage = !!reportActionID && props.isLoadingInitialReportActions; + const contentListHeight = useRef(0); + const layoutListHeight = useRef(0); + const isInitial = useRef(true); + // const isLoadingLinkedMessage = !!reportActionID && props.isLoadingInitialReportActions; const hasCachedActions = useInitialValue(() => _.size(props.reportActions) > 0); const mostRecentIOUReportActionID = useInitialValue(() => ReportActionsUtils.getMostRecentIOURequestActionID(props.reportActions)); + // const {windowHeight} = useWindowDimensions(); const prevNetworkRef = useRef(props.network); const prevAuthTokenType = usePrevious(props.session.authTokenType); @@ -174,6 +181,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro const isFocused = useIsFocused(); const reportID = props.report.reportID; + /** * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently * displaying. @@ -191,10 +199,12 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro [props.isLoadingNewerReportActions, props.isLoadingInitialReportActions, reportID, newestReportAction], ); - const {cattedArray: reportActions, fetchFunc, linkedIdIndex, listID} = useHandleList(reportActionID, allReportActions, throttledLoadNewerChats, route, isLoadingLinkedMessage); + const {cattedArray: reportActions, fetchFunc, linkedIdIndex, listID} = useHandleList(reportActionID, allReportActions, throttledLoadNewerChats, route); const hasNewestReportAction = lodashGet(reportActions[0], 'isNewestReportAction'); const newestReportAction = lodashGet(reportActions, '[0]'); + const oldestReportAction = _.last(reportActions); + const isWeReachedTheOldestAction = oldestReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED; /** * @returns {Boolean} @@ -211,6 +221,9 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro }; useEffect(() => { + if (reportActionID) { + return; + } openReportIfNecessary(); InteractionManager.runAfterInteractions(() => { @@ -282,6 +295,10 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro } }, [props.report, didSubscribeToReportTypingEvents, reportID]); + const onContentSizeChange = useCallback((w, h) => { + contentListHeight.current = h; + }, []); + /** * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently * displaying. @@ -292,10 +309,8 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro return; } - const oldestReportAction = _.last(reportActions); - // Don't load more chats if we're already at the beginning of the chat history - if (!oldestReportAction || oldestReportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) { + if (!oldestReportAction || isWeReachedTheOldestAction) { return; } // Retrieve the next REPORT.ACTIONS.LIMIT sized page of comments @@ -306,32 +321,47 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro const handleLoadNewerChats = useCallback( // eslint-disable-next-line rulesdir/prefer-early-return ({distanceFromStart}) => { - if ((reportActionID && linkedIdIndex > -1 && !hasNewestReportAction) || (!reportActionID && !hasNewestReportAction)) { + // const shouldFirstlyLoadOlderActions = Number(layoutListHeight.current) > Number(contentListHeight.current); + // const DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST = 164; + // const SPACER = 30; + // const MIN_PREDEFINED_PADDING = 16; + // const isListSmallerThanScreen = windowHeight - DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST - SPACER > contentListHeight.current; + // const isListEmpty = contentListHeight.current === MIN_PREDEFINED_PADDING; + // const shouldFirstlyLoadOlderActions = !isWeReachedTheOldestAction && isListSmallerThanScreen + // const shouldFirstlyLoadOlderActions = !isListSmallerThanScreen + if ((reportActionID && linkedIdIndex > -1 && !hasNewestReportAction && !isInitial.current) || (!reportActionID && !hasNewestReportAction)) { fetchFunc({firstReportActionID, distanceFromStart}); } + isInitial.current = false; }, + // [hasNewestReportAction, linkedIdIndex, firstReportActionID, fetchFunc, reportActionID, windowHeight, isWeReachedTheOldestAction], [hasNewestReportAction, linkedIdIndex, firstReportActionID, fetchFunc, reportActionID], ); /** * Runs when the FlatList finishes laying out */ - const recordTimeToMeasureItemLayout = () => { - if (didLayout.current) { - return; - } + const recordTimeToMeasureItemLayout = useCallback( + (e) => { + layoutListHeight.current = e.nativeEvent.layout.height; - didLayout.current = true; - Timing.end(CONST.TIMING.SWITCH_REPORT, hasCachedActions ? CONST.TIMING.WARM : CONST.TIMING.COLD); + if (didLayout.current) { + return; + } - // Capture the init measurement only once not per each chat switch as the value gets overwritten - if (!ReportActionsView.initMeasured) { - Performance.markEnd(CONST.TIMING.REPORT_INITIAL_RENDER); - ReportActionsView.initMeasured = true; - } else { - Performance.markEnd(CONST.TIMING.SWITCH_REPORT); - } - }; + didLayout.current = true; + Timing.end(CONST.TIMING.SWITCH_REPORT, hasCachedActions ? CONST.TIMING.WARM : CONST.TIMING.COLD); + + // Capture the init measurement only once not per each chat switch as the value gets overwritten + if (!ReportActionsView.initMeasured) { + Performance.markEnd(CONST.TIMING.REPORT_INITIAL_RENDER); + ReportActionsView.initMeasured = true; + } else { + Performance.markEnd(CONST.TIMING.SWITCH_REPORT); + } + }, + [hasCachedActions], + ); // Comments have not loaded at all yet do nothing if (!_.size(reportActions)) { @@ -354,6 +384,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro reportScrollManager={reportScrollManager} policy={props.policy} listID={listID} + onContentSizeChange={onContentSizeChange} /> From 7f94dedc55023093419f1447e37a9c49cdd60887 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Sat, 23 Dec 2023 10:38:34 +0100 Subject: [PATCH 030/245] fix bottom loader for invisible actions --- src/libs/ReportActionsUtils.ts | 26 +++++++++++++------------- src/pages/home/ReportScreen.js | 12 ++++-------- tests/unit/ReportActionsUtilsTest.js | 2 +- 3 files changed, 18 insertions(+), 22 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index fad712e19fbd..f5f2637d4f2d 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -183,7 +183,7 @@ function isTransactionThread(parentReportAction: OnyxEntry): boole * This gives us a stable order even in the case of multiple reportActions created on the same millisecond * */ -function getSortedReportActions(reportActions: ReportAction[] | null, shouldSortInDescendingOrder = false, shouldMarkTheFirstItemAsNewest = false): ReportAction[] { +function getSortedReportActions(reportActions: ReportAction[] | null, shouldSortInDescendingOrder = false): ReportAction[] { if (!Array.isArray(reportActions)) { throw new Error(`ReportActionsUtils.getSortedReportActions requires an array, received ${typeof reportActions}`); } @@ -211,14 +211,6 @@ function getSortedReportActions(reportActions: ReportAction[] | null, shouldSort return (first.reportActionID < second.reportActionID ? -1 : 1) * invertedMultiplier; }); - // If shouldMarkTheFirstItemAsNewest is true, label the first reportAction as isNewestReportAction - if (shouldMarkTheFirstItemAsNewest && sortedActions?.length > 0) { - sortedActions[0] = { - ...sortedActions[0], - isNewestReportAction: true, - }; - } - return sortedActions; } @@ -566,19 +558,27 @@ function filterOutDeprecatedReportActions(reportActions: ReportActions | null): * to ensure they will always be displayed in the same order (in case multiple actions have the same timestamp). * This is all handled with getSortedReportActions() which is used by several other methods to keep the code DRY. */ -function getSortedReportActionsForDisplay(reportActions: ReportActions | null, shouldMarkTheFirstItemAsNewest = false): ReportAction[] { +function getSortedReportActionsForDisplay(reportActions: ReportActions | null): ReportAction[] { const filteredReportActions = Object.entries(reportActions ?? {}) // .filter(([key, reportAction]) => shouldReportActionBeVisible(reportAction, key)) .map((entry) => entry[1]); const baseURLAdjustedReportActions = filteredReportActions.map((reportAction) => replaceBaseURL(reportAction)); - return getSortedReportActions(baseURLAdjustedReportActions, true, shouldMarkTheFirstItemAsNewest); + return getSortedReportActions(baseURLAdjustedReportActions, true); } -function getReportActionsWithoutRemoved(reportActions: ReportAction[] | null): ReportAction[] { +function getReportActionsWithoutRemoved(reportActions: ReportAction[] | null, shouldMarkTheFirstItemAsNewest = false): ReportAction[] { if (!reportActions) { return []; } - return reportActions.filter((item) => shouldReportActionBeVisible(item, item.reportActionID)); + const filtered = reportActions.filter((item) => shouldReportActionBeVisible(item, item.reportActionID)); + + if (shouldMarkTheFirstItemAsNewest && filtered?.length > 0) { + filtered[0] = { + ...filtered[0], + isNewestReportAction: true, + }; + } + return filtered; } /** diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 3b442f975f5a..6c223b89e09e 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -66,9 +66,6 @@ const propTypes = { /** The report metadata loading states */ reportMetadata: reportMetadataPropTypes, - /** Array of report actions for this report */ - sortedReportActions: PropTypes.arrayOf(PropTypes.shape(reportActionPropTypes)), - /** Whether the composer is full size */ isComposerFullSize: PropTypes.bool, @@ -104,7 +101,7 @@ const propTypes = { const defaultProps = { isSidebarLoaded: false, - sortedReportActions: [], + // sortedReportActions: [], report: {}, reportMetadata: { isLoadingInitialReportActions: true, @@ -181,9 +178,9 @@ function ReportScreen({ const reportActions = useMemo(() => { if (allReportActions?.length === 0) return []; - const sorterReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, true); - const cattedRangeOfReportActions = ReportActionsUtils.getRangeFromArrayByID(sorterReportActions, reportActionID); - const reportActionsWithoutDeleted = ReportActionsUtils.getReportActionsWithoutRemoved(cattedRangeOfReportActions); + const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions); + const cattedRangeOfReportActions = ReportActionsUtils.getRangeFromArrayByID(sortedReportActions, reportActionID); + const reportActionsWithoutDeleted = ReportActionsUtils.getReportActionsWithoutRemoved(cattedRangeOfReportActions, true); return reportActionsWithoutDeleted; }, [reportActionID, allReportActions, isOffline]); const [isBannerVisible, setIsBannerVisible] = useState(true); @@ -199,7 +196,6 @@ function ReportScreen({ const {addWorkspaceRoomOrChatPendingAction, addWorkspaceRoomOrChatErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report); const screenWrapperStyle = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}]; - // There are no reportActions at all to display and we are still in the process of loading the next set of actions. const isLoadingInitialReportActions = _.isEmpty(reportActions) && reportMetadata.isLoadingInitialReportActions; diff --git a/tests/unit/ReportActionsUtilsTest.js b/tests/unit/ReportActionsUtilsTest.js index 545d442e4799..6e7620b75d76 100644 --- a/tests/unit/ReportActionsUtilsTest.js +++ b/tests/unit/ReportActionsUtilsTest.js @@ -239,7 +239,7 @@ describe('ReportActionsUtils', () => { ]; const resultWithoutNewestFlag = ReportActionsUtils.getSortedReportActionsForDisplay(input); - const resultWithNewestFlag = ReportActionsUtils.getSortedReportActionsForDisplay(input, true); + const resultWithNewestFlag = ReportActionsUtils.getReportActionsWithoutRemoved(input, true); input.pop(); // Mark the newest report action as the newest report action resultWithoutNewestFlag[0] = { From 91b8a48d3f3ec03ff1fb9aeb3d945b19ac249986 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 27 Dec 2023 21:09:16 +0100 Subject: [PATCH 031/245] run hover after interaction --- src/components/Hoverable/index.tsx | 31 +++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/components/Hoverable/index.tsx b/src/components/Hoverable/index.tsx index 9c641cfc19be..eba0542a2d38 100644 --- a/src/components/Hoverable/index.tsx +++ b/src/components/Hoverable/index.tsx @@ -1,5 +1,5 @@ import React, {ForwardedRef, forwardRef, MutableRefObject, ReactElement, RefAttributes, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; -import {DeviceEventEmitter} from 'react-native'; +import {DeviceEventEmitter, InteractionManager} from 'react-native'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import CONST from '@src/CONST'; import HoverableProps from './types'; @@ -41,16 +41,41 @@ function assignRef(ref: ((instance: HTMLElement | null) => void) | MutableRefObj } } +type UseHoveredReturnType = [boolean, (newValue: boolean) => void]; +function useHovered(initialValue: boolean, runHoverAfterInteraction: boolean): UseHoveredReturnType { + const [state, setState] = useState(initialValue); + + const interceptedSetState = useCallback((newValue: boolean) => { + if (runHoverAfterInteraction) { + InteractionManager.runAfterInteractions(() => { + setState(newValue); + }); + } else { + setState(newValue); + } + }, []); + return [state, interceptedSetState]; +} + /** * It is necessary to create a Hoverable component instead of relying solely on Pressable support for hover state, * because nesting Pressables causes issues where the hovered state of the child cannot be easily propagated to the * parent. https://github.com/necolas/react-native-web/issues/1875 */ function Hoverable( - {disabled = false, onHoverIn = () => {}, onHoverOut = () => {}, onMouseEnter = () => {}, onMouseLeave = () => {}, children, shouldHandleScroll = false}: HoverableProps, + { + disabled = false, + onHoverIn = () => {}, + onHoverOut = () => {}, + onMouseEnter = () => {}, + onMouseLeave = () => {}, + children, + shouldHandleScroll = false, + runHoverAfterInteraction = false, + }: HoverableProps, outerRef: ForwardedRef, ) { - const [isHovered, setIsHovered] = useState(false); + const [isHovered, setIsHovered] = useHovered(false, runHoverAfterInteraction); const isScrolling = useRef(false); const isHoveredRef = useRef(false); From 3f652b2348ed4df85b5bd598ee1edb3c468bc350 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 27 Dec 2023 21:09:29 +0100 Subject: [PATCH 032/245] add types --- src/components/Hoverable/types.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/Hoverable/types.ts b/src/components/Hoverable/types.ts index 430b865f50c5..99b26f2ee10a 100644 --- a/src/components/Hoverable/types.ts +++ b/src/components/Hoverable/types.ts @@ -21,6 +21,9 @@ type HoverableProps = { /** Decides whether to handle the scroll behaviour to show hover once the scroll ends */ shouldHandleScroll?: boolean; + + /** Call setHovered(true) with runAfterInteraction */ + runHoverAfterInteraction?: boolean; }; export default HoverableProps; From 7ab464f2aae858aa892fa8ff87ca5f239b546475 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 27 Dec 2023 21:11:31 +0100 Subject: [PATCH 033/245] remove shouldMarkTheFirstItemAsNewest --- src/libs/ReportActionsUtils.ts | 12 ++---------- src/pages/home/ReportScreen.js | 5 ++--- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index f5f2637d4f2d..e4892fdfef32 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -566,19 +566,11 @@ function getSortedReportActionsForDisplay(reportActions: ReportActions | null): return getSortedReportActions(baseURLAdjustedReportActions, true); } -function getReportActionsWithoutRemoved(reportActions: ReportAction[] | null, shouldMarkTheFirstItemAsNewest = false): ReportAction[] { +function getReportActionsWithoutRemoved(reportActions: ReportAction[] | null): ReportAction[] { if (!reportActions) { return []; } - const filtered = reportActions.filter((item) => shouldReportActionBeVisible(item, item.reportActionID)); - - if (shouldMarkTheFirstItemAsNewest && filtered?.length > 0) { - filtered[0] = { - ...filtered[0], - isNewestReportAction: true, - }; - } - return filtered; + return reportActions.filter((item) => shouldReportActionBeVisible(item, item.reportActionID)); } /** diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 6c223b89e09e..1dd5f1caf582 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -39,7 +39,6 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import HeaderView from './HeaderView'; -import reportActionPropTypes from './report/reportActionPropTypes'; import ReportActionsView from './report/ReportActionsView'; import ReportFooter from './report/ReportFooter'; import {ActionListContext, ReactionListContext} from './ReportScreenContext'; @@ -180,7 +179,7 @@ function ReportScreen({ if (allReportActions?.length === 0) return []; const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions); const cattedRangeOfReportActions = ReportActionsUtils.getRangeFromArrayByID(sortedReportActions, reportActionID); - const reportActionsWithoutDeleted = ReportActionsUtils.getReportActionsWithoutRemoved(cattedRangeOfReportActions, true); + const reportActionsWithoutDeleted = ReportActionsUtils.getReportActionsWithoutRemoved(cattedRangeOfReportActions); return reportActionsWithoutDeleted; }, [reportActionID, allReportActions, isOffline]); const [isBannerVisible, setIsBannerVisible] = useState(true); @@ -490,7 +489,7 @@ function ReportScreen({ style={[styles.flex1, styles.justifyContentEnd, styles.overflowHidden]} onLayout={onListLayout} > - {isReportReadyForDisplay && !isLoading && ( + {isReportReadyForDisplay && ( Date: Wed, 27 Dec 2023 21:11:57 +0100 Subject: [PATCH 034/245] add runHoverAfterInteraction to report --- src/pages/home/report/ReportActionItem.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index c81e47016dcc..77d269d799eb 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -677,6 +677,7 @@ function ReportActionItem(props) { > {(hovered) => ( From d594cec109a0e9fe59dfdb2fa338396ad03c7005 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 27 Dec 2023 21:13:55 +0100 Subject: [PATCH 035/245] adjust the chat cutting for proper layout --- src/pages/home/report/ReportActionsView.js | 49 +++++++++++----------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 6b5b86a1950b..1a28b222c4e9 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -16,7 +16,7 @@ import useCopySelectionHelper from '@hooks/useCopySelectionHelper'; import useInitialValue from '@hooks/useInitialValue'; import usePrevious from '@hooks/usePrevious'; import useReportScrollManager from '@hooks/useReportScrollManager'; -// import useWindowDimensions from '@hooks/useWindowDimensions'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import compose from '@libs/compose'; import getIsReportFullyVisible from '@libs/getIsReportFullyVisible'; import Performance from '@libs/Performance'; @@ -100,7 +100,7 @@ function getReportActionID(route) { const useHandleList = (linkedID, messageArray, fetchFn, route, isLoadingLinkedMessage) => { const [edgeID, setEdgeID] = useState(linkedID); - const [listID, setListID] = useState(1); + const [listID, setListID] = useState(() => Math.round(Math.random() * 100)); const isFirstRender = useRef(true); const index = useMemo(() => { @@ -116,21 +116,18 @@ const useHandleList = (linkedID, messageArray, fetchFn, route, isLoadingLinkedMe setEdgeID(''); }, [route, linkedID]); + const cattedArray = useMemo(() => { if (!linkedID || index === -1) { return messageArray; } - if ((linkedID && !edgeID) || (linkedID && isFirstRender.current)) { + if (isFirstRender.current) { setListID((i) => i + 1); - isFirstRender.current = false; return messageArray.slice(index, messageArray.length); - } else if (linkedID && edgeID) { + } else if (edgeID) { const amountOfItemsBeforeLinkedOne = 10; const newStartIndex = index >= amountOfItemsBeforeLinkedOne ? index - amountOfItemsBeforeLinkedOne : 0; - if (index) { - return messageArray.slice(newStartIndex, messageArray.length); - } - return messageArray; + return messageArray.slice(newStartIndex, messageArray.length); } return messageArray; }, [linkedID, messageArray, edgeID, index, isLoadingLinkedMessage]); @@ -145,7 +142,14 @@ const useHandleList = (linkedID, messageArray, fetchFn, route, isLoadingLinkedMe fetchFn({distanceFromStart}); } - setEdgeID(firstReportActionID); + if (isFirstRender.current) { + isFirstRender.current = false; + InteractionManager.runAfterInteractions(() => { + setEdgeID(firstReportActionID); + }); + } else { + setEdgeID(firstReportActionID); + } }, [setEdgeID, fetchFn, hasMoreCashed], ); @@ -169,10 +173,9 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro const contentListHeight = useRef(0); const layoutListHeight = useRef(0); const isInitial = useRef(true); - // const isLoadingLinkedMessage = !!reportActionID && props.isLoadingInitialReportActions; const hasCachedActions = useInitialValue(() => _.size(props.reportActions) > 0); const mostRecentIOUReportActionID = useInitialValue(() => ReportActionsUtils.getMostRecentIOURequestActionID(props.reportActions)); - // const {windowHeight} = useWindowDimensions(); + const {windowHeight} = useWindowDimensions(); const prevNetworkRef = useRef(props.network); const prevAuthTokenType = usePrevious(props.session.authTokenType); @@ -201,7 +204,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro const {cattedArray: reportActions, fetchFunc, linkedIdIndex, listID} = useHandleList(reportActionID, allReportActions, throttledLoadNewerChats, route); - const hasNewestReportAction = lodashGet(reportActions[0], 'isNewestReportAction'); + const hasNewestReportAction = lodashGet(reportActions[0], 'created') === props.report.lastVisibleActionCreated; const newestReportAction = lodashGet(reportActions, '[0]'); const oldestReportAction = _.last(reportActions); const isWeReachedTheOldestAction = oldestReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED; @@ -321,21 +324,19 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro const handleLoadNewerChats = useCallback( // eslint-disable-next-line rulesdir/prefer-early-return ({distanceFromStart}) => { - // const shouldFirstlyLoadOlderActions = Number(layoutListHeight.current) > Number(contentListHeight.current); - // const DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST = 164; - // const SPACER = 30; - // const MIN_PREDEFINED_PADDING = 16; - // const isListSmallerThanScreen = windowHeight - DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST - SPACER > contentListHeight.current; - // const isListEmpty = contentListHeight.current === MIN_PREDEFINED_PADDING; - // const shouldFirstlyLoadOlderActions = !isWeReachedTheOldestAction && isListSmallerThanScreen - // const shouldFirstlyLoadOlderActions = !isListSmallerThanScreen - if ((reportActionID && linkedIdIndex > -1 && !hasNewestReportAction && !isInitial.current) || (!reportActionID && !hasNewestReportAction)) { + const DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST = 164; + const SPACER = 30; + const isContentSmallerThanList = windowHeight - DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST - SPACER > contentListHeight.current; + + if ( + (reportActionID && linkedIdIndex > -1 && !hasNewestReportAction && !isInitial.current && !isContentSmallerThanList) || + (!reportActionID && !hasNewestReportAction && !isContentSmallerThanList) + ) { fetchFunc({firstReportActionID, distanceFromStart}); } isInitial.current = false; }, - // [hasNewestReportAction, linkedIdIndex, firstReportActionID, fetchFunc, reportActionID, windowHeight, isWeReachedTheOldestAction], - [hasNewestReportAction, linkedIdIndex, firstReportActionID, fetchFunc, reportActionID], + [hasNewestReportAction, linkedIdIndex, firstReportActionID, fetchFunc, reportActionID, windowHeight], ); /** From 96a1d42b2a3930e442d7d9cc392f76e0f1864b33 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 27 Dec 2023 21:45:50 +0100 Subject: [PATCH 036/245] bring back 'Inverted fix' --- .../react-native-web+0.19.9+001+initial.patch | 872 +++++++++++++----- ...react-native-web+0.19.9+002+fix-mvcp.patch | 687 -------------- ...tive-web+0.19.9+002+measureInWindow.patch} | 0 ...e-web+0.19.9+003+fix-pointer-events.patch} | 0 ...-native-web+0.19.9+005+fixLastSpacer.patch | 29 - 5 files changed, 617 insertions(+), 971 deletions(-) delete mode 100644 patches/react-native-web+0.19.9+002+fix-mvcp.patch rename patches/{react-native-web+0.19.9+003+measureInWindow.patch => react-native-web+0.19.9+002+measureInWindow.patch} (100%) rename patches/{react-native-web+0.19.9+004+fix-pointer-events.patch => react-native-web+0.19.9+003+fix-pointer-events.patch} (100%) delete mode 100644 patches/react-native-web+0.19.9+005+fixLastSpacer.patch diff --git a/patches/react-native-web+0.19.9+001+initial.patch b/patches/react-native-web+0.19.9+001+initial.patch index d88ef83d4bcd..91ba6bfd59c0 100644 --- a/patches/react-native-web+0.19.9+001+initial.patch +++ b/patches/react-native-web+0.19.9+001+initial.patch @@ -1,286 +1,648 @@ diff --git a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js -index c879838..288316c 100644 +index c879838..0c9dfcb 100644 --- a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js +++ b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js -@@ -117,6 +117,14 @@ function findLastWhere(arr, predicate) { - * - */ - class VirtualizedList extends StateSafePureComponent { -+ pushOrUnshift(input, item) { -+ if (this.props.inverted) { -+ input.unshift(item); -+ } else { -+ input.push(item); +@@ -285,7 +285,7 @@ class VirtualizedList extends StateSafePureComponent { + // $FlowFixMe[missing-local-annot] + + constructor(_props) { +- var _this$props$updateCel; ++ var _this$props$updateCel, _this$props$maintainV, _this$props$maintainV2; + super(_props); + this._getScrollMetrics = () => { + return this._scrollMetrics; +@@ -520,6 +520,11 @@ class VirtualizedList extends StateSafePureComponent { + visibleLength, + zoomScale + }; ++ if (this.state.pendingScrollUpdateCount > 0) { ++ this.setState(state => ({ ++ pendingScrollUpdateCount: state.pendingScrollUpdateCount - 1 ++ })); ++ } + this._updateViewableItems(this.props, this.state.cellsAroundViewport); + if (!this.props) { + return; +@@ -569,7 +574,7 @@ class VirtualizedList extends StateSafePureComponent { + this._updateCellsToRender = () => { + this._updateViewableItems(this.props, this.state.cellsAroundViewport); + this.setState((state, props) => { +- var cellsAroundViewport = this._adjustCellsAroundViewport(props, state.cellsAroundViewport); ++ var cellsAroundViewport = this._adjustCellsAroundViewport(props, state.cellsAroundViewport, state.pendingScrollUpdateCount); + var renderMask = VirtualizedList._createRenderMask(props, cellsAroundViewport, this._getNonViewportRenderRegions(props)); + if (cellsAroundViewport.first === state.cellsAroundViewport.first && cellsAroundViewport.last === state.cellsAroundViewport.last && renderMask.equals(state.renderMask)) { + return null; +@@ -589,7 +594,7 @@ class VirtualizedList extends StateSafePureComponent { + return { + index, + item, +- key: this._keyExtractor(item, index, props), ++ key: VirtualizedList._keyExtractor(item, index, props), + isViewable + }; + }; +@@ -621,12 +626,10 @@ class VirtualizedList extends StateSafePureComponent { + }; + this._getFrameMetrics = (index, props) => { + var data = props.data, +- getItem = props.getItem, + getItemCount = props.getItemCount, + getItemLayout = props.getItemLayout; + invariant(index >= 0 && index < getItemCount(data), 'Tried to get frame for out of range index ' + index); +- var item = getItem(data, index); +- var frame = this._frames[this._keyExtractor(item, index, props)]; ++ var frame = this._frames[VirtualizedList._getItemKey(props, index)]; + if (!frame || frame.index !== index) { + if (getItemLayout) { + /* $FlowFixMe[prop-missing] (>=0.63.0 site=react_native_fb) This comment +@@ -650,7 +653,7 @@ class VirtualizedList extends StateSafePureComponent { + + // The last cell we rendered may be at a new index. Bail if we don't know + // where it is. +- if (focusedCellIndex >= itemCount || this._keyExtractor(props.getItem(props.data, focusedCellIndex), focusedCellIndex, props) !== this._lastFocusedCellKey) { ++ if (focusedCellIndex >= itemCount || VirtualizedList._getItemKey(props, focusedCellIndex) !== this._lastFocusedCellKey) { + return []; + } + var first = focusedCellIndex; +@@ -690,9 +693,15 @@ class VirtualizedList extends StateSafePureComponent { + } + } + var initialRenderRegion = VirtualizedList._initialRenderRegion(_props); ++ var minIndexForVisible = (_this$props$maintainV = (_this$props$maintainV2 = this.props.maintainVisibleContentPosition) == null ? void 0 : _this$props$maintainV2.minIndexForVisible) !== null && _this$props$maintainV !== void 0 ? _this$props$maintainV : 0; + this.state = { + cellsAroundViewport: initialRenderRegion, +- renderMask: VirtualizedList._createRenderMask(_props, initialRenderRegion) ++ renderMask: VirtualizedList._createRenderMask(_props, initialRenderRegion), ++ firstVisibleItemKey: this.props.getItemCount(this.props.data) > minIndexForVisible ? VirtualizedList._getItemKey(this.props, minIndexForVisible) : null, ++ // When we have a non-zero initialScrollIndex, we will receive a ++ // scroll event later so this will prevent the window from updating ++ // until we get a valid offset. ++ pendingScrollUpdateCount: this.props.initialScrollIndex != null && this.props.initialScrollIndex > 0 ? 1 : 0 + }; + + // REACT-NATIVE-WEB patch to preserve during future RN merges: Support inverted wheel scroller. +@@ -748,6 +757,26 @@ class VirtualizedList extends StateSafePureComponent { + } + } + } ++ static _findItemIndexWithKey(props, key, hint) { ++ var itemCount = props.getItemCount(props.data); ++ if (hint != null && hint >= 0 && hint < itemCount) { ++ var curKey = VirtualizedList._getItemKey(props, hint); ++ if (curKey === key) { ++ return hint; ++ } ++ } ++ for (var ii = 0; ii < itemCount; ii++) { ++ var _curKey = VirtualizedList._getItemKey(props, ii); ++ if (_curKey === key) { ++ return ii; ++ } + } ++ return null; + } -+ - // scrollToEnd may be janky without getItemLayout prop - scrollToEnd(params) { - var animated = params ? params.animated : true; -@@ -350,6 +358,7 @@ class VirtualizedList extends StateSafePureComponent { - }; - this._defaultRenderScrollComponent = props => { - var onRefresh = props.onRefresh; -+ var inversionStyle = this.props.inverted ? this.props.horizontal ? styles.rowReverse : styles.columnReverse : null; - if (this._isNestedWithSameOrientation()) { - // $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors - return /*#__PURE__*/React.createElement(View, props); -@@ -367,13 +376,16 @@ class VirtualizedList extends StateSafePureComponent { - refreshing: props.refreshing, - onRefresh: onRefresh, - progressViewOffset: props.progressViewOffset -- }) : props.refreshControl -+ }) : props.refreshControl, -+ contentContainerStyle: [inversionStyle, this.props.contentContainerStyle] - })) - ); - } else { - // $FlowFixMe[prop-missing] Invalid prop usage - // $FlowFixMe[incompatible-use] -- return /*#__PURE__*/React.createElement(ScrollView, props); -+ return /*#__PURE__*/React.createElement(ScrollView, _extends({}, props, { -+ contentContainerStyle: [inversionStyle, this.props.contentContainerStyle] -+ })); ++ static _getItemKey(props, index) { ++ var item = props.getItem(props.data, index); ++ return VirtualizedList._keyExtractor(item, index, props); ++ } + static _createRenderMask(props, cellsAroundViewport, additionalRegions) { + var itemCount = props.getItemCount(props.data); + invariant(cellsAroundViewport.first >= 0 && cellsAroundViewport.last >= cellsAroundViewport.first - 1 && cellsAroundViewport.last < itemCount, "Invalid cells around viewport \"[" + cellsAroundViewport.first + ", " + cellsAroundViewport.last + "]\" was passed to VirtualizedList._createRenderMask"); +@@ -796,7 +825,7 @@ class VirtualizedList extends StateSafePureComponent { + } + } + } +- _adjustCellsAroundViewport(props, cellsAroundViewport) { ++ _adjustCellsAroundViewport(props, cellsAroundViewport, pendingScrollUpdateCount) { + var data = props.data, + getItemCount = props.getItemCount; + var onEndReachedThreshold = onEndReachedThresholdOrDefault(props.onEndReachedThreshold); +@@ -819,17 +848,9 @@ class VirtualizedList extends StateSafePureComponent { + last: Math.min(cellsAroundViewport.last + renderAhead, getItemCount(data) - 1) + }; + } else { +- // If we have a non-zero initialScrollIndex and run this before we've scrolled, +- // we'll wipe out the initialNumToRender rendered elements starting at initialScrollIndex. +- // So let's wait until we've scrolled the view to the right place. And until then, +- // we will trust the initialScrollIndex suggestion. +- +- // Thus, we want to recalculate the windowed render limits if any of the following hold: +- // - initialScrollIndex is undefined or is 0 +- // - initialScrollIndex > 0 AND scrolling is complete +- // - initialScrollIndex > 0 AND the end of the list is visible (this handles the case +- // where the list is shorter than the visible area) +- if (props.initialScrollIndex && !this._scrollMetrics.offset && Math.abs(distanceFromEnd) >= Number.EPSILON) { ++ // If we have a pending scroll update, we should not adjust the render window as it ++ // might override the correct window. ++ if (pendingScrollUpdateCount > 0) { + return cellsAroundViewport.last >= getItemCount(data) ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) : cellsAroundViewport; } + newCellsAroundViewport = computeWindowedRenderLimits(props, maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch), windowSizeOrDefault(props.windowSize), cellsAroundViewport, this.__getFrameMetricsApprox, this._scrollMetrics); +@@ -902,16 +923,36 @@ class VirtualizedList extends StateSafePureComponent { + } + } + static getDerivedStateFromProps(newProps, prevState) { ++ var _newProps$maintainVis, _newProps$maintainVis2; + // first and last could be stale (e.g. if a new, shorter items props is passed in), so we make + // sure we're rendering a reasonable range here. + var itemCount = newProps.getItemCount(newProps.data); + if (itemCount === prevState.renderMask.numCells()) { + return prevState; + } +- var constrainedCells = VirtualizedList._constrainToItemCount(prevState.cellsAroundViewport, newProps); ++ var maintainVisibleContentPositionAdjustment = null; ++ var prevFirstVisibleItemKey = prevState.firstVisibleItemKey; ++ var minIndexForVisible = (_newProps$maintainVis = (_newProps$maintainVis2 = newProps.maintainVisibleContentPosition) == null ? void 0 : _newProps$maintainVis2.minIndexForVisible) !== null && _newProps$maintainVis !== void 0 ? _newProps$maintainVis : 0; ++ var newFirstVisibleItemKey = newProps.getItemCount(newProps.data) > minIndexForVisible ? VirtualizedList._getItemKey(newProps, minIndexForVisible) : null; ++ if (newProps.maintainVisibleContentPosition != null && prevFirstVisibleItemKey != null && newFirstVisibleItemKey != null) { ++ if (newFirstVisibleItemKey !== prevFirstVisibleItemKey) { ++ // Fast path if items were added at the start of the list. ++ var hint = itemCount - prevState.renderMask.numCells() + minIndexForVisible; ++ var firstVisibleItemIndex = VirtualizedList._findItemIndexWithKey(newProps, prevFirstVisibleItemKey, hint); ++ maintainVisibleContentPositionAdjustment = firstVisibleItemIndex != null ? firstVisibleItemIndex - minIndexForVisible : null; ++ } else { ++ maintainVisibleContentPositionAdjustment = null; ++ } ++ } ++ var constrainedCells = VirtualizedList._constrainToItemCount(maintainVisibleContentPositionAdjustment != null ? { ++ first: prevState.cellsAroundViewport.first + maintainVisibleContentPositionAdjustment, ++ last: prevState.cellsAroundViewport.last + maintainVisibleContentPositionAdjustment ++ } : prevState.cellsAroundViewport, newProps); + return { + cellsAroundViewport: constrainedCells, +- renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells) ++ renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells), ++ firstVisibleItemKey: newFirstVisibleItemKey, ++ pendingScrollUpdateCount: maintainVisibleContentPositionAdjustment != null ? prevState.pendingScrollUpdateCount + 1 : prevState.pendingScrollUpdateCount }; - this._onCellLayout = (e, cellKey, index) => { -@@ -683,7 +695,7 @@ class VirtualizedList extends StateSafePureComponent { - onViewableItemsChanged = _this$props3.onViewableItemsChanged, - viewabilityConfig = _this$props3.viewabilityConfig; - if (onViewableItemsChanged) { -- this._viewabilityTuples.push({ -+ this.pushOrUnshift(this._viewabilityTuples, { - viewabilityHelper: new ViewabilityHelper(viewabilityConfig), - onViewableItemsChanged: onViewableItemsChanged - }); -@@ -937,10 +949,10 @@ class VirtualizedList extends StateSafePureComponent { - var key = _this._keyExtractor(item, ii, _this.props); + } + _pushCells(cells, stickyHeaderIndices, stickyIndicesFromProps, first, last, inversionStyle) { +@@ -934,7 +975,7 @@ class VirtualizedList extends StateSafePureComponent { + last = Math.min(end, last); + var _loop = function _loop() { + var item = getItem(data, ii); +- var key = _this._keyExtractor(item, ii, _this.props); ++ var key = VirtualizedList._keyExtractor(item, ii, _this.props); _this._indicesToKeys.set(ii, key); if (stickyIndicesFromProps.has(ii + stickyOffset)) { -- stickyHeaderIndices.push(cells.length); -+ _this.pushOrUnshift(stickyHeaderIndices, cells.length); - } - var shouldListenForLayout = getItemLayout == null || debug || _this._fillRateHelper.enabled(); -- cells.push( /*#__PURE__*/React.createElement(CellRenderer, _extends({ -+ _this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(CellRenderer, _extends({ - CellRendererComponent: CellRendererComponent, - ItemSeparatorComponent: ii < end ? ItemSeparatorComponent : undefined, - ListItemComponent: ListItemComponent, -@@ -1012,14 +1024,14 @@ class VirtualizedList extends StateSafePureComponent { - // 1. Add cell for ListHeaderComponent - if (ListHeaderComponent) { - if (stickyIndicesFromProps.has(0)) { -- stickyHeaderIndices.push(0); -+ this.pushOrUnshift(stickyHeaderIndices, 0); - } - var _element = /*#__PURE__*/React.isValidElement(ListHeaderComponent) ? ListHeaderComponent : - /*#__PURE__*/ - // $FlowFixMe[not-a-component] - // $FlowFixMe[incompatible-type-arg] - React.createElement(ListHeaderComponent, null); -- cells.push( /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { -+ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { + stickyHeaderIndices.push(cells.length); +@@ -969,20 +1010,23 @@ class VirtualizedList extends StateSafePureComponent { + } + static _constrainToItemCount(cells, props) { + var itemCount = props.getItemCount(props.data); +- var last = Math.min(itemCount - 1, cells.last); ++ var lastPossibleCellIndex = itemCount - 1; ++ ++ // Constraining `last` may significantly shrink the window. Adjust `first` ++ // to expand the window if the new `last` results in a new window smaller ++ // than the number of cells rendered per batch. + var maxToRenderPerBatch = maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch); ++ var maxFirst = Math.max(0, lastPossibleCellIndex - maxToRenderPerBatch); + return { +- first: clamp(0, itemCount - 1 - maxToRenderPerBatch, cells.first), +- last ++ first: clamp(0, cells.first, maxFirst), ++ last: Math.min(lastPossibleCellIndex, cells.last) + }; + } + _isNestedWithSameOrientation() { + var nestedContext = this.context; + return !!(nestedContext && !!nestedContext.horizontal === horizontalOrDefault(this.props.horizontal)); + } +- _keyExtractor(item, index, props +- // $FlowFixMe[missing-local-annot] +- ) { ++ static _keyExtractor(item, index, props) { + if (props.keyExtractor != null) { + return props.keyExtractor(item, index); + } +@@ -1022,7 +1066,12 @@ class VirtualizedList extends StateSafePureComponent { + cells.push( /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { cellKey: this._getCellKey() + '-header', key: "$header" - }, /*#__PURE__*/React.createElement(View, { -@@ -1038,7 +1050,7 @@ class VirtualizedList extends StateSafePureComponent { - // $FlowFixMe[not-a-component] - // $FlowFixMe[incompatible-type-arg] - React.createElement(ListEmptyComponent, null); -- cells.push( /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { -+ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { - cellKey: this._getCellKey() + '-empty', - key: "$empty" - }, /*#__PURE__*/React.cloneElement(_element2, { -@@ -1077,7 +1089,7 @@ class VirtualizedList extends StateSafePureComponent { - var firstMetrics = this.__getFrameMetricsApprox(section.first, this.props); - var lastMetrics = this.__getFrameMetricsApprox(last, this.props); - var spacerSize = lastMetrics.offset + lastMetrics.length - firstMetrics.offset; -- cells.push( /*#__PURE__*/React.createElement(View, { -+ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(View, { - key: "$spacer-" + section.first, - style: { - [spacerKey]: spacerSize -@@ -1100,7 +1112,7 @@ class VirtualizedList extends StateSafePureComponent { - // $FlowFixMe[not-a-component] - // $FlowFixMe[incompatible-type-arg] - React.createElement(ListFooterComponent, null); -- cells.push( /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { -+ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { - cellKey: this._getFooterCellKey(), - key: "$footer" - }, /*#__PURE__*/React.createElement(View, { -@@ -1266,7 +1278,7 @@ class VirtualizedList extends StateSafePureComponent { - * suppresses an error found when Flow v0.68 was deployed. To see the - * error delete this comment and run Flow. */ - if (frame.inLayout) { -- framesInLayout.push(frame); -+ this.pushOrUnshift(framesInLayout, frame); - } +- }, /*#__PURE__*/React.createElement(View, { ++ }, /*#__PURE__*/React.createElement(View ++ // We expect that header component will be a single native view so make it ++ // not collapsable to avoid this view being flattened and make this assumption ++ // no longer true. ++ , { ++ collapsable: false, + onLayout: this._onLayoutHeader, + style: [inversionStyle, this.props.ListHeaderComponentStyle] + }, +@@ -1124,7 +1173,11 @@ class VirtualizedList extends StateSafePureComponent { + // TODO: Android support + invertStickyHeaders: this.props.invertStickyHeaders !== undefined ? this.props.invertStickyHeaders : this.props.inverted, + stickyHeaderIndices, +- style: inversionStyle ? [inversionStyle, this.props.style] : this.props.style ++ style: inversionStyle ? [inversionStyle, this.props.style] : this.props.style, ++ maintainVisibleContentPosition: this.props.maintainVisibleContentPosition != null ? _objectSpread(_objectSpread({}, this.props.maintainVisibleContentPosition), {}, { ++ // Adjust index to account for ListHeaderComponent. ++ minIndexForVisible: this.props.maintainVisibleContentPosition.minIndexForVisible + (this.props.ListHeaderComponent ? 1 : 0) ++ }) : undefined + }); + this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1; + var innerRet = /*#__PURE__*/React.createElement(VirtualizedListContextProvider, { +@@ -1307,8 +1360,12 @@ class VirtualizedList extends StateSafePureComponent { + onStartReached = _this$props8.onStartReached, + onStartReachedThreshold = _this$props8.onStartReachedThreshold, + onEndReached = _this$props8.onEndReached, +- onEndReachedThreshold = _this$props8.onEndReachedThreshold, +- initialScrollIndex = _this$props8.initialScrollIndex; ++ onEndReachedThreshold = _this$props8.onEndReachedThreshold; ++ // If we have any pending scroll updates it means that the scroll metrics ++ // are out of date and we should not call any of the edge reached callbacks. ++ if (this.state.pendingScrollUpdateCount > 0) { ++ return; ++ } + var _this$_scrollMetrics2 = this._scrollMetrics, + contentLength = _this$_scrollMetrics2.contentLength, + visibleLength = _this$_scrollMetrics2.visibleLength, +@@ -1348,16 +1405,10 @@ class VirtualizedList extends StateSafePureComponent { + // and call onStartReached only once for a given content length, + // and only if onEndReached is not being executed + else if (onStartReached != null && this.state.cellsAroundViewport.first === 0 && isWithinStartThreshold && this._scrollMetrics.contentLength !== this._sentStartForContentLength) { +- // On initial mount when using initialScrollIndex the offset will be 0 initially +- // and will trigger an unexpected onStartReached. To avoid this we can use +- // timestamp to differentiate between the initial scroll metrics and when we actually +- // received the first scroll event. +- if (!initialScrollIndex || this._scrollMetrics.timestamp !== 0) { +- this._sentStartForContentLength = this._scrollMetrics.contentLength; +- onStartReached({ +- distanceFromStart +- }); +- } ++ this._sentStartForContentLength = this._scrollMetrics.contentLength; ++ onStartReached({ ++ distanceFromStart ++ }); + } + + // If the user scrolls away from the start or end and back again, +@@ -1412,6 +1463,11 @@ class VirtualizedList extends StateSafePureComponent { } - var windowTop = this.__getFrameMetricsApprox(this.state.cellsAroundViewport.first, this.props).offset; -@@ -1452,6 +1464,12 @@ var styles = StyleSheet.create({ - left: 0, - borderColor: 'red', - borderWidth: 2 -+ }, -+ rowReverse: { -+ flexDirection: 'row-reverse' -+ }, -+ columnReverse: { -+ flexDirection: 'column-reverse' } - }); - export default VirtualizedList; -\ No newline at end of file + _updateViewableItems(props, cellsAroundViewport) { ++ // If we have any pending scroll updates it means that the scroll metrics ++ // are out of date and we should not call any of the visibility callbacks. ++ if (this.state.pendingScrollUpdateCount > 0) { ++ return; ++ } + this._viewabilityTuples.forEach(tuple => { + tuple.viewabilityHelper.onUpdate(props, this._scrollMetrics.offset, this._scrollMetrics.visibleLength, this._getFrameMetrics, this._createViewToken, tuple.onViewableItemsChanged, cellsAroundViewport); + }); diff --git a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js -index c7d68bb..46b3fc9 100644 +index c7d68bb..43f9653 100644 --- a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js +++ b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js -@@ -167,6 +167,14 @@ function findLastWhere( - class VirtualizedList extends StateSafePureComponent { - static contextType: typeof VirtualizedListContext = VirtualizedListContext; +@@ -75,6 +75,10 @@ type ViewabilityHelperCallbackTuple = { + type State = { + renderMask: CellRenderMask, + cellsAroundViewport: {first: number, last: number}, ++ // Used to track items added at the start of the list for maintainVisibleContentPosition. ++ firstVisibleItemKey: ?string, ++ // When > 0 the scroll position available in JS is considered stale and should not be used. ++ pendingScrollUpdateCount: number, + }; + + /** +@@ -447,9 +451,24 @@ class VirtualizedList extends StateSafePureComponent { + + const initialRenderRegion = VirtualizedList._initialRenderRegion(props); -+ pushOrUnshift(input: Array, item: Item) { -+ if (this.props.inverted) { -+ input.unshift(item) -+ } else { -+ input.push(item) ++ const minIndexForVisible = ++ this.props.maintainVisibleContentPosition?.minIndexForVisible ?? 0; ++ + this.state = { + cellsAroundViewport: initialRenderRegion, + renderMask: VirtualizedList._createRenderMask(props, initialRenderRegion), ++ firstVisibleItemKey: ++ this.props.getItemCount(this.props.data) > minIndexForVisible ++ ? VirtualizedList._getItemKey(this.props, minIndexForVisible) ++ : null, ++ // When we have a non-zero initialScrollIndex, we will receive a ++ // scroll event later so this will prevent the window from updating ++ // until we get a valid offset. ++ pendingScrollUpdateCount: ++ this.props.initialScrollIndex != null && ++ this.props.initialScrollIndex > 0 ++ ? 1 ++ : 0, + }; + + // REACT-NATIVE-WEB patch to preserve during future RN merges: Support inverted wheel scroller. +@@ -534,6 +553,40 @@ class VirtualizedList extends StateSafePureComponent { + } + } + ++ static _findItemIndexWithKey( ++ props: Props, ++ key: string, ++ hint: ?number, ++ ): ?number { ++ const itemCount = props.getItemCount(props.data); ++ if (hint != null && hint >= 0 && hint < itemCount) { ++ const curKey = VirtualizedList._getItemKey(props, hint); ++ if (curKey === key) { ++ return hint; ++ } + } ++ for (let ii = 0; ii < itemCount; ii++) { ++ const curKey = VirtualizedList._getItemKey(props, ii); ++ if (curKey === key) { ++ return ii; ++ } ++ } ++ return null; ++ } ++ ++ static _getItemKey( ++ props: { ++ data: Props['data'], ++ getItem: Props['getItem'], ++ keyExtractor: Props['keyExtractor'], ++ ... ++ }, ++ index: number, ++ ): string { ++ const item = props.getItem(props.data, index); ++ return VirtualizedList._keyExtractor(item, index, props); + } + - // scrollToEnd may be janky without getItemLayout prop - scrollToEnd(params?: ?{animated?: ?boolean, ...}) { - const animated = params ? params.animated : true; -@@ -438,7 +446,7 @@ class VirtualizedList extends StateSafePureComponent { + static _createRenderMask( + props: Props, + cellsAroundViewport: {first: number, last: number}, +@@ -617,6 +670,7 @@ class VirtualizedList extends StateSafePureComponent { + _adjustCellsAroundViewport( + props: Props, + cellsAroundViewport: {first: number, last: number}, ++ pendingScrollUpdateCount: number, + ): {first: number, last: number} { + const {data, getItemCount} = props; + const onEndReachedThreshold = onEndReachedThresholdOrDefault( +@@ -648,21 +702,9 @@ class VirtualizedList extends StateSafePureComponent { + ), + }; } else { - const {onViewableItemsChanged, viewabilityConfig} = this.props; - if (onViewableItemsChanged) { -- this._viewabilityTuples.push({ -+ this.pushOrUnshift(this._viewabilityTuples, { - viewabilityHelper: new ViewabilityHelper(viewabilityConfig), - onViewableItemsChanged: onViewableItemsChanged, - }); -@@ -814,13 +822,13 @@ class VirtualizedList extends StateSafePureComponent { +- // If we have a non-zero initialScrollIndex and run this before we've scrolled, +- // we'll wipe out the initialNumToRender rendered elements starting at initialScrollIndex. +- // So let's wait until we've scrolled the view to the right place. And until then, +- // we will trust the initialScrollIndex suggestion. +- +- // Thus, we want to recalculate the windowed render limits if any of the following hold: +- // - initialScrollIndex is undefined or is 0 +- // - initialScrollIndex > 0 AND scrolling is complete +- // - initialScrollIndex > 0 AND the end of the list is visible (this handles the case +- // where the list is shorter than the visible area) +- if ( +- props.initialScrollIndex && +- !this._scrollMetrics.offset && +- Math.abs(distanceFromEnd) >= Number.EPSILON +- ) { ++ // If we have a pending scroll update, we should not adjust the render window as it ++ // might override the correct window. ++ if (pendingScrollUpdateCount > 0) { + return cellsAroundViewport.last >= getItemCount(data) + ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) + : cellsAroundViewport; +@@ -771,14 +813,59 @@ class VirtualizedList extends StateSafePureComponent { + return prevState; + } + ++ let maintainVisibleContentPositionAdjustment: ?number = null; ++ const prevFirstVisibleItemKey = prevState.firstVisibleItemKey; ++ const minIndexForVisible = ++ newProps.maintainVisibleContentPosition?.minIndexForVisible ?? 0; ++ const newFirstVisibleItemKey = ++ newProps.getItemCount(newProps.data) > minIndexForVisible ++ ? VirtualizedList._getItemKey(newProps, minIndexForVisible) ++ : null; ++ if ( ++ newProps.maintainVisibleContentPosition != null && ++ prevFirstVisibleItemKey != null && ++ newFirstVisibleItemKey != null ++ ) { ++ if (newFirstVisibleItemKey !== prevFirstVisibleItemKey) { ++ // Fast path if items were added at the start of the list. ++ const hint = ++ itemCount - prevState.renderMask.numCells() + minIndexForVisible; ++ const firstVisibleItemIndex = VirtualizedList._findItemIndexWithKey( ++ newProps, ++ prevFirstVisibleItemKey, ++ hint, ++ ); ++ maintainVisibleContentPositionAdjustment = ++ firstVisibleItemIndex != null ++ ? firstVisibleItemIndex - minIndexForVisible ++ : null; ++ } else { ++ maintainVisibleContentPositionAdjustment = null; ++ } ++ } ++ + const constrainedCells = VirtualizedList._constrainToItemCount( +- prevState.cellsAroundViewport, ++ maintainVisibleContentPositionAdjustment != null ++ ? { ++ first: ++ prevState.cellsAroundViewport.first + ++ maintainVisibleContentPositionAdjustment, ++ last: ++ prevState.cellsAroundViewport.last + ++ maintainVisibleContentPositionAdjustment, ++ } ++ : prevState.cellsAroundViewport, + newProps, + ); + + return { + cellsAroundViewport: constrainedCells, + renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells), ++ firstVisibleItemKey: newFirstVisibleItemKey, ++ pendingScrollUpdateCount: ++ maintainVisibleContentPositionAdjustment != null ++ ? prevState.pendingScrollUpdateCount + 1 ++ : prevState.pendingScrollUpdateCount, + }; + } + +@@ -810,7 +897,7 @@ class VirtualizedList extends StateSafePureComponent { + + for (let ii = first; ii <= last; ii++) { + const item = getItem(data, ii); +- const key = this._keyExtractor(item, ii, this.props); ++ const key = VirtualizedList._keyExtractor(item, ii, this.props); this._indicesToKeys.set(ii, key); if (stickyIndicesFromProps.has(ii + stickyOffset)) { -- stickyHeaderIndices.push(cells.length); -+ this.pushOrUnshift(stickyHeaderIndices, (cells.length)); - } +@@ -853,15 +940,19 @@ class VirtualizedList extends StateSafePureComponent { + props: Props, + ): {first: number, last: number} { + const itemCount = props.getItemCount(props.data); +- const last = Math.min(itemCount - 1, cells.last); ++ const lastPossibleCellIndex = itemCount - 1; - const shouldListenForLayout = - getItemLayout == null || debug || this._fillRateHelper.enabled(); ++ // Constraining `last` may significantly shrink the window. Adjust `first` ++ // to expand the window if the new `last` results in a new window smaller ++ // than the number of cells rendered per batch. + const maxToRenderPerBatch = maxToRenderPerBatchOrDefault( + props.maxToRenderPerBatch, + ); ++ const maxFirst = Math.max(0, lastPossibleCellIndex - maxToRenderPerBatch); -- cells.push( -+ this.pushOrUnshift(cells, - { - // 1. Add cell for ListHeaderComponent - if (ListHeaderComponent) { - if (stickyIndicesFromProps.has(0)) { -- stickyHeaderIndices.push(0); -+ this.pushOrUnshift(stickyHeaderIndices, 0); - } - const element = React.isValidElement(ListHeaderComponent) ? ( - ListHeaderComponent -@@ -932,7 +940,7 @@ class VirtualizedList extends StateSafePureComponent { - // $FlowFixMe[incompatible-type-arg] - - ); -- cells.push( -+ this.pushOrUnshift(cells, - { + _getSpacerKey = (isVertical: boolean): string => + isVertical ? 'height' : 'width'; + +- _keyExtractor( ++ static _keyExtractor( + item: Item, + index: number, + props: { + keyExtractor?: ?(item: Item, index: number) => string, + ... + }, +- // $FlowFixMe[missing-local-annot] +- ) { ++ ): string { + if (props.keyExtractor != null) { + return props.keyExtractor(item, index); + } +@@ -937,6 +1027,10 @@ class VirtualizedList extends StateSafePureComponent { cellKey={this._getCellKey() + '-header'} key="$header"> -@@ -963,7 +971,7 @@ class VirtualizedList extends StateSafePureComponent { - // $FlowFixMe[incompatible-type-arg] - - )): any); -- cells.push( -+ this.pushOrUnshift(cells, - -@@ -1017,7 +1025,7 @@ class VirtualizedList extends StateSafePureComponent { - const lastMetrics = this.__getFrameMetricsApprox(last, this.props); - const spacerSize = - lastMetrics.offset + lastMetrics.length - firstMetrics.offset; -- cells.push( -+ this.pushOrUnshift(cells, - { - // $FlowFixMe[incompatible-type-arg] - - ); -- cells.push( -+ this.pushOrUnshift(cells, - -@@ -1246,6 +1254,12 @@ class VirtualizedList extends StateSafePureComponent { - * LTI update could not be added via codemod */ - _defaultRenderScrollComponent = props => { - const onRefresh = props.onRefresh; -+ const inversionStyle = this.props.inverted -+ ? this.props.horizontal -+ ? styles.rowReverse -+ : styles.columnReverse -+ : null; -+ - if (this._isNestedWithSameOrientation()) { - // $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors - return ; -@@ -1273,12 +1287,24 @@ class VirtualizedList extends StateSafePureComponent { - props.refreshControl - ) - } -+ contentContainerStyle={[ -+ inversionStyle, -+ this.props.contentContainerStyle, -+ ]} - /> - ); - } else { - // $FlowFixMe[prop-missing] Invalid prop usage - // $FlowFixMe[incompatible-use] -- return ; -+ return ( -+ -+ ); - } - }; + { + style: inversionStyle + ? [inversionStyle, this.props.style] + : this.props.style, ++ maintainVisibleContentPosition: ++ this.props.maintainVisibleContentPosition != null ++ ? { ++ ...this.props.maintainVisibleContentPosition, ++ // Adjust index to account for ListHeaderComponent. ++ minIndexForVisible: ++ this.props.maintainVisibleContentPosition.minIndexForVisible + ++ (this.props.ListHeaderComponent ? 1 : 0), ++ } ++ : undefined, + }; -@@ -1432,7 +1458,7 @@ class VirtualizedList extends StateSafePureComponent { - * suppresses an error found when Flow v0.68 was deployed. To see the - * error delete this comment and run Flow. */ - if (frame.inLayout) { -- framesInLayout.push(frame); -+ this.pushOrUnshift(framesInLayout, frame); - } + this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1; +@@ -1516,8 +1620,12 @@ class VirtualizedList extends StateSafePureComponent { + onStartReachedThreshold, + onEndReached, + onEndReachedThreshold, +- initialScrollIndex, + } = this.props; ++ // If we have any pending scroll updates it means that the scroll metrics ++ // are out of date and we should not call any of the edge reached callbacks. ++ if (this.state.pendingScrollUpdateCount > 0) { ++ return; ++ } + const {contentLength, visibleLength, offset} = this._scrollMetrics; + let distanceFromStart = offset; + let distanceFromEnd = contentLength - visibleLength - offset; +@@ -1569,14 +1677,8 @@ class VirtualizedList extends StateSafePureComponent { + isWithinStartThreshold && + this._scrollMetrics.contentLength !== this._sentStartForContentLength + ) { +- // On initial mount when using initialScrollIndex the offset will be 0 initially +- // and will trigger an unexpected onStartReached. To avoid this we can use +- // timestamp to differentiate between the initial scroll metrics and when we actually +- // received the first scroll event. +- if (!initialScrollIndex || this._scrollMetrics.timestamp !== 0) { +- this._sentStartForContentLength = this._scrollMetrics.contentLength; +- onStartReached({distanceFromStart}); +- } ++ this._sentStartForContentLength = this._scrollMetrics.contentLength; ++ onStartReached({distanceFromStart}); } - const windowTop = this.__getFrameMetricsApprox( -@@ -2044,6 +2070,12 @@ const styles = StyleSheet.create({ - borderColor: 'red', - borderWidth: 2, - }, -+ rowReverse: { -+ flexDirection: 'row-reverse', -+ }, -+ columnReverse: { -+ flexDirection: 'column-reverse', -+ }, - }); - export default VirtualizedList; -\ No newline at end of file + // If the user scrolls away from the start or end and back again, +@@ -1703,6 +1805,11 @@ class VirtualizedList extends StateSafePureComponent { + visibleLength, + zoomScale, + }; ++ if (this.state.pendingScrollUpdateCount > 0) { ++ this.setState(state => ({ ++ pendingScrollUpdateCount: state.pendingScrollUpdateCount - 1, ++ })); ++ } + this._updateViewableItems(this.props, this.state.cellsAroundViewport); + if (!this.props) { + return; +@@ -1818,6 +1925,7 @@ class VirtualizedList extends StateSafePureComponent { + const cellsAroundViewport = this._adjustCellsAroundViewport( + props, + state.cellsAroundViewport, ++ state.pendingScrollUpdateCount, + ); + const renderMask = VirtualizedList._createRenderMask( + props, +@@ -1848,7 +1956,7 @@ class VirtualizedList extends StateSafePureComponent { + return { + index, + item, +- key: this._keyExtractor(item, index, props), ++ key: VirtualizedList._keyExtractor(item, index, props), + isViewable, + }; + }; +@@ -1909,13 +2017,12 @@ class VirtualizedList extends StateSafePureComponent { + inLayout?: boolean, + ... + } => { +- const {data, getItem, getItemCount, getItemLayout} = props; ++ const {data, getItemCount, getItemLayout} = props; + invariant( + index >= 0 && index < getItemCount(data), + 'Tried to get frame for out of range index ' + index, + ); +- const item = getItem(data, index); +- const frame = this._frames[this._keyExtractor(item, index, props)]; ++ const frame = this._frames[VirtualizedList._getItemKey(props, index)]; + if (!frame || frame.index !== index) { + if (getItemLayout) { + /* $FlowFixMe[prop-missing] (>=0.63.0 site=react_native_fb) This comment +@@ -1950,11 +2057,8 @@ class VirtualizedList extends StateSafePureComponent { + // where it is. + if ( + focusedCellIndex >= itemCount || +- this._keyExtractor( +- props.getItem(props.data, focusedCellIndex), +- focusedCellIndex, +- props, +- ) !== this._lastFocusedCellKey ++ VirtualizedList._getItemKey(props, focusedCellIndex) !== ++ this._lastFocusedCellKey + ) { + return []; + } +@@ -1995,6 +2099,11 @@ class VirtualizedList extends StateSafePureComponent { + props: FrameMetricProps, + cellsAroundViewport: {first: number, last: number}, + ) { ++ // If we have any pending scroll updates it means that the scroll metrics ++ // are out of date and we should not call any of the visibility callbacks. ++ if (this.state.pendingScrollUpdateCount > 0) { ++ return; ++ } + this._viewabilityTuples.forEach(tuple => { + tuple.viewabilityHelper.onUpdate( + props, diff --git a/patches/react-native-web+0.19.9+002+fix-mvcp.patch b/patches/react-native-web+0.19.9+002+fix-mvcp.patch deleted file mode 100644 index afd681bba3b0..000000000000 --- a/patches/react-native-web+0.19.9+002+fix-mvcp.patch +++ /dev/null @@ -1,687 +0,0 @@ -diff --git a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js -index a6fe142..faeb323 100644 ---- a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js -+++ b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js -@@ -293,7 +293,7 @@ class VirtualizedList extends StateSafePureComponent { - // $FlowFixMe[missing-local-annot] - - constructor(_props) { -- var _this$props$updateCel; -+ var _this$props$updateCel, _this$props$maintainV, _this$props$maintainV2; - super(_props); - this._getScrollMetrics = () => { - return this._scrollMetrics; -@@ -532,6 +532,11 @@ class VirtualizedList extends StateSafePureComponent { - visibleLength, - zoomScale - }; -+ if (this.state.pendingScrollUpdateCount > 0) { -+ this.setState(state => ({ -+ pendingScrollUpdateCount: state.pendingScrollUpdateCount - 1 -+ })); -+ } - this._updateViewableItems(this.props, this.state.cellsAroundViewport); - if (!this.props) { - return; -@@ -581,7 +586,7 @@ class VirtualizedList extends StateSafePureComponent { - this._updateCellsToRender = () => { - this._updateViewableItems(this.props, this.state.cellsAroundViewport); - this.setState((state, props) => { -- var cellsAroundViewport = this._adjustCellsAroundViewport(props, state.cellsAroundViewport); -+ var cellsAroundViewport = this._adjustCellsAroundViewport(props, state.cellsAroundViewport, state.pendingScrollUpdateCount); - var renderMask = VirtualizedList._createRenderMask(props, cellsAroundViewport, this._getNonViewportRenderRegions(props)); - if (cellsAroundViewport.first === state.cellsAroundViewport.first && cellsAroundViewport.last === state.cellsAroundViewport.last && renderMask.equals(state.renderMask)) { - return null; -@@ -601,7 +606,7 @@ class VirtualizedList extends StateSafePureComponent { - return { - index, - item, -- key: this._keyExtractor(item, index, props), -+ key: VirtualizedList._keyExtractor(item, index, props), - isViewable - }; - }; -@@ -633,12 +638,10 @@ class VirtualizedList extends StateSafePureComponent { - }; - this._getFrameMetrics = (index, props) => { - var data = props.data, -- getItem = props.getItem, - getItemCount = props.getItemCount, - getItemLayout = props.getItemLayout; - invariant(index >= 0 && index < getItemCount(data), 'Tried to get frame for out of range index ' + index); -- var item = getItem(data, index); -- var frame = this._frames[this._keyExtractor(item, index, props)]; -+ var frame = this._frames[VirtualizedList._getItemKey(props, index)]; - if (!frame || frame.index !== index) { - if (getItemLayout) { - /* $FlowFixMe[prop-missing] (>=0.63.0 site=react_native_fb) This comment -@@ -662,7 +665,7 @@ class VirtualizedList extends StateSafePureComponent { - - // The last cell we rendered may be at a new index. Bail if we don't know - // where it is. -- if (focusedCellIndex >= itemCount || this._keyExtractor(props.getItem(props.data, focusedCellIndex), focusedCellIndex, props) !== this._lastFocusedCellKey) { -+ if (focusedCellIndex >= itemCount || VirtualizedList._getItemKey(props, focusedCellIndex) !== this._lastFocusedCellKey) { - return []; - } - var first = focusedCellIndex; -@@ -702,9 +705,15 @@ class VirtualizedList extends StateSafePureComponent { - } - } - var initialRenderRegion = VirtualizedList._initialRenderRegion(_props); -+ var minIndexForVisible = (_this$props$maintainV = (_this$props$maintainV2 = this.props.maintainVisibleContentPosition) == null ? void 0 : _this$props$maintainV2.minIndexForVisible) !== null && _this$props$maintainV !== void 0 ? _this$props$maintainV : 0; - this.state = { - cellsAroundViewport: initialRenderRegion, -- renderMask: VirtualizedList._createRenderMask(_props, initialRenderRegion) -+ renderMask: VirtualizedList._createRenderMask(_props, initialRenderRegion), -+ firstVisibleItemKey: this.props.getItemCount(this.props.data) > minIndexForVisible ? VirtualizedList._getItemKey(this.props, minIndexForVisible) : null, -+ // When we have a non-zero initialScrollIndex, we will receive a -+ // scroll event later so this will prevent the window from updating -+ // until we get a valid offset. -+ pendingScrollUpdateCount: this.props.initialScrollIndex != null && this.props.initialScrollIndex > 0 ? 1 : 0 - }; - - // REACT-NATIVE-WEB patch to preserve during future RN merges: Support inverted wheel scroller. -@@ -715,7 +724,7 @@ class VirtualizedList extends StateSafePureComponent { - var clientLength = this.props.horizontal ? ev.target.clientWidth : ev.target.clientHeight; - var isEventTargetScrollable = scrollLength > clientLength; - var delta = this.props.horizontal ? ev.deltaX || ev.wheelDeltaX : ev.deltaY || ev.wheelDeltaY; -- var leftoverDelta = delta; -+ var leftoverDelta = delta * 0.5; - if (isEventTargetScrollable) { - leftoverDelta = delta < 0 ? Math.min(delta + scrollOffset, 0) : Math.max(delta - (scrollLength - clientLength - scrollOffset), 0); - } -@@ -760,6 +769,26 @@ class VirtualizedList extends StateSafePureComponent { - } - } - } -+ static _findItemIndexWithKey(props, key, hint) { -+ var itemCount = props.getItemCount(props.data); -+ if (hint != null && hint >= 0 && hint < itemCount) { -+ var curKey = VirtualizedList._getItemKey(props, hint); -+ if (curKey === key) { -+ return hint; -+ } -+ } -+ for (var ii = 0; ii < itemCount; ii++) { -+ var _curKey = VirtualizedList._getItemKey(props, ii); -+ if (_curKey === key) { -+ return ii; -+ } -+ } -+ return null; -+ } -+ static _getItemKey(props, index) { -+ var item = props.getItem(props.data, index); -+ return VirtualizedList._keyExtractor(item, index, props); -+ } - static _createRenderMask(props, cellsAroundViewport, additionalRegions) { - var itemCount = props.getItemCount(props.data); - invariant(cellsAroundViewport.first >= 0 && cellsAroundViewport.last >= cellsAroundViewport.first - 1 && cellsAroundViewport.last < itemCount, "Invalid cells around viewport \"[" + cellsAroundViewport.first + ", " + cellsAroundViewport.last + "]\" was passed to VirtualizedList._createRenderMask"); -@@ -808,7 +837,7 @@ class VirtualizedList extends StateSafePureComponent { - } - } - } -- _adjustCellsAroundViewport(props, cellsAroundViewport) { -+ _adjustCellsAroundViewport(props, cellsAroundViewport, pendingScrollUpdateCount) { - var data = props.data, - getItemCount = props.getItemCount; - var onEndReachedThreshold = onEndReachedThresholdOrDefault(props.onEndReachedThreshold); -@@ -831,17 +860,9 @@ class VirtualizedList extends StateSafePureComponent { - last: Math.min(cellsAroundViewport.last + renderAhead, getItemCount(data) - 1) - }; - } else { -- // If we have a non-zero initialScrollIndex and run this before we've scrolled, -- // we'll wipe out the initialNumToRender rendered elements starting at initialScrollIndex. -- // So let's wait until we've scrolled the view to the right place. And until then, -- // we will trust the initialScrollIndex suggestion. -- -- // Thus, we want to recalculate the windowed render limits if any of the following hold: -- // - initialScrollIndex is undefined or is 0 -- // - initialScrollIndex > 0 AND scrolling is complete -- // - initialScrollIndex > 0 AND the end of the list is visible (this handles the case -- // where the list is shorter than the visible area) -- if (props.initialScrollIndex && !this._scrollMetrics.offset && Math.abs(distanceFromEnd) >= Number.EPSILON) { -+ // If we have a pending scroll update, we should not adjust the render window as it -+ // might override the correct window. -+ if (pendingScrollUpdateCount > 0) { - return cellsAroundViewport.last >= getItemCount(data) ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) : cellsAroundViewport; - } - newCellsAroundViewport = computeWindowedRenderLimits(props, maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch), windowSizeOrDefault(props.windowSize), cellsAroundViewport, this.__getFrameMetricsApprox, this._scrollMetrics); -@@ -914,16 +935,36 @@ class VirtualizedList extends StateSafePureComponent { - } - } - static getDerivedStateFromProps(newProps, prevState) { -+ var _newProps$maintainVis, _newProps$maintainVis2; - // first and last could be stale (e.g. if a new, shorter items props is passed in), so we make - // sure we're rendering a reasonable range here. - var itemCount = newProps.getItemCount(newProps.data); - if (itemCount === prevState.renderMask.numCells()) { - return prevState; - } -- var constrainedCells = VirtualizedList._constrainToItemCount(prevState.cellsAroundViewport, newProps); -+ var maintainVisibleContentPositionAdjustment = null; -+ var prevFirstVisibleItemKey = prevState.firstVisibleItemKey; -+ var minIndexForVisible = (_newProps$maintainVis = (_newProps$maintainVis2 = newProps.maintainVisibleContentPosition) == null ? void 0 : _newProps$maintainVis2.minIndexForVisible) !== null && _newProps$maintainVis !== void 0 ? _newProps$maintainVis : 0; -+ var newFirstVisibleItemKey = newProps.getItemCount(newProps.data) > minIndexForVisible ? VirtualizedList._getItemKey(newProps, minIndexForVisible) : null; -+ if (newProps.maintainVisibleContentPosition != null && prevFirstVisibleItemKey != null && newFirstVisibleItemKey != null) { -+ if (newFirstVisibleItemKey !== prevFirstVisibleItemKey) { -+ // Fast path if items were added at the start of the list. -+ var hint = itemCount - prevState.renderMask.numCells() + minIndexForVisible; -+ var firstVisibleItemIndex = VirtualizedList._findItemIndexWithKey(newProps, prevFirstVisibleItemKey, hint); -+ maintainVisibleContentPositionAdjustment = firstVisibleItemIndex != null ? firstVisibleItemIndex - minIndexForVisible : null; -+ } else { -+ maintainVisibleContentPositionAdjustment = null; -+ } -+ } -+ var constrainedCells = VirtualizedList._constrainToItemCount(maintainVisibleContentPositionAdjustment != null ? { -+ first: prevState.cellsAroundViewport.first + maintainVisibleContentPositionAdjustment, -+ last: prevState.cellsAroundViewport.last + maintainVisibleContentPositionAdjustment -+ } : prevState.cellsAroundViewport, newProps); - return { - cellsAroundViewport: constrainedCells, -- renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells) -+ renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells), -+ firstVisibleItemKey: newFirstVisibleItemKey, -+ pendingScrollUpdateCount: maintainVisibleContentPositionAdjustment != null ? prevState.pendingScrollUpdateCount + 1 : prevState.pendingScrollUpdateCount - }; - } - _pushCells(cells, stickyHeaderIndices, stickyIndicesFromProps, first, last, inversionStyle) { -@@ -946,7 +987,7 @@ class VirtualizedList extends StateSafePureComponent { - last = Math.min(end, last); - var _loop = function _loop() { - var item = getItem(data, ii); -- var key = _this._keyExtractor(item, ii, _this.props); -+ var key = VirtualizedList._keyExtractor(item, ii, _this.props); - _this._indicesToKeys.set(ii, key); - if (stickyIndicesFromProps.has(ii + stickyOffset)) { - _this.pushOrUnshift(stickyHeaderIndices, cells.length); -@@ -981,20 +1022,23 @@ class VirtualizedList extends StateSafePureComponent { - } - static _constrainToItemCount(cells, props) { - var itemCount = props.getItemCount(props.data); -- var last = Math.min(itemCount - 1, cells.last); -+ var lastPossibleCellIndex = itemCount - 1; -+ -+ // Constraining `last` may significantly shrink the window. Adjust `first` -+ // to expand the window if the new `last` results in a new window smaller -+ // than the number of cells rendered per batch. - var maxToRenderPerBatch = maxToRenderPerBatchOrDefault(props.maxToRenderPerBatch); -+ var maxFirst = Math.max(0, lastPossibleCellIndex - maxToRenderPerBatch); - return { -- first: clamp(0, itemCount - 1 - maxToRenderPerBatch, cells.first), -- last -+ first: clamp(0, cells.first, maxFirst), -+ last: Math.min(lastPossibleCellIndex, cells.last) - }; - } - _isNestedWithSameOrientation() { - var nestedContext = this.context; - return !!(nestedContext && !!nestedContext.horizontal === horizontalOrDefault(this.props.horizontal)); - } -- _keyExtractor(item, index, props -- // $FlowFixMe[missing-local-annot] -- ) { -+ static _keyExtractor(item, index, props) { - if (props.keyExtractor != null) { - return props.keyExtractor(item, index); - } -@@ -1034,7 +1078,12 @@ class VirtualizedList extends StateSafePureComponent { - this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { - cellKey: this._getCellKey() + '-header', - key: "$header" -- }, /*#__PURE__*/React.createElement(View, { -+ }, /*#__PURE__*/React.createElement(View -+ // We expect that header component will be a single native view so make it -+ // not collapsable to avoid this view being flattened and make this assumption -+ // no longer true. -+ , { -+ collapsable: false, - onLayout: this._onLayoutHeader, - style: [inversionStyle, this.props.ListHeaderComponentStyle] - }, -@@ -1136,7 +1185,11 @@ class VirtualizedList extends StateSafePureComponent { - // TODO: Android support - invertStickyHeaders: this.props.invertStickyHeaders !== undefined ? this.props.invertStickyHeaders : this.props.inverted, - stickyHeaderIndices, -- style: inversionStyle ? [inversionStyle, this.props.style] : this.props.style -+ style: inversionStyle ? [inversionStyle, this.props.style] : this.props.style, -+ maintainVisibleContentPosition: this.props.maintainVisibleContentPosition != null ? _objectSpread(_objectSpread({}, this.props.maintainVisibleContentPosition), {}, { -+ // Adjust index to account for ListHeaderComponent. -+ minIndexForVisible: this.props.maintainVisibleContentPosition.minIndexForVisible + (this.props.ListHeaderComponent ? 1 : 0) -+ }) : undefined - }); - this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1; - var innerRet = /*#__PURE__*/React.createElement(VirtualizedListContextProvider, { -@@ -1319,8 +1372,12 @@ class VirtualizedList extends StateSafePureComponent { - onStartReached = _this$props8.onStartReached, - onStartReachedThreshold = _this$props8.onStartReachedThreshold, - onEndReached = _this$props8.onEndReached, -- onEndReachedThreshold = _this$props8.onEndReachedThreshold, -- initialScrollIndex = _this$props8.initialScrollIndex; -+ onEndReachedThreshold = _this$props8.onEndReachedThreshold; -+ // If we have any pending scroll updates it means that the scroll metrics -+ // are out of date and we should not call any of the edge reached callbacks. -+ if (this.state.pendingScrollUpdateCount > 0) { -+ return; -+ } - var _this$_scrollMetrics2 = this._scrollMetrics, - contentLength = _this$_scrollMetrics2.contentLength, - visibleLength = _this$_scrollMetrics2.visibleLength, -@@ -1360,16 +1417,10 @@ class VirtualizedList extends StateSafePureComponent { - // and call onStartReached only once for a given content length, - // and only if onEndReached is not being executed - else if (onStartReached != null && this.state.cellsAroundViewport.first === 0 && isWithinStartThreshold && this._scrollMetrics.contentLength !== this._sentStartForContentLength) { -- // On initial mount when using initialScrollIndex the offset will be 0 initially -- // and will trigger an unexpected onStartReached. To avoid this we can use -- // timestamp to differentiate between the initial scroll metrics and when we actually -- // received the first scroll event. -- if (!initialScrollIndex || this._scrollMetrics.timestamp !== 0) { -- this._sentStartForContentLength = this._scrollMetrics.contentLength; -- onStartReached({ -- distanceFromStart -- }); -- } -+ this._sentStartForContentLength = this._scrollMetrics.contentLength; -+ onStartReached({ -+ distanceFromStart -+ }); - } - - // If the user scrolls away from the start or end and back again, -@@ -1424,6 +1475,11 @@ class VirtualizedList extends StateSafePureComponent { - } - } - _updateViewableItems(props, cellsAroundViewport) { -+ // If we have any pending scroll updates it means that the scroll metrics -+ // are out of date and we should not call any of the visibility callbacks. -+ if (this.state.pendingScrollUpdateCount > 0) { -+ return; -+ } - this._viewabilityTuples.forEach(tuple => { - tuple.viewabilityHelper.onUpdate(props, this._scrollMetrics.offset, this._scrollMetrics.visibleLength, this._getFrameMetrics, this._createViewToken, tuple.onViewableItemsChanged, cellsAroundViewport); - }); -diff --git a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js -index d896fb1..f303b31 100644 ---- a/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js -+++ b/node_modules/react-native-web/src/vendor/react-native/VirtualizedList/index.js -@@ -75,6 +75,10 @@ type ViewabilityHelperCallbackTuple = { - type State = { - renderMask: CellRenderMask, - cellsAroundViewport: {first: number, last: number}, -+ // Used to track items added at the start of the list for maintainVisibleContentPosition. -+ firstVisibleItemKey: ?string, -+ // When > 0 the scroll position available in JS is considered stale and should not be used. -+ pendingScrollUpdateCount: number, - }; - - /** -@@ -455,9 +459,24 @@ class VirtualizedList extends StateSafePureComponent { - - const initialRenderRegion = VirtualizedList._initialRenderRegion(props); - -+ const minIndexForVisible = -+ this.props.maintainVisibleContentPosition?.minIndexForVisible ?? 0; -+ - this.state = { - cellsAroundViewport: initialRenderRegion, - renderMask: VirtualizedList._createRenderMask(props, initialRenderRegion), -+ firstVisibleItemKey: -+ this.props.getItemCount(this.props.data) > minIndexForVisible -+ ? VirtualizedList._getItemKey(this.props, minIndexForVisible) -+ : null, -+ // When we have a non-zero initialScrollIndex, we will receive a -+ // scroll event later so this will prevent the window from updating -+ // until we get a valid offset. -+ pendingScrollUpdateCount: -+ this.props.initialScrollIndex != null && -+ this.props.initialScrollIndex > 0 -+ ? 1 -+ : 0, - }; - - // REACT-NATIVE-WEB patch to preserve during future RN merges: Support inverted wheel scroller. -@@ -470,7 +489,7 @@ class VirtualizedList extends StateSafePureComponent { - const delta = this.props.horizontal - ? ev.deltaX || ev.wheelDeltaX - : ev.deltaY || ev.wheelDeltaY; -- let leftoverDelta = delta; -+ let leftoverDelta = delta * 5; - if (isEventTargetScrollable) { - leftoverDelta = delta < 0 - ? Math.min(delta + scrollOffset, 0) -@@ -542,6 +561,40 @@ class VirtualizedList extends StateSafePureComponent { - } - } - -+ static _findItemIndexWithKey( -+ props: Props, -+ key: string, -+ hint: ?number, -+ ): ?number { -+ const itemCount = props.getItemCount(props.data); -+ if (hint != null && hint >= 0 && hint < itemCount) { -+ const curKey = VirtualizedList._getItemKey(props, hint); -+ if (curKey === key) { -+ return hint; -+ } -+ } -+ for (let ii = 0; ii < itemCount; ii++) { -+ const curKey = VirtualizedList._getItemKey(props, ii); -+ if (curKey === key) { -+ return ii; -+ } -+ } -+ return null; -+ } -+ -+ static _getItemKey( -+ props: { -+ data: Props['data'], -+ getItem: Props['getItem'], -+ keyExtractor: Props['keyExtractor'], -+ ... -+ }, -+ index: number, -+ ): string { -+ const item = props.getItem(props.data, index); -+ return VirtualizedList._keyExtractor(item, index, props); -+ } -+ - static _createRenderMask( - props: Props, - cellsAroundViewport: {first: number, last: number}, -@@ -625,6 +678,7 @@ class VirtualizedList extends StateSafePureComponent { - _adjustCellsAroundViewport( - props: Props, - cellsAroundViewport: {first: number, last: number}, -+ pendingScrollUpdateCount: number, - ): {first: number, last: number} { - const {data, getItemCount} = props; - const onEndReachedThreshold = onEndReachedThresholdOrDefault( -@@ -656,21 +710,9 @@ class VirtualizedList extends StateSafePureComponent { - ), - }; - } else { -- // If we have a non-zero initialScrollIndex and run this before we've scrolled, -- // we'll wipe out the initialNumToRender rendered elements starting at initialScrollIndex. -- // So let's wait until we've scrolled the view to the right place. And until then, -- // we will trust the initialScrollIndex suggestion. -- -- // Thus, we want to recalculate the windowed render limits if any of the following hold: -- // - initialScrollIndex is undefined or is 0 -- // - initialScrollIndex > 0 AND scrolling is complete -- // - initialScrollIndex > 0 AND the end of the list is visible (this handles the case -- // where the list is shorter than the visible area) -- if ( -- props.initialScrollIndex && -- !this._scrollMetrics.offset && -- Math.abs(distanceFromEnd) >= Number.EPSILON -- ) { -+ // If we have a pending scroll update, we should not adjust the render window as it -+ // might override the correct window. -+ if (pendingScrollUpdateCount > 0) { - return cellsAroundViewport.last >= getItemCount(data) - ? VirtualizedList._constrainToItemCount(cellsAroundViewport, props) - : cellsAroundViewport; -@@ -779,14 +821,59 @@ class VirtualizedList extends StateSafePureComponent { - return prevState; - } - -+ let maintainVisibleContentPositionAdjustment: ?number = null; -+ const prevFirstVisibleItemKey = prevState.firstVisibleItemKey; -+ const minIndexForVisible = -+ newProps.maintainVisibleContentPosition?.minIndexForVisible ?? 0; -+ const newFirstVisibleItemKey = -+ newProps.getItemCount(newProps.data) > minIndexForVisible -+ ? VirtualizedList._getItemKey(newProps, minIndexForVisible) -+ : null; -+ if ( -+ newProps.maintainVisibleContentPosition != null && -+ prevFirstVisibleItemKey != null && -+ newFirstVisibleItemKey != null -+ ) { -+ if (newFirstVisibleItemKey !== prevFirstVisibleItemKey) { -+ // Fast path if items were added at the start of the list. -+ const hint = -+ itemCount - prevState.renderMask.numCells() + minIndexForVisible; -+ const firstVisibleItemIndex = VirtualizedList._findItemIndexWithKey( -+ newProps, -+ prevFirstVisibleItemKey, -+ hint, -+ ); -+ maintainVisibleContentPositionAdjustment = -+ firstVisibleItemIndex != null -+ ? firstVisibleItemIndex - minIndexForVisible -+ : null; -+ } else { -+ maintainVisibleContentPositionAdjustment = null; -+ } -+ } -+ - const constrainedCells = VirtualizedList._constrainToItemCount( -- prevState.cellsAroundViewport, -+ maintainVisibleContentPositionAdjustment != null -+ ? { -+ first: -+ prevState.cellsAroundViewport.first + -+ maintainVisibleContentPositionAdjustment, -+ last: -+ prevState.cellsAroundViewport.last + -+ maintainVisibleContentPositionAdjustment, -+ } -+ : prevState.cellsAroundViewport, - newProps, - ); - - return { - cellsAroundViewport: constrainedCells, - renderMask: VirtualizedList._createRenderMask(newProps, constrainedCells), -+ firstVisibleItemKey: newFirstVisibleItemKey, -+ pendingScrollUpdateCount: -+ maintainVisibleContentPositionAdjustment != null -+ ? prevState.pendingScrollUpdateCount + 1 -+ : prevState.pendingScrollUpdateCount, - }; - } - -@@ -818,11 +905,11 @@ class VirtualizedList extends StateSafePureComponent { - - for (let ii = first; ii <= last; ii++) { - const item = getItem(data, ii); -- const key = this._keyExtractor(item, ii, this.props); -+ const key = VirtualizedList._keyExtractor(item, ii, this.props); - - this._indicesToKeys.set(ii, key); - if (stickyIndicesFromProps.has(ii + stickyOffset)) { -- this.pushOrUnshift(stickyHeaderIndices, (cells.length)); -+ this.pushOrUnshift(stickyHeaderIndices, cells.length); - } - - const shouldListenForLayout = -@@ -861,15 +948,19 @@ class VirtualizedList extends StateSafePureComponent { - props: Props, - ): {first: number, last: number} { - const itemCount = props.getItemCount(props.data); -- const last = Math.min(itemCount - 1, cells.last); -+ const lastPossibleCellIndex = itemCount - 1; - -+ // Constraining `last` may significantly shrink the window. Adjust `first` -+ // to expand the window if the new `last` results in a new window smaller -+ // than the number of cells rendered per batch. - const maxToRenderPerBatch = maxToRenderPerBatchOrDefault( - props.maxToRenderPerBatch, - ); -+ const maxFirst = Math.max(0, lastPossibleCellIndex - maxToRenderPerBatch); - - return { -- first: clamp(0, itemCount - 1 - maxToRenderPerBatch, cells.first), -- last, -+ first: clamp(0, cells.first, maxFirst), -+ last: Math.min(lastPossibleCellIndex, cells.last), - }; - } - -@@ -891,15 +982,14 @@ class VirtualizedList extends StateSafePureComponent { - _getSpacerKey = (isVertical: boolean): string => - isVertical ? 'height' : 'width'; - -- _keyExtractor( -+ static _keyExtractor( - item: Item, - index: number, - props: { - keyExtractor?: ?(item: Item, index: number) => string, - ... - }, -- // $FlowFixMe[missing-local-annot] -- ) { -+ ): string { - if (props.keyExtractor != null) { - return props.keyExtractor(item, index); - } -@@ -945,6 +1035,10 @@ class VirtualizedList extends StateSafePureComponent { - cellKey={this._getCellKey() + '-header'} - key="$header"> - { - style: inversionStyle - ? [inversionStyle, this.props.style] - : this.props.style, -+ maintainVisibleContentPosition: -+ this.props.maintainVisibleContentPosition != null -+ ? { -+ ...this.props.maintainVisibleContentPosition, -+ // Adjust index to account for ListHeaderComponent. -+ minIndexForVisible: -+ this.props.maintainVisibleContentPosition.minIndexForVisible + -+ (this.props.ListHeaderComponent ? 1 : 0), -+ } -+ : undefined, - }; - - this._hasMore = this.state.cellsAroundViewport.last < itemCount - 1; -@@ -1255,11 +1359,10 @@ class VirtualizedList extends StateSafePureComponent { - _defaultRenderScrollComponent = props => { - const onRefresh = props.onRefresh; - const inversionStyle = this.props.inverted -- ? this.props.horizontal -- ? styles.rowReverse -- : styles.columnReverse -- : null; -- -+ ? this.props.horizontal -+ ? styles.rowReverse -+ : styles.columnReverse -+ : null; - if (this._isNestedWithSameOrientation()) { - // $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors - return ; -@@ -1542,8 +1645,12 @@ class VirtualizedList extends StateSafePureComponent { - onStartReachedThreshold, - onEndReached, - onEndReachedThreshold, -- initialScrollIndex, - } = this.props; -+ // If we have any pending scroll updates it means that the scroll metrics -+ // are out of date and we should not call any of the edge reached callbacks. -+ if (this.state.pendingScrollUpdateCount > 0) { -+ return; -+ } - const {contentLength, visibleLength, offset} = this._scrollMetrics; - let distanceFromStart = offset; - let distanceFromEnd = contentLength - visibleLength - offset; -@@ -1595,14 +1702,8 @@ class VirtualizedList extends StateSafePureComponent { - isWithinStartThreshold && - this._scrollMetrics.contentLength !== this._sentStartForContentLength - ) { -- // On initial mount when using initialScrollIndex the offset will be 0 initially -- // and will trigger an unexpected onStartReached. To avoid this we can use -- // timestamp to differentiate between the initial scroll metrics and when we actually -- // received the first scroll event. -- if (!initialScrollIndex || this._scrollMetrics.timestamp !== 0) { -- this._sentStartForContentLength = this._scrollMetrics.contentLength; -- onStartReached({distanceFromStart}); -- } -+ this._sentStartForContentLength = this._scrollMetrics.contentLength; -+ onStartReached({distanceFromStart}); - } - - // If the user scrolls away from the start or end and back again, -@@ -1729,6 +1830,11 @@ class VirtualizedList extends StateSafePureComponent { - visibleLength, - zoomScale, - }; -+ if (this.state.pendingScrollUpdateCount > 0) { -+ this.setState(state => ({ -+ pendingScrollUpdateCount: state.pendingScrollUpdateCount - 1, -+ })); -+ } - this._updateViewableItems(this.props, this.state.cellsAroundViewport); - if (!this.props) { - return; -@@ -1844,6 +1950,7 @@ class VirtualizedList extends StateSafePureComponent { - const cellsAroundViewport = this._adjustCellsAroundViewport( - props, - state.cellsAroundViewport, -+ state.pendingScrollUpdateCount, - ); - const renderMask = VirtualizedList._createRenderMask( - props, -@@ -1874,7 +1981,7 @@ class VirtualizedList extends StateSafePureComponent { - return { - index, - item, -- key: this._keyExtractor(item, index, props), -+ key: VirtualizedList._keyExtractor(item, index, props), - isViewable, - }; - }; -@@ -1935,13 +2042,12 @@ class VirtualizedList extends StateSafePureComponent { - inLayout?: boolean, - ... - } => { -- const {data, getItem, getItemCount, getItemLayout} = props; -+ const {data, getItemCount, getItemLayout} = props; - invariant( - index >= 0 && index < getItemCount(data), - 'Tried to get frame for out of range index ' + index, - ); -- const item = getItem(data, index); -- const frame = this._frames[this._keyExtractor(item, index, props)]; -+ const frame = this._frames[VirtualizedList._getItemKey(props, index)]; - if (!frame || frame.index !== index) { - if (getItemLayout) { - /* $FlowFixMe[prop-missing] (>=0.63.0 site=react_native_fb) This comment -@@ -1976,11 +2082,8 @@ class VirtualizedList extends StateSafePureComponent { - // where it is. - if ( - focusedCellIndex >= itemCount || -- this._keyExtractor( -- props.getItem(props.data, focusedCellIndex), -- focusedCellIndex, -- props, -- ) !== this._lastFocusedCellKey -+ VirtualizedList._getItemKey(props, focusedCellIndex) !== -+ this._lastFocusedCellKey - ) { - return []; - } -@@ -2021,6 +2124,11 @@ class VirtualizedList extends StateSafePureComponent { - props: FrameMetricProps, - cellsAroundViewport: {first: number, last: number}, - ) { -+ // If we have any pending scroll updates it means that the scroll metrics -+ // are out of date and we should not call any of the visibility callbacks. -+ if (this.state.pendingScrollUpdateCount > 0) { -+ return; -+ } - this._viewabilityTuples.forEach(tuple => { - tuple.viewabilityHelper.onUpdate( - props, diff --git a/patches/react-native-web+0.19.9+003+measureInWindow.patch b/patches/react-native-web+0.19.9+002+measureInWindow.patch similarity index 100% rename from patches/react-native-web+0.19.9+003+measureInWindow.patch rename to patches/react-native-web+0.19.9+002+measureInWindow.patch diff --git a/patches/react-native-web+0.19.9+004+fix-pointer-events.patch b/patches/react-native-web+0.19.9+003+fix-pointer-events.patch similarity index 100% rename from patches/react-native-web+0.19.9+004+fix-pointer-events.patch rename to patches/react-native-web+0.19.9+003+fix-pointer-events.patch diff --git a/patches/react-native-web+0.19.9+005+fixLastSpacer.patch b/patches/react-native-web+0.19.9+005+fixLastSpacer.patch deleted file mode 100644 index 0ca5ac778e0b..000000000000 --- a/patches/react-native-web+0.19.9+005+fixLastSpacer.patch +++ /dev/null @@ -1,29 +0,0 @@ -diff --git a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js -index faeb323..68d740a 100644 ---- a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js -+++ b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js -@@ -78,14 +78,6 @@ function scrollEventThrottleOrDefault(scrollEventThrottle) { - function windowSizeOrDefault(windowSize) { - return windowSize !== null && windowSize !== void 0 ? windowSize : 21; - } --function findLastWhere(arr, predicate) { -- for (var i = arr.length - 1; i >= 0; i--) { -- if (predicate(arr[i])) { -- return arr[i]; -- } -- } -- return null; --} - - /** - * Base implementation for the more convenient [``](https://reactnative.dev/docs/flatlist) -@@ -1119,7 +1111,8 @@ class VirtualizedList extends StateSafePureComponent { - _keylessItemComponentName = ''; - var spacerKey = this._getSpacerKey(!horizontal); - var renderRegions = this.state.renderMask.enumerateRegions(); -- var lastSpacer = findLastWhere(renderRegions, r => r.isSpacer); -+ var lastRegion = renderRegions[renderRegions.length - 1]; -+ var lastSpacer = lastRegion?.isSpacer ? lastRegion : null; - for (var _iterator = _createForOfIteratorHelperLoose(renderRegions), _step; !(_step = _iterator()).done;) { - var section = _step.value; - if (section.isSpacer) { \ No newline at end of file From 89227b2efe5b52bc559860b5182de6f913151f00 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Sat, 30 Dec 2023 15:35:08 +0100 Subject: [PATCH 037/245] fix Safari --- src/pages/home/report/ReportActionsView.js | 23 ++++++++++------------ 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 1a28b222c4e9..383970a670d2 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -17,6 +17,7 @@ import useInitialValue from '@hooks/useInitialValue'; import usePrevious from '@hooks/usePrevious'; import useReportScrollManager from '@hooks/useReportScrollManager'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import * as Browser from '@libs/Browser'; import compose from '@libs/compose'; import getIsReportFullyVisible from '@libs/getIsReportFullyVisible'; import Performance from '@libs/Performance'; @@ -98,7 +99,10 @@ function getReportActionID(route) { return {reportActionID: lodashGet(route, 'params.reportActionID', null), reportID: lodashGet(route, 'params.reportID', null)}; } -const useHandleList = (linkedID, messageArray, fetchFn, route, isLoadingLinkedMessage) => { +// NOTE: The current delay is a temporary workaround due to a limitation in React Native Web. This will be removed once a forthcoming patch to React Native Web is applied. +const TIMEOUT = Browser.isSafari() && Browser.isMobileSafari ? 1100 : 70; + +const useHandleList = (linkedID, messageArray, fetchFn, route) => { const [edgeID, setEdgeID] = useState(linkedID); const [listID, setListID] = useState(() => Math.round(Math.random() * 100)); const isFirstRender = useRef(true); @@ -116,7 +120,6 @@ const useHandleList = (linkedID, messageArray, fetchFn, route, isLoadingLinkedMe setEdgeID(''); }, [route, linkedID]); - const cattedArray = useMemo(() => { if (!linkedID || index === -1) { return messageArray; @@ -130,7 +133,7 @@ const useHandleList = (linkedID, messageArray, fetchFn, route, isLoadingLinkedMe return messageArray.slice(newStartIndex, messageArray.length); } return messageArray; - }, [linkedID, messageArray, edgeID, index, isLoadingLinkedMessage]); + }, [linkedID, messageArray, edgeID, index]); const hasMoreCashed = cattedArray.length < messageArray.length; @@ -143,10 +146,10 @@ const useHandleList = (linkedID, messageArray, fetchFn, route, isLoadingLinkedMe } if (isFirstRender.current) { - isFirstRender.current = false; - InteractionManager.runAfterInteractions(() => { + setTimeout(() => { + isFirstRender.current = false; setEdgeID(firstReportActionID); - }); + }, TIMEOUT); } else { setEdgeID(firstReportActionID); } @@ -172,7 +175,6 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro const didSubscribeToReportTypingEvents = useRef(false); const contentListHeight = useRef(0); const layoutListHeight = useRef(0); - const isInitial = useRef(true); const hasCachedActions = useInitialValue(() => _.size(props.reportActions) > 0); const mostRecentIOUReportActionID = useInitialValue(() => ReportActionsUtils.getMostRecentIOURequestActionID(props.reportActions)); const {windowHeight} = useWindowDimensions(); @@ -327,14 +329,9 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro const DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST = 164; const SPACER = 30; const isContentSmallerThanList = windowHeight - DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST - SPACER > contentListHeight.current; - - if ( - (reportActionID && linkedIdIndex > -1 && !hasNewestReportAction && !isInitial.current && !isContentSmallerThanList) || - (!reportActionID && !hasNewestReportAction && !isContentSmallerThanList) - ) { + if ((reportActionID && linkedIdIndex > -1 && !hasNewestReportAction && !isContentSmallerThanList) || (!reportActionID && !hasNewestReportAction && !isContentSmallerThanList)) { fetchFunc({firstReportActionID, distanceFromStart}); } - isInitial.current = false; }, [hasNewestReportAction, linkedIdIndex, firstReportActionID, fetchFunc, reportActionID, windowHeight], ); From ac7c0c4133fb31aefd789d767472d4fe813ee86a Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 3 Jan 2024 15:25:04 +0100 Subject: [PATCH 038/245] bump WINDOW_SIZE --- src/components/InvertedFlatList/BaseInvertedFlatList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx index 783e88266803..45d5a996dead 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx @@ -2,7 +2,7 @@ import React, {ForwardedRef, forwardRef} from 'react'; import {FlatListProps} from 'react-native'; import FlatList from '@components/FlatList'; -const WINDOW_SIZE = 15; +const WINDOW_SIZE = 21; function BaseInvertedFlatList(props: FlatListProps, ref: ForwardedRef) { return ( From 6fb63e469b17234a41d6c3a269475dc3b3f36b9b Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 3 Jan 2024 15:29:55 +0100 Subject: [PATCH 039/245] fix outdated loader data before navigating --- src/pages/home/ReportScreen.js | 38 ++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 219b42d8d06f..e8c399e52903 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -167,17 +167,21 @@ function ReportScreen({ const {translate} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); const {isOffline} = useNetwork(); + const {reportActionID, reportID} = getReportActionID(route); - const firstRenderRef = useRef(true); const flatListRef = useRef(); const reactionListRef = useRef(); + const firstRenderRef = useRef(true); const prevReport = usePrevious(report); + const firstRenderLinkingLoaderRef = useRef(!!reportActionID); + const [firstRenderLinkingLoader, setFirstRenderLinkingLoader] = useState(!!reportActionID); const prevUserLeavingStatus = usePrevious(userLeavingStatus); - const {reportActionID, reportID} = getReportActionID(route); const [isLinkingToMessage, setLinkingToMessageTrigger] = useState(false); const reportActions = useMemo(() => { - if (allReportActions?.length === 0) return []; + if (!!allReportActions && allReportActions.length === 0) { + return []; + } const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions); const cattedRangeOfReportActions = ReportActionsUtils.getRangeFromArrayByID(sortedReportActions, reportActionID); const reportActionsWithoutDeleted = ReportActionsUtils.getReportActionsWithoutRemoved(cattedRangeOfReportActions); @@ -450,6 +454,32 @@ function ReportScreen({ const actionListValue = useMemo(() => ({flatListRef, scrollPosition, setScrollPosition}), [flatListRef, scrollPosition, setScrollPosition]); + // Use `useMemo` to prevent displaying stale information. The `useMemo` hook is preferred over `useEffect` here because it runs during the render phase, thus avoiding a flash of outdated content which could occur if state updates were scheduled asynchronously. + // + // This `useMemo` handles the state just after initial report actions have been loaded. It ensures that the loader state is set correctly during the initial rendering phase when linking to a report. + useMemo(() => { + if (reportMetadata.isLoadingInitialReportActions) { + return; + } + requestAnimationFrame(() => { + firstRenderLinkingLoaderRef.current = true; + setFirstRenderLinkingLoader(true); + }); + }, [route, setFirstRenderLinkingLoader]); + // This `useMemo` updates the loader state after the initial rendering phase is complete and the report actions are no longer loading, ensuring the loader is hidden at the correct time. + useMemo(() => { + if (!firstRenderLinkingLoaderRef || !firstRenderLinkingLoaderRef.current || reportMetadata.isLoadingInitialReportActions) { + return; + } + requestAnimationFrame(() => { + firstRenderLinkingLoaderRef.current = false; + setFirstRenderLinkingLoader(false); + }); + }, [reportMetadata.isLoadingInitialReportActions, setFirstRenderLinkingLoader]); + const shouldShowSkeleton = useMemo( + () => firstRenderLinkingLoader || !isReportReadyForDisplay || isLoadingInitialReportActions || isLoading || (!!reportActionID && reportMetadata.isLoadingInitialReportActions), + [isReportReadyForDisplay, isLoadingInitialReportActions, isLoading, reportActionID, reportMetadata.isLoadingInitialReportActions, firstRenderLinkingLoader], + ); return ( @@ -518,7 +548,7 @@ function ReportScreen({ {/* Note: The ReportActionsSkeletonView should be allowed to mount even if the initial report actions are not loaded. If we prevent rendering the report while they are loading then we'll unnecessarily unmount the ReportActionsView which will clear the new marker lines initial state. */} - {(!isReportReadyForDisplay || isLoadingInitialReportActions || isLoading) && } + {shouldShowSkeleton && } {isReportReadyForDisplay ? ( Date: Wed, 3 Jan 2024 15:31:13 +0100 Subject: [PATCH 040/245] refactor initialNumToRender --- src/pages/home/report/ReportActionsList.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 16f2d95b23b0..9c1e592f6f2a 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -12,7 +12,6 @@ import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withW import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; import compose from '@libs/compose'; import DateUtils from '@libs/DateUtils'; import getPlatform from '@libs/getPlatform'; @@ -493,8 +492,7 @@ function ReportActionsList({ renderItem={renderItem} contentContainerStyle={contentContainerStyle} keyExtractor={keyExtractor} - // initialNumToRender={initialNumToRender} - initialNumToRender={50} + initialNumToRender={initialNumToRender} onEndReached={loadOlderChats} onEndReachedThreshold={0.75} onStartReached={loadNewerChats} From 17a6b90e9fa852eb08a8203da587f0f9affd3301 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 3 Jan 2024 16:18:10 +0100 Subject: [PATCH 041/245] add debounce for fetching newer actions --- src/pages/home/report/ReportActionsView.js | 40 ++++++++++++++-------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 383970a670d2..b9920724e582 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -99,8 +99,8 @@ function getReportActionID(route) { return {reportActionID: lodashGet(route, 'params.reportActionID', null), reportID: lodashGet(route, 'params.reportID', null)}; } -// NOTE: The current delay is a temporary workaround due to a limitation in React Native Web. This will be removed once a forthcoming patch to React Native Web is applied. -const TIMEOUT = Browser.isSafari() && Browser.isMobileSafari ? 1100 : 70; +// Set a longer timeout for Safari on mobile due to FlatList issues. +const TIMEOUT = Browser.isSafari() || Browser.isMobileSafari ? 200 : 100; const useHandleList = (linkedID, messageArray, fetchFn, route) => { const [edgeID, setEdgeID] = useState(linkedID); @@ -116,8 +116,11 @@ const useHandleList = (linkedID, messageArray, fetchFn, route) => { }, [messageArray, linkedID, edgeID]); useMemo(() => { - isFirstRender.current = true; - setEdgeID(''); + // Clear edgeID before navigating to a linked message + requestAnimationFrame(() => { + isFirstRender.current = true; + setEdgeID(''); + }); }, [route, linkedID]); const cattedArray = useMemo(() => { @@ -125,33 +128,42 @@ const useHandleList = (linkedID, messageArray, fetchFn, route) => { return messageArray; } if (isFirstRender.current) { + // On first render, position the view at the linked message setListID((i) => i + 1); return messageArray.slice(index, messageArray.length); } else if (edgeID) { - const amountOfItemsBeforeLinkedOne = 10; + // On subsequent renders, load additional messages + const amountOfItemsBeforeLinkedOne = 20; const newStartIndex = index >= amountOfItemsBeforeLinkedOne ? index - amountOfItemsBeforeLinkedOne : 0; - return messageArray.slice(newStartIndex, messageArray.length); + return newStartIndex ? messageArray.slice(newStartIndex, messageArray.length) : messageArray; } return messageArray; }, [linkedID, messageArray, edgeID, index]); const hasMoreCashed = cattedArray.length < messageArray.length; + const debouncedSetEdgeID = _.throttle((firstReportActionID) => { + setEdgeID(firstReportActionID); + }, 200); + const paginate = useCallback( - ({firstReportActionID, distanceFromStart}) => { + ({firstReportActionID}) => { // This function is a placeholder as the actual pagination is handled by cattedArray // It's here if you need to trigger any side effects during pagination if (!hasMoreCashed) { - fetchFn({distanceFromStart}); + // Fetch new messages if all current messages have been shown + fetchFn(); + setEdgeID(firstReportActionID); + return; } - if (isFirstRender.current) { + isFirstRender.current = false; + // Delay to ensure the linked message is displayed correctly. setTimeout(() => { - isFirstRender.current = false; setEdgeID(firstReportActionID); }, TIMEOUT); } else { - setEdgeID(firstReportActionID); + debouncedSetEdgeID(firstReportActionID); } }, [setEdgeID, fetchFn, hasMoreCashed], @@ -209,7 +221,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro const hasNewestReportAction = lodashGet(reportActions[0], 'created') === props.report.lastVisibleActionCreated; const newestReportAction = lodashGet(reportActions, '[0]'); const oldestReportAction = _.last(reportActions); - const isWeReachedTheOldestAction = oldestReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED; + const isWeReachedTheOldestAction = lodashGet(oldestReportAction, 'actionName') === CONST.REPORT.ACTIONS.TYPE.CREATED; /** * @returns {Boolean} @@ -325,12 +337,12 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro const firstReportActionID = useMemo(() => reportActions[0]?.reportActionID, [reportActions]); const handleLoadNewerChats = useCallback( // eslint-disable-next-line rulesdir/prefer-early-return - ({distanceFromStart}) => { + () => { const DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST = 164; const SPACER = 30; const isContentSmallerThanList = windowHeight - DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST - SPACER > contentListHeight.current; if ((reportActionID && linkedIdIndex > -1 && !hasNewestReportAction && !isContentSmallerThanList) || (!reportActionID && !hasNewestReportAction && !isContentSmallerThanList)) { - fetchFunc({firstReportActionID, distanceFromStart}); + fetchFunc({firstReportActionID}); } }, [hasNewestReportAction, linkedIdIndex, firstReportActionID, fetchFunc, reportActionID, windowHeight], From b9e739a060cc927f884933d01924d3e827493ecb Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 3 Jan 2024 18:18:51 +0100 Subject: [PATCH 042/245] add 'scroll to the bottom' --- src/pages/home/report/ReportActionsList.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 9c1e592f6f2a..867f767448c2 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -15,6 +15,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import DateUtils from '@libs/DateUtils'; import getPlatform from '@libs/getPlatform'; +import Navigation from '@libs/Navigation/Navigation'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import Visibility from '@libs/Visibility'; @@ -22,6 +23,7 @@ import reportPropTypes from '@pages/reportPropTypes'; import variables from '@styles/variables'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; import FloatingMessageCounter from './FloatingMessageCounter'; import ListBoundaryLoader from './ListBoundaryLoader/ListBoundaryLoader'; import reportActionPropTypes from './reportActionPropTypes'; @@ -167,6 +169,7 @@ function ReportActionsList({ const sortedVisibleReportActions = _.filter(sortedReportActions, (s) => isOffline || s.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || s.errors); const lastActionIndex = lodashGet(sortedVisibleReportActions, [0, 'reportActionID']); const reportActionSize = useRef(sortedVisibleReportActions.length); + const hasNewestReportAction = lodashGet(sortedReportActions[0], 'created') === report.lastVisibleActionCreated; const previousLastIndex = useRef(lastActionIndex); @@ -319,6 +322,11 @@ function ReportActionsList({ }; const scrollToBottomAndMarkReportAsRead = () => { + if (!hasNewestReportAction) { + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.reportID)); + Report.openReport({reportID: report.reportID}); + return; + } reportScrollManager.scrollToBottom(); readActionSkipped.current = false; Report.readNewestAction(report.reportID); @@ -457,7 +465,7 @@ function ReportActionsList({ ); const onContentSizeChangeInner = useCallback( (w, h) => { - onContentSizeChange(w,h) + onContentSizeChange(w, h); }, [onContentSizeChange], ); @@ -479,7 +487,7 @@ function ReportActionsList({ return ( <> From 2f7da440a805fef9cbf96f770d0c279c9d23af56 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 5 Jan 2024 15:17:11 +0100 Subject: [PATCH 043/245] refactor useMemo calculations --- src/components/FlatList/MVCPFlatList.js | 2 +- src/pages/home/ReportScreen.js | 30 +++++++++---------------- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/src/components/FlatList/MVCPFlatList.js b/src/components/FlatList/MVCPFlatList.js index 0abb1dc4a873..44cb50b98e11 100644 --- a/src/components/FlatList/MVCPFlatList.js +++ b/src/components/FlatList/MVCPFlatList.js @@ -46,7 +46,7 @@ const MVCPFlatList = React.forwardRef(({maintainVisibleContentPosition, horizont return horizontal ? scrollRef.current.getScrollableNode().scrollLeft : scrollRef.current.getScrollableNode().scrollTop; }, [horizontal]); - const getContentView = React.useCallback(() => scrollRef.current?.getScrollableNode().childNodes[0], []); + const getContentView = React.useCallback(() => scrollRef.current?.getScrollableNode()?.childNodes[0], []); const scrollToOffset = React.useCallback( (offset, animated) => { diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index e8c399e52903..edcfa724c1d8 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -1,8 +1,8 @@ import {useIsFocused} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {View} from 'react-native'; +import React, {useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; +import {InteractionManager, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import Banner from '@components/Banner'; @@ -173,7 +173,6 @@ function ReportScreen({ const reactionListRef = useRef(); const firstRenderRef = useRef(true); const prevReport = usePrevious(report); - const firstRenderLinkingLoaderRef = useRef(!!reportActionID); const [firstRenderLinkingLoader, setFirstRenderLinkingLoader] = useState(!!reportActionID); const prevUserLeavingStatus = usePrevious(userLeavingStatus); const [isLinkingToMessage, setLinkingToMessageTrigger] = useState(false); @@ -454,31 +453,24 @@ function ReportScreen({ const actionListValue = useMemo(() => ({flatListRef, scrollPosition, setScrollPosition}), [flatListRef, scrollPosition, setScrollPosition]); - // Use `useMemo` to prevent displaying stale information. The `useMemo` hook is preferred over `useEffect` here because it runs during the render phase, thus avoiding a flash of outdated content which could occur if state updates were scheduled asynchronously. - // - // This `useMemo` handles the state just after initial report actions have been loaded. It ensures that the loader state is set correctly during the initial rendering phase when linking to a report. - useMemo(() => { - if (reportMetadata.isLoadingInitialReportActions) { + useLayoutEffect(() => { + if (!reportActionID) { return; } requestAnimationFrame(() => { - firstRenderLinkingLoaderRef.current = true; setFirstRenderLinkingLoader(true); }); - }, [route, setFirstRenderLinkingLoader]); - // This `useMemo` updates the loader state after the initial rendering phase is complete and the report actions are no longer loading, ensuring the loader is hidden at the correct time. - useMemo(() => { - if (!firstRenderLinkingLoaderRef || !firstRenderLinkingLoaderRef.current || reportMetadata.isLoadingInitialReportActions) { + }, [route, reportActionID]); + useEffect(() => { + if (!firstRenderLinkingLoader || reportMetadata.isLoadingInitialReportActions) { return; } - requestAnimationFrame(() => { - firstRenderLinkingLoaderRef.current = false; - setFirstRenderLinkingLoader(false); - }); - }, [reportMetadata.isLoadingInitialReportActions, setFirstRenderLinkingLoader]); + setFirstRenderLinkingLoader(false); + }, [firstRenderLinkingLoader, reportMetadata.isLoadingInitialReportActions]); + const shouldShowSkeleton = useMemo( () => firstRenderLinkingLoader || !isReportReadyForDisplay || isLoadingInitialReportActions || isLoading || (!!reportActionID && reportMetadata.isLoadingInitialReportActions), - [isReportReadyForDisplay, isLoadingInitialReportActions, isLoading, reportActionID, reportMetadata.isLoadingInitialReportActions, firstRenderLinkingLoader], + [firstRenderLinkingLoader, isReportReadyForDisplay, isLoadingInitialReportActions, isLoading, reportActionID, reportMetadata.isLoadingInitialReportActions], ); return ( From 5a9bd2f1b101e42bde227179fcbeed6f2546821e Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Sat, 6 Jan 2024 19:02:23 +0100 Subject: [PATCH 044/245] remove useMemo calculations --- src/components/FloatingActionButton.js | 5 +- src/pages/home/report/ReportActionsView.js | 86 +++++++++++----------- 2 files changed, 47 insertions(+), 44 deletions(-) diff --git a/src/components/FloatingActionButton.js b/src/components/FloatingActionButton.js index 59e741001063..bb973ab3665f 100644 --- a/src/components/FloatingActionButton.js +++ b/src/components/FloatingActionButton.js @@ -6,6 +6,7 @@ import Svg, {Path} from 'react-native-svg'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import CheckForPreviousReportActionIDClean from '@libs/migrations/CheckForPreviousReportActionIDClean'; import variables from '@styles/variables'; import PressableWithFeedback from './Pressable/PressableWithFeedback'; import Tooltip from './Tooltip/PopoverAnchorTooltip'; @@ -106,7 +107,9 @@ const FloatingActionButton = React.forwardRef(({onPress, isActive, accessibility fabPressable.current.blur(); onPress(e); }} - onLongPress={() => {}} + onLongPress={() => { + CheckForPreviousReportActionIDClean(); + }} style={[styles.floatingActionButton, animatedStyle]} > { + const [edgeID, setEdgeID] = useState(); + const isCuttingForFirstRender = useRef(true); + + useLayoutEffect(() => { + setEdgeID(); + }, [route, linkedID]); + + const listID = useMemo(() => { + isCuttingForFirstRender.current = true; + listIDCount += 1; + return listIDCount; + }, [route]); -const useHandleList = (linkedID, messageArray, fetchFn, route) => { - const [edgeID, setEdgeID] = useState(linkedID); - const [listID, setListID] = useState(() => Math.round(Math.random() * 100)); - const isFirstRender = useRef(true); const index = useMemo(() => { if (!linkedID) { return -1; } - return messageArray.findIndex((obj) => String(obj.reportActionID) === String(edgeID || linkedID)); - }, [messageArray, linkedID, edgeID]); - - useMemo(() => { - // Clear edgeID before navigating to a linked message - requestAnimationFrame(() => { - isFirstRender.current = true; - setEdgeID(''); - }); - }, [route, linkedID]); + const indx = messageArray.findIndex((obj) => String(obj.reportActionID) === String(isCuttingForFirstRender.current ? linkedID : edgeID)); + return indx; + }, [messageArray, edgeID, linkedID]); const cattedArray = useMemo(() => { - if (!linkedID || index === -1) { + if (!linkedID) { return messageArray; } - if (isFirstRender.current) { - // On first render, position the view at the linked message - setListID((i) => i + 1); + if (isLoading || index === -1) { + return []; + } + + if (isCuttingForFirstRender.current) { return messageArray.slice(index, messageArray.length); - } else if (edgeID) { - // On subsequent renders, load additional messages - const amountOfItemsBeforeLinkedOne = 20; + } else { + const amountOfItemsBeforeLinkedOne = 15; const newStartIndex = index >= amountOfItemsBeforeLinkedOne ? index - amountOfItemsBeforeLinkedOne : 0; return newStartIndex ? messageArray.slice(newStartIndex, messageArray.length) : messageArray; } - return messageArray; - }, [linkedID, messageArray, edgeID, index]); + }, [linkedID, messageArray, index, isLoading, edgeID]); const hasMoreCashed = cattedArray.length < messageArray.length; - const debouncedSetEdgeID = _.throttle((firstReportActionID) => { - setEdgeID(firstReportActionID); - }, 200); - const paginate = useCallback( ({firstReportActionID}) => { // This function is a placeholder as the actual pagination is handled by cattedArray // It's here if you need to trigger any side effects during pagination if (!hasMoreCashed) { - // Fetch new messages if all current messages have been shown + isCuttingForFirstRender.current = false; fetchFn(); - setEdgeID(firstReportActionID); - return; } - if (isFirstRender.current) { - isFirstRender.current = false; - // Delay to ensure the linked message is displayed correctly. - setTimeout(() => { + if (isCuttingForFirstRender.current) { + isCuttingForFirstRender.current = false; + InteractionManager.runAfterInteractions(() => { setEdgeID(firstReportActionID); - }, TIMEOUT); + }); } else { - debouncedSetEdgeID(firstReportActionID); + setEdgeID(firstReportActionID); } }, - [setEdgeID, fetchFn, hasMoreCashed], + [fetchFn, hasMoreCashed], ); return { @@ -216,8 +209,12 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro [props.isLoadingNewerReportActions, props.isLoadingInitialReportActions, reportID, newestReportAction], ); - const {cattedArray: reportActions, fetchFunc, linkedIdIndex, listID} = useHandleList(reportActionID, allReportActions, throttledLoadNewerChats, route); - + const { + cattedArray: reportActions, + fetchFunc, + linkedIdIndex, + listID, + } = useHandleList(reportActionID, allReportActions, throttledLoadNewerChats, route, !!reportActionID && props.isLoadingInitialReportActions); const hasNewestReportAction = lodashGet(reportActions[0], 'created') === props.report.lastVisibleActionCreated; const newestReportAction = lodashGet(reportActions, '[0]'); const oldestReportAction = _.last(reportActions); @@ -338,6 +335,9 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro const handleLoadNewerChats = useCallback( // eslint-disable-next-line rulesdir/prefer-early-return () => { + if (props.isLoadingInitialReportActions || props.isLoadingOlderReportActions) { + return; + } const DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST = 164; const SPACER = 30; const isContentSmallerThanList = windowHeight - DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST - SPACER > contentListHeight.current; From b3bd5d2fda8cd3148c61cb8a3239ee2a9bd80387 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 8 Jan 2024 10:21:47 +0100 Subject: [PATCH 045/245] cleanup comments --- src/components/FloatingActionButton.js | 5 +-- src/libs/Permissions.ts | 2 +- src/libs/ReportActionsUtils.ts | 43 +------------------------- src/pages/home/ReportScreen.js | 9 +----- 4 files changed, 4 insertions(+), 55 deletions(-) diff --git a/src/components/FloatingActionButton.js b/src/components/FloatingActionButton.js index bb973ab3665f..59e741001063 100644 --- a/src/components/FloatingActionButton.js +++ b/src/components/FloatingActionButton.js @@ -6,7 +6,6 @@ import Svg, {Path} from 'react-native-svg'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import CheckForPreviousReportActionIDClean from '@libs/migrations/CheckForPreviousReportActionIDClean'; import variables from '@styles/variables'; import PressableWithFeedback from './Pressable/PressableWithFeedback'; import Tooltip from './Tooltip/PopoverAnchorTooltip'; @@ -107,9 +106,7 @@ const FloatingActionButton = React.forwardRef(({onPress, isActive, accessibility fabPressable.current.blur(); onPress(e); }} - onLongPress={() => { - CheckForPreviousReportActionIDClean(); - }} + onLongPress={() => {}} style={[styles.floatingActionButton, animatedStyle]} > ): boolean { } function canUseCommentLinking(betas: OnyxEntry): boolean { - return '!!betas?.includes(CONST.BETAS.BETA_COMMENT_LINKING) || canUseAllBetas(betas)'; + return !!betas?.includes(CONST.BETAS.BETA_COMMENT_LINKING) || canUseAllBetas(betas); } function canUseReportFields(betas: OnyxEntry): boolean { diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index cb3e07afe692..853c871f1801 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -217,45 +217,6 @@ function getSortedReportActions(reportActions: ReportAction[] | null, shouldSort return sortedActions; } -// /** -// * Given an object of reportActions, sorts them, and then adds the previousReportActionID to each item except the first. -// * @param {Object} reportActions -// * @returns {Array} -// */ -// function processReportActions(reportActions) { //TODO: remove after previousReportActionID is stable -// // Separate new and sorted reportActions -// const newReportActions = _.filter(reportActions, (action) => !action.previousReportActionID); -// const sortedReportActions = _.filter(reportActions, (action) => action.previousReportActionID); - -// // Sort the new reportActions -// const sortedNewReportActions = getSortedReportActionsForDisplay(newReportActions); - -// // Then, iterate through the sorted new reportActions and add the previousReportActionID to each item except the first -// const processedReportActions = sortedNewReportActions.map((action, index) => { -// if (index === sortedNewReportActions.length - 1) { -// return action; // Return the first item as is -// } -// return { -// ...action, -// previousReportActionID: sortedNewReportActions[index + 1].reportActionID, -// }; -// }); - -// if (processedReportActions[processedReportActions.length - 1]?.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED) { -// processedReportActions.pop(); -// } - -// // Determine the order of merging based on reportActionID values -// const lastSortedReportActionID = _.last(sortedReportActions)?.reportActionTimestamp || 0; -// const firstProcessedReportActionID = _.first(processedReportActions)?.reportActionTimestamp || Infinity; - -// if (firstProcessedReportActionID > lastSortedReportActionID) { -// return [...sortedReportActions, ...processedReportActions]; -// } else { -// return [...processedReportActions, ...sortedReportActions]; -// } -// } - /** * Returns the range of report actions from the given array which include current id * the range is consistent @@ -562,9 +523,7 @@ function filterOutDeprecatedReportActions(reportActions: ReportActions | null): * This is all handled with getSortedReportActions() which is used by several other methods to keep the code DRY. */ function getSortedReportActionsForDisplay(reportActions: ReportActions | null): ReportAction[] { - const filteredReportActions = Object.entries(reportActions ?? {}) - // .filter(([key, reportAction]) => shouldReportActionBeVisible(reportAction, key)) - .map((entry) => entry[1]); + const filteredReportActions = Object.entries(reportActions ?? {}).map((entry) => entry[1]); const baseURLAdjustedReportActions = filteredReportActions.map((reportAction) => replaceBaseURL(reportAction)); return getSortedReportActions(baseURLAdjustedReportActions, true); } diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index edcfa724c1d8..65f7008b27d4 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -2,7 +2,7 @@ import {useIsFocused} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; -import {InteractionManager, View} from 'react-native'; +import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import Banner from '@components/Banner'; @@ -100,7 +100,6 @@ const propTypes = { const defaultProps = { isSidebarLoaded: false, - // sortedReportActions: [], report: {}, reportMetadata: { isLoadingInitialReportActions: true, @@ -616,12 +615,6 @@ export default compose( key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM}${getReportID(route)}`, initialValue: false, }, - // sortedReportActions: { - // key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getReportID(route)}`, - // canEvict: false, - // selector: ReportActionsUtils.getSortedReportActionsForDisplay, - // // selector: ReportActionsUtils.processReportActions, - // }, }, true, ), From edd217c91a6486e548d1e36c7f6511e1d02d51d4 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 8 Jan 2024 18:21:03 +0100 Subject: [PATCH 046/245] fix setIsHovered warnings --- src/components/Hoverable/index.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/Hoverable/index.tsx b/src/components/Hoverable/index.tsx index cbda0312beee..e8a39aea68a7 100644 --- a/src/components/Hoverable/index.tsx +++ b/src/components/Hoverable/index.tsx @@ -95,7 +95,7 @@ function Hoverable( } setIsHovered(hovered); }, - [disabled, shouldHandleScroll], + [disabled, shouldHandleScroll, setIsHovered], ); useEffect(() => { @@ -119,7 +119,7 @@ function Hoverable( }); return () => scrollingListener.remove(); - }, [shouldHandleScroll]); + }, [shouldHandleScroll, setIsHovered]); useEffect(() => { if (!DeviceCapabilities.hasHoverSupport()) { @@ -147,14 +147,14 @@ function Hoverable( document.addEventListener('mouseover', unsetHoveredIfOutside); return () => document.removeEventListener('mouseover', unsetHoveredIfOutside); - }, [isHovered]); + }, [isHovered, setIsHovered]); useEffect(() => { if (!disabled || !isHovered) { return; } setIsHovered(false); - }, [disabled, isHovered]); + }, [disabled, isHovered, setIsHovered]); useEffect(() => { if (disabled) { @@ -209,7 +209,7 @@ function Hoverable( child.props.onBlur(event); } }, - [child.props], + [child.props, setIsHovered], ); // We need to access the ref of a children from both parent and current component From 68ee0ab9349b70f2f296fc93397bb0e133ace075 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 8 Jan 2024 18:21:26 +0100 Subject: [PATCH 047/245] use patches --- ...eact-native+virtualized-lists+0.72.8.patch | 34 +++++++++++++++++++ ...-native-web+0.19.9+004+fixLastSpacer.patch | 29 ++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 patches/@react-native+virtualized-lists+0.72.8.patch create mode 100644 patches/react-native-web+0.19.9+004+fixLastSpacer.patch diff --git a/patches/@react-native+virtualized-lists+0.72.8.patch b/patches/@react-native+virtualized-lists+0.72.8.patch new file mode 100644 index 000000000000..b7f9c39f572d --- /dev/null +++ b/patches/@react-native+virtualized-lists+0.72.8.patch @@ -0,0 +1,34 @@ +diff --git a/node_modules/@react-native/virtualized-lists/Lists/VirtualizedList.js b/node_modules/@react-native/virtualized-lists/Lists/VirtualizedList.js +index ef5a3f0..2590edd 100644 +--- a/node_modules/@react-native/virtualized-lists/Lists/VirtualizedList.js ++++ b/node_modules/@react-native/virtualized-lists/Lists/VirtualizedList.js +@@ -125,19 +125,6 @@ function windowSizeOrDefault(windowSize: ?number) { + return windowSize ?? 21; + } + +-function findLastWhere( +- arr: $ReadOnlyArray, +- predicate: (element: T) => boolean, +-): T | null { +- for (let i = arr.length - 1; i >= 0; i--) { +- if (predicate(arr[i])) { +- return arr[i]; +- } +- } +- +- return null; +-} +- + /** + * Base implementation for the more convenient [``](https://reactnative.dev/docs/flatlist) + * and [``](https://reactnative.dev/docs/sectionlist) components, which are also better +@@ -1019,7 +1006,8 @@ class VirtualizedList extends StateSafePureComponent { + const spacerKey = this._getSpacerKey(!horizontal); + + const renderRegions = this.state.renderMask.enumerateRegions(); +- const lastSpacer = findLastWhere(renderRegions, r => r.isSpacer); ++ const lastRegion = renderRegions[renderRegions.length - 1]; ++ const lastSpacer = lastRegion?.isSpacer ? lastRegion : null; + + for (const section of renderRegions) { + if (section.isSpacer) { \ No newline at end of file diff --git a/patches/react-native-web+0.19.9+004+fixLastSpacer.patch b/patches/react-native-web+0.19.9+004+fixLastSpacer.patch new file mode 100644 index 000000000000..f5441d087277 --- /dev/null +++ b/patches/react-native-web+0.19.9+004+fixLastSpacer.patch @@ -0,0 +1,29 @@ +diff --git a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js +index 7f6c880..b05da08 100644 +--- a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js ++++ b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js +@@ -78,14 +78,6 @@ function scrollEventThrottleOrDefault(scrollEventThrottle) { + function windowSizeOrDefault(windowSize) { + return windowSize !== null && windowSize !== void 0 ? windowSize : 21; + } +-function findLastWhere(arr, predicate) { +- for (var i = arr.length - 1; i >= 0; i--) { +- if (predicate(arr[i])) { +- return arr[i]; +- } +- } +- return null; +-} + + /** + * Base implementation for the more convenient [``](https://reactnative.dev/docs/flatlist) +@@ -1107,7 +1099,8 @@ class VirtualizedList extends StateSafePureComponent { + _keylessItemComponentName = ''; + var spacerKey = this._getSpacerKey(!horizontal); + var renderRegions = this.state.renderMask.enumerateRegions(); +- var lastSpacer = findLastWhere(renderRegions, r => r.isSpacer); ++ var lastRegion = renderRegions[renderRegions.length - 1]; ++ var lastSpacer = lastRegion?.isSpacer ? lastRegion : null; + for (var _iterator = _createForOfIteratorHelperLoose(renderRegions), _step; !(_step = _iterator()).done;) { + var section = _step.value; + if (section.isSpacer) { From 7cdc05887c0e10478b1b5488e2a010bb3cc9bb0b Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 8 Jan 2024 18:52:02 +0100 Subject: [PATCH 048/245] optional scrollToBottom --- src/pages/home/report/ReportActionsList.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 39423ed156e1..84e6a4a4d6c4 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -262,6 +262,9 @@ function ReportActionsList({ }, [report.reportID]); useEffect(() => { + if (linkedReportActionID) { + return; + } InteractionManager.runAfterInteractions(() => { reportScrollManager.scrollToBottom(); }); From a8f17f8e473b6bbe1d9de928ae819ba34fc44725 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 8 Jan 2024 19:48:35 +0100 Subject: [PATCH 049/245] hovering issue --- src/pages/home/report/ReportActionsView.js | 39 ++++++++++++++++------ 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index cbe74e3fd551..782dcfa8acf7 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -97,10 +97,15 @@ function getReportActionID(route) { return {reportActionID: lodashGet(route, 'params.reportActionID', null), reportID: lodashGet(route, 'params.reportID', null)}; } +const DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST = 164; +const SPACER = 30; +const AMOUNT_OF_ITEMS_BEFORE_LINKED_ONE = 15; + let listIDCount = 1; const useHandleList = (linkedID, messageArray, fetchFn, route, isLoading) => { const [edgeID, setEdgeID] = useState(); const isCuttingForFirstRender = useRef(true); + const isCuttingForFirstBatch = useRef(false); useLayoutEffect(() => { setEdgeID(); @@ -108,18 +113,18 @@ const useHandleList = (linkedID, messageArray, fetchFn, route, isLoading) => { const listID = useMemo(() => { isCuttingForFirstRender.current = true; + isCuttingForFirstBatch.current = false; listIDCount += 1; return listIDCount; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [route]); - const index = useMemo(() => { if (!linkedID) { return -1; } - const indx = messageArray.findIndex((obj) => String(obj.reportActionID) === String(isCuttingForFirstRender.current ? linkedID : edgeID)); - return indx; + return messageArray.findIndex((obj) => String(obj.reportActionID) === String(isCuttingForFirstRender.current ? linkedID : edgeID)); }, [messageArray, edgeID, linkedID]); const cattedArray = useMemo(() => { @@ -133,10 +138,13 @@ const useHandleList = (linkedID, messageArray, fetchFn, route, isLoading) => { if (isCuttingForFirstRender.current) { return messageArray.slice(index, messageArray.length); } else { - const amountOfItemsBeforeLinkedOne = 15; - const newStartIndex = index >= amountOfItemsBeforeLinkedOne ? index - amountOfItemsBeforeLinkedOne : 0; + // Sometimes the layout is wrong. This helps get the slide right for one item. + const dynamicBatchSize = isCuttingForFirstBatch.current ? 1 : AMOUNT_OF_ITEMS_BEFORE_LINKED_ONE; + const newStartIndex = index >= dynamicBatchSize ? index - dynamicBatchSize : 0; + isCuttingForFirstBatch.current = false; return newStartIndex ? messageArray.slice(newStartIndex, messageArray.length) : messageArray; } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [linkedID, messageArray, index, isLoading, edgeID]); const hasMoreCashed = cattedArray.length < messageArray.length; @@ -151,6 +159,7 @@ const useHandleList = (linkedID, messageArray, fetchFn, route, isLoading) => { } if (isCuttingForFirstRender.current) { isCuttingForFirstRender.current = false; + isCuttingForFirstBatch.current = true; InteractionManager.runAfterInteractions(() => { setEdgeID(firstReportActionID); }); @@ -245,6 +254,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro return; } Report.openReport({reportID, reportActionID}); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [route]); useEffect(() => { @@ -307,6 +317,8 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro contentListHeight.current = h; }, []); + const checkIfContentSmallerThanList = useCallback(() => windowHeight - DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST - SPACER > contentListHeight.current, [windowHeight]); + /** * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently * displaying. @@ -325,21 +337,28 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro Report.getOlderActions(reportID, oldestReportAction.reportActionID); }; - const firstReportActionID = useMemo(() => reportActions[0]?.reportActionID, [reportActions]); + const firstReportActionID = useMemo(() => lodashGet(newestReportAction, 'reportActionID'), [newestReportAction]); const handleLoadNewerChats = useCallback( // eslint-disable-next-line rulesdir/prefer-early-return () => { if (props.isLoadingInitialReportActions || props.isLoadingOlderReportActions) { return; } - const DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST = 164; - const SPACER = 30; - const isContentSmallerThanList = windowHeight - DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST - SPACER > contentListHeight.current; + const isContentSmallerThanList = checkIfContentSmallerThanList(); if ((reportActionID && linkedIdIndex > -1 && !hasNewestReportAction && !isContentSmallerThanList) || (!reportActionID && !hasNewestReportAction && !isContentSmallerThanList)) { fetchFunc({firstReportActionID}); } }, - [hasNewestReportAction, linkedIdIndex, firstReportActionID, fetchFunc, reportActionID, windowHeight], + [ + props.isLoadingInitialReportActions, + props.isLoadingOlderReportActions, + checkIfContentSmallerThanList, + reportActionID, + linkedIdIndex, + hasNewestReportAction, + fetchFunc, + firstReportActionID, + ], ); /** From e8dc64387e83b26a213785f9e51854f13f0c289f Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 9 Jan 2024 12:32:20 +0100 Subject: [PATCH 050/245] fix loader blinking --- src/pages/home/ReportScreen.js | 38 ++++++++++++++++------------------ 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 39f895fecc7e..268bcc2fa497 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -1,7 +1,7 @@ import {useIsFocused} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -171,8 +171,8 @@ function ReportScreen({ const flatListRef = useRef(); const reactionListRef = useRef(); const firstRenderRef = useRef(true); + const isLinkingLoaderRef = useRef(!!reportActionID); const prevReport = usePrevious(report); - const [firstRenderLinkingLoader, setFirstRenderLinkingLoader] = useState(!!reportActionID); const prevUserLeavingStatus = usePrevious(userLeavingStatus); const [isLinkingToMessage, setLinkingToMessageTrigger] = useState(false); @@ -184,8 +184,21 @@ function ReportScreen({ const cattedRangeOfReportActions = ReportActionsUtils.getRangeFromArrayByID(sortedReportActions, reportActionID); const reportActionsWithoutDeleted = ReportActionsUtils.getReportActionsWithoutRemoved(cattedRangeOfReportActions); return reportActionsWithoutDeleted; - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, [reportActionID, allReportActions, isOffline]); + + // We define this here because if we have a cached elements, reportActions would trigger them immediately, causing a visible blink. Therefore, it's necessary to define it simultaneously with reportActions. We use a ref for this purpose, as there's no need to trigger a re-render, unlike changing the state with isLoadingInitialReportActions would do. + useMemo(() => { + isLinkingLoaderRef.current = !!reportActionID; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [route]); + useMemo(() => { + if (reportMetadata.isLoadingInitialReportActions) { + return; + } + isLinkingLoaderRef.current = false; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [reportMetadata.isLoadingInitialReportActions]); const [isBannerVisible, setIsBannerVisible] = useState(true); const [listHeight, setListHeight] = useState(0); const [scrollPosition, setScrollPosition] = useState({}); @@ -453,24 +466,9 @@ function ReportScreen({ const actionListValue = useMemo(() => ({flatListRef, scrollPosition, setScrollPosition}), [flatListRef, scrollPosition, setScrollPosition]); - useLayoutEffect(() => { - if (!reportActionID) { - return; - } - requestAnimationFrame(() => { - setFirstRenderLinkingLoader(true); - }); - }, [route, reportActionID]); - useEffect(() => { - if (!firstRenderLinkingLoader || reportMetadata.isLoadingInitialReportActions) { - return; - } - setFirstRenderLinkingLoader(false); - }, [firstRenderLinkingLoader, reportMetadata.isLoadingInitialReportActions]); - const shouldShowSkeleton = useMemo( - () => firstRenderLinkingLoader || !isReportReadyForDisplay || isLoadingInitialReportActions || isLoading || (!!reportActionID && reportMetadata.isLoadingInitialReportActions), - [firstRenderLinkingLoader, isReportReadyForDisplay, isLoadingInitialReportActions, isLoading, reportActionID, reportMetadata.isLoadingInitialReportActions], + () => isLinkingLoaderRef.current || !isReportReadyForDisplay || isLoadingInitialReportActions || isLoading || (!!reportActionID && reportMetadata.isLoadingInitialReportActions), + [isReportReadyForDisplay, isLoadingInitialReportActions, isLoading, reportActionID, reportMetadata.isLoadingInitialReportActions], ); return ( From 2ac370df9c1720cd795e0f3797ba6f80b394256e Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 9 Jan 2024 18:29:16 +0100 Subject: [PATCH 051/245] temporary fix due to broken main --- src/components/Tooltip/BaseTooltip/index.tsx | 2 +- tests/actions/IOUTest.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Tooltip/BaseTooltip/index.tsx b/src/components/Tooltip/BaseTooltip/index.tsx index 2adde759b847..4e44f918a24a 100644 --- a/src/components/Tooltip/BaseTooltip/index.tsx +++ b/src/components/Tooltip/BaseTooltip/index.tsx @@ -189,7 +189,7 @@ function Tooltip( (e: MouseEvent) => { updateTargetAndMousePosition(e); if (React.isValidElement(children)) { - children.props.onMouseEnter(e); + // children.props.onMouseEnter(e); } }, [children, updateTargetAndMousePosition], diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index 320f1203f4d2..c5dfd4909ce7 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -2259,7 +2259,7 @@ describe('actions/IOU', () => { const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs); // When Opening a thread report with the given details - Report.openReport(thread.reportID, userLogins, thread, createIOUAction.reportActionID); + Report.openReport({reportID: thread.reportID}, userLogins, thread, createIOUAction.reportActionID); await waitForBatchedUpdates(); // Then The iou action has the transaction report id as a child report ID From 5549ffd1d19c3e25f4f989e627c38c51e124889f Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 10 Jan 2024 21:40:56 +0100 Subject: [PATCH 052/245] implement scrolling functionality prior to adding pagination --- src/pages/home/ReportScreen.js | 46 ++++++++------- src/pages/home/report/ReportActionsList.js | 3 +- src/pages/home/report/ReportActionsView.js | 66 ++++++++++++++-------- 3 files changed, 69 insertions(+), 46 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 3345c6064aa5..312a02dfa1c2 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -1,7 +1,7 @@ import {useIsFocused} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -173,10 +173,10 @@ function ReportScreen({ const flatListRef = useRef(); const reactionListRef = useRef(); const firstRenderRef = useRef(true); - const isLinkingLoaderRef = useRef(!!reportActionID); + const shouldTriggerLoadingRef = useRef(!!reportActionID); const prevReport = usePrevious(report); const prevUserLeavingStatus = usePrevious(userLeavingStatus); - const [isLinkingToMessage, setLinkingToMessageTrigger] = useState(false); + const [isLinkingToMessage, setLinkingToMessage] = useState(!!reportActionID); const reportActions = useMemo(() => { if (!!allReportActions && allReportActions.length === 0) { @@ -189,18 +189,6 @@ function ReportScreen({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [reportActionID, allReportActions, isOffline]); - // We define this here because if we have a cached elements, reportActions would trigger them immediately, causing a visible blink. Therefore, it's necessary to define it simultaneously with reportActions. We use a ref for this purpose, as there's no need to trigger a re-render, unlike changing the state with isLoadingInitialReportActions would do. - useMemo(() => { - isLinkingLoaderRef.current = !!reportActionID; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [route]); - useMemo(() => { - if (reportMetadata.isLoadingInitialReportActions) { - return; - } - isLinkingLoaderRef.current = false; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [reportMetadata.isLoadingInitialReportActions]); const [isBannerVisible, setIsBannerVisible] = useState(true); const [listHeight, setListHeight] = useState(0); const [scrollPosition, setScrollPosition] = useState({}); @@ -211,6 +199,15 @@ function ReportScreen({ Performance.markStart(CONST.TIMING.CHAT_RENDER); } + // Define here because reportActions are recalculated before mount, allowing data to display faster than useEffect can trigger. If we have cached reportActions, they will be shown immediately. We aim to display a loader first, then fetch relevant reportActions, and finally show them. + useMemo(() => { + shouldTriggerLoadingRef.current = !!reportActionID; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [route, reportActionID]); + useLayoutEffect(() => { + setLinkingToMessage(!!reportActionID); + }, [route, reportActionID]); + const {addWorkspaceRoomOrChatPendingAction, addWorkspaceRoomOrChatErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report); const screenWrapperStyle = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}]; @@ -507,9 +504,22 @@ function ReportScreen({ const actionListValue = useMemo(() => ({flatListRef, scrollPosition, setScrollPosition}), [flatListRef, scrollPosition, setScrollPosition]); const shouldShowSkeleton = useMemo( - () => isLinkingLoaderRef.current || !isReportReadyForDisplay || isLoadingInitialReportActions || isLoading || (!!reportActionID && reportMetadata.isLoadingInitialReportActions), - [isReportReadyForDisplay, isLoadingInitialReportActions, isLoading, reportActionID, reportMetadata.isLoadingInitialReportActions], + () => isLinkingToMessage || !isReportReadyForDisplay || isLoadingInitialReportActions || isLoading || (!!reportActionID && reportMetadata.isLoadingInitialReportActions), + [isLinkingToMessage, isReportReadyForDisplay, isLoadingInitialReportActions, isLoading, reportActionID, reportMetadata.isLoadingInitialReportActions], ); + + // This helps in tracking from the moment 'route' triggers useMemo until isLoadingInitialReportActions becomes true. It prevents blinking when loading reportActions from cache. + useEffect(() => { + if (reportMetadata.isLoadingInitialReportActions && shouldTriggerLoadingRef.current) { + shouldTriggerLoadingRef.current = false; + return; + } + if (!reportMetadata.isLoadingInitialReportActions && !shouldTriggerLoadingRef.current) { + shouldTriggerLoadingRef.current = false; + setLinkingToMessage(false); + } + }, [reportMetadata.isLoadingInitialReportActions]); + return ( @@ -563,8 +573,6 @@ function ReportScreen({ { - const [edgeID, setEdgeID] = useState(); +let listIDCount = Math.round(Math.random() * 100); + +/** + * useHandleList manages the logic for handling a list of messages with pagination and dynamic loading. + * It determines the part of the message array to display ('cattedArray') based on the current linked message, + * and manages pagination through 'paginate' function. + * + * @param {string} linkedID - ID of the linked message used for initial focus. + * @param {array} messageArray - Array of messages. + * @param {function} fetchFn - Function to fetch more messages. + * @param {string} route - Current route, used to reset states on route change. + * @param {boolean} isLoading - Loading state indicator. + * @param {object} reportScrollManager - Manages scrolling functionality. + * @returns {object} An object containing the sliced message array, the pagination function, + * index of the linked message, and a unique list ID. + */ +const useHandleList = (linkedID, messageArray, fetchFn, route, isLoading, reportScrollManager) => { + // we don't set edgeID on initial render as linkedID as it should trigger cattedArray after linked message was positioned + const [edgeID, setEdgeID] = useState(''); const isCuttingForFirstRender = useRef(true); - const isCuttingForFirstBatch = useRef(false); useLayoutEffect(() => { - setEdgeID(); + setEdgeID(''); }, [route, linkedID]); const listID = useMemo(() => { isCuttingForFirstRender.current = true; - isCuttingForFirstBatch.current = false; listIDCount += 1; return listIDCount; // eslint-disable-next-line react-hooks/exhaustive-deps @@ -138,12 +153,10 @@ const useHandleList = (linkedID, messageArray, fetchFn, route, isLoading) => { if (isCuttingForFirstRender.current) { return messageArray.slice(index, messageArray.length); } else { - // Sometimes the layout is wrong. This helps get the slide right for one item. - const dynamicBatchSize = isCuttingForFirstBatch.current ? 1 : AMOUNT_OF_ITEMS_BEFORE_LINKED_ONE; - const newStartIndex = index >= dynamicBatchSize ? index - dynamicBatchSize : 0; - isCuttingForFirstBatch.current = false; + const newStartIndex = index >= PAGINATION_SIZE ? index - PAGINATION_SIZE : 0; return newStartIndex ? messageArray.slice(newStartIndex, messageArray.length) : messageArray; } + // edgeID is needed to trigger batching once the report action has been positioned // eslint-disable-next-line react-hooks/exhaustive-deps }, [linkedID, messageArray, index, isLoading, edgeID]); @@ -152,22 +165,19 @@ const useHandleList = (linkedID, messageArray, fetchFn, route, isLoading) => { const paginate = useCallback( ({firstReportActionID}) => { // This function is a placeholder as the actual pagination is handled by cattedArray - // It's here if you need to trigger any side effects during pagination if (!hasMoreCashed) { isCuttingForFirstRender.current = false; fetchFn(); } if (isCuttingForFirstRender.current) { + // This is a workaround because 'autoscrollToTopThreshold' does not always function correctly. + // We manually trigger a scroll to a slight offset to ensure the expected scroll behavior. + reportScrollManager.ref.current?.scrollToOffset({animated: false, offset: 1}); isCuttingForFirstRender.current = false; - isCuttingForFirstBatch.current = true; - InteractionManager.runAfterInteractions(() => { - setEdgeID(firstReportActionID); - }); - } else { - setEdgeID(firstReportActionID); } + setEdgeID(firstReportActionID); }, - [fetchFn, hasMoreCashed], + [fetchFn, hasMoreCashed, reportScrollManager.ref], ); return { @@ -182,6 +192,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro useCopySelectionHelper(); const reactionListRef = useContext(ReactionListContext); const route = useRoute(); + const reportScrollManager = useReportScrollManager(); const {reportActionID} = getReportActionID(route); const didLayout = useRef(false); const didSubscribeToReportTypingEvents = useRef(false); @@ -221,10 +232,11 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro fetchFunc, linkedIdIndex, listID, - } = useHandleList(reportActionID, allReportActions, throttledLoadNewerChats, route, !!reportActionID && props.isLoadingInitialReportActions); + } = useHandleList(reportActionID, allReportActions, throttledLoadNewerChats, route, !!reportActionID && props.isLoadingInitialReportActions, reportScrollManager); + const hasNewestReportAction = lodashGet(reportActions[0], 'created') === props.report.lastVisibleActionCreated; const newestReportAction = lodashGet(reportActions, '[0]'); - const oldestReportAction = _.last(reportActions); + const oldestReportAction = useMemo(() => _.last(reportActions), [reportActions]); const isWeReachedTheOldestAction = lodashGet(oldestReportAction, 'actionName') === CONST.REPORT.ACTIONS.TYPE.CREATED; /** @@ -238,6 +250,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro if (props.report.isOptimisticReport || !_.isEmpty(createChatError)) { return; } + Report.openReport({reportID, reportActionID}); }; @@ -253,6 +266,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro if (!reportActionID) { return; } + Report.openReport({reportID, reportActionID}); // eslint-disable-next-line react-hooks/exhaustive-deps }, [route]); @@ -335,13 +349,13 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro } // Retrieve the next REPORT.ACTIONS.LIMIT sized page of comments Report.getOlderActions(reportID, oldestReportAction.reportActionID); - }, [props.network.isOffline, props.isLoadingOlderReportActions, oldestReportAction, reportID]); + }, [props.network.isOffline, props.isLoadingOlderReportActions, oldestReportAction, isWeReachedTheOldestAction, reportID]); const firstReportActionID = useMemo(() => lodashGet(newestReportAction, 'reportActionID'), [newestReportAction]); const handleLoadNewerChats = useCallback( // eslint-disable-next-line rulesdir/prefer-early-return () => { - if (props.isLoadingInitialReportActions || props.isLoadingOlderReportActions) { + if (props.isLoadingInitialReportActions || props.isLoadingOlderReportActions || props.network.isOffline) { return; } const isContentSmallerThanList = checkIfContentSmallerThanList(); @@ -358,6 +372,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro hasNewestReportAction, fetchFunc, firstReportActionID, + props.network.isOffline, ], ); @@ -407,6 +422,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro policy={props.policy} listID={listID} onContentSizeChange={onContentSizeChange} + reportScrollManager={reportScrollManager} /> From 8036bce2a10f95ce27b72a92efbc9f8f49fc047d Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 10 Jan 2024 22:25:43 +0100 Subject: [PATCH 053/245] use memo for oldestReportAction after merge --- src/pages/home/report/ReportActionsView.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index c33e411f0ece..b52c7fbbbdf1 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -5,7 +5,6 @@ import {useIsFocused, useRoute} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; -import {InteractionManager} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import networkPropTypes from '@components/networkPropTypes'; @@ -343,8 +342,6 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro return; } - const oldestReportAction = _.last(props.reportActions); - // Don't load more chats if we're already at the beginning of the chat history if (!oldestReportAction || isWeReachedTheOldestAction) { return; From abe9dc43036aa33234facb0fa1d36705b2833007 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 10 Jan 2024 23:15:34 +0100 Subject: [PATCH 054/245] scrollToOffsetWithoutAnimation --- src/components/Hoverable/ActiveHoverable.tsx | 4 ++-- src/hooks/useReportScrollManager/index.native.ts | 16 +++++++++++++++- src/hooks/useReportScrollManager/index.ts | 16 +++++++++++++++- src/hooks/useReportScrollManager/types.ts | 1 + src/pages/home/report/ReportActionsView.js | 4 ++-- 5 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/components/Hoverable/ActiveHoverable.tsx b/src/components/Hoverable/ActiveHoverable.tsx index 8fff59fe6eba..6037092a562d 100644 --- a/src/components/Hoverable/ActiveHoverable.tsx +++ b/src/components/Hoverable/ActiveHoverable.tsx @@ -8,7 +8,7 @@ import type HoverableProps from './types'; type ActiveHoverableProps = Omit; type UseHoveredReturnType = [boolean, (newValue: boolean) => void]; - +// This is a workaround specifically for the web part of comment linking. Without this adjustment, you might observe sliding effects due to conflicts between MVCPFlatList implementation and this file. Check it once https://github.com/necolas/react-native-web/pull/2588 is merged function useHovered(initialValue: boolean, runHoverAfterInteraction: boolean): UseHoveredReturnType { const [state, setState] = useState(initialValue); @@ -20,7 +20,7 @@ function useHovered(initialValue: boolean, runHoverAfterInteraction: boolean): U } else { setState(newValue); } - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return [state, interceptedSetState]; } diff --git a/src/hooks/useReportScrollManager/index.native.ts b/src/hooks/useReportScrollManager/index.native.ts index 6666a4ebd0f2..0af995ddc1f0 100644 --- a/src/hooks/useReportScrollManager/index.native.ts +++ b/src/hooks/useReportScrollManager/index.native.ts @@ -29,7 +29,21 @@ function useReportScrollManager(): ReportScrollManagerData { flatListRef.current?.scrollToOffset({animated: false, offset: 0}); }, [flatListRef, setScrollPosition]); - return {ref: flatListRef, scrollToIndex, scrollToBottom}; + /** + * Scroll to the offset of the flatlist. + */ + const scrollToOffsetWithoutAnimation = useCallback( + (offset: number) => { + if (!flatListRef?.current) { + return; + } + + flatListRef.current.scrollToOffset({animated: false, offset}); + }, + [flatListRef], + ); + + return {ref: flatListRef, scrollToIndex, scrollToBottom, scrollToOffsetWithoutAnimation}; } export default useReportScrollManager; diff --git a/src/hooks/useReportScrollManager/index.ts b/src/hooks/useReportScrollManager/index.ts index 8b56cd639d08..d9b3605b9006 100644 --- a/src/hooks/useReportScrollManager/index.ts +++ b/src/hooks/useReportScrollManager/index.ts @@ -28,7 +28,21 @@ function useReportScrollManager(): ReportScrollManagerData { flatListRef.current.scrollToOffset({animated: false, offset: 0}); }, [flatListRef]); - return {ref: flatListRef, scrollToIndex, scrollToBottom}; + /** + * Scroll to the bottom of the flatlist. + */ + const scrollToOffsetWithoutAnimation = useCallback( + (offset: number) => { + if (!flatListRef?.current) { + return; + } + + flatListRef.current.scrollToOffset({animated: false, offset}); + }, + [flatListRef], + ); + + return {ref: flatListRef, scrollToIndex, scrollToBottom, scrollToOffsetWithoutAnimation}; } export default useReportScrollManager; diff --git a/src/hooks/useReportScrollManager/types.ts b/src/hooks/useReportScrollManager/types.ts index 5182f7269a9c..f29b5dfd44a2 100644 --- a/src/hooks/useReportScrollManager/types.ts +++ b/src/hooks/useReportScrollManager/types.ts @@ -4,6 +4,7 @@ type ReportScrollManagerData = { ref: FlatListRefType; scrollToIndex: (index: number, isEditing?: boolean) => void; scrollToBottom: () => void; + scrollToOffsetWithoutAnimation: (offset: number) => void; }; export default ReportScrollManagerData; diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index b52c7fbbbdf1..8200f88b078c 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -171,12 +171,12 @@ const useHandleList = (linkedID, messageArray, fetchFn, route, isLoading, report if (isCuttingForFirstRender.current) { // This is a workaround because 'autoscrollToTopThreshold' does not always function correctly. // We manually trigger a scroll to a slight offset to ensure the expected scroll behavior. - reportScrollManager.ref.current?.scrollToOffset({animated: false, offset: 1}); + reportScrollManager.scrollToOffsetWithoutAnimation(1); isCuttingForFirstRender.current = false; } setEdgeID(firstReportActionID); }, - [fetchFn, hasMoreCashed, reportScrollManager.ref], + [fetchFn, hasMoreCashed, reportScrollManager], ); return { From 498716727b0c23e185216dbb03b522347d1191eb Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 10 Jan 2024 23:23:04 +0100 Subject: [PATCH 055/245] undo runHoverAfterInteraction --- src/components/Hoverable/ActiveHoverable.tsx | 31 ++++-------------- .../CheckForPreviousReportActionIDClean.ts | 32 ------------------- 2 files changed, 7 insertions(+), 56 deletions(-) delete mode 100644 src/libs/migrations/CheckForPreviousReportActionIDClean.ts diff --git a/src/components/Hoverable/ActiveHoverable.tsx b/src/components/Hoverable/ActiveHoverable.tsx index 6037092a562d..028fdd30cf35 100644 --- a/src/components/Hoverable/ActiveHoverable.tsx +++ b/src/components/Hoverable/ActiveHoverable.tsx @@ -1,32 +1,15 @@ import type {Ref} from 'react'; import {cloneElement, forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {DeviceEventEmitter, InteractionManager} from 'react-native'; +import {DeviceEventEmitter} from 'react-native'; import mergeRefs from '@libs/mergeRefs'; import {getReturnValue} from '@libs/ValueUtils'; import CONST from '@src/CONST'; import type HoverableProps from './types'; type ActiveHoverableProps = Omit; -type UseHoveredReturnType = [boolean, (newValue: boolean) => void]; -// This is a workaround specifically for the web part of comment linking. Without this adjustment, you might observe sliding effects due to conflicts between MVCPFlatList implementation and this file. Check it once https://github.com/necolas/react-native-web/pull/2588 is merged -function useHovered(initialValue: boolean, runHoverAfterInteraction: boolean): UseHoveredReturnType { - const [state, setState] = useState(initialValue); - - const interceptedSetState = useCallback((newValue: boolean) => { - if (runHoverAfterInteraction) { - InteractionManager.runAfterInteractions(() => { - setState(newValue); - }); - } else { - setState(newValue); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - return [state, interceptedSetState]; -} -function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, children, runHoverAfterInteraction = false}: ActiveHoverableProps, outerRef: Ref) { - const [isHovered, setIsHovered] = useHovered(false, runHoverAfterInteraction); +function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, children}: ActiveHoverableProps, outerRef: Ref) { + const [isHovered, setIsHovered] = useState(false); const elementRef = useRef(null); const isScrollingRef = useRef(false); @@ -40,7 +23,7 @@ function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, children, r } setIsHovered(hovered); }, - [setIsHovered, shouldHandleScroll], + [shouldHandleScroll], ); useEffect(() => { @@ -64,7 +47,7 @@ function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, children, r }); return () => scrollingListener.remove(); - }, [setIsHovered, shouldHandleScroll]); + }, [shouldHandleScroll]); useEffect(() => { // Do not mount a listener if the component is not hovered @@ -89,7 +72,7 @@ function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, children, r document.addEventListener('mouseover', unsetHoveredIfOutside); return () => document.removeEventListener('mouseover', unsetHoveredIfOutside); - }, [setIsHovered, isHovered, elementRef]); + }, [isHovered, elementRef]); useEffect(() => { const unsetHoveredWhenDocumentIsHidden = () => document.visibilityState === 'hidden' && setIsHovered(false); @@ -130,7 +113,7 @@ function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, children, r child.props.onBlur?.(event); }, - [setIsHovered, child.props], + [child.props], ); return cloneElement(child, { diff --git a/src/libs/migrations/CheckForPreviousReportActionIDClean.ts b/src/libs/migrations/CheckForPreviousReportActionIDClean.ts deleted file mode 100644 index 4362ae79114b..000000000000 --- a/src/libs/migrations/CheckForPreviousReportActionIDClean.ts +++ /dev/null @@ -1,32 +0,0 @@ -import Onyx, {OnyxCollection} from 'react-native-onyx'; -import ONYXKEYS from '@src/ONYXKEYS'; -import * as OnyxTypes from '@src/types/onyx'; - -function getReportActionsFromOnyx(): Promise> { - return new Promise((resolve) => { - const connectionID = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - waitForCollectionCallback: true, - callback: (allReportActions) => { - Onyx.disconnect(connectionID); - return resolve(allReportActions); - }, - }); - }); -} - -/** - * This migration checks for the 'previousReportActionID' key in the first valid reportAction of a report in Onyx. - * If the key is not found then all reportActions for all reports are removed from Onyx. - */ -export default function (): Promise { - return getReportActionsFromOnyx().then((allReportActions) => { - const onyxData: OnyxCollection = {}; - - Object.keys(allReportActions ?? {}).forEach((onyxKey) => { - onyxData[onyxKey] = {}; - }); - - return Onyx.multiSet(onyxData as Record<`${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS}`, Record>); - }); -} From 7aa008a1e9c0bc1223bc64d3361144f4b81b858e Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 10 Jan 2024 23:46:17 +0100 Subject: [PATCH 056/245] remove outdated test --- tests/unit/ReportActionsUtilsTest.js | 59 ---------------------------- 1 file changed, 59 deletions(-) diff --git a/tests/unit/ReportActionsUtilsTest.js b/tests/unit/ReportActionsUtilsTest.js index efdfc7ba10c4..107941e32006 100644 --- a/tests/unit/ReportActionsUtilsTest.js +++ b/tests/unit/ReportActionsUtilsTest.js @@ -191,65 +191,6 @@ describe('ReportActionsUtils', () => { expect(result).toStrictEqual(input); }); - describe('getSortedReportActionsForDisplay with marked the first reportAction', () => { - it('should filter out non-whitelisted actions', () => { - const input = [ - { - created: '2022-11-13 22:27:01.825', - reportActionID: '8401445780099176', - actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, - message: [{html: 'Hello world'}], - }, - { - created: '2022-11-12 22:27:01.825', - reportActionID: '6401435781022176', - actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, - message: [{html: 'Hello world'}], - }, - { - created: '2022-11-11 22:27:01.825', - reportActionID: '2962390724708756', - actionName: CONST.REPORT.ACTIONS.TYPE.IOU, - message: [{html: 'Hello world'}], - }, - { - created: '2022-11-10 22:27:01.825', - reportActionID: '1609646094152486', - actionName: CONST.REPORT.ACTIONS.TYPE.RENAMED, - message: [{html: 'Hello world'}], - }, - { - created: '2022-11-09 22:27:01.825', - reportActionID: '8049485084562457', - actionName: CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.UPDATE_FIELD, - message: [{html: 'updated the Approval Mode from "Submit and Approve" to "Submit and Close"'}], - }, - { - created: '2022-11-08 22:27:06.825', - reportActionID: '1661970171066216', - actionName: CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED, - message: [{html: 'Waiting for the bank account'}], - }, - { - created: '2022-11-06 22:27:08.825', - reportActionID: '1661970171066220', - actionName: CONST.REPORT.ACTIONS.TYPE.TASKEDITED, - message: [{html: 'I have changed the task'}], - }, - ]; - - const resultWithoutNewestFlag = ReportActionsUtils.getSortedReportActionsForDisplay(input); - const resultWithNewestFlag = ReportActionsUtils.getReportActionsWithoutRemoved(input, true); - input.pop(); - // Mark the newest report action as the newest report action - resultWithoutNewestFlag[0] = { - ...resultWithoutNewestFlag[0], - isNewestReportAction: true, - }; - expect(resultWithoutNewestFlag).toStrictEqual(resultWithNewestFlag); - }); - }); - it('should filter out closed actions', () => { const input = [ { From dda07b4371c28bae3c844c23c1b02fab333109f5 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 11 Jan 2024 12:24:00 +0100 Subject: [PATCH 057/245] rename const --- .../workflows/reassurePerformanceTests.yml | 1 - src/components/Hoverable/types.ts | 3 - src/libs/ReportActionsUtils.ts | 33 +++++------ src/pages/home/ReportScreen.js | 57 +++++++------------ 4 files changed, 35 insertions(+), 59 deletions(-) diff --git a/.github/workflows/reassurePerformanceTests.yml b/.github/workflows/reassurePerformanceTests.yml index 116f178868c1..64b4536d9241 100644 --- a/.github/workflows/reassurePerformanceTests.yml +++ b/.github/workflows/reassurePerformanceTests.yml @@ -42,4 +42,3 @@ jobs: with: DURATION_DEVIATION_PERCENTAGE: 20 COUNT_DEVIATION: 0 - diff --git a/src/components/Hoverable/types.ts b/src/components/Hoverable/types.ts index 13059c2e8316..6963e3b5178c 100644 --- a/src/components/Hoverable/types.ts +++ b/src/components/Hoverable/types.ts @@ -18,9 +18,6 @@ type HoverableProps = { /** Decides whether to handle the scroll behaviour to show hover once the scroll ends */ shouldHandleScroll?: boolean; - - /** Call setHovered(true) with runAfterInteraction */ - runHoverAfterInteraction?: boolean; }; export default HoverableProps; diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 8ab68e6e278e..511c1e782864 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -216,25 +216,19 @@ function getSortedReportActions(reportActions: ReportAction[] | null, shouldSort return sortedActions; } - -/** - * Returns the range of report actions from the given array which include current id - * the range is consistent - * - * param {ReportAction[]} array - * param {String} id - * returns {ReportAction} - */ -function getRangeFromArrayByID(array: ReportAction[], id?: string): ReportAction[] { +// Returns the largest gapless range of reportActions including a the provided reportActionID, where a "gap" is defined as a reportAction's `previousReportActionID` not matching the previous reportAction in the sortedReportActions array. +// See unit tests for example of inputs and expected outputs. +function getContinuousReportActionChain(sortedReportActions: ReportAction[], id?: string): ReportAction[] { let index; if (id) { - index = array.findIndex((obj) => obj.reportActionID === id); + index = sortedReportActions.findIndex((obj) => obj.reportActionID === id); } else { - index = array.findIndex((obj) => obj.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + index = sortedReportActions.findIndex((obj) => obj.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); } if (index === -1) { + Log.hmmm('[getContinuousReportActionChain] The linked reportAction is missing and needs to be fetched'); return []; } @@ -244,23 +238,23 @@ function getRangeFromArrayByID(array: ReportAction[], id?: string): ReportAction // Iterate forwards through the array, starting from endIndex. This loop checks the continuity of actions by: // 1. Comparing the current item's previousReportActionID with the next item's reportActionID. // This ensures that we are moving in a sequence of related actions from newer to older. - while (endIndex < array.length - 1 && array[endIndex].previousReportActionID === array[endIndex + 1].reportActionID) { + while (endIndex < sortedReportActions.length - 1 && sortedReportActions[endIndex].previousReportActionID === sortedReportActions[endIndex + 1].reportActionID) { endIndex++; } - // Iterate backwards through the array, starting from startIndex. This loop has two main checks: + // Iterate backwards through the sortedReportActions, starting from startIndex. This loop has two main checks: // 1. It compares the current item's reportActionID with the previous item's previousReportActionID. // This is to ensure continuity in a sequence of actions. // 2. If the first condition fails, it then checks if the previous item has a pendingAction of 'add'. // This additional check is to include recently sent messages that might not yet be part of the established sequence. while ( - (startIndex > 0 && array[startIndex].reportActionID === array[startIndex - 1].previousReportActionID) || - array[startIndex - 1]?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD + (startIndex > 0 && sortedReportActions[startIndex].reportActionID === sortedReportActions[startIndex - 1].previousReportActionID) || + sortedReportActions[startIndex - 1]?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD ) { startIndex--; } - return array.slice(startIndex, endIndex + 1); + return sortedReportActions.slice(startIndex, endIndex + 1); } /** @@ -533,7 +527,8 @@ function filterOutDeprecatedReportActions(reportActions: ReportActions | null): * This is all handled with getSortedReportActions() which is used by several other methods to keep the code DRY. */ function getSortedReportActionsForDisplay(reportActions: ReportActions | null): ReportAction[] { - const filteredReportActions = Object.entries(reportActions ?? {}).map((entry) => entry[1]); + const filteredReportActions = Object.values(reportActions ?? {}); + const baseURLAdjustedReportActions = filteredReportActions.map((reportAction) => replaceBaseURL(reportAction)); return getSortedReportActions(baseURLAdjustedReportActions, true); } @@ -897,7 +892,7 @@ export { shouldReportActionBeVisible, shouldHideNewMarker, shouldReportActionBeVisibleAsLastAction, - getRangeFromArrayByID, + getContinuousReportActionChain, hasRequestFromCurrentAccount, getFirstVisibleReportActionID, isMemberChangeAction, diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 312a02dfa1c2..fd9872cd4d65 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -134,17 +134,6 @@ function getReportID(route) { // Placing the default value outside of `lodash.get()` is intentional. return String(lodashGet(route, 'params.reportID') || 0); } -/** - * Get the currently viewed report ID as number - * - * @param {Object} route - * @param {Object} route.params - * @param {String} route.params.reportID - * @returns {String} - */ -function getReportActionID(route) { - return {reportActionID: lodashGet(route, 'params.reportActionID', null), reportID: lodashGet(route, 'params.reportID', null)}; -} function ReportScreen({ betas, @@ -168,26 +157,27 @@ function ReportScreen({ const {translate} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); const {isOffline} = useNetwork(); - const {reportActionID, reportID} = getReportActionID(route); - const flatListRef = useRef(); const reactionListRef = useRef(); const firstRenderRef = useRef(true); - const shouldTriggerLoadingRef = useRef(!!reportActionID); + const reportIDFromRoute = getReportID(route); + const reportActionIDFromRoute = lodashGet(route, 'params.reportActionID', null); + const shouldTriggerLoadingRef = useRef(!!reportActionIDFromRoute); const prevReport = usePrevious(report); const prevUserLeavingStatus = usePrevious(userLeavingStatus); - const [isLinkingToMessage, setLinkingToMessage] = useState(!!reportActionID); + const [isLinkingToMessage, setLinkingToMessage] = useState(!!reportActionIDFromRoute); const reportActions = useMemo(() => { if (!!allReportActions && allReportActions.length === 0) { return []; } const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions); - const cattedRangeOfReportActions = ReportActionsUtils.getRangeFromArrayByID(sortedReportActions, reportActionID); - const reportActionsWithoutDeleted = ReportActionsUtils.getReportActionsWithoutRemoved(cattedRangeOfReportActions); + const currentRangeOfReportActions = ReportActionsUtils.getContinuousReportActionChain(sortedReportActions, reportActionIDFromRoute); + // eslint-disable-next-line rulesdir/prefer-underscore-method + const reportActionsWithoutDeleted = currentRangeOfReportActions.filter((item) => ReportActionsUtils.shouldReportActionBeVisible(item, item.reportActionID)); return reportActionsWithoutDeleted; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [reportActionID, allReportActions, isOffline]); + }, [reportActionIDFromRoute, allReportActions, isOffline]); const [isBannerVisible, setIsBannerVisible] = useState(true); const [listHeight, setListHeight] = useState(0); @@ -200,13 +190,10 @@ function ReportScreen({ } // Define here because reportActions are recalculated before mount, allowing data to display faster than useEffect can trigger. If we have cached reportActions, they will be shown immediately. We aim to display a loader first, then fetch relevant reportActions, and finally show them. - useMemo(() => { - shouldTriggerLoadingRef.current = !!reportActionID; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [route, reportActionID]); useLayoutEffect(() => { - setLinkingToMessage(!!reportActionID); - }, [route, reportActionID]); + shouldTriggerLoadingRef.current = !!reportActionIDFromRoute; + setLinkingToMessage(!!reportActionIDFromRoute); + }, [route, reportActionIDFromRoute]); const {addWorkspaceRoomOrChatPendingAction, addWorkspaceRoomOrChatErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report); const screenWrapperStyle = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}]; @@ -218,7 +205,7 @@ function ReportScreen({ const shouldHideReport = !ReportUtils.canAccessReport(report, policies, betas); - const isLoading = !reportID || !isSidebarLoaded || _.isEmpty(personalDetails); + const isLoading = !reportIDFromRoute || !isSidebarLoaded || _.isEmpty(personalDetails); const parentReportAction = ReportActionsUtils.getParentReportAction(report); const isSingleTransactionView = ReportUtils.isMoneyRequest(report); @@ -241,7 +228,7 @@ function ReportScreen({ let headerView = ( { - Report.openReport({reportID, reportActionID: reportActionID || ''}); - }, [reportID, reportActionID]); + Report.openReport({reportID: reportIDFromRoute, reportActionID: reportActionIDFromRoute || ''}); + }, [reportIDFromRoute, reportActionIDFromRoute]); const isFocused = useIsFocused(); useEffect(() => { @@ -463,7 +450,7 @@ function ReportScreen({ ]); useEffect(() => { - if (!ReportUtils.isValidReportIDFromPath(reportID)) { + if (!ReportUtils.isValidReportIDFromPath(reportIDFromRoute)) { return; } // Ensures subscription event succeeds when the report/workspace room is created optimistically. @@ -472,10 +459,10 @@ function ReportScreen({ // Existing reports created will have empty fields for `pendingFields`. const didCreateReportSuccessfully = !report.pendingFields || (!report.pendingFields.addWorkspaceRoom && !report.pendingFields.createChat); if (!didSubscribeToReportLeavingEvents.current && didCreateReportSuccessfully) { - Report.subscribeToReportLeavingEvents(reportID); + Report.subscribeToReportLeavingEvents(reportIDFromRoute); didSubscribeToReportLeavingEvents.current = true; } - }, [report, didSubscribeToReportLeavingEvents, reportID]); + }, [report, didSubscribeToReportLeavingEvents, reportIDFromRoute]); const onListLayout = useCallback((e) => { setListHeight((prev) => lodashGet(e, 'nativeEvent.layout.height', prev)); @@ -504,8 +491,8 @@ function ReportScreen({ const actionListValue = useMemo(() => ({flatListRef, scrollPosition, setScrollPosition}), [flatListRef, scrollPosition, setScrollPosition]); const shouldShowSkeleton = useMemo( - () => isLinkingToMessage || !isReportReadyForDisplay || isLoadingInitialReportActions || isLoading || (!!reportActionID && reportMetadata.isLoadingInitialReportActions), - [isLinkingToMessage, isReportReadyForDisplay, isLoadingInitialReportActions, isLoading, reportActionID, reportMetadata.isLoadingInitialReportActions], + () => isLinkingToMessage || !isReportReadyForDisplay || isLoadingInitialReportActions || isLoading || (!!reportActionIDFromRoute && reportMetadata.isLoadingInitialReportActions), + [isLinkingToMessage, isReportReadyForDisplay, isLoadingInitialReportActions, isLoading, reportActionIDFromRoute, reportMetadata.isLoadingInitialReportActions], ); // This helps in tracking from the moment 'route' triggers useMemo until isLoadingInitialReportActions becomes true. It prevents blinking when loading reportActions from cache. @@ -515,7 +502,6 @@ function ReportScreen({ return; } if (!reportMetadata.isLoadingInitialReportActions && !shouldTriggerLoadingRef.current) { - shouldTriggerLoadingRef.current = false; setLinkingToMessage(false); } }, [reportMetadata.isLoadingInitialReportActions]); @@ -574,7 +560,7 @@ function ReportScreen({ reportActions={reportActions} report={report} fetchReport={fetchReport} - reportActionID={reportActionID} + reportActionID={reportActionIDFromRoute} isLoadingInitialReportActions={reportMetadata.isLoadingInitialReportActions} isLoadingNewerReportActions={reportMetadata.isLoadingNewerReportActions} isLoadingOlderReportActions={reportMetadata.isLoadingOlderReportActions} @@ -626,7 +612,6 @@ export default compose( allReportActions: { key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getReportID(route)}`, canEvict: false, - selector: (reportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), }, report: { key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${getReportID(route)}`, From 4f9d65eacfa1395bd79908b948a0bed76cbe32e5 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 11 Jan 2024 15:09:13 +0100 Subject: [PATCH 058/245] remove isLoadingInitialReportActions --- src/pages/home/ReportScreen.js | 14 +++++--------- src/pages/home/report/ReportActionsView.js | 17 +---------------- 2 files changed, 6 insertions(+), 25 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index fd9872cd4d65..a49e6dda88de 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -165,7 +165,7 @@ function ReportScreen({ const shouldTriggerLoadingRef = useRef(!!reportActionIDFromRoute); const prevReport = usePrevious(report); const prevUserLeavingStatus = usePrevious(userLeavingStatus); - const [isLinkingToMessage, setLinkingToMessage] = useState(!!reportActionIDFromRoute); + const [isPrepareLinkingToMessage, setLinkingToMessage] = useState(!!reportActionIDFromRoute); const reportActions = useMemo(() => { if (!!allReportActions && allReportActions.length === 0) { @@ -197,10 +197,6 @@ function ReportScreen({ const {addWorkspaceRoomOrChatPendingAction, addWorkspaceRoomOrChatErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report); const screenWrapperStyle = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}]; - - // There are no reportActions at all to display and we are still in the process of loading the next set of actions. - const isLoadingInitialReportActions = _.isEmpty(reportActions) && reportMetadata.isLoadingInitialReportActions; - const isOptimisticDelete = lodashGet(report, 'statusNum') === CONST.REPORT.STATUS.CLOSED; const shouldHideReport = !ReportUtils.canAccessReport(report, policies, betas); @@ -296,12 +292,12 @@ function ReportScreen({ // It possible that we may not have the report object yet in Onyx yet e.g. we navigated to a URL for an accessible report that // is not stored locally yet. If report.reportID exists, then the report has been stored locally and nothing more needs to be done. // If it doesn't exist, then we fetch the report from the API. - if (report.reportID && report.reportID === getReportID(route) && !isLoadingInitialReportActions) { + if (report.reportID && report.reportID === getReportID(route) && !reportMetadata.isLoadingInitialReportActions) { return; } fetchReport(); - }, [report.reportID, route, isLoadingInitialReportActions, fetchReport]); + }, [report.reportID, route, reportMetadata.isLoadingInitialReportActions, fetchReport]); const dismissBanner = useCallback(() => { setIsBannerVisible(false); @@ -491,8 +487,8 @@ function ReportScreen({ const actionListValue = useMemo(() => ({flatListRef, scrollPosition, setScrollPosition}), [flatListRef, scrollPosition, setScrollPosition]); const shouldShowSkeleton = useMemo( - () => isLinkingToMessage || !isReportReadyForDisplay || isLoadingInitialReportActions || isLoading || (!!reportActionIDFromRoute && reportMetadata.isLoadingInitialReportActions), - [isLinkingToMessage, isReportReadyForDisplay, isLoadingInitialReportActions, isLoading, reportActionIDFromRoute, reportMetadata.isLoadingInitialReportActions], + () => isPrepareLinkingToMessage || !isReportReadyForDisplay || isLoading || reportMetadata.isLoadingInitialReportActions, + [isPrepareLinkingToMessage, isReportReadyForDisplay, isLoading, reportMetadata.isLoadingInitialReportActions], ); // This helps in tracking from the moment 'route' triggers useMemo until isLoadingInitialReportActions becomes true. It prevents blinking when loading reportActions from cache. diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 8200f88b078c..e3090e24f0c8 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -1,6 +1,3 @@ -/* eslint-disable no-else-return */ - -/* eslint-disable rulesdir/prefer-underscore-method */ import {useIsFocused, useRoute} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; @@ -85,18 +82,6 @@ const defaultProps = { }, }; -/** - * Get the currently viewed report ID as number - * - * @param {Object} route - * @param {Object} route.params - * @param {String} route.params.reportID - * @returns {String} - */ -function getReportActionID(route) { - return {reportActionID: lodashGet(route, 'params.reportActionID', null), reportID: lodashGet(route, 'params.reportID', null)}; -} - const DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST = 120; const SPACER = 16; const PAGINATION_SIZE = 15; @@ -192,7 +177,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro const reactionListRef = useContext(ReactionListContext); const route = useRoute(); const reportScrollManager = useReportScrollManager(); - const {reportActionID} = getReportActionID(route); + const reportActionID = lodashGet(route, 'params.reportActionID', null); const didLayout = useRef(false); const didSubscribeToReportTypingEvents = useRef(false); const contentListHeight = useRef(0); From f459394cf5fbc81c18cbca9758ef81ae8ee67568 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 11 Jan 2024 16:13:29 +0100 Subject: [PATCH 059/245] refactor getSortedReportActionsForDisplay --- patches/@react-native+virtualized-lists+0.72.8.patch | 2 +- src/libs/ReportActionsUtils.ts | 12 ++++++++++-- src/pages/home/ReportScreen.js | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/patches/@react-native+virtualized-lists+0.72.8.patch b/patches/@react-native+virtualized-lists+0.72.8.patch index b7f9c39f572d..a3bef95f1618 100644 --- a/patches/@react-native+virtualized-lists+0.72.8.patch +++ b/patches/@react-native+virtualized-lists+0.72.8.patch @@ -31,4 +31,4 @@ index ef5a3f0..2590edd 100644 + const lastSpacer = lastRegion?.isSpacer ? lastRegion : null; for (const section of renderRegions) { - if (section.isSpacer) { \ No newline at end of file + if (section.isSpacer) { diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 511c1e782864..a3a051968516 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -526,8 +526,16 @@ function filterOutDeprecatedReportActions(reportActions: ReportActions | null): * to ensure they will always be displayed in the same order (in case multiple actions have the same timestamp). * This is all handled with getSortedReportActions() which is used by several other methods to keep the code DRY. */ -function getSortedReportActionsForDisplay(reportActions: ReportActions | null): ReportAction[] { - const filteredReportActions = Object.values(reportActions ?? {}); +function getSortedReportActionsForDisplay(reportActions: ReportActions | null, shouldIncludeInvisibleActions = true): ReportAction[] { + let filteredReportActions; + + if (shouldIncludeInvisibleActions) { + filteredReportActions = Object.entries(reportActions ?? {}) + .filter(([key, reportAction]) => shouldReportActionBeVisible(reportAction, key)) + .map((entry) => entry[1]); + } else { + filteredReportActions = Object.values(reportActions ?? {}); + } const baseURLAdjustedReportActions = filteredReportActions.map((reportAction) => replaceBaseURL(reportAction)); return getSortedReportActions(baseURLAdjustedReportActions, true); diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index a49e6dda88de..65c2c6bfc25b 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -171,7 +171,7 @@ function ReportScreen({ if (!!allReportActions && allReportActions.length === 0) { return []; } - const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions); + const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, false); const currentRangeOfReportActions = ReportActionsUtils.getContinuousReportActionChain(sortedReportActions, reportActionIDFromRoute); // eslint-disable-next-line rulesdir/prefer-underscore-method const reportActionsWithoutDeleted = currentRangeOfReportActions.filter((item) => ReportActionsUtils.shouldReportActionBeVisible(item, item.reportActionID)); From 8be49c4f14bbe7aafcda9a8e5dde3a99ca3ab901 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 11 Jan 2024 16:33:45 +0100 Subject: [PATCH 060/245] refactor openReport action --- .../ReportActionItem/MoneyRequestAction.js | 4 +-- src/libs/actions/Report.ts | 25 ++++++++----------- src/pages/home/ReportScreen.js | 2 +- .../report/ContextMenu/ContextMenuActions.js | 4 +-- src/pages/home/report/ReportActionsList.js | 2 +- src/pages/home/report/ReportActionsView.js | 4 +-- .../withReportAndReportActionOrNotFound.tsx | 2 +- tests/actions/IOUTest.js | 10 ++++---- tests/actions/ReportTest.js | 2 +- 9 files changed, 26 insertions(+), 29 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestAction.js b/src/components/ReportActionItem/MoneyRequestAction.js index f988542a3e6c..35e8fd3dcd68 100644 --- a/src/components/ReportActionItem/MoneyRequestAction.js +++ b/src/components/ReportActionItem/MoneyRequestAction.js @@ -108,11 +108,11 @@ function MoneyRequestAction({ if (!childReportID) { const thread = ReportUtils.buildTransactionThread(action, requestReportID); const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs); - Report.openReport({reportID: thread.reportID}, userLogins, thread, action.reportActionID); + Report.openReport(thread.reportID, '', userLogins, thread, action.reportActionID); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(thread.reportID)); return; } - Report.openReport({reportID: childReportID}); + Report.openReport(childReportID); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID)); }; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index dd783dde5037..f56fea47f09b 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -460,16 +460,12 @@ function reportActionsExist(reportID: string): boolean { return allReportActions?.[reportID] !== undefined; } -type OpenReportProps = { - reportID: string; - reportActionID?: string; -}; - /** * Gets the latest page of report actions and updates the last read message * If a chat with the passed reportID is not found, we will create a chat based on the passed participantList * - * @param Object reportID, reportActionID + * @param reportID The ID of the report to open + * @param reportActionID The ID of the report action to navigate to * @param participantLoginList The list of users that are included in a new chat, not including the user creating it * @param newReportObject The optimistic report object created when making a new chat, saved as optimistic data * @param parentReportActionID The parent report action that a thread was created from (only passed for new threads) @@ -477,7 +473,8 @@ type OpenReportProps = { * @param participantAccountIDList The list of accountIDs that are included in a new chat, not including the user creating it */ function openReport( - {reportID, reportActionID}: OpenReportProps, + reportID: string, + reportActionID?: string, participantLoginList: string[] = [], newReportObject: Partial = {}, parentReportActionID = '0', @@ -697,7 +694,7 @@ function navigateToAndOpenReport(userLogins: string[], shouldDismissModal = true const reportID = chat ? chat.reportID : newChat.reportID; // We want to pass newChat here because if anything is passed in that param (even an existing chat), we will try to create a chat on the server - openReport({reportID}, userLogins, newChat); + openReport(reportID, '', userLogins, newChat); if (shouldDismissModal) { Navigation.dismissModal(reportID); } else { @@ -719,7 +716,7 @@ function navigateToAndOpenReportWithAccountIDs(participantAccountIDs: number[]) const reportID = chat ? chat.reportID : newChat.reportID; // We want to pass newChat here because if anything is passed in that param (even an existing chat), we will try to create a chat on the server - openReport({reportID}, [], newChat, '0', false, participantAccountIDs); + openReport(reportID, '', [], newChat, '0', false, participantAccountIDs); Navigation.dismissModal(reportID); } @@ -732,7 +729,7 @@ function navigateToAndOpenReportWithAccountIDs(participantAccountIDs: number[]) */ function navigateToAndOpenChildReport(childReportID = '0', parentReportAction: Partial = {}, parentReportID = '0') { if (childReportID !== '0') { - openReport({reportID: childReportID}); + openReport(childReportID); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID)); } else { const participantAccountIDs = [...new Set([currentUserAccountID, Number(parentReportAction.actorAccountID)])]; @@ -753,7 +750,7 @@ function navigateToAndOpenChildReport(childReportID = '0', parentReportAction: P ); const participantLogins = PersonalDetailsUtils.getLoginsByAccountIDs(newChat?.participantAccountIDs ?? []); - openReport({reportID: newChat.reportID}, participantLogins, newChat, parentReportAction.reportActionID); + openReport(newChat.reportID, '', participantLogins, newChat, parentReportAction.reportActionID); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(newChat.reportID)); } } @@ -1456,7 +1453,7 @@ function updateNotificationPreference( */ function toggleSubscribeToChildReport(childReportID = '0', parentReportAction: Partial = {}, parentReportID = '0', prevNotificationPreference?: NotificationPreference) { if (childReportID !== '0') { - openReport({reportID: childReportID}); + openReport(childReportID); const parentReportActionID = parentReportAction?.reportActionID ?? '0'; if (!prevNotificationPreference || prevNotificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN) { updateNotificationPreference(childReportID, prevNotificationPreference, CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, false, parentReportID, parentReportActionID); @@ -1482,7 +1479,7 @@ function toggleSubscribeToChildReport(childReportID = '0', parentReportAction: P ); const participantLogins = PersonalDetailsUtils.getLoginsByAccountIDs(participantAccountIDs); - openReport({reportID: newChat.reportID}, participantLogins, newChat, parentReportAction.reportActionID); + openReport(newChat.reportID, '', participantLogins, newChat, parentReportAction.reportActionID); const notificationPreference = prevNotificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN ? CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS : CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; updateNotificationPreference(newChat.reportID, prevNotificationPreference, notificationPreference, false, parentReportID, parentReportAction?.reportActionID); @@ -2030,7 +2027,7 @@ function openReportFromDeepLink(url: string, isAuthenticated: boolean) { if (reportID && !isAuthenticated) { // Call the OpenReport command to check in the server if it's a public room. If so, we'll open it as an anonymous user - openReport({reportID}, [], {}, '0', true); + openReport(reportID, '', [], {}, '0', true); // Show the sign-in page if the app is offline if (isNetworkOffline) { diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 65c2c6bfc25b..edbf153cee24 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -269,7 +269,7 @@ function ReportScreen({ }, [route, report]); const fetchReport = useCallback(() => { - Report.openReport({reportID: reportIDFromRoute, reportActionID: reportActionIDFromRoute || ''}); + Report.openReport(reportIDFromRoute, reportActionIDFromRoute); }, [reportIDFromRoute, reportActionIDFromRoute]); const isFocused = useIsFocused(); diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.js index ae62ce067b80..0fb6b5bba412 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.js +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.js @@ -376,11 +376,11 @@ export default [ if (!childReportID) { const thread = ReportUtils.buildTransactionThread(reportAction, reportID); const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs); - Report.openReport({reportID: thread.reportID}, userLogins, thread, reportAction.reportActionID); + Report.openReport(thread.reportID, '', userLogins, thread, reportAction.reportActionID); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(thread.reportID)); return; } - Report.openReport({reportID: childReportID}); + Report.openReport(childReportID); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID)); return; } diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index bff5024e6bff..e5138a1d8c63 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -343,7 +343,7 @@ function ReportActionsList({ const scrollToBottomAndMarkReportAsRead = () => { if (!hasNewestReportAction) { Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.reportID)); - Report.openReport({reportID: report.reportID}); + Report.openReport(report.reportID); return; } reportScrollManager.scrollToBottom(); diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index e3090e24f0c8..a181240e298f 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -235,7 +235,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro return; } - Report.openReport({reportID, reportActionID}); + Report.openReport(reportID, reportActionID); }; useEffect(() => { @@ -251,7 +251,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro return; } - Report.openReport({reportID, reportActionID}); + Report.openReport(reportID, reportActionID); // eslint-disable-next-line react-hooks/exhaustive-deps }, [route]); diff --git a/src/pages/home/report/withReportAndReportActionOrNotFound.tsx b/src/pages/home/report/withReportAndReportActionOrNotFound.tsx index 1ec956a2b09c..fb0a00e2d10d 100644 --- a/src/pages/home/report/withReportAndReportActionOrNotFound.tsx +++ b/src/pages/home/report/withReportAndReportActionOrNotFound.tsx @@ -63,7 +63,7 @@ export default function (WrappedComponent: if (!props.isSmallScreenWidth || (isNotEmptyObject(props.report) && isNotEmptyObject(reportAction))) { return; } - Report.openReport({reportID: props.route.params.reportID}); + Report.openReport(props.route.params.reportID); // eslint-disable-next-line react-hooks/exhaustive-deps }, [props.isSmallScreenWidth, props.route.params.reportID]); diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index 7dbbc05f95f5..b0b44ea204d7 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -2183,7 +2183,7 @@ describe('actions/IOU', () => { const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs); // When Opening a thread report with the given details - Report.openReport({reportID: thread.reportID}, userLogins, thread, createIOUAction.reportActionID); + Report.openReport(thread.reportID, '', userLogins, thread, createIOUAction.reportActionID); await waitForBatchedUpdates(); // Then The iou action has the transaction report id as a child report ID @@ -2262,7 +2262,7 @@ describe('actions/IOU', () => { const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs); // When Opening a thread report with the given details - Report.openReport({reportID: thread.reportID}, userLogins, thread, createIOUAction.reportActionID); + Report.openReport(thread.reportID, '', userLogins, thread, createIOUAction.reportActionID); await waitForBatchedUpdates(); // Then The iou action has the transaction report id as a child report ID @@ -2332,7 +2332,7 @@ describe('actions/IOU', () => { const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs); jest.advanceTimersByTime(10); - Report.openReport({reportID: thread.reportID}, userLogins, thread, createIOUAction.reportActionID); + Report.openReport(thread.reportID, '', userLogins, thread, createIOUAction.reportActionID); await waitForBatchedUpdates(); Onyx.connect({ @@ -2424,7 +2424,7 @@ describe('actions/IOU', () => { jest.advanceTimersByTime(10); const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs); - Report.openReport({reportID: thread.reportID}, userLogins, thread, createIOUAction.reportActionID); + Report.openReport(thread.reportID, '', userLogins, thread, createIOUAction.reportActionID); await waitForBatchedUpdates(); @@ -2650,7 +2650,7 @@ describe('actions/IOU', () => { jest.advanceTimersByTime(10); const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs); - Report.openReport({reportID: thread.reportID}, userLogins, thread, createIOUAction.reportActionID); + Report.openReport(thread.reportID, '', userLogins, thread, createIOUAction.reportActionID); await waitForBatchedUpdates(); const allReportActions = await new Promise((resolve) => { diff --git a/tests/actions/ReportTest.js b/tests/actions/ReportTest.js index d118fd3a977e..a94db507637b 100644 --- a/tests/actions/ReportTest.js +++ b/tests/actions/ReportTest.js @@ -268,7 +268,7 @@ describe('actions/Report', () => { // When the user visits the report jest.advanceTimersByTime(10); currentTime = DateUtils.getDBTime(); - Report.openReport({reportID: REPORT_ID}); + Report.openReport(REPORT_ID); Report.readNewestAction(REPORT_ID); waitForBatchedUpdates(); return waitForBatchedUpdates(); From d67396fee59bbc97bf01f27cf98d99660d184c9f Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 11 Jan 2024 17:04:43 +0100 Subject: [PATCH 061/245] refactor initialNumToRender --- src/pages/home/ReportScreen.js | 7 ++++++- .../home/report/getInitialNumToRender/index.native.ts | 4 ++++ src/pages/home/report/getInitialNumToRender/index.ts | 4 ++++ 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 src/pages/home/report/getInitialNumToRender/index.native.ts create mode 100644 src/pages/home/report/getInitialNumToRender/index.ts diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index edbf153cee24..c6e13c2fc572 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -268,6 +268,11 @@ function ReportScreen({ return reportIDFromPath !== '' && report.reportID && !isTransitioning; }, [route, report]); + const isShowReportActionList = useMemo( + () => isReportReadyForDisplay && !isLoading && !(_.isEmpty(reportActions) && reportMetadata.isLoadingInitialReportActions), + [isReportReadyForDisplay, isLoading, reportActions, reportMetadata.isLoadingInitialReportActions], + ); + const fetchReport = useCallback(() => { Report.openReport(reportIDFromRoute, reportActionIDFromRoute); }, [reportIDFromRoute, reportActionIDFromRoute]); @@ -551,7 +556,7 @@ function ReportScreen({ style={[styles.flex1, styles.justifyContentEnd, styles.overflowHidden]} onLayout={onListLayout} > - {isReportReadyForDisplay && ( + {isShowReportActionList && ( Date: Thu, 11 Jan 2024 18:38:17 +0100 Subject: [PATCH 062/245] replace getReportID in ReportScreen --- src/pages/home/ReportScreen.js | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 11d1505a1e8d..90bb3d80df74 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -208,7 +208,7 @@ function ReportScreen({ const isLoading = !reportIDFromRoute || !isSidebarLoaded || _.isEmpty(personalDetails); const isSingleTransactionView = ReportUtils.isMoneyRequest(report); const policy = policies[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`] || {}; - const isTopMostReportId = currentReportID === getReportID(route); + const isTopMostReportId = currentReportID === reportIDFromRoute; const didSubscribeToReportLeavingEvents = useRef(false); useEffect(() => { @@ -260,12 +260,10 @@ function ReportScreen({ * @returns {Boolean} */ const isReportReadyForDisplay = useMemo(() => { - const reportIDFromPath = getReportID(route); - // This is necessary so that when we are retrieving the next report data from Onyx the ReportActionsView will remount completely - const isTransitioning = report && report.reportID !== reportIDFromPath; - return reportIDFromPath !== '' && report.reportID && !isTransitioning; - }, [route, report]); + const isTransitioning = report && report.reportID !== reportIDFromRoute; + return reportIDFromRoute !== '' && report.reportID && !isTransitioning; + }, [report, reportIDFromRoute]); const isShowReportActionList = useMemo( () => isReportReadyForDisplay && !isLoading && !(_.isEmpty(reportActions) && reportMetadata.isLoadingInitialReportActions), @@ -285,23 +283,21 @@ function ReportScreen({ }, [report.reportID, isFocused]); const fetchReportIfNeeded = useCallback(() => { - const reportIDFromPath = getReportID(route); - // Report ID will be empty when the reports collection is empty. // This could happen when we are loading the collection for the first time after logging in. - if (!ReportUtils.isValidReportIDFromPath(reportIDFromPath)) { + if (!ReportUtils.isValidReportIDFromPath(reportIDFromRoute)) { return; } // It possible that we may not have the report object yet in Onyx yet e.g. we navigated to a URL for an accessible report that // is not stored locally yet. If report.reportID exists, then the report has been stored locally and nothing more needs to be done. // If it doesn't exist, then we fetch the report from the API. - if (report.reportID && report.reportID === getReportID(route) && !reportMetadata.isLoadingInitialReportActions) { + if (report.reportID && report.reportID === reportIDFromRoute && !reportMetadata.isLoadingInitialReportActions) { return; } fetchReport(); - }, [report.reportID, route, reportMetadata.isLoadingInitialReportActions, fetchReport]); + }, [report.reportID, reportMetadata.isLoadingInitialReportActions, fetchReport, reportIDFromRoute]); const dismissBanner = useCallback(() => { setIsBannerVisible(false); @@ -339,10 +335,10 @@ function ReportScreen({ if (email) { assignee = _.find(_.values(allPersonalDetails), (p) => p.login === email) || {}; } - Task.createTaskAndNavigate(getReportID(route), title, '', assignee.login, assignee.accountID, assignee.assigneeChatReport, report.policyID); + Task.createTaskAndNavigate(reportIDFromRoute, title, '', assignee.login, assignee.accountID, assignee.assigneeChatReport, report.policyID); return true; }, - [allPersonalDetails, report.policyID, route], + [allPersonalDetails, report.policyID, reportIDFromRoute], ); /** @@ -354,9 +350,9 @@ function ReportScreen({ if (isTaskCreated) { return; } - Report.addComment(getReportID(route), text); + Report.addComment(reportIDFromRoute, text); }, - [route, handleCreateTask], + [handleCreateTask, reportIDFromRoute], ); // Clear notifications for the current report when it's opened and re-focused @@ -398,7 +394,6 @@ function ReportScreen({ const onyxReportID = report.reportID; const prevOnyxReportID = prevReport.reportID; - const routeReportID = getReportID(route); // Navigate to the Concierge chat if the room was removed from another device (e.g. user leaving a room or removed from a room) if ( @@ -406,7 +401,7 @@ function ReportScreen({ (!prevUserLeavingStatus && userLeavingStatus) || // optimistic case (prevOnyxReportID && - prevOnyxReportID === routeReportID && + prevOnyxReportID === reportIDFromRoute && !onyxReportID && prevReport.statusNum === CONST.REPORT.STATUS.OPEN && (report.statusNum === CONST.REPORT.STATUS.CLOSED || (!report.statusNum && !prevReport.parentReportID && prevReport.chatType === CONST.REPORT.CHAT_TYPE.POLICY_ROOM))) || @@ -429,7 +424,7 @@ function ReportScreen({ // the ReportScreen never actually unmounts and the reportID in the route also doesn't change. // Therefore, we need to compare if the existing reportID is the same as the one in the route // before deciding that we shouldn't call OpenReport. - if (onyxReportID === prevReport.reportID && (!onyxReportID || onyxReportID === routeReportID)) { + if (onyxReportID === prevReport.reportID && (!onyxReportID || onyxReportID === reportIDFromRoute)) { return; } @@ -447,6 +442,7 @@ function ReportScreen({ prevReport.parentReportID, prevReport.chatType, prevReport, + reportIDFromRoute, ]); useEffect(() => { From d6b6c1123a1f3125dcfa14341c65b485ed84427c Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 11 Jan 2024 21:39:58 +0100 Subject: [PATCH 063/245] renaming --- src/libs/ReportActionsUtils.ts | 8 +++---- src/pages/home/report/ReportActionItem.js | 1 - src/pages/home/report/ReportActionsView.js | 27 +++++++++++----------- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index a3a051968516..838c6e4f646f 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -218,6 +218,7 @@ function getSortedReportActions(reportActions: ReportAction[] | null, shouldSort } // Returns the largest gapless range of reportActions including a the provided reportActionID, where a "gap" is defined as a reportAction's `previousReportActionID` not matching the previous reportAction in the sortedReportActions array. // See unit tests for example of inputs and expected outputs. +// Note: sortedReportActions sorted in descending order function getContinuousReportActionChain(sortedReportActions: ReportAction[], id?: string): ReportAction[] { let index; @@ -228,7 +229,6 @@ function getContinuousReportActionChain(sortedReportActions: ReportAction[], id? } if (index === -1) { - Log.hmmm('[getContinuousReportActionChain] The linked reportAction is missing and needs to be fetched'); return []; } @@ -526,15 +526,15 @@ function filterOutDeprecatedReportActions(reportActions: ReportActions | null): * to ensure they will always be displayed in the same order (in case multiple actions have the same timestamp). * This is all handled with getSortedReportActions() which is used by several other methods to keep the code DRY. */ -function getSortedReportActionsForDisplay(reportActions: ReportActions | null, shouldIncludeInvisibleActions = true): ReportAction[] { +function getSortedReportActionsForDisplay(reportActions: ReportActions | null, shouldIncludeInvisibleActions = false): ReportAction[] { let filteredReportActions; if (shouldIncludeInvisibleActions) { + filteredReportActions = Object.values(reportActions ?? {}); + } else { filteredReportActions = Object.entries(reportActions ?? {}) .filter(([key, reportAction]) => shouldReportActionBeVisible(reportAction, key)) .map((entry) => entry[1]); - } else { - filteredReportActions = Object.values(reportActions ?? {}); } const baseURLAdjustedReportActions = filteredReportActions.map((reportAction) => replaceBaseURL(reportAction)); diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 9efb24c93b88..b1130af5d2ff 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -675,7 +675,6 @@ function ReportActionItem(props) { > {(hovered) => ( diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index a181240e298f..62503f1a0cf9 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -95,14 +95,14 @@ let listIDCount = Math.round(Math.random() * 100); * * @param {string} linkedID - ID of the linked message used for initial focus. * @param {array} messageArray - Array of messages. - * @param {function} fetchFn - Function to fetch more messages. + * @param {function} fetchNewerActon - Function to fetch more messages. * @param {string} route - Current route, used to reset states on route change. * @param {boolean} isLoading - Loading state indicator. * @param {object} reportScrollManager - Manages scrolling functionality. * @returns {object} An object containing the sliced message array, the pagination function, * index of the linked message, and a unique list ID. */ -const useHandleList = (linkedID, messageArray, fetchFn, route, isLoading, reportScrollManager) => { +const useHandleList = (linkedID, messageArray, fetchNewerActon, route, isLoading, reportScrollManager) => { // we don't set edgeID on initial render as linkedID as it should trigger cattedArray after linked message was positioned const [edgeID, setEdgeID] = useState(''); const isCuttingForFirstRender = useRef(true); @@ -145,13 +145,14 @@ const useHandleList = (linkedID, messageArray, fetchFn, route, isLoading, report }, [linkedID, messageArray, index, isLoading, edgeID]); const hasMoreCashed = cattedArray.length < messageArray.length; + const newestReportAction = lodashGet(cattedArray, '[0]'); const paginate = useCallback( ({firstReportActionID}) => { // This function is a placeholder as the actual pagination is handled by cattedArray if (!hasMoreCashed) { isCuttingForFirstRender.current = false; - fetchFn(); + fetchNewerActon(newestReportAction); } if (isCuttingForFirstRender.current) { // This is a workaround because 'autoscrollToTopThreshold' does not always function correctly. @@ -161,7 +162,7 @@ const useHandleList = (linkedID, messageArray, fetchFn, route, isLoading, report } setEdgeID(firstReportActionID); }, - [fetchFn, hasMoreCashed, reportScrollManager], + [fetchNewerActon, hasMoreCashed, reportScrollManager, newestReportAction], ); return { @@ -198,17 +199,15 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently * displaying. */ - const throttledLoadNewerChats = useCallback( - () => { + const fetchNewerAction = useCallback( + (newestReportAction) => { if (props.isLoadingNewerReportActions || props.isLoadingInitialReportActions) { return; } - // eslint-disable-next-line no-use-before-define Report.getNewerActions(reportID, newestReportAction.reportActionID); }, - // eslint-disable-next-line no-use-before-define - [props.isLoadingNewerReportActions, props.isLoadingInitialReportActions, reportID, newestReportAction], + [props.isLoadingNewerReportActions, props.isLoadingInitialReportActions, reportID], ); const { @@ -216,12 +215,12 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro fetchFunc, linkedIdIndex, listID, - } = useHandleList(reportActionID, allReportActions, throttledLoadNewerChats, route, !!reportActionID && props.isLoadingInitialReportActions, reportScrollManager); + } = useHandleList(reportActionID, allReportActions, fetchNewerAction, route, !!reportActionID && props.isLoadingInitialReportActions, reportScrollManager); const hasNewestReportAction = lodashGet(reportActions[0], 'created') === props.report.lastVisibleActionCreated; const newestReportAction = lodashGet(reportActions, '[0]'); const oldestReportAction = useMemo(() => _.last(reportActions), [reportActions]); - const isWeReachedTheOldestAction = lodashGet(oldestReportAction, 'actionName') === CONST.REPORT.ACTIONS.TYPE.CREATED; + const hasCreatedAction = lodashGet(oldestReportAction, 'actionName') === CONST.REPORT.ACTIONS.TYPE.CREATED; /** * @returns {Boolean} @@ -328,12 +327,12 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro } // Don't load more chats if we're already at the beginning of the chat history - if (!oldestReportAction || isWeReachedTheOldestAction) { + if (!oldestReportAction || hasCreatedAction) { return; } // Retrieve the next REPORT.ACTIONS.LIMIT sized page of comments Report.getOlderActions(reportID, oldestReportAction.reportActionID); - }, [props.network.isOffline, props.isLoadingOlderReportActions, oldestReportAction, isWeReachedTheOldestAction, reportID]); + }, [props.network.isOffline, props.isLoadingOlderReportActions, oldestReportAction, hasCreatedAction, reportID]); const firstReportActionID = useMemo(() => lodashGet(newestReportAction, 'reportActionID'), [newestReportAction]); const handleLoadNewerChats = useCallback( @@ -517,7 +516,7 @@ export default compose( withLocalize, withNetwork(), withOnyx({ - sesion: { + session: { key: ONYXKEYS.SESSION, }, }), From f18d81ab8a147d1830c8a1065e47dced8c2fcdda Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 11 Jan 2024 21:47:13 +0100 Subject: [PATCH 064/245] add tests --- ...-native-web+0.19.9+004+fixLastSpacer.patch | 2 +- tests/unit/ReportActionsUtilsTest.js | 143 ++++++++++++++++++ 2 files changed, 144 insertions(+), 1 deletion(-) diff --git a/patches/react-native-web+0.19.9+004+fixLastSpacer.patch b/patches/react-native-web+0.19.9+004+fixLastSpacer.patch index f5441d087277..08b5637a50c8 100644 --- a/patches/react-native-web+0.19.9+004+fixLastSpacer.patch +++ b/patches/react-native-web+0.19.9+004+fixLastSpacer.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js -index 7f6c880..b05da08 100644 +index faeb323..68d740a 100644 --- a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js +++ b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js @@ -78,14 +78,6 @@ function scrollEventThrottleOrDefault(scrollEventThrottle) { diff --git a/tests/unit/ReportActionsUtilsTest.js b/tests/unit/ReportActionsUtilsTest.js index 107941e32006..3a439f953579 100644 --- a/tests/unit/ReportActionsUtilsTest.js +++ b/tests/unit/ReportActionsUtilsTest.js @@ -256,6 +256,149 @@ describe('ReportActionsUtils', () => { expect(result).toStrictEqual(input); }); }); + describe('getContinuousReportActionChain', () => { + it('given an input ID of 1, ..., 7 it will return the report actions with id 1 - 7', () => { + const input = [ + // Given these sortedReportActions + {reportActionID: 1, previousReportActionID: null}, + {reportActionID: 2, previousReportActionID: 1}, + {reportActionID: 3, previousReportActionID: 2}, + {reportActionID: 4, previousReportActionID: 3}, + {reportActionID: 5, previousReportActionID: 4}, + {reportActionID: 6, previousReportActionID: 5}, + {reportActionID: 7, previousReportActionID: 6}, + + // Note: there's a "gap" here because the previousReportActionID (8) does not match the ID of the previous reportAction in the array (7) + {reportActionID: 9, previousReportActionID: 8}, + {reportActionID: 10, previousReportActionID: 9}, + {reportActionID: 11, previousReportActionID: 10}, + {reportActionID: 12, previousReportActionID: 11}, + + // Note: another gap + {reportActionID: 14, previousReportActionID: 13}, + {reportActionID: 15, previousReportActionID: 14}, + {reportActionID: 16, previousReportActionID: 15}, + {reportActionID: 17, previousReportActionID: 16}, + ]; + + const expectedResult = [ + {reportActionID: 1, previousReportActionID: null}, + {reportActionID: 2, previousReportActionID: 1}, + {reportActionID: 3, previousReportActionID: 2}, + {reportActionID: 4, previousReportActionID: 3}, + {reportActionID: 5, previousReportActionID: 4}, + {reportActionID: 6, previousReportActionID: 5}, + {reportActionID: 7, previousReportActionID: 6}, + ]; + // Reversing the input array to simulate descending order sorting as per our data structure + const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), 3); + input.pop(); + expect(result).toStrictEqual(expectedResult.reverse()); + }); + + it('given an input ID of 9, ..., 12 it will return the report actions with id 9 - 12', () => { + const input = [ + // Given these sortedReportActions + {reportActionID: 1, previousReportActionID: null}, + {reportActionID: 2, previousReportActionID: 1}, + {reportActionID: 3, previousReportActionID: 2}, + {reportActionID: 4, previousReportActionID: 3}, + {reportActionID: 5, previousReportActionID: 4}, + {reportActionID: 6, previousReportActionID: 5}, + {reportActionID: 7, previousReportActionID: 6}, + + // Note: there's a "gap" here because the previousReportActionID (8) does not match the ID of the previous reportAction in the array (7) + {reportActionID: 9, previousReportActionID: 8}, + {reportActionID: 10, previousReportActionID: 9}, + {reportActionID: 11, previousReportActionID: 10}, + {reportActionID: 12, previousReportActionID: 11}, + + // Note: another gap + {reportActionID: 14, previousReportActionID: 13}, + {reportActionID: 15, previousReportActionID: 14}, + {reportActionID: 16, previousReportActionID: 15}, + {reportActionID: 17, previousReportActionID: 16}, + ]; + + const expectedResult = [ + {reportActionID: 9, previousReportActionID: 8}, + {reportActionID: 10, previousReportActionID: 9}, + {reportActionID: 11, previousReportActionID: 10}, + {reportActionID: 12, previousReportActionID: 11}, + ]; + // Reversing the input array to simulate descending order sorting as per our data structure + const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), 8); + input.pop(); + expect(result).toStrictEqual(expectedResult.reverse()); + }); + + it('given an input ID of 14, ..., 17 it will return the report actions with id 14 - 17', () => { + const input = [ + // Given these sortedReportActions + {reportActionID: 1, previousReportActionID: null}, + {reportActionID: 2, previousReportActionID: 1}, + {reportActionID: 3, previousReportActionID: 2}, + {reportActionID: 4, previousReportActionID: 3}, + {reportActionID: 5, previousReportActionID: 4}, + {reportActionID: 6, previousReportActionID: 5}, + {reportActionID: 7, previousReportActionID: 6}, + + // Note: there's a "gap" here because the previousReportActionID (8) does not match the ID of the previous reportAction in the array (7) + {reportActionID: 9, previousReportActionID: 8}, + {reportActionID: 10, previousReportActionID: 9}, + {reportActionID: 11, previousReportActionID: 10}, + {reportActionID: 12, previousReportActionID: 11}, + + // Note: another gap + {reportActionID: 14, previousReportActionID: 13}, + {reportActionID: 15, previousReportActionID: 14}, + {reportActionID: 16, previousReportActionID: 15}, + {reportActionID: 17, previousReportActionID: 16}, + ]; + + const expectedResult = [ + {reportActionID: 14, previousReportActionID: 13}, + {reportActionID: 15, previousReportActionID: 14}, + {reportActionID: 16, previousReportActionID: 15}, + {reportActionID: 17, previousReportActionID: 16}, + ]; + // Reversing the input array to simulate descending order sorting as per our data structure + const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), 16); + input.pop(); + expect(result).toStrictEqual(expectedResult.reverse()); + }); + + it('given an input ID of 8 or 13 which are not exist in Onyx it will return an empty array', () => { + const input = [ + // Given these sortedReportActions + {reportActionID: 1, previousReportActionID: null}, + {reportActionID: 2, previousReportActionID: 1}, + {reportActionID: 3, previousReportActionID: 2}, + {reportActionID: 4, previousReportActionID: 3}, + {reportActionID: 5, previousReportActionID: 4}, + {reportActionID: 6, previousReportActionID: 5}, + {reportActionID: 7, previousReportActionID: 6}, + + // Note: there's a "gap" here because the previousReportActionID (8) does not match the ID of the previous reportAction in the array (7) + {reportActionID: 9, previousReportActionID: 8}, + {reportActionID: 10, previousReportActionID: 9}, + {reportActionID: 11, previousReportActionID: 10}, + {reportActionID: 12, previousReportActionID: 11}, + + // Note: another gap + {reportActionID: 14, previousReportActionID: 13}, + {reportActionID: 15, previousReportActionID: 14}, + {reportActionID: 16, previousReportActionID: 15}, + {reportActionID: 17, previousReportActionID: 16}, + ]; + + const expectedResult = []; + // Reversing the input array to simulate descending order sorting as per our data structure + const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), 8); + input.pop(); + expect(result).toStrictEqual(expectedResult.reverse()); + }); + }); describe('getLastVisibleAction', () => { it('should return the last visible action for a report', () => { From 44bba8518bcf065348f3e6dd6402edf74cc0d04b Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 11 Jan 2024 22:29:15 +0100 Subject: [PATCH 065/245] add prop types --- src/pages/home/ReportScreen.js | 20 +++++++++----------- src/pages/home/report/ReportActionsList.js | 5 +++-- tests/unit/ReportActionsUtilsTest.js | 2 +- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 90bb3d80df74..ebe8dd309392 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -19,7 +19,6 @@ import withCurrentReportID, {withCurrentReportIDDefaultProps, withCurrentReportI import withViewportOffsetTop from '@components/withViewportOffsetTop'; import useAppFocusEvent from '@hooks/useAppFocusEvent'; import useLocalize from '@hooks/useLocalize'; -import useNetwork from '@hooks/useNetwork'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -65,6 +64,9 @@ const propTypes = { /** The report currently being looked at */ report: reportPropTypes, + /** Array of all report actions for this report */ + allReportActions: PropTypes.arrayOf(PropTypes.shape(reportActionPropTypes)), + /** The report metadata loading states */ reportMetadata: reportMetadataPropTypes, @@ -162,7 +164,6 @@ function ReportScreen({ const styles = useThemeStyles(); const {translate} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); - const {isOffline} = useNetwork(); const flatListRef = useRef(); const reactionListRef = useRef(); const firstRenderRef = useRef(true); @@ -174,16 +175,13 @@ function ReportScreen({ const [isPrepareLinkingToMessage, setLinkingToMessage] = useState(!!reportActionIDFromRoute); const reportActions = useMemo(() => { - if (!!allReportActions && allReportActions.length === 0) { + if (_.isEmpty(allReportActions)) { return []; } - const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, false); + const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, true); const currentRangeOfReportActions = ReportActionsUtils.getContinuousReportActionChain(sortedReportActions, reportActionIDFromRoute); - // eslint-disable-next-line rulesdir/prefer-underscore-method - const reportActionsWithoutDeleted = currentRangeOfReportActions.filter((item) => ReportActionsUtils.shouldReportActionBeVisible(item, item.reportActionID)); - return reportActionsWithoutDeleted; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [reportActionIDFromRoute, allReportActions, isOffline]); + return _.filter(currentRangeOfReportActions, (reportAction) => ReportActionsUtils.shouldReportActionBeVisible(reportAction, reportAction.reportActionID)); + }, [reportActionIDFromRoute, allReportActions]); const [isBannerVisible, setIsBannerVisible] = useState(true); const [listHeight, setListHeight] = useState(0); @@ -265,7 +263,7 @@ function ReportScreen({ return reportIDFromRoute !== '' && report.reportID && !isTransitioning; }, [report, reportIDFromRoute]); - const isShowReportActionList = useMemo( + const shouldShowReportActionList = useMemo( () => isReportReadyForDisplay && !isLoading && !(_.isEmpty(reportActions) && reportMetadata.isLoadingInitialReportActions), [isReportReadyForDisplay, reportActions, isLoading, reportMetadata.isLoadingInitialReportActions], ); @@ -551,7 +549,7 @@ function ReportScreen({ style={[styles.flex1, styles.justifyContentEnd, styles.overflowHidden]} onLayout={onListLayout} > - {isShowReportActionList && ( + {shouldShowReportActionList && ( { {reportActionID: 12, previousReportActionID: 11}, ]; // Reversing the input array to simulate descending order sorting as per our data structure - const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), 8); + const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), 10); input.pop(); expect(result).toStrictEqual(expectedResult.reverse()); }); From 9a54d64c96fb5c0549d7528d9b53d32e6426425b Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Thu, 11 Jan 2024 18:13:31 -0500 Subject: [PATCH 066/245] MVCPFlatList fixes --- src/components/FlatList/MVCPFlatList.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/FlatList/MVCPFlatList.js b/src/components/FlatList/MVCPFlatList.js index 44cb50b98e11..5131f1cc2c49 100644 --- a/src/components/FlatList/MVCPFlatList.js +++ b/src/components/FlatList/MVCPFlatList.js @@ -67,12 +67,13 @@ const MVCPFlatList = React.forwardRef(({maintainVisibleContentPosition, horizont } const scrollOffset = getScrollOffset(); + lastScrollOffsetRef.current = scrollOffset; const contentViewLength = contentView.childNodes.length; for (let i = mvcpMinIndexForVisible; i < contentViewLength; i++) { const subview = contentView.childNodes[i]; const subviewOffset = horizontal ? subview.offsetLeft : subview.offsetTop; - if (subviewOffset > scrollOffset || i === contentViewLength - 1) { + if (subviewOffset > scrollOffset) { prevFirstVisibleOffsetRef.current = subviewOffset; firstVisibleViewRef.current = subview; break; @@ -125,6 +126,7 @@ const MVCPFlatList = React.forwardRef(({maintainVisibleContentPosition, horizont } adjustForMaintainVisibleContentPosition(); + prepareForMaintainVisibleContentPosition(); }); }); mutationObserver.observe(contentView, { @@ -134,7 +136,7 @@ const MVCPFlatList = React.forwardRef(({maintainVisibleContentPosition, horizont }); mutationObserverRef.current = mutationObserver; - }, [adjustForMaintainVisibleContentPosition, getContentView, getScrollOffset, scrollToOffset]); + }, [adjustForMaintainVisibleContentPosition, prepareForMaintainVisibleContentPosition, getContentView, getScrollOffset, scrollToOffset]); React.useEffect(() => { requestAnimationFrame(() => { @@ -168,13 +170,11 @@ const MVCPFlatList = React.forwardRef(({maintainVisibleContentPosition, horizont const onScrollInternal = React.useCallback( (ev) => { - lastScrollOffsetRef.current = getScrollOffset(); - prepareForMaintainVisibleContentPosition(); onScroll?.(ev); }, - [getScrollOffset, prepareForMaintainVisibleContentPosition, onScroll], + [prepareForMaintainVisibleContentPosition, onScroll], ); return ( From a4d9a2e005ebce5882e86f1c02bb4c37a193b044 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Thu, 11 Jan 2024 23:37:45 -0500 Subject: [PATCH 067/245] More fixes --- src/components/FlatList/MVCPFlatList.js | 8 +++----- src/components/InvertedFlatList/BaseInvertedFlatList.tsx | 2 -- src/pages/home/report/ReportActionsView.js | 5 +---- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/components/FlatList/MVCPFlatList.js b/src/components/FlatList/MVCPFlatList.js index 5131f1cc2c49..b738dedd91af 100644 --- a/src/components/FlatList/MVCPFlatList.js +++ b/src/components/FlatList/MVCPFlatList.js @@ -138,11 +138,9 @@ const MVCPFlatList = React.forwardRef(({maintainVisibleContentPosition, horizont mutationObserverRef.current = mutationObserver; }, [adjustForMaintainVisibleContentPosition, prepareForMaintainVisibleContentPosition, getContentView, getScrollOffset, scrollToOffset]); - React.useEffect(() => { - requestAnimationFrame(() => { - prepareForMaintainVisibleContentPosition(); - setupMutationObserver(); - }); + React.useLayoutEffect(() => { + prepareForMaintainVisibleContentPosition(); + setupMutationObserver(); }, [prepareForMaintainVisibleContentPosition, setupMutationObserver]); const setMergedRef = useMergeRefs(scrollRef, forwardedRef); diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx index fe7b9bba463e..48401d68c50c 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx @@ -4,7 +4,6 @@ import type {FlatListProps} from 'react-native'; import FlatList from '@components/FlatList'; const WINDOW_SIZE = 21; -const AUTOSCROLL_TO_TOP_THRESHOLD = 128; function BaseInvertedFlatList(props: FlatListProps, ref: ForwardedRef) { return ( @@ -15,7 +14,6 @@ function BaseInvertedFlatList(props: FlatListProps, ref: ForwardedRef diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 62503f1a0cf9..05d156347ba0 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -155,14 +155,11 @@ const useHandleList = (linkedID, messageArray, fetchNewerActon, route, isLoading fetchNewerActon(newestReportAction); } if (isCuttingForFirstRender.current) { - // This is a workaround because 'autoscrollToTopThreshold' does not always function correctly. - // We manually trigger a scroll to a slight offset to ensure the expected scroll behavior. - reportScrollManager.scrollToOffsetWithoutAnimation(1); isCuttingForFirstRender.current = false; } setEdgeID(firstReportActionID); }, - [fetchNewerActon, hasMoreCashed, reportScrollManager, newestReportAction], + [fetchNewerActon, hasMoreCashed, newestReportAction], ); return { From 1f10abb207219d3d8def8acb4a97a3e66b10db02 Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Fri, 12 Jan 2024 00:04:16 -0500 Subject: [PATCH 068/245] Use minIndexForVisible 1 to dodge loading views --- src/components/InvertedFlatList/BaseInvertedFlatList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx index 48401d68c50c..e686b7441fb5 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx @@ -13,7 +13,7 @@ function BaseInvertedFlatList(props: FlatListProps, ref: ForwardedRef From a07c9a92c3c3f4b44cab87b1482c555b6e7e8f3f Mon Sep 17 00:00:00 2001 From: Janic Duplessis Date: Fri, 12 Jan 2024 00:06:07 -0500 Subject: [PATCH 069/245] Add comment --- src/components/InvertedFlatList/BaseInvertedFlatList.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx index e686b7441fb5..8c087a46fe3a 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx @@ -13,6 +13,7 @@ function BaseInvertedFlatList(props: FlatListProps, ref: ForwardedRef Date: Fri, 12 Jan 2024 00:06:36 -0500 Subject: [PATCH 070/245] Remove windowSize since 21 is the default --- src/components/InvertedFlatList/BaseInvertedFlatList.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx index 8c087a46fe3a..b3e996cc4e85 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx @@ -3,15 +3,12 @@ import React, {forwardRef} from 'react'; import type {FlatListProps} from 'react-native'; import FlatList from '@components/FlatList'; -const WINDOW_SIZE = 21; - function BaseInvertedFlatList(props: FlatListProps, ref: ForwardedRef) { return ( Date: Fri, 12 Jan 2024 00:09:28 -0500 Subject: [PATCH 071/245] Fix lint --- src/pages/home/report/ReportActionsView.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 05d156347ba0..f577d9d3b6fa 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -98,11 +98,10 @@ let listIDCount = Math.round(Math.random() * 100); * @param {function} fetchNewerActon - Function to fetch more messages. * @param {string} route - Current route, used to reset states on route change. * @param {boolean} isLoading - Loading state indicator. - * @param {object} reportScrollManager - Manages scrolling functionality. * @returns {object} An object containing the sliced message array, the pagination function, * index of the linked message, and a unique list ID. */ -const useHandleList = (linkedID, messageArray, fetchNewerActon, route, isLoading, reportScrollManager) => { +const useHandleList = (linkedID, messageArray, fetchNewerActon, route, isLoading) => { // we don't set edgeID on initial render as linkedID as it should trigger cattedArray after linked message was positioned const [edgeID, setEdgeID] = useState(''); const isCuttingForFirstRender = useRef(true); @@ -123,7 +122,7 @@ const useHandleList = (linkedID, messageArray, fetchNewerActon, route, isLoading return -1; } - return messageArray.findIndex((obj) => String(obj.reportActionID) === String(isCuttingForFirstRender.current ? linkedID : edgeID)); + return _.findIndex(messageArray, (obj) => String(obj.reportActionID) === String(isCuttingForFirstRender.current ? linkedID : edgeID)); }, [messageArray, edgeID, linkedID]); const cattedArray = useMemo(() => { @@ -136,10 +135,9 @@ const useHandleList = (linkedID, messageArray, fetchNewerActon, route, isLoading if (isCuttingForFirstRender.current) { return messageArray.slice(index, messageArray.length); - } else { - const newStartIndex = index >= PAGINATION_SIZE ? index - PAGINATION_SIZE : 0; - return newStartIndex ? messageArray.slice(newStartIndex, messageArray.length) : messageArray; } + const newStartIndex = index >= PAGINATION_SIZE ? index - PAGINATION_SIZE : 0; + return newStartIndex ? messageArray.slice(newStartIndex, messageArray.length) : messageArray; // edgeID is needed to trigger batching once the report action has been positioned // eslint-disable-next-line react-hooks/exhaustive-deps }, [linkedID, messageArray, index, isLoading, edgeID]); @@ -212,7 +210,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro fetchFunc, linkedIdIndex, listID, - } = useHandleList(reportActionID, allReportActions, fetchNewerAction, route, !!reportActionID && props.isLoadingInitialReportActions, reportScrollManager); + } = useHandleList(reportActionID, allReportActions, fetchNewerAction, route, !!reportActionID && props.isLoadingInitialReportActions); const hasNewestReportAction = lodashGet(reportActions[0], 'created') === props.report.lastVisibleActionCreated; const newestReportAction = lodashGet(reportActions, '[0]'); From d08dea532480085710e2acc190401eca7af9ff1c Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 12 Jan 2024 15:47:43 +0100 Subject: [PATCH 072/245] fix issue with navigating between different reports when actions are cached --- src/pages/home/ReportScreen.js | 17 ++++++++--------- src/pages/home/report/ReportActionsView.js | 21 ++++++++++++--------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index ebe8dd309392..527092990583 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -173,7 +173,7 @@ function ReportScreen({ const prevReport = usePrevious(report); const prevUserLeavingStatus = usePrevious(userLeavingStatus); const [isPrepareLinkingToMessage, setLinkingToMessage] = useState(!!reportActionIDFromRoute); - + const isFocused = useIsFocused(); const reportActions = useMemo(() => { if (_.isEmpty(allReportActions)) { return []; @@ -183,6 +183,7 @@ function ReportScreen({ return _.filter(currentRangeOfReportActions, (reportAction) => ReportActionsUtils.shouldReportActionBeVisible(reportAction, reportAction.reportActionID)); }, [reportActionIDFromRoute, allReportActions]); + const isLoadingInitialReportActions = _.isEmpty(reportActions) && reportMetadata.isLoadingInitialReportActions; const [isBannerVisible, setIsBannerVisible] = useState(true); const [listHeight, setListHeight] = useState(0); const [scrollPosition, setScrollPosition] = useState({}); @@ -263,15 +264,17 @@ function ReportScreen({ return reportIDFromRoute !== '' && report.reportID && !isTransitioning; }, [report, reportIDFromRoute]); + const shouldShowSkeleton = + isPrepareLinkingToMessage || !isReportReadyForDisplay || isLoadingInitialReportActions || isLoading || (!!reportActionIDFromRoute && reportMetadata.isLoadingInitialReportActions); + const shouldShowReportActionList = useMemo( - () => isReportReadyForDisplay && !isLoading && !(_.isEmpty(reportActions) && reportMetadata.isLoadingInitialReportActions), - [isReportReadyForDisplay, reportActions, isLoading, reportMetadata.isLoadingInitialReportActions], + () => isReportReadyForDisplay && !isLoadingInitialReportActions && !isLoading, + [isReportReadyForDisplay, isLoading, isLoadingInitialReportActions], ); const fetchReport = useCallback(() => { Report.openReport(reportIDFromRoute, reportActionIDFromRoute); }, [reportIDFromRoute, reportActionIDFromRoute]); - const isFocused = useIsFocused(); useEffect(() => { if (!report.reportID || !isFocused) { @@ -484,11 +487,6 @@ function ReportScreen({ const actionListValue = useMemo(() => ({flatListRef, scrollPosition, setScrollPosition}), [flatListRef, scrollPosition, setScrollPosition]); - const shouldShowSkeleton = useMemo( - () => isPrepareLinkingToMessage || !isReportReadyForDisplay || isLoading || reportMetadata.isLoadingInitialReportActions, - [isPrepareLinkingToMessage, isReportReadyForDisplay, isLoading, reportMetadata.isLoadingInitialReportActions], - ); - // This helps in tracking from the moment 'route' triggers useMemo until isLoadingInitialReportActions becomes true. It prevents blinking when loading reportActions from cache. useEffect(() => { if (reportMetadata.isLoadingInitialReportActions && shouldTriggerLoadingRef.current) { @@ -560,6 +558,7 @@ function ReportScreen({ isLoadingOlderReportActions={reportMetadata.isLoadingOlderReportActions} isComposerFullSize={isComposerFullSize} policy={policy} + isContentReady={!shouldShowSkeleton} /> )} diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index f577d9d3b6fa..946006b46030 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -108,7 +108,7 @@ const useHandleList = (linkedID, messageArray, fetchNewerActon, route, isLoading useLayoutEffect(() => { setEdgeID(''); - }, [route, linkedID]); + }, [route]); const listID = useMemo(() => { isCuttingForFirstRender.current = true; @@ -118,12 +118,12 @@ const useHandleList = (linkedID, messageArray, fetchNewerActon, route, isLoading }, [route]); const index = useMemo(() => { - if (!linkedID) { + if (!linkedID || isLoading) { return -1; } return _.findIndex(messageArray, (obj) => String(obj.reportActionID) === String(isCuttingForFirstRender.current ? linkedID : edgeID)); - }, [messageArray, edgeID, linkedID]); + }, [messageArray, edgeID, linkedID, isLoading]); const cattedArray = useMemo(() => { if (!linkedID) { @@ -142,13 +142,13 @@ const useHandleList = (linkedID, messageArray, fetchNewerActon, route, isLoading // eslint-disable-next-line react-hooks/exhaustive-deps }, [linkedID, messageArray, index, isLoading, edgeID]); - const hasMoreCashed = cattedArray.length < messageArray.length; + const hasMoreCached = cattedArray.length < messageArray.length; const newestReportAction = lodashGet(cattedArray, '[0]'); const paginate = useCallback( ({firstReportActionID}) => { // This function is a placeholder as the actual pagination is handled by cattedArray - if (!hasMoreCashed) { + if (!hasMoreCached) { isCuttingForFirstRender.current = false; fetchNewerActon(newestReportAction); } @@ -157,7 +157,7 @@ const useHandleList = (linkedID, messageArray, fetchNewerActon, route, isLoading } setEdgeID(firstReportActionID); }, - [fetchNewerActon, hasMoreCashed, newestReportAction], + [fetchNewerActon, hasMoreCached, newestReportAction], ); return { @@ -181,14 +181,14 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro const hasCachedActions = useInitialValue(() => _.size(props.reportActions) > 0); const mostRecentIOUReportActionID = useInitialValue(() => ReportActionsUtils.getMostRecentIOURequestActionID(props.reportActions)); const {windowHeight} = useWindowDimensions(); + const isFocused = useIsFocused(); const prevNetworkRef = useRef(props.network); const prevAuthTokenType = usePrevious(props.session.authTokenType); const prevIsSmallScreenWidthRef = useRef(props.isSmallScreenWidth); - - const isFocused = useIsFocused(); const reportID = props.report.reportID; + const isLoading = (!!reportActionID && props.isLoadingInitialReportActions)|| !props.isContentReady; /** * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently @@ -210,7 +210,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro fetchFunc, linkedIdIndex, listID, - } = useHandleList(reportActionID, allReportActions, fetchNewerAction, route, !!reportActionID && props.isLoadingInitialReportActions); + } = useHandleList(reportActionID, allReportActions, fetchNewerAction, route, isLoading); const hasNewestReportAction = lodashGet(reportActions[0], 'created') === props.report.lastVisibleActionCreated; const newestReportAction = lodashGet(reportActions, '[0]'); @@ -412,6 +412,9 @@ ReportActionsView.defaultProps = defaultProps; ReportActionsView.displayName = 'ReportActionsView'; function arePropsEqual(oldProps, newProps) { + if (!_.isEqual(oldProps.isContentReady, newProps.isContentReady)) { + return false; + } if (!_.isEqual(oldProps.reportActions, newProps.reportActions)) { return false; } From 7533b5118d99f5b42b0ffbb857e43484f1da44a9 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 12 Jan 2024 16:39:46 +0100 Subject: [PATCH 073/245] delete scrollToOffsetWithoutAnimation --- src/hooks/useReportScrollManager/index.native.ts | 16 +--------------- src/hooks/useReportScrollManager/index.ts | 16 +--------------- src/hooks/useReportScrollManager/types.ts | 1 - 3 files changed, 2 insertions(+), 31 deletions(-) diff --git a/src/hooks/useReportScrollManager/index.native.ts b/src/hooks/useReportScrollManager/index.native.ts index 0af995ddc1f0..6666a4ebd0f2 100644 --- a/src/hooks/useReportScrollManager/index.native.ts +++ b/src/hooks/useReportScrollManager/index.native.ts @@ -29,21 +29,7 @@ function useReportScrollManager(): ReportScrollManagerData { flatListRef.current?.scrollToOffset({animated: false, offset: 0}); }, [flatListRef, setScrollPosition]); - /** - * Scroll to the offset of the flatlist. - */ - const scrollToOffsetWithoutAnimation = useCallback( - (offset: number) => { - if (!flatListRef?.current) { - return; - } - - flatListRef.current.scrollToOffset({animated: false, offset}); - }, - [flatListRef], - ); - - return {ref: flatListRef, scrollToIndex, scrollToBottom, scrollToOffsetWithoutAnimation}; + return {ref: flatListRef, scrollToIndex, scrollToBottom}; } export default useReportScrollManager; diff --git a/src/hooks/useReportScrollManager/index.ts b/src/hooks/useReportScrollManager/index.ts index d9b3605b9006..8b56cd639d08 100644 --- a/src/hooks/useReportScrollManager/index.ts +++ b/src/hooks/useReportScrollManager/index.ts @@ -28,21 +28,7 @@ function useReportScrollManager(): ReportScrollManagerData { flatListRef.current.scrollToOffset({animated: false, offset: 0}); }, [flatListRef]); - /** - * Scroll to the bottom of the flatlist. - */ - const scrollToOffsetWithoutAnimation = useCallback( - (offset: number) => { - if (!flatListRef?.current) { - return; - } - - flatListRef.current.scrollToOffset({animated: false, offset}); - }, - [flatListRef], - ); - - return {ref: flatListRef, scrollToIndex, scrollToBottom, scrollToOffsetWithoutAnimation}; + return {ref: flatListRef, scrollToIndex, scrollToBottom}; } export default useReportScrollManager; diff --git a/src/hooks/useReportScrollManager/types.ts b/src/hooks/useReportScrollManager/types.ts index f29b5dfd44a2..5182f7269a9c 100644 --- a/src/hooks/useReportScrollManager/types.ts +++ b/src/hooks/useReportScrollManager/types.ts @@ -4,7 +4,6 @@ type ReportScrollManagerData = { ref: FlatListRefType; scrollToIndex: (index: number, isEditing?: boolean) => void; scrollToBottom: () => void; - scrollToOffsetWithoutAnimation: (offset: number) => void; }; export default ReportScrollManagerData; From 9478b8039e3737beaa9d72824f0cd035614bfebe Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 12 Jan 2024 16:40:20 +0100 Subject: [PATCH 074/245] delete getReportActionsWithoutRemoved --- src/libs/ReportActionsUtils.ts | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 838c6e4f646f..1c634584178a 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -216,9 +216,12 @@ function getSortedReportActions(reportActions: ReportAction[] | null, shouldSort return sortedActions; } -// Returns the largest gapless range of reportActions including a the provided reportActionID, where a "gap" is defined as a reportAction's `previousReportActionID` not matching the previous reportAction in the sortedReportActions array. -// See unit tests for example of inputs and expected outputs. -// Note: sortedReportActions sorted in descending order + +/** + * Returns the largest gapless range of reportActions including a the provided reportActionID, where a "gap" is defined as a reportAction's `previousReportActionID` not matching the previous reportAction in the sortedReportActions array. + * See unit tests for example of inputs and expected outputs. + * Note: sortedReportActions sorted in descending order + */ function getContinuousReportActionChain(sortedReportActions: ReportAction[], id?: string): ReportAction[] { let index; @@ -541,13 +544,6 @@ function getSortedReportActionsForDisplay(reportActions: ReportActions | null, s return getSortedReportActions(baseURLAdjustedReportActions, true); } -function getReportActionsWithoutRemoved(reportActions: ReportAction[] | null): ReportAction[] { - if (!reportActions) { - return []; - } - return reportActions.filter((item) => shouldReportActionBeVisible(item, item.reportActionID)); -} - /** * In some cases, there can be multiple closed report actions in a chat report. * This method returns the last closed report action so we can always show the correct archived report reason. @@ -873,7 +869,6 @@ export { getReportPreviewAction, getSortedReportActions, getSortedReportActionsForDisplay, - getReportActionsWithoutRemoved, isConsecutiveActionMadeByPreviousActor, isCreatedAction, isCreatedTaskReportAction, From 67d0dc822bdeed78ecf6b3c028ddb06f9ee86500 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 12 Jan 2024 17:41:21 +0100 Subject: [PATCH 075/245] renaming and adding comments to pagination handler --- src/libs/actions/Report.ts | 2 +- src/pages/home/ReportScreen.js | 2 +- src/pages/home/report/ReportActionsView.js | 85 +++++++++++----------- 3 files changed, 46 insertions(+), 43 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index b53684c0b6e7..b9365c0c900a 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -465,7 +465,7 @@ function reportActionsExist(reportID: string): boolean { * If a chat with the passed reportID is not found, we will create a chat based on the passed participantList * * @param reportID The ID of the report to open - * @param reportActionID The ID of the report action to navigate to + * @param reportActionID The ID used to fetch a specific range of report actions related to the current reportActionID when opening a chat. * @param participantLoginList The list of users that are included in a new chat, not including the user creating it * @param newReportObject The optimistic report object created when making a new chat, saved as optimistic data * @param parentReportActionID The parent report action that a thread was created from (only passed for new threads) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 527092990583..eb97af5a6090 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -109,6 +109,7 @@ const propTypes = { const defaultProps = { isSidebarLoaded: false, parentReportAction: {}, + allReportActions: {}, report: {}, reportMetadata: { isLoadingInitialReportActions: true, @@ -551,7 +552,6 @@ function ReportScreen({ { - // we don't set edgeID on initial render as linkedID as it should trigger cattedArray after linked message was positioned - const [edgeID, setEdgeID] = useState(''); - const isCuttingForFirstRender = useRef(true); +const usePaginatedReportActionList = (linkedID, allReportActions, fetchNewerReportActions, route, isLoading) => { + // we don't set currentReportActionID on initial render as linkedID as it should trigger visibleReportActions after linked message was positioned + const [currentReportActionID, setCurrentReportActionID] = useState(''); + const isFirstLinkedActionRender = useRef(true); useLayoutEffect(() => { - setEdgeID(''); + setCurrentReportActionID(''); }, [route]); const listID = useMemo(() => { - isCuttingForFirstRender.current = true; + isFirstLinkedActionRender.current = true; listIDCount += 1; return listIDCount; // eslint-disable-next-line react-hooks/exhaustive-deps @@ -122,53 +122,53 @@ const useHandleList = (linkedID, messageArray, fetchNewerActon, route, isLoading return -1; } - return _.findIndex(messageArray, (obj) => String(obj.reportActionID) === String(isCuttingForFirstRender.current ? linkedID : edgeID)); - }, [messageArray, edgeID, linkedID, isLoading]); + return _.findIndex(allReportActions, (obj) => String(obj.reportActionID) === String(isFirstLinkedActionRender.current ? linkedID : currentReportActionID)); + }, [allReportActions, currentReportActionID, linkedID, isLoading]); - const cattedArray = useMemo(() => { + const visibleReportActions = useMemo(() => { if (!linkedID) { - return messageArray; + return allReportActions; } if (isLoading || index === -1) { return []; } - if (isCuttingForFirstRender.current) { - return messageArray.slice(index, messageArray.length); + if (isFirstLinkedActionRender.current) { + return allReportActions.slice(index, allReportActions.length); } const newStartIndex = index >= PAGINATION_SIZE ? index - PAGINATION_SIZE : 0; - return newStartIndex ? messageArray.slice(newStartIndex, messageArray.length) : messageArray; - // edgeID is needed to trigger batching once the report action has been positioned + return newStartIndex ? allReportActions.slice(newStartIndex, allReportActions.length) : allReportActions; + // currentReportActionID is needed to trigger batching once the report action has been positioned // eslint-disable-next-line react-hooks/exhaustive-deps - }, [linkedID, messageArray, index, isLoading, edgeID]); + }, [linkedID, allReportActions, index, isLoading, currentReportActionID]); - const hasMoreCached = cattedArray.length < messageArray.length; - const newestReportAction = lodashGet(cattedArray, '[0]'); + const hasMoreCached = visibleReportActions.length < allReportActions.length; + const newestReportAction = lodashGet(visibleReportActions, '[0]'); - const paginate = useCallback( + const handleReportActionPagination = useCallback( ({firstReportActionID}) => { - // This function is a placeholder as the actual pagination is handled by cattedArray + // This function is a placeholder as the actual pagination is handled by visibleReportActions if (!hasMoreCached) { - isCuttingForFirstRender.current = false; - fetchNewerActon(newestReportAction); + isFirstLinkedActionRender.current = false; + fetchNewerReportActions(newestReportAction); } - if (isCuttingForFirstRender.current) { - isCuttingForFirstRender.current = false; + if (isFirstLinkedActionRender.current) { + isFirstLinkedActionRender.current = false; } - setEdgeID(firstReportActionID); + setCurrentReportActionID(firstReportActionID); }, - [fetchNewerActon, hasMoreCached, newestReportAction], + [fetchNewerReportActions, hasMoreCached, newestReportAction], ); return { - cattedArray, - fetchFunc: paginate, + visibleReportActions, + loadMoreReportActionsHandler: handleReportActionPagination, linkedIdIndex: index, listID, }; }; -function ReportActionsView({reportActions: allReportActions, fetchReport, ...props}) { +function ReportActionsView({reportActions: allReportActions, ...props}) { useCopySelectionHelper(); const reactionListRef = useContext(ReactionListContext); const route = useRoute(); @@ -188,7 +188,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro const prevIsSmallScreenWidthRef = useRef(props.isSmallScreenWidth); const reportID = props.report.reportID; - const isLoading = (!!reportActionID && props.isLoadingInitialReportActions)|| !props.isContentReady; + const isLoading = (!!reportActionID && props.isLoadingInitialReportActions) || !props.isContentReady; /** * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently @@ -206,11 +206,11 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro ); const { - cattedArray: reportActions, - fetchFunc, + visibleReportActions: reportActions, + loadMoreReportActionsHandler, linkedIdIndex, listID, - } = useHandleList(reportActionID, allReportActions, fetchNewerAction, route, isLoading); + } = usePaginatedReportActionList(reportActionID, allReportActions, fetchNewerAction, route, isLoading); const hasNewestReportAction = lodashGet(reportActions[0], 'created') === props.report.lastVisibleActionCreated; const newestReportAction = lodashGet(reportActions, '[0]'); @@ -245,6 +245,9 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro return; } + // This function is triggered when a user clicks on a link to navigate to a report. + // For each link click, we retrieve the report data again, even though it may already be cached. + // There should be only one openReport execution per page start or navigating Report.openReport(reportID, reportActionID); // eslint-disable-next-line react-hooks/exhaustive-deps }, [route]); @@ -330,7 +333,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro }, [props.network.isOffline, props.isLoadingOlderReportActions, oldestReportAction, hasCreatedAction, reportID]); const firstReportActionID = useMemo(() => lodashGet(newestReportAction, 'reportActionID'), [newestReportAction]); - const handleLoadNewerChats = useCallback( + const loadNewerChats = useCallback( // eslint-disable-next-line rulesdir/prefer-early-return () => { if (props.isLoadingInitialReportActions || props.isLoadingOlderReportActions || props.network.isOffline) { @@ -338,7 +341,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro } const isContentSmallerThanList = checkIfContentSmallerThanList(); if ((reportActionID && linkedIdIndex > -1 && !hasNewestReportAction && !isContentSmallerThanList) || (!reportActionID && !hasNewestReportAction && !isContentSmallerThanList)) { - fetchFunc({firstReportActionID}); + loadMoreReportActionsHandler({firstReportActionID}); } }, [ @@ -348,7 +351,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro reportActionID, linkedIdIndex, hasNewestReportAction, - fetchFunc, + loadMoreReportActionsHandler, firstReportActionID, props.network.isOffline, ], @@ -392,7 +395,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro sortedReportActions={reportActions} mostRecentIOUReportActionID={mostRecentIOUReportActionID} loadOlderChats={loadOlderChats} - loadNewerChats={handleLoadNewerChats} + loadNewerChats={loadNewerChats} isLinkingLoader={!!reportActionID && props.isLoadingInitialReportActions} isLoadingInitialReportActions={props.isLoadingInitialReportActions} isLoadingOlderReportActions={props.isLoadingOlderReportActions} From 9c743dcd0b75593e975e23349c6e426eb0c73b30 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 12 Jan 2024 17:45:56 +0100 Subject: [PATCH 076/245] cleanup --- .../getInitialNumToRender/index.native.ts | 0 src/libs/getInitialNumToRender/index.ts | 5 +++++ src/pages/home/report/ReportActionsList.js | 9 ++------- src/pages/home/report/getInitialNumToRender/index.ts | 4 ---- 4 files changed, 7 insertions(+), 11 deletions(-) rename src/{pages/home/report => libs}/getInitialNumToRender/index.native.ts (100%) create mode 100644 src/libs/getInitialNumToRender/index.ts delete mode 100644 src/pages/home/report/getInitialNumToRender/index.ts diff --git a/src/pages/home/report/getInitialNumToRender/index.native.ts b/src/libs/getInitialNumToRender/index.native.ts similarity index 100% rename from src/pages/home/report/getInitialNumToRender/index.native.ts rename to src/libs/getInitialNumToRender/index.native.ts diff --git a/src/libs/getInitialNumToRender/index.ts b/src/libs/getInitialNumToRender/index.ts new file mode 100644 index 000000000000..62b6d6dee275 --- /dev/null +++ b/src/libs/getInitialNumToRender/index.ts @@ -0,0 +1,5 @@ +function getInitialNumToRender(numToRender: number): number { + // For web and desktop environments, it's crucial to set this value equal to or higher than the 'batch per render' setting. If it's set lower, the 'onStartReached' event will be triggered excessively, every time an additional item enters the virtualized list. + return Math.max(numToRender, 50); +} +export default getInitialNumToRender; diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index cdeaebf5f2bc..35bb851deca3 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -14,7 +14,7 @@ import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import DateUtils from '@libs/DateUtils'; -import getPlatform from '@libs/getPlatform'; +import getInitialNumToRender from '@libs/getInitialNumToRender'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -25,7 +25,6 @@ import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import FloatingMessageCounter from './FloatingMessageCounter'; -import getInitialNumToRender from './getInitialNumToRender/index'; import ListBoundaryLoader from './ListBoundaryLoader/ListBoundaryLoader'; import reportActionPropTypes from './reportActionPropTypes'; import ReportActionsListItemRenderer from './ReportActionsListItemRenderer'; @@ -157,8 +156,6 @@ function ReportActionsList({ } return cacheUnreadMarkers.get(report.reportID); }; - const platform = getPlatform(); - const isNative = platform === CONST.PLATFORM.IOS || platform === CONST.PLATFORM.ANDROID; const [currentUnreadMarker, setCurrentUnreadMarker] = useState(markerInit); const scrollingVerticalOffset = useRef(0); const readActionSkipped = useRef(false); @@ -362,12 +359,10 @@ function ReportActionsList({ const availableHeight = windowHeight - (CONST.CHAT_FOOTER_MIN_HEIGHT + variables.contentHeaderHeight); const numToRender = Math.ceil(availableHeight / minimumReportActionHeight); if (linkedReportActionID) { - // For web and desktop environments, it's crucial to set this value equal to or higher than the 'batch per render' setting. If it's set lower, the 'onStartReached' event will be triggered excessively, every time an additional item enters the virtualized list. - return getInitialNumToRender(numToRender); } return numToRender; - }, [styles.chatItem.paddingBottom, styles.chatItem.paddingTop, windowHeight, linkedReportActionID, isNative]); + }, [styles.chatItem.paddingBottom, styles.chatItem.paddingTop, windowHeight, linkedReportActionID]); /** * Thread's divider line should hide when the first chat in the thread is marked as unread. diff --git a/src/pages/home/report/getInitialNumToRender/index.ts b/src/pages/home/report/getInitialNumToRender/index.ts deleted file mode 100644 index eb94492d6ad4..000000000000 --- a/src/pages/home/report/getInitialNumToRender/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -function getInitialNumToRender(numToRender: number): number { - return Math.max(numToRender, 50); -} -export default getInitialNumToRender; From 8887f7c0c3c6079c40ab3e2e5fc5ca858f6b732e Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 12 Jan 2024 18:14:12 +0100 Subject: [PATCH 077/245] clean artifacts from patch --- patches/react-native-web+0.19.9+004+fixLastSpacer.patch | 2 +- src/pages/home/ReportScreen.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/patches/react-native-web+0.19.9+004+fixLastSpacer.patch b/patches/react-native-web+0.19.9+004+fixLastSpacer.patch index 08b5637a50c8..fc48c00094dc 100644 --- a/patches/react-native-web+0.19.9+004+fixLastSpacer.patch +++ b/patches/react-native-web+0.19.9+004+fixLastSpacer.patch @@ -17,7 +17,7 @@ index faeb323..68d740a 100644 /** * Base implementation for the more convenient [``](https://reactnative.dev/docs/flatlist) -@@ -1107,7 +1099,8 @@ class VirtualizedList extends StateSafePureComponent { +@@ -1119,7 +1111,8 @@ class VirtualizedList extends StateSafePureComponent { _keylessItemComponentName = ''; var spacerKey = this._getSpacerKey(!horizontal); var renderRegions = this.state.renderMask.enumerateRegions(); diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index eb97af5a6090..d244cab25c8c 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -64,8 +64,8 @@ const propTypes = { /** The report currently being looked at */ report: reportPropTypes, - /** Array of all report actions for this report */ - allReportActions: PropTypes.arrayOf(PropTypes.shape(reportActionPropTypes)), + /** All the report actions for this report */ + allReportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)), /** The report metadata loading states */ reportMetadata: reportMetadataPropTypes, From c2fc7730f6ee12622f43e16a1038f1ba197428d6 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Sun, 14 Jan 2024 14:37:01 +0100 Subject: [PATCH 078/245] add optional autoscrollToTopThreshold --- .../InvertedFlatList/BaseInvertedFlatList.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx index b3e996cc4e85..e2d52b9b16d1 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx @@ -3,15 +3,23 @@ import React, {forwardRef} from 'react'; import type {FlatListProps} from 'react-native'; import FlatList from '@components/FlatList'; -function BaseInvertedFlatList(props: FlatListProps, ref: ForwardedRef) { +type BaseInvertedFlatListProps = FlatListProps & { + enableAutoscrollToTopThreshold?: boolean; +}; + +const AUTOSCROLL_TO_TOP_THRESHOLD = 128; + +function BaseInvertedFlatList(props: BaseInvertedFlatListProps, ref: ForwardedRef) { + const {enableAutoscrollToTopThreshold, ...rest} = props; return ( From dbcc38b0b6dfb5c794a2e5fa4af74a9147f4da7e Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Sun, 14 Jan 2024 15:18:00 +0100 Subject: [PATCH 079/245] avoid scrollToBottom while linking --- src/pages/home/ReportScreen.js | 5 +--- src/pages/home/report/ReportActionsList.js | 27 ++++++++++++++-------- src/pages/home/report/ReportActionsView.js | 1 + 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index d244cab25c8c..a0d072b421ea 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -268,10 +268,7 @@ function ReportScreen({ const shouldShowSkeleton = isPrepareLinkingToMessage || !isReportReadyForDisplay || isLoadingInitialReportActions || isLoading || (!!reportActionIDFromRoute && reportMetadata.isLoadingInitialReportActions); - const shouldShowReportActionList = useMemo( - () => isReportReadyForDisplay && !isLoadingInitialReportActions && !isLoading, - [isReportReadyForDisplay, isLoading, isLoadingInitialReportActions], - ); + const shouldShowReportActionList = isReportReadyForDisplay && !isLoading; const fetchReport = useCallback(() => { Report.openReport(reportIDFromRoute, reportActionIDFromRoute); diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 35bb851deca3..9bdaa428cce3 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -142,6 +142,7 @@ function ReportActionsList({ listID, onContentSizeChange, reportScrollManager, + enableAutoscrollToTopThreshold, }) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -189,12 +190,12 @@ function ReportActionsList({ }, [opacity]); useEffect(() => { - if (previousLastIndex.current !== lastActionIndex && reportActionSize.current > sortedVisibleReportActions.length) { + if (previousLastIndex.current !== lastActionIndex && reportActionSize.current > sortedVisibleReportActions.length && hasNewestReportAction) { reportScrollManager.scrollToBottom(); } previousLastIndex.current = lastActionIndex; reportActionSize.current = sortedVisibleReportActions.length; - }, [lastActionIndex, sortedVisibleReportActions.length, reportScrollManager]); + }, [lastActionIndex, sortedVisibleReportActions.length, reportScrollManager, hasNewestReportAction]); useEffect(() => { // If the reportID changes, we reset the userActiveSince to null, we need to do it because @@ -277,6 +278,18 @@ function ReportActionsList({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const scrollToBottomForCurrentUserAction = useCallback( + (isFromCurrentUser) => { + // If a new comment is added and it's from the current user scroll to the bottom otherwise leave the user positioned where + // they are now in the list. + if (!isFromCurrentUser || !hasNewestReportAction) { + return; + } + InteractionManager.runAfterInteractions(() => reportScrollManager.scrollToBottom()); + }, + [hasNewestReportAction, reportScrollManager], + ); + useEffect(() => { // Why are we doing this, when in the cleanup of the useEffect we are already calling the unsubscribe function? // Answer: On web, when navigating to another report screen, the previous report screen doesn't get unmounted, @@ -292,14 +305,7 @@ function ReportActionsList({ // This callback is triggered when a new action arrives via Pusher and the event is emitted from Report.js. This allows us to maintain // a single source of truth for the "new action" event instead of trying to derive that a new action has appeared from looking at props. - const unsubscribe = Report.subscribeToNewActionEvent(report.reportID, (isFromCurrentUser) => { - // If a new comment is added and it's from the current user scroll to the bottom otherwise leave the user positioned where - // they are now in the list. - if (!isFromCurrentUser) { - return; - } - InteractionManager.runAfterInteractions(() => reportScrollManager.scrollToBottom()); - }); + const unsubscribe = Report.subscribeToNewActionEvent(report.reportID, scrollToBottomForCurrentUserAction); const cleanup = () => { if (unsubscribe) { @@ -529,6 +535,7 @@ function ReportActionsList({ onScrollToIndexFailed={() => {}} extraData={extraData} key={listID} + enableAutoscrollToTopThreshold={enableAutoscrollToTopThreshold} /> diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index bc06b22b8354..417b35e42ec7 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -404,6 +404,7 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { listID={listID} onContentSizeChange={onContentSizeChange} reportScrollManager={reportScrollManager} + enableAutoscrollToTopThreshold={hasNewestReportAction} /> From fbd2c9dc192a5088671bfa832f651215fcf0ad06 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 15 Jan 2024 10:52:09 +0100 Subject: [PATCH 080/245] ensure Automatic Scrolling Works with Comment Linking --- src/pages/home/report/ReportActionsList.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 9bdaa428cce3..38bf80a8059e 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -176,6 +176,10 @@ function ReportActionsList({ const previousLastIndex = useRef(lastActionIndex); const linkedReportActionID = lodashGet(route, 'params.reportActionID', ''); + const isLastPendingActionIsAdd = lodashGet(sortedVisibleReportActions, [0, 'pendingAction']) === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD; + + // This is utilized for automatically scrolling to the bottom when sending a new message, in cases where comment linking is used and the user is already at the end of the list. + const isNewestActionAvailableAndPendingAdd = linkedReportActionID && isLastPendingActionIsAdd && hasNewestReportAction; // This state is used to force a re-render when the user manually marks a message as unread // by using a timestamp you can force re-renders without having to worry about if another message was marked as unread before @@ -190,12 +194,15 @@ function ReportActionsList({ }, [opacity]); useEffect(() => { - if (previousLastIndex.current !== lastActionIndex && reportActionSize.current > sortedVisibleReportActions.length && hasNewestReportAction) { + if ( + (previousLastIndex.current !== lastActionIndex && reportActionSize.current > sortedVisibleReportActions.length && hasNewestReportAction) || + isNewestActionAvailableAndPendingAdd + ) { reportScrollManager.scrollToBottom(); } previousLastIndex.current = lastActionIndex; reportActionSize.current = sortedVisibleReportActions.length; - }, [lastActionIndex, sortedVisibleReportActions.length, reportScrollManager, hasNewestReportAction]); + }, [lastActionIndex, sortedVisibleReportActions, reportScrollManager, hasNewestReportAction, isLastPendingActionIsAdd, linkedReportActionID, isNewestActionAvailableAndPendingAdd]); useEffect(() => { // If the reportID changes, we reset the userActiveSince to null, we need to do it because From f64e8c818a5222553fb51c334cb7947e267c524d Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 15 Jan 2024 10:53:25 +0100 Subject: [PATCH 081/245] correct linking Issue for the first message in chat --- src/pages/home/report/ReportActionsView.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 417b35e42ec7..a5f293ec340f 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -339,8 +339,14 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { if (props.isLoadingInitialReportActions || props.isLoadingOlderReportActions || props.network.isOffline) { return; } - const isContentSmallerThanList = checkIfContentSmallerThanList(); - if ((reportActionID && linkedIdIndex > -1 && !hasNewestReportAction && !isContentSmallerThanList) || (!reportActionID && !hasNewestReportAction && !isContentSmallerThanList)) { + // Determines if loading older reports is necessary when the content is smaller than the list + // and there are fewer than 23 items, indicating we've reached the oldest message. + const isLoadingOlderReportsFirstNeeded = checkIfContentSmallerThanList() && reportActions.length > 23; + + if ( + (reportActionID && linkedIdIndex > -1 && !hasNewestReportAction && !isLoadingOlderReportsFirstNeeded) || + (!reportActionID && !hasNewestReportAction && !isLoadingOlderReportsFirstNeeded) + ) { loadMoreReportActionsHandler({firstReportActionID}); } }, @@ -354,6 +360,7 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { loadMoreReportActionsHandler, firstReportActionID, props.network.isOffline, + reportActions.length, ], ); From 2d77e7d24b66efb9480093566e8443fb3a7724ce Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 15 Jan 2024 12:09:12 +0100 Subject: [PATCH 082/245] fix test --- src/libs/ReportActionsUtils.ts | 2 +- src/pages/home/report/ReportActionsView.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 1c634584178a..ae5b4e0b5500 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -257,7 +257,7 @@ function getContinuousReportActionChain(sortedReportActions: ReportAction[], id? startIndex--; } - return sortedReportActions.slice(startIndex, endIndex + 1); + return sortedReportActions.slice(startIndex, id ? endIndex + 1 : sortedReportActions.length); } /** diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index a5f293ec340f..d7448d5a3bcd 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -411,7 +411,7 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { listID={listID} onContentSizeChange={onContentSizeChange} reportScrollManager={reportScrollManager} - enableAutoscrollToTopThreshold={hasNewestReportAction} + enableAutoscrollToTopThreshold={hasNewestReportAction && !reportActionID} /> From 052087626b84c00503e82ec64b19da12c6c207f8 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 15 Jan 2024 15:39:27 +0100 Subject: [PATCH 083/245] fix console warning --- .../InvertedFlatList/BaseInvertedFlatList.tsx | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx index e2d52b9b16d1..7405462c585e 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx @@ -1,26 +1,38 @@ import type {ForwardedRef} from 'react'; -import React, {forwardRef} from 'react'; +import React, {forwardRef, useMemo} from 'react'; import type {FlatListProps} from 'react-native'; import FlatList from '@components/FlatList'; type BaseInvertedFlatListProps = FlatListProps & { enableAutoscrollToTopThreshold?: boolean; }; - +type VisibleContentPositionConfig = { + minIndexForVisible: number; + autoscrollToTopThreshold?: number; +}; const AUTOSCROLL_TO_TOP_THRESHOLD = 128; function BaseInvertedFlatList(props: BaseInvertedFlatListProps, ref: ForwardedRef) { const {enableAutoscrollToTopThreshold, ...rest} = props; + + const maintainVisibleContentPosition = useMemo(() => { + const config: VisibleContentPositionConfig = { + // This needs to be 1 to avoid using loading views as anchors. + minIndexForVisible: 1, + }; + + if (enableAutoscrollToTopThreshold) { + config.autoscrollToTopThreshold = AUTOSCROLL_TO_TOP_THRESHOLD; + } + + return config; + }, [enableAutoscrollToTopThreshold]); return ( ); From 008b7fcfd49afe23292c73073fea87b0b25c9399 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 15 Jan 2024 19:07:10 +0100 Subject: [PATCH 084/245] correct scrolling issue during initial chat load --- src/pages/home/ReportScreen.js | 2 +- src/pages/home/report/ReportActionsView.js | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 4870d69fe39e..b12faf24dfc7 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -557,7 +557,7 @@ function ReportScreen({ isLoadingOlderReportActions={reportMetadata.isLoadingOlderReportActions} isComposerFullSize={isComposerFullSize} policy={policy} - isContentReady={!shouldShowSkeleton} + isReadyForCommentLinking={!shouldShowSkeleton} /> )} diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index d7448d5a3bcd..0972145a5eaa 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -98,10 +98,12 @@ let listIDCount = Math.round(Math.random() * 100); * @param {function} fetchNewerReportActions - Function to fetch more messages. * @param {string} route - Current route, used to reset states on route change. * @param {boolean} isLoading - Loading state indicator. + * @param {boolean} triggerListID - Used to trigger a listID change. * @returns {object} An object containing the sliced message array, the pagination function, * index of the linked message, and a unique list ID. */ -const usePaginatedReportActionList = (linkedID, allReportActions, fetchNewerReportActions, route, isLoading) => { +const usePaginatedReportActionList = (linkedID, allReportActions, fetchNewerReportActions, route, isLoading, triggerListID) => { + // triggerListID is used when navigating to a chat with messages loaded from LHN. Typically, these include thread actions, task actions, etc. Since these messages aren't the latest, we don't maintain their position and instead trigger a recalculation of their positioning in the list. // we don't set currentReportActionID on initial render as linkedID as it should trigger visibleReportActions after linked message was positioned const [currentReportActionID, setCurrentReportActionID] = useState(''); const isFirstLinkedActionRender = useRef(true); @@ -115,7 +117,7 @@ const usePaginatedReportActionList = (linkedID, allReportActions, fetchNewerRepo listIDCount += 1; return listIDCount; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [route]); + }, [route, triggerListID]); const index = useMemo(() => { if (!linkedID || isLoading) { @@ -178,7 +180,6 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { const didSubscribeToReportTypingEvents = useRef(false); const contentListHeight = useRef(0); const layoutListHeight = useRef(0); - const hasCachedActions = useInitialValue(() => _.size(props.reportActions) > 0); const mostRecentIOUReportActionID = useInitialValue(() => ReportActionsUtils.getMostRecentIOURequestActionID(props.reportActions)); const {windowHeight} = useWindowDimensions(); const isFocused = useIsFocused(); @@ -188,7 +189,7 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { const prevIsSmallScreenWidthRef = useRef(props.isSmallScreenWidth); const reportID = props.report.reportID; - const isLoading = (!!reportActionID && props.isLoadingInitialReportActions) || !props.isContentReady; + const isLoading = (!!reportActionID && props.isLoadingInitialReportActions) || !props.isReadyForCommentLinking; /** * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently @@ -210,8 +211,8 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { loadMoreReportActionsHandler, linkedIdIndex, listID, - } = usePaginatedReportActionList(reportActionID, allReportActions, fetchNewerAction, route, isLoading); - + } = usePaginatedReportActionList(reportActionID, allReportActions, fetchNewerAction, route, isLoading, props.isLoadingInitialReportActions); + const hasCachedActions = useInitialValue(() => _.size(reportActions) > 0); const hasNewestReportAction = lodashGet(reportActions[0], 'created') === props.report.lastVisibleActionCreated; const newestReportAction = lodashGet(reportActions, '[0]'); const oldestReportAction = useMemo(() => _.last(reportActions), [reportActions]); @@ -320,7 +321,7 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { */ const loadOlderChats = useCallback(() => { // Only fetch more if we are neither already fetching (so that we don't initiate duplicate requests) nor offline. - if (props.network.isOffline || props.isLoadingOlderReportActions) { + if (props.network.isOffline || props.isLoadingOlderReportActions || props.isLoadingInitialReportActions) { return; } @@ -330,7 +331,7 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { } // Retrieve the next REPORT.ACTIONS.LIMIT sized page of comments Report.getOlderActions(reportID, oldestReportAction.reportActionID); - }, [props.network.isOffline, props.isLoadingOlderReportActions, oldestReportAction, hasCreatedAction, reportID]); + }, [props.network.isOffline, props.isLoadingOlderReportActions, props.isLoadingInitialReportActions, oldestReportAction, hasCreatedAction, reportID]); const firstReportActionID = useMemo(() => lodashGet(newestReportAction, 'reportActionID'), [newestReportAction]); const loadNewerChats = useCallback( @@ -423,7 +424,7 @@ ReportActionsView.defaultProps = defaultProps; ReportActionsView.displayName = 'ReportActionsView'; function arePropsEqual(oldProps, newProps) { - if (!_.isEqual(oldProps.isContentReady, newProps.isContentReady)) { + if (!_.isEqual(oldProps.isReadyForCommentLinking, newProps.isReadyForCommentLinking)) { return false; } if (!_.isEqual(oldProps.reportActions, newProps.reportActions)) { From 29f7f23294035eba945ce4f9b608d4c6d3c30b81 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 15 Jan 2024 19:46:48 +0100 Subject: [PATCH 085/245] bring back reportScrollManager to ReportActionList --- src/pages/home/report/ReportActionsList.js | 3 ++- src/pages/home/report/ReportActionsView.js | 3 --- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 38bf80a8059e..d2d00440eb8b 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -11,6 +11,7 @@ import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultPro import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import useReportScrollManager from '@hooks/useReportScrollManager'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import DateUtils from '@libs/DateUtils'; @@ -141,7 +142,6 @@ function ReportActionsList({ isComposerFullSize, listID, onContentSizeChange, - reportScrollManager, enableAutoscrollToTopThreshold, }) { const styles = useThemeStyles(); @@ -150,6 +150,7 @@ function ReportActionsList({ const route = useRoute(); const opacity = useSharedValue(0); const userActiveSince = useRef(null); + const reportScrollManager = useReportScrollManager(); const markerInit = () => { if (!cacheUnreadMarkers.has(report.reportID)) { diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 0972145a5eaa..c838411fa80f 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -11,7 +11,6 @@ import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withW import useCopySelectionHelper from '@hooks/useCopySelectionHelper'; import useInitialValue from '@hooks/useInitialValue'; import usePrevious from '@hooks/usePrevious'; -import useReportScrollManager from '@hooks/useReportScrollManager'; import useWindowDimensions from '@hooks/useWindowDimensions'; import compose from '@libs/compose'; import getIsReportFullyVisible from '@libs/getIsReportFullyVisible'; @@ -174,7 +173,6 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { useCopySelectionHelper(); const reactionListRef = useContext(ReactionListContext); const route = useRoute(); - const reportScrollManager = useReportScrollManager(); const reportActionID = lodashGet(route, 'params.reportActionID', null); const didLayout = useRef(false); const didSubscribeToReportTypingEvents = useRef(false); @@ -411,7 +409,6 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { policy={props.policy} listID={listID} onContentSizeChange={onContentSizeChange} - reportScrollManager={reportScrollManager} enableAutoscrollToTopThreshold={hasNewestReportAction && !reportActionID} /> From 59aaf17a94c733eb078d92d831767561dd3b0b1f Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 16 Jan 2024 13:33:43 +0100 Subject: [PATCH 086/245] update naming conventions and typing --- .../InvertedFlatList/BaseInvertedFlatList.tsx | 18 ++++++++---------- src/pages/home/ReportScreen.js | 4 ++-- src/pages/home/report/ReportActionsList.js | 6 +++--- src/pages/home/report/ReportActionsView.js | 2 +- .../index.native.ts | 0 .../index.ts | 0 6 files changed, 14 insertions(+), 16 deletions(-) rename src/{libs/getInitialNumToRender => pages/home/report/getInitialNumReportActionsToRender}/index.native.ts (100%) rename src/{libs/getInitialNumToRender => pages/home/report/getInitialNumReportActionsToRender}/index.ts (100%) diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx index 7405462c585e..ebb4d01d1f23 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx @@ -1,32 +1,30 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef, useMemo} from 'react'; -import type {FlatListProps} from 'react-native'; +import type {FlatListProps, ScrollViewProps} from 'react-native'; import FlatList from '@components/FlatList'; type BaseInvertedFlatListProps = FlatListProps & { - enableAutoscrollToTopThreshold?: boolean; -}; -type VisibleContentPositionConfig = { - minIndexForVisible: number; - autoscrollToTopThreshold?: number; + shouldEnableAutoscrollToTopThreshold?: boolean; }; + const AUTOSCROLL_TO_TOP_THRESHOLD = 128; function BaseInvertedFlatList(props: BaseInvertedFlatListProps, ref: ForwardedRef) { - const {enableAutoscrollToTopThreshold, ...rest} = props; + const {shouldEnableAutoscrollToTopThreshold, ...rest} = props; const maintainVisibleContentPosition = useMemo(() => { - const config: VisibleContentPositionConfig = { + const config: ScrollViewProps['maintainVisibleContentPosition'] = { // This needs to be 1 to avoid using loading views as anchors. minIndexForVisible: 1, }; - if (enableAutoscrollToTopThreshold) { + if (shouldEnableAutoscrollToTopThreshold) { config.autoscrollToTopThreshold = AUTOSCROLL_TO_TOP_THRESHOLD; } return config; - }, [enableAutoscrollToTopThreshold]); + }, [shouldEnableAutoscrollToTopThreshold]); + return ( { if (_.isEmpty(allReportActions)) { @@ -268,7 +268,7 @@ function ReportScreen({ }, [report, reportIDFromRoute]); const shouldShowSkeleton = - isPrepareLinkingToMessage || !isReportReadyForDisplay || isLoadingInitialReportActions || isLoading || (!!reportActionIDFromRoute && reportMetadata.isLoadingInitialReportActions); + isLinkingToMessage || !isReportReadyForDisplay || isLoadingInitialReportActions || isLoading || (!!reportActionIDFromRoute && reportMetadata.isLoadingInitialReportActions); const shouldShowReportActionList = isReportReadyForDisplay && !isLoading; diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index d2d00440eb8b..6aa0daa139ba 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -15,7 +15,6 @@ import useReportScrollManager from '@hooks/useReportScrollManager'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import DateUtils from '@libs/DateUtils'; -import getInitialNumToRender from '@libs/getInitialNumToRender'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -26,6 +25,7 @@ import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import FloatingMessageCounter from './FloatingMessageCounter'; +import getInitialNumToRender from './getInitialNumReportActionsToRender'; import ListBoundaryLoader from './ListBoundaryLoader/ListBoundaryLoader'; import reportActionPropTypes from './reportActionPropTypes'; import ReportActionsListItemRenderer from './ReportActionsListItemRenderer'; @@ -142,7 +142,7 @@ function ReportActionsList({ isComposerFullSize, listID, onContentSizeChange, - enableAutoscrollToTopThreshold, + shouldEnableAutoscrollToTopThreshold, }) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -543,7 +543,7 @@ function ReportActionsList({ onScrollToIndexFailed={() => {}} extraData={extraData} key={listID} - enableAutoscrollToTopThreshold={enableAutoscrollToTopThreshold} + shouldEnableAutoscrollToTopThreshold={shouldEnableAutoscrollToTopThreshold} /> diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index c838411fa80f..e61ebf0f11f4 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -409,7 +409,7 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { policy={props.policy} listID={listID} onContentSizeChange={onContentSizeChange} - enableAutoscrollToTopThreshold={hasNewestReportAction && !reportActionID} + shouldEnableAutoscrollToTopThreshold={hasNewestReportAction && !reportActionID} /> diff --git a/src/libs/getInitialNumToRender/index.native.ts b/src/pages/home/report/getInitialNumReportActionsToRender/index.native.ts similarity index 100% rename from src/libs/getInitialNumToRender/index.native.ts rename to src/pages/home/report/getInitialNumReportActionsToRender/index.native.ts diff --git a/src/libs/getInitialNumToRender/index.ts b/src/pages/home/report/getInitialNumReportActionsToRender/index.ts similarity index 100% rename from src/libs/getInitialNumToRender/index.ts rename to src/pages/home/report/getInitialNumReportActionsToRender/index.ts From 00b041ca4d94fd0ddeaf925a28e34983420cff18 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 17 Jan 2024 15:32:08 +0100 Subject: [PATCH 087/245] fix typo and add CheckForPreviousReportActionID migration --- src/components/FlatList/MVCPFlatList.js | 2 +- src/libs/migrateOnyx.js | 3 ++- src/pages/home/report/ReportActionsList.js | 7 +++++-- .../report/getInitialNumReportActionsToRender/index.ts | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/components/FlatList/MVCPFlatList.js b/src/components/FlatList/MVCPFlatList.js index b738dedd91af..abc3be4e2052 100644 --- a/src/components/FlatList/MVCPFlatList.js +++ b/src/components/FlatList/MVCPFlatList.js @@ -43,7 +43,7 @@ const MVCPFlatList = React.forwardRef(({maintainVisibleContentPosition, horizont if (scrollRef.current == null) { return 0; } - return horizontal ? scrollRef.current.getScrollableNode().scrollLeft : scrollRef.current.getScrollableNode().scrollTop; + return horizontal ? scrollRef.current?.getScrollableNode().scrollLeft : scrollRef.current?.getScrollableNode().scrollTop; }, [horizontal]); const getContentView = React.useCallback(() => scrollRef.current?.getScrollableNode()?.childNodes[0], []); diff --git a/src/libs/migrateOnyx.js b/src/libs/migrateOnyx.js index 9b8b4056e3e5..036750fa5d4f 100644 --- a/src/libs/migrateOnyx.js +++ b/src/libs/migrateOnyx.js @@ -5,6 +5,7 @@ import PersonalDetailsByAccountID from './migrations/PersonalDetailsByAccountID' import RemoveEmptyReportActionsDrafts from './migrations/RemoveEmptyReportActionsDrafts'; import RenameReceiptFilename from './migrations/RenameReceiptFilename'; import TransactionBackupsToCollection from './migrations/TransactionBackupsToCollection'; +import CheckForPreviousReportActionID from './migrations/CheckForPreviousReportActionID'; export default function () { const startTime = Date.now(); @@ -12,7 +13,7 @@ export default function () { return new Promise((resolve) => { // Add all migrations to an array so they are executed in order - const migrationPromises = [PersonalDetailsByAccountID, RenameReceiptFilename, KeyReportActionsDraftByReportActionID, TransactionBackupsToCollection, RemoveEmptyReportActionsDrafts]; + const migrationPromises = [CheckForPreviousReportActionID, PersonalDetailsByAccountID, RenameReceiptFilename, KeyReportActionsDraftByReportActionID, TransactionBackupsToCollection, RemoveEmptyReportActionsDrafts]; // Reduce all promises down to a single promise. All promises run in a linear fashion, waiting for the // previous promise to finish before moving onto the next one. diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 6aa0daa139ba..dc58f5a42cbe 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -180,7 +180,7 @@ function ReportActionsList({ const isLastPendingActionIsAdd = lodashGet(sortedVisibleReportActions, [0, 'pendingAction']) === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD; // This is utilized for automatically scrolling to the bottom when sending a new message, in cases where comment linking is used and the user is already at the end of the list. - const isNewestActionAvailableAndPendingAdd = linkedReportActionID && isLastPendingActionIsAdd && hasNewestReportAction; + const isNewestActionAvailableAndPendingAdd = linkedReportActionID && isLastPendingActionIsAdd; // This state is used to force a re-render when the user manually marks a message as unread // by using a timestamp you can force re-renders without having to worry about if another message was marked as unread before @@ -199,7 +199,10 @@ function ReportActionsList({ (previousLastIndex.current !== lastActionIndex && reportActionSize.current > sortedVisibleReportActions.length && hasNewestReportAction) || isNewestActionAvailableAndPendingAdd ) { - reportScrollManager.scrollToBottom(); + // runAfterInteractions is used for isNewestActionAvailableAndPendingAdd + InteractionManager.runAfterInteractions(() => { + reportScrollManager.scrollToBottom(); + }); } previousLastIndex.current = lastActionIndex; reportActionSize.current = sortedVisibleReportActions.length; diff --git a/src/pages/home/report/getInitialNumReportActionsToRender/index.ts b/src/pages/home/report/getInitialNumReportActionsToRender/index.ts index 62b6d6dee275..68ff8c4cab3f 100644 --- a/src/pages/home/report/getInitialNumReportActionsToRender/index.ts +++ b/src/pages/home/report/getInitialNumReportActionsToRender/index.ts @@ -1,5 +1,5 @@ function getInitialNumToRender(numToRender: number): number { - // For web and desktop environments, it's crucial to set this value equal to or higher than the 'batch per render' setting. If it's set lower, the 'onStartReached' event will be triggered excessively, every time an additional item enters the virtualized list. + // For web and desktop environments, it's crucial to set this value equal to or higher than the maxToRenderPerBatch setting. If it's set lower, the 'onStartReached' event will be triggered excessively, every time an additional item enters the virtualized list. return Math.max(numToRender, 50); } export default getInitialNumToRender; From cc523b2401a2171418761795792bb3a56461811f Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 17 Jan 2024 15:47:06 +0100 Subject: [PATCH 088/245] fix 'new message' appearing on initial loading --- src/pages/home/report/ReportActionsList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index dc58f5a42cbe..b5e36c292643 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -519,7 +519,7 @@ function ReportActionsList({ return ( <> From f3b7090d9210b6e502d103870f22a6f453ad42d7 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 17 Jan 2024 15:59:40 +0100 Subject: [PATCH 089/245] lint --- src/libs/migrateOnyx.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/libs/migrateOnyx.js b/src/libs/migrateOnyx.js index 036750fa5d4f..a2a846bddf5f 100644 --- a/src/libs/migrateOnyx.js +++ b/src/libs/migrateOnyx.js @@ -1,11 +1,11 @@ import _ from 'underscore'; import Log from './Log'; +import CheckForPreviousReportActionID from './migrations/CheckForPreviousReportActionID'; import KeyReportActionsDraftByReportActionID from './migrations/KeyReportActionsDraftByReportActionID'; import PersonalDetailsByAccountID from './migrations/PersonalDetailsByAccountID'; import RemoveEmptyReportActionsDrafts from './migrations/RemoveEmptyReportActionsDrafts'; import RenameReceiptFilename from './migrations/RenameReceiptFilename'; import TransactionBackupsToCollection from './migrations/TransactionBackupsToCollection'; -import CheckForPreviousReportActionID from './migrations/CheckForPreviousReportActionID'; export default function () { const startTime = Date.now(); @@ -13,7 +13,14 @@ export default function () { return new Promise((resolve) => { // Add all migrations to an array so they are executed in order - const migrationPromises = [CheckForPreviousReportActionID, PersonalDetailsByAccountID, RenameReceiptFilename, KeyReportActionsDraftByReportActionID, TransactionBackupsToCollection, RemoveEmptyReportActionsDrafts]; + const migrationPromises = [ + CheckForPreviousReportActionID, + PersonalDetailsByAccountID, + RenameReceiptFilename, + KeyReportActionsDraftByReportActionID, + TransactionBackupsToCollection, + RemoveEmptyReportActionsDrafts, + ]; // Reduce all promises down to a single promise. All promises run in a linear fashion, waiting for the // previous promise to finish before moving onto the next one. From c77d7b1427fb52331ed03d351e8d3c26758afc62 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 18 Jan 2024 16:36:57 +0100 Subject: [PATCH 090/245] adjust gap handling in response to REPORTPREVIEW movement --- src/libs/ReportActionsUtils.ts | 2 +- src/pages/home/report/ReportActionsView.js | 27 ++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index fcf897292bb4..f6d8c139b802 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -257,7 +257,7 @@ function getContinuousReportActionChain(sortedReportActions: ReportAction[], id? startIndex--; } - return sortedReportActions.slice(startIndex, id ? endIndex + 1 : sortedReportActions.length); + return sortedReportActions.slice(startIndex, endIndex + 1); } /** diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index e61ebf0f11f4..123875f1f28a 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -388,6 +388,33 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { [hasCachedActions], ); + useEffect(() => { + // Temporary solution for handling REPORTPREVIEW. More details: https://expensify.slack.com/archives/C035J5C9FAP/p1705417778466539?thread_ts=1705035404.136629&cid=C035J5C9FAP + // This code should be removed once REPORTPREVIEW is no longer repositioned. + // We need to call openReport for gaps created by moving REPORTPREVIEW, which causes mismatches in previousReportActionID and reportActionID of adjacent reportActions. The server returns the correct sequence, allowing us to overwrite incorrect data with the correct one. + + const shouldOpenReport = + !hasCreatedAction && + props.isReadyForCommentLinking && + reportActions.length < 24 && + !props.isLoadingInitialReportAction && + !props.isLoadingOlderReportActions && + !props.isLoadingNewerReportActions; + + if (shouldOpenReport) { + Report.openReport(reportID, reportActionID); + } + }, [ + hasCreatedAction, + reportID, + reportActions, + reportActionID, + props.isReadyForCommentLinking, + props.isLoadingOlderReportActions, + props.isLoadingNewerReportActions, + props.isLoadingInitialReportAction, + ]); + // Comments have not loaded at all yet do nothing if (!_.size(reportActions)) { return null; From 2b8a4d788526576a411ad7db036b04338ca1ebdc Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 18 Jan 2024 16:45:30 +0100 Subject: [PATCH 091/245] undo adjust gap handling in response to REPORTPREVIEW movement --- src/pages/home/report/ReportActionsView.js | 27 ---------------------- 1 file changed, 27 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 123875f1f28a..e61ebf0f11f4 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -388,33 +388,6 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { [hasCachedActions], ); - useEffect(() => { - // Temporary solution for handling REPORTPREVIEW. More details: https://expensify.slack.com/archives/C035J5C9FAP/p1705417778466539?thread_ts=1705035404.136629&cid=C035J5C9FAP - // This code should be removed once REPORTPREVIEW is no longer repositioned. - // We need to call openReport for gaps created by moving REPORTPREVIEW, which causes mismatches in previousReportActionID and reportActionID of adjacent reportActions. The server returns the correct sequence, allowing us to overwrite incorrect data with the correct one. - - const shouldOpenReport = - !hasCreatedAction && - props.isReadyForCommentLinking && - reportActions.length < 24 && - !props.isLoadingInitialReportAction && - !props.isLoadingOlderReportActions && - !props.isLoadingNewerReportActions; - - if (shouldOpenReport) { - Report.openReport(reportID, reportActionID); - } - }, [ - hasCreatedAction, - reportID, - reportActions, - reportActionID, - props.isReadyForCommentLinking, - props.isLoadingOlderReportActions, - props.isLoadingNewerReportActions, - props.isLoadingInitialReportAction, - ]); - // Comments have not loaded at all yet do nothing if (!_.size(reportActions)) { return null; From 104884a5d839d37137d95074c1ad54dc17e28b7c Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 18 Jan 2024 18:17:30 +0100 Subject: [PATCH 092/245] implement a blocking view when the linked link does not belong to the current report --- src/components/FlatList/MVCPFlatList.js | 2 +- src/pages/home/ReportScreen.js | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/components/FlatList/MVCPFlatList.js b/src/components/FlatList/MVCPFlatList.js index abc3be4e2052..1b6bca14ecf3 100644 --- a/src/components/FlatList/MVCPFlatList.js +++ b/src/components/FlatList/MVCPFlatList.js @@ -43,7 +43,7 @@ const MVCPFlatList = React.forwardRef(({maintainVisibleContentPosition, horizont if (scrollRef.current == null) { return 0; } - return horizontal ? scrollRef.current?.getScrollableNode().scrollLeft : scrollRef.current?.getScrollableNode().scrollTop; + return horizontal ? scrollRef.current?.getScrollableNode()?.scrollLeft : scrollRef.current?.getScrollableNode()?.scrollTop; }, [horizontal]); const getContentView = React.useCallback(() => scrollRef.current?.getScrollableNode()?.childNodes[0], []); diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index b111273b2085..9ae6dca7519d 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -6,8 +6,10 @@ import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import Banner from '@components/Banner'; +import BlockingView from '@components/BlockingViews/BlockingView'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import DragAndDropProvider from '@components/DragAndDrop/Provider'; +import * as Illustrations from '@components/Icon/Illustrations'; import MoneyReportHeader from '@components/MoneyReportHeader'; import MoneyRequestHeader from '@components/MoneyRequestHeader'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; @@ -33,6 +35,7 @@ import * as ReportUtils from '@libs/ReportUtils'; import personalDetailsPropType from '@pages/personalDetailsPropType'; import reportMetadataPropTypes from '@pages/reportMetadataPropTypes'; import reportPropTypes from '@pages/reportPropTypes'; +import variables from '@styles/variables'; import * as ComposerActions from '@userActions/Composer'; import * as Report from '@userActions/Report'; import * as Task from '@userActions/Task'; @@ -503,6 +506,25 @@ function ReportScreen({ } }, [reportMetadata.isLoadingInitialReportActions]); + const onLinkPress = () => { + Navigation.setParams({reportActionID: ''}); + fetchReport(); + }; + + if (!shouldShowSkeleton && reportActionIDFromRoute && _.isEmpty(reportActions) && !isLinkingToMessage) { + return ( + + ); + } + return ( From 3ce108095265034916b8885a2af4351a1454a8fa Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 18 Jan 2024 18:28:35 +0100 Subject: [PATCH 093/245] bring back 'adjust gap handling in response to REPORTPREVIEW movement' --- src/pages/home/report/ReportActionsView.js | 28 ++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index e61ebf0f11f4..3fd1fb44be20 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -388,6 +388,34 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { [hasCachedActions], ); + useEffect(() => { + // Temporary solution for handling REPORTPREVIEW. More details: https://expensify.slack.com/archives/C035J5C9FAP/p1705417778466539?thread_ts=1705035404.136629&cid=C035J5C9FAP + // This code should be removed once REPORTPREVIEW is no longer repositioned. + // We need to call openReport for gaps created by moving REPORTPREVIEW, which causes mismatches in previousReportActionID and reportActionID of adjacent reportActions. The server returns the correct sequence, allowing us to overwrite incorrect data with the correct one. + + const shouldOpenReport = + !hasCreatedAction && + props.isReadyForCommentLinking && + reportActions.length < 24 && + reportActions.length > 1 && + !props.isLoadingInitialReportAction && + !props.isLoadingOlderReportActions && + !props.isLoadingNewerReportActions; + + if (shouldOpenReport) { + Report.openReport(reportID, reportActionID); + } + }, [ + hasCreatedAction, + reportID, + reportActions, + reportActionID, + props.isReadyForCommentLinking, + props.isLoadingOlderReportActions, + props.isLoadingNewerReportActions, + props.isLoadingInitialReportAction, + ]); + // Comments have not loaded at all yet do nothing if (!_.size(reportActions)) { return null; From 825a5439fad4d931ac32fb9d9a4b4c4331e3528b Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 19 Jan 2024 16:54:20 +0100 Subject: [PATCH 094/245] lint --- src/pages/home/report/ReportActionsView.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 3fd1fb44be20..8b95131dea42 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -188,6 +188,7 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { const prevIsSmallScreenWidthRef = useRef(props.isSmallScreenWidth); const reportID = props.report.reportID; const isLoading = (!!reportActionID && props.isLoadingInitialReportActions) || !props.isReadyForCommentLinking; + const firstReportActionName = lodashGet(reportActions, ['0', 'actionName']); /** * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently @@ -211,8 +212,8 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { listID, } = usePaginatedReportActionList(reportActionID, allReportActions, fetchNewerAction, route, isLoading, props.isLoadingInitialReportActions); const hasCachedActions = useInitialValue(() => _.size(reportActions) > 0); - const hasNewestReportAction = lodashGet(reportActions[0], 'created') === props.report.lastVisibleActionCreated; - const newestReportAction = lodashGet(reportActions, '[0]'); + const hasNewestReportAction = lodashGet(reportActions, ['0', 'created']) === props.report.lastVisibleActionCreated; + const newestReportAction = lodashGet(reportActions, ['0']); const oldestReportAction = useMemo(() => _.last(reportActions), [reportActions]); const hasCreatedAction = lodashGet(oldestReportAction, 'actionName') === CONST.REPORT.ACTIONS.TYPE.CREATED; @@ -394,6 +395,7 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { // We need to call openReport for gaps created by moving REPORTPREVIEW, which causes mismatches in previousReportActionID and reportActionID of adjacent reportActions. The server returns the correct sequence, allowing us to overwrite incorrect data with the correct one. const shouldOpenReport = + firstReportActionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && !hasCreatedAction && props.isReadyForCommentLinking && reportActions.length < 24 && @@ -410,6 +412,7 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { reportID, reportActions, reportActionID, + firstReportActionName, props.isReadyForCommentLinking, props.isLoadingOlderReportActions, props.isLoadingNewerReportActions, From e7ee260d308d5827a1dcf8aee8f972a076e2fb06 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 19 Jan 2024 17:06:31 +0100 Subject: [PATCH 095/245] lint after merge --- src/pages/home/report/ReportActionsView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 8b95131dea42..a08a202f845a 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -188,7 +188,6 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { const prevIsSmallScreenWidthRef = useRef(props.isSmallScreenWidth); const reportID = props.report.reportID; const isLoading = (!!reportActionID && props.isLoadingInitialReportActions) || !props.isReadyForCommentLinking; - const firstReportActionName = lodashGet(reportActions, ['0', 'actionName']); /** * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently @@ -216,6 +215,7 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { const newestReportAction = lodashGet(reportActions, ['0']); const oldestReportAction = useMemo(() => _.last(reportActions), [reportActions]); const hasCreatedAction = lodashGet(oldestReportAction, 'actionName') === CONST.REPORT.ACTIONS.TYPE.CREATED; + const firstReportActionName = lodashGet(reportActions, ['0', 'actionName']); /** * @returns {Boolean} From 07929ee2966356371516c2a747a787c251cf35f5 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 19 Jan 2024 18:06:03 +0100 Subject: [PATCH 096/245] fix test --- tests/ui/UnreadIndicatorsTest.js | 18 +++++++++--------- tests/utils/TestHelper.js | 4 +++- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/tests/ui/UnreadIndicatorsTest.js b/tests/ui/UnreadIndicatorsTest.js index e4d4d877f66b..97d50fb392f3 100644 --- a/tests/ui/UnreadIndicatorsTest.js +++ b/tests/ui/UnreadIndicatorsTest.js @@ -249,15 +249,15 @@ function signInAndGetAppWithUnreadChat() { }, ], }, - 1: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 10), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '1'), - 2: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 20), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '2'), - 3: TestHelper.buildTestReportComment(reportAction3CreatedDate, USER_B_ACCOUNT_ID, '3'), - 4: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 40), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '4'), - 5: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 50), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '5'), - 6: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 60), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '6'), - 7: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 70), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '7'), - 8: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 80), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '8'), - 9: TestHelper.buildTestReportComment(reportAction9CreatedDate, USER_B_ACCOUNT_ID, '9'), + 1: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 10), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '1', createdReportActionID), + 2: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 20), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '2', '1'), + 3: TestHelper.buildTestReportComment(reportAction3CreatedDate, USER_B_ACCOUNT_ID, '3', '2'), + 4: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 40), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '4', '3'), + 5: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 50), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '5', '4'), + 6: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 60), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '6', '5'), + 7: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 70), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '7', '6'), + 8: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 80), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '8', '7'), + 9: TestHelper.buildTestReportComment(reportAction9CreatedDate, USER_B_ACCOUNT_ID, '9', '8'), }); await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, { [USER_B_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_B_EMAIL, USER_B_ACCOUNT_ID, 'B'), diff --git a/tests/utils/TestHelper.js b/tests/utils/TestHelper.js index dd95ab4efb67..4a331496541a 100644 --- a/tests/utils/TestHelper.js +++ b/tests/utils/TestHelper.js @@ -198,9 +198,10 @@ function setPersonalDetails(login, accountID) { * @param {String} created * @param {Number} actorAccountID * @param {String} actionID + * @param {String} previousReportActionID * @returns {Object} */ -function buildTestReportComment(created, actorAccountID, actionID = null) { +function buildTestReportComment(created, actorAccountID, actionID = null, previousReportActionID = null) { const reportActionID = actionID || NumberUtils.rand64(); return { actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, @@ -209,6 +210,7 @@ function buildTestReportComment(created, actorAccountID, actionID = null) { message: [{type: 'COMMENT', html: `Comment ${actionID}`, text: `Comment ${actionID}`}], reportActionID, actorAccountID, + previousReportActionID, }; } From c1c260fb20dad42c5ee43b84a80cdcc1686dd5df Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Sat, 20 Jan 2024 18:15:31 +0100 Subject: [PATCH 097/245] refactor autosScrollToTopThreshold --- .../InvertedFlatList/BaseInvertedFlatList.tsx | 8 +++---- src/pages/home/report/ReportActionsList.js | 4 ++-- src/pages/home/report/ReportActionsView.js | 24 +++++++++++++++---- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx index ebb4d01d1f23..e007f63c8e97 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx @@ -4,13 +4,13 @@ import type {FlatListProps, ScrollViewProps} from 'react-native'; import FlatList from '@components/FlatList'; type BaseInvertedFlatListProps = FlatListProps & { - shouldEnableAutoscrollToTopThreshold?: boolean; + shouldEnableAutoScrollToTopThreshold?: boolean; }; const AUTOSCROLL_TO_TOP_THRESHOLD = 128; function BaseInvertedFlatList(props: BaseInvertedFlatListProps, ref: ForwardedRef) { - const {shouldEnableAutoscrollToTopThreshold, ...rest} = props; + const {shouldEnableAutoScrollToTopThreshold, ...rest} = props; const maintainVisibleContentPosition = useMemo(() => { const config: ScrollViewProps['maintainVisibleContentPosition'] = { @@ -18,12 +18,12 @@ function BaseInvertedFlatList(props: BaseInvertedFlatListProps, ref: Forwa minIndexForVisible: 1, }; - if (shouldEnableAutoscrollToTopThreshold) { + if (shouldEnableAutoScrollToTopThreshold) { config.autoscrollToTopThreshold = AUTOSCROLL_TO_TOP_THRESHOLD; } return config; - }, [shouldEnableAutoscrollToTopThreshold]); + }, [shouldEnableAutoScrollToTopThreshold]); return ( {}} extraData={extraData} key={listID} - shouldEnableAutoscrollToTopThreshold={shouldEnableAutoscrollToTopThreshold} + shouldEnableAutoScrollToTopThreshold={shouldEnableAutoScrollToTopThreshold} /> diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index a08a202f845a..00584859c4f9 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -2,6 +2,7 @@ import {useIsFocused, useRoute} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; +import {InteractionManager} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import networkPropTypes from '@components/networkPropTypes'; @@ -184,7 +185,7 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { const prevNetworkRef = useRef(props.network); const prevAuthTokenType = usePrevious(props.session.authTokenType); - + const [isInitialLinkedView, setIsInitialLinkedView] = useState(false); const prevIsSmallScreenWidthRef = useRef(props.isSmallScreenWidth); const reportID = props.report.reportID; const isLoading = (!!reportActionID && props.isLoadingInitialReportActions) || !props.isReadyForCommentLinking; @@ -393,13 +394,12 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { // Temporary solution for handling REPORTPREVIEW. More details: https://expensify.slack.com/archives/C035J5C9FAP/p1705417778466539?thread_ts=1705035404.136629&cid=C035J5C9FAP // This code should be removed once REPORTPREVIEW is no longer repositioned. // We need to call openReport for gaps created by moving REPORTPREVIEW, which causes mismatches in previousReportActionID and reportActionID of adjacent reportActions. The server returns the correct sequence, allowing us to overwrite incorrect data with the correct one. - const shouldOpenReport = firstReportActionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && !hasCreatedAction && props.isReadyForCommentLinking && reportActions.length < 24 && - reportActions.length > 1 && + reportActions.length >= 1 && !props.isLoadingInitialReportAction && !props.isLoadingOlderReportActions && !props.isLoadingNewerReportActions; @@ -419,10 +419,26 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { props.isLoadingInitialReportAction, ]); + // Check if the first report action in the list is the one we're currently linked to + const isTheFirstReportActionIsLinked = firstReportActionID !== reportActionID; + + useEffect(() => { + if (isTheFirstReportActionIsLinked) { + // this should be applied after we navigated to linked reportAction + InteractionManager.runAfterInteractions(() => { + setIsInitialLinkedView(true); + }); + } else { + setIsInitialLinkedView(false); + } + }, [isTheFirstReportActionIsLinked]); + // Comments have not loaded at all yet do nothing if (!_.size(reportActions)) { return null; } + // AutoScroll is disabled when we do linking to a specific reportAction + const shouldEnableAutoScroll = hasNewestReportAction && (!reportActionID || isInitialLinkedView); return ( <> @@ -440,7 +456,7 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { policy={props.policy} listID={listID} onContentSizeChange={onContentSizeChange} - shouldEnableAutoscrollToTopThreshold={hasNewestReportAction && !reportActionID} + shouldEnableAutoScrollToTopThreshold={shouldEnableAutoScroll} /> From 14ab0d60bebe946ac1a14de387fdefbf4ddd7b3c Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 23 Jan 2024 14:41:20 +0100 Subject: [PATCH 098/245] determine if a linked report action is deleted --- src/pages/home/ReportScreen.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 9ae6dca7519d..2a62cdf43466 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -511,7 +511,14 @@ function ReportScreen({ fetchReport(); }; - if (!shouldShowSkeleton && reportActionIDFromRoute && _.isEmpty(reportActions) && !isLinkingToMessage) { + const isLinkedReportActionDeleted = useMemo(() => { + if (!reportActionIDFromRoute) { + return false; + } + return ReportActionsUtils.isDeletedAction(allReportActions[reportActionIDFromRoute]); + }, [reportActionIDFromRoute, allReportActions]); + + if (isLinkedReportActionDeleted || (!shouldShowSkeleton && reportActionIDFromRoute && _.isEmpty(reportActions) && !isLinkingToMessage)) { return ( Date: Wed, 24 Jan 2024 16:42:32 +0100 Subject: [PATCH 099/245] refactor linking loader --- src/pages/home/ReportScreen.js | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 2a62cdf43466..2fd3c4ed3f21 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -2,7 +2,7 @@ import {useIsFocused} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; -import {View} from 'react-native'; +import {InteractionManager, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import Banner from '@components/Banner'; @@ -173,7 +173,6 @@ function ReportScreen({ const firstRenderRef = useRef(true); const reportIDFromRoute = getReportID(route); const reportActionIDFromRoute = lodashGet(route, 'params.reportActionID', null); - const shouldTriggerLoadingRef = useRef(!!reportActionIDFromRoute); const prevReport = usePrevious(report); const prevUserLeavingStatus = usePrevious(userLeavingStatus); const [isLinkingToMessage, setLinkingToMessage] = useState(!!reportActionIDFromRoute); @@ -187,6 +186,11 @@ function ReportScreen({ return _.filter(currentRangeOfReportActions, (reportAction) => ReportActionsUtils.shouldReportActionBeVisible(reportAction, reportAction.reportActionID)); }, [reportActionIDFromRoute, allReportActions]); + // Define here because reportActions are recalculated before mount, allowing data to display faster than useEffect can trigger. If we have cached reportActions, they will be shown immediately. We aim to display a loader first, then fetch relevant reportActions, and finally show them. + useLayoutEffect(() => { + setLinkingToMessage(!!reportActionIDFromRoute); + }, [route, reportActionIDFromRoute]); + const [isBannerVisible, setIsBannerVisible] = useState(true); const [listHeight, setListHeight] = useState(0); const [scrollPosition, setScrollPosition] = useState({}); @@ -197,12 +201,6 @@ function ReportScreen({ Performance.markStart(CONST.TIMING.CHAT_RENDER); } - // Define here because reportActions are recalculated before mount, allowing data to display faster than useEffect can trigger. If we have cached reportActions, they will be shown immediately. We aim to display a loader first, then fetch relevant reportActions, and finally show them. - useLayoutEffect(() => { - shouldTriggerLoadingRef.current = !!reportActionIDFromRoute; - setLinkingToMessage(!!reportActionIDFromRoute); - }, [route, reportActionIDFromRoute]); - const {addWorkspaceRoomOrChatPendingAction, addWorkspaceRoomOrChatErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report); const screenWrapperStyle = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}]; @@ -497,13 +495,9 @@ function ReportScreen({ // This helps in tracking from the moment 'route' triggers useMemo until isLoadingInitialReportActions becomes true. It prevents blinking when loading reportActions from cache. useEffect(() => { - if (reportMetadata.isLoadingInitialReportActions && shouldTriggerLoadingRef.current) { - shouldTriggerLoadingRef.current = false; - return; - } - if (!reportMetadata.isLoadingInitialReportActions && !shouldTriggerLoadingRef.current) { + InteractionManager.runAfterInteractions(() => { setLinkingToMessage(false); - } + }); }, [reportMetadata.isLoadingInitialReportActions]); const onLinkPress = () => { @@ -515,7 +509,7 @@ function ReportScreen({ if (!reportActionIDFromRoute) { return false; } - return ReportActionsUtils.isDeletedAction(allReportActions[reportActionIDFromRoute]); + return !_.isEmpty(allReportActions[reportActionIDFromRoute]) && ReportActionsUtils.isDeletedAction(allReportActions[reportActionIDFromRoute]); }, [reportActionIDFromRoute, allReportActions]); if (isLinkedReportActionDeleted || (!shouldShowSkeleton && reportActionIDFromRoute && _.isEmpty(reportActions) && !isLinkingToMessage)) { From c3d93ea4ef92087aafc186ffa1f4dd909aa98cb4 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 24 Jan 2024 16:46:43 +0100 Subject: [PATCH 100/245] hide loading indicator when delete --- src/pages/home/report/ReportActionsList.js | 9 +++++++-- src/pages/home/report/ReportActionsView.js | 8 +++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 1b6de509000a..8bc26eea7d70 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -177,7 +177,9 @@ function ReportActionsList({ const previousLastIndex = useRef(lastActionIndex); const linkedReportActionID = lodashGet(route, 'params.reportActionID', ''); - const isLastPendingActionIsAdd = lodashGet(sortedVisibleReportActions, [0, 'pendingAction']) === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD; + const lastPendingAction = lodashGet(sortedReportActions, [0, 'pendingAction']) + const isLastPendingActionIsAdd = lastPendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD; + const isLastPendingActionIsDelete = lastPendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; // This is utilized for automatically scrolling to the bottom when sending a new message, in cases where comment linking is used and the user is already at the end of the list. const isNewestActionAvailableAndPendingAdd = linkedReportActionID && isLastPendingActionIsAdd; @@ -516,10 +518,13 @@ function ReportActionsList({ ); }, [isLoadingNewerReportActions, isOffline]); + // When performing comment linking, initially 25 items are added to the list. Subsequent fetches add 15 items from the cache or 50 items from the server. + // This is to ensure that the user is able to see the 'scroll to newer comments' button when they do comment linking and have not reached the end of the list yet. + const canScrollToNewerComments = !isLoadingInitialReportActions && !hasNewestReportAction && sortedReportActions.length > 25 && !isLastPendingActionIsDelete; return ( <> diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 00584859c4f9..8b674b9e05a9 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -337,7 +337,12 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { const loadNewerChats = useCallback( // eslint-disable-next-line rulesdir/prefer-early-return () => { - if (props.isLoadingInitialReportActions || props.isLoadingOlderReportActions || props.network.isOffline) { + if ( + props.isLoadingInitialReportActions || + props.isLoadingOlderReportActions || + props.network.isOffline || + newestReportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE + ) { return; } // Determines if loading older reports is necessary when the content is smaller than the list @@ -362,6 +367,7 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { firstReportActionID, props.network.isOffline, reportActions.length, + newestReportAction, ], ); From 1d4aef156bcac0759cb1c5dbd51189071dffb6c8 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 25 Jan 2024 11:43:54 +0100 Subject: [PATCH 101/245] fix scrolling to the bottom on action deletion from the same account on a different device --- .../InvertedFlatList/BaseInvertedFlatList.tsx | 1 + src/pages/home/report/ReportActionsList.js | 21 +++++++------------ src/pages/home/report/ReportActionsView.js | 4 ++-- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx index e007f63c8e97..d83e54f74d66 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx @@ -39,3 +39,4 @@ function BaseInvertedFlatList(props: BaseInvertedFlatListProps, ref: Forwa BaseInvertedFlatList.displayName = 'BaseInvertedFlatList'; export default forwardRef(BaseInvertedFlatList); +export {AUTOSCROLL_TO_TOP_THRESHOLD}; diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 8bc26eea7d70..3a397b4f6cf6 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -6,6 +6,7 @@ import {DeviceEventEmitter, InteractionManager} from 'react-native'; import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import _ from 'underscore'; import InvertedFlatList from '@components/InvertedFlatList'; +import {AUTOSCROLL_TO_TOP_THRESHOLD} from '@components/InvertedFlatList/BaseInvertedFlatList'; import {withPersonalDetails} from '@components/OnyxProvider'; import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; @@ -177,12 +178,7 @@ function ReportActionsList({ const previousLastIndex = useRef(lastActionIndex); const linkedReportActionID = lodashGet(route, 'params.reportActionID', ''); - const lastPendingAction = lodashGet(sortedReportActions, [0, 'pendingAction']) - const isLastPendingActionIsAdd = lastPendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD; - const isLastPendingActionIsDelete = lastPendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; - - // This is utilized for automatically scrolling to the bottom when sending a new message, in cases where comment linking is used and the user is already at the end of the list. - const isNewestActionAvailableAndPendingAdd = linkedReportActionID && isLastPendingActionIsAdd; + const isLastPendingActionIsDelete = lodashGet(sortedReportActions, [0, 'pendingAction']) === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; // This state is used to force a re-render when the user manually marks a message as unread // by using a timestamp you can force re-renders without having to worry about if another message was marked as unread before @@ -198,17 +194,16 @@ function ReportActionsList({ useEffect(() => { if ( - (previousLastIndex.current !== lastActionIndex && reportActionSize.current > sortedVisibleReportActions.length && hasNewestReportAction) || - isNewestActionAvailableAndPendingAdd + scrollingVerticalOffset.current < AUTOSCROLL_TO_TOP_THRESHOLD && + previousLastIndex.current !== lastActionIndex && + reportActionSize.current > sortedVisibleReportActions.length && + hasNewestReportAction ) { - // runAfterInteractions is used for isNewestActionAvailableAndPendingAdd - InteractionManager.runAfterInteractions(() => { - reportScrollManager.scrollToBottom(); - }); + reportScrollManager.scrollToBottom(); } previousLastIndex.current = lastActionIndex; reportActionSize.current = sortedVisibleReportActions.length; - }, [lastActionIndex, sortedVisibleReportActions, reportScrollManager, hasNewestReportAction, isLastPendingActionIsAdd, linkedReportActionID, isNewestActionAvailableAndPendingAdd]); + }, [lastActionIndex, sortedVisibleReportActions, reportScrollManager, hasNewestReportAction, linkedReportActionID]); useEffect(() => { // If the reportID changes, we reset the userActiveSince to null, we need to do it because diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 8b674b9e05a9..eb6c7634f536 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -426,7 +426,7 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { ]); // Check if the first report action in the list is the one we're currently linked to - const isTheFirstReportActionIsLinked = firstReportActionID !== reportActionID; + const isTheFirstReportActionIsLinked = firstReportActionID === reportActionID; useEffect(() => { if (isTheFirstReportActionIsLinked) { @@ -444,7 +444,7 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { return null; } // AutoScroll is disabled when we do linking to a specific reportAction - const shouldEnableAutoScroll = hasNewestReportAction && (!reportActionID || isInitialLinkedView); + const shouldEnableAutoScroll = hasNewestReportAction && (!reportActionID || !isInitialLinkedView); return ( <> From 4f4a2c21d112e39a41b2339acc0046893114a83c Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 29 Jan 2024 12:14:43 +0100 Subject: [PATCH 102/245] 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 103/245] 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 104/245] 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 105/245] 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 106/245] update after merge --- src/libs/API/parameters/OpenReportParams.ts | 1 + src/pages/home/report/ReportActionsView.js | 21 ++++++++++++++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/libs/API/parameters/OpenReportParams.ts b/src/libs/API/parameters/OpenReportParams.ts index 477a002516de..8eaed6bc0fde 100644 --- a/src/libs/API/parameters/OpenReportParams.ts +++ b/src/libs/API/parameters/OpenReportParams.ts @@ -1,5 +1,6 @@ type OpenReportParams = { reportID: string; + reportActionID?: string; emailList?: string; accountIDList?: string; parentReportActionID?: string; diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index ec227f21518c..31656060c7f2 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -186,7 +186,7 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { const mostRecentIOUReportActionID = useMemo(() => ReportActionsUtils.getMostRecentIOURequestActionID(props.reportActions), [props.reportActions]); const prevNetworkRef = useRef(props.network); const prevAuthTokenType = usePrevious(props.session.authTokenType); - const [isInitialLinkedView, setIsInitialLinkedView] = useState(false); + const [isInitialLinkedView, setIsInitialLinkedView] = useState(!!reportActionID); const prevIsSmallScreenWidthRef = useRef(props.isSmallScreenWidth); const reportID = props.report.reportID; const isLoading = (!!reportActionID && props.isLoadingInitialReportActions) || !props.isReadyForCommentLinking; @@ -430,14 +430,25 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { const isTheFirstReportActionIsLinked = firstReportActionID === reportActionID; useEffect(() => { + let timerId; + if (isTheFirstReportActionIsLinked) { - // this should be applied after we navigated to linked reportAction + setIsInitialLinkedView(true); + } else { + // After navigating to the linked reportAction, apply this to correctly set + // `autoscrollToTopThreshold` prop when linking to a specific reportAction. InteractionManager.runAfterInteractions(() => { - setIsInitialLinkedView(true); + // Using a short delay to ensure the view is updated after interactions + timerId = setTimeout(() => setIsInitialLinkedView(false), 10); }); - } else { - setIsInitialLinkedView(false); } + + return () => { + if (!timerId) { + return; + } + clearTimeout(timerId); + }; }, [isTheFirstReportActionIsLinked]); // Comments have not loaded at all yet do nothing From 28eae1dd56a9d69ba0753c4794cfd1b88ef5225a Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 1 Feb 2024 17:26:12 +0100 Subject: [PATCH 107/245] WIP handling whisperedToAccountIDs, INVITE_TO_ROOM, CLOSED, CREATED --- src/libs/ReportActionsUtils.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 559994e2a172..c921a323b0e7 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -237,7 +237,14 @@ function getContinuousReportActionChain(sortedReportActions: ReportAction[], id? // Iterate forwards through the array, starting from endIndex. This loop checks the continuity of actions by: // 1. Comparing the current item's previousReportActionID with the next item's reportActionID. // This ensures that we are moving in a sequence of related actions from newer to older. - while (endIndex < sortedReportActions.length - 1 && sortedReportActions[endIndex].previousReportActionID === sortedReportActions[endIndex + 1].reportActionID) { + while ( + (endIndex < sortedReportActions.length - 1 && sortedReportActions[endIndex].previousReportActionID === sortedReportActions[endIndex + 1].reportActionID) || + sortedReportActions[endIndex + 1]?.whisperedToAccountIDs?.length || + sortedReportActions[endIndex]?.whisperedToAccountIDs?.length || + sortedReportActions[endIndex]?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || + sortedReportActions[endIndex + 1]?.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED || + sortedReportActions[endIndex + 1]?.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED + ) { endIndex++; } @@ -529,6 +536,7 @@ function getSortedReportActionsForDisplay(reportActions: ReportActions | null, s let filteredReportActions; if (shouldIncludeInvisibleActions) { + // filteredReportActions = Object.values(reportActions ?? {}).filter((action) => action?.resolution !== CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE) filteredReportActions = Object.values(reportActions ?? {}); } else { filteredReportActions = Object.entries(reportActions ?? {}) From 150e9a4b3e1be297b4c19bc01eb9f5fac0c66c08 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 2 Feb 2024 10:55:38 +0100 Subject: [PATCH 108/245] 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 109/245] 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 110/245] fix after merge (add allReportActions to memo) --- src/pages/home/ReportScreen.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 406c47b847af..888b05c1bed7 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -708,7 +708,7 @@ export default compose( ReportScreen, (prevProps, nextProps) => prevProps.isSidebarLoaded === nextProps.isSidebarLoaded && - _.isEqual(prevProps.reportActions, nextProps.reportActions) && + _.isEqual(prevProps.allReportActions, nextProps.allReportActions) && _.isEqual(prevProps.reportMetadata, nextProps.reportMetadata) && prevProps.isComposerFullSize === nextProps.isComposerFullSize && _.isEqual(prevProps.betas, nextProps.betas) && From 17c4fba70c1d4f8a9aaebd8e7a7c045227ba83ab Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 7 Feb 2024 11:27:38 +0100 Subject: [PATCH 111/245] 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 112/245] 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 bb4ce473435efb3e5def3c75b07dea516aeaeea3 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Thu, 15 Feb 2024 04:37:42 +0500 Subject: [PATCH 113/245] 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 114/245] 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 64ccb57734d6492a088525bc1e83d8fa8373eb40 Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Fri, 16 Feb 2024 17:59:45 -0500 Subject: [PATCH 115/245] add new isDeleted property --- src/libs/ReportActionsUtils.ts | 7 +++++-- src/types/onyx/ReportAction.ts | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index f9e2cd2220b3..68cf7fbf2239 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -94,9 +94,12 @@ function isCreatedAction(reportAction: OnyxEntry): boolean { } function isDeletedAction(reportAction: OnyxEntry): boolean { - // A deleted comment has either an empty array or an object with html field with empty string as value const message = reportAction?.message ?? []; - return message.length === 0 || message[0]?.html === ''; + + // A legacy deleted comment has either an empty array or an object with html field with empty string as value + const isLegacyDeletedComment = reportAction?.actionName === 'ADDCOMMENT' && (!message.length || !message[0]?.html); + + return message[0]?.isDeleted || isLegacyDeletedComment; } function isDeletedParentAction(reportAction: OnyxEntry): boolean { diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index 8f732a253cb5..0a9d7bc4c2f4 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -68,6 +68,8 @@ type Message = { /** resolution for actionable mention whisper */ resolution?: ValueOf | null; + + isDeleted?: boolean; }; type ImageMetadata = { From e974f9f0e2db1155407442c8494200f5b33711a7 Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 20 Feb 2024 10:59:26 +0700 Subject: [PATCH 116/245] fix: lint --- src/pages/RoomMembersPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/RoomMembersPage.tsx b/src/pages/RoomMembersPage.tsx index 8005465146f8..94f1e0bfc59c 100644 --- a/src/pages/RoomMembersPage.tsx +++ b/src/pages/RoomMembersPage.tsx @@ -198,7 +198,7 @@ function RoomMembersPage({report, session, policies}: RoomMembersPageProps) { }, ], pendingAction: pendingAccounts?.[accountID]?.pendingAction, - errors: pendingAccounts?.[accountID]?.errors + errors: pendingAccounts?.[accountID]?.errors, }); }); @@ -212,7 +212,7 @@ function RoomMembersPage({report, session, policies}: RoomMembersPageProps) { */ const dismissError = useCallback( (item: ListItem) => { - if(!item.accountID){ + if (!item.accountID) { return; } if (item.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { From 62c8956937bbb1146f78ab5b26f7d9b0f13be348 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 21 Feb 2024 10:08:52 +0700 Subject: [PATCH 117/245] add es text --- src/languages/es.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index d97721eeb163..feb0bda93565 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2101,8 +2101,8 @@ export default { noActivityYet: 'Sin actividad todavía', people: { error: { - genericAdd: '', - genericRemove: '', + genericAdd: 'Hubo un problema al agregar este miembro de la sala de chat.', + genericRemove: 'Hubo un problema al eliminar a ese miembro de la sala de chat.', }, }, }, From beb654bcedce02f066bc7eef7a2f62327118f31e Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 22 Feb 2024 10:26:08 +0700 Subject: [PATCH 118/245] fix: change en texts --- src/languages/en.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index e02c8e7fad86..657148a34226 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2076,8 +2076,8 @@ export default { noActivityYet: 'No activity yet', people: { error: { - genericAdd: 'There was a problem adding this room member.', - genericRemove: 'There was a problem removing that room member.', + genericAdd: 'There was a problem adding this member to the room.', + genericRemove: 'There was a problem removing this member from the room.', }, }, }, From 9054944828891069752adb31d42745713f29dba6 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 27 Feb 2024 18:31:12 +0530 Subject: [PATCH 119/245] Initial implementation of button --- assets/images/track-expense.svg | 1 + src/CONST.ts | 1 + src/components/Icon/Expensicons.ts | 2 ++ src/libs/Permissions.ts | 5 ++++ .../FloatingActionButtonAndPopover.js | 23 +++++++++++++------ 5 files changed, 25 insertions(+), 7 deletions(-) create mode 100644 assets/images/track-expense.svg diff --git a/assets/images/track-expense.svg b/assets/images/track-expense.svg new file mode 100644 index 000000000000..6fb7eb9befec --- /dev/null +++ b/assets/images/track-expense.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/CONST.ts b/src/CONST.ts index 8abd4c087b16..a26bd61f3b1e 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -307,6 +307,7 @@ const CONST = { BETA_COMMENT_LINKING: 'commentLinking', VIOLATIONS: 'violations', REPORT_FIELDS: 'reportFields', + TRACK_EXPENSE: 'trackExpense', }, BUTTON_STATES: { DEFAULT: 'default', diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index 2a7ed30abf1a..5bdec7ca7174 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -135,6 +135,7 @@ import Sync from '@assets/images/sync.svg'; import Task from '@assets/images/task.svg'; import ThreeDots from '@assets/images/three-dots.svg'; import ThumbsUp from '@assets/images/thumbs-up.svg'; +import TrackExpense from '@assets/images/track-expense.svg'; import Transfer from '@assets/images/transfer.svg'; import Trashcan from '@assets/images/trashcan.svg'; import Unlock from '@assets/images/unlock.svg'; @@ -302,4 +303,5 @@ export { ChatBubbleAdd, ChatBubbleUnread, Lightbulb, + TrackExpense, }; diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index ce5e0e674c59..52276783576d 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -26,6 +26,10 @@ function canUseViolations(betas: OnyxEntry): boolean { return !!betas?.includes(CONST.BETAS.VIOLATIONS) || canUseAllBetas(betas); } +function canUseTrackExpense(betas: OnyxEntry): boolean { + return !!betas?.includes(CONST.BETAS.TRACK_EXPENSE) || canUseAllBetas(betas); +} + /** * Link previews are temporarily disabled. */ @@ -39,5 +43,6 @@ export default { canUseCommentLinking, canUseLinkPreviews, canUseViolations, + canUseTrackExpense, canUseReportFields, }; diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js index 573cbe370aa7..85c5ddd55dd8 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js @@ -9,6 +9,7 @@ import withNavigation from '@components/withNavigation'; import withNavigationFocus from '@components/withNavigationFocus'; import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; import useLocalize from '@hooks/useLocalize'; +import usePermissions from '@hooks/usePermissions'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; @@ -75,6 +76,7 @@ function FloatingActionButtonAndPopover(props) { const {translate} = useLocalize(); const [isCreateMenuActive, setIsCreateMenuActive] = useState(false); const fabRef = useRef(null); + const {canUseTrackExpense} = usePermissions(); const prevIsFocused = usePrevious(props.isFocused); @@ -179,13 +181,20 @@ function FloatingActionButtonAndPopover(props) { text: translate('iou.sendMoney'), onSelected: () => interceptAnonymousUser(() => IOU.startMoneyRequest(CONST.IOU.TYPE.SEND)), }, - ...[ - { - icon: Expensicons.Task, - text: translate('newTaskPage.assignTask'), - onSelected: () => interceptAnonymousUser(() => Task.clearOutTaskInfoAndNavigate()), - }, - ], + ...(canUseTrackExpense + ? [ + { + icon: Expensicons.TrackExpense, + text: 'Track Expense', + onSelected: () => interceptAnonymousUser(() => IOU.startMoneyRequest(CONST.IOU.TYPE.SEND)), + }, + ] + : []), + { + icon: Expensicons.Task, + text: translate('newTaskPage.assignTask'), + onSelected: () => interceptAnonymousUser(() => Task.clearOutTaskInfoAndNavigate()), + }, { icon: Expensicons.Heart, text: translate('sidebarScreen.saveTheWorld'), From c532babaa7b0fcbc8845cbf7c1578705c4f34453 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 27 Feb 2024 19:17:03 +0530 Subject: [PATCH 120/245] Fixed svg --- assets/images/track-expense.svg | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/assets/images/track-expense.svg b/assets/images/track-expense.svg index 6fb7eb9befec..c15f28b72dd7 100644 --- a/assets/images/track-expense.svg +++ b/assets/images/track-expense.svg @@ -1 +1,9 @@ - \ No newline at end of file + + + + + + + + + \ No newline at end of file From 2feddbf24d392f85e37c3999b9f5e22f75f518e5 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 27 Feb 2024 21:19:04 +0530 Subject: [PATCH 121/245] Trying to start track expense flow --- src/CONST.ts | 2 ++ src/libs/IOUUtils.ts | 2 +- src/libs/ReportUtils.ts | 8 ++++++++ .../FloatingActionButtonAndPopover.js | 20 ++++++++++++++++++- src/pages/iou/request/IOURequestStartPage.js | 3 ++- .../request/step/IOURequestStepScan/index.js | 2 +- .../step/IOURequestStepScan/index.native.js | 2 +- 7 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index a26bd61f3b1e..9b0afa627672 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -690,6 +690,7 @@ const CONST = { DOMAIN_ALL: 'domainAll', POLICY_ROOM: 'policyRoom', POLICY_EXPENSE_CHAT: 'policyExpenseChat', + SELF_DM: 'selfDM', }, WORKSPACE_CHAT_ROOMS: { ANNOUNCE: '#announce', @@ -1264,6 +1265,7 @@ const CONST = { SEND: 'send', SPLIT: 'split', REQUEST: 'request', + TRACK_EXPENSE: 'track-expense', }, REQUEST_TYPE: { DISTANCE: 'distance', diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts index 56ac47676a37..07bb22f43b31 100644 --- a/src/libs/IOUUtils.ts +++ b/src/libs/IOUUtils.ts @@ -104,7 +104,7 @@ function isIOUReportPendingCurrencyConversion(iouReport: Report): boolean { * Checks if the iou type is one of request, send, or split. */ function isValidMoneyRequestType(iouType: string): boolean { - const moneyRequestType: string[] = [CONST.IOU.TYPE.REQUEST, CONST.IOU.TYPE.SPLIT, CONST.IOU.TYPE.SEND]; + const moneyRequestType: string[] = [CONST.IOU.TYPE.REQUEST, CONST.IOU.TYPE.SPLIT, CONST.IOU.TYPE.SEND, CONST.IOU.TYPE.TRACK_EXPENSE]; return moneyRequestType.includes(iouType); } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 747ba27780a3..e634689041d5 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4248,6 +4248,10 @@ function canRequestMoney(report: OnyxEntry, policy: OnyxEntry, o return !isPolicyExpenseChat(report) || isOwnPolicyExpenseChat; } +function isSelfDM(report: OnyxEntry): boolean { + return getChatType(report) === CONST.REPORT.CHAT_TYPE.SELF_DM; +} + /** * Helper method to define what money request options we want to show for particular method. * There are 3 money request options: Request, Split and Send: @@ -4284,6 +4288,10 @@ function getMoneyRequestOptions(report: OnyxEntry, policy: OnyxEntry 1; let options: Array> = []; + if (isSelfDM(report)) { + options = [CONST.IOU.TYPE.TRACK_EXPENSE]; + } + // User created policy rooms and default rooms like #admins or #announce will always have the Split Bill option // unless there are no other participants at all (e.g. #admins room for a policy with only 1 admin) // DM chats will have the Split Bill option only when there are at least 2 other people in the chat. diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js index 85c5ddd55dd8..bcf9c77ac2f7 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js @@ -51,6 +51,12 @@ const propTypes = { name: PropTypes.string, }), + /** The account details for the logged in user */ + account: PropTypes.shape({ + /** Whether or not the user is a policy admin */ + selfDMReportID: PropTypes.string, + }), + /** Indicated whether the report data is loading */ isLoading: PropTypes.bool, @@ -63,6 +69,7 @@ const defaultProps = { allPolicies: {}, isLoading: false, innerRef: null, + account: {}, }; /** @@ -186,7 +193,15 @@ function FloatingActionButtonAndPopover(props) { { icon: Expensicons.TrackExpense, text: 'Track Expense', - onSelected: () => interceptAnonymousUser(() => IOU.startMoneyRequest(CONST.IOU.TYPE.SEND)), + onSelected: () => + interceptAnonymousUser(() => + IOU.startMoneyRequest_temporaryForRefactor( + CONST.IOU.TYPE.TRACK_EXPENSE, + // When starting to create a track expense from the global FAB, we need to retrieve selfDM reportID. + // If it doesn't exist, we generate a random optimistic reportID and use it for all of the routes in the creation flow. + props.account.selfDMReportID || ReportUtils.generateReportID(), + ), + ), }, ] : []), @@ -255,5 +270,8 @@ export default compose( isLoading: { key: ONYXKEYS.IS_LOADING_APP, }, + account: { + key: ONYXKEYS.ACCOUNT, + }, }), )(FloatingActionButtonAndPopoverWithRef); diff --git a/src/pages/iou/request/IOURequestStartPage.js b/src/pages/iou/request/IOURequestStartPage.js index 05e3d7c96311..8b7ef2a17973 100644 --- a/src/pages/iou/request/IOURequestStartPage.js +++ b/src/pages/iou/request/IOURequestStartPage.js @@ -75,6 +75,7 @@ function IOURequestStartPage({ [CONST.IOU.TYPE.REQUEST]: translate('iou.requestMoney'), [CONST.IOU.TYPE.SEND]: translate('iou.sendMoney'), [CONST.IOU.TYPE.SPLIT]: translate('iou.splitBill'), + [CONST.IOU.TYPE.TRACK_EXPENSE]: 'Track Expense', }; const transactionRequestType = useRef(TransactionUtils.getRequestType(transaction)); const previousIOURequestType = usePrevious(transactionRequestType.current); @@ -103,7 +104,7 @@ function IOURequestStartPage({ const isExpenseChat = ReportUtils.isPolicyExpenseChat(report); const isExpenseReport = ReportUtils.isExpenseReport(report); - const shouldDisplayDistanceRequest = isExpenseChat || isExpenseReport || isFromGlobalCreate; + const shouldDisplayDistanceRequest = isExpenseChat || isExpenseReport || isFromGlobalCreate || iouType === CONST.IOU.TYPE.TRACK_EXPENSE; // Allow the user to create the request if we are creating the request in global menu or the report can create the request const isAllowedToCreateRequest = _.isEmpty(report.reportID) || ReportUtils.canCreateRequest(report, policy, iouType); diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.js b/src/pages/iou/request/step/IOURequestStepScan/index.js index 7da97c34cc2b..7de121af52b4 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.js @@ -122,7 +122,7 @@ function IOURequestStepScan({ } // If the transaction was created from the global create, the person needs to select participants, so take them there. - if (isFromGlobalCreate) { + if (isFromGlobalCreate && iouType !== CONST.IOU.TYPE.TRACK_EXPENSE) { Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_PARTICIPANTS.getRoute(iouType, transactionID, reportID)); return; } diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.js b/src/pages/iou/request/step/IOURequestStepScan/index.native.js index b23420b5ef69..d6b90c0de439 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.js @@ -182,7 +182,7 @@ function IOURequestStepScan({ } // If the transaction was created from the global create, the person needs to select participants, so take them there. - if (isFromGlobalCreate) { + if (isFromGlobalCreate && iouType !== CONST.IOU.TYPE.TRACK_EXPENSE) { Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_PARTICIPANTS.getRoute(iouType, transactionID, reportID)); return; } From f2741741f2ea556ab643f07dbd3e3a8fac2dd853 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 27 Feb 2024 22:09:35 +0530 Subject: [PATCH 122/245] Minor fixes --- ...eyTemporaryForRefactorRequestConfirmationList.js | 7 +++++-- .../iou/request/step/IOURequestStepConfirmation.js | 13 +++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 8eeaeaf87eff..894525ca7a02 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -247,6 +247,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const isTypeRequest = iouType === CONST.IOU.TYPE.REQUEST; const isTypeSplit = iouType === CONST.IOU.TYPE.SPLIT; const isTypeSend = iouType === CONST.IOU.TYPE.SEND; + const isTypeTrackExpense = iouType === CONST.IOU.TYPE.TRACK_EXPENSE; const {unit, rate, currency} = mileageRate; const distance = lodashGet(transaction, 'routes.route0.distance', 0); @@ -370,7 +371,9 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const splitOrRequestOptions = useMemo(() => { let text; - if (isTypeSplit && iouAmount === 0) { + if (isTypeTrackExpense) { + text = "Track Expense"; + } else if (isTypeSplit && iouAmount === 0) { text = translate('iou.split'); } else if ((receiptPath && isTypeRequest) || isDistanceRequestWithPendingRoute) { text = translate('iou.request'); @@ -387,7 +390,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ value: iouType, }, ]; - }, [isTypeSplit, isTypeRequest, iouType, iouAmount, receiptPath, formattedAmount, isDistanceRequestWithPendingRoute, translate]); + }, [isTypeTrackExpense, isTypeSplit, iouAmount, receiptPath, isTypeRequest, isDistanceRequestWithPendingRoute, iouType, translate, formattedAmount]); const selectedParticipants = useMemo(() => _.filter(pickedParticipants, (participant) => participant.selected), [pickedParticipants]); const personalDetailsOfPayee = useMemo(() => payeePersonalDetails || currentUserPersonalDetails, [payeePersonalDetails, currentUserPersonalDetails]); diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.js b/src/pages/iou/request/step/IOURequestStepConfirmation.js index 0744fbd600a7..01b0c74d6f64 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.js +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.js @@ -95,7 +95,16 @@ function IOURequestStepConfirmation({ const transactionTaxCode = transaction.taxRate && transaction.taxRate.keyForList; const transactionTaxAmount = transaction.taxAmount; const requestType = TransactionUtils.getRequestType(transaction); - const headerTitle = iouType === CONST.IOU.TYPE.SPLIT ? translate('iou.split') : translate(TransactionUtils.getHeaderTitleTranslationKey(transaction)); + const headerTitle = useMemo(() => { + if (iouType === CONST.IOU.TYPE.SPLIT) { + return translate('iou.splitBill'); + } + if (iouType === CONST.IOU.TYPE.TRACK_EXPENSE) { + return 'Track Expense'; + } + return translate(TransactionUtils.getHeaderTitleTranslationKey(transaction)); + } + , [iouType, transaction, translate]); const participants = useMemo( () => _.map(transaction.participants, (participant) => { @@ -407,7 +416,7 @@ function IOURequestStepConfirmation({ Date: Tue, 27 Feb 2024 18:26:51 +0100 Subject: [PATCH 123/245] VideoPlayerThumbnail migreated to ts --- ...rThumbnail.js => VideoPlayerThumbnail.tsx} | 22 +++---------------- src/components/VideoPlayerPreview/types.ts | 9 ++++++++ 2 files changed, 12 insertions(+), 19 deletions(-) rename src/components/VideoPlayerPreview/{VideoPlayerThumbnail.js => VideoPlayerThumbnail.tsx} (78%) create mode 100644 src/components/VideoPlayerPreview/types.ts diff --git a/src/components/VideoPlayerPreview/VideoPlayerThumbnail.js b/src/components/VideoPlayerPreview/VideoPlayerThumbnail.tsx similarity index 78% rename from src/components/VideoPlayerPreview/VideoPlayerThumbnail.js rename to src/components/VideoPlayerPreview/VideoPlayerThumbnail.tsx index 595442c317d5..896c8778ba06 100644 --- a/src/components/VideoPlayerPreview/VideoPlayerThumbnail.js +++ b/src/components/VideoPlayerPreview/VideoPlayerThumbnail.tsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import React from 'react'; import {View} from 'react-native'; import Icon from '@components/Icon'; @@ -12,20 +11,9 @@ import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as ReportUtils from '@libs/ReportUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; +import VideoPlayerThumbnailProps from './types'; -const propTypes = { - onPress: PropTypes.func.isRequired, - - accessibilityLabel: PropTypes.string.isRequired, - - thumbnailUrl: PropTypes.string, -}; - -const defaultProps = { - thumbnailUrl: undefined, -}; - -function VideoPlayerThumbnail({thumbnailUrl, onPress, accessibilityLabel}) { +function VideoPlayerThumbnail({thumbnailUrl = undefined, onPress, accessibilityLabel}: VideoPlayerThumbnailProps) { const styles = useThemeStyles(); return ( @@ -48,9 +36,7 @@ function VideoPlayerThumbnail({thumbnailUrl, onPress, accessibilityLabel}) { onPress={onPress} onPressIn={() => DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} onPressOut={() => ControlSelection.unblock()} - onLongPress={(event) => - showContextMenuForReport(event, anchor, (report && report.reportID) || '', action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report)) - } + onLongPress={(event) => showContextMenuForReport(event, anchor, report?.reportID ?? '', action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report))} > void; + accessibilityLabel: string; +}; + +export default VideoPlayerThumbnailProps; From bd012151b8117eeab27e2438ea23331f7c8c591f Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 28 Feb 2024 10:27:27 +0700 Subject: [PATCH 124/245] remove report generic errors --- src/languages/en.ts | 6 ------ src/languages/es.ts | 6 ------ src/libs/actions/Report.ts | 4 ++-- 3 files changed, 2 insertions(+), 14 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index c73eb967ffbb..91f3198ca1e4 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2110,12 +2110,6 @@ export default { genericUpdateReportFieldFailureMessage: 'Unexpected error while updating the field, please try again later', genericUpdateReporNameEditFailureMessage: 'Unexpected error while renaming the report, please try again later', noActivityYet: 'No activity yet', - people: { - error: { - genericAdd: 'There was a problem adding this member to the room.', - genericRemove: 'There was a problem removing this member from the room.', - }, - }, }, 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 c59517c3d3e4..d17355e69d55 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2138,12 +2138,6 @@ export default { genericUpdateReportFieldFailureMessage: 'Error inesperado al actualizar el campo. Por favor, inténtalo más tarde', genericUpdateReporNameEditFailureMessage: 'Error inesperado al cambiar el nombre del informe. Vuelva a intentarlo más tarde.', noActivityYet: 'Sin actividad todavía', - people: { - error: { - genericAdd: 'Hubo un problema al agregar este miembro de la sala de chat.', - genericRemove: 'Hubo un problema al eliminar a ese miembro de la sala de chat.', - }, - }, }, 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 40f8a6275285..7248db9cd03b 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -2386,7 +2386,7 @@ function inviteToRoom(reportID: string, inviteeEmailsToAccountIDs: Record Date: Wed, 28 Feb 2024 09:25:09 +0530 Subject: [PATCH 125/245] Temporary disable track expense distance --- src/pages/iou/request/IOURequestStartPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/request/IOURequestStartPage.js b/src/pages/iou/request/IOURequestStartPage.js index 8b7ef2a17973..b0d2983c55be 100644 --- a/src/pages/iou/request/IOURequestStartPage.js +++ b/src/pages/iou/request/IOURequestStartPage.js @@ -104,7 +104,7 @@ function IOURequestStartPage({ const isExpenseChat = ReportUtils.isPolicyExpenseChat(report); const isExpenseReport = ReportUtils.isExpenseReport(report); - const shouldDisplayDistanceRequest = isExpenseChat || isExpenseReport || isFromGlobalCreate || iouType === CONST.IOU.TYPE.TRACK_EXPENSE; + const shouldDisplayDistanceRequest = isExpenseChat || isExpenseReport || isFromGlobalCreate; // Allow the user to create the request if we are creating the request in global menu or the report can create the request const isAllowedToCreateRequest = _.isEmpty(report.reportID) || ReportUtils.canCreateRequest(report, policy, iouType); From 1a7099077402761a95b75b7f997c349be30ddbc5 Mon Sep 17 00:00:00 2001 From: smelaa Date: Wed, 28 Feb 2024 15:00:14 +0100 Subject: [PATCH 126/245] Migration of VideoPlayerPreview --- .../VideoPlayerThumbnail.tsx | 2 +- .../{index.js => index.tsx} | 43 ++++++------------- src/components/VideoPlayerPreview/types.ts | 21 ++++++++- 3 files changed, 33 insertions(+), 33 deletions(-) rename src/components/VideoPlayerPreview/{index.js => index.tsx} (73%) diff --git a/src/components/VideoPlayerPreview/VideoPlayerThumbnail.tsx b/src/components/VideoPlayerPreview/VideoPlayerThumbnail.tsx index 896c8778ba06..c7342a0d80e6 100644 --- a/src/components/VideoPlayerPreview/VideoPlayerThumbnail.tsx +++ b/src/components/VideoPlayerPreview/VideoPlayerThumbnail.tsx @@ -11,7 +11,7 @@ import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as ReportUtils from '@libs/ReportUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -import VideoPlayerThumbnailProps from './types'; +import type {VideoPlayerThumbnailProps} from './types'; function VideoPlayerThumbnail({thumbnailUrl = undefined, onPress, accessibilityLabel}: VideoPlayerThumbnailProps) { const styles = useThemeStyles(); diff --git a/src/components/VideoPlayerPreview/index.js b/src/components/VideoPlayerPreview/index.tsx similarity index 73% rename from src/components/VideoPlayerPreview/index.js rename to src/components/VideoPlayerPreview/index.tsx index 252bc53fc839..2bc370711c00 100644 --- a/src/components/VideoPlayerPreview/index.js +++ b/src/components/VideoPlayerPreview/index.tsx @@ -1,4 +1,4 @@ -import PropTypes from 'prop-types'; +import type {VideoReadyForDisplayEvent} from 'expo-av'; import React, {useEffect, useState} from 'react'; import {View} from 'react-native'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -10,32 +10,17 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useThumbnailDimensions from '@hooks/useThumbnailDimensions'; import useWindowDimensions from '@hooks/useWindowDimensions'; import CONST from '@src/CONST'; +import type {VideoPlayerPreviewProps} from './types'; import VideoPlayerThumbnail from './VideoPlayerThumbnail'; -const propTypes = { - videoUrl: PropTypes.string.isRequired, - - videoDimensions: PropTypes.shape({ - width: PropTypes.number.isRequired, - height: PropTypes.number.isRequired, - }), - - videoDuration: PropTypes.number, - - thumbnailUrl: PropTypes.string, - - fileName: PropTypes.string.isRequired, - - onShowModalPress: PropTypes.func.isRequired, -}; - -const defaultProps = { - videoDimensions: CONST.VIDEO_PLAYER.DEFAULT_VIDEO_DIMENSIONS, - thumbnailUrl: undefined, - videoDuration: 0, -}; - -function VideoPlayerPreview({videoUrl, thumbnailUrl, fileName, videoDimensions, videoDuration, onShowModalPress}) { +function VideoPlayerPreview({ + videoUrl, + thumbnailUrl = undefined, + fileName, + videoDimensions = CONST.VIDEO_PLAYER.DEFAULT_VIDEO_DIMENSIONS, + videoDuration = 0, + onShowModalPress, +}: VideoPlayerPreviewProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {currentlyPlayingURL, updateCurrentlyPlayingURL} = usePlaybackContext(); @@ -44,8 +29,8 @@ function VideoPlayerPreview({videoUrl, thumbnailUrl, fileName, videoDimensions, const [measuredDimensions, setMeasuredDimensions] = useState(videoDimensions); const {thumbnailDimensionsStyles} = useThumbnailDimensions(measuredDimensions.width, measuredDimensions.height); - const onVideoLoaded = (e) => { - setMeasuredDimensions({width: e.srcElement.videoWidth, height: e.srcElement.videoHeight}); + const onVideoLoaded = (event: VideoReadyForDisplayEvent & {srcElement: HTMLVideoElement}) => { + setMeasuredDimensions({width: event.srcElement.videoWidth, height: event.srcElement.videoHeight}); }; const handleOnPress = () => { @@ -75,7 +60,7 @@ function VideoPlayerPreview({videoUrl, thumbnailUrl, fileName, videoDimensions, void} videoDuration={videoDuration} shouldUseSmallVideoControls style={[styles.w100, styles.h100]} @@ -94,8 +79,6 @@ function VideoPlayerPreview({videoUrl, thumbnailUrl, fileName, videoDimensions, ); } -VideoPlayerPreview.propTypes = propTypes; -VideoPlayerPreview.defaultProps = defaultProps; VideoPlayerPreview.displayName = 'VideoPlayerPreview'; export default VideoPlayerPreview; diff --git a/src/components/VideoPlayerPreview/types.ts b/src/components/VideoPlayerPreview/types.ts index 2d1d1557e00a..4b4204b9f852 100644 --- a/src/components/VideoPlayerPreview/types.ts +++ b/src/components/VideoPlayerPreview/types.ts @@ -2,8 +2,25 @@ import type {GestureResponderEvent} from 'react-native'; type VideoPlayerThumbnailProps = { thumbnailUrl: string | undefined; - onPress: (event?: GestureResponderEvent | KeyboardEvent) => void; + onPress: (event?: GestureResponderEvent | KeyboardEvent) => void | Promise; accessibilityLabel: string; }; -export default VideoPlayerThumbnailProps; +type VideoPlayerPreviewProps = { + videoUrl: string; + + videoDimensions: { + width: number; + height: number; + }; + + videoDuration: number; + + thumbnailUrl?: string; + + fileName: string; + + onShowModalPress: (event?: GestureResponderEvent | KeyboardEvent) => void | Promise; +}; + +export type {VideoPlayerThumbnailProps, VideoPlayerPreviewProps}; From aed45f85048d5eac559b893cff6c81e4a04c5b83 Mon Sep 17 00:00:00 2001 From: smelaa Date: Wed, 28 Feb 2024 16:44:39 +0100 Subject: [PATCH 127/245] Addressing review comments --- .../VideoPlayerThumbnail.tsx | 10 +++++-- src/components/VideoPlayerPreview/index.tsx | 29 +++++++++++++------ src/components/VideoPlayerPreview/types.ts | 26 ----------------- 3 files changed, 28 insertions(+), 37 deletions(-) delete mode 100644 src/components/VideoPlayerPreview/types.ts diff --git a/src/components/VideoPlayerPreview/VideoPlayerThumbnail.tsx b/src/components/VideoPlayerPreview/VideoPlayerThumbnail.tsx index c7342a0d80e6..aa4b06760f0d 100644 --- a/src/components/VideoPlayerPreview/VideoPlayerThumbnail.tsx +++ b/src/components/VideoPlayerPreview/VideoPlayerThumbnail.tsx @@ -1,5 +1,6 @@ import React from 'react'; import {View} from 'react-native'; +import type {GestureResponderEvent} from 'react-native'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import Image from '@components/Image'; @@ -11,9 +12,14 @@ import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as ReportUtils from '@libs/ReportUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -import type {VideoPlayerThumbnailProps} from './types'; -function VideoPlayerThumbnail({thumbnailUrl = undefined, onPress, accessibilityLabel}: VideoPlayerThumbnailProps) { +type VideoPlayerThumbnailProps = { + thumbnailUrl: string | undefined; + onPress: (event?: GestureResponderEvent | KeyboardEvent) => void | Promise; + accessibilityLabel: string; +}; + +function VideoPlayerThumbnail({thumbnailUrl, onPress, accessibilityLabel}: VideoPlayerThumbnailProps) { const styles = useThemeStyles(); return ( diff --git a/src/components/VideoPlayerPreview/index.tsx b/src/components/VideoPlayerPreview/index.tsx index 2bc370711c00..391596fa6961 100644 --- a/src/components/VideoPlayerPreview/index.tsx +++ b/src/components/VideoPlayerPreview/index.tsx @@ -1,6 +1,7 @@ import type {VideoReadyForDisplayEvent} from 'expo-av'; import React, {useEffect, useState} from 'react'; import {View} from 'react-native'; +import type {GestureResponderEvent} from 'react-native'; import * as Expensicons from '@components/Icon/Expensicons'; import VideoPlayer from '@components/VideoPlayer'; import IconButton from '@components/VideoPlayer/IconButton'; @@ -10,17 +11,23 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useThumbnailDimensions from '@hooks/useThumbnailDimensions'; import useWindowDimensions from '@hooks/useWindowDimensions'; import CONST from '@src/CONST'; -import type {VideoPlayerPreviewProps} from './types'; import VideoPlayerThumbnail from './VideoPlayerThumbnail'; -function VideoPlayerPreview({ - videoUrl, - thumbnailUrl = undefined, - fileName, - videoDimensions = CONST.VIDEO_PLAYER.DEFAULT_VIDEO_DIMENSIONS, - videoDuration = 0, - onShowModalPress, -}: VideoPlayerPreviewProps) { +type VideoDimensions = { + width: number; + height: number; +}; + +type VideoPlayerPreviewProps = { + videoUrl: string; + videoDimensions: VideoDimensions; + videoDuration: number; + thumbnailUrl?: string; + fileName: string; + onShowModalPress: (event?: GestureResponderEvent | KeyboardEvent) => void | Promise; +}; + +function VideoPlayerPreview({videoUrl, thumbnailUrl, fileName, videoDimensions = CONST.VIDEO_PLAYER.DEFAULT_VIDEO_DIMENSIONS, videoDuration = 0, onShowModalPress}: VideoPlayerPreviewProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {currentlyPlayingURL, updateCurrentlyPlayingURL} = usePlaybackContext(); @@ -29,6 +36,10 @@ function VideoPlayerPreview({ const [measuredDimensions, setMeasuredDimensions] = useState(videoDimensions); const {thumbnailDimensionsStyles} = useThumbnailDimensions(measuredDimensions.width, measuredDimensions.height); + // onVideoLoaded is passed to VideoPlayer, then BaseVideoPlayer and then as a prop onReadyForDisplay of Video. + // Therefore, the type of the event should be VideoReadyForDisplayEvent, however it does not include srcElement in its definition, + // as srcElement is present only for web implementation of Video. VideoPlayerPreview is used only for web. + const onVideoLoaded = (event: VideoReadyForDisplayEvent & {srcElement: HTMLVideoElement}) => { setMeasuredDimensions({width: event.srcElement.videoWidth, height: event.srcElement.videoHeight}); }; diff --git a/src/components/VideoPlayerPreview/types.ts b/src/components/VideoPlayerPreview/types.ts deleted file mode 100644 index 4b4204b9f852..000000000000 --- a/src/components/VideoPlayerPreview/types.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type {GestureResponderEvent} from 'react-native'; - -type VideoPlayerThumbnailProps = { - thumbnailUrl: string | undefined; - onPress: (event?: GestureResponderEvent | KeyboardEvent) => void | Promise; - accessibilityLabel: string; -}; - -type VideoPlayerPreviewProps = { - videoUrl: string; - - videoDimensions: { - width: number; - height: number; - }; - - videoDuration: number; - - thumbnailUrl?: string; - - fileName: string; - - onShowModalPress: (event?: GestureResponderEvent | KeyboardEvent) => void | Promise; -}; - -export type {VideoPlayerThumbnailProps, VideoPlayerPreviewProps}; From d176eaf34b3767527809fbec08642fccc023f5d7 Mon Sep 17 00:00:00 2001 From: smelaa Date: Wed, 28 Feb 2024 17:30:02 +0100 Subject: [PATCH 128/245] VideoPopovermenu migrated to ts --- src/components/Popover/types.ts | 2 +- src/components/PopoverMenu.tsx | 2 +- .../VideoPopoverMenu/{index.js => index.tsx} | 32 +++++++------------ 3 files changed, 14 insertions(+), 22 deletions(-) rename src/components/VideoPopoverMenu/{index.js => index.tsx} (54%) diff --git a/src/components/Popover/types.ts b/src/components/Popover/types.ts index e06037f47b63..fc73f6fc5d6b 100644 --- a/src/components/Popover/types.ts +++ b/src/components/Popover/types.ts @@ -20,7 +20,7 @@ type PopoverProps = BaseModalProps & anchorAlignment?: AnchorAlignment; /** The anchor ref of the popover */ - anchorRef: RefObject; + anchorRef?: RefObject; /** Whether disable the animations */ disableAnimation?: boolean; diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx index 4ee070e19893..d59153e680e7 100644 --- a/src/components/PopoverMenu.tsx +++ b/src/components/PopoverMenu.tsx @@ -51,7 +51,7 @@ type PopoverMenuProps = Partial & { anchorPosition: AnchorPosition; /** Ref of the anchor */ - anchorRef: RefObject; + anchorRef?: RefObject; /** Where the popover should be positioned relative to the anchor points. */ anchorAlignment?: AnchorAlignment; diff --git a/src/components/VideoPopoverMenu/index.js b/src/components/VideoPopoverMenu/index.tsx similarity index 54% rename from src/components/VideoPopoverMenu/index.js rename to src/components/VideoPopoverMenu/index.tsx index 01aaa8e35174..41f60452e0ad 100644 --- a/src/components/VideoPopoverMenu/index.js +++ b/src/components/VideoPopoverMenu/index.tsx @@ -1,28 +1,23 @@ -import PropTypes from 'prop-types'; import React from 'react'; +import type {PopoverMenuItem} from '@components/PopoverMenu'; import PopoverMenu from '@components/PopoverMenu'; import {useVideoPopoverMenuContext} from '@components/VideoPlayerContexts/VideoPopoverMenuContext'; +import type {AnchorPosition} from '@styles/index'; -const propTypes = { - isPopoverVisible: PropTypes.bool, - - hidePopover: PropTypes.func, - - anchorPosition: PropTypes.shape({ - horizontal: PropTypes.number.isRequired, - vertical: PropTypes.number.isRequired, - }), +type VideoPopoverMenuProps = { + isPopoverVisible: boolean; + hidePopover: (selectedItem?: PopoverMenuItem, index?: number) => void; + anchorPosition: AnchorPosition; }; -const defaultProps = { - isPopoverVisible: false, - anchorPosition: { + +function VideoPopoverMenu({ + isPopoverVisible = false, + hidePopover = () => {}, + anchorPosition = { horizontal: 0, vertical: 0, }, - hidePopover: () => {}, -}; - -function VideoPopoverMenu({isPopoverVisible, hidePopover, anchorPosition}) { +}: VideoPopoverMenuProps) { const {menuItems} = useVideoPopoverMenuContext(); return ( @@ -36,9 +31,6 @@ function VideoPopoverMenu({isPopoverVisible, hidePopover, anchorPosition}) { /> ); } - -VideoPopoverMenu.propTypes = propTypes; -VideoPopoverMenu.defaultProps = defaultProps; VideoPopoverMenu.displayName = 'VideoPopoverMenu'; export default VideoPopoverMenu; From d13e2bed6170dd4f22c6f139c1391f18d3877926 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Thu, 29 Feb 2024 06:17:39 +0530 Subject: [PATCH 129/245] fix: IOU - CMD+ENTER command takes you to the IOU confirmation page without selecting members. Signed-off-by: Krishna Gupta --- .../SelectionList/BaseSelectionList.tsx | 21 ++++++-- src/components/SelectionList/types.ts | 2 +- src/pages/RoomInvitePage.tsx | 52 ++++++++++++------- src/pages/workspace/WorkspaceInvitePage.tsx | 26 +++++++--- 4 files changed, 69 insertions(+), 32 deletions(-) diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 1c69d00b3910..16424518d53d 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -371,11 +371,22 @@ function BaseSelectionList( }); /** Calls confirm action when pressing CTRL (CMD) + Enter */ - useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.CTRL_ENTER, onConfirm ?? selectFocusedOption, { - captureOnInputs: true, - shouldBubble: !flattenedSections.allOptions[focusedIndex], - isActive: !disableKeyboardShortcuts && isFocused, - }); + useKeyboardShortcut( + CONST.KEYBOARD_SHORTCUTS.CTRL_ENTER, + (e) => { + const focusedOption = flattenedSections.allOptions[focusedIndex]; + if (onConfirm && (flattenedSections.selectedOptions.length || focusedOption)) { + onConfirm(e, focusedOption); + return; + } + selectFocusedOption(); + }, + { + captureOnInputs: true, + shouldBubble: !flattenedSections.allOptions[focusedIndex], + isActive: !disableKeyboardShortcuts && isFocused, + }, + ); return ( = Partial & { confirmButtonText?: string; /** Callback to fire when the confirm button is pressed */ - onConfirm?: (e?: GestureResponderEvent | KeyboardEvent | undefined) => void; + onConfirm?: (e?: GestureResponderEvent | KeyboardEvent | undefined, option?: TItem) => void; /** Whether to show the vertical scroll indicator */ showScrollIndicator?: boolean; diff --git a/src/pages/RoomInvitePage.tsx b/src/pages/RoomInvitePage.tsx index 40a1b009b38d..66d13d496e8c 100644 --- a/src/pages/RoomInvitePage.tsx +++ b/src/pages/RoomInvitePage.tsx @@ -2,7 +2,7 @@ import {useNavigation} from '@react-navigation/native'; import type {StackNavigationProp} from '@react-navigation/stack'; import Str from 'expensify-common/lib/str'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; -import type {SectionListData} from 'react-native'; +import type {GestureResponderEvent, SectionListData} from 'react-native'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; @@ -164,31 +164,47 @@ function RoomInvitePage({betas, personalDetails, report, policies}: RoomInvitePa [selectedOptions], ); - const validate = useCallback(() => selectedOptions.length > 0, [selectedOptions]); + const validate = useCallback((options: ReportUtils.OptionData[]) => options.length > 0, []); // Non policy members should not be able to view the participants of a room const reportID = report?.reportID; const isPolicyMember = useMemo(() => (report?.policyID ? PolicyUtils.isPolicyMember(report.policyID, policies as Record) : false), [report?.policyID, policies]); const backRoute = useMemo(() => reportID && (isPolicyMember ? ROUTES.ROOM_MEMBERS.getRoute(reportID) : ROUTES.REPORT_WITH_ID_DETAILS.getRoute(reportID)), [isPolicyMember, reportID]); const reportName = useMemo(() => ReportUtils.getReportName(report), [report]); - const inviteUsers = useCallback(() => { - if (!validate()) { - return; - } - const invitedEmailsToAccountIDs: PolicyUtils.MemberEmailsToAccountIDs = {}; - selectedOptions.forEach((option) => { - const login = option.login ?? ''; - const accountID = option.accountID; - if (!login.toLowerCase().trim() || !accountID) { + + const inviteUsers = useCallback( + (e?: GestureResponderEvent | KeyboardEvent | undefined, option?: OptionsListUtils.MemberForList) => { + const options = [...selectedOptions]; + + if (option && e && 'key' in e && e.key === 'Enter') { + const isOptionInList = selectedOptions.some((selectedOption) => selectedOption.login === option?.login); + + if (option && !isOptionInList) { + toggleOption(option); + options.push(option); + } + } + + if (!validate(options)) { return; } - invitedEmailsToAccountIDs[login] = Number(accountID); - }); - if (reportID) { - Report.inviteToRoom(reportID, invitedEmailsToAccountIDs); - } - Navigation.navigate(backRoute); - }, [selectedOptions, backRoute, reportID, validate]); + + const invitedEmailsToAccountIDs: PolicyUtils.MemberEmailsToAccountIDs = {}; + options.forEach((selectedOption) => { + const login = selectedOption.login ?? ''; + const accountID = selectedOption.accountID; + if (!login.toLowerCase().trim() || !accountID) { + return; + } + invitedEmailsToAccountIDs[login] = Number(accountID); + }); + if (reportID) { + Report.inviteToRoom(reportID, invitedEmailsToAccountIDs); + } + Navigation.navigate(backRoute); + }, + [selectedOptions, backRoute, reportID, validate, toggleOption], + ); const headerMessage = useMemo(() => { const searchValue = searchTerm.trim().toLowerCase(); diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index 3c1b009aac70..e59176b6d79c 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -2,7 +2,7 @@ import {useNavigation} from '@react-navigation/native'; import type {StackNavigationProp, StackScreenProps} from '@react-navigation/stack'; import Str from 'expensify-common/lib/str'; import React, {useEffect, useMemo, useState} from 'react'; -import type {SectionListData} from 'react-native'; +import type {GestureResponderEvent, SectionListData} from 'react-native'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; @@ -236,9 +236,9 @@ function WorkspaceInvitePage({ setSelectedOptions(newSelectedOptions); }; - const validate = (): boolean => { + const validate = (options: OptionsListUtils.MemberForList[]): boolean => { const errors: Errors = {}; - if (selectedOptions.length <= 0) { + if (options.length <= 0) { errors.noUserSelected = 'true'; } @@ -246,15 +246,25 @@ function WorkspaceInvitePage({ return isEmptyObject(errors); }; - const inviteUser = () => { - if (!validate()) { + const inviteUser = (e?: GestureResponderEvent | KeyboardEvent | undefined, option?: MemberForList) => { + const options = [...selectedOptions]; + if (option && e && 'key' in e && e.key === 'Enter') { + const isOptionInList = selectedOptions.some((selectedOption) => selectedOption.login === option?.login); + + if (option && !isOptionInList) { + toggleOption(option); + options.push(option); + } + } + + if (!validate(options)) { return; } const invitedEmailsToAccountIDs: InvitedEmailsToAccountIDs = {}; - selectedOptions.forEach((option) => { - const login = option.login ?? ''; - const accountID = option.accountID ?? ''; + options.forEach((selectedOption) => { + const login = selectedOption.login ?? ''; + const accountID = selectedOption.accountID ?? ''; if (!login.toLowerCase().trim() || !accountID) { return; } From aeed06fcd22e270ac789f09a0137b0d27aeb6fbe Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 29 Feb 2024 11:28:36 +0700 Subject: [PATCH 130/245] remove unnecessary change --- src/libs/actions/Report.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index a4247e8c92ac..12c379c828e3 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -2435,6 +2435,7 @@ function inviteToRoom(reportID: string, inviteeEmailsToAccountIDs: Record !targetAccountIDs.includes(id)); const visibleChatMemberAccountIDsAfterRemoval = report?.visibleChatMemberAccountIDs?.filter((id: number) => !targetAccountIDs.includes(id)); @@ -2460,6 +2461,7 @@ function removeFromRoom(reportID: string, targetAccountIDs: number[]) { }, ]; + console.log('optimisticData', optimisticData); const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, From 03eae4166948392968284a6c800fd892b2afc67c Mon Sep 17 00:00:00 2001 From: smelaa Date: Thu, 29 Feb 2024 11:41:04 +0100 Subject: [PATCH 131/245] ProgressBar migrated to ts --- .../ProgressBar/{index.js => index.tsx} | 31 ++++++++----------- 1 file changed, 13 insertions(+), 18 deletions(-) rename src/components/VideoPlayer/VideoPlayerControls/ProgressBar/{index.js => index.tsx} (73%) diff --git a/src/components/VideoPlayer/VideoPlayerControls/ProgressBar/index.js b/src/components/VideoPlayer/VideoPlayerControls/ProgressBar/index.tsx similarity index 73% rename from src/components/VideoPlayer/VideoPlayerControls/ProgressBar/index.js rename to src/components/VideoPlayer/VideoPlayerControls/ProgressBar/index.tsx index c6eb1a179726..72df96410e1c 100644 --- a/src/components/VideoPlayer/VideoPlayerControls/ProgressBar/index.js +++ b/src/components/VideoPlayer/VideoPlayerControls/ProgressBar/index.tsx @@ -1,25 +1,22 @@ -import PropTypes from 'prop-types'; import React, {useEffect, useState} from 'react'; +import type {LayoutChangeEvent, ViewStyle} from 'react-native'; +import type {GestureStateChangeEvent, GestureUpdateEvent, PanGestureChangeEventPayload, PanGestureHandlerEventPayload} from 'react-native-gesture-handler'; import {Gesture, GestureDetector} from 'react-native-gesture-handler'; import Animated, {runOnJS, useAnimatedStyle, useSharedValue} from 'react-native-reanimated'; import {usePlaybackContext} from '@components/VideoPlayerContexts/PlaybackContext'; import useThemeStyles from '@hooks/useThemeStyles'; -const propTypes = { - duration: PropTypes.number.isRequired, - - position: PropTypes.number.isRequired, - - seekPosition: PropTypes.func.isRequired, +type ProgressBarProps = { + duration: number; + position: number; + seekPosition: (newPosition: number) => void; }; -const defaultProps = {}; - -function getProgress(currentPosition, maxPosition) { +function getProgress(currentPosition: number, maxPosition: number) { return Math.min(Math.max((currentPosition / maxPosition) * 100, 0), 100); } -function ProgressBar({duration, position, seekPosition}) { +function ProgressBar({duration, position, seekPosition}: ProgressBarProps) { const styles = useThemeStyles(); const {pauseVideo, playVideo, checkVideoPlaying} = usePlaybackContext(); const [sliderWidth, setSliderWidth] = useState(1); @@ -27,18 +24,18 @@ function ProgressBar({duration, position, seekPosition}) { const progressWidth = useSharedValue(0); const wasVideoPlayingOnCheck = useSharedValue(false); - const onCheckVideoPlaying = (isPlaying) => { + const onCheckVideoPlaying = (isPlaying: boolean) => { wasVideoPlayingOnCheck.value = isPlaying; }; - const progressBarInteraction = (event) => { + const progressBarInteraction = (event: GestureUpdateEvent | GestureStateChangeEvent) => { const progress = getProgress(event.x, sliderWidth); progressWidth.value = progress; runOnJS(seekPosition)((progress * duration) / 100); }; - const onSliderLayout = (e) => { - setSliderWidth(e.nativeEvent.layout.width); + const onSliderLayout = (event: LayoutChangeEvent) => { + setSliderWidth(event.nativeEvent.layout.width); }; const pan = Gesture.Pan() @@ -66,7 +63,7 @@ function ProgressBar({duration, position, seekPosition}) { progressWidth.value = getProgress(position, duration); }, [duration, isSliderPressed, position, progressWidth]); - const progressBarStyle = useAnimatedStyle(() => ({width: `${progressWidth.value}%`})); + const progressBarStyle = useAnimatedStyle(() => ({width: `${progressWidth.value}%`} as ViewStyle)); return ( @@ -85,8 +82,6 @@ function ProgressBar({duration, position, seekPosition}) { ); } -ProgressBar.propTypes = propTypes; -ProgressBar.defaultProps = defaultProps; ProgressBar.displayName = 'ProgressBar'; export default ProgressBar; From 07947cd4d3a64315362ac5199a27cb620ddc2a86 Mon Sep 17 00:00:00 2001 From: smelaa Date: Thu, 29 Feb 2024 13:44:08 +0100 Subject: [PATCH 132/245] VolumeButton migrated to ts --- .../VolumeButton/{index.js => index.tsx} | 29 +++++++------------ 1 file changed, 11 insertions(+), 18 deletions(-) rename src/components/VideoPlayer/VideoPlayerControls/VolumeButton/{index.js => index.tsx} (82%) diff --git a/src/components/VideoPlayer/VideoPlayerControls/VolumeButton/index.js b/src/components/VideoPlayer/VideoPlayerControls/VolumeButton/index.tsx similarity index 82% rename from src/components/VideoPlayer/VideoPlayerControls/VolumeButton/index.js rename to src/components/VideoPlayer/VideoPlayerControls/VolumeButton/index.tsx index 45f47eb87c36..b6e5d3af8003 100644 --- a/src/components/VideoPlayer/VideoPlayerControls/VolumeButton/index.js +++ b/src/components/VideoPlayer/VideoPlayerControls/VolumeButton/index.tsx @@ -1,6 +1,7 @@ -import PropTypes from 'prop-types'; import React, {memo, useCallback, useState} from 'react'; +import type {LayoutChangeEvent, ViewStyle} from 'react-native'; import {View} from 'react-native'; +import type {GestureStateChangeEvent, GestureUpdateEvent, PanGestureChangeEventPayload, PanGestureHandlerEventPayload} from 'react-native-gesture-handler'; import {Gesture, GestureDetector} from 'react-native-gesture-handler'; import Animated, {runOnJS, useAnimatedStyle, useDerivedValue} from 'react-native-reanimated'; import Hoverable from '@components/Hoverable'; @@ -10,18 +11,13 @@ import {useVolumeContext} from '@components/VideoPlayerContexts/VolumeContext'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as NumberUtils from '@libs/NumberUtils'; -import stylePropTypes from '@styles/stylePropTypes'; -const propTypes = { - style: stylePropTypes.isRequired, - small: PropTypes.bool, +type VolumeButtonProps = { + style: ViewStyle; + small: boolean; }; -const defaultProps = { - small: false, -}; - -const getVolumeIcon = (volume) => { +const getVolumeIcon = (volume: number) => { if (volume === 0) { return Expensicons.Mute; } @@ -31,7 +27,7 @@ const getVolumeIcon = (volume) => { return Expensicons.VolumeHigh; }; -function VolumeButton({style, small}) { +function VolumeButton({style, small = false}: VolumeButtonProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {updateVolume, volume} = useVolumeContext(); @@ -39,12 +35,12 @@ function VolumeButton({style, small}) { const [volumeIcon, setVolumeIcon] = useState({icon: getVolumeIcon(volume.value)}); const [isSliderBeingUsed, setIsSliderBeingUsed] = useState(false); - const onSliderLayout = useCallback((e) => { - setSliderHeight(e.nativeEvent.layout.height); + const onSliderLayout = useCallback((event: LayoutChangeEvent) => { + setSliderHeight(event.nativeEvent.layout.height); }, []); const changeVolumeOnPan = useCallback( - (event) => { + (event: GestureStateChangeEvent | GestureUpdateEvent) => { const val = NumberUtils.roundToTwoDecimalPlaces(1 - event.y / sliderHeight); volume.value = NumberUtils.clamp(val, 0, 1); }, @@ -65,7 +61,7 @@ function VolumeButton({style, small}) { const progressBarStyle = useAnimatedStyle(() => ({height: `${volume.value * 100}%`})); - const updateIcon = useCallback((vol) => { + const updateIcon = useCallback((vol: number) => { setVolumeIcon({icon: getVolumeIcon(vol)}); }, []); @@ -98,7 +94,6 @@ function VolumeButton({style, small}) { tooltipText={volume.value === 0 ? translate('videoPlayer.unmute') : translate('videoPlayer.mute')} onPress={() => updateVolume(volume.value === 0 ? 1 : 0)} src={volumeIcon.icon} - fill={styles.white} small={small} shouldForceRenderingTooltipBelow /> @@ -108,8 +103,6 @@ function VolumeButton({style, small}) { ); } -VolumeButton.propTypes = propTypes; -VolumeButton.defaultProps = defaultProps; VolumeButton.displayName = 'VolumeButton'; export default memo(VolumeButton); From 1c8b7890fc02179c7adb9d6acc07b619c7e6807f Mon Sep 17 00:00:00 2001 From: smelaa Date: Thu, 29 Feb 2024 14:59:43 +0100 Subject: [PATCH 133/245] VideoPlayerControls migrated to ts --- .../VolumeButton/index.tsx | 2 +- .../{index.js => index.tsx} | 49 +++++++------------ 2 files changed, 18 insertions(+), 33 deletions(-) rename src/components/VideoPlayer/VideoPlayerControls/{index.js => index.tsx} (81%) diff --git a/src/components/VideoPlayer/VideoPlayerControls/VolumeButton/index.tsx b/src/components/VideoPlayer/VideoPlayerControls/VolumeButton/index.tsx index b6e5d3af8003..ee93eb672774 100644 --- a/src/components/VideoPlayer/VideoPlayerControls/VolumeButton/index.tsx +++ b/src/components/VideoPlayer/VideoPlayerControls/VolumeButton/index.tsx @@ -14,7 +14,7 @@ import * as NumberUtils from '@libs/NumberUtils'; type VolumeButtonProps = { style: ViewStyle; - small: boolean; + small?: boolean; }; const getVolumeIcon = (volume: number) => { diff --git a/src/components/VideoPlayer/VideoPlayerControls/index.js b/src/components/VideoPlayer/VideoPlayerControls/index.tsx similarity index 81% rename from src/components/VideoPlayer/VideoPlayerControls/index.js rename to src/components/VideoPlayer/VideoPlayerControls/index.tsx index 5a926123feef..6af44a8e3dda 100644 --- a/src/components/VideoPlayer/VideoPlayerControls/index.js +++ b/src/components/VideoPlayer/VideoPlayerControls/index.tsx @@ -1,55 +1,42 @@ -import PropTypes from 'prop-types'; +import type {Video} from 'expo-av'; +import type {MutableRefObject} from 'react'; import React, {useCallback, useMemo, useState} from 'react'; +import type {GestureResponderEvent, LayoutChangeEvent, ViewStyle} from 'react-native'; import {View} from 'react-native'; import Animated from 'react-native-reanimated'; import * as Expensicons from '@components/Icon/Expensicons'; -import refPropTypes from '@components/refPropTypes'; import Text from '@components/Text'; import IconButton from '@components/VideoPlayer/IconButton'; import convertMillisecondsToTime from '@components/VideoPlayer/utils'; import {usePlaybackContext} from '@components/VideoPlayerContexts/PlaybackContext'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import stylePropTypes from '@styles/stylePropTypes'; import CONST from '@src/CONST'; import ProgressBar from './ProgressBar'; import VolumeButton from './VolumeButton'; -const propTypes = { - duration: PropTypes.number.isRequired, - - position: PropTypes.number.isRequired, - - url: PropTypes.string.isRequired, - - videoPlayerRef: refPropTypes.isRequired, - - isPlaying: PropTypes.bool.isRequired, - +type VideoPlayerControlsProps = { + duration: number; + position: number; + url: string; + videoPlayerRef: MutableRefObject ); diff --git a/src/languages/en.ts b/src/languages/en.ts index 0a52cca62ef5..53a7758799d3 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -503,6 +503,8 @@ export default { send: 'send money', split: 'split a bill', request: 'request money', + // eslint-disable-next-line @typescript-eslint/naming-convention + 'track-expense': 'track an expense', }, }, reportAction: { @@ -592,6 +594,7 @@ export default { participants: 'Participants', requestMoney: 'Request money', sendMoney: 'Send money', + trackExpense: 'Track expense', pay: 'Pay', cancelPayment: 'Cancel payment', cancelPaymentConfirmation: 'Are you sure that you want to cancel this payment?', diff --git a/src/languages/es.ts b/src/languages/es.ts index 013255c1e11e..aaee08c4a9e9 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -496,6 +496,8 @@ export default { send: 'enviar dinero', split: 'dividir una factura', request: 'pedir dinero', + // eslint-disable-next-line @typescript-eslint/naming-convention + 'track-expense': 'rastrear un gasto', }, }, reportAction: { @@ -585,6 +587,7 @@ export default { participants: 'Participantes', requestMoney: 'Pedir dinero', sendMoney: 'Enviar dinero', + trackExpense: 'Seguimiento de gastos', pay: 'Pagar', cancelPayment: 'Cancelar el pago', cancelPaymentConfirmation: '¿Estás seguro de que quieres cancelar este pago?', diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 07f0df962455..dbe60d04b45b 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -249,9 +249,11 @@ Onyx.connect({ }); const policyExpenseReports: OnyxCollection = {}; +const allReports: OnyxCollection = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT, callback: (report, key) => { + allReports[key] = report; if (!ReportUtils.isPolicyExpenseChat(report)) { return; } @@ -738,6 +740,35 @@ function createOption( return result; } +/** + * Get the option for a given report. + */ +function getReportOption(participant: Participant): ReportUtils.OptionData { + const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${participant.reportID}`]; + + const option = createOption( + report?.visibleChatMemberAccountIDs ?? [], + allPersonalDetails ?? {}, + report ?? null, + {}, + { + showChatPreviewLine: false, + forcePolicyNamePreview: false, + }, + ); + + // Update text & alternateText because createOption returns workspace name only if report is owned by the user + if (option.isSelfDM) { + option.alternateText = Localize.translateLocal('reportActionsView.yourSpace'); + } else { + option.text = ReportUtils.getPolicyName(report); + option.alternateText = Localize.translateLocal('workspace.common.workspace'); + } + option.selected = participant.selected; + option.isSelected = participant.selected; + return option; +} + /** * Get the option for a policy expense report. */ @@ -2068,6 +2099,7 @@ export { formatSectionsFromSearchTerm, transformedTaxRates, getShareLogOptions, + getReportOption, }; export type {MemberForList, CategorySection, GetOptions}; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 67991d71a559..5ac7ae562de3 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -921,6 +921,15 @@ function isConciergeChatReport(report: OnyxEntry): boolean { return report?.participantAccountIDs?.length === 1 && Number(report.participantAccountIDs?.[0]) === CONST.ACCOUNT_ID.CONCIERGE && !isChatThread(report); } +function findSelfDMReportID(): string | undefined { + if (!allReports) { + return; + } + + const selfDMReport = Object.values(allReports).find((report) => isSelfDM(report) && !isThread(report)); + return selfDMReport?.reportID; +} + /** * Checks if the supplied report belongs to workspace based on the provided params. If the report's policyID is _FAKE_ or has no value, it means this report is a DM. * In this case report and workspace members must be compared to determine whether the report belongs to the workspace. @@ -4341,7 +4350,7 @@ function canRequestMoney(report: OnyxEntry, policy: OnyxEntry, o /** * Helper method to define what money request options we want to show for particular method. - * There are 3 money request options: Request, Split and Send: + * There are 4 money request options: Request, Split, Send and Track expense: * - Request option should show for: * - DMs * - own policy expense chats @@ -4353,13 +4362,16 @@ function canRequestMoney(report: OnyxEntry, policy: OnyxEntry, o * - chat/ policy rooms with more than 1 participants * - groups chats with 3 and more participants * - corporate workspace chats + * - Track expense option should show for: + * - Self DMs + * - admin rooms * * None of the options should show in chat threads or if there is some special Expensify account * as a participant of the report. */ -function getMoneyRequestOptions(report: OnyxEntry, policy: OnyxEntry, reportParticipants: number[]): Array> { +function getMoneyRequestOptions(report: OnyxEntry, policy: OnyxEntry, reportParticipants: number[], canUseTrackExpense = true): Array> { // In any thread or task report, we do not allow any new money requests yet - if (isChatThread(report) || isTaskReport(report) || isSelfDM(report)) { + if (isChatThread(report) || isTaskReport(report) || (!canUseTrackExpense && isSelfDM(report))) { return []; } @@ -4387,6 +4399,10 @@ function getMoneyRequestOptions(report: OnyxEntry, policy: OnyxEntry) { // If the report is iou or expense report, we should get the chat report to set participant for request money const chatReport = ReportUtils.isMoneyRequestReport(report) ? ReportUtils.getReport(report.chatReportID) : report; const currentUserAccountID = currentUserPersonalDetails.accountID; - const participants: Participant[] = ReportUtils.isPolicyExpenseChat(chatReport) - ? [{reportID: chatReport?.reportID, isPolicyExpenseChat: true, selected: true}] - : (chatReport?.participantAccountIDs ?? []).filter((accountID) => currentUserAccountID !== accountID).map((accountID) => ({accountID, selected: true})); + const shouldAddAsReport = iouType === CONST.IOU.TYPE.TRACK_EXPENSE && !isEmptyObject(chatReport) && (ReportUtils.isSelfDM(chatReport) || ReportUtils.isAdminRoom(chatReport)); + const participants: Participant[] = + ReportUtils.isPolicyExpenseChat(chatReport) || shouldAddAsReport + ? [{reportID: chatReport?.reportID, isPolicyExpenseChat: ReportUtils.isPolicyExpenseChat(chatReport), selected: true}] + : (chatReport?.participantAccountIDs ?? []).filter((accountID) => currentUserAccountID !== accountID).map((accountID) => ({accountID, selected: true})); Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {participants, participantsAutoAssigned: true}); } diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx index 68c7f0883683..e1e9fe25efda 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx @@ -13,6 +13,7 @@ import PopoverMenu from '@components/PopoverMenu'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import Tooltip from '@components/Tooltip/PopoverAnchorTooltip'; import useLocalize from '@hooks/useLocalize'; +import usePermissions from '@hooks/usePermissions'; import usePrevious from '@hooks/usePrevious'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -115,6 +116,7 @@ function AttachmentPickerWithMenuItems({ const styles = useThemeStyles(); const {translate} = useLocalize(); const {windowHeight} = useWindowDimensions(); + const {canUseTrackExpense} = usePermissions(); /** * Returns the list of IOU Options @@ -136,12 +138,17 @@ function AttachmentPickerWithMenuItems({ text: translate('iou.sendMoney'), onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.SEND, report?.reportID), }, + [CONST.IOU.TYPE.TRACK_EXPENSE]: { + icon: Expensicons.TrackExpense, + text: translate('iou.trackExpense'), + onSelected: () => IOU.startMoneyRequest_temporaryForRefactor(CONST.IOU.TYPE.TRACK_EXPENSE, report?.reportID ?? ''), + }, }; - return ReportUtils.getMoneyRequestOptions(report, policy, reportParticipantIDs ?? []).map((option) => ({ + return ReportUtils.getMoneyRequestOptions(report, policy, reportParticipantIDs ?? [], canUseTrackExpense).map((option) => ({ ...options[option], })); - }, [report, policy, reportParticipantIDs, translate]); + }, [translate, report, policy, reportParticipantIDs, canUseTrackExpense]); /** * Determines if we can show the task option diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js index bcf9c77ac2f7..c075b8a84b89 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js @@ -192,14 +192,14 @@ function FloatingActionButtonAndPopover(props) { ? [ { icon: Expensicons.TrackExpense, - text: 'Track Expense', + text: translate('iou.trackExpense'), onSelected: () => interceptAnonymousUser(() => IOU.startMoneyRequest_temporaryForRefactor( CONST.IOU.TYPE.TRACK_EXPENSE, // When starting to create a track expense from the global FAB, we need to retrieve selfDM reportID. // If it doesn't exist, we generate a random optimistic reportID and use it for all of the routes in the creation flow. - props.account.selfDMReportID || ReportUtils.generateReportID(), + props.account.selfDMReportID || ReportUtils.findSelfDMReportID() || ReportUtils.generateReportID(), ), ), }, diff --git a/src/pages/iou/request/IOURequestStartPage.js b/src/pages/iou/request/IOURequestStartPage.js index 1cd34db66da5..f0557d48da75 100644 --- a/src/pages/iou/request/IOURequestStartPage.js +++ b/src/pages/iou/request/IOURequestStartPage.js @@ -77,7 +77,7 @@ function IOURequestStartPage({ [CONST.IOU.TYPE.REQUEST]: translate('iou.requestMoney'), [CONST.IOU.TYPE.SEND]: translate('iou.sendMoney'), [CONST.IOU.TYPE.SPLIT]: translate('iou.splitBill'), - [CONST.IOU.TYPE.TRACK_EXPENSE]: 'Track Expense', + [CONST.IOU.TYPE.TRACK_EXPENSE]: translate('iou.trackExpense'), }; const transactionRequestType = useRef(TransactionUtils.getRequestType(transaction)); const previousIOURequestType = usePrevious(transactionRequestType.current); diff --git a/src/pages/iou/request/step/IOURequestStepAmount.js b/src/pages/iou/request/step/IOURequestStepAmount.js index 9fdd2bea24f4..07882e95a9ae 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.js +++ b/src/pages/iou/request/step/IOURequestStepAmount.js @@ -144,7 +144,7 @@ function IOURequestStepAmount({ // inside a report. In this case, the participants can be automatically assigned from the report and the user can skip the participants step and go straight // to the confirm step. if (report.reportID) { - IOU.setMoneyRequestParticipantsFromReport(transactionID, report); + IOU.setMoneyRequestParticipantsFromReport(transactionID, report, iouType); Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(iouType, transactionID, reportID)); return; } diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.js b/src/pages/iou/request/step/IOURequestStepConfirmation.js index de5c6811d277..6a2a5dc6f70f 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.js +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.js @@ -101,15 +101,15 @@ function IOURequestStepConfirmation({ return translate('iou.splitBill'); } if (iouType === CONST.IOU.TYPE.TRACK_EXPENSE) { - return 'Track Expense'; + return translate('iou.trackExpense'); } return translate(TransactionUtils.getHeaderTitleTranslationKey(transaction)); }, [iouType, transaction, translate]); const participants = useMemo( () => _.map(transaction.participants, (participant) => { - const isPolicyExpenseChat = lodashGet(participant, 'isPolicyExpenseChat', false); - return isPolicyExpenseChat ? OptionsListUtils.getPolicyExpenseReportOption(participant) : OptionsListUtils.getParticipantsOption(participant, personalDetails); + const participantReportID = lodashGet(participant, 'reportID', ''); + return participantReportID ? OptionsListUtils.getReportOption(participant) : OptionsListUtils.getParticipantsOption(participant, personalDetails); }), [transaction.participants, personalDetails], ); @@ -130,7 +130,7 @@ function IOURequestStepConfirmation({ if (policyExpenseChat) { Policy.openDraftWorkspaceRequest(policyExpenseChat.policyID); } - }, [participants, transaction.billable, policy, transactionID]); + }, [isOffline, participants, transaction.billable, policy, transactionID]); const defaultBillable = lodashGet(policy, 'defaultBillable', false); useEffect(() => { @@ -186,13 +186,6 @@ function IOURequestStepConfirmation({ IOU.navigateToStartStepIfScanFileCannotBeRead(receiptFilename, receiptPath, onSuccess, requestType, iouType, transactionID, reportID, receiptType); }, [receiptType, receiptPath, receiptFilename, requestType, iouType, transactionID, reportID]); - useEffect(() => { - const policyExpenseChat = _.find(participants, (participant) => participant.isPolicyExpenseChat); - if (policyExpenseChat) { - Policy.openDraftWorkspaceRequest(policyExpenseChat.policyID); - } - }, [isOffline, participants, transaction.billable, policy]); - /** * @param {Array} selectedParticipants * @param {String} trimmedComment diff --git a/src/pages/iou/request/step/IOURequestStepDistance.js b/src/pages/iou/request/step/IOURequestStepDistance.js index 320359192c8d..7df5df4cb203 100644 --- a/src/pages/iou/request/step/IOURequestStepDistance.js +++ b/src/pages/iou/request/step/IOURequestStepDistance.js @@ -127,7 +127,7 @@ function IOURequestStepDistance({ // inside a report. In this case, the participants can be automatically assigned from the report and the user can skip the participants step and go straight // to the confirm step. if (report.reportID) { - IOU.setMoneyRequestParticipantsFromReport(transactionID, report); + IOU.setMoneyRequestParticipantsFromReport(transactionID, report, iouType); Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(iouType, transactionID, reportID)); return; } diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.js b/src/pages/iou/request/step/IOURequestStepScan/index.js index 7de121af52b4..05961bd6c4c3 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.js @@ -129,7 +129,7 @@ function IOURequestStepScan({ // If the transaction was created from the + menu from the composer inside of a chat, the participants can automatically // be added to the transaction (taken from the chat report participants) and then the person is taken to the confirmation step. - IOU.setMoneyRequestParticipantsFromReport(transactionID, report); + IOU.setMoneyRequestParticipantsFromReport(transactionID, report, iouType); Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(iouType, transactionID, reportID)); }, [iouType, report, reportID, transactionID, isFromGlobalCreate, backTo]); diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.js b/src/pages/iou/request/step/IOURequestStepScan/index.native.js index f421417b53f6..2ef49af80441 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.js @@ -189,7 +189,7 @@ function IOURequestStepScan({ // If the transaction was created from the + menu from the composer inside of a chat, the participants can automatically // be added to the transaction (taken from the chat report participants) and then the person is taken to the confirmation step. - IOU.setMoneyRequestParticipantsFromReport(transactionID, report); + IOU.setMoneyRequestParticipantsFromReport(transactionID, report, iouType); Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(iouType, transactionID, reportID)); }, [iouType, report, reportID, transactionID, isFromGlobalCreate, backTo]); diff --git a/src/pages/iou/request/step/IOURequestStepTaxAmountPage.js b/src/pages/iou/request/step/IOURequestStepTaxAmountPage.js index 29263d92078f..7a75e9f48805 100644 --- a/src/pages/iou/request/step/IOURequestStepTaxAmountPage.js +++ b/src/pages/iou/request/step/IOURequestStepTaxAmountPage.js @@ -131,6 +131,7 @@ function IOURequestStepTaxAmountPage({ // inside a report. In this case, the participants can be automatically assigned from the report and the user can skip the participants step and go straight // to the confirm step. if (report.reportID) { + // TODO: Is this really needed at all? IOU.setMoneyRequestParticipantsFromReport(transactionID, report); Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(iouType, transactionID, reportID)); return; From 33a0c3ebcc9b4390706dc68247a02abd26c69f71 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Thu, 7 Mar 2024 16:23:49 +0700 Subject: [PATCH 153/245] fix: Workspace Avatar Error is impossible to dismiss --- src/components/AvatarWithImagePicker.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/AvatarWithImagePicker.tsx b/src/components/AvatarWithImagePicker.tsx index 5755c69641c8..b6cee205dd0e 100644 --- a/src/components/AvatarWithImagePicker.tsx +++ b/src/components/AvatarWithImagePicker.tsx @@ -283,7 +283,7 @@ function AvatarWithImagePicker({ return ( - + From 063683632f751b995922e2e0805aa19db6e2cb2a Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Thu, 7 Mar 2024 16:37:22 +0700 Subject: [PATCH 154/245] feat: add margin top error --- src/pages/workspace/WorkspaceProfilePage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/WorkspaceProfilePage.tsx b/src/pages/workspace/WorkspaceProfilePage.tsx index 796f32c343f2..ae102d0fbe57 100644 --- a/src/pages/workspace/WorkspaceProfilePage.tsx +++ b/src/pages/workspace/WorkspaceProfilePage.tsx @@ -124,7 +124,7 @@ function WorkspaceProfilePage({policy, currencyList = {}, route}: WorkSpaceProfi originalFileName={policy?.originalFileName} disabled={readOnly} disabledStyle={styles.cursorDefault} - errorRowStyles={undefined} + errorRowStyles={styles.mt1} /> Date: Thu, 7 Mar 2024 16:44:50 +0700 Subject: [PATCH 155/245] fix margin bottom workspace detail --- src/pages/workspace/WorkspaceProfilePage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/WorkspaceProfilePage.tsx b/src/pages/workspace/WorkspaceProfilePage.tsx index ae102d0fbe57..dfc8b11b4390 100644 --- a/src/pages/workspace/WorkspaceProfilePage.tsx +++ b/src/pages/workspace/WorkspaceProfilePage.tsx @@ -107,7 +107,7 @@ function WorkspaceProfilePage({policy, currencyList = {}, route}: WorkSpaceProfi type={CONST.ICON_TYPE_WORKSPACE} fallbackIcon={Expensicons.FallbackWorkspaceAvatar} style={[ - isSmallScreenWidth ? styles.mb1 : styles.mb3, + styles.mb1, isSmallScreenWidth ? styles.mtn17 : styles.mtn20, styles.alignItemsStart, styles.sectionMenuItemTopDescription, From 800c99cd40ccd9352cf193bfe8afe1c5f908e2b0 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Thu, 7 Mar 2024 17:02:44 +0700 Subject: [PATCH 156/245] fix lint --- src/pages/workspace/WorkspaceProfilePage.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/pages/workspace/WorkspaceProfilePage.tsx b/src/pages/workspace/WorkspaceProfilePage.tsx index dfc8b11b4390..0847a2281f79 100644 --- a/src/pages/workspace/WorkspaceProfilePage.tsx +++ b/src/pages/workspace/WorkspaceProfilePage.tsx @@ -106,12 +106,7 @@ function WorkspaceProfilePage({policy, currencyList = {}, route}: WorkSpaceProfi DefaultAvatar={DefaultAvatar} type={CONST.ICON_TYPE_WORKSPACE} fallbackIcon={Expensicons.FallbackWorkspaceAvatar} - style={[ - styles.mb1, - isSmallScreenWidth ? styles.mtn17 : styles.mtn20, - styles.alignItemsStart, - styles.sectionMenuItemTopDescription, - ]} + style={[styles.mb1, isSmallScreenWidth ? styles.mtn17 : styles.mtn20, styles.alignItemsStart, styles.sectionMenuItemTopDescription]} isUsingDefaultAvatar={!policy?.avatar ?? null} onImageSelected={(file) => Policy.updateWorkspaceAvatar(policy?.id ?? '', file as File)} onImageRemoved={() => Policy.deleteWorkspaceAvatar(policy?.id ?? '')} From c2ef07b39e38d3da0b742db6ce65444164742fd7 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Thu, 7 Mar 2024 20:30:56 +0530 Subject: [PATCH 157/245] Completed BE endpoint connection --- src/libs/API/parameters/TrackExpenseParams.ts | 25 ++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 + src/libs/actions/IOU.ts | 343 ++++++++++++++++++ .../step/IOURequestStepConfirmation.js | 41 ++- 5 files changed, 411 insertions(+), 1 deletion(-) create mode 100644 src/libs/API/parameters/TrackExpenseParams.ts diff --git a/src/libs/API/parameters/TrackExpenseParams.ts b/src/libs/API/parameters/TrackExpenseParams.ts new file mode 100644 index 000000000000..0e17a316bb9f --- /dev/null +++ b/src/libs/API/parameters/TrackExpenseParams.ts @@ -0,0 +1,25 @@ +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; +import type {Receipt} from '@src/types/onyx/Transaction'; + +type TrackExpenseParams = { + amount: number; + currency: string; + comment: string; + created: string; + merchant: string; + iouReportID?: string; + chatReportID: string; + transactionID: string; + reportActionID: string; + createdChatReportActionID: string; + createdExpenseReportActionID?: string; + reportPreviewReportActionID?: string; + receipt: Receipt; + receiptState?: ValueOf; + tag?: string; + transactionThreadReportID: string; + createdReportActionIDForThread: string; +}; + +export default TrackExpenseParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 211bc06f26a3..d05dde006973 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -156,3 +156,4 @@ export type {default as SetWorkspaceAutoReportingFrequencyParams} from './SetWor export type {default as SetWorkspaceAutoReportingMonthlyOffsetParams} from './SetWorkspaceAutoReportingMonthlyOffsetParams'; export type {default as SetWorkspaceApprovalModeParams} from './SetWorkspaceApprovalModeParams'; export type {default as SwitchToOldDotParams} from './SwitchToOldDotParams'; +export type {default as TrackExpenseParams} from './TrackExpenseParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 115355210f75..9b47d1efd41d 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -157,6 +157,7 @@ const WRITE_COMMANDS = { CANCEL_PAYMENT: 'CancelPayment', ACCEPT_ACH_CONTRACT_FOR_BANK_ACCOUNT: 'AcceptACHContractForBankAccount', SWITCH_TO_OLD_DOT: 'SwitchToOldDot', + TRACK_EXPENSE: 'TrackExpense', } as const; type WriteCommand = ValueOf; @@ -312,6 +313,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SET_WORKSPACE_AUTO_REPORTING_MONTHLY_OFFSET]: Parameters.SetWorkspaceAutoReportingMonthlyOffsetParams; [WRITE_COMMANDS.SET_WORKSPACE_APPROVAL_MODE]: Parameters.SetWorkspaceApprovalModeParams; [WRITE_COMMANDS.SWITCH_TO_OLD_DOT]: Parameters.SwitchToOldDotParams; + [WRITE_COMMANDS.TRACK_EXPENSE]: Parameters.TrackExpenseParams; }; const READ_COMMANDS = { diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index bd1f92ab6490..26e78ee4cca8 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -22,6 +22,7 @@ import type { SplitBillParams, StartSplitBillParams, SubmitReportParams, + TrackExpenseParams, UpdateMoneyRequestParams, } from '@libs/API/parameters'; import {WRITE_COMMANDS} from '@libs/API/types'; @@ -84,6 +85,19 @@ type MoneyRequestInformation = { onyxData: OnyxData; }; +type TrackExpenseInformation = { + iouReport?: OnyxTypes.Report; + chatReport: OnyxTypes.Report; + transaction: OnyxTypes.Transaction; + iouAction: OptimisticIOUReportAction; + createdChatReportActionID: string; + createdExpenseReportActionID: string; + reportPreviewAction?: OnyxTypes.ReportAction; + transactionThreadReportID: string; + createdReportActionIDForThread: string; + onyxData: OnyxData; +}; + type SplitData = { chatReportID: string; transactionID: string; @@ -794,6 +808,159 @@ function buildOnyxDataForMoneyRequest( return [optimisticData, successData, failureData]; } +/** Builds the Onyx data for track expense */ +function buildOnyxDataForTrackExpense( + chatReport: OnyxEntry, + transaction: OnyxTypes.Transaction, + iouAction: OptimisticIOUReportAction, + transactionThreadReport: OptimisticChatReport, + transactionThreadCreatedReportAction: OptimisticCreatedReportAction, +): [OnyxUpdate[], OnyxUpdate[], OnyxUpdate[]] { + const isScanRequest = TransactionUtils.isScanRequest(transaction); + const clearedPendingFields = Object.fromEntries(Object.keys(transaction.pendingFields ?? {}).map((key) => [key, null])); + const optimisticData: OnyxUpdate[] = []; + + if (chatReport) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, + value: { + ...chatReport, + lastMessageText: iouAction.message?.[0].text, + lastMessageHtml: iouAction.message?.[0].html, + lastReadTime: DateUtils.getDBTime(), + }, + }); + } + + optimisticData.push( + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, + value: transaction, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, + value: { + [iouAction.reportActionID]: iouAction as OnyxTypes.ReportAction, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`, + value: transactionThreadReport, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`, + value: { + [transactionThreadCreatedReportAction.reportActionID]: transactionThreadCreatedReportAction, + }, + }, + + // Remove the temporary transaction used during the creation flow + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`, + value: null, + }, + ); + + const successData: OnyxUpdate[] = []; + + successData.push( + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`, + value: { + pendingFields: null, + errorFields: null, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, + value: { + pendingAction: null, + pendingFields: clearedPendingFields, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, + value: { + [iouAction.reportActionID]: { + pendingAction: null, + errors: null, + }, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`, + value: { + [transactionThreadCreatedReportAction.reportActionID]: { + pendingAction: null, + errors: null, + }, + }, + }, + ); + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`, + value: { + lastReadTime: chatReport?.lastReadTime, + lastMessageText: chatReport?.lastMessageText, + lastMessageHtml: chatReport?.lastMessageHtml, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`, + value: { + errorFields: { + createChat: ErrorUtils.getMicroSecondOnyxError('report.genericCreateReportFailureMessage'), + }, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, + value: { + errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'), + pendingAction: null, + pendingFields: clearedPendingFields, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, + value: { + [iouAction.reportActionID]: { + // Disabling this line since transaction.filename can be an empty string + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + errors: getReceiptError(transaction.receipt, transaction.filename || transaction.receipt?.filename, isScanRequest), + }, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReport.reportID}`, + value: { + [transactionThreadCreatedReportAction.reportActionID]: { + errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'), + }, + }, + }, + ]; + + return [optimisticData, successData, failureData]; +} + /** * Gathers all the data needed to make a money request. It attempts to find existing reports, iouReports, and receipts. If it doesn't find them, then * it creates optimistic versions of them and uses those instead @@ -1017,6 +1184,125 @@ function getMoneyRequestInformation( }; } +/** + * Gathers all the data needed to make an expense. It attempts to find existing reports, iouReports, and receipts. If it doesn't find them, then + * it creates optimistic versions of them and uses those instead + */ +function getTrackExpenseInformation( + parentChatReport: OnyxEntry | EmptyObject, + participant: Participant, + comment: string, + amount: number, + currency: string, + created: string, + merchant: string, + receipt: Receipt | undefined, + payeeEmail = currentUserEmail, +): TrackExpenseInformation | EmptyObject { + // STEP 1: Get existing chat report + let chatReport = !isEmptyObject(parentChatReport) && parentChatReport?.reportID ? parentChatReport : null; + + // The chatReport always exist and we can get it from Onyx if chatReport is null. + if (!chatReport) { + chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${participant.reportID}`] ?? null; + } + + // If we still don't have a report, it likely doens't exist and we will early return here as it should not happen + // Maybe later, we can build an optimistic selfDM chat. + if (!chatReport) { + return {}; + } + + // STEP 2: Get the money request report. + // TODO: This is deferred to later as we are not sure if we create iouReport at all in future. + // We can build an optimistic iouReport here if needed. + + // STEP 3: Build optimistic receipt and transaction + const receiptObject: Receipt = {}; + let filename; + if (receipt?.source) { + receiptObject.source = receipt.source; + receiptObject.state = receipt.state ?? CONST.IOU.RECEIPT_STATE.SCANREADY; + filename = receipt.name; + } + const existingTransaction = allTransactionDrafts[`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`]; + const isDistanceRequest = existingTransaction && existingTransaction.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE; + let optimisticTransaction = TransactionUtils.buildOptimisticTransaction( + amount, + currency, + chatReport.reportID, + comment, + created, + '', + '', + merchant, + receiptObject, + filename, + null, + '', + '', + false, + isDistanceRequest ? {waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD} : undefined, + ); + + // If there is an existing transaction (which is the case for distance requests), then the data from the existing transaction + // needs to be manually merged into the optimistic transaction. This is because buildOnyxDataForMoneyRequest() uses `Onyx.set()` for the transaction + // data. This is a big can of worms to change it to `Onyx.merge()` as explored in https://expensify.slack.com/archives/C05DWUDHVK7/p1692139468252109. + // I want to clean this up at some point, but it's possible this will live in the code for a while so I've created https://github.com/Expensify/App/issues/25417 + // to remind me to do this. + if (isDistanceRequest) { + optimisticTransaction = fastMerge(existingTransaction, optimisticTransaction, false); + } + + // STEP 4: Build optimistic reportActions. We need: + // 1. IOU action for the chatReport + // 2. The transaction thread, which requires the iouAction, and CREATED action for the transaction thread + const currentTime = DateUtils.getDBTime(); + const iouAction = ReportUtils.buildOptimisticIOUReportAction( + CONST.IOU.REPORT_ACTION_TYPE.CREATE, + amount, + currency, + comment, + [participant], + optimisticTransaction.transactionID, + undefined, + chatReport.reportID, + false, + false, + receiptObject, + false, + currentTime, + ); + const optimisticTransactionThread = ReportUtils.buildTransactionThread(iouAction, chatReport.reportID); + const optimisticCreatedActionForTransactionThread = ReportUtils.buildOptimisticCreatedReportAction(payeeEmail); + + // STEP 5: Build Onyx Data + const [optimisticData, successData, failureData] = buildOnyxDataForTrackExpense( + chatReport, + optimisticTransaction, + iouAction, + optimisticTransactionThread, + optimisticCreatedActionForTransactionThread, + ); + + return { + chatReport, + iouReport: undefined, + transaction: optimisticTransaction, + iouAction, + createdChatReportActionID: '0', + createdExpenseReportActionID: '0', + reportPreviewAction: undefined, + transactionThreadReportID: optimisticTransactionThread.reportID, + createdReportActionIDForThread: optimisticCreatedActionForTransactionThread.reportActionID, + onyxData: { + optimisticData, + successData, + failureData, + }, + }; +} + /** Requests money based on a distance (eg. mileage from a map) */ function createDistanceRequest( report: OnyxTypes.Report, @@ -1635,6 +1921,62 @@ function requestMoney( Report.notifyNewAction(activeReportID, payeeAccountID); } +/** + * Track an expense + */ +function trackExpense( + report: OnyxTypes.Report, + amount: number, + currency: string, + created: string, + merchant: string, + payeeEmail: string, + payeeAccountID: number, + participant: Participant, + comment: string, + receipt: Receipt, +) { + const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created); + const { + iouReport, + chatReport, + transaction, + iouAction, + createdChatReportActionID, + createdExpenseReportActionID, + reportPreviewAction, + transactionThreadReportID, + createdReportActionIDForThread, + onyxData, + } = getTrackExpenseInformation(report, participant, comment, amount, currency, currentCreated, merchant, receipt, payeeEmail); + const activeReportID = report.reportID; + + const parameters: TrackExpenseParams = { + amount, + currency, + comment, + created: currentCreated, + merchant, + iouReportID: iouReport?.reportID ?? '0', + chatReportID: chatReport.reportID, + transactionID: transaction.transactionID, + reportActionID: iouAction.reportActionID, + createdChatReportActionID, + createdExpenseReportActionID, + reportPreviewReportActionID: reportPreviewAction?.reportActionID ?? '0', + receipt, + receiptState: receipt?.state, + tag: '', + transactionThreadReportID, + createdReportActionIDForThread, + }; + + API.write(WRITE_COMMANDS.TRACK_EXPENSE, parameters, onyxData); + resetMoneyRequestInfo(); + Navigation.dismissModal(activeReportID); + Report.notifyNewAction(activeReportID, payeeAccountID); +} + /** * Build the Onyx data and IOU split necessary for splitting a bill with 3+ users. * 1. Build the optimistic Onyx data for the group chat, i.e. chatReport and iouReportAction creating the former if it doesn't yet exist. @@ -4347,4 +4689,5 @@ export { cancelPayment, navigateToStartStepIfScanFileCannotBeRead, savePreferredPaymentMethod, + trackExpense, }; diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.js b/src/pages/iou/request/step/IOURequestStepConfirmation.js index 6a2a5dc6f70f..e518a2ac4616 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.js +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.js @@ -218,6 +218,29 @@ function IOURequestStepConfirmation({ [report, transaction, transactionTaxCode, transactionTaxAmount, currentUserPersonalDetails.login, currentUserPersonalDetails.accountID, policy, policyTags, policyCategories], ); + /** + * @param {Array} selectedParticipants + * @param {String} trimmedComment + * @param {File} [receiptObj] + */ + const trackExpense = useCallback( + (selectedParticipants, trimmedComment, receiptObj) => { + IOU.trackExpense( + report, + transaction.amount, + transaction.currency, + transaction.created, + transaction.merchant, + currentUserPersonalDetails.login, + currentUserPersonalDetails.accountID, + selectedParticipants[0], + trimmedComment, + receiptObj, + ); + }, + [report, transaction, currentUserPersonalDetails.login, currentUserPersonalDetails.accountID], + ); + /** * @param {Array} selectedParticipants * @param {String} trimmedComment @@ -309,6 +332,11 @@ function IOURequestStepConfirmation({ return; } + if (iouType === CONST.IOU.TYPE.TRACK_EXPENSE) { + trackExpense(selectedParticipants, trimmedComment, receiptFile); + return; + } + if (receiptFile) { // If the transaction amount is zero, then the money is being requested through the "Scan" flow and the GPS coordinates need to be included. if (transaction.amount === 0) { @@ -347,7 +375,18 @@ function IOURequestStepConfirmation({ requestMoney(selectedParticipants, trimmedComment); }, - [iouType, transaction, currentUserPersonalDetails.login, currentUserPersonalDetails.accountID, report, requestType, createDistanceRequest, requestMoney, receiptFile], + [ + transaction, + iouType, + receiptFile, + requestType, + requestMoney, + currentUserPersonalDetails.login, + currentUserPersonalDetails.accountID, + report.reportID, + trackExpense, + createDistanceRequest, + ], ); /** From a40eef3c48c5bc0eb2bfa997a136e4a724be154c Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Fri, 8 Mar 2024 03:10:14 +0500 Subject: [PATCH 158/245] fix: ts errors --- src/components/ReportActionItem/MoneyReportView.tsx | 2 +- src/libs/ReportUtils.ts | 3 +-- src/types/onyx/Policy.ts | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx index f0d5c4d125e4..7e4b1d9187b4 100644 --- a/src/components/ReportActionItem/MoneyReportView.tsx +++ b/src/components/ReportActionItem/MoneyReportView.tsx @@ -57,7 +57,7 @@ function MoneyReportView({report, policy, shouldShowHorizontalRule}: MoneyReport ]; const sortedPolicyReportFields = useMemo((): PolicyReportField[] => { - const fields = ReportUtils.getAvailableReportFields(report, Object.values(policy.reportFields || {})); + const fields = ReportUtils.getAvailableReportFields(report, Object.values(policy?.reportFields ?? {})); return fields.sort(({orderWeight: firstOrderWeight}, {orderWeight: secondOrderWeight}) => firstOrderWeight - secondOrderWeight); }, [policy, report]); diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index bcb373ab2716..053a99ad9afe 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -23,7 +23,6 @@ import type { PersonalDetailsList, Policy, PolicyReportField, - PolicyReportFields, Report, ReportAction, ReportMetadata, @@ -2042,7 +2041,7 @@ function getFormulaTypeReportField(reportFields: Record) { return Object.values(reportFields).find((field) => isReportFieldOfTypeTitle(field)); } diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index db99aab0167a..12b6fd5e18cc 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -281,4 +281,4 @@ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback< export default Policy; -export type {Unit, CustomUnit, Attributes, Rate, TaxRate, TaxRates, TaxRatesWithDefault}; +export type {PolicyReportField, PolicyReportFieldType, Unit, CustomUnit, Attributes, Rate, TaxRate, TaxRates, TaxRatesWithDefault}; From 2a425ad50f68f544c5cf513c7b1176f48904dc00 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Fri, 8 Mar 2024 17:22:35 +0530 Subject: [PATCH 159/245] Added gpsPoints in endpoint --- src/libs/API/parameters/TrackExpenseParams.ts | 1 + src/libs/actions/IOU.ts | 11 ++++--- .../step/IOURequestStepConfirmation.js | 33 ++++++++++++++++++- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/libs/API/parameters/TrackExpenseParams.ts b/src/libs/API/parameters/TrackExpenseParams.ts index 0e17a316bb9f..9965463235cc 100644 --- a/src/libs/API/parameters/TrackExpenseParams.ts +++ b/src/libs/API/parameters/TrackExpenseParams.ts @@ -18,6 +18,7 @@ type TrackExpenseParams = { receipt: Receipt; receiptState?: ValueOf; tag?: string; + gpsPoints?: string; transactionThreadReportID: string; createdReportActionIDForThread: string; }; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 26e78ee4cca8..8342d2d466f8 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -91,7 +91,7 @@ type TrackExpenseInformation = { transaction: OnyxTypes.Transaction; iouAction: OptimisticIOUReportAction; createdChatReportActionID: string; - createdExpenseReportActionID: string; + createdExpenseReportActionID?: string; reportPreviewAction?: OnyxTypes.ReportAction; transactionThreadReportID: string; createdReportActionIDForThread: string; @@ -1291,7 +1291,7 @@ function getTrackExpenseInformation( transaction: optimisticTransaction, iouAction, createdChatReportActionID: '0', - createdExpenseReportActionID: '0', + createdExpenseReportActionID: undefined, reportPreviewAction: undefined, transactionThreadReportID: optimisticTransactionThread.reportID, createdReportActionIDForThread: optimisticCreatedActionForTransactionThread.reportActionID, @@ -1935,6 +1935,7 @@ function trackExpense( participant: Participant, comment: string, receipt: Receipt, + gpsPoints = undefined, ) { const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created); const { @@ -1957,16 +1958,18 @@ function trackExpense( comment, created: currentCreated, merchant, - iouReportID: iouReport?.reportID ?? '0', + iouReportID: iouReport?.reportID, chatReportID: chatReport.reportID, transactionID: transaction.transactionID, reportActionID: iouAction.reportActionID, createdChatReportActionID, createdExpenseReportActionID, - reportPreviewReportActionID: reportPreviewAction?.reportActionID ?? '0', + reportPreviewReportActionID: reportPreviewAction?.reportActionID, receipt, receiptState: receipt?.state, tag: '', + // This needs to be a string of JSON because of limitations with the fetch() API and nested objects + gpsPoints: gpsPoints ? JSON.stringify(gpsPoints) : undefined, transactionThreadReportID, createdReportActionIDForThread, }; diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.js b/src/pages/iou/request/step/IOURequestStepConfirmation.js index e518a2ac4616..6285fd1c4e23 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.js +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.js @@ -224,7 +224,7 @@ function IOURequestStepConfirmation({ * @param {File} [receiptObj] */ const trackExpense = useCallback( - (selectedParticipants, trimmedComment, receiptObj) => { + (selectedParticipants, trimmedComment, receiptObj, gpsPoints) => { IOU.trackExpense( report, transaction.amount, @@ -236,6 +236,7 @@ function IOURequestStepConfirmation({ selectedParticipants[0], trimmedComment, receiptObj, + gpsPoints, ); }, [report, transaction, currentUserPersonalDetails.login, currentUserPersonalDetails.accountID], @@ -333,6 +334,36 @@ function IOURequestStepConfirmation({ } if (iouType === CONST.IOU.TYPE.TRACK_EXPENSE) { + if (receiptFile) { + // If the transaction amount is zero, then the money is being requested through the "Scan" flow and the GPS coordinates need to be included. + if (transaction.amount === 0) { + getCurrentPosition( + (successData) => { + trackExpense(selectedParticipants, trimmedComment, receiptFile, { + lat: successData.coords.latitude, + long: successData.coords.longitude, + }); + }, + (errorData) => { + Log.info('[IOURequestStepConfirmation] getCurrentPosition failed', false, errorData); + // When there is an error, the money can still be requested, it just won't include the GPS coordinates + trackExpense(selectedParticipants, trimmedComment, receiptFile); + }, + { + // It's OK to get a cached location that is up to an hour old because the only accuracy needed is the country the user is in + maximumAge: 1000 * 60 * 60, + + // 15 seconds, don't wait too long because the server can always fall back to using the IP address + timeout: 15000, + }, + ); + return; + } + + // Otherwise, the money is being requested through the "Manual" flow with an attached image and the GPS coordinates are not needed. + trackExpense(selectedParticipants, trimmedComment, receiptFile); + return; + } trackExpense(selectedParticipants, trimmedComment, receiptFile); return; } From 87fcc3316bdc827766874ff90d4e9bfa298f3a69 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Sat, 9 Mar 2024 02:13:25 +0500 Subject: [PATCH 160/245] rename report fields as field list --- .../ReportActionItem/MoneyReportView.tsx | 2 +- src/libs/ReportUtils.ts | 8 +++--- src/libs/actions/Report.ts | 6 ++-- src/pages/EditReportFieldPage.tsx | 4 +-- src/pages/home/ReportScreen.js | 4 +-- src/pages/home/report/ReportActionItem.tsx | 2 +- src/types/onyx/Policy.ts | 28 +++++++++++++++++-- src/types/onyx/Report.ts | 2 +- 8 files changed, 39 insertions(+), 17 deletions(-) diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx index 7e4b1d9187b4..61ff493e5358 100644 --- a/src/components/ReportActionItem/MoneyReportView.tsx +++ b/src/components/ReportActionItem/MoneyReportView.tsx @@ -57,7 +57,7 @@ function MoneyReportView({report, policy, shouldShowHorizontalRule}: MoneyReport ]; const sortedPolicyReportFields = useMemo((): PolicyReportField[] => { - const fields = ReportUtils.getAvailableReportFields(report, Object.values(policy?.reportFields ?? {})); + const fields = ReportUtils.getAvailableReportFields(report, Object.values(policy?.fieldList ?? {})); return fields.sort(({orderWeight: firstOrderWeight}, {orderWeight: secondOrderWeight}) => firstOrderWeight - secondOrderWeight); }, [policy, report]); diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 84361636acb4..6967727bd872 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2063,7 +2063,7 @@ function getTitleReportField(reportFields: Record) { * Get the report fields attached to the policy given policyID */ function getReportFieldsByPolicyID(policyID: string) { - return Object.entries(allPolicies ?? {}).find(([key]) => key.replace(ONYXKEYS.COLLECTION.POLICY, '') === policyID)?.[1]?.reportFields; + return Object.entries(allPolicies ?? {}).find(([key]) => key.replace(ONYXKEYS.COLLECTION.POLICY, '') === policyID)?.[1]?.fieldList; } /** @@ -2072,7 +2072,7 @@ function getReportFieldsByPolicyID(policyID: string) { function getAvailableReportFields(report: Report, policyReportFields: PolicyReportField[]): PolicyReportField[] { // Get the report fields that are attached to a report. These will persist even if a field is deleted from the policy. - const reportFields = Object.values(report.reportFields ?? {}); + const reportFields = Object.values(report.fieldList ?? {}); const reportIsSettled = isSettled(report.reportID); // If the report is settled, we don't want to show any new field that gets added to the policy. @@ -2083,7 +2083,7 @@ function getAvailableReportFields(report: Report, policyReportFields: PolicyRepo // If the report is unsettled, we want to merge the new fields that get added to the policy with the fields that // are attached to the report. const mergedFieldIds = Array.from(new Set([...policyReportFields.map(({fieldID}) => fieldID), ...reportFields.map(({fieldID}) => fieldID)])); - return mergedFieldIds.map((id) => report?.reportFields?.[id] ?? policyReportFields.find(({fieldID}) => fieldID === id)) as PolicyReportField[]; + return mergedFieldIds.map((id) => report?.fieldList?.[id] ?? policyReportFields.find(({fieldID}) => fieldID === id)) as PolicyReportField[]; } /** @@ -2091,7 +2091,7 @@ function getAvailableReportFields(report: Report, policyReportFields: PolicyRepo */ function getMoneyRequestReportName(report: OnyxEntry, policy: OnyxEntry | undefined = undefined): string { const isReportSettled = isSettled(report?.reportID ?? ''); - const reportFields = isReportSettled ? report?.reportFields : getReportFieldsByPolicyID(report?.policyID ?? ''); + const reportFields = isReportSettled ? report?.fieldList : getReportFieldsByPolicyID(report?.policyID ?? ''); const titleReportField = getFormulaTypeReportField(reportFields ?? {}); if (titleReportField && report?.reportName && reportFieldsEnabled(report)) { diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 94fe324d306a..c266b4d43887 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -1617,7 +1617,7 @@ function updateReportField(reportID: string, reportField: PolicyReportField, pre onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: { - reportFields: { + fieldList: { [reportField.fieldID]: reportField, }, pendingFields: { @@ -1627,7 +1627,7 @@ function updateReportField(reportID: string, reportField: PolicyReportField, pre }, ]; - if (reportField.type === 'dropdown') { + if (reportField.type === 'dropdown' && reportField.value) { optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.RECENTLY_USED_REPORT_FIELDS, @@ -1642,7 +1642,7 @@ function updateReportField(reportID: string, reportField: PolicyReportField, pre onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: { - reportFields: { + fieldList: { [reportField.fieldID]: previousReportField, }, pendingFields: { diff --git a/src/pages/EditReportFieldPage.tsx b/src/pages/EditReportFieldPage.tsx index 015b2cabd51c..95620a2b9389 100644 --- a/src/pages/EditReportFieldPage.tsx +++ b/src/pages/EditReportFieldPage.tsx @@ -40,7 +40,7 @@ type EditReportFieldPageProps = EditReportFieldPageOnyxProps & { }; function EditReportFieldPage({route, policy, report}: EditReportFieldPageProps) { - const reportField = report?.reportFields?.[route.params.fieldID] ?? policy?.reportFields?.[route.params.fieldID]; + const reportField = report?.fieldList?.[route.params.fieldID] ?? policy?.fieldList?.[route.params.fieldID]; const isDisabled = ReportUtils.isReportFieldDisabled(report, reportField ?? null, policy); if (!reportField || !report || isDisabled) { @@ -105,7 +105,7 @@ function EditReportFieldPage({route, policy, report}: EditReportFieldPageProps) fieldID={reportField.fieldID} fieldName={Str.UCFirst(reportField.name)} fieldValue={fieldValue} - fieldOptions={reportField.values} + fieldOptions={reportField.values.filter((value) => !(value in reportField.disabledOptions))} onSubmit={handleReportFieldChange} /> ); diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index da5a8e4aae27..689ac8b7d962 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -192,7 +192,7 @@ function ReportScreen({ managerID: reportProp.managerID, total: reportProp.total, nonReimbursableTotal: reportProp.nonReimbursableTotal, - reportFields: reportProp.reportFields, + reportFields: reportProp.fieldList, ownerAccountID: reportProp.ownerAccountID, currency: reportProp.currency, participantAccountIDs: reportProp.participantAccountIDs, @@ -229,7 +229,7 @@ function ReportScreen({ reportProp.managerID, reportProp.total, reportProp.nonReimbursableTotal, - reportProp.reportFields, + reportProp.fieldList, reportProp.ownerAccountID, reportProp.currency, reportProp.participantAccountIDs, diff --git a/src/pages/home/report/ReportActionItem.tsx b/src/pages/home/report/ReportActionItem.tsx index e8cf1cf23af9..6306c89da40c 100644 --- a/src/pages/home/report/ReportActionItem.tsx +++ b/src/pages/home/report/ReportActionItem.tsx @@ -921,7 +921,7 @@ export default withOnyx({ prevProps.report?.total === nextProps.report?.total && prevProps.report?.nonReimbursableTotal === nextProps.report?.nonReimbursableTotal && prevProps.linkedReportActionID === nextProps.linkedReportActionID && - lodashIsEqual(prevProps.report.reportFields, nextProps.report.reportFields) && + lodashIsEqual(prevProps.report.fieldList, nextProps.report.fieldList) && lodashIsEqual(prevProps.policy, nextProps.policy) && lodashIsEqual(prevParentReportAction, nextParentReportAction) ); diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index c45a5ad32c2d..65ce91544541 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -104,10 +104,32 @@ type PolicyReportField = { deletable: boolean; /** Value of the field */ - value: string; + value: string | null; /** Options to select from if field is of type dropdown */ values: string[]; + + target: string; + + /** Tax UDFs have keys holding the names of taxes (eg, VAT), values holding percentages (eg, 15%) and a value indicating the currently selected tax value (eg, 15%). */ + keys: string[]; + + /** list of externalIDs, this are either imported from the integrations or auto generated by us, each externalID */ + externalIDs: string[]; + + disabledOptions: boolean[]; + + /** Is this a tax user defined report field */ + isTax: boolean; + + /** This is the selected externalID in an expense. */ + externalID?: string | null; + + /** Automated action or integration that added this report field */ + origin?: string | null; + + /** This is indicates which default value we should use. It was preferred using this over having defaultValue (which we have anyway for historical reasons), since the values are not unique we can't determine which key the defaultValue is referring too. It was also preferred over having defaultKey since the keys are user editable and can be changed. The externalIDs work effectively as an ID, which never changes even after changing the key, value or position of the option. */ + defaultExternalID?: string | null; }; type PendingJoinRequestPolicy = { @@ -122,7 +144,7 @@ type PendingJoinRequestPolicy = { avatar?: string; }> >; -} +}; type Policy = OnyxCommon.OnyxValueWithOfflineFeedback< { @@ -270,7 +292,7 @@ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback< connections?: Record; /** Report fields attached to the policy */ - reportFields?: Record; + fieldList?: Record; /** Whether the Categories feature is enabled */ areCategoriesEnabled?: boolean; diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 49e5b07e9181..7c2570314243 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -171,7 +171,7 @@ type Report = OnyxCommon.OnyxValueWithOfflineFeedback< selected?: boolean; /** If the report contains reportFields, save the field id and its value */ - reportFields?: Record; + fieldList?: Record; }, PolicyReportField['fieldID'] >; From 92f803ae9a0bcfcee4a84cf312e935a833656664 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Sat, 9 Mar 2024 18:30:21 +0530 Subject: [PATCH 161/245] Fixing on which room we should allow track expense --- src/libs/API/parameters/TrackExpenseParams.ts | 6 +- src/libs/ReportUtils.ts | 2 +- src/libs/actions/IOU.ts | 78 ++++++++++++++++--- .../iou/request/step/IOURequestStepAmount.js | 2 +- .../step/IOURequestStepConfirmation.js | 26 ++++++- .../request/step/IOURequestStepDistance.js | 2 +- .../request/step/IOURequestStepScan/index.js | 2 +- .../step/IOURequestStepScan/index.native.js | 2 +- 8 files changed, 102 insertions(+), 18 deletions(-) diff --git a/src/libs/API/parameters/TrackExpenseParams.ts b/src/libs/API/parameters/TrackExpenseParams.ts index 9965463235cc..f48c8666f109 100644 --- a/src/libs/API/parameters/TrackExpenseParams.ts +++ b/src/libs/API/parameters/TrackExpenseParams.ts @@ -13,11 +13,15 @@ type TrackExpenseParams = { transactionID: string; reportActionID: string; createdChatReportActionID: string; - createdExpenseReportActionID?: string; + createdIOUReportActionID?: string; reportPreviewReportActionID?: string; receipt: Receipt; receiptState?: ValueOf; + category?: string; tag?: string; + taxCode: string; + taxAmount: number; + billable?: boolean; gpsPoints?: string; transactionThreadReportID: string; createdReportActionIDForThread: string; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 45d95a6f47be..0f5cc3507ed4 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4498,7 +4498,7 @@ function getMoneyRequestOptions(report: OnyxEntry, policy: OnyxEntry, + policyTagList?: OnyxEntry, + policyCategories?: OnyxEntry, ): [OnyxUpdate[], OnyxUpdate[], OnyxUpdate[]] { const isScanRequest = TransactionUtils.isScanRequest(transaction); const clearedPendingFields = Object.fromEntries(Object.keys(transaction.pendingFields ?? {}).map((key) => [key, null])); @@ -958,6 +961,22 @@ function buildOnyxDataForTrackExpense( }, ]; + // We don't need to compute violations unless we're on a paid policy + if (!policy || !PolicyUtils.isPaidGroupPolicy(policy)) { + return [optimisticData, successData, failureData]; + } + + const violationsOnyxData = ViolationsUtils.getViolationsOnyxData(transaction, [], !!policy.requiresTag, policyTagList ?? {}, !!policy.requiresCategory, policyCategories ?? {}); + + if (violationsOnyxData) { + optimisticData.push(violationsOnyxData); + failureData.push({ + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`, + value: [], + }); + } + return [optimisticData, successData, failureData]; } @@ -1186,6 +1205,12 @@ function getTrackExpenseInformation( created: string, merchant: string, receipt: Receipt | undefined, + category: string | undefined, + tag: string | undefined, + billable: boolean | undefined, + policy: OnyxEntry | undefined, + policyTagList: OnyxEntry | undefined, + policyCategories: OnyxEntry | undefined, payeeEmail = currentUserEmail, ): TrackExpenseInformation | EmptyObject { // STEP 1: Get existing chat report @@ -1228,9 +1253,9 @@ function getTrackExpenseInformation( receiptObject, filename, null, - '', - '', - false, + category, + tag, + billable, isDistanceRequest ? {waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD} : undefined, ); @@ -1272,6 +1297,9 @@ function getTrackExpenseInformation( iouAction, optimisticTransactionThread, optimisticCreatedActionForTransactionThread, + policy, + policyTagList, + policyCategories, ); return { @@ -1280,7 +1308,7 @@ function getTrackExpenseInformation( transaction: optimisticTransaction, iouAction, createdChatReportActionID: '0', - createdExpenseReportActionID: undefined, + createdIOUReportActionID: undefined, reportPreviewAction: undefined, transactionThreadReportID: optimisticTransactionThread.reportID, createdReportActionIDForThread: optimisticCreatedActionForTransactionThread.reportActionID, @@ -1924,6 +1952,14 @@ function trackExpense( participant: Participant, comment: string, receipt: Receipt, + category?: string, + tag?: string, + taxCode = '', + taxAmount = 0, + billable?: boolean, + policy?: OnyxEntry, + policyTagList?: OnyxEntry, + policyCategories?: OnyxEntry, gpsPoints = undefined, ) { const currentCreated = DateUtils.enrichMoneyRequestTimestamp(created); @@ -1933,12 +1969,28 @@ function trackExpense( transaction, iouAction, createdChatReportActionID, - createdExpenseReportActionID, + createdIOUReportActionID, reportPreviewAction, transactionThreadReportID, createdReportActionIDForThread, onyxData, - } = getTrackExpenseInformation(report, participant, comment, amount, currency, currentCreated, merchant, receipt, payeeEmail); + } = getTrackExpenseInformation( + report, + participant, + comment, + amount, + currency, + currentCreated, + merchant, + receipt, + category, + tag, + billable, + policy, + policyTagList, + policyCategories, + payeeEmail, + ); const activeReportID = report.reportID; const parameters: TrackExpenseParams = { @@ -1952,11 +2004,15 @@ function trackExpense( transactionID: transaction.transactionID, reportActionID: iouAction.reportActionID, createdChatReportActionID, - createdExpenseReportActionID, + createdIOUReportActionID, reportPreviewReportActionID: reportPreviewAction?.reportActionID, receipt, receiptState: receipt?.state, - tag: '', + category, + tag, + taxCode, + taxAmount, + billable, // This needs to be a string of JSON because of limitations with the fetch() API and nested objects gpsPoints: gpsPoints ? JSON.stringify(gpsPoints) : undefined, transactionThreadReportID, @@ -4464,11 +4520,11 @@ function replaceReceipt(transactionID: string, file: File, source: string) { * @param transactionID of the transaction to set the participants of * @param report attached to the transaction */ -function setMoneyRequestParticipantsFromReport(transactionID: string, report: OnyxTypes.Report, iouType: ValueOf) { +function setMoneyRequestParticipantsFromReport(transactionID: string, report: OnyxTypes.Report) { // If the report is iou or expense report, we should get the chat report to set participant for request money const chatReport = ReportUtils.isMoneyRequestReport(report) ? ReportUtils.getReport(report.chatReportID) : report; const currentUserAccountID = currentUserPersonalDetails.accountID; - const shouldAddAsReport = iouType === CONST.IOU.TYPE.TRACK_EXPENSE && !isEmptyObject(chatReport) && (ReportUtils.isSelfDM(chatReport) || ReportUtils.isAdminRoom(chatReport)); + const shouldAddAsReport = !isEmptyObject(chatReport) && ReportUtils.isSelfDM(chatReport); const participants: Participant[] = ReportUtils.isPolicyExpenseChat(chatReport) || shouldAddAsReport ? [{reportID: chatReport?.reportID, isPolicyExpenseChat: ReportUtils.isPolicyExpenseChat(chatReport), selected: true}] diff --git a/src/pages/iou/request/step/IOURequestStepAmount.js b/src/pages/iou/request/step/IOURequestStepAmount.js index 07882e95a9ae..9fdd2bea24f4 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.js +++ b/src/pages/iou/request/step/IOURequestStepAmount.js @@ -144,7 +144,7 @@ function IOURequestStepAmount({ // inside a report. In this case, the participants can be automatically assigned from the report and the user can skip the participants step and go straight // to the confirm step. if (report.reportID) { - IOU.setMoneyRequestParticipantsFromReport(transactionID, report, iouType); + IOU.setMoneyRequestParticipantsFromReport(transactionID, report); Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(iouType, transactionID, reportID)); return; } diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.js b/src/pages/iou/request/step/IOURequestStepConfirmation.js index 6285fd1c4e23..b7c669dcebc9 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.js +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.js @@ -236,10 +236,34 @@ function IOURequestStepConfirmation({ selectedParticipants[0], trimmedComment, receiptObj, + transaction.category, + transaction.tag, + transactionTaxCode, + transactionTaxAmount, + transaction.billable, + policy, + policyTags, + policyCategories, gpsPoints, ); }, - [report, transaction, currentUserPersonalDetails.login, currentUserPersonalDetails.accountID], + [ + report, + transaction.amount, + transaction.currency, + transaction.created, + transaction.merchant, + transaction.category, + transaction.tag, + transaction.billable, + currentUserPersonalDetails.login, + currentUserPersonalDetails.accountID, + transactionTaxCode, + transactionTaxAmount, + policy, + policyTags, + policyCategories, + ], ); /** diff --git a/src/pages/iou/request/step/IOURequestStepDistance.js b/src/pages/iou/request/step/IOURequestStepDistance.js index 7df5df4cb203..320359192c8d 100644 --- a/src/pages/iou/request/step/IOURequestStepDistance.js +++ b/src/pages/iou/request/step/IOURequestStepDistance.js @@ -127,7 +127,7 @@ function IOURequestStepDistance({ // inside a report. In this case, the participants can be automatically assigned from the report and the user can skip the participants step and go straight // to the confirm step. if (report.reportID) { - IOU.setMoneyRequestParticipantsFromReport(transactionID, report, iouType); + IOU.setMoneyRequestParticipantsFromReport(transactionID, report); Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(iouType, transactionID, reportID)); return; } diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.js b/src/pages/iou/request/step/IOURequestStepScan/index.js index 05961bd6c4c3..7de121af52b4 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.js @@ -129,7 +129,7 @@ function IOURequestStepScan({ // If the transaction was created from the + menu from the composer inside of a chat, the participants can automatically // be added to the transaction (taken from the chat report participants) and then the person is taken to the confirmation step. - IOU.setMoneyRequestParticipantsFromReport(transactionID, report, iouType); + IOU.setMoneyRequestParticipantsFromReport(transactionID, report); Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(iouType, transactionID, reportID)); }, [iouType, report, reportID, transactionID, isFromGlobalCreate, backTo]); diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.js b/src/pages/iou/request/step/IOURequestStepScan/index.native.js index 2ef49af80441..f421417b53f6 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.js @@ -189,7 +189,7 @@ function IOURequestStepScan({ // If the transaction was created from the + menu from the composer inside of a chat, the participants can automatically // be added to the transaction (taken from the chat report participants) and then the person is taken to the confirmation step. - IOU.setMoneyRequestParticipantsFromReport(transactionID, report, iouType); + IOU.setMoneyRequestParticipantsFromReport(transactionID, report); Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(iouType, transactionID, reportID)); }, [iouType, report, reportID, transactionID, isFromGlobalCreate, backTo]); From 9aaad393d02d65c711ae55b65104f26e03adb878 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Mon, 11 Mar 2024 14:53:19 +0700 Subject: [PATCH 162/245] fix: error style in workspace detail --- src/components/AvatarWithImagePicker.tsx | 2 +- src/pages/workspace/WorkspaceProfilePage.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/AvatarWithImagePicker.tsx b/src/components/AvatarWithImagePicker.tsx index b6cee205dd0e..ae2983989cc1 100644 --- a/src/components/AvatarWithImagePicker.tsx +++ b/src/components/AvatarWithImagePicker.tsx @@ -283,7 +283,7 @@ function AvatarWithImagePicker({ return ( - + Date: Tue, 12 Mar 2024 16:27:34 +0700 Subject: [PATCH 163/245] fix: style avatar with picker --- src/components/AvatarWithImagePicker.tsx | 3 ++- src/pages/workspace/WorkspaceProfilePage.tsx | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/AvatarWithImagePicker.tsx b/src/components/AvatarWithImagePicker.tsx index ae2983989cc1..0b1ddc30c996 100644 --- a/src/components/AvatarWithImagePicker.tsx +++ b/src/components/AvatarWithImagePicker.tsx @@ -283,11 +283,12 @@ function AvatarWithImagePicker({ return ( - + Policy.updateWorkspaceAvatar(policy?.id ?? '', file as File)} onImageRemoved={() => Policy.deleteWorkspaceAvatar(policy?.id ?? '')} From a83cf6ecf91eeab3df13c1c7db4837172bb47714 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 12 Mar 2024 15:26:05 +0100 Subject: [PATCH 164/245] undo https://github.com/Expensify/App/pull/37839/files --- src/libs/ReportActionsUtils.ts | 1 - src/libs/actions/Report.ts | 28 +++-------- src/pages/home/ReportScreen.tsx | 2 +- src/pages/home/report/ReportActionsList.tsx | 10 +++- src/pages/home/report/ReportActionsView.tsx | 56 ++++++++++----------- 5 files changed, 44 insertions(+), 53 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 064c35fda0b6..b8783073a407 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -261,7 +261,6 @@ function getSortedReportActions(reportActions: ReportAction[] | null, shouldSort function getContinuousReportActionChain(sortedReportActions: ReportAction[], id?: string): ReportAction[] { let index; - console.log('get.sortedReportActions.0', sortedReportActions); if (id) { index = sortedReportActions.findIndex((obj) => obj.reportActionID === id); } else { diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 375888fd6e9c..06958c5ddaf7 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -116,28 +116,14 @@ Onyx.connect({ // map of reportID to all reportActions for that report const allReportActions: OnyxCollection = {}; -// map of reportID to the ID of the oldest reportAction for that report -const oldestReportActions: Record = {}; - -// map of report to the ID of the newest action for that report -const newestReportActions: Record = {}; - Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - callback: (actions, key) => { - if (!key || !actions) { + callback: (action, key) => { + if (!key || !action) { return; } const reportID = CollectionUtils.extractCollectionItemID(key); - allReportActions[reportID] = actions; - const sortedActions = ReportActionsUtils.getSortedReportActions(Object.values(actions)); - - if (sortedActions.length === 0) { - return; - } - - oldestReportActions[reportID] = sortedActions[0].reportActionID; - newestReportActions[reportID] = sortedActions[sortedActions.length - 1].reportActionID; + allReportActions[reportID] = action; }, }); @@ -898,7 +884,7 @@ function reconnect(reportID: string) { * Gets the older actions that have not been read yet. * Normally happens when you scroll up on a chat, and the actions have not been read yet. */ -function getOlderActions(reportID: string) { +function getOlderActions(reportID: string, reportActionID: string) { const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -931,7 +917,7 @@ function getOlderActions(reportID: string) { const parameters: GetOlderActionsParams = { reportID, - reportActionID: oldestReportActions[reportID], + reportActionID, }; API.read(READ_COMMANDS.GET_OLDER_ACTIONS, parameters, {optimisticData, successData, failureData}); @@ -941,7 +927,7 @@ function getOlderActions(reportID: string) { * Gets the newer actions that have not been read yet. * Normally happens when you are not located at the bottom of the list and scroll down on a chat. */ -function getNewerActions(reportID: string) { +function getNewerActions(reportID: string, reportActionID: string) { const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -974,7 +960,7 @@ function getNewerActions(reportID: string) { const parameters: GetNewerActionsParams = { reportID, - reportActionID: newestReportActions[reportID], + reportActionID, }; API.read(READ_COMMANDS.GET_NEWER_ACTIONS, parameters, {optimisticData, successData, failureData}); diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 6ce05ac2e9ea..ce46c0b2a0cd 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -232,7 +232,7 @@ function ReportScreen({ } const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, true); const currentRangeOfReportActions = ReportActionsUtils.getContinuousReportActionChain(sortedReportActions, reportActionIDFromRoute); - return currentRangeOfReportActions.filter((reportAction) => ReportActionsUtils.shouldReportActionBeVisible(reportAction, reportAction.reportActionID)); + return currentRangeOfReportActions; }, [reportActionIDFromRoute, allReportActions]); // Define here because reportActions are recalculated before mount, allowing data to display faster than useEffect can trigger. If we have cached reportActions, they will be shown immediately. We aim to display a loader first, then fetch relevant reportActions, and finally show them. diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index 542255ce6982..fcfcb912dc22 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -167,7 +167,12 @@ function ReportActionsList({ const lastReadTimeRef = useRef(report.lastReadTime); const sortedVisibleReportActions = useMemo( - () => sortedReportActions.filter((reportAction) => isOffline || reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || reportAction.errors), + () => + sortedReportActions.filter( + (reportAction) => + (isOffline || reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || reportAction.errors) && + ReportActionsUtils.shouldReportActionBeVisible(reportAction, reportAction.reportActionID), + ), [sortedReportActions, isOffline], ); const lastActionIndex = sortedVisibleReportActions[0]?.reportActionID; @@ -575,7 +580,8 @@ function ReportActionsList({ ref={reportScrollManager.ref} testID="report-actions-list" style={styles.overscrollBehaviorContain} - data={sortedReportActions} + // data={sortedReportActions} + data={sortedVisibleReportActions} renderItem={renderItem} contentContainerStyle={contentContainerStyle} keyExtractor={keyExtractor} diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index 7a6e28a59d23..3cdaea4aaea6 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -1,8 +1,8 @@ import {useIsFocused, useRoute} from '@react-navigation/native'; import lodashGet from 'lodash/get'; +import lodashIsEqual from 'lodash/isEqual'; import React, {useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; import {InteractionManager} from 'react-native'; -import lodashIsEqual from 'lodash/isEqual'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import useCopySelectionHelper from '@hooks/useCopySelectionHelper'; @@ -56,21 +56,28 @@ const SPACER = 16; let listIDCount = Math.round(Math.random() * 100); -// /** -// * usePaginatedReportActionList manages the logic for handling a list of messages with pagination and dynamic loading. -// * It determines the part of the message array to display ('visibleReportActions') based on the current linked message, -// * and manages pagination through 'handleReportActionPagination' function. -// * -// * @param {string} linkedID - ID of the linked message used for initial focus. -// * @param {array} allReportActions - Array of messages. -// * @param {function} fetchNewerReportActions - Function to fetch more messages. -// * @param {string} route - Current route, used to reset states on route change. -// * @param {boolean} isLoading - Loading state indicator. -// * @param {boolean} triggerListID - Used to trigger a listID change. -// * @returns {object} An object containing the sliced message array, the pagination function, -// * index of the linked message, and a unique list ID. -// */ -const usePaginatedReportActionList = (linkedID:string, allReportActions: OnyxTypes.ReportAction[], fetchNewerReportActions: (newestReportAction: OnyxTypes.ReportAction) => void, route: string, isLoading: boolean, triggerListID: boolean) => { +/** + * usePaginatedReportActionList manages the logic for handling a list of messages with pagination and dynamic loading. + * It determines the part of the message array to display ('visibleReportActions') based on the current linked message, + * and manages pagination through 'handleReportActionPagination' function. + * + * linkedID - ID of the linked message used for initial focus. + * allReportActions - Array of messages. + * fetchNewerReportActions - Function to fetch more messages. + * route - Current route, used to reset states on route change. + * isLoading - Loading state indicator. + * triggerListID - Used to trigger a listID change. + * returns {object} An object containing the sliced message array, the pagination function, + * index of the linked message, and a unique list ID. + */ +const usePaginatedReportActionList = ( + linkedID: string, + allReportActions: OnyxTypes.ReportAction[], + fetchNewerReportActions: (newestReportAction: OnyxTypes.ReportAction) => void, + route: string, + isLoading: boolean, + triggerListID: boolean, +) => { // triggerListID is used when navigating to a chat with messages loaded from LHN. Typically, these include thread actions, task actions, etc. Since these messages aren't the latest, we don't maintain their position and instead trigger a recalculation of their positioning in the list. // we don't set currentReportActionID on initial render as linkedID as it should trigger visibleReportActions after linked message was positioned const [currentReportActionID, setCurrentReportActionID] = useState(''); @@ -139,7 +146,6 @@ const usePaginatedReportActionList = (linkedID:string, allReportActions: OnyxTy }; }; - function ReportActionsView({ report, session, @@ -150,16 +156,15 @@ function ReportActionsView({ isLoadingNewerReportActions = false, isReadyForCommentLinking = false, }: ReportActionsViewProps) { - useCopySelectionHelper(); const reactionListRef = useContext(ReactionListContext); const route = useRoute(); - const reportActionID = lodashGet(route, 'params.reportActionID', null); + const reportActionID = route?.params?.reportActionID ?? null; const didLayout = useRef(false); const didSubscribeToReportTypingEvents = useRef(false); -const network = useNetwork(); -const {isSmallScreenWidth} = useWindowDimensions(); + const network = useNetwork(); + const {isSmallScreenWidth} = useWindowDimensions(); const contentListHeight = useRef(0); const layoutListHeight = useRef(0); const {windowHeight} = useWindowDimensions(); @@ -317,12 +322,7 @@ const {isSmallScreenWidth} = useWindowDimensions(); const loadNewerChats = useCallback( // eslint-disable-next-line rulesdir/prefer-early-return () => { - if ( - isLoadingInitialReportActions || - isLoadingOlderReportActions || - network.isOffline || - newestReportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE - ) { + if (isLoadingInitialReportActions || isLoadingOlderReportActions || network.isOffline || newestReportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { return; } // Determines if loading older reports is necessary when the content is smaller than the list @@ -464,7 +464,7 @@ const {isSmallScreenWidth} = useWindowDimensions(); ReportActionsView.displayName = 'ReportActionsView'; ReportActionsView.initMeasured = false; - function arePropsEqual(oldProps: ReportActionsViewProps, newProps: ReportActionsViewProps): boolean { +function arePropsEqual(oldProps: ReportActionsViewProps, newProps: ReportActionsViewProps): boolean { if (!lodashIsEqual(oldProps.isReadyForCommentLinking, newProps.isReadyForCommentLinking)) { return false; } From 173c11b1298fd93af604898bb83abcccf5f83b7e Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 12 Mar 2024 15:26:34 +0100 Subject: [PATCH 165/245] sync package lock --- package-lock.json | 147 ---------------------------------------------- 1 file changed, 147 deletions(-) diff --git a/package-lock.json b/package-lock.json index bc373abcd9b0..5cf96414d5f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8045,153 +8045,6 @@ "integrity": "sha512-C9Br1BQqm6io6lvYHptlLcOHbzlaqxp9tS35P8Qj3pdiiYRTzU3KPvZ61rQ+ZnZ4FOQ6MwPsKsmB8+6WHkAY6Q==", "license": "MIT" }, - "node_modules/@onfido/active-video-capture": { - "version": "0.28.6", - "resolved": "https://registry.npmjs.org/@onfido/active-video-capture/-/active-video-capture-0.28.6.tgz", - "integrity": "sha512-RFUeKaOSjj/amPp6VzhVkq/7kIkutEnnttT9n5KDeD3Vx8a09KD3a/xvxdQppveHlDAYsdBP6LrJwSSpjXiprg==", - "dependencies": { - "@mediapipe/face_detection": "^0.4.1646425229", - "@mediapipe/face_mesh": "^0.4.1633559619", - "@onfido/castor": "^2.2.2", - "@onfido/castor-icons": "^2.12.0", - "@tensorflow-models/face-detection": "^1.0.1", - "@tensorflow-models/face-landmarks-detection": "^1.0.2", - "@tensorflow/tfjs-backend-wasm": "3.20.0", - "@tensorflow/tfjs-backend-webgl": "3.20.0", - "@tensorflow/tfjs-converter": "3.20.0", - "@tensorflow/tfjs-core": "3.20.0", - "preact": "10.11.3", - "react-webcam": "^7.2.0" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/@onfido/active-video-capture/node_modules/@tensorflow-models/face-landmarks-detection": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@tensorflow-models/face-landmarks-detection/-/face-landmarks-detection-1.0.5.tgz", - "integrity": "sha512-54XJPi8g29/MknJ33ZBrLsEzr9kw/dJtrJMMD3xrCrnRlfFQPIKQ5PI2Wml55Fz2p4U2hemzBB0/H+S94JddIQ==", - "dependencies": { - "rimraf": "^3.0.2" - }, - "peerDependencies": { - "@mediapipe/face_mesh": "~0.4.0", - "@tensorflow-models/face-detection": "~1.0.0", - "@tensorflow/tfjs-backend-webgl": "^3.12.0", - "@tensorflow/tfjs-converter": "^3.12.0", - "@tensorflow/tfjs-core": "^3.12.0" - } - }, - "node_modules/@onfido/active-video-capture/node_modules/@tensorflow/tfjs-backend-cpu": { - "version": "3.20.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-cpu/-/tfjs-backend-cpu-3.20.0.tgz", - "integrity": "sha512-gf075YaBLwSAAiUwa0D4GvYyUBhbJ1BVSivUNQmUfGKvIr2lIhF0qstBr033YTc3lhkbFSHEEPAHh/EfpqyjXQ==", - "dependencies": { - "@types/seedrandom": "^2.4.28", - "seedrandom": "^3.0.5" - }, - "engines": { - "yarn": ">= 1.3.2" - }, - "peerDependencies": { - "@tensorflow/tfjs-core": "3.20.0" - } - }, - "node_modules/@onfido/active-video-capture/node_modules/@tensorflow/tfjs-backend-wasm": { - "version": "3.20.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-wasm/-/tfjs-backend-wasm-3.20.0.tgz", - "integrity": "sha512-k+sDcrcPtGToLjKRffgtSqlcN4MC6g4hXWRarZfgvvyvFqpxVfVqrGYHGTirXdN47sKYhmcTSMvbM2quGaaQnA==", - "dependencies": { - "@tensorflow/tfjs-backend-cpu": "3.20.0", - "@types/emscripten": "~0.0.34" - }, - "peerDependencies": { - "@tensorflow/tfjs-core": "3.20.0" - } - }, - "node_modules/@onfido/active-video-capture/node_modules/@tensorflow/tfjs-backend-webgl": { - "version": "3.20.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-webgl/-/tfjs-backend-webgl-3.20.0.tgz", - "integrity": "sha512-SucbyQ08re3HvRgVfarRtKFIjNM4JvIAzcXmw4vaE/HrCtPEePkGO1VrmfQoN470EdUmGiwgqAjoyBvM2VOlVg==", - "dependencies": { - "@tensorflow/tfjs-backend-cpu": "3.20.0", - "@types/offscreencanvas": "~2019.3.0", - "@types/seedrandom": "^2.4.28", - "@types/webgl-ext": "0.0.30", - "@types/webgl2": "0.0.6", - "seedrandom": "^3.0.5" - }, - "engines": { - "yarn": ">= 1.3.2" - }, - "peerDependencies": { - "@tensorflow/tfjs-core": "3.20.0" - } - }, - "node_modules/@onfido/active-video-capture/node_modules/@tensorflow/tfjs-converter": { - "version": "3.20.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-converter/-/tfjs-converter-3.20.0.tgz", - "integrity": "sha512-8EIYqtQwvSYw9GFNW2OFU8Qnl/FQF/kKAsQJoORYaZ419WJo+FIZWbAWDtCpJSAgkgoHH1jYWgV9H313cVmqxg==", - "peerDependencies": { - "@tensorflow/tfjs-core": "3.20.0" - } - }, - "node_modules/@onfido/active-video-capture/node_modules/@tensorflow/tfjs-core": { - "version": "3.20.0", - "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-core/-/tfjs-core-3.20.0.tgz", - "integrity": "sha512-L16JyVA4a8jFJXFgB9/oYZxcGq/GfLypt5dMVTyedznARZZ9SiY/UMMbo3IKl9ZylG1dOVVTpjzV3EvBYfeJXw==", - "dependencies": { - "@types/long": "^4.0.1", - "@types/offscreencanvas": "~2019.3.0", - "@types/seedrandom": "^2.4.28", - "@types/webgl-ext": "0.0.30", - "@webgpu/types": "0.1.16", - "long": "4.0.0", - "node-fetch": "~2.6.1", - "seedrandom": "^3.0.5" - }, - "engines": { - "yarn": ">= 1.3.2" - } - }, - "node_modules/@onfido/active-video-capture/node_modules/@webgpu/types": { - "version": "0.1.16", - "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.16.tgz", - "integrity": "sha512-9E61voMP4+Rze02jlTXud++Htpjyyk8vw5Hyw9FGRrmhHQg2GqbuOfwf5Klrb8vTxc2XWI3EfO7RUHMpxTj26A==" - }, - "node_modules/@onfido/castor": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@onfido/castor/-/castor-2.3.0.tgz", - "integrity": "sha512-FkydkjedS6b2g3SqgZMYnVRZvUs/MkaEuXXJWG9+LNc7DMFT1K8smOnNuHzkiM3cJhXL6yAADdKE0mg+ZIrucQ==", - "dependencies": { - "@onfido/castor-tokens": "^1.0.0-beta.6", - "csstype": "^3.1.1" - }, - "peerDependencies": { - "@onfido/castor-icons": ">=1.0.0" - } - }, - "node_modules/@onfido/castor-icons": { - "version": "2.22.0", - "resolved": "https://registry.npmjs.org/@onfido/castor-icons/-/castor-icons-2.22.0.tgz", - "integrity": "sha512-7OnCvu5xqVWcBLqovZyb99NP0oHw7sjkVYXZhi438i0U6Pgecrhu/14Gc/IN/kvgDxWj9qmiYdd0qdjNaVckrQ==", - "peerDependencies": { - "react": ">=17 || ^16.14 || ^15.7 || ^0.14.10" - } - }, - "node_modules/@onfido/castor-tokens": { - "version": "1.0.0-beta.6", - "resolved": "https://registry.npmjs.org/@onfido/castor-tokens/-/castor-tokens-1.0.0-beta.6.tgz", - "integrity": "sha512-MfwuSlNdM0Ay0cI3LLyqZGsHW0e1Y1R/0IdQKVU575PdWQx1Q/538aOZMo/a3/oSW0pMEgfOm+mNqPx057cvWA==" - }, - "node_modules/@onfido/opencv": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@onfido/opencv/-/opencv-2.1.1.tgz", - "integrity": "sha512-Bwo0YsZrrdm+p5hpNFZ7yrqNVWJxOUbQW9aWDEUtkDWUL+nX2RHIR6F4lBGVmbqnG24anadS/+nEvy80SwD3tQ==", - "dependencies": { - "mirada": "^0.0.15" - } - }, "node_modules/@onfido/react-native-sdk": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/@onfido/react-native-sdk/-/react-native-sdk-10.6.0.tgz", From 05b53a25dcf96a5b1ef1cfa0967495c662814ec2 Mon Sep 17 00:00:00 2001 From: Marcin Swornowski Date: Tue, 12 Mar 2024 16:39:51 +0100 Subject: [PATCH 166/245] fix: added hideBankAccountErrors to handleBackButtonPress in BankInfo --- src/pages/ReimbursementAccount/BankInfo/BankInfo.tsx | 1 + src/pages/ReimbursementAccount/ReimbursementAccountPage.js | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pages/ReimbursementAccount/BankInfo/BankInfo.tsx b/src/pages/ReimbursementAccount/BankInfo/BankInfo.tsx index ed00fbcff422..d1092293031b 100644 --- a/src/pages/ReimbursementAccount/BankInfo/BankInfo.tsx +++ b/src/pages/ReimbursementAccount/BankInfo/BankInfo.tsx @@ -124,6 +124,7 @@ function BankInfo({reimbursementAccount, reimbursementAccountDraft, plaidLinkTok [BANK_INFO_STEP_KEYS.PLAID_ACCESS_TOKEN]: '', }; ReimbursementAccountUtils.updateReimbursementAccountDraft(bankAccountData); + ReimbursementAccountUtils.hideBankAccountErrors(); BankAccounts.setBankAccountSubStep(null); } } else { diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.js b/src/pages/ReimbursementAccount/ReimbursementAccountPage.js index 9855090e70d1..3145525daa52 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.js +++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.js @@ -484,9 +484,7 @@ function ReimbursementAccountPage({reimbursementAccount, route, onfidoToken, pol reimbursementAccount={reimbursementAccount} continue={continueFunction} policyName={policyName} - onBackButtonPress={() => { - Navigation.goBack(); - }} + onBackButtonPress={Navigation.goBack} /> ); } From 4461124adcf25444c2d552ab5a021840a01cbb75 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 12 Mar 2024 17:27:53 +0100 Subject: [PATCH 167/245] migrate to TS --- .../ReportActionItem/MoneyRequestAction.tsx | 12 ------- src/pages/home/ReportScreen.tsx | 29 ++++++++------- src/pages/home/report/ReportActionsList.tsx | 12 +++++-- src/pages/home/report/ReportActionsView.tsx | 35 +++++++++++-------- 4 files changed, 44 insertions(+), 44 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestAction.tsx b/src/components/ReportActionItem/MoneyRequestAction.tsx index a6c7754b0ce9..05891311ba6d 100644 --- a/src/components/ReportActionItem/MoneyRequestAction.tsx +++ b/src/components/ReportActionItem/MoneyRequestAction.tsx @@ -89,19 +89,7 @@ function MoneyRequestAction({ return; } -// <<<<<<< HEAD -// // If the childReportID is not present, we need to create a new thread -// const childReportID = action?.childReportID; -// if (!childReportID) { -// const thread = ReportUtils.buildTransactionThread(action, requestReportID); -// const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs ?? []); -// Report.openReport(thread.reportID, '', userLogins, thread, action.reportActionID); -// Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(thread.reportID)); -// return; -// } -// ======= const childReportID = action?.childReportID ?? '0'; -// >>>>>>> da7697734c1f759786eba0a643a062b4e39a47ad Report.openReport(childReportID); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID)); }; diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index ce46c0b2a0cd..b1138cd8a28e 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -1,8 +1,6 @@ import {useIsFocused} from '@react-navigation/native'; import type {StackScreenProps} from '@react-navigation/stack'; -import lodashGet from 'lodash/get'; import lodashIsEqual from 'lodash/isEqual'; -import PropTypes from 'prop-types'; import React, {memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; import {InteractionManager, View} from 'react-native'; import type {FlatList, ViewStyle} from 'react-native'; @@ -55,9 +53,11 @@ import ReportFooter from './report/ReportFooter'; import {ActionListContext, ReactionListContext} from './ReportScreenContext'; import type {ActionListContextType, ReactionListRef, ScrollPosition} from './ReportScreenContext'; +type ReportActionMap = Record; + type ReportScreenOnyxProps = { /** All the report actions for this report */ - allReportActions: OnyxTypes.ReportAction[]; + allReportActions: ReportActionMap; /** Tells us if the sidebar has rendered */ isSidebarLoaded: OnyxEntry; @@ -126,13 +126,12 @@ function ReportScreen({ userLeavingStatus = false, currentReportID = '', navigation, - errors, }: ReportScreenProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); const reportIDFromRoute = getReportID(route); - const reportActionIDFromRoute = lodashGet(route, 'params.reportActionID', null); + const reportActionIDFromRoute = route?.params?.reportActionID ?? ''; const isFocused = useIsFocused(); const firstRenderRef = useRef(true); @@ -227,7 +226,7 @@ function ReportScreen({ const prevUserLeavingStatus = usePrevious(userLeavingStatus); const [isLinkingToMessage, setLinkingToMessage] = useState(!!reportActionIDFromRoute); const reportActions = useMemo(() => { - if (_.isEmpty(allReportActions)) { + if (!allReportActions) { return []; } const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, true); @@ -315,11 +314,11 @@ function ReportScreen({ const isReportReadyForDisplay = useMemo((): boolean => { // This is necessary so that when we are retrieving the next report data from Onyx the ReportActionsView will remount completely const isTransitioning = report && report.reportID !== reportIDFromRoute; - return reportIDFromRoute !== '' && report.reportID && !isTransitioning; + return reportIDFromRoute !== '' && !!report.reportID && !isTransitioning; }, [report, reportIDFromRoute]); const shouldShowSkeleton = - isLinkingToMessage || !isReportReadyForDisplay || isLoadingInitialReportActions || isLoading || (!!reportActionIDFromRoute && reportMetadata.isLoadingInitialReportActions); + isLinkingToMessage || !isReportReadyForDisplay || isLoadingInitialReportActions || isLoading || (!!reportActionIDFromRoute && reportMetadata?.isLoadingInitialReportActions); const shouldShowReportActionList = isReportReadyForDisplay && !isLoading; @@ -503,7 +502,7 @@ function ReportScreen({ InteractionManager.runAfterInteractions(() => { setLinkingToMessage(false); }); - }, [reportMetadata.isLoadingInitialReportActions]); + }, [reportMetadata?.isLoadingInitialReportActions]); const onLinkPress = () => { Navigation.setParams({reportActionID: ''}); @@ -511,13 +510,14 @@ function ReportScreen({ }; const isLinkedReportActionDeleted = useMemo(() => { - if (!reportActionIDFromRoute) { + if (!reportActionIDFromRoute || !allReportActions) { return false; } - return !_.isEmpty(allReportActions[reportActionIDFromRoute]) && ReportActionsUtils.isDeletedAction(allReportActions[reportActionIDFromRoute]); + const action = allReportActions[reportActionIDFromRoute]; + return action && ReportActionsUtils.isDeletedAction(action); }, [reportActionIDFromRoute, allReportActions]); - if (isLinkedReportActionDeleted || (!shouldShowSkeleton && reportActionIDFromRoute && _.isEmpty(reportActions) && !isLinkingToMessage)) { + if (isLinkedReportActionDeleted || (!shouldShowSkeleton && reportActionIDFromRoute && reportActions?.length === 0 && !isLinkingToMessage)) { return ( )} @@ -692,6 +690,7 @@ export default withViewportOffsetTop( prevProps.currentReportID === nextProps.currentReportID && prevProps.viewportOffsetTop === nextProps.viewportOffsetTop && lodashIsEqual(prevProps.parentReportAction, nextProps.parentReportAction) && + lodashIsEqual(prevProps.route, nextProps.route) && lodashIsEqual(prevProps.report, nextProps.report), ), ), diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index fcfcb912dc22..4cc1f0e3720b 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -69,10 +69,19 @@ type ReportActionsListProps = WithCurrentUserPersonalDetailsProps & { loadOlderChats: () => void; /** Function to load newer chats */ - loadNewerChats: LoadNewerChats; + loadNewerChats: () => void; /** Whether the composer is in full size */ isComposerFullSize?: boolean; + + /** ID of the list */ + listID: number; + + /** Callback executed on content size change */ + onContentSizeChange: (w: number, h: number) => void; + + /** Should enable auto scroll to top threshold */ + shouldEnableAutoScrollToTopThreshold: boolean; }; const VERTICAL_OFFSET_THRESHOLD = 200; @@ -580,7 +589,6 @@ function ReportActionsList({ ref={reportScrollManager.ref} testID="report-actions-list" style={styles.overscrollBehaviorContain} - // data={sortedReportActions} data={sortedVisibleReportActions} renderItem={renderItem} contentContainerStyle={contentContainerStyle} diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index 3cdaea4aaea6..403855fede39 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -1,8 +1,9 @@ +import type {RouteProp} from '@react-navigation/native'; import {useIsFocused, useRoute} from '@react-navigation/native'; -import lodashGet from 'lodash/get'; import lodashIsEqual from 'lodash/isEqual'; import React, {useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; import {InteractionManager} from 'react-native'; +import type {LayoutChangeEvent} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import useCopySelectionHelper from '@hooks/useCopySelectionHelper'; @@ -11,6 +12,7 @@ import useNetwork from '@hooks/useNetwork'; import usePrevious from '@hooks/usePrevious'; import useWindowDimensions from '@hooks/useWindowDimensions'; import getIsReportFullyVisible from '@libs/getIsReportFullyVisible'; +import type {CentralPaneNavigatorParamList} from '@libs/Navigation/types'; import Performance from '@libs/Performance'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import {isUserCreatedPolicyRoom} from '@libs/ReportUtils'; @@ -20,6 +22,7 @@ import * as Report from '@userActions/Report'; import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import getInitialPaginationSize from './getInitialPaginationSize'; @@ -49,6 +52,9 @@ type ReportActionsViewProps = ReportActionsViewOnyxProps & { /** The report actions are loading newer data */ isLoadingNewerReportActions?: boolean; + + /** Whether the report is ready for comment linking */ + isReadyForCommentLinking?: boolean; }; const DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST = 120; @@ -74,7 +80,7 @@ const usePaginatedReportActionList = ( linkedID: string, allReportActions: OnyxTypes.ReportAction[], fetchNewerReportActions: (newestReportAction: OnyxTypes.ReportAction) => void, - route: string, + route: RouteProp, isLoading: boolean, triggerListID: boolean, ) => { @@ -124,7 +130,7 @@ const usePaginatedReportActionList = ( const newestReportAction = visibleReportActions?.[0]; const handleReportActionPagination = useCallback( - ({firstReportActionID}) => { + ({firstReportActionID}: {firstReportActionID: string}) => { // This function is a placeholder as the actual pagination is handled by visibleReportActions if (!hasMoreCached) { isFirstLinkedActionRender.current = false; @@ -158,8 +164,8 @@ function ReportActionsView({ }: ReportActionsViewProps) { useCopySelectionHelper(); const reactionListRef = useContext(ReactionListContext); - const route = useRoute(); - const reportActionID = route?.params?.reportActionID ?? null; + const route = useRoute>(); + const reportActionID = route?.params?.reportActionID; const didLayout = useRef(false); const didSubscribeToReportTypingEvents = useRef(false); @@ -169,7 +175,6 @@ function ReportActionsView({ const layoutListHeight = useRef(0); const {windowHeight} = useWindowDimensions(); const isFocused = useIsFocused(); - const mostRecentIOUReportActionID = useMemo(() => ReportActionsUtils.getMostRecentIOURequestActionID(reportActions), [reportActions]); const prevNetworkRef = useRef(network); const prevAuthTokenType = usePrevious(session?.authTokenType); const [isInitialLinkedView, setIsInitialLinkedView] = useState(!!reportActionID); @@ -182,7 +187,7 @@ function ReportActionsView({ * displaying. */ const fetchNewerAction = useCallback( - (newestReportAction) => { + (newestReportAction: OnyxTypes.ReportAction) => { if (isLoadingNewerReportActions || isLoadingInitialReportActions) { return; } @@ -198,12 +203,13 @@ function ReportActionsView({ linkedIdIndex, listID, } = usePaginatedReportActionList(reportActionID, allReportActions, fetchNewerAction, route, isLoading, isLoadingInitialReportActions); + const mostRecentIOUReportActionID = useMemo(() => ReportActionsUtils.getMostRecentIOURequestActionID(reportActions), [reportActions]); const hasCachedActions = useInitialValue(() => reportActions.length > 0); const hasNewestReportAction = reportActions[0]?.created === report.lastVisibleActionCreated; - const newestReportAction = lodashGet(reportActions, ['0']); + const newestReportAction = reportActions?.[0]; const oldestReportAction = useMemo(() => reportActions?.at(-1), [reportActions]); - const hasCreatedAction = lodashGet(oldestReportAction, 'actionName') === CONST.REPORT.ACTIONS.TYPE.CREATED; - const firstReportActionName = lodashGet(reportActions, ['0', 'actionName']); + const hasCreatedAction = oldestReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED; + const firstReportActionName = reportActions?.[0]?.actionName; const isReportFullyVisible = useMemo((): boolean => getIsReportFullyVisible(isFocused), [isFocused]); @@ -293,7 +299,7 @@ function ReportActionsView({ } }, [report.pendingFields, didSubscribeToReportTypingEvents, reportID]); - const onContentSizeChange = useCallback((w, h) => { + const onContentSizeChange = useCallback((w: number, h: number) => { contentListHeight.current = h; }, []); @@ -355,7 +361,7 @@ function ReportActionsView({ * Runs when the FlatList finishes laying out */ const recordTimeToMeasureItemLayout = useCallback( - (e) => { + (e: LayoutChangeEvent) => { layoutListHeight.current = e.nativeEvent.layout.height; if (didLayout.current) { @@ -409,7 +415,7 @@ function ReportActionsView({ const isTheFirstReportActionIsLinked = firstReportActionID === reportActionID; useEffect(() => { - let timerId; + let timerId: NodeJS.Timeout; if (isTheFirstReportActionIsLinked) { setIsInitialLinkedView(true); @@ -447,11 +453,10 @@ function ReportActionsView({ mostRecentIOUReportActionID={mostRecentIOUReportActionID} loadOlderChats={loadOlderChats} loadNewerChats={loadNewerChats} - isLinkingLoader={!!reportActionID && isLoadingInitialReportActions} + // isLinkingLoader={!!reportActionID && isLoadingInitialReportActions} isLoadingInitialReportActions={isLoadingInitialReportActions} isLoadingOlderReportActions={isLoadingOlderReportActions} isLoadingNewerReportActions={isLoadingNewerReportActions} - // policy={policy} listID={listID} onContentSizeChange={onContentSizeChange} shouldEnableAutoScrollToTopThreshold={shouldEnableAutoScroll} From 27608f1e15579216c3a783756001b39c7b178fde Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 12 Mar 2024 19:21:47 +0100 Subject: [PATCH 168/245] update tests --- tests/unit/ReportActionsUtilsTest.ts | 1396 ++++++++++++++++++++++++-- 1 file changed, 1298 insertions(+), 98 deletions(-) diff --git a/tests/unit/ReportActionsUtilsTest.ts b/tests/unit/ReportActionsUtilsTest.ts index 7a48bb73d336..aac8d445aa92 100644 --- a/tests/unit/ReportActionsUtilsTest.ts +++ b/tests/unit/ReportActionsUtilsTest.ts @@ -553,76 +553,732 @@ describe('ReportActionsUtils', () => { }); describe('getContinuousReportActionChain', () => { it('given an input ID of 1, ..., 7 it will return the report actions with id 1 - 7', () => { - const input = [ - // Given these sortedReportActions - {reportActionID: 1, previousReportActionID: null}, - {reportActionID: 2, previousReportActionID: 1}, - {reportActionID: 3, previousReportActionID: 2}, - {reportActionID: 4, previousReportActionID: 3}, - {reportActionID: 5, previousReportActionID: 4}, - {reportActionID: 6, previousReportActionID: 5}, - {reportActionID: 7, previousReportActionID: 6}, - - // Note: there's a "gap" here because the previousReportActionID (8) does not match the ID of the previous reportAction in the array (7) - {reportActionID: 9, previousReportActionID: 8}, - {reportActionID: 10, previousReportActionID: 9}, - {reportActionID: 11, previousReportActionID: 10}, - {reportActionID: 12, previousReportActionID: 11}, - - // Note: another gap - {reportActionID: 14, previousReportActionID: 13}, - {reportActionID: 15, previousReportActionID: 14}, - {reportActionID: 16, previousReportActionID: 15}, - {reportActionID: 17, previousReportActionID: 16}, - ]; - - const expectedResult = [ - {reportActionID: 1, previousReportActionID: null}, - {reportActionID: 2, previousReportActionID: 1}, - {reportActionID: 3, previousReportActionID: 2}, - {reportActionID: 4, previousReportActionID: 3}, - {reportActionID: 5, previousReportActionID: 4}, - {reportActionID: 6, previousReportActionID: 5}, - {reportActionID: 7, previousReportActionID: 6}, - ]; - // Reversing the input array to simulate descending order sorting as per our data structure - const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), 3); - input.pop(); - expect(result).toStrictEqual(expectedResult.reverse()); - }); - - it('given an input ID of 9, ..., 12 it will return the report actions with id 9 - 12', () => { - const input = [ + const input: ReportAction[] = [ // Given these sortedReportActions - {reportActionID: 1, previousReportActionID: null}, - {reportActionID: 2, previousReportActionID: 1}, - {reportActionID: 3, previousReportActionID: 2}, - {reportActionID: 4, previousReportActionID: 3}, - {reportActionID: 5, previousReportActionID: 4}, - {reportActionID: 6, previousReportActionID: 5}, - {reportActionID: 7, previousReportActionID: 6}, + { + reportActionID: '1', + previousReportActionID: undefined, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '2', + previousReportActionID: '1', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '3', + previousReportActionID: '2', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '4', + previousReportActionID: '3', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '5', + previousReportActionID: '4', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '6', + previousReportActionID: '5', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '7', + previousReportActionID: '6', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, // Note: there's a "gap" here because the previousReportActionID (8) does not match the ID of the previous reportAction in the array (7) - {reportActionID: 9, previousReportActionID: 8}, - {reportActionID: 10, previousReportActionID: 9}, - {reportActionID: 11, previousReportActionID: 10}, - {reportActionID: 12, previousReportActionID: 11}, + { + reportActionID: '9', + previousReportActionID: '8', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '10', + previousReportActionID: '9', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '11', + previousReportActionID: '10', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '12', + previousReportActionID: '11', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, // Note: another gap - {reportActionID: 14, previousReportActionID: 13}, - {reportActionID: 15, previousReportActionID: 14}, - {reportActionID: 16, previousReportActionID: 15}, - {reportActionID: 17, previousReportActionID: 16}, + { + reportActionID: '14', + previousReportActionID: '13', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '15', + previousReportActionID: '14', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '16', + previousReportActionID: '15', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '17', + previousReportActionID: '16', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, ]; const expectedResult = [ - {reportActionID: 9, previousReportActionID: 8}, - {reportActionID: 10, previousReportActionID: 9}, - {reportActionID: 11, previousReportActionID: 10}, - {reportActionID: 12, previousReportActionID: 11}, + { + reportActionID: '1', + previousReportActionID: undefined, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '2', + previousReportActionID: '1', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '3', + previousReportActionID: '2', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '4', + previousReportActionID: '3', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '5', + previousReportActionID: '4', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '6', + previousReportActionID: '5', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '7', + previousReportActionID: '6', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + ]; + // Reversing the input array to simulate descending order sorting as per our data structure + const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), '3'); + input.pop(); + expect(result).toStrictEqual(expectedResult.reverse()); + }); + + it('given an input ID of 9, ..., 12 it will return the report actions with id 9 - 12', () => { + const input: ReportAction[] = [ + // Given these sortedReportActions + { + reportActionID: '1', + previousReportActionID: undefined, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '2', + previousReportActionID: '1', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '3', + previousReportActionID: '2', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '4', + previousReportActionID: '3', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '5', + previousReportActionID: '4', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '6', + previousReportActionID: '5', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '7', + previousReportActionID: '6', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + + // Note: there's a "gap" here because the previousReportActionID (8) does not match the ID of the previous reportAction in the array (7) + { + reportActionID: '9', + previousReportActionID: '8', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '10', + previousReportActionID: '9', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '11', + previousReportActionID: '10', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '12', + previousReportActionID: '11', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + + // Note: another gap + { + reportActionID: '14', + previousReportActionID: '13', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '15', + previousReportActionID: '14', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '16', + previousReportActionID: '15', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '17', + previousReportActionID: '16', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + ]; + + const expectedResult = [ + { + reportActionID: '9', + previousReportActionID: '8', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '10', + previousReportActionID: '9', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '11', + previousReportActionID: '10', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '12', + previousReportActionID: '11', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, ]; // Reversing the input array to simulate descending order sorting as per our data structure - const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), 10); + const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), '10'); input.pop(); expect(result).toStrictEqual(expectedResult.reverse()); }); @@ -630,66 +1286,610 @@ describe('ReportActionsUtils', () => { it('given an input ID of 14, ..., 17 it will return the report actions with id 14 - 17', () => { const input = [ // Given these sortedReportActions - {reportActionID: 1, previousReportActionID: null}, - {reportActionID: 2, previousReportActionID: 1}, - {reportActionID: 3, previousReportActionID: 2}, - {reportActionID: 4, previousReportActionID: 3}, - {reportActionID: 5, previousReportActionID: 4}, - {reportActionID: 6, previousReportActionID: 5}, - {reportActionID: 7, previousReportActionID: 6}, + { + reportActionID: '1', + previousReportActionID: undefined, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '2', + previousReportActionID: '1', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '3', + previousReportActionID: '2', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '4', + previousReportActionID: '3', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '5', + previousReportActionID: '4', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '6', + previousReportActionID: '5', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '7', + previousReportActionID: '6', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, // Note: there's a "gap" here because the previousReportActionID (8) does not match the ID of the previous reportAction in the array (7) - {reportActionID: 9, previousReportActionID: 8}, - {reportActionID: 10, previousReportActionID: 9}, - {reportActionID: 11, previousReportActionID: 10}, - {reportActionID: 12, previousReportActionID: 11}, + { + reportActionID: '9', + previousReportActionID: '8', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '10', + previousReportActionID: '9', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '11', + previousReportActionID: '10', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '12', + previousReportActionID: '11', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, // Note: another gap - {reportActionID: 14, previousReportActionID: 13}, - {reportActionID: 15, previousReportActionID: 14}, - {reportActionID: 16, previousReportActionID: 15}, - {reportActionID: 17, previousReportActionID: 16}, + { + reportActionID: '14', + previousReportActionID: '13', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '15', + previousReportActionID: '14', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '16', + previousReportActionID: '15', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '17', + previousReportActionID: '16', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, ]; const expectedResult = [ - {reportActionID: 14, previousReportActionID: 13}, - {reportActionID: 15, previousReportActionID: 14}, - {reportActionID: 16, previousReportActionID: 15}, - {reportActionID: 17, previousReportActionID: 16}, + { + reportActionID: '14', + previousReportActionID: '13', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '15', + previousReportActionID: '14', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '16', + previousReportActionID: '15', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '17', + previousReportActionID: '16', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, ]; // Reversing the input array to simulate descending order sorting as per our data structure - const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), 16); + const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), '16'); input.pop(); expect(result).toStrictEqual(expectedResult.reverse()); }); it('given an input ID of 8 or 13 which are not exist in Onyx it will return an empty array', () => { - const input = [ + const input: ReportAction[] = [ // Given these sortedReportActions - {reportActionID: 1, previousReportActionID: null}, - {reportActionID: 2, previousReportActionID: 1}, - {reportActionID: 3, previousReportActionID: 2}, - {reportActionID: 4, previousReportActionID: 3}, - {reportActionID: 5, previousReportActionID: 4}, - {reportActionID: 6, previousReportActionID: 5}, - {reportActionID: 7, previousReportActionID: 6}, + { + reportActionID: '1', + previousReportActionID: undefined, + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '2', + previousReportActionID: '1', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '3', + previousReportActionID: '2', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '4', + previousReportActionID: '3', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '5', + previousReportActionID: '4', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '6', + previousReportActionID: '5', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '7', + previousReportActionID: '6', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, // Note: there's a "gap" here because the previousReportActionID (8) does not match the ID of the previous reportAction in the array (7) - {reportActionID: 9, previousReportActionID: 8}, - {reportActionID: 10, previousReportActionID: 9}, - {reportActionID: 11, previousReportActionID: 10}, - {reportActionID: 12, previousReportActionID: 11}, + { + reportActionID: '9', + previousReportActionID: '8', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '10', + previousReportActionID: '9', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '11', + previousReportActionID: '10', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '12', + previousReportActionID: '11', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, // Note: another gap - {reportActionID: 14, previousReportActionID: 13}, - {reportActionID: 15, previousReportActionID: 14}, - {reportActionID: 16, previousReportActionID: 15}, - {reportActionID: 17, previousReportActionID: 16}, + { + reportActionID: '14', + previousReportActionID: '13', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '15', + previousReportActionID: '14', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '16', + previousReportActionID: '15', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, + { + reportActionID: '17', + previousReportActionID: '16', + created: '2022-11-13 22:27:01.825', + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + originalMessage: { + html: 'Hello world', + whisperedTo: [], + }, + message: [ + { + html: 'Hello world', + type: 'Action type', + text: 'Action text', + }, + ], + }, ]; - const expectedResult = []; + const expectedResult: ReportAction[] = []; // Reversing the input array to simulate descending order sorting as per our data structure - const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), 8); + const result = ReportActionsUtils.getContinuousReportActionChain(input.reverse(), '8'); input.pop(); expect(result).toStrictEqual(expectedResult.reverse()); }); From a0e96e1feb9714a0af77fae7481613b1f68ddc3d Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 12 Mar 2024 19:22:48 +0100 Subject: [PATCH 169/245] fix type --- src/components/InvertedFlatList/index.tsx | 6 +++++- src/libs/ReportActionsUtils.ts | 7 +++---- src/libs/migrateOnyx.ts | 8 +++++++- src/pages/home/report/ReportActionsList.tsx | 8 ++++---- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/components/InvertedFlatList/index.tsx b/src/components/InvertedFlatList/index.tsx index 2b4d98733cc4..37ca3c6201b5 100644 --- a/src/components/InvertedFlatList/index.tsx +++ b/src/components/InvertedFlatList/index.tsx @@ -6,9 +6,13 @@ import CONST from '@src/CONST'; import BaseInvertedFlatList from './BaseInvertedFlatList'; import CellRendererComponent from './CellRendererComponent'; +type InvertedFlatListProps = FlatListProps & { + shouldEnableAutoScrollToTopThreshold?: boolean; +}; + // This is adapted from https://codesandbox.io/s/react-native-dsyse // It's a HACK alert since FlatList has inverted scrolling on web -function InvertedFlatList({onScroll: onScrollProp = () => {}, ...props}: FlatListProps, ref: ForwardedRef) { +function InvertedFlatList({onScroll: onScrollProp = () => {}, ...props}: InvertedFlatListProps, ref: ForwardedRef) { const lastScrollEvent = useRef(null); const scrollEndTimeout = useRef(null); const updateInProgress = useRef(false); diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index b8783073a407..f67878a71fe0 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -52,9 +52,6 @@ const policyChangeActionsSet = new Set(Object.values(CONST.REPORT.ACTION const allReports: OnyxCollection = {}; -type ActionableMentionWhisperResolution = { - resolution: ValueOf; -}; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT, callback: (report, key) => { @@ -578,7 +575,9 @@ function getSortedReportActionsForDisplay(reportActions: ReportActions | null | } if (shouldIncludeInvisibleActions) { - filteredReportActions = Object.values(reportActions).filter((action): action is ReportAction => !action?.resolution); + filteredReportActions = Object.values(reportActions).filter( + (action): action is ReportAction => !(action?.originalMessage as OriginalMessageActionableMentionWhisper['originalMessage'])?.resolution, + ); } else { filteredReportActions = Object.entries(reportActions) .filter(([key, reportAction]) => shouldReportActionBeVisible(reportAction, key)) diff --git a/src/libs/migrateOnyx.ts b/src/libs/migrateOnyx.ts index 58a2eebbb76a..536b912e69e0 100644 --- a/src/libs/migrateOnyx.ts +++ b/src/libs/migrateOnyx.ts @@ -11,7 +11,13 @@ export default function (): Promise { return new Promise((resolve) => { // Add all migrations to an array so they are executed in order - const migrationPromises = [CheckForPreviousReportActionID, RenameReceiptFilename, KeyReportActionsDraftByReportActionID, TransactionBackupsToCollection, RemoveEmptyReportActionsDrafts]; + const migrationPromises = [ + CheckForPreviousReportActionID, + RenameReceiptFilename, + KeyReportActionsDraftByReportActionID, + TransactionBackupsToCollection, + RemoveEmptyReportActionsDrafts, + ]; // Reduce all promises down to a single promise. All promises run in a linear fashion, waiting for the // previous promise to finish before moving onto the next one. diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index 4cc1f0e3720b..8419df479bef 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -81,7 +81,7 @@ type ReportActionsListProps = WithCurrentUserPersonalDetailsProps & { onContentSizeChange: (w: number, h: number) => void; /** Should enable auto scroll to top threshold */ - shouldEnableAutoScrollToTopThreshold: boolean; + shouldEnableAutoScrollToTopThreshold?: boolean; }; const VERTICAL_OFFSET_THRESHOLD = 200; @@ -300,7 +300,7 @@ function ReportActionsList({ }, []); const scrollToBottomForCurrentUserAction = useCallback( - (isFromCurrentUser) => { + (isFromCurrentUser: boolean) => { // If a new comment is added and it's from the current user scroll to the bottom otherwise leave the user positioned where // they are now in the list. if (!isFromCurrentUser || !hasNewestReportAction) { @@ -554,7 +554,7 @@ function ReportActionsList({ [onLayout], ); const onContentSizeChangeInner = useCallback( - (w, h) => { + (w: number, h: number) => { onContentSizeChange(w, h); }, [onContentSizeChange], @@ -618,4 +618,4 @@ ReportActionsList.displayName = 'ReportActionsList'; export default withCurrentUserPersonalDetails(memo(ReportActionsList)); -export type {LoadNewerChats}; +export type {LoadNewerChats, ReportActionsListProps}; From 8a1164a889288b268478c8ecad9bdbb82d18f203 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Wed, 13 Mar 2024 00:46:42 +0500 Subject: [PATCH 170/245] use prefix for report fields --- src/libs/ReportUtils.ts | 30 +++++++++++++++++++++++++++--- src/pages/EditReportFieldPage.tsx | 3 ++- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 28ec6880c371..7713d7494eae 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2076,8 +2076,15 @@ function getTitleReportField(reportFields: Record) { /** * Get the report fields attached to the policy given policyID */ -function getReportFieldsByPolicyID(policyID: string) { - return Object.entries(allPolicies ?? {}).find(([key]) => key.replace(ONYXKEYS.COLLECTION.POLICY, '') === policyID)?.[1]?.fieldList; +function getReportFieldsByPolicyID(policyID: string): Record { + const policyReportFields = Object.entries(allPolicies ?? {}).find(([key]) => key.replace(ONYXKEYS.COLLECTION.POLICY, '') === policyID); + const fieldList = policyReportFields?.[1]?.fieldList; + + if (!policyReportFields || !fieldList) { + return {}; + } + + return fieldList as Record; } /** @@ -2097,7 +2104,24 @@ function getAvailableReportFields(report: Report, policyReportFields: PolicyRepo // If the report is unsettled, we want to merge the new fields that get added to the policy with the fields that // are attached to the report. const mergedFieldIds = Array.from(new Set([...policyReportFields.map(({fieldID}) => fieldID), ...reportFields.map(({fieldID}) => fieldID)])); - return mergedFieldIds.map((id) => report?.fieldList?.[id] ?? policyReportFields.find(({fieldID}) => fieldID === id)) as PolicyReportField[]; + + const fields = mergedFieldIds.map((id) => { + const field = report?.fieldList?.[`expensify_${id as string}`]; + + if (field) { + return field as PolicyReportField; + } + + const policyReportField = policyReportFields.find(({fieldID}) => fieldID === id); + + if (policyReportField) { + return policyReportField; + } + + return null; + }); + + return fields.filter(Boolean) as PolicyReportField[]; } /** diff --git a/src/pages/EditReportFieldPage.tsx b/src/pages/EditReportFieldPage.tsx index 95620a2b9389..6f9886af4482 100644 --- a/src/pages/EditReportFieldPage.tsx +++ b/src/pages/EditReportFieldPage.tsx @@ -40,7 +40,8 @@ type EditReportFieldPageProps = EditReportFieldPageOnyxProps & { }; function EditReportFieldPage({route, policy, report}: EditReportFieldPageProps) { - const reportField = report?.fieldList?.[route.params.fieldID] ?? policy?.fieldList?.[route.params.fieldID]; + const fieldId = `expensify_${route.params.fieldID}`; + const reportField = report?.fieldList?.[fieldId] ?? policy?.fieldList?.[fieldId]; const isDisabled = ReportUtils.isReportFieldDisabled(report, reportField ?? null, policy); if (!reportField || !report || isDisabled) { From aa107372b3ea469dba05181e780f43bf841e0672 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Wed, 13 Mar 2024 00:56:08 +0500 Subject: [PATCH 171/245] fix: lint errors --- src/libs/ReportUtils.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 7713d7494eae..19d2577df223 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2083,8 +2083,8 @@ function getReportFieldsByPolicyID(policyID: string): Record; + + return fieldList; } /** @@ -2106,10 +2106,10 @@ function getAvailableReportFields(report: Report, policyReportFields: PolicyRepo const mergedFieldIds = Array.from(new Set([...policyReportFields.map(({fieldID}) => fieldID), ...reportFields.map(({fieldID}) => fieldID)])); const fields = mergedFieldIds.map((id) => { - const field = report?.fieldList?.[`expensify_${id as string}`]; + const field = report?.fieldList?.[`expensify_${id}`]; if (field) { - return field as PolicyReportField; + return field; } const policyReportField = policyReportFields.find(({fieldID}) => fieldID === id); From ce55a7e93d6c1a90ec07e75b1796d402488506b6 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 13 Mar 2024 09:01:31 +0100 Subject: [PATCH 172/245] remove outdated test --- tests/unit/ReportActionsUtilsTest.ts | 110 --------------------------- 1 file changed, 110 deletions(-) diff --git a/tests/unit/ReportActionsUtilsTest.ts b/tests/unit/ReportActionsUtilsTest.ts index aac8d445aa92..bf528eca3e81 100644 --- a/tests/unit/ReportActionsUtilsTest.ts +++ b/tests/unit/ReportActionsUtilsTest.ts @@ -305,116 +305,6 @@ describe('ReportActionsUtils', () => { expect(result).toStrictEqual(input); }); - describe('getSortedReportActionsForDisplay with marked the first reportAction', () => { - it('should filter out non-whitelisted actions', () => { - const input: ReportAction[] = [ - { - created: '2022-11-13 22:27:01.825', - reportActionID: '8401445780099176', - actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - created: '2022-11-12 22:27:01.825', - reportActionID: '6401435781022176', - actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - created: '2022-11-11 22:27:01.825', - reportActionID: '2962390724708756', - actionName: CONST.REPORT.ACTIONS.TYPE.IOU, - originalMessage: { - amount: 0, - currency: 'USD', - type: 'split', // change to const - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - created: '2022-11-10 22:27:01.825', - reportActionID: '1609646094152486', - actionName: CONST.REPORT.ACTIONS.TYPE.RENAMED, - originalMessage: { - html: 'Hello world', - lastModified: '2022-11-10 22:27:01.825', - oldName: 'old name', - newName: 'new name', - }, - message: [ - { - html: 'Hello world', - type: 'Action type', - text: 'Action text', - }, - ], - }, - { - created: '2022-11-09 22:27:01.825', - reportActionID: '8049485084562457', - actionName: CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.UPDATE_FIELD, - originalMessage: {}, - message: [{html: 'updated the Approval Mode from "Submit and Approve" to "Submit and Close"', type: 'Action type', text: 'Action text'}], - }, - { - created: '2022-11-08 22:27:06.825', - reportActionID: '1661970171066216', - actionName: CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED, - originalMessage: { - paymentType: 'ACH', - }, - message: [{html: 'Waiting for the bank account', type: 'Action type', text: 'Action text'}], - }, - { - created: '2022-11-06 22:27:08.825', - reportActionID: '1661970171066220', - actionName: CONST.REPORT.ACTIONS.TYPE.TASKEDITED, - originalMessage: { - html: 'Hello world', - whisperedTo: [], - }, - message: [{html: 'I have changed the task', type: 'Action type', text: 'Action text'}], - }, - ]; - - const resultWithoutNewestFlag = ReportActionsUtils.getSortedReportActionsForDisplay(input); - const resultWithNewestFlag = ReportActionsUtils.getSortedReportActionsForDisplay(input, true); - input.pop(); - // Mark the newest report action as the newest report action - resultWithoutNewestFlag[0] = { - ...resultWithoutNewestFlag[0], - isNewestReportAction: true, - }; - expect(resultWithoutNewestFlag).toStrictEqual(resultWithNewestFlag); - }); - }); - it('should filter out closed actions', () => { const input: ReportAction[] = [ { From 2b77edc3fa7ea5372fea151a7e2f326fcf02bf8b Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 13 Mar 2024 09:08:38 +0100 Subject: [PATCH 173/245] add selector for sortedAllReportActions --- src/pages/home/ReportScreen.tsx | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index b1138cd8a28e..6bd07f044dba 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -53,12 +53,7 @@ import ReportFooter from './report/ReportFooter'; import {ActionListContext, ReactionListContext} from './ReportScreenContext'; import type {ActionListContextType, ReactionListRef, ScrollPosition} from './ReportScreenContext'; -type ReportActionMap = Record; - type ReportScreenOnyxProps = { - /** All the report actions for this report */ - allReportActions: ReportActionMap; - /** Tells us if the sidebar has rendered */ isSidebarLoaded: OnyxEntry; @@ -77,8 +72,8 @@ type ReportScreenOnyxProps = { /** Whether the composer is full size */ isComposerFullSize: OnyxEntry; - /** All the report actions for this report */ - // reportActions: OnyxTypes.ReportAction[]; + /** An array containing all report actions related to this report, sorted based on a date criterion */ + sortedAllReportActions: OnyxTypes.ReportAction[]; /** The report currently being looked at */ report: OnyxEntry; @@ -110,7 +105,7 @@ function ReportScreen({ betas = [], route, report: reportProp, - allReportActions, + sortedAllReportActions, reportMetadata = { isLoadingInitialReportActions: true, isLoadingOlderReportActions: false, @@ -226,13 +221,12 @@ function ReportScreen({ const prevUserLeavingStatus = usePrevious(userLeavingStatus); const [isLinkingToMessage, setLinkingToMessage] = useState(!!reportActionIDFromRoute); const reportActions = useMemo(() => { - if (!allReportActions) { + if (!sortedAllReportActions.length) { return []; } - const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, true); - const currentRangeOfReportActions = ReportActionsUtils.getContinuousReportActionChain(sortedReportActions, reportActionIDFromRoute); + const currentRangeOfReportActions = ReportActionsUtils.getContinuousReportActionChain(sortedAllReportActions, reportActionIDFromRoute); return currentRangeOfReportActions; - }, [reportActionIDFromRoute, allReportActions]); + }, [reportActionIDFromRoute, sortedAllReportActions]); // Define here because reportActions are recalculated before mount, allowing data to display faster than useEffect can trigger. If we have cached reportActions, they will be shown immediately. We aim to display a loader first, then fetch relevant reportActions, and finally show them. useLayoutEffect(() => { @@ -510,14 +504,14 @@ function ReportScreen({ }; const isLinkedReportActionDeleted = useMemo(() => { - if (!reportActionIDFromRoute || !allReportActions) { + if (!reportActionIDFromRoute || !sortedAllReportActions) { return false; } - const action = allReportActions[reportActionIDFromRoute]; + const action = sortedAllReportActions.find(item => item.reportActionID === reportActionIDFromRoute); return action && ReportActionsUtils.isDeletedAction(action); - }, [reportActionIDFromRoute, allReportActions]); + }, [reportActionIDFromRoute, sortedAllReportActions]); - if (isLinkedReportActionDeleted || (!shouldShowSkeleton && reportActionIDFromRoute && reportActions?.length === 0 && !isLinkingToMessage)) { + if (isLinkedReportActionDeleted ?? (!shouldShowSkeleton && reportActionIDFromRoute && reportActions?.length === 0 && !isLinkingToMessage)) { return ( `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getReportID(route)}`, canEvict: false, + selector: (allReportActions: OnyxEntry) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, true), }, report: { key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${getReportID(route)}`, @@ -680,7 +675,7 @@ export default withViewportOffsetTop( ReportScreen, (prevProps, nextProps) => prevProps.isSidebarLoaded === nextProps.isSidebarLoaded && - lodashIsEqual(prevProps.allReportActions, nextProps.allReportActions) && + lodashIsEqual(prevProps.sortedAllReportActions, nextProps.sortedAllReportActions) && lodashIsEqual(prevProps.reportMetadata, nextProps.reportMetadata) && prevProps.isComposerFullSize === nextProps.isComposerFullSize && lodashIsEqual(prevProps.betas, nextProps.betas) && From 2c809c42faa17e3e2e4a214f761822beb23e0911 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 13 Mar 2024 09:21:15 +0100 Subject: [PATCH 174/245] lint --- src/pages/home/ReportScreen.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 6bd07f044dba..7561f02e9aaa 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -507,7 +507,7 @@ function ReportScreen({ if (!reportActionIDFromRoute || !sortedAllReportActions) { return false; } - const action = sortedAllReportActions.find(item => item.reportActionID === reportActionIDFromRoute); + const action = sortedAllReportActions.find((item) => item.reportActionID === reportActionIDFromRoute); return action && ReportActionsUtils.isDeletedAction(action); }, [reportActionIDFromRoute, sortedAllReportActions]); From c47d0e0605b519a09fe0bc71aabca7854ec7d0fc Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Wed, 13 Mar 2024 21:09:03 +0530 Subject: [PATCH 175/245] Fix types --- src/CONST.ts | 1 + src/libs/actions/IOU.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index fb02dae94c48..4872f51889e4 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1302,6 +1302,7 @@ const CONST = { CANCEL: 'cancel', DELETE: 'delete', APPROVE: 'approve', + TRACK: 'track', }, AMOUNT_MAX_LENGTH: 10, RECEIPT_STATE: { diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 227ed2f9e1b2..23b695b75ee0 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -1269,7 +1269,7 @@ function getTrackExpenseInformation( // 2. The transaction thread, which requires the iouAction, and CREATED action for the transaction thread const currentTime = DateUtils.getDBTime(); const iouAction = ReportUtils.buildOptimisticIOUReportAction( - CONST.IOU.REPORT_ACTION_TYPE.CREATE, + CONST.IOU.REPORT_ACTION_TYPE.TRACK, amount, currency, comment, @@ -1283,7 +1283,7 @@ function getTrackExpenseInformation( false, currentTime, ); - const optimisticTransactionThread = ReportUtils.buildTransactionThread(iouAction, chatReport.reportID); + const optimisticTransactionThread = ReportUtils.buildTransactionThread(iouAction, chatReport); const optimisticCreatedActionForTransactionThread = ReportUtils.buildOptimisticCreatedReportAction(payeeEmail); // STEP 5: Build Onyx Data From 6a5048026fec4005643646b9061dfb5f28230bd8 Mon Sep 17 00:00:00 2001 From: smelaa Date: Wed, 13 Mar 2024 17:52:48 +0100 Subject: [PATCH 176/245] Address review comments --- .../VideoPlayer/BaseVideoPlayer.tsx | 265 ------------------ .../VideoPlayerControls/ProgressBar/index.tsx | 2 +- .../VolumeButton/index.tsx | 4 +- .../VideoPlayer/VideoPlayerControls/index.tsx | 4 +- src/components/VideoPlayerPreview/index.tsx | 5 +- 5 files changed, 7 insertions(+), 273 deletions(-) delete mode 100644 src/components/VideoPlayer/BaseVideoPlayer.tsx diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx deleted file mode 100644 index dc1d4ffdebe9..000000000000 --- a/src/components/VideoPlayer/BaseVideoPlayer.tsx +++ /dev/null @@ -1,265 +0,0 @@ -/* eslint-disable no-underscore-dangle */ -import {AVPlaybackStatus, Video, VideoFullscreenUpdate} from 'expo-av'; -import React, {useCallback, useEffect, useRef, useState} from 'react'; -import type {GestureResponderEvent} from 'react-native'; -import {View} from 'react-native'; -import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; -import Hoverable from '@components/Hoverable'; -import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; -import {usePlaybackContext} from '@components/VideoPlayerContexts/PlaybackContext'; -import VideoPopoverMenu from '@components/VideoPopoverMenu'; -import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; -import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; -import * as Browser from '@libs/Browser'; -import * as DeviceCapabilities from '@libs/DeviceCapabilities'; -import CONST from '@src/CONST'; -import {videoPlayerDefaultProps, videoPlayerPropTypes} from './propTypes'; -import shouldReplayVideo from './shouldReplayVideo'; -import type VideoPlayerProps from './types'; -import VideoPlayerControls from './VideoPlayerControls'; - -const isMobileSafari = Browser.isMobileSafari(); - -function BaseVideoPlayer({ - url, - resizeMode, - onVideoLoaded, - isLooping, - style, - videoPlayerStyle, - videoStyle, - videoControlsStyle, - videoDuration, - shouldUseSharedVideoElement, - shouldUseSmallVideoControls, - shouldShowVideoControls, - onPlaybackStatusUpdate, - onFullscreenUpdate, - // TODO: investigate what is the root cause of the bug with unexpected video switching - // isVideoHovered caused a bug with unexpected video switching. We are investigating the root cause of the issue, - // but current workaround is just not to use it here for now. This causes not displaying the video controls when - // user hovers the mouse over the carousel arrows, but this UI bug feels much less troublesome for now. - // eslint-disable-next-line no-unused-vars - isVideoHovered, -}: VideoPlayerProps) { - const styles = useThemeStyles(); - const {isSmallScreenWidth} = useWindowDimensions(); - const {pauseVideo, playVideo, currentlyPlayingURL, updateSharedElements, sharedElement, originalParent, shareVideoPlayerElements, currentVideoPlayerRef, updateCurrentlyPlayingURL} = - usePlaybackContext(); - const [duration, setDuration] = useState(videoDuration * 1000); - const [position, setPosition] = useState(0); - const [isPlaying, setIsPlaying] = useState(false); - const [isLoading, setIsLoading] = useState(true); - const [isBuffering, setIsBuffering] = useState(true); - const [sourceURL] = useState(url.includes('blob:') || url.includes('file:///') ? url : addEncryptedAuthTokenToURL(url)); - const [isPopoverVisible, setIsPopoverVisible] = useState(false); - const [popoverAnchorPosition, setPopoverAnchorPosition] = useState({horizontal: 0, vertical: 0}); - const videoPlayerRef = useRef<>(null); - const videoPlayerElementParentRef = useRef(null); - const videoPlayerElementRef = useRef(null); - const sharedVideoPlayerParentRef = useRef(null); - const videoResumeTryNumber = useRef(0); - const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen(); - const isCurrentlyURLSet = currentlyPlayingURL === url; - const isUploading = CONST.ATTACHMENT_LOCAL_URL_PREFIX.some((prefix) => url.startsWith(prefix)); - - const togglePlayCurrentVideo = useCallback(() => { - videoResumeTryNumber.current = 0; - if (!isCurrentlyURLSet) { - updateCurrentlyPlayingURL(url); - } else if (isPlaying) { - pauseVideo(); - } else { - playVideo(); - } - }, [isCurrentlyURLSet, isPlaying, pauseVideo, playVideo, updateCurrentlyPlayingURL, url]); - - const showPopoverMenu = (event: GestureResponderEvent) => { - setPopoverAnchorPosition({horizontal: event.nativeEvent.pageX, vertical: event.nativeEvent.pageY}); - setIsPopoverVisible(true); - }; - - const hidePopoverMenu = () => { - setIsPopoverVisible(false); - }; - - // fix for iOS mWeb: preventing iOS native player edfault behavior from pausing the video when exiting fullscreen - const preventPausingWhenExitingFullscreen = useCallback( - (isVideoPlaying: boolean) => { - if (videoResumeTryNumber.current === 0 || isVideoPlaying) { - return; - } - if (videoResumeTryNumber.current === 1) { - playVideo(); - } - videoResumeTryNumber.current -= 1; - }, - [playVideo], - ); - - const handlePlaybackStatusUpdate = useCallback( - (status: AVPlaybackStatus) => { - if (!status.isLoaded){ - return; - } - if (shouldReplayVideo(status, isPlaying, duration, position)) { - videoPlayerRef.current?.setStatusAsync?.({positionMillis: 0, shouldPlay: true}); - } - const isVideoPlaying = status.isPlaying || false; - preventPausingWhenExitingFullscreen(isVideoPlaying); - setIsPlaying(isVideoPlaying); - setIsLoading(!status.isLoaded || Number.isNaN(status.durationMillis)); // when video is ready to display duration is not NaN - setIsBuffering(status.isBuffering || false); - setDuration(status.durationMillis || videoDuration * 1000); - setPosition(status.positionMillis || 0); - - onPlaybackStatusUpdate(status); - }, - [onPlaybackStatusUpdate, preventPausingWhenExitingFullscreen, videoDuration, isPlaying, duration, position], - ); - - const handleFullscreenUpdate = useCallback( - (event) => { - onFullscreenUpdate(event); - // fix for iOS native and mWeb: when switching to fullscreen and then exiting - // the fullscreen mode while playing, the video pauses - if (!isPlaying || event.fullscreenUpdate !== VideoFullscreenUpdate.PLAYER_DID_DISMISS) { - return; - } - - if (isMobileSafari) { - pauseVideo(); - } - playVideo(); - videoResumeTryNumber.current = 3; - }, - [isPlaying, onFullscreenUpdate, pauseVideo, playVideo], - ); - - const bindFunctions = useCallback(() => { - currentVideoPlayerRef.current._onPlaybackStatusUpdate = handlePlaybackStatusUpdate; - currentVideoPlayerRef.current._onFullscreenUpdate = handleFullscreenUpdate; - // update states after binding - currentVideoPlayerRef.current.getStatusAsync().then((status) => { - handlePlaybackStatusUpdate(status); - }); - }, [currentVideoPlayerRef, handleFullscreenUpdate, handlePlaybackStatusUpdate]); - - // update shared video elements - useEffect(() => { - if (shouldUseSharedVideoElement || url !== currentlyPlayingURL) { - return; - } - shareVideoPlayerElements(videoPlayerRef.current, videoPlayerElementParentRef.current, videoPlayerElementRef.current, isUploading); - }, [currentlyPlayingURL, shouldUseSharedVideoElement, shareVideoPlayerElements, updateSharedElements, url, isUploading]); - - // append shared video element to new parent (used for example in attachment modal) - useEffect(() => { - if (url !== currentlyPlayingURL || !sharedElement || !shouldUseSharedVideoElement) { - return; - } - - const newParentRef = sharedVideoPlayerParentRef.current; - videoPlayerRef.current = currentVideoPlayerRef.current; - if (currentlyPlayingURL === url) { - newParentRef.appendChild(sharedElement); - bindFunctions(); - } - return () => { - if (!originalParent && !newParentRef.childNodes[0]) { - return; - } - originalParent.appendChild(sharedElement); - }; - }, [bindFunctions, currentVideoPlayerRef, currentlyPlayingURL, isSmallScreenWidth, originalParent, sharedElement, shouldUseSharedVideoElement, url]); - - return ( - <> - - - {(isHovered) => ( - - {shouldUseSharedVideoElement ? ( - <> - - {/* We are adding transparent absolute View between appended video component and control buttons to enable - catching onMouse events from Attachment Carousel. Due to late appending React doesn't handle - element's events properly. */} - - - ) : ( - { - if (!el) { - return; - } - videoPlayerElementParentRef.current = el; - if (el.childNodes && el.childNodes[0]) { - videoPlayerElementRef.current = el.childNodes[0]; - } - }} - > - { - togglePlayCurrentVideo(); - }} - style={styles.flex1} - > - - - )} - - {(isLoading || isBuffering) && } - - {shouldShowVideoControls && !isLoading && (isPopoverVisible || isHovered || canUseTouchScreen) && ( - - )} - - )} - - - - - ); -} - -BaseVideoPlayer.propTypes = videoPlayerPropTypes; -BaseVideoPlayer.defaultProps = videoPlayerDefaultProps; -BaseVideoPlayer.displayName = 'BaseVideoPlayer'; - -export default BaseVideoPlayer; diff --git a/src/components/VideoPlayer/VideoPlayerControls/ProgressBar/index.tsx b/src/components/VideoPlayer/VideoPlayerControls/ProgressBar/index.tsx index 72df96410e1c..362411e3da3f 100644 --- a/src/components/VideoPlayer/VideoPlayerControls/ProgressBar/index.tsx +++ b/src/components/VideoPlayer/VideoPlayerControls/ProgressBar/index.tsx @@ -63,7 +63,7 @@ function ProgressBar({duration, position, seekPosition}: ProgressBarProps) { progressWidth.value = getProgress(position, duration); }, [duration, isSliderPressed, position, progressWidth]); - const progressBarStyle = useAnimatedStyle(() => ({width: `${progressWidth.value}%`} as ViewStyle)); + const progressBarStyle: ViewStyle = useAnimatedStyle(() => ({width: `${progressWidth.value}%`})); return ( diff --git a/src/components/VideoPlayer/VideoPlayerControls/VolumeButton/index.tsx b/src/components/VideoPlayer/VideoPlayerControls/VolumeButton/index.tsx index ee93eb672774..3dc30c1d46bb 100644 --- a/src/components/VideoPlayer/VideoPlayerControls/VolumeButton/index.tsx +++ b/src/components/VideoPlayer/VideoPlayerControls/VolumeButton/index.tsx @@ -1,5 +1,5 @@ import React, {memo, useCallback, useState} from 'react'; -import type {LayoutChangeEvent, ViewStyle} from 'react-native'; +import type {LayoutChangeEvent, StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import type {GestureStateChangeEvent, GestureUpdateEvent, PanGestureChangeEventPayload, PanGestureHandlerEventPayload} from 'react-native-gesture-handler'; import {Gesture, GestureDetector} from 'react-native-gesture-handler'; @@ -13,7 +13,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as NumberUtils from '@libs/NumberUtils'; type VolumeButtonProps = { - style: ViewStyle; + style: StyleProp; small?: boolean; }; diff --git a/src/components/VideoPlayer/VideoPlayerControls/index.tsx b/src/components/VideoPlayer/VideoPlayerControls/index.tsx index 28a2dc983b6f..c332533d202c 100644 --- a/src/components/VideoPlayer/VideoPlayerControls/index.tsx +++ b/src/components/VideoPlayer/VideoPlayerControls/index.tsx @@ -22,8 +22,8 @@ type VideoPlayerControlsProps = { videoPlayerRef: MutableRefObject From 9edbe166c422adff26386b5f04eaa05b11f4c6ae Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 19 Mar 2024 19:35:55 +0530 Subject: [PATCH 202/245] Fixed translations --- src/languages/en.ts | 13 +++++++++---- src/languages/es.ts | 16 ++++++++++++---- src/libs/ReportUtils.ts | 2 +- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 99f86e937aa0..47285af2515f 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -463,9 +463,14 @@ export default { copyEmailToClipboard: 'Copy email to clipboard', markAsUnread: 'Mark as unread', markAsRead: 'Mark as read', - editAction: ({action}: EditActionParams) => `Edit ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'request' : 'comment'}`, - deleteAction: ({action}: DeleteActionParams) => `Delete ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'request' : 'comment'}`, - deleteConfirmation: ({action}: DeleteConfirmationParams) => `Are you sure you want to delete this ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'request' : 'comment'}?`, + editAction: ({action}: EditActionParams) => + `Edit ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? `${action?.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK ? 'expense' : 'request'}` : 'comment'}`, + deleteAction: ({action}: DeleteActionParams) => + `Delete ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? `${action?.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK ? 'expense' : 'request'}` : 'comment'}`, + deleteConfirmation: ({action}: DeleteConfirmationParams) => + `Are you sure you want to delete this ${ + action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? `${action?.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK ? 'expense' : 'request'}` : 'comment' + }?`, onlyVisible: 'Only visible to', replyInThread: 'Reply in thread', joinThread: 'Join thread', @@ -628,7 +633,7 @@ export default { finished: 'Finished', requestAmount: ({amount}: RequestAmountParams) => `request ${amount}`, requestedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `requested ${formattedAmount}${comment ? ` for ${comment}` : ''}`, - trackedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `tracked ${formattedAmount}${comment ? ` for ${comment}` : ''}`, + trackedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `tracking ${formattedAmount}${comment ? ` for ${comment}` : ''}`, splitAmount: ({amount}: SplitAmountParams) => `split ${amount}`, didSplitAmount: ({formattedAmount, comment}: DidSplitAmountMessageParams) => `split ${formattedAmount}${comment ? ` for ${comment}` : ''}`, amountEach: ({amount}: AmountEachParams) => `${amount} each`, diff --git a/src/languages/es.ts b/src/languages/es.ts index 4f10fdb8451e..56e9702edca9 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -454,10 +454,18 @@ export default { copyEmailToClipboard: 'Copiar email al portapapeles', markAsUnread: 'Marcar como no leído', markAsRead: 'Marcar como leído', - editAction: ({action}: EditActionParams) => `Editar ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'solicitud' : 'comentario'}`, - deleteAction: ({action}: DeleteActionParams) => `Eliminar ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'solicitud' : 'comentario'}`, + editAction: ({action}: EditActionParams) => + `Editar ${ + action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? `${action?.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK ? 'gastos' : 'solicitud'}` : 'comentario' + }`, + deleteAction: ({action}: DeleteActionParams) => + `Eliminar ${ + action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? `${action?.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK ? 'gastos' : 'solicitud'}` : 'comentario' + }`, deleteConfirmation: ({action}: DeleteConfirmationParams) => - `¿Estás seguro de que quieres eliminar esta ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'solicitud' : 'comentario'}`, + `¿Estás seguro de que quieres eliminar esta ${ + action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? `${action?.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK ? 'gastos' : 'solicitud'}` : 'comentario' + }`, onlyVisible: 'Visible sólo para', replyInThread: 'Responder en el hilo', joinThread: 'Unirse al hilo', @@ -621,7 +629,7 @@ export default { finished: 'Finalizado', requestAmount: ({amount}: RequestAmountParams) => `solicitar ${amount}`, requestedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `solicité ${formattedAmount}${comment ? ` para ${comment}` : ''}`, - trackedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `rastreado ${formattedAmount}${comment ? ` para ${comment}` : ''}`, + trackedAmount: ({formattedAmount, comment}: RequestedAmountMessageParams) => `seguimiento ${formattedAmount}${comment ? ` para ${comment}` : ''}`, splitAmount: ({amount}: SplitAmountParams) => `dividir ${amount}`, didSplitAmount: ({formattedAmount, comment}: DidSplitAmountMessageParams) => `dividió ${formattedAmount}${comment ? ` para ${comment}` : ''}`, amountEach: ({amount}: AmountEachParams) => `${amount} cada uno`, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 4471f2be9f17..62e39c38efe0 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -3197,7 +3197,7 @@ function getIOUReportActionMessage(iouReportID: string, type: string, total: num iouMessage = `requested ${amount}${comment && ` for ${comment}`}`; break; case CONST.IOU.REPORT_ACTION_TYPE.TRACK: - iouMessage = `tracked ${amount}${comment && ` for ${comment}`}`; + iouMessage = `tracking ${amount}${comment && ` for ${comment}`}`; break; case CONST.IOU.REPORT_ACTION_TYPE.SPLIT: iouMessage = `split ${amount}${comment && ` for ${comment}`}`; From f3a35ddbfd6eb4821b9bbddf61e705bb396e656e Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Tue, 19 Mar 2024 15:13:48 +0100 Subject: [PATCH 203/245] [TS migration] Migrate remaining Test files to TypeScript #38536 --- src/libs/E2E/client.ts | 2 +- tests/e2e/nativeCommands/{index.js => index.ts} | 5 +++-- tests/unit/{LocaleCompareTest.js => LocaleCompareTest.ts} | 0 ...archCountryOptionsTest.js => searchCountryOptionsTest.ts} | 0 4 files changed, 4 insertions(+), 3 deletions(-) rename tests/e2e/nativeCommands/{index.js => index.ts} (75%) rename tests/unit/{LocaleCompareTest.js => LocaleCompareTest.ts} (100%) rename tests/unit/{searchCountryOptionsTest.js => searchCountryOptionsTest.ts} (100%) diff --git a/src/libs/E2E/client.ts b/src/libs/E2E/client.ts index 4c0e572cc9b2..5aa999267ead 100644 --- a/src/libs/E2E/client.ts +++ b/src/libs/E2E/client.ts @@ -105,4 +105,4 @@ export default { updateNetworkCache, getNetworkCache, }; -export type {TestResult, NativeCommand}; +export type {TestResult, NativeCommand, NativeCommandPayload}; diff --git a/tests/e2e/nativeCommands/index.js b/tests/e2e/nativeCommands/index.ts similarity index 75% rename from tests/e2e/nativeCommands/index.js rename to tests/e2e/nativeCommands/index.ts index 90dcb00bbcae..09d727b5d257 100644 --- a/tests/e2e/nativeCommands/index.js +++ b/tests/e2e/nativeCommands/index.ts @@ -1,14 +1,15 @@ +import type {NativeCommandPayload} from '@libs/E2E/client'; import adbBackspace from './adbBackspace'; import adbTypeText from './adbTypeText'; // eslint-disable-next-line rulesdir/prefer-import-module-contents import {NativeCommandsAction} from './NativeCommandsAction'; -const executeFromPayload = (actionName, payload) => { +const executeFromPayload = (actionName?: string, payload?: NativeCommandPayload) => { switch (actionName) { case NativeCommandsAction.scroll: throw new Error('Not implemented yet'); case NativeCommandsAction.type: - return adbTypeText(payload.text); + return adbTypeText(payload?.text ?? ''); case NativeCommandsAction.backspace: return adbBackspace(); default: diff --git a/tests/unit/LocaleCompareTest.js b/tests/unit/LocaleCompareTest.ts similarity index 100% rename from tests/unit/LocaleCompareTest.js rename to tests/unit/LocaleCompareTest.ts diff --git a/tests/unit/searchCountryOptionsTest.js b/tests/unit/searchCountryOptionsTest.ts similarity index 100% rename from tests/unit/searchCountryOptionsTest.js rename to tests/unit/searchCountryOptionsTest.ts From 910a31671de0f4a2b13d2db710c3371331035dab Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Tue, 19 Mar 2024 15:26:04 +0100 Subject: [PATCH 204/245] fix main --- src/pages/EnablePayments/OnfidoStep.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/EnablePayments/OnfidoStep.tsx b/src/pages/EnablePayments/OnfidoStep.tsx index 46c6e1c8e6ed..3ebcdad40c37 100644 --- a/src/pages/EnablePayments/OnfidoStep.tsx +++ b/src/pages/EnablePayments/OnfidoStep.tsx @@ -3,7 +3,6 @@ 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'; @@ -66,7 +65,7 @@ function OnfidoStep({walletOnfidoData = DEFAULT_WALLET_ONFIDO_DATA}: OnfidoStepP {shouldShowOnfido ? ( Date: Tue, 19 Mar 2024 15:57:01 +0100 Subject: [PATCH 205/245] add return type --- tests/e2e/nativeCommands/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/nativeCommands/index.ts b/tests/e2e/nativeCommands/index.ts index 09d727b5d257..31af618c8ec1 100644 --- a/tests/e2e/nativeCommands/index.ts +++ b/tests/e2e/nativeCommands/index.ts @@ -4,7 +4,7 @@ import adbTypeText from './adbTypeText'; // eslint-disable-next-line rulesdir/prefer-import-module-contents import {NativeCommandsAction} from './NativeCommandsAction'; -const executeFromPayload = (actionName?: string, payload?: NativeCommandPayload) => { +const executeFromPayload = (actionName?: string, payload?: NativeCommandPayload): boolean => { switch (actionName) { case NativeCommandsAction.scroll: throw new Error('Not implemented yet'); From 189c30efc700bcb758729f9bd4e3478c0a37e557 Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Tue, 19 Mar 2024 17:17:32 +0200 Subject: [PATCH 206/245] Add invite link inside share Workspace --- src/pages/workspace/WorkspaceProfileSharePage.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/workspace/WorkspaceProfileSharePage.tsx b/src/pages/workspace/WorkspaceProfileSharePage.tsx index 56fd8b783742..0bb6eefa28a4 100644 --- a/src/pages/workspace/WorkspaceProfileSharePage.tsx +++ b/src/pages/workspace/WorkspaceProfileSharePage.tsx @@ -32,9 +32,11 @@ function WorkspaceProfileSharePage({policy}: WithPolicyProps) { const policyName = policy?.name ?? ''; const id = policy?.id ?? ''; + const adminEmail = policy?.owner ?? ''; const urlWithTrailingSlash = Url.addTrailingForwardSlash(environmentURL); - const url = `${urlWithTrailingSlash}${ROUTES.WORKSPACE_PROFILE.getRoute(id)}`; + const url = `${urlWithTrailingSlash}${ROUTES.WORKSPACE_JOIN_USER.getRoute(id, adminEmail)}`; + return ( Date: Tue, 19 Mar 2024 22:46:06 +0700 Subject: [PATCH 207/245] fix: An extra space is created at the top of the chat list when pinning and unpinning --- src/components/LHNOptionsList/OptionRowLHN.tsx | 10 +++++----- src/libs/SidebarUtils.ts | 10 ++++++++++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index 923337ba9ada..eca9f728da30 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -55,12 +55,12 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti const hasBrickError = optionItem.brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; const shouldShowGreenDotIndicator = !hasBrickError && ReportUtils.requiresAttentionFromCurrentUser(optionItem, optionItem.parentReportAction); - const isHidden = optionItem.notificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; + // const isHidden = optionItem.notificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; - const shouldOverrideHidden = hasBrickError || isFocused || optionItem.isPinned; - if (isHidden && !shouldOverrideHidden) { - return null; - } + // const shouldOverrideHidden = hasBrickError || isFocused || optionItem.isPinned; + // if (isHidden && !shouldOverrideHidden) { + // return null; + // } const isInFocusMode = viewMode === CONST.OPTION_MODE.COMPACT; const textStyle = isFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText; diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 7bf163416054..8377a339ffff 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -79,9 +79,19 @@ function getOrderedReportIDs( let reportsToDisplay = allReportsDictValues.filter((report) => { const parentReportActionsKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.parentReportID}`; const parentReportActions = allReportActions?.[parentReportActionsKey]; + const reportActions = ReportActionsUtils.getAllReportActions(report.reportID); const parentReportAction = parentReportActions?.find((action) => action && report && action?.reportActionID === report?.parentReportActionID); const doesReportHaveViolations = betas.includes(CONST.BETAS.VIOLATIONS) && !!parentReportAction && ReportUtils.doesTransactionThreadHaveViolations(report, transactionViolations, parentReportAction); + const isHidden = report?.notificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; + const isFocused = report?.reportID === currentReportId; + const hasErrors = Object.keys(OptionsListUtils.getAllReportErrors(report, reportActions) ?? {}).length !== 0; + const hasBrickError = hasErrors || doesReportHaveViolations ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; + const shouldOverrideHidden = hasBrickError || isFocused || report.isPinned; + if (isHidden && !shouldOverrideHidden) { + return false; + } + return ReportUtils.shouldReportBeInOptionList({ report, currentReportId: currentReportId ?? '', From f393dd2927e4b6bde27703aa59808b41b9442544 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal <58412969+shubham1206agra@users.noreply.github.com> Date: Tue, 19 Mar 2024 21:40:07 +0530 Subject: [PATCH 208/245] Apply suggestions from code review Co-authored-by: Ishpaul Singh <104348397+ishpaul777@users.noreply.github.com> --- src/components/ReportActionItem/MoneyRequestPreview/index.tsx | 2 +- src/components/ReportActionItem/MoneyRequestPreview/types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestPreview/index.tsx b/src/components/ReportActionItem/MoneyRequestPreview/index.tsx index 5d5394d79777..b46e052f3420 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/index.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/index.tsx @@ -7,7 +7,7 @@ import MoneyRequestPreviewContent from './MoneyRequestPreviewContent'; import type {MoneyRequestPreviewOnyxProps, MoneyRequestPreviewProps} from './types'; function MoneyRequestPreview(props: MoneyRequestPreviewProps) { - // We should not render the component if there is no iouReport and it's not a split. + // We should not render the component if there is no iouReport and it's not a split or track expense. // Moved outside of the component scope to allow for easier use of hooks in the main component. // eslint-disable-next-line react/jsx-props-no-spreading return lodashIsEmpty(props.iouReport) && !(props.isBillSplit || props.isTrackExpense) ? null : ; diff --git a/src/components/ReportActionItem/MoneyRequestPreview/types.ts b/src/components/ReportActionItem/MoneyRequestPreview/types.ts index b569e8b0f382..3b3eda4ec30a 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/types.ts +++ b/src/components/ReportActionItem/MoneyRequestPreview/types.ts @@ -56,7 +56,7 @@ type MoneyRequestPreviewProps = MoneyRequestPreviewOnyxProps & { /** True if this is this IOU is a split instead of a 1:1 request */ isBillSplit: boolean; - /** True if this is this IOU is a track expense */ + /** Whether this IOU is a track expense */ isTrackExpense: boolean; /** True if the IOU Preview card is hovered */ From 13a8c951671c85e58d7a049d5b2458e153f3345e Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 19 Mar 2024 22:39:01 +0530 Subject: [PATCH 209/245] Fixed LHN alternative text --- src/libs/ReportUtils.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 62e39c38efe0..f45c65f8668a 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2490,6 +2490,28 @@ function getReportPreviewMessage( } } + if (!isEmptyObject(reportAction) && !isIOUReport(report) && reportAction && ReportActionsUtils.isTrackExpenseAction(reportAction)) { + // This covers group chats where the last action is a track expense action + const linkedTransaction = 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'); + } + + const transactionDetails = getTransactionDetails(linkedTransaction); + const formattedAmount = CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency ?? ''); + return Localize.translateLocal('iou.trackedAmount', {formattedAmount, comment: transactionDetails?.comment ?? ''}); + } + } + const containsNonReimbursable = hasNonReimbursableTransactions(report.reportID); const totalAmount = getMoneyRequestSpendBreakdown(report).totalDisplaySpend; const policyName = getPolicyName(report, false, policy); From 748280e1da9445072396cd39e6db021035688720 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Tue, 19 Mar 2024 18:32:51 +0100 Subject: [PATCH 210/245] Fix translations --- src/languages/es.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 0a3f059e0bb8..7bc60d4936f2 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1894,7 +1894,7 @@ export default { taxRateAlreadyExists: 'Ya existe un impuesto con este nombre', valuePercentageRange: 'Introduzca un porcentaje válido entre 0 y 100', genericFailureMessage: 'Se produjo un error al actualizar el tipo impositivo, inténtelo nuevamente.', - customNameRequired: 'Nombre del impuesto es obligatorio.', + customNameRequired: 'El nombre del impuesto es obligatorio.', }, }, emptyWorkspace: { From 68f5bbe5ebe88a196bdbfea263250d078d7b74db Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Tue, 19 Mar 2024 15:36:34 -0400 Subject: [PATCH 211/245] remove ADDCOMMENT check --- src/libs/ReportActionsUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 22e1666e0988..47f2cabba46c 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -98,7 +98,7 @@ function isDeletedAction(reportAction: OnyxEntry Date: Wed, 20 Mar 2024 05:44:13 +0530 Subject: [PATCH 212/245] Fixed bug after merge --- src/libs/ReportUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index bbd31c8b2468..2f0ecb4f9fb6 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2494,9 +2494,9 @@ function getReportPreviewMessage( } } - if (!isEmptyObject(reportAction) && !isIOUReport(report) && reportAction && ReportActionsUtils.isTrackExpenseAction(reportAction)) { + if (!isEmptyObject(iouReportAction) && !isIOUReport(report) && iouReportAction && ReportActionsUtils.isTrackExpenseAction(iouReportAction)) { // This covers group chats where the last action is a track expense action - const linkedTransaction = getLinkedTransaction(reportAction); + const linkedTransaction = getLinkedTransaction(iouReportAction); if (isEmptyObject(linkedTransaction)) { return reportActionMessage; } From cc425d7c32bf13c9c7fda8408d21eb050de192d0 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Wed, 20 Mar 2024 05:53:28 +0530 Subject: [PATCH 213/245] Fixed svg --- assets/images/document-plus.svg | 5 +++++ assets/images/track-expense.svg | 9 --------- src/components/Icon/Expensicons.ts | 4 ++-- .../AttachmentPickerWithMenuItems.tsx | 2 +- .../SidebarScreen/FloatingActionButtonAndPopover.js | 2 +- 5 files changed, 9 insertions(+), 13 deletions(-) create mode 100644 assets/images/document-plus.svg delete mode 100644 assets/images/track-expense.svg diff --git a/assets/images/document-plus.svg b/assets/images/document-plus.svg new file mode 100644 index 000000000000..bb50afc63c46 --- /dev/null +++ b/assets/images/document-plus.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/track-expense.svg b/assets/images/track-expense.svg deleted file mode 100644 index c15f28b72dd7..000000000000 --- a/assets/images/track-expense.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index 9fb20bd2ea91..6087e83603dd 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -44,6 +44,7 @@ import Copy from '@assets/images/copy.svg'; import CreditCard from '@assets/images/creditcard.svg'; import DocumentSlash from '@assets/images/document-slash.svg'; import Document from '@assets/images/document.svg'; +import DocumentPlus from '@assets/images/document-plus.svg'; import DotIndicatorUnfilled from '@assets/images/dot-indicator-unfilled.svg'; import DotIndicator from '@assets/images/dot-indicator.svg'; import DownArrow from '@assets/images/down.svg'; @@ -141,7 +142,6 @@ import Task from '@assets/images/task.svg'; import Tax from '@assets/images/tax.svg'; import ThreeDots from '@assets/images/three-dots.svg'; import ThumbsUp from '@assets/images/thumbs-up.svg'; -import TrackExpense from '@assets/images/track-expense.svg'; import Transfer from '@assets/images/transfer.svg'; import Trashcan from '@assets/images/trashcan.svg'; import Unlock from '@assets/images/unlock.svg'; @@ -315,5 +315,5 @@ export { ChatBubbleUnread, ChatBubbleReply, Lightbulb, - TrackExpense, + DocumentPlus, }; diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx index 54ea6b2a3b77..95533db02f06 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx @@ -139,7 +139,7 @@ function AttachmentPickerWithMenuItems({ onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.SEND, report?.reportID ?? ''), }, [CONST.IOU.TYPE.TRACK_EXPENSE]: { - icon: Expensicons.TrackExpense, + icon: Expensicons.DocumentPlus, text: translate('iou.trackExpense'), onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.TRACK_EXPENSE, report?.reportID ?? ''), }, diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js index a152d763a193..abf932eff96d 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js @@ -192,7 +192,7 @@ function FloatingActionButtonAndPopover(props) { ...(canUseTrackExpense ? [ { - icon: Expensicons.TrackExpense, + icon: Expensicons.DocumentPlus, text: translate('iou.trackExpense'), onSelected: () => interceptAnonymousUser(() => From 8a59e755b50ab17a4ab1d4cf94a8679deee3e601 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Wed, 20 Mar 2024 06:13:12 +0530 Subject: [PATCH 214/245] Fixed lint --- src/components/Icon/Expensicons.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index 6087e83603dd..7116ba2aab67 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -42,9 +42,9 @@ import Concierge from '@assets/images/concierge.svg'; import Connect from '@assets/images/connect.svg'; import Copy from '@assets/images/copy.svg'; import CreditCard from '@assets/images/creditcard.svg'; +import DocumentPlus from '@assets/images/document-plus.svg'; import DocumentSlash from '@assets/images/document-slash.svg'; import Document from '@assets/images/document.svg'; -import DocumentPlus from '@assets/images/document-plus.svg'; import DotIndicatorUnfilled from '@assets/images/dot-indicator-unfilled.svg'; import DotIndicator from '@assets/images/dot-indicator.svg'; import DownArrow from '@assets/images/down.svg'; From fb070033aec7b2573495f4eef545cc34cbcee84b Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Wed, 20 Mar 2024 06:19:30 +0530 Subject: [PATCH 215/245] Fixed svg fill --- assets/images/document-plus.svg | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/assets/images/document-plus.svg b/assets/images/document-plus.svg index bb50afc63c46..cce2e3027cea 100644 --- a/assets/images/document-plus.svg +++ b/assets/images/document-plus.svg @@ -1,5 +1,5 @@ - - - - + + + + From edc7d4d37748c596c3e403170c1332d325261662 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Wed, 20 Mar 2024 07:07:15 +0530 Subject: [PATCH 216/245] Fixed style on confirm step --- .../MoneyTemporaryForRefactorRequestConfirmationList.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 545a34b10e5c..138bfc937926 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -449,7 +449,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ } else { const formattedSelectedParticipants = _.map(selectedParticipants, (participant) => ({ ...participant, - isDisabled: !participant.isPolicyExpenseChat && ReportUtils.isOptimisticPersonalDetail(participant.accountID), + isDisabled: !participant.isPolicyExpenseChat && !participant.isSelfDM && ReportUtils.isOptimisticPersonalDetail(participant.accountID), })); sections.push({ title: translate('common.to'), @@ -541,6 +541,11 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const navigateToReportOrUserDetail = (option) => { const activeRoute = Navigation.getActiveRouteWithoutParams(); + if (option.isSelfDM) { + Navigation.navigate(ROUTES.PROFILE.getRoute(currentUserPersonalDetails.accountID, activeRoute)); + return; + } + if (option.accountID) { Navigation.navigate(ROUTES.PROFILE.getRoute(option.accountID, activeRoute)); } else if (option.reportID) { From b16cef4686f79920f675901fe60202e226e9a04e Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Wed, 20 Mar 2024 09:50:28 +0700 Subject: [PATCH 217/245] remove useless code --- src/components/LHNOptionsList/OptionRowLHN.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index eca9f728da30..5065d1cc7c13 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -54,14 +54,6 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti const hasBrickError = optionItem.brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; const shouldShowGreenDotIndicator = !hasBrickError && ReportUtils.requiresAttentionFromCurrentUser(optionItem, optionItem.parentReportAction); - - // const isHidden = optionItem.notificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; - - // const shouldOverrideHidden = hasBrickError || isFocused || optionItem.isPinned; - // if (isHidden && !shouldOverrideHidden) { - // return null; - // } - const isInFocusMode = viewMode === CONST.OPTION_MODE.COMPACT; const textStyle = isFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText; const textUnreadStyle = optionItem?.isUnread && optionItem.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.MUTE ? [textStyle, styles.sidebarLinkTextBold] : [textStyle]; From e0b531f56739d6b2d181849cda712eb3aff30dbf Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Wed, 20 Mar 2024 10:17:23 +0530 Subject: [PATCH 218/245] Implemented edit track expense, but doesn't work due to BE problems --- src/libs/ReportUtils.ts | 5 + src/libs/actions/IOU.ts | 215 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 216 insertions(+), 4 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 2f0ecb4f9fb6..4d7d19333d72 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2232,6 +2232,11 @@ function canEditMoneyRequest(reportAction: OnyxEntry): boolean { return true; } + // TODO: Uncomment this line when BE starts working properly (Editing Track Expense) + // if (reportAction.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK) { + // return true; + // } + if (reportAction.originalMessage.type !== CONST.IOU.REPORT_ACTION_TYPE.CREATE) { return false; } diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index eb245f218285..9d351435a096 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -1741,6 +1741,185 @@ function getUpdateMoneyRequestParams( }; } +/** + * @param transactionID + * @param transactionThreadReportID + * @param transactionChanges + * @param [transactionChanges.created] Present when updated the date field + * @param onlyIncludeChangedFields + * When 'true', then the returned params will only include the transaction details for the fields that were changed. + * When `false`, then the returned params will include all the transaction details, regardless of which fields were changed. + * This setting is necessary while the UpdateDistanceRequest API is refactored to be fully 1:1:1 in https://github.com/Expensify/App/issues/28358 + */ +function getUpdateTrackExpenseParams( + transactionID: string, + transactionThreadReportID: string, + transactionChanges: TransactionChanges, + onlyIncludeChangedFields: boolean, +): UpdateMoneyRequestData { + const optimisticData: OnyxUpdate[] = []; + const successData: OnyxUpdate[] = []; + const failureData: OnyxUpdate[] = []; + + // Step 1: Set any "pending fields" (ones updated while the user was offline) to have error messages in the failureData + const pendingFields = Object.fromEntries(Object.keys(transactionChanges).map((key) => [key, CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE])); + const clearedPendingFields = Object.fromEntries(Object.keys(transactionChanges).map((key) => [key, null])); + const errorFields = Object.fromEntries(Object.keys(pendingFields).map((key) => [key, {[DateUtils.getMicroseconds()]: Localize.translateLocal('iou.error.genericEditFailureMessage')}])); + + // Step 2: Get all the collections being updated + const transactionThread = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null; + const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; + const chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThread?.parentReportID}`] ?? null; + const isScanning = TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction); + let updatedTransaction = transaction ? TransactionUtils.getUpdatedTransaction(transaction, transactionChanges, false) : null; + const transactionDetails = ReportUtils.getTransactionDetails(updatedTransaction); + + if (transactionDetails?.waypoints) { + // This needs to be a JSON string since we're sending this to the MapBox API + transactionDetails.waypoints = JSON.stringify(transactionDetails.waypoints); + } + + const dataToIncludeInParams: Partial | undefined = onlyIncludeChangedFields + ? Object.fromEntries(Object.entries(transactionDetails ?? {}).filter(([key]) => Object.keys(transactionChanges).includes(key))) + : transactionDetails; + + const params: UpdateMoneyRequestParams = { + ...dataToIncludeInParams, + reportID: chatReport?.reportID, + transactionID, + }; + + const hasPendingWaypoints = 'waypoints' in transactionChanges; + if (transaction && updatedTransaction && hasPendingWaypoints) { + updatedTransaction = { + ...updatedTransaction, + amount: CONST.IOU.DEFAULT_AMOUNT, + modifiedAmount: CONST.IOU.DEFAULT_AMOUNT, + modifiedMerchant: Localize.translateLocal('iou.routePending'), + }; + + // Delete the draft transaction when editing waypoints when the server responds successfully and there are no errors + successData.push({ + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, + value: null, + }); + + // Revert the transaction's amount to the original value on failure. + // The IOU Report will be fully reverted in the failureData further below. + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + value: { + amount: transaction.amount, + modifiedAmount: transaction.modifiedAmount, + modifiedMerchant: transaction.modifiedMerchant, + }, + }); + } + + // Step 3: Build the modified expense report actions + // We don't create a modified report action if we're updating the waypoints, + // since there isn't actually any optimistic data we can create for them and the report action is created on the server + // with the response from the MapBox API + const updatedReportAction = ReportUtils.buildOptimisticModifiedExpenseReportAction(transactionThread, transaction, transactionChanges, false); + if (!hasPendingWaypoints) { + params.reportActionID = updatedReportAction.reportActionID; + + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThread?.reportID}`, + value: { + [updatedReportAction.reportActionID]: updatedReportAction as OnyxTypes.ReportAction, + }, + }); + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThread?.reportID}`, + value: { + [updatedReportAction.reportActionID]: {pendingAction: null}, + }, + }); + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThread?.reportID}`, + value: { + [updatedReportAction.reportActionID]: { + ...(updatedReportAction as OnyxTypes.ReportAction), + errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericEditFailureMessage'), + }, + }, + }); + } + + // Step 4: Update the report preview message (and report header) so LHN amount tracked is correct. + // Optimistically modify the transaction and the transaction thread + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + value: { + ...updatedTransaction, + pendingFields, + isLoading: hasPendingWaypoints, + errorFields: null, + }, + }); + + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`, + value: { + lastActorAccountID: updatedReportAction.actorAccountID, + }, + }); + + if (isScanning && ('amount' in transactionChanges || 'currency' in transactionChanges)) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, + value: { + [transactionThread?.parentReportActionID ?? '']: { + whisperedToAccountIDs: [], + }, + }, + }); + } + + // Clear out the error fields and loading states on success + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + value: { + pendingFields: clearedPendingFields, + isLoading: false, + errorFields: null, + }, + }); + + // Clear out loading states, pending fields, and add the error fields + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + value: { + pendingFields: clearedPendingFields, + isLoading: false, + errorFields, + }, + }); + + // Reset the transaction thread to its original state + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`, + value: transactionThread, + }); + + return { + params, + onyxData: {optimisticData, successData, failureData}, + }; +} + /** Updates the created date of a money request */ function updateMoneyRequestDate( transactionID: string, @@ -1753,7 +1932,14 @@ function updateMoneyRequestDate( const transactionChanges: TransactionChanges = { created: value, }; - const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTags, policyCategories, true); + const transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null; + let data: UpdateMoneyRequestData; + if (ReportUtils.isTrackExpenseReport(transactionThreadReport)) { + data = getUpdateTrackExpenseParams(transactionID, transactionThreadReportID, transactionChanges, true); + } else { + data = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTags, policyCategories, true); + } + const {params, onyxData} = data; API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_DATE, params, onyxData); } @@ -1785,7 +1971,14 @@ function updateMoneyRequestMerchant( const transactionChanges: TransactionChanges = { merchant: value, }; - const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList, policyCategories, true); + const transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null; + let data: UpdateMoneyRequestData; + if (ReportUtils.isTrackExpenseReport(transactionThreadReport)) { + data = getUpdateTrackExpenseParams(transactionID, transactionThreadReportID, transactionChanges, true); + } else { + data = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList, policyCategories, true); + } + const {params, onyxData} = data; API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_MERCHANT, params, onyxData); } @@ -1817,7 +2010,14 @@ function updateMoneyRequestDistance( const transactionChanges: TransactionChanges = { waypoints, }; - const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList, policyCategories, true); + const transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null; + let data: UpdateMoneyRequestData; + if (ReportUtils.isTrackExpenseReport(transactionThreadReport)) { + data = getUpdateTrackExpenseParams(transactionID, transactionThreadReportID, transactionChanges, true); + } else { + data = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList, policyCategories, true); + } + const {params, onyxData} = data; API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_DISTANCE, params, onyxData); } @@ -1849,7 +2049,14 @@ function updateMoneyRequestDescription( const transactionChanges: TransactionChanges = { comment, }; - const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList, policyCategories, true); + const transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null; + let data: UpdateMoneyRequestData; + if (ReportUtils.isTrackExpenseReport(transactionThreadReport)) { + data = getUpdateTrackExpenseParams(transactionID, transactionThreadReportID, transactionChanges, true); + } else { + data = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList, policyCategories, true); + } + const {params, onyxData} = data; API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_DESCRIPTION, params, onyxData); } From 7436d791f5b80c7c308080c36d2f65ba0aea6cc7 Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Wed, 20 Mar 2024 09:42:38 +0200 Subject: [PATCH 219/245] use email from session --- src/pages/workspace/WorkspaceProfileSharePage.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/workspace/WorkspaceProfileSharePage.tsx b/src/pages/workspace/WorkspaceProfileSharePage.tsx index 0bb6eefa28a4..340c63c19ea7 100644 --- a/src/pages/workspace/WorkspaceProfileSharePage.tsx +++ b/src/pages/workspace/WorkspaceProfileSharePage.tsx @@ -6,6 +6,7 @@ import ContextMenuItem from '@components/ContextMenuItem'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; +import {useSession} from '@components/OnyxProvider'; import QRShareWithDownload from '@components/QRShare/QRShareWithDownload'; import type QRShareWithDownloadHandle from '@components/QRShare/QRShareWithDownload/types'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -29,10 +30,11 @@ function WorkspaceProfileSharePage({policy}: WithPolicyProps) { const {environmentURL} = useEnvironment(); const qrCodeRef = useRef(null); const {isSmallScreenWidth} = useWindowDimensions(); + const session = useSession(); const policyName = policy?.name ?? ''; const id = policy?.id ?? ''; - const adminEmail = policy?.owner ?? ''; + const adminEmail = session?.email ?? ''; const urlWithTrailingSlash = Url.addTrailingForwardSlash(environmentURL); const url = `${urlWithTrailingSlash}${ROUTES.WORKSPACE_JOIN_USER.getRoute(id, adminEmail)}`; From 1ec0839fc856c9164cdc48353ecac945b919ce0a Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 20 Mar 2024 15:22:56 +0700 Subject: [PATCH 220/245] fix split bill issue --- src/ROUTES.ts | 12 ++++++------ src/components/MoneyRequestConfirmationList.tsx | 10 +++++++++- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 3db389950e24..efa41ca5046d 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -332,9 +332,9 @@ const ROUTES = { getUrlWithBackToParam(`create/${iouType}/taxAmount/${transactionID}/${reportID}`, backTo), }, MONEY_REQUEST_STEP_CATEGORY: { - route: ':action/:iouType/category/:transactionID/:reportID', - getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => - getUrlWithBackToParam(`${action}/${iouType}/category/${transactionID}/${reportID}`, backTo), + route: ':action/:iouType/category/:transactionID/:reportID/:reportActionID?', + getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string, backTo = '', reportActionID?: string) => + getUrlWithBackToParam(`${action}/${iouType}/category/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo), }, MONEY_REQUEST_STEP_CURRENCY: { route: 'create/:iouType/currency/:transactionID/:reportID/:pageIndex?', @@ -347,9 +347,9 @@ const ROUTES = { getUrlWithBackToParam(`${action}/${iouType}/date/${transactionID}/${reportID}`, backTo), }, MONEY_REQUEST_STEP_DESCRIPTION: { - route: ':action/:iouType/description/:transactionID/:reportID', - getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => - getUrlWithBackToParam(`${action}/${iouType}/description/${transactionID}/${reportID}`, backTo), + route: ':action/:iouType/description/:transactionID/:reportID/:reportActionID?', + getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string, backTo = '', reportActionID?: string) => + getUrlWithBackToParam(`${action}/${iouType}/description/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo), }, MONEY_REQUEST_STEP_DISTANCE: { route: 'create/:iouType/distance/:transactionID/:reportID', diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 2e8f80175b56..a4da7e551515 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -665,7 +665,14 @@ function MoneyRequestConfirmationList({ description={translate('common.description')} onPress={() => { Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams()), + ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute( + CONST.IOU.ACTION.EDIT, + iouType, + transaction?.transactionID ?? '', + reportID, + Navigation.getActiveRouteWithoutParams(), + reportActionID, + ), ); }} style={styles.moneyRequestMenuItem} @@ -757,6 +764,7 @@ function MoneyRequestConfirmationList({ transaction?.transactionID ?? '', reportID, Navigation.getActiveRouteWithoutParams(), + reportActionID, ), ); }} From 1e0386d8566820c97c04a44febd1ec892aa8e839 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 20 Mar 2024 09:50:00 +0100 Subject: [PATCH 221/245] move usePaginatedReportActionList back --- src/libs/ReportActionsUtils.ts | 93 ------------------ src/pages/home/report/ReportActionsView.tsx | 97 ++++++++++++++++++- .../getInitialPaginationSize/index.native.ts | 5 +- .../report/getInitialPaginationSize/index.ts | 5 +- 4 files changed, 97 insertions(+), 103 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 8b1d5dc94f85..8678d10d3f89 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -1021,98 +1021,6 @@ function getReportActionMessageText(reportAction: OnyxEntry | Empt return reportAction?.message?.reduce((acc, curr) => `${acc}${curr.text}`, '') ?? ''; } -let listIDCount = Math.round(Math.random() * 100); -/** - * usePaginatedReportActionList manages the logic for handling a list of messages with pagination and dynamic loading. - * It determines the part of the message array to display ('visibleReportActions') based on the current linked message, - * and manages pagination through 'handleReportActionPagination' function. - * - * linkedID - ID of the linked message used for initial focus. - * allReportActions - Array of messages. - * fetchNewerReportActions - Function to fetch more messages. - * route - Current route, used to reset states on route change. - * isLoading - Loading state indicator. - * triggerListID - Used to trigger a listID change. - * returns {object} An object containing the sliced message array, the pagination function, - * index of the linked message, and a unique list ID. - */ -const usePaginatedReportActionList = ( - linkedID: string, - localAllReportActions: OnyxTypes.ReportAction[], - fetchNewerReportActions: (newestReportAction: OnyxTypes.ReportAction) => void, - route: RouteProp, - isLoading: boolean, - triggerListID: boolean, -) => { - // triggerListID is used when navigating to a chat with messages loaded from LHN. Typically, these include thread actions, task actions, etc. Since these messages aren't the latest, we don't maintain their position and instead trigger a recalculation of their positioning in the list. - // we don't set currentReportActionID on initial render as linkedID as it should trigger visibleReportActions after linked message was positioned - const [currentReportActionID, setCurrentReportActionID] = useState(''); - const isFirstLinkedActionRender = useRef(true); - - useLayoutEffect(() => { - setCurrentReportActionID(''); - }, [route]); - - const listID = useMemo(() => { - isFirstLinkedActionRender.current = true; - listIDCount += 1; - return listIDCount; - // This needs to be triggered with each navigation to a comment. It happens when the route is changed. triggerListID is needed to trigger a listID change after initial mounting. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [route, triggerListID]); - - const index = useMemo(() => { - if (!linkedID || isLoading) { - return -1; - } - - return localAllReportActions.findIndex((obj) => String(obj.reportActionID) === String(isFirstLinkedActionRender.current ? linkedID : currentReportActionID)); - }, [localAllReportActions, currentReportActionID, linkedID, isLoading]); - - const visibleReportActions = useMemo(() => { - if (!linkedID) { - return localAllReportActions; - } - if (isLoading || index === -1) { - return []; - } - - if (isFirstLinkedActionRender.current) { - return localAllReportActions.slice(index, localAllReportActions.length); - } - const paginationSize = getInitialPaginationSize(); - const newStartIndex = index >= paginationSize ? index - paginationSize : 0; - return newStartIndex ? localAllReportActions.slice(newStartIndex, localAllReportActions.length) : localAllReportActions; - // currentReportActionID is needed to trigger batching once the report action has been positioned - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [linkedID, localAllReportActions, index, isLoading, currentReportActionID]); - - const hasMoreCached = visibleReportActions.length < localAllReportActions?.length; - const newestReportAction = visibleReportActions?.[0]; - - const handleReportActionPagination = useCallback( - ({firstReportActionID}: {firstReportActionID: string}) => { - // This function is a placeholder as the actual pagination is handled by visibleReportActions - if (!hasMoreCached) { - isFirstLinkedActionRender.current = false; - fetchNewerReportActions(newestReportAction); - } - if (isFirstLinkedActionRender.current) { - isFirstLinkedActionRender.current = false; - } - setCurrentReportActionID(firstReportActionID); - }, - [fetchNewerReportActions, hasMoreCached, newestReportAction], - ); - - return { - visibleReportActions, - loadMoreReportActionsHandler: handleReportActionPagination, - linkedIdIndex: index, - listID, - }; -}; - export { extractLinksFromMessageHtml, getAllReportActions, @@ -1172,7 +1080,6 @@ export { isCurrentActionUnread, isActionableJoinRequest, isActionableJoinRequestPending, - usePaginatedReportActionList, }; export type {LastVisibleMessage}; diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index 9b40f7ca9c3a..bd5415478f04 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -1,7 +1,7 @@ import type {RouteProp} from '@react-navigation/native'; import {useIsFocused, useRoute} from '@react-navigation/native'; import lodashIsEqual from 'lodash/isEqual'; -import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; import {InteractionManager} from 'react-native'; import type {LayoutChangeEvent} from 'react-native'; import {withOnyx} from 'react-native-onyx'; @@ -27,6 +27,7 @@ import type * as OnyxTypes from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import PopoverReactionList from './ReactionList/PopoverReactionList'; import ReportActionsList from './ReportActionsList'; +import getInitialPaginationSize from './getInitialPaginationSize'; type ReportActionsViewOnyxProps = { /** Session info for the currently logged in user. */ @@ -59,6 +60,98 @@ type ReportActionsViewProps = ReportActionsViewOnyxProps & { const DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST = 120; const SPACER = 16; +let listIDCount = Math.round(Math.random() * 100); + +/** + * usePaginatedReportActionList manages the logic for handling a list of messages with pagination and dynamic loading. + * It determines the part of the message array to display ('visibleReportActions') based on the current linked message, + * and manages pagination through 'handleReportActionPagination' function. + * + * linkedID - ID of the linked message used for initial focus. + * allReportActions - Array of messages. + * fetchNewerReportActions - Function to fetch more messages. + * route - Current route, used to reset states on route change. + * isLoading - Loading state indicator. + * triggerListID - Used to trigger a listID change. + * returns {object} An object containing the sliced message array, the pagination function, + * index of the linked message, and a unique list ID. + */ +function usePaginatedReportActionList ( + linkedID: string, + allReportActions: OnyxTypes.ReportAction[], + fetchNewerReportActions: (newestReportAction: OnyxTypes.ReportAction) => void, + route: RouteProp, + isLoading: boolean, + triggerListID: boolean, +) { + // triggerListID is used when navigating to a chat with messages loaded from LHN. Typically, these include thread actions, task actions, etc. Since these messages aren't the latest, we don't maintain their position and instead trigger a recalculation of their positioning in the list. + // we don't set currentReportActionID on initial render as linkedID as it should trigger visibleReportActions after linked message was positioned + const [currentReportActionID, setCurrentReportActionID] = useState(''); + const isFirstLinkedActionRender = useRef(true); + + useLayoutEffect(() => { + setCurrentReportActionID(''); + }, [route]); + + const listID = useMemo(() => { + isFirstLinkedActionRender.current = true; + listIDCount += 1; + return listIDCount; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [route, triggerListID]); + + const index = useMemo(() => { + if (!linkedID || isLoading) { + return -1; + } + + return allReportActions.findIndex((obj) => String(obj.reportActionID) === String(isFirstLinkedActionRender.current ? linkedID : currentReportActionID)); + }, [allReportActions, currentReportActionID, linkedID, isLoading]); + + const visibleReportActions = useMemo(() => { + if (!linkedID) { + return allReportActions; + } + if (isLoading || index === -1) { + return []; + } + + if (isFirstLinkedActionRender.current) { + return allReportActions.slice(index, allReportActions.length); + } + const paginationSize = getInitialPaginationSize; + const newStartIndex = index >= paginationSize ? index - paginationSize : 0; + return newStartIndex ? allReportActions.slice(newStartIndex, allReportActions.length) : allReportActions; + // currentReportActionID is needed to trigger batching once the report action has been positioned + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [linkedID, allReportActions, index, isLoading, currentReportActionID]); + + const hasMoreCached = visibleReportActions.length < allReportActions.length; + const newestReportAction = visibleReportActions?.[0]; + + const handleReportActionPagination = useCallback( + ({firstReportActionID}: {firstReportActionID: string}) => { + // This function is a placeholder as the actual pagination is handled by visibleReportActions + if (!hasMoreCached) { + isFirstLinkedActionRender.current = false; + fetchNewerReportActions(newestReportAction); + } + if (isFirstLinkedActionRender.current) { + isFirstLinkedActionRender.current = false; + } + setCurrentReportActionID(firstReportActionID); + }, + [fetchNewerReportActions, hasMoreCached, newestReportAction], + ); + + return { + visibleReportActions, + loadMoreReportActionsHandler: handleReportActionPagination, + linkedIdIndex: index, + listID, + }; +}; + function ReportActionsView({ report, session, @@ -109,7 +202,7 @@ function ReportActionsView({ loadMoreReportActionsHandler, linkedIdIndex, listID, - } = ReportActionsUtils.usePaginatedReportActionList(reportActionID, allReportActions, fetchNewerAction, route, isLoading, isLoadingInitialReportActions); + } = usePaginatedReportActionList(reportActionID, allReportActions, fetchNewerAction, route, isLoading, isLoadingInitialReportActions); const mostRecentIOUReportActionID = useMemo(() => ReportActionsUtils.getMostRecentIOURequestActionID(reportActions), [reportActions]); const hasCachedActions = useInitialValue(() => reportActions.length > 0); const hasNewestReportAction = reportActions[0]?.created === report.lastVisibleActionCreated; diff --git a/src/pages/home/report/getInitialPaginationSize/index.native.ts b/src/pages/home/report/getInitialPaginationSize/index.native.ts index 69dbf5025ac5..195448f7e450 100644 --- a/src/pages/home/report/getInitialPaginationSize/index.native.ts +++ b/src/pages/home/report/getInitialPaginationSize/index.native.ts @@ -1,6 +1,3 @@ import CONST from '@src/CONST'; -function getInitialPaginationSize(): number { - return CONST.MOBILE_PAGINATION_SIZE; -} -export default getInitialPaginationSize; +export default CONST.MOBILE_PAGINATION_SIZE; diff --git a/src/pages/home/report/getInitialPaginationSize/index.ts b/src/pages/home/report/getInitialPaginationSize/index.ts index d1467c0325b7..cc53d086b7b8 100644 --- a/src/pages/home/report/getInitialPaginationSize/index.ts +++ b/src/pages/home/report/getInitialPaginationSize/index.ts @@ -1,6 +1,3 @@ import CONST from '@src/CONST'; -function getInitialPaginationSize(): number { - return CONST.WEB_PAGINATION_SIZE; -} -export default getInitialPaginationSize; +export default CONST.WEB_PAGINATION_SIZE From 7b9e7e33c80b057efd549e3d38c6a50673688376 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 20 Mar 2024 12:06:19 +0100 Subject: [PATCH 222/245] renaming --- src/pages/home/report/ReportActionsView.tsx | 106 +++++++++----------- 1 file changed, 48 insertions(+), 58 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index bd5415478f04..1452637800a4 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -25,9 +25,9 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import getInitialPaginationSize from './getInitialPaginationSize'; import PopoverReactionList from './ReactionList/PopoverReactionList'; import ReportActionsList from './ReportActionsList'; -import getInitialPaginationSize from './getInitialPaginationSize'; type ReportActionsViewOnyxProps = { /** Session info for the currently logged in user. */ @@ -76,7 +76,7 @@ let listIDCount = Math.round(Math.random() * 100); * returns {object} An object containing the sliced message array, the pagination function, * index of the linked message, and a unique list ID. */ -function usePaginatedReportActionList ( +function usePaginatedReportActionList( linkedID: string, allReportActions: OnyxTypes.ReportAction[], fetchNewerReportActions: (newestReportAction: OnyxTypes.ReportAction) => void, @@ -117,11 +117,10 @@ function usePaginatedReportActionList ( } if (isFirstLinkedActionRender.current) { - return allReportActions.slice(index, allReportActions.length); + return allReportActions.slice(index); } const paginationSize = getInitialPaginationSize; - const newStartIndex = index >= paginationSize ? index - paginationSize : 0; - return newStartIndex ? allReportActions.slice(newStartIndex, allReportActions.length) : allReportActions; + return allReportActions.slice(Math.max(index - paginationSize, 0)); // currentReportActionID is needed to trigger batching once the report action has been positioned // eslint-disable-next-line react-hooks/exhaustive-deps }, [linkedID, allReportActions, index, isLoading, currentReportActionID]); @@ -147,10 +146,10 @@ function usePaginatedReportActionList ( return { visibleReportActions, loadMoreReportActionsHandler: handleReportActionPagination, - linkedIdIndex: index, + linkedIDIndex: index, listID, }; -}; +} function ReportActionsView({ report, @@ -170,14 +169,13 @@ function ReportActionsView({ const didSubscribeToReportTypingEvents = useRef(false); const network = useNetwork(); - const {isSmallScreenWidth} = useWindowDimensions(); + const {isSmallScreenWidth, windowHeight} = useWindowDimensions(); const contentListHeight = useRef(0); const layoutListHeight = useRef(0); - const {windowHeight} = useWindowDimensions(); const isFocused = useIsFocused(); const prevNetworkRef = useRef(network); const prevAuthTokenType = usePrevious(session?.authTokenType); - const [isInitialLinkedView, setIsInitialLinkedView] = useState(!!reportActionID); + const [isNavigatingToLinkedMessage, setNavigatingToLinkedMessage] = useState(!!reportActionID); const prevIsSmallScreenWidthRef = useRef(isSmallScreenWidth); const reportID = report.reportID; const isLoading = (!!reportActionID && isLoadingInitialReportActions) || !isReadyForCommentLinking; @@ -200,16 +198,15 @@ function ReportActionsView({ const { visibleReportActions: reportActions, loadMoreReportActionsHandler, - linkedIdIndex, + linkedIDIndex, listID, } = usePaginatedReportActionList(reportActionID, allReportActions, fetchNewerAction, route, isLoading, isLoadingInitialReportActions); const mostRecentIOUReportActionID = useMemo(() => ReportActionsUtils.getMostRecentIOURequestActionID(reportActions), [reportActions]); - const hasCachedActions = useInitialValue(() => reportActions.length > 0); + const hasCachedActionOnFirstRender = useInitialValue(() => reportActions.length > 0); const hasNewestReportAction = reportActions[0]?.created === report.lastVisibleActionCreated; - const newestReportAction = reportActions?.[0]; + const newestReportAction = useMemo(() => reportActions?.[0], [reportActions]); const oldestReportAction = useMemo(() => reportActions?.at(-1), [reportActions]); const hasCreatedAction = oldestReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED; - const firstReportActionName = reportActions?.[0]?.actionName; const isReportFullyVisible = useMemo((): boolean => getIsReportFullyVisible(isFocused), [isFocused]); @@ -338,39 +335,32 @@ function ReportActionsView({ Report.getOlderActions(reportID, oldestReportAction.reportActionID); }, [network.isOffline, isLoadingOlderReportActions, isLoadingInitialReportActions, oldestReportAction, hasCreatedAction, reportID]); - // const firstReportActionID = useMemo(() => lodashGet(newestReportAction, 'reportActionID'), [newestReportAction]); - const firstReportActionID = useMemo(() => newestReportAction?.reportActionID, [newestReportAction]); - const loadNewerChats = useCallback( - // eslint-disable-next-line rulesdir/prefer-early-return - () => { - if (isLoadingInitialReportActions || isLoadingOlderReportActions || network.isOffline || newestReportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { - return; - } - // Determines if loading older reports is necessary when the content is smaller than the list - // and there are fewer than 23 items, indicating we've reached the oldest message. - const isLoadingOlderReportsFirstNeeded = checkIfContentSmallerThanList() && reportActions.length > 23; - - if ( - (reportActionID && linkedIdIndex > -1 && !hasNewestReportAction && !isLoadingOlderReportsFirstNeeded) || - (!reportActionID && !hasNewestReportAction && !isLoadingOlderReportsFirstNeeded) - ) { - loadMoreReportActionsHandler({firstReportActionID}); - } - }, - [ - isLoadingInitialReportActions, - isLoadingOlderReportActions, - checkIfContentSmallerThanList, - reportActionID, - linkedIdIndex, - hasNewestReportAction, - loadMoreReportActionsHandler, - firstReportActionID, - network.isOffline, - reportActions.length, - newestReportAction, - ], - ); + const loadNewerChats = useCallback(() => { + if (isLoadingInitialReportActions || isLoadingOlderReportActions || network.isOffline || newestReportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { + return; + } + // Determines if loading older reports is necessary when the content is smaller than the list + // and there are fewer than 23 items, indicating we've reached the oldest message. + const isLoadingOlderReportsFirstNeeded = checkIfContentSmallerThanList() && reportActions.length > 23; + + if ( + (reportActionID && linkedIDIndex > -1 && !hasNewestReportAction && !isLoadingOlderReportsFirstNeeded) || + (!reportActionID && !hasNewestReportAction && !isLoadingOlderReportsFirstNeeded) + ) { + loadMoreReportActionsHandler({firstReportActionID: newestReportAction?.reportActionID}); + } + }, [ + isLoadingInitialReportActions, + isLoadingOlderReportActions, + checkIfContentSmallerThanList, + reportActionID, + linkedIDIndex, + hasNewestReportAction, + loadMoreReportActionsHandler, + network.isOffline, + reportActions.length, + newestReportAction, + ]); /** * Runs when the FlatList finishes laying out @@ -384,7 +374,7 @@ function ReportActionsView({ } didLayout.current = true; - Timing.end(CONST.TIMING.SWITCH_REPORT, hasCachedActions ? CONST.TIMING.WARM : CONST.TIMING.COLD); + Timing.end(CONST.TIMING.SWITCH_REPORT, hasCachedActionOnFirstRender ? CONST.TIMING.WARM : CONST.TIMING.COLD); // Capture the init measurement only once not per each chat switch as the value gets overwritten if (!ReportActionsView.initMeasured) { @@ -394,7 +384,7 @@ function ReportActionsView({ Performance.markEnd(CONST.TIMING.SWITCH_REPORT); } }, - [hasCachedActions], + [hasCachedActionOnFirstRender], ); useEffect(() => { @@ -402,7 +392,7 @@ function ReportActionsView({ // This code should be removed once REPORTPREVIEW is no longer repositioned. // We need to call openReport for gaps created by moving REPORTPREVIEW, which causes mismatches in previousReportActionID and reportActionID of adjacent reportActions. The server returns the correct sequence, allowing us to overwrite incorrect data with the correct one. const shouldOpenReport = - firstReportActionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && + newestReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && !hasCreatedAction && isReadyForCommentLinking && reportActions.length < 24 && @@ -419,7 +409,7 @@ function ReportActionsView({ reportID, reportActions, reportActionID, - firstReportActionName, + newestReportAction?.actionName, isReadyForCommentLinking, isLoadingOlderReportActions, isLoadingNewerReportActions, @@ -427,27 +417,27 @@ function ReportActionsView({ ]); // Check if the first report action in the list is the one we're currently linked to - const isTheFirstReportActionIsLinked = firstReportActionID === reportActionID; + const isTheFirstReportActionIsLinked = newestReportAction?.reportActionID === reportActionID; useEffect(() => { - let timerId: NodeJS.Timeout; + let timerID: NodeJS.Timeout; if (isTheFirstReportActionIsLinked) { - setIsInitialLinkedView(true); + setNavigatingToLinkedMessage(true); } else { // After navigating to the linked reportAction, apply this to correctly set // `autoscrollToTopThreshold` prop when linking to a specific reportAction. InteractionManager.runAfterInteractions(() => { // Using a short delay to ensure the view is updated after interactions - timerId = setTimeout(() => setIsInitialLinkedView(false), 10); + timerID = setTimeout(() => setNavigatingToLinkedMessage(false), 10); }); } return () => { - if (!timerId) { + if (!timerID) { return; } - clearTimeout(timerId); + clearTimeout(timerID); }; }, [isTheFirstReportActionIsLinked]); @@ -456,7 +446,7 @@ function ReportActionsView({ return null; } // AutoScroll is disabled when we do linking to a specific reportAction - const shouldEnableAutoScroll = hasNewestReportAction && (!reportActionID || !isInitialLinkedView); + const shouldEnableAutoScroll = hasNewestReportAction && (!reportActionID || !isNavigatingToLinkedMessage); return ( <> From 60eb0520c92ce837a55955b3ac259655aa70873e Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Wed, 20 Mar 2024 17:41:26 +0500 Subject: [PATCH 223/245] fix report fields --- .../ReportActionItem/MoneyReportView.tsx | 7 ++++--- src/libs/Permissions.ts | 2 +- src/libs/ReportUtils.ts | 14 ++++++++++--- src/libs/actions/Report.ts | 21 ++++++++++--------- src/pages/EditReportFieldDatePage.tsx | 16 +++++++------- src/pages/EditReportFieldDropdownPage.tsx | 14 ++++++++----- src/pages/EditReportFieldPage.tsx | 14 ++++++------- src/pages/EditReportFieldTextPage.tsx | 16 +++++++------- 8 files changed, 59 insertions(+), 45 deletions(-) diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx index 5272329860b3..e9b0ce3dae3f 100644 --- a/src/components/ReportActionItem/MoneyReportView.tsx +++ b/src/components/ReportActionItem/MoneyReportView.tsx @@ -72,13 +72,14 @@ function MoneyReportView({report, policy, shouldShowHorizontalRule}: MoneyReport const isTitleField = ReportUtils.isReportFieldOfTypeTitle(reportField); const fieldValue = isTitleField ? report.reportName : reportField.value ?? reportField.defaultValue; const isFieldDisabled = ReportUtils.isReportFieldDisabled(report, reportField, policy); + const fieldKey = ReportUtils.getReportFieldKey(reportField.fieldID); return ( ): boolean { - return !!betas?.includes(CONST.BETAS.ALL); + return true; // !!betas?.includes(CONST.BETAS.ALL); } function canUseChronos(betas: OnyxEntry): boolean { diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index a085a0876c40..267198f60bce 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2080,6 +2080,13 @@ function getTitleReportField(reportFields: Record) { return Object.values(reportFields).find((field) => isReportFieldOfTypeTitle(field)); } +/** + * Get the key for a report field + */ +function getReportFieldKey(reportFieldId: string) { + return `expensify_${reportFieldId}`; +} + /** * Get the report fields attached to the policy given policyID */ @@ -2091,7 +2098,7 @@ function getReportFieldsByPolicyID(policyID: string): Record; } /** @@ -2113,10 +2120,10 @@ function getAvailableReportFields(report: Report, policyReportFields: PolicyRepo const mergedFieldIds = Array.from(new Set([...policyReportFields.map(({fieldID}) => fieldID), ...reportFields.map(({fieldID}) => fieldID)])); const fields = mergedFieldIds.map((id) => { - const field = report?.fieldList?.[`expensify_${id}`]; + const field = report?.fieldList?.[getReportFieldKey(id)]; if (field) { - return field; + return field as PolicyReportField; } const policyReportField = policyReportFields.find(({fieldID}) => fieldID === id); @@ -5554,6 +5561,7 @@ export { hasUpdatedTotal, isReportFieldDisabled, getAvailableReportFields, + getReportFieldKey, reportFieldsEnabled, getAllAncestorReportActionIDs, getPendingChatMembers, diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 58168e8f269a..70eeaba9ec22 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -1590,6 +1590,7 @@ function updateReportName(reportID: string, value: string, previousValue: string function updateReportField(reportID: string, reportField: PolicyReportField, previousReportField: PolicyReportField) { const recentlyUsedValues = allRecentlyUsedReportFields?.[reportField.fieldID] ?? []; + const fieldID = ReportUtils.getReportFieldKey(reportField.fieldID); const optimisticData: OnyxUpdate[] = [ { @@ -1597,10 +1598,10 @@ function updateReportField(reportID: string, reportField: PolicyReportField, pre key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: { fieldList: { - [reportField.fieldID]: reportField, + [fieldID]: reportField, }, pendingFields: { - [reportField.fieldID]: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + [fieldID]: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, }, }, }, @@ -1611,7 +1612,7 @@ function updateReportField(reportID: string, reportField: PolicyReportField, pre onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.RECENTLY_USED_REPORT_FIELDS, value: { - [reportField.fieldID]: [...new Set([...recentlyUsedValues, reportField.value])], + [fieldID]: [...new Set([...recentlyUsedValues, reportField.value])], }, }); } @@ -1622,13 +1623,13 @@ function updateReportField(reportID: string, reportField: PolicyReportField, pre key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: { fieldList: { - [reportField.fieldID]: previousReportField, + [fieldID]: previousReportField, }, pendingFields: { - [reportField.fieldID]: null, + [fieldID]: null, }, errorFields: { - [reportField.fieldID]: ErrorUtils.getMicroSecondOnyxError('report.genericUpdateReportFieldFailureMessage'), + [fieldID]: ErrorUtils.getMicroSecondOnyxError('report.genericUpdateReportFieldFailureMessage'), }, }, }, @@ -1639,7 +1640,7 @@ function updateReportField(reportID: string, reportField: PolicyReportField, pre onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.RECENTLY_USED_REPORT_FIELDS, value: { - [reportField.fieldID]: recentlyUsedValues, + [fieldID]: recentlyUsedValues, }, }); } @@ -1650,10 +1651,10 @@ function updateReportField(reportID: string, reportField: PolicyReportField, pre key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: { pendingFields: { - [reportField.fieldID]: null, + [fieldID]: null, }, errorFields: { - [reportField.fieldID]: null, + [fieldID]: null, }, }, }, @@ -1661,7 +1662,7 @@ function updateReportField(reportID: string, reportField: PolicyReportField, pre const parameters = { reportID, - reportFields: JSON.stringify({[`expensify_${reportField.fieldID}`]: reportField}), + reportFields: JSON.stringify({[fieldID]: reportField}), }; API.write(WRITE_COMMANDS.SET_REPORT_FIELD, parameters, {optimisticData, failureData, successData}); diff --git a/src/pages/EditReportFieldDatePage.tsx b/src/pages/EditReportFieldDatePage.tsx index 45d2f31073ec..3d60884d3cfc 100644 --- a/src/pages/EditReportFieldDatePage.tsx +++ b/src/pages/EditReportFieldDatePage.tsx @@ -19,8 +19,8 @@ type EditReportFieldDatePageProps = { /** Name of the policy report field */ fieldName: string; - /** ID of the policy report field */ - fieldID: string; + /** Key of the policy report field */ + fieldKey: string; /** Flag to indicate if the field can be left blank */ isRequired: boolean; @@ -29,7 +29,7 @@ type EditReportFieldDatePageProps = { onSubmit: (form: FormOnyxValues) => void; }; -function EditReportFieldDatePage({fieldName, isRequired, onSubmit, fieldValue, fieldID}: EditReportFieldDatePageProps) { +function EditReportFieldDatePage({fieldName, isRequired, onSubmit, fieldValue, fieldKey}: EditReportFieldDatePageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const inputRef = useRef(null); @@ -37,12 +37,12 @@ function EditReportFieldDatePage({fieldName, isRequired, onSubmit, fieldValue, f const validate = useCallback( (value: FormOnyxValues) => { const errors: FormInputErrors = {}; - if (isRequired && value[fieldID].trim() === '') { - errors[fieldID] = 'common.error.fieldRequired'; + if (isRequired && value[fieldKey].trim() === '') { + errors[fieldKey] = 'common.error.fieldRequired'; } return errors; }, - [fieldID, isRequired], + [fieldKey, isRequired], ); return ( @@ -67,8 +67,8 @@ function EditReportFieldDatePage({fieldName, isRequired, onSubmit, fieldValue, f {/* @ts-expect-error TODO: Remove this once DatePicker (https://github.com/Expensify/App/issues/25148) is migrated to TypeScript. */} InputComponent={DatePicker} - inputID={fieldID} - name={fieldID} + inputID={fieldKey} + name={fieldKey} defaultValue={fieldValue} label={fieldName} accessibilityLabel={fieldName} diff --git a/src/pages/EditReportFieldDropdownPage.tsx b/src/pages/EditReportFieldDropdownPage.tsx index 1ad3c766221b..a314120fb0c6 100644 --- a/src/pages/EditReportFieldDropdownPage.tsx +++ b/src/pages/EditReportFieldDropdownPage.tsx @@ -17,8 +17,8 @@ type EditReportFieldDropdownPageComponentProps = { /** Name of the policy report field */ fieldName: string; - /** ID of the policy report field */ - fieldID: string; + /** Key of the policy report field */ + fieldKey: string; /** ID of the policy this report field belongs to */ // eslint-disable-next-line react/no-unused-prop-types @@ -37,12 +37,12 @@ type EditReportFieldDropdownPageOnyxProps = { type EditReportFieldDropdownPageProps = EditReportFieldDropdownPageComponentProps & EditReportFieldDropdownPageOnyxProps; -function EditReportFieldDropdownPage({fieldName, onSubmit, fieldID, fieldValue, fieldOptions, recentlyUsedReportFields}: EditReportFieldDropdownPageProps) { +function EditReportFieldDropdownPage({fieldName, onSubmit, fieldKey, fieldValue, fieldOptions, recentlyUsedReportFields}: EditReportFieldDropdownPageProps) { const [searchValue, setSearchValue] = useState(''); const styles = useThemeStyles(); const {getSafeAreaMargins} = useStyleUtils(); const {translate} = useLocalize(); - const recentlyUsedOptions = useMemo(() => recentlyUsedReportFields?.[fieldID] ?? [], [recentlyUsedReportFields, fieldID]); + const recentlyUsedOptions = useMemo(() => recentlyUsedReportFields?.[fieldKey] ?? [], [recentlyUsedReportFields, fieldKey]); const [headerMessage, setHeaderMessage] = useState(''); const sections = useMemo(() => { @@ -93,7 +93,11 @@ function EditReportFieldDropdownPage({fieldName, onSubmit, fieldID, fieldValue, boldStyle sections={sections} value={searchValue} - onSelectRow={(option: Record) => onSubmit({[fieldID]: option.text})} + onSelectRow={(option: Record) => + onSubmit({ + [fieldKey]: fieldValue === option.text ? '' : option.text, + }) + } onChangeText={setSearchValue} highlightSelectedOptions isRowMultilineSupported diff --git a/src/pages/EditReportFieldPage.tsx b/src/pages/EditReportFieldPage.tsx index 6f9886af4482..8c8376468c0f 100644 --- a/src/pages/EditReportFieldPage.tsx +++ b/src/pages/EditReportFieldPage.tsx @@ -40,8 +40,8 @@ type EditReportFieldPageProps = EditReportFieldPageOnyxProps & { }; function EditReportFieldPage({route, policy, report}: EditReportFieldPageProps) { - const fieldId = `expensify_${route.params.fieldID}`; - const reportField = report?.fieldList?.[fieldId] ?? policy?.fieldList?.[fieldId]; + const fieldKey = ReportUtils.getReportFieldKey(route.params.fieldID); + const reportField = report?.fieldList?.[fieldKey] ?? policy?.fieldList?.[fieldKey]; const isDisabled = ReportUtils.isReportFieldDisabled(report, reportField ?? null, policy); if (!reportField || !report || isDisabled) { @@ -63,11 +63,11 @@ function EditReportFieldPage({route, policy, report}: EditReportFieldPageProps) const isReportFieldTitle = ReportUtils.isReportFieldOfTypeTitle(reportField); const handleReportFieldChange = (form: FormOnyxValues) => { - const value = form[reportField.fieldID] || ''; + const value = form[fieldKey]; if (isReportFieldTitle) { ReportActions.updateReportName(report.reportID, value, report.reportName ?? ''); } else { - ReportActions.updateReportField(report.reportID, {...reportField, value}, reportField); + ReportActions.updateReportField(report.reportID, {...reportField, value: value === '' ? null : value}, reportField); } Navigation.dismissModal(report?.reportID); @@ -79,7 +79,7 @@ function EditReportFieldPage({route, policy, report}: EditReportFieldPageProps) return ( !(value in reportField.disabledOptions))} diff --git a/src/pages/EditReportFieldTextPage.tsx b/src/pages/EditReportFieldTextPage.tsx index 9cda559280a9..1a6cf96fb37a 100644 --- a/src/pages/EditReportFieldTextPage.tsx +++ b/src/pages/EditReportFieldTextPage.tsx @@ -19,8 +19,8 @@ type EditReportFieldTextPageProps = { /** Name of the policy report field */ fieldName: string; - /** ID of the policy report field */ - fieldID: string; + /** Key of the policy report field */ + fieldKey: string; /** Flag to indicate if the field can be left blank */ isRequired: boolean; @@ -29,7 +29,7 @@ type EditReportFieldTextPageProps = { onSubmit: (form: FormOnyxValues) => void; }; -function EditReportFieldTextPage({fieldName, onSubmit, fieldValue, isRequired, fieldID}: EditReportFieldTextPageProps) { +function EditReportFieldTextPage({fieldName, onSubmit, fieldValue, isRequired, fieldKey}: EditReportFieldTextPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const inputRef = useRef(null); @@ -37,12 +37,12 @@ function EditReportFieldTextPage({fieldName, onSubmit, fieldValue, isRequired, f const validate = useCallback( (values: FormOnyxValues) => { const errors: FormInputErrors = {}; - if (isRequired && values[fieldID].trim() === '') { - errors[fieldID] = 'common.error.fieldRequired'; + if (isRequired && values[fieldKey].trim() === '') { + errors[fieldKey] = 'common.error.fieldRequired'; } return errors; }, - [fieldID, isRequired], + [fieldKey, isRequired], ); return ( @@ -66,8 +66,8 @@ function EditReportFieldTextPage({fieldName, onSubmit, fieldValue, isRequired, f Date: Wed, 20 Mar 2024 13:58:12 +0100 Subject: [PATCH 224/245] remove unused layoutListHeight --- src/pages/home/report/ReportActionsView.tsx | 34 +++++++++------------ 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index 1452637800a4..b5df5e78d55a 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -171,7 +171,6 @@ function ReportActionsView({ const network = useNetwork(); const {isSmallScreenWidth, windowHeight} = useWindowDimensions(); const contentListHeight = useRef(0); - const layoutListHeight = useRef(0); const isFocused = useIsFocused(); const prevNetworkRef = useRef(network); const prevAuthTokenType = usePrevious(session?.authTokenType); @@ -365,27 +364,22 @@ function ReportActionsView({ /** * Runs when the FlatList finishes laying out */ - const recordTimeToMeasureItemLayout = useCallback( - (e: LayoutChangeEvent) => { - layoutListHeight.current = e.nativeEvent.layout.height; - - if (didLayout.current) { - return; - } + const recordTimeToMeasureItemLayout = useCallback(() => { + if (didLayout.current) { + return; + } - didLayout.current = true; - Timing.end(CONST.TIMING.SWITCH_REPORT, hasCachedActionOnFirstRender ? CONST.TIMING.WARM : CONST.TIMING.COLD); + didLayout.current = true; + Timing.end(CONST.TIMING.SWITCH_REPORT, hasCachedActionOnFirstRender ? CONST.TIMING.WARM : CONST.TIMING.COLD); - // Capture the init measurement only once not per each chat switch as the value gets overwritten - if (!ReportActionsView.initMeasured) { - Performance.markEnd(CONST.TIMING.REPORT_INITIAL_RENDER); - ReportActionsView.initMeasured = true; - } else { - Performance.markEnd(CONST.TIMING.SWITCH_REPORT); - } - }, - [hasCachedActionOnFirstRender], - ); + // Capture the init measurement only once not per each chat switch as the value gets overwritten + if (!ReportActionsView.initMeasured) { + Performance.markEnd(CONST.TIMING.REPORT_INITIAL_RENDER); + ReportActionsView.initMeasured = true; + } else { + Performance.markEnd(CONST.TIMING.SWITCH_REPORT); + } + }, [hasCachedActionOnFirstRender]); useEffect(() => { // Temporary solution for handling REPORTPREVIEW. More details: https://expensify.slack.com/archives/C035J5C9FAP/p1705417778466539?thread_ts=1705035404.136629&cid=C035J5C9FAP From af7c7f039c9fa3bf0d7d37457baffa81fee72cb6 Mon Sep 17 00:00:00 2001 From: ventura Date: Wed, 20 Mar 2024 05:59:02 -0700 Subject: [PATCH 225/245] resolve image crash --- package-lock.json | 7 +- package.json | 2 +- patches/expo-image+1.10.1+001+applyFill.patch | 112 ------------------ patches/expo-image+1.10.1+002+TintFix.patch | 38 ------ src/components/Lightbox/index.tsx | 2 +- 5 files changed, 6 insertions(+), 155 deletions(-) delete mode 100644 patches/expo-image+1.10.1+001+applyFill.patch delete mode 100644 patches/expo-image+1.10.1+002+TintFix.patch diff --git a/package-lock.json b/package-lock.json index 61d6a27821cd..118999e83814 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,7 +54,7 @@ "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#615f4a8662cd1abea9fdeee4d04847197c5e36ae", "expo": "^50.0.3", "expo-av": "~13.10.4", - "expo-image": "1.10.1", + "expo-image": "1.11.0", "expo-image-manipulator": "11.8.0", "htmlparser2": "^7.2.0", "idb-keyval": "^6.2.1", @@ -27514,8 +27514,9 @@ } }, "node_modules/expo-image": { - "version": "1.10.1", - "license": "MIT", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/expo-image/-/expo-image-1.11.0.tgz", + "integrity": "sha512-CfkSGWIGidxxqzErke16bCS9aefS04qvgjk+T9Nc34LAb3ysBAqCv5hoCnvylHOvi/7jTCC/ouLm5oLLqkDSRQ==", "dependencies": { "@react-native/assets-registry": "~0.73.1" }, diff --git a/package.json b/package.json index 92a6b9cde5e1..3161079012ad 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,7 @@ "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#615f4a8662cd1abea9fdeee4d04847197c5e36ae", "expo": "^50.0.3", "expo-av": "~13.10.4", - "expo-image": "1.10.1", + "expo-image": "1.11.0", "expo-image-manipulator": "11.8.0", "htmlparser2": "^7.2.0", "idb-keyval": "^6.2.1", diff --git a/patches/expo-image+1.10.1+001+applyFill.patch b/patches/expo-image+1.10.1+001+applyFill.patch deleted file mode 100644 index 5f168040d04d..000000000000 --- a/patches/expo-image+1.10.1+001+applyFill.patch +++ /dev/null @@ -1,112 +0,0 @@ -diff --git a/node_modules/expo-image/android/src/main/java/com/caverock/androidsvg/SVGStyler.kt b/node_modules/expo-image/android/src/main/java/com/caverock/androidsvg/SVGStyler.kt -index 619daf2..b58a0df 100644 ---- a/node_modules/expo-image/android/src/main/java/com/caverock/androidsvg/SVGStyler.kt -+++ b/node_modules/expo-image/android/src/main/java/com/caverock/androidsvg/SVGStyler.kt -@@ -1,5 +1,9 @@ - package com.caverock.androidsvg - -+import com.caverock.androidsvg.SVG.SPECIFIED_COLOR -+import com.caverock.androidsvg.SVG.SPECIFIED_FILL -+import com.caverock.androidsvg.SVG.SvgElementBase -+ - internal fun replaceColor(paint: SVG.SvgPaint?, newColor: Int) { - if (paint is SVG.Colour && paint !== SVG.Colour.TRANSPARENT) { - paint.colour = newColor -@@ -19,15 +23,83 @@ internal fun replaceStyles(style: SVG.Style?, newColor: Int) { - replaceColor(style.viewportFill, newColor) - } - --internal fun applyTintColor(element: SVG.SvgObject, newColor: Int) { -- if (element is SVG.SvgElementBase) { -+internal fun hasStyle(element: SvgElementBase): Boolean { -+ if (element.style == null && element.baseStyle == null) { -+ return false -+ } -+ -+ val style = element.style -+ val hasColorInStyle = style != null && -+ ( -+ style.color != null || style.fill != null || style.stroke != null || -+ style.stroke != null || style.stopColor != null || style.solidColor != null -+ ) -+ -+ if (hasColorInStyle) { -+ return true -+ } -+ -+ val baseStyle = element.baseStyle ?: return false -+ return baseStyle.color != null || baseStyle.fill != null || baseStyle.stroke != null || -+ baseStyle.viewportFill != null || baseStyle.stopColor != null || baseStyle.solidColor != null -+} -+ -+internal fun defineStyles(element: SvgElementBase, newColor: Int, hasStyle: Boolean) { -+ if (hasStyle) { -+ return -+ } -+ -+ val style = if (element.style != null) { -+ element.style -+ } else { -+ SVG.Style().also { -+ element.style = it -+ } -+ } -+ -+ val color = SVG.Colour(newColor) -+ when (element) { -+ is SVG.Path, -+ is SVG.Circle, -+ is SVG.Ellipse, -+ is SVG.Rect, -+ is SVG.SolidColor, -+ is SVG.Line, -+ is SVG.Polygon, -+ is SVG.PolyLine -> { -+ style.apply { -+ fill = color -+ -+ specifiedFlags = SPECIFIED_FILL -+ } -+ } -+ -+ is SVG.TextPath -> { -+ style.apply { -+ this.color = color -+ -+ specifiedFlags = SPECIFIED_COLOR -+ } -+ } -+ } -+} -+ -+internal fun applyTintColor(element: SVG.SvgObject, newColor: Int, parentDefinesStyle: Boolean) { -+ val definesStyle = if (element is SvgElementBase) { -+ val hasStyle = parentDefinesStyle || hasStyle(element) -+ - replaceStyles(element.baseStyle, newColor) - replaceStyles(element.style, newColor) -+ defineStyles(element, newColor, hasStyle) -+ -+ hasStyle -+ } else { -+ parentDefinesStyle - } - - if (element is SVG.SvgContainer) { - for (child in element.children) { -- applyTintColor(child, newColor) -+ applyTintColor(child, newColor, definesStyle) - } - } - } -@@ -36,8 +108,9 @@ fun applyTintColor(svg: SVG, newColor: Int) { - val root = svg.rootElement - - replaceStyles(root.style, newColor) -+ val hasStyle = hasStyle(root) - - for (child in root.children) { -- applyTintColor(child, newColor) -+ applyTintColor(child, newColor, hasStyle) - } - } diff --git a/patches/expo-image+1.10.1+002+TintFix.patch b/patches/expo-image+1.10.1+002+TintFix.patch deleted file mode 100644 index 92b56c039b43..000000000000 --- a/patches/expo-image+1.10.1+002+TintFix.patch +++ /dev/null @@ -1,38 +0,0 @@ -diff --git a/node_modules/expo-image/android/src/main/java/com/caverock/androidsvg/SVGStyler.kt b/node_modules/expo-image/android/src/main/java/com/caverock/androidsvg/SVGStyler.kt -index b58a0df..6b8da3c 100644 ---- a/node_modules/expo-image/android/src/main/java/com/caverock/androidsvg/SVGStyler.kt -+++ b/node_modules/expo-image/android/src/main/java/com/caverock/androidsvg/SVGStyler.kt -@@ -107,6 +107,7 @@ internal fun applyTintColor(element: SVG.SvgObject, newColor: Int, parentDefines - fun applyTintColor(svg: SVG, newColor: Int) { - val root = svg.rootElement - -+ replaceStyles(root.baseStyle, newColor) - replaceStyles(root.style, newColor) - val hasStyle = hasStyle(root) - -diff --git a/node_modules/expo-image/android/src/main/java/expo/modules/image/ExpoImageViewWrapper.kt b/node_modules/expo-image/android/src/main/java/expo/modules/image/ExpoImageViewWrapper.kt -index 602b570..8becf72 100644 ---- a/node_modules/expo-image/android/src/main/java/expo/modules/image/ExpoImageViewWrapper.kt -+++ b/node_modules/expo-image/android/src/main/java/expo/modules/image/ExpoImageViewWrapper.kt -@@ -31,6 +31,7 @@ import expo.modules.image.records.ImageLoadEvent - import expo.modules.image.records.ImageProgressEvent - import expo.modules.image.records.ImageTransition - import expo.modules.image.records.SourceMap -+import expo.modules.image.svg.SVGPictureDrawable - import expo.modules.kotlin.AppContext - import expo.modules.kotlin.tracing.beginAsyncTraceBlock - import expo.modules.kotlin.tracing.trace -@@ -127,7 +128,12 @@ class ExpoImageViewWrapper(context: Context, appContext: AppContext) : ExpoView( - internal var tintColor: Int? = null - set(value) { - field = value -- activeView.setTintColor(value) -+ // To apply the tint color to the SVG, we need to recreate the drawable. -+ if (activeView.drawable is SVGPictureDrawable) { -+ shouldRerender = true -+ } else { -+ activeView.setTintColor(value) -+ } - } - - internal var isFocusableProp: Boolean = false diff --git a/src/components/Lightbox/index.tsx b/src/components/Lightbox/index.tsx index 56852a8e2ea1..86a52c2baf6c 100644 --- a/src/components/Lightbox/index.tsx +++ b/src/components/Lightbox/index.tsx @@ -112,7 +112,7 @@ function Lightbox({isAuthTokenRequired = false, uri, onScaleChanged: onScaleChan return; } - setContentSize({width: width * PixelRatio.get(), height: height * PixelRatio.get()}); + setContentSize({width, height}); }, [contentSize, setContentSize], ); From 8ed7a1057e79986f9e945183e6131c8693a1b4b4 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Wed, 20 Mar 2024 18:00:27 +0500 Subject: [PATCH 226/245] remove un-needed changes --- src/libs/Permissions.ts | 2 +- src/libs/ReportUtils.ts | 4 ++-- src/libs/actions/Report.ts | 24 ++++++++++++------------ 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 1b06c1072c1d..26df03134fd5 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -3,7 +3,7 @@ import CONST from '@src/CONST'; import type Beta from '@src/types/onyx/Beta'; function canUseAllBetas(betas: OnyxEntry): boolean { - return true; // !!betas?.includes(CONST.BETAS.ALL); + return !!betas?.includes(CONST.BETAS.ALL); } function canUseChronos(betas: OnyxEntry): boolean { diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 267198f60bce..6aaf75693a6f 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2098,7 +2098,7 @@ function getReportFieldsByPolicyID(policyID: string): Record; + return fieldList; } /** @@ -2123,7 +2123,7 @@ function getAvailableReportFields(report: Report, policyReportFields: PolicyRepo const field = report?.fieldList?.[getReportFieldKey(id)]; if (field) { - return field as PolicyReportField; + return field; } const policyReportField = policyReportFields.find(({fieldID}) => fieldID === id); diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 70eeaba9ec22..97dc3a6319d0 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -1589,8 +1589,8 @@ function updateReportName(reportID: string, value: string, previousValue: string } function updateReportField(reportID: string, reportField: PolicyReportField, previousReportField: PolicyReportField) { - const recentlyUsedValues = allRecentlyUsedReportFields?.[reportField.fieldID] ?? []; - const fieldID = ReportUtils.getReportFieldKey(reportField.fieldID); + const fieldKey = ReportUtils.getReportFieldKey(reportField.fieldID); + const recentlyUsedValues = allRecentlyUsedReportFields?.[fieldKey] ?? []; const optimisticData: OnyxUpdate[] = [ { @@ -1598,10 +1598,10 @@ function updateReportField(reportID: string, reportField: PolicyReportField, pre key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: { fieldList: { - [fieldID]: reportField, + [fieldKey]: reportField, }, pendingFields: { - [fieldID]: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + [fieldKey]: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, }, }, }, @@ -1612,7 +1612,7 @@ function updateReportField(reportID: string, reportField: PolicyReportField, pre onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.RECENTLY_USED_REPORT_FIELDS, value: { - [fieldID]: [...new Set([...recentlyUsedValues, reportField.value])], + [fieldKey]: [...new Set([...recentlyUsedValues, reportField.value])], }, }); } @@ -1623,13 +1623,13 @@ function updateReportField(reportID: string, reportField: PolicyReportField, pre key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: { fieldList: { - [fieldID]: previousReportField, + [fieldKey]: previousReportField, }, pendingFields: { - [fieldID]: null, + [fieldKey]: null, }, errorFields: { - [fieldID]: ErrorUtils.getMicroSecondOnyxError('report.genericUpdateReportFieldFailureMessage'), + [fieldKey]: ErrorUtils.getMicroSecondOnyxError('report.genericUpdateReportFieldFailureMessage'), }, }, }, @@ -1640,7 +1640,7 @@ function updateReportField(reportID: string, reportField: PolicyReportField, pre onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.RECENTLY_USED_REPORT_FIELDS, value: { - [fieldID]: recentlyUsedValues, + [fieldKey]: recentlyUsedValues, }, }); } @@ -1651,10 +1651,10 @@ function updateReportField(reportID: string, reportField: PolicyReportField, pre key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: { pendingFields: { - [fieldID]: null, + [fieldKey]: null, }, errorFields: { - [fieldID]: null, + [fieldKey]: null, }, }, }, @@ -1662,7 +1662,7 @@ function updateReportField(reportID: string, reportField: PolicyReportField, pre const parameters = { reportID, - reportFields: JSON.stringify({[fieldID]: reportField}), + reportFields: JSON.stringify({[fieldKey]: reportField}), }; API.write(WRITE_COMMANDS.SET_REPORT_FIELD, parameters, {optimisticData, failureData, successData}); From 698dc28d4938aaa4a9e5ba702ea2d5fb566d96ca Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Wed, 20 Mar 2024 15:20:11 +0100 Subject: [PATCH 227/245] Limit Custom Tax Name to 8 characters --- src/CONST.ts | 1 + .../workspace/taxes/WorkspaceTaxesSettingsCustomTaxName.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/CONST.ts b/src/CONST.ts index 132b49c16ef7..07650bab31bc 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -358,6 +358,7 @@ const CONST = { NOT_INSTALLED: 'not-installed', }, TAX_RATES: { + CUSTOM_NAME_MAX_LENGTH: 8, NAME_MAX_LENGTH: 50, }, PLATFORM: { diff --git a/src/pages/workspace/taxes/WorkspaceTaxesSettingsCustomTaxName.tsx b/src/pages/workspace/taxes/WorkspaceTaxesSettingsCustomTaxName.tsx index 034ec7e21ade..fe0dc4c4b16f 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesSettingsCustomTaxName.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesSettingsCustomTaxName.tsx @@ -81,7 +81,7 @@ function WorkspaceTaxesSettingsCustomTaxName({ label={translate('workspace.editor.nameInputLabel')} accessibilityLabel={translate('workspace.editor.nameInputLabel')} defaultValue={policy?.taxRates?.name} - maxLength={CONST.TAX_RATES.NAME_MAX_LENGTH} + maxLength={CONST.TAX_RATES.CUSTOM_NAME_MAX_LENGTH} multiline={false} ref={inputCallbackRef} /> From 1d8b72da1b64a400496b4109de17cc06d24f8021 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Wed, 20 Mar 2024 19:22:59 +0500 Subject: [PATCH 228/245] Update src/libs/ReportUtils.ts Co-authored-by: Joel Davies --- 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 6aaf75693a6f..8ae30bb8c957 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2070,7 +2070,7 @@ function isReportFieldDisabled(report: OnyxEntry, reportField: OnyxEntry * Given a set of report fields, return the field of type formula */ function getFormulaTypeReportField(reportFields: Record) { - return Object.values(reportFields).find((field) => field.type === 'formula'); + return Object.values(reportFields).find((field) => field?.type === 'formula'); } /** From 889975b0a53083a17f794101b938c971e3466dbf Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Wed, 20 Mar 2024 15:34:19 +0100 Subject: [PATCH 229/245] Fix lint --- .../workspace/taxes/WorkspaceTaxesSettingsCustomTaxName.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/taxes/WorkspaceTaxesSettingsCustomTaxName.tsx b/src/pages/workspace/taxes/WorkspaceTaxesSettingsCustomTaxName.tsx index 21f9bc741e34..e9e359d9d059 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesSettingsCustomTaxName.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesSettingsCustomTaxName.tsx @@ -61,7 +61,6 @@ function WorkspaceTaxesSettingsCustomTaxName({ policyID={policyID} featureName={CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED} > - {' '} From b0a2e69cd5d02a0c7eb1976d25b08472dc462ff4 Mon Sep 17 00:00:00 2001 From: smelaa Date: Wed, 20 Mar 2024 16:01:54 +0100 Subject: [PATCH 230/245] Address review comments --- src/components/VideoPopoverMenu/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/VideoPopoverMenu/index.tsx b/src/components/VideoPopoverMenu/index.tsx index 2a692779c5a4..aad6364f91df 100644 --- a/src/components/VideoPopoverMenu/index.tsx +++ b/src/components/VideoPopoverMenu/index.tsx @@ -7,13 +7,13 @@ import type {AnchorPosition} from '@styles/index'; type VideoPopoverMenuProps = { /** Whether popover menu is visible. */ - isPopoverVisible: boolean; + isPopoverVisible?: boolean; /** Callback executed to hide popover when an item is selected. */ - hidePopover: (selectedItem?: PopoverMenuItem, index?: number) => void; + hidePopover?: (selectedItem?: PopoverMenuItem, index?: number) => void; /** The horizontal and vertical anchors points for the popover. */ - anchorPosition: AnchorPosition; + anchorPosition?: AnchorPosition; }; function VideoPopoverMenu({ From 82c9fc6721f7cf8337dc4cec76a3eb13a5db4db1 Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Wed, 20 Mar 2024 11:31:30 -0400 Subject: [PATCH 231/245] use strict equality check for html string --- src/libs/ReportActionsUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index adebdf807058..9dfa73b19e1a 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -106,7 +106,7 @@ function isDeletedAction(reportAction: OnyxEntry Date: Wed, 20 Mar 2024 10:42:07 -0600 Subject: [PATCH 232/245] use right api, parse int --- src/libs/API/types.ts | 2 +- .../workflows/WorkspaceAutoReportingFrequencyPage.tsx | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index c04702f38f6a..e6db0a901696 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -10,7 +10,7 @@ type ApiRequest = ValueOf; const WRITE_COMMANDS = { SET_WORKSPACE_AUTO_REPORTING: 'SetWorkspaceAutoReporting', SET_WORKSPACE_AUTO_REPORTING_FREQUENCY: 'SetWorkspaceAutoReportingFrequency', - SET_WORKSPACE_AUTO_REPORTING_MONTHLY_OFFSET: 'UpdatePolicy', + SET_WORKSPACE_AUTO_REPORTING_MONTHLY_OFFSET: 'SetWorkspaceAutoReportingOffset', SET_WORKSPACE_APPROVAL_MODE: 'SetWorkspaceApprovalMode', SET_WORKSPACE_PAYER: 'SetWorkspacePayer', SET_WORKSPACE_REIMBURSEMENT: 'SetWorkspaceReimbursement', diff --git a/src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx b/src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx index 901541a94a85..9f9feb787211 100644 --- a/src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx @@ -79,6 +79,9 @@ function WorkspaceAutoReportingFrequencyPage({policy, route}: WorkspaceAutoRepor if (typeof policy?.autoReportingOffset === 'number') { return toLocaleOrdinal(policy.autoReportingOffset); } + if (typeof policy?.autoReportingOffset === 'string' && parseInt(policy?.autoReportingOffset, 10)) { + return toLocaleOrdinal(parseInt(policy.autoReportingOffset, 10)); + } return translate(`workflowsPage.frequencies.${policy?.autoReportingOffset}`); }; From 4eb2d2fd0ec1e214c160d8f309714cc4886b8f69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Wed, 20 Mar 2024 11:05:55 -0600 Subject: [PATCH 233/245] Add condition to deploy only if it's not a pull request or if it's a pull request from a non-forked repository --- .github/workflows/deployExpensifyHelp.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/deployExpensifyHelp.yml b/.github/workflows/deployExpensifyHelp.yml index 831ec0c0b95e..d4577e112d59 100644 --- a/.github/workflows/deployExpensifyHelp.yml +++ b/.github/workflows/deployExpensifyHelp.yml @@ -46,6 +46,7 @@ jobs: - name: Deploy to Cloudflare Pages uses: cloudflare/pages-action@f0a1cd58cd66095dee69bfa18fa5efd1dde93bca id: deploy + if: github.event_name != 'pull_request' || (github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork) with: apiToken: ${{ secrets.CLOUDFLARE_PAGES_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} From 7a456edcc88dd1bf21bad4a4d7c4f4c24218d384 Mon Sep 17 00:00:00 2001 From: ventura Date: Wed, 20 Mar 2024 10:22:10 -0700 Subject: [PATCH 234/245] added podfile.lock --- ios/Podfile.lock | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index dd2084b238fb..cceb1abb3109 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1144,10 +1144,6 @@ PODS: - React - React-callinvoker - React-Core - - react-native-release-profiler (0.1.6): - - glog - - RCT-Folly (= 2022.05.16.00) - - React-Core - react-native-render-html (6.3.1): - React-Core - react-native-safe-area-context (4.8.2): @@ -1393,8 +1389,6 @@ PODS: - glog - RCT-Folly (= 2022.05.16.00) - React-Core - - RNShare (10.0.2): - - React-Core - RNSound (0.11.2): - React-Core - RNSound/Core (= 0.11.2) @@ -1477,7 +1471,6 @@ DEPENDENCIES: - react-native-performance (from `../node_modules/react-native-performance`) - react-native-plaid-link-sdk (from `../node_modules/react-native-plaid-link-sdk`) - react-native-quick-sqlite (from `../node_modules/react-native-quick-sqlite`) - - react-native-release-profiler (from `../node_modules/react-native-release-profiler`) - react-native-render-html (from `../node_modules/react-native-render-html`) - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) - react-native-view-shot (from `../node_modules/react-native-view-shot`) @@ -1523,7 +1516,6 @@ DEPENDENCIES: - RNReactNativeHapticFeedback (from `../node_modules/react-native-haptic-feedback`) - RNReanimated (from `../node_modules/react-native-reanimated`) - RNScreens (from `../node_modules/react-native-screens`) - - RNShare (from `../node_modules/react-native-share`) - RNSound (from `../node_modules/react-native-sound`) - RNSVG (from `../node_modules/react-native-svg`) - VisionCamera (from `../node_modules/react-native-vision-camera`) @@ -1676,8 +1668,6 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-plaid-link-sdk" react-native-quick-sqlite: :path: "../node_modules/react-native-quick-sqlite" - react-native-release-profiler: - :path: "../node_modules/react-native-release-profiler" react-native-render-html: :path: "../node_modules/react-native-render-html" react-native-safe-area-context: @@ -1768,8 +1758,6 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-reanimated" RNScreens: :path: "../node_modules/react-native-screens" - RNShare: - :path: "../node_modules/react-native-share" RNSound: :path: "../node_modules/react-native-sound" RNSVG: @@ -1865,7 +1853,6 @@ SPEC CHECKSUMS: react-native-performance: cef2b618d47b277fb5c3280b81a3aad1e72f2886 react-native-plaid-link-sdk: df1618a85a615d62ff34e34b76abb7a56497fbc1 react-native-quick-sqlite: bcc7a7a250a40222f18913a97cd356bf82d0a6c4 - react-native-release-profiler: 86f2004d5f8c4fff17d90a5580513519a685d7ae react-native-render-html: 96c979fe7452a0a41559685d2f83b12b93edac8c react-native-safe-area-context: 0ee144a6170530ccc37a0fd9388e28d06f516a89 react-native-view-shot: 6b7ed61d77d88580fed10954d45fad0eb2d47688 @@ -1911,7 +1898,6 @@ SPEC CHECKSUMS: RNReactNativeHapticFeedback: ec56a5f81c3941206fd85625fa669ffc7b4545f9 RNReanimated: 3850671fd0c67051ea8e1e648e8c3e86bf3a28eb RNScreens: b582cb834dc4133307562e930e8fa914b8c04ef2 - RNShare: 859ff710211285676b0bcedd156c12437ea1d564 RNSound: 6c156f925295bdc83e8e422e7d8b38d33bc71852 RNSVG: ba3e7232f45e34b7b47e74472386cf4e1a676d0a SDWebImage: 750adf017a315a280c60fde706ab1e552a3ae4e9 @@ -1921,7 +1907,7 @@ SPEC CHECKSUMS: SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Turf: 13d1a92d969ca0311bbc26e8356cca178ce95da2 VisionCamera: 0a6794d1974aed5d653d0d0cb900493e2583e35a - Yoga: 13c8ef87792450193e117976337b8527b49e8c03 + Yoga: e64aa65de36c0832d04e8c7bd614396c77a80047 PODFILE CHECKSUM: a431c146e1501391834a2f299a74093bac53b530 From 3edceec0c25ce6c3c7761146ac53dc652e406628 Mon Sep 17 00:00:00 2001 From: ventura Date: Wed, 20 Mar 2024 11:24:31 -0700 Subject: [PATCH 235/245] added Podfile.lock --- ios/Podfile.lock | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index cceb1abb3109..310003ee8adc 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -39,7 +39,7 @@ PODS: - React-Core - Expo (50.0.4): - ExpoModulesCore - - ExpoImage (1.10.1): + - ExpoImage (1.11.0): - ExpoModulesCore - SDWebImage (~> 5.17.0) - SDWebImageAVIFCoder (~> 0.10.1) @@ -1144,6 +1144,10 @@ PODS: - React - React-callinvoker - React-Core + - react-native-release-profiler (0.1.6): + - glog + - RCT-Folly (= 2022.05.16.00) + - React-Core - react-native-render-html (6.3.1): - React-Core - react-native-safe-area-context (4.8.2): @@ -1389,6 +1393,8 @@ PODS: - glog - RCT-Folly (= 2022.05.16.00) - React-Core + - RNShare (10.0.2): + - React-Core - RNSound (0.11.2): - React-Core - RNSound/Core (= 0.11.2) @@ -1471,6 +1477,7 @@ DEPENDENCIES: - react-native-performance (from `../node_modules/react-native-performance`) - react-native-plaid-link-sdk (from `../node_modules/react-native-plaid-link-sdk`) - react-native-quick-sqlite (from `../node_modules/react-native-quick-sqlite`) + - react-native-release-profiler (from `../node_modules/react-native-release-profiler`) - react-native-render-html (from `../node_modules/react-native-render-html`) - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) - react-native-view-shot (from `../node_modules/react-native-view-shot`) @@ -1516,6 +1523,7 @@ DEPENDENCIES: - RNReactNativeHapticFeedback (from `../node_modules/react-native-haptic-feedback`) - RNReanimated (from `../node_modules/react-native-reanimated`) - RNScreens (from `../node_modules/react-native-screens`) + - RNShare (from `../node_modules/react-native-share`) - RNSound (from `../node_modules/react-native-sound`) - RNSVG (from `../node_modules/react-native-svg`) - VisionCamera (from `../node_modules/react-native-vision-camera`) @@ -1668,6 +1676,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-plaid-link-sdk" react-native-quick-sqlite: :path: "../node_modules/react-native-quick-sqlite" + react-native-release-profiler: + :path: "../node_modules/react-native-release-profiler" react-native-render-html: :path: "../node_modules/react-native-render-html" react-native-safe-area-context: @@ -1758,6 +1768,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-reanimated" RNScreens: :path: "../node_modules/react-native-screens" + RNShare: + :path: "../node_modules/react-native-share" RNSound: :path: "../node_modules/react-native-sound" RNSVG: @@ -1778,7 +1790,7 @@ SPEC CHECKSUMS: EXAV: 09a4d87fa6b113fbb0ada3aade6799f78271cb44 EXImageLoader: 55080616b2fe9da19ef8c7f706afd9814e279b6b Expo: 1e3bcf9dd99de57a636127057f6b488f0609681a - ExpoImage: 1cdaa65a6a70bb01067e21ad1347ff2d973885f5 + ExpoImage: 390c524542b258f8173f475c1cc71f016444a7be ExpoImageManipulator: c1d7cb865eacd620a35659f3da34c70531f10b59 ExpoModulesCore: 96d1751929ad10622773bb729ab28a8423f0dd0c FBLazyVector: fbc4957d9aa695250b55d879c1d86f79d7e69ab4 @@ -1853,6 +1865,7 @@ SPEC CHECKSUMS: react-native-performance: cef2b618d47b277fb5c3280b81a3aad1e72f2886 react-native-plaid-link-sdk: df1618a85a615d62ff34e34b76abb7a56497fbc1 react-native-quick-sqlite: bcc7a7a250a40222f18913a97cd356bf82d0a6c4 + react-native-release-profiler: 86f2004d5f8c4fff17d90a5580513519a685d7ae react-native-render-html: 96c979fe7452a0a41559685d2f83b12b93edac8c react-native-safe-area-context: 0ee144a6170530ccc37a0fd9388e28d06f516a89 react-native-view-shot: 6b7ed61d77d88580fed10954d45fad0eb2d47688 @@ -1898,6 +1911,7 @@ SPEC CHECKSUMS: RNReactNativeHapticFeedback: ec56a5f81c3941206fd85625fa669ffc7b4545f9 RNReanimated: 3850671fd0c67051ea8e1e648e8c3e86bf3a28eb RNScreens: b582cb834dc4133307562e930e8fa914b8c04ef2 + RNShare: 859ff710211285676b0bcedd156c12437ea1d564 RNSound: 6c156f925295bdc83e8e422e7d8b38d33bc71852 RNSVG: ba3e7232f45e34b7b47e74472386cf4e1a676d0a SDWebImage: 750adf017a315a280c60fde706ab1e552a3ae4e9 From fdac852d7914ae410ad8826b3087f1cb940d06c6 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Wed, 20 Mar 2024 23:54:46 +0500 Subject: [PATCH 236/245] merge fixes --- src/ONYXKEYS.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 64dc809d8220..d74e691fe10e 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -303,7 +303,6 @@ const ONYXKEYS = { POLICY_TAGS: 'policyTags_', POLICY_RECENTLY_USED_TAGS: 'nvp_recentlyUsedTags_', OLD_POLICY_RECENTLY_USED_TAGS: 'policyRecentlyUsedTags_', - POLICY_REPORT_FIELDS: 'policyReportFields_', WORKSPACE_INVITE_MEMBERS_DRAFT: 'workspaceInviteMembersDraft_', WORKSPACE_INVITE_MESSAGE_DRAFT: 'workspaceInviteMessageDraft_', REPORT: 'report_', From acb2b06a5dc2fabf4c53f9c101baeecb1d12fc79 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 20 Mar 2024 20:16:40 +0100 Subject: [PATCH 237/245] move content of usePaginatedReportActionList to ReportActionsView --- src/pages/home/report/ReportActionsView.tsx | 176 +++++++----------- .../report/getInitialPaginationSize/index.ts | 2 +- 2 files changed, 70 insertions(+), 108 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index b5df5e78d55a..970c8c970f8f 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -3,7 +3,6 @@ import {useIsFocused, useRoute} from '@react-navigation/native'; import lodashIsEqual from 'lodash/isEqual'; import React, {useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; import {InteractionManager} from 'react-native'; -import type {LayoutChangeEvent} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import useCopySelectionHelper from '@hooks/useCopySelectionHelper'; @@ -62,95 +61,6 @@ const SPACER = 16; let listIDCount = Math.round(Math.random() * 100); -/** - * usePaginatedReportActionList manages the logic for handling a list of messages with pagination and dynamic loading. - * It determines the part of the message array to display ('visibleReportActions') based on the current linked message, - * and manages pagination through 'handleReportActionPagination' function. - * - * linkedID - ID of the linked message used for initial focus. - * allReportActions - Array of messages. - * fetchNewerReportActions - Function to fetch more messages. - * route - Current route, used to reset states on route change. - * isLoading - Loading state indicator. - * triggerListID - Used to trigger a listID change. - * returns {object} An object containing the sliced message array, the pagination function, - * index of the linked message, and a unique list ID. - */ -function usePaginatedReportActionList( - linkedID: string, - allReportActions: OnyxTypes.ReportAction[], - fetchNewerReportActions: (newestReportAction: OnyxTypes.ReportAction) => void, - route: RouteProp, - isLoading: boolean, - triggerListID: boolean, -) { - // triggerListID is used when navigating to a chat with messages loaded from LHN. Typically, these include thread actions, task actions, etc. Since these messages aren't the latest, we don't maintain their position and instead trigger a recalculation of their positioning in the list. - // we don't set currentReportActionID on initial render as linkedID as it should trigger visibleReportActions after linked message was positioned - const [currentReportActionID, setCurrentReportActionID] = useState(''); - const isFirstLinkedActionRender = useRef(true); - - useLayoutEffect(() => { - setCurrentReportActionID(''); - }, [route]); - - const listID = useMemo(() => { - isFirstLinkedActionRender.current = true; - listIDCount += 1; - return listIDCount; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [route, triggerListID]); - - const index = useMemo(() => { - if (!linkedID || isLoading) { - return -1; - } - - return allReportActions.findIndex((obj) => String(obj.reportActionID) === String(isFirstLinkedActionRender.current ? linkedID : currentReportActionID)); - }, [allReportActions, currentReportActionID, linkedID, isLoading]); - - const visibleReportActions = useMemo(() => { - if (!linkedID) { - return allReportActions; - } - if (isLoading || index === -1) { - return []; - } - - if (isFirstLinkedActionRender.current) { - return allReportActions.slice(index); - } - const paginationSize = getInitialPaginationSize; - return allReportActions.slice(Math.max(index - paginationSize, 0)); - // currentReportActionID is needed to trigger batching once the report action has been positioned - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [linkedID, allReportActions, index, isLoading, currentReportActionID]); - - const hasMoreCached = visibleReportActions.length < allReportActions.length; - const newestReportAction = visibleReportActions?.[0]; - - const handleReportActionPagination = useCallback( - ({firstReportActionID}: {firstReportActionID: string}) => { - // This function is a placeholder as the actual pagination is handled by visibleReportActions - if (!hasMoreCached) { - isFirstLinkedActionRender.current = false; - fetchNewerReportActions(newestReportAction); - } - if (isFirstLinkedActionRender.current) { - isFirstLinkedActionRender.current = false; - } - setCurrentReportActionID(firstReportActionID); - }, - [fetchNewerReportActions, hasMoreCached, newestReportAction], - ); - - return { - visibleReportActions, - loadMoreReportActionsHandler: handleReportActionPagination, - linkedIDIndex: index, - listID, - }; -} - function ReportActionsView({ report, session, @@ -168,6 +78,11 @@ function ReportActionsView({ const didLayout = useRef(false); const didSubscribeToReportTypingEvents = useRef(false); + // triggerListID is used when navigating to a chat with messages loaded from LHN. Typically, these include thread actions, task actions, etc. Since these messages aren't the latest,we don't maintain their position and instead trigger a recalculation of their positioning in the list. + // we don't set currentReportActionID on initial render as linkedID as it should trigger visibleReportActions after linked message was positioned + const [currentReportActionID, setCurrentReportActionID] = useState(''); + const isFirstLinkedActionRender = useRef(true); + const network = useNetwork(); const {isSmallScreenWidth, windowHeight} = useWindowDimensions(); const contentListHeight = useRef(0); @@ -194,19 +109,6 @@ function ReportActionsView({ [isLoadingNewerReportActions, isLoadingInitialReportActions, reportID], ); - const { - visibleReportActions: reportActions, - loadMoreReportActionsHandler, - linkedIDIndex, - listID, - } = usePaginatedReportActionList(reportActionID, allReportActions, fetchNewerAction, route, isLoading, isLoadingInitialReportActions); - const mostRecentIOUReportActionID = useMemo(() => ReportActionsUtils.getMostRecentIOURequestActionID(reportActions), [reportActions]); - const hasCachedActionOnFirstRender = useInitialValue(() => reportActions.length > 0); - const hasNewestReportAction = reportActions[0]?.created === report.lastVisibleActionCreated; - const newestReportAction = useMemo(() => reportActions?.[0], [reportActions]); - const oldestReportAction = useMemo(() => reportActions?.at(-1), [reportActions]); - const hasCreatedAction = oldestReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED; - const isReportFullyVisible = useMemo((): boolean => getIsReportFullyVisible(isFocused), [isFocused]); const openReportIfNecessary = () => { @@ -219,6 +121,66 @@ function ReportActionsView({ Report.openReport(reportID, reportActionID); }; + useLayoutEffect(() => { + setCurrentReportActionID(''); + }, [route]); + + const listID = useMemo(() => { + isFirstLinkedActionRender.current = true; + listIDCount += 1; + return listIDCount; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [route, isLoadingInitialReportActions]); + + const indexOfLinkedAction = useMemo(() => { + if (!reportActionID || isLoading) { + return -1; + } + + return allReportActions.findIndex((obj) => String(obj.reportActionID) === String(isFirstLinkedActionRender.current ? reportActionID : currentReportActionID)); + }, [allReportActions, currentReportActionID, reportActionID, isLoading]); + + const reportActions = useMemo(() => { + if (!reportActionID) { + return allReportActions; + } + if (isLoading || indexOfLinkedAction === -1) { + return []; + } + + if (isFirstLinkedActionRender.current) { + return allReportActions.slice(indexOfLinkedAction); + } + const paginationSize = getInitialPaginationSize; + return allReportActions.slice(Math.max(indexOfLinkedAction - paginationSize, 0)); + // currentReportActionID is needed to trigger batching once the report action has been positioned + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [reportActionID, allReportActions, indexOfLinkedAction, isLoading, currentReportActionID]); + + const hasMoreCached = reportActions.length < allReportActions.length; + const newestReportAction = useMemo(() => reportActions?.[0], [reportActions]); + const handleReportActionPagination = useCallback( + ({firstReportActionID}: {firstReportActionID: string}) => { + // This function is a placeholder as the actual pagination is handled by visibleReportActions + if (!hasMoreCached) { + isFirstLinkedActionRender.current = false; + fetchNewerAction(newestReportAction); + } + if (isFirstLinkedActionRender.current) { + isFirstLinkedActionRender.current = false; + } + setCurrentReportActionID(firstReportActionID); + }, + [fetchNewerAction, hasMoreCached, newestReportAction], + ); + + const mostRecentIOUReportActionID = useMemo(() => ReportActionsUtils.getMostRecentIOURequestActionID(reportActions), [reportActions]); + const hasCachedActionOnFirstRender = useInitialValue(() => reportActions.length > 0); + const hasNewestReportAction = reportActions[0]?.created === report.lastVisibleActionCreated; + + const oldestReportAction = useMemo(() => reportActions?.at(-1), [reportActions]); + const hasCreatedAction = oldestReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED; + useEffect(() => { if (reportActionID) { return; @@ -343,19 +305,19 @@ function ReportActionsView({ const isLoadingOlderReportsFirstNeeded = checkIfContentSmallerThanList() && reportActions.length > 23; if ( - (reportActionID && linkedIDIndex > -1 && !hasNewestReportAction && !isLoadingOlderReportsFirstNeeded) || + (reportActionID && indexOfLinkedAction > -1 && !hasNewestReportAction && !isLoadingOlderReportsFirstNeeded) || (!reportActionID && !hasNewestReportAction && !isLoadingOlderReportsFirstNeeded) ) { - loadMoreReportActionsHandler({firstReportActionID: newestReportAction?.reportActionID}); + handleReportActionPagination({firstReportActionID: newestReportAction?.reportActionID}); } }, [ isLoadingInitialReportActions, isLoadingOlderReportActions, checkIfContentSmallerThanList, reportActionID, - linkedIDIndex, + indexOfLinkedAction, hasNewestReportAction, - loadMoreReportActionsHandler, + handleReportActionPagination, network.isOffline, reportActions.length, newestReportAction, diff --git a/src/pages/home/report/getInitialPaginationSize/index.ts b/src/pages/home/report/getInitialPaginationSize/index.ts index cc53d086b7b8..87ec6856aa20 100644 --- a/src/pages/home/report/getInitialPaginationSize/index.ts +++ b/src/pages/home/report/getInitialPaginationSize/index.ts @@ -1,3 +1,3 @@ import CONST from '@src/CONST'; -export default CONST.WEB_PAGINATION_SIZE +export default CONST.WEB_PAGINATION_SIZE; From 6ad4da1d84e35011040c38e66195c3ec03463c77 Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 21 Mar 2024 02:31:54 +0700 Subject: [PATCH 238/245] fix: infinite loading after denying camera access --- .../request/step/IOURequestStepScan/index.js | 62 ++++++++++--------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.js b/src/pages/iou/request/step/IOURequestStepScan/index.js index ac8ef2867489..6bf517c30eb0 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.js @@ -93,38 +93,42 @@ function IOURequestStepScan({ return; } - navigator.mediaDevices.getUserMedia({video: {facingMode: {exact: 'environment'}, zoom: {ideal: 1}}}).then((stream) => { - _.forEach(stream.getTracks(), (track) => track.stop()); - // Only Safari 17+ supports zoom constraint - if (Browser.isMobileSafari() && stream.getTracks().length > 0) { - const deviceId = _.chain(stream.getTracks()) - .map((track) => track.getSettings()) - .find((setting) => setting.zoom === 1) - .get('deviceId') - .value(); - if (deviceId) { - setVideoConstraints({deviceId}); - return; + const defaultConstraints = {facingMode: {exact: 'environment'}}; + navigator.mediaDevices + .getUserMedia({video: {facingMode: {exact: 'environment'}, zoom: {ideal: 1}}}) + .then((stream) => { + _.forEach(stream.getTracks(), (track) => track.stop()); + // Only Safari 17+ supports zoom constraint + if (Browser.isMobileSafari() && stream.getTracks().length > 0) { + const deviceId = _.chain(stream.getTracks()) + .map((track) => track.getSettings()) + .find((setting) => setting.zoom === 1) + .get('deviceId') + .value(); + if (deviceId) { + setVideoConstraints({deviceId}); + return; + } } - } - 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'}}); + if (!navigator.mediaDevices.enumerateDevices) { + setVideoConstraints(defaultConstraints); 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(defaultConstraints); + return; + } + setVideoConstraints({deviceId: lastBackDeviceId}); + }); + }) + .catch(() => setVideoConstraints(defaultConstraints)); // eslint-disable-next-line react-hooks/exhaustive-deps }, [isTabActive]); From cb86c85f08a5d56a027860c5364bae68533253cf Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 20 Mar 2024 21:17:28 +0100 Subject: [PATCH 239/245] update listID --- src/libs/NumberUtils.ts | 10 +++++++++- src/libs/ReportActionsUtils.ts | 6 ------ src/pages/home/report/ReportActionsView.tsx | 8 +++++--- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/libs/NumberUtils.ts b/src/libs/NumberUtils.ts index 62d6fa00906a..2dfc1e722c58 100644 --- a/src/libs/NumberUtils.ts +++ b/src/libs/NumberUtils.ts @@ -92,4 +92,12 @@ function clamp(value: number, min: number, max: number): number { return Math.min(Math.max(value, min), max); } -export {rand64, generateHexadecimalValue, generateRandomInt, parseFloatAnyLocale, roundDownToLargestMultiple, roundToTwoDecimalPlaces, clamp}; +function generateNewRandomInt(old: number, min: number, max: number): number { + let newNum = old; + while (newNum === old) { + newNum = generateRandomInt(min, max); + } + return newNum; +} + +export {rand64, generateHexadecimalValue, generateRandomInt, parseFloatAnyLocale, roundDownToLargestMultiple, roundToTwoDecimalPlaces, clamp, generateNewRandomInt}; diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 8678d10d3f89..f069bcfa58ae 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -1,16 +1,11 @@ -import type {RouteProp} from '@react-navigation/native'; import fastMerge from 'expensify-common/lib/fastMerge'; import _ from 'lodash'; import lodashFindLast from 'lodash/findLast'; -import {useCallback, useLayoutEffect, useMemo, useRef, useState} from 'react'; import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; -import getInitialPaginationSize from '@pages/home/report/getInitialPaginationSize'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type SCREENS from '@src/SCREENS'; -import type * as OnyxTypes from '@src/types/onyx'; import type { ActionName, ChangeLog, @@ -31,7 +26,6 @@ import isReportMessageAttachment from './isReportMessageAttachment'; import * as Localize from './Localize'; import Log from './Log'; import type {MessageElementBase, MessageTextElement} from './MessageElement'; -import type {CentralPaneNavigatorParamList} from './Navigation/types'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import type {OptimisticIOUReportAction} from './ReportUtils'; diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index 970c8c970f8f..520a9a3604c5 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -12,6 +12,7 @@ import usePrevious from '@hooks/usePrevious'; import useWindowDimensions from '@hooks/useWindowDimensions'; import getIsReportFullyVisible from '@libs/getIsReportFullyVisible'; import type {CentralPaneNavigatorParamList} from '@libs/Navigation/types'; +import {generateNewRandomInt} from '@libs/NumberUtils'; import Performance from '@libs/Performance'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import {isUserCreatedPolicyRoom} from '@libs/ReportUtils'; @@ -59,7 +60,7 @@ type ReportActionsViewProps = ReportActionsViewOnyxProps & { const DIFF_BETWEEN_SCREEN_HEIGHT_AND_LIST = 120; const SPACER = 16; -let listIDCount = Math.round(Math.random() * 100); +let listOldID = Math.round(Math.random() * 100); function ReportActionsView({ report, @@ -127,8 +128,9 @@ function ReportActionsView({ const listID = useMemo(() => { isFirstLinkedActionRender.current = true; - listIDCount += 1; - return listIDCount; + const newID = generateNewRandomInt(listOldID, 1, Number.MAX_SAFE_INTEGER); + listOldID = newID; + return newID; // eslint-disable-next-line react-hooks/exhaustive-deps }, [route, isLoadingInitialReportActions]); From 1c62138109251d97a6ff5644d1c2f9d4bbc663fa Mon Sep 17 00:00:00 2001 From: James Dean Date: Wed, 20 Mar 2024 15:16:47 -0700 Subject: [PATCH 240/245] Update Introducing-Expensify-Chat.md --- docs/articles/new-expensify/chat/Introducing-Expensify-Chat.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/articles/new-expensify/chat/Introducing-Expensify-Chat.md b/docs/articles/new-expensify/chat/Introducing-Expensify-Chat.md index c7ae49e02292..096a3d1527be 100644 --- a/docs/articles/new-expensify/chat/Introducing-Expensify-Chat.md +++ b/docs/articles/new-expensify/chat/Introducing-Expensify-Chat.md @@ -12,7 +12,7 @@ For a quick snapshot of how Expensify Chat works, and New Expensify in general, # What’s Expensify Chat? -Expensify Chat is an instant messaging and payment platform. You can manage all your payments, wether for business or personal, and discuss the transactions themselves. +Expensify Chat is an instant messaging and payment platform. You can manage all your payments, whether for business or personal, and discuss the transactions themselves. With Expensify Chat, you can start a conversation about that missing receipt your employee forgot to submit or chat about splitting that electric bill with your roommates. Expensify makes sending and receiving money as easy as sending and receiving messages. Chat with anyone directly, in groups, or in rooms. From 8844d6ca99ad119ce62e376009ff5344d45876a2 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 21 Mar 2024 10:26:11 +0100 Subject: [PATCH 241/245] wrap VideoPlayer with forwardRef --- src/components/VideoPlayer/index.js | 18 ++++++++-------- src/components/VideoPlayer/index.native.js | 24 ++++++++++------------ 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/src/components/VideoPlayer/index.js b/src/components/VideoPlayer/index.js index c51e9bd680e1..997767a64f8f 100644 --- a/src/components/VideoPlayer/index.js +++ b/src/components/VideoPlayer/index.js @@ -2,18 +2,16 @@ import React, {forwardRef} from 'react'; import BaseVideoPlayer from './BaseVideoPlayer'; import {videoPlayerDefaultProps, videoPlayerPropTypes} from './propTypes'; -function VideoPlayer(props, ref) { - return ( - - ); -} +const VideoPlayer = forwardRef((props, ref) => ( + +)); VideoPlayer.displayName = 'VideoPlayer'; VideoPlayer.propTypes = videoPlayerPropTypes; VideoPlayer.defaultProps = videoPlayerDefaultProps; -export default forwardRef(VideoPlayer); +export default VideoPlayer; diff --git a/src/components/VideoPlayer/index.native.js b/src/components/VideoPlayer/index.native.js index 6581ff01cb75..1b8dda0bc136 100644 --- a/src/components/VideoPlayer/index.native.js +++ b/src/components/VideoPlayer/index.native.js @@ -3,21 +3,19 @@ import CONST from '@src/CONST'; import BaseVideoPlayer from './BaseVideoPlayer'; import {videoPlayerDefaultProps, videoPlayerPropTypes} from './propTypes'; -function VideoPlayer({videoControlsStyle, ...props}, ref) { - return ( - - ); -} +const VideoPlayer = forwardRef(({videoControlsStyle, ...props}, ref) => ( + +)); VideoPlayer.displayName = 'VideoPlayer'; VideoPlayer.propTypes = videoPlayerPropTypes; VideoPlayer.defaultProps = videoPlayerDefaultProps; -export default forwardRef(VideoPlayer); +export default VideoPlayer; From d149231d5fbbcf4732c9e18a89bae58cb84ae777 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 21 Mar 2024 11:02:30 +0100 Subject: [PATCH 242/245] undo wrapping --- src/components/VideoPlayer/index.js | 18 ++++++++-------- src/components/VideoPlayer/index.native.js | 24 ++++++++++++---------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/components/VideoPlayer/index.js b/src/components/VideoPlayer/index.js index 997767a64f8f..a46afa70a3d2 100644 --- a/src/components/VideoPlayer/index.js +++ b/src/components/VideoPlayer/index.js @@ -2,16 +2,18 @@ import React, {forwardRef} from 'react'; import BaseVideoPlayer from './BaseVideoPlayer'; import {videoPlayerDefaultProps, videoPlayerPropTypes} from './propTypes'; -const VideoPlayer = forwardRef((props, ref) => ( - -)); +function VideoPlayer(props, ref) { + return ( + + ); +} VideoPlayer.displayName = 'VideoPlayer'; VideoPlayer.propTypes = videoPlayerPropTypes; VideoPlayer.defaultProps = videoPlayerDefaultProps; -export default VideoPlayer; +export default forwardRef(VideoPlayer); \ No newline at end of file diff --git a/src/components/VideoPlayer/index.native.js b/src/components/VideoPlayer/index.native.js index 1b8dda0bc136..6581ff01cb75 100644 --- a/src/components/VideoPlayer/index.native.js +++ b/src/components/VideoPlayer/index.native.js @@ -3,19 +3,21 @@ import CONST from '@src/CONST'; import BaseVideoPlayer from './BaseVideoPlayer'; import {videoPlayerDefaultProps, videoPlayerPropTypes} from './propTypes'; -const VideoPlayer = forwardRef(({videoControlsStyle, ...props}, ref) => ( - -)); +function VideoPlayer({videoControlsStyle, ...props}, ref) { + return ( + + ); +} VideoPlayer.displayName = 'VideoPlayer'; VideoPlayer.propTypes = videoPlayerPropTypes; VideoPlayer.defaultProps = videoPlayerDefaultProps; -export default VideoPlayer; +export default forwardRef(VideoPlayer); From 9e0348aa3633779a07a8ed525a62452e09b309be Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 21 Mar 2024 11:03:34 +0100 Subject: [PATCH 243/245] fix previousReportActionID for mock reportActions --- tests/utils/ReportTestUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/utils/ReportTestUtils.ts b/tests/utils/ReportTestUtils.ts index 4a4ce89d0296..0fd54a1aa0ac 100644 --- a/tests/utils/ReportTestUtils.ts +++ b/tests/utils/ReportTestUtils.ts @@ -39,7 +39,7 @@ const getFakeReportAction = (index: number, actionName?: ActionName): ReportActi text: 'email@test.com', }, ], - previousReportActionID: '1', + previousReportActionID: (index === 0 ? 0 : index - 1).toString(), reportActionID: index.toString(), reportActionTimestamp: 1696243169753, sequenceNumber: 0, From 19a6229321cb456cadd38c68efc0a968138e5978 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 21 Mar 2024 13:00:59 +0100 Subject: [PATCH 244/245] fix previousReportActionID for createRandomReportAction, fix getFakeReportAction --- src/components/VideoPlayer/index.js | 2 +- tests/utils/LHNTestUtils.tsx | 7 +++++-- tests/utils/ReportTestUtils.ts | 9 +++++++-- tests/utils/collections/reportActions.ts | 2 +- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/components/VideoPlayer/index.js b/src/components/VideoPlayer/index.js index a46afa70a3d2..c51e9bd680e1 100644 --- a/src/components/VideoPlayer/index.js +++ b/src/components/VideoPlayer/index.js @@ -16,4 +16,4 @@ VideoPlayer.displayName = 'VideoPlayer'; VideoPlayer.propTypes = videoPlayerPropTypes; VideoPlayer.defaultProps = videoPlayerDefaultProps; -export default forwardRef(VideoPlayer); \ No newline at end of file +export default forwardRef(VideoPlayer); diff --git a/tests/utils/LHNTestUtils.tsx b/tests/utils/LHNTestUtils.tsx index 85c2d67f80bc..44bfcd46d399 100644 --- a/tests/utils/LHNTestUtils.tsx +++ b/tests/utils/LHNTestUtils.tsx @@ -142,11 +142,14 @@ function getFakeReport(participantAccountIDs = [1, 2], millisecondsInThePast = 0 function getFakeReportAction(actor = 'email1@test.com', millisecondsInThePast = 0): ReportAction { const timestamp = Date.now() - millisecondsInThePast; const created = DateUtils.getDBTime(timestamp); + const previousReportActionID = lastFakeReportActionID; + const reportActionID = ++lastFakeReportActionID; return { actor, actorAccountID: 1, - reportActionID: `${++lastFakeReportActionID}`, + reportActionID: `${reportActionID}`, + previousReportActionID: `${previousReportActionID}`, actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, shouldShow: true, created, @@ -183,7 +186,7 @@ function getFakeReportAction(actor = 'email1@test.com', millisecondsInThePast = }, ], originalMessage: { - childReportID: `${++lastFakeReportActionID}`, + childReportID: `${reportActionID}`, emojiReactions: { heart: { createdAt: '2023-08-28 15:27:52', diff --git a/tests/utils/ReportTestUtils.ts b/tests/utils/ReportTestUtils.ts index 0fd54a1aa0ac..59964cc5c7ab 100644 --- a/tests/utils/ReportTestUtils.ts +++ b/tests/utils/ReportTestUtils.ts @@ -39,8 +39,8 @@ const getFakeReportAction = (index: number, actionName?: ActionName): ReportActi text: 'email@test.com', }, ], - previousReportActionID: (index === 0 ? 0 : index - 1).toString(), reportActionID: index.toString(), + previousReportActionID: (index === 0 ? 0 : index - 1).toString(), reportActionTimestamp: 1696243169753, sequenceNumber: 0, shouldShow: true, @@ -48,7 +48,11 @@ const getFakeReportAction = (index: number, actionName?: ActionName): ReportActi whisperedToAccountIDs: [], } as ReportAction); -const getMockedSortedReportActions = (length = 100): ReportAction[] => Array.from({length}, (element, index): ReportAction => getFakeReportAction(index)); +const getMockedSortedReportActions = (length = 100): ReportAction[] => + Array.from({length}, (element, index): ReportAction => { + const actionName: ActionName = index === 0 ? 'CREATED' : 'ADDCOMMENT'; + return getFakeReportAction(index + 1, actionName); + }).reverse(); const getMockedReportActionsMap = (length = 100): ReportActions => { const mockReports: ReportActions[] = Array.from({length}, (element, index): ReportActions => { @@ -57,6 +61,7 @@ const getMockedReportActionsMap = (length = 100): ReportActions => { const reportAction = { ...createRandomReportAction(reportID), actionName, + previousReportActionID: index.toString(), originalMessage: { linkedReportID: reportID.toString(), }, diff --git a/tests/utils/collections/reportActions.ts b/tests/utils/collections/reportActions.ts index dcfa896f1ae4..f19e939083d2 100644 --- a/tests/utils/collections/reportActions.ts +++ b/tests/utils/collections/reportActions.ts @@ -33,7 +33,7 @@ export default function createRandomReportAction(index: number): ReportAction { // eslint-disable-next-line @typescript-eslint/no-explicit-any actionName: rand(flattenActionNamesValues(CONST.REPORT.ACTIONS.TYPE)) as any, reportActionID: index.toString(), - previousReportActionID: index.toString(), + previousReportActionID: (index === 0 ? 0 : index - 1).toString(), actorAccountID: index, person: [ { From 3d5b6b3c5e0675b71b2cc0c5859edeae7007a64e Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 21 Mar 2024 13:23:05 +0100 Subject: [PATCH 245/245] trigger test --- tests/utils/ReportTestUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/utils/ReportTestUtils.ts b/tests/utils/ReportTestUtils.ts index 59964cc5c7ab..3948baca3113 100644 --- a/tests/utils/ReportTestUtils.ts +++ b/tests/utils/ReportTestUtils.ts @@ -61,10 +61,10 @@ const getMockedReportActionsMap = (length = 100): ReportActions => { const reportAction = { ...createRandomReportAction(reportID), actionName, - previousReportActionID: index.toString(), originalMessage: { linkedReportID: reportID.toString(), }, + previousReportActionID: index.toString(), } as ReportAction; return {[reportID]: reportAction};