From 5258cff5cf967d8fa06b3b29d29195a63d64b0f6 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 29 Sep 2023 18:50:57 +0200 Subject: [PATCH 001/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] 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/484] bring back 'adjust gap handling in response to REPORTPREVIEW movement' --- src/pages/home/report/ReportActionsView.js | 28 ++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index e61ebf0f11f4..3fd1fb44be20 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -388,6 +388,34 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { [hasCachedActions], ); + useEffect(() => { + // Temporary solution for handling REPORTPREVIEW. More details: https://expensify.slack.com/archives/C035J5C9FAP/p1705417778466539?thread_ts=1705035404.136629&cid=C035J5C9FAP + // This code should be removed once REPORTPREVIEW is no longer repositioned. + // We need to call openReport for gaps created by moving REPORTPREVIEW, which causes mismatches in previousReportActionID and reportActionID of adjacent reportActions. The server returns the correct sequence, allowing us to overwrite incorrect data with the correct one. + + const shouldOpenReport = + !hasCreatedAction && + props.isReadyForCommentLinking && + reportActions.length < 24 && + reportActions.length > 1 && + !props.isLoadingInitialReportAction && + !props.isLoadingOlderReportActions && + !props.isLoadingNewerReportActions; + + if (shouldOpenReport) { + Report.openReport(reportID, reportActionID); + } + }, [ + hasCreatedAction, + reportID, + reportActions, + reportActionID, + props.isReadyForCommentLinking, + props.isLoadingOlderReportActions, + props.isLoadingNewerReportActions, + props.isLoadingInitialReportAction, + ]); + // Comments have not loaded at all yet do nothing if (!_.size(reportActions)) { return null; From d95ba1dc8ceee16866056e5f806e308aa0cb9460 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 19 Jan 2024 11:43:06 +0700 Subject: [PATCH 094/484] fix: Inconsistency of flashlight/torch behavior in Scan tab --- .../NavigationAwareCamera/index.js | 47 +------------------ .../request/step/IOURequestStepScan/index.js | 30 ++++++++++-- 2 files changed, 28 insertions(+), 49 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/index.js b/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/index.js index 10b16da13b6e..37223915f4a2 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/index.js +++ b/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/index.js @@ -1,61 +1,20 @@ import PropTypes from 'prop-types'; -import React, {useEffect, useRef} from 'react'; +import React from 'react'; import {View} from 'react-native'; import Webcam from 'react-webcam'; import useTabNavigatorFocus from '@hooks/useTabNavigatorFocus'; const propTypes = { - /** Flag to turn on/off the torch/flashlight - if available */ - torchOn: PropTypes.bool, - /** The index of the tab that contains this camera */ cameraTabIndex: PropTypes.number.isRequired, - - /** Callback function when media stream becomes available - user granted camera permissions and camera starts to work */ - onUserMedia: PropTypes.func, - - /** Callback function passing torch/flashlight capability as bool param of the browser */ - onTorchAvailability: PropTypes.func, -}; - -const defaultProps = { - onUserMedia: undefined, - onTorchAvailability: undefined, - torchOn: false, }; // Wraps a camera that will only be active when the tab is focused or as soon as it starts to become focused. -const NavigationAwareCamera = React.forwardRef(({torchOn, onTorchAvailability, cameraTabIndex, ...props}, ref) => { - const trackRef = useRef(null); +const NavigationAwareCamera = React.forwardRef(({cameraTabIndex, ...props}, ref) => { const shouldShowCamera = useTabNavigatorFocus({ tabIndex: cameraTabIndex, }); - const handleOnUserMedia = (stream) => { - if (props.onUserMedia) { - props.onUserMedia(stream); - } - - const [track] = stream.getVideoTracks(); - const capabilities = track.getCapabilities(); - if (capabilities.torch) { - trackRef.current = track; - } - if (onTorchAvailability) { - onTorchAvailability(!!capabilities.torch); - } - }; - - useEffect(() => { - if (!trackRef.current) { - return; - } - - trackRef.current.applyConstraints({ - advanced: [{torch: torchOn}], - }); - }, [torchOn]); - if (!shouldShowCamera) { return null; } @@ -67,7 +26,6 @@ const NavigationAwareCamera = React.forwardRef(({torchOn, onTorchAvailability, c // eslint-disable-next-line react/jsx-props-no-spreading {...props} ref={ref} - onUserMedia={handleOnUserMedia} /> ); @@ -75,6 +33,5 @@ const NavigationAwareCamera = React.forwardRef(({torchOn, onTorchAvailability, c NavigationAwareCamera.propTypes = propTypes; NavigationAwareCamera.displayName = 'NavigationAwareCamera'; -NavigationAwareCamera.defaultProps = defaultProps; export default NavigationAwareCamera; diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.js b/src/pages/iou/request/step/IOURequestStepScan/index.js index 7c6efca4a32f..c2e9882d5288 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.js @@ -74,6 +74,7 @@ function IOURequestStepScan({ const [isFlashLightOn, toggleFlashlight] = useReducer((state) => !state, false); const [isTorchAvailable, setIsTorchAvailable] = useState(false); const cameraRef = useRef(null); + const trackRef = useRef(null); const hideRecieptModal = () => { setIsAttachmentInvalid(false); @@ -162,11 +163,34 @@ function IOURequestStepScan({ navigateToConfirmationStep(); }; + const handleOnUserMedia = (stream) => { + setCameraPermissionState('granted'); + + const [track] = stream.getVideoTracks(); + const capabilities = track.getCapabilities(); + if (capabilities.torch) { + trackRef.current = track; + } + setIsTorchAvailable(!!capabilities.torch); + }; + const capturePhoto = useCallback(() => { if (!cameraRef.current.getScreenshot) { return; } + if (trackRef.current && isFlashLightOn) { + trackRef.current.applyConstraints({ + advanced: [{torch: true}], + }); + } const imageBase64 = cameraRef.current.getScreenshot(); + + if (trackRef.current && isFlashLightOn) { + trackRef.current.applyConstraints({ + advanced: [{torch: false}], + }); + } + const filename = `receipt_${Date.now()}.png`; const file = FileUtils.base64ToFile(imageBase64, filename); const source = URL.createObjectURL(file); @@ -178,7 +202,7 @@ function IOURequestStepScan({ } navigateToConfirmationStep(); - }, [cameraRef, action, transactionID, updateScanAndNavigate, navigateToConfirmationStep]); + }, [cameraRef, action, transactionID, updateScanAndNavigate, navigateToConfirmationStep, isFlashLightOn]); const panResponder = useRef( PanResponder.create({ @@ -209,14 +233,12 @@ function IOURequestStepScan({ )} setCameraPermissionState('granted')} + onUserMedia={handleOnUserMedia} onUserMediaError={() => setCameraPermissionState('denied')} style={{...styles.videoContainer, display: cameraPermissionState !== 'granted' ? 'none' : 'block'}} ref={cameraRef} screenshotFormat="image/png" videoConstraints={{facingMode: {exact: 'environment'}}} - torchOn={isFlashLightOn} - onTorchAvailability={setIsTorchAvailable} forceScreenshotSourceSize cameraTabIndex={1} /> From 825a5439fad4d931ac32fb9d9a4b4c4331e3528b Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 19 Jan 2024 16:54:20 +0100 Subject: [PATCH 095/484] 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 096/484] 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 097/484] 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 098/484] refactor autosScrollToTopThreshold --- .../InvertedFlatList/BaseInvertedFlatList.tsx | 8 +++---- src/pages/home/report/ReportActionsList.js | 4 ++-- src/pages/home/report/ReportActionsView.js | 24 +++++++++++++++---- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx index ebb4d01d1f23..e007f63c8e97 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx @@ -4,13 +4,13 @@ import type {FlatListProps, ScrollViewProps} from 'react-native'; import FlatList from '@components/FlatList'; type BaseInvertedFlatListProps = FlatListProps & { - shouldEnableAutoscrollToTopThreshold?: boolean; + shouldEnableAutoScrollToTopThreshold?: boolean; }; const AUTOSCROLL_TO_TOP_THRESHOLD = 128; function BaseInvertedFlatList(props: BaseInvertedFlatListProps, ref: ForwardedRef) { - const {shouldEnableAutoscrollToTopThreshold, ...rest} = props; + const {shouldEnableAutoScrollToTopThreshold, ...rest} = props; const maintainVisibleContentPosition = useMemo(() => { const config: ScrollViewProps['maintainVisibleContentPosition'] = { @@ -18,12 +18,12 @@ function BaseInvertedFlatList(props: BaseInvertedFlatListProps, ref: Forwa minIndexForVisible: 1, }; - if (shouldEnableAutoscrollToTopThreshold) { + if (shouldEnableAutoScrollToTopThreshold) { config.autoscrollToTopThreshold = AUTOSCROLL_TO_TOP_THRESHOLD; } return config; - }, [shouldEnableAutoscrollToTopThreshold]); + }, [shouldEnableAutoScrollToTopThreshold]); return ( {}} extraData={extraData} key={listID} - shouldEnableAutoscrollToTopThreshold={shouldEnableAutoscrollToTopThreshold} + shouldEnableAutoScrollToTopThreshold={shouldEnableAutoScrollToTopThreshold} /> diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index a08a202f845a..00584859c4f9 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -2,6 +2,7 @@ import {useIsFocused, useRoute} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; +import {InteractionManager} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import networkPropTypes from '@components/networkPropTypes'; @@ -184,7 +185,7 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { const prevNetworkRef = useRef(props.network); const prevAuthTokenType = usePrevious(props.session.authTokenType); - + const [isInitialLinkedView, setIsInitialLinkedView] = useState(false); const prevIsSmallScreenWidthRef = useRef(props.isSmallScreenWidth); const reportID = props.report.reportID; const isLoading = (!!reportActionID && props.isLoadingInitialReportActions) || !props.isReadyForCommentLinking; @@ -393,13 +394,12 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { // Temporary solution for handling REPORTPREVIEW. More details: https://expensify.slack.com/archives/C035J5C9FAP/p1705417778466539?thread_ts=1705035404.136629&cid=C035J5C9FAP // This code should be removed once REPORTPREVIEW is no longer repositioned. // We need to call openReport for gaps created by moving REPORTPREVIEW, which causes mismatches in previousReportActionID and reportActionID of adjacent reportActions. The server returns the correct sequence, allowing us to overwrite incorrect data with the correct one. - const shouldOpenReport = firstReportActionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && !hasCreatedAction && props.isReadyForCommentLinking && reportActions.length < 24 && - reportActions.length > 1 && + reportActions.length >= 1 && !props.isLoadingInitialReportAction && !props.isLoadingOlderReportActions && !props.isLoadingNewerReportActions; @@ -419,10 +419,26 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { props.isLoadingInitialReportAction, ]); + // Check if the first report action in the list is the one we're currently linked to + const isTheFirstReportActionIsLinked = firstReportActionID !== reportActionID; + + useEffect(() => { + if (isTheFirstReportActionIsLinked) { + // this should be applied after we navigated to linked reportAction + InteractionManager.runAfterInteractions(() => { + setIsInitialLinkedView(true); + }); + } else { + setIsInitialLinkedView(false); + } + }, [isTheFirstReportActionIsLinked]); + // Comments have not loaded at all yet do nothing if (!_.size(reportActions)) { return null; } + // AutoScroll is disabled when we do linking to a specific reportAction + const shouldEnableAutoScroll = hasNewestReportAction && (!reportActionID || isInitialLinkedView); return ( <> @@ -440,7 +456,7 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { policy={props.policy} listID={listID} onContentSizeChange={onContentSizeChange} - shouldEnableAutoscrollToTopThreshold={hasNewestReportAction && !reportActionID} + shouldEnableAutoScrollToTopThreshold={shouldEnableAutoScroll} /> From c08d9057304624e415ec9fd006e6c6df3c75aa94 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 22 Jan 2024 15:35:55 +0700 Subject: [PATCH 099/484] add settimeout --- src/CONST.ts | 1 + .../request/step/IOURequestStepScan/index.js | 39 +++++++++++-------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 0b10e5767328..264810572030 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -47,6 +47,7 @@ const CONST = { OUT: 'out', }, ARROW_HIDE_DELAY: 3000, + TORCH_EFFECT: 1000, API_ATTACHMENT_VALIDATIONS: { // 24 megabytes in bytes, this is limit set on servers, do not update without wider internal discussion diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.js b/src/pages/iou/request/step/IOURequestStepScan/index.js index c2e9882d5288..035db5dbb5a1 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.js @@ -174,23 +174,9 @@ function IOURequestStepScan({ setIsTorchAvailable(!!capabilities.torch); }; - const capturePhoto = useCallback(() => { - if (!cameraRef.current.getScreenshot) { - return; - } - if (trackRef.current && isFlashLightOn) { - trackRef.current.applyConstraints({ - advanced: [{torch: true}], - }); - } + const getScreenshot = useCallback(() => { const imageBase64 = cameraRef.current.getScreenshot(); - if (trackRef.current && isFlashLightOn) { - trackRef.current.applyConstraints({ - advanced: [{torch: false}], - }); - } - const filename = `receipt_${Date.now()}.png`; const file = FileUtils.base64ToFile(imageBase64, filename); const source = URL.createObjectURL(file); @@ -202,7 +188,28 @@ function IOURequestStepScan({ } navigateToConfirmationStep(); - }, [cameraRef, action, transactionID, updateScanAndNavigate, navigateToConfirmationStep, isFlashLightOn]); + }, [action, transactionID, updateScanAndNavigate, navigateToConfirmationStep]); + + const capturePhoto = useCallback(() => { + if (!cameraRef.current.getScreenshot) { + return; + } + + if (trackRef.current && isFlashLightOn) { + trackRef.current.applyConstraints({ + advanced: [{torch: true}], + }); + setTimeout(() => { + getScreenshot(); + trackRef.current.applyConstraints({ + advanced: [{torch: false}], + }); + }, CONST.TORCH_EFFECT); + return; + } + + getScreenshot(); + }, [cameraRef, isFlashLightOn, getScreenshot]); const panResponder = useRef( PanResponder.create({ From 14ab0d60bebe946ac1a14de387fdefbf4ddd7b3c Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 23 Jan 2024 14:41:20 +0100 Subject: [PATCH 100/484] determine if a linked report action is deleted --- src/pages/home/ReportScreen.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 9ae6dca7519d..2a62cdf43466 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -511,7 +511,14 @@ function ReportScreen({ fetchReport(); }; - if (!shouldShowSkeleton && reportActionIDFromRoute && _.isEmpty(reportActions) && !isLinkingToMessage) { + const isLinkedReportActionDeleted = useMemo(() => { + if (!reportActionIDFromRoute) { + return false; + } + return ReportActionsUtils.isDeletedAction(allReportActions[reportActionIDFromRoute]); + }, [reportActionIDFromRoute, allReportActions]); + + if (isLinkedReportActionDeleted || (!shouldShowSkeleton && reportActionIDFromRoute && _.isEmpty(reportActions) && !isLinkingToMessage)) { return ( Date: Wed, 24 Jan 2024 16:35:30 +0700 Subject: [PATCH 101/484] remove settimeout --- src/CONST.ts | 2 -- .../request/step/IOURequestStepScan/index.js | 17 +++++++++-------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 264810572030..ae5fbed6dafc 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -47,8 +47,6 @@ const CONST = { OUT: 'out', }, ARROW_HIDE_DELAY: 3000, - TORCH_EFFECT: 1000, - API_ATTACHMENT_VALIDATIONS: { // 24 megabytes in bytes, this is limit set on servers, do not update without wider internal discussion MAX_SIZE: 25165824, diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.js b/src/pages/iou/request/step/IOURequestStepScan/index.js index 035db5dbb5a1..cd262ff24906 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.js @@ -196,15 +196,16 @@ function IOURequestStepScan({ } if (trackRef.current && isFlashLightOn) { - trackRef.current.applyConstraints({ - advanced: [{torch: true}], - }); - setTimeout(() => { - getScreenshot(); - trackRef.current.applyConstraints({ - advanced: [{torch: false}], + trackRef.current + .applyConstraints({ + advanced: [{torch: true}], + }) + .then(() => { + getScreenshot(); + trackRef.current.applyConstraints({ + advanced: [{torch: false}], + }); }); - }, CONST.TORCH_EFFECT); return; } From f5f2327a74ccaeca6ba35f85bee9024055172c82 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 24 Jan 2024 16:42:32 +0100 Subject: [PATCH 102/484] 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 103/484] 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 104/484] 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 105/484] 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 106/484] 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 107/484] 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 108/484] 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 109/484] 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 110/484] 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 111/484] 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 112/484] 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 113/484] 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 114/484] 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 115/484] 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 116/484] 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 117/484] 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 d0606815bcc866b3b20fb1d359b9ce1aca0e150f Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Thu, 15 Feb 2024 19:25:29 +0000 Subject: [PATCH 118/484] refactor(typescript): migrate withwritablereportornotfound --- .../step/withWritableReportOrNotFound.js | 75 ------------------- .../step/withWritableReportOrNotFound.tsx | 48 ++++++++++++ 2 files changed, 48 insertions(+), 75 deletions(-) delete mode 100644 src/pages/iou/request/step/withWritableReportOrNotFound.js create mode 100644 src/pages/iou/request/step/withWritableReportOrNotFound.tsx diff --git a/src/pages/iou/request/step/withWritableReportOrNotFound.js b/src/pages/iou/request/step/withWritableReportOrNotFound.js deleted file mode 100644 index 978b84f321d1..000000000000 --- a/src/pages/iou/request/step/withWritableReportOrNotFound.js +++ /dev/null @@ -1,75 +0,0 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; -import React from 'react'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; -import getComponentDisplayName from '@libs/getComponentDisplayName'; -import * as ReportUtils from '@libs/ReportUtils'; -import reportPropTypes from '@pages/reportPropTypes'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import IOURequestStepRoutePropTypes from './IOURequestStepRoutePropTypes'; - -const propTypes = { - /** The HOC takes an optional ref as a prop and passes it as a ref to the wrapped component. - * That way, if a ref is passed to a component wrapped in the HOC, the ref is a reference to the wrapped component, not the HOC. */ - forwardedRef: PropTypes.func, - - /** The report corresponding to the reportID in the route params */ - report: reportPropTypes, - - route: IOURequestStepRoutePropTypes.isRequired, -}; - -const defaultProps = { - forwardedRef: () => {}, - report: {}, -}; - -export default function (WrappedComponent) { - // eslint-disable-next-line rulesdir/no-negated-variables - function WithWritableReportOrNotFound({forwardedRef, ...props}) { - const { - route: { - params: {iouType}, - }, - report, - } = props; - - const iouTypeParamIsInvalid = !_.contains(_.values(CONST.IOU.TYPE), iouType); - const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report); - if (iouTypeParamIsInvalid || !canUserPerformWriteAction) { - return ; - } - - return ( - - ); - } - - WithWritableReportOrNotFound.propTypes = propTypes; - WithWritableReportOrNotFound.defaultProps = defaultProps; - WithWritableReportOrNotFound.displayName = `withWritableReportOrNotFound(${getComponentDisplayName(WrappedComponent)})`; - - // eslint-disable-next-line rulesdir/no-negated-variables - const WithWritableReportOrNotFoundWithRef = React.forwardRef((props, ref) => ( - - )); - - WithWritableReportOrNotFoundWithRef.displayName = 'WithWritableReportOrNotFoundWithRef'; - - return withOnyx({ - report: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${lodashGet(route, 'params.reportID', '0')}`, - }, - })(WithWritableReportOrNotFoundWithRef); -} diff --git a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx new file mode 100644 index 000000000000..affd8b259d94 --- /dev/null +++ b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx @@ -0,0 +1,48 @@ +import type {RouteProp} from '@react-navigation/native'; +import type {ComponentType, ForwardedRef} from 'react'; +import React, {forwardRef} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; +import getComponentDisplayName from '@libs/getComponentDisplayName'; +import * as ReportUtils from '@libs/ReportUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Report} from '@src/types/onyx'; + +type WithWritableReportOrNotFoundOnyxProps = { + /** The report corresponding to the reportID in the route params */ + report: OnyxEntry; +}; + +type WithWritableReportOrNotFoundProps = WithWritableReportOrNotFoundOnyxProps & { + route: RouteProp>; +}; + +export default function (WrappedComponent: ComponentType) { + // eslint-disable-next-line rulesdir/no-negated-variables + function WithWritableReportOrNotFound(props: TProps, ref: ForwardedRef) { + const {report, route} = props; + const iouTypeParamIsInvalid = !Object.values(CONST.IOU.TYPE).includes(route.params?.iouType ?? ''); + const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report); + if (iouTypeParamIsInvalid || !canUserPerformWriteAction) { + return ; + } + + return ( + + ); + } + + WithWritableReportOrNotFound.displayName = `withWritableReportOrNotFound(${getComponentDisplayName(WrappedComponent)})`; + + return withOnyx({ + report: { + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params?.reportID ?? '0'}`, + }, + })(forwardRef(WithWritableReportOrNotFound)); +} From 64ccb57734d6492a088525bc1e83d8fa8373eb40 Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Fri, 16 Feb 2024 17:59:45 -0500 Subject: [PATCH 119/484] 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 120/484] 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 121/484] 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 122/484] 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 58d5b933dbe07128cf33ee4d161b4c78318bc00b Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 27 Feb 2024 17:54:29 +0700 Subject: [PATCH 123/484] add settimeout --- src/pages/iou/request/step/IOURequestStepScan/index.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.js b/src/pages/iou/request/step/IOURequestStepScan/index.js index 13d6b2c4e243..179138e8bd50 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.js @@ -201,10 +201,12 @@ function IOURequestStepScan({ advanced: [{torch: true}], }) .then(() => { - getScreenshot(); - trackRef.current.applyConstraints({ - advanced: [{torch: false}], - }); + setTimeout(() => { + getScreenshot(); + trackRef.current.applyConstraints({ + advanced: [{torch: false}], + }); + }, 2000); }); return; } From 9054944828891069752adb31d42745713f29dba6 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 27 Feb 2024 18:31:12 +0530 Subject: [PATCH 124/484] 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 125/484] 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 126/484] 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 127/484] 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 128/484] 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 9b1a8e6688a889ad8fd01a2ed5fba589db8cfd42 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Tue, 27 Feb 2024 23:08:31 +0000 Subject: [PATCH 129/484] fix: not found page shown when report is undefined --- src/pages/iou/request/step/withWritableReportOrNotFound.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx index affd8b259d94..c73bf7ced8aa 100644 --- a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx +++ b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx @@ -22,7 +22,7 @@ type WithWritableReportOrNotFoundProps = WithWritableReportOrNotFoundOnyxProps & export default function (WrappedComponent: ComponentType) { // eslint-disable-next-line rulesdir/no-negated-variables function WithWritableReportOrNotFound(props: TProps, ref: ForwardedRef) { - const {report, route} = props; + const {report = {reportID: ''}, route} = props; const iouTypeParamIsInvalid = !Object.values(CONST.IOU.TYPE).includes(route.params?.iouType ?? ''); const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report); if (iouTypeParamIsInvalid || !canUserPerformWriteAction) { From bd012151b8117eeab27e2438ea23331f7c8c591f Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 28 Feb 2024 10:27:27 +0700 Subject: [PATCH 130/484] 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 131/484] 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 3060c1e778249b8b755c584fa76aebb9e08ebebe Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 28 Feb 2024 10:59:40 +0700 Subject: [PATCH 132/484] clear timeout --- .../iou/request/step/IOURequestStepScan/index.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.js b/src/pages/iou/request/step/IOURequestStepScan/index.js index 179138e8bd50..add7e888fd58 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.js @@ -1,5 +1,5 @@ import lodashGet from 'lodash/get'; -import React, {useCallback, useContext, useReducer, useRef, useState} from 'react'; +import React, {useCallback, useContext, useEffect, useReducer, useRef, useState} from 'react'; import {ActivityIndicator, PanResponder, PixelRatio, View} from 'react-native'; import Hand from '@assets/images/hand.svg'; import ReceiptUpload from '@assets/images/receipt-upload.svg'; @@ -76,6 +76,8 @@ function IOURequestStepScan({ const cameraRef = useRef(null); const trackRef = useRef(null); + const getScreenshotTimeoutRef = useRef(null); + const hideRecieptModal = () => { setIsAttachmentInvalid(false); }; @@ -201,7 +203,7 @@ function IOURequestStepScan({ advanced: [{torch: true}], }) .then(() => { - setTimeout(() => { + getScreenshotTimeoutRef.current = setTimeout(() => { getScreenshot(); trackRef.current.applyConstraints({ advanced: [{torch: false}], @@ -220,6 +222,15 @@ function IOURequestStepScan({ }), ).current; + useEffect(() => { + return () => { + if (!getScreenshotTimeoutRef.current) { + return; + } + clearTimeout(getScreenshotTimeoutRef.current); + }; + }, []); + const mobileCameraView = () => ( <> From 518153347c6760462a92370b066dbace32a56781 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 28 Feb 2024 18:01:29 +0700 Subject: [PATCH 133/484] add return condition --- .../request/step/IOURequestStepScan/index.js | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.js b/src/pages/iou/request/step/IOURequestStepScan/index.js index add7e888fd58..e18bc861333f 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.js @@ -165,7 +165,7 @@ function IOURequestStepScan({ navigateToConfirmationStep(); }; - const handleOnUserMedia = (stream) => { + const setupCameraPermissionsAndCapabilities = (stream) => { setCameraPermissionState('granted'); const [track] = stream.getVideoTracks(); @@ -177,6 +177,10 @@ function IOURequestStepScan({ }; const getScreenshot = useCallback(() => { + if (!cameraRef.current || !cameraRef.current.getScreenshot) { + return; + } + const imageBase64 = cameraRef.current.getScreenshot(); const filename = `receipt_${Date.now()}.png`; @@ -192,11 +196,16 @@ function IOURequestStepScan({ navigateToConfirmationStep(); }, [action, transactionID, updateScanAndNavigate, navigateToConfirmationStep]); - const capturePhoto = useCallback(() => { - if (!cameraRef.current.getScreenshot) { + const clearTorchConstraints = useCallback(() => { + if (!trackRef.current) { return; } + trackRef.current.applyConstraints({ + advanced: [{torch: false}], + }); + }, []); + const capturePhoto = useCallback(() => { if (trackRef.current && isFlashLightOn) { trackRef.current .applyConstraints({ @@ -205,16 +214,14 @@ function IOURequestStepScan({ .then(() => { getScreenshotTimeoutRef.current = setTimeout(() => { getScreenshot(); - trackRef.current.applyConstraints({ - advanced: [{torch: false}], - }); + clearTorchConstraints(); }, 2000); }); return; } getScreenshot(); - }, [cameraRef, isFlashLightOn, getScreenshot]); + }, [isFlashLightOn, getScreenshot, clearTorchConstraints]); const panResponder = useRef( PanResponder.create({ @@ -222,14 +229,15 @@ function IOURequestStepScan({ }), ).current; - useEffect(() => { - return () => { + useEffect( + () => () => { if (!getScreenshotTimeoutRef.current) { return; } clearTimeout(getScreenshotTimeoutRef.current); - }; - }, []); + }, + [], + ); const mobileCameraView = () => ( <> @@ -254,7 +262,7 @@ function IOURequestStepScan({ )} setCameraPermissionState('denied')} style={{...styles.videoContainer, display: cameraPermissionState !== 'granted' ? 'none' : 'block'}} ref={cameraRef} From 1a7099077402761a95b75b7f997c349be30ddbc5 Mon Sep 17 00:00:00 2001 From: smelaa Date: Wed, 28 Feb 2024 15:00:14 +0100 Subject: [PATCH 134/484] 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 4b84689d4236d15ec2588f6367a5f55dea335880 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Wed, 28 Feb 2024 15:20:21 +0000 Subject: [PATCH 135/484] fix(typescript): incompatible type error --- src/pages/iou/request/step/IOURequestStepWaypoint.tsx | 5 +++-- src/pages/iou/request/step/withWritableReportOrNotFound.tsx | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepWaypoint.tsx b/src/pages/iou/request/step/IOURequestStepWaypoint.tsx index eee6da9e87ef..ac5cb307da86 100644 --- a/src/pages/iou/request/step/IOURequestStepWaypoint.tsx +++ b/src/pages/iou/request/step/IOURequestStepWaypoint.tsx @@ -33,7 +33,7 @@ import type * as OnyxTypes from '@src/types/onyx'; import type {Waypoint} from '@src/types/onyx/Transaction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; -import withWritableReportOrNotFound from './withWritableReportOrNotFound'; +import withWritableReportOrNotFound, {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; type IOURequestStepWaypointOnyxProps = { /** List of recent waypoints */ @@ -54,7 +54,8 @@ type IOURequestStepWaypointProps = { }; }; transaction: OnyxEntry; -} & IOURequestStepWaypointOnyxProps; +} & IOURequestStepWaypointOnyxProps & + WithWritableReportOrNotFoundProps; function IOURequestStepWaypoint({ route: { diff --git a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx index c73bf7ced8aa..5cc3d6e1ba01 100644 --- a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx +++ b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx @@ -46,3 +46,5 @@ export default function }, })(forwardRef(WithWritableReportOrNotFound)); } + +export type {WithWritableReportOrNotFoundProps}; From aed45f85048d5eac559b893cff6c81e4a04c5b83 Mon Sep 17 00:00:00 2001 From: smelaa Date: Wed, 28 Feb 2024 16:44:39 +0100 Subject: [PATCH 136/484] 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 137/484] 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 1b2b2269814ade467c444c5c829f485c11e1d16c Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Wed, 28 Feb 2024 16:38:25 +0000 Subject: [PATCH 138/484] fix: linter and typescript issues --- src/pages/iou/request/step/IOURequestStepWaypoint.tsx | 3 ++- src/pages/iou/request/step/withWritableReportOrNotFound.tsx | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepWaypoint.tsx b/src/pages/iou/request/step/IOURequestStepWaypoint.tsx index ac5cb307da86..91bd5e37a72e 100644 --- a/src/pages/iou/request/step/IOURequestStepWaypoint.tsx +++ b/src/pages/iou/request/step/IOURequestStepWaypoint.tsx @@ -33,7 +33,8 @@ import type * as OnyxTypes from '@src/types/onyx'; import type {Waypoint} from '@src/types/onyx/Transaction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; -import withWritableReportOrNotFound, {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; +import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; +import withWritableReportOrNotFound from './withWritableReportOrNotFound'; type IOURequestStepWaypointOnyxProps = { /** List of recent waypoints */ diff --git a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx index 5cc3d6e1ba01..ed4958e77f08 100644 --- a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx +++ b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx @@ -1,4 +1,3 @@ -import type {RouteProp} from '@react-navigation/native'; import type {ComponentType, ForwardedRef} from 'react'; import React, {forwardRef} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; @@ -16,7 +15,7 @@ type WithWritableReportOrNotFoundOnyxProps = { }; type WithWritableReportOrNotFoundProps = WithWritableReportOrNotFoundOnyxProps & { - route: RouteProp>; + route: {params: {iouType: string; reportID: string} | undefined}; }; export default function (WrappedComponent: ComponentType) { From 8cfade3b04b12520ce93d33cdd35c2e0edad53d8 Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 29 Feb 2024 00:19:36 +0700 Subject: [PATCH 139/484] remove unnecessary check --- src/pages/iou/request/step/IOURequestStepScan/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.js b/src/pages/iou/request/step/IOURequestStepScan/index.js index e18bc861333f..a11a9086e135 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.js @@ -177,7 +177,7 @@ function IOURequestStepScan({ }; const getScreenshot = useCallback(() => { - if (!cameraRef.current || !cameraRef.current.getScreenshot) { + if (!cameraRef.current) { return; } From d13e2bed6170dd4f22c6f139c1391f18d3877926 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Thu, 29 Feb 2024 06:17:39 +0530 Subject: [PATCH 140/484] 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 06ca091a5fef7552178ee4d767f7bf709a87abf4 Mon Sep 17 00:00:00 2001 From: Aswin S Date: Thu, 29 Feb 2024 09:49:30 +0530 Subject: [PATCH 141/484] fix: prevent tab switch while swiping horizontally on map --- src/components/SwipeInterceptPanResponder.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SwipeInterceptPanResponder.tsx b/src/components/SwipeInterceptPanResponder.tsx index fe1545d2f14b..48cfe4f90c5c 100644 --- a/src/components/SwipeInterceptPanResponder.tsx +++ b/src/components/SwipeInterceptPanResponder.tsx @@ -1,7 +1,7 @@ import {PanResponder} from 'react-native'; const SwipeInterceptPanResponder = PanResponder.create({ - onMoveShouldSetPanResponder: () => true, + onStartShouldSetPanResponder: () => true, onPanResponderTerminationRequest: () => false, }); From aeed06fcd22e270ac789f09a0137b0d27aeb6fbe Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 29 Feb 2024 11:28:36 +0700 Subject: [PATCH 142/484] 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 4607adda4ca244422ffce548780750df2d01610a Mon Sep 17 00:00:00 2001 From: Aswin S Date: Thu, 29 Feb 2024 10:13:51 +0530 Subject: [PATCH 143/484] misc: remove redundant file --- src/components/MapView/responder/index.android.ts | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 src/components/MapView/responder/index.android.ts diff --git a/src/components/MapView/responder/index.android.ts b/src/components/MapView/responder/index.android.ts deleted file mode 100644 index a0fce71d8ef5..000000000000 --- a/src/components/MapView/responder/index.android.ts +++ /dev/null @@ -1,8 +0,0 @@ -import {PanResponder} from 'react-native'; - -const responder = PanResponder.create({ - onStartShouldSetPanResponder: () => true, - onPanResponderTerminationRequest: () => false, -}); - -export default responder; From f9dd242735e6e64019807b4c5c981b0c80c89add Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Thu, 29 Feb 2024 10:51:11 +0100 Subject: [PATCH 144/484] migrate group 5 tests to ts --- .../actions/{ReportTest.js => ReportTest.ts} | 135 +++++++++--------- tests/unit/{APITest.js => APITest.ts} | 93 ++++++++---- .../{MigrationTest.js => MigrationTest.ts} | 61 ++++---- tests/unit/{NetworkTest.js => NetworkTest.ts} | 32 +++-- 4 files changed, 193 insertions(+), 128 deletions(-) rename tests/actions/{ReportTest.js => ReportTest.ts} (86%) rename tests/unit/{APITest.js => APITest.ts} (87%) rename tests/unit/{MigrationTest.js => MigrationTest.ts} (76%) rename tests/unit/{NetworkTest.js => NetworkTest.ts} (92%) diff --git a/tests/actions/ReportTest.js b/tests/actions/ReportTest.ts similarity index 86% rename from tests/actions/ReportTest.js rename to tests/actions/ReportTest.ts index a94db507637b..43ceaaad607e 100644 --- a/tests/actions/ReportTest.js +++ b/tests/actions/ReportTest.ts @@ -1,8 +1,9 @@ +/* eslint-disable @typescript-eslint/naming-convention */ import {afterEach, beforeAll, beforeEach, describe, expect, it} from '@jest/globals'; import {utcToZonedTime} from 'date-fns-tz'; -import lodashGet from 'lodash/get'; import Onyx from 'react-native-onyx'; -import _ from 'underscore'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type * as OnyxTypes from '@src/types/onyx'; import CONST from '../../src/CONST'; import OnyxUpdateManager from '../../src/libs/actions/OnyxUpdateManager'; import * as PersistedRequests from '../../src/libs/actions/PersistedRequests'; @@ -21,7 +22,7 @@ import waitForNetworkPromises from '../utils/waitForNetworkPromises'; const UTC = 'UTC'; jest.mock('../../src/libs/actions/Report', () => { - const originalModule = jest.requireActual('../../src/libs/actions/Report'); + const originalModule: typeof Report = jest.requireActual('../../src/libs/actions/Report'); return { ...originalModule, @@ -35,7 +36,7 @@ describe('actions/Report', () => { PusherHelper.setup(); Onyx.init({ keys: ONYXKEYS, - registerStorageEventListener: () => {}, + // registerStorageEventListener: () => {}, }); }); @@ -52,12 +53,12 @@ describe('actions/Report', () => { afterEach(PusherHelper.teardown); it('should store a new report action in Onyx when onyxApiUpdate event is handled via Pusher', () => { - global.fetch = TestHelper.getGlobalFetchMock(); + global.fetch = TestHelper.getGlobalFetchMock() as typeof fetch; const TEST_USER_ACCOUNT_ID = 1; const TEST_USER_LOGIN = 'test@test.com'; - const REPORT_ID = 1; - let reportActionID; + const REPORT_ID = '1'; + let reportActionID: string; const REPORT_ACTION = { actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, actorAccountID: TEST_USER_ACCOUNT_ID, @@ -68,7 +69,7 @@ describe('actions/Report', () => { shouldShow: true, }; - let reportActions; + let reportActions: OnyxEntry; Onyx.connect({ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, callback: (val) => (reportActions = val), @@ -88,7 +89,7 @@ describe('actions/Report', () => { return waitForBatchedUpdates(); }) .then(() => { - const resultAction = _.first(_.values(reportActions)); + const resultAction: OnyxEntry = Object.values(reportActions ?? [])[0]; reportActionID = resultAction.reportActionID; expect(resultAction.message).toEqual(REPORT_ACTION.message); @@ -125,12 +126,12 @@ describe('actions/Report', () => { }) .then(() => { // Verify there is only one action and our optimistic comment has been removed - expect(_.size(reportActions)).toBe(1); + expect(Object.keys(reportActions ?? {}).length).toBe(1); - const resultAction = reportActions[reportActionID]; + const resultAction = reportActions?.[reportActionID]; // Verify that our action is no longer in the loading state - expect(resultAction.pendingAction).toBeUndefined(); + expect(resultAction?.pendingAction).toBeUndefined(); }); }); @@ -139,10 +140,10 @@ describe('actions/Report', () => { const TEST_USER_LOGIN = 'test@test.com'; const REPORT_ID = '1'; - let reportIsPinned; + let reportIsPinned: boolean; Onyx.connect({ key: `${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, - callback: (val) => (reportIsPinned = lodashGet(val, 'isPinned')), + callback: (val) => (reportIsPinned = val?.isPinned ?? false), }); // Set up Onyx with some test user data @@ -167,7 +168,7 @@ describe('actions/Report', () => { return TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN) .then(() => TestHelper.setPersonalDetails(TEST_USER_LOGIN, TEST_USER_ACCOUNT_ID)) .then(() => { - global.fetch = TestHelper.getGlobalFetchMock(); + global.fetch = TestHelper.getGlobalFetchMock() as typeof fetch; // WHEN we add enough logs to send a packet for (let i = 0; i <= LOGGER_MAX_LOG_LINES; i++) { @@ -186,27 +187,27 @@ describe('actions/Report', () => { .then(() => { // THEN only ONE call to AddComment will happen const URL_ARGUMENT_INDEX = 0; - const addCommentCalls = _.filter(global.fetch.mock.calls, (callArguments) => callArguments[URL_ARGUMENT_INDEX].includes('AddComment')); + const addCommentCalls = (global.fetch as jest.Mock).mock.calls.filter((callArguments) => callArguments[URL_ARGUMENT_INDEX].includes('AddComment')); expect(addCommentCalls.length).toBe(1); }); }); it('should be updated correctly when new comments are added, deleted or marked as unread', () => { jest.useFakeTimers(); - global.fetch = TestHelper.getGlobalFetchMock(); + global.fetch = TestHelper.getGlobalFetchMock() as typeof fetch; const REPORT_ID = '1'; - let report; - let reportActionCreatedDate; - let currentTime; + let report: OnyxEntry; + let reportActionCreatedDate: string; + let currentTime: string; Onyx.connect({ key: `${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, callback: (val) => (report = val), }); - let reportActions; + let reportActions: OnyxTypes.ReportActions; Onyx.connect({ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, - callback: (val) => (reportActions = val), + callback: (val) => (reportActions = val ?? {}), }); const USER_1_LOGIN = 'user@test.com'; @@ -276,7 +277,7 @@ describe('actions/Report', () => { .then(() => { // The report will be read expect(ReportUtils.isUnread(report)).toBe(false); - expect(utcToZonedTime(report.lastReadTime, UTC).getTime()).toBeGreaterThanOrEqual(utcToZonedTime(currentTime, UTC).getTime()); + expect(utcToZonedTime(report?.lastReadTime ?? '', UTC).getTime()).toBeGreaterThanOrEqual(utcToZonedTime(currentTime, UTC).getTime()); // And no longer show the green dot for unread mentions in the LHN expect(ReportUtils.isUnreadWithMention(report)).toBe(false); @@ -290,7 +291,7 @@ describe('actions/Report', () => { // Then the report will be unread and show the green dot for unread mentions in LHN expect(ReportUtils.isUnread(report)).toBe(true); expect(ReportUtils.isUnreadWithMention(report)).toBe(true); - expect(report.lastReadTime).toBe(DateUtils.subtractMillisecondsFromDateTime(reportActionCreatedDate, 1)); + expect(report?.lastReadTime).toBe(DateUtils.subtractMillisecondsFromDateTime(reportActionCreatedDate, 1)); // When a new comment is added by the current user jest.advanceTimersByTime(10); @@ -302,8 +303,8 @@ describe('actions/Report', () => { // The report will be read, the green dot for unread mentions will go away, and the lastReadTime updated expect(ReportUtils.isUnread(report)).toBe(false); expect(ReportUtils.isUnreadWithMention(report)).toBe(false); - expect(utcToZonedTime(report.lastReadTime, UTC).getTime()).toBeGreaterThanOrEqual(utcToZonedTime(currentTime, UTC).getTime()); - expect(report.lastMessageText).toBe('Current User Comment 1'); + expect(utcToZonedTime(report?.lastReadTime ?? '', UTC).getTime()).toBeGreaterThanOrEqual(utcToZonedTime(currentTime, UTC).getTime()); + expect(report?.lastMessageText).toBe('Current User Comment 1'); // When another comment is added by the current user jest.advanceTimersByTime(10); @@ -314,8 +315,8 @@ describe('actions/Report', () => { .then(() => { // The report will be read and the lastReadTime updated expect(ReportUtils.isUnread(report)).toBe(false); - expect(utcToZonedTime(report.lastReadTime, UTC).getTime()).toBeGreaterThanOrEqual(utcToZonedTime(currentTime, UTC).getTime()); - expect(report.lastMessageText).toBe('Current User Comment 2'); + expect(utcToZonedTime(report?.lastReadTime ?? '', UTC).getTime()).toBeGreaterThanOrEqual(utcToZonedTime(currentTime, UTC).getTime()); + expect(report?.lastMessageText).toBe('Current User Comment 2'); // When another comment is added by the current user jest.advanceTimersByTime(10); @@ -326,8 +327,8 @@ describe('actions/Report', () => { .then(() => { // The report will be read and the lastReadTime updated expect(ReportUtils.isUnread(report)).toBe(false); - expect(utcToZonedTime(report.lastReadTime, UTC).getTime()).toBeGreaterThanOrEqual(utcToZonedTime(currentTime, UTC).getTime()); - expect(report.lastMessageText).toBe('Current User Comment 3'); + expect(utcToZonedTime(report?.lastReadTime ?? '', UTC).getTime()).toBeGreaterThanOrEqual(utcToZonedTime(currentTime, UTC).getTime()); + expect(report?.lastMessageText).toBe('Current User Comment 3'); const USER_1_BASE_ACTION = { actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, @@ -350,12 +351,14 @@ describe('actions/Report', () => { created: DateUtils.getDBTime(Date.now() - 2), reportActionID: '200', }, + 300: { ...USER_1_BASE_ACTION, message: [{type: 'COMMENT', html: 'Current User Comment 2', text: 'Current User Comment 2'}], created: DateUtils.getDBTime(Date.now() - 1), reportActionID: '300', }, + 400: { ...USER_1_BASE_ACTION, message: [{type: 'COMMENT', html: 'Current User Comment 3', text: 'Current User Comment 3'}], @@ -394,7 +397,7 @@ describe('actions/Report', () => { }) .then(() => { // Then no change will occur - expect(report.lastReadTime).toBe(reportActionCreatedDate); + expect(report?.lastReadTime).toBe(reportActionCreatedDate); expect(ReportUtils.isUnread(report)).toBe(false); // When the user manually marks a message as "unread" @@ -404,7 +407,7 @@ describe('actions/Report', () => { .then(() => { // Then we should expect the report to be to be unread expect(ReportUtils.isUnread(report)).toBe(true); - expect(report.lastReadTime).toBe(DateUtils.subtractMillisecondsFromDateTime(reportActionCreatedDate, 1)); + expect(report?.lastReadTime).toBe(DateUtils.subtractMillisecondsFromDateTime(reportActionCreatedDate, 1)); // If the user deletes the last comment after the lastReadTime the lastMessageText will reflect the new last comment Report.deleteReportComment(REPORT_ID, {...reportActions[400]}); @@ -412,7 +415,7 @@ describe('actions/Report', () => { }) .then(() => { expect(ReportUtils.isUnread(report)).toBe(false); - expect(report.lastMessageText).toBe('Current User Comment 2'); + expect(report?.lastMessageText).toBe('Current User Comment 2'); }); waitForBatchedUpdates(); // flushing onyx.set as it will be batched return setPromise; @@ -424,7 +427,7 @@ describe('actions/Report', () => { * already in the comment and the user deleted it on purpose. */ - global.fetch = TestHelper.getGlobalFetchMock(); + global.fetch = TestHelper.getGlobalFetchMock() as typeof fetch; // User edits comment to add link // We should generate link @@ -536,11 +539,11 @@ describe('actions/Report', () => { }); it('should properly toggle reactions on a message', () => { - global.fetch = TestHelper.getGlobalFetchMock(); + global.fetch = TestHelper.getGlobalFetchMock() as typeof fetch; const TEST_USER_ACCOUNT_ID = 1; const TEST_USER_LOGIN = 'test@test.com'; - const REPORT_ID = 1; + const REPORT_ID = '1'; const EMOJI_CODE = '👍'; const EMOJI_SKIN_TONE = 2; const EMOJI_NAME = '+1'; @@ -550,20 +553,20 @@ describe('actions/Report', () => { types: ['👍🏿', '👍🏾', '👍🏽', '👍🏼', '👍🏻'], }; - let reportActions; + let reportActions: OnyxTypes.ReportActions; Onyx.connect({ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, - callback: (val) => (reportActions = val), + callback: (val) => (reportActions = val ?? {}), }); - const reportActionsReactions = {}; + const reportActionsReactions: OnyxCollection = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS, callback: (val, key) => { - reportActionsReactions[key] = val; + reportActionsReactions[key] = val ?? {}; }, }); - let reportAction; - let reportActionID; + let reportAction: OnyxTypes.ReportAction; + let reportActionID: string; // Set up Onyx with some test user data return TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN) @@ -579,15 +582,15 @@ describe('actions/Report', () => { return waitForBatchedUpdates(); }) .then(() => { - reportAction = _.first(_.values(reportActions)); + reportAction = Object.values(reportActions)[0]; reportActionID = reportAction.reportActionID; // Add a reaction to the comment - Report.toggleEmojiReaction(REPORT_ID, reportAction, EMOJI); + Report.toggleEmojiReaction(REPORT_ID, reportAction, EMOJI, reportActionsReactions[0]); return waitForBatchedUpdates(); }) .then(() => { - reportAction = _.first(_.values(reportActions)); + reportAction = Object.values(reportActions)[0]; // Expect the reaction to exist in the reportActionsReactions collection expect(reportActionsReactions).toHaveProperty(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${reportActionID}`); @@ -597,8 +600,8 @@ describe('actions/Report', () => { expect(reportActionReaction).toHaveProperty(EMOJI.name); // Expect the emoji to have the user accountID - const reportActionReactionEmoji = reportActionReaction[EMOJI.name]; - expect(reportActionReactionEmoji.users).toHaveProperty(`${TEST_USER_ACCOUNT_ID}`); + const reportActionReactionEmoji = reportActionReaction?.[EMOJI.name]; + expect(reportActionReactionEmoji?.users).toHaveProperty(`${TEST_USER_ACCOUNT_ID}`); // Now we remove the reaction Report.toggleEmojiReaction(REPORT_ID, reportAction, EMOJI, reportActionReaction); @@ -608,23 +611,23 @@ describe('actions/Report', () => { // Expect the reaction to have null where the users reaction used to be expect(reportActionsReactions).toHaveProperty(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${reportActionID}`); const reportActionReaction = reportActionsReactions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${reportActionID}`]; - expect(reportActionReaction[EMOJI.name].users[TEST_USER_ACCOUNT_ID]).toBeUndefined(); + expect(reportActionReaction?.[EMOJI.name].users[TEST_USER_ACCOUNT_ID]).toBeUndefined(); }) .then(() => { - reportAction = _.first(_.values(reportActions)); + reportAction = Object.values(reportActions)[0]; // Add the same reaction to the same report action with a different skintone - Report.toggleEmojiReaction(REPORT_ID, reportAction, EMOJI); + Report.toggleEmojiReaction(REPORT_ID, reportAction, EMOJI, reportActionsReactions[0]); return waitForBatchedUpdates() .then(() => { - reportAction = _.first(_.values(reportActions)); + reportAction = Object.values(reportActions)[0]; const reportActionReaction = reportActionsReactions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${reportActionID}`]; Report.toggleEmojiReaction(REPORT_ID, reportAction, EMOJI, reportActionReaction, EMOJI_SKIN_TONE); return waitForBatchedUpdates(); }) .then(() => { - reportAction = _.first(_.values(reportActions)); + reportAction = Object.values(reportActions)[0]; // Expect the reaction to exist in the reportActionsReactions collection expect(reportActionsReactions).toHaveProperty(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${reportActionID}`); @@ -634,11 +637,11 @@ describe('actions/Report', () => { expect(reportActionReaction).toHaveProperty(EMOJI.name); // Expect the emoji to have the user accountID - const reportActionReactionEmoji = reportActionReaction[EMOJI.name]; - expect(reportActionReactionEmoji.users).toHaveProperty(`${TEST_USER_ACCOUNT_ID}`); + const reportActionReactionEmoji = reportActionReaction?.[EMOJI.name]; + expect(reportActionReactionEmoji?.users).toHaveProperty(`${TEST_USER_ACCOUNT_ID}`); // Expect two different skintone reactions - const reportActionReactionEmojiUserSkinTones = reportActionReactionEmoji.users[TEST_USER_ACCOUNT_ID].skinTones; + const reportActionReactionEmojiUserSkinTones = reportActionReactionEmoji?.users[TEST_USER_ACCOUNT_ID].skinTones; expect(reportActionReactionEmojiUserSkinTones).toHaveProperty('-1'); expect(reportActionReactionEmojiUserSkinTones).toHaveProperty('2'); @@ -650,17 +653,17 @@ describe('actions/Report', () => { // Expect the reaction to have null where the users reaction used to be expect(reportActionsReactions).toHaveProperty(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${reportActionID}`); const reportActionReaction = reportActionsReactions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${reportActionID}`]; - expect(reportActionReaction[EMOJI.name].users[TEST_USER_ACCOUNT_ID]).toBeUndefined(); + expect(reportActionReaction?.[EMOJI.name].users[TEST_USER_ACCOUNT_ID]).toBeUndefined(); }); }); }); it("shouldn't add the same reaction twice when changing preferred skin color and reaction doesn't support skin colors", () => { - global.fetch = TestHelper.getGlobalFetchMock(); + global.fetch = TestHelper.getGlobalFetchMock() as typeof fetch; const TEST_USER_ACCOUNT_ID = 1; const TEST_USER_LOGIN = 'test@test.com'; - const REPORT_ID = 1; + const REPORT_ID = '1'; const EMOJI_CODE = '😄'; const EMOJI_NAME = 'smile'; const EMOJI = { @@ -668,20 +671,20 @@ describe('actions/Report', () => { name: EMOJI_NAME, }; - let reportActions; + let reportActions: OnyxTypes.ReportActions = {}; Onyx.connect({ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, - callback: (val) => (reportActions = val), + callback: (val) => (reportActions = val ?? {}), }); - const reportActionsReactions = {}; + const reportActionsReactions: OnyxCollection = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS, callback: (val, key) => { - reportActionsReactions[key] = val; + reportActionsReactions[key] = val ?? {}; }, }); - let resultAction; + let resultAction: OnyxTypes.ReportAction; // Set up Onyx with some test user data return TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN) @@ -697,14 +700,14 @@ describe('actions/Report', () => { return waitForBatchedUpdates(); }) .then(() => { - resultAction = _.first(_.values(reportActions)); + resultAction = Object.values(reportActions)[0]; // Add a reaction to the comment Report.toggleEmojiReaction(REPORT_ID, resultAction, EMOJI, {}); return waitForBatchedUpdates(); }) .then(() => { - resultAction = _.first(_.values(reportActions)); + resultAction = Object.values(reportActions)[0]; // Now we toggle the reaction while the skin tone has changed. // As the emoji doesn't support skin tones, the emoji @@ -717,7 +720,7 @@ describe('actions/Report', () => { // Expect the reaction to have null where the users reaction used to be expect(reportActionsReactions).toHaveProperty(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${resultAction.reportActionID}`); const reportActionReaction = reportActionsReactions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${resultAction.reportActionID}`]; - expect(reportActionReaction[EMOJI.name].users[TEST_USER_ACCOUNT_ID]).toBeUndefined(); + expect(reportActionReaction?.[EMOJI.name].users[TEST_USER_ACCOUNT_ID]).toBeUndefined(); }); }); }); diff --git a/tests/unit/APITest.js b/tests/unit/APITest.ts similarity index 87% rename from tests/unit/APITest.js rename to tests/unit/APITest.ts index 30c935c48571..9c94730fb4cc 100644 --- a/tests/unit/APITest.js +++ b/tests/unit/APITest.ts @@ -1,5 +1,6 @@ -import Onyx from 'react-native-onyx'; -import _ from 'underscore'; +// import Onyx from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; +import reactNativeOnyxMock from '../../__mocks__/react-native-onyx'; import CONST from '../../src/CONST'; import * as PersistedRequests from '../../src/libs/actions/PersistedRequests'; import * as API from '../../src/libs/API'; @@ -14,16 +15,26 @@ import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import waitForNetworkPromises from '../utils/waitForNetworkPromises'; +const Onyx = reactNativeOnyxMock; + jest.mock('../../src/libs/Log'); Onyx.init({ keys: ONYXKEYS, }); +type Response = { + ok?: boolean; + status?: ValueOf | ValueOf; + jsonCode?: ValueOf; + title?: ValueOf; + type?: ValueOf; +}; + const originalXHR = HttpUtils.xhr; beforeEach(() => { - global.fetch = TestHelper.getGlobalFetchMock(); + global.fetch = TestHelper.getGlobalFetchMock() as typeof fetch; HttpUtils.xhr = originalXHR; MainQueue.clear(); HttpUtils.cancelPendingRequests(); @@ -53,8 +64,11 @@ describe('APITests', () => { return Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}) .then(() => { // When API Writes and Reads are called + // @ts-expect-error - mocking the parameter API.write('mock command', {param1: 'value1'}); + // @ts-expect-error - mocking the parameter API.read('mock command', {param2: 'value2'}); + // @ts-expect-error - mocking the parameter API.write('mock command', {param3: 'value3'}); return waitForBatchedUpdates(); }) @@ -89,7 +103,9 @@ describe('APITests', () => { }) .then(() => { // When API Write commands are made + // @ts-expect-error - mocking the parameter API.write('mock command', {param1: 'value1'}); + // @ts-expect-error - mocking the parameter API.write('mock command', {param2: 'value2'}); return waitForBatchedUpdates(); }) @@ -120,8 +136,11 @@ describe('APITests', () => { test('Write request should not be cleared until a backend response occurs', () => { // We're setting up xhr handler that will resolve calls programmatically - const xhrCalls = []; - const promises = []; + const xhrCalls: Array<{ + resolve: (value: Response | PromiseLike) => void; + reject: (value: unknown) => void; + }> = []; + const promises: Array> = []; jest.spyOn(HttpUtils, 'xhr').mockImplementation(() => { promises.push( @@ -130,7 +149,7 @@ describe('APITests', () => { }), ); - return _.last(promises); + return promises.slice(-1)[0]; }); // Given we have some requests made while we're offline @@ -138,7 +157,9 @@ describe('APITests', () => { Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}) .then(() => { // When API Write commands are made + // @ts-expect-error - mocking the parameter API.write('mock command', {param1: 'value1'}); + // @ts-expect-error - mocking the parameter API.write('mock command', {param2: 'value2'}); return waitForBatchedUpdates(); }) @@ -148,14 +169,14 @@ describe('APITests', () => { .then(waitForBatchedUpdates) .then(() => { // Then requests should remain persisted until the xhr call is resolved - expect(_.size(PersistedRequests.getAll())).toEqual(2); + expect(PersistedRequests.getAll().length).toEqual(2); xhrCalls[0].resolve({jsonCode: CONST.JSON_CODE.SUCCESS}); return waitForBatchedUpdates(); }) .then(waitForBatchedUpdates) .then(() => { - expect(_.size(PersistedRequests.getAll())).toEqual(1); + expect(PersistedRequests.getAll().length).toEqual(1); expect(PersistedRequests.getAll()).toEqual([expect.objectContaining({command: 'mock command', data: expect.objectContaining({param2: 'value2'})})]); // When a request fails it should be retried @@ -163,7 +184,7 @@ describe('APITests', () => { return waitForBatchedUpdates(); }) .then(() => { - expect(_.size(PersistedRequests.getAll())).toEqual(1); + expect(PersistedRequests.getAll().length).toEqual(1); expect(PersistedRequests.getAll()).toEqual([expect.objectContaining({command: 'mock command', data: expect.objectContaining({param2: 'value2'})})]); // We need to advance past the request throttle back off timer because the request won't be retried until then @@ -177,32 +198,30 @@ describe('APITests', () => { return waitForBatchedUpdates(); }) .then(() => { - expect(_.size(PersistedRequests.getAll())).toEqual(0); + expect(PersistedRequests.getAll().length).toEqual(0); }) ); }); // Given a retry response create a mock and run some expectations for retrying requests - const retryExpectations = (retryResponse) => { - let successfulResponse = { + + const retryExpectations = (Response: Response) => { + const successfulResponse = { ok: true, jsonCode: CONST.JSON_CODE.SUCCESS, - }; - - // We have to mock response.json() too - successfulResponse = { - ...successfulResponse, + // We have to mock response.json() too json: () => Promise.resolve(successfulResponse), }; // Given a mock where a retry response is returned twice before a successful response - global.fetch = jest.fn().mockResolvedValueOnce(retryResponse).mockResolvedValueOnce(retryResponse).mockResolvedValueOnce(successfulResponse); + global.fetch = jest.fn().mockResolvedValueOnce(Response).mockResolvedValueOnce(Response).mockResolvedValueOnce(successfulResponse); // Given we have a request made while we're offline return ( Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}) .then(() => { // When API Write commands are made + // @ts-expect-error - mocking the parameter API.write('mock command', {param1: 'value1'}); return waitForNetworkPromises(); }) @@ -215,7 +234,7 @@ describe('APITests', () => { expect(global.fetch).toHaveBeenCalledTimes(1); // And we still have 1 persisted request since it failed - expect(_.size(PersistedRequests.getAll())).toEqual(1); + expect(PersistedRequests.getAll().length).toEqual(1); expect(PersistedRequests.getAll()).toEqual([expect.objectContaining({command: 'mock command', data: expect.objectContaining({param1: 'value1'})})]); // We let the SequentialQueue process again after its wait time @@ -228,7 +247,7 @@ describe('APITests', () => { expect(global.fetch).toHaveBeenCalledTimes(2); // And we still have 1 persisted request since it failed - expect(_.size(PersistedRequests.getAll())).toEqual(1); + expect(PersistedRequests.getAll().length).toEqual(1); expect(PersistedRequests.getAll()).toEqual([expect.objectContaining({command: 'mock command', data: expect.objectContaining({param1: 'value1'})})]); // We let the SequentialQueue process again after its wait time @@ -241,7 +260,7 @@ describe('APITests', () => { expect(global.fetch).toHaveBeenCalledTimes(3); // The request succeeds so the queue is empty - expect(_.size(PersistedRequests.getAll())).toEqual(0); + expect(PersistedRequests.getAll().length).toEqual(0); }) ); }; @@ -258,7 +277,7 @@ describe('APITests', () => { // Given the response data returned when auth is down const responseData = { ok: true, - status: 200, + status: CONST.JSON_CODE.SUCCESS, jsonCode: CONST.JSON_CODE.EXP_ERROR, title: CONST.ERROR_TITLE.SOCKET, type: CONST.ERROR_TYPE.SOCKET, @@ -289,6 +308,7 @@ describe('APITests', () => { waitForBatchedUpdates() .then(() => Onyx.set(ONYXKEYS.NETWORK, {isOffline: true})) .then(() => { + // @ts-expect-error - mocking the parameter API.write('Mock', {param1: 'value1'}); return waitForBatchedUpdates(); }) @@ -297,7 +317,7 @@ describe('APITests', () => { .then(() => Onyx.set(ONYXKEYS.NETWORK, {isOffline: false})) .then(waitForBatchedUpdates) .then(() => { - const nonLogCalls = _.filter(xhr.mock.calls, ([commandName]) => commandName !== 'Log'); + const nonLogCalls = xhr.mock.calls.filter(([commandName]) => commandName !== 'Log'); // The request should be retried once and reauthenticate should be called the second time // expect(xhr).toHaveBeenCalledTimes(3); @@ -322,12 +342,19 @@ describe('APITests', () => { }) .then(() => { // When we queue 6 persistable commands and one not persistable + // @ts-expect-error - mocking the parameter API.write('MockCommand', {content: 'value1'}); + // @ts-expect-error - mocking the parameter API.write('MockCommand', {content: 'value2'}); + // @ts-expect-error - mocking the parameter API.write('MockCommand', {content: 'value3'}); + // @ts-expect-error - mocking the parameter API.read('MockCommand', {content: 'not-persisted'}); + // @ts-expect-error - mocking the parameter API.write('MockCommand', {content: 'value4'}); + // @ts-expect-error - mocking the parameter API.write('MockCommand', {content: 'value5'}); + // @ts-expect-error - mocking the parameter API.write('MockCommand', {content: 'value6'}); return waitForBatchedUpdates(); @@ -359,11 +386,17 @@ describe('APITests', () => { }) .then(() => { // When we queue 6 persistable commands + // @ts-expect-error - mocking the parameter API.write('MockCommand', {content: 'value1'}); + // @ts-expect-error - mocking the parameter API.write('MockCommand', {content: 'value2'}); + // @ts-expect-error - mocking the parameter API.write('MockCommand', {content: 'value3'}); + // @ts-expect-error - mocking the parameter API.write('MockCommand', {content: 'value4'}); + // @ts-expect-error - mocking the parameter API.write('MockCommand', {content: 'value5'}); + // @ts-expect-error - mocking the parameter API.write('MockCommand', {content: 'value6'}); return waitForBatchedUpdates(); }) @@ -402,7 +435,14 @@ describe('APITests', () => { }) .then(() => { // When we queue both non-persistable and persistable commands that will trigger reauthentication and go offline at the same time - API.makeRequestWithSideEffects('AuthenticatePusher', {content: 'value1'}); + API.makeRequestWithSideEffects('AuthenticatePusher', { + // eslint-disable-next-line @typescript-eslint/naming-convention + socket_id: 'socket_id', + // eslint-disable-next-line @typescript-eslint/naming-convention + channel_name: 'channel_name', + shouldRetry: false, + forceNetworkRequest: false, + }); Onyx.set(ONYXKEYS.NETWORK, {isOffline: true}); expect(NetworkStore.isOffline()).toBe(false); @@ -410,6 +450,7 @@ describe('APITests', () => { return waitForBatchedUpdates(); }) .then(() => { + // @ts-expect-error - mocking the parameter API.write('MockCommand'); expect(PersistedRequests.getAll().length).toBe(1); expect(NetworkStore.isOffline()).toBe(true); @@ -479,6 +520,7 @@ describe('APITests', () => { NetworkStore.resetHasReadRequiredDataFromStorage(); // And queue a Write request while offline + // @ts-expect-error - mocking the parameter API.write('MockCommand', {content: 'value1'}); // Then we should expect the request to get persisted @@ -515,8 +557,11 @@ describe('APITests', () => { expect(NetworkStore.isOffline()).toBe(false); // WHEN we make a request that should be retried, one that should not, and another that should + // @ts-expect-error - mocking the parameter API.write('MockCommandOne'); + // @ts-expect-error - mocking the parameter API.read('MockCommandTwo'); + // @ts-expect-error - mocking the parameter API.write('MockCommandThree'); // THEN the retryable requests should immediately be added to the persisted requests diff --git a/tests/unit/MigrationTest.js b/tests/unit/MigrationTest.ts similarity index 76% rename from tests/unit/MigrationTest.js rename to tests/unit/MigrationTest.ts index 65ab921ac9e1..6d18ec2f0c68 100644 --- a/tests/unit/MigrationTest.js +++ b/tests/unit/MigrationTest.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/naming-convention */ import Onyx from 'react-native-onyx'; import Log from '../../src/libs/Log'; import CheckForPreviousReportActionID from '../../src/libs/migrations/CheckForPreviousReportActionID'; @@ -7,13 +8,13 @@ import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; jest.mock('../../src/libs/getPlatform'); -let LogSpy; +let LogSpy: unknown; describe('Migrations', () => { beforeAll(() => { Onyx.init({keys: ONYXKEYS}); LogSpy = jest.spyOn(Log, 'info'); - Log.serverLoggingCallback = () => {}; + Log.serverLoggingCallback = () => Promise.resolve({requestID: '123'}); return waitForBatchedUpdates(); }); @@ -32,6 +33,7 @@ describe('Migrations', () => { it('Should remove all report actions given that a previousReportActionID does not exist', () => Onyx.multiSet({ [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]: { + // @ts-expect-error Preset necessary values 1: { reportActionID: '1', }, @@ -51,7 +53,7 @@ describe('Migrations', () => { callback: (allReportActions) => { Onyx.disconnect(connectionID); const expectedReportAction = {}; - expect(allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]).toMatchObject(expectedReportAction); + expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]).toMatchObject(expectedReportAction); }, }); })); @@ -59,6 +61,7 @@ describe('Migrations', () => { it('Should not remove any report action given that previousReportActionID exists in first valid report action', () => Onyx.multiSet({ [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]: { + // @ts-expect-error Preset necessary values 1: { reportActionID: '1', previousReportActionID: '0', @@ -87,12 +90,13 @@ describe('Migrations', () => { previousReportActionID: '1', }, }; - expect(allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]).toMatchObject(expectedReportAction); + expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]).toMatchObject(expectedReportAction); }, }); })); it('Should skip zombie report actions and proceed to remove all reportActions given that a previousReportActionID does not exist', () => + // @ts-expect-error Preset necessary values Onyx.multiSet({ [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]: {}, [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`]: null, @@ -117,15 +121,16 @@ describe('Migrations', () => { callback: (allReportActions) => { Onyx.disconnect(connectionID); const expectedReportAction = {}; - expect(allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]).toMatchObject(expectedReportAction); - expect(allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`]).toBeUndefined(); - expect(allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`]).toBeUndefined(); - expect(allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`]).toMatchObject(expectedReportAction); + expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]).toMatchObject(expectedReportAction); + expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`]).toBeUndefined(); + expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`]).toBeUndefined(); + expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`]).toMatchObject(expectedReportAction); }, }); })); it('Should skip zombie report actions and should not remove any report action given that previousReportActionID exists in first valid report action', () => + // @ts-expect-error Preset necessary values Onyx.multiSet({ [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]: {}, [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`]: null, @@ -160,15 +165,16 @@ describe('Migrations', () => { previousReportActionID: '23', }, }; - expect(allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]).toMatchObject(expectedReportAction1); - expect(allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`]).toBeUndefined(); - expect(allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`]).toBeUndefined(); - expect(allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`]).toMatchObject(expectedReportAction4); + expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]).toMatchObject(expectedReportAction1); + expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`]).toBeUndefined(); + expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`]).toBeUndefined(); + expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`]).toMatchObject(expectedReportAction4); }, }); })); it('Should skip if no valid reportActions', () => + // @ts-expect-error Preset necessary values Onyx.multiSet({ [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]: null, [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`]: {}, @@ -184,10 +190,10 @@ describe('Migrations', () => { callback: (allReportActions) => { Onyx.disconnect(connectionID); const expectedReportAction = {}; - expect(allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]).toBeUndefined(); - expect(allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`]).toMatchObject(expectedReportAction); - expect(allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`]).toMatchObject(expectedReportAction); - expect(allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`]).toBeUndefined(); + expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]).toBeUndefined(); + expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`]).toMatchObject(expectedReportAction); + expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`]).toMatchObject(expectedReportAction); + expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`]).toBeUndefined(); }, }); })); @@ -200,6 +206,7 @@ describe('Migrations', () => { )); it('Should move individual draft to a draft collection of report', () => + // @ts-expect-error Preset necessary values Onyx.multiSet({ [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_1`]: 'a', [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_2`]: 'b', @@ -221,16 +228,17 @@ describe('Migrations', () => { 3: 'c', 4: 'd', }; - expect(allReportActionsDrafts[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_1`]).toBeUndefined(); - expect(allReportActionsDrafts[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_2`]).toBeUndefined(); - expect(allReportActionsDrafts[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2_4`]).toBeUndefined(); - expect(allReportActionsDrafts[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1`]).toMatchObject(expectedReportActionDraft1); - expect(allReportActionsDrafts[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2`]).toMatchObject(expectedReportActionDraft2); + expect(allReportActionsDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_1`]).toBeUndefined(); + expect(allReportActionsDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_2`]).toBeUndefined(); + expect(allReportActionsDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2_4`]).toBeUndefined(); + expect(allReportActionsDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1`]).toMatchObject(expectedReportActionDraft1); + expect(allReportActionsDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2`]).toMatchObject(expectedReportActionDraft2); }, }); })); it('Should skip if nothing to migrate', () => + // @ts-expect-error Preset necessary values Onyx.multiSet({ [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_1`]: null, [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_2`]: null, @@ -246,15 +254,16 @@ describe('Migrations', () => { callback: (allReportActions) => { Onyx.disconnect(connectionID); const expectedReportActionDraft = {}; - expect(allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_1`]).toBeUndefined(); - expect(allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_2`]).toBeUndefined(); - expect(allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2_4`]).toBeUndefined(); - expect(allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2`]).toMatchObject(expectedReportActionDraft); + expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_1`]).toBeUndefined(); + expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_2`]).toBeUndefined(); + expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2_4`]).toBeUndefined(); + expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2`]).toMatchObject(expectedReportActionDraft); }, }); })); it("Shouldn't move empty individual draft to a draft collection of report", () => + // @ts-expect-error Preset necessary values Onyx.multiSet({ [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_1`]: '', [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1`]: {}, @@ -266,7 +275,7 @@ describe('Migrations', () => { waitForCollectionCallback: true, callback: (allReportActionsDrafts) => { Onyx.disconnect(connectionID); - expect(allReportActionsDrafts[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_1`]).toBeUndefined(); + expect(allReportActionsDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_1`]).toBeUndefined(); }, }); })); diff --git a/tests/unit/NetworkTest.js b/tests/unit/NetworkTest.ts similarity index 92% rename from tests/unit/NetworkTest.js rename to tests/unit/NetworkTest.ts index 29f5e344b35a..f8b5b6a7d345 100644 --- a/tests/unit/NetworkTest.js +++ b/tests/unit/NetworkTest.ts @@ -1,5 +1,6 @@ -import Onyx from 'react-native-onyx'; -import _ from 'underscore'; +import type {Mock} from 'jest-mock'; +import reactNativeOnyxMock from '../../__mocks__/react-native-onyx'; +// import Onyx from 'react-native-onyx'; import CONST from '../../src/CONST'; import OnyxUpdateManager from '../../src/libs/actions/OnyxUpdateManager'; import * as PersistedRequests from '../../src/libs/actions/PersistedRequests'; @@ -15,6 +16,8 @@ import ONYXKEYS from '../../src/ONYXKEYS'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; +const Onyx = reactNativeOnyxMock; + jest.mock('../../src/libs/Log'); Onyx.init({ @@ -25,7 +28,7 @@ OnyxUpdateManager(); const originalXHR = HttpUtils.xhr; beforeEach(() => { - global.fetch = TestHelper.getGlobalFetchMock(); + global.fetch = TestHelper.getGlobalFetchMock() as typeof fetch; HttpUtils.xhr = originalXHR; MainQueue.clear(); HttpUtils.cancelPendingRequests(); @@ -50,7 +53,7 @@ describe('NetworkTests', () => { const TEST_USER_LOGIN = 'test@testguy.com'; const TEST_USER_ACCOUNT_ID = 1; - let isOffline; + let isOffline: boolean | null = null; Onyx.connect({ key: ONYXKEYS.NETWORK, @@ -67,8 +70,9 @@ describe('NetworkTests', () => { global.fetch = jest.fn().mockRejectedValue(new TypeError(CONST.ERROR.FAILED_TO_FETCH)); const actualXhr = HttpUtils.xhr; - HttpUtils.xhr = jest.fn(); - HttpUtils.xhr + + const mockedXhr = jest.fn(); + mockedXhr .mockImplementationOnce(() => Promise.resolve({ jsonCode: CONST.JSON_CODE.NOT_AUTHENTICATED, @@ -100,6 +104,8 @@ describe('NetworkTests', () => { }), ); + HttpUtils.xhr = mockedXhr; + // This should first trigger re-authentication and then a Failed to fetch PersonalDetails.openPersonalDetails(); return waitForBatchedUpdates() @@ -113,8 +119,8 @@ describe('NetworkTests', () => { }) .then(() => { // Then we will eventually have 1 call to OpenPersonalDetailsPage and 1 calls to Authenticate - const callsToOpenPersonalDetails = _.filter(HttpUtils.xhr.mock.calls, ([command]) => command === 'OpenPersonalDetailsPage'); - const callsToAuthenticate = _.filter(HttpUtils.xhr.mock.calls, ([command]) => command === 'Authenticate'); + const callsToOpenPersonalDetails = (HttpUtils.xhr as Mock).mock.calls.filter(([command]) => command === 'OpenPersonalDetailsPage'); + const callsToAuthenticate = (HttpUtils.xhr as Mock).mock.calls.filter(([command]) => command === 'Authenticate'); expect(callsToOpenPersonalDetails.length).toBe(1); expect(callsToAuthenticate.length).toBe(1); @@ -133,8 +139,8 @@ describe('NetworkTests', () => { // When we sign in return TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN) .then(() => { - HttpUtils.xhr = jest.fn(); - HttpUtils.xhr + const mockedXhr = jest.fn(); + mockedXhr // And mock the first call to openPersonalDetails return with an expired session code .mockImplementationOnce(() => @@ -164,6 +170,8 @@ describe('NetworkTests', () => { }), ); + HttpUtils.xhr = mockedXhr; + // And then make 3 API READ requests in quick succession with an expired authToken and handle the response // It doesn't matter which requests these are really as all the response is mocked we just want to see // that we get re-authenticated @@ -175,8 +183,8 @@ describe('NetworkTests', () => { .then(() => { // We should expect to see the three calls to OpenApp, but only one call to Authenticate. // And we should also see the reconnection callbacks triggered. - const callsToOpenPersonalDetails = _.filter(HttpUtils.xhr.mock.calls, ([command]) => command === 'OpenPersonalDetailsPage'); - const callsToAuthenticate = _.filter(HttpUtils.xhr.mock.calls, ([command]) => command === 'Authenticate'); + const callsToOpenPersonalDetails = (HttpUtils.xhr as Mock).mock.calls.filter(([command]) => command === 'OpenPersonalDetailsPage'); + const callsToAuthenticate = (HttpUtils.xhr as Mock).mock.calls.filter(([command]) => command === 'Authenticate'); expect(callsToOpenPersonalDetails.length).toBe(3); expect(callsToAuthenticate.length).toBe(1); expect(reconnectionCallbacksSpy.mock.calls.length).toBe(3); From 03eae4166948392968284a6c800fd892b2afc67c Mon Sep 17 00:00:00 2001 From: smelaa Date: Thu, 29 Feb 2024 11:41:04 +0100 Subject: [PATCH 145/484] 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 146/484] 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 147/484] 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 )} {!headerMessage && !canSelectMultiple && customListHeader} Date: Thu, 29 Feb 2024 15:51:05 -0300 Subject: [PATCH 150/484] Migrate NVPs to their new keys --- src/ONYXKEYS.ts | 24 +++++++----- src/libs/migrateOnyx.ts | 3 +- src/libs/migrations/NVPMigration.ts | 61 +++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 11 deletions(-) create mode 100644 src/libs/migrations/NVPMigration.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index d4a0b8a21d66..d0b73c963ce1 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -17,7 +17,7 @@ const ONYXKEYS = { ACCOUNT_MANAGER_REPORT_ID: 'accountManagerReportID', /** Boolean flag only true when first set */ - NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER: 'isFirstTimeNewExpensifyUser', + NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER: 'nvp_isFirstTimeNewExpensifyUser', /** Holds an array of client IDs which is used for multi-tabs on web in order to know * which tab is the leader, and which ones are the followers */ @@ -109,22 +109,25 @@ const ONYXKEYS = { NVP_PRIORITY_MODE: 'nvp_priorityMode', /** Contains the users's block expiration (if they have one) */ - NVP_BLOCKED_FROM_CONCIERGE: 'private_blockedFromConcierge', + NVP_BLOCKED_FROM_CONCIERGE: 'nvp_private_blockedFromConcierge', /** A unique identifier that each user has that's used to send notifications */ - NVP_PRIVATE_PUSH_NOTIFICATION_ID: 'private_pushNotificationID', + NVP_PRIVATE_PUSH_NOTIFICATION_ID: 'nvp_private_pushNotificationID', /** The NVP with the last payment method used per policy */ - NVP_LAST_PAYMENT_METHOD: 'nvp_lastPaymentMethod', + NVP_LAST_PAYMENT_METHOD: 'nvp_private_lastPaymentMethod', /** This NVP holds to most recent waypoints that a person has used when creating a distance request */ NVP_RECENT_WAYPOINTS: 'expensify_recentWaypoints', /** This NVP will be `true` if the user has ever dismissed the engagement modal on either OldDot or NewDot. If it becomes true it should stay true forever. */ - NVP_HAS_DISMISSED_IDLE_PANEL: 'hasDismissedIdlePanel', + NVP_HAS_DISMISSED_IDLE_PANEL: 'nvp_hasDismissedIdlePanel', /** This NVP contains the choice that the user made on the engagement modal */ - NVP_INTRO_SELECTED: 'introSelected', + NVP_INTRO_SELECTED: 'nvp_introSelected', + + /** This NVP contains the active policyID */ + NVP_ACTIVE_POLICY_ID: 'nvp_expensify_activePolicyID', /** Does this user have push notifications enabled for this device? */ PUSH_NOTIFICATIONS_ENABLED: 'pushNotificationsEnabled', @@ -146,7 +149,7 @@ const ONYXKEYS = { ONFIDO_APPLICANT_ID: 'onfidoApplicantID', /** Indicates which locale should be used */ - NVP_PREFERRED_LOCALE: 'preferredLocale', + NVP_PREFERRED_LOCALE: 'nvp_preferredLocale', /** User's Expensify Wallet */ USER_WALLET: 'userWallet', @@ -170,7 +173,7 @@ const ONYXKEYS = { CARD_LIST: 'cardList', /** Whether the user has tried focus mode yet */ - NVP_TRY_FOCUS_MODE: 'tryFocusMode', + NVP_TRY_FOCUS_MODE: 'nvp_tryFocusMode', /** Whether the user has been shown the hold educational interstitial yet */ NVP_HOLD_USE_EXPLAINED: 'holdUseExplained', @@ -188,10 +191,10 @@ const ONYXKEYS = { REIMBURSEMENT_ACCOUNT: 'reimbursementAccount', /** Store preferred skintone for emoji */ - PREFERRED_EMOJI_SKIN_TONE: 'preferredEmojiSkinTone', + PREFERRED_EMOJI_SKIN_TONE: 'nvp_expensify_preferredEmojiSkinTone', /** Store frequently used emojis for this user */ - FREQUENTLY_USED_EMOJIS: 'frequentlyUsedEmojis', + FREQUENTLY_USED_EMOJIS: 'expensify_frequentlyUsedEmojis', /** Stores Workspace ID that will be tied to reimbursement account during setup */ REIMBURSEMENT_ACCOUNT_WORKSPACE_ID: 'reimbursementAccountWorkspaceID', @@ -568,6 +571,7 @@ type OnyxValuesMapping = { [ONYXKEYS.LOGS]: Record; [ONYXKEYS.SHOULD_STORE_LOGS]: boolean; [ONYXKEYS.CACHED_PDF_PATHS]: Record; + [ONYXKEYS.NVP_ACTIVE_POLICY_ID]: string; }; type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping; diff --git a/src/libs/migrateOnyx.ts b/src/libs/migrateOnyx.ts index 1202275067a5..5ce899cdd316 100644 --- a/src/libs/migrateOnyx.ts +++ b/src/libs/migrateOnyx.ts @@ -1,5 +1,6 @@ import Log from './Log'; import KeyReportActionsDraftByReportActionID from './migrations/KeyReportActionsDraftByReportActionID'; +import NVPMigration from './migrations/NVPMigration'; import RemoveEmptyReportActionsDrafts from './migrations/RemoveEmptyReportActionsDrafts'; import RenameReceiptFilename from './migrations/RenameReceiptFilename'; import TransactionBackupsToCollection from './migrations/TransactionBackupsToCollection'; @@ -10,7 +11,7 @@ export default function (): Promise { return new Promise((resolve) => { // Add all migrations to an array so they are executed in order - const migrationPromises = [RenameReceiptFilename, KeyReportActionsDraftByReportActionID, TransactionBackupsToCollection, RemoveEmptyReportActionsDrafts]; + const migrationPromises = [RenameReceiptFilename, KeyReportActionsDraftByReportActionID, TransactionBackupsToCollection, RemoveEmptyReportActionsDrafts, NVPMigration]; // 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/libs/migrations/NVPMigration.ts b/src/libs/migrations/NVPMigration.ts new file mode 100644 index 000000000000..1c3465a492a9 --- /dev/null +++ b/src/libs/migrations/NVPMigration.ts @@ -0,0 +1,61 @@ +import after from 'lodash/after'; +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; + +const migrations = { + // eslint-disable-next-line @typescript-eslint/naming-convention + nvp_lastPaymentMethod: ONYXKEYS.NVP_LAST_PAYMENT_METHOD, + isFirstTimeNewExpensifyUser: ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER, + preferredLocale: ONYXKEYS.NVP_PREFERRED_LOCALE, + preferredEmojiSkinTone: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, + frequentlyUsedEmojis: ONYXKEYS.FREQUENTLY_USED_EMOJIS, + // eslint-disable-next-line @typescript-eslint/naming-convention + private_blockedFromConcierge: ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE, + // eslint-disable-next-line @typescript-eslint/naming-convention + private_pushNotificationID: ONYXKEYS.NVP_PRIVATE_PUSH_NOTIFICATION_ID, + tryFocusMode: ONYXKEYS.NVP_TRY_FOCUS_MODE, + introSelected: ONYXKEYS.NVP_INTRO_SELECTED, + hasDismissedIdlePanel: ONYXKEYS.NVP_HAS_DISMISSED_IDLE_PANEL, +}; + +// This migration changes the keys of all the NVP related keys so that they are standardized +export default function () { + return new Promise((resolve) => { + // It's 1 more because activePolicyID is not in the migrations object above as it is nested inside an object + const resolveWhenDone = after(Object.entries(migrations).length + 1, () => resolve()); + + for (const [oldKey, newKey] of Object.entries(migrations)) { + const connectionID = Onyx.connect({ + // @ts-expect-error oldKey is a variable + key: oldKey, + callback: (value) => { + Onyx.disconnect(connectionID); + if (value !== null) { + // @ts-expect-error These keys are variables, so we can't check the type + Onyx.multiSet({ + [newKey]: value, + [oldKey]: null, + }); + } + resolveWhenDone(); + }, + }); + } + const connectionID = Onyx.connect({ + key: ONYXKEYS.ACCOUNT, + callback: (value) => { + Onyx.disconnect(connectionID); + if (value?.activePolicyID) { + const activePolicyID = value.activePolicyID; + const newValue = value; + delete newValue.activePolicyID; + Onyx.multiSet({ + [ONYXKEYS.NVP_ACTIVE_POLICY_ID]: activePolicyID, + [ONYXKEYS.ACCOUNT]: newValue, + }); + } + resolveWhenDone(); + }, + }); + }); +} From c4205502e9c039f5c6a4825052a51b18c1100150 Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Thu, 29 Feb 2024 20:39:56 +0100 Subject: [PATCH 151/484] Fix: Category - Checkbox is clickable outside near the right of checkbox --- src/components/SelectionList/BaseListItem.tsx | 2 +- src/styles/utils/index.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/SelectionList/BaseListItem.tsx b/src/components/SelectionList/BaseListItem.tsx index 98b1999625ee..5ea451c12f11 100644 --- a/src/components/SelectionList/BaseListItem.tsx +++ b/src/components/SelectionList/BaseListItem.tsx @@ -79,7 +79,7 @@ function BaseListItem({ accessibilityLabel={item.text} role={CONST.ROLE.BUTTON} onPress={handleCheckboxPress} - style={StyleUtils.getCheckboxPressableStyle()} + style={[StyleUtils.getCheckboxPressableStyle(), styles.mr3]} > {item.isSelected && ( diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 72719e4795c4..5470d976eafe 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -1481,7 +1481,6 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ getFullscreenCenteredContentStyles: () => [StyleSheet.absoluteFill, styles.justifyContentCenter, styles.alignItemsCenter], getMultiselectListStyles: (isSelected: boolean, isDisabled: boolean): ViewStyle => ({ - ...styles.mr3, ...(isSelected && styles.checkedContainer), ...(isSelected && styles.borderColorFocus), ...(isDisabled && styles.cursorDisabled), From 9ce6a3cf5ff6a901889a83a2e3d4e0a0149f572b Mon Sep 17 00:00:00 2001 From: Ionatan Wiznia Date: Thu, 29 Feb 2024 17:03:22 -0300 Subject: [PATCH 152/484] Remove nvp props from inside account --- src/ONYXKEYS.ts | 4 ++++ src/components/ReferralProgramCTA.tsx | 5 ++--- src/pages/NewChatPage.tsx | 5 ++--- ...poraryForRefactorRequestParticipantsSelector.js | 3 +-- .../MoneyRequestParticipantsSelector.js | 3 +-- src/pages/workspace/WorkspaceNewRoomPage.tsx | 8 +++----- src/types/onyx/Account.ts | 14 +------------- src/types/onyx/DismissedReferralBanners.ts | 11 +++++++++++ src/types/onyx/index.ts | 2 ++ 9 files changed, 27 insertions(+), 28 deletions(-) create mode 100644 src/types/onyx/DismissedReferralBanners.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index d0b73c963ce1..304c091a48a2 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -129,6 +129,9 @@ const ONYXKEYS = { /** This NVP contains the active policyID */ NVP_ACTIVE_POLICY_ID: 'nvp_expensify_activePolicyID', + /** This NVP contains the referral banners the user dismissed */ + NVP_DISMISSED_REFERRAL_BANNERS: 'dismissedReferralBanners', + /** Does this user have push notifications enabled for this device? */ PUSH_NOTIFICATIONS_ENABLED: 'pushNotificationsEnabled', @@ -572,6 +575,7 @@ type OnyxValuesMapping = { [ONYXKEYS.SHOULD_STORE_LOGS]: boolean; [ONYXKEYS.CACHED_PDF_PATHS]: Record; [ONYXKEYS.NVP_ACTIVE_POLICY_ID]: string; + [ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS]: OnyxTypes.DismissedReferralBanners; }; type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping; diff --git a/src/components/ReferralProgramCTA.tsx b/src/components/ReferralProgramCTA.tsx index 6db37ce1320a..40c3c8683578 100644 --- a/src/components/ReferralProgramCTA.tsx +++ b/src/components/ReferralProgramCTA.tsx @@ -8,7 +8,7 @@ import CONST from '@src/CONST'; import Navigation from '@src/libs/Navigation/Navigation'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {DismissedReferralBanners} from '@src/types/onyx/Account'; +import type DismissedReferralBanners from '@src/types/onyx/DismissedReferralBanners'; import Icon from './Icon'; import {Close} from './Icon/Expensicons'; import {PressableWithoutFeedback} from './Pressable'; @@ -82,7 +82,6 @@ function ReferralProgramCTA({referralContentType, dismissedReferralBanners}: Ref export default withOnyx({ dismissedReferralBanners: { - key: ONYXKEYS.ACCOUNT, - selector: (data) => data?.dismissedReferralBanners ?? {}, + key: ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS, }, })(ReferralProgramCTA); diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index 72393e89ae1a..a1de24da12d4 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -22,7 +22,7 @@ import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; -import type {DismissedReferralBanners} from '@src/types/onyx/Account'; +import type DismissedReferralBanners from '@src/types/onyx/DismissedReferralBanners'; type NewChatPageWithOnyxProps = { /** All reports shared with the user */ @@ -287,8 +287,7 @@ NewChatPage.displayName = 'NewChatPage'; export default withOnyx({ dismissedReferralBanners: { - key: ONYXKEYS.ACCOUNT, - selector: (data) => data?.dismissedReferralBanners ?? {}, + key: ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS, }, reports: { key: ONYXKEYS.COLLECTION.REPORT, diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 2865316b7fd5..1c31806086bd 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -360,8 +360,7 @@ MoneyTemporaryForRefactorRequestParticipantsSelector.displayName = 'MoneyTempora export default withOnyx({ dismissedReferralBanners: { - key: ONYXKEYS.ACCOUNT, - selector: (data) => data.dismissedReferralBanners || {}, + key: ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS, }, reports: { key: ONYXKEYS.COLLECTION.REPORT, diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index 3fde970327d7..85feafc76fe8 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -371,8 +371,7 @@ MoneyRequestParticipantsSelector.defaultProps = defaultProps; export default withOnyx({ dismissedReferralBanners: { - key: ONYXKEYS.ACCOUNT, - selector: (data) => data.dismissedReferralBanners || {}, + key: ONYXKEYS.NVP_DISMISSED_REFERRAL_BANNERS, }, reports: { key: ONYXKEYS.COLLECTION.REPORT, diff --git a/src/pages/workspace/WorkspaceNewRoomPage.tsx b/src/pages/workspace/WorkspaceNewRoomPage.tsx index b9236b0e7252..e4d319313136 100644 --- a/src/pages/workspace/WorkspaceNewRoomPage.tsx +++ b/src/pages/workspace/WorkspaceNewRoomPage.tsx @@ -35,7 +35,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {NewRoomForm} from '@src/types/form/NewRoomForm'; import INPUT_IDS from '@src/types/form/NewRoomForm'; -import type {Account, Policy, Report as ReportType, Session} from '@src/types/onyx'; +import type {Policy, Report as ReportType, Session} from '@src/types/onyx'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -53,7 +53,7 @@ type WorkspaceNewRoomPageOnyxProps = { session: OnyxEntry; /** policyID for main workspace */ - activePolicyID: OnyxEntry['activePolicyID']>; + activePolicyID: OnyxEntry>; }; type WorkspaceNewRoomPageProps = WorkspaceNewRoomPageOnyxProps; @@ -144,7 +144,6 @@ function WorkspaceNewRoomPage({policies, reports, formState, session, activePoli return; } Navigation.dismissModal(newRoomReportID); - // eslint-disable-next-line react-hooks/exhaustive-deps -- we just want this to update on changing the form State }, [isLoading, errorFields]); useEffect(() => { @@ -342,8 +341,7 @@ export default withOnyx account?.activePolicyID ?? null, + key: ONYXKEYS.NVP_ACTIVE_POLICY_ID, initialValue: null, }, })(WorkspaceNewRoomPage); diff --git a/src/types/onyx/Account.ts b/src/types/onyx/Account.ts index 534a8ad0f2bc..98ce460a7669 100644 --- a/src/types/onyx/Account.ts +++ b/src/types/onyx/Account.ts @@ -4,14 +4,6 @@ import type * as OnyxCommon from './OnyxCommon'; type TwoFactorAuthStep = ValueOf | ''; -type DismissedReferralBanners = { - [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.MONEY_REQUEST]?: boolean; - [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT]?: boolean; - [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY]?: boolean; - [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND]?: boolean; - [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SHARE_CODE]?: boolean; -}; - type Account = { /** Whether SAML is enabled for the current account */ isSAMLEnabled?: boolean; @@ -64,15 +56,11 @@ type Account = { /** Whether a sign is loading */ isLoading?: boolean; - /** The active policy ID. Initiating a SmartScan will create an expense on this policy by default. */ - activePolicyID?: string; - errors?: OnyxCommon.Errors | null; success?: string; codesAreCopied?: boolean; twoFactorAuthStep?: TwoFactorAuthStep; - dismissedReferralBanners?: DismissedReferralBanners; }; export default Account; -export type {TwoFactorAuthStep, DismissedReferralBanners}; +export type {TwoFactorAuthStep}; diff --git a/src/types/onyx/DismissedReferralBanners.ts b/src/types/onyx/DismissedReferralBanners.ts new file mode 100644 index 000000000000..43fa6472a6ae --- /dev/null +++ b/src/types/onyx/DismissedReferralBanners.ts @@ -0,0 +1,11 @@ +import type CONST from '@src/CONST'; + +type DismissedReferralBanners = { + [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.MONEY_REQUEST]?: boolean; + [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT]?: boolean; + [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY]?: boolean; + [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND]?: boolean; + [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SHARE_CODE]?: boolean; +}; + +export default DismissedReferralBanners; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 6846fc302639..cc9c3cd44831 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -11,6 +11,7 @@ import type Credentials from './Credentials'; import type Currency from './Currency'; import type {CurrencyList} from './Currency'; import type CustomStatusDraft from './CustomStatusDraft'; +import type DismissedReferralBanners from './DismissedReferralBanners'; import type Download from './Download'; import type FrequentlyUsedEmoji from './FrequentlyUsedEmoji'; import type {FundList} from './Fund'; @@ -85,6 +86,7 @@ export type { Currency, CurrencyList, CustomStatusDraft, + DismissedReferralBanners, Download, FrequentlyUsedEmoji, Fund, From 55f816dd080f2aaf5be2c3dfd90c9ffcb6ebfabd Mon Sep 17 00:00:00 2001 From: Ionatan Wiznia Date: Thu, 29 Feb 2024 17:10:46 -0300 Subject: [PATCH 153/484] Fix usage of referral banners in account --- src/libs/actions/User.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 5d089ed6e393..ec5991346872 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -961,11 +961,9 @@ function dismissReferralBanner(type: ValueOf Date: Thu, 29 Feb 2024 19:49:19 -0300 Subject: [PATCH 154/484] Suppress some errors --- src/libs/migrations/NVPMigration.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/libs/migrations/NVPMigration.ts b/src/libs/migrations/NVPMigration.ts index 1c3465a492a9..22bdd4a03615 100644 --- a/src/libs/migrations/NVPMigration.ts +++ b/src/libs/migrations/NVPMigration.ts @@ -45,9 +45,12 @@ export default function () { key: ONYXKEYS.ACCOUNT, callback: (value) => { Onyx.disconnect(connectionID); + // @ts-expect-error we are removing this property, so it is not in the type anymore if (value?.activePolicyID) { + // @ts-expect-error we are removing this property, so it is not in the type anymore const activePolicyID = value.activePolicyID; - const newValue = value; + const newValue = {...value}; + // @ts-expect-error we are removing this property, so it is not in the type anymore delete newValue.activePolicyID; Onyx.multiSet({ [ONYXKEYS.NVP_ACTIVE_POLICY_ID]: activePolicyID, From b17b23cb8306b8820f8d6ab547afb207ec2ab0f3 Mon Sep 17 00:00:00 2001 From: Ionatan Wiznia Date: Thu, 29 Feb 2024 20:05:31 -0300 Subject: [PATCH 155/484] Readd suppression --- src/pages/workspace/WorkspaceNewRoomPage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/workspace/WorkspaceNewRoomPage.tsx b/src/pages/workspace/WorkspaceNewRoomPage.tsx index e4d319313136..9771f8bccae2 100644 --- a/src/pages/workspace/WorkspaceNewRoomPage.tsx +++ b/src/pages/workspace/WorkspaceNewRoomPage.tsx @@ -144,6 +144,7 @@ function WorkspaceNewRoomPage({policies, reports, formState, session, activePoli return; } Navigation.dismissModal(newRoomReportID); + // eslint-disable-next-line react-hooks/exhaustive-deps -- we just want this to update on changing the form State }, [isLoading, errorFields]); useEffect(() => { From 89b2e6e14c1a0db6fa88c4b2251c0ac7b35e43b6 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Fri, 1 Mar 2024 11:26:49 +0530 Subject: [PATCH 156/484] update MoneyRequestParticipantsSelector. Signed-off-by: Krishna Gupta --- .../SelectionList/BaseSelectionList.tsx | 2 +- src/pages/RoomInvitePage.tsx | 2 +- ...yForRefactorRequestParticipantsSelector.js | 42 ++++++++----- .../MoneyRequestParticipantsSelector.js | 63 +++++++++++-------- src/pages/workspace/WorkspaceInvitePage.tsx | 2 +- 5 files changed, 68 insertions(+), 43 deletions(-) diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 9e555b4308b2..c75f8542d901 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -378,7 +378,7 @@ function BaseSelectionList( CONST.KEYBOARD_SHORTCUTS.CTRL_ENTER, (e) => { const focusedOption = flattenedSections.allOptions[focusedIndex]; - if (onConfirm && (flattenedSections.selectedOptions.length || focusedOption)) { + if (onConfirm) { onConfirm(e, focusedOption); return; } diff --git a/src/pages/RoomInvitePage.tsx b/src/pages/RoomInvitePage.tsx index 66d13d496e8c..8be364d9db1c 100644 --- a/src/pages/RoomInvitePage.tsx +++ b/src/pages/RoomInvitePage.tsx @@ -176,7 +176,7 @@ function RoomInvitePage({betas, personalDetails, report, policies}: RoomInvitePa (e?: GestureResponderEvent | KeyboardEvent | undefined, option?: OptionsListUtils.MemberForList) => { const options = [...selectedOptions]; - if (option && e && 'key' in e && e.key === 'Enter') { + if (option && e && 'key' in e && e.key === 'Enter' && !options.length) { const isOptionInList = selectedOptions.some((selectedOption) => selectedOption.login === option?.login); if (option && !isOptionInList) { diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 2865316b7fd5..f7f64b12f7a4 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -188,15 +188,18 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ * * @param {Object} option */ - const addSingleParticipant = (option) => { - onParticipantsAdded([ - { - ..._.pick(option, 'accountID', 'login', 'isPolicyExpenseChat', 'reportID', 'searchText'), - selected: true, - }, - ]); - onFinish(); - }; + const addSingleParticipant = useCallback( + (option) => { + onParticipantsAdded([ + { + ..._.pick(option, 'accountID', 'login', 'isPolicyExpenseChat', 'reportID', 'searchText'), + selected: true, + }, + ]); + onFinish(); + }, + [onFinish, onParticipantsAdded], + ); /** * Removes a selected option from list if already selected. If not already selected add this option to the list. @@ -258,13 +261,22 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ const shouldShowSplitBillErrorMessage = participants.length > 1 && hasPolicyExpenseChatParticipant; const isAllowedToSplit = iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE; - const handleConfirmSelection = useCallback(() => { - if (shouldShowSplitBillErrorMessage) { - return; - } + const handleConfirmSelection = useCallback( + (keyEvent, option) => { + const shouldAddSingleParticipant = option && keyEvent && 'key' in keyEvent && keyEvent.key === 'Enter' && !participants.length; + if (shouldShowSplitBillErrorMessage || (!participants.length && (!option || keyEvent.key !== 'Enter'))) { + return; + } - onFinish(CONST.IOU.TYPE.SPLIT); - }, [shouldShowSplitBillErrorMessage, onFinish]); + if (shouldAddSingleParticipant) { + addSingleParticipant(option); + return; + } + + onFinish(CONST.IOU.TYPE.SPLIT); + }, + [shouldShowSplitBillErrorMessage, onFinish, addSingleParticipant, participants], + ); const footerContent = useMemo( () => ( diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index 3fde970327d7..c55dbeab394b 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -195,25 +195,28 @@ function MoneyRequestParticipantsSelector({ * * @param {Object} option */ - const addSingleParticipant = (option) => { - if (participants.length) { - return; - } - onAddParticipants( - [ - { - accountID: option.accountID, - login: option.login, - isPolicyExpenseChat: option.isPolicyExpenseChat, - reportID: option.reportID, - selected: true, - searchText: option.searchText, - }, - ], - false, - ); - navigateToRequest(); - }; + const addSingleParticipant = useCallback( + (option) => { + if (participants.length) { + return; + } + onAddParticipants( + [ + { + accountID: option.accountID, + login: option.login, + isPolicyExpenseChat: option.isPolicyExpenseChat, + reportID: option.reportID, + selected: true, + searchText: option.searchText, + }, + ], + false, + ); + navigateToRequest(); + }, + [navigateToRequest, onAddParticipants, participants.length], + ); /** * Removes a selected option from list if already selected. If not already selected add this option to the list. @@ -274,13 +277,23 @@ function MoneyRequestParticipantsSelector({ const shouldShowSplitBillErrorMessage = participants.length > 1 && hasPolicyExpenseChatParticipant; const isAllowedToSplit = !isDistanceRequest && iouType !== CONST.IOU.TYPE.SEND; - const handleConfirmSelection = useCallback(() => { - if (shouldShowSplitBillErrorMessage) { - return; - } + const handleConfirmSelection = useCallback( + (keyEvent, option) => { + const shouldAddSingleParticipant = option && keyEvent && 'key' in keyEvent && keyEvent.key === 'Enter' && !participants.length; - navigateToSplit(); - }, [shouldShowSplitBillErrorMessage, navigateToSplit]); + if (shouldShowSplitBillErrorMessage || (!participants.length && (!option || keyEvent.key !== 'Enter'))) { + return; + } + + if (shouldAddSingleParticipant) { + addSingleParticipant(option); + return; + } + + navigateToSplit(); + }, + [shouldShowSplitBillErrorMessage, navigateToSplit, addSingleParticipant, participants.length], + ); const footerContent = useMemo( () => ( diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index b4effc13fa0e..9082dcbf0a80 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -248,7 +248,7 @@ function WorkspaceInvitePage({ const inviteUser = (e?: GestureResponderEvent | KeyboardEvent | undefined, option?: MemberForList) => { const options = [...selectedOptions]; - if (option && e && 'key' in e && e.key === 'Enter') { + if (option && e && 'key' in e && e.key === 'Enter' && !options.length) { const isOptionInList = selectedOptions.some((selectedOption) => selectedOption.login === option?.login); if (option && !isOptionInList) { From 71e17939f6bef57995d9ba94bc82dc06f35dc886 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Fri, 1 Mar 2024 11:33:07 +0530 Subject: [PATCH 157/484] remove redundant checks Signed-off-by: Krishna Gupta --- src/pages/RoomInvitePage.tsx | 3 ++- .../MoneyTemporaryForRefactorRequestParticipantsSelector.js | 4 ++-- .../MoneyRequestParticipantsSelector.js | 4 ++-- src/pages/workspace/WorkspaceInvitePage.tsx | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/pages/RoomInvitePage.tsx b/src/pages/RoomInvitePage.tsx index 8be364d9db1c..2f42220dc0d0 100644 --- a/src/pages/RoomInvitePage.tsx +++ b/src/pages/RoomInvitePage.tsx @@ -176,7 +176,8 @@ function RoomInvitePage({betas, personalDetails, report, policies}: RoomInvitePa (e?: GestureResponderEvent | KeyboardEvent | undefined, option?: OptionsListUtils.MemberForList) => { const options = [...selectedOptions]; - if (option && e && 'key' in e && e.key === 'Enter' && !options.length) { + // if we got + if (option && !options.length) { const isOptionInList = selectedOptions.some((selectedOption) => selectedOption.login === option?.login); if (option && !isOptionInList) { diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index f7f64b12f7a4..abe9ab772b40 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -263,8 +263,8 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ const handleConfirmSelection = useCallback( (keyEvent, option) => { - const shouldAddSingleParticipant = option && keyEvent && 'key' in keyEvent && keyEvent.key === 'Enter' && !participants.length; - if (shouldShowSplitBillErrorMessage || (!participants.length && (!option || keyEvent.key !== 'Enter'))) { + const shouldAddSingleParticipant = option && !participants.length; + if (shouldShowSplitBillErrorMessage || (!participants.length && !option)) { return; } diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index c55dbeab394b..191d80ea99cb 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -279,9 +279,9 @@ function MoneyRequestParticipantsSelector({ const handleConfirmSelection = useCallback( (keyEvent, option) => { - const shouldAddSingleParticipant = option && keyEvent && 'key' in keyEvent && keyEvent.key === 'Enter' && !participants.length; + const shouldAddSingleParticipant = option && !participants.length; - if (shouldShowSplitBillErrorMessage || (!participants.length && (!option || keyEvent.key !== 'Enter'))) { + if (shouldShowSplitBillErrorMessage || (!participants.length && !option)) { return; } diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index 9082dcbf0a80..e174d4a365de 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -248,7 +248,7 @@ function WorkspaceInvitePage({ const inviteUser = (e?: GestureResponderEvent | KeyboardEvent | undefined, option?: MemberForList) => { const options = [...selectedOptions]; - if (option && e && 'key' in e && e.key === 'Enter' && !options.length) { + if (option && !options.length) { const isOptionInList = selectedOptions.some((selectedOption) => selectedOption.login === option?.login); if (option && !isOptionInList) { From 215062897221677b5aa228a9efe17d2ae3129cea Mon Sep 17 00:00:00 2001 From: smelaa Date: Fri, 1 Mar 2024 14:42:55 +0100 Subject: [PATCH 158/484] Wrapping style in StyleProp component --- src/components/VideoPlayer/VideoPlayerControls/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/VideoPlayer/VideoPlayerControls/index.tsx b/src/components/VideoPlayer/VideoPlayerControls/index.tsx index be0290d29e33..28a2dc983b6f 100644 --- a/src/components/VideoPlayer/VideoPlayerControls/index.tsx +++ b/src/components/VideoPlayer/VideoPlayerControls/index.tsx @@ -1,7 +1,7 @@ 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 type {GestureResponderEvent, LayoutChangeEvent, StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import Animated from 'react-native-reanimated'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -23,7 +23,7 @@ type VideoPlayerControlsProps = { isPlaying: boolean; // Defines if component should have small icons and tighter spacing inline small: boolean; - style: ViewStyle; + style: StyleProp; showPopoverMenu: (event?: GestureResponderEvent | KeyboardEvent) => void | Promise; togglePlayCurrentVideo: (event?: GestureResponderEvent | KeyboardEvent) => void | Promise; }; From 39d33deebea4e1a27bf6a83cf58767755576aaf8 Mon Sep 17 00:00:00 2001 From: Ionatan Wiznia Date: Fri, 1 Mar 2024 14:14:34 -0300 Subject: [PATCH 159/484] Fix type errors --- src/components/ReferralProgramCTA.tsx | 8 ++++---- src/pages/NewChatPage.tsx | 5 ++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/components/ReferralProgramCTA.tsx b/src/components/ReferralProgramCTA.tsx index 40c3c8683578..bd6976c84e3d 100644 --- a/src/components/ReferralProgramCTA.tsx +++ b/src/components/ReferralProgramCTA.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import {withOnyx} from 'react-native-onyx'; +import {OnyxEntry, withOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -8,7 +8,7 @@ import CONST from '@src/CONST'; import Navigation from '@src/libs/Navigation/Navigation'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type DismissedReferralBanners from '@src/types/onyx/DismissedReferralBanners'; +import type * as OnyxTypes from '@src/types/onyx'; import Icon from './Icon'; import {Close} from './Icon/Expensicons'; import {PressableWithoutFeedback} from './Pressable'; @@ -16,7 +16,7 @@ import Text from './Text'; import Tooltip from './Tooltip'; type ReferralProgramCTAOnyxProps = { - dismissedReferralBanners: DismissedReferralBanners; + dismissedReferralBanners: OnyxEntry; }; type ReferralProgramCTAProps = ReferralProgramCTAOnyxProps & { @@ -36,7 +36,7 @@ function ReferralProgramCTA({referralContentType, dismissedReferralBanners}: Ref User.dismissReferralBanner(referralContentType); }; - if (!referralContentType || dismissedReferralBanners[referralContentType]) { + if (!referralContentType || dismissedReferralBanners?.[referralContentType]) { return null; } diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index a1de24da12d4..f4eccd52c78e 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -22,7 +22,6 @@ import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; -import type DismissedReferralBanners from '@src/types/onyx/DismissedReferralBanners'; type NewChatPageWithOnyxProps = { /** All reports shared with the user */ @@ -34,7 +33,7 @@ type NewChatPageWithOnyxProps = { betas: OnyxEntry; /** An object that holds data about which referral banners have been dismissed */ - dismissedReferralBanners: DismissedReferralBanners; + dismissedReferralBanners: OnyxEntry; /** Whether we are searching for reports in the server */ isSearchingForReports: OnyxEntry; @@ -265,7 +264,7 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, isSearchingF shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} shouldShowOptions={isOptionsDataReady && didScreenTransitionEnd} shouldShowConfirmButton - shouldShowReferralCTA={!dismissedReferralBanners[CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT]} + shouldShowReferralCTA={!dismissedReferralBanners?.[CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT]} referralContentType={CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT} confirmButtonText={selectedOptions.length > 1 ? translate('newChatPage.createGroup') : translate('newChatPage.createChat')} textInputAlert={isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''} From 3053b96a9432b9f5161bcfd3a09699e73f8fc86a Mon Sep 17 00:00:00 2001 From: Ionatan Wiznia Date: Fri, 1 Mar 2024 14:25:28 -0300 Subject: [PATCH 160/484] More lints --- src/components/ReferralProgramCTA.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/ReferralProgramCTA.tsx b/src/components/ReferralProgramCTA.tsx index bd6976c84e3d..c93b75bf11ad 100644 --- a/src/components/ReferralProgramCTA.tsx +++ b/src/components/ReferralProgramCTA.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import {OnyxEntry, withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; From 1b685c9eefb92cd25ea3b2469fa42d973b061c2e Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Fri, 1 Mar 2024 16:39:03 -0500 Subject: [PATCH 161/484] use timestamp value for deleted --- src/libs/ReportActionsUtils.ts | 2 +- src/types/onyx/ReportAction.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 6dbcda1bfc30..0fc6af695ac4 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -101,7 +101,7 @@ function isDeletedAction(reportAction: OnyxEntry): boolean { diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index a45d49ea06dd..fa14f24daf71 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -71,7 +71,8 @@ type Message = { /** resolution for actionable mention whisper */ resolution?: ValueOf | null; - isDeleted?: boolean; + /** The time this report action was deleted */ + deleted?: string; }; type ImageMetadata = { From a197c1dd553e71a1034c17b1a7bf3137bd3b26da Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Fri, 1 Mar 2024 17:10:16 -0500 Subject: [PATCH 162/484] remove old comment --- src/libs/ReportActionsUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 0fc6af695ac4..4899f2181a5a 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -95,7 +95,6 @@ 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 ?? []; // A legacy deleted comment has either an empty array or an object with html field with empty string as value From 94452f5d83510bf6dec19be805d2a0b1e492ca2c Mon Sep 17 00:00:00 2001 From: rayane-djouah <77965000+rayane-djouah@users.noreply.github.com> Date: Mon, 4 Mar 2024 01:24:19 +0100 Subject: [PATCH 163/484] line up checkboxes --- src/components/SelectionList/BaseSelectionList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 4e19cba00b2f..cde7eb775f23 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -433,7 +433,7 @@ function BaseSelectionList( ) : ( <> {!headerMessage && canSelectMultiple && shouldShowSelectAll && ( - + Date: Mon, 4 Mar 2024 07:41:21 +0530 Subject: [PATCH 164/484] Removed duplicate function --- src/libs/ReportUtils.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 11a32aa45b8d..2834240ecd82 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4313,10 +4313,6 @@ 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: From 7517fbf5cf3020add121de83952989dc47c57b9e Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Mon, 4 Mar 2024 08:20:26 +0530 Subject: [PATCH 165/484] fixed bad merge commit --- .../MoneyTemporaryForRefactorRequestConfirmationList.js | 2 +- src/pages/iou/request/step/IOURequestStepConfirmation.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 29ab4d53c55f..629f74205046 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -372,7 +372,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const splitOrRequestOptions = useMemo(() => { let text; if (isTypeTrackExpense) { - text = "Track Expense"; + text = 'Track Expense'; } else if (isTypeSplit && iouAmount === 0) { text = translate('iou.split'); } else if ((receiptPath && isTypeRequest) || isDistanceRequestWithPendingRoute) { diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.js b/src/pages/iou/request/step/IOURequestStepConfirmation.js index 73ec541fba4e..de5c6811d277 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.js +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.js @@ -104,8 +104,7 @@ function IOURequestStepConfirmation({ return 'Track Expense'; } return translate(TransactionUtils.getHeaderTitleTranslationKey(transaction)); - } - , [iouType, transaction, translate]); + }, [iouType, transaction, translate]); const participants = useMemo( () => _.map(transaction.participants, (participant) => { From 6c6690cae4f420e8d5b8e14fc6c417e743fdda36 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Mon, 4 Mar 2024 08:20:48 +0530 Subject: [PATCH 166/484] fixed bad merge commit --- src/libs/Permissions.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 37ef44b80af9..4fef0f15ae49 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -28,6 +28,7 @@ function canUseViolations(betas: OnyxEntry): boolean { function canUseTrackExpense(betas: OnyxEntry): boolean { return !!betas?.includes(CONST.BETAS.TRACK_EXPENSE) || canUseAllBetas(betas); +} function canUseWorkflowsDelayedSubmission(betas: OnyxEntry): boolean { return !!betas?.includes(CONST.BETAS.WORKFLOWS_DELAYED_SUBMISSION) || canUseAllBetas(betas); From d180fd24984f0c92a17b077e1009e048017b9899 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Mon, 4 Mar 2024 14:48:11 +0530 Subject: [PATCH 167/484] revert changes in RoomInvitePage & WorkspaceInvitePage. Signed-off-by: Krishna Gupta --- src/pages/RoomInvitePage.tsx | 52 +++++++-------------- src/pages/workspace/WorkspaceInvitePage.tsx | 26 ++++------- 2 files changed, 26 insertions(+), 52 deletions(-) diff --git a/src/pages/RoomInvitePage.tsx b/src/pages/RoomInvitePage.tsx index 06c58f33c812..482ff828e6a8 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 {GestureResponderEvent, SectionListData} from 'react-native'; +import type {SectionListData} from 'react-native'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; @@ -164,7 +164,7 @@ function RoomInvitePage({betas, personalDetails, report, policies}: RoomInvitePa [selectedOptions], ); - const validate = useCallback((options: ReportUtils.OptionData[]) => options.length > 0, []); + const validate = useCallback(() => selectedOptions.length > 0, [selectedOptions]); // Non policy members should not be able to view the participants of a room const reportID = report?.reportID; @@ -172,40 +172,24 @@ function RoomInvitePage({betas, personalDetails, report, policies}: RoomInvitePa 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( - (e?: GestureResponderEvent | KeyboardEvent | undefined, option?: OptionsListUtils.MemberForList) => { - const options = [...selectedOptions]; - - // if we got - if (option && !options.length) { - const isOptionInList = selectedOptions.some((selectedOption) => selectedOption.login === option?.login); - - if (option && !isOptionInList) { - toggleOption(option); - options.push(option); - } - } - - if (!validate(options)) { + 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) { return; } - - 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], - ); + invitedEmailsToAccountIDs[login] = Number(accountID); + }); + if (reportID) { + Report.inviteToRoom(reportID, invitedEmailsToAccountIDs); + } + Navigation.navigate(backRoute); + }, [selectedOptions, backRoute, reportID, validate]); const headerMessage = useMemo(() => { const searchValue = searchTerm.trim().toLowerCase(); diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index 14c391939005..67bf6f8064da 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -1,7 +1,7 @@ import {useNavigation} from '@react-navigation/native'; import type {StackNavigationProp, StackScreenProps} from '@react-navigation/stack'; import React, {useEffect, useMemo, useState} from 'react'; -import type {GestureResponderEvent, SectionListData} from 'react-native'; +import type {SectionListData} from 'react-native'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; @@ -235,9 +235,9 @@ function WorkspaceInvitePage({ setSelectedOptions(newSelectedOptions); }; - const validate = (options: OptionsListUtils.MemberForList[]): boolean => { + const validate = (): boolean => { const errors: Errors = {}; - if (options.length <= 0) { + if (selectedOptions.length <= 0) { errors.noUserSelected = 'true'; } @@ -245,25 +245,15 @@ function WorkspaceInvitePage({ return isEmptyObject(errors); }; - const inviteUser = (e?: GestureResponderEvent | KeyboardEvent | undefined, option?: MemberForList) => { - const options = [...selectedOptions]; - if (option && !options.length) { - const isOptionInList = selectedOptions.some((selectedOption) => selectedOption.login === option?.login); - - if (option && !isOptionInList) { - toggleOption(option); - options.push(option); - } - } - - if (!validate(options)) { + const inviteUser = () => { + if (!validate()) { return; } const invitedEmailsToAccountIDs: InvitedEmailsToAccountIDs = {}; - options.forEach((selectedOption) => { - const login = selectedOption.login ?? ''; - const accountID = selectedOption.accountID ?? ''; + selectedOptions.forEach((option) => { + const login = option.login ?? ''; + const accountID = option.accountID ?? ''; if (!login.toLowerCase().trim() || !accountID) { return; } From 24b28ee2aae23d58e4abd5d00b351aff8bb6380e Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Mon, 4 Mar 2024 14:49:01 +0530 Subject: [PATCH 168/484] minor spacing fix. Signed-off-by: Krishna Gupta --- src/pages/RoomInvitePage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/RoomInvitePage.tsx b/src/pages/RoomInvitePage.tsx index 482ff828e6a8..7bcd64397e20 100644 --- a/src/pages/RoomInvitePage.tsx +++ b/src/pages/RoomInvitePage.tsx @@ -171,7 +171,6 @@ function RoomInvitePage({betas, personalDetails, report, policies}: RoomInvitePa 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; From cf1aa20d91c338bedb96cc326ce8997bbc74beb0 Mon Sep 17 00:00:00 2001 From: Aswin S Date: Mon, 4 Mar 2024 15:11:32 +0530 Subject: [PATCH 169/484] fix: revert removal of onMoveShouldSetPanResponder --- src/components/SwipeInterceptPanResponder.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/SwipeInterceptPanResponder.tsx b/src/components/SwipeInterceptPanResponder.tsx index 48cfe4f90c5c..e778f0c49e54 100644 --- a/src/components/SwipeInterceptPanResponder.tsx +++ b/src/components/SwipeInterceptPanResponder.tsx @@ -1,7 +1,8 @@ -import {PanResponder} from 'react-native'; +import { PanResponder } from 'react-native'; const SwipeInterceptPanResponder = PanResponder.create({ onStartShouldSetPanResponder: () => true, + onMoveShouldSetPanResponder: () => true, onPanResponderTerminationRequest: () => false, }); From aa4d31ab0422c54ee43bf3aa9b8aa925fd19eb03 Mon Sep 17 00:00:00 2001 From: Aswin S Date: Mon, 4 Mar 2024 15:24:58 +0530 Subject: [PATCH 170/484] fix: clean lint --- src/components/SwipeInterceptPanResponder.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SwipeInterceptPanResponder.tsx b/src/components/SwipeInterceptPanResponder.tsx index e778f0c49e54..6a3d14b3b24b 100644 --- a/src/components/SwipeInterceptPanResponder.tsx +++ b/src/components/SwipeInterceptPanResponder.tsx @@ -1,4 +1,4 @@ -import { PanResponder } from 'react-native'; +import {PanResponder} from 'react-native'; const SwipeInterceptPanResponder = PanResponder.create({ onStartShouldSetPanResponder: () => true, From 8215b5377db03124eed0e166545c5cd2f9d16605 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Mon, 4 Mar 2024 12:57:26 +0100 Subject: [PATCH 171/484] address comments --- src/types/onyx/ReportAction.ts | 6 +- src/types/onyx/ReportActionsDrafts.ts | 5 + tests/actions/ReportTest.ts | 45 +++-- tests/unit/APITest.ts | 50 ++--- tests/unit/MigrationTest.ts | 252 +++++++++++++++----------- tests/unit/NetworkTest.ts | 35 ++-- 6 files changed, 228 insertions(+), 165 deletions(-) diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index bb5bf50ec6cf..0971fb6b77e1 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -2,6 +2,8 @@ import type {ValueOf} from 'type-fest'; import type {FileObject} from '@components/AttachmentModal'; import type {AvatarSource} from '@libs/UserUtils'; import type CONST from '@src/CONST'; +import type ONYXKEYS from '@src/ONYXKEYS'; +import type CollectionDataSet from '@src/types/utils/CollectionDataSet'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import type * as OnyxCommon from './OnyxCommon'; import type {Decision, Reaction} from './OriginalMessage'; @@ -224,5 +226,7 @@ type ReportAction = ReportActionBase & OriginalMessage; type ReportActions = Record; +type ReportActionCollectionDataSet = CollectionDataSet; + export default ReportAction; -export type {ReportActions, ReportActionBase, Message, LinkMetadata, OriginalMessage}; +export type {ReportActions, ReportActionBase, Message, LinkMetadata, OriginalMessage, ReportActionCollectionDataSet}; diff --git a/src/types/onyx/ReportActionsDrafts.ts b/src/types/onyx/ReportActionsDrafts.ts index 70d16c62a3bc..e4c51c61ed25 100644 --- a/src/types/onyx/ReportActionsDrafts.ts +++ b/src/types/onyx/ReportActionsDrafts.ts @@ -1,5 +1,10 @@ +import type ONYXKEYS from '@src/ONYXKEYS'; +import type CollectionDataSet from '@src/types/utils/CollectionDataSet'; import type ReportActionsDraft from './ReportActionsDraft'; type ReportActionsDrafts = Record; +type ReportActionsDraftCollectionDataSet = CollectionDataSet; + export default ReportActionsDrafts; +export type {ReportActionsDraftCollectionDataSet}; diff --git a/tests/actions/ReportTest.ts b/tests/actions/ReportTest.ts index 43ceaaad607e..251d26932128 100644 --- a/tests/actions/ReportTest.ts +++ b/tests/actions/ReportTest.ts @@ -3,17 +3,17 @@ import {afterEach, beforeAll, beforeEach, describe, expect, it} from '@jest/glob import {utcToZonedTime} from 'date-fns-tz'; import Onyx from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import CONST from '@src/CONST'; +import OnyxUpdateManager from '@src/libs/actions/OnyxUpdateManager'; +import * as PersistedRequests from '@src/libs/actions/PersistedRequests'; +import * as Report from '@src/libs/actions/Report'; +import * as User from '@src/libs/actions/User'; +import DateUtils from '@src/libs/DateUtils'; +import Log from '@src/libs/Log'; +import * as SequentialQueue from '@src/libs/Network/SequentialQueue'; +import * as ReportUtils from '@src/libs/ReportUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; -import CONST from '../../src/CONST'; -import OnyxUpdateManager from '../../src/libs/actions/OnyxUpdateManager'; -import * as PersistedRequests from '../../src/libs/actions/PersistedRequests'; -import * as Report from '../../src/libs/actions/Report'; -import * as User from '../../src/libs/actions/User'; -import DateUtils from '../../src/libs/DateUtils'; -import Log from '../../src/libs/Log'; -import * as SequentialQueue from '../../src/libs/Network/SequentialQueue'; -import * as ReportUtils from '../../src/libs/ReportUtils'; -import ONYXKEYS from '../../src/ONYXKEYS'; import getIsUsingFakeTimers from '../utils/getIsUsingFakeTimers'; import PusherHelper from '../utils/PusherHelper'; import * as TestHelper from '../utils/TestHelper'; @@ -21,8 +21,8 @@ import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import waitForNetworkPromises from '../utils/waitForNetworkPromises'; const UTC = 'UTC'; -jest.mock('../../src/libs/actions/Report', () => { - const originalModule: typeof Report = jest.requireActual('../../src/libs/actions/Report'); +jest.mock('@src/libs/actions/Report', () => { + const originalModule = jest.requireActual('@src/libs/actions/Report'); return { ...originalModule, @@ -36,7 +36,6 @@ describe('actions/Report', () => { PusherHelper.setup(); Onyx.init({ keys: ONYXKEYS, - // registerStorageEventListener: () => {}, }); }); @@ -53,7 +52,8 @@ describe('actions/Report', () => { afterEach(PusherHelper.teardown); it('should store a new report action in Onyx when onyxApiUpdate event is handled via Pusher', () => { - global.fetch = TestHelper.getGlobalFetchMock() as typeof fetch; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + global.fetch = TestHelper.getGlobalFetchMock(); const TEST_USER_ACCOUNT_ID = 1; const TEST_USER_LOGIN = 'test@test.com'; @@ -89,7 +89,7 @@ describe('actions/Report', () => { return waitForBatchedUpdates(); }) .then(() => { - const resultAction: OnyxEntry = Object.values(reportActions ?? [])[0]; + const resultAction: OnyxEntry = Object.values(reportActions ?? {})[0]; reportActionID = resultAction.reportActionID; expect(resultAction.message).toEqual(REPORT_ACTION.message); @@ -168,7 +168,8 @@ describe('actions/Report', () => { return TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN) .then(() => TestHelper.setPersonalDetails(TEST_USER_LOGIN, TEST_USER_ACCOUNT_ID)) .then(() => { - global.fetch = TestHelper.getGlobalFetchMock() as typeof fetch; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + global.fetch = TestHelper.getGlobalFetchMock(); // WHEN we add enough logs to send a packet for (let i = 0; i <= LOGGER_MAX_LOG_LINES; i++) { @@ -194,7 +195,8 @@ describe('actions/Report', () => { it('should be updated correctly when new comments are added, deleted or marked as unread', () => { jest.useFakeTimers(); - global.fetch = TestHelper.getGlobalFetchMock() as typeof fetch; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + global.fetch = TestHelper.getGlobalFetchMock(); const REPORT_ID = '1'; let report: OnyxEntry; let reportActionCreatedDate: string; @@ -427,7 +429,8 @@ describe('actions/Report', () => { * already in the comment and the user deleted it on purpose. */ - global.fetch = TestHelper.getGlobalFetchMock() as typeof fetch; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + global.fetch = TestHelper.getGlobalFetchMock(); // User edits comment to add link // We should generate link @@ -539,7 +542,8 @@ describe('actions/Report', () => { }); it('should properly toggle reactions on a message', () => { - global.fetch = TestHelper.getGlobalFetchMock() as typeof fetch; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + global.fetch = TestHelper.getGlobalFetchMock(); const TEST_USER_ACCOUNT_ID = 1; const TEST_USER_LOGIN = 'test@test.com'; @@ -659,7 +663,8 @@ describe('actions/Report', () => { }); it("shouldn't add the same reaction twice when changing preferred skin color and reaction doesn't support skin colors", () => { - global.fetch = TestHelper.getGlobalFetchMock() as typeof fetch; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + global.fetch = TestHelper.getGlobalFetchMock(); const TEST_USER_ACCOUNT_ID = 1; const TEST_USER_LOGIN = 'test@test.com'; diff --git a/tests/unit/APITest.ts b/tests/unit/APITest.ts index 9c94730fb4cc..359288b2a1ef 100644 --- a/tests/unit/APITest.ts +++ b/tests/unit/APITest.ts @@ -1,23 +1,23 @@ -// import Onyx from 'react-native-onyx'; +import MockedOnyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; -import reactNativeOnyxMock from '../../__mocks__/react-native-onyx'; -import CONST from '../../src/CONST'; -import * as PersistedRequests from '../../src/libs/actions/PersistedRequests'; -import * as API from '../../src/libs/API'; -import HttpUtils from '../../src/libs/HttpUtils'; -import * as MainQueue from '../../src/libs/Network/MainQueue'; -import * as NetworkStore from '../../src/libs/Network/NetworkStore'; -import * as SequentialQueue from '../../src/libs/Network/SequentialQueue'; -import * as Request from '../../src/libs/Request'; -import * as RequestThrottle from '../../src/libs/RequestThrottle'; -import ONYXKEYS from '../../src/ONYXKEYS'; +import CONST from '@src/CONST'; +import * as PersistedRequests from '@src/libs/actions/PersistedRequests'; +import * as API from '@src/libs/API'; +import HttpUtils from '@src/libs/HttpUtils'; +import * as MainQueue from '@src/libs/Network/MainQueue'; +import * as NetworkStore from '@src/libs/Network/NetworkStore'; +import * as SequentialQueue from '@src/libs/Network/SequentialQueue'; +import * as Request from '@src/libs/Request'; +import * as RequestThrottle from '@src/libs/RequestThrottle'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type ReactNativeOnyxMock from '../../__mocks__/react-native-onyx'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import waitForNetworkPromises from '../utils/waitForNetworkPromises'; -const Onyx = reactNativeOnyxMock; +const Onyx = MockedOnyx as typeof ReactNativeOnyxMock; -jest.mock('../../src/libs/Log'); +jest.mock('@src/libs/Log'); Onyx.init({ keys: ONYXKEYS, @@ -27,14 +27,21 @@ type Response = { ok?: boolean; status?: ValueOf | ValueOf; jsonCode?: ValueOf; + json?: () => Promise; title?: ValueOf; type?: ValueOf; }; +type XhrCalls = Array<{ + resolve: (value: Response | PromiseLike) => void; + reject: (value: unknown) => void; +}>; + const originalXHR = HttpUtils.xhr; beforeEach(() => { - global.fetch = TestHelper.getGlobalFetchMock() as typeof fetch; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + global.fetch = TestHelper.getGlobalFetchMock(); HttpUtils.xhr = originalXHR; MainQueue.clear(); HttpUtils.cancelPendingRequests(); @@ -136,10 +143,7 @@ describe('APITests', () => { test('Write request should not be cleared until a backend response occurs', () => { // We're setting up xhr handler that will resolve calls programmatically - const xhrCalls: Array<{ - resolve: (value: Response | PromiseLike) => void; - reject: (value: unknown) => void; - }> = []; + const xhrCalls: XhrCalls = []; const promises: Array> = []; jest.spyOn(HttpUtils, 'xhr').mockImplementation(() => { @@ -205,8 +209,8 @@ describe('APITests', () => { // Given a retry response create a mock and run some expectations for retrying requests - const retryExpectations = (Response: Response) => { - const successfulResponse = { + const retryExpectations = (response: Response) => { + const successfulResponse: Response = { ok: true, jsonCode: CONST.JSON_CODE.SUCCESS, // We have to mock response.json() too @@ -214,7 +218,7 @@ describe('APITests', () => { }; // Given a mock where a retry response is returned twice before a successful response - global.fetch = jest.fn().mockResolvedValueOnce(Response).mockResolvedValueOnce(Response).mockResolvedValueOnce(successfulResponse); + global.fetch = jest.fn().mockResolvedValueOnce(response).mockResolvedValueOnce(response).mockResolvedValueOnce(successfulResponse); // Given we have a request made while we're offline return ( @@ -275,7 +279,7 @@ describe('APITests', () => { test('write requests are retried when Auth is down', () => { // Given the response data returned when auth is down - const responseData = { + const responseData: Response = { ok: true, status: CONST.JSON_CODE.SUCCESS, jsonCode: CONST.JSON_CODE.EXP_ERROR, diff --git a/tests/unit/MigrationTest.ts b/tests/unit/MigrationTest.ts index 6d18ec2f0c68..bd1f79b8f838 100644 --- a/tests/unit/MigrationTest.ts +++ b/tests/unit/MigrationTest.ts @@ -1,14 +1,17 @@ /* eslint-disable @typescript-eslint/naming-convention */ import Onyx from 'react-native-onyx'; -import Log from '../../src/libs/Log'; -import CheckForPreviousReportActionID from '../../src/libs/migrations/CheckForPreviousReportActionID'; -import KeyReportActionsDraftByReportActionID from '../../src/libs/migrations/KeyReportActionsDraftByReportActionID'; -import ONYXKEYS from '../../src/ONYXKEYS'; +import CONST from '@src/CONST'; +import Log from '@src/libs/Log'; +import CheckForPreviousReportActionID from '@src/libs/migrations/CheckForPreviousReportActionID'; +import KeyReportActionsDraftByReportActionID from '@src/libs/migrations/KeyReportActionsDraftByReportActionID'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {ReportActionCollectionDataSet} from '@src/types/onyx/ReportAction'; +import type {ReportActionsDraftCollectionDataSet} from '@src/types/onyx/ReportActionsDrafts'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; -jest.mock('../../src/libs/getPlatform'); +jest.mock('@src/libs/getPlatform'); -let LogSpy: unknown; +let LogSpy: jest.SpyInstance>; describe('Migrations', () => { beforeAll(() => { @@ -30,18 +33,23 @@ describe('Migrations', () => { expect(LogSpy).toHaveBeenCalledWith('[Migrate Onyx] Skipped migration CheckForPreviousReportActionID because there were no reportActions'), )); - it('Should remove all report actions given that a previousReportActionID does not exist', () => - Onyx.multiSet({ - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]: { - // @ts-expect-error Preset necessary values - 1: { - reportActionID: '1', - }, - 2: { - reportActionID: '2', - }, + it('Should remove all report actions given that a previousReportActionID does not exist', () => { + const setQueries: ReportActionCollectionDataSet = {}; + + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`] = { + 1: { + reportActionID: '1', + created: '', + actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, + }, + 2: { + reportActionID: '2', + created: '', + actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, }, - }) + }; + + return Onyx.multiSet(setQueries) .then(CheckForPreviousReportActionID) .then(() => { expect(LogSpy).toHaveBeenCalledWith( @@ -56,22 +64,28 @@ describe('Migrations', () => { expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]).toMatchObject(expectedReportAction); }, }); - })); - - it('Should not remove any report action given that previousReportActionID exists in first valid report action', () => - Onyx.multiSet({ - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]: { - // @ts-expect-error Preset necessary values - 1: { - reportActionID: '1', - previousReportActionID: '0', - }, - 2: { - reportActionID: '2', - previousReportActionID: '1', - }, + }); + }); + + it('Should not remove any report action given that previousReportActionID exists in first valid report action', () => { + const setQueries: ReportActionCollectionDataSet = {}; + + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`] = { + 1: { + reportActionID: '1', + previousReportActionID: '0', + created: '', + actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, + }, + 2: { + reportActionID: '2', + previousReportActionID: '1', + created: '', + actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, }, - }) + }; + + return Onyx.multiSet(setQueries) .then(CheckForPreviousReportActionID) .then(() => { expect(LogSpy).toHaveBeenCalledWith('[Migrate Onyx] CheckForPreviousReportActionID Migration: previousReportActionID found. Migration complete'); @@ -93,23 +107,33 @@ describe('Migrations', () => { expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]).toMatchObject(expectedReportAction); }, }); - })); - - it('Should skip zombie report actions and proceed to remove all reportActions given that a previousReportActionID does not exist', () => - // @ts-expect-error Preset necessary values - Onyx.multiSet({ - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]: {}, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`]: null, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`]: null, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`]: { - 1: { - reportActionID: '1', - }, - 2: { - reportActionID: '2', - }, + }); + }); + + it('Should skip zombie report actions and proceed to remove all reportActions given that a previousReportActionID does not exist', () => { + const setQueries: ReportActionCollectionDataSet = {}; + + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`] = {}; + + // @ts-expect-error preset null value + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`] = null; + // @ts-expect-error preset null value + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`] = null; + + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`] = { + 1: { + reportActionID: '1', + created: '', + actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, + }, + 2: { + reportActionID: '2', + created: '', + actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, }, - }) + }; + + return Onyx.multiSet(setQueries) .then(CheckForPreviousReportActionID) .then(() => { expect(LogSpy).toHaveBeenCalledWith( @@ -127,25 +151,34 @@ describe('Migrations', () => { expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`]).toMatchObject(expectedReportAction); }, }); - })); - - it('Should skip zombie report actions and should not remove any report action given that previousReportActionID exists in first valid report action', () => - // @ts-expect-error Preset necessary values - Onyx.multiSet({ - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]: {}, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`]: null, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`]: null, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`]: { - 1: { - reportActionID: '1', - previousReportActionID: '10', - }, - 2: { - reportActionID: '2', - previousReportActionID: '23', - }, + }); + }); + + it('Should skip zombie report actions and should not remove any report action given that previousReportActionID exists in first valid report action', () => { + const setQueries: ReportActionCollectionDataSet = {}; + + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`] = {}; + // @ts-expect-error preset null value + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`] = null; + // @ts-expect-error preset null value + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`] = null; + + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`] = { + 1: { + reportActionID: '1', + previousReportActionID: '10', + created: '', + actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, + }, + 2: { + reportActionID: '2', + previousReportActionID: '23', + created: '', + actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, }, - }) + }; + + Onyx.multiSet(setQueries) .then(CheckForPreviousReportActionID) .then(() => { expect(LogSpy).toHaveBeenCalledWith('[Migrate Onyx] CheckForPreviousReportActionID Migration: previousReportActionID found. Migration complete'); @@ -171,16 +204,20 @@ describe('Migrations', () => { expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`]).toMatchObject(expectedReportAction4); }, }); - })); - - it('Should skip if no valid reportActions', () => - // @ts-expect-error Preset necessary values - Onyx.multiSet({ - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]: null, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`]: {}, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`]: {}, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`]: null, - }) + }); + }); + + it('Should skip if no valid reportActions', () => { + const setQueries: ReportActionCollectionDataSet = {}; + + // @ts-expect-error preset null value + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`] = null; + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`] = {}; + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`] = {}; + // @ts-expect-error preset null value + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`] = null; + + Onyx.multiSet(setQueries) .then(CheckForPreviousReportActionID) .then(() => { expect(LogSpy).toHaveBeenCalledWith('[Migrate Onyx] Skipped migration CheckForPreviousReportActionID because there were no valid reportActions'); @@ -196,7 +233,8 @@ describe('Migrations', () => { expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`]).toBeUndefined(); }, }); - })); + }); + }); }); describe('KeyReportActionsDraftByReportActionID', () => { @@ -205,14 +243,15 @@ describe('Migrations', () => { expect(LogSpy).toHaveBeenCalledWith('[Migrate Onyx] Skipped migration KeyReportActionsDraftByReportActionID because there were no reportActionsDrafts'), )); - it('Should move individual draft to a draft collection of report', () => - // @ts-expect-error Preset necessary values - Onyx.multiSet({ - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_1`]: 'a', - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_2`]: 'b', - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2`]: {3: 'c'}, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2_4`]: 'd', - }) + it('Should move individual draft to a draft collection of report', () => { + const setQueries: ReportActionsDraftCollectionDataSet = {}; + + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_1`] = 'a'; + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_2`] = 'b'; + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2`] = {3: 'c'}; + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2_4`] = 'd'; + + Onyx.multiSet(setQueries) .then(KeyReportActionsDraftByReportActionID) .then(() => { const connectionID = Onyx.connect({ @@ -235,16 +274,18 @@ describe('Migrations', () => { expect(allReportActionsDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2`]).toMatchObject(expectedReportActionDraft2); }, }); - })); - - it('Should skip if nothing to migrate', () => - // @ts-expect-error Preset necessary values - Onyx.multiSet({ - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_1`]: null, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_2`]: null, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2`]: {}, - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2_4`]: null, - }) + }); + }); + + it('Should skip if nothing to migrate', () => { + const setQueries: ReportActionsDraftCollectionDataSet = {}; + + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_1`] = null; + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_2`] = null; + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2`] = {}; + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2_4`] = null; + + Onyx.multiSet(setQueries) .then(KeyReportActionsDraftByReportActionID) .then(() => { expect(LogSpy).toHaveBeenCalledWith('[Migrate Onyx] Skipped migration KeyReportActionsDraftByReportActionID because there are no actions drafts to migrate'); @@ -260,14 +301,16 @@ describe('Migrations', () => { expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2`]).toMatchObject(expectedReportActionDraft); }, }); - })); - - it("Shouldn't move empty individual draft to a draft collection of report", () => - // @ts-expect-error Preset necessary values - Onyx.multiSet({ - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_1`]: '', - [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1`]: {}, - }) + }); + }); + + it("Shouldn't move empty individual draft to a draft collection of report", () => { + const setQueries: ReportActionsDraftCollectionDataSet = {}; + + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_1`] = ''; + setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1`] = {}; + + Onyx.multiSet(setQueries) .then(KeyReportActionsDraftByReportActionID) .then(() => { const connectionID = Onyx.connect({ @@ -278,6 +321,7 @@ describe('Migrations', () => { expect(allReportActionsDrafts?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_1`]).toBeUndefined(); }, }); - })); + }); + }); }); }); diff --git a/tests/unit/NetworkTest.ts b/tests/unit/NetworkTest.ts index f8b5b6a7d345..63b275a1a6b6 100644 --- a/tests/unit/NetworkTest.ts +++ b/tests/unit/NetworkTest.ts @@ -1,24 +1,24 @@ import type {Mock} from 'jest-mock'; -import reactNativeOnyxMock from '../../__mocks__/react-native-onyx'; -// import Onyx from 'react-native-onyx'; -import CONST from '../../src/CONST'; -import OnyxUpdateManager from '../../src/libs/actions/OnyxUpdateManager'; -import * as PersistedRequests from '../../src/libs/actions/PersistedRequests'; -import * as PersonalDetails from '../../src/libs/actions/PersonalDetails'; -import * as Session from '../../src/libs/actions/Session'; -import HttpUtils from '../../src/libs/HttpUtils'; -import Log from '../../src/libs/Log'; -import * as Network from '../../src/libs/Network'; -import * as MainQueue from '../../src/libs/Network/MainQueue'; -import * as NetworkStore from '../../src/libs/Network/NetworkStore'; -import NetworkConnection from '../../src/libs/NetworkConnection'; -import ONYXKEYS from '../../src/ONYXKEYS'; +import MockedOnyx from 'react-native-onyx'; +import CONST from '@src/CONST'; +import OnyxUpdateManager from '@src/libs/actions/OnyxUpdateManager'; +import * as PersistedRequests from '@src/libs/actions/PersistedRequests'; +import * as PersonalDetails from '@src/libs/actions/PersonalDetails'; +import * as Session from '@src/libs/actions/Session'; +import HttpUtils from '@src/libs/HttpUtils'; +import Log from '@src/libs/Log'; +import * as Network from '@src/libs/Network'; +import * as MainQueue from '@src/libs/Network/MainQueue'; +import * as NetworkStore from '@src/libs/Network/NetworkStore'; +import NetworkConnection from '@src/libs/NetworkConnection'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type ReactNativeOnyxMock from '../../__mocks__/react-native-onyx'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; -const Onyx = reactNativeOnyxMock; +const Onyx = MockedOnyx as typeof ReactNativeOnyxMock; -jest.mock('../../src/libs/Log'); +jest.mock('@src/libs/Log'); Onyx.init({ keys: ONYXKEYS, @@ -28,7 +28,8 @@ OnyxUpdateManager(); const originalXHR = HttpUtils.xhr; beforeEach(() => { - global.fetch = TestHelper.getGlobalFetchMock() as typeof fetch; + // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. + global.fetch = TestHelper.getGlobalFetchMock(); HttpUtils.xhr = originalXHR; MainQueue.clear(); HttpUtils.cancelPendingRequests(); From e0813e48574bc22e8a14844d8fae4afcd7c86f20 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Mon, 4 Mar 2024 14:09:18 +0100 Subject: [PATCH 172/484] fix test --- tests/unit/MigrationTest.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/unit/MigrationTest.ts b/tests/unit/MigrationTest.ts index bd1f79b8f838..d60761cd1d89 100644 --- a/tests/unit/MigrationTest.ts +++ b/tests/unit/MigrationTest.ts @@ -178,7 +178,7 @@ describe('Migrations', () => { }, }; - Onyx.multiSet(setQueries) + return Onyx.multiSet(setQueries) .then(CheckForPreviousReportActionID) .then(() => { expect(LogSpy).toHaveBeenCalledWith('[Migrate Onyx] CheckForPreviousReportActionID Migration: previousReportActionID found. Migration complete'); @@ -217,7 +217,7 @@ describe('Migrations', () => { // @ts-expect-error preset null value setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`] = null; - Onyx.multiSet(setQueries) + return Onyx.multiSet(setQueries) .then(CheckForPreviousReportActionID) .then(() => { expect(LogSpy).toHaveBeenCalledWith('[Migrate Onyx] Skipped migration CheckForPreviousReportActionID because there were no valid reportActions'); @@ -225,8 +225,8 @@ describe('Migrations', () => { key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, waitForCollectionCallback: true, callback: (allReportActions) => { - Onyx.disconnect(connectionID); const expectedReportAction = {}; + Onyx.disconnect(connectionID); expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]).toBeUndefined(); expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`]).toMatchObject(expectedReportAction); expect(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`]).toMatchObject(expectedReportAction); @@ -246,12 +246,15 @@ describe('Migrations', () => { it('Should move individual draft to a draft collection of report', () => { const setQueries: ReportActionsDraftCollectionDataSet = {}; + // @ts-expect-error preset invalid value setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_1`] = 'a'; + // @ts-expect-error preset invalid value setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_2`] = 'b'; setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2`] = {3: 'c'}; + // @ts-expect-error preset invalid value setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2_4`] = 'd'; - Onyx.multiSet(setQueries) + return Onyx.multiSet(setQueries) .then(KeyReportActionsDraftByReportActionID) .then(() => { const connectionID = Onyx.connect({ @@ -280,12 +283,9 @@ describe('Migrations', () => { it('Should skip if nothing to migrate', () => { const setQueries: ReportActionsDraftCollectionDataSet = {}; - setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_1`] = null; - setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_2`] = null; setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2`] = {}; - setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}2_4`] = null; - Onyx.multiSet(setQueries) + return Onyx.multiSet(setQueries) .then(KeyReportActionsDraftByReportActionID) .then(() => { expect(LogSpy).toHaveBeenCalledWith('[Migrate Onyx] Skipped migration KeyReportActionsDraftByReportActionID because there are no actions drafts to migrate'); @@ -307,10 +307,11 @@ describe('Migrations', () => { it("Shouldn't move empty individual draft to a draft collection of report", () => { const setQueries: ReportActionsDraftCollectionDataSet = {}; + // @ts-expect-error preset empty string value setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1_1`] = ''; setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}1`] = {}; - Onyx.multiSet(setQueries) + return Onyx.multiSet(setQueries) .then(KeyReportActionsDraftByReportActionID) .then(() => { const connectionID = Onyx.connect({ From 20a754b379b66894ec09165b49bd7e5d5716198e Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Mon, 4 Mar 2024 22:06:46 +0300 Subject: [PATCH 173/484] hide RBR for settled requests --- src/libs/SidebarUtils.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 51233838e6cf..505a0e6ce905 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -230,6 +230,11 @@ function getOptionData({ const participantPersonalDetailList = Object.values(OptionsListUtils.getPersonalDetailsForAccountIDs(participantAccountIDs, personalDetails)) as PersonalDetails[]; const personalDetail = participantPersonalDetailList[0] ?? {}; const hasErrors = Object.keys(result.allReportErrors ?? {}).length !== 0; + let shouldHideViolation = false; + if (hasViolations && parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) { + const {IOUReportID} = parentReportAction?.originalMessage ?? {}; + shouldHideViolation = ReportUtils.isSettled(IOUReportID); + } result.isThread = ReportUtils.isChatThread(report); result.isChatRoom = ReportUtils.isChatRoom(report); @@ -241,7 +246,7 @@ function getOptionData({ result.isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); result.shouldShowSubscript = ReportUtils.shouldReportShowSubscript(report); result.pendingAction = report.pendingFields?.addWorkspaceRoom ?? report.pendingFields?.createChat; - result.brickRoadIndicator = hasErrors || hasViolations ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; + result.brickRoadIndicator = hasErrors || (hasViolations && !shouldHideViolation) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; result.ownerAccountID = report.ownerAccountID; result.managerID = report.managerID; result.reportID = report.reportID; From bcc6ccbe7eaac5530a125adfb64bd84be931cdd8 Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 5 Mar 2024 10:20:12 +0700 Subject: [PATCH 174/484] lint fix --- src/libs/actions/Report.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 22b1367282f7..09ac33d76eaf 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -2487,7 +2487,6 @@ function removeFromRoom(reportID: string, targetAccountIDs: number[]) { }, ]; - console.log('optimisticData', optimisticData); const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, From 926701c1e653fed38fbcdd49f7020108ce53daad Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Tue, 5 Mar 2024 11:18:38 -0500 Subject: [PATCH 175/484] optimistically set deleted timestamp --- src/libs/actions/IOU.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 5f9657755b02..440f8743c293 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -2982,7 +2982,7 @@ function deleteMoneyRequest(transactionID: string, reportAction: OnyxTypes.Repor if (updatedReportPreviewAction?.message?.[0]) { updatedReportPreviewAction.message[0].text = messageText; - updatedReportPreviewAction.message[0].html = shouldDeleteIOUReport ? '' : messageText; + updatedReportPreviewAction.message[0].deleted = shouldDeleteIOUReport ? DateUtils.getDBTime() : ''; } if (updatedReportPreviewAction && reportPreviewAction?.childMoneyRequestCount && reportPreviewAction?.childMoneyRequestCount > 0) { From a3767180dfee9bf40fee255ce08259710da5fc50 Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Tue, 5 Mar 2024 11:19:08 -0500 Subject: [PATCH 176/484] stop deleting the preview action --- src/libs/actions/IOU.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 440f8743c293..b3598a81e9c2 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -3087,12 +3087,10 @@ function deleteMoneyRequest(transactionID: string, reportAction: OnyxTypes.Repor onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, value: { - [reportPreviewAction?.reportActionID ?? '']: shouldDeleteIOUReport - ? null - : { - pendingAction: null, - errors: null, - }, + [reportPreviewAction?.reportActionID ?? '']: { + pendingAction: null, + errors: null, + }, }, }, ]; From ca74d99394654f6e4af87744f6b2db0c91c7cf68 Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Tue, 5 Mar 2024 19:40:42 +0300 Subject: [PATCH 177/484] updated based on comments --- src/libs/SidebarUtils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 505a0e6ce905..267f77367854 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -230,10 +230,10 @@ function getOptionData({ const participantPersonalDetailList = Object.values(OptionsListUtils.getPersonalDetailsForAccountIDs(participantAccountIDs, personalDetails)) as PersonalDetails[]; const personalDetail = participantPersonalDetailList[0] ?? {}; const hasErrors = Object.keys(result.allReportErrors ?? {}).length !== 0; - let shouldHideViolation = false; + let shouldShowViolations = false; if (hasViolations && parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) { const {IOUReportID} = parentReportAction?.originalMessage ?? {}; - shouldHideViolation = ReportUtils.isSettled(IOUReportID); + shouldShowViolations = !ReportUtils.isSettled(IOUReportID); } result.isThread = ReportUtils.isChatThread(report); @@ -246,7 +246,7 @@ function getOptionData({ result.isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); result.shouldShowSubscript = ReportUtils.shouldReportShowSubscript(report); result.pendingAction = report.pendingFields?.addWorkspaceRoom ?? report.pendingFields?.createChat; - result.brickRoadIndicator = hasErrors || (hasViolations && !shouldHideViolation) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; + result.brickRoadIndicator = hasErrors || shouldShowViolations ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; result.ownerAccountID = report.ownerAccountID; result.managerID = report.managerID; result.reportID = report.reportID; From a2ada45f0e5ee307f0d8b6073b6dacecabe47256 Mon Sep 17 00:00:00 2001 From: Ionatan Wiznia Date: Tue, 5 Mar 2024 16:36:54 -0300 Subject: [PATCH 178/484] Migrate recently used tags too --- src/ONYXKEYS.ts | 2 +- src/libs/migrations/NVPMigration.ts | 29 +++++++++++++++++++++++++---- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index db9864e6800c..1087312a4acd 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -293,7 +293,7 @@ const ONYXKEYS = { POLICY_CATEGORIES: 'policyCategories_', POLICY_RECENTLY_USED_CATEGORIES: 'policyRecentlyUsedCategories_', POLICY_TAGS: 'policyTags_', - POLICY_RECENTLY_USED_TAGS: 'policyRecentlyUsedTags_', + POLICY_RECENTLY_USED_TAGS: 'nvp_policyRecentlyUsedTags_', POLICY_REPORT_FIELDS: 'policyReportFields_', WORKSPACE_INVITE_MEMBERS_DRAFT: 'workspaceInviteMembersDraft_', WORKSPACE_INVITE_MESSAGE_DRAFT: 'workspaceInviteMessageDraft_', diff --git a/src/libs/migrations/NVPMigration.ts b/src/libs/migrations/NVPMigration.ts index 22bdd4a03615..a6fe81fa0aee 100644 --- a/src/libs/migrations/NVPMigration.ts +++ b/src/libs/migrations/NVPMigration.ts @@ -21,8 +21,8 @@ const migrations = { // This migration changes the keys of all the NVP related keys so that they are standardized export default function () { return new Promise((resolve) => { - // It's 1 more because activePolicyID is not in the migrations object above as it is nested inside an object - const resolveWhenDone = after(Object.entries(migrations).length + 1, () => resolve()); + // We add the number of manual connections we add below + const resolveWhenDone = after(Object.entries(migrations).length + 2, () => resolve()); for (const [oldKey, newKey] of Object.entries(migrations)) { const connectionID = Onyx.connect({ @@ -41,10 +41,10 @@ export default function () { }, }); } - const connectionID = Onyx.connect({ + const connectionIDAccount = Onyx.connect({ key: ONYXKEYS.ACCOUNT, callback: (value) => { - Onyx.disconnect(connectionID); + Onyx.disconnect(connectionIDAccount); // @ts-expect-error we are removing this property, so it is not in the type anymore if (value?.activePolicyID) { // @ts-expect-error we are removing this property, so it is not in the type anymore @@ -60,5 +60,26 @@ export default function () { resolveWhenDone(); }, }); + const connectionIDRecentlyUsedTags = Onyx.connect({ + // @ts-expect-error The key was renamed, so it does not exist in the type definition + key: 'policyRecentlyUsedTags_', + waitForCollectionCallback: true, + callback: (value) => { + Onyx.disconnect(connectionIDRecentlyUsedTags); + if (!value) { + resolveWhenDone(); + return; + } + const newValue = {}; + for (const key of Object.keys(value)) { + // @ts-expect-error We have no fixed types here + newValue[`nvp_${key}`] = value[key]; + // @ts-expect-error We have no fixed types here + newValue[key] = null; + } + Onyx.multiSet(newValue); + resolveWhenDone(); + }, + }); }); } From f0c591094bbd81300c3c3497750dc871b86830d3 Mon Sep 17 00:00:00 2001 From: Ionatan Wiznia Date: Tue, 5 Mar 2024 16:47:21 -0300 Subject: [PATCH 179/484] Make collection load properly --- src/ONYXKEYS.ts | 2 ++ src/libs/migrations/NVPMigration.ts | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 1087312a4acd..d581e515e0f5 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -294,6 +294,7 @@ const ONYXKEYS = { POLICY_RECENTLY_USED_CATEGORIES: 'policyRecentlyUsedCategories_', POLICY_TAGS: 'policyTags_', POLICY_RECENTLY_USED_TAGS: 'nvp_policyRecentlyUsedTags_', + OLD_POLICY_RECENTLY_USED_TAGS: 'policyRecentlyUsedTags_', POLICY_REPORT_FIELDS: 'policyReportFields_', WORKSPACE_INVITE_MEMBERS_DRAFT: 'workspaceInviteMembersDraft_', WORKSPACE_INVITE_MESSAGE_DRAFT: 'workspaceInviteMessageDraft_', @@ -484,6 +485,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS]: OnyxTypes.TransactionViolations; [ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT]: OnyxTypes.Transaction; [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS]: OnyxTypes.RecentlyUsedTags; + [ONYXKEYS.COLLECTION.OLD_POLICY_RECENTLY_USED_TAGS]: OnyxTypes.RecentlyUsedTags; [ONYXKEYS.COLLECTION.SELECTED_TAB]: string; [ONYXKEYS.COLLECTION.PRIVATE_NOTES_DRAFT]: string; [ONYXKEYS.COLLECTION.NEXT_STEP]: OnyxTypes.ReportNextStep; diff --git a/src/libs/migrations/NVPMigration.ts b/src/libs/migrations/NVPMigration.ts index a6fe81fa0aee..6be142eb1f2a 100644 --- a/src/libs/migrations/NVPMigration.ts +++ b/src/libs/migrations/NVPMigration.ts @@ -61,8 +61,7 @@ export default function () { }, }); const connectionIDRecentlyUsedTags = Onyx.connect({ - // @ts-expect-error The key was renamed, so it does not exist in the type definition - key: 'policyRecentlyUsedTags_', + key: ONYXKEYS.COLLECTION.OLD_POLICY_RECENTLY_USED_TAGS, waitForCollectionCallback: true, callback: (value) => { Onyx.disconnect(connectionIDRecentlyUsedTags); From fdadc74041fbcac42c12ee063ab14ded025e2a21 Mon Sep 17 00:00:00 2001 From: Ionatan Wiznia Date: Tue, 5 Mar 2024 16:57:55 -0300 Subject: [PATCH 180/484] Correct onyx key --- src/ONYXKEYS.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index d581e515e0f5..13f578dae136 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -199,7 +199,7 @@ const ONYXKEYS = { PREFERRED_EMOJI_SKIN_TONE: 'nvp_expensify_preferredEmojiSkinTone', /** Store frequently used emojis for this user */ - FREQUENTLY_USED_EMOJIS: 'expensify_frequentlyUsedEmojis', + FREQUENTLY_USED_EMOJIS: 'nvp_expensify_frequentlyUsedEmojis', /** Stores Workspace ID that will be tied to reimbursement account during setup */ REIMBURSEMENT_ACCOUNT_WORKSPACE_ID: 'reimbursementAccountWorkspaceID', From 1290c364747c9a61908bb88b36ac75437251204e Mon Sep 17 00:00:00 2001 From: Ionatan Wiznia Date: Tue, 5 Mar 2024 17:46:36 -0300 Subject: [PATCH 181/484] Add nvp prefix --- src/ONYXKEYS.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 13f578dae136..031759c2b4eb 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -132,7 +132,7 @@ const ONYXKEYS = { NVP_ACTIVE_POLICY_ID: 'nvp_expensify_activePolicyID', /** This NVP contains the referral banners the user dismissed */ - NVP_DISMISSED_REFERRAL_BANNERS: 'dismissedReferralBanners', + NVP_DISMISSED_REFERRAL_BANNERS: 'nvp_dismissedReferralBanners', /** Does this user have push notifications enabled for this device? */ PUSH_NOTIFICATIONS_ENABLED: 'pushNotificationsEnabled', From d0b8b86917bec5cf0eab23712312c36f2b5a38b2 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Wed, 6 Mar 2024 15:42:19 +0700 Subject: [PATCH 182/484] Handle emoji tooltip and fix regression --- package-lock.json | 16 +++---- package.json | 2 +- .../EmojiWithTooltip/index.native.tsx | 10 +++++ src/components/EmojiWithTooltip/index.tsx | 42 +++++++++++++++++++ src/components/EmojiWithTooltip/types.ts | 8 ++++ .../BaseHTMLEngineProvider.tsx | 1 + .../HTMLRenderers/EmojiRenderer.tsx | 19 +++++++++ .../HTMLEngineProvider/HTMLRenderers/index.ts | 2 + src/libs/EmojiUtils.ts | 5 ++- .../report/comment/TextCommentFragment.tsx | 22 ++++------ .../shouldRenderAsText/index.native.ts | 12 ++++++ .../comment/shouldRenderAsText/index.ts | 8 ++++ src/styles/index.ts | 4 ++ 13 files changed, 128 insertions(+), 23 deletions(-) create mode 100644 src/components/EmojiWithTooltip/index.native.tsx create mode 100644 src/components/EmojiWithTooltip/index.tsx create mode 100644 src/components/EmojiWithTooltip/types.ts create mode 100644 src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx create mode 100644 src/pages/home/report/comment/shouldRenderAsText/index.native.ts create mode 100644 src/pages/home/report/comment/shouldRenderAsText/index.ts diff --git a/package-lock.json b/package-lock.json index cc717e8d6a0f..fd0fcf5163bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,7 +51,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#a8ed0f8e1be3a1e09016e07a74cfd13c85bbc167", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#45d3b61bb38b4f9a19ddf573ce1e212369b242db", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.10.1", @@ -25977,9 +25977,9 @@ } }, "node_modules/classnames": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.0.tgz", - "integrity": "sha512-FQuRlyKinxrb5gwJlfVASbSrDlikDJ07426TrfPsdGLvtochowmkbnSFdQGJ2aoXrSetq5KqGV9emvWpy+91xA==" + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.4.0.tgz", + "integrity": "sha512-lWxiIlphgAhTLN657pwU/ofFxsUTOWc2CRIFeoV5st0MGRJHStUnWIUJgDHxjUO/F0mXzGufXIM4Lfu/8h+MpA==" }, "node_modules/clean-css": { "version": "5.3.2", @@ -30977,11 +30977,11 @@ }, "node_modules/expensify-common": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#a8ed0f8e1be3a1e09016e07a74cfd13c85bbc167", - "integrity": "sha512-3d/JHWgeS+LFPRahCAXdLwnBYQk4XUYybtgCm7VsdmMDtCeGUTksLsEY7F1Zqm+ULqZjmCtYwAi8IPKy0fsSOw==", + "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#45d3b61bb38b4f9a19ddf573ce1e212369b242db", + "integrity": "sha512-R1ykTwH3Pdp2sFqE6AL3ihmo4OjLMDEc8mEqQwD9W+yoIDIScT6Wi5ewO5vZUNsyCiKnD+xvNU7I1d9VNRJkXw==", "license": "MIT", "dependencies": { - "classnames": "2.5.0", + "classnames": "2.4.0", "clipboard": "2.0.11", "html-entities": "^2.4.0", "jquery": "3.6.0", @@ -30990,7 +30990,7 @@ "prop-types": "15.8.1", "react": "16.12.0", "react-dom": "16.12.0", - "semver": "^7.6.0", + "semver": "^7.5.2", "simply-deferred": "git+https://github.com/Expensify/simply-deferred.git#77a08a95754660c7bd6e0b6979fdf84e8e831bf5", "ua-parser-js": "^1.0.37", "underscore": "1.13.6" diff --git a/package.json b/package.json index 5b498cb09dc2..f3cb2dbe2b2f 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,7 @@ "date-fns-tz": "^2.0.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#a8ed0f8e1be3a1e09016e07a74cfd13c85bbc167", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#45d3b61bb38b4f9a19ddf573ce1e212369b242db", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.10.1", diff --git a/src/components/EmojiWithTooltip/index.native.tsx b/src/components/EmojiWithTooltip/index.native.tsx new file mode 100644 index 000000000000..f6e9ee17fff8 --- /dev/null +++ b/src/components/EmojiWithTooltip/index.native.tsx @@ -0,0 +1,10 @@ +import Text from '@components/Text'; +import type EmojiWithTooltipProps from './types'; + +function EmojiWithTooltip({emojiCode, style = {}}: EmojiWithTooltipProps) { + return {emojiCode}; +} + +EmojiWithTooltip.displayName = 'EmojiWithTooltip'; + +export default EmojiWithTooltip; diff --git a/src/components/EmojiWithTooltip/index.tsx b/src/components/EmojiWithTooltip/index.tsx new file mode 100644 index 000000000000..32103544b3aa --- /dev/null +++ b/src/components/EmojiWithTooltip/index.tsx @@ -0,0 +1,42 @@ +import React, {useCallback} from 'react'; +import {View} from 'react-native'; +import Text from '@components/Text'; +import Tooltip from '@components/Tooltip'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as EmojiUtils from '@libs/EmojiUtils'; +import type EmojiWithTooltipProps from './types'; + +function EmojiWithTooltip({emojiCode, style = {}}: EmojiWithTooltipProps) { + const {preferredLocale} = useLocalize(); + const styles = useThemeStyles(); + const emoji = EmojiUtils.findEmojiByCode(emojiCode); + const emojiName = EmojiUtils.getEmojiName(emoji, preferredLocale); + + const emojiTooltipContent = useCallback( + () => ( + + + + {emojiCode} + + + {`:${emojiName}:`} + + ), + [emojiCode, emojiName, styles.alignItemsCenter, styles.ph2, styles.flexRow, styles.emojiTooltipWrapper, styles.fontColorReactionLabel, styles.onlyEmojisText, styles.textMicro], + ); + + return ( + + {emojiCode} + + ); +} + +EmojiWithTooltip.displayName = 'EmojiWithTooltip'; + +export default EmojiWithTooltip; diff --git a/src/components/EmojiWithTooltip/types.ts b/src/components/EmojiWithTooltip/types.ts new file mode 100644 index 000000000000..d13c389c0568 --- /dev/null +++ b/src/components/EmojiWithTooltip/types.ts @@ -0,0 +1,8 @@ +import type {StyleProp, TextStyle} from 'react-native'; + +type EmojiWithTooltipProps = { + emojiCode: string; + style?: StyleProp; +}; + +export default EmojiWithTooltipProps; diff --git a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx index bd4f72c63ec3..af04c11de41e 100755 --- a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx +++ b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx @@ -70,6 +70,7 @@ function BaseHTMLEngineProvider({textSelectable = false, children, enableExperim mixedUAStyles: {whiteSpace: 'pre'}, contentModel: HTMLContentModel.block, }), + emoji: HTMLElementModel.fromCustomModel({tagName: 'emoji', contentModel: HTMLContentModel.textual}), }), [styles.colorMuted, styles.formError, styles.mb0, styles.textLabelSupporting, styles.lh16], ); diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx new file mode 100644 index 000000000000..6582e99477a8 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import type {CustomRendererProps, TPhrasing, TText} from 'react-native-render-html'; +import EmojiWithTooltip from '@components/EmojiWithTooltip'; +import useThemeStyles from '@hooks/useThemeStyles'; + +function EmojiRenderer({tnode}: CustomRendererProps) { + const styles = useThemeStyles(); + const style = 'islarge' in tnode.attributes ? styles.onlyEmojisText : {}; + return ( + + ); +} + +EmojiRenderer.displayName = 'EmojiRenderer'; + +export default EmojiRenderer; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/index.ts b/src/components/HTMLEngineProvider/HTMLRenderers/index.ts index 1914bcf4b5ff..fdd0c89ec5a0 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/index.ts +++ b/src/components/HTMLEngineProvider/HTMLRenderers/index.ts @@ -2,6 +2,7 @@ import type {CustomTagRendererRecord} from 'react-native-render-html'; import AnchorRenderer from './AnchorRenderer'; import CodeRenderer from './CodeRenderer'; import EditedRenderer from './EditedRenderer'; +import EmojiRenderer from './EmojiRenderer'; import ImageRenderer from './ImageRenderer'; import MentionHereRenderer from './MentionHereRenderer'; import MentionUserRenderer from './MentionUserRenderer'; @@ -25,6 +26,7 @@ const HTMLEngineProviderComponentList: CustomTagRendererRecord = { /* eslint-disable @typescript-eslint/naming-convention */ 'mention-user': MentionUserRenderer, 'mention-here': MentionHereRenderer, + emoji: EmojiRenderer, 'next-step-email': NextStepEmailRenderer, /* eslint-enable @typescript-eslint/naming-convention */ }; diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index cab0f48d75fd..e236c06db399 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -37,7 +37,10 @@ const findEmojiByName = (name: string): Emoji => Emojis.emojiNameTable[name]; const findEmojiByCode = (code: string): Emoji => Emojis.emojiCodeTableWithSkinTones[code]; -const getEmojiName = (emoji: Emoji, lang: 'en' | 'es' = CONST.LOCALES.DEFAULT): string => { +const getEmojiName = (emoji: Emoji, lang: Locale = CONST.LOCALES.DEFAULT): string => { + if (!emoji) { + return ''; + } if (lang === CONST.LOCALES.DEFAULT) { return emoji.name; } diff --git a/src/pages/home/report/comment/TextCommentFragment.tsx b/src/pages/home/report/comment/TextCommentFragment.tsx index 951888a443c1..981d6771c8db 100644 --- a/src/pages/home/report/comment/TextCommentFragment.tsx +++ b/src/pages/home/report/comment/TextCommentFragment.tsx @@ -16,6 +16,7 @@ import CONST from '@src/CONST'; import type {OriginalMessageSource} from '@src/types/onyx/OriginalMessage'; import type {Message} from '@src/types/onyx/ReportAction'; import RenderCommentHTML from './RenderCommentHTML'; +import shouldRenderAsText from './shouldRenderAsText'; type TextCommentFragmentProps = { /** The reportAction's source */ @@ -47,20 +48,16 @@ function TextCommentFragment({fragment, styleAsDeleted, styleAsMuted = false, so const {translate} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); - // If the only difference between fragment.text and fragment.html is
tags - // we render it as text, not as html. - // This is done to render emojis with line breaks between them as text. - const differByLineBreaksOnly = Str.replaceAll(html, '
', '\n') === text; - - // Only render HTML if we have html in the fragment - if (!differByLineBreaksOnly) { + // If the only difference between fragment.text and fragment.html is
tags and emoji tag + // on native, we render it as text, not as html + // on other device, only render it as text if the only difference is
tag + const containsOnlyEmojis = EmojiUtils.containsOnlyEmojis(text); + if (!shouldRenderAsText(html, text) && !(containsOnlyEmojis && styleAsDeleted)) { const editedTag = fragment.isEdited ? `` : ''; - const htmlContent = styleAsDeleted ? `${html}` : html; + const htmlWithDeletedTag = styleAsDeleted ? `${html}` : html; - let htmlWithTag = editedTag ? `${htmlContent}${editedTag}` : htmlContent; - if (styleAsMuted) { - htmlWithTag = `${htmlWithTag}`; - } + const htmlContent = containsOnlyEmojis ? Str.replaceAll(htmlWithDeletedTag, '', '') : htmlWithDeletedTag; + const htmlWithTag = editedTag ? `${htmlContent}${editedTag}` : htmlContent; return ( ', '\n'); + const htmlWithoutEmojiOpenTag = Str.replaceAll(htmlWithoutLineBreak, '', ''); + return Str.replaceAll(htmlWithoutEmojiOpenTag, '', '') === text; +} diff --git a/src/pages/home/report/comment/shouldRenderAsText/index.ts b/src/pages/home/report/comment/shouldRenderAsText/index.ts new file mode 100644 index 000000000000..f26f43c528eb --- /dev/null +++ b/src/pages/home/report/comment/shouldRenderAsText/index.ts @@ -0,0 +1,8 @@ +import Str from 'expensify-common/lib/str'; + +/** + * Whether to render the report action as text + */ +export default function shouldRenderAsText(html: string, text: string): boolean { + return Str.replaceAll(html, '
', '\n') === text; +} diff --git a/src/styles/index.ts b/src/styles/index.ts index 405a05cfce78..bf2e205cf068 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -286,6 +286,10 @@ const styles = (theme: ThemeColors) => ...wordBreak.breakWord, ...spacing.pr4, }, + emojiTooltipWrapper: { + ...spacing.p2, + borderRadius: 8, + }, mentionSuggestionsAvatarContainer: { width: 24, From 7b3c4134c5f03cca3b8f17c5e140d77ea5db1a83 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Wed, 6 Mar 2024 11:20:48 +0100 Subject: [PATCH 183/484] Migrate g15 stories to TS --- src/components/CheckboxWithLabel.tsx | 2 + src/components/OptionRow.tsx | 2 + src/components/PopoverMenu.tsx | 2 +- ...ories.js => CheckboxWithLabel.stories.tsx} | 15 +++++--- ...nuItem.stories.js => MenuItem.stories.tsx} | 38 ++++++++++--------- ...onRow.stories.js => OptionRow.stories.tsx} | 3 +- ...enu.stories.js => PopoverMenu.stories.tsx} | 28 +++++++------- ...stories.js => SubscriptAvatar.stories.tsx} | 14 ++++--- 8 files changed, 59 insertions(+), 45 deletions(-) rename src/stories/{CheckboxWithLabel.stories.js => CheckboxWithLabel.stories.tsx} (73%) rename src/stories/{MenuItem.stories.js => MenuItem.stories.tsx} (77%) rename src/stories/{OptionRow.stories.js => OptionRow.stories.tsx} (94%) rename src/stories/{PopoverMenu.stories.js => PopoverMenu.stories.tsx} (78%) rename src/stories/{SubscriptAvatar.stories.js => SubscriptAvatar.stories.tsx} (77%) diff --git a/src/components/CheckboxWithLabel.tsx b/src/components/CheckboxWithLabel.tsx index 2919debe9cb1..dd169576186e 100644 --- a/src/components/CheckboxWithLabel.tsx +++ b/src/components/CheckboxWithLabel.tsx @@ -108,3 +108,5 @@ function CheckboxWithLabel( CheckboxWithLabel.displayName = 'CheckboxWithLabel'; export default React.forwardRef(CheckboxWithLabel); + +export type {CheckboxWithLabelProps}; diff --git a/src/components/OptionRow.tsx b/src/components/OptionRow.tsx index 7b45fd963fe7..97ef6885c80f 100644 --- a/src/components/OptionRow.tsx +++ b/src/components/OptionRow.tsx @@ -340,3 +340,5 @@ export default React.memo( prevProps.option.pendingAction === nextProps.option.pendingAction && prevProps.option.customIcon === nextProps.option.customIcon, ); + +export type {OptionRowProps}; diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx index a391ff061baa..3a211f90bd14 100644 --- a/src/components/PopoverMenu.tsx +++ b/src/components/PopoverMenu.tsx @@ -243,4 +243,4 @@ function PopoverMenu({ PopoverMenu.displayName = 'PopoverMenu'; export default React.memo(PopoverMenu); -export type {PopoverMenuItem}; +export type {PopoverMenuItem, PopoverMenuProps}; diff --git a/src/stories/CheckboxWithLabel.stories.js b/src/stories/CheckboxWithLabel.stories.tsx similarity index 73% rename from src/stories/CheckboxWithLabel.stories.js rename to src/stories/CheckboxWithLabel.stories.tsx index f978856aaefb..b5e8bc72f380 100644 --- a/src/stories/CheckboxWithLabel.stories.js +++ b/src/stories/CheckboxWithLabel.stories.tsx @@ -1,29 +1,33 @@ +import type {ComponentMeta, ComponentStory} from '@storybook/react'; import React from 'react'; import CheckboxWithLabel from '@components/CheckboxWithLabel'; +import type {CheckboxWithLabelProps} from '@components/CheckboxWithLabel'; import Text from '@components/Text'; // eslint-disable-next-line no-restricted-imports import {defaultStyles} from '@styles/index'; +type CheckboxWithLabelStory = ComponentStory; + /** * We use the Component Story Format for writing stories. Follow the docs here: * * https://storybook.js.org/docs/react/writing-stories/introduction#component-story-format */ -const story = { +const story: ComponentMeta = { title: 'Components/CheckboxWithLabel', component: CheckboxWithLabel, }; -function Template(args) { +function Template(args: CheckboxWithLabelProps) { // eslint-disable-next-line react/jsx-props-no-spreading return ; } // Arguments can be passed to the component by binding // See: https://storybook.js.org/docs/react/writing-stories/introduction#using-args -const Default = Template.bind({}); -const WithLabelComponent = Template.bind({}); -const WithErrors = Template.bind({}); +const Default: CheckboxWithLabelStory = Template.bind({}); +const WithLabelComponent: CheckboxWithLabelStory = Template.bind({}); +const WithErrors: CheckboxWithLabelStory = Template.bind({}); Default.args = { isChecked: true, label: 'Plain text label', @@ -44,7 +48,6 @@ WithLabelComponent.args = { WithErrors.args = { isChecked: false, - hasError: true, errorText: 'Please accept Terms before continuing.', onInputChange: () => {}, label: 'I accept the Terms & Conditions', diff --git a/src/stories/MenuItem.stories.js b/src/stories/MenuItem.stories.tsx similarity index 77% rename from src/stories/MenuItem.stories.js rename to src/stories/MenuItem.stories.tsx index 0e7260fa4d1a..4e02bcaf785f 100644 --- a/src/stories/MenuItem.stories.js +++ b/src/stories/MenuItem.stories.tsx @@ -1,26 +1,30 @@ +import type {ComponentMeta, ComponentStory} from '@storybook/react'; import React from 'react'; import Chase from '@assets/images/bankicons/chase.svg'; import MenuItem from '@components/MenuItem'; +import type {MenuItemProps} from '@components/MenuItem'; import variables from '@styles/variables'; +type MenuItemStory = ComponentStory; + /** * We use the Component Story Format for writing stories. Follow the docs here: * * https://storybook.js.org/docs/react/writing-stories/introduction#component-story-format */ -const story = { +const story: ComponentMeta = { title: 'Components/MenuItem', component: MenuItem, }; -function Template(args) { +function Template(args: MenuItemProps) { // eslint-disable-next-line react/jsx-props-no-spreading return ; } // Arguments can be passed to the component by binding // See: https://storybook.js.org/docs/react/writing-stories/introduction#using-args -const Default = Template.bind({}); +const Default: MenuItemStory = Template.bind({}); Default.args = { title: 'Alberta Bobbeth Charleson', icon: Chase, @@ -28,7 +32,7 @@ Default.args = { iconWidth: variables.iconSizeExtraLarge, }; -const Description = Template.bind({}); +const Description: MenuItemStory = Template.bind({}); Description.args = { title: 'Alberta Bobbeth Charleson', description: 'Account ending in 1111', @@ -37,7 +41,7 @@ Description.args = { iconWidth: variables.iconSizeExtraLarge, }; -const RightIcon = Template.bind({}); +const RightIcon: MenuItemStory = Template.bind({}); RightIcon.args = { title: 'Alberta Bobbeth Charleson', icon: Chase, @@ -46,7 +50,7 @@ RightIcon.args = { shouldShowRightIcon: true, }; -const RightIconAndDescription = Template.bind({}); +const RightIconAndDescription: MenuItemStory = Template.bind({}); RightIconAndDescription.args = { title: 'Alberta Bobbeth Charleson', description: 'Account ending in 1111', @@ -56,7 +60,7 @@ RightIconAndDescription.args = { shouldShowRightIcon: true, }; -const RightIconAndDescriptionWithLabel = Template.bind({}); +const RightIconAndDescriptionWithLabel: MenuItemStory = Template.bind({}); RightIconAndDescriptionWithLabel.args = { label: 'Account number', title: 'Alberta Bobbeth Charleson', @@ -67,7 +71,7 @@ RightIconAndDescriptionWithLabel.args = { shouldShowRightIcon: true, }; -const Selected = Template.bind({}); +const Selected: MenuItemStory = Template.bind({}); Selected.args = { title: 'Alberta Bobbeth Charleson', description: 'Account ending in 1111', @@ -78,7 +82,7 @@ Selected.args = { isSelected: true, }; -const BadgeText = Template.bind({}); +const BadgeText: MenuItemStory = Template.bind({}); BadgeText.args = { title: 'Alberta Bobbeth Charleson', icon: Chase, @@ -88,7 +92,7 @@ BadgeText.args = { badgeText: '$0.00', }; -const Focused = Template.bind({}); +const Focused: MenuItemStory = Template.bind({}); Focused.args = { title: 'Alberta Bobbeth Charleson', icon: Chase, @@ -98,7 +102,7 @@ Focused.args = { focused: true, }; -const Disabled = Template.bind({}); +const Disabled: MenuItemStory = Template.bind({}); Disabled.args = { title: 'Alberta Bobbeth Charleson', icon: Chase, @@ -108,17 +112,17 @@ Disabled.args = { disabled: true, }; -const BrickRoadIndicatorSuccess = Template.bind({}); -BrickRoadIndicatorSuccess.args = { +const BrickRoadIndicatorInfo: MenuItemStory = Template.bind({}); +BrickRoadIndicatorInfo.args = { title: 'Alberta Bobbeth Charleson', icon: Chase, iconHeight: variables.iconSizeExtraLarge, iconWidth: variables.iconSizeExtraLarge, shouldShowRightIcon: true, - brickRoadIndicator: 'success', + brickRoadIndicator: 'info', }; -const BrickRoadIndicatorFailure = Template.bind({}); +const BrickRoadIndicatorFailure: MenuItemStory = Template.bind({}); BrickRoadIndicatorFailure.args = { title: 'Alberta Bobbeth Charleson', icon: Chase, @@ -128,7 +132,7 @@ BrickRoadIndicatorFailure.args = { brickRoadIndicator: 'error', }; -const ErrorMessage = Template.bind({}); +const ErrorMessage: MenuItemStory = Template.bind({}); ErrorMessage.args = { title: 'Alberta Bobbeth Charleson', icon: Chase, @@ -149,7 +153,7 @@ export { BadgeText, Focused, Disabled, - BrickRoadIndicatorSuccess, + BrickRoadIndicatorInfo, BrickRoadIndicatorFailure, RightIconAndDescriptionWithLabel, ErrorMessage, diff --git a/src/stories/OptionRow.stories.js b/src/stories/OptionRow.stories.tsx similarity index 94% rename from src/stories/OptionRow.stories.js rename to src/stories/OptionRow.stories.tsx index 3096940dda5f..d2fffcd583dd 100644 --- a/src/stories/OptionRow.stories.js +++ b/src/stories/OptionRow.stories.tsx @@ -2,6 +2,7 @@ import React from 'react'; import * as Expensicons from '@components/Icon/Expensicons'; import OnyxProvider from '@components/OnyxProvider'; import OptionRow from '@components/OptionRow'; +import type {OptionRowProps} from '@components/OptionRow'; /* eslint-disable react/jsx-props-no-spreading */ @@ -42,7 +43,7 @@ export default { }, }; -function Template(args) { +function Template(args: OptionRowProps) { return ( diff --git a/src/stories/PopoverMenu.stories.js b/src/stories/PopoverMenu.stories.tsx similarity index 78% rename from src/stories/PopoverMenu.stories.js rename to src/stories/PopoverMenu.stories.tsx index c03a554741f1..2f1491bdd5f3 100644 --- a/src/stories/PopoverMenu.stories.js +++ b/src/stories/PopoverMenu.stories.tsx @@ -1,36 +1,40 @@ +import type {ComponentMeta, ComponentStory} from '@storybook/react'; import React from 'react'; import {SafeAreaProvider} from 'react-native-safe-area-context'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; import PopoverMenu from '@components/PopoverMenu'; +import type {PopoverMenuProps} from '@components/PopoverMenu'; // eslint-disable-next-line no-restricted-imports import themeColors from '@styles/theme/themes/dark'; +type PopoverMenuStory = ComponentStory; + /** * We use the Component Story Format for writing stories. Follow the docs here: * * https://storybook.js.org/docs/react/writing-stories/introduction#component-story-format */ -const story = { +const story: ComponentMeta = { title: 'Components/PopoverMenu', component: PopoverMenu, }; -function Template(args) { +function Template(args: PopoverMenuProps) { const [isVisible, setIsVisible] = React.useState(false); const toggleVisibility = () => setIsVisible(!isVisible); return ( <> ; + /** * We use the Component Story Format for writing stories. Follow the docs here: * @@ -23,27 +27,27 @@ export default { }, }; -function Template(args) { +function Template(args: SubscriptAvatarProps) { // eslint-disable-next-line react/jsx-props-no-spreading return ; } // Arguments can be passed to the component by binding // See: https://storybook.js.org/docs/react/writing-stories/introduction#using-args -const Default = Template.bind({}); +const Default: SubscriptAvatarStory = Template.bind({}); -const AvatarURLStory = Template.bind({}); +const AvatarURLStory: SubscriptAvatarStory = Template.bind({}); AvatarURLStory.args = { mainAvatar: {source: defaultAvatars.Avatar1, name: '', type: CONST.ICON_TYPE_AVATAR}, secondaryAvatar: {source: defaultAvatars.Avatar3, name: '', type: CONST.ICON_TYPE_AVATAR}, }; -const SubscriptIcon = Template.bind({}); +const SubscriptIcon: SubscriptAvatarStory = Template.bind({}); SubscriptIcon.args = { subscriptIcon: {source: Expensicons.DownArrow, width: 8, height: 8}, }; -const WorkspaceSubscriptIcon = Template.bind({}); +const WorkspaceSubscriptIcon: SubscriptAvatarStory = Template.bind({}); WorkspaceSubscriptIcon.args = { mainAvatar: {source: defaultAvatars.Avatar1, name: '', type: CONST.ICON_TYPE_WORKSPACE}, subscriptIcon: {source: Expensicons.DownArrow, width: 8, height: 8}, From fe7c953e9fd0206dd5dd308a5cd5e2d8c4309613 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Wed, 6 Mar 2024 11:43:30 +0100 Subject: [PATCH 184/484] Rename args to props --- src/stories/CheckboxWithLabel.stories.tsx | 4 ++-- src/stories/MenuItem.stories.tsx | 4 ++-- src/stories/OptionRow.stories.tsx | 4 ++-- src/stories/PopoverMenu.stories.tsx | 4 ++-- src/stories/SubscriptAvatar.stories.tsx | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/stories/CheckboxWithLabel.stories.tsx b/src/stories/CheckboxWithLabel.stories.tsx index b5e8bc72f380..8d3c1610e500 100644 --- a/src/stories/CheckboxWithLabel.stories.tsx +++ b/src/stories/CheckboxWithLabel.stories.tsx @@ -18,9 +18,9 @@ const story: ComponentMeta = { component: CheckboxWithLabel, }; -function Template(args: CheckboxWithLabelProps) { +function Template(props: CheckboxWithLabelProps) { // eslint-disable-next-line react/jsx-props-no-spreading - return ; + return ; } // Arguments can be passed to the component by binding diff --git a/src/stories/MenuItem.stories.tsx b/src/stories/MenuItem.stories.tsx index 4e02bcaf785f..da486656cddf 100644 --- a/src/stories/MenuItem.stories.tsx +++ b/src/stories/MenuItem.stories.tsx @@ -17,9 +17,9 @@ const story: ComponentMeta = { component: MenuItem, }; -function Template(args: MenuItemProps) { +function Template(props: MenuItemProps) { // eslint-disable-next-line react/jsx-props-no-spreading - return ; + return ; } // Arguments can be passed to the component by binding diff --git a/src/stories/OptionRow.stories.tsx b/src/stories/OptionRow.stories.tsx index d2fffcd583dd..ea83816ab340 100644 --- a/src/stories/OptionRow.stories.tsx +++ b/src/stories/OptionRow.stories.tsx @@ -43,10 +43,10 @@ export default { }, }; -function Template(args: OptionRowProps) { +function Template(props: OptionRowProps) { return ( - + ); } diff --git a/src/stories/PopoverMenu.stories.tsx b/src/stories/PopoverMenu.stories.tsx index 2f1491bdd5f3..8396a0ea15b5 100644 --- a/src/stories/PopoverMenu.stories.tsx +++ b/src/stories/PopoverMenu.stories.tsx @@ -20,7 +20,7 @@ const story: ComponentMeta = { component: PopoverMenu, }; -function Template(args: PopoverMenuProps) { +function Template(props: PopoverMenuProps) { const [isVisible, setIsVisible] = React.useState(false); const toggleVisibility = () => setIsVisible(!isVisible); return ( @@ -34,7 +34,7 @@ function Template(args: PopoverMenuProps) { ; + return ; } // Arguments can be passed to the component by binding From 54c7a4cb0d2dae6e9f56761c10f67d6040c43b4c Mon Sep 17 00:00:00 2001 From: Ionatan Wiznia Date: Wed, 6 Mar 2024 13:34:50 -0300 Subject: [PATCH 185/484] Early return, move NVP constants, only resolve promise when set is done --- src/ONYXKEYS.ts | 39 ++++++++++++++------------- src/libs/migrations/NVPMigration.ts | 42 +++++++++++++++-------------- 2 files changed, 42 insertions(+), 39 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 031759c2b4eb..33f38e0f5c91 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -16,9 +16,6 @@ const ONYXKEYS = { /** Holds the reportID for the report between the user and their account manager */ ACCOUNT_MANAGER_REPORT_ID: 'accountManagerReportID', - /** Boolean flag only true when first set */ - NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER: 'nvp_isFirstTimeNewExpensifyUser', - /** Holds an array of client IDs which is used for multi-tabs on web in order to know * which tab is the leader, and which ones are the followers */ ACTIVE_CLIENTS: 'activeClients', @@ -106,7 +103,11 @@ const ONYXKEYS = { STASHED_SESSION: 'stashedSession', BETAS: 'betas', - /** NVP keys + /** NVP keys */ + + /** Boolean flag only true when first set */ + NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER: 'nvp_isFirstTimeNewExpensifyUser', + /** Contains the user preference for the LHN priority mode */ NVP_PRIORITY_MODE: 'nvp_priorityMode', @@ -134,6 +135,21 @@ const ONYXKEYS = { /** This NVP contains the referral banners the user dismissed */ NVP_DISMISSED_REFERRAL_BANNERS: 'nvp_dismissedReferralBanners', + /** Indicates which locale should be used */ + NVP_PREFERRED_LOCALE: 'nvp_preferredLocale', + + /** Whether the user has tried focus mode yet */ + NVP_TRY_FOCUS_MODE: 'nvp_tryFocusMode', + + /** Whether the user has been shown the hold educational interstitial yet */ + NVP_HOLD_USE_EXPLAINED: 'holdUseExplained', + + /** Store preferred skintone for emoji */ + PREFERRED_EMOJI_SKIN_TONE: 'nvp_expensify_preferredEmojiSkinTone', + + /** Store frequently used emojis for this user */ + FREQUENTLY_USED_EMOJIS: 'nvp_expensify_frequentlyUsedEmojis', + /** Does this user have push notifications enabled for this device? */ PUSH_NOTIFICATIONS_ENABLED: 'pushNotificationsEnabled', @@ -153,9 +169,6 @@ const ONYXKEYS = { ONFIDO_TOKEN: 'onfidoToken', ONFIDO_APPLICANT_ID: 'onfidoApplicantID', - /** Indicates which locale should be used */ - NVP_PREFERRED_LOCALE: 'nvp_preferredLocale', - /** User's Expensify Wallet */ USER_WALLET: 'userWallet', @@ -177,12 +190,6 @@ const ONYXKEYS = { /** The user's cash card and imported cards (including the Expensify Card) */ CARD_LIST: 'cardList', - /** Whether the user has tried focus mode yet */ - NVP_TRY_FOCUS_MODE: 'nvp_tryFocusMode', - - /** Whether the user has been shown the hold educational interstitial yet */ - NVP_HOLD_USE_EXPLAINED: 'holdUseExplained', - /** Boolean flag used to display the focus mode notification */ FOCUS_MODE_NOTIFICATION: 'focusModeNotification', @@ -195,12 +202,6 @@ const ONYXKEYS = { /** Stores information about the active reimbursement account being set up */ REIMBURSEMENT_ACCOUNT: 'reimbursementAccount', - /** Store preferred skintone for emoji */ - PREFERRED_EMOJI_SKIN_TONE: 'nvp_expensify_preferredEmojiSkinTone', - - /** Store frequently used emojis for this user */ - FREQUENTLY_USED_EMOJIS: 'nvp_expensify_frequentlyUsedEmojis', - /** Stores Workspace ID that will be tied to reimbursement account during setup */ REIMBURSEMENT_ACCOUNT_WORKSPACE_ID: 'reimbursementAccountWorkspaceID', diff --git a/src/libs/migrations/NVPMigration.ts b/src/libs/migrations/NVPMigration.ts index 6be142eb1f2a..26375c1858eb 100644 --- a/src/libs/migrations/NVPMigration.ts +++ b/src/libs/migrations/NVPMigration.ts @@ -2,6 +2,7 @@ import after from 'lodash/after'; import Onyx from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; +// These are the oldKeyName: newKeyName of the NVPs we can migrate without any processing const migrations = { // eslint-disable-next-line @typescript-eslint/naming-convention nvp_lastPaymentMethod: ONYXKEYS.NVP_LAST_PAYMENT_METHOD, @@ -30,14 +31,15 @@ export default function () { key: oldKey, callback: (value) => { Onyx.disconnect(connectionID); - if (value !== null) { - // @ts-expect-error These keys are variables, so we can't check the type - Onyx.multiSet({ - [newKey]: value, - [oldKey]: null, - }); + if (value === null) { + resolveWhenDone(); + return; } - resolveWhenDone(); + // @ts-expect-error These keys are variables, so we can't check the type + Onyx.multiSet({ + [newKey]: value, + [oldKey]: null, + }).then(resolveWhenDone); }, }); } @@ -46,18 +48,19 @@ export default function () { callback: (value) => { Onyx.disconnect(connectionIDAccount); // @ts-expect-error we are removing this property, so it is not in the type anymore - if (value?.activePolicyID) { - // @ts-expect-error we are removing this property, so it is not in the type anymore - const activePolicyID = value.activePolicyID; - const newValue = {...value}; - // @ts-expect-error we are removing this property, so it is not in the type anymore - delete newValue.activePolicyID; - Onyx.multiSet({ - [ONYXKEYS.NVP_ACTIVE_POLICY_ID]: activePolicyID, - [ONYXKEYS.ACCOUNT]: newValue, - }); + if (!value?.activePolicyID) { + resolveWhenDone(); + return; } - resolveWhenDone(); + // @ts-expect-error we are removing this property, so it is not in the type anymore + const activePolicyID = value.activePolicyID; + const newValue = {...value}; + // @ts-expect-error we are removing this property, so it is not in the type anymore + delete newValue.activePolicyID; + Onyx.multiSet({ + [ONYXKEYS.NVP_ACTIVE_POLICY_ID]: activePolicyID, + [ONYXKEYS.ACCOUNT]: newValue, + }).then(resolveWhenDone); }, }); const connectionIDRecentlyUsedTags = Onyx.connect({ @@ -76,8 +79,7 @@ export default function () { // @ts-expect-error We have no fixed types here newValue[key] = null; } - Onyx.multiSet(newValue); - resolveWhenDone(); + Onyx.multiSet(newValue).then(resolveWhenDone); }, }); }); From 56b4a8a3b05ea050c92d477ec344617db3b0b97b Mon Sep 17 00:00:00 2001 From: smelaa Date: Wed, 6 Mar 2024 17:48:38 +0100 Subject: [PATCH 186/484] RoomHeaderAvatars migrated to ts --- src/ROUTES.ts | 2 +- src/components/Avatar.tsx | 44 +-------------- ...HeaderAvatars.js => RoomHeaderAvatars.tsx} | 54 +++++++++---------- src/components/types.ts | 44 +++++++++++++++ 4 files changed, 71 insertions(+), 73 deletions(-) rename src/components/{RoomHeaderAvatars.js => RoomHeaderAvatars.tsx} (72%) create mode 100644 src/components/types.ts diff --git a/src/ROUTES.ts b/src/ROUTES.ts index cfc287ba2cdc..8c4a9c47f570 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -37,7 +37,7 @@ const ROUTES = { }, PROFILE_AVATAR: { route: 'a/:accountID/avatar', - getRoute: (accountID: string) => `a/${accountID}/avatar` as const, + getRoute: (accountID: string | number) => `a/${accountID}/avatar` as const, }, TRANSITION_BETWEEN_APPS: 'transition', diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index 2b2d0a60f657..4ce50ecad0cc 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -1,55 +1,16 @@ import React, {useEffect, useState} from 'react'; -import type {ImageStyle, StyleProp, ViewStyle} from 'react-native'; +import type {ImageStyle, StyleProp} from 'react-native'; import {View} from 'react-native'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ReportUtils from '@libs/ReportUtils'; -import type {AvatarSource} from '@libs/UserUtils'; -import type {AvatarSizeName} from '@styles/utils'; import CONST from '@src/CONST'; -import type {AvatarType} from '@src/types/onyx/OnyxCommon'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import Image from './Image'; - -type AvatarProps = { - /** Source for the avatar. Can be a URL or an icon. */ - source?: AvatarSource; - - /** Extra styles to pass to Image */ - imageStyles?: StyleProp; - - /** Additional styles to pass to Icon */ - iconAdditionalStyles?: StyleProp; - - /** Extra styles to pass to View wrapper */ - containerStyles?: StyleProp; - - /** Set the size of Avatar */ - size?: AvatarSizeName; - - /** - * The fill color for the icon. Can be hex, rgb, rgba, or valid react-native named color such as 'red' or 'blue' - * If the avatar is type === workspace, this fill color will be ignored and decided based on the name prop. - */ - fill?: string; - - /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. - * If the avatar is type === workspace, this fallback icon will be ignored and decided based on the name prop. - */ - fallbackIcon?: AvatarSource; - - /** Used to locate fallback icon in end-to-end tests. */ - fallbackIconTestID?: string; - - /** Denotes whether it is an avatar or a workspace avatar */ - type?: AvatarType; - - /** Owner of the avatar. If user, displayName. If workspace, policy name */ - name?: string; -}; +import type AvatarProps from './types'; function Avatar({ source, @@ -124,4 +85,3 @@ function Avatar({ Avatar.displayName = 'Avatar'; export default Avatar; -export {type AvatarProps}; diff --git a/src/components/RoomHeaderAvatars.js b/src/components/RoomHeaderAvatars.tsx similarity index 72% rename from src/components/RoomHeaderAvatars.js rename to src/components/RoomHeaderAvatars.tsx index 64cc9ac7abf3..836e55e79567 100644 --- a/src/components/RoomHeaderAvatars.js +++ b/src/components/RoomHeaderAvatars.tsx @@ -1,63 +1,58 @@ -import PropTypes from 'prop-types'; import React, {memo} from 'react'; import {View} from 'react-native'; -import _ from 'underscore'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import Avatar from './Avatar'; -import avatarPropTypes from './avatarPropTypes'; import PressableWithoutFocus from './Pressable/PressableWithoutFocus'; import Text from './Text'; +import type AvatarProps from './types'; -const propTypes = { - icons: PropTypes.arrayOf(avatarPropTypes), - reportID: PropTypes.string, +type RoomHeaderAvatarsProps = { + icons: AvatarProps[]; + reportID: string; }; -const defaultProps = { - icons: [], - reportID: '', -}; - -function RoomHeaderAvatars(props) { - const navigateToAvatarPage = (icon) => { +function RoomHeaderAvatars({icons = [], reportID = ''}: RoomHeaderAvatarsProps) { + const navigateToAvatarPage = (icon: AvatarProps) => { if (icon.type === CONST.ICON_TYPE_WORKSPACE) { - Navigation.navigate(ROUTES.REPORT_AVATAR.getRoute(props.reportID)); + Navigation.navigate(ROUTES.REPORT_AVATAR.getRoute(reportID)); return; } - Navigation.navigate(ROUTES.PROFILE_AVATAR.getRoute(icon.id)); + if (icon.id !== undefined) { + Navigation.navigate(ROUTES.PROFILE_AVATAR.getRoute(icon.id)); + } }; const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - if (!props.icons.length) { + if (icons.length) { return null; } - if (props.icons.length === 1) { + if (icons.length === 1) { return ( navigateToAvatarPage(props.icons[0])} + onPress={() => navigateToAvatarPage(icons[0])} accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} - accessibilityLabel={props.icons[0].name} + accessibilityLabel={icons[0].name ?? ''} > ); } - const iconsToDisplay = props.icons.slice(0, CONST.REPORT.MAX_PREVIEW_AVATARS); + const iconsToDisplay = icons.slice(0, CONST.REPORT.MAX_PREVIEW_AVATARS); const iconStyle = [ styles.roomHeaderAvatar, @@ -68,8 +63,9 @@ function RoomHeaderAvatars(props) { return ( - {_.map(iconsToDisplay, (icon, index) => ( + {iconsToDisplay.map((icon, index) => ( @@ -77,7 +73,7 @@ function RoomHeaderAvatars(props) { style={[styles.mln4, StyleUtils.getAvatarBorderRadius(CONST.AVATAR_SIZE.LARGE_BORDERED, icon.type)]} onPress={() => navigateToAvatarPage(icon)} accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} - accessibilityLabel={icon.name} + accessibilityLabel={icon.name ?? ''} > - {index === CONST.REPORT.MAX_PREVIEW_AVATARS - 1 && props.icons.length - CONST.REPORT.MAX_PREVIEW_AVATARS !== 0 && ( + {index === CONST.REPORT.MAX_PREVIEW_AVATARS - 1 && icons.length - CONST.REPORT.MAX_PREVIEW_AVATARS !== 0 && ( <> - {`+${props.icons.length - CONST.REPORT.MAX_PREVIEW_AVATARS}`} + {`+${icons.length - CONST.REPORT.MAX_PREVIEW_AVATARS}`} )} @@ -110,8 +106,6 @@ function RoomHeaderAvatars(props) { ); } -RoomHeaderAvatars.defaultProps = defaultProps; -RoomHeaderAvatars.propTypes = propTypes; RoomHeaderAvatars.displayName = 'RoomHeaderAvatars'; export default memo(RoomHeaderAvatars); diff --git a/src/components/types.ts b/src/components/types.ts new file mode 100644 index 000000000000..a1cf9c2a0c4f --- /dev/null +++ b/src/components/types.ts @@ -0,0 +1,44 @@ +import type {ImageStyle, StyleProp, ViewStyle} from 'react-native'; +import type {AvatarSource} from '@libs/UserUtils'; +import type {AvatarSizeName} from '@styles/utils'; +import type {AvatarType} from '@src/types/onyx/OnyxCommon'; + +type AvatarProps = { + /** Source for the avatar. Can be a URL or an icon. */ + source?: AvatarSource; + + /** Extra styles to pass to Image */ + imageStyles?: StyleProp; + + /** Additional styles to pass to Icon */ + iconAdditionalStyles?: StyleProp; + + /** Extra styles to pass to View wrapper */ + containerStyles?: StyleProp; + + /** Set the size of Avatar */ + size?: AvatarSizeName; + + /** + * The fill color for the icon. Can be hex, rgb, rgba, or valid react-native named color such as 'red' or 'blue' + * If the avatar is type === workspace, this fill color will be ignored and decided based on the name prop. + */ + fill?: string; + + /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. + * If the avatar is type === workspace, this fallback icon will be ignored and decided based on the name prop. + */ + fallbackIcon?: AvatarSource; + + /** Used to locate fallback icon in end-to-end tests. */ + fallbackIconTestID?: string; + + /** Denotes whether it is an avatar or a workspace avatar */ + type?: AvatarType; + + /** Owner of the avatar. If user, displayName. If workspace, policy name */ + name?: string; + id?: string | number; +}; + +export default AvatarProps; From d34836f499b6ccb87d2e1fcfd327d31158704f0c Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Wed, 6 Mar 2024 12:02:32 -0500 Subject: [PATCH 187/484] use const --- 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 4899f2181a5a..e8d5c085fc5b 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -98,7 +98,7 @@ function isDeletedAction(reportAction: OnyxEntry Date: Wed, 6 Mar 2024 12:05:26 -0500 Subject: [PATCH 188/484] fix condition order --- 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 e8d5c085fc5b..8744e178d8c1 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -100,7 +100,7 @@ function isDeletedAction(reportAction: OnyxEntry): boolean { From af1e735cc2bf31d610ed4fb6a3fc2af697055f5c Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Wed, 6 Mar 2024 12:25:34 -0500 Subject: [PATCH 189/484] fix optional chaining --- 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 eb215a29bc78..22e1666e0988 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -100,7 +100,7 @@ function isDeletedAction(reportAction: OnyxEntry): boolean { From be58c4f67eaf2c6075da772f4554cc8693c99d3b Mon Sep 17 00:00:00 2001 From: Ionatan Wiznia Date: Wed, 6 Mar 2024 20:44:41 +0100 Subject: [PATCH 190/484] Update src/libs/migrations/NVPMigration.ts Co-authored-by: Tim Golen --- src/libs/migrations/NVPMigration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/migrations/NVPMigration.ts b/src/libs/migrations/NVPMigration.ts index 26375c1858eb..9ab774328f78 100644 --- a/src/libs/migrations/NVPMigration.ts +++ b/src/libs/migrations/NVPMigration.ts @@ -22,7 +22,7 @@ const migrations = { // This migration changes the keys of all the NVP related keys so that they are standardized export default function () { return new Promise((resolve) => { - // We add the number of manual connections we add below + // Resolve the migration when all the keys have been migrated. The number of keys is the size of the `migrations` object in addition to the ACCOUNT and OLD_POLICY_RECENTLY_USED_TAGS keys (which is why there is a +2). const resolveWhenDone = after(Object.entries(migrations).length + 2, () => resolve()); for (const [oldKey, newKey] of Object.entries(migrations)) { From 57d96be016668818515c567588ce6ce67c0a2974 Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 7 Mar 2024 15:41:52 +0700 Subject: [PATCH 191/484] lint fix --- src/libs/mapChildrenFlat.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/mapChildrenFlat.ts b/src/libs/mapChildrenFlat.ts index a50e9ebf54ab..73009a3340d4 100644 --- a/src/libs/mapChildrenFlat.ts +++ b/src/libs/mapChildrenFlat.ts @@ -16,7 +16,7 @@ import React from 'react'; */ const mapChildrenFlat = (element: C, fn: (child: C, index: number) => T) => { if (typeof element === 'function') { - return element(false); + return element(false) as C; } const mappedChildren = React.Children.map(element, fn); From 71dcc0364383f10e39cd8f0b05f9ba9c8459b1c2 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Thu, 7 Mar 2024 15:52:09 +0700 Subject: [PATCH 192/484] fix in app sound is played if user not viewing chat --- src/libs/actions/User.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 708fc5e8591d..e347fddfb4a7 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -30,6 +30,7 @@ import PusherUtils from '@libs/PusherUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import playSound, {SOUNDS} from '@libs/Sound'; import playSoundExcludingMobile from '@libs/Sound/playSoundExcludingMobile'; +import Visibility from '@libs/Visibility'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -489,7 +490,11 @@ const isChannelMuted = (reportId: string) => function playSoundForMessageType(pushJSON: OnyxServerUpdate[]) { const reportActionsOnly = pushJSON.filter((update) => update.key?.includes('reportActions_')); // "reportActions_5134363522480668" -> "5134363522480668" - const reportIDs = reportActionsOnly.map((value) => value.key.split('_')[1]); + const reportIDs = reportActionsOnly + .map((value) => value.key.split('_')[1]) + .filter((reportID) => { + return reportID === Navigation.getTopmostReportId() && Visibility.isVisible() && Visibility.hasFocus(); + }); Promise.all(reportIDs.map((reportID) => isChannelMuted(reportID))) .then((muted) => muted.every((isMuted) => isMuted)) From 82e1fe56e4710afdb0a30824cdc6c736c3ab9aeb Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Thu, 7 Mar 2024 14:41:24 +0530 Subject: [PATCH 193/484] Completing UI changes in Request for Track Expense --- ...oraryForRefactorRequestConfirmationList.js | 2 +- src/components/OptionRow.tsx | 7 +++- src/components/ReportWelcomeText.tsx | 10 +++--- src/languages/en.ts | 3 ++ src/languages/es.ts | 3 ++ src/libs/OptionsListUtils.ts | 32 +++++++++++++++++++ src/libs/ReportUtils.ts | 23 +++++++++++-- src/libs/actions/IOU.ts | 10 +++--- .../AttachmentPickerWithMenuItems.tsx | 11 +++++-- .../FloatingActionButtonAndPopover.js | 4 +-- src/pages/iou/request/IOURequestStartPage.js | 2 +- .../iou/request/step/IOURequestStepAmount.js | 2 +- .../step/IOURequestStepConfirmation.js | 15 +++------ .../request/step/IOURequestStepDistance.js | 2 +- .../request/step/IOURequestStepScan/index.js | 2 +- .../step/IOURequestStepScan/index.native.js | 2 +- .../step/IOURequestStepTaxAmountPage.js | 1 + 17 files changed, 98 insertions(+), 33 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index a3d61e5ad813..e60c99fce6d7 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -381,7 +381,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const splitOrRequestOptions = useMemo(() => { let text; if (isTypeTrackExpense) { - text = 'Track Expense'; + text = translate('iou.trackExpense'); } else if (isTypeSplit && iouAmount === 0) { text = translate('iou.split'); } else if ((receiptPath && isTypeRequest) || isDistanceRequestWithPendingRoute) { diff --git a/src/components/OptionRow.tsx b/src/components/OptionRow.tsx index 7b45fd963fe7..61319de5c56b 100644 --- a/src/components/OptionRow.tsx +++ b/src/components/OptionRow.tsx @@ -229,7 +229,12 @@ function OptionRow({ numberOfLines={isMultilineSupported ? 2 : 1} textStyles={displayNameStyle} shouldUseFullTitle={ - !!option.isChatRoom || !!option.isPolicyExpenseChat || !!option.isMoneyRequestReport || !!option.isThread || !!option.isTaskReport + !!option.isChatRoom || + !!option.isPolicyExpenseChat || + !!option.isMoneyRequestReport || + !!option.isThread || + !!option.isTaskReport || + !!option.isSelfDM } /> {option.alternateText ? ( diff --git a/src/components/ReportWelcomeText.tsx b/src/components/ReportWelcomeText.tsx index e9bbd0f27bdc..219199c25bc3 100644 --- a/src/components/ReportWelcomeText.tsx +++ b/src/components/ReportWelcomeText.tsx @@ -3,6 +3,7 @@ import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; +import usePermissions from '@hooks/usePermissions'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; @@ -33,6 +34,7 @@ type ReportWelcomeTextProps = ReportWelcomeTextOnyxProps & { function ReportWelcomeText({report, policy, personalDetails}: ReportWelcomeTextProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); + const {canUseTrackExpense} = usePermissions(); const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(report); const isChatRoom = ReportUtils.isChatRoom(report); const isSelfDM = ReportUtils.isSelfDM(report); @@ -42,7 +44,7 @@ function ReportWelcomeText({report, policy, personalDetails}: ReportWelcomeTextP const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(OptionsListUtils.getPersonalDetailsForAccountIDs(participantAccountIDs, personalDetails), isMultipleParticipant); const isUserPolicyAdmin = PolicyUtils.isPolicyAdmin(policy); const roomWelcomeMessage = ReportUtils.getRoomWelcomeMessage(report, isUserPolicyAdmin); - const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, policy, participantAccountIDs); + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, policy, participantAccountIDs, canUseTrackExpense); const additionalText = moneyRequestOptions.map((item) => translate(`reportActionsView.iouTypes.${item}`)).join(', '); const canEditPolicyDescription = ReportUtils.canEditPolicyDescription(policy); const reportName = ReportUtils.getReportName(report); @@ -158,9 +160,9 @@ function ReportWelcomeText({report, policy, personalDetails}: ReportWelcomeTextP ))} )} - {(moneyRequestOptions.includes(CONST.IOU.TYPE.SEND) || moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST)) && ( - {translate('reportActionsView.usePlusButton', {additionalText})} - )} + {(moneyRequestOptions.includes(CONST.IOU.TYPE.SEND) || + moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST) || + moneyRequestOptions.includes(CONST.IOU.TYPE.TRACK_EXPENSE)) && {translate('reportActionsView.usePlusButton', {additionalText})}} ); 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 0873e42968a2ed50b410973afd4687d81e843bb9 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Thu, 7 Mar 2024 16:16:41 +0700 Subject: [PATCH 194/484] fix lint --- src/libs/actions/User.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index e347fddfb4a7..77efb30ae874 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -492,9 +492,7 @@ function playSoundForMessageType(pushJSON: OnyxServerUpdate[]) { // "reportActions_5134363522480668" -> "5134363522480668" const reportIDs = reportActionsOnly .map((value) => value.key.split('_')[1]) - .filter((reportID) => { - return reportID === Navigation.getTopmostReportId() && Visibility.isVisible() && Visibility.hasFocus(); - }); + .filter((reportID) => reportID === Navigation.getTopmostReportId() && Visibility.isVisible() && Visibility.hasFocus()); Promise.all(reportIDs.map((reportID) => isChannelMuted(reportID))) .then((muted) => muted.every((isMuted) => isMuted)) From 33a0c3ebcc9b4390706dc68247a02abd26c69f71 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Thu, 7 Mar 2024 16:23:49 +0700 Subject: [PATCH 195/484] 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 196/484] 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 197/484] 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 198/484] 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 fa08a81639143936fe795593ecf386c9500e924d Mon Sep 17 00:00:00 2001 From: smelaa Date: Thu, 7 Mar 2024 11:37:29 +0100 Subject: [PATCH 199/484] Address reviewer's comments --- src/components/Avatar.tsx | 47 ++++++++++++++++++++++++++-- src/components/RoomHeaderAvatars.tsx | 6 ++-- src/components/types.ts | 44 -------------------------- 3 files changed, 48 insertions(+), 49 deletions(-) delete mode 100644 src/components/types.ts diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index 4ce50ecad0cc..e5725b779110 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -1,16 +1,58 @@ import React, {useEffect, useState} from 'react'; -import type {ImageStyle, StyleProp} from 'react-native'; import {View} from 'react-native'; +import type {ImageStyle, StyleProp, ViewStyle} from 'react-native'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ReportUtils from '@libs/ReportUtils'; +import type {AvatarSource} from '@libs/UserUtils'; +import type {AvatarSizeName} from '@styles/utils'; import CONST from '@src/CONST'; +import type {AvatarType} from '@src/types/onyx/OnyxCommon'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import Image from './Image'; -import type AvatarProps from './types'; + +type AvatarProps = { + /** Source for the avatar. Can be a URL or an icon. */ + source?: AvatarSource; + + /** Extra styles to pass to Image */ + imageStyles?: StyleProp; + + /** Additional styles to pass to Icon */ + iconAdditionalStyles?: StyleProp; + + /** Extra styles to pass to View wrapper */ + containerStyles?: StyleProp; + + /** Set the size of Avatar */ + size?: AvatarSizeName; + + /** + * The fill color for the icon. Can be hex, rgb, rgba, or valid react-native named color such as 'red' or 'blue' + * If the avatar is type === workspace, this fill color will be ignored and decided based on the name prop. + */ + fill?: string; + + /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. + * If the avatar is type === workspace, this fallback icon will be ignored and decided based on the name prop. + */ + fallbackIcon?: AvatarSource; + + /** Used to locate fallback icon in end-to-end tests. */ + fallbackIconTestID?: string; + + /** Denotes whether it is an avatar or a workspace avatar */ + type?: AvatarType; + + /** Owner of the avatar. If user, displayName. If workspace, policy name */ + name?: string; + // this prop is used in RoomHeaderAvatars + // eslint-disable-next-line react/no-unused-prop-types + id?: string | number; +}; function Avatar({ source, @@ -85,3 +127,4 @@ function Avatar({ Avatar.displayName = 'Avatar'; export default Avatar; +export {type AvatarProps}; diff --git a/src/components/RoomHeaderAvatars.tsx b/src/components/RoomHeaderAvatars.tsx index 836e55e79567..dd986ce39e31 100644 --- a/src/components/RoomHeaderAvatars.tsx +++ b/src/components/RoomHeaderAvatars.tsx @@ -5,10 +5,10 @@ import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; +import type {AvatarProps} from './Avatar'; import Avatar from './Avatar'; import PressableWithoutFocus from './Pressable/PressableWithoutFocus'; import Text from './Text'; -import type AvatarProps from './types'; type RoomHeaderAvatarsProps = { icons: AvatarProps[]; @@ -21,14 +21,14 @@ function RoomHeaderAvatars({icons = [], reportID = ''}: RoomHeaderAvatarsProps) Navigation.navigate(ROUTES.REPORT_AVATAR.getRoute(reportID)); return; } - if (icon.id !== undefined) { + if (icon.id) { Navigation.navigate(ROUTES.PROFILE_AVATAR.getRoute(icon.id)); } }; const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - if (icons.length) { + if (!icons.length) { return null; } diff --git a/src/components/types.ts b/src/components/types.ts deleted file mode 100644 index a1cf9c2a0c4f..000000000000 --- a/src/components/types.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type {ImageStyle, StyleProp, ViewStyle} from 'react-native'; -import type {AvatarSource} from '@libs/UserUtils'; -import type {AvatarSizeName} from '@styles/utils'; -import type {AvatarType} from '@src/types/onyx/OnyxCommon'; - -type AvatarProps = { - /** Source for the avatar. Can be a URL or an icon. */ - source?: AvatarSource; - - /** Extra styles to pass to Image */ - imageStyles?: StyleProp; - - /** Additional styles to pass to Icon */ - iconAdditionalStyles?: StyleProp; - - /** Extra styles to pass to View wrapper */ - containerStyles?: StyleProp; - - /** Set the size of Avatar */ - size?: AvatarSizeName; - - /** - * The fill color for the icon. Can be hex, rgb, rgba, or valid react-native named color such as 'red' or 'blue' - * If the avatar is type === workspace, this fill color will be ignored and decided based on the name prop. - */ - fill?: string; - - /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. - * If the avatar is type === workspace, this fallback icon will be ignored and decided based on the name prop. - */ - fallbackIcon?: AvatarSource; - - /** Used to locate fallback icon in end-to-end tests. */ - fallbackIconTestID?: string; - - /** Denotes whether it is an avatar or a workspace avatar */ - type?: AvatarType; - - /** Owner of the avatar. If user, displayName. If workspace, policy name */ - name?: string; - id?: string | number; -}; - -export default AvatarProps; From c2ef07b39e38d3da0b742db6ce65444164742fd7 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Thu, 7 Mar 2024 20:30:56 +0530 Subject: [PATCH 200/484] 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 17377dae19c912f0932ed6fed7e42fad63c747aa Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 7 Mar 2024 16:28:47 +0100 Subject: [PATCH 201/484] add type PolicyFeatureName --- src/types/onyx/Policy.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index b005a9d2756f..c378c0eb3983 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -82,6 +82,8 @@ type Connection = { type AutoReportingOffset = number | ValueOf; +type PolicyFeatureName = 'areCategoriesEnabled' | 'areTagsEnabled' | 'areDistanceRatesEnabled' | 'areWorkflowsEnabled' | 'areReportFieldsEnabled' | 'areConnectionsEnabled' | 'tax'; + type Policy = OnyxCommon.OnyxValueWithOfflineFeedback< { /** The ID of the policy */ @@ -250,4 +252,4 @@ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback< export default Policy; -export type {Unit, CustomUnit, Attributes, Rate, TaxRate, TaxRates, TaxRatesWithDefault}; +export type {Unit, CustomUnit, Attributes, Rate, TaxRate, TaxRates, TaxRatesWithDefault, PolicyFeatureName}; From 7ed9fa60385e8802367147dcda6eee703c6a395d Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 7 Mar 2024 16:28:56 +0100 Subject: [PATCH 202/484] add a helper isPolicyFeatureEnabled --- src/libs/PolicyUtils.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index b510edd7dcf4..3dfbc8688297 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -5,6 +5,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {PersonalDetailsList, Policy, PolicyMembers, PolicyTagList, PolicyTags} from '@src/types/onyx'; +import type {PolicyFeatureName} from '@src/types/onyx/Policy'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import Navigation from './Navigation/Navigation'; @@ -268,6 +269,10 @@ function goBackFromInvalidPolicy() { Navigation.navigateWithSwitchPolicyID({route: ROUTES.ALL_SETTINGS}); } +function isPolicyFeatureEnabled(policy: OnyxEntry | EmptyObject, featureName: PolicyFeatureName): boolean { + return Boolean(policy?.[featureName]); +} + export { getActivePolicies, hasAccountingConnections, @@ -299,6 +304,7 @@ export { getPathWithoutPolicyID, getPolicyMembersByIdWithoutCurrentUser, goBackFromInvalidPolicy, + isPolicyFeatureEnabled, }; export type {MemberEmailsToAccountIDs}; From c8a73121879bfdf872ba305f5bfe685a86678c93 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 7 Mar 2024 16:29:06 +0100 Subject: [PATCH 203/484] implement FeatureEnabledAccessOrNotFoundComponent --- .../FeatureEnabledAccessOrNotFoundWrapper.tsx | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 src/pages/workspace/FeatureEnabledAccessOrNotFoundWrapper.tsx diff --git a/src/pages/workspace/FeatureEnabledAccessOrNotFoundWrapper.tsx b/src/pages/workspace/FeatureEnabledAccessOrNotFoundWrapper.tsx new file mode 100644 index 000000000000..8f9ff546b98e --- /dev/null +++ b/src/pages/workspace/FeatureEnabledAccessOrNotFoundWrapper.tsx @@ -0,0 +1,70 @@ +/* eslint-disable rulesdir/no-negated-variables */ +import React, {useEffect} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import Navigation from '@libs/Navigation/Navigation'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; +import * as Policy from '@userActions/Policy'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type * as OnyxTypes from '@src/types/onyx'; +import type {PolicyFeatureName} from '@src/types/onyx/Policy'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; + +type FeatureEnabledAccessOrNotFoundOnyxProps = { + /** The report currently being looked at */ + policy: OnyxEntry; + + /** Indicated whether the report data is loading */ + isLoadingReportData: OnyxEntry; +}; + +type FeatureEnabledAccessOrNotFoundComponentProps = FeatureEnabledAccessOrNotFoundOnyxProps & { + /** The children to render */ + children: ((props: FeatureEnabledAccessOrNotFoundOnyxProps) => React.ReactNode) | React.ReactNode; + + /** The report currently being looked at */ + policyID: string; + + /** The current feature name that the user tries to get access */ + featureName: PolicyFeatureName; +}; + +function FeatureEnabledAccessOrNotFoundComponent(props: FeatureEnabledAccessOrNotFoundComponentProps) { + const isPolicyIDInRoute = !!props.policyID?.length; + + useEffect(() => { + if (!isPolicyIDInRoute || !isEmptyObject(props.policy)) { + // If the workspace is not required or is already loaded, we don't need to call the API + return; + } + + Policy.openWorkspace(props.policyID, []); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isPolicyIDInRoute, props.policyID]); + + const shouldShowFullScreenLoadingIndicator = props.isLoadingReportData !== false && (!Object.entries(props.policy ?? {}).length || !props.policy?.id); + + const shouldShowNotFoundPage = isEmptyObject(props.policy) || !props.policy?.id || !PolicyUtils.isPolicyFeatureEnabled(props.policy, props.featureName); + + if (shouldShowFullScreenLoadingIndicator) { + return ; + } + + if (shouldShowNotFoundPage) { + return Navigation.goBack(ROUTES.WORKSPACE_PROFILE.getRoute(props.policyID))} />; + } + + return typeof props.children === 'function' ? props.children(props) : props.children; +} + +export default withOnyx({ + policy: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID ?? ''}`, + }, + isLoadingReportData: { + key: ONYXKEYS.IS_LOADING_REPORT_DATA, + }, +})(FeatureEnabledAccessOrNotFoundComponent); From cf310c3cddc0d9de1574f166cee92490dff039d8 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 7 Mar 2024 16:29:41 +0100 Subject: [PATCH 204/484] integrate FeatureEnabledAccessOrNotFoundComponent to WorkspaceCategoriesPage --- .../categories/WorkspaceCategoriesPage.tsx | 92 ++++++++++--------- 1 file changed, 49 insertions(+), 43 deletions(-) diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index 52d18d8de276..f2987bf624a1 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -23,6 +23,7 @@ import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import type {CentralPaneNavigatorParamList} from '@navigation/types'; import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; @@ -152,51 +153,56 @@ function WorkspaceCategoriesPage({policy, policyCategories, route}: WorkspaceCat return ( - - - {!isSmallScreenWidth && headerButtons} - - {isSmallScreenWidth && {headerButtons}} - - {translate('workspace.categories.subtitle')} - - {isLoading && ( - - )} - {categoryList.length === 0 && !isLoading && ( - - )} - {categoryList.length > 0 && ( - - )} - + + {!isSmallScreenWidth && headerButtons} + + {isSmallScreenWidth && {headerButtons}} + + {translate('workspace.categories.subtitle')} + + {isLoading && ( + + )} + {categoryList.length === 0 && !isLoading && ( + + )} + {categoryList.length > 0 && ( + + )} + + ); From 941e7fa6390f7b81ac645bdf171f5780eb7be018 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 7 Mar 2024 16:41:47 +0100 Subject: [PATCH 205/484] integrate more features consts --- src/CONST.ts | 9 +++++++++ .../workspace/categories/WorkspaceCategoriesPage.tsx | 2 +- src/types/onyx/Policy.ts | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 6861fe174ffe..b0470046f9b6 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1412,6 +1412,15 @@ const CONST = { MAKE_MEMBER: 'makeMember', MAKE_ADMIN: 'makeAdmin', }, + MORE_FEATURES: { + ARE_CATEGORIES_ENABLED: 'areCategoriesEnabled', + ARE_TAGS_ENABLED: 'areTagsEnabled', + ARE_DISTANCE_RATES_ENABLED: 'areDistanceRatesEnabled', + ARE_WORKFLOWS_ENABLED: 'areWorkflowsEnabled', + ARE_REPORTFIELDS_ENABLED: 'areReportFieldsEnabled', + ARE_CONNECTIONS_ENABLED: 'areConnectionsEnabled', + ARE_TAXES_ENABLED: 'tax', + }, }, CUSTOM_UNITS: { diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index f2987bf624a1..e3362d26f014 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -155,7 +155,7 @@ function WorkspaceCategoriesPage({policy, policyCategories, route}: WorkspaceCat ; -type PolicyFeatureName = 'areCategoriesEnabled' | 'areTagsEnabled' | 'areDistanceRatesEnabled' | 'areWorkflowsEnabled' | 'areReportFieldsEnabled' | 'areConnectionsEnabled' | 'tax'; +type PolicyFeatureName = ValueOf; type Policy = OnyxCommon.OnyxValueWithOfflineFeedback< { From 699f38a993dbbea3d80c8cce7780e9ed8be70996 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 7 Mar 2024 16:48:11 +0100 Subject: [PATCH 206/484] integrate FeatureEnabledAccessOrNotFoundComponent to WorkspaceTagsPage --- .../workspace/tags/WorkspaceTagsPage.tsx | 71 ++++++++++--------- 1 file changed, 39 insertions(+), 32 deletions(-) diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index c82740eff361..125695856e4c 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -19,7 +19,9 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import * as PolicyUtils from '@libs/PolicyUtils'; import type {CentralPaneNavigatorParamList} from '@navigation/types'; import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; @@ -96,40 +98,45 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) { return ( - - - - {translate('workspace.tags.subtitle')} - - {tagList.length ? ( - {}} - onSelectAll={toggleAllTags} - showScrollIndicator - ListItem={TableListItem} - customListHeader={getCustomListHeader()} - listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} + + - ) : ( - - )} - + + {translate('workspace.tags.subtitle')} + + {tagList.length ? ( + {}} + onSelectAll={toggleAllTags} + showScrollIndicator + ListItem={TableListItem} + customListHeader={getCustomListHeader()} + listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} + /> + ) : ( + + )} + + ); From daacbfebcf1322ffedb65b430d6ea8e7c5ec4db9 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 7 Mar 2024 16:48:18 +0100 Subject: [PATCH 207/484] integrate FeatureEnabledAccessOrNotFoundComponent to WorkspaceCategoriesSettingsPage --- .../WorkspaceCategoriesSettingsPage.tsx | 59 +++++++++++-------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx index a2ce06270c33..02ae87ce05d0 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx @@ -11,7 +11,9 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {setWorkspaceRequiresCategory} from '@libs/actions/Policy'; import type {SettingsNavigatorParamList} from '@navigation/types'; import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; +import CONST from '@src/CONST'; import type SCREENS from '@src/SCREENS'; type WorkspaceCategoriesSettingsPageProps = StackScreenProps; @@ -27,33 +29,38 @@ function WorkspaceCategoriesSettingsPage({route}: WorkspaceCategoriesSettingsPag return ( - {({policy}) => ( - - - - - - - {translate('workspace.categories.requiresCategory')} - + + {({policy}) => ( + + + + + + + {translate('workspace.categories.requiresCategory')} + + - - - - - )} + + + + )} + ); From 7fb586ef9a7d7b255493c8d514a82024eac349fb Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 7 Mar 2024 16:48:26 +0100 Subject: [PATCH 208/484] integrate FeatureEnabledAccessOrNotFoundComponent to CreateCategoryPage --- .../categories/CreateCategoryPage.tsx | 56 ++++++++++--------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/src/pages/workspace/categories/CreateCategoryPage.tsx b/src/pages/workspace/categories/CreateCategoryPage.tsx index cfe28ba292b0..332371c866b6 100644 --- a/src/pages/workspace/categories/CreateCategoryPage.tsx +++ b/src/pages/workspace/categories/CreateCategoryPage.tsx @@ -16,6 +16,7 @@ import Navigation from '@libs/Navigation/Navigation'; import * as ValidationUtils from '@libs/ValidationUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; @@ -68,34 +69,39 @@ function CreateCategoryPage({route, policyCategories}: CreateCategoryPageProps) return ( - - - - - - + + + + + ); From 4bd3e8ac55a6140e96163deabeac37fc6bcfc0d5 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Thu, 7 Mar 2024 16:48:33 +0100 Subject: [PATCH 209/484] integrate FeatureEnabledAccessOrNotFoundComponent to CategorySettingsPage --- .../categories/CategorySettingsPage.tsx | 63 ++++++++++--------- 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/src/pages/workspace/categories/CategorySettingsPage.tsx b/src/pages/workspace/categories/CategorySettingsPage.tsx index 16f128e5ea1f..97e71abb7a2e 100644 --- a/src/pages/workspace/categories/CategorySettingsPage.tsx +++ b/src/pages/workspace/categories/CategorySettingsPage.tsx @@ -16,8 +16,10 @@ import * as ErrorUtils from '@libs/ErrorUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import * as Policy from '@userActions/Policy'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; @@ -46,36 +48,41 @@ function CategorySettingsPage({route, policyCategories}: CategorySettingsPagePro return ( - - - - Policy.clearCategoryErrors(route.params.policyID, route.params.categoryName)} - > - - - {translate('workspace.categories.enableCategory')} - + + + + Policy.clearCategoryErrors(route.params.policyID, route.params.categoryName)} + > + + + {translate('workspace.categories.enableCategory')} + + - - - - - + + + + + ); From f5040bfccb2ea4f808a94fc721b6116affd98401 Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Thu, 7 Mar 2024 14:49:42 -0500 Subject: [PATCH 210/484] clear isOptimisticReport on success --- src/libs/actions/IOU.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index cb3aa20ab6a7..6d8638bf4403 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -592,6 +592,7 @@ function buildOnyxDataForMoneyRequest( value: { pendingFields: null, errorFields: null, + isOptimisticReport: false, }, }); } From 1245b7963674bb46a19b47ddc28a4be7ae457d02 Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Thu, 7 Mar 2024 15:07:15 -0500 Subject: [PATCH 211/484] unify pre-fetch report checks and fix issues with fetching optimistic reports --- src/libs/shouldFetchReport.ts | 5 +++++ src/pages/home/ReportScreen.js | 5 +++++ src/pages/home/report/ReportActionsView.js | 17 ++++++++++++----- 3 files changed, 22 insertions(+), 5 deletions(-) create mode 100644 src/libs/shouldFetchReport.ts diff --git a/src/libs/shouldFetchReport.ts b/src/libs/shouldFetchReport.ts new file mode 100644 index 000000000000..f84058de4b99 --- /dev/null +++ b/src/libs/shouldFetchReport.ts @@ -0,0 +1,5 @@ +import {Report} from '@src/types/onyx'; + +export default function shouldFetchReport(report: Report) { + return !report?.isOptimisticReport && !report?.errorFields?.createChat; +} diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index da5a8e4aae27..2cc15c9d0356 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -30,6 +30,7 @@ import Performance from '@libs/Performance'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; +import shouldFetchReport from '@libs/shouldFetchReport'; import reportMetadataPropTypes from '@pages/reportMetadataPropTypes'; import reportPropTypes from '@pages/reportPropTypes'; import * as ComposerActions from '@userActions/Composer'; @@ -359,6 +360,10 @@ function ReportScreen({ return; } + if (!shouldFetchReport(report)) { + return; + } + Report.openReport(reportIDFromPath); }, [report.reportID, route, isLoadingInitialReportActions]); diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index ca3ee7d2ab6a..2d9b18b46b59 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -17,6 +17,7 @@ import Performance from '@libs/Performance'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import {isUserCreatedPolicyRoom} from '@libs/ReportUtils'; import {didUserLogInDuringSession} from '@libs/SessionUtils'; +import shouldFetchReport from '@libs/shouldFetchReport'; import {ReactionListContext} from '@pages/home/ReportScreenContext'; import reportPropTypes from '@pages/reportPropTypes'; import * as Report from '@userActions/Report'; @@ -107,15 +108,21 @@ function ReportActionsView(props) { const isReportFullyVisible = useMemo(() => getIsReportFullyVisible(isFocused), [isFocused]); const openReportIfNecessary = () => { - const createChatError = _.get(props.report, ['errorFields', 'createChat']); - // If the report is optimistic (AKA not yet created) we don't need to call openReport again - if (props.report.isOptimisticReport || !_.isEmpty(createChatError)) { + if (!shouldFetchReport(props.report)) { return; } Report.openReport(reportID); }; + const reconnectReportIfNecessary = () => { + if (!shouldFetchReport(props.report)) { + return; + } + + Report.reconnect(reportID); + }; + useEffect(() => { openReportIfNecessary(); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -131,7 +138,7 @@ function ReportActionsView(props) { if (isReportFullyVisible) { openReportIfNecessary(); } else { - Report.reconnect(reportID); + reconnectReportIfNecessary(); } } // update ref with current network state @@ -145,7 +152,7 @@ function ReportActionsView(props) { if (isReportFullyVisible) { openReportIfNecessary(); } else { - Report.reconnect(reportID); + reconnectReportIfNecessary(); } } // eslint-disable-next-line react-hooks/exhaustive-deps From b0d6dca3348a6992e630f3990de8599fbcf7e1e5 Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Thu, 7 Mar 2024 15:08:55 -0500 Subject: [PATCH 212/484] add comment --- src/libs/shouldFetchReport.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libs/shouldFetchReport.ts b/src/libs/shouldFetchReport.ts index f84058de4b99..9e47641e3b29 100644 --- a/src/libs/shouldFetchReport.ts +++ b/src/libs/shouldFetchReport.ts @@ -1,5 +1,7 @@ import {Report} from '@src/types/onyx'; export default function shouldFetchReport(report: Report) { + // If the report is optimistic, there's no need to fetch it. The original action should create it. + // If there is an error for creating the chat, there's no need to fetch it since it doesn't exist return !report?.isOptimisticReport && !report?.errorFields?.createChat; } From 03dba7ffa00dbb7dd66c30fab037e500b9428cf2 Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Thu, 7 Mar 2024 15:26:18 -0500 Subject: [PATCH 213/484] fix type import --- src/libs/shouldFetchReport.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/shouldFetchReport.ts b/src/libs/shouldFetchReport.ts index 9e47641e3b29..5259e88ca6d8 100644 --- a/src/libs/shouldFetchReport.ts +++ b/src/libs/shouldFetchReport.ts @@ -1,4 +1,4 @@ -import {Report} from '@src/types/onyx'; +import type Report from '@src/types/onyx/Report'; export default function shouldFetchReport(report: Report) { // If the report is optimistic, there's no need to fetch it. The original action should create it. From 6c74c15dc0bd413a970598ae4a56c365412d6652 Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Thu, 7 Mar 2024 15:26:52 -0500 Subject: [PATCH 214/484] update dependencies --- 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 2cc15c9d0356..dc77c9d6ac7a 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -365,7 +365,7 @@ function ReportScreen({ } Report.openReport(reportIDFromPath); - }, [report.reportID, route, isLoadingInitialReportActions]); + }, [report, route, isLoadingInitialReportActions]); const dismissBanner = useCallback(() => { setIsBannerVisible(false); From a40eef3c48c5bc0eb2bfa997a136e4a724be154c Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Fri, 8 Mar 2024 03:10:14 +0500 Subject: [PATCH 215/484] 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 216/484] 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 217/484] 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 218/484] 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 219/484] 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: Mon, 11 Mar 2024 18:18:54 +0300 Subject: [PATCH 220/484] took the condition into doesTransactionThreadHaveViolations --- src/libs/ReportUtils.ts | 9 ++++----- src/libs/SidebarUtils.ts | 7 +------ 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index e3708126322f..27ead70e65f7 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4048,7 +4048,8 @@ function doesTransactionThreadHaveViolations(report: OnyxEntry, transact if (report?.stateNum !== CONST.REPORT.STATE_NUM.OPEN && report?.stateNum !== CONST.REPORT.STATE_NUM.SUBMITTED) { return false; } - return TransactionUtils.hasViolation(IOUTransactionID, transactionViolations); + + return TransactionUtils.hasViolation(IOUTransactionID, transactionViolations) && !isSettled(IOUReportID); } /** @@ -4135,10 +4136,8 @@ function shouldReportBeInOptionList({ return true; } - const reportIsSettled = report.statusNum === CONST.REPORT.STATUS_NUM.REIMBURSED; - - // Always show IOU reports with violations unless they are reimbursed - if (isExpenseRequest(report) && doesReportHaveViolations && !reportIsSettled) { + // Always show IOU reports with violations + if (isExpenseRequest(report) && doesReportHaveViolations) { return true; } diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 3ad22a7f9c9d..71b3fd23a03c 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -250,11 +250,6 @@ function getOptionData({ const participantPersonalDetailList = Object.values(OptionsListUtils.getPersonalDetailsForAccountIDs(participantAccountIDs, personalDetails)) as PersonalDetails[]; const personalDetail = participantPersonalDetailList[0] ?? {}; const hasErrors = Object.keys(result.allReportErrors ?? {}).length !== 0; - let shouldShowViolations = false; - if (hasViolations && parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) { - const {IOUReportID} = parentReportAction?.originalMessage ?? {}; - shouldShowViolations = !ReportUtils.isSettled(IOUReportID); - } result.isThread = ReportUtils.isChatThread(report); result.isChatRoom = ReportUtils.isChatRoom(report); @@ -266,7 +261,7 @@ function getOptionData({ result.isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); result.shouldShowSubscript = ReportUtils.shouldReportShowSubscript(report); result.pendingAction = report.pendingFields?.addWorkspaceRoom ?? report.pendingFields?.createChat; - result.brickRoadIndicator = hasErrors || shouldShowViolations ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; + result.brickRoadIndicator = hasErrors || hasViolations ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; result.ownerAccountID = report.ownerAccountID; result.managerID = report.managerID; result.reportID = report.reportID; From 09203e49dbdf1bdf9ffc0155b4bc501255526796 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Mon, 11 Mar 2024 18:44:28 +0100 Subject: [PATCH 221/484] confirm navigation after onyx updates --- src/libs/actions/Policy.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index 8bfa2a4a11fd..6c5df9f94c95 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -2662,7 +2662,11 @@ function navigateWhenEnableFeature(policyID: string, featureRoute: Route) { return; } - Navigation.navigate(featureRoute); + new Promise((resolve) => { + resolve(); + }).then(() => { + Navigation.navigate(featureRoute); + }); } function enablePolicyCategories(policyID: string, enabled: boolean) { From 2040059e6f5efb16660d30e182dd2a7c78b7c057 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Mon, 11 Mar 2024 18:44:48 +0100 Subject: [PATCH 222/484] redirect instead of not found --- ...FeatureEnabledAccessOrRedirectWrapper.tsx} | 30 ++++++++++--------- .../categories/CategorySettingsPage.tsx | 6 ++-- .../categories/CreateCategoryPage.tsx | 6 ++-- .../categories/WorkspaceCategoriesPage.tsx | 6 ++-- .../WorkspaceCategoriesSettingsPage.tsx | 6 ++-- .../workspace/tags/WorkspaceTagsPage.tsx | 6 ++-- 6 files changed, 31 insertions(+), 29 deletions(-) rename src/pages/workspace/{FeatureEnabledAccessOrNotFoundWrapper.tsx => FeatureEnabledAccessOrRedirectWrapper.tsx} (68%) diff --git a/src/pages/workspace/FeatureEnabledAccessOrNotFoundWrapper.tsx b/src/pages/workspace/FeatureEnabledAccessOrRedirectWrapper.tsx similarity index 68% rename from src/pages/workspace/FeatureEnabledAccessOrNotFoundWrapper.tsx rename to src/pages/workspace/FeatureEnabledAccessOrRedirectWrapper.tsx index 8f9ff546b98e..d5799e617226 100644 --- a/src/pages/workspace/FeatureEnabledAccessOrNotFoundWrapper.tsx +++ b/src/pages/workspace/FeatureEnabledAccessOrRedirectWrapper.tsx @@ -5,7 +5,6 @@ import {withOnyx} from 'react-native-onyx'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; -import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import * as Policy from '@userActions/Policy'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -13,7 +12,7 @@ import type * as OnyxTypes from '@src/types/onyx'; import type {PolicyFeatureName} from '@src/types/onyx/Policy'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -type FeatureEnabledAccessOrNotFoundOnyxProps = { +type FeatureEnabledAccessOrRedirectOnyxProps = { /** The report currently being looked at */ policy: OnyxEntry; @@ -21,9 +20,9 @@ type FeatureEnabledAccessOrNotFoundOnyxProps = { isLoadingReportData: OnyxEntry; }; -type FeatureEnabledAccessOrNotFoundComponentProps = FeatureEnabledAccessOrNotFoundOnyxProps & { +type FeatureEnabledAccessOrRedirectComponentProps = FeatureEnabledAccessOrRedirectOnyxProps & { /** The children to render */ - children: ((props: FeatureEnabledAccessOrNotFoundOnyxProps) => React.ReactNode) | React.ReactNode; + children: ((props: FeatureEnabledAccessOrRedirectOnyxProps) => React.ReactNode) | React.ReactNode; /** The report currently being looked at */ policyID: string; @@ -32,8 +31,10 @@ type FeatureEnabledAccessOrNotFoundComponentProps = FeatureEnabledAccessOrNotFou featureName: PolicyFeatureName; }; -function FeatureEnabledAccessOrNotFoundComponent(props: FeatureEnabledAccessOrNotFoundComponentProps) { +function FeatureEnabledAccessOrRedirectComponent(props: FeatureEnabledAccessOrRedirectComponentProps) { const isPolicyIDInRoute = !!props.policyID?.length; + const shouldShowFullScreenLoadingIndicator = props.isLoadingReportData !== false && (!Object.entries(props.policy ?? {}).length || !props.policy?.id); + const shouldRedirect = !PolicyUtils.isPolicyFeatureEnabled(props.policy, props.featureName); useEffect(() => { if (!isPolicyIDInRoute || !isEmptyObject(props.policy)) { @@ -45,26 +46,27 @@ function FeatureEnabledAccessOrNotFoundComponent(props: FeatureEnabledAccessOrNo // eslint-disable-next-line react-hooks/exhaustive-deps }, [isPolicyIDInRoute, props.policyID]); - const shouldShowFullScreenLoadingIndicator = props.isLoadingReportData !== false && (!Object.entries(props.policy ?? {}).length || !props.policy?.id); + useEffect(() => { + if (!shouldRedirect) { + return; + } - const shouldShowNotFoundPage = isEmptyObject(props.policy) || !props.policy?.id || !PolicyUtils.isPolicyFeatureEnabled(props.policy, props.featureName); + Navigation.navigate(ROUTES.WORKSPACE_MORE_FEATURES.getRoute(props.policyID)); + }, [props.policyID, shouldRedirect]); - if (shouldShowFullScreenLoadingIndicator) { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + if (shouldShowFullScreenLoadingIndicator || shouldRedirect) { return ; } - if (shouldShowNotFoundPage) { - return Navigation.goBack(ROUTES.WORKSPACE_PROFILE.getRoute(props.policyID))} />; - } - return typeof props.children === 'function' ? props.children(props) : props.children; } -export default withOnyx({ +export default withOnyx({ policy: { key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID ?? ''}`, }, isLoadingReportData: { key: ONYXKEYS.IS_LOADING_REPORT_DATA, }, -})(FeatureEnabledAccessOrNotFoundComponent); +})(FeatureEnabledAccessOrRedirectComponent); diff --git a/src/pages/workspace/categories/CategorySettingsPage.tsx b/src/pages/workspace/categories/CategorySettingsPage.tsx index 97e71abb7a2e..cfb55ead8d6b 100644 --- a/src/pages/workspace/categories/CategorySettingsPage.tsx +++ b/src/pages/workspace/categories/CategorySettingsPage.tsx @@ -16,7 +16,7 @@ import * as ErrorUtils from '@libs/ErrorUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; +import FeatureEnabledAccessOrRedirectWrapper from '@pages/workspace/FeatureEnabledAccessOrRedirectWrapper'; import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; @@ -48,7 +48,7 @@ function CategorySettingsPage({route, policyCategories}: CategorySettingsPagePro return ( - @@ -82,7 +82,7 @@ function CategorySettingsPage({route, policyCategories}: CategorySettingsPagePro /> - + ); diff --git a/src/pages/workspace/categories/CreateCategoryPage.tsx b/src/pages/workspace/categories/CreateCategoryPage.tsx index 332371c866b6..f36cbac03ae3 100644 --- a/src/pages/workspace/categories/CreateCategoryPage.tsx +++ b/src/pages/workspace/categories/CreateCategoryPage.tsx @@ -16,7 +16,7 @@ import Navigation from '@libs/Navigation/Navigation'; import * as ValidationUtils from '@libs/ValidationUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; +import FeatureEnabledAccessOrRedirectWrapper from '@pages/workspace/FeatureEnabledAccessOrRedirectWrapper'; import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; @@ -69,7 +69,7 @@ function CreateCategoryPage({route, policyCategories}: CreateCategoryPageProps) return ( - @@ -101,7 +101,7 @@ function CreateCategoryPage({route, policyCategories}: CreateCategoryPageProps) /> - + ); diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index ce1d891d631e..397216686eea 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -23,7 +23,7 @@ import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import type {CentralPaneNavigatorParamList} from '@navigation/types'; import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; +import FeatureEnabledAccessOrRedirectWrapper from '@pages/workspace/FeatureEnabledAccessOrRedirectWrapper'; import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; @@ -154,7 +154,7 @@ function WorkspaceCategoriesPage({policy, policyCategories, route}: WorkspaceCat return ( - @@ -203,7 +203,7 @@ function WorkspaceCategoriesPage({policy, policyCategories, route}: WorkspaceCat /> )} - + ); diff --git a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx index 02ae87ce05d0..b0882573d51c 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx @@ -11,7 +11,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {setWorkspaceRequiresCategory} from '@libs/actions/Policy'; import type {SettingsNavigatorParamList} from '@navigation/types'; import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; +import FeatureEnabledAccessOrRedirectWrapper from '@pages/workspace/FeatureEnabledAccessOrRedirectWrapper'; import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import CONST from '@src/CONST'; import type SCREENS from '@src/SCREENS'; @@ -29,7 +29,7 @@ function WorkspaceCategoriesSettingsPage({route}: WorkspaceCategoriesSettingsPag return ( - @@ -60,7 +60,7 @@ function WorkspaceCategoriesSettingsPage({route}: WorkspaceCategoriesSettingsPag )} - + ); diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index efb7ff1296c8..b68b0991f167 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -19,7 +19,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import * as PolicyUtils from '@libs/PolicyUtils'; import type {CentralPaneNavigatorParamList} from '@navigation/types'; import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; -import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper'; +import FeatureEnabledAccessOrRedirectWrapper from '@pages/workspace/FeatureEnabledAccessOrRedirectWrapper'; import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -98,7 +98,7 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) { return ( - @@ -136,7 +136,7 @@ function WorkspaceTagsPage({policyTags, route}: WorkspaceTagsPageProps) { /> )} - + ); From 57f164f1bd99b22bc1b1344ba44267c9db47e32f Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Tue, 12 Mar 2024 16:27:34 +0700 Subject: [PATCH 223/484] 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 8ae4e2f934582c3b1e8226af39c7ae3bbee2680b Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Tue, 12 Mar 2024 17:11:14 +0700 Subject: [PATCH 224/484] keep the old logic --- src/pages/home/report/comment/TextCommentFragment.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/home/report/comment/TextCommentFragment.tsx b/src/pages/home/report/comment/TextCommentFragment.tsx index 981d6771c8db..7ff413f554b8 100644 --- a/src/pages/home/report/comment/TextCommentFragment.tsx +++ b/src/pages/home/report/comment/TextCommentFragment.tsx @@ -57,7 +57,11 @@ function TextCommentFragment({fragment, styleAsDeleted, styleAsMuted = false, so const htmlWithDeletedTag = styleAsDeleted ? `${html}` : html; const htmlContent = containsOnlyEmojis ? Str.replaceAll(htmlWithDeletedTag, '', '') : htmlWithDeletedTag; - const htmlWithTag = editedTag ? `${htmlContent}${editedTag}` : htmlContent; + let htmlWithTag = editedTag ? `${htmlContent}${editedTag}` : htmlContent; + + if (styleAsMuted) { + htmlWithTag = `${htmlWithTag}`; + } return ( Date: Tue, 12 Mar 2024 12:24:29 +0100 Subject: [PATCH 225/484] integrate FeatureEnabledAccessOrRedirectWrapper to PolicyDistanceRatesPage --- .../distanceRates/PolicyDistanceRatesPage.tsx | 92 ++++++++++--------- 1 file changed, 49 insertions(+), 43 deletions(-) diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx index fd6466da1758..0134771b9ec5 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx @@ -22,6 +22,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import type {CentralPaneNavigatorParamList} from '@navigation/types'; import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import FeatureEnabledAccessOrRedirectWrapper from '@pages/workspace/FeatureEnabledAccessOrRedirectWrapper'; import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import * as Policy from '@userActions/Policy'; import ButtonWithDropdownMenu from '@src/components/ButtonWithDropdownMenu'; @@ -232,52 +233,57 @@ function PolicyDistanceRatesPage({policy, route}: PolicyDistanceRatesPageProps) return ( - - - {!isSmallScreenWidth && headerButtons} - - {isSmallScreenWidth && {headerButtons}} - - {translate('workspace.distanceRates.centrallyManage')} - - {isLoading && ( - - )} - {Object.values(customUnitRates).length > 0 && ( - + {!isSmallScreenWidth && headerButtons} +
+ {isSmallScreenWidth && {headerButtons}} + + {translate('workspace.distanceRates.centrallyManage')} + + {isLoading && ( + + )} + {Object.values(customUnitRates).length > 0 && ( + + )} + setIsWarningModalVisible(false)} + isVisible={isWarningModalVisible} + title={translate('workspace.distanceRates.oopsNotSoFast')} + prompt={translate('workspace.distanceRates.workspaceNeeds')} + confirmText={translate('common.buttonConfirm')} + shouldShowCancelButton={false} /> - )} - setIsWarningModalVisible(false)} - isVisible={isWarningModalVisible} - title={translate('workspace.distanceRates.oopsNotSoFast')} - prompt={translate('workspace.distanceRates.workspaceNeeds')} - confirmText={translate('common.buttonConfirm')} - shouldShowCancelButton={false} - /> - + + ); From 1c1d7fd71312612ca94d2cbc84934701680fc92c Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 12 Mar 2024 12:24:37 +0100 Subject: [PATCH 226/484] integrate FeatureEnabledAccessOrRedirectWrapper to tags pages --- .../workspace/tags/WorkspaceEditTagsPage.tsx | 56 +++++++------ .../tags/WorkspaceTagsSettingsPage.tsx | 81 ++++++++++--------- 2 files changed, 75 insertions(+), 62 deletions(-) diff --git a/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx b/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx index 98ae6f726d73..b011051999ae 100644 --- a/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceEditTagsPage.tsx @@ -16,6 +16,7 @@ import * as Policy from '@libs/actions/Policy'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; +import FeatureEnabledAccessOrRedirectWrapper from '@pages/workspace/FeatureEnabledAccessOrRedirectWrapper'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; @@ -52,33 +53,38 @@ function WorkspaceEditTagsPage({route, policyTags}: WorkspaceEditTagsPageProps) ); return ( - - - - - - - - + + + + + + + + ); } diff --git a/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx index deac804980ea..7d675eb7fec3 100644 --- a/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsSettingsPage.tsx @@ -16,7 +16,9 @@ import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import FeatureEnabledAccessOrRedirectWrapper from '@pages/workspace/FeatureEnabledAccessOrRedirectWrapper'; import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; @@ -43,44 +45,49 @@ function WorkspaceTagsSettingsPage({route, policyTags}: WorkspaceTagsSettingsPag return ( - {({policy}) => ( - - - - - - - {translate('workspace.tags.requiresTag')} - + + {({policy}) => ( + + + + + + + {translate('workspace.tags.requiresTag')} + + - - - - Navigation.navigate(ROUTES.WORKSPACE_EDIT_TAGS.getRoute(route.params.policyID))} - /> - - - - )} + + + Navigation.navigate(ROUTES.WORKSPACE_EDIT_TAGS.getRoute(route.params.policyID))} + /> + +
+ + )} + ); From 45d502c521b3ab7303d6ec3cd0cb7fc0f4e943d6 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 12 Mar 2024 12:24:44 +0100 Subject: [PATCH 227/484] integrate FeatureEnabledAccessOrRedirectWrapper to workflows pages --- .../WorkspaceAutoReportingFrequencyPage.tsx | 53 ++++++++------- ...orkspaceAutoReportingMonthlyOffsetPage.tsx | 63 ++++++++++-------- .../WorkspaceWorkflowsApproverPage.tsx | 65 +++++++++++-------- .../workflows/WorkspaceWorkflowsPage.tsx | 54 ++++++++------- 4 files changed, 135 insertions(+), 100 deletions(-) diff --git a/src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx b/src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx index cf66af726a72..5a7e7a2fc3a9 100644 --- a/src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx @@ -1,3 +1,4 @@ +import type {StackScreenProps} from '@react-navigation/stack'; import React, {useState} from 'react'; import {FlatList} from 'react-native-gesture-handler'; import type {ValueOf} from 'type-fest'; @@ -11,18 +12,21 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Localize from '@libs/Localize'; import Navigation from '@libs/Navigation/Navigation'; +import type {CentralPaneNavigatorParamList} from '@libs/Navigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; +import FeatureEnabledAccessOrRedirectWrapper from '@pages/workspace/FeatureEnabledAccessOrRedirectWrapper'; import withPolicy from '@pages/workspace/withPolicy'; import type {WithPolicyOnyxProps} from '@pages/workspace/withPolicy'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; type AutoReportingFrequencyKey = Exclude, 'instant'>; type Locale = ValueOf; -type WorkspaceAutoReportingFrequencyPageProps = WithPolicyOnyxProps; +type WorkspaceAutoReportingFrequencyPageProps = WithPolicyOnyxProps & StackScreenProps; type WorkspaceAutoReportingFrequencyPageItem = { text: string; @@ -41,7 +45,7 @@ const getAutoReportingFrequencyDisplayNames = (locale: Locale): AutoReportingFre [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MANUAL]: Localize.translate(locale, 'workflowsPage.frequencies.manually'), }); -function WorkspaceAutoReportingFrequencyPage({policy}: WorkspaceAutoReportingFrequencyPageProps) { +function WorkspaceAutoReportingFrequencyPage({policy, route}: WorkspaceAutoReportingFrequencyPageProps) { const {translate, preferredLocale, toLocaleOrdinal} = useLocalize(); const styles = useThemeStyles(); const [isMonthlyFrequency, setIsMonthlyFrequency] = useState(policy?.autoReportingFrequency === CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MONTHLY); @@ -105,28 +109,33 @@ function WorkspaceAutoReportingFrequencyPage({policy}: WorkspaceAutoReportingFre ); return ( - - - - - item.text} - /> - - + + + + item.text} + /> + + + ); } diff --git a/src/pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage.tsx b/src/pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage.tsx index 84d70e799c42..3aad5fb9f0a4 100644 --- a/src/pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage.tsx @@ -1,3 +1,4 @@ +import type {StackScreenProps} from '@react-navigation/stack'; import React, {useState} from 'react'; import type {ValueOf} from 'type-fest'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; @@ -7,16 +8,19 @@ import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; +import type {CentralPaneNavigatorParamList} from '@libs/Navigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; +import FeatureEnabledAccessOrRedirectWrapper from '@pages/workspace/FeatureEnabledAccessOrRedirectWrapper'; import withPolicy from '@pages/workspace/withPolicy'; import type {WithPolicyOnyxProps} from '@pages/workspace/withPolicy'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; +import type SCREENS from '@src/SCREENS'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; const DAYS_OF_MONTH = 28; -type WorkspaceAutoReportingMonthlyOffsetProps = WithPolicyOnyxProps; +type WorkspaceAutoReportingMonthlyOffsetProps = WithPolicyOnyxProps & StackScreenProps; type AutoReportingOffsetKeys = ValueOf; @@ -27,7 +31,7 @@ type WorkspaceAutoReportingMonthlyOffsetPageItem = { isNumber?: boolean; }; -function WorkspaceAutoReportingMonthlyOffsetPage({policy}: WorkspaceAutoReportingMonthlyOffsetProps) { +function WorkspaceAutoReportingMonthlyOffsetPage({policy, route}: WorkspaceAutoReportingMonthlyOffsetProps) { const {translate, toLocaleOrdinal} = useLocalize(); const offset = policy?.autoReportingOffset ?? 0; const [searchText, setSearchText] = useState(''); @@ -67,34 +71,39 @@ function WorkspaceAutoReportingMonthlyOffsetPage({policy}: WorkspaceAutoReportin }; return ( - - - + + - - - + + + + ); } diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsApproverPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsApproverPage.tsx index 52406a8033d2..34c0f8989888 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsApproverPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsApproverPage.tsx @@ -1,3 +1,4 @@ +import type {StackScreenProps} from '@react-navigation/stack'; import React, {useCallback, useMemo, useState} from 'react'; import type {SectionListData} from 'react-native'; import {withOnyx} from 'react-native-onyx'; @@ -17,15 +18,18 @@ import compose from '@libs/compose'; import {formatPhoneNumber} from '@libs/LocalePhoneNumber'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; +import type {CentralPaneNavigatorParamList} from '@libs/Navigation/types'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as UserUtils from '@libs/UserUtils'; +import FeatureEnabledAccessOrRedirectWrapper from '@pages/workspace/FeatureEnabledAccessOrRedirectWrapper'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; import type {PersonalDetailsList, PolicyMember} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -34,11 +38,13 @@ type WorkspaceWorkflowsApproverPageOnyxProps = { personalDetails: OnyxEntry; }; -type WorkspaceWorkflowsApproverPageProps = WorkspaceWorkflowsApproverPageOnyxProps & WithPolicyAndFullscreenLoadingProps; +type WorkspaceWorkflowsApproverPageProps = WorkspaceWorkflowsApproverPageOnyxProps & + WithPolicyAndFullscreenLoadingProps & + StackScreenProps; type MemberOption = Omit & {accountID: number}; type MembersSection = SectionListData>; -function WorkspaceWorkflowsApproverPage({policy, policyMembers, personalDetails, isLoadingReportData = true}: WorkspaceWorkflowsApproverPageProps) { +function WorkspaceWorkflowsApproverPage({policy, policyMembers, personalDetails, isLoadingReportData = true, route}: WorkspaceWorkflowsApproverPageProps) { const {translate} = useLocalize(); const policyName = policy?.name ?? ''; const [searchTerm, setSearchTerm] = useState(''); @@ -161,33 +167,38 @@ function WorkspaceWorkflowsApproverPage({policy, policyMembers, personalDetails, }; return ( - - - - - - + + + + + + ); } diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx index ee3934cacc06..706b947b2704 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx @@ -17,6 +17,7 @@ import Permissions from '@libs/Permissions'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import type {CentralPaneNavigatorParamList} from '@navigation/types'; +import FeatureEnabledAccessOrRedirectWrapper from '@pages/workspace/FeatureEnabledAccessOrRedirectWrapper'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import withPolicy from '@pages/workspace/withPolicy'; import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections'; @@ -167,31 +168,36 @@ function WorkspaceWorkflowsPage({policy, betas, route}: WorkspaceWorkflowsPagePr const isPolicyAdmin = PolicyUtils.isPolicyAdmin(policy); return ( - - -
- - {translate('workflowsPage.workflowDescription')} - item.title} - /> - -
-
-
+ + +
+ + {translate('workflowsPage.workflowDescription')} + item.title} + /> + +
+
+
+ ); } From 6aa7212618fe0844871c94f080e4e621c8ad862c Mon Sep 17 00:00:00 2001 From: Kevin Brian Bader Date: Tue, 12 Mar 2024 14:50:42 +0200 Subject: [PATCH 228/484] Desktop - Login - Unable to enter the 2FA code or exit the screen --- src/libs/Navigation/Navigation.ts | 9 +++++++++ src/libs/desktopLoginRedirect/index.desktop.ts | 16 ++++++++++++++++ src/libs/desktopLoginRedirect/index.ts | 5 +++++ src/pages/ValidateLoginPage/index.website.tsx | 6 ++++++ 4 files changed, 36 insertions(+) create mode 100644 src/libs/desktopLoginRedirect/index.desktop.ts create mode 100644 src/libs/desktopLoginRedirect/index.ts diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index 4cd6a141bd3b..e05084e18690 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -347,6 +347,14 @@ function navigateWithSwitchPolicyID(params: SwitchPolicyIDParams) { return switchPolicyID(navigationRef.current, params); } +/** + * The `popToTop` action takes you back to the first screen in the stack, dismissing all the others. + * @note we used to call `Navigation.navigate()` before the new navigation was introduced. + */ +function popToTop() { + navigationRef.current?.dispatch(StackActions.popToTop()); +} + export default { setShouldPopAllStateOnUP, navigate, @@ -366,6 +374,7 @@ export default { parseHybridAppUrl, closeFullScreen, navigateWithSwitchPolicyID, + popToTop, }; export {navigationRef}; diff --git a/src/libs/desktopLoginRedirect/index.desktop.ts b/src/libs/desktopLoginRedirect/index.desktop.ts new file mode 100644 index 000000000000..e751fa1ffd78 --- /dev/null +++ b/src/libs/desktopLoginRedirect/index.desktop.ts @@ -0,0 +1,16 @@ +import Navigation from '@libs/Navigation/Navigation'; +import CONST from '@src/CONST'; +import type {AutoAuthState} from '@src/types/onyx/Session'; + +function desktopLoginRedirect(autoAuthState: AutoAuthState, isSignedIn: boolean) { + // NOT_STARTED - covers edge case of autoAuthState not being initialized yet (after logout) + // JUST_SIGNED_IN - confirms passing the magic code step -> we're either logged-in or shown 2FA screen + // !isSignedIn - confirms we're not signed-in yet as there's possible one last step (2FA validation) + const shouldPopToTop = (autoAuthState === CONST.AUTO_AUTH_STATE.NOT_STARTED || autoAuthState === CONST.AUTO_AUTH_STATE.JUST_SIGNED_IN) && !isSignedIn; + + if (shouldPopToTop) { + Navigation.isNavigationReady().then(() => Navigation.popToTop()); + } +} + +export default desktopLoginRedirect; diff --git a/src/libs/desktopLoginRedirect/index.ts b/src/libs/desktopLoginRedirect/index.ts new file mode 100644 index 000000000000..14f5750c3de9 --- /dev/null +++ b/src/libs/desktopLoginRedirect/index.ts @@ -0,0 +1,5 @@ +import type {AutoAuthState} from '@src/types/onyx/Session'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +function desktopLoginRedirect(autoAuthState: AutoAuthState, isSignedIn: boolean) {} +export default desktopLoginRedirect; diff --git a/src/pages/ValidateLoginPage/index.website.tsx b/src/pages/ValidateLoginPage/index.website.tsx index 2acad7815754..b8e8709215e8 100644 --- a/src/pages/ValidateLoginPage/index.website.tsx +++ b/src/pages/ValidateLoginPage/index.website.tsx @@ -4,6 +4,7 @@ import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import ExpiredValidateCodeModal from '@components/ValidateCode/ExpiredValidateCodeModal'; import JustSignedInModal from '@components/ValidateCode/JustSignedInModal'; import ValidateCodeModal from '@components/ValidateCode/ValidateCodeModal'; +import desktopLoginRedirect from '@libs/desktopLoginRedirect'; import Navigation from '@libs/Navigation/Navigation'; import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; @@ -43,6 +44,11 @@ function ValidateLoginPage({ // The user has initiated the sign in process on the same browser, in another tab. Session.signInWithValidateCode(Number(accountID), validateCode); + + // Since on Desktop we don't have multi-tab functionality to handle the login flow, + // we need to `popToTop` the stack after `signInWithValidateCode` in order to + // perform login for both 2FA and non-2FA accounts. + desktopLoginRedirect(autoAuthState, isSignedIn); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); From a83cf6ecf91eeab3df13c1c7db4837172bb47714 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 12 Mar 2024 15:26:05 +0100 Subject: [PATCH 229/484] 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 230/484] 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 8de01d0f2c7a81dfa8a542c071b93aa6e04714f5 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Tue, 12 Mar 2024 16:10:52 +0100 Subject: [PATCH 231/484] address comments --- src/types/onyx/ReportAction.ts | 6 +- tests/unit/MigrationTest.ts | 192 ++++++++++++++++++--------------- 2 files changed, 105 insertions(+), 93 deletions(-) diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index 4512f04964b8..f6c34fe742a4 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -2,8 +2,6 @@ import type {ValueOf} from 'type-fest'; import type {FileObject} from '@components/AttachmentModal'; import type {AvatarSource} from '@libs/UserUtils'; import type CONST from '@src/CONST'; -import type ONYXKEYS from '@src/ONYXKEYS'; -import type CollectionDataSet from '@src/types/utils/CollectionDataSet'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import type * as OnyxCommon from './OnyxCommon'; import type {Decision, Reaction} from './OriginalMessage'; @@ -229,7 +227,5 @@ type ReportAction = ReportActionBase & OriginalMessage; type ReportActions = Record; -type ReportActionCollectionDataSet = CollectionDataSet; - export default ReportAction; -export type {ReportActions, ReportActionBase, Message, LinkMetadata, OriginalMessage, ReportActionCollectionDataSet}; +export type {ReportActions, ReportActionBase, Message, LinkMetadata, OriginalMessage}; diff --git a/tests/unit/MigrationTest.ts b/tests/unit/MigrationTest.ts index d60761cd1d89..147588559e13 100644 --- a/tests/unit/MigrationTest.ts +++ b/tests/unit/MigrationTest.ts @@ -5,10 +5,11 @@ import Log from '@src/libs/Log'; import CheckForPreviousReportActionID from '@src/libs/migrations/CheckForPreviousReportActionID'; import KeyReportActionsDraftByReportActionID from '@src/libs/migrations/KeyReportActionsDraftByReportActionID'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {ReportActionCollectionDataSet} from '@src/types/onyx/ReportAction'; import type {ReportActionsDraftCollectionDataSet} from '@src/types/onyx/ReportActionsDrafts'; +import { toCollectionDataSet } from '@src/types/utils/CollectionDataSet'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; + jest.mock('@src/libs/getPlatform'); let LogSpy: jest.SpyInstance>; @@ -34,22 +35,23 @@ describe('Migrations', () => { )); it('Should remove all report actions given that a previousReportActionID does not exist', () => { - const setQueries: ReportActionCollectionDataSet = {}; - - setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`] = { - 1: { - reportActionID: '1', - created: '', - actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, - }, - 2: { - reportActionID: '2', - created: '', - actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, - }, - }; + const reportActionsCollectionDataSet = toCollectionDataSet( + ONYXKEYS.COLLECTION.REPORT_ACTIONS, + [ + { + 1: { + reportActionID: '1', + created: '', + actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, + reportID: '1', + }, + 2: {reportActionID: '2', created: '', actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, reportID: '1'}, + }, + ], + (item) => item[1].reportID ?? '', + ); - return Onyx.multiSet(setQueries) + return Onyx.multiSet(reportActionsCollectionDataSet) .then(CheckForPreviousReportActionID) .then(() => { expect(LogSpy).toHaveBeenCalledWith( @@ -68,24 +70,30 @@ describe('Migrations', () => { }); it('Should not remove any report action given that previousReportActionID exists in first valid report action', () => { - const setQueries: ReportActionCollectionDataSet = {}; - - setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`] = { - 1: { - reportActionID: '1', - previousReportActionID: '0', - created: '', - actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, - }, - 2: { - reportActionID: '2', - previousReportActionID: '1', - created: '', - actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, - }, - }; + const reportActionsCollectionDataSet = toCollectionDataSet( + ONYXKEYS.COLLECTION.REPORT_ACTIONS, + [ + { + 1: { + reportActionID: '1', + previousReportActionID: '0', + created: '', + actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, + reportID: '1', + }, + 2: { + reportActionID: '2', + previousReportActionID: '1', + created: '', + actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, + reportID: '1', + }, + }, + ], + (item) => item[1].reportID ?? '', + ); - return Onyx.multiSet(setQueries) + return Onyx.multiSet(reportActionsCollectionDataSet) .then(CheckForPreviousReportActionID) .then(() => { expect(LogSpy).toHaveBeenCalledWith('[Migrate Onyx] CheckForPreviousReportActionID Migration: previousReportActionID found. Migration complete'); @@ -111,29 +119,33 @@ describe('Migrations', () => { }); it('Should skip zombie report actions and proceed to remove all reportActions given that a previousReportActionID does not exist', () => { - const setQueries: ReportActionCollectionDataSet = {}; - - setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`] = {}; - - // @ts-expect-error preset null value - setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`] = null; - // @ts-expect-error preset null value - setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`] = null; - - setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`] = { - 1: { - reportActionID: '1', - created: '', - actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, - }, - 2: { - reportActionID: '2', - created: '', - actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, - }, - }; - - return Onyx.multiSet(setQueries) + const reportActionsCollectionDataSet = toCollectionDataSet( + ONYXKEYS.COLLECTION.REPORT_ACTIONS, + [ + { + 1: { + reportActionID: '1', + created: '', + actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, + reportID: '4', + }, + 2: { + reportActionID: '2', + created: '', + actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, + reportID: '4', + }, + }, + ], + (item) => item[1].reportID ?? '', + ); + + return Onyx.multiSet({ + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]: {}, + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`]: null, + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`]: null, + ...reportActionsCollectionDataSet, + }) .then(CheckForPreviousReportActionID) .then(() => { expect(LogSpy).toHaveBeenCalledWith( @@ -155,30 +167,35 @@ describe('Migrations', () => { }); it('Should skip zombie report actions and should not remove any report action given that previousReportActionID exists in first valid report action', () => { - const setQueries: ReportActionCollectionDataSet = {}; - - setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`] = {}; - // @ts-expect-error preset null value - setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`] = null; - // @ts-expect-error preset null value - setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`] = null; - - setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`] = { - 1: { - reportActionID: '1', - previousReportActionID: '10', - created: '', - actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, - }, - 2: { - reportActionID: '2', - previousReportActionID: '23', - created: '', - actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, - }, - }; - - return Onyx.multiSet(setQueries) + const reportActionsCollectionDataSet = toCollectionDataSet( + ONYXKEYS.COLLECTION.REPORT_ACTIONS, + [ + { + 1: { + reportActionID: '1', + previousReportActionID: '10', + created: '', + actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, + reportID: '4', + }, + 2: { + reportActionID: '2', + previousReportActionID: '23', + created: '', + actionName: CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, + reportID: '4', + }, + }, + ], + (item) => item[1].reportID ?? '', + ); + + return Onyx.multiSet({ + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]: {}, + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`]: null, + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`]: null, + ...reportActionsCollectionDataSet, + }) .then(CheckForPreviousReportActionID) .then(() => { expect(LogSpy).toHaveBeenCalledWith('[Migrate Onyx] CheckForPreviousReportActionID Migration: previousReportActionID found. Migration complete'); @@ -208,15 +225,14 @@ describe('Migrations', () => { }); it('Should skip if no valid reportActions', () => { - const setQueries: ReportActionCollectionDataSet = {}; - - // @ts-expect-error preset null value - setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`] = null; - setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`] = {}; - setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`] = {}; - // @ts-expect-error preset null value - setQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`] = null; + const setQueries = { + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]: null, + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`]: {}, + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`]: {}, + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}4`]: null, + }; + // @ts-expect-error preset null values return Onyx.multiSet(setQueries) .then(CheckForPreviousReportActionID) .then(() => { From 05b53a25dcf96a5b1ef1cfa0967495c662814ec2 Mon Sep 17 00:00:00 2001 From: Marcin Swornowski Date: Tue, 12 Mar 2024 16:39:51 +0100 Subject: [PATCH 232/484] 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 233/484] 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 234/484] 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 235/484] 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 236/484] 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 237/484] 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 9ad44bbb04f5e25744c7f3f4aba10ff472e30f9f Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Wed, 13 Mar 2024 01:38:45 +0000 Subject: [PATCH 238/484] refactor(typescript): apply pull request suggestion --- .../iou/request/step/withWritableReportOrNotFound.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx index ed4958e77f08..ecffaa431180 100644 --- a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx +++ b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx @@ -1,9 +1,11 @@ +import type {RouteProp} from '@react-navigation/core'; import type {ComponentType, ForwardedRef} from 'react'; import React, {forwardRef} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import getComponentDisplayName from '@libs/getComponentDisplayName'; +import type {MoneyRequestNavigatorParamList} from '@libs/Navigation/types'; import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -14,9 +16,7 @@ type WithWritableReportOrNotFoundOnyxProps = { report: OnyxEntry; }; -type WithWritableReportOrNotFoundProps = WithWritableReportOrNotFoundOnyxProps & { - route: {params: {iouType: string; reportID: string} | undefined}; -}; +type WithWritableReportOrNotFoundProps = WithWritableReportOrNotFoundOnyxProps & {route: RouteProp}; export default function (WrappedComponent: ComponentType) { // eslint-disable-next-line rulesdir/no-negated-variables @@ -41,7 +41,7 @@ export default function return withOnyx({ report: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params?.reportID ?? '0'}`, + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${route.params && 'reportID' in route.params ? route.params.reportID : '0'}`, }, })(forwardRef(WithWritableReportOrNotFound)); } From ce55a7e93d6c1a90ec07e75b1796d402488506b6 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 13 Mar 2024 09:01:31 +0100 Subject: [PATCH 239/484] 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 240/484] 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 241/484] 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 d9e986da966ca23367f56e45023c684d92605eee Mon Sep 17 00:00:00 2001 From: Michal Muzyk Date: Wed, 13 Mar 2024 09:44:13 +0100 Subject: [PATCH 242/484] feat: policy distance rate edit --- src/ONYXKEYS.ts | 3 + src/ROUTES.ts | 4 + src/SCREENS.ts | 1 + src/languages/en.ts | 2 + src/languages/es.ts | 2 + .../DeletePolicyDistanceRatesParams.ts | 7 + .../SetPolicyDistanceRatesEnabledParams.ts | 7 + .../UpdatePolicyDistanceRateValueParams.ts | 7 + src/libs/API/parameters/index.ts | 3 + src/libs/API/types.ts | 6 + .../AppNavigator/ModalStackNavigators.tsx | 1 + .../CENTRAL_PANE_TO_RHP_MAPPING.ts | 2 +- src/libs/Navigation/linkingConfig/config.ts | 3 + src/libs/Navigation/types.ts | 4 + src/libs/actions/Policy.ts | 240 ++++++++++++++++++ .../PolicyDistanceRateEditPage.tsx | 144 +++++++++++ .../distanceRates/PolicyDistanceRatesPage.tsx | 4 +- .../workspace/distanceRates/RateModal.tsx | 105 ++++++++ src/types/form/PolicyDistanceRateEditForm.ts | 18 ++ src/types/form/index.ts | 1 + 20 files changed, 561 insertions(+), 3 deletions(-) create mode 100644 src/libs/API/parameters/DeletePolicyDistanceRatesParams.ts create mode 100644 src/libs/API/parameters/SetPolicyDistanceRatesEnabledParams.ts create mode 100644 src/libs/API/parameters/UpdatePolicyDistanceRateValueParams.ts create mode 100644 src/pages/workspace/distanceRates/PolicyDistanceRateEditPage.tsx create mode 100644 src/pages/workspace/distanceRates/RateModal.tsx create mode 100644 src/types/form/PolicyDistanceRateEditForm.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index f0bc99e85e66..656f34c96c09 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -339,6 +339,8 @@ const ONYXKEYS = { WORKSPACE_RATE_AND_UNIT_FORM_DRAFT: 'workspaceRateAndUnitFormDraft', POLICY_CREATE_DISTANCE_RATE_FORM: 'policyCreateDistanceRateForm', POLICY_CREATE_DISTANCE_RATE_FORM_DRAFT: 'policyCreateDistanceRateFormDraft', + POLICY_DISTANCE_RATE_EDIT_FORM: 'policyDistanceRateEditForm', + POLICY_DISTANCE_RATE_EDIT_FORM_DRAFT: 'policyDistanceRateEditFormDraft', CLOSE_ACCOUNT_FORM: 'closeAccount', CLOSE_ACCOUNT_FORM_DRAFT: 'closeAccountDraft', PROFILE_SETTINGS_FORM: 'profileSettingsForm', @@ -456,6 +458,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.WORKSPACE_DESCRIPTION_FORM]: FormTypes.WorkspaceDescriptionForm; [ONYXKEYS.FORMS.POLICY_TAG_NAME_FORM]: FormTypes.PolicyTagNameForm; [ONYXKEYS.FORMS.POLICY_CREATE_DISTANCE_RATE_FORM]: FormTypes.PolicyCreateDistanceRateForm; + [ONYXKEYS.FORMS.POLICY_DISTANCE_RATE_EDIT_FORM]: FormTypes.PolicyDistanceRateEditForm; }; type OnyxFormDraftValuesMapping = { diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 151373fe2ad4..0e7bd1c30f1a 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -597,6 +597,10 @@ const ROUTES = { route: 'workspace/:policyID/distance-rates/settings', getRoute: (policyID: string) => `workspace/${policyID}/distance-rates/settings` as const, }, + WORKSPACE_DISTANCE_RATE_EDIT: { + route: 'workspace/:policyID/distance-rates/:rateID/edit', + getRoute: (policyID: string, rateID: string) => `workspace/${policyID}/distance-rates/${rateID}/edit` as const, + }, // Referral program promotion REFERRAL_DETAILS_MODAL: { diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 5105c12c10da..cc54d361e661 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -235,6 +235,7 @@ const SCREENS = { DISTANCE_RATES: 'Distance_Rates', CREATE_DISTANCE_RATE: 'Create_Distance_Rate', DISTANCE_RATES_SETTINGS: 'Distance_Rates_Settings', + DISTANCE_RATE_EDIT: 'Distance_Rate_Edit', }, EDIT_REQUEST: { diff --git a/src/languages/en.ts b/src/languages/en.ts index 911da2bd04c2..b6e6f4d8bb7e 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1977,6 +1977,8 @@ export default { disabled: 'Disabled', unit: 'Unit', defaultCategory: 'Default category', + deleteDistanceRate: 'Delete distance rate', + areYouSureDelete: 'Are you sure you want to delete this rate?', errors: { createRateGenericFailureMessage: 'An error occurred while creating the distance rate, please try again.', updateRateUnitGenericFailureMessage: 'An error occurred while updating the distance rate unit, please try again.', diff --git a/src/languages/es.ts b/src/languages/es.ts index 39cae1325f02..b1904d816146 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2002,6 +2002,8 @@ export default { disabled: 'Desactivada', unit: 'Unit', defaultCategory: 'Default category', + deleteDistanceRate: 'Delete distance rate', + areYouSureDelete: 'Are you sure you want to delete this rate?', errors: { createRateGenericFailureMessage: 'An error occurred while creating the distance rate, please try again.', updateRateUnitGenericFailureMessage: 'An error occurred while updating the distance rate unit, please try again.', diff --git a/src/libs/API/parameters/DeletePolicyDistanceRatesParams.ts b/src/libs/API/parameters/DeletePolicyDistanceRatesParams.ts new file mode 100644 index 000000000000..d9c31930897e --- /dev/null +++ b/src/libs/API/parameters/DeletePolicyDistanceRatesParams.ts @@ -0,0 +1,7 @@ +type DeletePolicyDistanceRatesParams = { + policyID: string; + customUnitID: string; + customUnitRateIDs: string[]; +}; + +export default DeletePolicyDistanceRatesParams; diff --git a/src/libs/API/parameters/SetPolicyDistanceRatesEnabledParams.ts b/src/libs/API/parameters/SetPolicyDistanceRatesEnabledParams.ts new file mode 100644 index 000000000000..a444bb7a1a8e --- /dev/null +++ b/src/libs/API/parameters/SetPolicyDistanceRatesEnabledParams.ts @@ -0,0 +1,7 @@ +type SetPolicyDistanceRatesEnabledParams = { + policyID: string; + customUnitID: string; + customUnitRates: string; +}; + +export default SetPolicyDistanceRatesEnabledParams; diff --git a/src/libs/API/parameters/UpdatePolicyDistanceRateValueParams.ts b/src/libs/API/parameters/UpdatePolicyDistanceRateValueParams.ts new file mode 100644 index 000000000000..477b6cc20c5a --- /dev/null +++ b/src/libs/API/parameters/UpdatePolicyDistanceRateValueParams.ts @@ -0,0 +1,7 @@ +type UpdatePolicyDistanceRateValueParams = { + policyID: string; + customUnitID: string; + customUnitRates: string; +}; + +export default UpdatePolicyDistanceRateValueParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 4ebeeaf6d692..14b934f91b2f 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -173,3 +173,6 @@ export type {default as OpenPolicyDistanceRatesPageParams} from './OpenPolicyDis export type {default as CreatePolicyDistanceRateParams} from './CreatePolicyDistanceRateParams'; export type {default as SetPolicyDistanceRatesUnitParams} from './SetPolicyDistanceRatesUnitParams'; export type {default as SetPolicyDistanceRatesDefaultCategoryParams} from './SetPolicyDistanceRatesDefaultCategoryParams'; +export type {default as UpdatePolicyDistanceRateValueParams} from './UpdatePolicyDistanceRateValueParams'; +export type {default as SetPolicyDistanceRatesEnabledParams} from './SetPolicyDistanceRatesEnabledParams'; +export type {default as DeletePolicyDistanceRatesParams} from './DeletePolicyDistanceRatesParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index a496143f9c0d..7d3c51aaeb47 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -172,6 +172,9 @@ const WRITE_COMMANDS = { CREATE_POLICY_DISTANCE_RATE: 'CreatePolicyDistanceRate', SET_POLICY_DISTANCE_RATES_UNIT: 'SetPolicyDistanceRatesUnit', SET_POLICY_DISTANCE_RATES_DEFAULT_CATEGORY: 'SetPolicyDistanceRatesDefaultCategory', + UPDATE_POLICY_DISTANCE_RATE_VALUE: 'UpdatePolicyDistanceRateValue', + SET_POLICY_DISTANCE_RATES_ENABLED: 'SetPolicyDistanceRatesEnabled', + DELETE_POLICY_DISTANCE_RATES: 'DeletePolicyDistanceRates', } as const; type WriteCommand = ValueOf; @@ -342,6 +345,9 @@ type WriteCommandParameters = { [WRITE_COMMANDS.CREATE_POLICY_DISTANCE_RATE]: Parameters.CreatePolicyDistanceRateParams; [WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_UNIT]: Parameters.SetPolicyDistanceRatesUnitParams; [WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_DEFAULT_CATEGORY]: Parameters.SetPolicyDistanceRatesDefaultCategoryParams; + [WRITE_COMMANDS.UPDATE_POLICY_DISTANCE_RATE_VALUE]: Parameters.UpdatePolicyDistanceRateValueParams; + [WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_ENABLED]: Parameters.SetPolicyDistanceRatesEnabledParams; + [WRITE_COMMANDS.DELETE_POLICY_DISTANCE_RATES]: Parameters.DeletePolicyDistanceRatesParams; }; const READ_COMMANDS = { diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx index 113ac41400ed..7fb6b5fe4c58 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx @@ -256,6 +256,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../pages/workspace/categories/CreateCategoryPage').default as React.ComponentType, [SCREENS.WORKSPACE.CREATE_DISTANCE_RATE]: () => require('../../../pages/workspace/distanceRates/PolicyNewDistanceRatePage').default as React.ComponentType, [SCREENS.WORKSPACE.DISTANCE_RATES_SETTINGS]: () => require('../../../pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage').default as React.ComponentType, + [SCREENS.WORKSPACE.DISTANCE_RATE_EDIT]: () => require('../../../pages/workspace/distanceRates/PolicyDistanceRateEditPage').default as React.ComponentType, [SCREENS.WORKSPACE.TAGS_SETTINGS]: () => require('../../../pages/workspace/tags/WorkspaceTagsSettingsPage').default as React.ComponentType, [SCREENS.WORKSPACE.TAGS_EDIT]: () => require('../../../pages/workspace/tags/WorkspaceEditTagsPage').default as React.ComponentType, [SCREENS.REIMBURSEMENT_ACCOUNT]: () => require('../../../pages/ReimbursementAccount/ReimbursementAccountPage').default as React.ComponentType, diff --git a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts index fb0839af3629..80ca3d387656 100755 --- a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts @@ -8,7 +8,7 @@ const CENTRAL_PANE_TO_RHP_MAPPING: Partial> = [SCREENS.WORKSPACE.WORKFLOWS]: [SCREENS.WORKSPACE.WORKFLOWS_APPROVER, SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY, SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET], [SCREENS.WORKSPACE.TAGS]: [SCREENS.WORKSPACE.TAGS_SETTINGS, SCREENS.WORKSPACE.TAGS_EDIT], [SCREENS.WORKSPACE.CATEGORIES]: [SCREENS.WORKSPACE.CATEGORY_CREATE, SCREENS.WORKSPACE.CATEGORY_SETTINGS, SCREENS.WORKSPACE.CATEGORIES_SETTINGS], - [SCREENS.WORKSPACE.DISTANCE_RATES]: [SCREENS.WORKSPACE.CREATE_DISTANCE_RATE, SCREENS.WORKSPACE.DISTANCE_RATES_SETTINGS], + [SCREENS.WORKSPACE.DISTANCE_RATES]: [SCREENS.WORKSPACE.CREATE_DISTANCE_RATE, SCREENS.WORKSPACE.DISTANCE_RATES_SETTINGS, SCREENS.WORKSPACE.DISTANCE_RATE_EDIT], }; export default CENTRAL_PANE_TO_RHP_MAPPING; diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index c2fa13039567..49bbed5aa0cd 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -303,6 +303,9 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.DISTANCE_RATES_SETTINGS]: { path: ROUTES.WORKSPACE_DISTANCE_RATES_SETTINGS.route, }, + [SCREENS.WORKSPACE.DISTANCE_RATE_EDIT]: { + path: ROUTES.WORKSPACE_DISTANCE_RATE_EDIT.route, + }, [SCREENS.WORKSPACE.TAGS_SETTINGS]: { path: ROUTES.WORKSPACE_TAGS_SETTINGS.route, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 1fa362e090e8..29c72b8705a7 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -214,6 +214,10 @@ type SettingsNavigatorParamList = { [SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: { policyID: string; }; + [SCREENS.WORKSPACE.DISTANCE_RATE_EDIT]: { + policyID: string; + rateID: string; + }; [SCREENS.WORKSPACE.TAGS_SETTINGS]: { policyID: string; }; diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index 8ee6553f14b5..577a3ecced50 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -14,6 +14,7 @@ import type { CreateWorkspaceFromIOUPaymentParams, CreateWorkspaceParams, DeleteMembersFromWorkspaceParams, + DeletePolicyDistanceRatesParams, DeleteWorkspaceAvatarParams, DeleteWorkspaceParams, EnablePolicyCategoriesParams, @@ -32,11 +33,13 @@ import type { OpenWorkspaceParams, OpenWorkspaceReimburseViewParams, SetPolicyDistanceRatesDefaultCategoryParams, + SetPolicyDistanceRatesEnabledParams, SetPolicyDistanceRatesUnitParams, SetWorkspaceApprovalModeParams, SetWorkspaceAutoReportingFrequencyParams, SetWorkspaceAutoReportingMonthlyOffsetParams, SetWorkspaceAutoReportingParams, + UpdatePolicyDistanceRateValueParams, UpdateWorkspaceAvatarParams, UpdateWorkspaceCustomUnitAndRateParams, UpdateWorkspaceDescriptionParams, @@ -3446,6 +3449,240 @@ function setPolicyDistanceRatesDefaultCategory(policyID: string, currentCustomUn API.write(WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_DEFAULT_CATEGORY, params, {optimisticData, successData, failureData}); } +function updatePolicyDistanceRateValue(policyID: string, customUnit: CustomUnit, customUnitRates: Rate[]) { + if (!policyID || !customUnit || !customUnitRates) { + return; + } + + const currentRates = customUnit.rates; + const optimisticRates: Record = {}; + const successRates: Record = {}; + + Object.keys(customUnit.rates).forEach((rateID) => { + if (!customUnitRates.map((customUnitRate) => customUnitRate.customUnitRateID).includes(rateID)) { + optimisticRates[rateID] = { + ...customUnitRates.find((rate) => rate.customUnitRateID === rateID), + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }; + successRates[rateID] = { + ...customUnitRates.find((rate) => rate.customUnitRateID === rateID), + pendingAction: null, + }; + } else { + optimisticRates[rateID] = { + ...currentRates[rateID], + }; + successRates[rateID] = { + ...currentRates[rateID], + }; + } + }); + + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + customUnits: { + [customUnit.customUnitID]: { + rates: optimisticRates, + }, + }, + }, + }, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + customUnits: { + [customUnit.customUnitID]: { + rates: successRates, + }, + }, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + customUnits: { + [customUnit.customUnitID]: { + rates: currentRates, + }, + errors: ErrorUtils.getMicroSecondOnyxError('workspace.distanceRates.errors.updateRateValueGenericFailureMessage'), + }, + }, + }, + ]; + + const params: UpdatePolicyDistanceRateValueParams = { + policyID, + customUnitID: customUnit.customUnitID, + customUnitRates: JSON.stringify(customUnitRates), + }; + + API.write(WRITE_COMMANDS.UPDATE_POLICY_DISTANCE_RATE_VALUE, params, {optimisticData, successData, failureData}); +} + +function setPolicyDistanceRatesEnabled(policyID: string, customUnit: CustomUnit, customUnitRates: Rate[]) { + if (!policyID || !customUnit || !customUnitRates) { + return; + } + + const currentRates = customUnit.rates; + const optimisticRates: Record = {}; + const successRates: Record = {}; + + Object.keys(customUnit.rates).forEach((rateID) => { + if (!customUnitRates.map((customUnitRate) => customUnitRate.customUnitRateID).includes(rateID)) { + return; + } + + optimisticRates[rateID] = { + ...customUnitRates.find((rate) => rate.customUnitRateID === rateID), + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }; + successRates[rateID] = { + ...customUnitRates.find((rate) => rate.customUnitRateID === rateID), + pendingAction: null, + }; + }); + + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + customUnits: { + [customUnit.customUnitID]: { + rates: optimisticRates, + }, + }, + }, + }, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + customUnits: { + [customUnit.customUnitID]: { + rates: successRates, + }, + }, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + customUnits: { + [customUnit.customUnitID]: { + rates: currentRates, + }, + errors: ErrorUtils.getMicroSecondOnyxError('workspace.distanceRates.errors.toggleRateEnabledGenericFailureMessage'), + }, + }, + }, + ]; + + const params: SetPolicyDistanceRatesEnabledParams = { + policyID, + customUnitID: customUnit.customUnitID, + customUnitRates: JSON.stringify(customUnitRates), + }; + + API.write(WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_ENABLED, params, {optimisticData, successData, failureData}); +} + +function deletePolicyDistanceRates(policyID: string, rateIDsToDelete: string[], customUnit?: CustomUnit) { + if (!policyID || !rateIDsToDelete || !customUnit) { + return; + } + + const currentRates = customUnit.rates; + const optimisticRates: Record = {}; + const successRates: Record = {}; + + Object.keys(customUnit.rates).forEach((rateID) => { + if (rateIDsToDelete.includes(rateID)) { + optimisticRates[rateID] = { + ...customUnit.rates[rateID], + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + }; + successRates[rateID] = { + ...customUnit.rates[rateID], + pendingAction: null, + }; + } else { + optimisticRates[rateID] = customUnit.rates[rateID]; + successRates[rateID] = customUnit.rates[rateID]; + } + }); + + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + customUnits: { + [customUnit.customUnitID]: { + rates: optimisticRates, + }, + }, + }, + }, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + customUnits: { + [customUnit.customUnitID]: { + rates: successRates, + }, + }, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + customUnits: { + [customUnit.customUnitID]: { + rates: currentRates, + }, + errors: ErrorUtils.getMicroSecondOnyxError('workspace.distanceRates.errors.deleteRateGenericFailureMessage'), + }, + }, + }, + ]; + + const params: DeletePolicyDistanceRatesParams = { + policyID, + customUnitID: customUnit.customUnitID, + customUnitRateIDs: rateIDsToDelete, + }; + + API.write(WRITE_COMMANDS.DELETE_POLICY_DISTANCE_RATES, params, {optimisticData, successData, failureData}); +} + export { removeMembers, updateWorkspaceMembersRole, @@ -3516,4 +3753,7 @@ export { clearCreateDistanceRateError, setPolicyDistanceRatesUnit, setPolicyDistanceRatesDefaultCategory, + updatePolicyDistanceRateValue, + setPolicyDistanceRatesEnabled, + deletePolicyDistanceRates, }; diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRateEditPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRateEditPage.tsx new file mode 100644 index 000000000000..443fee0484f4 --- /dev/null +++ b/src/pages/workspace/distanceRates/PolicyDistanceRateEditPage.tsx @@ -0,0 +1,144 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useState} from 'react'; +import {View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; +import ConfirmModal from '@components/ConfirmModal'; +import type {FormOnyxValues} from '@components/Form/types'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import * as Expensicons from '@components/Icon/Expensicons'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import ScreenWrapper from '@components/ScreenWrapper'; +import Switch from '@components/Switch'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import * as CurrencyUtils from '@libs/CurrencyUtils'; +import type {SettingsNavigatorParamList} from '@navigation/types'; +import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; +import * as Policy from '@userActions/Policy'; +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 RateModal from './RateModal'; + +type PolicyDistanceRateEditPageOnyxProps = { + /** Policy details */ + policy: OnyxEntry; +}; + +type PolicyDistanceRateEditPageProps = PolicyDistanceRateEditPageOnyxProps & StackScreenProps; + +function PolicyDistanceRateEditPage({policy, route}: PolicyDistanceRateEditPageProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const {windowWidth} = useWindowDimensions(); + + const [isRateModalVisible, setIsRateModalVisible] = useState(false); + const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); + + const policyID = route.params.policyID; + const rateID = route.params.rateID; + const customUnits = policy?.customUnits ?? {}; + const customUnit = customUnits[Object.keys(customUnits)[0]]; + const rate = customUnit.rates[rateID]; + const currency = rate.currency ?? CONST.CURRENCY.USD; + + const showRateModal = () => { + setIsRateModalVisible(true); + }; + + const hideRateModal = () => { + setIsRateModalVisible(false); + }; + + const updateRate = (values: FormOnyxValues) => { + Policy.updatePolicyDistanceRateValue(policyID, customUnit, [{...rate, rate: Number(values.rate)}]); + }; + + const toggleRate = () => { + Policy.setPolicyDistanceRatesEnabled(policyID, customUnit, [{...rate, enabled: !rate.enabled}]); + }; + + const deleteRate = () => { + Policy.deletePolicyDistanceRates(policyID, [rateID], customUnit); + setIsDeleteModalVisible(false); + }; + + const rateValueToDisplay = CurrencyUtils.convertAmountToDisplayString(rate.rate, currency); + const unitToDisplay = translate(`common.${customUnit?.attributes?.unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES}`); + + const threeDotsMenuItems = [ + { + icon: Expensicons.Trashcan, + text: translate('workspace.distanceRates.deleteDistanceRate'), + onSelected: () => { + setIsDeleteModalVisible(true); + }, + }, + ]; + + return ( + + + + + + {translate('workspace.distanceRates.enableRate')} + + + + + setIsDeleteModalVisible(false)} + prompt={translate('workspace.distanceRates.areYouSureDelete')} + confirmText={translate('common.delete')} + cancelText={translate('common.cancel')} + danger + /> + + + + ); +} + +PolicyDistanceRateEditPage.displayName = 'PolicyDistanceRateEditPage'; + +export default withOnyx({ + policy: { + key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY}${route.params.policyID}`, + }, +})(PolicyDistanceRateEditPage); diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx index 8f70fa9c13db..ef6808914ed7 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx @@ -116,8 +116,8 @@ function PolicyDistanceRatesPage({policy, route}: PolicyDistanceRatesPageProps) Navigation.navigate(ROUTES.WORKSPACE_DISTANCE_RATES_SETTINGS.getRoute(policyID)); }; - const editRate = () => { - // Navigation.navigate(ROUTES.WORKSPACE_EDIT_DISTANCE_RATE.getRoute(policyID, rateID)); + const editRate = (rate: RateForList) => { + Navigation.navigate(ROUTES.WORKSPACE_DISTANCE_RATE_EDIT.getRoute(policyID, rate.value)); }; const disableRates = () => { diff --git a/src/pages/workspace/distanceRates/RateModal.tsx b/src/pages/workspace/distanceRates/RateModal.tsx new file mode 100644 index 000000000000..45a0c62a3f35 --- /dev/null +++ b/src/pages/workspace/distanceRates/RateModal.tsx @@ -0,0 +1,105 @@ +import React from 'react'; +import AmountForm from '@components/AmountForm'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapperWithRef from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import Modal from '@components/Modal'; +import ScreenWrapper from '@components/ScreenWrapper'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as CurrencyUtils from '@libs/CurrencyUtils'; +import getPermittedDecimalSeparator from '@libs/getPermittedDecimalSeparator'; +import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; +import * as NumberUtils from '@libs/NumberUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/PolicyDistanceRateEditForm'; + +type RateModalProps = { + /** Whether the modal is visible */ + isVisible: boolean; + + /** Current rate value */ + currentRate?: string; + + /** Function to call when the user submits new rate */ + onRateSubmit: (values: FormOnyxValues) => void; + + /** Function to call when the user closes the category selector modal */ + onClose: () => void; + + /** Label to display on field */ + label: string; + + /** Currency to display next to rate */ + currency: string; +}; + +function RateModal({isVisible, currentRate = '', onRateSubmit, onClose, label, currency}: RateModalProps) { + const styles = useThemeStyles(); + const {translate, toLocaleDigit} = useLocalize(); + + const validate = (values: FormOnyxValues): FormInputErrors => { + const errors: FormInputErrors = {}; + const rate = values.rate; + const parsedRate = MoneyRequestUtils.replaceAllDigits(rate, toLocaleDigit); + const decimalSeparator = toLocaleDigit('.'); + // Allow one more decimal place for accuracy + const rateValueRegex = RegExp(String.raw`^-?\d{0,8}([${getPermittedDecimalSeparator(decimalSeparator)}]\d{1,${CurrencyUtils.getCurrencyDecimals(currency) + 1}})?$`, 'i'); + if (!rateValueRegex.test(parsedRate) || parsedRate === '') { + errors.rate = 'workspace.reimburse.invalidRateError'; + } else if (NumberUtils.parseFloatAnyLocale(parsedRate) <= 0) { + errors.rate = 'workspace.reimburse.lowRateError'; + } + return errors; + }; + + return ( + + + + + + + + + ); +} + +RateModal.displayName = 'RateModal'; + +export default RateModal; diff --git a/src/types/form/PolicyDistanceRateEditForm.ts b/src/types/form/PolicyDistanceRateEditForm.ts new file mode 100644 index 000000000000..2c7cb97b08d8 --- /dev/null +++ b/src/types/form/PolicyDistanceRateEditForm.ts @@ -0,0 +1,18 @@ +import type {ValueOf} from 'type-fest'; +import type Form from './Form'; + +const INPUT_IDS = { + RATE: 'rate', +} as const; + +type InputID = ValueOf; + +type PolicyDistanceRateEditForm = Form< + InputID, + { + [INPUT_IDS.RATE]: string; + } +>; + +export type {PolicyDistanceRateEditForm}; +export default INPUT_IDS; diff --git a/src/types/form/index.ts b/src/types/form/index.ts index 93bc1a774a6f..edc2e7d4d93a 100644 --- a/src/types/form/index.ts +++ b/src/types/form/index.ts @@ -40,4 +40,5 @@ export type {ReportPhysicalCardForm} from './ReportPhysicalCardForm'; export type {WorkspaceDescriptionForm} from './WorkspaceDescriptionForm'; export type {PolicyTagNameForm} from './PolicyTagNameForm'; export type {PolicyCreateDistanceRateForm} from './PolicyCreateDistanceRateForm'; +export type {PolicyDistanceRateEditForm} from './PolicyDistanceRateEditForm'; export type {default as Form} from './Form'; From 05b73c6c7458af65296defda31ef007d13e67918 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Wed, 13 Mar 2024 16:16:22 +0700 Subject: [PATCH 243/484] fix the case emoji inside codeblock --- src/components/InlineCodeBlock/index.native.tsx | 4 +++- src/components/InlineCodeBlock/index.tsx | 5 ++++- src/components/InlineCodeBlock/removeEmojiTag.ts | 11 +++++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 src/components/InlineCodeBlock/removeEmojiTag.ts diff --git a/src/components/InlineCodeBlock/index.native.tsx b/src/components/InlineCodeBlock/index.native.tsx index 85d02b7239ca..9075cdcc2140 100644 --- a/src/components/InlineCodeBlock/index.native.tsx +++ b/src/components/InlineCodeBlock/index.native.tsx @@ -1,12 +1,14 @@ import React from 'react'; import useThemeStyles from '@hooks/useThemeStyles'; +import removeEmojiTag from './removeEmojiTag'; import type InlineCodeBlockProps from './types'; import type {TTextOrTPhrasing} from './types'; import WrappedText from './WrappedText'; function InlineCodeBlock({TDefaultRenderer, defaultRendererProps, textStyle, boxModelStyle}: InlineCodeBlockProps) { const styles = useThemeStyles(); - + const data = removeEmojiTag(defaultRendererProps); + return ( ({TDefaultRenderer, const flattenTextStyle = StyleSheet.flatten(textStyle); const {textDecorationLine, ...textStyles} = flattenTextStyle; + const data = removeEmojiTag(defaultRendererProps); + return ( - {'data' in defaultRendererProps.tnode && defaultRendererProps.tnode.data} + {data} ); } diff --git a/src/components/InlineCodeBlock/removeEmojiTag.ts b/src/components/InlineCodeBlock/removeEmojiTag.ts new file mode 100644 index 000000000000..84de3999ee03 --- /dev/null +++ b/src/components/InlineCodeBlock/removeEmojiTag.ts @@ -0,0 +1,11 @@ +import type {TDefaultRendererProps} from 'react-native-render-html'; +import type {TTextOrTPhrasing} from './types'; + +// Create a temporary solution to display when there are emojis in the inline code block +// We can remove this after https://github.com/Expensify/App/issues/14676 is fixed +export default function removeEmojiTag(defaultRendererProps: TDefaultRendererProps): string { + if ('data' in defaultRendererProps.tnode) { + return defaultRendererProps.tnode.data; + } + return defaultRendererProps.tnode.children.map((child) => ('data' in child ? child.data : '')).join(''); +} From 38f03c95f3e953c2a1511935cb0c426ba4b535b7 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Wed, 13 Mar 2024 16:28:56 +0700 Subject: [PATCH 244/484] fix lint --- src/components/InlineCodeBlock/index.native.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/InlineCodeBlock/index.native.tsx b/src/components/InlineCodeBlock/index.native.tsx index 9075cdcc2140..4e28b8bfc147 100644 --- a/src/components/InlineCodeBlock/index.native.tsx +++ b/src/components/InlineCodeBlock/index.native.tsx @@ -8,7 +8,7 @@ import WrappedText from './WrappedText'; function InlineCodeBlock({TDefaultRenderer, defaultRendererProps, textStyle, boxModelStyle}: InlineCodeBlockProps) { const styles = useThemeStyles(); const data = removeEmojiTag(defaultRendererProps); - + return ( ({TDefaultRenderer, textStyles={textStyle} wordStyles={[boxModelStyle, styles.codeWordStyle]} > - {'data' in defaultRendererProps.tnode && defaultRendererProps.tnode.data} + {data} ); From dff681dfb13a67bc6e8618936ea2288cc8340d1c Mon Sep 17 00:00:00 2001 From: Jakub Kosmydel <104823336+kosmydel@users.noreply.github.com> Date: Wed, 13 Mar 2024 10:29:56 +0100 Subject: [PATCH 245/484] add edit tax modal --- src/ROUTES.ts | 4 + src/SCREENS.ts | 1 + .../AppNavigator/ModalStackNavigators.tsx | 1 + src/libs/Navigation/linkingConfig/config.ts | 3 + src/libs/Navigation/types.ts | 4 + src/libs/PolicyUtils.ts | 8 +- .../workspace/taxes/WorkspaceEditTaxPage.tsx | 97 +++++++++++++++++++ .../workspace/taxes/WorkspaceTaxesPage.tsx | 2 +- 8 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index fd30bb0a6ac9..5a9c0cc7ad2a 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -601,6 +601,10 @@ const ROUTES = { route: 'workspace/:policyID/taxes/new', getRoute: (policyID: string) => `workspace/${policyID}/taxes/new` as const, }, + WORKSPACE_TAXES_EDIT: { + route: 'workspace/:policyID/tax/:taxID', + getRoute: (policyID: string, taxID: string) => `workspace/${policyID}/tax/${encodeURI(taxID)}` as const, + }, WORKSPACE_DISTANCE_RATES: { route: 'workspace/:policyID/distance-rates', getRoute: (policyID: string) => `workspace/${policyID}/distance-rates` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 5e3126dfe7f5..11c2d38f4361 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -221,6 +221,7 @@ const SCREENS = { TAGS_EDIT: 'Tags_Edit', TAXES: 'Workspace_Taxes', TAXES_NEW: 'Workspace_Taxes_New', + TAXES_EDIT: 'Workspace_Taxes_Edit', TAG_CREATE: 'Tag_Create', CURRENCY: 'Workspace_Profile_Currency', WORKFLOWS: 'Workspace_Workflows', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx index 2a6a1a0dbb03..164dccbc10ad 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx @@ -277,6 +277,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage').default as React.ComponentType, [SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET]: () => require('../../../pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage').default as React.ComponentType, [SCREENS.WORKSPACE.TAXES_NEW]: () => require('../../../pages/workspace/taxes/WorkspaceNewTaxPage').default as React.ComponentType, + [SCREENS.WORKSPACE.TAXES_EDIT]: () => require('../../../pages/workspace/taxes/WorkspaceEditTaxPage').default as React.ComponentType, }); const EnablePaymentsStackNavigator = createModalStackNavigator({ diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index b759ff9e977e..d9fd1fc98c9c 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -318,6 +318,9 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.TAXES_NEW]: { path: ROUTES.WORKSPACE_TAXES_NEW.route, }, + [SCREENS.WORKSPACE.TAXES_EDIT]: { + path: ROUTES.WORKSPACE_TAXES_EDIT.route, + }, }, }, [SCREENS.RIGHT_MODAL.PRIVATE_NOTES]: { diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index c8ea81b2b5a7..dafe451262d2 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -212,6 +212,10 @@ type SettingsNavigatorParamList = { [SCREENS.WORKSPACE.TAXES_NEW]: { policyID: string; }; + [SCREENS.WORKSPACE.TAXES_EDIT]: { + policyID: string; + taxID: string; + }; } & ReimbursementAccountNavigatorParamList; type NewChatNavigatorParamList = { diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index d42ad0d56d77..fe6fec83730b 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -4,7 +4,7 @@ import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {PersonalDetailsList, Policy, PolicyMembers, PolicyTagList, PolicyTags} from '@src/types/onyx'; +import type {PersonalDetailsList, Policy, PolicyMembers, PolicyTagList, PolicyTags, TaxRate} from '@src/types/onyx'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import Navigation from './Navigation/Navigation'; @@ -272,6 +272,11 @@ function goBackFromInvalidPolicy() { Navigation.navigateWithSwitchPolicyID({route: ROUTES.ALL_SETTINGS}); } +/** Get a tax with given ID from policy */ +function getTaxByID(policy: OnyxEntry, taxID: string): TaxRate | undefined { + return policy?.taxRates?.taxes?.[taxID ?? '']; +} + export { getActivePolicies, hasAccountingConnections, @@ -303,6 +308,7 @@ export { getPolicyMembersByIdWithoutCurrentUser, goBackFromInvalidPolicy, hasTaxRateError, + getTaxByID, }; export type {MemberEmailsToAccountIDs}; diff --git a/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx b/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx new file mode 100644 index 000000000000..94524ae16cd7 --- /dev/null +++ b/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx @@ -0,0 +1,97 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useMemo} from 'react'; +import {View} from 'react-native'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import * as Expensicons from '@components/Icon/Expensicons'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import ScreenWrapper from '@components/ScreenWrapper'; +import Switch from '@components/Switch'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; +import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; +import type SCREENS from '@src/SCREENS'; + +type WorkspaceEditTaxPageBaseProps = WithPolicyAndFullscreenLoadingProps & StackScreenProps; + +function WorkspaceEditTaxPage({ + route: { + params: {taxID}, + }, + policy, +}: WorkspaceEditTaxPageBaseProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const currentTaxRate = PolicyUtils.getTaxByID(policy, taxID); + const {windowWidth} = useWindowDimensions(); + + const toggle = () => {}; + + const threeDotsMenuItems = useMemo(() => { + const menuItems = [ + { + icon: Expensicons.Trashcan, + text: translate('common.delete'), + onSelected: () => {}, + }, + ]; + return menuItems; + }, [translate]); + + return ( + + + + + {taxID ? ( + // TODO: Extract it to a separate component or use a common one + + + Enable rate + + + + + + ) : null} + {}} + /> + {}} + /> + + + + ); +} + +WorkspaceEditTaxPage.displayName = 'WorkspaceEditTaxPage'; + +export default withPolicyAndFullscreenLoading(WorkspaceEditTaxPage); diff --git a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx index b0436f20a522..cede2bd31e7d 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx @@ -182,7 +182,7 @@ function WorkspaceTaxesPage({policy, route}: WorkspaceTaxesPageProps) { canSelectMultiple sections={[{data: taxesList, indexOffset: 0, isDisabled: false}]} onCheckboxPress={toggleTax} - onSelectRow={() => {}} + onSelectRow={(tax: ListItem) => tax.keyForList && Navigation.navigate(ROUTES.WORKSPACE_TAXES_EDIT.getRoute(policy?.id ?? '', tax.keyForList))} onSelectAll={toggleAllTaxes} showScrollIndicator ListItem={TableListItem} From ff731cbbad482073e39f4638d4ca020a353ae75c Mon Sep 17 00:00:00 2001 From: Jakub Kosmydel <104823336+kosmydel@users.noreply.github.com> Date: Wed, 13 Mar 2024 10:45:08 +0100 Subject: [PATCH 246/484] add enabling/disabling taxes --- .../parameters/SetPolicyTaxesEnabledParams.ts | 10 +++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 + .../FULL_SCREEN_TO_RHP_MAPPING.ts | 2 +- src/libs/actions/TaxRate.ts | 75 ++++++++++++++++++- .../workspace/taxes/WorkspaceEditTaxPage.tsx | 10 ++- 6 files changed, 95 insertions(+), 5 deletions(-) create mode 100644 src/libs/API/parameters/SetPolicyTaxesEnabledParams.ts diff --git a/src/libs/API/parameters/SetPolicyTaxesEnabledParams.ts b/src/libs/API/parameters/SetPolicyTaxesEnabledParams.ts new file mode 100644 index 000000000000..0bc8550cd01b --- /dev/null +++ b/src/libs/API/parameters/SetPolicyTaxesEnabledParams.ts @@ -0,0 +1,10 @@ +type SetPolicyTaxesEnabledParams = { + policyID: string; + /** + * Stringified JSON object with type of following structure: + * Array<{taxCode: string, enabled: bool}> + */ + taxFields: string; +}; + +export default SetPolicyTaxesEnabledParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 643657e86614..bfe08dbab50f 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -176,3 +176,4 @@ export type {default as OpenPolicyWorkflowsPageParams} from './OpenPolicyWorkflo export type {default as OpenPolicyDistanceRatesPageParams} from './OpenPolicyDistanceRatesPageParams'; export type {default as OpenPolicyTaxesPageParams} from './OpenPolicyTaxesPageParams'; export type {default as CreatePolicyTagsParams} from './CreatePolicyTagsParams'; +export type {default as SetPolicyTaxesEnabledParams} from './SetPolicyTaxesEnabledParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 12c7f3c3bd5a..271aec0ec9be 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -173,6 +173,7 @@ const WRITE_COMMANDS = { ACCEPT_JOIN_REQUEST: 'AcceptJoinRequest', DECLINE_JOIN_REQUEST: 'DeclineJoinRequest', CREATE_POLICY_TAX: 'CreatePolicyTax', + SET_POLICY_TAXES_ENABLED: 'SetPolicyTaxesEnabled', } as const; type WriteCommand = ValueOf; @@ -344,6 +345,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.ACCEPT_JOIN_REQUEST]: Parameters.AcceptJoinRequestParams; [WRITE_COMMANDS.DECLINE_JOIN_REQUEST]: Parameters.DeclineJoinRequestParams; [WRITE_COMMANDS.CREATE_POLICY_TAX]: Parameters.CreatePolicyTaxParams; + [WRITE_COMMANDS.SET_POLICY_TAXES_ENABLED]: Parameters.SetPolicyTaxesEnabledParams; }; const READ_COMMANDS = { diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts index 2693f443d659..a7d3fad55788 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -13,7 +13,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { ], [SCREENS.WORKSPACE.TAGS]: [SCREENS.WORKSPACE.TAGS_SETTINGS, SCREENS.WORKSPACE.TAGS_EDIT, SCREENS.WORKSPACE.TAG_CREATE], [SCREENS.WORKSPACE.CATEGORIES]: [SCREENS.WORKSPACE.CATEGORY_CREATE, SCREENS.WORKSPACE.CATEGORY_SETTINGS, SCREENS.WORKSPACE.CATEGORIES_SETTINGS], - [SCREENS.WORKSPACE.TAXES]: [SCREENS.WORKSPACE.TAXES_NEW], + [SCREENS.WORKSPACE.TAXES]: [SCREENS.WORKSPACE.TAXES_NEW, SCREENS.WORKSPACE.TAXES_EDIT], }; export default FULL_SCREEN_TO_RHP_MAPPING; diff --git a/src/libs/actions/TaxRate.ts b/src/libs/actions/TaxRate.ts index 770417e56fe2..3c1f777f315e 100644 --- a/src/libs/actions/TaxRate.ts +++ b/src/libs/actions/TaxRate.ts @@ -1,14 +1,22 @@ +import type {OnyxCollection} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; -import type {CreatePolicyTaxParams} from '@libs/API/parameters'; +import type {CreatePolicyTaxParams, SetPolicyTaxesEnabledParams} from '@libs/API/parameters'; import {WRITE_COMMANDS} from '@libs/API/types'; import CONST from '@src/CONST'; import * as ErrorUtils from '@src/libs/ErrorUtils'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {TaxRate} from '@src/types/onyx'; +import type {Policy, TaxRate} from '@src/types/onyx'; import type {PendingAction} from '@src/types/onyx/OnyxCommon'; import type {OnyxData} from '@src/types/onyx/Request'; +let allPolicies: OnyxCollection; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.POLICY, + waitForCollectionCallback: true, + callback: (value) => (allPolicies = value), +}); + /** * Get tax value with percentage */ @@ -111,4 +119,65 @@ function clearTaxRateError(policyID: string, taxID: string, pendingAction?: Pend }); } -export {createWorkspaceTax, clearTaxRateError, getNextTaxID, getTaxValueWithPercentage}; +type TaxRateEnabledMap = Record>; + +function setPolicyTaxesEnabled(policyID: string, taxesIDsToUpdate: string[], isEnabled: boolean) { + const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; + const originalTaxes = {...policy?.taxRates?.taxes}; + + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + taxRates: { + taxes: taxesIDsToUpdate.reduce((acc, taxID) => { + acc[taxID] = {isDisabled: !isEnabled, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}; + return acc; + }, {} as TaxRateEnabledMap), + }, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + taxRates: { + taxes: taxesIDsToUpdate.reduce((acc, taxID) => { + acc[taxID] = {isDisabled: !isEnabled, pendingAction: null}; + return acc; + }, {} as TaxRateEnabledMap), + }, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + taxRates: { + taxes: taxesIDsToUpdate.reduce((acc, taxID) => { + acc[taxID] = {isDisabled: !!originalTaxes[taxID].isDisabled, pendingAction: null}; + return acc; + }, {} as TaxRateEnabledMap), + }, + }, + }, + ], + }; + + const parameters = { + policyID, + taxFields: JSON.stringify(taxesIDsToUpdate.map((taxID) => ({taxCode: taxID, enabled: isEnabled}))), + } satisfies SetPolicyTaxesEnabledParams; + + console.log({parameters}); + + API.write(WRITE_COMMANDS.SET_POLICY_TAXES_ENABLED, parameters, onyxData); +} + +export {createWorkspaceTax, clearTaxRateError, getNextTaxID, getTaxValueWithPercentage, setPolicyTaxesEnabled}; diff --git a/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx b/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx index 94524ae16cd7..22706f22faf1 100644 --- a/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx @@ -10,6 +10,7 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import {setPolicyTaxesEnabled} from '@libs/actions/TaxRate'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; @@ -29,7 +30,14 @@ function WorkspaceEditTaxPage({ const currentTaxRate = PolicyUtils.getTaxByID(policy, taxID); const {windowWidth} = useWindowDimensions(); - const toggle = () => {}; + const toggle = () => { + // TODO: Backend call doesn't exist yet + return; + if (!policy?.id || !currentTaxRate) { + return; + } + setPolicyTaxesEnabled(policy.id, [taxID], !currentTaxRate?.isDisabled); + }; const threeDotsMenuItems = useMemo(() => { const menuItems = [ From ea97afb6673f59c3112b9a85658538f6fb051c47 Mon Sep 17 00:00:00 2001 From: Jakub Kosmydel <104823336+kosmydel@users.noreply.github.com> Date: Wed, 13 Mar 2024 11:39:36 +0100 Subject: [PATCH 247/484] add deleting tax rates --- src/languages/en.ts | 2 + .../API/parameters/DeletePolicyTaxesParams.ts | 11 +++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 + src/libs/actions/TaxRate.ts | 83 +++++++++++++++++-- .../workspace/taxes/WorkspaceEditTaxPage.tsx | 28 ++++++- 6 files changed, 118 insertions(+), 9 deletions(-) create mode 100644 src/libs/API/parameters/DeletePolicyTaxesParams.ts diff --git a/src/languages/en.ts b/src/languages/en.ts index 4ca7b1e059ab..1c5a6931e10b 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1856,6 +1856,8 @@ export default { valuePercentageRange: 'Please enter a valid percentage between 0 and 100', genericFailureMessage: 'An error occurred while updating the tax rate, please try again.', }, + deleteTax: 'Delete tax', + deleteTaxConfirmation: 'Are you sure you want to delete this tax?', }, emptyWorkspace: { title: 'Create a workspace', diff --git a/src/libs/API/parameters/DeletePolicyTaxesParams.ts b/src/libs/API/parameters/DeletePolicyTaxesParams.ts new file mode 100644 index 000000000000..fe03d388a129 --- /dev/null +++ b/src/libs/API/parameters/DeletePolicyTaxesParams.ts @@ -0,0 +1,11 @@ +type DeletePolicyTaxesParams = { + policyID: string; + /** + * Stringified JSON object with type of following structure: + * Array + * Each element is a tax name + */ + taxCodes: string; +}; + +export default DeletePolicyTaxesParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index bfe08dbab50f..6567a3e22ad0 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -177,3 +177,4 @@ export type {default as OpenPolicyDistanceRatesPageParams} from './OpenPolicyDis export type {default as OpenPolicyTaxesPageParams} from './OpenPolicyTaxesPageParams'; export type {default as CreatePolicyTagsParams} from './CreatePolicyTagsParams'; export type {default as SetPolicyTaxesEnabledParams} from './SetPolicyTaxesEnabledParams'; +export type {default as DeletePolicyTaxesParams} from './DeletePolicyTaxesParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 271aec0ec9be..98e5d820363a 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -174,6 +174,7 @@ const WRITE_COMMANDS = { DECLINE_JOIN_REQUEST: 'DeclineJoinRequest', CREATE_POLICY_TAX: 'CreatePolicyTax', SET_POLICY_TAXES_ENABLED: 'SetPolicyTaxesEnabled', + DELETE_POLICY_TAXES: 'DeletePolicyTaxes', } as const; type WriteCommand = ValueOf; @@ -346,6 +347,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.DECLINE_JOIN_REQUEST]: Parameters.DeclineJoinRequestParams; [WRITE_COMMANDS.CREATE_POLICY_TAX]: Parameters.CreatePolicyTaxParams; [WRITE_COMMANDS.SET_POLICY_TAXES_ENABLED]: Parameters.SetPolicyTaxesEnabledParams; + [WRITE_COMMANDS.DELETE_POLICY_TAXES]: Parameters.DeletePolicyTaxesParams; }; const READ_COMMANDS = { diff --git a/src/libs/actions/TaxRate.ts b/src/libs/actions/TaxRate.ts index 3c1f777f315e..1bc1f3460af1 100644 --- a/src/libs/actions/TaxRate.ts +++ b/src/libs/actions/TaxRate.ts @@ -1,13 +1,13 @@ import type {OnyxCollection} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; -import type {CreatePolicyTaxParams, SetPolicyTaxesEnabledParams} from '@libs/API/parameters'; +import type {CreatePolicyTaxParams, DeletePolicyTaxesParams, SetPolicyTaxesEnabledParams} from '@libs/API/parameters'; import {WRITE_COMMANDS} from '@libs/API/types'; import CONST from '@src/CONST'; import * as ErrorUtils from '@src/libs/ErrorUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Policy, TaxRate} from '@src/types/onyx'; -import type {PendingAction} from '@src/types/onyx/OnyxCommon'; +import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import type {OnyxData} from '@src/types/onyx/Request'; let allPolicies: OnyxCollection; @@ -99,7 +99,7 @@ function createWorkspaceTax(policyID: string, taxRate: TaxRate) { API.write(WRITE_COMMANDS.CREATE_POLICY_TAX, parameters, onyxData); } -function clearTaxRateError(policyID: string, taxID: string, pendingAction?: PendingAction) { +function clearTaxRateError(policyID: string, taxID: string, pendingAction?: OnyxCommon.PendingAction) { if (pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD) { Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { taxRates: { @@ -175,9 +175,80 @@ function setPolicyTaxesEnabled(policyID: string, taxesIDsToUpdate: string[], isE taxFields: JSON.stringify(taxesIDsToUpdate.map((taxID) => ({taxCode: taxID, enabled: isEnabled}))), } satisfies SetPolicyTaxesEnabledParams; - console.log({parameters}); - API.write(WRITE_COMMANDS.SET_POLICY_TAXES_ENABLED, parameters, onyxData); } -export {createWorkspaceTax, clearTaxRateError, getNextTaxID, getTaxValueWithPercentage, setPolicyTaxesEnabled}; +type TaxRateDeleteMap = Record< + string, + | (Pick & { + errors: OnyxCommon.Errors | null; + }) + | null +>; + +/** + * API call to delete policy taxes + * @param taxesToDelete A tax IDs array to delete + */ +function deletePolicyTaxes(policyID: string, taxesToDelete: string[]) { + const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; + const policyTaxRates = policy?.taxRates?.taxes; + + if (!policyTaxRates) { + throw new Error('Policy or tax rates not found'); + } + + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + taxRates: { + taxes: taxesToDelete.reduce((acc, taxID) => { + acc[taxID] = {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, errors: null}; + return acc; + }, {} as TaxRateDeleteMap), + }, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + taxRates: { + taxes: taxesToDelete.reduce((acc, taxID) => { + acc[taxID] = null; + return acc; + }, {} as TaxRateDeleteMap), + }, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + taxRates: { + taxes: taxesToDelete.reduce((acc, taxID) => { + acc[taxID] = {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, errors: ErrorUtils.getMicroSecondOnyxError('workspace.taxes.genericFailureMessage')}; + return acc; + }, {} as TaxRateDeleteMap), + }, + }, + }, + ], + }; + + const parameters = { + policyID, + taxCodes: JSON.stringify(taxesToDelete.map((taxID) => policyTaxRates[taxID].name)), + } as DeletePolicyTaxesParams; + + API.write(WRITE_COMMANDS.DELETE_POLICY_TAXES, parameters, onyxData); +} + +export {createWorkspaceTax, clearTaxRateError, getNextTaxID, getTaxValueWithPercentage, setPolicyTaxesEnabled, deletePolicyTaxes}; diff --git a/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx b/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx index 22706f22faf1..26e36e7f9b38 100644 --- a/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx @@ -1,6 +1,7 @@ import type {StackScreenProps} from '@react-navigation/stack'; -import React, {useMemo} from 'react'; +import React, {useMemo, useState} from 'react'; import {View} from 'react-native'; +import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; @@ -10,7 +11,8 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import {setPolicyTaxesEnabled} from '@libs/actions/TaxRate'; +import {deletePolicyTaxes, setPolicyTaxesEnabled} from '@libs/actions/TaxRate'; +import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; @@ -29,6 +31,7 @@ function WorkspaceEditTaxPage({ const {translate} = useLocalize(); const currentTaxRate = PolicyUtils.getTaxByID(policy, taxID); const {windowWidth} = useWindowDimensions(); + const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); const toggle = () => { // TODO: Backend call doesn't exist yet @@ -39,12 +42,21 @@ function WorkspaceEditTaxPage({ setPolicyTaxesEnabled(policy.id, [taxID], !currentTaxRate?.isDisabled); }; + const deleteTax = () => { + if (!policy?.id) { + return; + } + deletePolicyTaxes(policy?.id, [taxID]); + setIsDeleteModalVisible(false); + Navigation.goBack(); + }; + const threeDotsMenuItems = useMemo(() => { const menuItems = [ { icon: Expensicons.Trashcan, text: translate('common.delete'), - onSelected: () => {}, + onSelected: () => setIsDeleteModalVisible(true), }, ]; return menuItems; @@ -96,6 +108,16 @@ function WorkspaceEditTaxPage({ />
+ setIsDeleteModalVisible(false)} + prompt={translate('workspace.taxes.deleteTaxConfirmation')} + confirmText={translate('common.delete')} + cancelText={translate('common.cancel')} + danger + /> ); } From c7bcfa84f2bb2bf8b66f50cbff4981473e463801 Mon Sep 17 00:00:00 2001 From: Michal Muzyk Date: Wed, 13 Mar 2024 13:14:57 +0100 Subject: [PATCH 248/484] fix: after rebase --- .../Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts | 2 +- src/libs/actions/Policy.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts index 905c62e5f812..8ca27c6dbf27 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -13,7 +13,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { ], [SCREENS.WORKSPACE.TAGS]: [SCREENS.WORKSPACE.TAGS_SETTINGS, SCREENS.WORKSPACE.TAGS_EDIT, SCREENS.WORKSPACE.TAG_CREATE], [SCREENS.WORKSPACE.CATEGORIES]: [SCREENS.WORKSPACE.CATEGORY_CREATE, SCREENS.WORKSPACE.CATEGORY_SETTINGS, SCREENS.WORKSPACE.CATEGORIES_SETTINGS], - [SCREENS.WORKSPACE.DISTANCE_RATES]: [SCREENS.WORKSPACE.CREATE_DISTANCE_RATE, SCREENS.WORKSPACE.DISTANCE_RATES_SETTINGS], + [SCREENS.WORKSPACE.DISTANCE_RATES]: [SCREENS.WORKSPACE.CREATE_DISTANCE_RATE, SCREENS.WORKSPACE.DISTANCE_RATES_SETTINGS, SCREENS.WORKSPACE.DISTANCE_RATE_EDIT], }; export default FULL_SCREEN_TO_RHP_MAPPING; diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index 51f63057cad0..5cfaf50a95a7 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -3714,7 +3714,7 @@ function updatePolicyDistanceRateValue(policyID: string, customUnit: CustomUnit, [customUnit.customUnitID]: { rates: currentRates, }, - errors: ErrorUtils.getMicroSecondOnyxError('workspace.distanceRates.errors.updateRateValueGenericFailureMessage'), + errors: ErrorUtils.getMicroSecondOnyxError('common.genericErrorMessage'), }, }, }, @@ -3790,7 +3790,7 @@ function setPolicyDistanceRatesEnabled(policyID: string, customUnit: CustomUnit, [customUnit.customUnitID]: { rates: currentRates, }, - errors: ErrorUtils.getMicroSecondOnyxError('workspace.distanceRates.errors.toggleRateEnabledGenericFailureMessage'), + errors: ErrorUtils.getMicroSecondOnyxError('common.genericErrorMessage'), }, }, }, @@ -3867,7 +3867,7 @@ function deletePolicyDistanceRates(policyID: string, rateIDsToDelete: string[], [customUnit.customUnitID]: { rates: currentRates, }, - errors: ErrorUtils.getMicroSecondOnyxError('workspace.distanceRates.errors.deleteRateGenericFailureMessage'), + errors: ErrorUtils.getMicroSecondOnyxError('common.genericErrorMessage'), }, }, }, From 4ac45359e77f6307f6c268d53e563d2bbf248188 Mon Sep 17 00:00:00 2001 From: Michal Muzyk Date: Wed, 13 Mar 2024 14:16:11 +0100 Subject: [PATCH 249/484] feat: api methods --- .../SetPolicyDistanceRatesEnabledParams.ts | 4 ++- .../UpdatePolicyDistanceRateValueParams.ts | 4 ++- src/libs/actions/Policy.ts | 18 ++--------- .../PolicyDistanceRateEditPage.tsx | 2 +- .../distanceRates/PolicyDistanceRatesPage.tsx | 30 +++++++++++++++++-- 5 files changed, 37 insertions(+), 21 deletions(-) diff --git a/src/libs/API/parameters/SetPolicyDistanceRatesEnabledParams.ts b/src/libs/API/parameters/SetPolicyDistanceRatesEnabledParams.ts index a444bb7a1a8e..e16985d74f21 100644 --- a/src/libs/API/parameters/SetPolicyDistanceRatesEnabledParams.ts +++ b/src/libs/API/parameters/SetPolicyDistanceRatesEnabledParams.ts @@ -1,7 +1,9 @@ +import type {Rate} from '@src/types/onyx/Policy'; + type SetPolicyDistanceRatesEnabledParams = { policyID: string; customUnitID: string; - customUnitRates: string; + customUnitRates: Rate[]; }; export default SetPolicyDistanceRatesEnabledParams; diff --git a/src/libs/API/parameters/UpdatePolicyDistanceRateValueParams.ts b/src/libs/API/parameters/UpdatePolicyDistanceRateValueParams.ts index 477b6cc20c5a..0074e08349c0 100644 --- a/src/libs/API/parameters/UpdatePolicyDistanceRateValueParams.ts +++ b/src/libs/API/parameters/UpdatePolicyDistanceRateValueParams.ts @@ -1,7 +1,9 @@ +import type {Rate} from '@src/types/onyx/Policy'; + type UpdatePolicyDistanceRateValueParams = { policyID: string; customUnitID: string; - customUnitRates: string; + customUnitRates: Rate[]; }; export default UpdatePolicyDistanceRateValueParams; diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index 5cfaf50a95a7..2e1a883c6a2e 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -3649,10 +3649,6 @@ function setPolicyDistanceRatesDefaultCategory(policyID: string, currentCustomUn } function updatePolicyDistanceRateValue(policyID: string, customUnit: CustomUnit, customUnitRates: Rate[]) { - if (!policyID || !customUnit || !customUnitRates) { - return; - } - const currentRates = customUnit.rates; const optimisticRates: Record = {}; const successRates: Record = {}; @@ -3723,17 +3719,13 @@ function updatePolicyDistanceRateValue(policyID: string, customUnit: CustomUnit, const params: UpdatePolicyDistanceRateValueParams = { policyID, customUnitID: customUnit.customUnitID, - customUnitRates: JSON.stringify(customUnitRates), + customUnitRates, }; API.write(WRITE_COMMANDS.UPDATE_POLICY_DISTANCE_RATE_VALUE, params, {optimisticData, successData, failureData}); } function setPolicyDistanceRatesEnabled(policyID: string, customUnit: CustomUnit, customUnitRates: Rate[]) { - if (!policyID || !customUnit || !customUnitRates) { - return; - } - const currentRates = customUnit.rates; const optimisticRates: Record = {}; const successRates: Record = {}; @@ -3799,17 +3791,13 @@ function setPolicyDistanceRatesEnabled(policyID: string, customUnit: CustomUnit, const params: SetPolicyDistanceRatesEnabledParams = { policyID, customUnitID: customUnit.customUnitID, - customUnitRates: JSON.stringify(customUnitRates), + customUnitRates, }; API.write(WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_ENABLED, params, {optimisticData, successData, failureData}); } -function deletePolicyDistanceRates(policyID: string, rateIDsToDelete: string[], customUnit?: CustomUnit) { - if (!policyID || !rateIDsToDelete || !customUnit) { - return; - } - +function deletePolicyDistanceRates(policyID: string, customUnit: CustomUnit, rateIDsToDelete: string[]) { const currentRates = customUnit.rates; const optimisticRates: Record = {}; const successRates: Record = {}; diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRateEditPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRateEditPage.tsx index 443fee0484f4..43112117afe4 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRateEditPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRateEditPage.tsx @@ -64,7 +64,7 @@ function PolicyDistanceRateEditPage({policy, route}: PolicyDistanceRateEditPageP }; const deleteRate = () => { - Policy.deletePolicyDistanceRates(policyID, [rateID], customUnit); + Policy.deletePolicyDistanceRates(policyID, customUnit, [rateID]); setIsDeleteModalVisible(false); }; diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx index d8d4f0baca46..14511f765d23 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx @@ -121,8 +121,16 @@ function PolicyDistanceRatesPage({policy, route}: PolicyDistanceRatesPageProps) }; const disableRates = () => { + if (customUnit === undefined) { + return; + } + if (selectedDistanceRates.length !== Object.values(customUnitRates).length) { - // run enableWorkspaceDistanceRates for all selected rows + Policy.setPolicyDistanceRatesEnabled( + policyID, + customUnit, + selectedDistanceRates.filter((rate) => rate.enabled), + ); return; } @@ -130,12 +138,28 @@ function PolicyDistanceRatesPage({policy, route}: PolicyDistanceRatesPageProps) }; const enableRates = () => { - // run enableWorkspaceDistanceRates for all selected rows + if (customUnit === undefined) { + return; + } + + Policy.setPolicyDistanceRatesEnabled( + policyID, + customUnit, + selectedDistanceRates.filter((rate) => !rate.enabled), + ); }; const deleteRates = () => { + if (customUnit === undefined) { + return; + } + if (selectedDistanceRates.length !== Object.values(customUnitRates).length) { - // run deleteWorkspaceDistanceRates for all selected rows + Policy.deletePolicyDistanceRates( + policyID, + customUnit, + selectedDistanceRates.map((rate) => rate.customUnitRateID ?? ''), + ); return; } From 058ee33767036a5aed1c4de73a2caab9ea4ab36b Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Wed, 13 Mar 2024 15:07:37 +0000 Subject: [PATCH 250/484] refactor(typescript): resolve type error --- src/pages/iou/MoneyRequestWaypointPage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/iou/MoneyRequestWaypointPage.tsx b/src/pages/iou/MoneyRequestWaypointPage.tsx index c21aae7cf063..dd65b76c8d38 100644 --- a/src/pages/iou/MoneyRequestWaypointPage.tsx +++ b/src/pages/iou/MoneyRequestWaypointPage.tsx @@ -20,6 +20,7 @@ function MoneyRequestWaypointPage({transactionID = '', route}: MoneyRequestWaypo // Put the transactionID into the route params so that WaypointEdit behaves the same when creating a new waypoint // or editing an existing waypoint. route={{ + ...route, params: { ...route.params, transactionID, From b1f48c99c92a2ff05e727e47b84e7f04887cfc90 Mon Sep 17 00:00:00 2001 From: Michal Muzyk Date: Wed, 13 Mar 2024 16:22:14 +0100 Subject: [PATCH 251/484] fix: onyx methods --- .../API/parameters/SetPolicyDistanceRatesEnabledParams.ts | 4 +--- .../API/parameters/UpdatePolicyDistanceRateValueParams.ts | 4 +--- src/libs/actions/Policy.ts | 4 ++-- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/libs/API/parameters/SetPolicyDistanceRatesEnabledParams.ts b/src/libs/API/parameters/SetPolicyDistanceRatesEnabledParams.ts index e16985d74f21..e8ab58470dd3 100644 --- a/src/libs/API/parameters/SetPolicyDistanceRatesEnabledParams.ts +++ b/src/libs/API/parameters/SetPolicyDistanceRatesEnabledParams.ts @@ -1,9 +1,7 @@ -import type {Rate} from '@src/types/onyx/Policy'; - type SetPolicyDistanceRatesEnabledParams = { policyID: string; customUnitID: string; - customUnitRates: Rate[]; + customUnitRate: string; }; export default SetPolicyDistanceRatesEnabledParams; diff --git a/src/libs/API/parameters/UpdatePolicyDistanceRateValueParams.ts b/src/libs/API/parameters/UpdatePolicyDistanceRateValueParams.ts index 0074e08349c0..025119522fdb 100644 --- a/src/libs/API/parameters/UpdatePolicyDistanceRateValueParams.ts +++ b/src/libs/API/parameters/UpdatePolicyDistanceRateValueParams.ts @@ -1,9 +1,7 @@ -import type {Rate} from '@src/types/onyx/Policy'; - type UpdatePolicyDistanceRateValueParams = { policyID: string; customUnitID: string; - customUnitRates: Rate[]; + customUnitRate: string; }; export default UpdatePolicyDistanceRateValueParams; diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index 2e1a883c6a2e..316bb87cd825 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -3719,7 +3719,7 @@ function updatePolicyDistanceRateValue(policyID: string, customUnit: CustomUnit, const params: UpdatePolicyDistanceRateValueParams = { policyID, customUnitID: customUnit.customUnitID, - customUnitRates, + customUnitRate: JSON.stringify(customUnitRates), }; API.write(WRITE_COMMANDS.UPDATE_POLICY_DISTANCE_RATE_VALUE, params, {optimisticData, successData, failureData}); @@ -3791,7 +3791,7 @@ function setPolicyDistanceRatesEnabled(policyID: string, customUnit: CustomUnit, const params: SetPolicyDistanceRatesEnabledParams = { policyID, customUnitID: customUnit.customUnitID, - customUnitRates, + customUnitRate: JSON.stringify(customUnitRates), }; API.write(WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_ENABLED, params, {optimisticData, successData, failureData}); From c47d0e0605b519a09fe0bc71aabca7854ec7d0fc Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Wed, 13 Mar 2024 21:09:03 +0530 Subject: [PATCH 252/484] 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 253/484] 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
diff --git a/src/pages/workspace/taxes/ValuePage.tsx b/src/pages/workspace/taxes/ValuePage.tsx index 6c733968aa5e..3aa990db13c3 100644 --- a/src/pages/workspace/taxes/ValuePage.tsx +++ b/src/pages/workspace/taxes/ValuePage.tsx @@ -9,7 +9,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import {renamePolicyTax, updatePolicyTaxValue} from '@libs/actions/TaxRate'; +import {updatePolicyTaxValue} from '@libs/actions/TaxRate'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; @@ -20,7 +20,6 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import INPUT_IDS from '@src/types/form/WorkspaceTaxValueForm'; -import type * as OnyxTypes from '@src/types/onyx'; type ValuePageProps = WithPolicyAndFullscreenLoadingProps & StackScreenProps; @@ -35,6 +34,8 @@ function ValuePage({ const currentTaxRate = PolicyUtils.getTaxByID(policy, taxID); const [value, setValue] = useState(currentTaxRate?.value?.replace('%', '')); + const goBack = useCallback(() => Navigation.goBack(ROUTES.WORKSPACE_TAXES_EDIT.getRoute(policyID ?? '', taxID)), [policyID, taxID]); + // TODO: Extract it to a separate file, and use it also when creating a new tax const validate = useCallback((values: FormOnyxValues) => { const errors = {}; @@ -49,9 +50,9 @@ function ValuePage({ const submit = useCallback( (values: FormOnyxValues) => { updatePolicyTaxValue(policyID, taxID, Number(values.value)); - Navigation.goBack(ROUTES.WORKSPACE_TAXES_EDIT.getRoute(policyID ?? '', taxID)); + goBack(); }, - [policyID, taxID], + [goBack, policyID, taxID], ); return ( @@ -60,7 +61,10 @@ function ValuePage({ shouldEnableMaxHeight testID={ValuePage.displayName} > - + Date: Thu, 14 Mar 2024 12:45:10 +0100 Subject: [PATCH 267/484] update to new backend --- src/libs/API/parameters/DeletePolicyTaxesParams.ts | 2 +- src/libs/API/parameters/SetPolicyTaxesEnabledParams.ts | 2 +- src/libs/actions/TaxRate.ts | 6 ++---- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/libs/API/parameters/DeletePolicyTaxesParams.ts b/src/libs/API/parameters/DeletePolicyTaxesParams.ts index fe03d388a129..9e0963cdcb28 100644 --- a/src/libs/API/parameters/DeletePolicyTaxesParams.ts +++ b/src/libs/API/parameters/DeletePolicyTaxesParams.ts @@ -5,7 +5,7 @@ type DeletePolicyTaxesParams = { * Array * Each element is a tax name */ - taxCodes: string; + taxNames: string; }; export default DeletePolicyTaxesParams; diff --git a/src/libs/API/parameters/SetPolicyTaxesEnabledParams.ts b/src/libs/API/parameters/SetPolicyTaxesEnabledParams.ts index 0bc8550cd01b..4ed0a05cfdec 100644 --- a/src/libs/API/parameters/SetPolicyTaxesEnabledParams.ts +++ b/src/libs/API/parameters/SetPolicyTaxesEnabledParams.ts @@ -4,7 +4,7 @@ type SetPolicyTaxesEnabledParams = { * Stringified JSON object with type of following structure: * Array<{taxCode: string, enabled: bool}> */ - taxFields: string; + taxFieldsArray: string; }; export default SetPolicyTaxesEnabledParams; diff --git a/src/libs/actions/TaxRate.ts b/src/libs/actions/TaxRate.ts index 50a8ed0a1bdc..ced92e12e4b8 100644 --- a/src/libs/actions/TaxRate.ts +++ b/src/libs/actions/TaxRate.ts @@ -170,7 +170,7 @@ function setPolicyTaxesEnabled(policyID: string, taxesIDsToUpdate: string[], isE const parameters = { policyID, - taxFields: JSON.stringify(taxesIDsToUpdate.map((taxID) => ({taxCode: originalTaxes[taxID].name, enabled: isEnabled}))), + taxFieldsArray: JSON.stringify(taxesIDsToUpdate.map((taxID) => ({taxCode: originalTaxes[taxID].name, enabled: isEnabled}))), } satisfies SetPolicyTaxesEnabledParams; API.write(WRITE_COMMANDS.SET_POLICY_TAXES_ENABLED, parameters, onyxData); @@ -243,7 +243,7 @@ function deletePolicyTaxes(policyID: string, taxesToDelete: string[]) { const parameters = { policyID, - taxCodes: JSON.stringify(taxesToDelete.map((taxID) => policyTaxRates[taxID].name)), + taxNames: JSON.stringify(taxesToDelete.map((taxID) => policyTaxRates[taxID].name)), } as DeletePolicyTaxesParams; API.write(WRITE_COMMANDS.DELETE_POLICY_TAXES, parameters, onyxData); @@ -257,8 +257,6 @@ function updatePolicyTaxValue(policyID: string, taxID: string, taxValue: number) const originalTaxRate = {...policy?.taxRates?.taxes[taxID]}; const stringTaxValue = `${taxValue}%`; - console.log({policy, originalTaxRate, stringTaxValue, taxValue, taxID}); - const onyxData: OnyxData = { optimisticData: [ { From 3b590c672aa27e32ef4f76c9c272e9126a9834e5 Mon Sep 17 00:00:00 2001 From: Jakub Kosmydel <104823336+kosmydel@users.noreply.github.com> Date: Thu, 14 Mar 2024 13:07:26 +0100 Subject: [PATCH 268/484] add bulk actions --- src/CONST.ts | 5 ++ .../ButtonWithDropdownMenu/types.ts | 4 +- src/languages/en.ts | 6 +- .../workspace/taxes/WorkspaceEditTaxPage.tsx | 2 +- .../workspace/taxes/WorkspaceTaxesPage.tsx | 73 +++++++++++++++---- 5 files changed, 72 insertions(+), 18 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index cf0d6ac57a08..bb04b27dc1a2 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1422,6 +1422,11 @@ const CONST = { DISABLE: 'disable', ENABLE: 'enable', }, + TAX_RATES_BULK_ACTION_TYPES: { + DELETE: 'delete', + DISABLE: 'disable', + ENABLE: 'enable', + }, }, CUSTOM_UNITS: { diff --git a/src/components/ButtonWithDropdownMenu/types.ts b/src/components/ButtonWithDropdownMenu/types.ts index 798369292958..83100788761f 100644 --- a/src/components/ButtonWithDropdownMenu/types.ts +++ b/src/components/ButtonWithDropdownMenu/types.ts @@ -12,6 +12,8 @@ type WorkspaceMemberBulkActionType = DeepValueOf; +type WorkspaceTaxRatesBulkActionType = DeepValueOf; + type DropdownOption = { value: TValueType; text: string; @@ -73,4 +75,4 @@ type ButtonWithDropdownMenuProps = { wrapperStyle?: StyleProp; }; -export type {PaymentType, WorkspaceMemberBulkActionType, WorkspaceDistanceRatesBulkActionType, DropdownOption, ButtonWithDropdownMenuProps}; +export type {PaymentType, WorkspaceMemberBulkActionType, WorkspaceDistanceRatesBulkActionType, DropdownOption, ButtonWithDropdownMenuProps, WorkspaceTaxRatesBulkActionType}; diff --git a/src/languages/en.ts b/src/languages/en.ts index d60861a838be..876399e9a864 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1860,8 +1860,12 @@ export default { valuePercentageRange: 'Please enter a valid percentage between 0 and 100', genericFailureMessage: 'An error occurred while updating the tax rate, please try again.', }, - deleteTax: 'Delete tax', deleteTaxConfirmation: 'Are you sure you want to delete this tax?', + actions: { + delete: 'Delete rate', + disable: 'Disable rate', + enable: 'Enable rate', + }, }, emptyWorkspace: { title: 'Create a workspace', diff --git a/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx b/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx index 2617b710f55c..e785790d64e4 100644 --- a/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceEditTaxPage.tsx @@ -108,7 +108,7 @@ function WorkspaceEditTaxPage({
setIsDeleteModalVisible(false)} diff --git a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx index cede2bd31e7d..04529014b2ac 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx @@ -2,6 +2,8 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {ActivityIndicator, View} from 'react-native'; import Button from '@components/Button'; +import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; +import type {DropdownOption, WorkspaceTaxRatesBulkActionType} from '@components/ButtonWithDropdownMenu/types'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -129,23 +131,64 @@ function WorkspaceTaxesPage({policy, route}: WorkspaceTaxesPageProps) {
); + const dropdownMenuOptions = useMemo(() => { + const options: Array> = [ + { + icon: Expensicons.Trashcan, + text: translate('workspace.taxes.actions.delete'), + value: CONST.POLICY.TAX_RATES_BULK_ACTION_TYPES.DELETE, + onSelected: () => {}, + }, + ]; + + // `Disable rates` when at least one enabled rate is selected. + if (selectedTaxesIDs.some((taxID) => !policy?.taxRates?.taxes[taxID]?.isDisabled)) { + options.push({ + icon: Expensicons.Document, + text: translate('workspace.taxes.actions.disable'), + value: CONST.POLICY.TAX_RATES_BULK_ACTION_TYPES.DISABLE, + }); + } + + // `Enable rates` when at least one disabled rate is selected. + if (selectedTaxesIDs.some((taxID) => policy?.taxRates?.taxes[taxID]?.isDisabled)) { + options.push({ + icon: Expensicons.Document, + text: translate('workspace.taxes.actions.enable'), + value: CONST.POLICY.TAX_RATES_BULK_ACTION_TYPES.ENABLE, + }); + } + return options; + }, [policy?.taxRates?.taxes, selectedTaxesIDs, translate]); + const headerButtons = ( -