From 5258cff5cf967d8fa06b3b29d29195a63d64b0f6 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 29 Sep 2023 18:50:57 +0200 Subject: [PATCH 0001/1208] update openReport --- src/components/ReportActionItem/MoneyRequestAction.js | 4 ++-- src/libs/actions/Report.js | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestAction.js b/src/components/ReportActionItem/MoneyRequestAction.js index b4a5e010b7a8..5ed47e4a244d 100644 --- a/src/components/ReportActionItem/MoneyRequestAction.js +++ b/src/components/ReportActionItem/MoneyRequestAction.js @@ -107,11 +107,11 @@ function MoneyRequestAction({ if (!childReportID) { const thread = ReportUtils.buildTransactionThread(action, requestReportID); const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs); - Report.openReport(thread.reportID, userLogins, thread, action.reportActionID); + Report.openReport({reportID: thread.reportID}, userLogins, thread, action.reportActionID); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(thread.reportID)); return; } - Report.openReport(childReportID); + Report.openReport({reportID: childReportID}); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID)); }; diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index a6e115fe5d9c..808bec8faee4 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -443,13 +443,14 @@ function reportActionsExist(reportID) { * If a chat with the passed reportID is not found, we will create a chat based on the passed participantList * * @param {String} reportID + * @param {String} reportActionID * @param {Array} participantLoginList The list of users that are included in a new chat, not including the user creating it * @param {Object} newReportObject The optimistic report object created when making a new chat, saved as optimistic data * @param {String} parentReportActionID The parent report action that a thread was created from (only passed for new threads) * @param {Boolean} isFromDeepLink Whether or not this report is being opened from a deep link * @param {Array} participantAccountIDList The list of accountIDs that are included in a new chat, not including the user creating it */ -function openReport(reportID, participantLoginList = [], newReportObject = {}, parentReportActionID = '0', isFromDeepLink = false, participantAccountIDList = []) { +function openReport({reportID, reportActionID = ''}, participantLoginList = [], newReportObject = {}, parentReportActionID = '0', isFromDeepLink = false, participantAccountIDList = []) { const optimisticReportData = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -512,6 +513,7 @@ function openReport(reportID, participantLoginList = [], newReportObject = {}, p const params = { reportID, + reportActionID, emailList: participantLoginList ? participantLoginList.join(',') : '', accountIDList: participantAccountIDList ? participantAccountIDList.join(',') : '', parentReportActionID, From 5cd7e4c36cb1951901dcdf1fb16b8d907466a17a Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 29 Sep 2023 18:52:25 +0200 Subject: [PATCH 0002/1208] update the rest openReport calls --- src/pages/home/report/ContextMenu/ContextMenuActions.js | 4 ++-- .../home/report/withReportAndReportActionOrNotFound.js | 2 +- tests/actions/IOUTest.js | 8 ++++---- tests/actions/ReportTest.js | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.js index 157ae66dc918..e19927bfb33b 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.js +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.js @@ -287,11 +287,11 @@ export default [ if (!childReportID) { const thread = ReportUtils.buildTransactionThread(reportAction, reportID); const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs); - Report.openReport(thread.reportID, userLogins, thread, reportAction.reportActionID); + Report.openReport({reportID: thread.reportID}, userLogins, thread, reportAction.reportActionID); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(thread.reportID)); return; } - Report.openReport(childReportID); + Report.openReport({reportID: childReportID}); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID)); return; } diff --git a/src/pages/home/report/withReportAndReportActionOrNotFound.js b/src/pages/home/report/withReportAndReportActionOrNotFound.js index 47f499423d28..19bbf5b36e32 100644 --- a/src/pages/home/report/withReportAndReportActionOrNotFound.js +++ b/src/pages/home/report/withReportAndReportActionOrNotFound.js @@ -97,7 +97,7 @@ export default function (WrappedComponent) { if (!props.isSmallScreenWidth || (!_.isEmpty(props.report) && !_.isEmpty(reportAction))) { return; } - Report.openReport(props.route.params.reportID); + Report.openReport({reportID: props.route.params.reportID}); // eslint-disable-next-line react-hooks/exhaustive-deps }, [props.isSmallScreenWidth, props.route.params.reportID]); diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index 3df3b137bab3..ef613c26020a 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -1714,7 +1714,7 @@ describe('actions/IOU', () => { const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs); // When Opening a thread report with the given details - Report.openReport(thread.reportID, userLogins, thread, createIOUAction.reportActionID); + Report.openReport({reportID: thread.reportID}, userLogins, thread, createIOUAction.reportActionID); await waitForBatchedUpdates(); // Then The iou action has the transaction report id as a child report ID @@ -1780,7 +1780,7 @@ describe('actions/IOU', () => { thread = ReportUtils.buildTransactionThread(createIOUAction); const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs); jest.advanceTimersByTime(10); - Report.openReport(thread.reportID, userLogins, thread, createIOUAction.reportActionID); + Report.openReport({reportID: thread.reportID}, userLogins, thread, createIOUAction.reportActionID); await waitForBatchedUpdates(); Onyx.connect({ @@ -1870,7 +1870,7 @@ describe('actions/IOU', () => { jest.advanceTimersByTime(10); const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs); - Report.openReport(thread.reportID, userLogins, thread, createIOUAction.reportActionID); + Report.openReport({reportID: thread.reportID}, userLogins, thread, createIOUAction.reportActionID); await waitForBatchedUpdates(); @@ -2083,7 +2083,7 @@ describe('actions/IOU', () => { jest.advanceTimersByTime(10); const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs); - Report.openReport(thread.reportID, userLogins, thread, createIOUAction.reportActionID); + Report.openReport({reportID: thread.reportID}, userLogins, thread, createIOUAction.reportActionID); await waitForBatchedUpdates(); const allReportActions = await new Promise((resolve) => { diff --git a/tests/actions/ReportTest.js b/tests/actions/ReportTest.js index 06d8304111cb..1968b4983e70 100644 --- a/tests/actions/ReportTest.js +++ b/tests/actions/ReportTest.js @@ -267,7 +267,7 @@ describe('actions/Report', () => { // When the user visits the report jest.advanceTimersByTime(10); currentTime = DateUtils.getDBTime(); - Report.openReport(REPORT_ID); + Report.openReport({reportID: REPORT_ID}); Report.readNewestAction(REPORT_ID); waitForBatchedUpdates(); return waitForBatchedUpdates(); From 65edb743531fa9c662388afd6f5db486375080a8 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Sat, 30 Sep 2023 10:18:59 +0200 Subject: [PATCH 0003/1208] WIP linking utils --- src/libs/ReportActionsUtils.js | 160 +++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js index 67c44784eeb2..29cd6cd9cc54 100644 --- a/src/libs/ReportActionsUtils.js +++ b/src/libs/ReportActionsUtils.js @@ -212,6 +212,163 @@ function getSortedReportActions(reportActions, shouldSortInDescendingOrder = fal .value(); } +// /** +// * Given an object of reportActions, sorts them, and then adds the previousReportActionID to each item except the first. +// * @param {Object} reportActions +// * @returns {Array} +// */ +// function processReportActions(reportActions) { +// // TODO: +// // Separate new and sorted reportActions +// const newReportActions = _.filter(reportActions, (action) => !action.previousReportActionID); +// const sortedReportActions = _.filter(reportActions, (action) => action.previousReportActionID); + +// // console.log( +// // 'getChat.Sort.N.0', +// // newReportActions.length, +// // newReportActions.map(({message}) => message[0].text), +// // ); +// // console.log( +// // 'getChat.Sort.N.0.0', +// // newReportActions.length, +// // newReportActions.map(({previousReportActionID}) => previousReportActionID), +// // ); +// // console.log( +// // 'getChat.Sort.S.0', +// // sortedReportActions.length, +// // sortedReportActions.map(({message}) => message[0].text), +// // ); +// // console.log( +// // 'getChat.Sort.S.0.0', +// // sortedReportActions.length, +// // sortedReportActions.map(({previousReportActionID}) => previousReportActionID), +// // ); +// // Sort the new reportActions +// const sortedNewReportActions = getSortedReportActionsForDisplay(newReportActions); + +// // console.log( +// // 'getChat.SORT.SS', +// // JSON.stringify( +// // sortedNewReportActions.map(({message, reportActionID, previousReportActionID}) => ({ +// // message: message[0].text, +// // reportActionID, +// // previousReportActionID, +// // })), +// // ), +// // ); + +// // Then, iterate through the sorted new reportActions and add the previousReportActionID to each item except the first +// const processedReportActions = sortedNewReportActions.map((action, index) => { +// if (index === sortedNewReportActions.length - 1) { +// return action; // Return the first item as is +// } +// return { +// ...action, +// previousReportActionID: sortedNewReportActions[index + 1].reportActionID, +// }; +// }); + +// // console.log( +// // 'getChat.SORT.BEFORE', +// // JSON.stringify( +// // processedReportActions.map(({message, reportActionID, previousReportActionID}) => ({ +// // message: message[0].text, +// // reportActionID, +// // previousReportActionID, +// // })), +// // ), +// // ); +// if (processedReportActions[processedReportActions.length - 1]?.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED) { +// processedReportActions.pop(); +// } +// // console.log( +// // 'getChat.SORT.AFTER', +// // JSON.stringify( +// // processedReportActions.map(({message, reportActionID, previousReportActionID}) => ({ +// // message: message[0].text, +// // reportActionID, +// // previousReportActionID, +// // })), +// // ), +// // ); + +// // Determine the order of merging based on reportActionID values +// const lastSortedReportActionID = _.last(sortedReportActions)?.reportActionTimestamp || 0; +// const firstProcessedReportActionID = _.first(processedReportActions)?.reportActionTimestamp || Infinity; + +// // console.log('getChat.Sort.1', getSortedReportActionsForDisplay(reportActions).length, [...sortedReportActions, ...processedReportActions].length); +// // console.log('getChat.Sort.1.1', _.last(sortedReportActions), _.first(processedReportActions)); +// if (firstProcessedReportActionID > lastSortedReportActionID) { +// // console.log( +// // 'getChat.Sort.2', +// // [...sortedReportActions, ...processedReportActions].map(({message}) => message[0].text), +// // ); +// return [...sortedReportActions, ...processedReportActions]; +// } else { +// // console.log( +// // 'getChat.Sort.3', +// // [...processedReportActions, ...sortedReportActions].map(({message}) => message[0].text), +// // ); +// return [...processedReportActions, ...sortedReportActions]; +// } +// } + +// Usage: +// const reportActions = [ +// { reportActionID: '1' }, +// { reportActionID: '2', previousReportActionID: '1' }, +// { reportActionID: '3' } +// ]; +// const updatedActions = processReportActions(reportActions); +// console.log(updatedActions); + +function getRangeFromArrayByID(array, id) { + // without gaps + let index; + + if (id) { + index = array.findIndex((obj) => obj.reportActionID === id); + } else { + index = 0; + } + + if (index === -1) { + return []; + } + + let startIndex = index; + let endIndex = index; + + // Move down the list and compare reportActionID with previousReportActionID + while (endIndex < array.length - 1 && array[endIndex].previousReportActionID === array[endIndex + 1].reportActionID) { + endIndex++; + } + + // Move up the list and compare previousReportActionID with reportActionID + while (startIndex > 0 && array[startIndex].reportActionID === array[startIndex - 1].previousReportActionID) { + startIndex--; + } + + return array.slice(startIndex, endIndex + 1); + // return array.slice(startIndex, endIndex); +} + +function getSlicedRangeFromArrayByID(array, id) { + let index; + if (id) { + index = array.findIndex((obj) => obj.reportActionID === id); + } else { + index = array.length - 1; + } + + if (index === -1) { + return []; + } + + // return array.slice(0, index+1); + return array.slice(index, array.length); +} + /** * Finds most recent IOU request action ID. * @@ -696,4 +853,7 @@ export { getAllReportActions, isReportActionAttachment, isNotifiableReportAction, + // processReportActions, + getRangeFromArrayByID, + getSlicedRangeFromArrayByID, }; From 03804dbf5e3a4ef33c1496ec185cdf69f5e3e697 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 2 Oct 2023 16:22:58 +0200 Subject: [PATCH 0004/1208] WIP navigation, useRouteChangeHandler --- src/libs/ReportUtils.js | 26 +++++ src/pages/home/ReportScreen.js | 105 +++++++++++++++-- src/pages/home/report/ReportActionsView.js | 125 ++++++++++++++++++--- 3 files changed, 232 insertions(+), 24 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index c03858cb15f3..2be9e68ae451 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1,4 +1,5 @@ /* eslint-disable rulesdir/prefer-underscore-method */ +import {useEffect, useRef} from 'react'; import _ from 'underscore'; import {format, parseISO} from 'date-fns'; import Str from 'expensify-common/lib/str'; @@ -3654,6 +3655,30 @@ function getIOUReportActionDisplayMessage(reportAction) { return displayMessage; } +/** + * Hook that triggers a fetch when reportActionID appears while reportID stays the same. + * + * @param {string} reportID - The current reportID from the route. + * @param {string|null} reportActionID - The current reportActionID from the route or null. + * @param {function} triggerFetch - The function to be triggered on the condition. + */ +function useRouteChangeHandler(reportID = '', reportActionID = '', triggerFetch = () => {}) { + // Store the previous reportID and reportActionID + const previousReportIDRef = useRef(null); + const previousReportActionIDRef = useRef(null); + + useEffect(() => { + // Check if reportID is the same and reportActionID just appeared + if (reportID === previousReportIDRef.current && reportActionID && !previousReportActionIDRef.current) { + triggerFetch(); + } + + // Update refs for the next render + previousReportIDRef.current = reportID; + previousReportActionIDRef.current = reportActionID; + }, [reportID, reportActionID, triggerFetch]); +} + export { getReportParticipantsTitle, isReportMessageAttachment, @@ -3795,4 +3820,5 @@ export { hasMissingSmartscanFields, getIOUReportActionDisplayMessage, isWaitingForTaskCompleteFromAssignee, + useRouteChangeHandler, }; diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index aedc9247a21f..29cc51fab73a 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -1,3 +1,4 @@ +/* eslint-disable rulesdir/prefer-underscore-method */ import React, {useRef, useState, useEffect, useMemo, useCallback} from 'react'; import {withOnyx} from 'react-native-onyx'; import {useFocusEffect} from '@react-navigation/native'; @@ -63,7 +64,7 @@ const propTypes = { reportMetadata: reportMetadataPropTypes, /** Array of report actions for this report */ - reportActions: PropTypes.arrayOf(PropTypes.shape(reportActionPropTypes)), + sortedReportActions: PropTypes.arrayOf(PropTypes.shape(reportActionPropTypes)), /** Whether the composer is full size */ isComposerFullSize: PropTypes.bool, @@ -100,7 +101,7 @@ const propTypes = { const defaultProps = { isSidebarLoaded: false, - reportActions: [], + sortedReportActions: [], report: { hasOutstandingIOU: false, }, @@ -139,13 +140,26 @@ const checkDefaultReport = (report) => report === defaultProps.report; function getReportID(route) { return String(lodashGet(route, 'params.reportID', null)); } +/** + * Get the currently viewed report ID as number + * + * @param {Object} route + * @param {Object} route.params + * @param {String} route.params.reportID + * @returns {String} + */ +function getReportActionID(route) { + console.log('get.ROUTE', route?.params); + return {reportActionID: lodashGet(route, 'params.reportActionID', null), reportID: lodashGet(route, 'params.reportID', null)}; +} function ReportScreen({ betas, route, report, reportMetadata, - reportActions, + // reportActions, + sortedReportActions, accountManagerReportID, personalDetails, markReadyForHydration, @@ -156,6 +170,7 @@ function ReportScreen({ errors, userLeavingStatus, currentReportID, + updateCurrentReportID, }) { const {translate} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); @@ -165,9 +180,53 @@ function ReportScreen({ const reactionListRef = useRef(); const prevReport = usePrevious(report); const prevUserLeavingStatus = usePrevious(userLeavingStatus); + const {reportActionID, reportID} = getReportActionID(route); + const [isLinkingToMessage, setLinkingToMessageTrigger] = useState(false); + + const reportActions = useMemo(() => { + const val = ReportActionsUtils.getRangeFromArrayByID(sortedReportActions, reportActionID); + console.log('get.ROOT.reportActions', reportActionID, sortedReportActions.length, val.length); + return val; + }, [sortedReportActions, reportActionID]); + // const isReportActionArrayCatted = useMemo(() => sortedReportActions.length !== withoutGaps.length, [withoutGaps, sortedReportActions]); + + // const reportActions = useMemo(() => { + // //TODO: OpenReport, it means that we clicked on the link in current chat and we need to get a proper range of reportActions + // // console.log( + // // 'get.reportActions.initialSorted', + // // // sortedReportActions.length, + // // sortedReportActions.length, + // // !!reportActionID, + // // sortedReportActions.map((item) => { + // // return {message: item.message[0].text, previousReportActionID: item?.previousReportActionID, reportActionID: item?.reportActionID}; + // // }), + // // ); + // // // // console.log('get.reportActions.initialSorted', sortedReportActions.map((item) =>item?.previousReportActionID)); + // // console.log( + // // 'get.reportActions.withoutGaps', + // // // withoutGaps.length, + // // withoutGaps.length, + // // !!reportActionID, + // // withoutGaps.map((item) => { + // // return {message: item.message[0].text, previousReportActionID: item?.previousReportActionID, reportActionID: item?.reportActionID}; + // // }), + // // ); + // return withoutGaps; + // // return sortedReportActions; + // }, [sortedReportActions, reportActionID]); + + // const reportActionsBeforeAndIncludingLinked = useMemo(() => { + // if (reportActionID) { + // firstRenderRefI.current = false; + // return ReportActionsUtils.getSlicedRangeFromArrayByID(reportActions, reportActionID); + // } + // return null; + // }, [reportActions, reportActionID]); + + // const [skeletonViewContainerHeight, setSkeletonViewContainerHeight] = useState(0); + // >>>>>>> Stashed changes const [isBannerVisible, setIsBannerVisible] = useState(true); - const reportID = getReportID(route); const {addWorkspaceRoomOrChatPendingAction, addWorkspaceRoomOrChatErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report); const screenWrapperStyle = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}]; @@ -237,6 +296,8 @@ function ReportScreen({ return reportIDFromPath !== '' && report.reportID && !isTransitioning; }, [route, report]); + // ReportUtils.useRouteChangeHandler(reportID, reportActionID, () => Report.openReport({reportID: reportIDFromPath, reportActionID: reportActionID || ''})) + const fetchReportIfNeeded = useCallback(() => { const reportIDFromPath = getReportID(route); @@ -249,11 +310,26 @@ function ReportScreen({ // It possible that we may not have the report object yet in Onyx yet e.g. we navigated to a URL for an accessible report that // is not stored locally yet. If report.reportID exists, then the report has been stored locally and nothing more needs to be done. // If it doesn't exist, then we fetch the report from the API. - if (report.reportID && report.reportID === getReportID(route)) { + if (report.reportID && report.reportID === getReportID(route) && !reportActionID) { return; } - Report.openReport(reportIDFromPath); - }, [report.reportID, route]); + console.log('getChat.OPEN_REPORT', reportActionID); + Report.openReport({reportID: reportIDFromPath, reportActionID: reportActionID || ''}); + }, [report.reportID, route, reportActionID]); + + // useEffect(() => { + // console.log('get.ROUTE.0', route); + // const {reportActionID} = getReportActionID(route); + // if (!reportActionID) return; + // fetchReportIfNeeded(); + // console.log('get.ROUTE.+++++++++', route); + // setLinkingToMessageTrigger(true); + // }, [route, fetchReportIfNeeded]); + // ReportUtils.useRouteChangeHandler(reportID, reportActionID, () => { + // console.log('getChat.OPEN_REPORT.000', reportActionID); + // fetchReportIfNeeded(); + // // updateCurrentReportID(getReportID(route)); + // }); const dismissBanner = useCallback(() => { setIsBannerVisible(false); @@ -283,7 +359,7 @@ function ReportScreen({ return; } - Report.openReport(report.reportID); + Report.openReport({reportID: report.reportID}); }); return () => unsubscribeVisibilityListener(); @@ -426,10 +502,15 @@ function ReportScreen({ style={[styles.flex1, styles.justifyContentEnd, styles.overflowHidden]} onLayout={onListLayout} > - {isReportReadyForDisplay && !isFirstlyLoadingReportActions && !isLoading && ( + {/* {isReportReadyForDisplay && !isFirstlyLoadingReportActions && !isLoading && ( */} + {isReportReadyForDisplay && !isLoading && ( `${ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM}${getReportID(route)}`, initialValue: false, }, + sortedReportActions: { + key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getReportID(route)}`, + canEvict: false, + selector: ReportActionsUtils.getSortedReportActionsForDisplay, + // selector: ReportActionsUtils.processReportActions, + }, }, true, ), diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index a0dee8abdb71..88e1dcef3e91 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -1,8 +1,8 @@ -import React, {useRef, useEffect, useContext, useMemo} from 'react'; +import React, {useRef, useEffect, useContext, useMemo, useState} from 'react'; import PropTypes from 'prop-types'; import _ from 'underscore'; import lodashGet from 'lodash/get'; -import {useIsFocused} from '@react-navigation/native'; +import {useIsFocused, useRoute} from '@react-navigation/native'; import * as Report from '../../../libs/actions/Report'; import reportActionPropTypes from './reportActionPropTypes'; import Timing from '../../../libs/actions/Timing'; @@ -65,15 +65,98 @@ const defaultProps = { isLoadingNewerReportActions: false, }; -function ReportActionsView(props) { +/** + * Get the currently viewed report ID as number + * + * @param {Object} route + * @param {Object} route.params + * @param {String} route.params.reportID + * @returns {String} + */ +function getReportActionID(route) { + return {reportActionID: lodashGet(route, 'params.reportActionID', null), reportID: lodashGet(route, 'params.reportID', null)}; +} + +function ReportActionsView({reportActions: allReportActions, ...props}) { useCopySelectionHelper(); const reactionListRef = useContext(ReactionListContext); + const route = useRoute(); + const {reportActionID} = getReportActionID(route); + // const reportActionID = '' + + // const [canShowAllReports, setShowAllReports] = useState(false); + const testRef = useRef(false); const didLayout = useRef(false); + // const isFirstRender = useRef(true); const didSubscribeToReportTypingEvents = useRef(false); - const isFetchNewerWasCalled = useRef(false); - const hasCachedActions = useRef(_.size(props.reportActions) > 0); + const [isFetchNewerWasCalled, setFetchNewerWasCalled] = useState(false); + const [isLinkingToMessage, setLinkingToMessageTrigger] = useState(false); + + const reportActionsBeforeAndIncludingLinked = useMemo(() => { + if (reportActionID && allReportActions?.length) { + return ReportActionsUtils.getSlicedRangeFromArrayByID(allReportActions, reportActionID); + } + return []; + }, [allReportActions, reportActionID]); + + const reportActions = useMemo(() => { + console.log( + 'get.reportActions.info|||', + '| reportActionID:', + reportActionID, + '| isFetchNewerWasCalled:', + isFetchNewerWasCalled, + '| isLinkingToMessage:', + isLinkingToMessage, + '| testRef.current:', + testRef.current, + '| reportActionsBeforeAndIncludingLinked:', + reportActionsBeforeAndIncludingLinked?.length, + '| allReportActions:', + allReportActions?.length, + ); + + if (!reportActionID || (!testRef.current && !isLinkingToMessage && !props.isLoadingInitialReportActions && isFetchNewerWasCalled)) { + console.log('get.reportActions.ALL||||', allReportActions.length); + return allReportActions; + } + console.log( + 'get.reportActions.CUT||||', + reportActionsBeforeAndIncludingLinked[0]?.message?.text || reportActionsBeforeAndIncludingLinked[0]?.message + ); + return reportActionsBeforeAndIncludingLinked; + }, [isFetchNewerWasCalled, allReportActions, reportActionsBeforeAndIncludingLinked, reportActionID, isLinkingToMessage, props.isLoadingInitialReportActions]); - const mostRecentIOUReportActionID = useRef(ReportActionsUtils.getMostRecentIOURequestActionID(props.reportActions)); + useEffect(() => { + console.log('get.ROUTE_CHANGED.triggered', route); + if (!reportActionID) { + return; + } + testRef.current = true; + setLinkingToMessageTrigger(true); + props.fetchReportIfNeeded(); + console.log('get.ROUTE_CHANGED.+++++++++'); + setTimeout(() => { + testRef.current = false; + setLinkingToMessageTrigger(false); + console.log('get.ROUTE_CHANGED.+++++++++FINISH', route); + }, 7000); + // setLinkingToMessageTrigger(true); + }, [route, props.fetchReportIfNeeded, reportActionID]); + + const isReportActionArrayCatted = useMemo(() => { + if (reportActions?.length !== allReportActions?.length && reportActionID) { + console.log('get.isReportActionArrayCatted.+++++++++'); + return true; + } + + console.log('get.isReportActionArrayCatted.----------'); + return false; + }, [reportActions, allReportActions, reportActionID, isFetchNewerWasCalled]); + + const hasCachedActions = useRef(_.size(reportActions) > 0); + + const mostRecentIOUReportActionID = useRef(ReportActionsUtils.getMostRecentIOURequestActionID(reportActions)); const prevNetworkRef = useRef(props.network); const prevIsSmallScreenWidthRef = useRef(props.isSmallScreenWidth); @@ -90,13 +173,13 @@ function ReportActionsView(props) { if (props.report.isOptimisticReport) { return; } - - Report.openReport(reportID); + Report.openReport({reportID, reportActionID}); }; useEffect(() => { openReportIfNecessary(); // eslint-disable-next-line react-hooks/exhaustive-deps + // isFirstRender.current = false; }, []); useEffect(() => { @@ -129,7 +212,7 @@ function ReportActionsView(props) { // update ref with current state prevIsSmallScreenWidthRef.current = props.isSmallScreenWidth; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props.isSmallScreenWidth, props.report, props.reportActions, isReportFullyVisible]); + }, [props.isSmallScreenWidth, props.report, reportActions, isReportFullyVisible]); useEffect(() => { // Ensures subscription event succeeds when the report/workspace room is created optimistically. @@ -153,7 +236,7 @@ function ReportActionsView(props) { return; } - const oldestReportAction = _.last(props.reportActions); + const oldestReportAction = _.last(reportActions); // Don't load more chats if we're already at the beginning of the chat history if (oldestReportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) { @@ -161,6 +244,7 @@ function ReportActionsView(props) { } // Retrieve the next REPORT.ACTIONS.LIMIT sized page of comments Report.getOlderActions(reportID, oldestReportAction.reportActionID); + setShowAllReports(true); }; /** @@ -182,11 +266,15 @@ function ReportActionsView(props) { // Additionally, we use throttling on the 'onStartReached' callback to further reduce the frequency of its invocation. // This should be removed once the issue of frequent re-renders is resolved. - if (!isFetchNewerWasCalled.current || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { - isFetchNewerWasCalled.current = true; + if (isReportActionArrayCatted || reportActionID) { + setFetchNewerWasCalled(true); return; } - const newestReportAction = _.first(props.reportActions); + if (!isFetchNewerWasCalled || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { + return; + } + + const newestReportAction = reportActions[0]; Report.getNewerActions(reportID, newestReportAction.reportActionID); }, 700); @@ -211,7 +299,7 @@ function ReportActionsView(props) { }; // Comments have not loaded at all yet do nothing - if (!_.size(props.reportActions)) { + if (!_.size(reportActions)) { return null; } @@ -220,7 +308,7 @@ function ReportActionsView(props) { Date: Tue, 3 Oct 2023 15:25:21 +0200 Subject: [PATCH 0005/1208] WIP linking --- src/libs/ReportActionsUtils.js | 3 +- src/pages/home/report/ReportActionsList.js | 10 +++++- src/pages/home/report/ReportActionsView.js | 37 ++++++---------------- 3 files changed, 21 insertions(+), 29 deletions(-) diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js index cc9efcf6c9cb..4665575f263f 100644 --- a/src/libs/ReportActionsUtils.js +++ b/src/libs/ReportActionsUtils.js @@ -355,7 +355,8 @@ function getSlicedRangeFromArrayByID(array, id) { } // return array.slice(0, index+1); - return array.slice(index, array.length); + // return array.slice(index, array.length); + return array.slice(index, array.length - index > 50 ? index + 50 : array.length); } /** diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 31e48dc0e46b..38f3ab5e6763 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -68,6 +68,7 @@ const defaultProps = { isLoadingInitialReportActions: false, isLoadingOlderReportActions: false, isLoadingNewerReportActions: false, + isLinkingLoader: false, ...withCurrentUserPersonalDetailsDefaultProps, }; @@ -117,6 +118,7 @@ function ReportActionsList({ loadOlderChats, onLayout, isComposerFullSize, + isLinkingLoader, }) { const reportScrollManager = useReportScrollManager(); const {translate} = useLocalize(); @@ -328,7 +330,7 @@ function ReportActionsList({ const shouldShowReportRecipientLocalTime = ReportUtils.canShowReportRecipientLocalTime(personalDetailsList, report, currentUserPersonalDetails.accountID) && !isComposerFullSize; const contentContainerStyle = useMemo( - () => [styles.chatContentScrollView, isLoadingNewerReportActions ? styles.chatContentScrollViewWithHeaderLoader : {}], + () => [styles.chatContentScrollView, isLoadingNewerReportActions ? styles.chatContentScrollViewWithHeaderLoader : {}], [isLoadingNewerReportActions], ); @@ -368,6 +370,12 @@ function ReportActionsList({ ); }, [isLoadingNewerReportActions]); + useEffect(() => { + if (!isLinkingLoader) { + return; + } + reportScrollManager.scrollToBottom(); + }, [isLinkingLoader, reportScrollManager]); return ( <> { - console.log('get.ROUTE_CHANGED.triggered', route); if (!reportActionID) { return; } - testRef.current = true; + setFetchNewerWasCalled(false); setLinkingToMessageTrigger(true); props.fetchReportIfNeeded(); - console.log('get.ROUTE_CHANGED.+++++++++'); setTimeout(() => { - testRef.current = false; setLinkingToMessageTrigger(false); - console.log('get.ROUTE_CHANGED.+++++++++FINISH', route); - }, 7000); + }, 700); // setLinkingToMessageTrigger(true); }, [route, props.fetchReportIfNeeded, reportActionID]); const isReportActionArrayCatted = useMemo(() => { if (reportActions?.length !== allReportActions?.length && reportActionID) { - console.log('get.isReportActionArrayCatted.+++++++++'); return true; } - - console.log('get.isReportActionArrayCatted.----------'); return false; - }, [reportActions, allReportActions, reportActionID, isFetchNewerWasCalled]); + }, [reportActions, allReportActions, reportActionID]); const hasCachedActions = useRef(_.size(reportActions) > 0); @@ -244,7 +229,6 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { } // Retrieve the next REPORT.ACTIONS.LIMIT sized page of comments Report.getOlderActions(reportID, oldestReportAction.reportActionID); - setShowAllReports(true); }; /** @@ -312,6 +296,8 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { mostRecentIOUReportActionID={mostRecentIOUReportActionID.current} loadOlderChats={loadOlderChats} loadNewerChats={loadNewerChats} + isLinkingLoader={!!reportActionID && props.isLoadingInitialReportActions} + isReportActionArrayCatted={isReportActionArrayCatted} isLoadingInitialReportActions={props.isLoadingInitialReportActions} isLoadingOlderReportActions={props.isLoadingOlderReportActions} isLoadingNewerReportActions={props.isLoadingNewerReportActions} @@ -351,12 +337,9 @@ function arePropsEqual(oldProps, newProps) { return false; } - // if (oldProps.isLinkingToMessage !== newProps.isLinkingToMessage) { - // return false; - // } - // if (oldisReportActionArrayCatted !== newisReportActionArrayCatted) { - // return false; - // } + if (oldProps.isLoadingNewerReportActions !== newProps.isLoadingNewerReportActions) { + return false; + } if (oldProps.report.lastReadTime !== newProps.report.lastReadTime) { return false; From 241a2994fd542a2ea3c930931465c313b042f70b Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 5 Oct 2023 16:26:13 +0200 Subject: [PATCH 0006/1208] still WIP --- src/pages/home/ReportScreen.js | 72 +++------------------- src/pages/home/report/ReportActionsView.js | 30 +++------ 2 files changed, 15 insertions(+), 87 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index a01f95e4f4fa..ddccb6d5fafe 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -149,7 +149,6 @@ function ReportScreen({ route, report, reportMetadata, - // reportActions, sortedReportActions, accountManagerReportID, personalDetails, @@ -176,46 +175,8 @@ function ReportScreen({ const reportActions = useMemo(() => { const val = ReportActionsUtils.getRangeFromArrayByID(sortedReportActions, reportActionID); - console.log('get.ROOT.reportActions', reportActionID, sortedReportActions.length, val.length); return val; }, [sortedReportActions, reportActionID]); - // const isReportActionArrayCatted = useMemo(() => sortedReportActions.length !== withoutGaps.length, [withoutGaps, sortedReportActions]); - - // const reportActions = useMemo(() => { - // //TODO: OpenReport, it means that we clicked on the link in current chat and we need to get a proper range of reportActions - // // console.log( - // // 'get.reportActions.initialSorted', - // // // sortedReportActions.length, - // // sortedReportActions.length, - // // !!reportActionID, - // // sortedReportActions.map((item) => { - // // return {message: item.message[0].text, previousReportActionID: item?.previousReportActionID, reportActionID: item?.reportActionID}; - // // }), - // // ); - // // // // console.log('get.reportActions.initialSorted', sortedReportActions.map((item) =>item?.previousReportActionID)); - // // console.log( - // // 'get.reportActions.withoutGaps', - // // // withoutGaps.length, - // // withoutGaps.length, - // // !!reportActionID, - // // withoutGaps.map((item) => { - // // return {message: item.message[0].text, previousReportActionID: item?.previousReportActionID, reportActionID: item?.reportActionID}; - // // }), - // // ); - // return withoutGaps; - // // return sortedReportActions; - // }, [sortedReportActions, reportActionID]); - - // const reportActionsBeforeAndIncludingLinked = useMemo(() => { - // if (reportActionID) { - // firstRenderRefI.current = false; - // return ReportActionsUtils.getSlicedRangeFromArrayByID(reportActions, reportActionID); - // } - // return null; - // }, [reportActions, reportActionID]); - - // const [skeletonViewContainerHeight, setSkeletonViewContainerHeight] = useState(0); - // >>>>>>> Stashed changes const [isBannerVisible, setIsBannerVisible] = useState(true); const {addWorkspaceRoomOrChatPendingAction, addWorkspaceRoomOrChatErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report); @@ -285,7 +246,9 @@ function ReportScreen({ return reportIDFromPath !== '' && report.reportID && !isTransitioning; }, [route, report]); - // ReportUtils.useRouteChangeHandler(reportID, reportActionID, () => Report.openReport({reportID: reportIDFromPath, reportActionID: reportActionID || ''})) + const fetchReport = useCallback(() => { + Report.openReport({reportID, reportActionID: reportActionID || ''}); + }, [reportID, reportActionID]); const fetchReportIfNeeded = useCallback(() => { const reportIDFromPath = getReportID(route); @@ -299,28 +262,12 @@ function ReportScreen({ // It possible that we may not have the report object yet in Onyx yet e.g. we navigated to a URL for an accessible report that // is not stored locally yet. If report.reportID exists, then the report has been stored locally and nothing more needs to be done. // If it doesn't exist, then we fetch the report from the API. - - // useEffect(() => { - // console.log('get.ROUTE.0', route); - // const {reportActionID} = getReportActionID(route); - // if (!reportActionID) return; - // fetchReportIfNeeded(); - // console.log('get.ROUTE.+++++++++', route); - // setLinkingToMessageTrigger(true); - // }, [route, fetchReportIfNeeded]); - // ReportUtils.useRouteChangeHandler(reportID, reportActionID, () => { - // console.log('getChat.OPEN_REPORT.000', reportActionID); - // fetchReportIfNeeded(); - // // updateCurrentReportID(getReportID(route)); - // }); - - // if (report.reportID && report.reportID === getReportID(route) && !reportActionID) { if (report.reportID && report.reportID === getReportID(route) && !reportMetadata.isLoadingInitialReportActions) { return; } - Report.openReport({reportID: reportIDFromPath, reportActionID: reportActionID || ''}); - }, [report.reportID, route, reportMetadata.isLoadingInitialReportActions]); + fetchReport(); + }, [report.reportID, route, reportMetadata.isLoadingInitialReportActions, fetchReport]); const dismissBanner = useCallback(() => { setIsBannerVisible(false); @@ -432,12 +379,7 @@ function ReportScreen({ // eslint-disable-next-line rulesdir/no-negated-variables const shouldShowNotFoundPage = useMemo( - () => (!firstRenderRef.current && - !report.reportID && - !isOptimisticDelete && - !reportMetadata.isLoadingInitialReportActions && - !isLoading && - !userLeavingStatus) || shouldHideReport, + () => (!firstRenderRef.current && !report.reportID && !isOptimisticDelete && !reportMetadata.isLoadingInitialReportActions && !isLoading && !userLeavingStatus) || shouldHideReport, [report, reportMetadata, isLoading, shouldHideReport, isOptimisticDelete, userLeavingStatus], ); @@ -496,7 +438,7 @@ function ReportScreen({ report={report} isLinkingToMessage={isLinkingToMessage} setLinkingToMessageTrigger={setLinkingToMessageTrigger} - fetchReportIfNeeded={fetchReportIfNeeded} + fetchReport={fetchReport} reportActionID={reportActionID} isLoadingInitialReportActions={reportMetadata.isLoadingInitialReportActions} isLoadingNewerReportActions={reportMetadata.isLoadingNewerReportActions} diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 3cfda35cd66c..c3ca835d7812 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -20,7 +20,6 @@ import reportPropTypes from '../../reportPropTypes'; import PopoverReactionList from './ReactionList/PopoverReactionList'; import getIsReportFullyVisible from '../../../libs/getIsReportFullyVisible'; import {ReactionListContext} from '../ReportScreenContext'; -import useReportScrollManager from '../../../hooks/useReportScrollManager'; const propTypes = { /** The report currently being looked at */ @@ -83,7 +82,6 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { const reactionListRef = useContext(ReactionListContext); const route = useRoute(); const {reportActionID} = getReportActionID(route); - const testRef = useRef(false); const didLayout = useRef(false); const didSubscribeToReportTypingEvents = useRef(false); const [isFetchNewerWasCalled, setFetchNewerWasCalled] = useState(false); @@ -97,22 +95,6 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { }, [allReportActions, reportActionID]); const reportActions = useMemo(() => { - console.log( - 'get.reportActions.info|||', - '| reportActionID:', - reportActionID, - '| isFetchNewerWasCalled:', - isFetchNewerWasCalled, - '| isLinkingToMessage:', - isLinkingToMessage, - '| testRef.current:', - testRef.current, - '| reportActionsBeforeAndIncludingLinked:', - reportActionsBeforeAndIncludingLinked?.length, - '| allReportActions:', - allReportActions?.length, - ); - if (!reportActionID || (!isLinkingToMessage && !props.isLoadingInitialReportActions && isFetchNewerWasCalled)) { return allReportActions; } @@ -124,13 +106,14 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { return; } setFetchNewerWasCalled(false); + setFetchNewerWasCalled(false); setLinkingToMessageTrigger(true); - props.fetchReportIfNeeded(); + props.fetchReport(); setTimeout(() => { setLinkingToMessageTrigger(false); - }, 700); + }, 300); // setLinkingToMessageTrigger(true); - }, [route, props.fetchReportIfNeeded, reportActionID]); + }, [route, reportActionID]); const isReportActionArrayCatted = useMemo(() => { if (reportActions?.length !== allReportActions?.length && reportActionID) { @@ -250,7 +233,7 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { // Additionally, we use throttling on the 'onStartReached' callback to further reduce the frequency of its invocation. // This should be removed once the issue of frequent re-renders is resolved. - if (isReportActionArrayCatted || reportActionID) { + if (!isFetchNewerWasCalled) { setFetchNewerWasCalled(true); return; } @@ -260,6 +243,9 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { const newestReportAction = reportActions[0]; Report.getNewerActions(reportID, newestReportAction.reportActionID); + // Report.getNewerActions(reportID, '2420805078232802130'); + // Report.getNewerActions(reportID, '569204055949619736'); + // Report.getNewerActions(reportID, '1134531619271003224'); }, 500); /** From 7ca764369daa8cb53154eb0b9d4d7be3250ea92d Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 9 Oct 2023 16:25:44 +0200 Subject: [PATCH 0007/1208] remove timer --- src/pages/home/report/ReportActionsView.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 8c0f5eb77c57..046c062d70be 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -108,9 +108,14 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { setFetchNewerWasCalled(false); setLinkingToMessageTrigger(true); props.fetchReport(); - setTimeout(() => { + const timeoutId = setTimeout(() => { setLinkingToMessageTrigger(false); - }, 300); + }, 500); + + // Return a cleanup function + return () => { + clearTimeout(timeoutId); + }; // setLinkingToMessageTrigger(true); }, [route, reportActionID]); @@ -233,8 +238,6 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { // This should be removed once the issue of frequent re-renders is resolved. if (!isFetchNewerWasCalled) { setFetchNewerWasCalled(true); - // if (!isFetchNewerWasCalled.current || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { - // isFetchNewerWasCalled.current = true; return; } if (!isFetchNewerWasCalled || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { From 8ce9ca0e65027ae31cc172d2150b314c6bd90ff0 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 9 Oct 2023 17:55:07 +0200 Subject: [PATCH 0008/1208] scroll to --- src/pages/home/ReportScreen.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index df85fcca52f0..4a29a05421fc 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -281,11 +281,17 @@ function ReportScreen({ * @param {String} text */ const onSubmitComment = useCallback( - (text) => { - Report.addComment(getReportID(route), text); - }, - [route], - ); + (text) => { + Report.addComment(getReportID(route), text); + // we need to scroll to the bottom of the list after the comment was added + const refID = setTimeout(() => { + flatListRef.current.scrollToOffset({animated: false, offset: 0}); + }, 10); + + return () => clearTimeout(refID); + }, + [route], + ); useFocusEffect( useCallback(() => { From 8efb8fd896b8d6347b334945acea3afa9bba22e1 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 10 Oct 2023 12:00:15 +0200 Subject: [PATCH 0009/1208] fix getSlicedRangeFromArrayByID --- src/libs/ReportActionsUtils.js | 58 ++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js index 373732172992..3d13394c133c 100644 --- a/src/libs/ReportActionsUtils.js +++ b/src/libs/ReportActionsUtils.js @@ -311,6 +311,14 @@ function getSortedReportActions(reportActions, shouldSortInDescendingOrder = fal // const updatedActions = processReportActions(reportActions); // console.log(updatedActions); +/** + * Returns the range of report actions from the given array which include current id + * the range is consistent + * + * @param {Array} array + * @param {String} id + * @returns {Array} + */ function getRangeFromArrayByID(array, id) { // without gaps let index; @@ -342,23 +350,45 @@ function getRangeFromArrayByID(array, id) { // return array.slice(startIndex, endIndex); } -function getSlicedRangeFromArrayByID(array, id) { - let index; - if (id) { - index = array.findIndex((obj) => obj.reportActionID === id); - } else { - index = array.length - 1; - } - - if (index === -1) { - return []; - } - // return array.slice(0, index+1); - // return array.slice(index, array.length); - return array.slice(index, array.length - index > 50 ? index + 50 : array.length); +/** + * Returns the sliced range of report actions from the given array. + * + * @param {Array} array + * @param {String} id + * @returns {Object} + * getSlicedRangeFromArrayByID([{id:1}, ..., {id: 100}], 50) => { catted: [{id:1}, ..., {id: 50}], expanded: [{id: 45}, ..., {id: 55}] } + */ +function getSlicedRangeFromArrayByID(array, id) { + let index; + if (id) { + index = array.findIndex((obj) => obj.reportActionID === id); + } else { + index = array.length - 1; + } + + if (index === -1) { + return { catted: [], expanded: [] }; + } + + const cattedEnd = array.length - index > 50 ? index + 50 : array.length; + const expandedStart = Math.max(0, index - 5); + + const catted = []; + const expanded = []; + + for (let i = expandedStart; i < cattedEnd; i++) { + if (i >= index) { + catted.push(array[i]); + } + expanded.push(array[i]); + } + // We need the expanded version to prevent jittering of list. So when user navigate to linked message we show to him the catted version. After that we show the expanded version. + // Then we can show all reports. + return { catted, expanded }; } + /** * Finds most recent IOU request action ID. * From cfda4c07ff3830e989ed99ed7a122212b7637106 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 10 Oct 2023 12:39:58 +0200 Subject: [PATCH 0010/1208] update cutting method --- src/pages/home/report/ReportActionsView.js | 79 ++++++++++++++-------- 1 file changed, 49 insertions(+), 30 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index f8ed94618f70..38174a867b80 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -77,47 +77,74 @@ function getReportActionID(route) { return {reportActionID: lodashGet(route, 'params.reportActionID', null), reportID: lodashGet(route, 'params.reportID', null)}; } -function ReportActionsView({reportActions: allReportActions, ...props}) { +function ReportActionsView({reportActions: allReportActions, fetchReport, ...props}) { useCopySelectionHelper(); const reactionListRef = useContext(ReactionListContext); const route = useRoute(); const {reportActionID} = getReportActionID(route); const didLayout = useRef(false); const didSubscribeToReportTypingEvents = useRef(false); - const [isFetchNewerWasCalled, setFetchNewerWasCalled] = useState(false); - const [isLinkingToMessage, setLinkingToMessageTrigger] = useState(false); + const [isLinkingToCattedMessage, setLinkingToCattedMessage] = useState(false); + const [isLinkingToExtendedMessage, setLinkingToExtendedMessage] = useState(false); + const isLoadingLinkedMessage = !!reportActionID && props.isLoadingInitialReportActions; - const reportActionsBeforeAndIncludingLinked = useMemo(() => { + useEffect(() => { + let timeoutIdCatted; + let timeoutIdExtended; + if (!isLoadingLinkedMessage) { + timeoutIdCatted = setTimeout(() => { + setLinkingToCattedMessage(false); + }, 100); + timeoutIdExtended = setTimeout(() => { + setLinkingToExtendedMessage(false); + }, 200); + } + return () => { + clearTimeout(timeoutIdCatted); + clearTimeout(timeoutIdExtended); + }; + }, [isLoadingLinkedMessage]); + + const {catted: reportActionsBeforeAndIncludingLinked, expanded: reportActionsBeforeAndIncludingLinkedExpanded} = useMemo(() => { if (reportActionID && allReportActions?.length) { return ReportActionsUtils.getSlicedRangeFromArrayByID(allReportActions, reportActionID); } - return []; + // catted means the reportActions before and including the linked message + // expanded means the reportActions before and including the linked message plus the next 5 + return {catted: [], expanded: []}; }, [allReportActions, reportActionID]); const reportActions = useMemo(() => { - if (!reportActionID || (!isLinkingToMessage && !props.isLoadingInitialReportActions && isFetchNewerWasCalled)) { + if (!reportActionID || (!isLinkingToCattedMessage && !isLoadingLinkedMessage && !isLinkingToExtendedMessage)) { return allReportActions; } + if ( + reportActionID && + !isLinkingToCattedMessage && + isLinkingToExtendedMessage && + reportActionsBeforeAndIncludingLinkedExpanded.length !== reportActionsBeforeAndIncludingLinked.length + ) { + return reportActionsBeforeAndIncludingLinkedExpanded; + } return reportActionsBeforeAndIncludingLinked; - }, [isFetchNewerWasCalled, allReportActions, reportActionsBeforeAndIncludingLinked, reportActionID, isLinkingToMessage, props.isLoadingInitialReportActions]); + }, [ + allReportActions, + reportActionsBeforeAndIncludingLinked, + reportActionID, + isLinkingToCattedMessage, + isLoadingLinkedMessage, + isLinkingToExtendedMessage, + reportActionsBeforeAndIncludingLinkedExpanded, + ]); useEffect(() => { if (!reportActionID) { return; } - setFetchNewerWasCalled(false); - setLinkingToMessageTrigger(true); - props.fetchReport(); - const timeoutId = setTimeout(() => { - setLinkingToMessageTrigger(false); - }, 500); - - // Return a cleanup function - return () => { - clearTimeout(timeoutId); - }; - // setLinkingToMessageTrigger(true); - }, [route, reportActionID]); + setLinkingToCattedMessage(true); + setLinkingToExtendedMessage(true); + fetchReport(); + }, [route, reportActionID, fetchReport]); const isReportActionArrayCatted = useMemo(() => { if (reportActions?.length !== allReportActions?.length && reportActionID) { @@ -237,26 +264,18 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { // // Additionally, we use throttling on the 'onStartReached' callback to further reduce the frequency of its invocation. // This should be removed once the issue of frequent re-renders is resolved. - if (!isFetchNewerWasCalled) { - setFetchNewerWasCalled(true); - return; - } - if (!isFetchNewerWasCalled || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { + if (!isLinkingToExtendedMessage || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { return; } const newestReportAction = reportActions[0]; Report.getNewerActions(reportID, newestReportAction.reportActionID); - // if (!isFetchNewerWasCalled.current || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { - // isFetchNewerWasCalled.current = true; - // return; - // } // const newestReportAction = _.first(props.reportActions); // Report.getNewerActions(reportID, newestReportAction.reportActionID); // Report.getNewerActions(reportID, '1134531619271003224'); }, 500), - [props.isLoadingNewerReportActions, props.isLoadingInitialReportActions, props.reportActions, reportID], + [props.isLoadingNewerReportActions, props.isLoadingInitialReportActions, isLinkingToExtendedMessage, reportID, reportActions], ); /** From 16c4231733589e88d66e0d98606aacdfe971f8e3 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 11 Oct 2023 18:16:50 +0200 Subject: [PATCH 0011/1208] remove comments --- src/libs/ReportActionsUtils.js | 75 +--------------------- src/libs/ReportUtils.js | 24 ------- src/pages/home/report/ReportActionsView.js | 13 +--- 3 files changed, 4 insertions(+), 108 deletions(-) diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js index 3d13394c133c..363ea4567f40 100644 --- a/src/libs/ReportActionsUtils.js +++ b/src/libs/ReportActionsUtils.js @@ -206,45 +206,14 @@ function getSortedReportActions(reportActions, shouldSortInDescendingOrder = fal // * @param {Object} reportActions // * @returns {Array} // */ -// function processReportActions(reportActions) { -// // TODO: +// function processReportActions(reportActions) { //TODO: remove after previousReportActionID is stable // // Separate new and sorted reportActions // const newReportActions = _.filter(reportActions, (action) => !action.previousReportActionID); // const sortedReportActions = _.filter(reportActions, (action) => action.previousReportActionID); -// // console.log( -// // 'getChat.Sort.N.0', -// // newReportActions.length, -// // newReportActions.map(({message}) => message[0].text), -// // ); -// // console.log( -// // 'getChat.Sort.N.0.0', -// // newReportActions.length, -// // newReportActions.map(({previousReportActionID}) => previousReportActionID), -// // ); -// // console.log( -// // 'getChat.Sort.S.0', -// // sortedReportActions.length, -// // sortedReportActions.map(({message}) => message[0].text), -// // ); -// // console.log( -// // 'getChat.Sort.S.0.0', -// // sortedReportActions.length, -// // sortedReportActions.map(({previousReportActionID}) => previousReportActionID), -// // ); // // Sort the new reportActions // const sortedNewReportActions = getSortedReportActionsForDisplay(newReportActions); -// // console.log( -// // 'getChat.SORT.SS', -// // JSON.stringify( -// // sortedNewReportActions.map(({message, reportActionID, previousReportActionID}) => ({ -// // message: message[0].text, -// // reportActionID, -// // previousReportActionID, -// // })), -// // ), -// // ); // // Then, iterate through the sorted new reportActions and add the previousReportActionID to each item except the first // const processedReportActions = sortedNewReportActions.map((action, index) => { @@ -257,59 +226,22 @@ function getSortedReportActions(reportActions, shouldSortInDescendingOrder = fal // }; // }); -// // console.log( -// // 'getChat.SORT.BEFORE', -// // JSON.stringify( -// // processedReportActions.map(({message, reportActionID, previousReportActionID}) => ({ -// // message: message[0].text, -// // reportActionID, -// // previousReportActionID, -// // })), -// // ), -// // ); // if (processedReportActions[processedReportActions.length - 1]?.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED) { // processedReportActions.pop(); // } -// // console.log( -// // 'getChat.SORT.AFTER', -// // JSON.stringify( -// // processedReportActions.map(({message, reportActionID, previousReportActionID}) => ({ -// // message: message[0].text, -// // reportActionID, -// // previousReportActionID, -// // })), -// // ), -// // ); // // Determine the order of merging based on reportActionID values // const lastSortedReportActionID = _.last(sortedReportActions)?.reportActionTimestamp || 0; // const firstProcessedReportActionID = _.first(processedReportActions)?.reportActionTimestamp || Infinity; -// // console.log('getChat.Sort.1', getSortedReportActionsForDisplay(reportActions).length, [...sortedReportActions, ...processedReportActions].length); -// // console.log('getChat.Sort.1.1', _.last(sortedReportActions), _.first(processedReportActions)); // if (firstProcessedReportActionID > lastSortedReportActionID) { -// // console.log( -// // 'getChat.Sort.2', -// // [...sortedReportActions, ...processedReportActions].map(({message}) => message[0].text), -// // ); // return [...sortedReportActions, ...processedReportActions]; // } else { -// // console.log( -// // 'getChat.Sort.3', -// // [...processedReportActions, ...sortedReportActions].map(({message}) => message[0].text), -// // ); // return [...processedReportActions, ...sortedReportActions]; // } // } -// Usage: -// const reportActions = [ -// { reportActionID: '1' }, -// { reportActionID: '2', previousReportActionID: '1' }, -// { reportActionID: '3' } -// ]; -// const updatedActions = processReportActions(reportActions); -// console.log(updatedActions); + /** * Returns the range of report actions from the given array which include current id @@ -320,7 +252,6 @@ function getSortedReportActions(reportActions, shouldSortInDescendingOrder = fal * @returns {Array} */ function getRangeFromArrayByID(array, id) { - // without gaps let index; if (id) { @@ -347,7 +278,6 @@ function getRangeFromArrayByID(array, id) { } return array.slice(startIndex, endIndex + 1); - // return array.slice(startIndex, endIndex); } @@ -874,7 +804,6 @@ export { getAllReportActions, isReportActionAttachment, isNotifiableReportAction, - // processReportActions, getRangeFromArrayByID, getSlicedRangeFromArrayByID, }; diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 79f052bd21b4..4a4876132220 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -3807,29 +3807,6 @@ function getIOUReportActionDisplayMessage(reportAction) { return displayMessage; } -/** - * Hook that triggers a fetch when reportActionID appears while reportID stays the same. - * - * @param {string} reportID - The current reportID from the route. - * @param {string|null} reportActionID - The current reportActionID from the route or null. - * @param {function} triggerFetch - The function to be triggered on the condition. - */ -function useRouteChangeHandler(reportID = '', reportActionID = '', triggerFetch = () => {}) { - // Store the previous reportID and reportActionID - const previousReportIDRef = useRef(null); - const previousReportActionIDRef = useRef(null); - - useEffect(() => { - // Check if reportID is the same and reportActionID just appeared - if (reportID === previousReportIDRef.current && reportActionID && !previousReportActionIDRef.current) { - triggerFetch(); - } - - // Update refs for the next render - previousReportIDRef.current = reportID; - previousReportActionIDRef.current = reportActionID; - }, [reportID, reportActionID, triggerFetch]); - } /** * @param {Object} report * @returns {Boolean} @@ -3982,6 +3959,5 @@ export { hasMissingSmartscanFields, getIOUReportActionDisplayMessage, isWaitingForTaskCompleteFromAssignee, - useRouteChangeHandler, isReportDraft, }; diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 06e0b4d865fc..eaa4d9bf3a24 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -84,13 +84,11 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro const {reportActionID} = getReportActionID(route); const didLayout = useRef(false); const didSubscribeToReportTypingEvents = useRef(false); - + const isFirstRender = useRef(true); const [isLinkingToCattedMessage, setLinkingToCattedMessage] = useState(false); const [isLinkingToExtendedMessage, setLinkingToExtendedMessage] = useState(false); const isLoadingLinkedMessage = !!reportActionID && props.isLoadingInitialReportActions; - const isFirstRender = useRef(true); //TODO: - // const hasCachedActions = useRef(_.size(props.reportActions) > 0); //TODO: useEffect(() => { @@ -183,7 +181,6 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro useEffect(() => { openReportIfNecessary(); // eslint-disable-next-line react-hooks/exhaustive-deps - // isFirstRender.current = false; }, []); useEffect(() => { @@ -269,21 +266,15 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro // // Additionally, we use throttling on the 'onStartReached' callback to further reduce the frequency of its invocation. // This should be removed once the issue of frequent re-renders is resolved. - - // if (!isLinkingToExtendedMessage || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { // TODO: // // onStartReached is triggered during the first render. Since we use OpenReport on the first render and are confident about the message ordering, we can safely skip this call - if (isFirstRender.current || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { + if (isFirstRender.current || isLinkingToExtendedMessage || distanceFromStart <= CONST.CHAT_HEADER_LOADER_HEIGHT) { isFirstRender.current = false; return; } const newestReportAction = reportActions[0]; Report.getNewerActions(reportID, newestReportAction.reportActionID); - - // const newestReportAction = _.first(props.reportActions); - // Report.getNewerActions(reportID, newestReportAction.reportActionID); - // Report.getNewerActions(reportID, '1134531619271003224'); }, 500), [props.isLoadingNewerReportActions, props.isLoadingInitialReportActions, isLinkingToExtendedMessage, reportID, reportActions], ); From adb4162f36d17c3eec13e3cf44d2a77a75769a1a Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 24 Oct 2023 13:43:48 +0200 Subject: [PATCH 0012/1208] include deleted message in getRangeFromArrayByID --- src/libs/ReportActionsUtils.ts | 16 +++++++++++---- src/pages/home/ReportScreen.js | 36 ++++++++++++++++++---------------- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index b329281a3078..071505d1067e 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -19,8 +19,8 @@ type LastVisibleMessage = { }; type SlicedResult = { - catted: ReportAction[]; - expanded: ReportAction[]; + catted: ReportAction[]; + expanded: ReportAction[]; }; const allReports: OnyxCollection = {}; @@ -218,7 +218,7 @@ function getSortedReportActions(reportActions: ReportAction[] | null, shouldSort * param {String} id * returns {ReportAction} */ -function getRangeFromArrayByID(array: ReportAction[], id: string): ReportAction[] { +function getRangeFromArrayByID(array: ReportAction[], id?: string): ReportAction[] { let index; if (id) { @@ -543,12 +543,19 @@ function filterOutDeprecatedReportActions(reportActions: ReportActions | null): */ function getSortedReportActionsForDisplay(reportActions: ReportActions | null): ReportAction[] { const filteredReportActions = Object.entries(reportActions ?? {}) - .filter(([key, reportAction]) => shouldReportActionBeVisible(reportAction, key)) + // .filter(([key, reportAction]) => shouldReportActionBeVisible(reportAction, key)) .map((entry) => entry[1]); const baseURLAdjustedReportActions = filteredReportActions.map((reportAction) => replaceBaseURL(reportAction)); return getSortedReportActions(baseURLAdjustedReportActions, true); } +function getReportActionsWithoutRemoved(reportActions: ReportAction[] | null): ReportAction[] { + if (!reportActions) { + return []; + } + return reportActions.filter((item) => shouldReportActionBeVisible(item, item.reportActionID)); +} + /** * In some cases, there can be multiple closed report actions in a chat report. * This method returns the last closed report action so we can always show the correct archived report reason. @@ -734,6 +741,7 @@ export { getReportPreviewAction, getSortedReportActions, getSortedReportActionsForDisplay, + getReportActionsWithoutRemoved, isConsecutiveActionMadeByPreviousActor, isCreatedAction, isCreatedTaskReportAction, diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index c1acb0d3de40..0a73be16297c 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -150,7 +150,8 @@ function ReportScreen({ route, report, reportMetadata, - sortedReportActions, + // sortedReportActions, + allReportActions, accountManagerReportID, personalDetails, markReadyForHydration, @@ -175,9 +176,12 @@ function ReportScreen({ const [isLinkingToMessage, setLinkingToMessageTrigger] = useState(false); const reportActions = useMemo(() => { - const val = ReportActionsUtils.getRangeFromArrayByID(sortedReportActions, reportActionID); - return val; - }, [sortedReportActions, reportActionID]); + if (allReportActions?.length === 0) return []; + const sorterReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions); + const cattedRangeOfReportActions = ReportActionsUtils.getRangeFromArrayByID(sorterReportActions, reportActionID); + const reportActionsWithoutDeleted = ReportActionsUtils.getReportActionsWithoutRemoved(cattedRangeOfReportActions); + return reportActionsWithoutDeleted; + }, [reportActionID, allReportActions]); const [isBannerVisible, setIsBannerVisible] = useState(true); const [listHeight, setListHeight] = useState(0); @@ -289,10 +293,10 @@ function ReportScreen({ flatListRef.current.scrollToOffset({animated: false, offset: 0}); }, 10); - return () => clearTimeout(refID); - }, - [route], - ); + return () => clearTimeout(refID); + }, + [route], + ); useFocusEffect( useCallback(() => { @@ -455,7 +459,6 @@ function ReportScreen({ onLayout={onListLayout} > {isReportReadyForDisplay && !isLoading && ( - `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getReportID(route)}`, canEvict: false, - selector: ReportActionsUtils.getSortedReportActionsForDisplay, }, report: { key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${getReportID(route)}`, @@ -550,12 +552,12 @@ export default compose( key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM}${getReportID(route)}`, initialValue: false, }, - sortedReportActions: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getReportID(route)}`, - canEvict: false, - selector: ReportActionsUtils.getSortedReportActionsForDisplay, - // selector: ReportActionsUtils.processReportActions, - }, + // sortedReportActions: { + // key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getReportID(route)}`, + // canEvict: false, + // selector: ReportActionsUtils.getSortedReportActionsForDisplay, + // // selector: ReportActionsUtils.processReportActions, + // }, }, true, ), From c8692c0887191667c1335814eb05f2a77c90ee2a Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 27 Oct 2023 00:44:15 +0700 Subject: [PATCH 0013/1208] fix: app uses ultra-wide camera by default --- src/pages/iou/ReceiptSelector/index.js | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/pages/iou/ReceiptSelector/index.js b/src/pages/iou/ReceiptSelector/index.js index ca9fe90575e7..9181d0bd2701 100644 --- a/src/pages/iou/ReceiptSelector/index.js +++ b/src/pages/iou/ReceiptSelector/index.js @@ -1,5 +1,5 @@ import {View, Text, PixelRatio, ActivityIndicator, PanResponder} from 'react-native'; -import React, {useCallback, useContext, useReducer, useRef, useState} from 'react'; +import React, {useCallback, useContext, useEffect, useReducer, useRef, useState} from 'react'; import lodashGet from 'lodash/get'; import _ from 'underscore'; import PropTypes from 'prop-types'; @@ -84,6 +84,7 @@ function ReceiptSelector({route, transactionID, iou, report}) { const [isFlashLightOn, toggleFlashlight] = useReducer((state) => !state, false); const [isTorchAvailable, setIsTorchAvailable] = useState(true); const cameraRef = useRef(null); + const normalCameraDeviceIdRef = useRef(null); const hideReciptModal = () => { setIsAttachmentInvalid(false); @@ -169,6 +170,24 @@ function ReceiptSelector({route, transactionID, iou, report}) { }), ).current; + /** + * On phones that have ultra-wide lens, default camera is ultra-wide. + * The last deviceId is of regular len camera. + */ + useEffect(() => { + if (!navigator || !navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) { + return; + } + + navigator.mediaDevices.enumerateDevices().then((devices) => { + normalCameraDeviceIdRef.current = _.chain(devices) + .filter((device) => device.kind === 'videoinput') + .last() + .get('deviceId', '') + .value(); + }); + }, []); + const mobileCameraView = () => ( <> @@ -197,7 +216,7 @@ function ReceiptSelector({route, transactionID, iou, report}) { style={{...styles.videoContainer, display: cameraPermissionState !== 'granted' ? 'none' : 'block'}} ref={cameraRef} screenshotFormat="image/png" - videoConstraints={{facingMode: {exact: 'environment'}}} + videoConstraints={{facingMode: {exact: 'environment'}, deviceId: normalCameraDeviceIdRef.current}} torchOn={isFlashLightOn} onTorchAvailability={setIsTorchAvailable} forceScreenshotSourceSize From 3d3bd77d7c1d633ed42adef361952bc588db050f Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 27 Oct 2023 16:54:07 +0700 Subject: [PATCH 0014/1208] useState approach --- src/pages/iou/ReceiptSelector/index.js | 47 +++++++++++++++----------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/src/pages/iou/ReceiptSelector/index.js b/src/pages/iou/ReceiptSelector/index.js index 9181d0bd2701..601a1abd4773 100644 --- a/src/pages/iou/ReceiptSelector/index.js +++ b/src/pages/iou/ReceiptSelector/index.js @@ -84,7 +84,32 @@ function ReceiptSelector({route, transactionID, iou, report}) { const [isFlashLightOn, toggleFlashlight] = useReducer((state) => !state, false); const [isTorchAvailable, setIsTorchAvailable] = useState(true); const cameraRef = useRef(null); - const normalCameraDeviceIdRef = useRef(null); + const [videoConstraints, setVideoConstraints] = useState({facingMode: {exact: 'environment'}}); + + /** + * On phones that have ultra-wide lens, react-webcam uses ultra-wide by default. + * The last deviceId is of regular len camera. + */ + useEffect(() => { + navigator.mediaDevices.getUserMedia({audio: true, video: true}).then(() => { + if (!navigator.mediaDevices.enumerateDevices) { + return; + } + + navigator.mediaDevices.enumerateDevices().then((devices) => { + const lastBackDeviceId = _.chain(devices) + .reverse() + .find((item) => item.label && item.label.endsWith('facing back')) + .get('deviceId', '') + .value(); + + if (!lastBackDeviceId) { + return; + } + setVideoConstraints({deviceId: lastBackDeviceId}); + }); + }); + }, []); const hideReciptModal = () => { setIsAttachmentInvalid(false); @@ -170,24 +195,6 @@ function ReceiptSelector({route, transactionID, iou, report}) { }), ).current; - /** - * On phones that have ultra-wide lens, default camera is ultra-wide. - * The last deviceId is of regular len camera. - */ - useEffect(() => { - if (!navigator || !navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) { - return; - } - - navigator.mediaDevices.enumerateDevices().then((devices) => { - normalCameraDeviceIdRef.current = _.chain(devices) - .filter((device) => device.kind === 'videoinput') - .last() - .get('deviceId', '') - .value(); - }); - }, []); - const mobileCameraView = () => ( <> @@ -216,7 +223,7 @@ function ReceiptSelector({route, transactionID, iou, report}) { style={{...styles.videoContainer, display: cameraPermissionState !== 'granted' ? 'none' : 'block'}} ref={cameraRef} screenshotFormat="image/png" - videoConstraints={{facingMode: {exact: 'environment'}, deviceId: normalCameraDeviceIdRef.current}} + videoConstraints={videoConstraints} torchOn={isFlashLightOn} onTorchAvailability={setIsTorchAvailable} forceScreenshotSourceSize From 56f99e6edd04dbf09188dce97e9c61820be40f8c Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 30 Oct 2023 09:44:17 +0700 Subject: [PATCH 0015/1208] remove getUserMedia --- src/pages/iou/ReceiptSelector/index.js | 56 +++++++++++++------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/src/pages/iou/ReceiptSelector/index.js b/src/pages/iou/ReceiptSelector/index.js index 1ead9ca45076..168ee8230ff1 100644 --- a/src/pages/iou/ReceiptSelector/index.js +++ b/src/pages/iou/ReceiptSelector/index.js @@ -84,30 +84,30 @@ function ReceiptSelector({route, transactionID, iou, report}) { const [isFlashLightOn, toggleFlashlight] = useReducer((state) => !state, false); const [isTorchAvailable, setIsTorchAvailable] = useState(true); const cameraRef = useRef(null); - const [videoConstraints, setVideoConstraints] = useState({facingMode: {exact: 'environment'}}); + const [videoConstraints, setVideoConstraints] = useState(null); /** * On phones that have ultra-wide lens, react-webcam uses ultra-wide by default. * The last deviceId is of regular len camera. */ useEffect(() => { - navigator.mediaDevices.getUserMedia({audio: true, video: true}).then(() => { - if (!navigator.mediaDevices.enumerateDevices) { + if (!navigator.mediaDevices.enumerateDevices) { + setVideoConstraints({facingMode: {exact: 'environment'}}); + return; + } + + navigator.mediaDevices.enumerateDevices().then((devices) => { + const lastBackDeviceId = _.chain(devices) + .filter((item) => item.label && item.label.endsWith('facing back')) + .last() + .get('deviceId', '') + .value(); + + if (!lastBackDeviceId) { + setVideoConstraints({facingMode: {exact: 'environment'}}); return; } - - navigator.mediaDevices.enumerateDevices().then((devices) => { - const lastBackDeviceId = _.chain(devices) - .reverse() - .find((item) => item.label && item.label.endsWith('facing back')) - .get('deviceId', '') - .value(); - - if (!lastBackDeviceId) { - return; - } - setVideoConstraints({deviceId: lastBackDeviceId}); - }); + setVideoConstraints({deviceId: lastBackDeviceId}); }); }, []); @@ -217,17 +217,19 @@ function ReceiptSelector({route, transactionID, iou, report}) { {translate('receipt.cameraAccess')} )} - setCameraPermissionState('granted')} - onUserMediaError={() => setCameraPermissionState('denied')} - style={{...styles.videoContainer, display: cameraPermissionState !== 'granted' ? 'none' : 'block'}} - ref={cameraRef} - screenshotFormat="image/png" - videoConstraints={videoConstraints} - torchOn={isFlashLightOn} - onTorchAvailability={setIsTorchAvailable} - forceScreenshotSourceSize - /> + {videoConstraints && ( + setCameraPermissionState('granted')} + onUserMediaError={() => setCameraPermissionState('denied')} + style={{...styles.videoContainer, display: cameraPermissionState !== 'granted' ? 'none' : 'block'}} + ref={cameraRef} + screenshotFormat="image/png" + videoConstraints={videoConstraints} + torchOn={isFlashLightOn} + onTorchAvailability={setIsTorchAvailable} + forceScreenshotSourceSize + /> + )} From 3180d32f66ebd7cc30603e17cae250d3bb705368 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 30 Oct 2023 18:11:32 +0700 Subject: [PATCH 0016/1208] close active stream --- src/pages/iou/ReceiptSelector/index.js | 30 +++++++++++++++----------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/pages/iou/ReceiptSelector/index.js b/src/pages/iou/ReceiptSelector/index.js index 168ee8230ff1..304d7c44da43 100644 --- a/src/pages/iou/ReceiptSelector/index.js +++ b/src/pages/iou/ReceiptSelector/index.js @@ -91,23 +91,27 @@ function ReceiptSelector({route, transactionID, iou, report}) { * The last deviceId is of regular len camera. */ useEffect(() => { - if (!navigator.mediaDevices.enumerateDevices) { - setVideoConstraints({facingMode: {exact: 'environment'}}); - return; - } + navigator.mediaDevices.getUserMedia({audio: true, video: true}).then((stream) => { + _.forEach(stream.getTracks(), (videoStream) => videoStream.stop()); - navigator.mediaDevices.enumerateDevices().then((devices) => { - const lastBackDeviceId = _.chain(devices) - .filter((item) => item.label && item.label.endsWith('facing back')) - .last() - .get('deviceId', '') - .value(); - - if (!lastBackDeviceId) { + if (!navigator.mediaDevices.enumerateDevices) { setVideoConstraints({facingMode: {exact: 'environment'}}); return; } - setVideoConstraints({deviceId: lastBackDeviceId}); + + navigator.mediaDevices.enumerateDevices().then((devices) => { + const lastBackDeviceId = _.chain(devices) + .filter((item) => item.kind === 'videoinput') + .last() + .get('deviceId', '') + .value(); + + if (!lastBackDeviceId) { + setVideoConstraints({facingMode: {exact: 'environment'}}); + return; + } + setVideoConstraints({deviceId: lastBackDeviceId}); + }); }); }, []); From c853f697eafa8d3b070bf4bf518aae31f769e720 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 6 Nov 2023 12:22:43 +0700 Subject: [PATCH 0017/1208] do not request permission on desktop --- src/pages/iou/ReceiptSelector/index.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pages/iou/ReceiptSelector/index.js b/src/pages/iou/ReceiptSelector/index.js index fdef1a7af941..17323b3ae687 100644 --- a/src/pages/iou/ReceiptSelector/index.js +++ b/src/pages/iou/ReceiptSelector/index.js @@ -91,6 +91,10 @@ function ReceiptSelector({route, transactionID, iou, report}) { * The last deviceId is of regular len camera. */ useEffect(() => { + if (!Browser.isMobile()) { + return; + } + navigator.mediaDevices.getUserMedia({video: true}).then((stream) => { _.forEach(stream.getTracks(), (videoStream) => videoStream.stop()); From 1f073b03dc0192086ab3c215dd535a271dfb1e85 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 7 Nov 2023 19:59:23 +0100 Subject: [PATCH 0018/1208] fix sliding --- .../InvertedFlatList/BaseInvertedFlatList.js | 2 +- src/pages/home/report/ReportActionsList.js | 11 +---- src/pages/home/report/ReportActionsView.js | 40 +++++++++++-------- 3 files changed, 26 insertions(+), 27 deletions(-) diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.js b/src/components/InvertedFlatList/BaseInvertedFlatList.js index 802ae373d22a..b71311b0a173 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.js +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.js @@ -136,7 +136,7 @@ function BaseInvertedFlatList(props) { windowSize={15} maintainVisibleContentPosition={{ minIndexForVisible: 0, - autoscrollToTopThreshold: variables.listItemHeightNormal, + // autoscrollToTopThreshold: variables.listItemHeightNormal, }} inverted /> diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 5520221c3b56..b0e62281845b 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -130,9 +130,8 @@ function ReportActionsList({ loadOlderChats, onLayout, isComposerFullSize, - isLinkingLoader, + reportScrollManager, }) { - const reportScrollManager = useReportScrollManager(); const {translate} = useLocalize(); const {isOffline} = useNetwork(); const route = useRoute(); @@ -360,7 +359,7 @@ function ReportActionsList({ const shouldShowReportRecipientLocalTime = ReportUtils.canShowReportRecipientLocalTime(personalDetailsList, report, currentUserPersonalDetails.accountID) && !isComposerFullSize; const contentContainerStyle = useMemo( - () => [styles.chatContentScrollView, isLoadingNewerReportActions ? styles.chatContentScrollViewWithHeaderLoader : {}], + () => [styles.chatContentScrollView, isLoadingNewerReportActions ? styles.chatContentScrollViewWithHeaderLoader : {}], [isLoadingNewerReportActions], ); @@ -404,12 +403,6 @@ function ReportActionsList({ ); }, [isLoadingNewerReportActions]); - useEffect(() => { - if (!isLinkingLoader) { - return; - } - reportScrollManager.scrollToBottom(); - }, [isLinkingLoader, reportScrollManager]); return ( <> { - let timeoutIdCatted; - let timeoutIdExtended; - if (!isLoadingLinkedMessage) { - timeoutIdCatted = setTimeout(() => { - setLinkingToCattedMessage(false); - }, 100); - timeoutIdExtended = setTimeout(() => { - setLinkingToExtendedMessage(false); - }, 200); - } - return () => { - clearTimeout(timeoutIdCatted); - clearTimeout(timeoutIdExtended); - }; - }, [isLoadingLinkedMessage]); const {catted: reportActionsBeforeAndIncludingLinked, expanded: reportActionsBeforeAndIncludingLinkedExpanded} = useMemo(() => { if (reportActionID && allReportActions?.length) { @@ -158,10 +145,28 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro if (!reportActionID) { return; } + if (scrollToBottom) { + scrollToBottom(); + } setLinkingToCattedMessage(true); setLinkingToExtendedMessage(true); fetchReport(); - }, [route, reportActionID, fetchReport]); + + const timeoutIdCatted = setTimeout(() => { + setLinkingToCattedMessage(false); + }, 100); + const timeoutIdExtended = setTimeout(() => { + setLinkingToExtendedMessage(false); + }, 200); + + return () => { + if (!timeoutIdCatted && !timeoutIdExtended) { + return; + } + clearTimeout(timeoutIdCatted); + clearTimeout(timeoutIdExtended); + }; + }, [route, reportActionID, fetchReport, scrollToBottom]); const isReportActionArrayCatted = useMemo(() => { if (reportActions?.length !== allReportActions?.length && reportActionID) { @@ -346,6 +351,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro isLoadingInitialReportActions={props.isLoadingInitialReportActions} isLoadingOlderReportActions={props.isLoadingOlderReportActions} isLoadingNewerReportActions={props.isLoadingNewerReportActions} + reportScrollManager={reportScrollManager} policy={props.policy} /> From c7bbfd4cf1529e6ba40bf206e96bc70eee26c9a9 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 8 Nov 2023 16:59:28 +0700 Subject: [PATCH 0019/1208] only request permission on Scan tab focus --- src/pages/iou/ReceiptSelector/index.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pages/iou/ReceiptSelector/index.js b/src/pages/iou/ReceiptSelector/index.js index 4e482d13fb44..6d65d250f1a6 100644 --- a/src/pages/iou/ReceiptSelector/index.js +++ b/src/pages/iou/ReceiptSelector/index.js @@ -1,8 +1,10 @@ +import {useIsFocused} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useContext, useEffect, useReducer, useRef, useState} from 'react'; import {ActivityIndicator, PanResponder, PixelRatio, Text, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; +import _ from 'underscore'; import Hand from '@assets/images/hand.svg'; import ReceiptUpload from '@assets/images/receipt-upload.svg'; import Shutter from '@assets/images/shutter.svg'; @@ -84,13 +86,14 @@ function ReceiptSelector({route, transactionID, iou, report}) { const [isTorchAvailable, setIsTorchAvailable] = useState(true); const cameraRef = useRef(null); const [videoConstraints, setVideoConstraints] = useState(null); + const isCameraActive = useIsFocused(); /** * On phones that have ultra-wide lens, react-webcam uses ultra-wide by default. * The last deviceId is of regular len camera. */ useEffect(() => { - if (!Browser.isMobile()) { + if (!_.isEmpty(videoConstraints) || !isCameraActive || !Browser.isMobile()) { return; } @@ -116,7 +119,7 @@ function ReceiptSelector({route, transactionID, iou, report}) { setVideoConstraints({deviceId: lastBackDeviceId}); }); }); - }, []); + }, [isCameraActive]); const hideReciptModal = () => { setIsAttachmentInvalid(false); From 586259d96d984f6fdeb0dfab37700a8248e841f2 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 8 Nov 2023 17:02:22 +0700 Subject: [PATCH 0020/1208] fix lint --- src/pages/iou/ReceiptSelector/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/iou/ReceiptSelector/index.js b/src/pages/iou/ReceiptSelector/index.js index 6d65d250f1a6..63f382baa7ed 100644 --- a/src/pages/iou/ReceiptSelector/index.js +++ b/src/pages/iou/ReceiptSelector/index.js @@ -119,6 +119,7 @@ function ReceiptSelector({route, transactionID, iou, report}) { setVideoConstraints({deviceId: lastBackDeviceId}); }); }); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [isCameraActive]); const hideReciptModal = () => { From 2b7a7355d25c7207cccd723b5322ef90884777b6 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Sun, 12 Nov 2023 18:06:39 +0100 Subject: [PATCH 0021/1208] 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 6b6afd8326c14044681838a44c5e4b929ef77e26 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 15 Nov 2023 15:06:15 +0100 Subject: [PATCH 0022/1208] [TS migration] Migrate 'Onfido' component --- .../{BaseOnfidoWeb.js => BaseOnfidoWeb.tsx} | 50 ++++++++++--------- src/components/Onfido/index.desktop.js | 11 ---- .../{index.native.js => index.native.tsx} | 16 +++--- .../Onfido/{index.website.js => index.tsx} | 11 ++-- src/components/Onfido/onfidoPropTypes.js | 15 ------ src/components/Onfido/types.ts | 20 ++++++++ 6 files changed, 58 insertions(+), 65 deletions(-) rename src/components/Onfido/{BaseOnfidoWeb.js => BaseOnfidoWeb.tsx} (81%) delete mode 100644 src/components/Onfido/index.desktop.js rename src/components/Onfido/{index.native.js => index.native.tsx} (79%) rename src/components/Onfido/{index.website.js => index.tsx} (64%) delete mode 100644 src/components/Onfido/onfidoPropTypes.js create mode 100644 src/components/Onfido/types.ts diff --git a/src/components/Onfido/BaseOnfidoWeb.js b/src/components/Onfido/BaseOnfidoWeb.tsx similarity index 81% rename from src/components/Onfido/BaseOnfidoWeb.js rename to src/components/Onfido/BaseOnfidoWeb.tsx index 5c0f83902e55..79842823a975 100644 --- a/src/components/Onfido/BaseOnfidoWeb.js +++ b/src/components/Onfido/BaseOnfidoWeb.tsx @@ -1,7 +1,6 @@ -import lodashGet from 'lodash/get'; import * as OnfidoSDK from 'onfido-sdk-ui'; -import React, {forwardRef, useEffect} from 'react'; -import _ from 'underscore'; +import React, {ForwardedRef, forwardRef, useEffect} from 'react'; +import type {LocaleContextProps} from '@components/LocaleContextProvider'; import useLocalize from '@hooks/useLocalize'; import Log from '@libs/Log'; import fontFamily from '@styles/fontFamily'; @@ -10,9 +9,15 @@ import themeColors from '@styles/themes/default'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import './index.css'; -import onfidoPropTypes from './onfidoPropTypes'; +import type {OnfidoElement, OnfidoProps} from './types'; -function initializeOnfido({sdkToken, onSuccess, onError, onUserExit, preferredLocale, translate}) { +type LocaleProps = Pick; + +type OnfidoEvent = Event & { + detail?: Record; +}; + +function initializeOnfido({sdkToken, onSuccess, onError, onUserExit, preferredLocale, translate}: OnfidoProps & LocaleProps) { OnfidoSDK.init({ token: sdkToken, containerId: CONST.ONFIDO.CONTAINER_ID, @@ -22,7 +27,7 @@ function initializeOnfido({sdkToken, onSuccess, onError, onUserExit, preferredLo fontFamilySubtitle: `${fontFamily.EXP_NEUE}, -apple-system, serif`, fontFamilyBody: `${fontFamily.EXP_NEUE}, -apple-system, serif`, fontSizeTitle: `${variables.fontSizeLarge}px`, - fontWeightTitle: fontWeightBold, + fontWeightTitle: Number(fontWeightBold), fontWeightSubtitle: 400, fontSizeSubtitle: `${variables.fontSizeNormal}px`, colorContentTitle: themeColors.text, @@ -47,7 +52,6 @@ function initializeOnfido({sdkToken, onSuccess, onError, onUserExit, preferredLo colorBorderLinkUnderline: themeColors.link, colorBackgroundLinkHover: themeColors.link, colorBackgroundLinkActive: themeColors.link, - authAccentColor: themeColors.link, colorBackgroundInfoPill: themeColors.link, colorBackgroundSelector: themeColors.appBG, colorBackgroundDocTypeButton: themeColors.success, @@ -59,11 +63,10 @@ function initializeOnfido({sdkToken, onSuccess, onError, onUserExit, preferredLo { type: CONST.ONFIDO.TYPE.DOCUMENT, options: { - useLiveDocumentCapture: true, forceCrossDevice: true, hideCountrySelection: true, - country: 'USA', documentTypes: { + // eslint-disable-next-line @typescript-eslint/naming-convention driving_licence: { country: 'USA', }, @@ -78,17 +81,15 @@ function initializeOnfido({sdkToken, onSuccess, onError, onUserExit, preferredLo }, }, ], - smsNumberCountryCode: CONST.ONFIDO.SMS_NUMBER_COUNTRY_CODE.US, - showCountrySelection: false, onComplete: (data) => { - if (_.isEmpty(data)) { + if (!Object.keys(data).length) { Log.warn('Onfido completed with no data'); } onSuccess(data); }, onError: (error) => { - const errorMessage = lodashGet(error, 'message', CONST.ERROR.UNKNOWN_ERROR); - const errorType = lodashGet(error, 'type'); + const errorMessage = error.message ?? CONST.ERROR.UNKNOWN_ERROR; + const errorType = error.type; Log.hmmm('Onfido error', {errorType, errorMessage}); onError(errorMessage); }, @@ -101,32 +102,33 @@ function initializeOnfido({sdkToken, onSuccess, onError, onUserExit, preferredLo }, language: { // We need to use ES_ES as locale key because the key `ES` is not a valid config key for Onfido - locale: preferredLocale === CONST.LOCALES.ES ? CONST.LOCALES.ES_ES_ONFIDO : preferredLocale, + locale: preferredLocale === CONST.LOCALES.ES ? CONST.LOCALES.ES_ES_ONFIDO : (preferredLocale as OnfidoSDK.SupportedLanguages), // Provide a custom phrase for the back button so that the first letter is capitalized, // and translate the phrase while we're at it. See the issue and documentation for more context. // https://github.com/Expensify/App/issues/17244 // https://documentation.onfido.com/sdk/web/#custom-languages phrases: { + // eslint-disable-next-line @typescript-eslint/naming-convention 'generic.back': translate('common.back'), }, }, }); } -function logOnFidoEvent(event) { +function logOnFidoEvent(event: OnfidoEvent) { Log.hmmm('Receiving Onfido analytic event', event.detail); } -const Onfido = forwardRef((props, ref) => { +function Onfido({sdkToken, onSuccess, onError, onUserExit}: OnfidoProps, ref: ForwardedRef) { const {preferredLocale, translate} = useLocalize(); useEffect(() => { initializeOnfido({ - sdkToken: props.sdkToken, - onSuccess: props.onSuccess, - onError: props.onError, - onUserExit: props.onUserExit, + sdkToken, + onSuccess, + onError, + onUserExit, preferredLocale, translate, }); @@ -143,8 +145,8 @@ const Onfido = forwardRef((props, ref) => { ref={ref} /> ); -}); +} Onfido.displayName = 'Onfido'; -Onfido.propTypes = onfidoPropTypes; -export default Onfido; + +export default forwardRef(Onfido); diff --git a/src/components/Onfido/index.desktop.js b/src/components/Onfido/index.desktop.js deleted file mode 100644 index e455eaf78d32..000000000000 --- a/src/components/Onfido/index.desktop.js +++ /dev/null @@ -1,11 +0,0 @@ -import BaseOnfidoWeb from './BaseOnfidoWeb'; -import onfidoPropTypes from './onfidoPropTypes'; - -// On desktop, we do not want to teardown onfido, because it causes a crash. -// See https://github.com/Expensify/App/issues/6082 -const Onfido = BaseOnfidoWeb; - -Onfido.propTypes = onfidoPropTypes; -Onfido.displayName = 'Onfido'; - -export default Onfido; diff --git a/src/components/Onfido/index.native.js b/src/components/Onfido/index.native.tsx similarity index 79% rename from src/components/Onfido/index.native.js rename to src/components/Onfido/index.native.tsx index ed0578187d3c..e09eeec4f322 100644 --- a/src/components/Onfido/index.native.js +++ b/src/components/Onfido/index.native.tsx @@ -1,15 +1,13 @@ import {OnfidoCaptureType, OnfidoCountryCode, OnfidoDocumentType, Onfido as OnfidoSDK} from '@onfido/react-native-sdk'; -import lodashGet from 'lodash/get'; import React, {useEffect} from 'react'; import {Alert, Linking} from 'react-native'; -import _ from 'underscore'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import useLocalize from '@hooks/useLocalize'; import Log from '@libs/Log'; import CONST from '@src/CONST'; -import onfidoPropTypes from './onfidoPropTypes'; +import type {OnfidoProps} from './types'; -function Onfido({sdkToken, onUserExit, onSuccess, onError}) { +function Onfido({sdkToken, onUserExit, onSuccess, onError}: OnfidoProps) { const {translate} = useLocalize(); useEffect(() => { @@ -28,19 +26,20 @@ function Onfido({sdkToken, onUserExit, onSuccess, onError}) { }) .then(onSuccess) .catch((error) => { - const errorMessage = lodashGet(error, 'message', CONST.ERROR.UNKNOWN_ERROR); - const errorType = lodashGet(error, 'type'); + const errorMessage = error.message ?? CONST.ERROR.UNKNOWN_ERROR; + const errorType = error.type; + Log.hmmm('Onfido error on native', {errorType, errorMessage}); // If the user cancels the Onfido flow we won't log this error as it's normal. In the React Native SDK the user exiting the flow will trigger this error which we can use as // our "user exited the flow" callback. On web, this event has it's own callback passed as a config so we don't need to bother with this there. - if (_.contains([CONST.ONFIDO.ERROR.USER_CANCELLED, CONST.ONFIDO.ERROR.USER_TAPPED_BACK, CONST.ONFIDO.ERROR.USER_EXITED], errorMessage)) { + if ([CONST.ONFIDO.ERROR.USER_CANCELLED, CONST.ONFIDO.ERROR.USER_TAPPED_BACK, CONST.ONFIDO.ERROR.USER_EXITED].includes(errorMessage)) { onUserExit(); return; } // Handle user camera permission on iOS and Android - if (_.contains([CONST.ONFIDO.ERROR.USER_CAMERA_PERMISSION, CONST.ONFIDO.ERROR.USER_CAMERA_DENINED, CONST.ONFIDO.ERROR.USER_CAMERA_CONSENT_DENIED], errorMessage)) { + if ([CONST.ONFIDO.ERROR.USER_CAMERA_PERMISSION, CONST.ONFIDO.ERROR.USER_CAMERA_DENINED, CONST.ONFIDO.ERROR.USER_CAMERA_CONSENT_DENIED].includes(errorMessage)) { Alert.alert( translate('onfidoStep.cameraPermissionsNotGranted'), translate('onfidoStep.cameraRequestMessage'), @@ -71,7 +70,6 @@ function Onfido({sdkToken, onUserExit, onSuccess, onError}) { return ; } -Onfido.propTypes = onfidoPropTypes; Onfido.displayName = 'Onfido'; export default Onfido; diff --git a/src/components/Onfido/index.website.js b/src/components/Onfido/index.tsx similarity index 64% rename from src/components/Onfido/index.website.js rename to src/components/Onfido/index.tsx index 12ad1edd8fb9..139dc3cec405 100644 --- a/src/components/Onfido/index.website.js +++ b/src/components/Onfido/index.tsx @@ -1,14 +1,14 @@ -import lodashGet from 'lodash/get'; import React, {useEffect, useRef} from 'react'; import BaseOnfidoWeb from './BaseOnfidoWeb'; -import onfidoPropTypes from './onfidoPropTypes'; +import type {OnfidoElement, OnfidoProps} from './types'; -function Onfido({sdkToken, onSuccess, onError, onUserExit}) { - const baseOnfidoRef = useRef(null); +function Onfido({sdkToken, onSuccess, onError, onUserExit}: OnfidoProps) { + const baseOnfidoRef = useRef(null); useEffect( () => () => { - const onfidoOut = lodashGet(baseOnfidoRef.current, 'onfidoOut'); + const onfidoOut = baseOnfidoRef.current?.onfidoOut; + if (!onfidoOut) { return; } @@ -29,7 +29,6 @@ function Onfido({sdkToken, onSuccess, onError, onUserExit}) { ); } -Onfido.propTypes = onfidoPropTypes; Onfido.displayName = 'Onfido'; export default Onfido; diff --git a/src/components/Onfido/onfidoPropTypes.js b/src/components/Onfido/onfidoPropTypes.js deleted file mode 100644 index ff0023c70058..000000000000 --- a/src/components/Onfido/onfidoPropTypes.js +++ /dev/null @@ -1,15 +0,0 @@ -import PropTypes from 'prop-types'; - -export default { - /** Token used to initialize the Onfido SDK */ - sdkToken: PropTypes.string.isRequired, - - /** Called when the user intentionally exits the flow without completing it */ - onUserExit: PropTypes.func.isRequired, - - /** Called when the user is totally done with Onfido */ - onSuccess: PropTypes.func.isRequired, - - /** Called when Onfido throws an error */ - onError: PropTypes.func.isRequired, -}; diff --git a/src/components/Onfido/types.ts b/src/components/Onfido/types.ts new file mode 100644 index 000000000000..a4fe3d93f05e --- /dev/null +++ b/src/components/Onfido/types.ts @@ -0,0 +1,20 @@ +import {OnfidoError, OnfidoResult} from '@onfido/react-native-sdk'; +import * as OnfidoSDK from 'onfido-sdk-ui'; + +type OnfidoElement = HTMLDivElement & {onfidoOut?: OnfidoSDK.SdkHandle}; + +type OnfidoProps = { + /** Token used to initialize the Onfido SDK */ + sdkToken: string; + + /** Called when the user intentionally exits the flow without completing it */ + onUserExit: (userExitCode?: OnfidoSDK.UserExitCode) => void; + + /** Called when the user is totally done with Onfido */ + onSuccess: (data: OnfidoSDK.SdkResponse | OnfidoResult | OnfidoError) => void; + + /** Called when Onfido throws an error */ + onError: (error?: string) => void; +}; + +export type {OnfidoProps, OnfidoElement}; From 9f8621852748158ed52ddef21dec575a31b33312 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 28 Nov 2023 09:55:12 +0100 Subject: [PATCH 0023/1208] lint --- src/components/InvertedFlatList/BaseInvertedFlatList.js | 1 - src/pages/home/ReportScreen.js | 6 +++--- src/pages/home/report/ReportActionsView.js | 7 +------ 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.js b/src/components/InvertedFlatList/BaseInvertedFlatList.js index 18dc09b9feb1..2862236daa07 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.js +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.js @@ -17,7 +17,6 @@ const defaultProps = { data: [], }; - const BaseInvertedFlatList = forwardRef((props, ref) => ( { From 8b7f74c22a935f44879af65c652dec2c32360bbf Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 4 Dec 2023 15:58:16 +0700 Subject: [PATCH 0024/1208] bool check videoConstraints --- src/pages/iou/ReceiptSelector/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/ReceiptSelector/index.js b/src/pages/iou/ReceiptSelector/index.js index 67211f5c42ee..c68c20b5c643 100644 --- a/src/pages/iou/ReceiptSelector/index.js +++ b/src/pages/iou/ReceiptSelector/index.js @@ -229,7 +229,7 @@ function ReceiptSelector({route, transactionID, iou, report}) { {translate('receipt.cameraAccess')} )} - {videoConstraints && ( + {!_.isEmpty(videoConstraints) && ( setCameraPermissionState('granted')} onUserMediaError={() => setCameraPermissionState('denied')} From 2d083d329e4f5e0b2677bff3dc55d7b0dd9497e5 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 18 Dec 2023 13:16:23 +0100 Subject: [PATCH 0025/1208] 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 0026/1208] 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 0027/1208] 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 0028/1208] 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 0029/1208] 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 0030/1208] 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 0031/1208] 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 0032/1208] 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 0033/1208] 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 0034/1208] 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 0035/1208] 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 0036/1208] 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 0037/1208] 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 0038/1208] 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 0039/1208] 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 0040/1208] 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 0041/1208] 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 0042/1208] 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 0043/1208] 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 0044/1208] 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 0045/1208] 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 0046/1208] 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 0047/1208] 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 0048/1208] 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 0049/1208] 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 0050/1208] 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 0051/1208] 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 0052/1208] 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 0053/1208] 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 0054/1208] 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 0055/1208] 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 0056/1208] 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 0057/1208] 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 0058/1208] 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 0059/1208] 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 0060/1208] 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 0061/1208] 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 0062/1208] 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 0063/1208] 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 0064/1208] 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 0065/1208] 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 0066/1208] 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 0067/1208] 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 0068/1208] 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 0069/1208] 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 0070/1208] 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 0071/1208] 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 0072/1208] 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 0073/1208] 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 0074/1208] 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 0075/1208] 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 0076/1208] 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 0077/1208] 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 0078/1208] 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 0079/1208] 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 0080/1208] Fix lint --- src/pages/home/report/ReportActionsView.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 05d156347ba0..f577d9d3b6fa 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -98,11 +98,10 @@ let listIDCount = Math.round(Math.random() * 100); * @param {function} fetchNewerActon - Function to fetch more messages. * @param {string} route - Current route, used to reset states on route change. * @param {boolean} isLoading - Loading state indicator. - * @param {object} reportScrollManager - Manages scrolling functionality. * @returns {object} An object containing the sliced message array, the pagination function, * index of the linked message, and a unique list ID. */ -const useHandleList = (linkedID, messageArray, fetchNewerActon, route, isLoading, reportScrollManager) => { +const useHandleList = (linkedID, messageArray, fetchNewerActon, route, isLoading) => { // we don't set edgeID on initial render as linkedID as it should trigger cattedArray after linked message was positioned const [edgeID, setEdgeID] = useState(''); const isCuttingForFirstRender = useRef(true); @@ -123,7 +122,7 @@ const useHandleList = (linkedID, messageArray, fetchNewerActon, route, isLoading return -1; } - return messageArray.findIndex((obj) => String(obj.reportActionID) === String(isCuttingForFirstRender.current ? linkedID : edgeID)); + return _.findIndex(messageArray, (obj) => String(obj.reportActionID) === String(isCuttingForFirstRender.current ? linkedID : edgeID)); }, [messageArray, edgeID, linkedID]); const cattedArray = useMemo(() => { @@ -136,10 +135,9 @@ const useHandleList = (linkedID, messageArray, fetchNewerActon, route, isLoading if (isCuttingForFirstRender.current) { return messageArray.slice(index, messageArray.length); - } else { - const newStartIndex = index >= PAGINATION_SIZE ? index - PAGINATION_SIZE : 0; - return newStartIndex ? messageArray.slice(newStartIndex, messageArray.length) : messageArray; } + const newStartIndex = index >= PAGINATION_SIZE ? index - PAGINATION_SIZE : 0; + return newStartIndex ? messageArray.slice(newStartIndex, messageArray.length) : messageArray; // edgeID is needed to trigger batching once the report action has been positioned // eslint-disable-next-line react-hooks/exhaustive-deps }, [linkedID, messageArray, index, isLoading, edgeID]); @@ -212,7 +210,7 @@ function ReportActionsView({reportActions: allReportActions, fetchReport, ...pro fetchFunc, linkedIdIndex, listID, - } = useHandleList(reportActionID, allReportActions, fetchNewerAction, route, !!reportActionID && props.isLoadingInitialReportActions, reportScrollManager); + } = useHandleList(reportActionID, allReportActions, fetchNewerAction, route, !!reportActionID && props.isLoadingInitialReportActions); const hasNewestReportAction = lodashGet(reportActions[0], 'created') === props.report.lastVisibleActionCreated; const newestReportAction = lodashGet(reportActions, '[0]'); From 167775a4d69447f674cad364a891105e53b83e09 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 12 Jan 2024 14:19:45 +0700 Subject: [PATCH 0081/1208] Reapply changes --- .../request/step/IOURequestStepScan/index.js | 68 +++++++++++++++---- 1 file changed, 55 insertions(+), 13 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.js b/src/pages/iou/request/step/IOURequestStepScan/index.js index c0c96826d124..3aa4235dea5d 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.js @@ -1,6 +1,7 @@ import lodashGet from 'lodash/get'; -import React, {useCallback, useContext, useReducer, useRef, useState} from 'react'; +import React, {useCallback, useContext, useEffect, useReducer, useRef, useState} from 'react'; import {ActivityIndicator, PanResponder, PixelRatio, Text, View} from 'react-native'; +import _ from 'underscore'; import Hand from '@assets/images/hand.svg'; import ReceiptUpload from '@assets/images/receipt-upload.svg'; import Shutter from '@assets/images/shutter.svg'; @@ -14,6 +15,7 @@ import * as Expensicons from '@components/Icon/Expensicons'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import transactionPropTypes from '@components/transactionPropTypes'; import useLocalize from '@hooks/useLocalize'; +import useTabNavigatorFocus from '@hooks/useTabNavigatorFocus'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -74,6 +76,44 @@ function IOURequestStepScan({ const [isTorchAvailable, setIsTorchAvailable] = useState(false); const cameraRef = useRef(null); + const [videoConstraints, setVideoConstraints] = useState(null); + const tabIndex = 1; + const isScanTabActive = useTabNavigatorFocus({tabIndex}); + + /** + * On phones that have ultra-wide lens, react-webcam uses ultra-wide by default. + * The last deviceId is of regular len camera. + */ + useEffect(() => { + if (!_.isEmpty(videoConstraints) || !isScanTabActive || !Browser.isMobile()) { + return; + } + + navigator.mediaDevices.getUserMedia({video: true}).then((stream) => { + _.forEach(stream.getTracks(), (videoStream) => videoStream.stop()); + + if (!navigator.mediaDevices.enumerateDevices) { + setVideoConstraints({facingMode: {exact: 'environment'}}); + return; + } + + navigator.mediaDevices.enumerateDevices().then((devices) => { + const lastBackDeviceId = _.chain(devices) + .filter((item) => item.kind === 'videoinput') + .last() + .get('deviceId', '') + .value(); + + if (!lastBackDeviceId) { + setVideoConstraints({facingMode: {exact: 'environment'}}); + return; + } + setVideoConstraints({deviceId: lastBackDeviceId}); + }); + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isScanTabActive]); + const hideRecieptModal = () => { setIsAttachmentInvalid(false); }; @@ -211,18 +251,20 @@ function IOURequestStepScan({ {translate('receipt.cameraAccess')} )} - setCameraPermissionState('granted')} - onUserMediaError={() => setCameraPermissionState('denied')} - style={{...styles.videoContainer, display: cameraPermissionState !== 'granted' ? 'none' : 'block'}} - ref={cameraRef} - screenshotFormat="image/png" - videoConstraints={{facingMode: {exact: 'environment'}}} - torchOn={isFlashLightOn} - onTorchAvailability={setIsTorchAvailable} - forceScreenshotSourceSize - cameraTabIndex={1} - /> + {!_.isEmpty(videoConstraints) && ( + setCameraPermissionState('granted')} + onUserMediaError={() => setCameraPermissionState('denied')} + style={{...styles.videoContainer, display: cameraPermissionState !== 'granted' ? 'none' : 'block'}} + ref={cameraRef} + screenshotFormat="image/png" + videoConstraints={videoConstraints} + torchOn={isFlashLightOn} + onTorchAvailability={setIsTorchAvailable} + forceScreenshotSourceSize + cameraTabIndex={tabIndex} + /> + )} From 6afd422e26ec35c5912b7b00945dc9c3d57e2804 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 12 Jan 2024 14:30:37 +0700 Subject: [PATCH 0082/1208] fix lint --- src/pages/iou/request/step/IOURequestStepScan/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.js b/src/pages/iou/request/step/IOURequestStepScan/index.js index 3aa4235dea5d..2a7c8be25950 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.js @@ -78,14 +78,14 @@ function IOURequestStepScan({ const [videoConstraints, setVideoConstraints] = useState(null); const tabIndex = 1; - const isScanTabActive = useTabNavigatorFocus({tabIndex}); + const isTabActive = useTabNavigatorFocus({tabIndex}); /** * On phones that have ultra-wide lens, react-webcam uses ultra-wide by default. * The last deviceId is of regular len camera. */ useEffect(() => { - if (!_.isEmpty(videoConstraints) || !isScanTabActive || !Browser.isMobile()) { + if (!_.isEmpty(videoConstraints) || !isTabActive || !Browser.isMobile()) { return; } @@ -112,7 +112,7 @@ function IOURequestStepScan({ }); }); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isScanTabActive]); + }, [isTabActive]); const hideRecieptModal = () => { setIsAttachmentInvalid(false); From d08dea532480085710e2acc190401eca7af9ff1c Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 12 Jan 2024 15:47:43 +0100 Subject: [PATCH 0083/1208] 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 0084/1208] 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 0085/1208] 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 0086/1208] 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 0087/1208] 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 0088/1208] 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 0089/1208] 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 0090/1208] 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 0091/1208] 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 0092/1208] 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 0093/1208] 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 0094/1208] 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 0095/1208] 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 0096/1208] 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 0097/1208] 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 0098/1208] 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 0099/1208] 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 0100/1208] 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 0101/1208] 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 0102/1208] 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 0103/1208] 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 0104/1208] 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 0105/1208] 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 0106/1208] 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 0107/1208] 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 0108/1208] fix test --- tests/ui/UnreadIndicatorsTest.js | 18 +++++++++--------- tests/utils/TestHelper.js | 4 +++- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/tests/ui/UnreadIndicatorsTest.js b/tests/ui/UnreadIndicatorsTest.js index e4d4d877f66b..97d50fb392f3 100644 --- a/tests/ui/UnreadIndicatorsTest.js +++ b/tests/ui/UnreadIndicatorsTest.js @@ -249,15 +249,15 @@ function signInAndGetAppWithUnreadChat() { }, ], }, - 1: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 10), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '1'), - 2: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 20), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '2'), - 3: TestHelper.buildTestReportComment(reportAction3CreatedDate, USER_B_ACCOUNT_ID, '3'), - 4: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 40), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '4'), - 5: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 50), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '5'), - 6: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 60), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '6'), - 7: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 70), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '7'), - 8: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 80), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '8'), - 9: TestHelper.buildTestReportComment(reportAction9CreatedDate, USER_B_ACCOUNT_ID, '9'), + 1: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 10), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '1', createdReportActionID), + 2: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 20), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '2', '1'), + 3: TestHelper.buildTestReportComment(reportAction3CreatedDate, USER_B_ACCOUNT_ID, '3', '2'), + 4: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 40), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '4', '3'), + 5: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 50), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '5', '4'), + 6: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 60), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '6', '5'), + 7: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 70), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '7', '6'), + 8: TestHelper.buildTestReportComment(format(addSeconds(TEN_MINUTES_AGO, 80), CONST.DATE.FNS_DB_FORMAT_STRING), USER_B_ACCOUNT_ID, '8', '7'), + 9: TestHelper.buildTestReportComment(reportAction9CreatedDate, USER_B_ACCOUNT_ID, '9', '8'), }); await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, { [USER_B_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_B_EMAIL, USER_B_ACCOUNT_ID, 'B'), diff --git a/tests/utils/TestHelper.js b/tests/utils/TestHelper.js index dd95ab4efb67..4a331496541a 100644 --- a/tests/utils/TestHelper.js +++ b/tests/utils/TestHelper.js @@ -198,9 +198,10 @@ function setPersonalDetails(login, accountID) { * @param {String} created * @param {Number} actorAccountID * @param {String} actionID + * @param {String} previousReportActionID * @returns {Object} */ -function buildTestReportComment(created, actorAccountID, actionID = null) { +function buildTestReportComment(created, actorAccountID, actionID = null, previousReportActionID = null) { const reportActionID = actionID || NumberUtils.rand64(); return { actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, @@ -209,6 +210,7 @@ function buildTestReportComment(created, actorAccountID, actionID = null) { message: [{type: 'COMMENT', html: `Comment ${actionID}`, text: `Comment ${actionID}`}], reportActionID, actorAccountID, + previousReportActionID, }; } From bd5f17703af96409056a743804362e7f7efbf149 Mon Sep 17 00:00:00 2001 From: Toby Sullivan Date: Fri, 24 Nov 2023 16:07:50 -0800 Subject: [PATCH 0109/1208] Include comment in report name for amounts owing --- src/languages/en.ts | 2 +- src/languages/es.ts | 2 +- src/languages/types.ts | 2 +- src/libs/ReportUtils.ts | 38 +++++++++++++++++++++++--------------- 4 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index b6da38df21a0..87b12f631631 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -601,7 +601,7 @@ export default { splitAmount: ({amount}: SplitAmountParams) => `split ${amount}`, didSplitAmount: ({formattedAmount, comment}: DidSplitAmountMessageParams) => `split ${formattedAmount}${comment ? ` for ${comment}` : ''}`, amountEach: ({amount}: AmountEachParams) => `${amount} each`, - payerOwesAmount: ({payer, amount}: PayerOwesAmountParams) => `${payer} owes ${amount}`, + payerOwesAmount: ({payer, amount, comment}: PayerOwesAmountParams) => `${payer} owes ${amount}${comment ? ` for ${comment}` : ''}`, payerOwes: ({payer}: PayerOwesParams) => `${payer} owes: `, payerPaidAmount: ({payer, amount}: PayerPaidAmountParams): string => `${payer ? `${payer} ` : ''}paid ${amount}`, payerPaid: ({payer}: PayerPaidParams) => `${payer} paid: `, diff --git a/src/languages/es.ts b/src/languages/es.ts index 2478c8ba8bd2..2c7e58655ca8 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -594,7 +594,7 @@ export default { splitAmount: ({amount}: SplitAmountParams) => `dividir ${amount}`, didSplitAmount: ({formattedAmount, comment}: DidSplitAmountMessageParams) => `dividió ${formattedAmount}${comment ? ` para ${comment}` : ''}`, amountEach: ({amount}: AmountEachParams) => `${amount} cada uno`, - payerOwesAmount: ({payer, amount}: PayerOwesAmountParams) => `${payer} debe ${amount}`, + payerOwesAmount: ({payer, amount, comment}: PayerOwesAmountParams) => `${payer} debe ${amount}${comment ? ` para ${comment}` : ''}`, payerOwes: ({payer}: PayerOwesParams) => `${payer} debe: `, payerPaidAmount: ({payer, amount}: PayerPaidAmountParams) => `${payer ? `${payer} ` : ''}pagó ${amount}`, payerPaid: ({payer}: PayerPaidParams) => `${payer} pagó: `, diff --git a/src/languages/types.ts b/src/languages/types.ts index 3185b7a8f6f1..eab9991b73d8 100644 --- a/src/languages/types.ts +++ b/src/languages/types.ts @@ -115,7 +115,7 @@ type DidSplitAmountMessageParams = {formattedAmount: string; comment: string}; type AmountEachParams = {amount: number}; -type PayerOwesAmountParams = {payer: string; amount: number | string}; +type PayerOwesAmountParams = {payer: string; amount: number | string; comment?: string}; type PayerOwesParams = {payer: string}; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index d48567ebdaf3..32fb047d9126 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2178,26 +2178,28 @@ function getReportPreviewMessage( return reportActionMessage; } + let linkedTransaction: Transaction | EmptyObject = {}; + if (!isEmptyObject(reportAction)) { + linkedTransaction = TransactionUtils.getLinkedTransaction(reportAction); + } + if (!isEmptyObject(reportAction) && !isIOUReport(report) && reportAction && ReportActionsUtils.isSplitBillAction(reportAction)) { // This covers group chats where the last action is a split bill action - const linkedTransaction = TransactionUtils.getLinkedTransaction(reportAction); if (isEmptyObject(linkedTransaction)) { return reportActionMessage; } - if (!isEmptyObject(linkedTransaction)) { - if (TransactionUtils.isReceiptBeingScanned(linkedTransaction)) { - return Localize.translateLocal('iou.receiptScanning'); - } - - if (TransactionUtils.hasMissingSmartscanFields(linkedTransaction)) { - return Localize.translateLocal('iou.receiptMissingDetails'); - } + if (TransactionUtils.isReceiptBeingScanned(linkedTransaction)) { + return Localize.translateLocal('iou.receiptScanning'); + } - const transactionDetails = getTransactionDetails(linkedTransaction); - const formattedAmount = CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency ?? ''); - return Localize.translateLocal('iou.didSplitAmount', {formattedAmount, comment: transactionDetails?.comment ?? ''}); + if (TransactionUtils.hasMissingSmartscanFields(linkedTransaction)) { + return Localize.translateLocal('iou.receiptMissingDetails'); } + + const transactionDetails = getTransactionDetails(linkedTransaction); + const formattedAmount = CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency ?? ''); + return Localize.translateLocal('iou.didSplitAmount', {formattedAmount, comment: transactionDetails?.comment ?? ''}); } const totalAmount = getMoneyRequestReimbursableTotal(report); @@ -2214,8 +2216,6 @@ function getReportPreviewMessage( } if (!isEmptyObject(reportAction) && shouldConsiderReceiptBeingScanned && reportAction && ReportActionsUtils.isMoneyRequestAction(reportAction)) { - const linkedTransaction = TransactionUtils.getLinkedTransaction(reportAction); - if (!isEmptyObject(linkedTransaction) && TransactionUtils.hasReceipt(linkedTransaction) && TransactionUtils.isReceiptBeingScanned(linkedTransaction)) { return Localize.translateLocal('iou.receiptScanning'); } @@ -2261,7 +2261,15 @@ function getReportPreviewMessage( return `${requestorName ? `${requestorName}: ` : ''}${Localize.translateLocal('iou.requestedAmount', {formattedAmount: amountToDisplay})}`; } - return Localize.translateLocal(containsNonReimbursable ? 'iou.payerSpentAmount' : 'iou.payerOwesAmount', {payer: payerName ?? '', amount: formattedAmount}); + if (containsNonReimbursable) { + return Localize.translateLocal('iou.payerSpentAmount', {payer: payerName ?? '', amount: formattedAmount}); + } + + if (!isEmptyObject(linkedTransaction)) { + const comment = TransactionUtils.getDescription(linkedTransaction); + return Localize.translateLocal('iou.payerOwesAmount', {payer: payerName ?? '', amount: formattedAmount, comment}); + } + return Localize.translateLocal('iou.payerOwesAmount', {payer: payerName ?? '', amount: formattedAmount}); } /** From ee2c15d47495cc7a81ffc8e52319cd0bcd817fdc Mon Sep 17 00:00:00 2001 From: Toby Sullivan Date: Fri, 19 Jan 2024 09:30:14 -0800 Subject: [PATCH 0110/1208] Reuse translateLocal call --- src/libs/ReportUtils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 32fb047d9126..4cdcdc863aa3 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2265,11 +2265,11 @@ function getReportPreviewMessage( return Localize.translateLocal('iou.payerSpentAmount', {payer: payerName ?? '', amount: formattedAmount}); } + let comment: string | undefined if (!isEmptyObject(linkedTransaction)) { - const comment = TransactionUtils.getDescription(linkedTransaction); - return Localize.translateLocal('iou.payerOwesAmount', {payer: payerName ?? '', amount: formattedAmount, comment}); + comment = TransactionUtils.getDescription(linkedTransaction); } - return Localize.translateLocal('iou.payerOwesAmount', {payer: payerName ?? '', amount: formattedAmount}); + return Localize.translateLocal('iou.payerOwesAmount', {payer: payerName ?? '', amount: formattedAmount, comment}); } /** From eeca194eaaddc0ef1ae75312d29b4552e8024aab Mon Sep 17 00:00:00 2001 From: Toby Sullivan Date: Fri, 19 Jan 2024 10:29:53 -0800 Subject: [PATCH 0111/1208] Remove redundant type declaration --- src/libs/ReportUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 4cdcdc863aa3..d1d38bbff305 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2265,7 +2265,7 @@ function getReportPreviewMessage( return Localize.translateLocal('iou.payerSpentAmount', {payer: payerName ?? '', amount: formattedAmount}); } - let comment: string | undefined + let comment if (!isEmptyObject(linkedTransaction)) { comment = TransactionUtils.getDescription(linkedTransaction); } From db0d8d1ecc0436fbba67479a230a9de0809850f4 Mon Sep 17 00:00:00 2001 From: Toby Sullivan Date: Fri, 19 Jan 2024 10:51:27 -0800 Subject: [PATCH 0112/1208] Avoid variable reassignments --- src/libs/ReportUtils.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index d1d38bbff305..e57dff6b4ae2 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2178,11 +2178,7 @@ function getReportPreviewMessage( return reportActionMessage; } - let linkedTransaction: Transaction | EmptyObject = {}; - if (!isEmptyObject(reportAction)) { - linkedTransaction = TransactionUtils.getLinkedTransaction(reportAction); - } - + const linkedTransaction = !isEmptyObject(reportAction) ? TransactionUtils.getLinkedTransaction(reportAction) : {}; if (!isEmptyObject(reportAction) && !isIOUReport(report) && reportAction && ReportActionsUtils.isSplitBillAction(reportAction)) { // This covers group chats where the last action is a split bill action if (isEmptyObject(linkedTransaction)) { @@ -2265,10 +2261,7 @@ function getReportPreviewMessage( return Localize.translateLocal('iou.payerSpentAmount', {payer: payerName ?? '', amount: formattedAmount}); } - let comment - if (!isEmptyObject(linkedTransaction)) { - comment = TransactionUtils.getDescription(linkedTransaction); - } + const comment = !isEmptyObject(linkedTransaction) ? TransactionUtils.getDescription(linkedTransaction) : undefined; return Localize.translateLocal('iou.payerOwesAmount', {payer: payerName ?? '', amount: formattedAmount, comment}); } From c2731c056e804b06b93cbe63c0ed79f9ab8d9ea6 Mon Sep 17 00:00:00 2001 From: Toby Sullivan Date: Fri, 19 Jan 2024 10:52:31 -0800 Subject: [PATCH 0113/1208] Remove unrelated code change --- src/libs/ReportUtils.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index e57dff6b4ae2..ee85c1aca958 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2185,17 +2185,19 @@ function getReportPreviewMessage( return reportActionMessage; } - if (TransactionUtils.isReceiptBeingScanned(linkedTransaction)) { - return Localize.translateLocal('iou.receiptScanning'); - } + if (!isEmptyObject(linkedTransaction)) { + if (TransactionUtils.isReceiptBeingScanned(linkedTransaction)) { + return Localize.translateLocal('iou.receiptScanning'); + } - if (TransactionUtils.hasMissingSmartscanFields(linkedTransaction)) { - return Localize.translateLocal('iou.receiptMissingDetails'); - } + if (TransactionUtils.hasMissingSmartscanFields(linkedTransaction)) { + return Localize.translateLocal('iou.receiptMissingDetails'); + } - const transactionDetails = getTransactionDetails(linkedTransaction); - const formattedAmount = CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency ?? ''); - return Localize.translateLocal('iou.didSplitAmount', {formattedAmount, comment: transactionDetails?.comment ?? ''}); + const transactionDetails = getTransactionDetails(linkedTransaction); + const formattedAmount = CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency ?? ''); + return Localize.translateLocal('iou.didSplitAmount', {formattedAmount, comment: transactionDetails?.comment ?? ''}); + } } const totalAmount = getMoneyRequestReimbursableTotal(report); From 036ac5c5a87fb72784b60fb899c656d346b29a74 Mon Sep 17 00:00:00 2001 From: Toby Sullivan Date: Fri, 19 Jan 2024 14:51:11 -0800 Subject: [PATCH 0114/1208] Update editRegularMoneyTransaction --- src/libs/actions/IOU.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 7ee752a1f0ef..410da027351a 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -2272,10 +2272,16 @@ function editRegularMoneyRequest(transactionID, transactionThreadReportID, trans // Update the last message of the chat report const hasNonReimbursableTransactions = ReportUtils.hasNonReimbursableTransactions(iouReport); - const messageText = Localize.translateLocal(hasNonReimbursableTransactions ? 'iou.payerSpentAmount' : 'iou.payerOwesAmount', { - payer: ReportUtils.getPersonalDetailsForAccountID(updatedMoneyRequestReport.managerID).login || '', - amount: CurrencyUtils.convertToDisplayString(updatedMoneyRequestReport.total, updatedMoneyRequestReport.currency), - }); + const payer = ReportUtils.getPersonalDetailsForAccountID(updatedMoneyRequestReport.managerID).login || '' + const formattedAmount = CurrencyUtils.convertToDisplayString(updatedMoneyRequestReport.total, updatedMoneyRequestReport.currency) + let messageText + if (hasNonReimbursableTransactions) { + messageText = Localize.translateLocal('iou.payerSpentAmount', { payer, amount: formattedAmount }); + } else { + const comment = TransactionUtils.getDescription(updatedTransaction) + messageText = Localize.translateLocal('iou.payerOwesAmount', { payer, amount: formattedAmount, comment }); + } + updatedChatReport.lastMessageText = messageText; updatedChatReport.lastMessageHtml = messageText; } From 40073f04b4eed15987aa815497b75776cefc2e5b Mon Sep 17 00:00:00 2001 From: Toby Sullivan Date: Fri, 19 Jan 2024 16:00:57 -0800 Subject: [PATCH 0115/1208] Include request description when request deleted --- src/libs/actions/IOU.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 410da027351a..f861a37b1051 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -28,6 +28,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import * as Policy from './Policy'; import * as Report from './Report'; +import { isEmptyObject } from '@src/types/utils/EmptyObject'; let betas; Onyx.connect({ @@ -2569,10 +2570,16 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView updatedIOUReport.lastVisibleActionCreated = lodashGet(lastVisibleAction, 'created'); const hasNonReimbursableTransactions = ReportUtils.hasNonReimbursableTransactions(iouReport); - const messageText = Localize.translateLocal(hasNonReimbursableTransactions ? 'iou.payerSpentAmount' : 'iou.payerOwesAmount', { - payer: ReportUtils.getPersonalDetailsForAccountID(updatedIOUReport.managerID).login || '', - amount: CurrencyUtils.convertToDisplayString(updatedIOUReport.total, updatedIOUReport.currency), - }); + const payer = ReportUtils.getPersonalDetailsForAccountID(updatedIOUReport.managerID).login || ''; + const formattedAmount = CurrencyUtils.convertToDisplayString(updatedIOUReport.total, updatedIOUReport.currency); + let messageText + if (hasNonReimbursableTransactions) { + messageText = Localize.translateLocal('iou.payerSpentAmount', {payer, amount: formattedAmount}) + } else { + const comment = !isEmptyObject(transaction) ? TransactionUtils.getDescription(transaction) : undefined; + messageText = Localize.translateLocal('iou.payerOwesAmount', {payer, amount: formattedAmount, comment}) + } + updatedReportPreviewAction.message[0].text = messageText; updatedReportPreviewAction.message[0].html = messageText; From f7032dd759854ce6da538e79dd855894814c9b12 Mon Sep 17 00:00:00 2001 From: Toby Sullivan Date: Fri, 19 Jan 2024 16:03:13 -0800 Subject: [PATCH 0116/1208] Include IOU description in report name --- src/libs/ReportUtils.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index ee85c1aca958..2928e54a79ee 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1928,7 +1928,9 @@ function getMoneyRequestReportName(report: OnyxEntry, policy: OnyxEntry< } if (isProcessingReport(report) || isDraftExpenseReport(report) || moneyRequestTotal === 0) { - return Localize.translateLocal('iou.payerOwesAmount', {payer: payerOrApproverName, amount: formattedAmount}); + const reportTransactions = !isEmptyObject(report) ? TransactionUtils.getAllReportTransactions(report.reportID) : [] + const comment = reportTransactions.length === 1 ? TransactionUtils.getDescription(reportTransactions[0]) : undefined + return Localize.translateLocal('iou.payerOwesAmount', {payer: payerOrApproverName, amount: formattedAmount, comment}); } return payerPaidAmountMessage; From 2e2ac1f645337fa4d5255fc2fbfe350384a2571a Mon Sep 17 00:00:00 2001 From: Toby Sullivan Date: Fri, 19 Jan 2024 16:06:50 -0800 Subject: [PATCH 0117/1208] Run prettier and lint --- src/libs/ReportUtils.ts | 4 ++-- src/libs/actions/IOU.js | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 2928e54a79ee..004488d3a151 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1928,8 +1928,8 @@ function getMoneyRequestReportName(report: OnyxEntry, policy: OnyxEntry< } if (isProcessingReport(report) || isDraftExpenseReport(report) || moneyRequestTotal === 0) { - const reportTransactions = !isEmptyObject(report) ? TransactionUtils.getAllReportTransactions(report.reportID) : [] - const comment = reportTransactions.length === 1 ? TransactionUtils.getDescription(reportTransactions[0]) : undefined + const reportTransactions = !isEmptyObject(report) ? TransactionUtils.getAllReportTransactions(report.reportID) : []; + const comment = reportTransactions.length === 1 ? TransactionUtils.getDescription(reportTransactions[0]) : undefined; return Localize.translateLocal('iou.payerOwesAmount', {payer: payerOrApproverName, amount: formattedAmount, comment}); } diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index f861a37b1051..e7a579d7b671 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -26,9 +26,9 @@ import ViolationsUtils from '@libs/Violations/ViolationsUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import * as Policy from './Policy'; import * as Report from './Report'; -import { isEmptyObject } from '@src/types/utils/EmptyObject'; let betas; Onyx.connect({ @@ -2273,14 +2273,14 @@ function editRegularMoneyRequest(transactionID, transactionThreadReportID, trans // Update the last message of the chat report const hasNonReimbursableTransactions = ReportUtils.hasNonReimbursableTransactions(iouReport); - const payer = ReportUtils.getPersonalDetailsForAccountID(updatedMoneyRequestReport.managerID).login || '' - const formattedAmount = CurrencyUtils.convertToDisplayString(updatedMoneyRequestReport.total, updatedMoneyRequestReport.currency) - let messageText + const payer = ReportUtils.getPersonalDetailsForAccountID(updatedMoneyRequestReport.managerID).login || ''; + const formattedAmount = CurrencyUtils.convertToDisplayString(updatedMoneyRequestReport.total, updatedMoneyRequestReport.currency); + let messageText; if (hasNonReimbursableTransactions) { - messageText = Localize.translateLocal('iou.payerSpentAmount', { payer, amount: formattedAmount }); + messageText = Localize.translateLocal('iou.payerSpentAmount', {payer, amount: formattedAmount}); } else { - const comment = TransactionUtils.getDescription(updatedTransaction) - messageText = Localize.translateLocal('iou.payerOwesAmount', { payer, amount: formattedAmount, comment }); + const comment = TransactionUtils.getDescription(updatedTransaction); + messageText = Localize.translateLocal('iou.payerOwesAmount', {payer, amount: formattedAmount, comment}); } updatedChatReport.lastMessageText = messageText; @@ -2572,12 +2572,12 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView const hasNonReimbursableTransactions = ReportUtils.hasNonReimbursableTransactions(iouReport); const payer = ReportUtils.getPersonalDetailsForAccountID(updatedIOUReport.managerID).login || ''; const formattedAmount = CurrencyUtils.convertToDisplayString(updatedIOUReport.total, updatedIOUReport.currency); - let messageText + let messageText; if (hasNonReimbursableTransactions) { - messageText = Localize.translateLocal('iou.payerSpentAmount', {payer, amount: formattedAmount}) + messageText = Localize.translateLocal('iou.payerSpentAmount', {payer, amount: formattedAmount}); } else { const comment = !isEmptyObject(transaction) ? TransactionUtils.getDescription(transaction) : undefined; - messageText = Localize.translateLocal('iou.payerOwesAmount', {payer, amount: formattedAmount, comment}) + messageText = Localize.translateLocal('iou.payerOwesAmount', {payer, amount: formattedAmount, comment}); } updatedReportPreviewAction.message[0].text = messageText; From c1c260fb20dad42c5ee43b84a80cdcc1686dd5df Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Sat, 20 Jan 2024 18:15:31 +0100 Subject: [PATCH 0118/1208] 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 0119/1208] 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 0120/1208] 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 0121/1208] 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 0122/1208] 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 0123/1208] 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 0124/1208] fix scrolling to the bottom on action deletion from the same account on a different device --- .../InvertedFlatList/BaseInvertedFlatList.tsx | 1 + src/pages/home/report/ReportActionsList.js | 21 +++++++------------ src/pages/home/report/ReportActionsView.js | 4 ++-- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx index e007f63c8e97..d83e54f74d66 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.tsx +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.tsx @@ -39,3 +39,4 @@ function BaseInvertedFlatList(props: BaseInvertedFlatListProps, ref: Forwa BaseInvertedFlatList.displayName = 'BaseInvertedFlatList'; export default forwardRef(BaseInvertedFlatList); +export {AUTOSCROLL_TO_TOP_THRESHOLD}; diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 8bc26eea7d70..3a397b4f6cf6 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -6,6 +6,7 @@ import {DeviceEventEmitter, InteractionManager} from 'react-native'; import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import _ from 'underscore'; import InvertedFlatList from '@components/InvertedFlatList'; +import {AUTOSCROLL_TO_TOP_THRESHOLD} from '@components/InvertedFlatList/BaseInvertedFlatList'; import {withPersonalDetails} from '@components/OnyxProvider'; import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; @@ -177,12 +178,7 @@ function ReportActionsList({ const previousLastIndex = useRef(lastActionIndex); const linkedReportActionID = lodashGet(route, 'params.reportActionID', ''); - const lastPendingAction = lodashGet(sortedReportActions, [0, 'pendingAction']) - const isLastPendingActionIsAdd = lastPendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD; - const isLastPendingActionIsDelete = lastPendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; - - // This is utilized for automatically scrolling to the bottom when sending a new message, in cases where comment linking is used and the user is already at the end of the list. - const isNewestActionAvailableAndPendingAdd = linkedReportActionID && isLastPendingActionIsAdd; + const isLastPendingActionIsDelete = lodashGet(sortedReportActions, [0, 'pendingAction']) === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; // This state is used to force a re-render when the user manually marks a message as unread // by using a timestamp you can force re-renders without having to worry about if another message was marked as unread before @@ -198,17 +194,16 @@ function ReportActionsList({ useEffect(() => { if ( - (previousLastIndex.current !== lastActionIndex && reportActionSize.current > sortedVisibleReportActions.length && hasNewestReportAction) || - isNewestActionAvailableAndPendingAdd + scrollingVerticalOffset.current < AUTOSCROLL_TO_TOP_THRESHOLD && + previousLastIndex.current !== lastActionIndex && + reportActionSize.current > sortedVisibleReportActions.length && + hasNewestReportAction ) { - // runAfterInteractions is used for isNewestActionAvailableAndPendingAdd - InteractionManager.runAfterInteractions(() => { - reportScrollManager.scrollToBottom(); - }); + reportScrollManager.scrollToBottom(); } previousLastIndex.current = lastActionIndex; reportActionSize.current = sortedVisibleReportActions.length; - }, [lastActionIndex, sortedVisibleReportActions, reportScrollManager, hasNewestReportAction, isLastPendingActionIsAdd, linkedReportActionID, isNewestActionAvailableAndPendingAdd]); + }, [lastActionIndex, sortedVisibleReportActions, reportScrollManager, hasNewestReportAction, linkedReportActionID]); useEffect(() => { // If the reportID changes, we reset the userActiveSince to null, we need to do it because diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 8b674b9e05a9..eb6c7634f536 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -426,7 +426,7 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { ]); // Check if the first report action in the list is the one we're currently linked to - const isTheFirstReportActionIsLinked = firstReportActionID !== reportActionID; + const isTheFirstReportActionIsLinked = firstReportActionID === reportActionID; useEffect(() => { if (isTheFirstReportActionIsLinked) { @@ -444,7 +444,7 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { return null; } // AutoScroll is disabled when we do linking to a specific reportAction - const shouldEnableAutoScroll = hasNewestReportAction && (!reportActionID || isInitialLinkedView); + const shouldEnableAutoScroll = hasNewestReportAction && (!reportActionID || !isInitialLinkedView); return ( <> From 719d63d473ecb93ecc8894b9598c47bbae4d7158 Mon Sep 17 00:00:00 2001 From: Toby Sullivan Date: Fri, 26 Jan 2024 10:29:54 -0800 Subject: [PATCH 0125/1208] Undo changes to text outside LHN preview --- src/libs/ReportUtils.ts | 4 +--- src/libs/actions/IOU.js | 29 ++++++++--------------------- 2 files changed, 9 insertions(+), 24 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index ac1686f88544..edcd57078e0c 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1931,9 +1931,7 @@ function getMoneyRequestReportName(report: OnyxEntry, policy: OnyxEntry< } if (isProcessingReport(report) || isDraftExpenseReport(report) || moneyRequestTotal === 0) { - const reportTransactions = !isEmptyObject(report) ? TransactionUtils.getAllReportTransactions(report.reportID) : []; - const comment = reportTransactions.length === 1 ? TransactionUtils.getDescription(reportTransactions[0]) : undefined; - return Localize.translateLocal('iou.payerOwesAmount', {payer: payerOrApproverName, amount: formattedAmount, comment}); + return Localize.translateLocal('iou.payerOwesAmount', {payer: payerOrApproverName, amount: formattedAmount}); } return payerPaidAmountMessage; diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index e7a579d7b671..7ee752a1f0ef 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -26,7 +26,6 @@ import ViolationsUtils from '@libs/Violations/ViolationsUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; import * as Policy from './Policy'; import * as Report from './Report'; @@ -2273,16 +2272,10 @@ function editRegularMoneyRequest(transactionID, transactionThreadReportID, trans // Update the last message of the chat report const hasNonReimbursableTransactions = ReportUtils.hasNonReimbursableTransactions(iouReport); - const payer = ReportUtils.getPersonalDetailsForAccountID(updatedMoneyRequestReport.managerID).login || ''; - const formattedAmount = CurrencyUtils.convertToDisplayString(updatedMoneyRequestReport.total, updatedMoneyRequestReport.currency); - let messageText; - if (hasNonReimbursableTransactions) { - messageText = Localize.translateLocal('iou.payerSpentAmount', {payer, amount: formattedAmount}); - } else { - const comment = TransactionUtils.getDescription(updatedTransaction); - messageText = Localize.translateLocal('iou.payerOwesAmount', {payer, amount: formattedAmount, comment}); - } - + const messageText = Localize.translateLocal(hasNonReimbursableTransactions ? 'iou.payerSpentAmount' : 'iou.payerOwesAmount', { + payer: ReportUtils.getPersonalDetailsForAccountID(updatedMoneyRequestReport.managerID).login || '', + amount: CurrencyUtils.convertToDisplayString(updatedMoneyRequestReport.total, updatedMoneyRequestReport.currency), + }); updatedChatReport.lastMessageText = messageText; updatedChatReport.lastMessageHtml = messageText; } @@ -2570,16 +2563,10 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView updatedIOUReport.lastVisibleActionCreated = lodashGet(lastVisibleAction, 'created'); const hasNonReimbursableTransactions = ReportUtils.hasNonReimbursableTransactions(iouReport); - const payer = ReportUtils.getPersonalDetailsForAccountID(updatedIOUReport.managerID).login || ''; - const formattedAmount = CurrencyUtils.convertToDisplayString(updatedIOUReport.total, updatedIOUReport.currency); - let messageText; - if (hasNonReimbursableTransactions) { - messageText = Localize.translateLocal('iou.payerSpentAmount', {payer, amount: formattedAmount}); - } else { - const comment = !isEmptyObject(transaction) ? TransactionUtils.getDescription(transaction) : undefined; - messageText = Localize.translateLocal('iou.payerOwesAmount', {payer, amount: formattedAmount, comment}); - } - + const messageText = Localize.translateLocal(hasNonReimbursableTransactions ? 'iou.payerSpentAmount' : 'iou.payerOwesAmount', { + payer: ReportUtils.getPersonalDetailsForAccountID(updatedIOUReport.managerID).login || '', + amount: CurrencyUtils.convertToDisplayString(updatedIOUReport.total, updatedIOUReport.currency), + }); updatedReportPreviewAction.message[0].text = messageText; updatedReportPreviewAction.message[0].html = messageText; From e90e0085e5e300fd841c6cf4cfafd7d56b71f288 Mon Sep 17 00:00:00 2001 From: Dylan Date: Mon, 29 Jan 2024 16:04:33 +0700 Subject: [PATCH 0126/1208] remove NewDistanceRequestPage and EditRequestDistancePage --- src/ROUTES.ts | 10 +- .../MoneyRequestConfirmationList.js | 12 -- ...oraryForRefactorRequestConfirmationList.js | 10 +- .../ReportActionItem/MoneyRequestView.js | 12 +- .../AppNavigator/ModalStackNavigators.tsx | 1 - src/libs/Navigation/linkingConfig.ts | 1 - src/libs/Navigation/types.ts | 5 +- src/pages/EditRequestDistancePage.js | 122 ------------------ src/pages/EditRequestPage.js | 11 -- src/pages/iou/MoneyRequestSelectorPage.js | 12 -- src/pages/iou/NewDistanceRequestPage.js | 85 ------------ 11 files changed, 27 insertions(+), 254 deletions(-) delete mode 100644 src/pages/EditRequestDistancePage.js delete mode 100644 src/pages/iou/NewDistanceRequestPage.js diff --git a/src/ROUTES.ts b/src/ROUTES.ts index deabdc0ac853..b985f993367a 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -295,10 +295,6 @@ const ROUTES = { route: ':iouType/new/receipt/:reportID?', getRoute: (iouType: string, reportID = '') => `${iouType}/new/receipt/${reportID}` as const, }, - MONEY_REQUEST_DISTANCE: { - route: ':iouType/new/address/:reportID?', - getRoute: (iouType: string, reportID = '') => `${iouType}/new/address/${reportID}` as const, - }, MONEY_REQUEST_DISTANCE_TAB: { route: ':iouType/new/:reportID?/distance', getRoute: (iouType: string, reportID = '') => `${iouType}/new/${reportID}/distance` as const, @@ -350,9 +346,9 @@ const ROUTES = { getUrlWithBackToParam(`create/${iouType}/description/${transactionID}/${reportID}`, backTo), }, MONEY_REQUEST_STEP_DISTANCE: { - route: 'create/:iouType/distance/:transactionID/:reportID', - getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => - getUrlWithBackToParam(`create/${iouType}/distance/${transactionID}/${reportID}`, backTo), + route: ':action/:iouType/distance/:transactionID/:reportID', + getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => + getUrlWithBackToParam(`${action}/${iouType}/distance/${transactionID}/${reportID}`, backTo), }, MONEY_REQUEST_STEP_MERCHANT: { route: 'create/:iouType/merchant/:transactionID/:reportID', diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index d967d04ab94b..101e135d36e1 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -703,18 +703,6 @@ function MoneyRequestConfirmationList(props) { error={shouldDisplayFieldError && TransactionUtils.isCreatedMissing(transaction) ? translate('common.error.enterDate') : ''} /> )} - {props.isDistanceRequest && ( - Navigation.navigate(ROUTES.MONEY_REQUEST_DISTANCE.getRoute(props.iouType, props.reportID))} - disabled={didConfirm || !isTypeRequest} - interactive={!props.isReadOnly} - /> - )} {shouldShowMerchant && ( - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams())) + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute( + CONST.IOU.ACTION.CREATE, + CONST.IOU.TYPE.REQUEST, + transaction.transactionID, + reportID, + Navigation.getActiveRouteWithoutParams(), + ), + ) } disabled={didConfirm || !isTypeRequest} interactive={!isReadOnly} diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js index 3121328138ee..6cc2119a9044 100644 --- a/src/components/ReportActionItem/MoneyRequestView.js +++ b/src/components/ReportActionItem/MoneyRequestView.js @@ -301,7 +301,17 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate interactive={canEditDistance} shouldShowRightIcon={canEditDistance} titleStyle={styles.flex1} - onPress={() => Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.DISTANCE))} + onPress={() => + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute( + CONST.IOU.ACTION.EDIT, + CONST.IOU.TYPE.REQUEST, + transaction.transactionID, + report.reportID, + Navigation.getActiveRouteWithoutParams(), + ), + ) + } /> ) : ( diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx index 3a843e400409..9c2361916934 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx @@ -103,7 +103,6 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator require('../../../pages/settings/Wallet/AddDebitCardPage').default as React.ComponentType, [SCREENS.IOU_SEND.ENABLE_PAYMENTS]: () => require('../../../pages/EnablePayments/EnablePaymentsPage').default as React.ComponentType, [SCREENS.MONEY_REQUEST.WAYPOINT]: () => require('../../../pages/iou/MoneyRequestWaypointPage').default as React.ComponentType, - [SCREENS.MONEY_REQUEST.DISTANCE]: () => require('../../../pages/iou/NewDistanceRequestPage').default as React.ComponentType, [SCREENS.MONEY_REQUEST.RECEIPT]: () => require('../../../pages/EditRequestReceiptPage').default as React.ComponentType, }); diff --git a/src/libs/Navigation/linkingConfig.ts b/src/libs/Navigation/linkingConfig.ts index d4e04d5402e2..715b14c4cb90 100644 --- a/src/libs/Navigation/linkingConfig.ts +++ b/src/libs/Navigation/linkingConfig.ts @@ -435,7 +435,6 @@ const linkingConfig: LinkingOptions = { [SCREENS.MONEY_REQUEST.TAG]: ROUTES.MONEY_REQUEST_TAG.route, [SCREENS.MONEY_REQUEST.MERCHANT]: ROUTES.MONEY_REQUEST_MERCHANT.route, [SCREENS.MONEY_REQUEST.RECEIPT]: ROUTES.MONEY_REQUEST_RECEIPT.route, - [SCREENS.MONEY_REQUEST.DISTANCE]: ROUTES.MONEY_REQUEST_DISTANCE.route, [SCREENS.IOU_SEND.ENABLE_PAYMENTS]: ROUTES.IOU_SEND_ENABLE_PAYMENTS, [SCREENS.IOU_SEND.ADD_BANK_ACCOUNT]: ROUTES.IOU_SEND_ADD_BANK_ACCOUNT, [SCREENS.IOU_SEND.ADD_DEBIT_CARD]: ROUTES.IOU_SEND_ADD_DEBIT_CARD, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index b4a77f96cc74..ea2b48df4ed2 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -238,9 +238,12 @@ type MoneyRequestNavigatorParamList = { waypointIndex: string; threadReportID: number; }; - [SCREENS.MONEY_REQUEST.DISTANCE]: { + [SCREENS.MONEY_REQUEST.STEP_DISTANCE]: { + action: string; iouType: ValueOf; + transactionID: string; reportID: string; + backTo: string; }; [SCREENS.MONEY_REQUEST.RECEIPT]: { iouType: string; diff --git a/src/pages/EditRequestDistancePage.js b/src/pages/EditRequestDistancePage.js deleted file mode 100644 index f3ea76a3390a..000000000000 --- a/src/pages/EditRequestDistancePage.js +++ /dev/null @@ -1,122 +0,0 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; -import React, {useEffect, useRef} from 'react'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import DistanceRequest from '@components/DistanceRequest'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import ScreenWrapper from '@components/ScreenWrapper'; -import transactionPropTypes from '@components/transactionPropTypes'; -import useLocalize from '@hooks/useLocalize'; -import useNetwork from '@hooks/useNetwork'; -import usePrevious from '@hooks/usePrevious'; -import Navigation from '@libs/Navigation/Navigation'; -import * as IOU from '@userActions/IOU'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import reportPropTypes from './reportPropTypes'; - -const propTypes = { - /** The transactionID we're currently editing */ - transactionID: PropTypes.string.isRequired, - - /** The report to with which the distance request is associated */ - report: reportPropTypes.isRequired, - - /** Passed from the navigator */ - route: PropTypes.shape({ - /** Parameters the route gets */ - params: PropTypes.shape({ - /** Type of IOU */ - iouType: PropTypes.oneOf(_.values(CONST.IOU.TYPE)), - - /** Id of the report on which the distance request is being created */ - reportID: PropTypes.string, - }), - }).isRequired, - - /* Onyx props */ - /** The original transaction that is being edited */ - transaction: transactionPropTypes, - - /** backup version of the original transaction */ - transactionBackup: transactionPropTypes, -}; - -const defaultProps = { - transaction: {}, - transactionBackup: {}, -}; - -function EditRequestDistancePage({report, route, transaction, transactionBackup}) { - const {isOffline} = useNetwork(); - const {translate} = useLocalize(); - const hasWaypointError = useRef(false); - const prevIsLoading = usePrevious(transaction.isLoading); - - useEffect(() => { - hasWaypointError.current = Boolean(lodashGet(transaction, 'errorFields.route') || lodashGet(transaction, 'errorFields.waypoints')); - - // When the loading goes from true to false, then we know the transaction has just been - // saved to the server. Check for errors. If there are no errors, then the modal can be closed. - if (prevIsLoading && !transaction.isLoading && !hasWaypointError.current) { - Navigation.dismissModal(report.reportID); - } - }, [transaction, prevIsLoading, report]); - - /** - * Save the changes to the original transaction object - * @param {Object} waypoints - */ - const saveTransaction = (waypoints) => { - // If nothing was changed, simply go to transaction thread - // We compare only addresses because numbers are rounded while backup - const oldWaypoints = lodashGet(transactionBackup, 'comment.waypoints', {}); - const oldAddresses = _.mapObject(oldWaypoints, (waypoint) => _.pick(waypoint, 'address')); - const addresses = _.mapObject(waypoints, (waypoint) => _.pick(waypoint, 'address')); - if (_.isEqual(oldAddresses, addresses)) { - Navigation.dismissModal(report.reportID); - return; - } - - IOU.updateMoneyRequestDistance(transaction.transactionID, report.reportID, waypoints); - - // If the client is offline, then the modal can be closed as well (because there are no errors or other feedback to show them - // until they come online again and sync with the server). - if (isOffline) { - Navigation.dismissModal(report.reportID); - } - }; - - return ( - - Navigation.goBack()} - /> - - - ); -} - -EditRequestDistancePage.propTypes = propTypes; -EditRequestDistancePage.defaultProps = defaultProps; -EditRequestDistancePage.displayName = 'EditRequestDistancePage'; -export default withOnyx({ - transaction: { - key: (props) => `${ONYXKEYS.COLLECTION.TRANSACTION}${props.transactionID}`, - }, - transactionBackup: { - key: (props) => `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${props.transactionID}`, - }, -})(EditRequestDistancePage); diff --git a/src/pages/EditRequestPage.js b/src/pages/EditRequestPage.js index 3eb9d88f1120..9f1ba51806e5 100644 --- a/src/pages/EditRequestPage.js +++ b/src/pages/EditRequestPage.js @@ -23,7 +23,6 @@ import EditRequestAmountPage from './EditRequestAmountPage'; import EditRequestCategoryPage from './EditRequestCategoryPage'; import EditRequestCreatedPage from './EditRequestCreatedPage'; import EditRequestDescriptionPage from './EditRequestDescriptionPage'; -import EditRequestDistancePage from './EditRequestDistancePage'; import EditRequestMerchantPage from './EditRequestMerchantPage'; import EditRequestReceiptPage from './EditRequestReceiptPage'; import EditRequestTagPage from './EditRequestTagPage'; @@ -264,16 +263,6 @@ function EditRequestPage({report, route, policyCategories, policyTags, parentRep ); } - if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.DISTANCE) { - return ( - - ); - } - return ( { const moneyRequestID = `${iouType}${reportID}`; @@ -133,13 +128,6 @@ function MoneyRequestSelectorPage(props) { initialParams={{reportID, iouType}} /> {() => } - {shouldDisplayDistanceRequest && ( - - )} ) : ( diff --git a/src/pages/iou/NewDistanceRequestPage.js b/src/pages/iou/NewDistanceRequestPage.js deleted file mode 100644 index 750ac5d0141e..000000000000 --- a/src/pages/iou/NewDistanceRequestPage.js +++ /dev/null @@ -1,85 +0,0 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; -import React, {useCallback, useEffect} from 'react'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import DistanceRequest from '@components/DistanceRequest'; -import Navigation from '@libs/Navigation/Navigation'; -import reportPropTypes from '@pages/reportPropTypes'; -import * as IOU from '@userActions/IOU'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; -import {iouPropTypes} from './propTypes'; - -const propTypes = { - /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ - iou: iouPropTypes, - - /** The report on which the request is initiated on */ - report: reportPropTypes, - - /** Passed from the navigator */ - route: PropTypes.shape({ - /** Parameters the route gets */ - params: PropTypes.shape({ - /** Type of IOU */ - iouType: PropTypes.oneOf(_.values(CONST.IOU.TYPE)), - /** Id of the report on which the distance request is being created */ - reportID: PropTypes.string, - }), - }), -}; - -const defaultProps = { - iou: {}, - report: {}, - route: { - params: { - iouType: '', - reportID: '', - }, - }, -}; - -// This component is responsible for getting the transactionID from the IOU key, or creating the transaction if it doesn't exist yet, and then passing the transactionID. -// You can't use Onyx props in the withOnyx mapping, so we need to set up and access the transactionID here, and then pass it down so that DistanceRequest can subscribe to the transaction. -function NewDistanceRequestPage({iou, report, route}) { - const iouType = lodashGet(route, 'params.iouType', 'request'); - const isEditingNewRequest = Navigation.getActiveRoute().includes('address'); - - useEffect(() => { - if (iou.transactionID) { - return; - } - IOU.setUpDistanceTransaction(); - }, [iou.transactionID]); - - const onSubmit = useCallback(() => { - if (isEditingNewRequest) { - Navigation.goBack(ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(iouType, report.reportID)); - return; - } - IOU.navigateToNextPage(iou, iouType, report); - }, [iou, iouType, isEditingNewRequest, report]); - - return ( - - ); -} - -NewDistanceRequestPage.displayName = 'NewDistanceRequestPage'; -NewDistanceRequestPage.propTypes = propTypes; -NewDistanceRequestPage.defaultProps = defaultProps; -export default withOnyx({ - iou: {key: ONYXKEYS.IOU}, - report: { - key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT}${lodashGet(route, 'params.reportID')}`, - }, -})(NewDistanceRequestPage); From 4f4a2c21d112e39a41b2339acc0046893114a83c Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 29 Jan 2024 12:14:43 +0100 Subject: [PATCH 0127/1208] 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 0128/1208] 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 0129/1208] 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 0130/1208] 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 0131/1208] update after merge --- src/libs/API/parameters/OpenReportParams.ts | 1 + src/pages/home/report/ReportActionsView.js | 21 ++++++++++++++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/libs/API/parameters/OpenReportParams.ts b/src/libs/API/parameters/OpenReportParams.ts index 477a002516de..8eaed6bc0fde 100644 --- a/src/libs/API/parameters/OpenReportParams.ts +++ b/src/libs/API/parameters/OpenReportParams.ts @@ -1,5 +1,6 @@ type OpenReportParams = { reportID: string; + reportActionID?: string; emailList?: string; accountIDList?: string; parentReportActionID?: string; diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index ec227f21518c..31656060c7f2 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -186,7 +186,7 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { const mostRecentIOUReportActionID = useMemo(() => ReportActionsUtils.getMostRecentIOURequestActionID(props.reportActions), [props.reportActions]); const prevNetworkRef = useRef(props.network); const prevAuthTokenType = usePrevious(props.session.authTokenType); - const [isInitialLinkedView, setIsInitialLinkedView] = useState(false); + const [isInitialLinkedView, setIsInitialLinkedView] = useState(!!reportActionID); const prevIsSmallScreenWidthRef = useRef(props.isSmallScreenWidth); const reportID = props.report.reportID; const isLoading = (!!reportActionID && props.isLoadingInitialReportActions) || !props.isReadyForCommentLinking; @@ -430,14 +430,25 @@ function ReportActionsView({reportActions: allReportActions, ...props}) { const isTheFirstReportActionIsLinked = firstReportActionID === reportActionID; useEffect(() => { + let timerId; + if (isTheFirstReportActionIsLinked) { - // this should be applied after we navigated to linked reportAction + setIsInitialLinkedView(true); + } else { + // After navigating to the linked reportAction, apply this to correctly set + // `autoscrollToTopThreshold` prop when linking to a specific reportAction. InteractionManager.runAfterInteractions(() => { - setIsInitialLinkedView(true); + // Using a short delay to ensure the view is updated after interactions + timerId = setTimeout(() => setIsInitialLinkedView(false), 10); }); - } else { - setIsInitialLinkedView(false); } + + return () => { + if (!timerId) { + return; + } + clearTimeout(timerId); + }; }, [isTheFirstReportActionIsLinked]); // Comments have not loaded at all yet do nothing From 2060d5e2bfc161a08b7058ce88b3603add93a3bc Mon Sep 17 00:00:00 2001 From: Jayesh Mangwani Date: Tue, 30 Jan 2024 20:15:47 +0530 Subject: [PATCH 0132/1208] TS-migration: TermsStep Page --- .../{TermsStep.js => TermsStep.tsx} | 69 ++++++++----------- 1 file changed, 30 insertions(+), 39 deletions(-) rename src/pages/EnablePayments/{TermsStep.js => TermsStep.tsx} (67%) diff --git a/src/pages/EnablePayments/TermsStep.js b/src/pages/EnablePayments/TermsStep.tsx similarity index 67% rename from src/pages/EnablePayments/TermsStep.js rename to src/pages/EnablePayments/TermsStep.tsx index a09e1801c3b0..a03f3607d56e 100644 --- a/src/pages/EnablePayments/TermsStep.js +++ b/src/pages/EnablePayments/TermsStep.tsx @@ -6,39 +6,34 @@ import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import * as ErrorUtils from '@libs/ErrorUtils'; import * as BankAccounts from '@userActions/BankAccounts'; import ONYXKEYS from '@src/ONYXKEYS'; import LongTermsForm from './TermsPage/LongTermsForm'; import ShortTermsForm from './TermsPage/ShortTermsForm'; -import userWalletPropTypes from './userWalletPropTypes'; -import walletTermsPropTypes from './walletTermsPropTypes'; - -const propTypes = { - /** The user's wallet */ - userWallet: userWalletPropTypes, +import useLocalize from '@hooks/useLocalize'; +import type {OnyxEntry} from 'react-native-onyx'; +import {WalletTerms, UserWallet} from '@src/types/onyx'; +type TermsStepOnyxProps = { /** Comes from Onyx. Information about the terms for the wallet */ - walletTerms: walletTermsPropTypes, - - ...withLocalizePropTypes, -}; + walletTerms: OnyxEntry; +} -const defaultProps = { - userWallet: {}, - walletTerms: {}, +type TermsStepProps = TermsStepOnyxProps & { + /** The user's wallet */ + userWallet: OnyxEntry; }; -function TermsStep(props) { +function TermsStep(props: TermsStepProps) { const styles = useThemeStyles(); const [hasAcceptedDisclosure, setHasAcceptedDisclosure] = useState(false); const [hasAcceptedPrivacyPolicyAndWalletAgreement, setHasAcceptedPrivacyPolicyAndWalletAgreement] = useState(false); const [error, setError] = useState(false); + const {translate} = useLocalize(); - const errorMessage = error ? 'common.error.acceptTerms' : ErrorUtils.getLatestErrorMessage(props.walletTerms) || ''; + const errorMessage = error ? 'common.error.acceptTerms' : ErrorUtils.getLatestErrorMessage(props.walletTerms??{}) || ''; const toggleDisclosure = () => { setHasAcceptedDisclosure(!hasAcceptedDisclosure); @@ -59,7 +54,7 @@ function TermsStep(props) { return ( <> - + ( - {`${props.translate('termsStep.haveReadAndAgree')}`} - {`${props.translate('termsStep.electronicDisclosures')}.`} + {`${translate('termsStep.haveReadAndAgree')}`} + {`${translate('termsStep.electronicDisclosures')}.`} )} /> ( - {`${props.translate('termsStep.agreeToThe')} `} + {`${translate('termsStep.agreeToThe')} `} - {`${props.translate('common.privacy')} `} + {`${translate('common.privacy')} `} - {`${props.translate('common.and')} `} + {`${translate('common.and')} `} - {`${props.translate('termsStep.walletAgreement')}.`} + {`${translate('termsStep.walletAgreement')}.`} )} /> { if (!hasAcceptedDisclosure || !hasAcceptedPrivacyPolicyAndWalletAgreement) { setError(true); @@ -104,12 +99,12 @@ function TermsStep(props) { setError(false); BankAccounts.acceptWalletTerms({ hasAcceptedTerms: hasAcceptedDisclosure && hasAcceptedPrivacyPolicyAndWalletAgreement, - reportID: props.walletTerms.chatReportID, + reportID: props.walletTerms?.chatReportID??'', }); }} message={errorMessage} isAlertVisible={error || Boolean(errorMessage)} - isLoading={!!props.walletTerms.isLoading} + isLoading={!!props.walletTerms?.isLoading} containerStyles={[styles.mh0, styles.mv4]} /> @@ -118,13 +113,9 @@ function TermsStep(props) { } TermsStep.displayName = 'TermsPage'; -TermsStep.propTypes = propTypes; -TermsStep.defaultProps = defaultProps; -export default compose( - withLocalize, - withOnyx({ - walletTerms: { - key: ONYXKEYS.WALLET_TERMS, - }, - }), -)(TermsStep); + +export default withOnyx({ + walletTerms: { + key: ONYXKEYS.WALLET_TERMS, + }, +})(TermsStep); From 744a3b5f380317b65ac91925a7e1f37d36273a79 Mon Sep 17 00:00:00 2001 From: Jayesh Mangwani Date: Tue, 30 Jan 2024 20:19:34 +0530 Subject: [PATCH 0133/1208] TS-migration: ShortTermsForm Page --- .../{ShortTermsForm.js => ShortTermsForm.tsx} | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) rename src/pages/EnablePayments/TermsPage/{ShortTermsForm.js => ShortTermsForm.tsx} (93%) diff --git a/src/pages/EnablePayments/TermsPage/ShortTermsForm.js b/src/pages/EnablePayments/TermsPage/ShortTermsForm.tsx similarity index 93% rename from src/pages/EnablePayments/TermsPage/ShortTermsForm.js rename to src/pages/EnablePayments/TermsPage/ShortTermsForm.tsx index 40824f47b036..1c9d37bb30e9 100644 --- a/src/pages/EnablePayments/TermsPage/ShortTermsForm.js +++ b/src/pages/EnablePayments/TermsPage/ShortTermsForm.tsx @@ -5,19 +5,16 @@ import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as CurrencyUtils from '@libs/CurrencyUtils'; -import userWalletPropTypes from '@pages/EnablePayments/userWalletPropTypes'; import CONST from '@src/CONST'; +import type {OnyxEntry} from 'react-native-onyx'; +import {UserWallet} from '@src/types/onyx'; -const propTypes = { - /** The user's wallet */ - userWallet: userWalletPropTypes, +type ShortTermsFormProps = { + /** The user's wallet */ + userWallet: OnyxEntry; }; -const defaultProps = { - userWallet: {}, -}; - -function ShortTermsForm(props) { +function ShortTermsForm(props: ShortTermsFormProps) { const styles = useThemeStyles(); const {translate, numberFormat} = useLocalize(); return ( @@ -25,7 +22,7 @@ function ShortTermsForm(props) { {translate('termsStep.shortTermsForm.expensifyPaymentsAccount', { walletProgram: - props.userWallet.walletProgramID === CONST.WALLET.MTL_WALLET_PROGRAM_ID ? CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS : CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK, + props.userWallet?.walletProgramID === CONST.WALLET.MTL_WALLET_PROGRAM_ID ? CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS : CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK, })} @@ -150,8 +147,6 @@ function ShortTermsForm(props) { ); } -ShortTermsForm.propTypes = propTypes; -ShortTermsForm.defaultProps = defaultProps; ShortTermsForm.displayName = 'ShortTermsForm'; export default ShortTermsForm; From 3f86b2eb3d06eef77cfc51c34a51b4945dddc1a3 Mon Sep 17 00:00:00 2001 From: Jayesh Mangwani Date: Tue, 30 Jan 2024 20:22:23 +0530 Subject: [PATCH 0134/1208] TS-migration: LongTermsForm Page --- .../TermsPage/{LongTermsForm.js => LongTermsForm.tsx} | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) rename src/pages/EnablePayments/TermsPage/{LongTermsForm.js => LongTermsForm.tsx} (98%) diff --git a/src/pages/EnablePayments/TermsPage/LongTermsForm.js b/src/pages/EnablePayments/TermsPage/LongTermsForm.tsx similarity index 98% rename from src/pages/EnablePayments/TermsPage/LongTermsForm.js rename to src/pages/EnablePayments/TermsPage/LongTermsForm.tsx index fad19c5ecf6f..ec89856642d9 100644 --- a/src/pages/EnablePayments/TermsPage/LongTermsForm.js +++ b/src/pages/EnablePayments/TermsPage/LongTermsForm.tsx @@ -66,7 +66,7 @@ function LongTermsForm() { ]; const getLongTermsSections = () => - _.map(termsData, (section, index) => ( + termsData.map((section, index) => ( // eslint-disable-next-line react/no-array-index-key @@ -105,7 +105,6 @@ function LongTermsForm() { Date: Tue, 30 Jan 2024 20:24:10 +0530 Subject: [PATCH 0135/1208] TS-migration: FailedKYC Page --- .../{FailedKYC.js => FailedKYC.tsx} | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) rename src/pages/EnablePayments/{FailedKYC.js => FailedKYC.tsx} (65%) diff --git a/src/pages/EnablePayments/FailedKYC.js b/src/pages/EnablePayments/FailedKYC.tsx similarity index 65% rename from src/pages/EnablePayments/FailedKYC.js rename to src/pages/EnablePayments/FailedKYC.tsx index fc54ea9c1074..25672772c216 100644 --- a/src/pages/EnablePayments/FailedKYC.js +++ b/src/pages/EnablePayments/FailedKYC.tsx @@ -2,35 +2,31 @@ import React from 'react'; import {View} from 'react-native'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; +import useLocalize from '@hooks/useLocalize'; -const propTypes = { - ...withLocalizePropTypes, -}; - -function FailedKYC(props) { +function FailedKYC() { + const {translate} = useLocalize(); const styles = useThemeStyles(); return ( - {props.translate('additionalDetailsStep.failedKYCTextBefore')} + {translate('additionalDetailsStep.failedKYCTextBefore')} {CONST.EMAIL.CONCIERGE} - {props.translate('additionalDetailsStep.failedKYCTextAfter')} + {translate('additionalDetailsStep.failedKYCTextAfter')} ); } -FailedKYC.propTypes = propTypes; FailedKYC.displayName = 'FailedKYC'; -export default withLocalize(FailedKYC); +export default FailedKYC; From b4c5a8a9d66a629167114fd56c2bb68850a27d11 Mon Sep 17 00:00:00 2001 From: Jayesh Mangwani Date: Tue, 30 Jan 2024 20:38:51 +0530 Subject: [PATCH 0136/1208] TS-migration: ActivateStep Page --- src/pages/EnablePayments/ActivateStep.js | 72 ----------------------- src/pages/EnablePayments/ActivateStep.tsx | 62 +++++++++++++++++++ 2 files changed, 62 insertions(+), 72 deletions(-) delete mode 100644 src/pages/EnablePayments/ActivateStep.js create mode 100644 src/pages/EnablePayments/ActivateStep.tsx diff --git a/src/pages/EnablePayments/ActivateStep.js b/src/pages/EnablePayments/ActivateStep.js deleted file mode 100644 index 92342c28af73..000000000000 --- a/src/pages/EnablePayments/ActivateStep.js +++ /dev/null @@ -1,72 +0,0 @@ -import React from 'react'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import ConfirmationPage from '@components/ConfirmationPage'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import LottieAnimations from '@components/LottieAnimations'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import compose from '@libs/compose'; -import * as PaymentMethods from '@userActions/PaymentMethods'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import userWalletPropTypes from './userWalletPropTypes'; -import walletTermsPropTypes from './walletTermsPropTypes'; - -const propTypes = { - ...withLocalizePropTypes, - - /** The user's wallet */ - userWallet: userWalletPropTypes, - - /** Information about the user accepting the terms for payments */ - walletTerms: walletTermsPropTypes, -}; - -const defaultProps = { - userWallet: {}, - walletTerms: { - source: '', - chatReportID: 0, - }, -}; - -function ActivateStep(props) { - const isActivatedWallet = _.contains([CONST.WALLET.TIER_NAME.GOLD, CONST.WALLET.TIER_NAME.PLATINUM], props.userWallet.tierName); - const animation = isActivatedWallet ? LottieAnimations.Fireworks : LottieAnimations.ReviewingBankInfo; - let continueButtonText = ''; - - if (props.walletTerms.chatReportID) { - continueButtonText = props.translate('activateStep.continueToPayment'); - } else if (props.walletTerms.source === CONST.KYC_WALL_SOURCE.ENABLE_WALLET) { - continueButtonText = props.translate('common.continue'); - } else { - continueButtonText = props.translate('activateStep.continueToTransfer'); - } - - return ( - <> - - PaymentMethods.continueSetup()} - /> - - ); -} - -ActivateStep.propTypes = propTypes; -ActivateStep.defaultProps = defaultProps; -ActivateStep.displayName = 'ActivateStep'; - -export default compose( - withLocalize, - withOnyx({ - walletTerms: { - key: ONYXKEYS.WALLET_TERMS, - }, - }), -)(ActivateStep); diff --git a/src/pages/EnablePayments/ActivateStep.tsx b/src/pages/EnablePayments/ActivateStep.tsx new file mode 100644 index 000000000000..d16190f0c0af --- /dev/null +++ b/src/pages/EnablePayments/ActivateStep.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import {withOnyx} from 'react-native-onyx'; +import ConfirmationPage from '@components/ConfirmationPage'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import LottieAnimations from '@components/LottieAnimations'; +import * as PaymentMethods from '@userActions/PaymentMethods'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import useLocalize from '@hooks/useLocalize'; +import type {OnyxEntry} from 'react-native-onyx'; +import {WalletTerms, UserWallet} from '@src/types/onyx'; + +type ActivateStepOnyxProps = { + /** Information about the user accepting the terms for payments */ + walletTerms: OnyxEntry; +}; + +type ActivateStepProps = ActivateStepOnyxProps & { + /** The user's wallet */ + userWallet: OnyxEntry; +}; + +function ActivateStep({ + userWallet, + walletTerms +}: ActivateStepProps) { + const {translate} = useLocalize(); + const isActivatedWallet = userWallet?.tierName && [CONST.WALLET.TIER_NAME.GOLD, CONST.WALLET.TIER_NAME.PLATINUM].some((name) => name === userWallet.tierName); + + const animation = isActivatedWallet ? LottieAnimations.Fireworks : LottieAnimations.ReviewingBankInfo; + let continueButtonText = ''; + + if (walletTerms?.chatReportID) { + continueButtonText = translate('activateStep.continueToPayment'); + } else if (walletTerms?.source === CONST.KYC_WALL_SOURCE.ENABLE_WALLET) { + continueButtonText = translate('common.continue'); + } else { + continueButtonText = translate('activateStep.continueToTransfer'); + } + + return ( + <> + + PaymentMethods.continueSetup()} + /> + + ); +} + +ActivateStep.displayName = 'ActivateStep'; + +export default withOnyx({ + walletTerms: { + key: ONYXKEYS.WALLET_TERMS, + }, +})(ActivateStep); \ No newline at end of file From e48ebb1d332c795947548c4027161b791b5869ff Mon Sep 17 00:00:00 2001 From: Toby Sullivan Date: Tue, 30 Jan 2024 09:46:28 -0800 Subject: [PATCH 0137/1208] Display report description for 1-to-1 money request --- src/libs/ReportUtils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index edcd57078e0c..eeebe7e8b78a 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2247,7 +2247,7 @@ function getReportPreviewMessage( return Localize.translateLocal('iou.waitingOnBankAccount', {submitterDisplayName}); } - const containsNonReimbursable = hasNonReimbursableTransactions(report.reportID); + const comment = !isEmptyObject(linkedTransaction) ? TransactionUtils.getDescription(linkedTransaction) : undefined; const lastActorID = reportAction?.actorAccountID; @@ -2259,14 +2259,14 @@ function getReportPreviewMessage( // We only want to show the actor name in the preview if it's not the current user who took the action const requestorName = lastActorID && lastActorID !== currentUserAccountID ? getDisplayNameForParticipant(lastActorID, !isPreviewMessageForParentChatReport) : ''; - return `${requestorName ? `${requestorName}: ` : ''}${Localize.translateLocal('iou.requestedAmount', {formattedAmount: amountToDisplay})}`; + return `${requestorName ? `${requestorName}: ` : ''}${Localize.translateLocal('iou.requestedAmount', {formattedAmount: amountToDisplay, comment})}`; } + const containsNonReimbursable = hasNonReimbursableTransactions(report.reportID); if (containsNonReimbursable) { return Localize.translateLocal('iou.payerSpentAmount', {payer: payerName ?? '', amount: formattedAmount}); } - const comment = !isEmptyObject(linkedTransaction) ? TransactionUtils.getDescription(linkedTransaction) : undefined; return Localize.translateLocal('iou.payerOwesAmount', {payer: payerName ?? '', amount: formattedAmount, comment}); } From 0ac0bedbd17cf7e86935e71828efd1175a7ac131 Mon Sep 17 00:00:00 2001 From: Jayesh Mangwani Date: Wed, 31 Jan 2024 01:59:28 +0530 Subject: [PATCH 0138/1208] TS-migration: OnfidoStep & OnfidoPrivacy Page --- src/pages/EnablePayments/ActivateStep.tsx | 13 ++-- src/pages/EnablePayments/FailedKYC.tsx | 2 +- .../{OnfidoPrivacy.js => OnfidoPrivacy.tsx} | 64 +++++++++---------- .../{OnfidoStep.js => OnfidoStep.tsx} | 35 +++++----- .../TermsPage/ShortTermsForm.tsx | 8 ++- src/pages/EnablePayments/TermsStep.tsx | 12 ++-- 6 files changed, 64 insertions(+), 70 deletions(-) rename src/pages/EnablePayments/{OnfidoPrivacy.js => OnfidoPrivacy.tsx} (74%) rename src/pages/EnablePayments/{OnfidoStep.js => OnfidoStep.tsx} (69%) diff --git a/src/pages/EnablePayments/ActivateStep.tsx b/src/pages/EnablePayments/ActivateStep.tsx index d16190f0c0af..0dbb98e53a5f 100644 --- a/src/pages/EnablePayments/ActivateStep.tsx +++ b/src/pages/EnablePayments/ActivateStep.tsx @@ -1,14 +1,14 @@ import React from 'react'; import {withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import ConfirmationPage from '@components/ConfirmationPage'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import LottieAnimations from '@components/LottieAnimations'; +import useLocalize from '@hooks/useLocalize'; import * as PaymentMethods from '@userActions/PaymentMethods'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import useLocalize from '@hooks/useLocalize'; -import type {OnyxEntry} from 'react-native-onyx'; -import {WalletTerms, UserWallet} from '@src/types/onyx'; +import {UserWallet, WalletTerms} from '@src/types/onyx'; type ActivateStepOnyxProps = { /** Information about the user accepting the terms for payments */ @@ -20,10 +20,7 @@ type ActivateStepProps = ActivateStepOnyxProps & { userWallet: OnyxEntry; }; -function ActivateStep({ - userWallet, - walletTerms -}: ActivateStepProps) { +function ActivateStep({userWallet, walletTerms}: ActivateStepProps) { const {translate} = useLocalize(); const isActivatedWallet = userWallet?.tierName && [CONST.WALLET.TIER_NAME.GOLD, CONST.WALLET.TIER_NAME.PLATINUM].some((name) => name === userWallet.tierName); @@ -59,4 +56,4 @@ export default withOnyx({ walletTerms: { key: ONYXKEYS.WALLET_TERMS, }, -})(ActivateStep); \ No newline at end of file +})(ActivateStep); diff --git a/src/pages/EnablePayments/FailedKYC.tsx b/src/pages/EnablePayments/FailedKYC.tsx index 25672772c216..6b393229d62f 100644 --- a/src/pages/EnablePayments/FailedKYC.tsx +++ b/src/pages/EnablePayments/FailedKYC.tsx @@ -2,9 +2,9 @@ import React from 'react'; import {View} from 'react-native'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; -import useLocalize from '@hooks/useLocalize'; function FailedKYC() { const {translate} = useLocalize(); diff --git a/src/pages/EnablePayments/OnfidoPrivacy.js b/src/pages/EnablePayments/OnfidoPrivacy.tsx similarity index 74% rename from src/pages/EnablePayments/OnfidoPrivacy.js rename to src/pages/EnablePayments/OnfidoPrivacy.tsx index 77b884fb2934..d8e9f616b8a7 100644 --- a/src/pages/EnablePayments/OnfidoPrivacy.js +++ b/src/pages/EnablePayments/OnfidoPrivacy.tsx @@ -1,7 +1,8 @@ import lodashGet from 'lodash/get'; import React, {useRef} from 'react'; -import {View} from 'react-native'; +import {ScrollView, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import _ from 'underscore'; import FixedFooter from '@components/FixedFooter'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; @@ -9,43 +10,41 @@ import FormScrollView from '@components/FormScrollView'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import * as ErrorUtils from '@libs/ErrorUtils'; import * as BankAccounts from '@userActions/BankAccounts'; import ONYXKEYS from '@src/ONYXKEYS'; -import walletOnfidoDataPropTypes from './walletOnfidoDataPropTypes'; +import {WalletOnfido} from '@src/types/onyx'; -const propTypes = { - /** Stores various information used to build the UI and call any APIs */ - walletOnfidoData: walletOnfidoDataPropTypes, - - ...withLocalizePropTypes, +const DEFAULT_WALLET_ONFIDO_DATA = { + applicantID: '', + sdkToken: '', + loading: false, + errors: {}, + fixableErrors: [], + hasAcceptedPrivacyPolicy: false, }; -const defaultProps = { - walletOnfidoData: { - applicantID: '', - sdkToken: '', - loading: false, - errors: {}, - fixableErrors: [], - hasAcceptedPrivacyPolicy: false, - }, +type OnfidoPrivacyOnyxProps = { + /** Stores various information used to build the UI and call any APIs */ + walletOnfidoData: OnyxEntry; }; -function OnfidoPrivacy({walletOnfidoData, translate, form}) { +type OnfidoPrivacyProps = OnfidoPrivacyOnyxProps & {}; + +function OnfidoPrivacy({walletOnfidoData = DEFAULT_WALLET_ONFIDO_DATA}: OnfidoPrivacyProps) { + const {translate} = useLocalize(); const styles = useThemeStyles(); - const {isLoading = false, hasAcceptedPrivacyPolicy} = walletOnfidoData; + const {isLoading = false, hasAcceptedPrivacyPolicy} = walletOnfidoData ?? {}; - const formRef = useRef(null); + const formRef = useRef(null); const openOnfidoFlow = () => { BankAccounts.openOnfidoFlow(); }; - let onfidoError = ErrorUtils.getLatestErrorMessage(walletOnfidoData) || ''; + let onfidoError = ErrorUtils.getLatestErrorMessage(walletOnfidoData ?? {}) || ''; const onfidoFixableErrors = lodashGet(walletOnfidoData, 'fixableErrors', []); onfidoError += !_.isEmpty(onfidoFixableErrors) ? `\n${onfidoFixableErrors.join('\n')}` : ''; @@ -70,7 +69,7 @@ function OnfidoPrivacy({walletOnfidoData, translate, form}) { isAlertVisible={Boolean(onfidoError)} onSubmit={openOnfidoFlow} onFixTheErrorsLinkPressed={() => { - form.scrollTo({y: 0, animated: true}); + formRef.current?.scrollTo({y: 0, animated: true}); }} message={onfidoError} isLoading={isLoading} @@ -85,18 +84,13 @@ function OnfidoPrivacy({walletOnfidoData, translate, form}) { ); } -OnfidoPrivacy.propTypes = propTypes; -OnfidoPrivacy.defaultProps = defaultProps; OnfidoPrivacy.displayName = 'OnfidoPrivacy'; -export default compose( - withLocalize, - withOnyx({ - walletOnfidoData: { - key: ONYXKEYS.WALLET_ONFIDO, +export default withOnyx({ + walletOnfidoData: { + key: ONYXKEYS.WALLET_ONFIDO, - // Let's get a new onfido token each time the user hits this flow (as it should only be once) - initWithStoredValues: false, - }, - }), -)(OnfidoPrivacy); + // Let's get a new onfido token each time the user hits this flow (as it should only be once) + initWithStoredValues: false, + }, +})(OnfidoPrivacy); diff --git a/src/pages/EnablePayments/OnfidoStep.js b/src/pages/EnablePayments/OnfidoStep.tsx similarity index 69% rename from src/pages/EnablePayments/OnfidoStep.js rename to src/pages/EnablePayments/OnfidoStep.tsx index 8b40c88f62fb..5e36f2f302a6 100644 --- a/src/pages/EnablePayments/OnfidoStep.js +++ b/src/pages/EnablePayments/OnfidoStep.tsx @@ -1,7 +1,9 @@ import React, {useCallback} from 'react'; import {withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +// @ts-expect-error TODO: Remove this once Onfido (https://github.com/Expensify/App/issues/25136) is migrated to TypeScript. import Onfido from '@components/Onfido'; import useLocalize from '@hooks/useLocalize'; import Growl from '@libs/Growl'; @@ -11,25 +13,25 @@ import * as Wallet from '@userActions/Wallet'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import {WalletOnfido} from '@src/types/onyx'; import OnfidoPrivacy from './OnfidoPrivacy'; -import walletOnfidoDataPropTypes from './walletOnfidoDataPropTypes'; -const propTypes = { - /** Stores various information used to build the UI and call any APIs */ - walletOnfidoData: walletOnfidoDataPropTypes, +const DEFAULT_WALLET_ONFIDO_DATA = { + loading: false, + hasAcceptedPrivacyPolicy: false, }; -const defaultProps = { - walletOnfidoData: { - loading: false, - hasAcceptedPrivacyPolicy: false, - }, +type OnfidoStepOnyxProps = { + /** Stores various information used to build the UI and call any APIs */ + walletOnfidoData: OnyxEntry; }; -function OnfidoStep({walletOnfidoData}) { +type OnfidoStepProps = OnfidoStepOnyxProps; + +function OnfidoStep({walletOnfidoData = DEFAULT_WALLET_ONFIDO_DATA}: OnfidoStepProps) { const {translate} = useLocalize(); - const shouldShowOnfido = walletOnfidoData.hasAcceptedPrivacyPolicy && !walletOnfidoData.isLoading && !walletOnfidoData.error && walletOnfidoData.sdkToken; + const shouldShowOnfido = walletOnfidoData?.hasAcceptedPrivacyPolicy && !walletOnfidoData.isLoading && !walletOnfidoData.errors && walletOnfidoData.sdkToken; const goBack = useCallback(() => { Navigation.goBack(ROUTES.HOME); @@ -44,15 +46,16 @@ function OnfidoStep({walletOnfidoData}) { }, [translate]); const verifyIdentity = useCallback( + // @ts-expect-error TODO: Remove this once Onfido (https://github.com/Expensify/App/issues/25136) is migrated to TypeScript. (data) => { BankAccounts.verifyIdentity({ onfidoData: JSON.stringify({ ...data, - applicantID: walletOnfidoData.applicantID, + applicantID: walletOnfidoData?.applicantID, }), }); }, - [walletOnfidoData.applicantID], + [walletOnfidoData?.applicantID], ); return ( @@ -70,18 +73,16 @@ function OnfidoStep({walletOnfidoData}) { onSuccess={verifyIdentity} /> ) : ( - + )} ); } -OnfidoStep.propTypes = propTypes; -OnfidoStep.defaultProps = defaultProps; OnfidoStep.displayName = 'OnfidoStep'; -export default withOnyx({ +export default withOnyx({ walletOnfidoData: { key: ONYXKEYS.WALLET_ONFIDO, diff --git a/src/pages/EnablePayments/TermsPage/ShortTermsForm.tsx b/src/pages/EnablePayments/TermsPage/ShortTermsForm.tsx index 1c9d37bb30e9..b45b8b657a75 100644 --- a/src/pages/EnablePayments/TermsPage/ShortTermsForm.tsx +++ b/src/pages/EnablePayments/TermsPage/ShortTermsForm.tsx @@ -1,16 +1,16 @@ import React from 'react'; import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import CONST from '@src/CONST'; -import type {OnyxEntry} from 'react-native-onyx'; import {UserWallet} from '@src/types/onyx'; type ShortTermsFormProps = { - /** The user's wallet */ + /** The user's wallet */ userWallet: OnyxEntry; }; @@ -22,7 +22,9 @@ function ShortTermsForm(props: ShortTermsFormProps) { {translate('termsStep.shortTermsForm.expensifyPaymentsAccount', { walletProgram: - props.userWallet?.walletProgramID === CONST.WALLET.MTL_WALLET_PROGRAM_ID ? CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS : CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK, + props.userWallet?.walletProgramID === CONST.WALLET.MTL_WALLET_PROGRAM_ID + ? CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS + : CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK, })} diff --git a/src/pages/EnablePayments/TermsStep.tsx b/src/pages/EnablePayments/TermsStep.tsx index a03f3607d56e..cef54ba463e6 100644 --- a/src/pages/EnablePayments/TermsStep.tsx +++ b/src/pages/EnablePayments/TermsStep.tsx @@ -1,25 +1,25 @@ import React, {useEffect, useState} from 'react'; import {ScrollView} from 'react-native'; import {withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import CheckboxWithLabel from '@components/CheckboxWithLabel'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; import * as BankAccounts from '@userActions/BankAccounts'; import ONYXKEYS from '@src/ONYXKEYS'; +import {UserWallet, WalletTerms} from '@src/types/onyx'; import LongTermsForm from './TermsPage/LongTermsForm'; import ShortTermsForm from './TermsPage/ShortTermsForm'; -import useLocalize from '@hooks/useLocalize'; -import type {OnyxEntry} from 'react-native-onyx'; -import {WalletTerms, UserWallet} from '@src/types/onyx'; type TermsStepOnyxProps = { /** Comes from Onyx. Information about the terms for the wallet */ walletTerms: OnyxEntry; -} +}; type TermsStepProps = TermsStepOnyxProps & { /** The user's wallet */ @@ -33,7 +33,7 @@ function TermsStep(props: TermsStepProps) { const [error, setError] = useState(false); const {translate} = useLocalize(); - const errorMessage = error ? 'common.error.acceptTerms' : ErrorUtils.getLatestErrorMessage(props.walletTerms??{}) || ''; + const errorMessage = error ? 'common.error.acceptTerms' : ErrorUtils.getLatestErrorMessage(props.walletTerms ?? {}) || ''; const toggleDisclosure = () => { setHasAcceptedDisclosure(!hasAcceptedDisclosure); @@ -99,7 +99,7 @@ function TermsStep(props: TermsStepProps) { setError(false); BankAccounts.acceptWalletTerms({ hasAcceptedTerms: hasAcceptedDisclosure && hasAcceptedPrivacyPolicyAndWalletAgreement, - reportID: props.walletTerms?.chatReportID??'', + reportID: props.walletTerms?.chatReportID ?? '', }); }} message={errorMessage} From 0feb8c4e71dad1c3cd3d11f7a2c7670387bd09c7 Mon Sep 17 00:00:00 2001 From: Jayesh Mangwani Date: Wed, 31 Jan 2024 04:23:28 +0530 Subject: [PATCH 0139/1208] TS-migration: EnablePaymentsPage Page --- ...PaymentsPage.js => EnablePaymentsPage.tsx} | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) rename src/pages/EnablePayments/{EnablePaymentsPage.js => EnablePaymentsPage.tsx} (78%) diff --git a/src/pages/EnablePayments/EnablePaymentsPage.js b/src/pages/EnablePayments/EnablePaymentsPage.tsx similarity index 78% rename from src/pages/EnablePayments/EnablePaymentsPage.js rename to src/pages/EnablePayments/EnablePaymentsPage.tsx index 257eab1d38d3..6d2defc82df0 100644 --- a/src/pages/EnablePayments/EnablePaymentsPage.js +++ b/src/pages/EnablePayments/EnablePaymentsPage.tsx @@ -1,6 +1,6 @@ import React, {useEffect} from 'react'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {OnyxEntry} from 'react-native-onyx'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -11,28 +11,27 @@ import * as Wallet from '@userActions/Wallet'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {UserWallet} from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import ActivateStep from './ActivateStep'; import AdditionalDetailsStep from './AdditionalDetailsStep'; import FailedKYC from './FailedKYC'; // Steps import OnfidoStep from './OnfidoStep'; import TermsStep from './TermsStep'; -import userWalletPropTypes from './userWalletPropTypes'; -const propTypes = { +type EnablePaymentsPageOnyxProps = { /** The user's wallet */ - userWallet: userWalletPropTypes, + userWallet: OnyxEntry; }; -const defaultProps = { - userWallet: {}, -}; +type EnablePaymentsPageProps = EnablePaymentsPageOnyxProps & {}; -function EnablePaymentsPage({userWallet}) { +function EnablePaymentsPage({userWallet}: EnablePaymentsPageProps) { const {translate} = useLocalize(); const {isOffline} = useNetwork(); - const {isPendingOnfidoResult, hasFailedOnfido} = userWallet; + const {isPendingOnfidoResult, hasFailedOnfido} = userWallet ?? {}; useEffect(() => { if (isOffline) { @@ -47,18 +46,18 @@ function EnablePaymentsPage({userWallet}) { Wallet.openEnablePaymentsPage(); }, [isOffline, isPendingOnfidoResult, hasFailedOnfido]); - if (_.isEmpty(userWallet)) { + if (isEmptyObject(userWallet)) { return ; } return ( {() => { - if (userWallet.errorCode === CONST.WALLET.ERROR.KYC) { + if (userWallet?.errorCode === CONST.WALLET.ERROR.KYC) { return ( <> ({ userWallet: { key: ONYXKEYS.USER_WALLET, From dae08c96c8d7d3008858edf666fa452c6269d310 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Wed, 31 Jan 2024 12:56:40 +0530 Subject: [PATCH 0140/1208] Remove MoneyRequestSelectorPage.js and copy any changes since Nov 27 into IOURequestStartPage.js. Signed-off-by: Krishna Gupta --- ...oraryForRefactorRequestConfirmationList.js | 4 +-- .../AttachmentPickerWithMenuItems.js | 3 +- .../FloatingActionButtonAndPopover.js | 11 ++++-- src/pages/iou/request/IOURequestStartPage.js | 36 ++++++++++--------- ...yForRefactorRequestParticipantsSelector.js | 2 +- .../step/IOURequestStepConfirmation.js | 17 ++++++--- .../step/IOURequestStepParticipants.js | 12 +++++-- 7 files changed, 56 insertions(+), 29 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 2aff0444a59e..f48820654768 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -266,8 +266,8 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ // Do not hide fields in case of send money request const shouldShowAllFields = isDistanceRequest || shouldExpandFields || !shouldShowSmartScanFields || isTypeSend || isEditingSplitBill; - const shouldShowDate = shouldShowSmartScanFields || isDistanceRequest; - const shouldShowMerchant = shouldShowSmartScanFields && !isDistanceRequest; + const shouldShowDate = (shouldShowSmartScanFields || isDistanceRequest) && !isTypeSend; + const shouldShowMerchant = shouldShowSmartScanFields && !isDistanceRequest && !isTypeSend; // Fetches the first tag list of the policy const policyTag = PolicyUtils.getTag(policyTags); diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js index 444dd939142b..4091016baeeb 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js @@ -20,7 +20,6 @@ import * as Browser from '@libs/Browser'; import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; -import * as IOU from '@userActions/IOU'; import * as Report from '@userActions/Report'; import * as Task from '@userActions/Task'; import CONST from '@src/CONST'; @@ -154,7 +153,7 @@ function AttachmentPickerWithMenuItems({ [CONST.IOU.TYPE.SEND]: { icon: Expensicons.Send, text: translate('iou.sendMoney'), - onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.SEND, report.reportID), + onSelected: () => Navigation.navigate(ROUTES.MONEY_REQUEST_CREATE.getRoute(CONST.IOU.TYPE.SEND, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, report.reportID)), }, }; diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js index bbcdc5cebef4..4ab4e5fbb081 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js @@ -15,7 +15,6 @@ import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; import * as App from '@userActions/App'; -import * as IOU from '@userActions/IOU'; import * as Policy from '@userActions/Policy'; import * as Session from '@userActions/Session'; import * as Task from '@userActions/Task'; @@ -182,7 +181,15 @@ function FloatingActionButtonAndPopover(props) { { icon: Expensicons.Send, text: props.translate('iou.sendMoney'), - onSelected: () => interceptAnonymousUser(() => IOU.startMoneyRequest(CONST.IOU.TYPE.SEND)), + // onSelected: () => interceptAnonymousUser(() => IOU.startMoneyRequest(CONST.IOU.TYPE.SEND)), + onSelected: () => + interceptAnonymousUser(() => + Navigation.navigate( + // When starting to create a send money request from the global FAB, there is not an existing report yet. A random optimistic reportID is generated and used + // for all of the routes in the creation flow. + ROUTES.MONEY_REQUEST_CREATE.getRoute(CONST.IOU.TYPE.SEND, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, ReportUtils.generateReportID()), + ), + ), }, ...[ { diff --git a/src/pages/iou/request/IOURequestStartPage.js b/src/pages/iou/request/IOURequestStartPage.js index 3d80ab89347d..c790b30c5f7d 100644 --- a/src/pages/iou/request/IOURequestStartPage.js +++ b/src/pages/iou/request/IOURequestStartPage.js @@ -156,22 +156,26 @@ function IOURequestStartPage({ title={tabTitles[iouType]} onBackButtonPress={navigateBack} /> - ( - - )} - > - {() => } - {() => } - {shouldDisplayDistanceRequest && {() => }} - + {iouType === CONST.IOU.TYPE.REQUEST || iouType === CONST.IOU.TYPE.SPLIT ? ( + ( + + )} + > + {() => } + {() => } + {shouldDisplayDistanceRequest && {() => }} + + ) : ( + + )} diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 15f98205839e..5d84b0d81b68 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -256,7 +256,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ // the app from crashing on native when you try to do this, we'll going to hide the button if you have a workspace and other participants const hasPolicyExpenseChatParticipant = _.some(participants, (participant) => participant.isPolicyExpenseChat); const shouldShowSplitBillErrorMessage = participants.length > 1 && hasPolicyExpenseChatParticipant; - const isAllowedToSplit = iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE; + const isAllowedToSplit = iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE && iouType !== CONST.IOU.TYPE.SEND; const referralContentType = iouType === CONST.IOU.TYPE.SEND ? CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY : CONST.REFERRAL_PROGRAM.CONTENT_TYPES.MONEY_REQUEST; const handleConfirmSelection = useCallback(() => { diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.js b/src/pages/iou/request/step/IOURequestStepConfirmation.js index 6028a735d132..bdee619dba9f 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.js +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.js @@ -92,7 +92,15 @@ 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 = () => { + if (iouType === CONST.IOU.TYPE.SPLIT) { + return translate('iou.split'); + } + if (iouType === CONST.IOU.TYPE.SEND) { + return translate('common.send'); + } + return translate(TransactionUtils.getHeaderTitleTranslationKey(transaction)); + }; const participants = useMemo( () => _.map(transaction.participants, (participant) => { @@ -287,7 +295,7 @@ function IOURequestStepConfirmation({ const sendMoney = useCallback( (paymentMethodType) => { const currency = transaction.currency; - const trimmedComment = transaction.comment.trim(); + const trimmedComment = lodashGet(transaction, 'comment.comment', '').trim(); const participant = participants[0]; if (paymentMethodType === CONST.IOU.PAYMENT_TYPE.ELSEWHERE) { @@ -299,8 +307,9 @@ function IOURequestStepConfirmation({ IOU.sendMoneyWithWallet(report, transaction.amount, currency, trimmedComment, currentUserPersonalDetails.accountID, participant); } }, - [transaction.amount, transaction.comment, participants, transaction.currency, currentUserPersonalDetails.accountID, report], + [transaction, participants, currentUserPersonalDetails.accountID, report], ); + const addNewParticipant = (option) => { const newParticipants = _.map(transaction.participants, (participant) => { if (participant.accountID === option.accountID) { @@ -327,7 +336,7 @@ function IOURequestStepConfirmation({ {({safeAreaPaddingBottomStyle}) => ( { + if (iouType === CONST.IOU.TYPE.SPLIT) { + return translate('iou.split'); + } + if (iouType === CONST.IOU.TYPE.SEND) { + return translate('common.send'); + } + translate(TransactionUtils.getHeaderTitleTranslationKey(transaction)); + }; const receiptFilename = lodashGet(transaction, 'filename'); const receiptPath = lodashGet(transaction, 'receipt.source'); @@ -81,7 +89,7 @@ function IOURequestStepParticipants({ return ( Date: Thu, 1 Feb 2024 17:26:12 +0100 Subject: [PATCH 0141/1208] 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 0142/1208] 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 0143/1208] 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 0144/1208] 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 cdaa1aac1a3c6916bfd2bfb1452db2c36f6100be Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Sat, 3 Feb 2024 16:27:01 +0530 Subject: [PATCH 0145/1208] remove redundant code and old component. Signed-off-by: Krishna Gupta --- .../AppNavigator/ModalStackNavigators.tsx | 1 - src/libs/Navigation/linkingConfig/config.ts | 18 -- src/libs/Navigation/types.ts | 1 - .../FloatingActionButtonAndPopover.js | 2 +- src/pages/iou/MoneyRequestSelectorPage.js | 169 ------------------ ...yForRefactorRequestParticipantsSelector.js | 1 - 6 files changed, 1 insertion(+), 191 deletions(-) delete mode 100644 src/pages/iou/MoneyRequestSelectorPage.js diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx index 4606f867c3fc..60b87fd85f3d 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx @@ -93,7 +93,6 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator require('../../../pages/iou/request/step/IOURequestStepScan').default as React.ComponentType, [SCREENS.MONEY_REQUEST.STEP_TAG]: () => require('../../../pages/iou/request/step/IOURequestStepTag').default as React.ComponentType, [SCREENS.MONEY_REQUEST.STEP_WAYPOINT]: () => require('../../../pages/iou/request/step/IOURequestStepWaypoint').default as React.ComponentType, - [SCREENS.MONEY_REQUEST.ROOT]: () => require('../../../pages/iou/MoneyRequestSelectorPage').default as React.ComponentType, [SCREENS.MONEY_REQUEST.AMOUNT]: () => require('../../../pages/iou/steps/NewRequestAmountPage').default as React.ComponentType, [SCREENS.MONEY_REQUEST.PARTICIPANTS]: () => require('../../../pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage').default as React.ComponentType, [SCREENS.MONEY_REQUEST.CONFIRMATION]: () => require('../../../pages/iou/steps/MoneyRequestConfirmPage').default as React.ComponentType, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index f1c9c316fe93..72c2c9d305f3 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -379,24 +379,6 @@ const config: LinkingOptions['config'] = { [SCREENS.MONEY_REQUEST.STEP_SCAN]: ROUTES.MONEY_REQUEST_STEP_SCAN.route, [SCREENS.MONEY_REQUEST.STEP_TAG]: ROUTES.MONEY_REQUEST_STEP_TAG.route, [SCREENS.MONEY_REQUEST.STEP_WAYPOINT]: ROUTES.MONEY_REQUEST_STEP_WAYPOINT.route, - [SCREENS.MONEY_REQUEST.ROOT]: { - path: ROUTES.MONEY_REQUEST.route, - exact: true, - screens: { - [SCREENS.MONEY_REQUEST.MANUAL_TAB]: { - path: ROUTES.MONEY_REQUEST_MANUAL_TAB, - exact: true, - }, - [SCREENS.MONEY_REQUEST.SCAN_TAB]: { - path: ROUTES.MONEY_REQUEST_SCAN_TAB, - exact: true, - }, - [SCREENS.MONEY_REQUEST.DISTANCE_TAB]: { - path: ROUTES.MONEY_REQUEST_DISTANCE_TAB.route, - exact: true, - }, - }, - }, [SCREENS.MONEY_REQUEST.AMOUNT]: ROUTES.MONEY_REQUEST_AMOUNT.route, [SCREENS.MONEY_REQUEST.STEP_TAX_AMOUNT]: ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.route, [SCREENS.MONEY_REQUEST.STEP_TAX_RATE]: ROUTES.MONEY_REQUEST_STEP_TAX_RATE.route, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 3c4cf17853f1..13eeb972305a 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -193,7 +193,6 @@ type RoomInviteNavigatorParamList = { }; type MoneyRequestNavigatorParamList = { - [SCREENS.MONEY_REQUEST.ROOT]: undefined; [SCREENS.MONEY_REQUEST.AMOUNT]: undefined; [SCREENS.MONEY_REQUEST.PARTICIPANTS]: { iouType: string; diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js index 7a3390ab5478..b2844374b6ae 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js @@ -174,7 +174,7 @@ function FloatingActionButtonAndPopover(props) { }, { icon: Expensicons.Send, - text: props.translate('iou.sendMoney'), + text: translate('iou.sendMoney'), // onSelected: () => interceptAnonymousUser(() => IOU.startMoneyRequest(CONST.IOU.TYPE.SEND)), onSelected: () => interceptAnonymousUser(() => diff --git a/src/pages/iou/MoneyRequestSelectorPage.js b/src/pages/iou/MoneyRequestSelectorPage.js deleted file mode 100644 index 0a0efc38313a..000000000000 --- a/src/pages/iou/MoneyRequestSelectorPage.js +++ /dev/null @@ -1,169 +0,0 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; -import React, {useEffect, useState} from 'react'; -import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; -import DragAndDropProvider from '@components/DragAndDrop/Provider'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import ScreenWrapper from '@components/ScreenWrapper'; -import TabSelector from '@components/TabSelector/TabSelector'; -import useLocalize from '@hooks/useLocalize'; -import usePrevious from '@hooks/usePrevious'; -import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; -import * as DeviceCapabilities from '@libs/DeviceCapabilities'; -import * as IOUUtils from '@libs/IOUUtils'; -import Navigation from '@libs/Navigation/Navigation'; -import OnyxTabNavigator, {TopTab} from '@libs/Navigation/OnyxTabNavigator'; -import * as ReportUtils from '@libs/ReportUtils'; -import withReportOrNotFound from '@pages/home/report/withReportOrNotFound'; -import reportPropTypes from '@pages/reportPropTypes'; -import * as IOU from '@userActions/IOU'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import NewDistanceRequestPage from './NewDistanceRequestPage'; -import IOURequestStepScan from './request/step/IOURequestStepScan'; -import NewRequestAmountPage from './steps/NewRequestAmountPage'; - -const propTypes = { - /** React Navigation route */ - route: PropTypes.shape({ - /** Params from the route */ - params: PropTypes.shape({ - /** The type of IOU report, i.e. bill, request, send */ - iouType: PropTypes.string, - - /** The report ID of the IOU */ - reportID: PropTypes.string, - }), - }).isRequired, - - /** Report on which the money request is being created */ - report: reportPropTypes, - - /** The policy tied to the report */ - policy: PropTypes.shape({ - /** Type of the policy */ - type: PropTypes.string, - }), - - /** Which tab has been selected */ - selectedTab: PropTypes.string, -}; - -const defaultProps = { - selectedTab: CONST.TAB_REQUEST.SCAN, - report: {}, - policy: {}, -}; - -function MoneyRequestSelectorPage(props) { - const styles = useThemeStyles(); - const [isDraggingOver, setIsDraggingOver] = useState(false); - - const iouType = lodashGet(props.route, 'params.iouType', ''); - const reportID = lodashGet(props.route, 'params.reportID', ''); - const {translate} = useLocalize(); - - const title = { - [CONST.IOU.TYPE.REQUEST]: translate('iou.requestMoney'), - [CONST.IOU.TYPE.SEND]: translate('iou.sendMoney'), - [CONST.IOU.TYPE.SPLIT]: translate('iou.splitBill'), - }; - const isFromGlobalCreate = !reportID; - const isExpenseChat = ReportUtils.isPolicyExpenseChat(props.report); - const isExpenseReport = ReportUtils.isExpenseReport(props.report); - const shouldDisplayDistanceRequest = isExpenseChat || isExpenseReport || isFromGlobalCreate; - - const resetMoneyRequestInfo = () => { - const moneyRequestID = `${iouType}${reportID}`; - IOU.resetMoneyRequestInfo(moneyRequestID); - }; - - // 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(props.report.reportID) || ReportUtils.canCreateRequest(props.report, props.policy, iouType); - const prevSelectedTab = usePrevious(props.selectedTab); - - useEffect(() => { - if (prevSelectedTab === props.selectedTab) { - return; - } - - resetMoneyRequestInfo(); - // resetMoneyRequestInfo function is not added as dependencies since they don't change between renders - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props.selectedTab, prevSelectedTab]); - - return ( - - {({safeAreaPaddingBottomStyle}) => ( - - - - - {iouType === CONST.IOU.TYPE.REQUEST || iouType === CONST.IOU.TYPE.SPLIT ? ( - ( - - )} - > - - {() => } - {shouldDisplayDistanceRequest && ( - - )} - - ) : ( - - )} - - - - )} - - ); -} - -MoneyRequestSelectorPage.propTypes = propTypes; -MoneyRequestSelectorPage.defaultProps = defaultProps; -MoneyRequestSelectorPage.displayName = 'MoneyRequestSelectorPage'; - -export default compose( - withReportOrNotFound(false), - withOnyx({ - selectedTab: { - key: `${ONYXKEYS.COLLECTION.SELECTED_TAB}${CONST.TAB.RECEIPT_TAB_ID}`, - }, - policy: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${lodashGet(report, 'policyID')}`, - }, - }), -)(MoneyRequestSelectorPage); diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 6e9de5b62a7e..a4807572bf56 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -258,7 +258,6 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ const hasPolicyExpenseChatParticipant = _.some(participants, (participant) => participant.isPolicyExpenseChat); const shouldShowSplitBillErrorMessage = participants.length > 1 && hasPolicyExpenseChatParticipant; const isAllowedToSplit = iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE && iouType !== CONST.IOU.TYPE.SEND; - const referralContentType = iouType === CONST.IOU.TYPE.SEND ? CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY : CONST.REFERRAL_PROGRAM.CONTENT_TYPES.MONEY_REQUEST; const handleConfirmSelection = useCallback(() => { if (shouldShowSplitBillErrorMessage) { From b915c4ef809709a29734f0a67a51d423b1eb8d2b Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Sat, 3 Feb 2024 16:53:52 +0530 Subject: [PATCH 0146/1208] remove redundant code and comment Signed-off-by: Krishna Gupta --- src/libs/actions/IOU.js | 11 ----------- .../SidebarScreen/FloatingActionButtonAndPopover.js | 1 - 2 files changed, 12 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 4db89a1e926b..053d48b7ed0f 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -3572,16 +3572,6 @@ function setMoneyRequestParticipantsFromReport(transactionID, report) { Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {participants, participantsAutoAssigned: true}); } -/** - * Initialize money request info and navigate to the MoneyRequest page - * @param {String} iouType - * @param {String} reportID - */ -function startMoneyRequest(iouType, reportID = '') { - resetMoneyRequestInfo(`${iouType}${reportID}`); - Navigation.navigate(ROUTES.MONEY_REQUEST.getRoute(iouType, reportID)); -} - /** * @param {String} id */ @@ -3799,7 +3789,6 @@ export { submitReport, payMoneyRequest, sendMoneyWithWallet, - startMoneyRequest, startMoneyRequest_temporaryForRefactor, resetMoneyRequestCategory, resetMoneyRequestCategory_temporaryForRefactor, diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js index b2844374b6ae..52251da7a67a 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js @@ -175,7 +175,6 @@ function FloatingActionButtonAndPopover(props) { { icon: Expensicons.Send, text: translate('iou.sendMoney'), - // onSelected: () => interceptAnonymousUser(() => IOU.startMoneyRequest(CONST.IOU.TYPE.SEND)), onSelected: () => interceptAnonymousUser(() => Navigation.navigate( From 9bd67d67aee253e4614ef64854169229282f806d Mon Sep 17 00:00:00 2001 From: Jayesh Mangwani Date: Mon, 5 Feb 2024 16:14:46 +0530 Subject: [PATCH 0147/1208] merge main, fix lint --- src/pages/EnablePayments/ActivateStep.tsx | 2 +- .../EnablePayments/EnablePaymentsPage.tsx | 3 ++- src/pages/EnablePayments/OnfidoPrivacy.tsx | 23 +++++++++++-------- src/pages/EnablePayments/OnfidoStep.tsx | 2 +- .../TermsPage/LongTermsForm.tsx | 1 - .../TermsPage/ShortTermsForm.tsx | 2 +- src/pages/EnablePayments/TermsStep.tsx | 2 +- 7 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/pages/EnablePayments/ActivateStep.tsx b/src/pages/EnablePayments/ActivateStep.tsx index 0dbb98e53a5f..e0bea7488140 100644 --- a/src/pages/EnablePayments/ActivateStep.tsx +++ b/src/pages/EnablePayments/ActivateStep.tsx @@ -8,7 +8,7 @@ import useLocalize from '@hooks/useLocalize'; import * as PaymentMethods from '@userActions/PaymentMethods'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import {UserWallet, WalletTerms} from '@src/types/onyx'; +import type {UserWallet, WalletTerms} from '@src/types/onyx'; type ActivateStepOnyxProps = { /** Information about the user accepting the terms for payments */ diff --git a/src/pages/EnablePayments/EnablePaymentsPage.tsx b/src/pages/EnablePayments/EnablePaymentsPage.tsx index 6d2defc82df0..1384875fe031 100644 --- a/src/pages/EnablePayments/EnablePaymentsPage.tsx +++ b/src/pages/EnablePayments/EnablePaymentsPage.tsx @@ -25,7 +25,7 @@ type EnablePaymentsPageOnyxProps = { userWallet: OnyxEntry; }; -type EnablePaymentsPageProps = EnablePaymentsPageOnyxProps & {}; +type EnablePaymentsPageProps = EnablePaymentsPageOnyxProps; function EnablePaymentsPage({userWallet}: EnablePaymentsPageProps) { const {translate} = useLocalize(); @@ -38,6 +38,7 @@ function EnablePaymentsPage({userWallet}: EnablePaymentsPageProps) { return; } + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing if (isPendingOnfidoResult || hasFailedOnfido) { Navigation.navigate(ROUTES.SETTINGS_WALLET, CONST.NAVIGATION.TYPE.UP); return; diff --git a/src/pages/EnablePayments/OnfidoPrivacy.tsx b/src/pages/EnablePayments/OnfidoPrivacy.tsx index d8e9f616b8a7..80fa08c009ca 100644 --- a/src/pages/EnablePayments/OnfidoPrivacy.tsx +++ b/src/pages/EnablePayments/OnfidoPrivacy.tsx @@ -1,9 +1,8 @@ -import lodashGet from 'lodash/get'; import React, {useRef} from 'react'; -import {ScrollView, View} from 'react-native'; +import {View} from 'react-native'; +import type {ScrollView} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; -import _ from 'underscore'; import FixedFooter from '@components/FixedFooter'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import FormScrollView from '@components/FormScrollView'; @@ -15,7 +14,8 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; import * as BankAccounts from '@userActions/BankAccounts'; import ONYXKEYS from '@src/ONYXKEYS'; -import {WalletOnfido} from '@src/types/onyx'; +import type {WalletOnfido} from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; const DEFAULT_WALLET_ONFIDO_DATA = { applicantID: '', @@ -31,22 +31,25 @@ type OnfidoPrivacyOnyxProps = { walletOnfidoData: OnyxEntry; }; -type OnfidoPrivacyProps = OnfidoPrivacyOnyxProps & {}; +type OnfidoPrivacyProps = OnfidoPrivacyOnyxProps; function OnfidoPrivacy({walletOnfidoData = DEFAULT_WALLET_ONFIDO_DATA}: OnfidoPrivacyProps) { const {translate} = useLocalize(); - const styles = useThemeStyles(); - const {isLoading = false, hasAcceptedPrivacyPolicy} = walletOnfidoData ?? {}; - const formRef = useRef(null); + const styles = useThemeStyles(); + if (!walletOnfidoData) { + return; + } + const {isLoading = false, hasAcceptedPrivacyPolicy} = walletOnfidoData; const openOnfidoFlow = () => { BankAccounts.openOnfidoFlow(); }; let onfidoError = ErrorUtils.getLatestErrorMessage(walletOnfidoData ?? {}) || ''; - const onfidoFixableErrors = lodashGet(walletOnfidoData, 'fixableErrors', []); - onfidoError += !_.isEmpty(onfidoFixableErrors) ? `\n${onfidoFixableErrors.join('\n')}` : ''; + + const onfidoFixableErrors = walletOnfidoData?.fixableErrors ?? []; + onfidoError += !isEmptyObject(onfidoFixableErrors) ? `\n${onfidoFixableErrors.join('\n')}` : ''; return ( diff --git a/src/pages/EnablePayments/OnfidoStep.tsx b/src/pages/EnablePayments/OnfidoStep.tsx index 5e36f2f302a6..04cadb24fecf 100644 --- a/src/pages/EnablePayments/OnfidoStep.tsx +++ b/src/pages/EnablePayments/OnfidoStep.tsx @@ -13,7 +13,7 @@ import * as Wallet from '@userActions/Wallet'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import {WalletOnfido} from '@src/types/onyx'; +import type {WalletOnfido} from '@src/types/onyx'; import OnfidoPrivacy from './OnfidoPrivacy'; const DEFAULT_WALLET_ONFIDO_DATA = { diff --git a/src/pages/EnablePayments/TermsPage/LongTermsForm.tsx b/src/pages/EnablePayments/TermsPage/LongTermsForm.tsx index ec89856642d9..81d18c5dfc44 100644 --- a/src/pages/EnablePayments/TermsPage/LongTermsForm.tsx +++ b/src/pages/EnablePayments/TermsPage/LongTermsForm.tsx @@ -1,6 +1,5 @@ import React from 'react'; import {View} from 'react-native'; -import _ from 'underscore'; import CollapsibleSection from '@components/CollapsibleSection'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; diff --git a/src/pages/EnablePayments/TermsPage/ShortTermsForm.tsx b/src/pages/EnablePayments/TermsPage/ShortTermsForm.tsx index b45b8b657a75..f4db904c07f3 100644 --- a/src/pages/EnablePayments/TermsPage/ShortTermsForm.tsx +++ b/src/pages/EnablePayments/TermsPage/ShortTermsForm.tsx @@ -7,7 +7,7 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import CONST from '@src/CONST'; -import {UserWallet} from '@src/types/onyx'; +import type {UserWallet} from '@src/types/onyx'; type ShortTermsFormProps = { /** The user's wallet */ diff --git a/src/pages/EnablePayments/TermsStep.tsx b/src/pages/EnablePayments/TermsStep.tsx index cef54ba463e6..fe463f88792d 100644 --- a/src/pages/EnablePayments/TermsStep.tsx +++ b/src/pages/EnablePayments/TermsStep.tsx @@ -12,7 +12,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; import * as BankAccounts from '@userActions/BankAccounts'; import ONYXKEYS from '@src/ONYXKEYS'; -import {UserWallet, WalletTerms} from '@src/types/onyx'; +import type {UserWallet, WalletTerms} from '@src/types/onyx'; import LongTermsForm from './TermsPage/LongTermsForm'; import ShortTermsForm from './TermsPage/ShortTermsForm'; From c0f4f030db1ff9e61da920b9c4393a82cf42a59b Mon Sep 17 00:00:00 2001 From: Jayesh Mangwani Date: Mon, 5 Feb 2024 19:22:28 +0530 Subject: [PATCH 0148/1208] TS-migration: IdologyQuestions Page --- src/ONYXKEYS.ts | 4 + ...ologyQuestions.js => IdologyQuestions.tsx} | 81 +++++++------------ src/types/onyx/Form.ts | 5 ++ src/types/onyx/index.ts | 2 + 4 files changed, 39 insertions(+), 53 deletions(-) rename src/pages/EnablePayments/{IdologyQuestions.js => IdologyQuestions.tsx} (69%) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 7328fb2543ad..3046b5a16bdd 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -367,6 +367,8 @@ const ONYXKEYS = { REIMBURSEMENT_ACCOUNT_FORM_DRAFT: 'reimbursementAccountDraft', PERSONAL_BANK_ACCOUNT: 'personalBankAccountForm', PERSONAL_BANK_ACCOUNT_DRAFT: 'personalBankAccountFormDraft', + IDOLOGY_QUESTIONS_FORM: 'idologyQuestions', + IDOLOGY_QUESTIONS_FORM_DRAFT: 'idologyQuestionsDraft', }, } as const; @@ -556,6 +558,8 @@ type OnyxValues = { [ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT]: OnyxTypes.PersonalBankAccount; [ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_DRAFT]: OnyxTypes.PersonalBankAccount; + [ONYXKEYS.FORMS.IDOLOGY_QUESTIONS_FORM]: OnyxTypes.IdologyQuestionsForm; + [ONYXKEYS.FORMS.IDOLOGY_QUESTIONS_FORM_DRAFT]: OnyxTypes.IdologyQuestionsForm; }; type OnyxKeyValue = OnyxEntry; diff --git a/src/pages/EnablePayments/IdologyQuestions.js b/src/pages/EnablePayments/IdologyQuestions.tsx similarity index 69% rename from src/pages/EnablePayments/IdologyQuestions.js rename to src/pages/EnablePayments/IdologyQuestions.tsx index a0c202b0bbbc..4a5ef72019c0 100644 --- a/src/pages/EnablePayments/IdologyQuestions.js +++ b/src/pages/EnablePayments/IdologyQuestions.tsx @@ -1,10 +1,10 @@ -import PropTypes from 'prop-types'; import React, {useState} from 'react'; import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {WalletAdditionalQuestionDetails} from 'src/types/onyx'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; +import type {OnyxFormValuesFields} from '@components/Form/types'; +import type {Choice} from '@components/RadioButtons'; import SingleChoiceQuestion from '@components/SingleChoiceQuestion'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; @@ -12,53 +12,36 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as BankAccounts from '@userActions/BankAccounts'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {Errors} from '@src/types/onyx/OnyxCommon'; const MAX_SKIP = 1; const SKIP_QUESTION_TEXT = 'Skip Question'; -const propTypes = { +type IdologyQuestionsProps = { /** Questions returned by Idology */ /** example: [{"answer":["1251","6253","113","None of the above","Skip Question"],"prompt":"Which number goes with your address on MASONIC AVE?","type":"street.number.b"}, ...] */ - questions: PropTypes.arrayOf( - PropTypes.shape({ - prompt: PropTypes.string, - type: PropTypes.string, - answer: PropTypes.arrayOf(PropTypes.string), - }), - ), + questions: WalletAdditionalQuestionDetails[]; /** ID from Idology, referencing those questions */ - idNumber: PropTypes.string, - - walletAdditionalDetails: PropTypes.shape({ - /** Are we waiting for a response? */ - isLoading: PropTypes.bool, - - /** Any additional error message to show */ - errors: PropTypes.objectOf(PropTypes.string), - - /** What error do we need to handle */ - errorCode: PropTypes.string, - }), + idNumber: string; }; -const defaultProps = { - questions: [], - idNumber: '', - walletAdditionalDetails: {}, +type Answer = { + question: string; + answer: string; }; -function IdologyQuestions({questions, idNumber}) { +function IdologyQuestions({questions, idNumber}: IdologyQuestionsProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0); const [shouldHideSkipAnswer, setShouldHideSkipAnswer] = useState(false); - const [userAnswers, setUserAnswers] = useState([]); + const [userAnswers, setUserAnswers] = useState([]); const currentQuestion = questions[currentQuestionIndex] || {}; - const possibleAnswers = _.filter( - _.map(currentQuestion.answer, (answer) => { + const possibleAnswers: Choice[] = currentQuestion.answer + .map((answer) => { if (shouldHideSkipAnswer && answer === SKIP_QUESTION_TEXT) { return; } @@ -67,15 +50,11 @@ function IdologyQuestions({questions, idNumber}) { label: answer, value: answer, }; - }), - ); + }) + .filter((answer): answer is Choice => answer !== undefined); - /** - * Put question answer in the state. - * @param {String} answer - */ - const chooseAnswer = (answer) => { - const tempAnswers = _.map(userAnswers, _.clone); + const chooseAnswer = (answer: string) => { + const tempAnswers: Answer[] = userAnswers.map((userAnswer) => ({...userAnswer})); tempAnswers[currentQuestionIndex] = {question: currentQuestion.type, answer}; @@ -90,11 +69,11 @@ function IdologyQuestions({questions, idNumber}) { return; } // Get the number of questions that were skipped by the user. - const skippedQuestionsCount = _.filter(userAnswers, (answer) => answer.answer === SKIP_QUESTION_TEXT).length; + const skippedQuestionsCount = userAnswers.filter((answer) => answer.answer === SKIP_QUESTION_TEXT).length; // We have enough answers, let's call expectID KBA to verify them if (userAnswers.length - skippedQuestionsCount >= questions.length - MAX_SKIP) { - const tempAnswers = _.map(userAnswers, _.clone); + const tempAnswers: Answer[] = userAnswers.map((answer) => ({...answer})); // Auto skip any remaining questions if (tempAnswers.length < questions.length) { @@ -112,8 +91,8 @@ function IdologyQuestions({questions, idNumber}) { } }; - const validate = (values) => { - const errors = {}; + const validate = (values: OnyxFormValuesFields) => { + const errors: Errors = {}; if (!values.answer) { errors.answer = translate('additionalDetailsStep.selectAnswer'); } @@ -132,7 +111,7 @@ function IdologyQuestions({questions, idNumber}) { @@ -155,11 +136,5 @@ function IdologyQuestions({questions, idNumber}) { } IdologyQuestions.displayName = 'IdologyQuestions'; -IdologyQuestions.propTypes = propTypes; -IdologyQuestions.defaultProps = defaultProps; - -export default withOnyx({ - walletAdditionalDetails: { - key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, - }, -})(IdologyQuestions); + +export default IdologyQuestions; diff --git a/src/types/onyx/Form.ts b/src/types/onyx/Form.ts index 48f386afcbb0..79190aa3e6f6 100644 --- a/src/types/onyx/Form.ts +++ b/src/types/onyx/Form.ts @@ -68,6 +68,10 @@ type CloseAccountForm = Form<{ phoneOrEmail: string; }>; +type IdologyQuestionsForm = Form<{ + answer: string; +}>; + export default Form; export type { @@ -84,4 +88,5 @@ export type { WorkspaceSettingsForm, ReportFieldEditForm, CloseAccountForm, + IdologyQuestionsForm, }; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index d0ac2ce395fa..13c305abe384 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -14,6 +14,7 @@ import type { CloseAccountForm, DateOfBirthForm, DisplayNameForm, + IdologyQuestionsForm, IKnowATeacherForm, IntroSchoolPrincipalForm, NewRoomForm, @@ -165,4 +166,5 @@ export type { IntroSchoolPrincipalForm, PrivateNotesForm, ReportFieldEditForm, + IdologyQuestionsForm, }; From 61d24b89c0132dd7ce1049a62b0e88e9814b3c5f Mon Sep 17 00:00:00 2001 From: Jayesh Mangwani Date: Mon, 5 Feb 2024 21:29:42 +0530 Subject: [PATCH 0149/1208] TS-migration: AdditionalDetailsStep Page --- src/ONYXKEYS.ts | 4 + .../withCurrentUserPersonalDetails.tsx | 2 +- src/libs/actions/PersonalDetails.ts | 3 +- ...tailsStep.js => AdditionalDetailsStep.tsx} | 120 ++++++------------ src/types/onyx/Form.ts | 13 ++ src/types/onyx/index.ts | 2 + 6 files changed, 64 insertions(+), 80 deletions(-) rename src/pages/EnablePayments/{AdditionalDetailsStep.js => AdditionalDetailsStep.tsx} (73%) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 3046b5a16bdd..2e4e5b564616 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -369,6 +369,8 @@ const ONYXKEYS = { PERSONAL_BANK_ACCOUNT_DRAFT: 'personalBankAccountFormDraft', IDOLOGY_QUESTIONS_FORM: 'idologyQuestions', IDOLOGY_QUESTIONS_FORM_DRAFT: 'idologyQuestionsDraft', + ADDITIONAL_DETAILS_FORM: 'additionalDetailStep', + ADDITIONAL_DETAILS_FORM_DRAFT: 'additionalDetailStepDraft', }, } as const; @@ -560,6 +562,8 @@ type OnyxValues = { [ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_DRAFT]: OnyxTypes.PersonalBankAccount; [ONYXKEYS.FORMS.IDOLOGY_QUESTIONS_FORM]: OnyxTypes.IdologyQuestionsForm; [ONYXKEYS.FORMS.IDOLOGY_QUESTIONS_FORM_DRAFT]: OnyxTypes.IdologyQuestionsForm; + [ONYXKEYS.FORMS.ADDITIONAL_DETAILS_FORM]: OnyxTypes.AdditionalDetailStepForm; + [ONYXKEYS.FORMS.ADDITIONAL_DETAILS_FORM_DRAFT]: OnyxTypes.AdditionalDetailStepForm; }; type OnyxKeyValue = OnyxEntry; diff --git a/src/components/withCurrentUserPersonalDetails.tsx b/src/components/withCurrentUserPersonalDetails.tsx index 9406c8634c1b..75bdb03ea6d8 100644 --- a/src/components/withCurrentUserPersonalDetails.tsx +++ b/src/components/withCurrentUserPersonalDetails.tsx @@ -64,4 +64,4 @@ export default function ; }; -const defaultProps = { - walletAdditionalDetails: { - errorFields: {}, - isLoading: false, - errors: {}, - questions: [], - idNumber: '', - errorCode: '', - }, - ...withCurrentUserPersonalDetailsDefaultProps, -}; +type AdditionalDetailsStepProps = AdditionalDetailsStepOnyxProps & WithCurrentUserPersonalDetailsProps; const fieldNameTranslationKeys = { legalFirstName: 'additionalDetailsStep.legalFirstNameLabel', @@ -77,20 +50,17 @@ const fieldNameTranslationKeys = { dob: 'common.dob', ssn: 'common.ssnLast4', ssnFull9: 'common.ssnFull9', -}; +} as const; -function AdditionalDetailsStep({walletAdditionalDetails, translate, currentUserPersonalDetails}) { +function AdditionalDetailsStep({walletAdditionalDetails = DEFAULT_WALLET_ADDITIONAL_DETAILS, currentUserPersonalDetails}: AdditionalDetailsStepProps) { + const {translate} = useLocalize(); const styles = useThemeStyles(); const currentDate = new Date(); const minDate = subYears(currentDate, CONST.DATE_BIRTH.MAX_AGE); const maxDate = subYears(currentDate, CONST.DATE_BIRTH.MIN_AGE_FOR_PAYMENT); - const shouldAskForFullSSN = walletAdditionalDetails.errorCode === CONST.WALLET.ERROR.SSN; + const shouldAskForFullSSN = walletAdditionalDetails?.errorCode === CONST.WALLET.ERROR.SSN; - /** - * @param {Object} values The values object is passed from FormProvider and contains info for each form element that has an inputID - * @returns {Object} - */ - const validate = (values) => { + const validate = (values: OnyxFormValuesFields) => { const requiredFields = ['legalFirstName', 'legalLastName', 'addressStreet', 'addressCity', 'addressZipCode', 'phoneNumber', 'dob', 'ssn', 'addressState']; const errors = ValidationUtils.getFieldRequiredErrors(values, requiredFields); @@ -116,7 +86,7 @@ function AdditionalDetailsStep({walletAdditionalDetails, translate, currentUserP // walletAdditionalDetails stores errors returned by the server. If the server returns an SSN error // then the user needs to provide the full 9 digit SSN. - if (walletAdditionalDetails.errorCode === CONST.WALLET.ERROR.SSN) { + if (walletAdditionalDetails?.errorCode === CONST.WALLET.ERROR.SSN) { if (values.ssn && !ValidationUtils.isValidSSNFullNine(values.ssn)) { errors.ssn = 'additionalDetailsStep.ssnFull9Error'; } @@ -127,26 +97,23 @@ function AdditionalDetailsStep({walletAdditionalDetails, translate, currentUserP return errors; }; - /** - * @param {Object} values The values object is passed from FormProvider and contains info for each form element that has an inputID - */ - const activateWallet = (values) => { + const activateWallet = (values: OnyxFormValuesFields) => { const personalDetails = { - phoneNumber: parsePhoneNumber(values.phoneNumber, {regionCode: CONST.COUNTRY.US}).number.significant || '', - legalFirstName: values.legalFirstName || '', - legalLastName: values.legalLastName || '', - addressStreet: values.addressStreet || '', - addressCity: values.addressCity || '', - addressState: values.addressState || '', - addressZip: values.addressZipCode || '', - dob: values.dob || '', - ssn: values.ssn || '', + phoneNumber: (values.phoneNumber && parsePhoneNumber(values.phoneNumber, {regionCode: CONST.COUNTRY.US}).number?.significant) ?? '', + legalFirstName: values.legalFirstName ?? '', + legalLastName: values.legalLastName ?? '', + addressStreet: values.addressStreet ?? '', + addressCity: values.addressCity ?? '', + addressState: values.addressState ?? '', + addressZip: values.addressZipCode ?? '', + dob: values.dob ?? '', + ssn: values.ssn ?? '', }; // Attempt to set the personal details Wallet.updatePersonalDetails(personalDetails); }; - if (!_.isEmpty(walletAdditionalDetails.questions)) { + if (walletAdditionalDetails?.questions && walletAdditionalDetails.questions.length > 0) { return ( Wallet.setAdditionalDetailsQuestions(null)} + onBackButtonPress={() => Wallet.setAdditionalDetailsQuestions([], walletAdditionalDetails?.idNumber ?? '')} /> ); @@ -180,7 +147,7 @@ function AdditionalDetailsStep({walletAdditionalDetails, translate, currentUserP ({ walletAdditionalDetails: { key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, }, - }), -)(AdditionalDetailsStep); + })(AdditionalDetailsStep), +); diff --git a/src/types/onyx/Form.ts b/src/types/onyx/Form.ts index 79190aa3e6f6..646950dcb986 100644 --- a/src/types/onyx/Form.ts +++ b/src/types/onyx/Form.ts @@ -72,6 +72,18 @@ type IdologyQuestionsForm = Form<{ answer: string; }>; +type AdditionalDetailStepForm = Form<{ + legalFirstName: string; + legalLastName: string; + addressStreet: string; + addressCity: string; + addressZipCode: string; + phoneNumber: string; + dob: string; + ssn: string; + addressState: string; +}>; + export default Form; export type { @@ -89,4 +101,5 @@ export type { ReportFieldEditForm, CloseAccountForm, IdologyQuestionsForm, + AdditionalDetailStepForm, }; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 13c305abe384..ff6c782a37e1 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -11,6 +11,7 @@ import type CustomStatusDraft from './CustomStatusDraft'; import type Download from './Download'; import type { AddDebitCardForm, + AdditionalDetailStepForm, CloseAccountForm, DateOfBirthForm, DisplayNameForm, @@ -167,4 +168,5 @@ export type { PrivateNotesForm, ReportFieldEditForm, IdologyQuestionsForm, + AdditionalDetailStepForm, }; From 0824fb95fa3df6b9f06e96d4d42266deb000c50d Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Tue, 6 Feb 2024 11:08:40 +0530 Subject: [PATCH 0150/1208] update startMoneyRequest_temporaryForRefactor to startMoneyRequest. Signed-off-by: Krishna Gupta --- src/libs/actions/IOU.ts | 9 +-------- src/pages/iou/request/IOURequestStartPage.js | 4 ++-- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 569b6b26b728..f20e0b0519c7 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -225,7 +225,7 @@ Onyx.connect({ * @param iouRequestType one of manual/scan/distance */ // eslint-disable-next-line @typescript-eslint/naming-convention -function startMoneyRequest_temporaryForRefactor(reportID: string, isFromGlobalCreate: boolean, iouRequestType: IOURequestType = CONST.IOU.REQUEST_TYPE.MANUAL) { +function startMoneyRequest(reportID: string, isFromGlobalCreate: boolean, iouRequestType: IOURequestType = CONST.IOU.REQUEST_TYPE.MANUAL) { // Generate a brand new transactionID const newTransactionID = CONST.IOU.OPTIMISTIC_TRANSACTION_ID; // Disabling this line since currentDate can be an empty string @@ -3530,12 +3530,6 @@ function setMoneyRequestParticipantsFromReport(transactionID: string, report: On Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {participants, participantsAutoAssigned: true}); } -/** Initialize money request info and navigate to the MoneyRequest page */ -function startMoneyRequest(iouType: string, reportID = '') { - resetMoneyRequestInfo(`${iouType}${reportID}`); - Navigation.navigate(ROUTES.MONEY_REQUEST.getRoute(iouType, reportID)); -} - function setMoneyRequestId(id: string) { Onyx.merge(ONYXKEYS.IOU, {id}); } @@ -3686,7 +3680,6 @@ export { payMoneyRequest, sendMoneyWithWallet, startMoneyRequest, - startMoneyRequest_temporaryForRefactor, resetMoneyRequestCategory, resetMoneyRequestCategory_temporaryForRefactor, resetMoneyRequestInfo, diff --git a/src/pages/iou/request/IOURequestStartPage.js b/src/pages/iou/request/IOURequestStartPage.js index c790b30c5f7d..dd6420bf2c1a 100644 --- a/src/pages/iou/request/IOURequestStartPage.js +++ b/src/pages/iou/request/IOURequestStartPage.js @@ -106,7 +106,7 @@ function IOURequestStartPage({ if (transaction.reportID === reportID) { return; } - IOU.startMoneyRequest_temporaryForRefactor(reportID, isFromGlobalCreate, transactionRequestType.current); + IOU.startMoneyRequest(reportID, isFromGlobalCreate, transactionRequestType.current); }, [transaction, reportID, iouType, isFromGlobalCreate]); const isExpenseChat = ReportUtils.isPolicyExpenseChat(report); @@ -125,7 +125,7 @@ function IOURequestStartPage({ if (newIouType === previousIOURequestType) { return; } - IOU.startMoneyRequest_temporaryForRefactor(reportID, isFromGlobalCreate, newIouType); + IOU.startMoneyRequest(reportID, isFromGlobalCreate, newIouType); transactionRequestType.current = newIouType; }, [previousIOURequestType, reportID, isFromGlobalCreate], From 4e69672e311a7509f05b5e2946814c131c5284b2 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Tue, 6 Feb 2024 11:35:40 +0530 Subject: [PATCH 0151/1208] remove: unused routes from ROUTES.ts Signed-off-by: Krishna Gupta --- src/ROUTES.ts | 7 ------- src/pages/iou/request/step/IOURequestStepWaypoint.js | 6 +++--- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 016e4267803b..aabed6fdb0a1 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -301,13 +301,6 @@ const ROUTES = { route: ':iouType/new/address/:reportID?', getRoute: (iouType: string, reportID = '') => `${iouType}/new/address/${reportID}` as const, }, - MONEY_REQUEST_DISTANCE_TAB: { - route: ':iouType/new/:reportID?/distance', - getRoute: (iouType: string, reportID = '') => `${iouType}/new/${reportID}/distance` as const, - }, - MONEY_REQUEST_MANUAL_TAB: ':iouType/new/:reportID?/manual', - MONEY_REQUEST_SCAN_TAB: ':iouType/new/:reportID?/scan', - MONEY_REQUEST_CREATE: { route: 'create/:iouType/start/:transactionID/:reportID', getRoute: (iouType: ValueOf, transactionID: string, reportID: string) => `create/${iouType}/start/${transactionID}/${reportID}` as const, diff --git a/src/pages/iou/request/step/IOURequestStepWaypoint.js b/src/pages/iou/request/step/IOURequestStepWaypoint.js index 4c35951bc297..89e2b2cc297d 100644 --- a/src/pages/iou/request/step/IOURequestStepWaypoint.js +++ b/src/pages/iou/request/step/IOURequestStepWaypoint.js @@ -158,13 +158,13 @@ function IOURequestStepWaypoint({ } // Other flows will be handled by selecting a waypoint with selectWaypoint as this is mainly for the offline flow - Navigation.goBack(ROUTES.MONEY_REQUEST_DISTANCE_TAB.getRoute(iouType)); + Navigation.goBack(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(iouType, transactionID, reportID)); }; const deleteStopAndHideModal = () => { Transaction.removeWaypoint(transaction, pageIndex, true); setIsDeleteStopModalOpen(false); - Navigation.goBack(ROUTES.MONEY_REQUEST_DISTANCE_TAB.getRoute(iouType)); + Navigation.goBack(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(iouType, transactionID, reportID)); }; /** @@ -200,7 +200,7 @@ function IOURequestStepWaypoint({ title={translate(waypointDescriptionKey)} shouldShowBackButton onBackButtonPress={() => { - Navigation.goBack(ROUTES.MONEY_REQUEST_DISTANCE_TAB.getRoute(iouType)); + Navigation.goBack(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(iouType, transactionID, reportID)); }} shouldShowThreeDotsButton={shouldShowThreeDotsButton} shouldSetModalVisibility={false} From 013376d144397e0335f1eedb794be9fd81f12f88 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Tue, 6 Feb 2024 13:59:30 +0530 Subject: [PATCH 0152/1208] minor fix. Signed-off-by: Krishna Gupta --- src/pages/iou/request/step/IOURequestStepParticipants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.js b/src/pages/iou/request/step/IOURequestStepParticipants.js index daae4bcf0831..a11877739cb5 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.js +++ b/src/pages/iou/request/step/IOURequestStepParticipants.js @@ -46,7 +46,7 @@ function IOURequestStepParticipants({ if (iouType === CONST.IOU.TYPE.SEND) { return translate('common.send'); } - translate(TransactionUtils.getHeaderTitleTranslationKey(transaction)); + return translate(TransactionUtils.getHeaderTitleTranslationKey(transaction)); }; const receiptFilename = lodashGet(transaction, 'filename'); const receiptPath = lodashGet(transaction, 'receipt.source'); From 423af4e488e11f3df71ce02f6b295e7292b978bc Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Tue, 6 Feb 2024 14:14:51 +0530 Subject: [PATCH 0153/1208] updated sendMoney callback dependencies. Signed-off-by: Krishna Gupta --- .../iou/request/step/IOURequestStepConfirmation.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.js b/src/pages/iou/request/step/IOURequestStepConfirmation.js index 70d5e1fceac7..c66dc91f4f64 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.js +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.js @@ -308,7 +308,13 @@ function IOURequestStepConfirmation({ const sendMoney = useCallback( (paymentMethodType) => { const currency = transaction.currency; - const trimmedComment = lodashGet(transaction, 'comment.comment', '').trim(); + + let trimmedComment = ''; + + if (transaction.comment && transaction.comment.comment) { + trimmedComment = transaction.comment.comment.trim(); + } + const participant = participants[0]; if (paymentMethodType === CONST.IOU.PAYMENT_TYPE.ELSEWHERE) { @@ -320,7 +326,7 @@ function IOURequestStepConfirmation({ IOU.sendMoneyWithWallet(report, transaction.amount, currency, trimmedComment, currentUserPersonalDetails.accountID, participant); } }, - [transaction, participants, currentUserPersonalDetails.accountID, report], + [transaction.amount, transaction.comment, transaction.currency, participants, currentUserPersonalDetails.accountID, report], ); const addNewParticipant = (option) => { From 1ad742de9d5b27113e74d85691016089ff0d89ae Mon Sep 17 00:00:00 2001 From: Toby Sullivan Date: Tue, 6 Feb 2024 08:08:34 -0800 Subject: [PATCH 0154/1208] Fix prettier formatting --- src/libs/ReportUtils.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 09496c6b18f1..e485cf615a41 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2257,7 +2257,14 @@ function getReportPreviewMessage( }); } - if (!isEmptyObject(linkedTransaction) && !isEmptyObject(reportAction) && shouldConsiderReceiptBeingScanned && TransactionUtils.hasReceipt(linkedTransaction) && TransactionUtils.isReceiptBeingScanned(linkedTransaction) && ReportActionsUtils.isMoneyRequestAction(reportAction)) { + if ( + !isEmptyObject(linkedTransaction) && + !isEmptyObject(reportAction) && + shouldConsiderReceiptBeingScanned && + TransactionUtils.hasReceipt(linkedTransaction) && + TransactionUtils.isReceiptBeingScanned(linkedTransaction) && + ReportActionsUtils.isMoneyRequestAction(reportAction) + ) { return Localize.translateLocal('iou.receiptScanning'); } From 21e3ec0ad48fc8a99f30c75c27fc809be6a5f1bf Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 6 Feb 2024 23:51:15 +0700 Subject: [PATCH 0155/1208] fix: app crashes when changing from auditor to employee --- src/pages/home/ReportScreen.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index bfe27910c943..2e2d7251de6f 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -418,7 +418,7 @@ function ReportScreen({ !onyxReportID && prevReport.statusNum === CONST.REPORT.STATUS_NUM.OPEN && (report.statusNum === CONST.REPORT.STATUS_NUM.CLOSED || (!report.statusNum && !prevReport.parentReportID && prevReport.chatType === CONST.REPORT.CHAT_TYPE.POLICY_ROOM))) || - ((ReportUtils.isMoneyRequest(prevReport) || ReportUtils.isMoneyRequestReport(prevReport)) && _.isEmpty(report)) + ((ReportUtils.isMoneyRequest(prevReport) || ReportUtils.isMoneyRequestReport(prevReport) || ReportUtils.isPolicyExpenseChat(prevReport)) && _.isEmpty(report)) ) { Navigation.dismissModal(); if (Navigation.getTopmostReportId() === prevOnyxReportID) { @@ -643,7 +643,7 @@ export default compose( key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : 0}`, selector: (parentReportActions, props) => { const parentReportActionID = lodashGet(props, 'report.parentReportActionID'); - if (!parentReportActionID) { + if (!parentReportActionID || !parentReportActions) { return {}; } return lodashGet(parentReportActions, parentReportActionID); From 17c4fba70c1d4f8a9aaebd8e7a7c045227ba83ab Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Wed, 7 Feb 2024 11:27:38 +0100 Subject: [PATCH 0156/1208] 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 0157/1208] add route to memo --- src/components/FlatList/MVCPFlatList.js | 2 +- src/pages/home/ReportScreen.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/FlatList/MVCPFlatList.js b/src/components/FlatList/MVCPFlatList.js index 1b6bca14ecf3..c815774eeabd 100644 --- a/src/components/FlatList/MVCPFlatList.js +++ b/src/components/FlatList/MVCPFlatList.js @@ -51,7 +51,7 @@ const MVCPFlatList = React.forwardRef(({maintainVisibleContentPosition, horizont const scrollToOffset = React.useCallback( (offset, animated) => { const behavior = animated ? 'smooth' : 'instant'; - scrollRef.current?.getScrollableNode().scroll(horizontal ? {left: offset, behavior} : {top: offset, behavior}); + scrollRef.current?.getScrollableNode()?.scroll(horizontal ? {left: offset, behavior} : {top: offset, behavior}); }, [horizontal], ); diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 116ca0f41762..6609d06e0c63 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -719,6 +719,7 @@ export default compose( prevProps.isComposerFullSize === nextProps.isComposerFullSize && _.isEqual(prevProps.betas, nextProps.betas) && _.isEqual(prevProps.policies, nextProps.policies) && + _.isEqual(prevProps.route, nextProps.route) && prevProps.accountManagerReportID === nextProps.accountManagerReportID && prevProps.userLeavingStatus === nextProps.userLeavingStatus && prevProps.currentReportID === nextProps.currentReportID && From 5f89ad6cf8fdcc0a4a5522a35588a36c40670f70 Mon Sep 17 00:00:00 2001 From: Jayesh Mangwani Date: Thu, 8 Feb 2024 17:16:50 +0530 Subject: [PATCH 0158/1208] merge main, lint fixed and prettified code --- src/libs/PersonalDetailsUtils.ts | 2 +- src/pages/EnablePayments/OnfidoPrivacy.tsx | 2 +- src/pages/EnablePayments/TermsStep.tsx | 2 +- src/types/onyx/Form.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/PersonalDetailsUtils.ts b/src/libs/PersonalDetailsUtils.ts index aa9a7f79e582..69b0f386a76c 100644 --- a/src/libs/PersonalDetailsUtils.ts +++ b/src/libs/PersonalDetailsUtils.ts @@ -1,11 +1,11 @@ import Str from 'expensify-common/lib/str'; import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; +import type {CurrentUserPersonalDetails} from '@components/withCurrentUserPersonalDetails'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PersonalDetails, PersonalDetailsList, PrivatePersonalDetails} from '@src/types/onyx'; import type {OnyxData} from '@src/types/onyx/Request'; -import type {CurrentUserPersonalDetails} from '@components/withCurrentUserPersonalDetails'; import * as LocalePhoneNumber from './LocalePhoneNumber'; import * as Localize from './Localize'; import * as UserUtils from './UserUtils'; diff --git a/src/pages/EnablePayments/OnfidoPrivacy.tsx b/src/pages/EnablePayments/OnfidoPrivacy.tsx index 80fa08c009ca..5d8cdc751e87 100644 --- a/src/pages/EnablePayments/OnfidoPrivacy.tsx +++ b/src/pages/EnablePayments/OnfidoPrivacy.tsx @@ -46,7 +46,7 @@ function OnfidoPrivacy({walletOnfidoData = DEFAULT_WALLET_ONFIDO_DATA}: OnfidoPr BankAccounts.openOnfidoFlow(); }; - let onfidoError = ErrorUtils.getLatestErrorMessage(walletOnfidoData ?? {}) || ''; + let onfidoError = ErrorUtils.getLatestErrorMessage(walletOnfidoData ?? {}) ?? ''; const onfidoFixableErrors = walletOnfidoData?.fixableErrors ?? []; onfidoError += !isEmptyObject(onfidoFixableErrors) ? `\n${onfidoFixableErrors.join('\n')}` : ''; diff --git a/src/pages/EnablePayments/TermsStep.tsx b/src/pages/EnablePayments/TermsStep.tsx index fe463f88792d..4836feae9f9b 100644 --- a/src/pages/EnablePayments/TermsStep.tsx +++ b/src/pages/EnablePayments/TermsStep.tsx @@ -33,7 +33,7 @@ function TermsStep(props: TermsStepProps) { const [error, setError] = useState(false); const {translate} = useLocalize(); - const errorMessage = error ? 'common.error.acceptTerms' : ErrorUtils.getLatestErrorMessage(props.walletTerms ?? {}) || ''; + const errorMessage = error ? 'common.error.acceptTerms' : ErrorUtils.getLatestErrorMessage(props.walletTerms ?? {}) ?? ''; const toggleDisclosure = () => { setHasAcceptedDisclosure(!hasAcceptedDisclosure); diff --git a/src/types/onyx/Form.ts b/src/types/onyx/Form.ts index bb78487892bf..7043449ae6cf 100644 --- a/src/types/onyx/Form.ts +++ b/src/types/onyx/Form.ts @@ -82,7 +82,7 @@ type AdditionalDetailStepForm = Form<{ dob: string; ssn: string; addressState: string; -}> +}>; type RoomNameForm = Form<{ roomName: string; From ed7c4d21e101a1aeede28c6eaeb20e81101938d7 Mon Sep 17 00:00:00 2001 From: Jayesh Mangwani Date: Fri, 9 Feb 2024 20:05:31 +0530 Subject: [PATCH 0159/1208] fix: lint --- src/pages/EnablePayments/OnfidoPrivacy.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/EnablePayments/OnfidoPrivacy.tsx b/src/pages/EnablePayments/OnfidoPrivacy.tsx index 8618499397dc..03f8df3ad82a 100644 --- a/src/pages/EnablePayments/OnfidoPrivacy.tsx +++ b/src/pages/EnablePayments/OnfidoPrivacy.tsx @@ -46,8 +46,8 @@ function OnfidoPrivacy({walletOnfidoData = DEFAULT_WALLET_ONFIDO_DATA}: OnfidoPr BankAccounts.openOnfidoFlow(); }; - const onfidoError = ErrorUtils.getLatestErrorMessage(walletOnfidoData) || ''; - const onfidoFixableErrors = walletOnfidoData?.fixableErrors??[]; + const onfidoError = ErrorUtils.getLatestErrorMessage(walletOnfidoData) ?? ''; + const onfidoFixableErrors = walletOnfidoData?.fixableErrors ?? []; if (Array.isArray(onfidoError)) { onfidoError[0] += !isEmptyObject(onfidoFixableErrors) ? `\n${onfidoFixableErrors.join('\n')}` : ''; } From e6df088475612cd7578a3c3036bd21036a9701ff Mon Sep 17 00:00:00 2001 From: Jayesh Mangwani Date: Sat, 10 Feb 2024 13:11:13 +0530 Subject: [PATCH 0160/1208] updated key from ADDITIONAL_DETAILS_FORM to WALLET_ADDITIONAL_DETAILS --- src/ONYXKEYS.ts | 8 ++++---- src/libs/actions/Wallet.ts | 9 +++++---- src/pages/EnablePayments/AdditionalDetailsStep.tsx | 6 +++--- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 90a6860fb1de..b14f0985bfed 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -369,8 +369,8 @@ const ONYXKEYS = { PERSONAL_BANK_ACCOUNT_DRAFT: 'personalBankAccountFormDraft', IDOLOGY_QUESTIONS_FORM: 'idologyQuestions', IDOLOGY_QUESTIONS_FORM_DRAFT: 'idologyQuestionsDraft', - ADDITIONAL_DETAILS_FORM: 'additionalDetailStep', - ADDITIONAL_DETAILS_FORM_DRAFT: 'additionalDetailStepDraft', + WALLET_ADDITIONAL_DETAILS: 'walletAdditionalDetailsForm', + WALLET_ADDITIONAL_DETAILS_DRAFT: 'walletAdditionalDetailsFormDraft', }, } as const; @@ -562,8 +562,8 @@ type OnyxValues = { [ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT_DRAFT]: OnyxTypes.PersonalBankAccount; [ONYXKEYS.FORMS.IDOLOGY_QUESTIONS_FORM]: OnyxTypes.IdologyQuestionsForm; [ONYXKEYS.FORMS.IDOLOGY_QUESTIONS_FORM_DRAFT]: OnyxTypes.IdologyQuestionsForm; - [ONYXKEYS.FORMS.ADDITIONAL_DETAILS_FORM]: OnyxTypes.AdditionalDetailStepForm; - [ONYXKEYS.FORMS.ADDITIONAL_DETAILS_FORM_DRAFT]: OnyxTypes.AdditionalDetailStepForm; + [ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS]: OnyxTypes.AdditionalDetailStepForm; + [ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS_DRAFT]: OnyxTypes.AdditionalDetailStepForm; }; type OnyxKeyValue = OnyxEntry; diff --git a/src/libs/actions/Wallet.ts b/src/libs/actions/Wallet.ts index b03b5e8f6d3d..4d0ba7a67fd1 100644 --- a/src/libs/actions/Wallet.ts +++ b/src/libs/actions/Wallet.ts @@ -75,11 +75,12 @@ function setKYCWallSource(source?: ValueOf, chatRe /** * Validates a user's provided details against a series of checks */ + function updatePersonalDetails(personalDetails: UpdatePersonalDetailsForWalletParams) { const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, + key: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, value: { isLoading: true, errors: null, @@ -91,7 +92,7 @@ function updatePersonalDetails(personalDetails: UpdatePersonalDetailsForWalletPa const finallyData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, + key: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, value: { isLoading: false, }, @@ -232,7 +233,7 @@ function answerQuestionsForWallet(answers: WalletQuestionAnswer[], idNumber: str const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, + key: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, value: { isLoading: true, }, @@ -242,7 +243,7 @@ function answerQuestionsForWallet(answers: WalletQuestionAnswer[], idNumber: str const finallyData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, + key: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, value: { isLoading: false, }, diff --git a/src/pages/EnablePayments/AdditionalDetailsStep.tsx b/src/pages/EnablePayments/AdditionalDetailsStep.tsx index ad802a74c050..c9b557a197e8 100644 --- a/src/pages/EnablePayments/AdditionalDetailsStep.tsx +++ b/src/pages/EnablePayments/AdditionalDetailsStep.tsx @@ -60,7 +60,7 @@ function AdditionalDetailsStep({walletAdditionalDetails = DEFAULT_WALLET_ADDITIO const maxDate = subYears(currentDate, CONST.DATE_BIRTH.MIN_AGE_FOR_PAYMENT); const shouldAskForFullSSN = walletAdditionalDetails?.errorCode === CONST.WALLET.ERROR.SSN; - const validate = (values: OnyxFormValuesFields) => { + const validate = (values: OnyxFormValuesFields) => { const requiredFields = ['legalFirstName', 'legalLastName', 'addressStreet', 'addressCity', 'addressZipCode', 'phoneNumber', 'dob', 'ssn', 'addressState']; const errors = ValidationUtils.getFieldRequiredErrors(values, requiredFields); @@ -97,7 +97,7 @@ function AdditionalDetailsStep({walletAdditionalDetails = DEFAULT_WALLET_ADDITIO return errors; }; - const activateWallet = (values: OnyxFormValuesFields) => { + const activateWallet = (values: OnyxFormValuesFields) => { const personalDetails = { phoneNumber: (values.phoneNumber && parsePhoneNumber(values.phoneNumber, {regionCode: CONST.COUNTRY.US}).number?.significant) ?? '', legalFirstName: values.legalFirstName ?? '', @@ -147,7 +147,7 @@ function AdditionalDetailsStep({walletAdditionalDetails = DEFAULT_WALLET_ADDITIO Date: Mon, 12 Feb 2024 12:58:28 +0530 Subject: [PATCH 0161/1208] rearranged import for GetPhysicalCardForm --- src/types/onyx/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index ca3abbca28b9..c9742595272f 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -16,8 +16,8 @@ import type { CloseAccountForm, DateOfBirthForm, DisplayNameForm, - IdologyQuestionsForm, GetPhysicalCardForm, + IdologyQuestionsForm, IKnowATeacherForm, IntroSchoolPrincipalForm, NewRoomForm, From ee76e50335a44ed6053292ee8deb78423a3edacf Mon Sep 17 00:00:00 2001 From: Jayesh Mangwani Date: Mon, 12 Feb 2024 20:17:11 +0530 Subject: [PATCH 0162/1208] updated IDOLOGY_QUESTIONS_FORM key name --- src/ONYXKEYS.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index f19adfbeedd3..29a2955facfa 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -367,8 +367,8 @@ const ONYXKEYS = { REIMBURSEMENT_ACCOUNT_FORM_DRAFT: 'reimbursementAccountDraft', PERSONAL_BANK_ACCOUNT: 'personalBankAccountForm', PERSONAL_BANK_ACCOUNT_DRAFT: 'personalBankAccountFormDraft', - IDOLOGY_QUESTIONS_FORM: 'idologyQuestions', - IDOLOGY_QUESTIONS_FORM_DRAFT: 'idologyQuestionsDraft', + IDOLOGY_QUESTIONS_FORM: 'idologyQuestionsForm', + IDOLOGY_QUESTIONS_FORM_DRAFT: 'idologyQuestionsFormDraft', WALLET_ADDITIONAL_DETAILS: 'walletAdditionalDetailsForm', WALLET_ADDITIONAL_DETAILS_DRAFT: 'walletAdditionalDetailsFormDraft', }, From 26e354f83e6c3fc8c7281d12d911ecd8c24ff0d4 Mon Sep 17 00:00:00 2001 From: Toby Sullivan Date: Mon, 12 Feb 2024 09:12:06 -0800 Subject: [PATCH 0163/1208] Fix formatting error --- src/libs/ReportUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index e329d6e51a49..d86c838ccccd 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2308,7 +2308,6 @@ function getReportPreviewMessage( return Localize.translateLocal('iou.waitingOnBankAccount', {submitterDisplayName}); } - const lastActorID = reportAction?.actorAccountID; const comment = !isEmptyObject(linkedTransaction) ? TransactionUtils.getDescription(linkedTransaction) : undefined; let amount = originalMessage?.amount; From 92789b565db5233a5f6dfdc243f266c92a406aa7 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Tue, 13 Feb 2024 20:55:10 +0530 Subject: [PATCH 0164/1208] Remove redundant constants for the old screen names. Signed-off-by: Krishna Gupta --- src/SCREENS.ts | 4 ---- src/pages/iou/request/step/IOURequestStepConfirmation.js | 6 +----- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 1626fdbd1898..6da12b453ef8 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -122,9 +122,6 @@ const SCREENS = { SAML_SIGN_IN: 'SAMLSignIn', MONEY_REQUEST: { - MANUAL_TAB: 'manual', - SCAN_TAB: 'scan', - DISTANCE_TAB: 'distance', CREATE: 'Money_Request_Create', STEP_CONFIRMATION: 'Money_Request_Step_Confirmation', START: 'Money_Request_Start', @@ -141,7 +138,6 @@ const SCREENS = { STEP_WAYPOINT: 'Money_Request_Step_Waypoint', STEP_TAX_AMOUNT: 'Money_Request_Step_Tax_Amount', STEP_TAX_RATE: 'Money_Request_Step_Tax_Rate', - ROOT: 'Money_Request', AMOUNT: 'Money_Request_Amount', PARTICIPANTS: 'Money_Request_Participants', CONFIRMATION: 'Money_Request_Confirmation', diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.js b/src/pages/iou/request/step/IOURequestStepConfirmation.js index 038841027fae..6536541c289b 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.js +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.js @@ -360,11 +360,7 @@ function IOURequestStepConfirmation({ (paymentMethodType) => { const currency = transaction.currency; - let trimmedComment = ''; - - if (transaction.comment && transaction.comment.comment) { - trimmedComment = transaction.comment.comment.trim(); - } + const trimmedComment = transaction.comment && transaction.comment.comment ? transaction.comment.comment.trim() : ''; const participant = participants[0]; From 40cd54db77e7a89c46163a4ba0074fe5c7c114dc Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Wed, 14 Feb 2024 15:39:01 +0100 Subject: [PATCH 0165/1208] Migrate ReimbursementAccount to ts --- .../RestartBankAccountSetupParams.ts | 6 ++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 + .../deleteFromBankAccountList.ts | 16 ++++ .../actions/ReimbursementAccount/errors.ts | 44 ++++++++++ .../actions/ReimbursementAccount/index.ts | 63 ++++++++++++++ .../ReimbursementAccount/navigation.ts | 24 ++++++ .../resetFreePlanBankAccount.ts | 84 +++++++++++++++++++ .../actions/ReimbursementAccount/store.ts | 69 +++++++++++++++ 9 files changed, 309 insertions(+) create mode 100644 src/libs/API/parameters/RestartBankAccountSetupParams.ts create mode 100644 src/libs/actions/ReimbursementAccount/deleteFromBankAccountList.ts create mode 100644 src/libs/actions/ReimbursementAccount/errors.ts create mode 100644 src/libs/actions/ReimbursementAccount/index.ts create mode 100644 src/libs/actions/ReimbursementAccount/navigation.ts create mode 100644 src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.ts create mode 100644 src/libs/actions/ReimbursementAccount/store.ts diff --git a/src/libs/API/parameters/RestartBankAccountSetupParams.ts b/src/libs/API/parameters/RestartBankAccountSetupParams.ts new file mode 100644 index 000000000000..b338eac0dea1 --- /dev/null +++ b/src/libs/API/parameters/RestartBankAccountSetupParams.ts @@ -0,0 +1,6 @@ +type RestartBankAccountSetupParams = { + bankAccountID: number; + ownerEmail: string; +}; + +export default RestartBankAccountSetupParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 482c5e0336c4..90b27d825580 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -2,6 +2,7 @@ export type {default as ActivatePhysicalExpensifyCardParams} from './ActivatePhy export type {default as AddNewContactMethodParams} from './AddNewContactMethodParams'; export type {default as AddPaymentCardParams} from './AddPaymentCardParams'; export type {default as AddPersonalBankAccountParams} from './AddPersonalBankAccountParams'; +export type {default as RestartBankAccountSetupParams} from './RestartBankAccountSetupParams'; export type {default as AddSchoolPrincipalParams} from './AddSchoolPrincipalParams'; export type {default as AuthenticatePusherParams} from './AuthenticatePusherParams'; export type {default as BankAccountHandlePlaidErrorParams} from './BankAccountHandlePlaidErrorParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index f5d99d8cf40e..4f811b85e709 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -76,6 +76,7 @@ const WRITE_COMMANDS = { ADD_ATTACHMENT: 'AddAttachment', CONNECT_BANK_ACCOUNT_WITH_PLAID: 'ConnectBankAccountWithPlaid', ADD_PERSONAL_BANK_ACCOUNT: 'AddPersonalBankAccount', + RESTART_BANK_ACCOUNT_SETUP: 'RestartBankAccountSetup', OPT_IN_TO_PUSH_NOTIFICATIONS: 'OptInToPushNotifications', OPT_OUT_OF_PUSH_NOTIFICATIONS: 'OptOutOfPushNotifications', RECONNECT_TO_REPORT: 'ReconnectToReport', @@ -213,6 +214,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.ADD_ATTACHMENT]: Parameters.AddCommentOrAttachementParams; [WRITE_COMMANDS.CONNECT_BANK_ACCOUNT_WITH_PLAID]: Parameters.ConnectBankAccountWithPlaidParams; [WRITE_COMMANDS.ADD_PERSONAL_BANK_ACCOUNT]: Parameters.AddPersonalBankAccountParams; + [WRITE_COMMANDS.RESTART_BANK_ACCOUNT_SETUP]: Parameters.RestartBankAccountSetupParams; [WRITE_COMMANDS.OPT_IN_TO_PUSH_NOTIFICATIONS]: Parameters.OptInOutToPushNotificationsParams; [WRITE_COMMANDS.OPT_OUT_OF_PUSH_NOTIFICATIONS]: Parameters.OptInOutToPushNotificationsParams; [WRITE_COMMANDS.RECONNECT_TO_REPORT]: Parameters.ReconnectToReportParams; diff --git a/src/libs/actions/ReimbursementAccount/deleteFromBankAccountList.ts b/src/libs/actions/ReimbursementAccount/deleteFromBankAccountList.ts new file mode 100644 index 000000000000..d9a2dd130d62 --- /dev/null +++ b/src/libs/actions/ReimbursementAccount/deleteFromBankAccountList.ts @@ -0,0 +1,16 @@ +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; +import * as store from './store'; + +/** + * Deletes a bank account from bankAccountList + */ +function deleteFromBankAccountList(bankAccountID: number) { + // We should delete the bankAccountID key from the bankAccountList object before setting it in Onyx + const bankAccountList = store.getBankAccountList(); + delete bankAccountList?.[bankAccountID]; + + Onyx.merge(ONYXKEYS.BANK_ACCOUNT_LIST, bankAccountList); +} + +export default deleteFromBankAccountList; diff --git a/src/libs/actions/ReimbursementAccount/errors.ts b/src/libs/actions/ReimbursementAccount/errors.ts new file mode 100644 index 000000000000..c65da17690bb --- /dev/null +++ b/src/libs/actions/ReimbursementAccount/errors.ts @@ -0,0 +1,44 @@ +import Onyx from 'react-native-onyx'; +import * as ErrorUtils from '@libs/ErrorUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Errors} from '@src/types/onyx/OnyxCommon'; + +/** + * Set the current fields with errors. + */ +function setPersonalBankAccountFormValidationErrorFields(errors: Errors) { + // We set 'errors' to null first because we don't have a way yet to replace a specific property without merging it + Onyx.merge(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {errors: null}); + Onyx.merge(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {errors}); +} + +/** + * Set the current fields with errors. + + */ +function setBankAccountFormValidationErrors(errors: Errors) { + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {errors: null}); + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {errors}); +} + +/** + * Clear validation messages from reimbursement account + */ +function resetReimbursementAccount() { + setBankAccountFormValidationErrors({}); + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, { + errors: null, + pendingAction: null, + }); +} + +/** + * Set the current error message. + */ +function showBankAccountFormValidationError(error: string | null) { + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, { + errors: ErrorUtils.getMicroSecondOnyxError(error), + }); +} + +export {setBankAccountFormValidationErrors, setPersonalBankAccountFormValidationErrorFields, showBankAccountFormValidationError, resetReimbursementAccount}; diff --git a/src/libs/actions/ReimbursementAccount/index.ts b/src/libs/actions/ReimbursementAccount/index.ts new file mode 100644 index 000000000000..5c9bf1c822d1 --- /dev/null +++ b/src/libs/actions/ReimbursementAccount/index.ts @@ -0,0 +1,63 @@ +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {ReimbursementAccountForm} from '@src/types/form'; +import type {BankAccountSubStep} from '@src/types/onyx/ReimbursementAccount'; +import deleteFromBankAccountList from './deleteFromBankAccountList'; +import resetFreePlanBankAccount from './resetFreePlanBankAccount'; + +export {goToWithdrawalAccountSetupStep, navigateToBankAccountRoute} from './navigation'; +export {setBankAccountFormValidationErrors, setPersonalBankAccountFormValidationErrorFields, resetReimbursementAccount, showBankAccountFormValidationError} from './errors'; + +/** + * Set the current sub step in first step of adding withdrawal bank account: + * - `null` if we want to go back to the view where the user selects between connecting via Plaid or connecting manually + * - CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL to ask them to enter their accountNumber and routingNumber + * - CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID to ask them to login to their bank via Plaid + * + * @param subStep + * @returns + */ +function setBankAccountSubStep(subStep: BankAccountSubStep) { + return Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {achData: {subStep}}); +} + +function hideBankAccountErrors() { + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {error: '', errors: null}); +} + +function setWorkspaceIDForReimbursementAccount(workspaceID: string) { + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT_WORKSPACE_ID, workspaceID); +} + +/** + * @param bankAccountData + */ +function updateReimbursementAccountDraft(bankAccountData: ReimbursementAccountForm) { + Onyx.merge(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT, bankAccountData); + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {draftStep: undefined}); +} + +/** + * Triggers a modal to open allowing the user to reset their bank account + */ +function requestResetFreePlanBankAccount() { + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {shouldShowResetModal: true}); +} + +/** + * Hides modal allowing the user to reset their bank account + */ +function cancelResetFreePlanBankAccount() { + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {shouldShowResetModal: false}); +} + +export { + resetFreePlanBankAccount, + setBankAccountSubStep, + hideBankAccountErrors, + setWorkspaceIDForReimbursementAccount, + updateReimbursementAccountDraft, + requestResetFreePlanBankAccount, + cancelResetFreePlanBankAccount, + deleteFromBankAccountList, +}; diff --git a/src/libs/actions/ReimbursementAccount/navigation.ts b/src/libs/actions/ReimbursementAccount/navigation.ts new file mode 100644 index 000000000000..2c3eb7cf0384 --- /dev/null +++ b/src/libs/actions/ReimbursementAccount/navigation.ts @@ -0,0 +1,24 @@ +import Onyx from 'react-native-onyx'; +import Navigation from '@libs/Navigation/Navigation'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type {BankAccountStep} from '@src/types/onyx/ReimbursementAccount'; + +/** + * Navigate to a specific step in the VBA flow + */ +function goToWithdrawalAccountSetupStep(stepID: BankAccountStep) { + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {achData: {currentStep: stepID}}); +} + +/** + * Navigate to the correct bank account route based on the bank account state and type + * + * @param policyID - The policy ID associated with the bank account. + * @param [backTo=''] - An optional return path. If provided, it will be URL-encoded and appended to the resulting URL. + */ +function navigateToBankAccountRoute(policyID: string, backTo?: string) { + Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute('', policyID, backTo)); +} + +export {goToWithdrawalAccountSetupStep, navigateToBankAccountRoute}; diff --git a/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.ts b/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.ts new file mode 100644 index 000000000000..3cc34db0846f --- /dev/null +++ b/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.ts @@ -0,0 +1,84 @@ +import type {OnyxEntry} from 'react-native-onyx'; +import Onyx from 'react-native-onyx'; +import * as API from '@libs/API'; +import {WRITE_COMMANDS} from '@libs/API/types'; +import * as PlaidDataProps from '@pages/ReimbursementAccount/plaidDataPropTypes'; +import * as ReimbursementAccountProps from '@pages/ReimbursementAccount/reimbursementAccountPropTypes'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type * as OnyxTypes from '@src/types/onyx'; + +/** + * Reset user's reimbursement account. This will delete the bank account. + */ +function resetFreePlanBankAccount(bankAccountID: number, session: OnyxEntry) { + if (!bankAccountID) { + throw new Error('Missing bankAccountID when attempting to reset free plan bank account'); + } + if (!session?.email) { + throw new Error('Missing credentials when attempting to reset free plan bank account'); + } + + API.write( + WRITE_COMMANDS.RESTART_BANK_ACCOUNT_SETUP, + { + bankAccountID, + ownerEmail: session.email, + }, + { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + value: { + shouldShowResetModal: false, + isLoading: true, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + achData: null, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.SET, + key: ONYXKEYS.ONFIDO_TOKEN, + value: '', + }, + { + onyxMethod: Onyx.METHOD.SET, + key: ONYXKEYS.ONFIDO_APPLICANT_ID, + value: '', + }, + { + onyxMethod: Onyx.METHOD.SET, + key: ONYXKEYS.PLAID_DATA, + value: PlaidDataProps.plaidDataDefaultProps, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: ONYXKEYS.PLAID_LINK_TOKEN, + value: '', + }, + { + onyxMethod: Onyx.METHOD.SET, + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + value: ReimbursementAccountProps.reimbursementAccountDefaultProps, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT, + value: {}, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + value: {isLoading: false, pendingAction: null}, + }, + ], + }, + ); +} + +export default resetFreePlanBankAccount; diff --git a/src/libs/actions/ReimbursementAccount/store.ts b/src/libs/actions/ReimbursementAccount/store.ts new file mode 100644 index 000000000000..bdceb4e2ad5d --- /dev/null +++ b/src/libs/actions/ReimbursementAccount/store.ts @@ -0,0 +1,69 @@ +import type {OnyxEntry} from 'react-native-onyx'; +import Onyx from 'react-native-onyx'; +import BankAccount from '@libs/models/BankAccount'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type * as OnyxTypes from '@src/types/onyx'; +import type {ACHData} from '@src/types/onyx/ReimbursementAccount'; +import type {EmptyObject} from '@src/types/utils/EmptyObject'; + +/** Reimbursement account actively being set up */ +let reimbursementAccountInSetup: ACHData | EmptyObject = {}; +Onyx.connect({ + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + callback: (val) => { + reimbursementAccountInSetup = val?.achData ?? {}; + }, +}); + +let reimbursementAccountWorkspaceID: OnyxEntry = null; +Onyx.connect({ + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT_WORKSPACE_ID, + callback: (val) => { + reimbursementAccountWorkspaceID = val; + }, +}); + +let bankAccountList: OnyxEntry = null; +Onyx.connect({ + key: ONYXKEYS.BANK_ACCOUNT_LIST, + callback: (val) => { + bankAccountList = val; + }, +}); + +let credentials: OnyxEntry = null; +Onyx.connect({ + key: ONYXKEYS.CREDENTIALS, + callback: (val) => { + credentials = val; + }, +}); + +function getReimbursementAccountInSetup() { + return reimbursementAccountInSetup; +} + +function getBankAccountList() { + return bankAccountList; +} + +function hasCreditBankAccount() { + if (!bankAccountList) { + return false; + } + + Object.entries(bankAccountList).some(([, bankAccountJSON]) => { + const bankAccount = new BankAccount(bankAccountJSON); + return bankAccount.isDefaultCredit(); + }); +} + +function getCredentials() { + return credentials; +} + +function getReimbursementAccountWorkspaceID() { + return reimbursementAccountWorkspaceID; +} + +export {getReimbursementAccountInSetup, getBankAccountList, getCredentials, getReimbursementAccountWorkspaceID, hasCreditBankAccount}; From 6a886fefb7d026fb767ec41172b226e1dfad4842 Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Wed, 14 Feb 2024 15:39:51 +0100 Subject: [PATCH 0166/1208] Remove old js implementations --- .../deleteFromBankAccountList.js | 18 ---- .../actions/ReimbursementAccount/errors.js | 47 ----------- .../actions/ReimbursementAccount/index.js | 61 -------------- .../ReimbursementAccount/navigation.js | 25 ------ .../resetFreePlanBankAccount.js | 83 ------------------- .../actions/ReimbursementAccount/store.js | 63 -------------- 6 files changed, 297 deletions(-) delete mode 100644 src/libs/actions/ReimbursementAccount/deleteFromBankAccountList.js delete mode 100644 src/libs/actions/ReimbursementAccount/errors.js delete mode 100644 src/libs/actions/ReimbursementAccount/index.js delete mode 100644 src/libs/actions/ReimbursementAccount/navigation.js delete mode 100644 src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.js delete mode 100644 src/libs/actions/ReimbursementAccount/store.js diff --git a/src/libs/actions/ReimbursementAccount/deleteFromBankAccountList.js b/src/libs/actions/ReimbursementAccount/deleteFromBankAccountList.js deleted file mode 100644 index 6161066c1c69..000000000000 --- a/src/libs/actions/ReimbursementAccount/deleteFromBankAccountList.js +++ /dev/null @@ -1,18 +0,0 @@ -import Onyx from 'react-native-onyx'; -import ONYXKEYS from '@src/ONYXKEYS'; -import * as store from './store'; - -/** - * Deletes a bank account from bankAccountList - * - * @param {Number} bankAccountID - */ -function deleteFromBankAccountList(bankAccountID) { - // We should delete the bankAccountID key from the bankAccountList object before setting it in Onyx - const bankAccountList = store.getBankAccountList(); - delete bankAccountList[bankAccountID]; - - Onyx.merge(ONYXKEYS.BANK_ACCOUNT_LIST, bankAccountList); -} - -export default deleteFromBankAccountList; diff --git a/src/libs/actions/ReimbursementAccount/errors.js b/src/libs/actions/ReimbursementAccount/errors.js deleted file mode 100644 index fd2eaf852bce..000000000000 --- a/src/libs/actions/ReimbursementAccount/errors.js +++ /dev/null @@ -1,47 +0,0 @@ -import Onyx from 'react-native-onyx'; -import * as ErrorUtils from '@libs/ErrorUtils'; -import ONYXKEYS from '@src/ONYXKEYS'; - -/** - * Set the current fields with errors. - * @param {Object} errorFields - */ -function setPersonalBankAccountFormValidationErrorFields(errorFields) { - // We set 'errorFields' to null first because we don't have a way yet to replace a specific property without merging it - Onyx.merge(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {errorFields: null}); - Onyx.merge(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {errorFields}); -} - -/** - * Set the current fields with errors. - * - * @param {Object} errorFields - */ -function setBankAccountFormValidationErrors(errorFields) { - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {errorFields: null}); - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {errorFields}); -} - -/** - * Clear validation messages from reimbursement account - */ -function resetReimbursementAccount() { - setBankAccountFormValidationErrors({}); - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, { - errors: null, - pendingAction: null, - }); -} - -/** - * Set the current error message. - * - * @param {String} error - */ -function showBankAccountFormValidationError(error) { - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, { - errors: ErrorUtils.getMicroSecondOnyxError(error), - }); -} - -export {setBankAccountFormValidationErrors, setPersonalBankAccountFormValidationErrorFields, showBankAccountFormValidationError, resetReimbursementAccount}; diff --git a/src/libs/actions/ReimbursementAccount/index.js b/src/libs/actions/ReimbursementAccount/index.js deleted file mode 100644 index 12b5b940a0f2..000000000000 --- a/src/libs/actions/ReimbursementAccount/index.js +++ /dev/null @@ -1,61 +0,0 @@ -import Onyx from 'react-native-onyx'; -import ONYXKEYS from '@src/ONYXKEYS'; -import deleteFromBankAccountList from './deleteFromBankAccountList'; -import resetFreePlanBankAccount from './resetFreePlanBankAccount'; - -export {goToWithdrawalAccountSetupStep, navigateToBankAccountRoute} from './navigation'; -export {setBankAccountFormValidationErrors, setPersonalBankAccountFormValidationErrorFields, resetReimbursementAccount, showBankAccountFormValidationError} from './errors'; - -/** - * Set the current sub step in first step of adding withdrawal bank account: - * - `null` if we want to go back to the view where the user selects between connecting via Plaid or connecting manually - * - CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL to ask them to enter their accountNumber and routingNumber - * - CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID to ask them to login to their bank via Plaid - * - * @param {String | null} subStep - * @returns {Promise} - */ -function setBankAccountSubStep(subStep) { - return Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {achData: {subStep}}); -} - -function hideBankAccountErrors() { - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {error: '', errors: null}); -} - -function setWorkspaceIDForReimbursementAccount(workspaceID) { - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT_WORKSPACE_ID, workspaceID); -} - -/** - * @param {Object} bankAccountData - */ -function updateReimbursementAccountDraft(bankAccountData) { - Onyx.merge(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT, bankAccountData); - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {draftStep: undefined}); -} - -/** - * Triggers a modal to open allowing the user to reset their bank account - */ -function requestResetFreePlanBankAccount() { - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {shouldShowResetModal: true}); -} - -/** - * Hides modal allowing the user to reset their bank account - */ -function cancelResetFreePlanBankAccount() { - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {shouldShowResetModal: false}); -} - -export { - resetFreePlanBankAccount, - setBankAccountSubStep, - hideBankAccountErrors, - setWorkspaceIDForReimbursementAccount, - updateReimbursementAccountDraft, - requestResetFreePlanBankAccount, - cancelResetFreePlanBankAccount, - deleteFromBankAccountList, -}; diff --git a/src/libs/actions/ReimbursementAccount/navigation.js b/src/libs/actions/ReimbursementAccount/navigation.js deleted file mode 100644 index 6c82561c16ee..000000000000 --- a/src/libs/actions/ReimbursementAccount/navigation.js +++ /dev/null @@ -1,25 +0,0 @@ -import Onyx from 'react-native-onyx'; -import Navigation from '@libs/Navigation/Navigation'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; - -/** - * Navigate to a specific step in the VBA flow - * - * @param {String} stepID - */ -function goToWithdrawalAccountSetupStep(stepID) { - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {achData: {currentStep: stepID}}); -} - -/** - * Navigate to the correct bank account route based on the bank account state and type - * - * @param {string} policyID - The policy ID associated with the bank account. - * @param {string} [backTo=''] - An optional return path. If provided, it will be URL-encoded and appended to the resulting URL. - */ -function navigateToBankAccountRoute(policyID, backTo) { - Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute('', policyID, backTo)); -} - -export {goToWithdrawalAccountSetupStep, navigateToBankAccountRoute}; diff --git a/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.js b/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.js deleted file mode 100644 index 962800fb2e55..000000000000 --- a/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.js +++ /dev/null @@ -1,83 +0,0 @@ -import Onyx from 'react-native-onyx'; -import * as API from '@libs/API'; -import * as PlaidDataProps from '@pages/ReimbursementAccount/plaidDataPropTypes'; -import * as ReimbursementAccountProps from '@pages/ReimbursementAccount/reimbursementAccountPropTypes'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; - -/** - * Reset user's reimbursement account. This will delete the bank account. - * @param {Number} bankAccountID - * @param {Object} session - */ -function resetFreePlanBankAccount(bankAccountID, session) { - if (!bankAccountID) { - throw new Error('Missing bankAccountID when attempting to reset free plan bank account'); - } - if (!session.email) { - throw new Error('Missing credentials when attempting to reset free plan bank account'); - } - - API.write( - 'RestartBankAccountSetup', - { - bankAccountID, - ownerEmail: session.email, - }, - { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, - value: { - shouldShowResetModal: false, - isLoading: true, - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, - achData: null, - }, - }, - ], - successData: [ - { - onyxMethod: Onyx.METHOD.SET, - key: ONYXKEYS.ONFIDO_TOKEN, - value: '', - }, - { - onyxMethod: Onyx.METHOD.SET, - key: ONYXKEYS.ONFIDO_APPLICANT_ID, - value: '', - }, - { - onyxMethod: Onyx.METHOD.SET, - key: ONYXKEYS.PLAID_DATA, - value: PlaidDataProps.plaidDataDefaultProps, - }, - { - onyxMethod: Onyx.METHOD.SET, - key: ONYXKEYS.PLAID_LINK_TOKEN, - value: '', - }, - { - onyxMethod: Onyx.METHOD.SET, - key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, - value: ReimbursementAccountProps.reimbursementAccountDefaultProps, - }, - { - onyxMethod: Onyx.METHOD.SET, - key: ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT, - value: {}, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, - value: {isLoading: false, pendingAction: null}, - }, - ], - }, - ); -} - -export default resetFreePlanBankAccount; diff --git a/src/libs/actions/ReimbursementAccount/store.js b/src/libs/actions/ReimbursementAccount/store.js deleted file mode 100644 index 4b8549b60b2e..000000000000 --- a/src/libs/actions/ReimbursementAccount/store.js +++ /dev/null @@ -1,63 +0,0 @@ -import lodashGet from 'lodash/get'; -import Onyx from 'react-native-onyx'; -import _ from 'underscore'; -import BankAccount from '@libs/models/BankAccount'; -import ONYXKEYS from '@src/ONYXKEYS'; - -/** Reimbursement account actively being set up */ -let reimbursementAccountInSetup = {}; -Onyx.connect({ - key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, - callback: (val) => { - reimbursementAccountInSetup = lodashGet(val, 'achData', {}); - }, -}); - -let reimbursementAccountWorkspaceID = null; -Onyx.connect({ - key: ONYXKEYS.REIMBURSEMENT_ACCOUNT_WORKSPACE_ID, - callback: (val) => { - reimbursementAccountWorkspaceID = val; - }, -}); - -let bankAccountList = null; -Onyx.connect({ - key: ONYXKEYS.BANK_ACCOUNT_LIST, - callback: (val) => { - bankAccountList = val; - }, -}); - -let credentials; -Onyx.connect({ - key: ONYXKEYS.CREDENTIALS, - callback: (val) => { - credentials = val || {}; - }, -}); - -function getReimbursementAccountInSetup() { - return reimbursementAccountInSetup; -} - -function getBankAccountList() { - return bankAccountList; -} - -function hasCreditBankAccount() { - return _.some(bankAccountList, (bankAccountJSON) => { - const bankAccount = new BankAccount(bankAccountJSON); - return bankAccount.isDefaultCredit(); - }); -} - -function getCredentials() { - return credentials; -} - -function getReimbursementAccountWorkspaceID() { - return reimbursementAccountWorkspaceID; -} - -export {getReimbursementAccountInSetup, getBankAccountList, getCredentials, getReimbursementAccountWorkspaceID, hasCreditBankAccount}; From 8b6fb5e1e3639f3532c1183e953f416417c6151c Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Wed, 14 Feb 2024 15:58:46 +0100 Subject: [PATCH 0167/1208] Add ErrorFields to PersonalBankAccount type --- src/libs/actions/ReimbursementAccount/errors.ts | 14 +++++++------- src/types/onyx/PersonalBankAccount.ts | 3 +++ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/libs/actions/ReimbursementAccount/errors.ts b/src/libs/actions/ReimbursementAccount/errors.ts index c65da17690bb..f85426f8d4fe 100644 --- a/src/libs/actions/ReimbursementAccount/errors.ts +++ b/src/libs/actions/ReimbursementAccount/errors.ts @@ -1,24 +1,24 @@ import Onyx from 'react-native-onyx'; import * as ErrorUtils from '@libs/ErrorUtils'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Errors} from '@src/types/onyx/OnyxCommon'; +import type {ErrorFields} from '@src/types/onyx/OnyxCommon'; /** * Set the current fields with errors. */ -function setPersonalBankAccountFormValidationErrorFields(errors: Errors) { +function setPersonalBankAccountFormValidationErrorFields(errorFields: ErrorFields) { // We set 'errors' to null first because we don't have a way yet to replace a specific property without merging it - Onyx.merge(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {errors: null}); - Onyx.merge(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {errors}); + Onyx.merge(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {errorFields: null}); + Onyx.merge(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {errorFields}); } /** * Set the current fields with errors. */ -function setBankAccountFormValidationErrors(errors: Errors) { - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {errors: null}); - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {errors}); +function setBankAccountFormValidationErrors(errorFields: ErrorFields) { + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {errorFields: null}); + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {errorFields}); } /** diff --git a/src/types/onyx/PersonalBankAccount.ts b/src/types/onyx/PersonalBankAccount.ts index 3714cc9f314b..3e52a3cf59f3 100644 --- a/src/types/onyx/PersonalBankAccount.ts +++ b/src/types/onyx/PersonalBankAccount.ts @@ -5,6 +5,9 @@ type PersonalBankAccount = { /** An error message to display to the user */ errors?: OnyxCommon.Errors; + /** Error objects keyed by field name containing errors keyed by microtime */ + errorFields?: OnyxCommon.ErrorFields; + /** Whether we should show the view that the bank account was successfully added */ shouldShowSuccess?: boolean; From 53fb383b51d8382c2cfc3e8fc9068adbddd82c07 Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Wed, 14 Feb 2024 16:03:55 +0100 Subject: [PATCH 0168/1208] Adjust types --- src/libs/actions/ReimbursementAccount/index.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/libs/actions/ReimbursementAccount/index.ts b/src/libs/actions/ReimbursementAccount/index.ts index 5c9bf1c822d1..416c5e956189 100644 --- a/src/libs/actions/ReimbursementAccount/index.ts +++ b/src/libs/actions/ReimbursementAccount/index.ts @@ -13,11 +13,8 @@ export {setBankAccountFormValidationErrors, setPersonalBankAccountFormValidation * - `null` if we want to go back to the view where the user selects between connecting via Plaid or connecting manually * - CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL to ask them to enter their accountNumber and routingNumber * - CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID to ask them to login to their bank via Plaid - * - * @param subStep - * @returns */ -function setBankAccountSubStep(subStep: BankAccountSubStep) { +function setBankAccountSubStep(subStep: BankAccountSubStep | null) { return Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {achData: {subStep}}); } @@ -25,13 +22,10 @@ function hideBankAccountErrors() { Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {error: '', errors: null}); } -function setWorkspaceIDForReimbursementAccount(workspaceID: string) { +function setWorkspaceIDForReimbursementAccount(workspaceID: string | null) { Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT_WORKSPACE_ID, workspaceID); } -/** - * @param bankAccountData - */ function updateReimbursementAccountDraft(bankAccountData: ReimbursementAccountForm) { Onyx.merge(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT, bankAccountData); Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {draftStep: undefined}); From dcc5adff694c41c31a48acfbe042bea0e72dac17 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Wed, 14 Feb 2024 16:07:13 +0000 Subject: [PATCH 0169/1208] refactor(typescript): migrate settings preferences --- src/libs/LocaleUtils.ts | 17 ++++++ .../{LanguagePage.js => LanguagePage.tsx} | 26 ++++----- ...PreferencesPage.js => PreferencesPage.tsx} | 39 ++++++-------- ...iorityModePage.js => PriorityModePage.tsx} | 54 ++++++++----------- .../{ThemePage.js => ThemePage.tsx} | 25 ++++----- 5 files changed, 76 insertions(+), 85 deletions(-) create mode 100644 src/libs/LocaleUtils.ts rename src/pages/settings/Preferences/{LanguagePage.js => LanguagePage.tsx} (53%) rename src/pages/settings/Preferences/{PreferencesPage.js => PreferencesPage.tsx} (87%) rename src/pages/settings/Preferences/{PriorityModePage.js => PriorityModePage.tsx} (50%) rename src/pages/settings/Preferences/{ThemePage.js => ThemePage.tsx} (71%) diff --git a/src/libs/LocaleUtils.ts b/src/libs/LocaleUtils.ts new file mode 100644 index 000000000000..2bcbb946c7c0 --- /dev/null +++ b/src/libs/LocaleUtils.ts @@ -0,0 +1,17 @@ +import type {ValueOf} from 'type-fest'; +import CONST from '@src/CONST'; + +const getLanguageFromLocale = (locale: ValueOf): (typeof CONST.LANGUAGES)[number] => { + switch (locale) { + case CONST.LOCALES.ES_ES: + case CONST.LOCALES.ES_ES_ONFIDO: + case CONST.LOCALES.ES: + return CONST.LOCALES.ES; + case CONST.LOCALES.EN: + return CONST.LOCALES.EN; + default: + return CONST.LOCALES.DEFAULT; + } +}; + +export default {getLanguageFromLocale}; diff --git a/src/pages/settings/Preferences/LanguagePage.js b/src/pages/settings/Preferences/LanguagePage.tsx similarity index 53% rename from src/pages/settings/Preferences/LanguagePage.js rename to src/pages/settings/Preferences/LanguagePage.tsx index ce93e94222b5..68ceeb0a1d81 100644 --- a/src/pages/settings/Preferences/LanguagePage.js +++ b/src/pages/settings/Preferences/LanguagePage.tsx @@ -1,27 +1,20 @@ -import PropTypes from 'prop-types'; import React from 'react'; -import _ from 'underscore'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; import * as App from '@userActions/App'; import CONST from '@src/CONST'; -const propTypes = { - ...withLocalizePropTypes, +function LanguagePage() { + const {translate, preferredLocale} = useLocalize(); - /** The preferred language of the App */ - preferredLocale: PropTypes.string.isRequired, -}; - -function LanguagePage(props) { - const localesToLanguages = _.map(CONST.LANGUAGES, (language) => ({ + const localesToLanguages = CONST.LANGUAGES.map((language) => ({ value: language, - text: props.translate(`languagePage.languages.${language}.label`), + text: translate(`languagePage.languages.${language}.label`), keyForList: language, - isSelected: props.preferredLocale === language, + isSelected: preferredLocale === language, })); return ( @@ -30,19 +23,18 @@ function LanguagePage(props) { testID={LanguagePage.displayName} > Navigation.goBack()} /> App.setLocaleAndNavigate(language.value)} - initiallyFocusedOptionKey={_.find(localesToLanguages, (locale) => locale.isSelected).keyForList} + initiallyFocusedOptionKey={localesToLanguages.find((locale) => locale.isSelected)?.keyForList} /> ); } LanguagePage.displayName = 'LanguagePage'; -LanguagePage.propTypes = propTypes; -export default withLocalize(LanguagePage); +export default LanguagePage; diff --git a/src/pages/settings/Preferences/PreferencesPage.js b/src/pages/settings/Preferences/PreferencesPage.tsx similarity index 87% rename from src/pages/settings/Preferences/PreferencesPage.js rename to src/pages/settings/Preferences/PreferencesPage.tsx index 5ac78f6d20c6..4b93b330e6e9 100755 --- a/src/pages/settings/Preferences/PreferencesPage.js +++ b/src/pages/settings/Preferences/PreferencesPage.tsx @@ -1,8 +1,8 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React from 'react'; import {ScrollView, View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Illustrations from '@components/Icon/Illustrations'; import LottieAnimations from '@components/LottieAnimations'; @@ -16,33 +16,28 @@ import useEnvironment from '@hooks/useEnvironment'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import LocaleUtils from '@libs/LocaleUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as User from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {User as UserType} from '@src/types/onyx'; -const propTypes = { +type PreferencesPageOnyxProps = { /** The chat priority mode */ - priorityMode: PropTypes.string, + priorityMode: OnyxEntry>; /** The app's color theme */ - preferredTheme: PropTypes.string, + preferredTheme: OnyxEntry>; /** The details about the user that is signed in */ - user: PropTypes.shape({ - /** Whether or not the user is subscribed to news updates */ - isSubscribedToNewsletter: PropTypes.bool, - }), + user: OnyxEntry; }; -const defaultProps = { - priorityMode: CONST.PRIORITY_MODE.DEFAULT, - preferredTheme: CONST.DEFAULT_THEME, - user: {}, -}; +type PreferencesPageProps = PreferencesPageOnyxProps; -function PreferencesPage(props) { +function PreferencesPage({priorityMode, preferredTheme, user}: PreferencesPageProps) { const styles = useThemeStyles(); const {isProduction} = useEnvironment(); const {translate, preferredLocale} = useLocalize(); @@ -85,7 +80,7 @@ function PreferencesPage(props) { @@ -97,28 +92,28 @@ function PreferencesPage(props) { Navigation.navigate(ROUTES.SETTINGS_PRIORITY_MODE)} wrapperStyle={styles.sectionMenuItemTopDescription} /> Navigation.navigate(ROUTES.SETTINGS_LANGUAGE)} wrapperStyle={styles.sectionMenuItemTopDescription} /> Navigation.navigate(ROUTES.SETTINGS_THEME)} wrapperStyle={styles.sectionMenuItemTopDescription} @@ -144,11 +139,9 @@ function PreferencesPage(props) { ); } -PreferencesPage.propTypes = propTypes; -PreferencesPage.defaultProps = defaultProps; PreferencesPage.displayName = 'PreferencesPage'; -export default withOnyx({ +export default withOnyx({ priorityMode: { key: ONYXKEYS.NVP_PRIORITY_MODE, }, diff --git a/src/pages/settings/Preferences/PriorityModePage.js b/src/pages/settings/Preferences/PriorityModePage.tsx similarity index 50% rename from src/pages/settings/Preferences/PriorityModePage.js rename to src/pages/settings/Preferences/PriorityModePage.tsx index 983e3cb26746..e6c94c73021e 100644 --- a/src/pages/settings/Preferences/PriorityModePage.js +++ b/src/pages/settings/Preferences/PriorityModePage.tsx @@ -1,48 +1,45 @@ -import PropTypes from 'prop-types'; import React, {useCallback} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import _, {compose} from 'underscore'; +import type {ValueOf} from 'type-fest'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import Text from '@components/Text'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import * as User from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -const propTypes = { +type PriorityModePageOnyxProps = { /** The chat priority mode */ - priorityMode: PropTypes.string, - - ...withLocalizePropTypes, + priorityMode: OnyxEntry>; }; -const defaultProps = { - priorityMode: CONST.PRIORITY_MODE.DEFAULT, -}; +type PriorityModePageProps = PriorityModePageOnyxProps; -function PriorityModePage(props) { +function PriorityModePage({priorityMode}: PriorityModePageProps) { + const {translate} = useLocalize(); const styles = useThemeStyles(); - const priorityModes = _.map(_.values(CONST.PRIORITY_MODE), (mode) => ({ + const priorityModes = Object.values(CONST.PRIORITY_MODE).map((mode) => ({ value: mode, - text: props.translate(`priorityModePage.priorityModes.${mode}.label`), - alternateText: props.translate(`priorityModePage.priorityModes.${mode}.description`), + text: translate(`priorityModePage.priorityModes.${mode}.label`), + alternateText: translate(`priorityModePage.priorityModes.${mode}.description`), keyForList: mode, - isSelected: props.priorityMode === mode, + isSelected: priorityMode === mode, })); const updateMode = useCallback( - (mode) => { - if (mode.value === props.priorityMode) { + (mode: (typeof priorityModes)[number]) => { + if (mode.value === priorityMode) { Navigation.goBack(); return; } User.updateChatPriorityMode(mode.value); }, - [props.priorityMode], + [priorityMode], ); return ( @@ -51,28 +48,23 @@ function PriorityModePage(props) { testID={PriorityModePage.displayName} > Navigation.goBack()} /> - {props.translate('priorityModePage.explainerText')} + {translate('priorityModePage.explainerText')} mode.isSelected).keyForList} + initiallyFocusedOptionKey={priorityModes.find((mode) => mode.isSelected)?.keyForList} /> ); } PriorityModePage.displayName = 'PriorityModePage'; -PriorityModePage.propTypes = propTypes; -PriorityModePage.defaultProps = defaultProps; -export default compose( - withLocalize, - withOnyx({ - priorityMode: { - key: ONYXKEYS.NVP_PRIORITY_MODE, - }, - }), -)(PriorityModePage); +export default withOnyx({ + priorityMode: { + key: ONYXKEYS.NVP_PRIORITY_MODE, + }, +})(PriorityModePage); diff --git a/src/pages/settings/Preferences/ThemePage.js b/src/pages/settings/Preferences/ThemePage.tsx similarity index 71% rename from src/pages/settings/Preferences/ThemePage.js rename to src/pages/settings/Preferences/ThemePage.tsx index 4907056be761..4d89e600770b 100644 --- a/src/pages/settings/Preferences/ThemePage.js +++ b/src/pages/settings/Preferences/ThemePage.tsx @@ -1,7 +1,7 @@ -import PropTypes from 'prop-types'; import React from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {ValueOf} from 'type-fest'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; @@ -13,23 +13,22 @@ import * as User from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -const propTypes = { +type ThemePageOnyxProps = { /** The theme of the app */ - preferredTheme: PropTypes.string, + preferredTheme: OnyxEntry>; }; -const defaultProps = { - preferredTheme: CONST.THEME.DEFAULT, -}; +type ThemePageProps = ThemePageOnyxProps; -function ThemePage(props) { +function ThemePage({preferredTheme}: ThemePageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const localesToThemes = _.map(_.values(_.omit(CONST.THEME, 'DEFAULT', 'FALLBACK')), (theme) => ({ + const {DEFAULT, FALLBACK, ...themes} = CONST.THEME; + const localesToThemes = Object.values(themes).map((theme) => ({ value: theme, text: translate(`themePage.themes.${theme}.label`), keyForList: theme, - isSelected: (props.preferredTheme || CONST.THEME.DEFAULT) === theme, + isSelected: (preferredTheme ?? CONST.THEME.DEFAULT) === theme, })); return ( @@ -49,17 +48,15 @@ function ThemePage(props) { User.updateTheme(theme.value)} - initiallyFocusedOptionKey={_.find(localesToThemes, (theme) => theme.isSelected).keyForList} + initiallyFocusedOptionKey={localesToThemes.find((theme) => theme.isSelected)?.keyForList} /> ); } ThemePage.displayName = 'ThemePage'; -ThemePage.propTypes = propTypes; -ThemePage.defaultProps = defaultProps; -export default withOnyx({ +export default withOnyx({ preferredTheme: { key: ONYXKEYS.PREFERRED_THEME, }, From dfd6b8bedea3616571ba853945abef2913504d35 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Thu, 15 Feb 2024 01:59:35 +0530 Subject: [PATCH 0170/1208] removed MoneyRequest route. Signed-off-by: Krishna Gupta --- src/ROUTES.ts | 6 ------ src/pages/iou/IOUCurrencySelection.js | 4 +--- src/pages/iou/request/step/IOURequestStepParticipants.js | 9 +++++---- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 615d7c708d1d..691ff5c78551 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -260,12 +260,6 @@ const ROUTES = { route: 'r/:reportID/invite', getRoute: (reportID: string) => `r/${reportID}/invite` as const, }, - - // To see the available iouType, please refer to CONST.IOU.TYPE - MONEY_REQUEST: { - route: ':iouType/new/:reportID?', - getRoute: (iouType: string, reportID = '') => `${iouType}/new/${reportID}` as const, - }, MONEY_REQUEST_AMOUNT: { route: ':iouType/new/amount/:reportID?', getRoute: (iouType: string, reportID = '') => `${iouType}/new/amount/${reportID}` as const, diff --git a/src/pages/iou/IOUCurrencySelection.js b/src/pages/iou/IOUCurrencySelection.js index 2a48897bfc85..50833534cb0e 100644 --- a/src/pages/iou/IOUCurrencySelection.js +++ b/src/pages/iou/IOUCurrencySelection.js @@ -71,8 +71,6 @@ function IOUCurrencySelection(props) { const [searchValue, setSearchValue] = useState(''); const optionsSelectorRef = useRef(); const selectedCurrencyCode = (lodashGet(props.route, 'params.currency', props.iou.currency) || CONST.CURRENCY.USD).toUpperCase(); - const iouType = lodashGet(props.route, 'params.iouType', CONST.IOU.TYPE.REQUEST); - const reportID = lodashGet(props.route, 'params.reportID', ''); const threadReportID = lodashGet(props.route, 'params.threadReportID', ''); // Decides whether to allow or disallow editing a money request @@ -161,7 +159,7 @@ function IOUCurrencySelection(props) { <> Navigation.goBack(ROUTES.MONEY_REQUEST.getRoute(iouType, reportID))} + onBackButtonPress={() => Navigation.goBack(ROUTES.EDIT_REQUEST.getRoute(threadReportID, CONST.EDIT_REQUEST_FIELD.AMOUNT))} /> { + const headerTitle = useMemo(() => { if (iouType === CONST.IOU.TYPE.SPLIT) { return translate('iou.split'); } @@ -47,7 +47,8 @@ function IOURequestStepParticipants({ return translate('common.send'); } return translate(TransactionUtils.getHeaderTitleTranslationKey(transaction)); - }; + }, [iouType, transaction, translate]); + const receiptFilename = lodashGet(transaction, 'filename'); const receiptPath = lodashGet(transaction, 'receipt.source'); @@ -89,7 +90,7 @@ function IOURequestStepParticipants({ return ( Date: Thu, 15 Feb 2024 02:04:51 +0530 Subject: [PATCH 0171/1208] minor fix. Signed-off-by: Krishna Gupta --- src/pages/iou/IOUCurrencySelection.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pages/iou/IOUCurrencySelection.js b/src/pages/iou/IOUCurrencySelection.js index 50833534cb0e..125b95046d67 100644 --- a/src/pages/iou/IOUCurrencySelection.js +++ b/src/pages/iou/IOUCurrencySelection.js @@ -17,7 +17,6 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; import {iouDefaultProps, iouPropTypes} from './propTypes'; /** @@ -72,6 +71,7 @@ function IOUCurrencySelection(props) { const optionsSelectorRef = useRef(); const selectedCurrencyCode = (lodashGet(props.route, 'params.currency', props.iou.currency) || CONST.CURRENCY.USD).toUpperCase(); const threadReportID = lodashGet(props.route, 'params.threadReportID', ''); + const backTo = lodashGet(props.route, 'params.backTo', ''); // Decides whether to allow or disallow editing a money request useEffect(() => { @@ -96,7 +96,6 @@ function IOUCurrencySelection(props) { const confirmCurrencySelection = useCallback( (option) => { - const backTo = lodashGet(props.route, 'params.backTo', ''); Keyboard.dismiss(); // When we refresh the web, the money request route gets cleared from the navigation stack. @@ -108,7 +107,7 @@ function IOUCurrencySelection(props) { Navigation.navigate(`${props.route.params.backTo}?currency=${option.currencyCode}`); } }, - [props.route, props.navigation], + [props.route, props.navigation, backTo], ); const {translate, currencyList} = props; @@ -159,7 +158,7 @@ function IOUCurrencySelection(props) { <> Navigation.goBack(ROUTES.EDIT_REQUEST.getRoute(threadReportID, CONST.EDIT_REQUEST_FIELD.AMOUNT))} + onBackButtonPress={() => Navigation.goBack(backTo)} /> Date: Thu, 15 Feb 2024 04:37:42 +0500 Subject: [PATCH 0172/1208] 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 0173/1208] 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 0174/1208] 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 2c34dbf214c8511caa8357cfc186aacec0b0fef1 Mon Sep 17 00:00:00 2001 From: Jayesh Mangwani Date: Fri, 16 Feb 2024 14:39:23 +0530 Subject: [PATCH 0175/1208] fixed additional details step and idology questions types errors --- src/ONYXKEYS.ts | 2 ++ .../EnablePayments/AdditionalDetailsStep.tsx | 24 +++++++++++++------ src/pages/EnablePayments/IdologyQuestions.tsx | 4 ++-- src/types/form/index.ts | 2 ++ 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index d94f65954a98..05f064d90d5f 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -429,6 +429,8 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM]: FormTypes.ReimbursementAccountForm; [ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT]: FormTypes.PersonalBankAccountForm; [ONYXKEYS.FORMS.WORKSPACE_DESCRIPTION_FORM]: FormTypes.WorkspaceDescriptionForm; + [ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS]: FormTypes.AdditionalDetailStepForm; + [ONYXKEYS.FORMS.IDOLOGY_QUESTIONS_FORM]: FormTypes.IdologyQuestionsForm; }; type OnyxFormDraftValuesMapping = { diff --git a/src/pages/EnablePayments/AdditionalDetailsStep.tsx b/src/pages/EnablePayments/AdditionalDetailsStep.tsx index c9b557a197e8..b347c93e296e 100644 --- a/src/pages/EnablePayments/AdditionalDetailsStep.tsx +++ b/src/pages/EnablePayments/AdditionalDetailsStep.tsx @@ -6,7 +6,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import DatePicker from '@components/DatePicker'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; -import type {OnyxFormValuesFields} from '@components/Form/types'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; @@ -25,6 +25,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {WalletAdditionalDetails} from '@src/types/onyx'; import IdologyQuestions from './IdologyQuestions'; +import INPUT_IDS from '@src/types/form/AdditionalDetailStepForm'; const DEFAULT_WALLET_ADDITIONAL_DETAILS = { errorFields: {}, @@ -51,7 +52,16 @@ const fieldNameTranslationKeys = { ssn: 'common.ssnLast4', ssnFull9: 'common.ssnFull9', } as const; - +const STEP_FIELDS = [ + INPUT_IDS.LEGAL_FIRST_NAME, + INPUT_IDS.LEGAL_LAST_NAME, + INPUT_IDS.ADDRESS_STREET, + INPUT_IDS.ADDRESS_CITY, + INPUT_IDS.PHONE_NUMBER, + INPUT_IDS.DOB, + INPUT_IDS.ADDRESS_STATE, + INPUT_IDS.SSN +]; function AdditionalDetailsStep({walletAdditionalDetails = DEFAULT_WALLET_ADDITIONAL_DETAILS, currentUserPersonalDetails}: AdditionalDetailsStepProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); @@ -60,9 +70,9 @@ function AdditionalDetailsStep({walletAdditionalDetails = DEFAULT_WALLET_ADDITIO const maxDate = subYears(currentDate, CONST.DATE_BIRTH.MIN_AGE_FOR_PAYMENT); const shouldAskForFullSSN = walletAdditionalDetails?.errorCode === CONST.WALLET.ERROR.SSN; - const validate = (values: OnyxFormValuesFields) => { + const validate = (values: FormOnyxValues): FormInputErrors => { const requiredFields = ['legalFirstName', 'legalLastName', 'addressStreet', 'addressCity', 'addressZipCode', 'phoneNumber', 'dob', 'ssn', 'addressState']; - const errors = ValidationUtils.getFieldRequiredErrors(values, requiredFields); + const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); if (values.dob) { if (!ValidationUtils.isValidPastDate(values.dob) || !ValidationUtils.meetsMaximumAgeRequirement(values.dob)) { @@ -97,7 +107,7 @@ function AdditionalDetailsStep({walletAdditionalDetails = DEFAULT_WALLET_ADDITIO return errors; }; - const activateWallet = (values: OnyxFormValuesFields) => { + const activateWallet = (values: FormOnyxValues) => { const personalDetails = { phoneNumber: (values.phoneNumber && parsePhoneNumber(values.phoneNumber, {regionCode: CONST.COUNTRY.US}).number?.significant) ?? '', legalFirstName: values.legalFirstName ?? '', @@ -197,10 +207,10 @@ function AdditionalDetailsStep({walletAdditionalDetails = DEFAULT_WALLET_ADDITIO placeholder={translate('common.phoneNumberPlaceholder')} shouldSaveDraft /> - InputComponent={DatePicker} inputID="dob" - // @ts-expect-error TODO: Remove this once DatePicker (https://github.com/Expensify/App/issues/25148) is migrated to TypeScript. containerStyles={[styles.mt4]} label={translate(fieldNameTranslationKeys.dob)} placeholder={translate('common.dob')} diff --git a/src/pages/EnablePayments/IdologyQuestions.tsx b/src/pages/EnablePayments/IdologyQuestions.tsx index 4a5ef72019c0..f54f5a7cccba 100644 --- a/src/pages/EnablePayments/IdologyQuestions.tsx +++ b/src/pages/EnablePayments/IdologyQuestions.tsx @@ -3,7 +3,7 @@ import {View} from 'react-native'; import type {WalletAdditionalQuestionDetails} from 'src/types/onyx'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; -import type {OnyxFormValuesFields} from '@components/Form/types'; +import type {FormOnyxValues,FormInputErrors} from '@components/Form/types'; import type {Choice} from '@components/RadioButtons'; import SingleChoiceQuestion from '@components/SingleChoiceQuestion'; import Text from '@components/Text'; @@ -91,7 +91,7 @@ function IdologyQuestions({questions, idNumber}: IdologyQuestionsProps) { } }; - const validate = (values: OnyxFormValuesFields) => { + const validate = (values: FormOnyxValues) => { const errors: Errors = {}; if (!values.answer) { errors.answer = translate('additionalDetailsStep.selectAnswer'); diff --git a/src/types/form/index.ts b/src/types/form/index.ts index d9263991023c..9e457f83ace6 100644 --- a/src/types/form/index.ts +++ b/src/types/form/index.ts @@ -34,4 +34,6 @@ export type {WorkspaceRateAndUnitForm} from './WorkspaceRateAndUnitForm'; export type {WorkspaceSettingsForm} from './WorkspaceSettingsForm'; export type {WorkspaceDescriptionForm} from './WorkspaceDescriptionForm'; export type {WorkspaceProfileDescriptionForm} from './WorkspaceProfileDescriptionForm'; +export type {AdditionalDetailStepForm} from './AdditionalDetailStepForm'; +export type {IdologyQuestionsForm} from './IdologyQuestionsForm'; export type {default as Form} from './Form'; From 98ef6936cc57af1d9b20a24e1053eb8b7dad7a04 Mon Sep 17 00:00:00 2001 From: Jayesh Mangwani Date: Fri, 16 Feb 2024 14:46:29 +0530 Subject: [PATCH 0176/1208] code prettified & lint fixed --- .../EnablePayments/AdditionalDetailsStep.tsx | 5 ++--- src/pages/EnablePayments/IdologyQuestions.tsx | 4 ++-- src/types/form/AdditionalDetailStepForm.ts | 2 +- src/types/form/IdologyQuestionsForm.ts | 2 +- src/types/onyx/index.ts | 18 +----------------- 5 files changed, 7 insertions(+), 24 deletions(-) diff --git a/src/pages/EnablePayments/AdditionalDetailsStep.tsx b/src/pages/EnablePayments/AdditionalDetailsStep.tsx index b347c93e296e..97041a5b021a 100644 --- a/src/pages/EnablePayments/AdditionalDetailsStep.tsx +++ b/src/pages/EnablePayments/AdditionalDetailsStep.tsx @@ -23,9 +23,9 @@ import AddressForm from '@pages/ReimbursementAccount/AddressForm'; import * as Wallet from '@userActions/Wallet'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/AdditionalDetailStepForm'; import type {WalletAdditionalDetails} from '@src/types/onyx'; import IdologyQuestions from './IdologyQuestions'; -import INPUT_IDS from '@src/types/form/AdditionalDetailStepForm'; const DEFAULT_WALLET_ADDITIONAL_DETAILS = { errorFields: {}, @@ -60,7 +60,7 @@ const STEP_FIELDS = [ INPUT_IDS.PHONE_NUMBER, INPUT_IDS.DOB, INPUT_IDS.ADDRESS_STATE, - INPUT_IDS.SSN + INPUT_IDS.SSN, ]; function AdditionalDetailsStep({walletAdditionalDetails = DEFAULT_WALLET_ADDITIONAL_DETAILS, currentUserPersonalDetails}: AdditionalDetailsStepProps) { const {translate} = useLocalize(); @@ -71,7 +71,6 @@ function AdditionalDetailsStep({walletAdditionalDetails = DEFAULT_WALLET_ADDITIO const shouldAskForFullSSN = walletAdditionalDetails?.errorCode === CONST.WALLET.ERROR.SSN; const validate = (values: FormOnyxValues): FormInputErrors => { - const requiredFields = ['legalFirstName', 'legalLastName', 'addressStreet', 'addressCity', 'addressZipCode', 'phoneNumber', 'dob', 'ssn', 'addressState']; const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); if (values.dob) { diff --git a/src/pages/EnablePayments/IdologyQuestions.tsx b/src/pages/EnablePayments/IdologyQuestions.tsx index f54f5a7cccba..cadb7092f0c4 100644 --- a/src/pages/EnablePayments/IdologyQuestions.tsx +++ b/src/pages/EnablePayments/IdologyQuestions.tsx @@ -3,7 +3,7 @@ import {View} from 'react-native'; import type {WalletAdditionalQuestionDetails} from 'src/types/onyx'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; -import type {FormOnyxValues,FormInputErrors} from '@components/Form/types'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; import type {Choice} from '@components/RadioButtons'; import SingleChoiceQuestion from '@components/SingleChoiceQuestion'; import Text from '@components/Text'; @@ -91,7 +91,7 @@ function IdologyQuestions({questions, idNumber}: IdologyQuestionsProps) { } }; - const validate = (values: FormOnyxValues) => { + const validate = (values: FormOnyxValues): FormInputErrors => { const errors: Errors = {}; if (!values.answer) { errors.answer = translate('additionalDetailsStep.selectAnswer'); diff --git a/src/types/form/AdditionalDetailStepForm.ts b/src/types/form/AdditionalDetailStepForm.ts index b432ae87e2cf..f102d03679ba 100644 --- a/src/types/form/AdditionalDetailStepForm.ts +++ b/src/types/form/AdditionalDetailStepForm.ts @@ -25,4 +25,4 @@ type AdditionalDetailStepForm = Form<{ }>; export type {AdditionalDetailStepForm}; -export default INPUT_IDS; \ No newline at end of file +export default INPUT_IDS; diff --git a/src/types/form/IdologyQuestionsForm.ts b/src/types/form/IdologyQuestionsForm.ts index eb48b9027541..5b8d50c68abf 100644 --- a/src/types/form/IdologyQuestionsForm.ts +++ b/src/types/form/IdologyQuestionsForm.ts @@ -9,4 +9,4 @@ type IdologyQuestionsForm = Form<{ }>; export type {IdologyQuestionsForm}; -export default INPUT_IDS; \ No newline at end of file +export default INPUT_IDS; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index e116aa99e95a..de9e96d8590b 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -11,23 +11,7 @@ import type Credentials from './Credentials'; import type Currency from './Currency'; import type CustomStatusDraft from './CustomStatusDraft'; import type Download from './Download'; -import type { - AddDebitCardForm, - AdditionalDetailStepForm, - CloseAccountForm, - DateOfBirthForm, - DisplayNameForm, - GetPhysicalCardForm, - IdologyQuestionsForm, - IKnowATeacherForm, - IntroSchoolPrincipalForm, - NewRoomForm, - PrivateNotesForm, - ReportFieldEditForm, - RoomNameForm, - WorkspaceSettingsForm, -} from './Form'; -import type Form from './Form'; +import type {AdditionalDetailStepForm, IdologyQuestionsForm, PrivateNotesForm, ReportFieldEditForm, RoomNameForm} from './Form'; import type FrequentlyUsedEmoji from './FrequentlyUsedEmoji'; import type {FundList} from './Fund'; import type Fund from './Fund'; From 0c056bed123c0eff882e120b692b04428fa5cf40 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 16 Feb 2024 12:59:10 +0100 Subject: [PATCH 0177/1208] ref: move SignInOrAvatarWithOptionalStatus to TS --- ...js => SignInOrAvatarWithOptionalStatus.tsx} | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) rename src/pages/home/sidebar/{SignInOrAvatarWithOptionalStatus.js => SignInOrAvatarWithOptionalStatus.tsx} (61%) diff --git a/src/pages/home/sidebar/SignInOrAvatarWithOptionalStatus.js b/src/pages/home/sidebar/SignInOrAvatarWithOptionalStatus.tsx similarity index 61% rename from src/pages/home/sidebar/SignInOrAvatarWithOptionalStatus.js rename to src/pages/home/sidebar/SignInOrAvatarWithOptionalStatus.tsx index 0ea6195cd713..2a9356d78232 100644 --- a/src/pages/home/sidebar/SignInOrAvatarWithOptionalStatus.js +++ b/src/pages/home/sidebar/SignInOrAvatarWithOptionalStatus.tsx @@ -1,6 +1,3 @@ -/* eslint-disable rulesdir/onyx-props-must-have-default */ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React from 'react'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import * as Session from '@userActions/Session'; @@ -8,18 +5,13 @@ import AvatarWithOptionalStatus from './AvatarWithOptionalStatus'; import PressableAvatarWithIndicator from './PressableAvatarWithIndicator'; import SignInButton from './SignInButton'; -const propTypes = { - /** Whether the create menu is open or not */ - isCreateMenuOpen: PropTypes.bool, +type SignInOrAvatarWithOptionalStatusProps = { + isCreateMenuOpen?: boolean; }; -const defaultProps = { - isCreateMenuOpen: false, -}; - -function SignInOrAvatarWithOptionalStatus({isCreateMenuOpen}) { +function SignInOrAvatarWithOptionalStatus({isCreateMenuOpen = false}: SignInOrAvatarWithOptionalStatusProps) { const currentUserPersonalDetails = useCurrentUserPersonalDetails(); - const emojiStatus = lodashGet(currentUserPersonalDetails, 'status.emojiCode', ''); + const emojiStatus = currentUserPersonalDetails.status?.emojiCode ?? ''; if (Session.isAnonymousUser()) { return ; @@ -35,7 +27,5 @@ function SignInOrAvatarWithOptionalStatus({isCreateMenuOpen}) { return ; } -SignInOrAvatarWithOptionalStatus.propTypes = propTypes; -SignInOrAvatarWithOptionalStatus.defaultProps = defaultProps; SignInOrAvatarWithOptionalStatus.displayName = 'SignInOrAvatarWithOptionalStatus'; export default SignInOrAvatarWithOptionalStatus; From 6bd5633ec4631b4a1ef76b03c5a7e576d7d15ca8 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 16 Feb 2024 13:04:18 +0100 Subject: [PATCH 0178/1208] ref: move SignInButton to TS --- src/pages/home/sidebar/{SignInButton.js => SignInButton.tsx} | 1 - 1 file changed, 1 deletion(-) rename src/pages/home/sidebar/{SignInButton.js => SignInButton.tsx} (95%) diff --git a/src/pages/home/sidebar/SignInButton.js b/src/pages/home/sidebar/SignInButton.tsx similarity index 95% rename from src/pages/home/sidebar/SignInButton.js rename to src/pages/home/sidebar/SignInButton.tsx index f89deb6f65b2..1dc65bfd5050 100644 --- a/src/pages/home/sidebar/SignInButton.js +++ b/src/pages/home/sidebar/SignInButton.tsx @@ -1,4 +1,3 @@ -/* eslint-disable rulesdir/onyx-props-must-have-default */ import React from 'react'; import {View} from 'react-native'; import Button from '@components/Button'; From 5ed0bc2c9fc59b181d3067e88b7c10c36e0ed497 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 16 Feb 2024 13:05:47 +0100 Subject: [PATCH 0179/1208] ref: removed SidebarNavigationContext --- src/pages/home/sidebar/SidebarNavigationContext.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/pages/home/sidebar/SidebarNavigationContext.js diff --git a/src/pages/home/sidebar/SidebarNavigationContext.js b/src/pages/home/sidebar/SidebarNavigationContext.js deleted file mode 100644 index e69de29bb2d1..000000000000 From 5443274f38ae358c00e9013f8100f7195ebe77e5 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 16 Feb 2024 16:19:53 +0100 Subject: [PATCH 0180/1208] ref: move SidebarLinksData to TS --- src/libs/ReportUtils.ts | 2 +- src/libs/SidebarUtils.ts | 14 +- ...debarLinksData.js => SidebarLinksData.tsx} | 296 +++++++----------- 3 files changed, 127 insertions(+), 185 deletions(-) rename src/pages/home/sidebar/{SidebarLinksData.js => SidebarLinksData.tsx} (53%) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index b5da21c0f67e..40581f46e76a 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -3824,7 +3824,7 @@ function shouldReportBeInOptionList({ report: OnyxEntry; currentReportId: string; isInGSDMode: boolean; - betas: Beta[]; + betas: OnyxEntry; policies: OnyxCollection; excludeEmptyChats: boolean; doesReportHaveViolations: boolean; diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 5fe646c5ad13..7b757d7fc5b8 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -79,10 +79,10 @@ let hasInitialReportActions = false; */ function getOrderedReportIDs( currentReportId: string | null, - allReports: Record, - betas: Beta[], - policies: Record, - priorityMode: ValueOf, + allReports: OnyxEntry>, + betas: OnyxEntry, + policies: OnyxEntry>, + priorityMode: OnyxEntry>, allReportActions: OnyxCollection, transactionViolations: OnyxCollection, currentPolicyID = '', @@ -110,7 +110,7 @@ function getOrderedReportIDs( const isInGSDMode = priorityMode === CONST.PRIORITY_MODE.GSD; const isInDefaultMode = !isInGSDMode; - const allReportsDictValues = Object.values(allReports); + const allReportsDictValues = Object.values(allReports ?? {}); // Filter out all the reports that shouldn't be displayed let reportsToDisplay = allReportsDictValues.filter((report) => { @@ -118,7 +118,7 @@ function getOrderedReportIDs( const parentReportActions = allReportActions?.[parentReportActionsKey]; const parentReportAction = parentReportActions?.find((action) => action && report && action?.reportActionID === report?.parentReportActionID); const doesReportHaveViolations = - betas.includes(CONST.BETAS.VIOLATIONS) && !!parentReportAction && ReportUtils.doesTransactionThreadHaveViolations(report, transactionViolations, parentReportAction); + betas?.includes(CONST.BETAS.VIOLATIONS) && !!parentReportAction && ReportUtils.doesTransactionThreadHaveViolations(report, transactionViolations, parentReportAction); return ReportUtils.shouldReportBeInOptionList({ report, currentReportId: currentReportId ?? '', @@ -126,7 +126,7 @@ function getOrderedReportIDs( betas, policies, excludeEmptyChats: true, - doesReportHaveViolations, + doesReportHaveViolations: !!doesReportHaveViolations, }); }); diff --git a/src/pages/home/sidebar/SidebarLinksData.js b/src/pages/home/sidebar/SidebarLinksData.tsx similarity index 53% rename from src/pages/home/sidebar/SidebarLinksData.js rename to src/pages/home/sidebar/SidebarLinksData.tsx index 3bd538e8beab..b128fcaf33ec 100644 --- a/src/pages/home/sidebar/SidebarLinksData.js +++ b/src/pages/home/sidebar/SidebarLinksData.tsx @@ -1,154 +1,114 @@ +import {useIsFocused} from '@react-navigation/native'; import {deepEqual} from 'fast-equals'; -import lodashGet from 'lodash/get'; -import lodashMap from 'lodash/map'; -import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import {View} from 'react-native'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import networkPropTypes from '@components/networkPropTypes'; -import {withNetwork} from '@components/OnyxProvider'; -import withCurrentReportID from '@components/withCurrentReportID'; -import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; -import withNavigationFocus from '@components/withNavigationFocus'; +import type {EdgeInsets} from 'react-native-safe-area-context'; +import type {ValueOf} from 'type-fest'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; +import useCurrentReportID from '@hooks/useCurrentReportID'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import {getPolicyMembersByIdWithoutCurrentUser} from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import SidebarUtils from '@libs/SidebarUtils'; -import reportPropTypes from '@pages/reportPropTypes'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import SidebarLinks, {basePropTypes} from './SidebarLinks'; +import type * as OnyxTypes from '@src/types/onyx'; +import type {Message} from '@src/types/onyx/ReportAction'; +import SidebarLinks from './SidebarLinks'; -const propTypes = { - ...basePropTypes, - - /* Onyx Props */ - /** List of reports */ - chatReports: PropTypes.objectOf(reportPropTypes), - - /** All report actions for all reports */ - - /** Object of report actions for this report */ - allReportActions: PropTypes.objectOf( - PropTypes.arrayOf( - PropTypes.shape({ - error: PropTypes.string, - message: PropTypes.arrayOf( - PropTypes.shape({ - moderationDecision: PropTypes.shape({ - decision: PropTypes.string, - }), - }), - ), - }), - ), - ), - - /** Whether the reports are loading. When false it means they are ready to be used. */ - isLoadingApp: PropTypes.bool, - - /** The chat priority mode */ - priorityMode: PropTypes.string, - - /** Beta features list */ - betas: PropTypes.arrayOf(PropTypes.string), - - network: networkPropTypes.isRequired, - - /** The policies which the user has access to */ - // eslint-disable-next-line react/forbid-prop-types - policies: PropTypes.object, - - // eslint-disable-next-line react/forbid-prop-types - policyMembers: PropTypes.object, - - /** Session info for the currently logged in user. */ - session: PropTypes.shape({ - /** Currently logged in user accountID */ - accountID: PropTypes.number, - }), - /** All of the transaction violations */ - transactionViolations: PropTypes.shape({ - violations: PropTypes.arrayOf( - PropTypes.shape({ - /** The transaction ID */ - transactionID: PropTypes.number, - - /** The transaction violation type */ - type: PropTypes.string, - - /** The transaction violation message */ - message: PropTypes.string, - - /** The transaction violation data */ - data: PropTypes.shape({ - /** The transaction violation data field */ - field: PropTypes.string, - - /** The transaction violation data value */ - value: PropTypes.string, - }), - }), - ), - }), +type SidebarLinksDataOnyxProps = { + chatReports: OnyxEntry< + Pick< + OnyxTypes.Report, + | 'reportID' + | 'participantAccountIDs' + | 'hasDraft' + | 'isPinned' + | 'isHidden' + | 'notificationPreference' + | 'errorFields' + | 'lastMessageText' + | 'lastVisibleActionCreated' + | 'iouReportID' + | 'total' + | 'nonReimbursableTotal' + | 'hasOutstandingChildRequest' + | 'isWaitingOnBankAccount' + | 'statusNum' + | 'stateNum' + | 'chatType' + | 'type' + | 'policyID' + | 'visibility' + | 'lastReadTime' + | 'reportName' + | 'policyName' + | 'oldPolicyName' + | 'ownerAccountID' + | 'currency' + | 'managerID' + | 'parentReportActionID' + | 'parentReportID' + | 'isDeletedParentAction' + > & {isUnreadWithMention: boolean} + >; + isLoadingApp: OnyxEntry; + priorityMode: OnyxEntry>; + betas: OnyxEntry; + allReportActions: OnyxEntry>>; + policies: OnyxEntry>; + policyMembers: OnyxCollection; + transactionViolations: OnyxCollection; }; -const defaultProps = { - chatReports: {}, - allReportActions: {}, - isLoadingApp: true, - priorityMode: CONST.PRIORITY_MODE.DEFAULT, - betas: [], - policies: {}, - policyMembers: {}, - session: { - accountID: '', - }, - transactionViolations: {}, +type SidebarLinksDataProps = SidebarLinksDataOnyxProps & { + onLinkClick: (reportID: number) => void; + insets: EdgeInsets; }; function SidebarLinksData({ - isFocused, allReportActions, betas, chatReports, - currentReportID, insets, - isLoadingApp, + isLoadingApp = true, onLinkClick, policies, - priorityMode, - network, + priorityMode = CONST.PRIORITY_MODE.DEFAULT, policyMembers, - session: {accountID}, + // session: {accountID}, transactionViolations, -}) { +}: SidebarLinksDataProps) { + const {currentReportID} = useCurrentReportID() ?? {}; + const {accountID} = useCurrentUserPersonalDetails(); + const network = useNetwork(); + const isFocused = useIsFocused(); const styles = useThemeStyles(); const {activeWorkspaceID} = useActiveWorkspace(); const {translate} = useLocalize(); const prevPriorityMode = usePrevious(priorityMode); - const policyMemberAccountIDs = getPolicyMembersByIdWithoutCurrentUser(policyMembers, activeWorkspaceID, accountID); // eslint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => Policy.openWorkspace(activeWorkspaceID, policyMemberAccountIDs), [activeWorkspaceID]); + useEffect(() => Policy.openWorkspace(activeWorkspaceID ?? '', policyMemberAccountIDs), [activeWorkspaceID]); - const reportIDsRef = useRef(null); + const reportIDsRef = useRef(null); const isLoading = isLoadingApp; const optionListItems = useMemo(() => { const reportIDs = SidebarUtils.getOrderedReportIDs( null, - chatReports, + chatReports as OnyxEntry>, betas, - policies, + policies as OnyxEntry>, priorityMode, - allReportActions, + allReportActions as OnyxEntry>, transactionViolations, activeWorkspaceID, policyMemberAccountIDs, @@ -161,7 +121,7 @@ function SidebarLinksData({ // 1. We need to update existing reports only once while loading because they are updated several times during loading and causes this regression: https://github.com/Expensify/App/issues/24596#issuecomment-1681679531 // 2. If the user is offline, we need to update the reports unconditionally, since the loading of report data might be stuck in this case. // 3. Changing priority mode to Most Recent will call OpenApp. If there is an existing reports and the priority mode is updated, we want to immediately update the list instead of waiting the OpenApp request to complete - if (!isLoading || !reportIDsRef.current || network.isOffline || (reportIDsRef.current && prevPriorityMode !== priorityMode)) { + if (!isLoading || !reportIDsRef.current || !!network.isOffline || (reportIDsRef.current && prevPriorityMode !== priorityMode)) { reportIDsRef.current = reportIDs; } return reportIDsRef.current || []; @@ -173,14 +133,14 @@ function SidebarLinksData({ // the current report is missing from the list, which should very rarely happen. In this // case we re-generate the list a 2nd time with the current report included. const optionListItemsWithCurrentReport = useMemo(() => { - if (currentReportID && !_.contains(optionListItems, currentReportID)) { + if (currentReportID && !optionListItems?.includes(currentReportID)) { return SidebarUtils.getOrderedReportIDs( currentReportID, - chatReports, + chatReports as OnyxEntry>, betas, - policies, + policies as OnyxEntry>, priorityMode, - allReportActions, + allReportActions as OnyxEntry>, transactionViolations, activeWorkspaceID, policyMemberAccountIDs, @@ -191,7 +151,7 @@ function SidebarLinksData({ const currentReportIDRef = useRef(currentReportID); currentReportIDRef.current = currentReportID; - const isActiveReport = useCallback((reportID) => currentReportIDRef.current === reportID, []); + const isActiveReport = useCallback((reportID: string) => currentReportIDRef.current === reportID, []); return ( +const chatReportSelector = (report: OnyxEntry) => report && { reportID: report.reportID, participantAccountIDs: report.participantAccountIDs, @@ -233,7 +190,7 @@ const chatReportSelector = (report) => isHidden: report.isHidden, notificationPreference: report.notificationPreference, errorFields: { - addWorkspaceRoom: report.errorFields && report.errorFields.addWorkspaceRoom, + addWorkspaceRoom: report.errorFields?.addWorkspaceRoom, }, lastMessageText: report.lastMessageText, lastVisibleActionCreated: report.lastVisibleActionCreated, @@ -264,78 +221,63 @@ const chatReportSelector = (report) => isUnreadWithMention: ReportUtils.isUnreadWithMention(report), }; -/** - * @param {Object} [reportActions] - * @returns {Object|undefined} - */ -const reportActionsSelector = (reportActions) => +const reportActionsSelector = (reportActions: OnyxEntry) => reportActions && - lodashMap(reportActions, (reportAction) => { - const {reportActionID, parentReportActionID, actionName, errors = []} = reportAction; - const decision = lodashGet(reportAction, 'message[0].moderationDecision.decision'); + Object.values(reportActions).map((reportAction) => { + const {reportActionID, actionName, errors} = reportAction; + const decision = reportAction.message?.[0].moderationDecision?.decision; return { reportActionID, - parentReportActionID, actionName, errors, message: [ { moderationDecision: {decision}, }, - ], + ] as Message[], }; }); -/** - * @param {Object} [policy] - * @returns {Object|undefined} - */ -const policySelector = (policy) => +const policySelector = (policy: OnyxEntry) => policy && { type: policy.type, name: policy.name, avatar: policy.avatar, }; -export default compose( - withCurrentReportID, - withCurrentUserPersonalDetails, - withNavigationFocus, - withNetwork(), - withOnyx({ - chatReports: { - key: ONYXKEYS.COLLECTION.REPORT, - selector: chatReportSelector, - initialValue: {}, - }, - isLoadingApp: { - key: ONYXKEYS.IS_LOADING_APP, - }, - priorityMode: { - key: ONYXKEYS.NVP_PRIORITY_MODE, - initialValue: CONST.PRIORITY_MODE.DEFAULT, - }, - betas: { - key: ONYXKEYS.BETAS, - initialValue: [], - }, - allReportActions: { - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - selector: reportActionsSelector, - initialValue: {}, - }, - policies: { - key: ONYXKEYS.COLLECTION.POLICY, - selector: policySelector, - initialValue: {}, - }, - policyMembers: { - key: ONYXKEYS.COLLECTION.POLICY_MEMBERS, - }, - transactionViolations: { - key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, - initialValue: {}, - }, - }), -)(SidebarLinksData); +export default withOnyx({ + chatReports: { + key: ONYXKEYS.COLLECTION.REPORT, + selector: chatReportSelector, + initialValue: {}, + }, + isLoadingApp: { + key: ONYXKEYS.IS_LOADING_APP, + }, + priorityMode: { + key: ONYXKEYS.NVP_PRIORITY_MODE, + initialValue: CONST.PRIORITY_MODE.DEFAULT, + }, + betas: { + key: ONYXKEYS.BETAS, + initialValue: [], + }, + allReportActions: { + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + selector: reportActionsSelector, + initialValue: {}, + }, + policies: { + key: ONYXKEYS.COLLECTION.POLICY, + selector: policySelector, + initialValue: {}, + }, + policyMembers: { + key: ONYXKEYS.COLLECTION.POLICY_MEMBERS, + }, + transactionViolations: { + key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, + initialValue: {}, + }, +})(SidebarLinksData); From bc5cd4df25c3dbd9e8a07e14a8abc44d61ec32b0 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 16 Feb 2024 16:46:36 +0100 Subject: [PATCH 0181/1208] ref: move SidebarLinks to TS --- src/components/LHNOptionsList/types.ts | 2 +- .../{SidebarLinks.js => SidebarLinks.tsx} | 64 +++++++++---------- src/pages/home/sidebar/SidebarLinksData.tsx | 4 +- 3 files changed, 32 insertions(+), 38 deletions(-) rename src/pages/home/sidebar/{SidebarLinks.js => SidebarLinks.tsx} (78%) diff --git a/src/components/LHNOptionsList/types.ts b/src/components/LHNOptionsList/types.ts index 58bea97f04c9..f3d6bde9d41c 100644 --- a/src/components/LHNOptionsList/types.ts +++ b/src/components/LHNOptionsList/types.ts @@ -45,7 +45,7 @@ type CustomLHNOptionsListProps = { contentContainerStyles?: StyleProp; /** Sections for the section list */ - data: string[]; + data: string[] | null; /** Callback to fire when a row is selected */ onSelectRow?: (optionItem: OptionData, popoverAnchor: RefObject) => void; diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.tsx similarity index 78% rename from src/pages/home/sidebar/SidebarLinks.js rename to src/pages/home/sidebar/SidebarLinks.tsx index 9431bae68d8a..52165c148727 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.tsx @@ -1,10 +1,9 @@ -/* eslint-disable rulesdir/onyx-props-must-have-default */ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import {InteractionManager, StyleSheet, View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {EdgeInsets} from 'react-native-safe-area-context'; +import type {ValueOf} from 'type-fest'; import Breadcrumbs from '@components/Breadcrumbs'; import LHNOptionsList from '@components/LHNOptionsList/LHNOptionsList'; import OptionsListSkeletonView from '@components/OptionsListSkeletonView'; @@ -16,37 +15,33 @@ import KeyboardShortcut from '@libs/KeyboardShortcut'; import Navigation from '@libs/Navigation/Navigation'; import onyxSubscribe from '@libs/onyxSubscribe'; import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu'; -import safeAreaInsetPropTypes from '@pages/safeAreaInsetPropTypes'; import * as App from '@userActions/App'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {Modal, Policy, Report} from '@src/types/onyx'; -const basePropTypes = { - /** Toggles the navigation menu open and closed */ - onLinkClick: PropTypes.func.isRequired, - - /** Safe area insets required for mobile devices margins */ - insets: safeAreaInsetPropTypes.isRequired, +type SidebarLinksOnyxProps = { + activePolicy: OnyxEntry; }; -const propTypes = { - ...basePropTypes, - - optionListItems: PropTypes.arrayOf(PropTypes.string).isRequired, - - isLoading: PropTypes.bool.isRequired, - - // eslint-disable-next-line react/require-default-props - priorityMode: PropTypes.oneOf(_.values(CONST.PRIORITY_MODE)), - - isActiveReport: PropTypes.func.isRequired, +type SidebarLinksProps = SidebarLinksOnyxProps & { + onLinkClick: () => void; + insets: EdgeInsets; + optionListItems: string[] | null; + isLoading: OnyxEntry; + priorityMode?: OnyxEntry>; + isActiveReport: (reportID: string) => boolean; + isCreateMenuOpen?: boolean; + + // eslint-disable-next-line react/no-unused-prop-types -- its used in withOnyx + activeWorkspaceID: string | undefined; }; -function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priorityMode = CONST.PRIORITY_MODE.DEFAULT, isActiveReport, isCreateMenuOpen, activePolicy}) { +function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priorityMode = CONST.PRIORITY_MODE.DEFAULT, isActiveReport, isCreateMenuOpen, activePolicy}: SidebarLinksProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const modal = useRef({}); + const modal = useRef({}); const {translate, updateLocale} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); @@ -67,7 +62,7 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority const unsubscribeOnyxModal = onyxSubscribe({ key: ONYXKEYS.MODAL, callback: (modalArg) => { - if (_.isNull(modalArg) || typeof modalArg !== 'object') { + if (modalArg === null || typeof modalArg !== 'object') { return; } modal.current = modalArg; @@ -105,18 +100,19 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority /** * Show Report page with selected report id - * - * @param {Object} option - * @param {String} option.reportID */ const showReportPage = useCallback( - (option) => { + (option: Report) => { // Prevent opening Report page when clicking LHN row quickly after clicking FAB icon // or when clicking the active LHN row on large screens // or when continuously clicking different LHNs, only apply to small screen // since getTopmostReportId always returns on other devices const reportActionID = Navigation.getTopmostReportActionId(); - if (isCreateMenuOpen || (option.reportID === Navigation.getTopmostReportId() && !reportActionID) || (isSmallScreenWidth && isActiveReport(option.reportID) && !reportActionID)) { + if ( + !!isCreateMenuOpen || + (option.reportID === Navigation.getTopmostReportId() && !reportActionID) || + (isSmallScreenWidth && isActiveReport(option.reportID) && !reportActionID) + ) { return; } Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(option.reportID)); @@ -137,7 +133,7 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority activePolicy ? { type: CONST.BREADCRUMB_TYPE.STRONG, - text: lodashGet(activePolicy, 'name', ''), + text: activePolicy.name ?? '', } : { type: CONST.BREADCRUMB_TYPE.ROOT, @@ -158,7 +154,7 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority optionMode={viewMode} onFirstItemRendered={App.setSidebarLoaded} /> - {isLoading && optionListItems.length === 0 && ( + {isLoading && optionListItems?.length === 0 && ( @@ -168,12 +164,10 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority ); } -SidebarLinks.propTypes = propTypes; SidebarLinks.displayName = 'SidebarLinks'; -export default withOnyx({ +export default withOnyx({ activePolicy: { key: ({activeWorkspaceID}) => `${ONYXKEYS.COLLECTION.POLICY}${activeWorkspaceID}`, }, })(SidebarLinks); -export {basePropTypes}; diff --git a/src/pages/home/sidebar/SidebarLinksData.tsx b/src/pages/home/sidebar/SidebarLinksData.tsx index b128fcaf33ec..ddd232e99275 100644 --- a/src/pages/home/sidebar/SidebarLinksData.tsx +++ b/src/pages/home/sidebar/SidebarLinksData.tsx @@ -69,7 +69,7 @@ type SidebarLinksDataOnyxProps = { }; type SidebarLinksDataProps = SidebarLinksDataOnyxProps & { - onLinkClick: (reportID: number) => void; + onLinkClick: () => void; insets: EdgeInsets; }; @@ -101,7 +101,7 @@ function SidebarLinksData({ const reportIDsRef = useRef(null); const isLoading = isLoadingApp; - const optionListItems = useMemo(() => { + const optionListItems: string[] | null = useMemo(() => { const reportIDs = SidebarUtils.getOrderedReportIDs( null, chatReports as OnyxEntry>, From 3fb3715847c5c82958147940cdcb68073456aedf Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 16 Feb 2024 16:54:54 +0100 Subject: [PATCH 0182/1208] ref: move PressableAvatarWithIndicator to TS --- ...or.js => PressableAvatarWithIndicator.tsx} | 52 ++++++------------- 1 file changed, 17 insertions(+), 35 deletions(-) rename src/pages/home/sidebar/{PressableAvatarWithIndicator.js => PressableAvatarWithIndicator.tsx} (56%) diff --git a/src/pages/home/sidebar/PressableAvatarWithIndicator.js b/src/pages/home/sidebar/PressableAvatarWithIndicator.tsx similarity index 56% rename from src/pages/home/sidebar/PressableAvatarWithIndicator.js rename to src/pages/home/sidebar/PressableAvatarWithIndicator.tsx index 63c5936e957b..e07b6e856823 100644 --- a/src/pages/home/sidebar/PressableAvatarWithIndicator.js +++ b/src/pages/home/sidebar/PressableAvatarWithIndicator.tsx @@ -1,44 +1,30 @@ -/* eslint-disable rulesdir/onyx-props-must-have-default */ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React, {useCallback} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import AvatarWithIndicator from '@components/AvatarWithIndicator'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; -import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; -import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import * as UserUtils from '@libs/UserUtils'; -import personalDetailsPropType from '@pages/personalDetailsPropType'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -const propTypes = { - /** Whether the create menu is open or not */ - isCreateMenuOpen: PropTypes.bool, - - /** The personal details of the person who is logged in */ - currentUserPersonalDetails: personalDetailsPropType, - +type PressableAvatarWithIndicatorOnyxProps = { /** Indicates whether the app is loading initial data */ - isLoading: PropTypes.bool, + isLoading: OnyxEntry; }; -const defaultProps = { - isCreateMenuOpen: false, - currentUserPersonalDetails: { - pendingFields: {avatar: ''}, - accountID: '', - avatar: '', - }, - isLoading: true, +type PressableAvatarWithIndicatorProps = PressableAvatarWithIndicatorOnyxProps & { + /** Whether the create menu is open or not */ + isCreateMenuOpen: boolean; }; -function PressableAvatarWithIndicator({isCreateMenuOpen, currentUserPersonalDetails, isLoading}) { +function PressableAvatarWithIndicator({isCreateMenuOpen = false, isLoading = true}: PressableAvatarWithIndicatorProps) { const {translate} = useLocalize(); + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const showSettingsPage = useCallback(() => { if (isCreateMenuOpen) { @@ -55,26 +41,22 @@ function PressableAvatarWithIndicator({isCreateMenuOpen, currentUserPersonalDeta role={CONST.ROLE.BUTTON} onPress={showSettingsPage} > - + ); } -PressableAvatarWithIndicator.propTypes = propTypes; -PressableAvatarWithIndicator.defaultProps = defaultProps; PressableAvatarWithIndicator.displayName = 'PressableAvatarWithIndicator'; -export default compose( - withCurrentUserPersonalDetails, - withOnyx({ - isLoading: { - key: ONYXKEYS.IS_LOADING_APP, - }, - }), -)(PressableAvatarWithIndicator); + +export default withOnyx({ + isLoading: { + key: ONYXKEYS.IS_LOADING_APP, + }, +})(PressableAvatarWithIndicator); From 5fa814c327d1e9767afa896e3304487885039547 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 16 Feb 2024 16:57:26 +0100 Subject: [PATCH 0183/1208] ref: move AvatarWithOptionalStatus to TS --- ...alStatus.js => AvatarWithOptionalStatus.tsx} | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) rename src/pages/home/sidebar/{AvatarWithOptionalStatus.js => AvatarWithOptionalStatus.tsx} (81%) diff --git a/src/pages/home/sidebar/AvatarWithOptionalStatus.js b/src/pages/home/sidebar/AvatarWithOptionalStatus.tsx similarity index 81% rename from src/pages/home/sidebar/AvatarWithOptionalStatus.js rename to src/pages/home/sidebar/AvatarWithOptionalStatus.tsx index e1ff3982a0cc..5597d46c29bc 100644 --- a/src/pages/home/sidebar/AvatarWithOptionalStatus.js +++ b/src/pages/home/sidebar/AvatarWithOptionalStatus.tsx @@ -1,5 +1,3 @@ -/* eslint-disable rulesdir/onyx-props-must-have-default */ -import PropTypes from 'prop-types'; import React, {useCallback} from 'react'; import {View} from 'react-native'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; @@ -12,20 +10,15 @@ import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import PressableAvatarWithIndicator from './PressableAvatarWithIndicator'; -const propTypes = { +type AvatarWithOptionalStatusProps = { /** Whether the create menu is open or not */ - isCreateMenuOpen: PropTypes.bool, + isCreateMenuOpen: boolean; /** Emoji status */ - emojiStatus: PropTypes.string, + emojiStatus: string; }; -const defaultProps = { - isCreateMenuOpen: false, - emojiStatus: '', -}; - -function AvatarWithOptionalStatus({emojiStatus, isCreateMenuOpen}) { +function AvatarWithOptionalStatus({emojiStatus = '', isCreateMenuOpen = false}: AvatarWithOptionalStatusProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -61,7 +54,5 @@ function AvatarWithOptionalStatus({emojiStatus, isCreateMenuOpen}) { ); } -AvatarWithOptionalStatus.propTypes = propTypes; -AvatarWithOptionalStatus.defaultProps = defaultProps; AvatarWithOptionalStatus.displayName = 'AvatarWithOptionalStatus'; export default AvatarWithOptionalStatus; From 6733581a39b700a147c1708f606cdc6fede9b1ee Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 16 Feb 2024 17:02:16 +0100 Subject: [PATCH 0184/1208] ref: move BaseSidebarScreen to TS --- src/pages/home/sidebar/SidebarLinks.tsx | 2 +- src/pages/home/sidebar/SidebarLinksData.tsx | 2 +- .../{BaseSidebarScreen.js => BaseSidebarScreen.tsx} | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) rename src/pages/home/sidebar/SidebarScreen/{BaseSidebarScreen.js => BaseSidebarScreen.tsx} (94%) diff --git a/src/pages/home/sidebar/SidebarLinks.tsx b/src/pages/home/sidebar/SidebarLinks.tsx index 52165c148727..f01ed07ea476 100644 --- a/src/pages/home/sidebar/SidebarLinks.tsx +++ b/src/pages/home/sidebar/SidebarLinks.tsx @@ -27,7 +27,7 @@ type SidebarLinksOnyxProps = { type SidebarLinksProps = SidebarLinksOnyxProps & { onLinkClick: () => void; - insets: EdgeInsets; + insets: EdgeInsets | undefined; optionListItems: string[] | null; isLoading: OnyxEntry; priorityMode?: OnyxEntry>; diff --git a/src/pages/home/sidebar/SidebarLinksData.tsx b/src/pages/home/sidebar/SidebarLinksData.tsx index ddd232e99275..ab84f69e824b 100644 --- a/src/pages/home/sidebar/SidebarLinksData.tsx +++ b/src/pages/home/sidebar/SidebarLinksData.tsx @@ -70,7 +70,7 @@ type SidebarLinksDataOnyxProps = { type SidebarLinksDataProps = SidebarLinksDataOnyxProps & { onLinkClick: () => void; - insets: EdgeInsets; + insets: EdgeInsets | undefined; }; function SidebarLinksData({ diff --git a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx similarity index 94% rename from src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js rename to src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx index 9188a859d175..314b3921cc0b 100644 --- a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js +++ b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx @@ -17,7 +17,7 @@ const startTimer = () => { Performance.markStart(CONST.TIMING.SWITCH_REPORT); }; -function BaseSidebarScreen(props) { +function BaseSidebarScreen() { const styles = useThemeStyles(); useEffect(() => { Performance.markStart(CONST.TIMING.SIDEBAR_LOADED); @@ -37,7 +37,6 @@ function BaseSidebarScreen(props) { )} From 64ccb57734d6492a088525bc1e83d8fa8373eb40 Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Fri, 16 Feb 2024 17:59:45 -0500 Subject: [PATCH 0185/1208] 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 b258238c5c7150ad34fef64b086d1f649be3266d Mon Sep 17 00:00:00 2001 From: Andrew Gable Date: Fri, 16 Feb 2024 17:34:00 -0700 Subject: [PATCH 0186/1208] Add back some HybridApp logic after TS migration to fix Authentication --- src/pages/LogOutPreviousUserPage.tsx | 36 +++++++++++++++++++++------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/src/pages/LogOutPreviousUserPage.tsx b/src/pages/LogOutPreviousUserPage.tsx index f68344604dfa..5191756ccf0b 100644 --- a/src/pages/LogOutPreviousUserPage.tsx +++ b/src/pages/LogOutPreviousUserPage.tsx @@ -1,6 +1,6 @@ import type {StackScreenProps} from '@react-navigation/stack'; -import React, {useEffect} from 'react'; -import {Linking} from 'react-native'; +import React, {useContext, useEffect} from 'react'; +import {Linking, NativeModules} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; @@ -9,11 +9,17 @@ import type {AuthScreensParamList} from '@navigation/types'; import * as SessionActions from '@userActions/Session'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; -import type {Session} from '@src/types/onyx'; +import type {Account, Session} from '@src/types/onyx'; +import InitialUrlContext from "@libs/InitialUrlContext"; +import CONST from "@src/CONST"; +import lodashGet from "lodash/get"; +import ROUTES, {type Route} from "@src/ROUTES"; +import Navigation from "@navigation/Navigation"; type LogOutPreviousUserPageOnyxProps = { /** The data about the current session which will be set once the user is authenticated and we return to this component as an AuthScreen */ session: OnyxEntry; + account: OnyxEntry; }; type LogOutPreviousUserPageProps = LogOutPreviousUserPageOnyxProps & StackScreenProps; @@ -22,10 +28,12 @@ type LogOutPreviousUserPageProps = LogOutPreviousUserPageOnyxProps & StackScreen // out if the transition is for another user. // // This component should not do any other navigation as that handled in App.setUpPoliciesAndNavigate -function LogOutPreviousUserPage({session, route}: LogOutPreviousUserPageProps) { +function LogOutPreviousUserPage({session, route, account}: LogOutPreviousUserPageProps) { + const initUrl = useContext(InitialUrlContext); useEffect(() => { - Linking.getInitialURL().then((transitionURL) => { + Linking.getInitialURL().then((url) => { const sessionEmail = session?.email; + const transitionURL = NativeModules.HybridAppModule ? CONST.DEEPLINK_BASE_URL + initUrl : url; const isLoggingInAsNewUser = SessionUtils.isLoggingInAsNewUser(transitionURL ?? undefined, sessionEmail); if (isLoggingInAsNewUser) { @@ -42,11 +50,20 @@ function LogOutPreviousUserPage({session, route}: LogOutPreviousUserPageProps) { const shortLivedAuthToken = route.params.shortLivedAuthToken ?? ''; SessionActions.signInWithShortLivedAuthToken(email, shortLivedAuthToken); } + const exitTo = route.params.exitTo as Route | null; + // We don't want to navigate to the exitTo route when creating a new workspace from a deep link, + // because we already handle creating the optimistic policy and navigating to it in App.setUpPoliciesAndNavigate, + // which is already called when AuthScreens mounts. + if (exitTo && exitTo !== ROUTES.WORKSPACE_NEW && !account?.isLoading && !isLoggingInAsNewUser) { + Navigation.isNavigationReady().then(() => { + // remove this screen and navigate to exit route + const exitUrl = NativeModules.HybridAppModule ? Navigation.parseHybridAppUrl(exitTo) : exitTo; + Navigation.goBack(); + Navigation.navigate(exitUrl); + }); + } }); - - // We only want to run this effect once on mount (when the page first loads after transitioning from OldDot) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [initUrl]); return ; } @@ -54,6 +71,7 @@ function LogOutPreviousUserPage({session, route}: LogOutPreviousUserPageProps) { LogOutPreviousUserPage.displayName = 'LogOutPreviousUserPage'; export default withOnyx({ + account: {key: ONYXKEYS.ACCOUNT}, session: { key: ONYXKEYS.SESSION, }, From 2961243d5da20f2336fe43adbfa15d9de9ffb469 Mon Sep 17 00:00:00 2001 From: Andrew Gable Date: Fri, 16 Feb 2024 17:42:50 -0700 Subject: [PATCH 0187/1208] Fix lint --- src/pages/LogOutPreviousUserPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/LogOutPreviousUserPage.tsx b/src/pages/LogOutPreviousUserPage.tsx index 5191756ccf0b..297a9166956c 100644 --- a/src/pages/LogOutPreviousUserPage.tsx +++ b/src/pages/LogOutPreviousUserPage.tsx @@ -12,8 +12,8 @@ import type SCREENS from '@src/SCREENS'; import type {Account, Session} from '@src/types/onyx'; import InitialUrlContext from "@libs/InitialUrlContext"; import CONST from "@src/CONST"; -import lodashGet from "lodash/get"; -import ROUTES, {type Route} from "@src/ROUTES"; +import ROUTES from "@src/ROUTES"; +import type {Route} from "@src/ROUTES"; import Navigation from "@navigation/Navigation"; type LogOutPreviousUserPageOnyxProps = { From 87817e505679354928b93145edf8a450b109e4fe Mon Sep 17 00:00:00 2001 From: Andrew Gable Date: Fri, 16 Feb 2024 17:54:14 -0700 Subject: [PATCH 0188/1208] More lint --- src/pages/LogOutPreviousUserPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/LogOutPreviousUserPage.tsx b/src/pages/LogOutPreviousUserPage.tsx index 297a9166956c..c6144c70db3e 100644 --- a/src/pages/LogOutPreviousUserPage.tsx +++ b/src/pages/LogOutPreviousUserPage.tsx @@ -63,7 +63,7 @@ function LogOutPreviousUserPage({session, route, account}: LogOutPreviousUserPag }); } }); - }, [initUrl]); + }, [initUrl, account, route, session]); return ; } From d72cda0382c4d6f35d7f040bf14d31aa7c062d55 Mon Sep 17 00:00:00 2001 From: Andrew Gable Date: Fri, 16 Feb 2024 18:00:05 -0700 Subject: [PATCH 0189/1208] Prettier fixes --- src/pages/LogOutPreviousUserPage.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pages/LogOutPreviousUserPage.tsx b/src/pages/LogOutPreviousUserPage.tsx index c6144c70db3e..a10988945c4e 100644 --- a/src/pages/LogOutPreviousUserPage.tsx +++ b/src/pages/LogOutPreviousUserPage.tsx @@ -4,17 +4,17 @@ import {Linking, NativeModules} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import InitialUrlContext from '@libs/InitialUrlContext'; import * as SessionUtils from '@libs/SessionUtils'; +import Navigation from '@navigation/Navigation'; import type {AuthScreensParamList} from '@navigation/types'; import * as SessionActions from '@userActions/Session'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type {Route} from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {Account, Session} from '@src/types/onyx'; -import InitialUrlContext from "@libs/InitialUrlContext"; -import CONST from "@src/CONST"; -import ROUTES from "@src/ROUTES"; -import type {Route} from "@src/ROUTES"; -import Navigation from "@navigation/Navigation"; type LogOutPreviousUserPageOnyxProps = { /** The data about the current session which will be set once the user is authenticated and we return to this component as an AuthScreen */ From bc076b5e6a38acf273ea46721ecad6c18dc39aa9 Mon Sep 17 00:00:00 2001 From: Someshwar Tripathi Date: Sun, 18 Feb 2024 23:02:11 +0530 Subject: [PATCH 0190/1208] Increase editing space for recovery code --- src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js index 4afba77b77b5..7cbf05dba783 100755 --- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js +++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js @@ -324,7 +324,7 @@ function BaseValidateCodeForm(props) { accessibilityLabel={props.translate('recoveryCodeForm.recoveryCode')} value={recoveryCode} onChangeText={(text) => onTextInput(text, 'recoveryCode')} - maxLength={CONST.RECOVERY_CODE_LENGTH} + maxLength={CONST.FORM_CHARACTER_LIMIT} label={props.translate('recoveryCodeForm.recoveryCode')} errorText={formError.recoveryCode || ''} hasError={hasError} From 04fb21000447ec284980392bd55ff7fade561a9a Mon Sep 17 00:00:00 2001 From: Someshwar Tripathi Date: Sun, 18 Feb 2024 23:16:23 +0530 Subject: [PATCH 0191/1208] Add logic to trim text on input --- src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js index 7cbf05dba783..2bc90efaceb5 100755 --- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js +++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js @@ -179,7 +179,7 @@ function BaseValidateCodeForm(props) { setInput = setRecoveryCode; } - setInput(text); + setInput(text.trim()); setFormError((prevError) => ({...prevError, [key]: ''})); if (props.account.errors) { From e974f9f0e2db1155407442c8494200f5b33711a7 Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 20 Feb 2024 10:59:26 +0700 Subject: [PATCH 0192/1208] 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 2fad64eddb709a5bdde140f53ce65beb63b64578 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 20 Feb 2024 10:02:39 +0100 Subject: [PATCH 0193/1208] ref: move FloatingActionButtonAndPopover to TS --- src/libs/actions/Policy.ts | 6 +- src/libs/actions/Task.ts | 2 +- ....js => FloatingActionButtonAndPopover.tsx} | 139 +++++++----------- 3 files changed, 58 insertions(+), 89 deletions(-) rename src/pages/home/sidebar/SidebarScreen/{FloatingActionButtonAndPopover.js => FloatingActionButtonAndPopover.tsx} (70%) diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index 9e6745bbc291..911837cd3bf5 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -81,7 +81,7 @@ type OptimisticCustomUnits = { outputCurrency: string; }; -type PoliciesRecord = Record>; +type PoliciesRecord = Record; type NewCustomUnit = { customUnitID: string; @@ -206,8 +206,8 @@ function updateLastAccessedWorkspace(policyID: OnyxEntry) { /** * Check if the user has any active free policies (aka workspaces) */ -function hasActiveFreePolicy(policies: Array> | PoliciesRecord): boolean { - const adminFreePolicies = Object.values(policies).filter((policy) => policy && policy.type === CONST.POLICY.TYPE.FREE && policy.role === CONST.POLICY.ROLE.ADMIN); +function hasActiveFreePolicy(policies: OnyxEntry): boolean { + const adminFreePolicies = Object.values(policies ?? {}).filter((policy) => policy && policy.type === CONST.POLICY.TYPE.FREE && policy.role === CONST.POLICY.ROLE.ADMIN); if (adminFreePolicies.length === 0) { return false; diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index 28cecf460a5f..b05e7cd6b2d9 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -645,7 +645,7 @@ function setParentReportID(parentReportID: string) { /** * Clears out the task info from the store and navigates to the NewTaskDetails page */ -function clearOutTaskInfoAndNavigate(reportID: string) { +function clearOutTaskInfoAndNavigate(reportID?: string) { clearOutTaskInfo(); if (reportID && reportID !== '0') { setParentReportID(reportID); diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx similarity index 70% rename from src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js rename to src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index 0df490fa4466..83cc719f2e90 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -1,17 +1,16 @@ -import PropTypes from 'prop-types'; +import {useIsFocused, useNavigation} from '@react-navigation/native'; +import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import FloatingActionButton from '@components/FloatingActionButton'; import * as Expensicons from '@components/Icon/Expensicons'; import PopoverMenu from '@components/PopoverMenu'; -import withNavigation from '@components/withNavigation'; -import withNavigationFocus from '@components/withNavigationFocus'; -import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; @@ -22,74 +21,53 @@ import * as Task from '@userActions/Task'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type * as OnyxTypes from '@src/types/onyx'; -/** - * @param {Object} [policy] - * @returns {Object|undefined} - */ -const policySelector = (policy) => - policy && { - type: policy.type, - role: policy.role, - isPolicyExpenseChatEnabled: policy.isPolicyExpenseChatEnabled, - pendingAction: policy.pendingAction, - }; - -const propTypes = { - ...windowDimensionsPropTypes, +type FloatingActionButtonAndPopoverOnyxProps = { + /** The list of policies the user has access to. */ + allPolicies: OnyxEntry>; + isLoading: OnyxEntry; +}; +type FloatingActionButtonAndPopoverProps = FloatingActionButtonAndPopoverOnyxProps & { /* Callback function when the menu is shown */ - onShowCreateMenu: PropTypes.func, + onShowCreateMenu: () => void; /* Callback function before the menu is hidden */ - onHideCreateMenu: PropTypes.func, - - /** The list of policies the user has access to. */ - allPolicies: PropTypes.shape({ - /** The policy name */ - name: PropTypes.string, - }), - - /** Indicated whether the report data is loading */ - isLoading: PropTypes.bool, - - /** Forwarded ref to FloatingActionButtonAndPopover */ - innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + onHideCreateMenu: () => void; }; -const defaultProps = { - onHideCreateMenu: () => {}, - onShowCreateMenu: () => {}, - allPolicies: {}, - isLoading: false, - innerRef: null, + +type FloatingActionButtonAndPopoverRef = { + hideCreateMenu: () => void; }; /** * Responsible for rendering the {@link PopoverMenu}, and the accompanying * FAB that can open or close the menu. - * @param {Object} props - * @returns {JSX.Element} */ -function FloatingActionButtonAndPopover(props) { +function FloatingActionButtonAndPopover( + {onHideCreateMenu = () => {}, onShowCreateMenu = () => {}, isLoading, allPolicies}: FloatingActionButtonAndPopoverProps, + ref: ForwardedRef, +) { + console.log('allPolices', allPolicies); const styles = useThemeStyles(); const {translate} = useLocalize(); const [isCreateMenuActive, setIsCreateMenuActive] = useState(false); - const fabRef = useRef(null); + const fabRef = useRef(null); + const {isSmallScreenWidth, windowHeight} = useWindowDimensions(); + const isFocused = useIsFocused(); - const prevIsFocused = usePrevious(props.isFocused); + const prevIsFocused = usePrevious(isFocused); /** * Check if LHN status changed from active to inactive. * Used to close already opened FAB menu when open any other pages (i.e. Press Command + K on web). - * - * @param {Object} prevProps - * @return {Boolean} */ const didScreenBecomeInactive = useCallback( () => // When any other page is opened over LHN - !props.isFocused && prevIsFocused, - [props.isFocused, prevIsFocused], + !isFocused && prevIsFocused, + [isFocused, prevIsFocused], ); /** @@ -97,14 +75,14 @@ function FloatingActionButtonAndPopover(props) { */ const showCreateMenu = useCallback( () => { - if (!props.isFocused && props.isSmallScreenWidth) { + if (!isFocused && isSmallScreenWidth) { return; } setIsCreateMenuActive(true); - props.onShowCreateMenu(); + onShowCreateMenu(); }, // eslint-disable-next-line react-hooks/exhaustive-deps - [props.isFocused, props.isSmallScreenWidth], + [isFocused, isSmallScreenWidth], ); /** @@ -118,7 +96,7 @@ function FloatingActionButtonAndPopover(props) { return; } setIsCreateMenuActive(false); - props.onHideCreateMenu(); + onHideCreateMenu(); }, // eslint-disable-next-line react-hooks/exhaustive-deps [isCreateMenuActive], @@ -133,7 +111,7 @@ function FloatingActionButtonAndPopover(props) { hideCreateMenu(); }, [didScreenBecomeInactive, hideCreateMenu]); - useImperativeHandle(props.innerRef, () => ({ + useImperativeHandle(ref, () => ({ hideCreateMenu() { hideCreateMenu(); }, @@ -151,10 +129,10 @@ function FloatingActionButtonAndPopover(props) { interceptAnonymousUser(() => Navigation.navigate(ROUTES.TEACHERS_UNITE)), }, - ...(!props.isLoading && !Policy.hasActiveFreePolicy(props.allPolicies) + ...(!isLoading && !Policy.hasActiveFreePolicy(allPolicies) ? [ { displayInDefaultIconColor: true, - contentFit: 'contain', + contentFit: 'contain' as const, icon: Expensicons.NewWorkspace, iconWidth: 46, iconHeight: 40, @@ -219,31 +197,22 @@ function FloatingActionButtonAndPopover(props) { ); } -FloatingActionButtonAndPopover.propTypes = propTypes; -FloatingActionButtonAndPopover.defaultProps = defaultProps; FloatingActionButtonAndPopover.displayName = 'FloatingActionButtonAndPopover'; -const FloatingActionButtonAndPopoverWithRef = forwardRef((props, ref) => ( - -)); - -FloatingActionButtonAndPopoverWithRef.displayName = 'FloatingActionButtonAndPopoverWithRef'; - -export default compose( - withNavigation, - withNavigationFocus, - withWindowDimensions, - withOnyx({ - allPolicies: { - key: ONYXKEYS.COLLECTION.POLICY, - selector: policySelector, - }, - isLoading: { - key: ONYXKEYS.IS_LOADING_APP, - }, - }), -)(FloatingActionButtonAndPopoverWithRef); +const policySelector = (policy: OnyxEntry) => + policy && { + type: policy.type, + role: policy.role, + isPolicyExpenseChatEnabled: policy.isPolicyExpenseChatEnabled, + pendingAction: policy.pendingAction, + }; + +export default withOnyx({ + allPolicies: { + key: ONYXKEYS.COLLECTION.POLICY, + selector: policySelector, + }, + isLoading: { + key: ONYXKEYS.IS_LOADING_APP, + }, +})(forwardRef(FloatingActionButtonAndPopover)); From 6465479de01d8d88e890ce6d24828380ab5be40d Mon Sep 17 00:00:00 2001 From: Someshwar Tripathi Date: Tue, 20 Feb 2024 16:12:33 +0530 Subject: [PATCH 0194/1208] Update recovery code trim logic --- src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js index 2bc90efaceb5..db14010f5f37 100755 --- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js +++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js @@ -179,7 +179,7 @@ function BaseValidateCodeForm(props) { setInput = setRecoveryCode; } - setInput(text.trim()); + setInput(text); setFormError((prevError) => ({...prevError, [key]: ''})); if (props.account.errors) { @@ -283,7 +283,7 @@ function BaseValidateCodeForm(props) { setFormError({recoveryCode: 'recoveryCodeForm.error.pleaseFillRecoveryCode'}); return; } - if (!ValidationUtils.isValidRecoveryCode(recoveryCode)) { + if (!ValidationUtils.isValidRecoveryCode(recoveryCode.trim())) { setFormError({recoveryCode: 'recoveryCodeForm.error.incorrectRecoveryCode'}); return; } @@ -303,7 +303,7 @@ function BaseValidateCodeForm(props) { } setFormError({}); - const recoveryCodeOr2faCode = props.isUsingRecoveryCode ? recoveryCode : twoFactorAuthCode; + const recoveryCodeOr2faCode = props.isUsingRecoveryCode ? recoveryCode.trim() : twoFactorAuthCode; const accountID = lodashGet(props.credentials, 'accountID'); if (accountID) { From c9158cbd92eaed9d32315fb38fda611e4c182079 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 20 Feb 2024 14:48:56 +0100 Subject: [PATCH 0195/1208] fix: wip --- src/libs/actions/Policy.ts | 2 +- .../sidebar/BottomTabBarFloatingActionButton/index.tsx | 1 - .../home/sidebar/SidebarScreen/BaseSidebarScreen.tsx | 2 -- .../SidebarScreen/FloatingActionButtonAndPopover.tsx | 9 ++++----- .../home/sidebar/SidebarScreen/{index.js => index.tsx} | 8 +++++--- src/pages/home/sidebar/SidebarScreen/sidebarPropTypes.js | 7 ------- 6 files changed, 10 insertions(+), 19 deletions(-) rename src/pages/home/sidebar/SidebarScreen/{index.js => index.tsx} (75%) delete mode 100644 src/pages/home/sidebar/SidebarScreen/sidebarPropTypes.js diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index e780001a7ecc..261e15af8d48 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -81,7 +81,7 @@ type OptimisticCustomUnits = { outputCurrency: string; }; -type PoliciesRecord = Record; +type PoliciesRecord = Record; type NewCustomUnit = { customUnitID: string; diff --git a/src/pages/home/sidebar/BottomTabBarFloatingActionButton/index.tsx b/src/pages/home/sidebar/BottomTabBarFloatingActionButton/index.tsx index 788dd4ae5bc8..33b89be8fd17 100644 --- a/src/pages/home/sidebar/BottomTabBarFloatingActionButton/index.tsx +++ b/src/pages/home/sidebar/BottomTabBarFloatingActionButton/index.tsx @@ -32,7 +32,6 @@ function BottomTabBarFloatingActionButton() { return ( diff --git a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx index 314b3921cc0b..b3901e1ae06f 100644 --- a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx +++ b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx @@ -7,7 +7,6 @@ import Performance from '@libs/Performance'; import SidebarLinksData from '@pages/home/sidebar/SidebarLinksData'; import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; -import sidebarPropTypes from './sidebarPropTypes'; /** * Function called when a pinned chat is selected. @@ -44,7 +43,6 @@ function BaseSidebarScreen() { ); } -BaseSidebarScreen.propTypes = sidebarPropTypes; BaseSidebarScreen.displayName = 'BaseSidebarScreen'; export default BaseSidebarScreen; diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index 83cc719f2e90..01d5ddaff475 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -1,4 +1,4 @@ -import {useIsFocused, useNavigation} from '@react-navigation/native'; +import {useIsFocused} from '@react-navigation/native'; import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; import {View} from 'react-native'; @@ -25,13 +25,13 @@ import type * as OnyxTypes from '@src/types/onyx'; type FloatingActionButtonAndPopoverOnyxProps = { /** The list of policies the user has access to. */ - allPolicies: OnyxEntry>; + allPolicies: OnyxEntry>>; isLoading: OnyxEntry; }; type FloatingActionButtonAndPopoverProps = FloatingActionButtonAndPopoverOnyxProps & { /* Callback function when the menu is shown */ - onShowCreateMenu: () => void; + onShowCreateMenu?: () => void; /* Callback function before the menu is hidden */ onHideCreateMenu: () => void; @@ -49,7 +49,6 @@ function FloatingActionButtonAndPopover( {onHideCreateMenu = () => {}, onShowCreateMenu = () => {}, isLoading, allPolicies}: FloatingActionButtonAndPopoverProps, ref: ForwardedRef, ) { - console.log('allPolices', allPolicies); const styles = useThemeStyles(); const {translate} = useLocalize(); const [isCreateMenuActive, setIsCreateMenuActive] = useState(false); @@ -200,7 +199,7 @@ function FloatingActionButtonAndPopover( FloatingActionButtonAndPopover.displayName = 'FloatingActionButtonAndPopover'; const policySelector = (policy: OnyxEntry) => - policy && { + !!policy && { type: policy.type, role: policy.role, isPolicyExpenseChatEnabled: policy.isPolicyExpenseChatEnabled, diff --git a/src/pages/home/sidebar/SidebarScreen/index.js b/src/pages/home/sidebar/SidebarScreen/index.tsx similarity index 75% rename from src/pages/home/sidebar/SidebarScreen/index.js rename to src/pages/home/sidebar/SidebarScreen/index.tsx index 7086e8a8561a..f017750c912c 100755 --- a/src/pages/home/sidebar/SidebarScreen/index.js +++ b/src/pages/home/sidebar/SidebarScreen/index.tsx @@ -1,10 +1,13 @@ import React from 'react'; +import type {LayoutChangeEvent} from 'react-native'; import useWindowDimensions from '@hooks/useWindowDimensions'; import FreezeWrapper from '@libs/Navigation/FreezeWrapper'; import BaseSidebarScreen from './BaseSidebarScreen'; -import sidebarPropTypes from './sidebarPropTypes'; -function SidebarScreen(props) { +type SidebarScreenProps = { + onLayout: (event: LayoutChangeEvent) => void; +}; +function SidebarScreen(props: SidebarScreenProps) { const {isSmallScreenWidth} = useWindowDimensions(); return ( @@ -17,7 +20,6 @@ function SidebarScreen(props) { ); } -SidebarScreen.propTypes = sidebarPropTypes; SidebarScreen.displayName = 'SidebarScreen'; export default SidebarScreen; diff --git a/src/pages/home/sidebar/SidebarScreen/sidebarPropTypes.js b/src/pages/home/sidebar/SidebarScreen/sidebarPropTypes.js deleted file mode 100644 index 61a9194bb1e5..000000000000 --- a/src/pages/home/sidebar/SidebarScreen/sidebarPropTypes.js +++ /dev/null @@ -1,7 +0,0 @@ -import PropTypes from 'prop-types'; - -const sidebarPropTypes = { - /** Callback when onLayout of sidebar is called */ - onLayout: PropTypes.func, -}; -export default sidebarPropTypes; From dc58fff4022813cc70e994bc2f1f97f4e427cebf Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 20 Feb 2024 15:42:06 +0100 Subject: [PATCH 0196/1208] fix: wip --- .../FloatingActionButtonAndPopover.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index 01d5ddaff475..9291401d4569 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -26,6 +26,8 @@ import type * as OnyxTypes from '@src/types/onyx'; type FloatingActionButtonAndPopoverOnyxProps = { /** The list of policies the user has access to. */ allPolicies: OnyxEntry>>; + + /** Wheater app is in loading state */ isLoading: OnyxEntry; }; @@ -199,12 +201,14 @@ function FloatingActionButtonAndPopover( FloatingActionButtonAndPopover.displayName = 'FloatingActionButtonAndPopover'; const policySelector = (policy: OnyxEntry) => - !!policy && { - type: policy.type, - role: policy.role, - isPolicyExpenseChatEnabled: policy.isPolicyExpenseChatEnabled, - pendingAction: policy.pendingAction, - }; + policy + ? { + type: policy.type, + role: policy.role, + isPolicyExpenseChatEnabled: policy.isPolicyExpenseChatEnabled, + pendingAction: policy.pendingAction, + } + : null; export default withOnyx({ allPolicies: { From 150ac223e8fa53ce4a5e6d6aca63d7d370edf48e Mon Sep 17 00:00:00 2001 From: Someshwar Tripathi Date: Wed, 21 Feb 2024 01:42:52 +0530 Subject: [PATCH 0197/1208] Reorder Tooltip component in IconButton --- src/components/VideoPlayer/IconButton.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/VideoPlayer/IconButton.js b/src/components/VideoPlayer/IconButton.js index 71c1a2150692..7a9a93f5a626 100644 --- a/src/components/VideoPlayer/IconButton.js +++ b/src/components/VideoPlayer/IconButton.js @@ -40,12 +40,12 @@ const defaultProps = { function IconButton({src, fill, onPress, style, hoverStyle, tooltipText, small, shouldForceRenderingTooltipBelow}) { const styles = useThemeStyles(); return ( - - - {(isHovered) => ( + + {(isHovered) => ( + - )} - - + + )} + ); } From f278edc12c5d04148ae1539290ed7303ef68b8ad Mon Sep 17 00:00:00 2001 From: Someshwar Tripathi Date: Wed, 21 Feb 2024 02:16:10 +0530 Subject: [PATCH 0198/1208] Simplify component by using hoverStyle prop --- src/components/VideoPlayer/IconButton.js | 38 +++++++++++------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/src/components/VideoPlayer/IconButton.js b/src/components/VideoPlayer/IconButton.js index 7a9a93f5a626..eaf7ec532fb2 100644 --- a/src/components/VideoPlayer/IconButton.js +++ b/src/components/VideoPlayer/IconButton.js @@ -1,6 +1,5 @@ import PropTypes from 'prop-types'; import React from 'react'; -import Hoverable from '@components/Hoverable'; import Icon from '@components/Icon'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import Tooltip from '@components/Tooltip'; @@ -40,26 +39,23 @@ const defaultProps = { function IconButton({src, fill, onPress, style, hoverStyle, tooltipText, small, shouldForceRenderingTooltipBelow}) { const styles = useThemeStyles(); return ( - - {(isHovered) => ( - - - - - - )} - + + + + + ); } From 4add919636d3e398a22bafa6f55901603183f2f9 Mon Sep 17 00:00:00 2001 From: RohanSasne Date: Wed, 21 Feb 2024 03:21:30 +0530 Subject: [PATCH 0199/1208] Add whitespace check --- .../substeps/PhoneNumberBusiness.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/pages/ReimbursementAccount/BusinessInfo/substeps/PhoneNumberBusiness.tsx b/src/pages/ReimbursementAccount/BusinessInfo/substeps/PhoneNumberBusiness.tsx index e0d369e099d1..3a851b001ff7 100644 --- a/src/pages/ReimbursementAccount/BusinessInfo/substeps/PhoneNumberBusiness.tsx +++ b/src/pages/ReimbursementAccount/BusinessInfo/substeps/PhoneNumberBusiness.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useState} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; @@ -26,10 +26,11 @@ type PhoneNumberBusinessProps = PhoneNumberBusinessOnyxProps & SubStepProps; const COMPANY_PHONE_NUMBER_KEY = INPUT_IDS.BUSINESS_INFO_STEP.COMPANY_PHONE; const STEP_FIELDS = [COMPANY_PHONE_NUMBER_KEY]; -const validate = (values: FormOnyxValues): FormInputErrors => { +const validate = (values: FormOnyxValues, inputValue: string): FormInputErrors => { const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); - if (values.companyPhone && !ValidationUtils.isValidUSPhone(values.companyPhone, true)) { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + if ((values.companyPhone && !ValidationUtils.isValidUSPhone(values.companyPhone, true)) || inputValue.trim() !== inputValue) { errors.companyPhone = 'bankAccount.error.phoneNumber'; } @@ -41,6 +42,12 @@ function PhoneNumberBusiness({reimbursementAccount, onNext, isEditing}: PhoneNum const styles = useThemeStyles(); const defaultCompanyPhoneNumber = reimbursementAccount?.achData?.companyPhone ?? ''; + const [inputValue, setInputValue] = useState(defaultCompanyPhoneNumber); + + const handleInputChange = (newValue: string) => { + setInputValue(newValue); + }; + const handleSubmit = useReimbursementAccountStepFormSubmit({ fieldIds: STEP_FIELDS, isEditing, @@ -51,7 +58,7 @@ function PhoneNumberBusiness({reimbursementAccount, onNext, isEditing}: PhoneNum validate(values, inputValue)} onSubmit={handleSubmit} style={[styles.mh5, styles.flexGrow1]} submitButtonStyles={[styles.pb5, styles.mb0]} @@ -68,6 +75,7 @@ function PhoneNumberBusiness({reimbursementAccount, onNext, isEditing}: PhoneNum defaultValue={defaultCompanyPhoneNumber} shouldSaveDraft={!isEditing} containerStyles={[styles.mt6]} + onChangeText={handleInputChange} /> ); From 62c8956937bbb1146f78ab5b26f7d9b0f13be348 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 21 Feb 2024 10:08:52 +0700 Subject: [PATCH 0200/1208] 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 cb7bfac8c0e73a6ef1814436e7fe35237dc3f5de Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 21 Feb 2024 12:12:14 +0100 Subject: [PATCH 0201/1208] TS updates after merging main --- src/components/Onfido/BaseOnfidoWeb.tsx | 54 +++++++++++-------- src/components/Onfido/index.native.tsx | 13 ++--- src/components/Onfido/types.ts | 10 ++-- .../VerifyIdentity/VerifyIdentity.tsx | 6 +-- 4 files changed, 47 insertions(+), 36 deletions(-) diff --git a/src/components/Onfido/BaseOnfidoWeb.tsx b/src/components/Onfido/BaseOnfidoWeb.tsx index ee206b15fc24..2722b9e9be79 100644 --- a/src/components/Onfido/BaseOnfidoWeb.tsx +++ b/src/components/Onfido/BaseOnfidoWeb.tsx @@ -1,17 +1,28 @@ -import lodashGet from 'lodash/get'; import * as OnfidoSDK from 'onfido-sdk-ui'; import React, {forwardRef, useEffect} from 'react'; -import _ from 'underscore'; +import type {ForwardedRef} from 'react'; +import type {LocaleContextProps} from '@components/LocaleContextProvider'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import Log from '@libs/Log'; +import type {ThemeColors} from '@styles/theme/types'; import FontUtils from '@styles/utils/FontUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import './index.css'; -import onfidoPropTypes from './onfidoPropTypes'; +import type {OnfidoElement, OnfidoProps} from './types'; -function initializeOnfido({sdkToken, onSuccess, onError, onUserExit, preferredLocale, translate, theme}) { +type InitializeOnfidoProps = OnfidoProps & + Pick & { + theme: ThemeColors; + }; + +type OnfidoEvent = Event & { + detail?: Record; +}; + +function initializeOnfido({sdkToken, onSuccess, onError, onUserExit, preferredLocale, translate, theme}: InitializeOnfidoProps) { OnfidoSDK.init({ token: sdkToken, containerId: CONST.ONFIDO.CONTAINER_ID, @@ -21,7 +32,7 @@ function initializeOnfido({sdkToken, onSuccess, onError, onUserExit, preferredLo fontFamilySubtitle: `${FontUtils.fontFamily.platform.EXP_NEUE}, -apple-system, serif`, fontFamilyBody: `${FontUtils.fontFamily.platform.EXP_NEUE}, -apple-system, serif`, fontSizeTitle: `${variables.fontSizeLarge}px`, - fontWeightTitle: FontUtils.fontWeight.bold, + fontWeightTitle: Number(FontUtils.fontWeight.bold), fontWeightSubtitle: 400, fontSizeSubtitle: `${variables.fontSizeNormal}px`, colorContentTitle: theme.text, @@ -46,7 +57,6 @@ function initializeOnfido({sdkToken, onSuccess, onError, onUserExit, preferredLo colorBorderLinkUnderline: theme.link, colorBackgroundLinkHover: theme.link, colorBackgroundLinkActive: theme.link, - authAccentColor: theme.link, colorBackgroundInfoPill: theme.link, colorBackgroundSelector: theme.appBG, colorBackgroundDocTypeButton: theme.success, @@ -58,11 +68,10 @@ function initializeOnfido({sdkToken, onSuccess, onError, onUserExit, preferredLo { type: CONST.ONFIDO.TYPE.DOCUMENT, options: { - useLiveDocumentCapture: true, forceCrossDevice: true, hideCountrySelection: true, - country: 'USA', documentTypes: { + // eslint-disable-next-line @typescript-eslint/naming-convention driving_licence: { country: 'USA', }, @@ -77,17 +86,15 @@ function initializeOnfido({sdkToken, onSuccess, onError, onUserExit, preferredLo }, }, ], - smsNumberCountryCode: CONST.ONFIDO.SMS_NUMBER_COUNTRY_CODE.US, - showCountrySelection: false, onComplete: (data) => { - if (_.isEmpty(data)) { + if (isEmptyObject(data)) { Log.warn('Onfido completed with no data'); } onSuccess(data); }, onError: (error) => { - const errorMessage = lodashGet(error, 'message', CONST.ERROR.UNKNOWN_ERROR); - const errorType = lodashGet(error, 'type'); + const errorMessage = error.message ?? CONST.ERROR.UNKNOWN_ERROR; + const errorType = error.type; Log.hmmm('Onfido error', {errorType, errorMessage}); onError(errorMessage); }, @@ -100,33 +107,34 @@ function initializeOnfido({sdkToken, onSuccess, onError, onUserExit, preferredLo }, language: { // We need to use ES_ES as locale key because the key `ES` is not a valid config key for Onfido - locale: preferredLocale === CONST.LOCALES.ES ? CONST.LOCALES.ES_ES_ONFIDO : preferredLocale, + locale: preferredLocale === CONST.LOCALES.ES ? CONST.LOCALES.ES_ES_ONFIDO : (preferredLocale as OnfidoSDK.SupportedLanguages), // Provide a custom phrase for the back button so that the first letter is capitalized, // and translate the phrase while we're at it. See the issue and documentation for more context. // https://github.com/Expensify/App/issues/17244 // https://documentation.onfido.com/sdk/web/#custom-languages phrases: { + // eslint-disable-next-line @typescript-eslint/naming-convention 'generic.back': translate('common.back'), }, }, }); } -function logOnFidoEvent(event) { +function logOnFidoEvent(event: OnfidoEvent) { Log.hmmm('Receiving Onfido analytic event', event.detail); } -const Onfido = forwardRef((props, ref) => { +function Onfido({sdkToken, onSuccess, onError, onUserExit}: OnfidoProps, ref: ForwardedRef) { const {preferredLocale, translate} = useLocalize(); const theme = useTheme(); useEffect(() => { initializeOnfido({ - sdkToken: props.sdkToken, - onSuccess: props.onSuccess, - onError: props.onError, - onUserExit: props.onUserExit, + sdkToken, + onSuccess, + onError, + onUserExit, preferredLocale, translate, theme, @@ -144,8 +152,8 @@ const Onfido = forwardRef((props, ref) => { ref={ref} /> ); -}); +} Onfido.displayName = 'Onfido'; -Onfido.propTypes = onfidoPropTypes; -export default Onfido; + +export default forwardRef(Onfido); diff --git a/src/components/Onfido/index.native.tsx b/src/components/Onfido/index.native.tsx index eae01d14869e..a7e7a277fff9 100644 --- a/src/components/Onfido/index.native.tsx +++ b/src/components/Onfido/index.native.tsx @@ -1,13 +1,13 @@ -import {OnfidoCaptureType, OnfidoCountryCode, OnfidoDocumentType, Onfido as OnfidoSDK} from '@onfido/react-native-sdk'; +import {OnfidoCaptureType, OnfidoCountryCode, OnfidoDocumentType, Onfido as OnfidoSDK, OnfidoTheme} from '@onfido/react-native-sdk'; import React, {useEffect} from 'react'; import {Alert, Linking} from 'react-native'; import {checkMultiple, PERMISSIONS, RESULTS} from 'react-native-permissions'; -import _ from 'underscore'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import useLocalize from '@hooks/useLocalize'; import getPlatform from '@libs/getPlatform'; import Log from '@libs/Log'; import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; import type {OnfidoProps} from './types'; function Onfido({sdkToken, onUserExit, onSuccess, onError}: OnfidoProps) { @@ -16,6 +16,7 @@ function Onfido({sdkToken, onUserExit, onSuccess, onError}: OnfidoProps) { useEffect(() => { OnfidoSDK.start({ sdkToken, + theme: OnfidoTheme.AUTOMATIC, flowSteps: { welcome: true, captureFace: { @@ -41,13 +42,13 @@ function Onfido({sdkToken, onUserExit, onSuccess, onError}: OnfidoProps) { return; } - if (!_.isEmpty(errorMessage) && getPlatform() === CONST.PLATFORM.IOS) { + if (!!errorMessage && getPlatform() === CONST.PLATFORM.IOS) { checkMultiple([PERMISSIONS.IOS.MICROPHONE, PERMISSIONS.IOS.CAMERA]) .then((statuses) => { const isMicAllowed = statuses[PERMISSIONS.IOS.MICROPHONE] === RESULTS.GRANTED; const isCameraAllowed = statuses[PERMISSIONS.IOS.CAMERA] === RESULTS.GRANTED; - let alertTitle = ''; - let alertMessage = ''; + let alertTitle: TranslationPaths | '' = ''; + let alertMessage: TranslationPaths | '' = ''; if (!isCameraAllowed) { alertTitle = 'onfidoStep.cameraPermissionsNotGranted'; alertMessage = 'onfidoStep.cameraRequestMessage'; @@ -56,7 +57,7 @@ function Onfido({sdkToken, onUserExit, onSuccess, onError}: OnfidoProps) { alertMessage = 'onfidoStep.microphoneRequestMessage'; } - if (!_.isEmpty(alertTitle) && !_.isEmpty(alertMessage)) { + if (!!alertTitle && !!alertMessage) { Alert.alert( translate(alertTitle), translate(alertMessage), diff --git a/src/components/Onfido/types.ts b/src/components/Onfido/types.ts index a4fe3d93f05e..e341dfd64960 100644 --- a/src/components/Onfido/types.ts +++ b/src/components/Onfido/types.ts @@ -1,5 +1,7 @@ -import {OnfidoError, OnfidoResult} from '@onfido/react-native-sdk'; -import * as OnfidoSDK from 'onfido-sdk-ui'; +import type {OnfidoResult} from '@onfido/react-native-sdk'; +import type * as OnfidoSDK from 'onfido-sdk-ui'; + +type OnfidoData = OnfidoSDK.SdkResponse | OnfidoResult; type OnfidoElement = HTMLDivElement & {onfidoOut?: OnfidoSDK.SdkHandle}; @@ -11,10 +13,10 @@ type OnfidoProps = { onUserExit: (userExitCode?: OnfidoSDK.UserExitCode) => void; /** Called when the user is totally done with Onfido */ - onSuccess: (data: OnfidoSDK.SdkResponse | OnfidoResult | OnfidoError) => void; + onSuccess: (data: OnfidoData) => void; /** Called when Onfido throws an error */ onError: (error?: string) => void; }; -export type {OnfidoProps, OnfidoElement}; +export type {OnfidoProps, OnfidoElement, OnfidoData}; diff --git a/src/pages/ReimbursementAccount/VerifyIdentity/VerifyIdentity.tsx b/src/pages/ReimbursementAccount/VerifyIdentity/VerifyIdentity.tsx index d17166365a39..f7c4df6fd915 100644 --- a/src/pages/ReimbursementAccount/VerifyIdentity/VerifyIdentity.tsx +++ b/src/pages/ReimbursementAccount/VerifyIdentity/VerifyIdentity.tsx @@ -5,8 +5,8 @@ import {withOnyx} from 'react-native-onyx'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import InteractiveStepSubHeader from '@components/InteractiveStepSubHeader'; -// @ts-expect-error TODO: Remove this once Onfido (https://github.com/Expensify/App/issues/25136) is migrated to TypeScript. import Onfido from '@components/Onfido'; +import type {OnfidoData} from '@components/Onfido/types'; import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -40,7 +40,7 @@ function VerifyIdentity({reimbursementAccount, onBackButtonPress, onfidoApplican const policyID = reimbursementAccount?.achData?.policyID ?? ''; const handleOnfidoSuccess = useCallback( - (onfidoData: Record) => { + (onfidoData: OnfidoData) => { BankAccounts.verifyIdentityForBankAccount(Number(reimbursementAccount?.achData?.bankAccountID ?? '0'), {...onfidoData, applicantID: onfidoApplicantID}, policyID); BankAccounts.updateReimbursementAccountDraft({isOnfidoSetupComplete: true}); }, @@ -74,7 +74,7 @@ function VerifyIdentity({reimbursementAccount, onBackButtonPress, onfidoApplican Date: Wed, 21 Feb 2024 12:29:27 +0100 Subject: [PATCH 0202/1208] Add OnfidoDataWithApplicantID type --- src/components/Onfido/types.ts | 7 ++++++- src/libs/actions/BankAccounts.ts | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/Onfido/types.ts b/src/components/Onfido/types.ts index e341dfd64960..90403f1ab179 100644 --- a/src/components/Onfido/types.ts +++ b/src/components/Onfido/types.ts @@ -1,8 +1,13 @@ import type {OnfidoResult} from '@onfido/react-native-sdk'; import type * as OnfidoSDK from 'onfido-sdk-ui'; +import type {OnyxEntry} from 'react-native-onyx'; type OnfidoData = OnfidoSDK.SdkResponse | OnfidoResult; +type OnfidoDataWithApplicantID = OnfidoData & { + applicantID: OnyxEntry; +}; + type OnfidoElement = HTMLDivElement & {onfidoOut?: OnfidoSDK.SdkHandle}; type OnfidoProps = { @@ -19,4 +24,4 @@ type OnfidoProps = { onError: (error?: string) => void; }; -export type {OnfidoProps, OnfidoElement, OnfidoData}; +export type {OnfidoProps, OnfidoElement, OnfidoData, OnfidoDataWithApplicantID}; diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index 30dd03b6e780..90e4a6c4aaed 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -1,4 +1,5 @@ import Onyx from 'react-native-onyx'; +import type {OnfidoDataWithApplicantID} from '@components/Onfido/types'; import * as API from '@libs/API'; import type { AddPersonalBankAccountParams, @@ -436,7 +437,7 @@ function connectBankAccountManually(bankAccountID: number, bankAccount: PlaidBan /** * Verify the user's identity via Onfido */ -function verifyIdentityForBankAccount(bankAccountID: number, onfidoData: Record, policyID: string) { +function verifyIdentityForBankAccount(bankAccountID: number, onfidoData: OnfidoDataWithApplicantID, policyID: string) { const parameters: VerifyIdentityForBankAccountParams = { bankAccountID, onfidoData: JSON.stringify(onfidoData), From 4bb8acb56b6f3995a58eebf0f96cb975d6a94baa Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 21 Feb 2024 12:42:45 +0100 Subject: [PATCH 0203/1208] fix: removed unused props spreading --- src/pages/home/sidebar/SidebarScreen/index.tsx | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/pages/home/sidebar/SidebarScreen/index.tsx b/src/pages/home/sidebar/SidebarScreen/index.tsx index f017750c912c..e448a9cad332 100755 --- a/src/pages/home/sidebar/SidebarScreen/index.tsx +++ b/src/pages/home/sidebar/SidebarScreen/index.tsx @@ -1,21 +1,14 @@ import React from 'react'; -import type {LayoutChangeEvent} from 'react-native'; import useWindowDimensions from '@hooks/useWindowDimensions'; import FreezeWrapper from '@libs/Navigation/FreezeWrapper'; import BaseSidebarScreen from './BaseSidebarScreen'; -type SidebarScreenProps = { - onLayout: (event: LayoutChangeEvent) => void; -}; -function SidebarScreen(props: SidebarScreenProps) { +function SidebarScreen() { const {isSmallScreenWidth} = useWindowDimensions(); return ( - + ); } From 24fb379d31f4fa6647a12795ff7308be4cdd8f47 Mon Sep 17 00:00:00 2001 From: Jayesh Mangwani Date: Wed, 21 Feb 2024 19:48:37 +0530 Subject: [PATCH 0204/1208] fix loading issue for idology questions and fixed onValueChange for SingleChoiceQuestion --- src/libs/actions/Wallet.ts | 4 ++-- src/pages/EnablePayments/IdologyQuestions.tsx | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/libs/actions/Wallet.ts b/src/libs/actions/Wallet.ts index 9cb4b28bef20..ffc68c562c4c 100644 --- a/src/libs/actions/Wallet.ts +++ b/src/libs/actions/Wallet.ts @@ -233,7 +233,7 @@ function answerQuestionsForWallet(answers: WalletQuestionAnswer[], idNumber: str const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, + key: ONYXKEYS.FORMS.IDOLOGY_QUESTIONS_FORM, value: { isLoading: true, }, @@ -243,7 +243,7 @@ function answerQuestionsForWallet(answers: WalletQuestionAnswer[], idNumber: str const finallyData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, + key: ONYXKEYS.FORMS.IDOLOGY_QUESTIONS_FORM, value: { isLoading: false, }, diff --git a/src/pages/EnablePayments/IdologyQuestions.tsx b/src/pages/EnablePayments/IdologyQuestions.tsx index cadb7092f0c4..b00010671518 100644 --- a/src/pages/EnablePayments/IdologyQuestions.tsx +++ b/src/pages/EnablePayments/IdologyQuestions.tsx @@ -126,9 +126,10 @@ function IdologyQuestions({questions, idNumber}: IdologyQuestionsProps) { prompt={currentQuestion.prompt} possibleAnswers={possibleAnswers} currentQuestionIndex={currentQuestionIndex} - onInputChange={chooseAnswer} - // NOTEME: check the PR where this was added - // onValueChange={chooseAnswer} + onValueChange={(value) => { + chooseAnswer(String(value)); + }} + onInputChange={() => {}} /> From a8097cd6a6be5b86a7d045304d1d8cf6e668c00c Mon Sep 17 00:00:00 2001 From: Alex Beaman Date: Wed, 21 Feb 2024 16:33:00 +0200 Subject: [PATCH 0205/1208] Initial historical action to show --- src/CONST.ts | 1 + src/pages/home/report/ReportActionItem.js | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/CONST.ts b/src/CONST.ts index 6a57738d06ec..dade8443bfce 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -653,6 +653,7 @@ const CONST = { LEAVE_ROOM: 'LEAVEROOM', UPDATE_ROOM_DESCRIPTION: 'UPDATEROOMDESCRIPTION', }, + UNAPPROVED: 'UNAPPROVED', UNHOLD: 'UNHOLD', }, THREAD_DISABLED: ['CREATED'], diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 66394190fde6..dac0353623a5 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -464,7 +464,11 @@ function ReportActionItem(props) { children = ; } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE) { children = ; - } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED) { + } else if ([ + CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, + CONST.REPORT.ACTIONS.TYPE.UNAPPROVED + ].includes(props.action.actionName)) { + // This handles all historical actions from OldDot that we just want to display the message text children = ; } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.HOLD) { children = ; From 708e818589c46508701cf0c6bfc0c93bbef437c8 Mon Sep 17 00:00:00 2001 From: Jayesh Mangwani Date: Wed, 21 Feb 2024 20:46:53 +0530 Subject: [PATCH 0206/1208] extracted URLs to CONST and deleted unused files --- src/CONST.ts | 4 ++++ .../EnablePayments/AdditionalDetailsStep.tsx | 2 +- src/pages/EnablePayments/IdologyQuestions.tsx | 3 ++- src/pages/EnablePayments/OnfidoPrivacy.tsx | 7 ++++--- src/pages/EnablePayments/TermsStep.tsx | 8 ++++--- .../walletAdditionalDetailsDraftPropTypes.js | 13 ------------ .../walletOnfidoDataPropTypes.js | 21 ------------------- .../ReimbursementAccount/BankAccountStep.js | 2 +- .../substeps/ConfirmAgreements.tsx | 3 ++- 9 files changed, 19 insertions(+), 44 deletions(-) delete mode 100644 src/pages/EnablePayments/walletAdditionalDetailsDraftPropTypes.js delete mode 100644 src/pages/EnablePayments/walletOnfidoDataPropTypes.js diff --git a/src/CONST.ts b/src/CONST.ts index ac8533c620b9..228f8bb97953 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -505,6 +505,10 @@ const CONST = { TERMS_URL: `${USE_EXPENSIFY_URL}/terms`, PRIVACY_URL: `${USE_EXPENSIFY_URL}/privacy`, LICENSES_URL: `${USE_EXPENSIFY_URL}/licenses`, + ACH_TERMS_URL: `${USE_EXPENSIFY_URL}/achterms`, + WALLET_AGREEMENT_URL: `${USE_EXPENSIFY_URL}/walletagreement`, + HELP_LINK_URL: `${USE_EXPENSIFY_URL}/usa-patriot-act`, + ELECTRONIC_DISCLOSURES_URL: `${USE_EXPENSIFY_URL}/esignagreement`, GITHUB_RELEASE_URL: 'https://api.github.com/repos/expensify/app/releases/latest', ADD_SECONDARY_LOGIN_URL: encodeURI('settings?param={"section":"account","openModal":"secondaryLogin"}'), MANAGE_CARDS_URL: 'domain_companycards', diff --git a/src/pages/EnablePayments/AdditionalDetailsStep.tsx b/src/pages/EnablePayments/AdditionalDetailsStep.tsx index 97041a5b021a..cdf166536c5f 100644 --- a/src/pages/EnablePayments/AdditionalDetailsStep.tsx +++ b/src/pages/EnablePayments/AdditionalDetailsStep.tsx @@ -150,7 +150,7 @@ function AdditionalDetailsStep({walletAdditionalDetails = DEFAULT_WALLET_ADDITIO {translate('additionalDetailsStep.helpText')} {translate('additionalDetailsStep.helpLink')} diff --git a/src/pages/EnablePayments/IdologyQuestions.tsx b/src/pages/EnablePayments/IdologyQuestions.tsx index b00010671518..13c71f7005a9 100644 --- a/src/pages/EnablePayments/IdologyQuestions.tsx +++ b/src/pages/EnablePayments/IdologyQuestions.tsx @@ -11,6 +11,7 @@ import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as BankAccounts from '@userActions/BankAccounts'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Errors} from '@src/types/onyx/OnyxCommon'; @@ -105,7 +106,7 @@ function IdologyQuestions({questions, idNumber}: IdologyQuestionsProps) { {translate('additionalDetailsStep.helpTextIdologyQuestions')} {translate('additionalDetailsStep.helpLink')} diff --git a/src/pages/EnablePayments/OnfidoPrivacy.tsx b/src/pages/EnablePayments/OnfidoPrivacy.tsx index 03f8df3ad82a..99117e3f1e99 100644 --- a/src/pages/EnablePayments/OnfidoPrivacy.tsx +++ b/src/pages/EnablePayments/OnfidoPrivacy.tsx @@ -13,6 +13,7 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; import * as BankAccounts from '@userActions/BankAccounts'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {WalletOnfido} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -60,11 +61,11 @@ function OnfidoPrivacy({walletOnfidoData = DEFAULT_WALLET_ONFIDO_DATA}: OnfidoPr {translate('onfidoStep.acceptTerms')} - {translate('onfidoStep.facialScan')} + {translate('onfidoStep.facialScan')} {', '} - {translate('common.privacy')} + {translate('common.privacy')} {` ${translate('common.and')} `} - {translate('common.termsOfService')}. + {translate('common.termsOfService')}. diff --git a/src/pages/EnablePayments/TermsStep.tsx b/src/pages/EnablePayments/TermsStep.tsx index 4836feae9f9b..ecb636e854f0 100644 --- a/src/pages/EnablePayments/TermsStep.tsx +++ b/src/pages/EnablePayments/TermsStep.tsx @@ -11,6 +11,7 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; import * as BankAccounts from '@userActions/BankAccounts'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {UserWallet, WalletTerms} from '@src/types/onyx'; import LongTermsForm from './TermsPage/LongTermsForm'; @@ -69,7 +70,8 @@ function TermsStep(props: TermsStepProps) { LabelComponent={() => ( {`${translate('termsStep.haveReadAndAgree')}`} - {`${translate('termsStep.electronicDisclosures')}.`} + + {`${translate('termsStep.electronicDisclosures')}.`} )} /> @@ -80,11 +82,11 @@ function TermsStep(props: TermsStepProps) { {`${translate('termsStep.agreeToThe')} `} - {`${translate('common.privacy')} `} + {`${translate('common.privacy')} `} {`${translate('common.and')} `} - {`${translate('termsStep.walletAgreement')}.`} + {`${translate('termsStep.walletAgreement')}.`} )} /> diff --git a/src/pages/EnablePayments/walletAdditionalDetailsDraftPropTypes.js b/src/pages/EnablePayments/walletAdditionalDetailsDraftPropTypes.js deleted file mode 100644 index 747fa82f9fa3..000000000000 --- a/src/pages/EnablePayments/walletAdditionalDetailsDraftPropTypes.js +++ /dev/null @@ -1,13 +0,0 @@ -import PropTypes from 'prop-types'; - -export default PropTypes.shape({ - legalFirstName: PropTypes.string, - legalLastName: PropTypes.string, - addressStreet: PropTypes.string, - addressCity: PropTypes.string, - addressState: PropTypes.string, - addressZip: PropTypes.string, - phoneNumber: PropTypes.string, - dob: PropTypes.string, - ssn: PropTypes.string, -}); diff --git a/src/pages/EnablePayments/walletOnfidoDataPropTypes.js b/src/pages/EnablePayments/walletOnfidoDataPropTypes.js deleted file mode 100644 index cedc1f2777b5..000000000000 --- a/src/pages/EnablePayments/walletOnfidoDataPropTypes.js +++ /dev/null @@ -1,21 +0,0 @@ -import PropTypes from 'prop-types'; - -export default PropTypes.shape({ - /** Unique identifier returned from openOnfidoFlow then re-sent to ActivateWallet with Onfido response data */ - applicantID: PropTypes.string, - - /** Token used to initialize the Onfido SDK token */ - sdkToken: PropTypes.string, - - /** Loading state to provide feedback when we are waiting for a request to finish */ - loading: PropTypes.bool, - - /** Error message to inform the user of any problem that might occur */ - error: PropTypes.string, - - /** A list of Onfido errors that the user can fix in order to attempt the Onfido flow again */ - fixableErrors: PropTypes.arrayOf(PropTypes.string), - - /** Whether the user has accepted the privacy policy of Onfido or not */ - hasAcceptedPrivacyPolicy: PropTypes.bool, -}); diff --git a/src/pages/ReimbursementAccount/BankAccountStep.js b/src/pages/ReimbursementAccount/BankAccountStep.js index 278036430dbf..2c40c5a2ed2f 100644 --- a/src/pages/ReimbursementAccount/BankAccountStep.js +++ b/src/pages/ReimbursementAccount/BankAccountStep.js @@ -179,7 +179,7 @@ function BankAccountStep(props) { )} - {props.translate('common.privacy')} + {props.translate('common.privacy')} Link.openExternalLink('https://community.expensify.com/discussion/5677/deep-dive-how-expensify-protects-your-information/')} style={[styles.flexRow, styles.alignItemsCenter]} diff --git a/src/pages/ReimbursementAccount/CompleteVerification/substeps/ConfirmAgreements.tsx b/src/pages/ReimbursementAccount/CompleteVerification/substeps/ConfirmAgreements.tsx index aa554ca87eb2..fae7702053b7 100644 --- a/src/pages/ReimbursementAccount/CompleteVerification/substeps/ConfirmAgreements.tsx +++ b/src/pages/ReimbursementAccount/CompleteVerification/substeps/ConfirmAgreements.tsx @@ -11,6 +11,7 @@ import useLocalize from '@hooks/useLocalize'; import type {SubStepProps} from '@hooks/useSubStep/types'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ValidationUtils from '@libs/ValidationUtils'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; import type {ReimbursementAccount} from '@src/types/onyx'; @@ -90,7 +91,7 @@ function ConfirmAgreements({onNext, reimbursementAccount}: ConfirmAgreementsProp LabelComponent={() => ( {translate('common.iAcceptThe')} - {`${translate('completeVerificationStep.termsAndConditions')}`} + {`${translate('completeVerificationStep.termsAndConditions')}`} )} defaultValue={defaultValues.acceptTermsAndConditions} From 7064cfe6e4487972ff995c5886994cd45f80e5ea Mon Sep 17 00:00:00 2001 From: ruben-rebelo Date: Wed, 21 Feb 2024 15:52:22 +0000 Subject: [PATCH 0207/1208] [TS migration] Migrate postTestBuildComment to typescript --- ...uildComment.js => postTestBuildComment.ts} | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) rename tests/unit/{postTestBuildComment.js => postTestBuildComment.ts} (83%) diff --git a/tests/unit/postTestBuildComment.js b/tests/unit/postTestBuildComment.ts similarity index 83% rename from tests/unit/postTestBuildComment.js rename to tests/unit/postTestBuildComment.ts index ff77ca190c08..df0a623aaced 100644 --- a/tests/unit/postTestBuildComment.js +++ b/tests/unit/postTestBuildComment.ts @@ -1,10 +1,11 @@ import * as core from '@actions/core'; import {when} from 'jest-when'; +import type {Writable} from 'type-fest'; import ghAction from '../../.github/actions/javascript/postTestBuildComment/postTestBuildComment'; import GithubUtils from '../../.github/libs/GithubUtils'; const mockGetInput = jest.fn(); -const mockCreateComment = jest.fn(); +const createCommentMock = jest.spyOn(GithubUtils, 'createComment'); const mockListComments = jest.fn(); const mockGraphql = jest.fn(); jest.spyOn(GithubUtils, 'octokit', 'get').mockReturnValue({ @@ -12,7 +13,11 @@ jest.spyOn(GithubUtils, 'octokit', 'get').mockReturnValue({ listComments: mockListComments, }, }); -jest.spyOn(GithubUtils, 'paginate', 'get').mockReturnValue((endpoint, params) => endpoint(params).then(({data}) => data)); + +jest.spyOn(GithubUtils, 'paginate', 'get').mockReturnValue((endpoint: (params: Record) => Promise<{data: TData}>, params: Record) => + endpoint(params).then((response) => response.data), +); + jest.spyOn(GithubUtils, 'graphql', 'get').mockReturnValue(mockGraphql); jest.mock('@actions/github', () => ({ @@ -49,11 +54,12 @@ const message = `:test_tube::test_tube: Use the links below to test this adhoc b :eyes: [View the workflow run that generated this build](https://github.com/Expensify/App/actions/runs/1234) :eyes: `; +const asMutable = (value: T): Writable => value as Writable; + describe('Post test build comments action tests', () => { beforeAll(() => { // Mock core module - core.getInput = mockGetInput; - GithubUtils.createComment = mockCreateComment; + asMutable(core).getInput = mockGetInput; }); test('Test GH action', async () => { @@ -66,11 +72,12 @@ describe('Post test build comments action tests', () => { when(core.getInput).calledWith('IOS_LINK').mockReturnValue('https://expensify.app/IOS_LINK'); when(core.getInput).calledWith('WEB_LINK').mockReturnValue('https://expensify.app/WEB_LINK'); when(core.getInput).calledWith('DESKTOP_LINK').mockReturnValue('https://expensify.app/DESKTOP_LINK'); - GithubUtils.createComment.mockResolvedValue(true); + createCommentMock.mockResolvedValue(true); mockListComments.mockResolvedValue({ data: [ { body: ':test_tube::test_tube: Use the links below to test this adhoc build on Android, iOS, Desktop, and Web. Happy testing!', + // eslint-disable-next-line @typescript-eslint/naming-convention node_id: 'IC_abcd', }, ], @@ -86,7 +93,7 @@ describe('Post test build comments action tests', () => { } } `); - expect(GithubUtils.createComment).toBeCalledTimes(1); - expect(GithubUtils.createComment).toBeCalledWith('App', 12, message); + expect(createCommentMock).toBeCalledTimes(1); + expect(createCommentMock).toBeCalledWith('App', 12, message); }); }); From 0ead9f440a8ff6d5bd8167a27dc0bf2e3def6ca8 Mon Sep 17 00:00:00 2001 From: ruben-rebelo Date: Wed, 21 Feb 2024 15:58:26 +0000 Subject: [PATCH 0208/1208] [TS migration][postTestBuildComment] Added last ts issue fix --- tests/unit/postTestBuildComment.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/unit/postTestBuildComment.ts b/tests/unit/postTestBuildComment.ts index df0a623aaced..808e85d5c8a8 100644 --- a/tests/unit/postTestBuildComment.ts +++ b/tests/unit/postTestBuildComment.ts @@ -63,7 +63,9 @@ describe('Post test build comments action tests', () => { }); test('Test GH action', async () => { - when(core.getInput).calledWith('PR_NUMBER', {required: true}).mockReturnValue(12); + when(core.getInput) + .calledWith('PR_NUMBER', {required: true}) + .mockReturnValue(12 as unknown as string); when(core.getInput).calledWith('ANDROID', {required: true}).mockReturnValue('success'); when(core.getInput).calledWith('IOS', {required: true}).mockReturnValue('success'); when(core.getInput).calledWith('WEB', {required: true}).mockReturnValue('success'); From 09e448f7ddbe7992a943a63abc17f960e71cd647 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 21 Feb 2024 17:37:35 +0100 Subject: [PATCH 0209/1208] fix: added ugly fix for onyx selectors --- src/pages/home/sidebar/SidebarLinksData.tsx | 74 +++++++++---------- .../FloatingActionButtonAndPopover.tsx | 10 ++- 2 files changed, 43 insertions(+), 41 deletions(-) diff --git a/src/pages/home/sidebar/SidebarLinksData.tsx b/src/pages/home/sidebar/SidebarLinksData.tsx index 29f58d679760..182f413272f0 100644 --- a/src/pages/home/sidebar/SidebarLinksData.tsx +++ b/src/pages/home/sidebar/SidebarLinksData.tsx @@ -23,42 +23,42 @@ import type * as OnyxTypes from '@src/types/onyx'; import type {Message} from '@src/types/onyx/ReportAction'; import SidebarLinks from './SidebarLinks'; +type PickedReport = Pick< + OnyxTypes.Report, + | 'reportID' + | 'participantAccountIDs' + | 'hasDraft' + | 'isPinned' + | 'isHidden' + | 'notificationPreference' + | 'errorFields' + | 'lastMessageText' + | 'lastVisibleActionCreated' + | 'iouReportID' + | 'total' + | 'nonReimbursableTotal' + | 'hasOutstandingChildRequest' + | 'isWaitingOnBankAccount' + | 'statusNum' + | 'stateNum' + | 'chatType' + | 'type' + | 'policyID' + | 'visibility' + | 'lastReadTime' + | 'reportName' + | 'policyName' + | 'oldPolicyName' + | 'ownerAccountID' + | 'currency' + | 'managerID' + | 'parentReportActionID' + | 'parentReportID' + | 'isDeletedParentAction' +>; + type SidebarLinksDataOnyxProps = { - chatReports: OnyxCollection< - Pick< - OnyxTypes.Report, - | 'reportID' - | 'participantAccountIDs' - | 'hasDraft' - | 'isPinned' - | 'isHidden' - | 'notificationPreference' - | 'errorFields' - | 'lastMessageText' - | 'lastVisibleActionCreated' - | 'iouReportID' - | 'total' - | 'nonReimbursableTotal' - | 'hasOutstandingChildRequest' - | 'isWaitingOnBankAccount' - | 'statusNum' - | 'stateNum' - | 'chatType' - | 'type' - | 'policyID' - | 'visibility' - | 'lastReadTime' - | 'reportName' - | 'policyName' - | 'oldPolicyName' - | 'ownerAccountID' - | 'currency' - | 'managerID' - | 'parentReportActionID' - | 'parentReportID' - | 'isDeletedParentAction' - > & {isUnreadWithMention: boolean} - >; + chatReports: OnyxCollection; isLoadingApp: OnyxEntry; priorityMode: OnyxEntry>; betas: OnyxEntry; @@ -248,7 +248,7 @@ const policySelector = (policy: OnyxEntry) => export default withOnyx({ chatReports: { key: ONYXKEYS.COLLECTION.REPORT, - selector: chatReportSelector, + selector: chatReportSelector as unknown as (report: OnyxEntry) => OnyxCollection, initialValue: {}, }, isLoadingApp: { @@ -269,7 +269,7 @@ export default withOnyx({ }, policies: { key: ONYXKEYS.COLLECTION.POLICY, - selector: policySelector, + selector: policySelector as unknown as (policy: OnyxEntry) => OnyxCollection>, initialValue: {}, }, policyMembers: { diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index 2a0682b776cb..208a9c0a9deb 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -1,5 +1,5 @@ import {useIsFocused} from '@react-navigation/native'; -import type {ForwardedRef} from 'react'; +import type {ForwardedRef, RefAttributes} from 'react'; import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; @@ -170,7 +170,7 @@ function FloatingActionButtonAndPopover( text: translate('sidebarScreen.saveTheWorld'), onSelected: () => interceptAnonymousUser(() => Navigation.navigate(ROUTES.TEACHERS_UNITE)), }, - ...(!isLoading && !Policy.hasActiveFreePolicy(allPolicies) + ...(!isLoading && !Policy.hasActiveFreePolicy(allPolicies as Record) ? [ { displayInDefaultIconColor: true, @@ -211,10 +211,12 @@ const policySelector = (policy: OnyxEntry) => } : null; -export default withOnyx({ +export default withOnyx, FloatingActionButtonAndPopoverOnyxProps>({ allPolicies: { key: ONYXKEYS.COLLECTION.POLICY, - selector: policySelector, + selector: policySelector as unknown as ( + policy: OnyxEntry, + ) => OnyxEntry>>, }, isLoading: { key: ONYXKEYS.IS_LOADING_APP, From ce8832837a03264d7a95085f06a247a03fdd0ff2 Mon Sep 17 00:00:00 2001 From: Toby Sullivan Date: Wed, 21 Feb 2024 09:21:25 -0800 Subject: [PATCH 0210/1208] Get linked transaction only when absent --- src/libs/ReportUtils.ts | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 9f150c241540..1a2a435199d0 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2305,9 +2305,9 @@ function getReportPreviewMessage( return reportActionMessage; } - const linkedTransaction = !isEmptyObject(reportAction) ? TransactionUtils.getLinkedTransaction(reportAction) : {}; if (!isEmptyObject(reportAction) && !isIOUReport(report) && reportAction && ReportActionsUtils.isSplitBillAction(reportAction)) { // This covers group chats where the last action is a split bill action + const linkedTransaction = TransactionUtils.getLinkedTransaction(reportAction); if (isEmptyObject(linkedTransaction)) { return reportActionMessage; } @@ -2341,14 +2341,12 @@ function getReportPreviewMessage( }); } - if ( - !isEmptyObject(linkedTransaction) && - !isEmptyObject(reportAction) && - shouldConsiderScanningReceiptOrPendingRoute && - TransactionUtils.hasReceipt(linkedTransaction) && - TransactionUtils.isReceiptBeingScanned(linkedTransaction) && - ReportActionsUtils.isMoneyRequestAction(reportAction) - ) { + let linkedTransaction; + if (!isEmptyObject(reportAction) && shouldConsiderScanningReceiptOrPendingRoute && reportAction && ReportActionsUtils.isMoneyRequestAction(reportAction)) { + linkedTransaction = TransactionUtils.getLinkedTransaction(reportAction); + } + + if (!isEmptyObject(linkedTransaction) && TransactionUtils.hasReceipt(linkedTransaction) && TransactionUtils.isReceiptBeingScanned(linkedTransaction)) { return Localize.translateLocal('iou.receiptScanning'); } @@ -2385,7 +2383,6 @@ function getReportPreviewMessage( } const lastActorID = reportAction?.actorAccountID; - const comment = !isEmptyObject(linkedTransaction) ? TransactionUtils.getDescription(linkedTransaction) : undefined; let amount = originalMessage?.amount; let currency = originalMessage?.currency ? originalMessage?.currency : report.currency; @@ -2394,6 +2391,11 @@ function getReportPreviewMessage( currency = TransactionUtils.getCurrency(linkedTransaction); } + if (isEmptyObject(linkedTransaction) && !isEmptyObject(reportAction)) { + linkedTransaction = TransactionUtils.getLinkedTransaction(reportAction); + } + const comment = !isEmptyObject(linkedTransaction) ? TransactionUtils.getDescription(linkedTransaction) : undefined; + // if we have the amount in the originalMessage and lastActorID, we can use that to display the preview message for the latest request if (amount !== undefined && lastActorID && !isPreviewMessageForParentChatReport) { const amountToDisplay = CurrencyUtils.convertToDisplayString(Math.abs(amount), currency); From beb654bcedce02f066bc7eef7a2f62327118f31e Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 22 Feb 2024 10:26:08 +0700 Subject: [PATCH 0211/1208] 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 a5ea70b6898e063ef27b2e11884cfaf716d633e7 Mon Sep 17 00:00:00 2001 From: ruben-rebelo Date: Thu, 22 Feb 2024 09:05:21 +0000 Subject: [PATCH 0212/1208] [TS migration][postTestBuildComment] Updated asMutable to the util --- src/utils/asMutable.ts | 5 +++++ tests/unit/postTestBuildComment.ts | 4 +--- 2 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 src/utils/asMutable.ts diff --git a/src/utils/asMutable.ts b/src/utils/asMutable.ts new file mode 100644 index 000000000000..57c49058cd14 --- /dev/null +++ b/src/utils/asMutable.ts @@ -0,0 +1,5 @@ +import type {Writable} from 'type-fest'; + +const asMutable = (value: T): Writable => value as Writable; + +export default asMutable; diff --git a/tests/unit/postTestBuildComment.ts b/tests/unit/postTestBuildComment.ts index 808e85d5c8a8..1c1e9ad35564 100644 --- a/tests/unit/postTestBuildComment.ts +++ b/tests/unit/postTestBuildComment.ts @@ -1,6 +1,6 @@ import * as core from '@actions/core'; import {when} from 'jest-when'; -import type {Writable} from 'type-fest'; +import asMutable from '@src/utils/asMutable'; import ghAction from '../../.github/actions/javascript/postTestBuildComment/postTestBuildComment'; import GithubUtils from '../../.github/libs/GithubUtils'; @@ -54,8 +54,6 @@ const message = `:test_tube::test_tube: Use the links below to test this adhoc b :eyes: [View the workflow run that generated this build](https://github.com/Expensify/App/actions/runs/1234) :eyes: `; -const asMutable = (value: T): Writable => value as Writable; - describe('Post test build comments action tests', () => { beforeAll(() => { // Mock core module From dfad42627dc4711bd9311b30b397f4468c414ee3 Mon Sep 17 00:00:00 2001 From: ruben-rebelo Date: Thu, 22 Feb 2024 09:17:26 +0000 Subject: [PATCH 0213/1208] [TS migration][postTestBuildComment] Changed file location and name --- src/{utils/asMutable.ts => types/utils/AsMutable.ts} | 0 tests/unit/postTestBuildComment.ts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/{utils/asMutable.ts => types/utils/AsMutable.ts} (100%) diff --git a/src/utils/asMutable.ts b/src/types/utils/AsMutable.ts similarity index 100% rename from src/utils/asMutable.ts rename to src/types/utils/AsMutable.ts diff --git a/tests/unit/postTestBuildComment.ts b/tests/unit/postTestBuildComment.ts index 1c1e9ad35564..26c0711a34cd 100644 --- a/tests/unit/postTestBuildComment.ts +++ b/tests/unit/postTestBuildComment.ts @@ -1,6 +1,6 @@ import * as core from '@actions/core'; import {when} from 'jest-when'; -import asMutable from '@src/utils/asMutable'; +import asMutable from '@src/types/utils/AsMutable'; import ghAction from '../../.github/actions/javascript/postTestBuildComment/postTestBuildComment'; import GithubUtils from '../../.github/libs/GithubUtils'; From 8b2a982a0d31ca5348a4b29a38accd1b2fc90519 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Thu, 22 Feb 2024 19:16:54 +0530 Subject: [PATCH 0214/1208] minor fic. Signed-off-by: Krishna Gupta --- .../ReportActionCompose/AttachmentPickerWithMenuItems.js | 3 +++ .../sidebar/SidebarScreen/FloatingActionButtonAndPopover.js | 1 + 2 files changed, 4 insertions(+) diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js index 3828a43cd54d..af8f31e8c6b4 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js @@ -18,11 +18,14 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; import compose from '@libs/compose'; +import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; +import * as IOU from '@userActions/IOU'; import * as Report from '@userActions/Report'; import * as Task from '@userActions/Task'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; const propTypes = { /** The report currently being looked at */ diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js index b514c374e34c..5b9dce43ddcc 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js @@ -16,6 +16,7 @@ import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; import * as App from '@userActions/App'; +import * as IOU from '@userActions/IOU'; import * as Policy from '@userActions/Policy'; import * as Task from '@userActions/Task'; import CONST from '@src/CONST'; From 2fa5415ddd6951a4ffe88c5b2c0ae278cdf60e75 Mon Sep 17 00:00:00 2001 From: RohanSasne Date: Fri, 23 Feb 2024 00:00:54 +0530 Subject: [PATCH 0215/1208] Set isEditing to true to trim the input --- .../substeps/PhoneNumberBusiness.tsx | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/pages/ReimbursementAccount/BusinessInfo/substeps/PhoneNumberBusiness.tsx b/src/pages/ReimbursementAccount/BusinessInfo/substeps/PhoneNumberBusiness.tsx index 3a851b001ff7..650dd32e069d 100644 --- a/src/pages/ReimbursementAccount/BusinessInfo/substeps/PhoneNumberBusiness.tsx +++ b/src/pages/ReimbursementAccount/BusinessInfo/substeps/PhoneNumberBusiness.tsx @@ -1,4 +1,4 @@ -import React, {useState} from 'react'; +import React from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; @@ -26,11 +26,11 @@ type PhoneNumberBusinessProps = PhoneNumberBusinessOnyxProps & SubStepProps; const COMPANY_PHONE_NUMBER_KEY = INPUT_IDS.BUSINESS_INFO_STEP.COMPANY_PHONE; const STEP_FIELDS = [COMPANY_PHONE_NUMBER_KEY]; -const validate = (values: FormOnyxValues, inputValue: string): FormInputErrors => { +const validate = (values: FormOnyxValues): FormInputErrors => { const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - if ((values.companyPhone && !ValidationUtils.isValidUSPhone(values.companyPhone, true)) || inputValue.trim() !== inputValue) { + if ((values.companyPhone && !ValidationUtils.isValidUSPhone(values.companyPhone, true))) { errors.companyPhone = 'bankAccount.error.phoneNumber'; } @@ -42,15 +42,9 @@ function PhoneNumberBusiness({reimbursementAccount, onNext, isEditing}: PhoneNum const styles = useThemeStyles(); const defaultCompanyPhoneNumber = reimbursementAccount?.achData?.companyPhone ?? ''; - const [inputValue, setInputValue] = useState(defaultCompanyPhoneNumber); - - const handleInputChange = (newValue: string) => { - setInputValue(newValue); - }; - const handleSubmit = useReimbursementAccountStepFormSubmit({ fieldIds: STEP_FIELDS, - isEditing, + isEditing: true, onNext, }); @@ -58,7 +52,7 @@ function PhoneNumberBusiness({reimbursementAccount, onNext, isEditing}: PhoneNum validate(values, inputValue)} + validate={validate} onSubmit={handleSubmit} style={[styles.mh5, styles.flexGrow1]} submitButtonStyles={[styles.pb5, styles.mb0]} @@ -75,7 +69,6 @@ function PhoneNumberBusiness({reimbursementAccount, onNext, isEditing}: PhoneNum defaultValue={defaultCompanyPhoneNumber} shouldSaveDraft={!isEditing} containerStyles={[styles.mt6]} - onChangeText={handleInputChange} /> ); From 59830b8f8138d918be3f4d6b1a121d694762857b Mon Sep 17 00:00:00 2001 From: RohanSasne Date: Fri, 23 Feb 2024 00:02:32 +0530 Subject: [PATCH 0216/1208] Set isEditing to true to trim the input --- .../BusinessInfo/substeps/PhoneNumberBusiness.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/ReimbursementAccount/BusinessInfo/substeps/PhoneNumberBusiness.tsx b/src/pages/ReimbursementAccount/BusinessInfo/substeps/PhoneNumberBusiness.tsx index 650dd32e069d..e2746dbab59f 100644 --- a/src/pages/ReimbursementAccount/BusinessInfo/substeps/PhoneNumberBusiness.tsx +++ b/src/pages/ReimbursementAccount/BusinessInfo/substeps/PhoneNumberBusiness.tsx @@ -29,8 +29,7 @@ const STEP_FIELDS = [COMPANY_PHONE_NUMBER_KEY]; const validate = (values: FormOnyxValues): FormInputErrors => { const errors = ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - if ((values.companyPhone && !ValidationUtils.isValidUSPhone(values.companyPhone, true))) { + if (values.companyPhone && !ValidationUtils.isValidUSPhone(values.companyPhone, true)) { errors.companyPhone = 'bankAccount.error.phoneNumber'; } From 840685854cf85f535be41e9df7792673d3572504 Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Fri, 23 Feb 2024 16:15:20 +0100 Subject: [PATCH 0217/1208] Fix typescript errors --- .../RestartBankAccountSetupParams.ts | 1 + .../actions/ReimbursementAccount/index.ts | 2 +- .../resetFreePlanBankAccount.ts | 42 ++++++++++++++++++- .../BankInfo/BankInfo.tsx | 2 +- 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/libs/API/parameters/RestartBankAccountSetupParams.ts b/src/libs/API/parameters/RestartBankAccountSetupParams.ts index b338eac0dea1..5cb4fa132d10 100644 --- a/src/libs/API/parameters/RestartBankAccountSetupParams.ts +++ b/src/libs/API/parameters/RestartBankAccountSetupParams.ts @@ -1,6 +1,7 @@ type RestartBankAccountSetupParams = { bankAccountID: number; ownerEmail: string; + policyID: string; }; export default RestartBankAccountSetupParams; diff --git a/src/libs/actions/ReimbursementAccount/index.ts b/src/libs/actions/ReimbursementAccount/index.ts index 416c5e956189..dd1c784d2218 100644 --- a/src/libs/actions/ReimbursementAccount/index.ts +++ b/src/libs/actions/ReimbursementAccount/index.ts @@ -26,7 +26,7 @@ function setWorkspaceIDForReimbursementAccount(workspaceID: string | null) { Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT_WORKSPACE_ID, workspaceID); } -function updateReimbursementAccountDraft(bankAccountData: ReimbursementAccountForm) { +function updateReimbursementAccountDraft(bankAccountData: Partial) { Onyx.merge(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT, bankAccountData); Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {draftStep: undefined}); } diff --git a/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.ts b/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.ts index 9eea9504d604..b3effa220e59 100644 --- a/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.ts +++ b/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.ts @@ -6,6 +6,7 @@ import * as PlaidDataProps from '@pages/ReimbursementAccount/plaidDataPropTypes' import * as ReimbursementAccountProps from '@pages/ReimbursementAccount/reimbursementAccountPropTypes'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; import type * as OnyxTypes from '@src/types/onyx'; /** @@ -60,6 +61,7 @@ function resetFreePlanBankAccount(bankAccountID: number, session: OnyxEntry Date: Fri, 23 Feb 2024 16:21:05 +0100 Subject: [PATCH 0218/1208] Fix comment typo --- src/libs/actions/ReimbursementAccount/errors.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/ReimbursementAccount/errors.ts b/src/libs/actions/ReimbursementAccount/errors.ts index f85426f8d4fe..05c375364329 100644 --- a/src/libs/actions/ReimbursementAccount/errors.ts +++ b/src/libs/actions/ReimbursementAccount/errors.ts @@ -7,7 +7,7 @@ import type {ErrorFields} from '@src/types/onyx/OnyxCommon'; * Set the current fields with errors. */ function setPersonalBankAccountFormValidationErrorFields(errorFields: ErrorFields) { - // We set 'errors' to null first because we don't have a way yet to replace a specific property without merging it + // We set 'errorFields' to null first because we don't have a way yet to replace a specific property without merging it Onyx.merge(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {errorFields: null}); Onyx.merge(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {errorFields}); } From fe351822d7fcf92f73ea1cf27f81ddee245d2391 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 23 Feb 2024 16:24:33 +0100 Subject: [PATCH 0219/1208] fix: tests --- src/pages/home/sidebar/SidebarLinksData.tsx | 106 ++++++++++++-------- 1 file changed, 64 insertions(+), 42 deletions(-) diff --git a/src/pages/home/sidebar/SidebarLinksData.tsx b/src/pages/home/sidebar/SidebarLinksData.tsx index 182f413272f0..5a5e7a8e0db2 100644 --- a/src/pages/home/sidebar/SidebarLinksData.tsx +++ b/src/pages/home/sidebar/SidebarLinksData.tsx @@ -6,8 +6,9 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import type {EdgeInsets} from 'react-native-safe-area-context'; import type {ValueOf} from 'type-fest'; +import type {CurrentReportIDContextValue} from '@components/withCurrentReportID'; +import withCurrentReportID from '@components/withCurrentReportID'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; -import useCurrentReportID from '@hooks/useCurrentReportID'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -58,21 +59,40 @@ type PickedReport = Pick< >; type SidebarLinksDataOnyxProps = { + /** List of reports */ chatReports: OnyxCollection; + + /** Wheather the reports are loading. When false it means they are ready to be used. */ isLoadingApp: OnyxEntry; + + /** The chat priority mode */ priorityMode: OnyxEntry>; + + /** Beta features list */ betas: OnyxEntry; + + /** All report actions for all reports */ allReportActions: OnyxEntry>>; + + /** The policies which the user has access to */ policies: OnyxCollection>; - policyMembers: OnyxCollection; + + /** All of the transaction violations */ transactionViolations: OnyxCollection; -}; -type SidebarLinksDataProps = SidebarLinksDataOnyxProps & { - onLinkClick: () => void; - insets: EdgeInsets | undefined; + /** All policy members */ + policyMembers: OnyxCollection; }; +type SidebarLinksDataProps = CurrentReportIDContextValue & + SidebarLinksDataOnyxProps & { + /** Toggles the navigation menu open and closed */ + onLinkClick: () => void; + + /** Safe area insets required for mobile devices margins */ + insets: EdgeInsets | undefined; + }; + function SidebarLinksData({ allReportActions, betas, @@ -84,8 +104,8 @@ function SidebarLinksData({ priorityMode = CONST.PRIORITY_MODE.DEFAULT, policyMembers, transactionViolations, + currentReportID, }: SidebarLinksDataProps) { - const {currentReportID} = useCurrentReportID() ?? {}; const {accountID} = useCurrentUserPersonalDetails(); const network = useNetwork(); const isFocused = useIsFocused(); @@ -245,38 +265,40 @@ const policySelector = (policy: OnyxEntry) => avatar: policy.avatar, }; -export default withOnyx({ - chatReports: { - key: ONYXKEYS.COLLECTION.REPORT, - selector: chatReportSelector as unknown as (report: OnyxEntry) => OnyxCollection, - initialValue: {}, - }, - isLoadingApp: { - key: ONYXKEYS.IS_LOADING_APP, - }, - priorityMode: { - key: ONYXKEYS.NVP_PRIORITY_MODE, - initialValue: CONST.PRIORITY_MODE.DEFAULT, - }, - betas: { - key: ONYXKEYS.BETAS, - initialValue: [], - }, - allReportActions: { - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - selector: reportActionsSelector, - initialValue: {}, - }, - policies: { - key: ONYXKEYS.COLLECTION.POLICY, - selector: policySelector as unknown as (policy: OnyxEntry) => OnyxCollection>, - initialValue: {}, - }, - policyMembers: { - key: ONYXKEYS.COLLECTION.POLICY_MEMBERS, - }, - transactionViolations: { - key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, - initialValue: {}, - }, -})(SidebarLinksData); +export default withCurrentReportID( + withOnyx({ + chatReports: { + key: ONYXKEYS.COLLECTION.REPORT, + selector: chatReportSelector as unknown as (report: OnyxEntry) => OnyxCollection, + initialValue: {}, + }, + isLoadingApp: { + key: ONYXKEYS.IS_LOADING_APP, + }, + priorityMode: { + key: ONYXKEYS.NVP_PRIORITY_MODE, + initialValue: CONST.PRIORITY_MODE.DEFAULT, + }, + betas: { + key: ONYXKEYS.BETAS, + initialValue: [], + }, + allReportActions: { + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + selector: reportActionsSelector, + initialValue: {}, + }, + policies: { + key: ONYXKEYS.COLLECTION.POLICY, + selector: policySelector as unknown as (policy: OnyxEntry) => OnyxCollection>, + initialValue: {}, + }, + policyMembers: { + key: ONYXKEYS.COLLECTION.POLICY_MEMBERS, + }, + transactionViolations: { + key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, + initialValue: {}, + }, + })(SidebarLinksData), +); From a78c3f8f66403c475c9468cdeafc460352f174ad Mon Sep 17 00:00:00 2001 From: tienifr Date: Sat, 24 Feb 2024 13:51:28 +0700 Subject: [PATCH 0220/1208] remove redundant change --- 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 7f923b20864e..fe5ed9bb1150 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -648,7 +648,7 @@ export default compose( key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report ? report.parentReportID : 0}`, selector: (parentReportActions, props) => { const parentReportActionID = lodashGet(props, 'report.parentReportActionID'); - if (!parentReportActionID || !parentReportActions) { + if (!parentReportActionID) { return {}; } return lodashGet(parentReportActions, parentReportActionID, {}); From 229e1561643f0df80486d414a8f9136cf5584007 Mon Sep 17 00:00:00 2001 From: tienifr Date: Sat, 24 Feb 2024 14:44:40 +0700 Subject: [PATCH 0221/1208] fix: infinite loading in policy expense chat when changing from auditor to employee --- src/pages/home/ReportScreen.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index fe5ed9bb1150..997989c97aa4 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -137,6 +137,17 @@ function getReportID(route) { return String(lodashGet(route, 'params.reportID') || 0); } +/** + * Check is the report is deleted. + * We currently use useMemo to memorize every properties of the report + * so we can't check using isEmpty. + * + * @param {Object} report + */ +function isDeletedReport(report) { + return _.every(report, _.isUndefined); +} + function ReportScreen({ betas, route, @@ -421,7 +432,7 @@ function ReportScreen({ !onyxReportID && prevReport.statusNum === CONST.REPORT.STATUS_NUM.OPEN && (report.statusNum === CONST.REPORT.STATUS_NUM.CLOSED || (!report.statusNum && !prevReport.parentReportID && prevReport.chatType === CONST.REPORT.CHAT_TYPE.POLICY_ROOM))) || - ((ReportUtils.isMoneyRequest(prevReport) || ReportUtils.isMoneyRequestReport(prevReport) || ReportUtils.isPolicyExpenseChat(prevReport)) && _.isEmpty(report)) + ((ReportUtils.isMoneyRequest(prevReport) || ReportUtils.isMoneyRequestReport(prevReport) || ReportUtils.isPolicyExpenseChat(prevReport)) && isDeletedReport(report)) ) { Navigation.dismissModal(); if (Navigation.getTopmostReportId() === prevOnyxReportID) { From feeceae1bcbd1d11ef2d8d0f94c1815f8fbcc810 Mon Sep 17 00:00:00 2001 From: tienifr Date: Sat, 24 Feb 2024 15:38:10 +0700 Subject: [PATCH 0222/1208] fix lint --- src/pages/home/ReportScreen.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 997989c97aa4..9c3d31fda9e7 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -143,6 +143,7 @@ function getReportID(route) { * so we can't check using isEmpty. * * @param {Object} report + * @returns {Boolean} */ function isDeletedReport(report) { return _.every(report, _.isUndefined); From d2b3674759cd5be4572b09228d5a87d9801f4661 Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Mon, 26 Feb 2024 11:58:43 +0100 Subject: [PATCH 0223/1208] Change Object.entries to Object.values --- src/libs/actions/ReimbursementAccount/store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/ReimbursementAccount/store.ts b/src/libs/actions/ReimbursementAccount/store.ts index bdceb4e2ad5d..30005b6fdc04 100644 --- a/src/libs/actions/ReimbursementAccount/store.ts +++ b/src/libs/actions/ReimbursementAccount/store.ts @@ -52,7 +52,7 @@ function hasCreditBankAccount() { return false; } - Object.entries(bankAccountList).some(([, bankAccountJSON]) => { + Object.values(bankAccountList).some((bankAccountJSON) => { const bankAccount = new BankAccount(bankAccountJSON); return bankAccount.isDefaultCredit(); }); From 60ea1a16422b9452a62464600f73d89da28b8beb Mon Sep 17 00:00:00 2001 From: hurali97 Date: Mon, 26 Feb 2024 16:03:36 +0500 Subject: [PATCH 0224/1208] perf: remove redundant translations --- src/CONST.ts | 1 + src/libs/PersonalDetailsUtils.ts | 9 +++++++-- src/libs/ReportUtils.ts | 23 +++++++++++++++-------- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 8abd4c087b16..bb167bb5d8d8 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -41,6 +41,7 @@ const KEYBOARD_SHORTCUT_NAVIGATION_TYPE = 'NAVIGATION_SHORTCUT'; const cardActiveStates: number[] = [2, 3, 4, 7]; const CONST = { + MERGED_ACCOUNT_PREFIX: 'MERGED_', ANDROID_PACKAGE_NAME, ANIMATED_TRANSITION: 300, ANIMATED_TRANSITION_FROM_VALUE: 100, diff --git a/src/libs/PersonalDetailsUtils.ts b/src/libs/PersonalDetailsUtils.ts index 55aee10e611a..7cd6ac5bdedd 100644 --- a/src/libs/PersonalDetailsUtils.ts +++ b/src/libs/PersonalDetailsUtils.ts @@ -24,9 +24,14 @@ Onyx.connect({ }, }); +const hiddenText = Localize.translateLocal('common.hidden'); +const substringStartIndex = 8; function getDisplayNameOrDefault(passedPersonalDetails?: Partial | null, defaultValue = '', shouldFallbackToHidden = true): string { - const displayName = passedPersonalDetails?.displayName ? passedPersonalDetails.displayName.replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, '') : ''; - const fallbackValue = shouldFallbackToHidden ? Localize.translateLocal('common.hidden') : ''; + let displayName = passedPersonalDetails?.displayName ?? ''; + if (displayName.startsWith(CONST.MERGED_ACCOUNT_PREFIX)) { + displayName = displayName.substring(substringStartIndex); + } + const fallbackValue = shouldFallbackToHidden ? hiddenText : ''; return displayName || defaultValue || fallbackValue; } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 8813501e2b3f..adfefec932e5 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -575,17 +575,18 @@ function getPolicyType(report: OnyxEntry, policies: OnyxCollection | undefined | EmptyObject, returnEmptyIfNotFound = false, policy: OnyxEntry | undefined = undefined): string { - const noPolicyFound = returnEmptyIfNotFound ? '' : Localize.translateLocal('workspace.common.unavailable'); + const noPolicyFound = returnEmptyIfNotFound ? '' : unavailableWorkspaceText; if (isEmptyObject(report)) { return noPolicyFound; } if ((!allPolicies || Object.keys(allPolicies).length === 0) && !report?.policyName) { - return Localize.translateLocal('workspace.common.unavailable'); + return unavailableWorkspaceText; } const finalPolicy = policy ?? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; @@ -773,11 +774,12 @@ function isAnnounceRoom(report: OnyxEntry): boolean { return getChatType(report) === CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE; } +const chatTypes = [CONST.REPORT.CHAT_TYPE.POLICY_ADMINS, CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE, CONST.REPORT.CHAT_TYPE.DOMAIN_ALL]; /** * Whether the provided report is a default room */ function isDefaultRoom(report: OnyxEntry): boolean { - return [CONST.REPORT.CHAT_TYPE.POLICY_ADMINS, CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE, CONST.REPORT.CHAT_TYPE.DOMAIN_ALL].some((type) => type === getChatType(report)); + return chatTypes.some((type) => type === getChatType(report)); } /** @@ -1625,6 +1627,7 @@ function getPersonalDetailsForAccountID(accountID: number): Partial, policy: OnyxEntry = nu } if (parentReportAction?.message?.[0]?.isDeletedParentAction) { - return Localize.translateLocal('parentReportAction.deletedMessage'); + return deletedMessageText; } const isAttachment = ReportActionsUtils.isReportActionAttachment(!isEmptyObject(parentReportAction) ? parentReportAction : null); const parentReportActionMessage = (parentReportAction?.message?.[0]?.text ?? '').replace(/(\r\n|\n|\r)/gm, ' '); if (isAttachment && parentReportActionMessage) { - return `[${Localize.translateLocal('common.attachment')}]`; + return `[${attachmentText}]`; } if ( parentReportAction?.message?.[0]?.moderationDecision?.decision === CONST.MODERATION.MODERATOR_DECISION_PENDING_HIDE || @@ -2529,7 +2536,7 @@ function getReportName(report: OnyxEntry, policy: OnyxEntry = nu } if (isTaskReport(report) && isCanceledTaskReport(report, parentReportAction)) { - return Localize.translateLocal('parentReportAction.deletedTask'); + return deletedTaskText; } if (isChatRoom(report) || isTaskReport(report)) { @@ -2545,7 +2552,7 @@ function getReportName(report: OnyxEntry, policy: OnyxEntry = nu } if (isArchivedRoom(report)) { - formattedName += ` (${Localize.translateLocal('common.archived')})`; + formattedName += ` (${archivedText})`; } if (formattedName) { From 22f0ffa46f75a5828e6edbf6774c0b53e688fbda Mon Sep 17 00:00:00 2001 From: hurali97 Date: Mon, 26 Feb 2024 16:29:41 +0500 Subject: [PATCH 0225/1208] perf: only parse the number if it matches the pattern --- src/CONST.ts | 1 + src/libs/LocalePhoneNumber.ts | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/src/CONST.ts b/src/CONST.ts index bb167bb5d8d8..fce096a9957a 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -42,6 +42,7 @@ const cardActiveStates: number[] = [2, 3, 4, 7]; const CONST = { MERGED_ACCOUNT_PREFIX: 'MERGED_', + SMS_DOMAIN_PATTERN: 'expensify.sms', ANDROID_PACKAGE_NAME, ANIMATED_TRANSITION: 300, ANIMATED_TRANSITION_FROM_VALUE: 100, diff --git a/src/libs/LocalePhoneNumber.ts b/src/libs/LocalePhoneNumber.ts index 933aa7937560..4fc13322b0cf 100644 --- a/src/libs/LocalePhoneNumber.ts +++ b/src/libs/LocalePhoneNumber.ts @@ -1,5 +1,6 @@ import Str from 'expensify-common/lib/str'; import Onyx from 'react-native-onyx'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {parsePhoneNumber} from './PhoneNumber'; @@ -18,6 +19,10 @@ function formatPhoneNumber(number: string): string { return ''; } + // do not parse the string, if it's not a phone number + if (number.indexOf(CONST.SMS_DOMAIN_PATTERN) === -1) { + return number; + } const numberWithoutSMSDomain = Str.removeSMSDomain(number); const parsedPhoneNumber = parsePhoneNumber(numberWithoutSMSDomain); From c34bee5fd6a110df9a4a34ea4e7d28f7084aed28 Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Mon, 26 Feb 2024 12:44:47 +0100 Subject: [PATCH 0226/1208] Add missing isOnfidoSetupComplete --- .../actions/ReimbursementAccount/resetFreePlanBankAccount.ts | 1 + src/types/form/ReimbursementAccountForm.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.ts b/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.ts index b3effa220e59..3d529ce54cd6 100644 --- a/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.ts +++ b/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.ts @@ -96,6 +96,7 @@ function resetFreePlanBankAccount(bankAccountID: number, session: OnyxEntry Date: Mon, 26 Feb 2024 18:54:01 +0700 Subject: [PATCH 0227/1208] fix: telephoto camera on safari 17+ --- .../request/step/IOURequestStepScan/index.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.js b/src/pages/iou/request/step/IOURequestStepScan/index.js index 2f7ca1519144..09eb85348ef7 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.js @@ -90,14 +90,24 @@ function IOURequestStepScan({ return; } - navigator.mediaDevices.getUserMedia({video: true}).then((stream) => { - _.forEach(stream.getTracks(), (videoStream) => videoStream.stop()); - + navigator.mediaDevices.getUserMedia({video: {facingMode: {exact: 'environment'}, zoom: {ideal: 1}}}).then((stream) => { + // Only Safari 17+ supports zoom constraint + if (Browser.isMobileSafari() && stream.getTracks().length > 0) { + const deviceId = _.chain(stream.getTracks()) + .map((track) => track.getSettings()) + .find((setting) => setting.zoom === 1) + .get('deviceId') + .value(); + if (deviceId) { + setVideoConstraints({deviceId}); + return; + } + } + _.forEach(stream.getTracks(), (track) => track.stop()); if (!navigator.mediaDevices.enumerateDevices) { setVideoConstraints({facingMode: {exact: 'environment'}}); return; } - navigator.mediaDevices.enumerateDevices().then((devices) => { const lastBackDeviceId = _.chain(devices) .filter((item) => item.kind === 'videoinput') From f7ec94a46916c897df9c29ff34151db2a4d5f45a Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 26 Feb 2024 19:01:09 +0700 Subject: [PATCH 0228/1208] change function name --- src/pages/home/ReportScreen.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 9c3d31fda9e7..157be9118c6c 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -145,7 +145,7 @@ function getReportID(route) { * @param {Object} report * @returns {Boolean} */ -function isDeletedReport(report) { +function isEmpty(report) { return _.every(report, _.isUndefined); } @@ -433,7 +433,7 @@ function ReportScreen({ !onyxReportID && prevReport.statusNum === CONST.REPORT.STATUS_NUM.OPEN && (report.statusNum === CONST.REPORT.STATUS_NUM.CLOSED || (!report.statusNum && !prevReport.parentReportID && prevReport.chatType === CONST.REPORT.CHAT_TYPE.POLICY_ROOM))) || - ((ReportUtils.isMoneyRequest(prevReport) || ReportUtils.isMoneyRequestReport(prevReport) || ReportUtils.isPolicyExpenseChat(prevReport)) && isDeletedReport(report)) + ((ReportUtils.isMoneyRequest(prevReport) || ReportUtils.isMoneyRequestReport(prevReport) || ReportUtils.isPolicyExpenseChat(prevReport)) && isEmpty(report)) ) { Navigation.dismissModal(); if (Navigation.getTopmostReportId() === prevOnyxReportID) { From 10e34a68998ccc74ecba3f7b981afa8231bdf947 Mon Sep 17 00:00:00 2001 From: Carlos Barros <765936+barros001@users.noreply.github.com> Date: Mon, 26 Feb 2024 17:29:53 -0500 Subject: [PATCH 0229/1208] disable submit button when user is self approving but self approval is disabled --- src/components/MoneyReportHeader.tsx | 3 +++ src/components/ReportActionItem/ReportPreview.tsx | 2 ++ src/libs/ReportUtils.ts | 10 ++++++++++ 3 files changed, 15 insertions(+) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 3f551da788f5..2e4e9c080224 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -91,6 +91,7 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money }, [isPaidGroupPolicy, isManager, isDraft, isApproved, isSettled, isOnInstantSubmitPolicy, isOnSubmitAndClosePolicy]); const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton; const shouldShowSubmitButton = isDraft && reimbursableSpend !== 0; + const shouldDisableSubmitButton = !ReportUtils.isAllowedToSubmitDraftExpenseReport(moneyRequestReport); const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE; const shouldShowNextStep = isFromPaidPolicy && !!nextStep?.message?.length; const shouldShowAnyButton = shouldShowSettlementButton || shouldShowApproveButton || shouldShowSubmitButton || shouldShowNextStep; @@ -157,6 +158,7 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money text={translate('common.submit')} style={[styles.mnw120, styles.pv2, styles.pr0]} onPress={() => IOU.submitReport(moneyRequestReport)} + isDisabled={shouldDisableSubmitButton} /> )} @@ -188,6 +190,7 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money text={translate('common.submit')} style={[styles.w100, styles.pr0]} onPress={() => IOU.submitReport(moneyRequestReport)} + isDisabled={shouldDisableSubmitButton} /> )} diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 591767234b8b..b984972ace7b 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -156,6 +156,7 @@ function ReportPreview({ }); const shouldShowSubmitButton = isDraftExpenseReport && reimbursableSpend !== 0; + const shouldDisableSubmitButton = !ReportUtils.isAllowedToSubmitDraftExpenseReport(iouReport); // The submit button should be success green colour only if the user is submitter and the policy does not have Scheduled Submit turned on const isWaitingForSubmissionFromCurrentUser = useMemo( @@ -331,6 +332,7 @@ function ReportPreview({ text={translate('common.submit')} style={styles.mt3} onPress={() => iouReport && IOU.submitReport(iouReport)} + isDisabled={shouldDisableSubmitButton} /> )} diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 8813501e2b3f..69c903bc74d1 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5011,6 +5011,15 @@ function canBeAutoReimbursed(report: OnyxEntry, policy: OnyxEntry): boolean { + const policy = getPolicy(report?.policyID); + const {submitsTo, isPreventSelfApprovalEnabled, preventSelfApprovalEnabled} = policy; + + const isSelfApproval = currentUserAccountID === submitsTo; + + return !((isPreventSelfApprovalEnabled ?? preventSelfApprovalEnabled) && isSelfApproval); +} + export { getReportParticipantsTitle, isReportMessageAttachment, @@ -5211,6 +5220,7 @@ export { canEditRoomVisibility, canEditPolicyDescription, getPolicyDescriptionText, + isAllowedToSubmitDraftExpenseReport, }; export type { From 1bd040a987d028eab44cf597ae92856210ab25d9 Mon Sep 17 00:00:00 2001 From: Carlos Barros <765936+barros001@users.noreply.github.com> Date: Mon, 26 Feb 2024 17:43:08 -0500 Subject: [PATCH 0230/1208] do not show pay buttons if report is not yet approved --- src/components/MoneyReportHeader.tsx | 16 ++++++++++++---- .../ReportActionItem/ReportPreview.tsx | 16 ++++++++++++---- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 2e4e9c080224..9cedc09bbd26 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -76,10 +76,6 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money const isOnInstantSubmitPolicy = PolicyUtils.isInstantSubmitEnabled(policy); const isOnSubmitAndClosePolicy = PolicyUtils.isSubmitAndClose(policy); - const shouldShowPayButton = useMemo( - () => isPayer && !isDraft && !isSettled && !moneyRequestReport.isWaitingOnBankAccount && reimbursableSpend !== 0 && !ReportUtils.isArchivedRoom(chatReport) && !isAutoReimbursable, - [isPayer, isDraft, isSettled, moneyRequestReport, reimbursableSpend, chatReport, isAutoReimbursable], - ); const shouldShowApproveButton = useMemo(() => { if (!isPaidGroupPolicy) { return false; @@ -89,6 +85,18 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money } return isManager && !isDraft && !isApproved && !isSettled; }, [isPaidGroupPolicy, isManager, isDraft, isApproved, isSettled, isOnInstantSubmitPolicy, isOnSubmitAndClosePolicy]); + const shouldShowPayButton = useMemo( + () => + isPayer && + !isDraft && + !isSettled && + !shouldShowApproveButton && + !moneyRequestReport.isWaitingOnBankAccount && + reimbursableSpend !== 0 && + !ReportUtils.isArchivedRoom(chatReport) && + !isAutoReimbursable, + [isPayer, isDraft, isSettled, moneyRequestReport, reimbursableSpend, chatReport, isAutoReimbursable, shouldShowApproveButton], + ); const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton; const shouldShowSubmitButton = isDraft && reimbursableSpend !== 0; const shouldDisableSubmitButton = !ReportUtils.isAllowedToSubmitDraftExpenseReport(moneyRequestReport); diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index b984972ace7b..5a4bde217b1d 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -217,10 +217,6 @@ function ReportPreview({ : isPolicyAdmin || (isMoneyRequestReport && isCurrentUserManager); const isOnInstantSubmitPolicy = PolicyUtils.isInstantSubmitEnabled(policy); const isOnSubmitAndClosePolicy = PolicyUtils.isSubmitAndClose(policy); - const shouldShowPayButton = useMemo( - () => isPayer && !isDraftExpenseReport && !iouSettled && !iouReport?.isWaitingOnBankAccount && reimbursableSpend !== 0 && !iouCanceled && !isAutoReimbursable, - [isPayer, isDraftExpenseReport, iouSettled, reimbursableSpend, iouCanceled, isAutoReimbursable, iouReport], - ); const shouldShowApproveButton = useMemo(() => { if (!isPaidGroupPolicy) { return false; @@ -230,6 +226,18 @@ function ReportPreview({ } return isCurrentUserManager && !isDraftExpenseReport && !isApproved && !iouSettled; }, [isPaidGroupPolicy, isCurrentUserManager, isDraftExpenseReport, isApproved, isOnInstantSubmitPolicy, isOnSubmitAndClosePolicy, iouSettled]); + const shouldShowPayButton = useMemo( + () => + isPayer && + !isDraftExpenseReport && + !iouSettled && + !shouldShowApproveButton && + !iouReport?.isWaitingOnBankAccount && + reimbursableSpend !== 0 && + !iouCanceled && + !isAutoReimbursable, + [isPayer, isDraftExpenseReport, iouSettled, reimbursableSpend, iouCanceled, isAutoReimbursable, iouReport, shouldShowApproveButton], + ); const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton; /* From b1290026a2be9945632cb8fc56e9c438bc9776ea Mon Sep 17 00:00:00 2001 From: Carlos Barros <765936+barros001@users.noreply.github.com> Date: Mon, 26 Feb 2024 18:01:18 -0500 Subject: [PATCH 0231/1208] disable approve button if user is self approving but self approval is disabled --- src/components/MoneyReportHeader.tsx | 7 ++++--- src/components/ReportActionItem/ReportPreview.tsx | 5 +++-- src/libs/ReportUtils.ts | 11 +++++++++++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 9cedc09bbd26..3c84da5cba57 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -99,7 +99,8 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money ); const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton; const shouldShowSubmitButton = isDraft && reimbursableSpend !== 0; - const shouldDisableSubmitButton = !ReportUtils.isAllowedToSubmitDraftExpenseReport(moneyRequestReport); + const shouldDisableSubmitButton = shouldShowSubmitButton && !ReportUtils.isAllowedToSubmitDraftExpenseReport(moneyRequestReport); + const shouldDisableSettlementButton = shouldShowSettlementButton && shouldShowApproveButton && !ReportUtils.isAllowedToApproveExpenseReport(moneyRequestReport); const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE; const shouldShowNextStep = isFromPaidPolicy && !!nextStep?.message?.length; const shouldShowAnyButton = shouldShowSettlementButton || shouldShowApproveButton || shouldShowSubmitButton || shouldShowNextStep; @@ -154,7 +155,7 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money shouldShowApproveButton={shouldShowApproveButton} style={[styles.pv2]} formattedAmount={formattedAmount} - isDisabled={!canAllowSettlement} + isDisabled={!canAllowSettlement || shouldDisableSettlementButton} /> )} @@ -186,7 +187,7 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money shouldHidePaymentOptions={!shouldShowPayButton} shouldShowApproveButton={shouldShowApproveButton} formattedAmount={formattedAmount} - isDisabled={!canAllowSettlement} + isDisabled={!canAllowSettlement || shouldDisableSettlementButton} /> )} diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 5a4bde217b1d..4bf7c1af03b9 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -156,7 +156,7 @@ function ReportPreview({ }); const shouldShowSubmitButton = isDraftExpenseReport && reimbursableSpend !== 0; - const shouldDisableSubmitButton = !ReportUtils.isAllowedToSubmitDraftExpenseReport(iouReport); + const shouldDisableSubmitButton = shouldShowSubmitButton && !ReportUtils.isAllowedToSubmitDraftExpenseReport(iouReport); // The submit button should be success green colour only if the user is submitter and the policy does not have Scheduled Submit turned on const isWaitingForSubmissionFromCurrentUser = useMemo( @@ -239,6 +239,7 @@ function ReportPreview({ [isPayer, isDraftExpenseReport, iouSettled, reimbursableSpend, iouCanceled, isAutoReimbursable, iouReport, shouldShowApproveButton], ); const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton; + const shouldDisableSettlementButton = shouldShowSettlementButton && shouldShowApproveButton && !ReportUtils.isAllowedToApproveExpenseReport(iouReport); /* Show subtitle if at least one of the money requests is not being smart scanned, and either: @@ -330,7 +331,7 @@ function ReportPreview({ horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, }} - isDisabled={!canAllowSettlement} + isDisabled={!canAllowSettlement || shouldDisableSettlementButton} /> )} {shouldShowSubmitButton && ( diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 69c903bc74d1..b082ec8789e5 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5020,6 +5020,16 @@ function isAllowedToSubmitDraftExpenseReport(report: OnyxEntry): boolean return !((isPreventSelfApprovalEnabled ?? preventSelfApprovalEnabled) && isSelfApproval); } +function isAllowedToApproveExpenseReport(report: OnyxEntry): boolean { + const policy = getPolicy(report?.policyID); + const {approver, submitsTo, isPreventSelfApprovalEnabled, preventSelfApprovalEnabled} = policy; + + // TODO: check if we should be using "approver" here instead of the same "submitsTo". "approver" is "undefined" currently. + const isSelfApproval = currentUserAccountID === (approver ?? submitsTo); + + return !((isPreventSelfApprovalEnabled ?? preventSelfApprovalEnabled) && isSelfApproval); +} + export { getReportParticipantsTitle, isReportMessageAttachment, @@ -5221,6 +5231,7 @@ export { canEditPolicyDescription, getPolicyDescriptionText, isAllowedToSubmitDraftExpenseReport, + isAllowedToApproveExpenseReport, }; export type { From 643c7e150cd9e7f42f559c5df0e384182c7a0c0d Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 27 Feb 2024 12:44:01 +0500 Subject: [PATCH 0232/1208] perf: return early if displayName exists --- src/libs/PersonalDetailsUtils.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/libs/PersonalDetailsUtils.ts b/src/libs/PersonalDetailsUtils.ts index 7cd6ac5bdedd..912e9aba36aa 100644 --- a/src/libs/PersonalDetailsUtils.ts +++ b/src/libs/PersonalDetailsUtils.ts @@ -25,14 +25,22 @@ Onyx.connect({ }); const hiddenText = Localize.translateLocal('common.hidden'); -const substringStartIndex = 8; +const substringStartIndex = CONST.MERGED_ACCOUNT_PREFIX.length; function getDisplayNameOrDefault(passedPersonalDetails?: Partial | null, defaultValue = '', shouldFallbackToHidden = true): string { let displayName = passedPersonalDetails?.displayName ?? ''; if (displayName.startsWith(CONST.MERGED_ACCOUNT_PREFIX)) { displayName = displayName.substring(substringStartIndex); } + + /** + * If displayName exists, return it early so we don't have to allocate + * memory for the fallback string. + */ + if (displayName) { + return displayName; + } const fallbackValue = shouldFallbackToHidden ? hiddenText : ''; - return displayName || defaultValue || fallbackValue; + return defaultValue || fallbackValue; } /** From c683db7392b2ad06320785a78661bf56c5c2bc4e Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 27 Feb 2024 13:10:35 +0500 Subject: [PATCH 0233/1208] test: fix failing test for formatPhoneNumber --- src/libs/LocalePhoneNumber.ts | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/libs/LocalePhoneNumber.ts b/src/libs/LocalePhoneNumber.ts index 4fc13322b0cf..ec704ec4d44b 100644 --- a/src/libs/LocalePhoneNumber.ts +++ b/src/libs/LocalePhoneNumber.ts @@ -10,6 +10,28 @@ Onyx.connect({ callback: (val) => (countryCodeByIP = val ?? 1), }); +/** + * Checks whether the given string contains any numbers. + * It uses indexOf instead of regex and includes for performance reasons. + * + * @param text + * @returns boolean + */ +function containsNumbers(text: string) { + return ( + text.indexOf('0') !== -1 || + text.indexOf('1') !== -1 || + text.indexOf('2') !== -1 || + text.indexOf('3') !== -1 || + text.indexOf('4') !== -1 || + text.indexOf('5') !== -1 || + text.indexOf('6') !== -1 || + text.indexOf('7') !== -1 || + text.indexOf('8') !== -1 || + text.indexOf('9') !== -1 + ); +} + /** * Returns a locally converted phone number for numbers from the same region * and an internationally converted phone number with the country code for numbers from other regions @@ -20,7 +42,7 @@ function formatPhoneNumber(number: string): string { } // do not parse the string, if it's not a phone number - if (number.indexOf(CONST.SMS_DOMAIN_PATTERN) === -1) { + if (number.indexOf(CONST.SMS_DOMAIN_PATTERN) === -1 && !containsNumbers(number)) { return number; } const numberWithoutSMSDomain = Str.removeSMSDomain(number); From 454a9b258fee05e310d2378b5bfd2cdaf915b251 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 27 Feb 2024 15:24:10 +0500 Subject: [PATCH 0234/1208] refactor: move translations to utils --- src/libs/CommonTranslationUtils.ts | 49 ++++++++++++++++++++++++++++++ src/libs/PersonalDetailsUtils.ts | 11 +++++-- src/libs/ReportUtils.ts | 21 +++++-------- 3 files changed, 66 insertions(+), 15 deletions(-) create mode 100644 src/libs/CommonTranslationUtils.ts diff --git a/src/libs/CommonTranslationUtils.ts b/src/libs/CommonTranslationUtils.ts new file mode 100644 index 000000000000..80a06e51ce9f --- /dev/null +++ b/src/libs/CommonTranslationUtils.ts @@ -0,0 +1,49 @@ +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; +import * as Localize from './Localize'; + +/** + * This file contains common translations that are used in multiple places in the app. + * This is done to avoid duplicate translations and to keep the translations consistent. + * This also allows us to not repeatedly translate the same string which may happen due + * to translations being done for eg, in a loop. + * + * This was identified as part of a performance audit. + * details: https://github.com/Expensify/App/issues/35234#issuecomment-1926911643 + */ + +let deletedTaskText = ''; +let deletedMessageText = ''; +let attachmentText = ''; +let archivedText = ''; +let hiddenText = ''; +let unavailableWorkspaceText = ''; + +function isTranslationAvailable() { + return deletedTaskText && deletedMessageText && attachmentText && archivedText && hiddenText && unavailableWorkspaceText; +} + +Onyx.connect({ + key: ONYXKEYS.NVP_PREFERRED_LOCALE, + callback: (val) => { + if (!val && isTranslationAvailable()) { + return; + } + + deletedTaskText = Localize.translateLocal('parentReportAction.deletedTask'); + deletedMessageText = Localize.translateLocal('parentReportAction.deletedMessage'); + attachmentText = Localize.translateLocal('common.attachment'); + archivedText = Localize.translateLocal('common.archived'); + hiddenText = Localize.translateLocal('common.hidden'); + unavailableWorkspaceText = Localize.translateLocal('workspace.common.unavailable'); + }, +}); + +export default { + deletedTaskText: () => deletedTaskText, + deletedMessageText: () => deletedMessageText, + attachmentText: () => attachmentText, + archivedText: () => archivedText, + hiddenText: () => hiddenText, + unavailableWorkspaceText: () => unavailableWorkspaceText, +}; diff --git a/src/libs/PersonalDetailsUtils.ts b/src/libs/PersonalDetailsUtils.ts index 912e9aba36aa..7a9b3fe96598 100644 --- a/src/libs/PersonalDetailsUtils.ts +++ b/src/libs/PersonalDetailsUtils.ts @@ -5,6 +5,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PersonalDetails, PersonalDetailsList, PrivatePersonalDetails} from '@src/types/onyx'; import type {OnyxData} from '@src/types/onyx/Request'; +import CommonTranslationUtils from './CommonTranslationUtils'; import * as LocalePhoneNumber from './LocalePhoneNumber'; import * as Localize from './Localize'; import * as UserUtils from './UserUtils'; @@ -24,10 +25,16 @@ Onyx.connect({ }, }); -const hiddenText = Localize.translateLocal('common.hidden'); +/** + * Index for the substring method to remove the merged account prefix. + */ const substringStartIndex = CONST.MERGED_ACCOUNT_PREFIX.length; + function getDisplayNameOrDefault(passedPersonalDetails?: Partial | null, defaultValue = '', shouldFallbackToHidden = true): string { let displayName = passedPersonalDetails?.displayName ?? ''; + /** + * If the displayName starts with the merged account prefix, remove it. + */ if (displayName.startsWith(CONST.MERGED_ACCOUNT_PREFIX)) { displayName = displayName.substring(substringStartIndex); } @@ -39,7 +46,7 @@ function getDisplayNameOrDefault(passedPersonalDetails?: Partial, policies: OnyxCollection | undefined | EmptyObject, returnEmptyIfNotFound = false, policy: OnyxEntry | undefined = undefined): string { - const noPolicyFound = returnEmptyIfNotFound ? '' : unavailableWorkspaceText; + const noPolicyFound = returnEmptyIfNotFound ? '' : CommonTranslationUtils.unavailableWorkspaceText(); if (isEmptyObject(report)) { return noPolicyFound; } if ((!allPolicies || Object.keys(allPolicies).length === 0) && !report?.policyName) { - return unavailableWorkspaceText; + return CommonTranslationUtils.unavailableWorkspaceText(); } const finalPolicy = policy ?? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; @@ -1627,7 +1627,6 @@ function getPersonalDetailsForAccountID(accountID: number): Partial, policy: OnyxEntry = nu } if (parentReportAction?.message?.[0]?.isDeletedParentAction) { - return deletedMessageText; + return CommonTranslationUtils.deletedMessageText(); } const isAttachment = ReportActionsUtils.isReportActionAttachment(!isEmptyObject(parentReportAction) ? parentReportAction : null); const parentReportActionMessage = (parentReportAction?.message?.[0]?.text ?? '').replace(/(\r\n|\n|\r)/gm, ' '); if (isAttachment && parentReportActionMessage) { - return `[${attachmentText}]`; + return `[${CommonTranslationUtils.attachmentText()}]`; } if ( parentReportAction?.message?.[0]?.moderationDecision?.decision === CONST.MODERATION.MODERATOR_DECISION_PENDING_HIDE || @@ -2536,7 +2531,7 @@ function getReportName(report: OnyxEntry, policy: OnyxEntry = nu } if (isTaskReport(report) && isCanceledTaskReport(report, parentReportAction)) { - return deletedTaskText; + return CommonTranslationUtils.deletedTaskText(); } if (isChatRoom(report) || isTaskReport(report)) { @@ -2552,7 +2547,7 @@ function getReportName(report: OnyxEntry, policy: OnyxEntry = nu } if (isArchivedRoom(report)) { - formattedName += ` (${archivedText})`; + formattedName += ` (${CommonTranslationUtils.archivedText()})`; } if (formattedName) { From eb8f26ccb2fabb54fbb7f6c054e13dd53127c9ee Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 27 Feb 2024 15:24:28 +0500 Subject: [PATCH 0235/1208] fix: linting --- src/libs/LocalePhoneNumber.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/LocalePhoneNumber.ts b/src/libs/LocalePhoneNumber.ts index ec704ec4d44b..798e0a50c43e 100644 --- a/src/libs/LocalePhoneNumber.ts +++ b/src/libs/LocalePhoneNumber.ts @@ -13,7 +13,7 @@ Onyx.connect({ /** * Checks whether the given string contains any numbers. * It uses indexOf instead of regex and includes for performance reasons. - * + * * @param text * @returns boolean */ From 58d5b933dbe07128cf33ee4d161b4c78318bc00b Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 27 Feb 2024 17:54:29 +0700 Subject: [PATCH 0236/1208] 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 fc7670078ef0da6c4865c6dc37b16552c262a727 Mon Sep 17 00:00:00 2001 From: Carlos Barros <765936+barros001@users.noreply.github.com> Date: Tue, 27 Feb 2024 06:49:57 -0500 Subject: [PATCH 0237/1208] build optimistic next step message saying you are not allowed to approve your own reports --- src/libs/NextStepUtils.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index f03c34b1696e..9a931af92518 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -244,6 +244,32 @@ function buildNextStep( ]; } + // Prevented self approval + if ((isPreventSelfApprovalEnabled ?? preventSelfApprovalEnabled) && isSelfApproval) { + optimisticNextStep.message = [ + { + text: "Oops! Looks like you're ", + }, + { + text: isManager ? 'reviewing' : 'approving', + }, + { + text: ' your own report. ', + type: 'strong', + }, + { + text: 'Approving your own reports is ', + }, + { + text: 'forbidden', + type: 'strong', + }, + { + text: ' by your policy.', + }, + ]; + } + break; } From 57370376e524abafc3d6e9e0c97bb649df40ffdf Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 27 Feb 2024 14:01:08 +0100 Subject: [PATCH 0238/1208] fix: resolve comments --- src/components/LHNOptionsList/types.ts | 2 +- src/libs/ReportUtils.ts | 9 +-- src/libs/SidebarUtils.ts | 28 +++++---- src/libs/actions/Policy.ts | 2 +- .../home/sidebar/AvatarWithOptionalStatus.tsx | 4 +- src/pages/home/sidebar/SidebarLinks.tsx | 27 +++++---- src/pages/home/sidebar/SidebarLinksData.tsx | 59 +++++-------------- .../FloatingActionButtonAndPopover.tsx | 19 +++--- .../SignInOrAvatarWithOptionalStatus.tsx | 1 + 9 files changed, 67 insertions(+), 84 deletions(-) diff --git a/src/components/LHNOptionsList/types.ts b/src/components/LHNOptionsList/types.ts index f3d6bde9d41c..58bea97f04c9 100644 --- a/src/components/LHNOptionsList/types.ts +++ b/src/components/LHNOptionsList/types.ts @@ -45,7 +45,7 @@ type CustomLHNOptionsListProps = { contentContainerStyles?: StyleProp; /** Sections for the section list */ - data: string[] | null; + data: string[]; /** Callback to fire when a row is selected */ onSelectRow?: (optionItem: OptionData, popoverAnchor: RefObject) => void; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index b19cd837a033..b23da56bcd8f 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -704,8 +704,8 @@ function isDraftExpenseReport(report: OnyxEntry | EmptyObject): boolean /** * Checks if the supplied report has a common policy member with the array passed in params. */ -function hasParticipantInArray(report: Report, policyMemberAccountIDs: number[]) { - if (!report.participantAccountIDs) { +function hasParticipantInArray(report: OnyxEntry, policyMemberAccountIDs: number[]) { + if (!report?.participantAccountIDs) { return false; } @@ -921,9 +921,10 @@ function isConciergeChatReport(report: OnyxEntry): boolean { * 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. */ -function doesReportBelongToWorkspace(report: Report, policyMemberAccountIDs: number[], policyID?: string) { +function doesReportBelongToWorkspace(report: OnyxEntry, policyMemberAccountIDs: number[], policyID?: string) { return ( - isConciergeChatReport(report) || (report.policyID === CONST.POLICY.ID_FAKE || !report.policyID ? hasParticipantInArray(report, policyMemberAccountIDs) : report.policyID === policyID) + isConciergeChatReport(report) || + (report?.policyID === CONST.POLICY.ID_FAKE || !report?.policyID ? hasParticipantInArray(report, policyMemberAccountIDs) : report?.policyID === policyID) ); } diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 44a17a6d50a0..463c796fb5ce 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -64,9 +64,9 @@ function compareStringDates(a: string, b: string): 0 | 1 | -1 { */ function getOrderedReportIDs( currentReportId: string | null, - allReports: OnyxEntry>, + allReports: OnyxCollection, betas: OnyxEntry, - policies: OnyxEntry>, + policies: OnyxCollection, priorityMode: OnyxEntry>, allReportActions: OnyxCollection, transactionViolations: OnyxCollection, @@ -83,7 +83,7 @@ function getOrderedReportIDs( const parentReportActions = allReportActions?.[parentReportActionsKey]; const parentReportAction = parentReportActions?.find((action) => action && report && action?.reportActionID === report?.parentReportActionID); const doesReportHaveViolations = - betas?.includes(CONST.BETAS.VIOLATIONS) && !!parentReportAction && ReportUtils.doesTransactionThreadHaveViolations(report, transactionViolations, parentReportAction); + !!betas?.includes(CONST.BETAS.VIOLATIONS) && !!parentReportAction && ReportUtils.doesTransactionThreadHaveViolations(report, transactionViolations, parentReportAction); return ReportUtils.shouldReportBeInOptionList({ report, currentReportId: currentReportId ?? '', @@ -91,7 +91,7 @@ function getOrderedReportIDs( betas, policies, excludeEmptyChats: true, - doesReportHaveViolations: !!doesReportHaveViolations, + doesReportHaveViolations, }); }); @@ -114,7 +114,7 @@ function getOrderedReportIDs( // - Sorted by reportDisplayName in GSD (focus) view mode const pinnedAndGBRReports: Report[] = []; const draftReports: Report[] = []; - const nonArchivedReports: Report[] = []; + const nonArchivedReports: Array> = []; const archivedReports: Report[] = []; if (currentPolicyID || policyMemberAccountIDs.length > 0) { @@ -125,16 +125,18 @@ function getOrderedReportIDs( // Normally, the spread operator would be used here to clone the report and prevent the need to reassign the params. // However, this code needs to be very performant to handle thousands of reports, so in the interest of speed, we're just going to disable this lint rule and add // the reportDisplayName property to the report object directly. - // eslint-disable-next-line no-param-reassign - report.displayName = ReportUtils.getReportName(report); + if (report) { + // eslint-disable-next-line no-param-reassign + report.displayName = ReportUtils.getReportName(report); + } - const isPinned = report.isPinned ?? false; - const reportAction = ReportActionsUtils.getReportAction(report.parentReportID ?? '', report.parentReportActionID ?? ''); - if (isPinned || ReportUtils.requiresAttentionFromCurrentUser(report, reportAction)) { + const isPinned = report?.isPinned ?? false; + const reportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '', report?.parentReportActionID ?? ''); + if ((isPinned || ReportUtils.requiresAttentionFromCurrentUser(report, reportAction)) && report) { pinnedAndGBRReports.push(report); - } else if (report.hasDraft) { + } else if (report?.hasDraft) { draftReports.push(report); - } else if (ReportUtils.isArchivedRoom(report)) { + } else if (ReportUtils.isArchivedRoom(report) && report) { archivedReports.push(report); } else { nonArchivedReports.push(report); @@ -160,7 +162,7 @@ function getOrderedReportIDs( // Now that we have all the reports grouped and sorted, they must be flattened into an array and only return the reportID. // The order the arrays are concatenated in matters and will determine the order that the groups are displayed in the sidebar. - const LHNReports = [...pinnedAndGBRReports, ...draftReports, ...nonArchivedReports, ...archivedReports].map((report) => report.reportID); + const LHNReports = [...pinnedAndGBRReports, ...draftReports, ...nonArchivedReports, ...archivedReports].map((report) => report?.reportID ?? ''); return LHNReports; } diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index 850d21e4858e..0296b1a8ea6f 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -84,7 +84,7 @@ type OptimisticCustomUnits = { outputCurrency: string; }; -type PoliciesRecord = Record; +type PoliciesRecord = Record>; type NewCustomUnit = { customUnitID: string; diff --git a/src/pages/home/sidebar/AvatarWithOptionalStatus.tsx b/src/pages/home/sidebar/AvatarWithOptionalStatus.tsx index 5597d46c29bc..0e1dedaf3651 100644 --- a/src/pages/home/sidebar/AvatarWithOptionalStatus.tsx +++ b/src/pages/home/sidebar/AvatarWithOptionalStatus.tsx @@ -12,10 +12,10 @@ import PressableAvatarWithIndicator from './PressableAvatarWithIndicator'; type AvatarWithOptionalStatusProps = { /** Whether the create menu is open or not */ - isCreateMenuOpen: boolean; + isCreateMenuOpen?: boolean; /** Emoji status */ - emojiStatus: string; + emojiStatus?: string; }; function AvatarWithOptionalStatus({emojiStatus = '', isCreateMenuOpen = false}: AvatarWithOptionalStatusProps) { diff --git a/src/pages/home/sidebar/SidebarLinks.tsx b/src/pages/home/sidebar/SidebarLinks.tsx index b0dd6f5ac067..5e0e37b1a164 100644 --- a/src/pages/home/sidebar/SidebarLinks.tsx +++ b/src/pages/home/sidebar/SidebarLinks.tsx @@ -26,19 +26,30 @@ type SidebarLinksOnyxProps = { }; type SidebarLinksProps = SidebarLinksOnyxProps & { + /** Toggles the navigation menu open and closed */ onLinkClick: () => void; - insets: EdgeInsets | undefined; - optionListItems: string[] | null; + + /** Safe area insets required for mobile devices margins */ + insets: EdgeInsets; + + /** List of options to display */ + optionListItems: string[]; + + /** Wheather the reports are loading. When false it means they are ready to be used. */ isLoading: OnyxEntry; + + /** The chat priority mode */ priorityMode?: OnyxEntry>; + + /** Method to change currently active report */ isActiveReport: (reportID: string) => boolean; - isCreateMenuOpen?: boolean; + /** ID of currently active workspace */ // eslint-disable-next-line react/no-unused-prop-types -- its used in withOnyx activeWorkspaceID: string | undefined; }; -function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priorityMode = CONST.PRIORITY_MODE.DEFAULT, isActiveReport, isCreateMenuOpen, activePolicy}: SidebarLinksProps) { +function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priorityMode = CONST.PRIORITY_MODE.DEFAULT, isActiveReport, activePolicy}: SidebarLinksProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const modal = useRef({}); @@ -105,17 +116,13 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority // or when continuously clicking different LHNs, only apply to small screen // since getTopmostReportId always returns on other devices const reportActionID = Navigation.getTopmostReportActionId(); - if ( - !!isCreateMenuOpen || - (option.reportID === Navigation.getTopmostReportId() && !reportActionID) || - (isSmallScreenWidth && isActiveReport(option.reportID) && !reportActionID) - ) { + if ((option.reportID === Navigation.getTopmostReportId() && !reportActionID) || (isSmallScreenWidth && isActiveReport(option.reportID) && !reportActionID)) { return; } Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(option.reportID)); onLinkClick(); }, - [isCreateMenuOpen, isSmallScreenWidth, isActiveReport, onLinkClick], + [isSmallScreenWidth, isActiveReport, onLinkClick], ); const viewMode = priorityMode === CONST.PRIORITY_MODE.GSD ? CONST.OPTION_MODE.COMPACT : CONST.OPTION_MODE.DEFAULT; diff --git a/src/pages/home/sidebar/SidebarLinksData.tsx b/src/pages/home/sidebar/SidebarLinksData.tsx index ee99b0c9cf59..6e71e30ac1b6 100644 --- a/src/pages/home/sidebar/SidebarLinksData.tsx +++ b/src/pages/home/sidebar/SidebarLinksData.tsx @@ -24,43 +24,13 @@ import type * as OnyxTypes from '@src/types/onyx'; import type {Message} from '@src/types/onyx/ReportAction'; import SidebarLinks from './SidebarLinks'; -type PickedReport = Pick< - OnyxTypes.Report, - | 'reportID' - | 'participantAccountIDs' - | 'hasDraft' - | 'isPinned' - | 'isHidden' - | 'notificationPreference' - | 'errorFields' - | 'lastMessageText' - | 'lastVisibleActionCreated' - | 'iouReportID' - | 'total' - | 'nonReimbursableTotal' - | 'hasOutstandingChildRequest' - | 'isWaitingOnBankAccount' - | 'statusNum' - | 'stateNum' - | 'chatType' - | 'type' - | 'policyID' - | 'visibility' - | 'lastReadTime' - | 'reportName' - | 'policyName' - | 'oldPolicyName' - | 'ownerAccountID' - | 'currency' - | 'managerID' - | 'parentReportActionID' - | 'parentReportID' - | 'isDeletedParentAction' ->; +type ChatReportSelector = ReturnType & {isUnreadWithMention: boolean}; +type PolicySelector = ReturnType; +type ReportActionsSelector = ReturnType; type SidebarLinksDataOnyxProps = { /** List of reports */ - chatReports: OnyxCollection; + chatReports: OnyxCollection; /** Wheather the reports are loading. When false it means they are ready to be used. */ isLoadingApp: OnyxEntry; @@ -72,10 +42,10 @@ type SidebarLinksDataOnyxProps = { betas: OnyxEntry; /** All report actions for all reports */ - allReportActions: OnyxEntry>>; + allReportActions: OnyxEntry; /** The policies which the user has access to */ - policies: OnyxCollection>; + policies: OnyxCollection; /** All of the transaction violations */ transactionViolations: OnyxCollection; @@ -90,7 +60,7 @@ type SidebarLinksDataProps = CurrentReportIDContextValue & onLinkClick: () => void; /** Safe area insets required for mobile devices margins */ - insets: EdgeInsets | undefined; + insets: EdgeInsets; }; function SidebarLinksData({ @@ -120,7 +90,7 @@ function SidebarLinksData({ const reportIDsRef = useRef(null); const isLoading = isLoadingApp; - const optionListItems: string[] | null = useMemo(() => { + const optionListItems: string[] = useMemo(() => { const reportIDs = SidebarUtils.getOrderedReportIDs( null, chatReports as OnyxEntry>, @@ -133,7 +103,7 @@ function SidebarLinksData({ policyMemberAccountIDs, ); - if (deepEqual(reportIDsRef.current, reportIDs)) { + if (reportIDsRef.current && deepEqual(reportIDsRef.current, reportIDs)) { return reportIDsRef.current; } @@ -170,7 +140,7 @@ function SidebarLinksData({ const currentReportIDRef = useRef(currentReportID); currentReportIDRef.current = currentReportID; - const isActiveReport = useCallback((reportID: string) => currentReportIDRef.current === reportID, []); + const isActiveReport = useCallback((reportID: string): boolean => currentReportIDRef.current === reportID, []); return ( ) => report && { @@ -270,7 +239,8 @@ export default withCurrentReportID( withOnyx({ chatReports: { key: ONYXKEYS.COLLECTION.REPORT, - selector: chatReportSelector as unknown as (report: OnyxEntry) => OnyxCollection, + // This assertion is needed because the selector in withOnyx expects that the return type will be the same as type in ONYXKEYS but in this case it's not, this is a bug in withOnyx but it's impossible to fix it, when useOnyx will be introduce it will be fixed. + selector: chatReportSelector as unknown as (report: OnyxEntry) => OnyxCollection, initialValue: {}, }, isLoadingApp: { @@ -291,7 +261,8 @@ export default withCurrentReportID( }, policies: { key: ONYXKEYS.COLLECTION.POLICY, - selector: policySelector as unknown as (policy: OnyxEntry) => OnyxCollection>, + // This assertion is needed because the selector in withOnyx expects that the return type will be the same as type in ONYXKEYS but in this case it's not, this is a bug in withOnyx but it's impossible to fix it, when useOnyx will be introduce it will be fixed. + selector: policySelector as unknown as (policy: OnyxEntry) => OnyxCollection, initialValue: {}, }, policyMembers: { @@ -303,3 +274,5 @@ export default withCurrentReportID( }, })(SidebarLinksData), ); + +export type {PolicySelector}; diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index 208a9c0a9deb..c080272acbd2 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -2,7 +2,7 @@ import {useIsFocused} from '@react-navigation/native'; import type {ForwardedRef, RefAttributes} from 'react'; import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; import {View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import FloatingActionButton from '@components/FloatingActionButton'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -14,6 +14,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; +import type {PolicySelector} from '@pages/home/sidebar/SidebarLinksData'; import * as App from '@userActions/App'; import * as IOU from '@userActions/IOU'; import * as Policy from '@userActions/Policy'; @@ -25,7 +26,7 @@ import type * as OnyxTypes from '@src/types/onyx'; type FloatingActionButtonAndPopoverOnyxProps = { /** The list of policies the user has access to. */ - allPolicies: OnyxEntry>>; + allPolicies: OnyxCollection; /** Wheater app is in loading state */ isLoading: OnyxEntry; @@ -36,7 +37,7 @@ type FloatingActionButtonAndPopoverProps = FloatingActionButtonAndPopoverOnyxPro onShowCreateMenu?: () => void; /* Callback function before the menu is hidden */ - onHideCreateMenu: () => void; + onHideCreateMenu?: () => void; }; type FloatingActionButtonAndPopoverRef = { @@ -48,7 +49,7 @@ type FloatingActionButtonAndPopoverRef = { * FAB that can open or close the menu. */ function FloatingActionButtonAndPopover( - {onHideCreateMenu = () => {}, onShowCreateMenu = () => {}, isLoading, allPolicies}: FloatingActionButtonAndPopoverProps, + {onHideCreateMenu, onShowCreateMenu, isLoading, allPolicies}: FloatingActionButtonAndPopoverProps, ref: ForwardedRef, ) { const styles = useThemeStyles(); @@ -80,7 +81,7 @@ function FloatingActionButtonAndPopover( return; } setIsCreateMenuActive(true); - onShowCreateMenu(); + onShowCreateMenu?.(); }, // eslint-disable-next-line react-hooks/exhaustive-deps [isFocused, isSmallScreenWidth], @@ -97,7 +98,7 @@ function FloatingActionButtonAndPopover( return; } setIsCreateMenuActive(false); - onHideCreateMenu(); + onHideCreateMenu?.(); }, // eslint-disable-next-line react-hooks/exhaustive-deps [isCreateMenuActive], @@ -170,7 +171,7 @@ function FloatingActionButtonAndPopover( text: translate('sidebarScreen.saveTheWorld'), onSelected: () => interceptAnonymousUser(() => Navigation.navigate(ROUTES.TEACHERS_UNITE)), }, - ...(!isLoading && !Policy.hasActiveFreePolicy(allPolicies as Record) + ...(!isLoading && !Policy.hasActiveFreePolicy(allPolicies as OnyxEntry>) ? [ { displayInDefaultIconColor: true, @@ -214,9 +215,7 @@ const policySelector = (policy: OnyxEntry) => export default withOnyx, FloatingActionButtonAndPopoverOnyxProps>({ allPolicies: { key: ONYXKEYS.COLLECTION.POLICY, - selector: policySelector as unknown as ( - policy: OnyxEntry, - ) => OnyxEntry>>, + selector: policySelector as unknown as (policy: OnyxEntry) => OnyxCollection, }, isLoading: { key: ONYXKEYS.IS_LOADING_APP, diff --git a/src/pages/home/sidebar/SignInOrAvatarWithOptionalStatus.tsx b/src/pages/home/sidebar/SignInOrAvatarWithOptionalStatus.tsx index 2a9356d78232..e8c90bb6eb08 100644 --- a/src/pages/home/sidebar/SignInOrAvatarWithOptionalStatus.tsx +++ b/src/pages/home/sidebar/SignInOrAvatarWithOptionalStatus.tsx @@ -6,6 +6,7 @@ import PressableAvatarWithIndicator from './PressableAvatarWithIndicator'; import SignInButton from './SignInButton'; type SignInOrAvatarWithOptionalStatusProps = { + /** Whether the create menu is open or not */ isCreateMenuOpen?: boolean; }; From 9054944828891069752adb31d42745713f29dba6 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 27 Feb 2024 18:31:12 +0530 Subject: [PATCH 0239/1208] 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 0240/1208] 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 0241/1208] 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 f011e58cac8812b6fd09c2c03a198c5f6ba0b32c Mon Sep 17 00:00:00 2001 From: Toby Sullivan Date: Tue, 27 Feb 2024 08:12:02 -0800 Subject: [PATCH 0242/1208] Hide description when last message is multi-request preview --- src/libs/OptionsListUtils.ts | 1 + src/libs/ReportUtils.ts | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 3d11795f5452..3e2f89f274a1 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -561,6 +561,7 @@ function getLastMessageTextForReport(report: OnyxEntry, lastActorDetails ReportUtils.isChatReport(report), null, true, + lastReportAction, ); } else if (ReportActionUtils.isReimbursementQueuedAction(lastReportAction)) { lastMessageTextFromReport = ReportUtils.getReimbursementQueuedActionMessage(lastReportAction, report); diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index be1fd977c145..e6775ef59bd3 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2289,6 +2289,7 @@ function getReportPreviewMessage( isPreviewMessageForParentChatReport = false, policy: OnyxEntry = null, isForListPreview = false, + originalReportAction: OnyxEntry | EmptyObject = reportAction, ): string { const reportActionMessage = reportAction?.message?.[0].html ?? ''; @@ -2387,7 +2388,9 @@ function getReportPreviewMessage( if (isEmptyObject(linkedTransaction) && !isEmptyObject(reportAction)) { linkedTransaction = TransactionUtils.getLinkedTransaction(reportAction); } - const comment = !isEmptyObject(linkedTransaction) ? TransactionUtils.getDescription(linkedTransaction) : undefined; + const hideComment = + !isEmptyObject(originalReportAction) && ReportActionsUtils.isReportPreviewAction(originalReportAction) && ReportActionsUtils.getNumberOfMoneyRequests(originalReportAction) !== 1; + const comment = !isEmptyObject(linkedTransaction) && !hideComment ? TransactionUtils.getDescription(linkedTransaction) : undefined; // if we have the amount in the originalMessage and lastActorID, we can use that to display the preview message for the latest request if (amount !== undefined && lastActorID && !isPreviewMessageForParentChatReport) { From f2741741f2ea556ab643f07dbd3e3a8fac2dd853 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 27 Feb 2024 22:09:35 +0530 Subject: [PATCH 0243/1208] 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 0244/1208] 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 a818f52490e9b109c408c5d7349f987fcfc206a1 Mon Sep 17 00:00:00 2001 From: Toby Sullivan Date: Tue, 27 Feb 2024 09:51:03 -0800 Subject: [PATCH 0245/1208] Tidy up conditions for request descriptions --- src/libs/ReportUtils.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 9ecfd2e92beb..4dcbc60dd849 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2395,9 +2395,11 @@ function getReportPreviewMessage( if (isEmptyObject(linkedTransaction) && !isEmptyObject(reportAction)) { linkedTransaction = TransactionUtils.getLinkedTransaction(reportAction); } - const hideComment = - !isEmptyObject(originalReportAction) && ReportActionsUtils.isReportPreviewAction(originalReportAction) && ReportActionsUtils.getNumberOfMoneyRequests(originalReportAction) !== 1; - const comment = !isEmptyObject(linkedTransaction) && !hideComment ? TransactionUtils.getDescription(linkedTransaction) : undefined; + + let comment = !isEmptyObject(linkedTransaction) ? TransactionUtils.getDescription(linkedTransaction) : undefined; + if (!isEmptyObject(originalReportAction) && ReportActionsUtils.isReportPreviewAction(originalReportAction) && ReportActionsUtils.getNumberOfMoneyRequests(originalReportAction) !== 1) { + comment = undefined; + } // if we have the amount in the originalMessage and lastActorID, we can use that to display the preview message for the latest request if (amount !== undefined && lastActorID && !isPreviewMessageForParentChatReport) { From 86c59a1cee4065805e2643d07ba2de07469c9555 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Tue, 27 Feb 2024 20:24:44 +0000 Subject: [PATCH 0246/1208] refactor(typescript): extract type from constant --- src/pages/settings/Preferences/PriorityModePage.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/pages/settings/Preferences/PriorityModePage.tsx b/src/pages/settings/Preferences/PriorityModePage.tsx index f3f4b282105c..4e9bf374ce01 100644 --- a/src/pages/settings/Preferences/PriorityModePage.tsx +++ b/src/pages/settings/Preferences/PriorityModePage.tsx @@ -14,6 +14,14 @@ import * as User from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +type PriorityModeItem = { + value: ValueOf; + text: string; + alternateText: string; + keyForList: ValueOf; + isSelected: boolean; +}; + type PriorityModePageOnyxProps = { /** The chat priority mode */ priorityMode: OnyxEntry>; @@ -24,7 +32,7 @@ type PriorityModePageProps = PriorityModePageOnyxProps; function PriorityModePage({priorityMode}: PriorityModePageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); - const priorityModes = Object.values(CONST.PRIORITY_MODE).map((mode) => ({ + const priorityModes = Object.values(CONST.PRIORITY_MODE).map((mode) => ({ value: mode, text: translate(`priorityModePage.priorityModes.${mode}.label`), alternateText: translate(`priorityModePage.priorityModes.${mode}.description`), @@ -33,7 +41,7 @@ function PriorityModePage({priorityMode}: PriorityModePageProps) { })); const updateMode = useCallback( - (mode: (typeof priorityModes)[number]) => { + (mode: PriorityModeItem) => { if (mode.value === priorityMode) { Navigation.goBack(); return; From 0c353fd3501046b18a2eba7be862438260aaa159 Mon Sep 17 00:00:00 2001 From: Carlos Barros <765936+barros001@users.noreply.github.com> Date: Tue, 27 Feb 2024 15:30:30 -0500 Subject: [PATCH 0247/1208] remove isAllowedToApproveExpenseReport method --- src/components/MoneyReportHeader.tsx | 2 +- src/components/ReportActionItem/ReportPreview.tsx | 2 +- src/libs/ReportUtils.ts | 11 ----------- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 3c84da5cba57..d78255afa0a3 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -100,7 +100,7 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton; const shouldShowSubmitButton = isDraft && reimbursableSpend !== 0; const shouldDisableSubmitButton = shouldShowSubmitButton && !ReportUtils.isAllowedToSubmitDraftExpenseReport(moneyRequestReport); - const shouldDisableSettlementButton = shouldShowSettlementButton && shouldShowApproveButton && !ReportUtils.isAllowedToApproveExpenseReport(moneyRequestReport); + const shouldDisableSettlementButton = shouldShowSettlementButton && shouldShowApproveButton && !ReportUtils.isAllowedToSubmitDraftExpenseReport(moneyRequestReport); const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE; const shouldShowNextStep = isFromPaidPolicy && !!nextStep?.message?.length; const shouldShowAnyButton = shouldShowSettlementButton || shouldShowApproveButton || shouldShowSubmitButton || shouldShowNextStep; diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 4bf7c1af03b9..7c68291c5e78 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -239,7 +239,7 @@ function ReportPreview({ [isPayer, isDraftExpenseReport, iouSettled, reimbursableSpend, iouCanceled, isAutoReimbursable, iouReport, shouldShowApproveButton], ); const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton; - const shouldDisableSettlementButton = shouldShowSettlementButton && shouldShowApproveButton && !ReportUtils.isAllowedToApproveExpenseReport(iouReport); + const shouldDisableSettlementButton = shouldShowSettlementButton && shouldShowApproveButton && !ReportUtils.isAllowedToSubmitDraftExpenseReport(iouReport); /* Show subtitle if at least one of the money requests is not being smart scanned, and either: diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index b082ec8789e5..69c903bc74d1 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5020,16 +5020,6 @@ function isAllowedToSubmitDraftExpenseReport(report: OnyxEntry): boolean return !((isPreventSelfApprovalEnabled ?? preventSelfApprovalEnabled) && isSelfApproval); } -function isAllowedToApproveExpenseReport(report: OnyxEntry): boolean { - const policy = getPolicy(report?.policyID); - const {approver, submitsTo, isPreventSelfApprovalEnabled, preventSelfApprovalEnabled} = policy; - - // TODO: check if we should be using "approver" here instead of the same "submitsTo". "approver" is "undefined" currently. - const isSelfApproval = currentUserAccountID === (approver ?? submitsTo); - - return !((isPreventSelfApprovalEnabled ?? preventSelfApprovalEnabled) && isSelfApproval); -} - export { getReportParticipantsTitle, isReportMessageAttachment, @@ -5231,7 +5221,6 @@ export { canEditPolicyDescription, getPolicyDescriptionText, isAllowedToSubmitDraftExpenseReport, - isAllowedToApproveExpenseReport, }; export type { From cb6108686f33371ded80a94de04b848740398103 Mon Sep 17 00:00:00 2001 From: Carlos Barros <765936+barros001@users.noreply.github.com> Date: Tue, 27 Feb 2024 15:33:38 -0500 Subject: [PATCH 0248/1208] updated shouldDisableSettlementButton logic --- src/components/MoneyReportHeader.tsx | 7 ++++--- src/components/ReportActionItem/ReportPreview.tsx | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index d78255afa0a3..697f8aa20265 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -100,7 +100,8 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton; const shouldShowSubmitButton = isDraft && reimbursableSpend !== 0; const shouldDisableSubmitButton = shouldShowSubmitButton && !ReportUtils.isAllowedToSubmitDraftExpenseReport(moneyRequestReport); - const shouldDisableSettlementButton = shouldShowSettlementButton && shouldShowApproveButton && !ReportUtils.isAllowedToSubmitDraftExpenseReport(moneyRequestReport); + const shouldDisableSettlementButton = + !canAllowSettlement || (shouldShowSettlementButton && shouldShowApproveButton && !ReportUtils.isAllowedToSubmitDraftExpenseReport(moneyRequestReport)); const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE; const shouldShowNextStep = isFromPaidPolicy && !!nextStep?.message?.length; const shouldShowAnyButton = shouldShowSettlementButton || shouldShowApproveButton || shouldShowSubmitButton || shouldShowNextStep; @@ -155,7 +156,7 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money shouldShowApproveButton={shouldShowApproveButton} style={[styles.pv2]} formattedAmount={formattedAmount} - isDisabled={!canAllowSettlement || shouldDisableSettlementButton} + isDisabled={shouldDisableSettlementButton} /> )} @@ -187,7 +188,7 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money shouldHidePaymentOptions={!shouldShowPayButton} shouldShowApproveButton={shouldShowApproveButton} formattedAmount={formattedAmount} - isDisabled={!canAllowSettlement || shouldDisableSettlementButton} + isDisabled={shouldDisableSettlementButton} /> )} diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 7c68291c5e78..c77839b03ad9 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -239,7 +239,7 @@ function ReportPreview({ [isPayer, isDraftExpenseReport, iouSettled, reimbursableSpend, iouCanceled, isAutoReimbursable, iouReport, shouldShowApproveButton], ); const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton; - const shouldDisableSettlementButton = shouldShowSettlementButton && shouldShowApproveButton && !ReportUtils.isAllowedToSubmitDraftExpenseReport(iouReport); + const shouldDisableSettlementButton = !canAllowSettlement || (shouldShowSettlementButton && shouldShowApproveButton && !ReportUtils.isAllowedToSubmitDraftExpenseReport(iouReport)); /* Show subtitle if at least one of the money requests is not being smart scanned, and either: @@ -331,7 +331,7 @@ function ReportPreview({ horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, }} - isDisabled={!canAllowSettlement || shouldDisableSettlementButton} + isDisabled={shouldDisableSettlementButton} /> )} {shouldShowSubmitButton && ( From 03c5f52f25456c04c7da5638e8d5a29dba947237 Mon Sep 17 00:00:00 2001 From: Carlos Barros <765936+barros001@users.noreply.github.com> Date: Tue, 27 Feb 2024 16:20:57 -0500 Subject: [PATCH 0249/1208] added isAllowedToApproveExpenseReport; added test case for nextStep --- src/components/MoneyReportHeader.tsx | 3 +- .../ReportActionItem/ReportPreview.tsx | 2 +- src/libs/NextStepUtils.ts | 47 +++++++++---------- src/libs/ReportUtils.ts | 11 +++++ tests/unit/NextStepUtilsTest.ts | 32 +++++++++++++ 5 files changed, 67 insertions(+), 28 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 697f8aa20265..dc89ce1bdc3a 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -100,8 +100,7 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton; const shouldShowSubmitButton = isDraft && reimbursableSpend !== 0; const shouldDisableSubmitButton = shouldShowSubmitButton && !ReportUtils.isAllowedToSubmitDraftExpenseReport(moneyRequestReport); - const shouldDisableSettlementButton = - !canAllowSettlement || (shouldShowSettlementButton && shouldShowApproveButton && !ReportUtils.isAllowedToSubmitDraftExpenseReport(moneyRequestReport)); + const shouldDisableSettlementButton = !canAllowSettlement || (shouldShowSettlementButton && shouldShowApproveButton && !ReportUtils.isAllowedToApproveExpenseReport(moneyRequestReport)); const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE; const shouldShowNextStep = isFromPaidPolicy && !!nextStep?.message?.length; const shouldShowAnyButton = shouldShowSettlementButton || shouldShowApproveButton || shouldShowSubmitButton || shouldShowNextStep; diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index c77839b03ad9..b03c0efce81a 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -239,7 +239,7 @@ function ReportPreview({ [isPayer, isDraftExpenseReport, iouSettled, reimbursableSpend, iouCanceled, isAutoReimbursable, iouReport, shouldShowApproveButton], ); const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton; - const shouldDisableSettlementButton = !canAllowSettlement || (shouldShowSettlementButton && shouldShowApproveButton && !ReportUtils.isAllowedToSubmitDraftExpenseReport(iouReport)); + const shouldDisableSettlementButton = !canAllowSettlement || (shouldShowSettlementButton && shouldShowApproveButton && !ReportUtils.isAllowedToApproveExpenseReport(iouReport)); /* Show subtitle if at least one of the money requests is not being smart scanned, and either: diff --git a/src/libs/NextStepUtils.ts b/src/libs/NextStepUtils.ts index 9a931af92518..f8db5fdd2c6b 100644 --- a/src/libs/NextStepUtils.ts +++ b/src/libs/NextStepUtils.ts @@ -242,32 +242,29 @@ function buildNextStep( text: ' %expenses.', }, ]; - } - // Prevented self approval - if ((isPreventSelfApprovalEnabled ?? preventSelfApprovalEnabled) && isSelfApproval) { - optimisticNextStep.message = [ - { - text: "Oops! Looks like you're ", - }, - { - text: isManager ? 'reviewing' : 'approving', - }, - { - text: ' your own report. ', - type: 'strong', - }, - { - text: 'Approving your own reports is ', - }, - { - text: 'forbidden', - type: 'strong', - }, - { - text: ' by your policy.', - }, - ]; + // Prevented self approval + if ((isPreventSelfApprovalEnabled ?? preventSelfApprovalEnabled) && isSelfApproval) { + optimisticNextStep.message = [ + { + text: "Oops! Looks like you're reviewing", + }, + { + text: ' your own report. ', + type: 'strong', + }, + { + text: 'Approving your own reports is ', + }, + { + text: 'forbidden', + type: 'strong', + }, + { + text: ' by your policy.', + }, + ]; + } } break; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 69c903bc74d1..d0c3d3e6603d 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5020,6 +5020,16 @@ function isAllowedToSubmitDraftExpenseReport(report: OnyxEntry): boolean return !((isPreventSelfApprovalEnabled ?? preventSelfApprovalEnabled) && isSelfApproval); } +function isAllowedToApproveExpenseReport(report: OnyxEntry): boolean { + const policy = getPolicy(report?.policyID); + const {ownerAccountID, submitsTo, isPreventSelfApprovalEnabled, preventSelfApprovalEnabled} = policy; + + const isOwner = currentUserAccountID === ownerAccountID; + const isSelfApproval = currentUserAccountID === submitsTo; + + return !((isPreventSelfApprovalEnabled ?? preventSelfApprovalEnabled) && isOwner && isSelfApproval); +} + export { getReportParticipantsTitle, isReportMessageAttachment, @@ -5221,6 +5231,7 @@ export { canEditPolicyDescription, getPolicyDescriptionText, isAllowedToSubmitDraftExpenseReport, + isAllowedToApproveExpenseReport, }; export type { diff --git a/tests/unit/NextStepUtilsTest.ts b/tests/unit/NextStepUtilsTest.ts index 622881bc7979..9c202ca4ebd9 100644 --- a/tests/unit/NextStepUtilsTest.ts +++ b/tests/unit/NextStepUtilsTest.ts @@ -440,6 +440,38 @@ describe('libs/NextStepUtils', () => { expect(result).toMatchObject(optimisticNextStep); }); + + test('prevented self approval', () => { + optimisticNextStep.title = 'Next Steps:'; + optimisticNextStep.message = [ + { + text: "Oops! Looks like you're reviewing", + }, + { + text: ' your own report. ', + type: 'strong', + }, + { + text: 'Approving your own reports is ', + }, + { + text: 'forbidden', + type: 'strong', + }, + { + text: ' by your policy.', + }, + ]; + + return Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + submitsTo: currentUserAccountID, + preventSelfApprovalEnabled: true, + }).then(() => { + const result = NextStepUtils.buildNextStep(report, CONST.REPORT.STATUS_NUM.SUBMITTED); + + expect(result).toMatchObject(optimisticNextStep); + }); + }); }); describe('it generates an optimistic nextStep once a report has been approved', () => { From eea5225703ec2ff14de8b395acbbdba1e387667f Mon Sep 17 00:00:00 2001 From: Carlos Barros <765936+barros001@users.noreply.github.com> Date: Tue, 27 Feb 2024 16:29:42 -0500 Subject: [PATCH 0250/1208] updated shouldDisableSettlementButton logic --- src/components/MoneyReportHeader.tsx | 3 ++- src/components/ReportActionItem/ReportPreview.tsx | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index dc89ce1bdc3a..ec3340b6f0f0 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -100,7 +100,8 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton; const shouldShowSubmitButton = isDraft && reimbursableSpend !== 0; const shouldDisableSubmitButton = shouldShowSubmitButton && !ReportUtils.isAllowedToSubmitDraftExpenseReport(moneyRequestReport); - const shouldDisableSettlementButton = !canAllowSettlement || (shouldShowSettlementButton && shouldShowApproveButton && !ReportUtils.isAllowedToApproveExpenseReport(moneyRequestReport)); + const shouldDisableSettlementButton = + shouldShowSettlementButton && ((shouldShowPayButton && !canAllowSettlement) || (shouldShowApproveButton && !ReportUtils.isAllowedToApproveExpenseReport(moneyRequestReport))); const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE; const shouldShowNextStep = isFromPaidPolicy && !!nextStep?.message?.length; const shouldShowAnyButton = shouldShowSettlementButton || shouldShowApproveButton || shouldShowSubmitButton || shouldShowNextStep; diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index b03c0efce81a..9b7eb5a360a5 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -239,7 +239,8 @@ function ReportPreview({ [isPayer, isDraftExpenseReport, iouSettled, reimbursableSpend, iouCanceled, isAutoReimbursable, iouReport, shouldShowApproveButton], ); const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton; - const shouldDisableSettlementButton = !canAllowSettlement || (shouldShowSettlementButton && shouldShowApproveButton && !ReportUtils.isAllowedToApproveExpenseReport(iouReport)); + const shouldDisableSettlementButton = + shouldShowSettlementButton && ((shouldShowPayButton && !canAllowSettlement) || (shouldShowApproveButton && !ReportUtils.isAllowedToApproveExpenseReport(iouReport))); /* Show subtitle if at least one of the money requests is not being smart scanned, and either: From 9b1a8e6688a889ad8fd01a2ed5fba589db8cfd42 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Tue, 27 Feb 2024 23:08:31 +0000 Subject: [PATCH 0251/1208] 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 0252/1208] 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 0253/1208] 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 0254/1208] 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 34cd2e8402fe6fab0135e503114b7d514360a76b Mon Sep 17 00:00:00 2001 From: hurali97 Date: Wed, 28 Feb 2024 13:03:14 +0500 Subject: [PATCH 0255/1208] refactor: use regex and dedicated const for sms domain --- src/CONST.ts | 1 - src/libs/LocalePhoneNumber.ts | 26 ++------------------------ 2 files changed, 2 insertions(+), 25 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index fce096a9957a..bb167bb5d8d8 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -42,7 +42,6 @@ const cardActiveStates: number[] = [2, 3, 4, 7]; const CONST = { MERGED_ACCOUNT_PREFIX: 'MERGED_', - SMS_DOMAIN_PATTERN: 'expensify.sms', ANDROID_PACKAGE_NAME, ANIMATED_TRANSITION: 300, ANIMATED_TRANSITION_FROM_VALUE: 100, diff --git a/src/libs/LocalePhoneNumber.ts b/src/libs/LocalePhoneNumber.ts index 798e0a50c43e..460d5fc0fe9f 100644 --- a/src/libs/LocalePhoneNumber.ts +++ b/src/libs/LocalePhoneNumber.ts @@ -10,28 +10,6 @@ Onyx.connect({ callback: (val) => (countryCodeByIP = val ?? 1), }); -/** - * Checks whether the given string contains any numbers. - * It uses indexOf instead of regex and includes for performance reasons. - * - * @param text - * @returns boolean - */ -function containsNumbers(text: string) { - return ( - text.indexOf('0') !== -1 || - text.indexOf('1') !== -1 || - text.indexOf('2') !== -1 || - text.indexOf('3') !== -1 || - text.indexOf('4') !== -1 || - text.indexOf('5') !== -1 || - text.indexOf('6') !== -1 || - text.indexOf('7') !== -1 || - text.indexOf('8') !== -1 || - text.indexOf('9') !== -1 - ); -} - /** * Returns a locally converted phone number for numbers from the same region * and an internationally converted phone number with the country code for numbers from other regions @@ -41,8 +19,8 @@ function formatPhoneNumber(number: string): string { return ''; } - // do not parse the string, if it's not a phone number - if (number.indexOf(CONST.SMS_DOMAIN_PATTERN) === -1 && !containsNumbers(number)) { + // do not parse the string, if it doesn't contain the SMS domain and it's not a phone number + if (number.indexOf(CONST.SMS.DOMAIN) === -1 && !CONST.REGEX.DIGITS_AND_PLUS.test(number)) { return number; } const numberWithoutSMSDomain = Str.removeSMSDomain(number); From 707f56c7f913dcf6e7e764933c7e3b7f78021294 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Wed, 28 Feb 2024 13:03:42 +0500 Subject: [PATCH 0256/1208] refactor: remove allocating a variable and return the evaluated expresssion --- src/libs/PersonalDetailsUtils.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/libs/PersonalDetailsUtils.ts b/src/libs/PersonalDetailsUtils.ts index 7a9b3fe96598..d5946feb2f74 100644 --- a/src/libs/PersonalDetailsUtils.ts +++ b/src/libs/PersonalDetailsUtils.ts @@ -46,8 +46,7 @@ function getDisplayNameOrDefault(passedPersonalDetails?: Partial Date: Wed, 28 Feb 2024 08:33:16 +0000 Subject: [PATCH 0257/1208] Rename AsMutable.ts to asMutable.ts --- src/types/utils/{AsMutable.ts => asMutable.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/types/utils/{AsMutable.ts => asMutable.ts} (100%) diff --git a/src/types/utils/AsMutable.ts b/src/types/utils/asMutable.ts similarity index 100% rename from src/types/utils/AsMutable.ts rename to src/types/utils/asMutable.ts From 5a33f683158f2282b2134478411c30671d4b8492 Mon Sep 17 00:00:00 2001 From: ruben-rebelo Date: Wed, 28 Feb 2024 08:35:38 +0000 Subject: [PATCH 0258/1208] [TS migration][postTestBuildComments] Feedback --- tests/unit/postTestBuildComment.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/unit/postTestBuildComment.ts b/tests/unit/postTestBuildComment.ts index 26c0711a34cd..24028ee4f1bd 100644 --- a/tests/unit/postTestBuildComment.ts +++ b/tests/unit/postTestBuildComment.ts @@ -1,6 +1,6 @@ import * as core from '@actions/core'; import {when} from 'jest-when'; -import asMutable from '@src/types/utils/AsMutable'; +import asMutable from '@src/types/utils/asMutable'; import ghAction from '../../.github/actions/javascript/postTestBuildComment/postTestBuildComment'; import GithubUtils from '../../.github/libs/GithubUtils'; @@ -61,9 +61,7 @@ describe('Post test build comments action tests', () => { }); test('Test GH action', async () => { - when(core.getInput) - .calledWith('PR_NUMBER', {required: true}) - .mockReturnValue(12 as unknown as string); + when(core.getInput).calledWith('PR_NUMBER', {required: true}).mockReturnValue(12); when(core.getInput).calledWith('ANDROID', {required: true}).mockReturnValue('success'); when(core.getInput).calledWith('IOS', {required: true}).mockReturnValue('success'); when(core.getInput).calledWith('WEB', {required: true}).mockReturnValue('success'); @@ -94,6 +92,6 @@ describe('Post test build comments action tests', () => { } `); expect(createCommentMock).toBeCalledTimes(1); - expect(createCommentMock).toBeCalledWith('App', 12, message); + expect(createCommentMock).toBeCalledWith('App', '12', message); }); }); From 518153347c6760462a92370b066dbace32a56781 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 28 Feb 2024 18:01:29 +0700 Subject: [PATCH 0259/1208] 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 f2d910b045b28d8d95a0e8e0dce3b8f3959f00ac Mon Sep 17 00:00:00 2001 From: Jayesh Mangwani Date: Wed, 28 Feb 2024 19:25:41 +0530 Subject: [PATCH 0260/1208] fix: updated IdologyQuestionsForm and AdditionalDetailStepForm values usage of the updated Form --- src/pages/EnablePayments/TermsStep.tsx | 2 -- src/types/form/AdditionalDetailStepForm.ts | 28 +++++++++++++--------- src/types/form/IdologyQuestionsForm.ts | 12 +++++++--- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/pages/EnablePayments/TermsStep.tsx b/src/pages/EnablePayments/TermsStep.tsx index 7a4fc76d87c5..df9b14cffca4 100644 --- a/src/pages/EnablePayments/TermsStep.tsx +++ b/src/pages/EnablePayments/TermsStep.tsx @@ -7,8 +7,6 @@ import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; - -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; diff --git a/src/types/form/AdditionalDetailStepForm.ts b/src/types/form/AdditionalDetailStepForm.ts index f102d03679ba..0d2c96487392 100644 --- a/src/types/form/AdditionalDetailStepForm.ts +++ b/src/types/form/AdditionalDetailStepForm.ts @@ -1,3 +1,4 @@ +import type {ValueOf} from 'type-fest'; import type Form from './Form'; const INPUT_IDS = { @@ -12,17 +13,22 @@ const INPUT_IDS = { SSN: 'ssn', } as const; -type AdditionalDetailStepForm = Form<{ - [INPUT_IDS.LEGAL_FIRST_NAME]: string; - [INPUT_IDS.LEGAL_LAST_NAME]: string; - [INPUT_IDS.PHONE_NUMBER]: string; - [INPUT_IDS.ADDRESS_STREET]: string; - [INPUT_IDS.ADDRESS_CITY]: string; - [INPUT_IDS.ADDRESS_ZIP_CODE]: string; - [INPUT_IDS.ADDRESS_STATE]: string; - [INPUT_IDS.DOB]: string; - [INPUT_IDS.SSN]: string; -}>; +type InputID = ValueOf; + +type AdditionalDetailStepForm = Form< + InputID, + { + [INPUT_IDS.LEGAL_FIRST_NAME]: string; + [INPUT_IDS.LEGAL_LAST_NAME]: string; + [INPUT_IDS.PHONE_NUMBER]: string; + [INPUT_IDS.ADDRESS_STREET]: string; + [INPUT_IDS.ADDRESS_CITY]: string; + [INPUT_IDS.ADDRESS_ZIP_CODE]: string; + [INPUT_IDS.ADDRESS_STATE]: string; + [INPUT_IDS.DOB]: string; + [INPUT_IDS.SSN]: string; + } +>; export type {AdditionalDetailStepForm}; export default INPUT_IDS; diff --git a/src/types/form/IdologyQuestionsForm.ts b/src/types/form/IdologyQuestionsForm.ts index 5b8d50c68abf..250e8c6f7766 100644 --- a/src/types/form/IdologyQuestionsForm.ts +++ b/src/types/form/IdologyQuestionsForm.ts @@ -1,12 +1,18 @@ +import type {ValueOf} from 'type-fest'; import type Form from './Form'; const INPUT_IDS = { ANSWER: 'answer', } as const; -type IdologyQuestionsForm = Form<{ - [INPUT_IDS.ANSWER]: string; -}>; +type InputID = ValueOf; + +type IdologyQuestionsForm = Form< + InputID, + { + [INPUT_IDS.ANSWER]: string; + } +>; export type {IdologyQuestionsForm}; export default INPUT_IDS; From 1a7099077402761a95b75b7f997c349be30ddbc5 Mon Sep 17 00:00:00 2001 From: smelaa Date: Wed, 28 Feb 2024 15:00:14 +0100 Subject: [PATCH 0261/1208] 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 0262/1208] 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 0263/1208] 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 0264/1208] 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 0265/1208] 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 0266/1208] 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 0267/1208] 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 0268/1208] 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 0269/1208] 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 0270/1208] 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 f36550f1001bba5e995b1ef682dc22da19b28605 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Thu, 29 Feb 2024 13:00:44 +0500 Subject: [PATCH 0271/1208] refactor: use single-line comments --- src/libs/PersonalDetailsUtils.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/libs/PersonalDetailsUtils.ts b/src/libs/PersonalDetailsUtils.ts index d5946feb2f74..97e2dc91492b 100644 --- a/src/libs/PersonalDetailsUtils.ts +++ b/src/libs/PersonalDetailsUtils.ts @@ -25,24 +25,19 @@ Onyx.connect({ }, }); -/** - * Index for the substring method to remove the merged account prefix. - */ +// Index for the substring method to remove the merged account prefix. const substringStartIndex = CONST.MERGED_ACCOUNT_PREFIX.length; function getDisplayNameOrDefault(passedPersonalDetails?: Partial | null, defaultValue = '', shouldFallbackToHidden = true): string { let displayName = passedPersonalDetails?.displayName ?? ''; - /** - * If the displayName starts with the merged account prefix, remove it. - */ + + // If the displayName starts with the merged account prefix, remove it. if (displayName.startsWith(CONST.MERGED_ACCOUNT_PREFIX)) { displayName = displayName.substring(substringStartIndex); } - /** - * If displayName exists, return it early so we don't have to allocate - * memory for the fallback string. - */ + // If displayName exists, return it early so we don't have to allocate + // memory for the fallback string. if (displayName) { return displayName; } From e01e18bccbd26929b3a49d211a523c4723989d92 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 29 Feb 2024 09:54:53 +0100 Subject: [PATCH 0272/1208] fix: resolve comments --- src/libs/SidebarUtils.ts | 2 +- src/pages/home/sidebar/SidebarLinksData.tsx | 4 ++-- .../sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 463c796fb5ce..41a0f4b87a6c 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -136,7 +136,7 @@ function getOrderedReportIDs( pinnedAndGBRReports.push(report); } else if (report?.hasDraft) { draftReports.push(report); - } else if (ReportUtils.isArchivedRoom(report) && report) { + } else if (report && ReportUtils.isArchivedRoom(report)) { archivedReports.push(report); } else { nonArchivedReports.push(report); diff --git a/src/pages/home/sidebar/SidebarLinksData.tsx b/src/pages/home/sidebar/SidebarLinksData.tsx index 6e71e30ac1b6..b4d118c5278b 100644 --- a/src/pages/home/sidebar/SidebarLinksData.tsx +++ b/src/pages/home/sidebar/SidebarLinksData.tsx @@ -239,7 +239,7 @@ export default withCurrentReportID( withOnyx({ chatReports: { key: ONYXKEYS.COLLECTION.REPORT, - // This assertion is needed because the selector in withOnyx expects that the return type will be the same as type in ONYXKEYS but in this case it's not, this is a bug in withOnyx but it's impossible to fix it, when useOnyx will be introduce it will be fixed. + // This assertion is needed because the selector in withOnyx expects that the return type will be the same as type in ONYXKEYS but for collection keys the selector is executed for each collection item. This is a bug in withOnyx typings that we don't have a solution yet, when useOnyx hook is introduced it will be fixed. selector: chatReportSelector as unknown as (report: OnyxEntry) => OnyxCollection, initialValue: {}, }, @@ -261,7 +261,7 @@ export default withCurrentReportID( }, policies: { key: ONYXKEYS.COLLECTION.POLICY, - // This assertion is needed because the selector in withOnyx expects that the return type will be the same as type in ONYXKEYS but in this case it's not, this is a bug in withOnyx but it's impossible to fix it, when useOnyx will be introduce it will be fixed. + // This assertion is needed because the selector in withOnyx expects that the return type will be the same as type in ONYXKEYS but for collection keys the selector is executed for each collection item. This is a bug in withOnyx typings that we don't have a solution yet, when useOnyx hook is introduced it will be fixed. selector: policySelector as unknown as (policy: OnyxEntry) => OnyxCollection, initialValue: {}, }, diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index c080272acbd2..a550dbd91657 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -215,6 +215,7 @@ const policySelector = (policy: OnyxEntry) => export default withOnyx, FloatingActionButtonAndPopoverOnyxProps>({ allPolicies: { key: ONYXKEYS.COLLECTION.POLICY, + // This assertion is needed because the selector in withOnyx expects that the return type will be the same as type in ONYXKEYS but for collection keys the selector is executed for each collection item. This is a bug in withOnyx typings that we don't have a solution yet, when useOnyx hook is introduced it will be fixed. selector: policySelector as unknown as (policy: OnyxEntry) => OnyxCollection, }, isLoading: { From 3484b06e87d161c3534329418a047a29cd28db82 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 29 Feb 2024 10:28:10 +0100 Subject: [PATCH 0273/1208] fix: typecheck --- tests/utils/LHNTestUtils.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/utils/LHNTestUtils.tsx b/tests/utils/LHNTestUtils.tsx index 80f28002f975..f6bd01bbee74 100644 --- a/tests/utils/LHNTestUtils.tsx +++ b/tests/utils/LHNTestUtils.tsx @@ -282,7 +282,6 @@ function MockedSidebarLinks({currentReportID = ''}: MockedSidebarLinksProps) { return ( {}} insets={{ top: 0, @@ -290,7 +289,7 @@ function MockedSidebarLinks({currentReportID = ''}: MockedSidebarLinksProps) { right: 0, bottom: 0, }} - isSmallScreenWidth={false} + // @ts-expect-error - normally this comes from withCurrentReportID hoc , but here we are just mocking this currentReportID={currentReportID} /> From f9dd242735e6e64019807b4c5c981b0c80c89add Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Thu, 29 Feb 2024 10:51:11 +0100 Subject: [PATCH 0274/1208] 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 0275/1208] 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 0276/1208] 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 0277/1208] 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 0281/1208] 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 0282/1208] 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 0283/1208] 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 0284/1208] 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 0285/1208] 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 0286/1208] 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 3630b1b0c4c7729889d3a3ad585cf88394c4b7f0 Mon Sep 17 00:00:00 2001 From: Someshwar Tripathi Date: Fri, 1 Mar 2024 09:11:00 +0530 Subject: [PATCH 0287/1208] Increase editing space for recovery code input --- src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx index f7e08c128df5..7d1caebb2e47 100755 --- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx +++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx @@ -290,7 +290,7 @@ function BaseValidateCodeForm({account, credentials, session, autoComplete, isUs accessibilityLabel={translate('recoveryCodeForm.recoveryCode')} value={recoveryCode} onChangeText={(text) => onTextInput(text, 'recoveryCode')} - maxLength={CONST.RECOVERY_CODE_LENGTH} + maxLength={CONST.FORM_CHARACTER_LIMIT} label={translate('recoveryCodeForm.recoveryCode')} errorText={formError?.recoveryCode ?? ''} hasError={hasError} From 016d7a978c197d42c5844d66cd0482593c039321 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Fri, 1 Mar 2024 13:31:32 +0800 Subject: [PATCH 0288/1208] don't categorize when searching --- src/pages/EditReportFieldDropdownPage.tsx | 28 +++++++++++++++++------ 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/pages/EditReportFieldDropdownPage.tsx b/src/pages/EditReportFieldDropdownPage.tsx index 1ad3c766221b..ddd5330e389e 100644 --- a/src/pages/EditReportFieldDropdownPage.tsx +++ b/src/pages/EditReportFieldDropdownPage.tsx @@ -46,15 +46,29 @@ function EditReportFieldDropdownPage({fieldName, onSubmit, fieldID, fieldValue, const [headerMessage, setHeaderMessage] = useState(''); const sections = useMemo(() => { - const filteredRecentOptions = recentlyUsedOptions.filter((option) => option.toLowerCase().includes(searchValue.toLowerCase())); - const filteredRestOfOptions = fieldOptions.filter((option) => !filteredRecentOptions.includes(option) && option.toLowerCase().includes(searchValue.toLowerCase())); - setHeaderMessage(!filteredRecentOptions.length && !filteredRestOfOptions.length ? translate('common.noResultsFound') : ''); + if (searchValue) { + const filteredOptions = fieldOptions.filter((option) => option.toLowerCase().includes(searchValue.toLowerCase())); + setHeaderMessage(!filteredOptions.length ? translate('common.noResultsFound') : ''); + return [ + { + shouldShow: false, + data: filteredOptions.map((option) => ({ + text: option, + keyForList: option, + searchText: option, + tooltipText: option, + })), + }, + ]; + } + const restOfOptions = fieldOptions.filter((option) => !recentlyUsedOptions.includes(option)); + setHeaderMessage(!restOfOptions.length && !recentlyUsedOptions.length ? translate('common.noResultsFound') : ''); return [ { title: translate('common.recents'), - shouldShow: filteredRecentOptions.length > 0, - data: filteredRecentOptions.map((option) => ({ + shouldShow: recentlyUsedOptions.length > 0, + data: recentlyUsedOptions.map((option) => ({ text: option, keyForList: option, searchText: option, @@ -63,8 +77,8 @@ function EditReportFieldDropdownPage({fieldName, onSubmit, fieldID, fieldValue, }, { title: translate('common.all'), - shouldShow: filteredRestOfOptions.length > 0, - data: filteredRestOfOptions.map((option) => ({ + shouldShow: restOfOptions.length > 0, + data: restOfOptions.map((option) => ({ text: option, keyForList: option, searchText: option, From 89b2e6e14c1a0db6fa88c4b2251c0ac7b35e43b6 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Fri, 1 Mar 2024 11:26:49 +0530 Subject: [PATCH 0289/1208] 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 0290/1208] 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 57370c83d1ea54e9a7f4753bcdd47f7fc42e58de Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Fri, 1 Mar 2024 10:10:16 +0100 Subject: [PATCH 0291/1208] Migrate web proxy to TS --- config/{proxyConfig.js => proxyConfig.ts} | 2 +- package.json | 2 +- web/{proxy.js => proxy.ts} | 22 +++++++++++++--------- 3 files changed, 15 insertions(+), 11 deletions(-) rename config/{proxyConfig.js => proxyConfig.ts} (92%) rename web/{proxy.js => proxy.ts} (78%) diff --git a/config/proxyConfig.js b/config/proxyConfig.ts similarity index 92% rename from config/proxyConfig.js rename to config/proxyConfig.ts index fa09c436461f..6a74d145df85 100644 --- a/config/proxyConfig.js +++ b/config/proxyConfig.ts @@ -3,7 +3,7 @@ * We only specify for staging URLs as API requests are sent to the production * servers by default. */ -module.exports = { +export default { STAGING: '/staging/', STAGING_SECURE: '/staging-secure/', }; diff --git a/package.json b/package.json index e3c23d4538d3..e757f000f5be 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "ipad-sm": "concurrently \"npx react-native run-ios --simulator=\\\"iPad Pro (11-inch) (4th generation)\\\" --mode=\\\"DebugDevelopment\\\" --scheme=\\\"New Expensify Dev\\\"\"", "start": "npx react-native start", "web": "scripts/set-pusher-suffix.sh && concurrently npm:web-proxy npm:web-server", - "web-proxy": "ts-node web/proxy.js", + "web-proxy": "ts-node web/proxy.ts", "web-server": "webpack-dev-server --open --config config/webpack/webpack.dev.js", "build": "webpack --config config/webpack/webpack.common.js --env envFile=.env.production", "build-staging": "webpack --config config/webpack/webpack.common.js --env envFile=.env.staging", diff --git a/web/proxy.js b/web/proxy.ts similarity index 78% rename from web/proxy.js rename to web/proxy.ts index 0d82ae60b678..130f5a67e51a 100644 --- a/web/proxy.js +++ b/web/proxy.ts @@ -1,7 +1,10 @@ -const http = require('http'); -const https = require('https'); -const proxyConfig = require('../config/proxyConfig'); -require('dotenv').config(); +import dotenv from 'dotenv'; +import http from 'http'; +import type {IncomingMessage, ServerResponse} from 'http'; +import https from 'https'; +import proxyConfig from '../config/proxyConfig'; + +dotenv.config(); if (process.env.USE_WEB_PROXY === 'false') { process.stdout.write('Skipping proxy as USE_WEB_PROXY was set to false.\n'); @@ -20,7 +23,7 @@ console.log(`Creating proxy with host: ${host} for production API and ${stagingH * possible to work on the app within a limited development * environment that has no local API. */ -const server = http.createServer((request, response) => { +const server = http.createServer((request: IncomingMessage, response: ServerResponse) => { let hostname = host; let requestPath = request.url; @@ -37,10 +40,10 @@ const server = http.createServer((request, response) => { * /receipts/w_... => request sent to production server * /staging/chat-attachments/46545... => request sent to staging server */ - if (request.url.startsWith(proxyConfig.STAGING_SECURE)) { + if (request.url?.startsWith(proxyConfig.STAGING_SECURE)) { hostname = stagingSecureHost; requestPath = request.url.replace(proxyConfig.STAGING_SECURE, '/'); - } else if (request.url.startsWith(proxyConfig.STAGING)) { + } else if (request.url?.startsWith(proxyConfig.STAGING)) { hostname = stagingHost; requestPath = request.url.replace(proxyConfig.STAGING, '/'); } @@ -52,14 +55,15 @@ const server = http.createServer((request, response) => { headers: { ...request.headers, host: hostname, - 'user-agent': request.headers['user-agent'].concat(' Development-NewDot/1.0'), + // eslint-disable-next-line @typescript-eslint/naming-convention + 'user-agent': request.headers['user-agent']?.concat(' Development-NewDot/1.0'), }, port: 443, }); request.pipe(proxyRequest); proxyRequest.on('response', (proxyResponse) => { - response.writeHead(proxyResponse.statusCode, proxyResponse.headers); + response.writeHead(proxyResponse.statusCode ?? 0, proxyResponse.headers); proxyResponse.pipe(response); }); From 3c5b6a272a89cda8d9a17d50008f24a84b4ea91d Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Fri, 1 Mar 2024 10:22:24 +0100 Subject: [PATCH 0292/1208] Fix GH actions --- .github/actions/javascript/bumpVersion/index.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/actions/javascript/bumpVersion/index.js b/.github/actions/javascript/bumpVersion/index.js index d17760baa91f..8fe84446ba82 100644 --- a/.github/actions/javascript/bumpVersion/index.js +++ b/.github/actions/javascript/bumpVersion/index.js @@ -2657,12 +2657,17 @@ createToken('XRANGELOOSE', `^${src[t.GTLT]}\\s*${src[t.XRANGEPLAINLOOSE]}$`) // Coercion. // Extract anything that could conceivably be a part of a valid semver -createToken('COERCE', `${'(^|[^\\d])' + +createToken('COERCEPLAIN', `${'(^|[^\\d])' + '(\\d{1,'}${MAX_SAFE_COMPONENT_LENGTH}})` + `(?:\\.(\\d{1,${MAX_SAFE_COMPONENT_LENGTH}}))?` + - `(?:\\.(\\d{1,${MAX_SAFE_COMPONENT_LENGTH}}))?` + + `(?:\\.(\\d{1,${MAX_SAFE_COMPONENT_LENGTH}}))?`) +createToken('COERCE', `${src[t.COERCEPLAIN]}(?:$|[^\\d])`) +createToken('COERCEFULL', src[t.COERCEPLAIN] + + `(?:${src[t.PRERELEASE]})?` + + `(?:${src[t.BUILD]})?` + `(?:$|[^\\d])`) createToken('COERCERTL', src[t.COERCE], true) +createToken('COERCERTLFULL', src[t.COERCEFULL], true) // Tilde ranges. // Meaning is "reasonably at or greater than" From c19ccde1e07006228b2eb26752c03680e5580564 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Fri, 1 Mar 2024 10:25:34 +0100 Subject: [PATCH 0293/1208] Revert unnecessary changes --- config/{proxyConfig.ts => proxyConfig.js} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename config/{proxyConfig.ts => proxyConfig.js} (92%) diff --git a/config/proxyConfig.ts b/config/proxyConfig.js similarity index 92% rename from config/proxyConfig.ts rename to config/proxyConfig.js index 6a74d145df85..fa09c436461f 100644 --- a/config/proxyConfig.ts +++ b/config/proxyConfig.js @@ -3,7 +3,7 @@ * We only specify for staging URLs as API requests are sent to the production * servers by default. */ -export default { +module.exports = { STAGING: '/staging/', STAGING_SECURE: '/staging-secure/', }; From 215062897221677b5aa228a9efe17d2ae3129cea Mon Sep 17 00:00:00 2001 From: smelaa Date: Fri, 1 Mar 2024 14:42:55 +0100 Subject: [PATCH 0294/1208] 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 b99c2cf228e15d8e138a23d7b3dae277ed89d8e3 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 1 Mar 2024 17:25:20 +0100 Subject: [PATCH 0295/1208] [TS migration] Migrate 'Desktop' files --- config/webpack/webpack.desktop.js | 4 +- ...{ELECTRON_EVENTS.js => ELECTRON_EVENTS.ts} | 4 +- desktop/README.md | 4 +- desktop/contextBridge.js | 98 ---------- desktop/contextBridge.ts | 82 ++++++++ desktop/{main.js => main.ts} | 178 ++++++++++-------- desktop/{start.js => start.ts} | 9 +- package.json | 2 +- src/types/modules/electron.d.ts | 9 +- 9 files changed, 195 insertions(+), 195 deletions(-) rename desktop/{ELECTRON_EVENTS.js => ELECTRON_EVENTS.ts} (90%) delete mode 100644 desktop/contextBridge.js create mode 100644 desktop/contextBridge.ts rename desktop/{main.js => main.ts} (82%) rename desktop/{start.js => start.ts} (88%) diff --git a/config/webpack/webpack.desktop.js b/config/webpack/webpack.desktop.js index 2612e2b190fa..20ee4a4025df 100644 --- a/config/webpack/webpack.desktop.js +++ b/config/webpack/webpack.desktop.js @@ -28,8 +28,8 @@ module.exports = (env) => { name: 'desktop-main', target: 'electron-main', entry: { - main: './desktop/main.js', - contextBridge: './desktop/contextBridge.js', + main: './desktop/main.ts', + contextBridge: './desktop/contextBridge.ts', }, output: { filename: '[name].js', diff --git a/desktop/ELECTRON_EVENTS.js b/desktop/ELECTRON_EVENTS.ts similarity index 90% rename from desktop/ELECTRON_EVENTS.js rename to desktop/ELECTRON_EVENTS.ts index ee8c0521892e..de0bd655e12c 100644 --- a/desktop/ELECTRON_EVENTS.js +++ b/desktop/ELECTRON_EVENTS.ts @@ -9,6 +9,6 @@ const ELECTRON_EVENTS = { KEYBOARD_SHORTCUTS_PAGE: 'keyboard-shortcuts-page', START_UPDATE: 'start-update', UPDATE_DOWNLOADED: 'update-downloaded', -}; +} as const; -module.exports = ELECTRON_EVENTS; +export default ELECTRON_EVENTS; diff --git a/desktop/README.md b/desktop/README.md index 77abff97a898..4493196b5ed4 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -31,7 +31,7 @@ The New Expensify desktop app is built using [Electron.js](https://www.electronj The desktop app is organized in three pieces: 1. The Electron main process - - Implemented in https://github.com/Expensify/App/blob/main/desktop/main.js. + - Implemented in https://github.com/Expensify/App/blob/main/desktop/main.ts. - This file has access to the full set of Electron and Node.JS APIs. 2. The Electron renderer process - This is the webpack-bundled version of our react-native-web app (except using `index.desktop.js` files instead of `index.website.js`, where applicable) @@ -131,7 +131,7 @@ The root [package.json](../package.json) serves for `devDependencies` and shared The [desktop/package.json](./package.json) serves for desktop (electron-main) specific dependencies We use Webpack with a [desktop specific config](../config/webpack/webpack.desktop.js) to bundle our js code Half of the config takes care of packaging root package dependencies - everything related to rendering App in the Electron window. Packaged under `dist/www` -The other half is about bundling the `main.js` script which initializes Electron and renders `www` +The other half is about bundling the `main.ts` script which initializes Electron and renders `www` ## See what is getting packaged in the app If you suspect unnecessary items might be getting packaged you can inspect the package content in `desktop-build/` diff --git a/desktop/contextBridge.js b/desktop/contextBridge.js deleted file mode 100644 index a8b89cdc0b64..000000000000 --- a/desktop/contextBridge.js +++ /dev/null @@ -1,98 +0,0 @@ -const _ = require('underscore'); -const {contextBridge, ipcRenderer} = require('electron'); -const ELECTRON_EVENTS = require('./ELECTRON_EVENTS'); - -const WHITELIST_CHANNELS_RENDERER_TO_MAIN = [ - ELECTRON_EVENTS.REQUEST_DEVICE_ID, - ELECTRON_EVENTS.REQUEST_FOCUS_APP, - ELECTRON_EVENTS.REQUEST_UPDATE_BADGE_COUNT, - ELECTRON_EVENTS.REQUEST_VISIBILITY, - ELECTRON_EVENTS.START_UPDATE, - ELECTRON_EVENTS.LOCALE_UPDATED, -]; - -const WHITELIST_CHANNELS_MAIN_TO_RENDERER = [ELECTRON_EVENTS.KEYBOARD_SHORTCUTS_PAGE, ELECTRON_EVENTS.UPDATE_DOWNLOADED, ELECTRON_EVENTS.FOCUS, ELECTRON_EVENTS.BLUR]; - -const getErrorMessage = (channel) => `Electron context bridge cannot be used with channel '${channel}'`; - -/** - * The following methods will be available in the renderer process under `window.electron`. - */ -contextBridge.exposeInMainWorld('electron', { - /** - * Send data asynchronously from renderer process to main process. - * Note that this is a one-way channel – main will not respond. In order to get a response from main, either: - * - * - Use `sendSync` - * - Or implement `invoke` if you want to maintain asynchronous communication: https://www.electronjs.org/docs/latest/api/ipc-renderer#ipcrendererinvokechannel-args - * - * @param {String} channel - * @param {*} data - */ - send: (channel, data) => { - if (!_.contains(WHITELIST_CHANNELS_RENDERER_TO_MAIN, channel)) { - throw new Error(getErrorMessage(channel)); - } - - ipcRenderer.send(channel, data); - }, - - /** - * Send data synchronously from renderer process to main process. Main process may return a result. - * - * @param {String} channel - * @param {*} data - * @returns {*} - */ - sendSync: (channel, data) => { - if (!_.contains(WHITELIST_CHANNELS_RENDERER_TO_MAIN, channel)) { - throw new Error(getErrorMessage(channel)); - } - - return ipcRenderer.sendSync(channel, data); - }, - - /** - * Execute a function in the main process and return a promise that resolves with its response. - * - * @param {String} channel - * @param {*} args - * @returns {Promise} - */ - invoke: (channel, ...args) => { - if (!_.contains(WHITELIST_CHANNELS_RENDERER_TO_MAIN, channel)) { - throw new Error(getErrorMessage(channel)); - } - - return ipcRenderer.invoke(channel, ...args); - }, - - /** - * Set up a listener for events emitted from the main process and sent to the renderer process. - * - * @param {String} channel - * @param {Function} func - */ - on: (channel, func) => { - if (!_.contains(WHITELIST_CHANNELS_MAIN_TO_RENDERER, channel)) { - throw new Error(getErrorMessage(channel)); - } - - // Deliberately strip event as it includes `sender` - ipcRenderer.on(channel, (event, ...args) => func(...args)); - }, - - /** - * Remove listeners for a single channel from the main process and sent to the renderer process. - * - * @param {String} channel - * @param {Function} func - */ - removeAllListeners: (channel) => { - if (!_.contains(WHITELIST_CHANNELS_MAIN_TO_RENDERER, channel)) { - throw new Error(getErrorMessage(channel)); - } - - ipcRenderer.removeAllListeners(channel); - }, -}); diff --git a/desktop/contextBridge.ts b/desktop/contextBridge.ts new file mode 100644 index 000000000000..f2693259e51a --- /dev/null +++ b/desktop/contextBridge.ts @@ -0,0 +1,82 @@ +import {contextBridge, ipcRenderer} from 'electron'; +import ELECTRON_EVENTS from './ELECTRON_EVENTS'; + +type ContextBridgeApi = { + send: (channel: string, data?: unknown) => void; + sendSync: (channel: string, data?: unknown) => unknown; + invoke: (channel: string, ...args: unknown[]) => Promise; + on: (channel: string, func: (...args: unknown[]) => void) => void; + removeAllListeners: (channel: string) => void; +}; + +const WHITELIST_CHANNELS_RENDERER_TO_MAIN = [ + ELECTRON_EVENTS.REQUEST_DEVICE_ID, + ELECTRON_EVENTS.REQUEST_FOCUS_APP, + ELECTRON_EVENTS.REQUEST_UPDATE_BADGE_COUNT, + ELECTRON_EVENTS.REQUEST_VISIBILITY, + ELECTRON_EVENTS.START_UPDATE, + ELECTRON_EVENTS.LOCALE_UPDATED, +]; + +const WHITELIST_CHANNELS_MAIN_TO_RENDERER = [ELECTRON_EVENTS.KEYBOARD_SHORTCUTS_PAGE, ELECTRON_EVENTS.UPDATE_DOWNLOADED, ELECTRON_EVENTS.FOCUS, ELECTRON_EVENTS.BLUR]; + +const getErrorMessage = (channel: string): string => `Electron context bridge cannot be used with channel '${channel}'`; + +/** + * The following methods will be available in the renderer process under `window.electron`. + */ +contextBridge.exposeInMainWorld('electron', { + /** + * Send data asynchronously from renderer process to main process. + * Note that this is a one-way channel – main will not respond. In order to get a response from main, either: + * + * - Use `sendSync` + * - Or implement `invoke` if you want to maintain asynchronous communication: https://www.electronjs.org/docs/latest/api/ipc-renderer#ipcrendererinvokechannel-args + */ + send: (channel: string, data: unknown) => { + if (!WHITELIST_CHANNELS_RENDERER_TO_MAIN.some((whitelistChannel) => whitelistChannel === channel)) { + throw new Error(getErrorMessage(channel)); + } + + ipcRenderer.send(channel, data); + }, + + /** Send data synchronously from renderer process to main process. Main process may return a result. */ + sendSync: (channel: string, data: unknown): unknown => { + if (!WHITELIST_CHANNELS_RENDERER_TO_MAIN.some((whitelistChannel) => whitelistChannel === channel)) { + throw new Error(getErrorMessage(channel)); + } + + return ipcRenderer.sendSync(channel, data); + }, + + /** Execute a function in the main process and return a promise that resolves with its response. */ + invoke: (channel: string, ...args: unknown[]): Promise => { + if (!WHITELIST_CHANNELS_RENDERER_TO_MAIN.some((whitelistChannel) => whitelistChannel === channel)) { + throw new Error(getErrorMessage(channel)); + } + + return ipcRenderer.invoke(channel, ...args); + }, + + /** Set up a listener for events emitted from the main process and sent to the renderer process. */ + on: (channel: string, func: (...args: unknown[]) => void) => { + if (!WHITELIST_CHANNELS_MAIN_TO_RENDERER.some((whitelistChannel) => whitelistChannel === channel)) { + throw new Error(getErrorMessage(channel)); + } + + // Deliberately strip event as it includes `sender` + ipcRenderer.on(channel, (event, ...args) => func(...args)); + }, + + /** Remove listeners for a single channel from the main process and sent to the renderer process. */ + removeAllListeners: (channel: string) => { + if (!WHITELIST_CHANNELS_MAIN_TO_RENDERER.some((whitelistChannel) => whitelistChannel === channel)) { + throw new Error(getErrorMessage(channel)); + } + + ipcRenderer.removeAllListeners(channel); + }, +}); + +export default ContextBridgeApi; diff --git a/desktop/main.js b/desktop/main.ts similarity index 82% rename from desktop/main.js rename to desktop/main.ts index 4b38c5d36ab3..071ddb7c7a41 100644 --- a/desktop/main.js +++ b/desktop/main.ts @@ -1,17 +1,21 @@ -const {app, dialog, clipboard, BrowserWindow, Menu, MenuItem, shell, ipcMain} = require('electron'); -const _ = require('underscore'); -const serve = require('electron-serve'); -const contextMenu = require('electron-context-menu'); -const {autoUpdater} = require('electron-updater'); -const log = require('electron-log'); -const {machineId} = require('node-machine-id'); -const ELECTRON_EVENTS = require('./ELECTRON_EVENTS'); -const checkForUpdates = require('../src/libs/checkForUpdates'); -const CONFIG = require('../src/CONFIG').default; -const CONST = require('../src/CONST').default; -const Localize = require('../src/libs/Localize'); - -const port = process.env.PORT || 8082; +import {app, BrowserWindow, clipboard, dialog, ipcMain, Menu, MenuItem, shell} from 'electron'; +import type {BrowserView, MenuItemConstructorOptions, WebContents, WebviewTag} from 'electron'; +import contextMenu from 'electron-context-menu'; +import log from 'electron-log'; +import type {ElectronLog} from 'electron-log'; +import serve from 'electron-serve'; +import {autoUpdater} from 'electron-updater'; +import {machineId} from 'node-machine-id'; +import checkForUpdates from '@libs/checkForUpdates'; +import * as Localize from '@libs/Localize'; +import CONFIG from '@src/CONFIG'; +import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; +import type PlatformSpecificUpdater from '@src/setup/platformSetup/types'; +import type {Locale} from '@src/types/onyx'; +import ELECTRON_EVENTS from './ELECTRON_EVENTS'; + +const port = process.env.PORT ?? 8082; const {DESKTOP_SHORTCUT_ACCELERATOR, LOCALES} = CONST; // Setup google api key in process environment, we are setting it this way intentionally. It is required by the @@ -34,43 +38,46 @@ app.commandLine.appendSwitch('enable-network-information-downlink-max'); /** * Inserts the plain text from the clipboard into the provided browser window's web contents. * - * @param {BrowserWindow} browserWindow - The Electron BrowserWindow instance where the text should be inserted. + * @param browserWindow - The Electron BrowserWindow instance where the text should be inserted. */ -function pasteAsPlainText(browserWindow) { +function pasteAsPlainText(browserWindow: BrowserWindow | BrowserView | WebviewTag | WebContents) { const text = clipboard.readText(); - browserWindow.webContents.insertText(text); + + if ('webContents' in browserWindow) { + browserWindow.webContents.insertText(text); + } } /** * Initialize the right-click menu * See https://github.com/sindresorhus/electron-context-menu * - * @param {String} preferredLocale - The current user language to be used for translating menu labels. - * @returns {Function} A dispose function to clean up the created context menu. + * @param preferredLocale - The current user language to be used for translating menu labels. + * @returns A dispose function to clean up the created context menu. */ - -function createContextMenu(preferredLocale = LOCALES.DEFAULT) { +function createContextMenu(preferredLocale: Locale = LOCALES.DEFAULT): () => void { return contextMenu({ labels: { cut: Localize.translate(preferredLocale, 'desktopApplicationMenu.cut'), paste: Localize.translate(preferredLocale, 'desktopApplicationMenu.paste'), copy: Localize.translate(preferredLocale, 'desktopApplicationMenu.copy'), }, - append: (defaultActions, parameters, browserWindow) => [ - new MenuItem({ - // Only enable the menu item for Editable context which supports paste - visible: parameters.isEditable && parameters.editFlags.canPaste, - role: 'pasteAndMatchStyle', - accelerator: DESKTOP_SHORTCUT_ACCELERATOR.PASTE_AND_MATCH_STYLE, - label: Localize.translate(preferredLocale, 'desktopApplicationMenu.pasteAndMatchStyle'), - }), - new MenuItem({ - label: Localize.translate(preferredLocale, 'desktopApplicationMenu.pasteAsPlainText'), - visible: parameters.isEditable && parameters.editFlags.canPaste && clipboard.readText().length > 0, - accelerator: DESKTOP_SHORTCUT_ACCELERATOR.PASTE_AS_PLAIN_TEXT, - click: () => pasteAsPlainText(browserWindow), - }), - ], + append: (defaultActions, parameters, browserWindow) => + [ + new MenuItem({ + // Only enable the menu item for Editable context which supports paste + visible: parameters.isEditable && parameters.editFlags.canPaste, + role: 'pasteAndMatchStyle', + accelerator: DESKTOP_SHORTCUT_ACCELERATOR.PASTE_AND_MATCH_STYLE, + label: Localize.translate(preferredLocale, 'desktopApplicationMenu.pasteAndMatchStyle'), + }), + new MenuItem({ + label: Localize.translate(preferredLocale, 'desktopApplicationMenu.pasteAsPlainText'), + visible: parameters.isEditable && parameters.editFlags.canPaste && clipboard.readText().length > 0, + accelerator: DESKTOP_SHORTCUT_ACCELERATOR.PASTE_AS_PLAIN_TEXT, + click: () => pasteAsPlainText(browserWindow), + }), + ] as unknown as MenuItemConstructorOptions[], }); } @@ -79,11 +86,11 @@ let disposeContextMenu = createContextMenu(); // Send all autoUpdater logs to a log file: ~/Library/Logs/new.expensify.desktop/main.log // See https://www.npmjs.com/package/electron-log autoUpdater.logger = log; -autoUpdater.logger.transports.file.level = 'info'; +(autoUpdater.logger as ElectronLog).transports.file.level = 'info'; // Send all Console logs to a log file: ~/Library/Logs/new.expensify.desktop/main.log // See https://www.npmjs.com/package/electron-log -_.assign(console, log.functions); +Object.assign(console, log.functions); // This sets up the command line arguments used to manage the update. When // the --expected-update-version flag is set, the app will open pre-hidden @@ -92,23 +99,24 @@ _.assign(console, log.functions); const EXPECTED_UPDATE_VERSION_FLAG = '--expected-update-version'; const APP_DOMAIN = __DEV__ ? `https://dev.new.expensify.com:${port}` : 'app://-'; -let expectedUpdateVersion; -for (let i = 0; i < process.argv.length; i++) { - const arg = process.argv[i]; - if (arg.startsWith(`${EXPECTED_UPDATE_VERSION_FLAG}=`)) { - expectedUpdateVersion = arg.substr(`${EXPECTED_UPDATE_VERSION_FLAG}=`.length); +let expectedUpdateVersion: string; +process.argv.forEach((arg) => { + if (!arg.startsWith(`${EXPECTED_UPDATE_VERSION_FLAG}=`)) { + return; } -} + + expectedUpdateVersion = arg.substr(`${EXPECTED_UPDATE_VERSION_FLAG}=`.length); +}); // Add the listeners and variables required to ensure that auto-updating // happens correctly. let hasUpdate = false; -let downloadedVersion; +let downloadedVersion: string; // Note that we have to subscribe to this separately and cannot use Localize.translateLocal, // because the only way code can be shared between the main and renderer processes at runtime is via the context bridge // So we track preferredLocale separately via ELECTRON_EVENTS.LOCALE_UPDATED -const preferredLocale = CONST.LOCALES.DEFAULT; +const preferredLocale: Locale = CONST.LOCALES.DEFAULT; const appProtocol = CONST.DEEPLINK_BASE_URL.replace('://', ''); @@ -120,12 +128,8 @@ const quitAndInstallWithUpdate = () => { autoUpdater.quitAndInstall(); }; -/** - * Menu Item callback to triggers an update check - * @param {MenuItem} menuItem - * @param {BrowserWindow} browserWindow - */ -const manuallyCheckForUpdates = (menuItem, browserWindow) => { +/** Menu Item callback to triggers an update check */ +const manuallyCheckForUpdates = (menuItem: MenuItem, browserWindow?: BrowserWindow) => { // Disable item until the check (and download) is complete // eslint: menu item flags like enabled or visible can be dynamically toggled by mutating the object // eslint-disable-next-line no-param-reassign @@ -135,7 +139,11 @@ const manuallyCheckForUpdates = (menuItem, browserWindow) => { .checkForUpdates() .catch((error) => ({error})) .then((result) => { - const downloadPromise = result && result.downloadPromise; + const downloadPromise = result && 'downloadPromise' in result ? result.downloadPromise : undefined; + + if (!browserWindow) { + return; + } if (downloadPromise) { dialog.showMessageBox(browserWindow, { @@ -144,7 +152,7 @@ const manuallyCheckForUpdates = (menuItem, browserWindow) => { detail: Localize.translate(preferredLocale, 'checkForUpdatesModal.available.message'), buttons: [Localize.translate(preferredLocale, 'checkForUpdatesModal.available.soundsGood')], }); - } else if (result && result.error) { + } else if (result && 'error' in result && result.error) { dialog.showMessageBox(browserWindow, { type: 'error', message: Localize.translate(preferredLocale, 'checkForUpdatesModal.error.title'), @@ -170,25 +178,30 @@ const manuallyCheckForUpdates = (menuItem, browserWindow) => { }); }; -/** - * Trigger event to show keyboard shortcuts - * @param {BrowserWindow} browserWindow - */ -const showKeyboardShortcutsPage = (browserWindow) => { +/** Trigger event to show keyboard shortcuts */ +const showKeyboardShortcutsPage = (browserWindow: BrowserWindow) => { if (!browserWindow.isVisible()) { return; } browserWindow.webContents.send(ELECTRON_EVENTS.KEYBOARD_SHORTCUTS_PAGE); }; -// Actual auto-update listeners -const electronUpdater = (browserWindow) => ({ +/** Actual auto-update listeners */ +const electronUpdater = (browserWindow: BrowserWindow): PlatformSpecificUpdater => ({ init: () => { autoUpdater.on(ELECTRON_EVENTS.UPDATE_DOWNLOADED, (info) => { const systemMenu = Menu.getApplicationMenu(); + const updateMenuItem = systemMenu?.getMenuItemById(`update`); + const checkForUpdatesMenuItem = systemMenu?.getMenuItemById(`checkForUpdates`); + downloadedVersion = info.version; - systemMenu.getMenuItemById(`update`).visible = true; - systemMenu.getMenuItemById(`checkForUpdates`).visible = false; + + if (updateMenuItem) { + updateMenuItem.visible = true; + } + if (checkForUpdatesMenuItem) { + checkForUpdatesMenuItem.visible = false; + } if (browserWindow.isVisible()) { browserWindow.webContents.send(ELECTRON_EVENTS.UPDATE_DOWNLOADED, info.version); } else { @@ -204,29 +217,26 @@ const electronUpdater = (browserWindow) => ({ }, }); -/* - * @param {Menu} systemMenu - */ -const localizeMenuItems = (submenu, updatedLocale) => - _.map(submenu, (menu) => { - const newMenu = _.clone(menu); +const localizeMenuItems = (submenu: MenuItemConstructorOptions[], updatedLocale: Locale): MenuItemConstructorOptions[] => + submenu.map((menu) => { + const newMenu: MenuItemConstructorOptions = {...menu}; if (menu.id) { - const labelTranslation = Localize.translate(updatedLocale, `desktopApplicationMenu.${menu.id}`); + const labelTranslation = Localize.translate(updatedLocale, `desktopApplicationMenu.${menu.id}` as TranslationPaths); if (labelTranslation) { newMenu.label = labelTranslation; } } if (menu.submenu) { - newMenu.submenu = localizeMenuItems(menu.submenu, updatedLocale); + newMenu.submenu = localizeMenuItems(menu.submenu as MenuItemConstructorOptions[], updatedLocale); } return newMenu; }); -const mainWindow = () => { - let deeplinkUrl; - let browserWindow; +const mainWindow = (): Promise => { + let deeplinkUrl: string; + let browserWindow: BrowserWindow; - const loadURL = __DEV__ ? (win) => win.loadURL(`https://dev.new.expensify.com:${port}`) : serve({directory: `${__dirname}/www`}); + const loadURL = __DEV__ ? (win: BrowserWindow): Promise => win.loadURL(`https://dev.new.expensify.com:${port}`) : serve({directory: `${__dirname}/www`}); // Prod and staging set the icon in the electron-builder config, so only update it here for dev if (__DEV__) { @@ -296,7 +306,9 @@ const mainWindow = () => { if (!__DEV__) { // Modify the origin and referer for requests sent to our API webRequest.onBeforeSendHeaders(validDestinationFilters, (details, callback) => { + // @ts-expect-error need to confirm if it's used details.requestHeaders.origin = CONFIG.EXPENSIFY.URL_EXPENSIFY_CASH; + // @ts-expect-error need to confirm if it's used details.requestHeaders.referer = CONFIG.EXPENSIFY.URL_EXPENSIFY_CASH; callback({requestHeaders: details.requestHeaders}); }); @@ -304,9 +316,11 @@ const mainWindow = () => { // Modify access-control-allow-origin header and CSP for the response webRequest.onHeadersReceived(validDestinationFilters, (details, callback) => { - details.responseHeaders['access-control-allow-origin'] = [APP_DOMAIN]; - if (details.responseHeaders['content-security-policy']) { - details.responseHeaders['content-security-policy'] = _.map(details.responseHeaders['content-security-policy'], (value) => + if (details.responseHeaders) { + details.responseHeaders['access-control-allow-origin'] = [APP_DOMAIN]; + } + if (details.responseHeaders?.['content-security-policy']) { + details.responseHeaders['content-security-policy'] = details.responseHeaders['content-security-policy'].map((value) => value.startsWith('frame-ancestors') ? `${value} ${APP_DOMAIN}` : value, ); } @@ -319,7 +333,7 @@ const mainWindow = () => { browserWindow.setTitle('New Expensify'); } - const initialMenuTemplate = [ + const initialMenuTemplate: MenuItemConstructorOptions[] = [ { id: 'mainMenu', label: Localize.translate(preferredLocale, `desktopApplicationMenu.mainMenu`), @@ -404,6 +418,7 @@ const mainWindow = () => { submenu: [ { id: 'back', + // @ts-expect-error role doesn't exist but removing cause problems role: 'back', accelerator: process.platform === 'darwin' ? 'Cmd+[' : 'Shift+[', click: () => { @@ -411,6 +426,7 @@ const mainWindow = () => { }, }, { + // @ts-expect-error role doesn't exist but removing cause problems role: 'back', visible: false, accelerator: process.platform === 'darwin' ? 'Cmd+Left' : 'Shift+Left', @@ -420,6 +436,7 @@ const mainWindow = () => { }, { id: 'forward', + // @ts-expect-error role doesn't exist but removing cause problems role: 'forward', accelerator: process.platform === 'darwin' ? 'Cmd+]' : 'Shift+]', click: () => { @@ -427,6 +444,7 @@ const mainWindow = () => { }, }, { + // @ts-expect-error role doesn't exist but removing cause problems role: 'forward', visible: false, accelerator: process.platform === 'darwin' ? 'Cmd+Right' : 'Shift+Right', @@ -485,7 +503,7 @@ const mainWindow = () => { // When the user clicks a link that has target="_blank" (which is all external links) // open the default browser instead of a new electron window browserWindow.webContents.setWindowOpenHandler(({url}) => { - const denial = {action: 'deny'}; + const denial = {action: 'deny'} as const; // Make sure local urls stay in electron perimeter if (url.substr(0, 'file://'.length).toLowerCase() === 'file://') { diff --git a/desktop/start.js b/desktop/start.ts similarity index 88% rename from desktop/start.js rename to desktop/start.ts index 05a1b031350d..9efc7e04c9be 100644 --- a/desktop/start.js +++ b/desktop/start.ts @@ -1,7 +1,10 @@ #!/usr/bin/env node -const portfinder = require('portfinder'); +import {config} from 'dotenv'; +import portfinder from 'portfinder'; + const concurrently = require('concurrently'); -require('dotenv').config(); + +config(); const basePort = 8082; @@ -39,6 +42,8 @@ portfinder }, ]; + // concurrently lib problem + // eslint-disable-next-line @typescript-eslint/no-unsafe-return return concurrently(processes, { inputStream: process.stdin, prefix: 'name', diff --git a/package.json b/package.json index e3c23d4538d3..20a9418eb74d 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "build": "webpack --config config/webpack/webpack.common.js --env envFile=.env.production", "build-staging": "webpack --config config/webpack/webpack.common.js --env envFile=.env.staging", "build-adhoc": "webpack --config config/webpack/webpack.common.js --env envFile=.env.adhoc", - "desktop": "scripts/set-pusher-suffix.sh && ts-node desktop/start.js", + "desktop": "scripts/set-pusher-suffix.sh && ts-node desktop/start.ts", "desktop-build": "scripts/build-desktop.sh production", "desktop-build-staging": "scripts/build-desktop.sh staging", "createDocsRoutes": "ts-node .github/scripts/createDocsRoutes.js", diff --git a/src/types/modules/electron.d.ts b/src/types/modules/electron.d.ts index 09e33f29ba38..7b2ecaa3866d 100644 --- a/src/types/modules/electron.d.ts +++ b/src/types/modules/electron.d.ts @@ -1,11 +1,4 @@ -// TODO: Move this type to desktop/contextBridge.js once it is converted to TS -type ContextBridgeApi = { - send: (channel: string, data?: unknown) => void; - sendSync: (channel: string, data?: unknown) => unknown; - invoke: (channel: string, ...args: unknown) => Promise; - on: (channel: string, func: () => void) => void; - removeAllListeners: (channel: string) => void; -}; +import type ContextBridgeApi from '@desktop/contextBridge'; declare global { // eslint-disable-next-line @typescript-eslint/consistent-type-definitions From 39d33deebea4e1a27bf6a83cf58767755576aaf8 Mon Sep 17 00:00:00 2001 From: Ionatan Wiznia Date: Fri, 1 Mar 2024 14:14:34 -0300 Subject: [PATCH 0296/1208] 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 75cd434c4c72e8cb77ed5d8872ae136750be894b Mon Sep 17 00:00:00 2001 From: Ted Harris Date: Fri, 1 Mar 2024 17:21:01 +0000 Subject: [PATCH 0297/1208] Add relevant oldDot messages we're missing in newDot to const --- src/CONST.ts | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index d9e00f0a207e..c02d4e4bd4f5 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -561,27 +561,55 @@ const CONST = { SPLIT_REPORTID: '-2', ACTIONS: { LIMIT: 50, + // OldDot Actions render getMessage from Web-Expensify/lib/Report/Action PHP files via getMarkedReimbursedMessage in ReportActionItem.js TYPE: { + ACTIONABLEMENTIONWHISPER: 'ACTIONABLEMENTIONWHISPER', ADDCOMMENT: 'ADDCOMMENT', - APPROVED: 'APPROVED', + APPROVED: 'APPROVED', // OldDot Action + CHANGEFIELD: 'CHANGEFIELD', // OldDot Action + CHANGEPOLICY: 'CHANGEPOLICY', // OldDot Action + CHANGETYPE: 'CHANGETYPE', // OldDot Action CHRONOSOOOLIST: 'CHRONOSOOOLIST', CLOSED: 'CLOSED', CREATED: 'CREATED', + DELEGATESUBMIT: 'DELEGATESUBMIT', // OldDot Action + DELETEDACCOUNT: 'DELETEDACCOUNT', // OldDot Action + DONATION: 'DONATION', // OldDot Action + EXPORTEDTOINTEGRATION: 'EXPORTEDTOINTEGRATION', // OldDot Action + EXPORTEDTOQUICKBOOKS: 'EXPORTEDTOQUICKBOOKS', // OldDot Action + FORWARDED: 'FORWARDED', // OldDot Action HOLD: 'HOLD', IOU: 'IOU', - MARKEDREIMBURSED: 'MARKEDREIMBURSED', + INTEGRATIONSMESSAGE: 'INTEGRATIONSMESSAGE', // OldDot Action + MANAGERATTACHRECEIPT: 'MANAGERATTACHRECEIPT', // OldDot Action + MANAGERDETACHRECEIPT: 'MANAGERDETACHRECEIPT', // OldDot Action + MARKEDREIMBURSED: 'MARKEDREIMBURSED', // OldDot Action + MARKREIMBURSEDFROMINTEGRATION: 'MARKREIMBURSEDFROMINTEGRATION', // OldDot Action MODIFIEDEXPENSE: 'MODIFIEDEXPENSE', MOVED: 'MOVED', + OUTDATEDBANKACCOUNT: 'OUTDATEDBANKACCOUNT', // OldDot Action + REIMBURSEMENTACHBOUNCE: 'REIMBURSEMENTACHBOUNCE', // OldDot Action + REIMBURSEMENTACHCANCELLED: 'REIMBURSEMENTACHCANCELLED', // OldDot Action + REIMBURSEMENTACCOUNTCHANGED: 'REIMBURSEMENTACCOUNTCHANGED', // OldDot Action + REIMBURSEMENTDELAYED: 'REIMBURSEMENTDELAYED', // OldDot Action REIMBURSEMENTQUEUED: 'REIMBURSEMENTQUEUED', REIMBURSEMENTDEQUEUED: 'REIMBURSEMENTDEQUEUED', + REIMBURSEMENTREQUESTED: 'REIMBURSEMENTREQUESTED', // OldDot Action + REIMBURSEMENTSETUP: 'REIMBURSEMENTSETUP', // OldDot Action RENAMED: 'RENAMED', REPORTPREVIEW: 'REPORTPREVIEW', + SELECTEDFORRANDOMAUDIT: 'SELECTEDFORRANDOMAUDIT', // OldDot Action + SHARE: 'SHARE', // OldDot Action + STRIPEPAID: 'STRIPEPAID', // OldDot Action SUBMITTED: 'SUBMITTED', + TAKECONTROL: 'TAKECONTROL', // OldDot Action TASKCANCELLED: 'TASKCANCELLED', TASKCOMPLETED: 'TASKCOMPLETED', TASKEDITED: 'TASKEDITED', TASKREOPENED: 'TASKREOPENED', - ACTIONABLEMENTIONWHISPER: 'ACTIONABLEMENTIONWHISPER', + UNAPPROVED: 'UNAPPROVED', // OldDot Action + UNHOLD: 'UNHOLD', + UNSHARE: 'UNSHARE', // OldDot Action POLICYCHANGELOG: { ADD_APPROVER_RULE: 'POLICYCHANGELOG_ADD_APPROVER_RULE', ADD_BUDGET: 'POLICYCHANGELOG_ADD_BUDGET', @@ -655,8 +683,6 @@ const CONST = { LEAVE_ROOM: 'LEAVEROOM', UPDATE_ROOM_DESCRIPTION: 'UPDATEROOMDESCRIPTION', }, - UNAPPROVED: 'UNAPPROVED', - UNHOLD: 'UNHOLD', }, THREAD_DISABLED: ['CREATED'], }, From 9c8833f407324b061f0b079a799bb25225997319 Mon Sep 17 00:00:00 2001 From: Ted Harris Date: Fri, 1 Mar 2024 17:21:18 +0000 Subject: [PATCH 0298/1208] Render oldDot messages from const --- src/pages/home/report/ReportActionItem.js | 27 ++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index b07421a66479..8859d0a7c060 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -465,8 +465,33 @@ function ReportActionItem(props) { } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE) { children = ; } else if ([ + CONST.REPORT.ACTIONS.TYPE.CHANGEFIELD, + CONST.REPORT.ACTIONS.TYPE.CHANGEPOLICY, + CONST.REPORT.ACTIONS.TYPE.CHANGETYPE, + CONST.REPORT.ACTIONS.TYPE.DELEGATESUBMIT, + CONST.REPORT.ACTIONS.TYPE.DELETEDACCOUNT, + CONST.REPORT.ACTIONS.TYPE.DONATION, + CONST.REPORT.ACTIONS.TYPE.EXPORTEDTOINTEGRATION, + CONST.REPORT.ACTIONS.TYPE.EXPORTEDTOQUICKBOOKS, + CONST.REPORT.ACTIONS.TYPE.FORWARDED, + CONST.REPORT.ACTIONS.TYPE.INTEGRATIONSMESSAGE, + CONST.REPORT.ACTIONS.TYPE.MANAGERATTACHRECEIPT, + CONST.REPORT.ACTIONS.TYPE.MANAGERDETACHRECEIPT, CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED, - CONST.REPORT.ACTIONS.TYPE.UNAPPROVED + CONST.REPORT.ACTIONS.TYPE.MARKREIMBURSEDFROMINTEGRATION, + CONST.REPORT.ACTIONS.TYPE.OUTDATEDBANKACCOUNT, + CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTACHBOUNCE, + CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTACHCANCELLED, + CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTACCOUNTCHANGED, + CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTDELAYED, + CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTREQUESTED, + CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTSETUP, + CONST.REPORT.ACTIONS.TYPE.SELECTEDFORRANDOMAUDIT, + CONST.REPORT.ACTIONS.TYPE.SHARE, + CONST.REPORT.ACTIONS.TYPE.STRIPEPAID, + CONST.REPORT.ACTIONS.TYPE.TAKECONTROL, + CONST.REPORT.ACTIONS.TYPE.UNAPPROVED, + CONST.REPORT.ACTIONS.TYPE.UNSHARE ].includes(props.action.actionName)) { // This handles all historical actions from OldDot that we just want to display the message text children = ; From 53f4289ce51a6bd375e841b098043fd166fafd78 Mon Sep 17 00:00:00 2001 From: Ted Harris Date: Fri, 1 Mar 2024 17:21:59 +0000 Subject: [PATCH 0299/1208] Update CONST.ts --- src/CONST.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CONST.ts b/src/CONST.ts index c02d4e4bd4f5..9e6cf4990188 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -565,7 +565,7 @@ const CONST = { TYPE: { ACTIONABLEMENTIONWHISPER: 'ACTIONABLEMENTIONWHISPER', ADDCOMMENT: 'ADDCOMMENT', - APPROVED: 'APPROVED', // OldDot Action + APPROVED: 'APPROVED', CHANGEFIELD: 'CHANGEFIELD', // OldDot Action CHANGEPOLICY: 'CHANGEPOLICY', // OldDot Action CHANGETYPE: 'CHANGETYPE', // OldDot Action From 3053b96a9432b9f5161bcfd3a09699e73f8fc86a Mon Sep 17 00:00:00 2001 From: Ionatan Wiznia Date: Fri, 1 Mar 2024 14:25:28 -0300 Subject: [PATCH 0300/1208] 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 9eab6033922d3cade18acc4c1f827776f4c79871 Mon Sep 17 00:00:00 2001 From: Andrew Gable Date: Fri, 1 Mar 2024 11:39:47 -0700 Subject: [PATCH 0301/1208] Apply suggestions from code review Co-authored-by: Rory Abraham <47436092+roryabraham@users.noreply.github.com> Co-authored-by: Manan --- src/pages/LogOutPreviousUserPage.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/pages/LogOutPreviousUserPage.tsx b/src/pages/LogOutPreviousUserPage.tsx index a10988945c4e..522883c38881 100644 --- a/src/pages/LogOutPreviousUserPage.tsx +++ b/src/pages/LogOutPreviousUserPage.tsx @@ -29,11 +29,11 @@ type LogOutPreviousUserPageProps = LogOutPreviousUserPageOnyxProps & StackScreen // // This component should not do any other navigation as that handled in App.setUpPoliciesAndNavigate function LogOutPreviousUserPage({session, route, account}: LogOutPreviousUserPageProps) { - const initUrl = useContext(InitialUrlContext); + const initUrlFromOldApp = useContext(InitialUrlContext); useEffect(() => { Linking.getInitialURL().then((url) => { const sessionEmail = session?.email; - const transitionURL = NativeModules.HybridAppModule ? CONST.DEEPLINK_BASE_URL + initUrl : url; + const transitionURL = NativeModules.HybridAppModule ? `${CONST.DEEPLINK_BASE_URL}${initUrl}` : url; const isLoggingInAsNewUser = SessionUtils.isLoggingInAsNewUser(transitionURL ?? undefined, sessionEmail); if (isLoggingInAsNewUser) { @@ -71,7 +71,10 @@ function LogOutPreviousUserPage({session, route, account}: LogOutPreviousUserPag LogOutPreviousUserPage.displayName = 'LogOutPreviousUserPage'; export default withOnyx({ - account: {key: ONYXKEYS.ACCOUNT}, + isAccountLoading: { + key: ONYXKEYS.ACCOUNT, + selector: (account) => account?.isLoading, + }, session: { key: ONYXKEYS.SESSION, }, From 1b685c9eefb92cd25ea3b2469fa42d973b061c2e Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Fri, 1 Mar 2024 16:39:03 -0500 Subject: [PATCH 0302/1208] 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 0303/1208] 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 ce048d53cf21d17eb4ae0ce7ca4031ef9f16e699 Mon Sep 17 00:00:00 2001 From: Jayesh Mangwani Date: Sat, 2 Mar 2024 12:06:35 +0530 Subject: [PATCH 0304/1208] revert the WALLET_ADDITIONAL_DETAILS key name --- src/ONYXKEYS.ts | 4 ++-- src/pages/EnablePayments/AdditionalDetailsStep.tsx | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index afcbd059a9f8..389c270f7510 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -399,8 +399,8 @@ const ONYXKEYS = { EXIT_SURVEY_RESPONSE_FORM_DRAFT: 'exitSurveyResponseFormDraft', IDOLOGY_QUESTIONS_FORM: 'idologyQuestionsForm', IDOLOGY_QUESTIONS_FORM_DRAFT: 'idologyQuestionsFormDraft', - WALLET_ADDITIONAL_DETAILS: 'walletAdditionalDetailsForm', - WALLET_ADDITIONAL_DETAILS_DRAFT: 'walletAdditionalDetailsFormDraft', + WALLET_ADDITIONAL_DETAILS: 'walletAdditionalDetails', + WALLET_ADDITIONAL_DETAILS_DRAFT: 'walletAdditionalDetailsDraft', }, } as const; diff --git a/src/pages/EnablePayments/AdditionalDetailsStep.tsx b/src/pages/EnablePayments/AdditionalDetailsStep.tsx index cdf166536c5f..0b663261865f 100644 --- a/src/pages/EnablePayments/AdditionalDetailsStep.tsx +++ b/src/pages/EnablePayments/AdditionalDetailsStep.tsx @@ -237,6 +237,7 @@ AdditionalDetailsStep.displayName = 'AdditionalDetailsStep'; export default withCurrentUserPersonalDetails( withOnyx({ + // @ts-expect-error: ONYXKEYS.WALLET_ADDITIONAL_DETAILS is conflicting with ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS walletAdditionalDetails: { key: ONYXKEYS.WALLET_ADDITIONAL_DETAILS, }, From a3115ba8ba75d23895af7eb1d4e320d3976641ae Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Sun, 3 Mar 2024 17:23:19 +0530 Subject: [PATCH 0305/1208] fix failing typechecks. Signed-off-by: Krishna Gupta --- src/libs/actions/Session/index.ts | 2 +- src/pages/LogInWithShortLivedAuthTokenPage.tsx | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index 76f335a3bec0..5abe235ca029 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -855,7 +855,7 @@ function handleExitToNavigation(exitTo: Routes | HybridAppRoute) { InteractionManager.runAfterInteractions(() => { waitForUserSignIn().then(() => { Navigation.waitForProtectedRoutes().then(() => { - const url = NativeModules.HybridAppModule ? Navigation.parseHybridAppUrl(exitTo) : exitTo; + const url = NativeModules.HybridAppModule ? Navigation.parseHybridAppUrl(exitTo) : (exitTo as Routes); Navigation.navigate(url); }); }); diff --git a/src/pages/LogInWithShortLivedAuthTokenPage.tsx b/src/pages/LogInWithShortLivedAuthTokenPage.tsx index 4e7372f10dc6..17cc971a075f 100644 --- a/src/pages/LogInWithShortLivedAuthTokenPage.tsx +++ b/src/pages/LogInWithShortLivedAuthTokenPage.tsx @@ -17,6 +17,7 @@ import Navigation from '@libs/Navigation/Navigation'; import type {PublicScreensParamList} from '@libs/Navigation/types'; import * as Session from '@userActions/Session'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {Route as Routes} from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {Account} from '@src/types/onyx'; @@ -51,7 +52,7 @@ function LogInWithShortLivedAuthTokenPage({route, account}: LogInWithShortLivedA if (exitTo) { Navigation.isNavigationReady().then(() => { - const url = NativeModules.HybridAppModule ? Navigation.parseHybridAppUrl(exitTo) : exitTo; + const url = NativeModules.HybridAppModule ? Navigation.parseHybridAppUrl(exitTo) : (exitTo as Routes); Navigation.navigate(url); }); } 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 0306/1208] 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:29:39 +0530 Subject: [PATCH 0307/1208] Update IconButton to use PressableWithFeedback --- src/components/VideoPlayer/IconButton.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/VideoPlayer/IconButton.js b/src/components/VideoPlayer/IconButton.js index eaf7ec532fb2..5af9bc87e66f 100644 --- a/src/components/VideoPlayer/IconButton.js +++ b/src/components/VideoPlayer/IconButton.js @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import Icon from '@components/Icon'; -import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; +import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import Tooltip from '@components/Tooltip'; import useThemeStyles from '@hooks/useThemeStyles'; import stylePropTypes from '@styles/stylePropTypes'; @@ -43,7 +43,7 @@ function IconButton({src, fill, onPress, style, hoverStyle, tooltipText, small, text={tooltipText} shouldForceRenderingBelow={shouldForceRenderingTooltipBelow} > - - + ); } From d6d6c89a2e013af7374de322808cef05d1d46eb5 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Mon, 4 Mar 2024 07:41:21 +0530 Subject: [PATCH 0308/1208] 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 3ad7a4f59cd23558b7d70422ec5ba15f1f3a2a36 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Mon, 4 Mar 2024 07:42:44 +0530 Subject: [PATCH 0309/1208] fix route iou type for send. Signed-off-by: Krishna Gupta --- src/pages/iou/request/step/IOURequestStepParticipants.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.js b/src/pages/iou/request/step/IOURequestStepParticipants.js index 954983808c80..38af1af8302d 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.js +++ b/src/pages/iou/request/step/IOURequestStepParticipants.js @@ -118,7 +118,14 @@ function IOURequestStepParticipants({ const goToNextStep = useCallback( (selectedIouType) => { const isSplit = selectedIouType === CONST.IOU.TYPE.SPLIT; - const nextStepIOUType = !isSplit && iouType !== CONST.IOU.TYPE.REQUEST ? CONST.IOU.TYPE.REQUEST : iouType; + let nextStepIOUType; + + if (isSplit && iouType !== CONST.IOU.TYPE.REQUEST) { + nextStepIOUType = CONST.IOU.TYPE.SPLIT; + } else { + nextStepIOUType = iouType === CONST.IOU.TYPE.SEND ? CONST.IOU.TYPE.SEND : CONST.IOU.TYPE.REQUEST; + } + IOU.setMoneyRequestTag(transactionID, ''); IOU.setMoneyRequestCategory(transactionID, ''); Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(nextStepIOUType, transactionID, selectedReportID.current || reportID)); From 7517fbf5cf3020add121de83952989dc47c57b9e Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Mon, 4 Mar 2024 08:20:26 +0530 Subject: [PATCH 0310/1208] 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 0311/1208] 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 e02b55a5f95fe10e4f22a28b16f20aaf75977c5f Mon Sep 17 00:00:00 2001 From: hurali97 Date: Mon, 4 Mar 2024 13:02:21 +0500 Subject: [PATCH 0312/1208] refactor: use default policy room chat types from CONST --- src/CONST.ts | 17 ++++++++++------- src/libs/ReportUtils.ts | 3 +-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index bb167bb5d8d8..78c562ea2cde 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -37,11 +37,20 @@ const keyInputRightArrow = KeyCommand?.constants?.keyInputRightArrow ?? 'keyInpu // describes if a shortcut key can cause navigation const KEYBOARD_SHORTCUT_NAVIGATION_TYPE = 'NAVIGATION_SHORTCUT'; +const chatTypes = { + POLICY_ANNOUNCE: 'policyAnnounce', + POLICY_ADMINS: 'policyAdmins', + DOMAIN_ALL: 'domainAll', + POLICY_ROOM: 'policyRoom', + POLICY_EXPENSE_CHAT: 'policyExpenseChat', +} as const; + // Explicit type annotation is required const cardActiveStates: number[] = [2, 3, 4, 7]; const CONST = { MERGED_ACCOUNT_PREFIX: 'MERGED_', + DEFAULT_POLICY_ROOM_CHAT_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL], ANDROID_PACKAGE_NAME, ANIMATED_TRANSITION: 300, ANIMATED_TRANSITION_FROM_VALUE: 100, @@ -684,13 +693,7 @@ const CONST = { IOU: 'iou', TASK: 'task', }, - CHAT_TYPE: { - POLICY_ANNOUNCE: 'policyAnnounce', - POLICY_ADMINS: 'policyAdmins', - DOMAIN_ALL: 'domainAll', - POLICY_ROOM: 'policyRoom', - POLICY_EXPENSE_CHAT: 'policyExpenseChat', - }, + CHAT_TYPE: chatTypes, WORKSPACE_CHAT_ROOMS: { ANNOUNCE: '#announce', ADMINS: '#admins', diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 37fee63780fb..93d2fd5da37d 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -774,12 +774,11 @@ function isAnnounceRoom(report: OnyxEntry): boolean { return getChatType(report) === CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE; } -const chatTypes = [CONST.REPORT.CHAT_TYPE.POLICY_ADMINS, CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE, CONST.REPORT.CHAT_TYPE.DOMAIN_ALL]; /** * Whether the provided report is a default room */ function isDefaultRoom(report: OnyxEntry): boolean { - return chatTypes.some((type) => type === getChatType(report)); + return CONST.DEFAULT_POLICY_ROOM_CHAT_TYPES.some((type) => type === getChatType(report)); } /** From fc05a9f5fa50bb94c3ca18b7fa79bac208070ebe Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Mon, 4 Mar 2024 17:06:01 +0800 Subject: [PATCH 0313/1208] don't include selected value in recent and all --- src/pages/EditReportFieldDropdownPage.tsx | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/pages/EditReportFieldDropdownPage.tsx b/src/pages/EditReportFieldDropdownPage.tsx index ddd5330e389e..b21a9d685062 100644 --- a/src/pages/EditReportFieldDropdownPage.tsx +++ b/src/pages/EditReportFieldDropdownPage.tsx @@ -62,13 +62,23 @@ function EditReportFieldDropdownPage({fieldName, onSubmit, fieldID, fieldValue, ]; } - const restOfOptions = fieldOptions.filter((option) => !recentlyUsedOptions.includes(option)); - setHeaderMessage(!restOfOptions.length && !recentlyUsedOptions.length ? translate('common.noResultsFound') : ''); + const selectedValue = fieldValue; + + setHeaderMessage(!fieldOptions.length && !recentlyUsedOptions.length ? translate('common.noResultsFound') : ''); return [ + { + shouldShow: false, + data: [{ + text: selectedValue, + keyForList: selectedValue, + searchText: selectedValue, + tooltipText: selectedValue, + }], + }, { title: translate('common.recents'), shouldShow: recentlyUsedOptions.length > 0, - data: recentlyUsedOptions.map((option) => ({ + data: recentlyUsedOptions.filter((option) => option !== selectedValue).map((option) => ({ text: option, keyForList: option, searchText: option, @@ -77,8 +87,8 @@ function EditReportFieldDropdownPage({fieldName, onSubmit, fieldID, fieldValue, }, { title: translate('common.all'), - shouldShow: restOfOptions.length > 0, - data: restOfOptions.map((option) => ({ + shouldShow: fieldOptions.length > 0, + data: fieldOptions.filter((option) => option !== selectedValue).map((option) => ({ text: option, keyForList: option, searchText: option, From 24309d60de21e8c71775262782c45ce751a5cb7b Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Mon, 4 Mar 2024 17:10:56 +0800 Subject: [PATCH 0314/1208] fix lint --- src/pages/EditReportFieldDropdownPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/EditReportFieldDropdownPage.tsx b/src/pages/EditReportFieldDropdownPage.tsx index b21a9d685062..1b6631b60883 100644 --- a/src/pages/EditReportFieldDropdownPage.tsx +++ b/src/pages/EditReportFieldDropdownPage.tsx @@ -96,7 +96,7 @@ function EditReportFieldDropdownPage({fieldName, onSubmit, fieldID, fieldValue, })), }, ]; - }, [fieldOptions, recentlyUsedOptions, searchValue, translate]); + }, [fieldValue, fieldOptions, recentlyUsedOptions, searchValue, translate]); return ( Date: Mon, 4 Mar 2024 17:16:38 +0800 Subject: [PATCH 0315/1208] prettier --- src/pages/EditReportFieldDropdownPage.tsx | 42 +++++++++++++---------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/src/pages/EditReportFieldDropdownPage.tsx b/src/pages/EditReportFieldDropdownPage.tsx index 1b6631b60883..0fe7511b5731 100644 --- a/src/pages/EditReportFieldDropdownPage.tsx +++ b/src/pages/EditReportFieldDropdownPage.tsx @@ -68,32 +68,38 @@ function EditReportFieldDropdownPage({fieldName, onSubmit, fieldID, fieldValue, return [ { shouldShow: false, - data: [{ - text: selectedValue, - keyForList: selectedValue, - searchText: selectedValue, - tooltipText: selectedValue, - }], + data: [ + { + text: selectedValue, + keyForList: selectedValue, + searchText: selectedValue, + tooltipText: selectedValue, + }, + ], }, { title: translate('common.recents'), shouldShow: recentlyUsedOptions.length > 0, - data: recentlyUsedOptions.filter((option) => option !== selectedValue).map((option) => ({ - text: option, - keyForList: option, - searchText: option, - tooltipText: option, - })), + data: recentlyUsedOptions + .filter((option) => option !== selectedValue) + .map((option) => ({ + text: option, + keyForList: option, + searchText: option, + tooltipText: option, + })), }, { title: translate('common.all'), shouldShow: fieldOptions.length > 0, - data: fieldOptions.filter((option) => option !== selectedValue).map((option) => ({ - text: option, - keyForList: option, - searchText: option, - tooltipText: option, - })), + data: fieldOptions + .filter((option) => option !== selectedValue) + .map((option) => ({ + text: option, + keyForList: option, + searchText: option, + tooltipText: option, + })), }, ]; }, [fieldValue, fieldOptions, recentlyUsedOptions, searchValue, translate]); From d180fd24984f0c92a17b077e1009e048017b9899 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Mon, 4 Mar 2024 14:48:11 +0530 Subject: [PATCH 0316/1208] 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 0317/1208] 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 0318/1208] 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 0319/1208] 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 7cbca08a4bd4b93ec49f6277df41f476744a33e7 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Mon, 4 Mar 2024 15:55:46 +0500 Subject: [PATCH 0320/1208] revert: bring back localize.translateLocal --- src/libs/PersonalDetailsUtils.ts | 2 +- src/libs/ReportUtils.ts | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/libs/PersonalDetailsUtils.ts b/src/libs/PersonalDetailsUtils.ts index 97e2dc91492b..5a3de690d8f8 100644 --- a/src/libs/PersonalDetailsUtils.ts +++ b/src/libs/PersonalDetailsUtils.ts @@ -41,7 +41,7 @@ function getDisplayNameOrDefault(passedPersonalDetails?: Partial, policies: OnyxCollection | undefined | EmptyObject, returnEmptyIfNotFound = false, policy: OnyxEntry | undefined = undefined): string { - const noPolicyFound = returnEmptyIfNotFound ? '' : CommonTranslationUtils.unavailableWorkspaceText(); + const noPolicyFound = returnEmptyIfNotFound ? '' : Localize.translateLocal('workspace.common.unavailable'); if (isEmptyObject(report)) { return noPolicyFound; } if ((!allPolicies || Object.keys(allPolicies).length === 0) && !report?.policyName) { - return CommonTranslationUtils.unavailableWorkspaceText(); + return Localize.translateLocal('workspace.common.unavailable'); } const finalPolicy = policy ?? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; @@ -1647,7 +1647,7 @@ function getDisplayNameForParticipant(accountID?: number, shouldUseShortForm = f const longName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails, formattedLogin, shouldFallbackToHidden); // If the user's personal details (first name) should be hidden, make sure we return "hidden" instead of the short name - if (shouldFallbackToHidden && longName === CommonTranslationUtils.hiddenText()) { + if (shouldFallbackToHidden && longName === Localize.translateLocal('common.hidden')) { return longName; } @@ -2508,13 +2508,13 @@ function getReportName(report: OnyxEntry, policy: OnyxEntry = nu } if (parentReportAction?.message?.[0]?.isDeletedParentAction) { - return CommonTranslationUtils.deletedMessageText(); + return Localize.translateLocal('parentReportAction.deletedMessage'); } const isAttachment = ReportActionsUtils.isReportActionAttachment(!isEmptyObject(parentReportAction) ? parentReportAction : null); const parentReportActionMessage = (parentReportAction?.message?.[0]?.text ?? '').replace(/(\r\n|\n|\r)/gm, ' '); if (isAttachment && parentReportActionMessage) { - return `[${CommonTranslationUtils.attachmentText()}]`; + return `[${Localize.translateLocal('common.attachment')}]`; } if ( parentReportAction?.message?.[0]?.moderationDecision?.decision === CONST.MODERATION.MODERATOR_DECISION_PENDING_HIDE || @@ -2530,7 +2530,7 @@ function getReportName(report: OnyxEntry, policy: OnyxEntry = nu } if (isTaskReport(report) && isCanceledTaskReport(report, parentReportAction)) { - return CommonTranslationUtils.deletedTaskText(); + return Localize.translateLocal('parentReportAction.deletedTask'); } if (isChatRoom(report) || isTaskReport(report)) { @@ -2546,7 +2546,7 @@ function getReportName(report: OnyxEntry, policy: OnyxEntry = nu } if (isArchivedRoom(report)) { - formattedName += ` (${CommonTranslationUtils.archivedText()})`; + formattedName += ` (${Localize.translateLocal('common.archived')})`; } if (formattedName) { From 4df1d3da5dd9f6cb7862c8c62b105688cbbe9274 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Mon, 4 Mar 2024 16:51:30 +0500 Subject: [PATCH 0321/1208] feat: add cache for translated values in Localize --- src/libs/Localize/index.ts | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/libs/Localize/index.ts b/src/libs/Localize/index.ts index 64d07897aa8a..9cadc352ca9a 100644 --- a/src/libs/Localize/index.ts +++ b/src/libs/Localize/index.ts @@ -93,11 +93,42 @@ function translate(desiredLanguage: 'en' | 'es' | throw new Error(`${phraseKey} was not found in the default language`); } +/** + * Map to store translated values for each locale + * This is used to avoid translating the same phrase multiple times. + * + * The data is stored in the following format: + * + * { + * "name_en": "Name", + * "name_es": "Nombre", + * } + * + * Note: We are not storing any translated values for phrases with variables, + * as they have higher chance of being unique, so we'll end up wasting space + * in our cache. + */ +const translatedValues = new Map(); + /** * Uses the locale in this file updated by the Onyx subscriber. */ function translateLocal(phrase: TKey, ...variables: PhraseParameters>) { - return translate(BaseLocaleListener.getPreferredLocale(), phrase, ...variables); + const preferredLocale = BaseLocaleListener.getPreferredLocale(); + const key = `${phrase}_${preferredLocale}`; + const isVariablesEmpty = variables.length === 0; + + // If the phrase is already translated and there are no variables, return the translated value + if (translatedValues.has(key) && isVariablesEmpty) { + return translatedValues.get(key) as string; + } + const translatedText = translate(preferredLocale, phrase, ...variables); + + // We don't want to store translated values for phrases with variables + if (isVariablesEmpty) { + translatedValues.set(key, translatedText); + } + return translatedText; } /** From 8215b5377db03124eed0e166545c5cd2f9d16605 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Mon, 4 Mar 2024 12:57:26 +0100 Subject: [PATCH 0322/1208] 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 55eb5d2f765099561de1a8720e2cb1345ca013e3 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Mon, 4 Mar 2024 17:13:32 +0500 Subject: [PATCH 0323/1208] refactor: delete common translation utils --- src/libs/CommonTranslationUtils.ts | 49 ------------------------------ 1 file changed, 49 deletions(-) delete mode 100644 src/libs/CommonTranslationUtils.ts diff --git a/src/libs/CommonTranslationUtils.ts b/src/libs/CommonTranslationUtils.ts deleted file mode 100644 index 80a06e51ce9f..000000000000 --- a/src/libs/CommonTranslationUtils.ts +++ /dev/null @@ -1,49 +0,0 @@ -import Onyx from 'react-native-onyx'; -import ONYXKEYS from '@src/ONYXKEYS'; -import * as Localize from './Localize'; - -/** - * This file contains common translations that are used in multiple places in the app. - * This is done to avoid duplicate translations and to keep the translations consistent. - * This also allows us to not repeatedly translate the same string which may happen due - * to translations being done for eg, in a loop. - * - * This was identified as part of a performance audit. - * details: https://github.com/Expensify/App/issues/35234#issuecomment-1926911643 - */ - -let deletedTaskText = ''; -let deletedMessageText = ''; -let attachmentText = ''; -let archivedText = ''; -let hiddenText = ''; -let unavailableWorkspaceText = ''; - -function isTranslationAvailable() { - return deletedTaskText && deletedMessageText && attachmentText && archivedText && hiddenText && unavailableWorkspaceText; -} - -Onyx.connect({ - key: ONYXKEYS.NVP_PREFERRED_LOCALE, - callback: (val) => { - if (!val && isTranslationAvailable()) { - return; - } - - deletedTaskText = Localize.translateLocal('parentReportAction.deletedTask'); - deletedMessageText = Localize.translateLocal('parentReportAction.deletedMessage'); - attachmentText = Localize.translateLocal('common.attachment'); - archivedText = Localize.translateLocal('common.archived'); - hiddenText = Localize.translateLocal('common.hidden'); - unavailableWorkspaceText = Localize.translateLocal('workspace.common.unavailable'); - }, -}); - -export default { - deletedTaskText: () => deletedTaskText, - deletedMessageText: () => deletedMessageText, - attachmentText: () => attachmentText, - archivedText: () => archivedText, - hiddenText: () => hiddenText, - unavailableWorkspaceText: () => unavailableWorkspaceText, -}; From 6f58e2f765e9860d27d47e3968fa29d5c41200b8 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Mon, 4 Mar 2024 20:20:44 +0800 Subject: [PATCH 0324/1208] conditionally add the section when it's not empty --- src/pages/EditReportFieldDropdownPage.tsx | 62 +++++++++++++---------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/src/pages/EditReportFieldDropdownPage.tsx b/src/pages/EditReportFieldDropdownPage.tsx index 0fe7511b5731..8831cca12197 100644 --- a/src/pages/EditReportFieldDropdownPage.tsx +++ b/src/pages/EditReportFieldDropdownPage.tsx @@ -65,8 +65,14 @@ function EditReportFieldDropdownPage({fieldName, onSubmit, fieldID, fieldValue, const selectedValue = fieldValue; setHeaderMessage(!fieldOptions.length && !recentlyUsedOptions.length ? translate('common.noResultsFound') : ''); - return [ - { + + const newSections = []; + + const filteredRecentlyUsedOptions = recentlyUsedOptions.filter((option) => option !== selectedValue); + const filteredFieldOptions = fieldOptions.filter((option) => option !== selectedValue); + + if (selectedValue) { + newSections.push({ shouldShow: false, data: [ { @@ -76,32 +82,36 @@ function EditReportFieldDropdownPage({fieldName, onSubmit, fieldID, fieldValue, tooltipText: selectedValue, }, ], - }, - { + }); + } + + if (filteredRecentlyUsedOptions.length > 0) { + newSections.push({ title: translate('common.recents'), - shouldShow: recentlyUsedOptions.length > 0, - data: recentlyUsedOptions - .filter((option) => option !== selectedValue) - .map((option) => ({ - text: option, - keyForList: option, - searchText: option, - tooltipText: option, - })), - }, - { + shouldShow: true, + data: filteredRecentlyUsedOptions.map((option) => ({ + text: option, + keyForList: option, + searchText: option, + tooltipText: option, + })), + }); + } + + if (filteredFieldOptions.length > 0) { + newSections.push({ title: translate('common.all'), - shouldShow: fieldOptions.length > 0, - data: fieldOptions - .filter((option) => option !== selectedValue) - .map((option) => ({ - text: option, - keyForList: option, - searchText: option, - tooltipText: option, - })), - }, - ]; + shouldShow: true, + data: filteredFieldOptions.map((option) => ({ + text: option, + keyForList: option, + searchText: option, + tooltipText: option, + })), + }); + } + + return newSections; }, [fieldValue, fieldOptions, recentlyUsedOptions, searchValue, translate]); return ( From e643960c7ef54a8e2451f052c926ab664be127a4 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Mon, 4 Mar 2024 17:43:24 +0500 Subject: [PATCH 0325/1208] refactor: avoid multiple cache lookups --- src/libs/Localize/index.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/libs/Localize/index.ts b/src/libs/Localize/index.ts index 9cadc352ca9a..ad5e93af24ee 100644 --- a/src/libs/Localize/index.ts +++ b/src/libs/Localize/index.ts @@ -118,9 +118,13 @@ function translateLocal(phrase: TKey, ...variable const key = `${phrase}_${preferredLocale}`; const isVariablesEmpty = variables.length === 0; - // If the phrase is already translated and there are no variables, return the translated value - if (translatedValues.has(key) && isVariablesEmpty) { - return translatedValues.get(key) as string; + // Directly access and assign the translated value from the cache, instead of + // going through map.has() and map.get() to avoid multiple lookups. + const valueFromCache = translatedValues.get(key); + + // If the phrase is already translated, return the translated value + if (valueFromCache) { + return valueFromCache; } const translatedText = translate(preferredLocale, phrase, ...variables); From f3d6f1f73255fe186eea218f6600365dbfe06e41 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Mon, 4 Mar 2024 17:43:39 +0500 Subject: [PATCH 0326/1208] fix: lint --- src/libs/PersonalDetailsUtils.ts | 1 - src/libs/ReportUtils.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/libs/PersonalDetailsUtils.ts b/src/libs/PersonalDetailsUtils.ts index c5de7456629d..391cabef0790 100644 --- a/src/libs/PersonalDetailsUtils.ts +++ b/src/libs/PersonalDetailsUtils.ts @@ -5,7 +5,6 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PersonalDetails, PersonalDetailsList, PrivatePersonalDetails} from '@src/types/onyx'; import type {OnyxData} from '@src/types/onyx/Request'; -import CommonTranslationUtils from './CommonTranslationUtils'; import * as LocalePhoneNumber from './LocalePhoneNumber'; import * as Localize from './Localize'; import * as UserUtils from './UserUtils'; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 1c64deff881e..f7d49389e200 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -51,7 +51,6 @@ import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type IconAsset from '@src/types/utils/IconAsset'; import * as CollectionUtils from './CollectionUtils'; -import CommonTranslationUtils from './CommonTranslationUtils'; import * as CurrencyUtils from './CurrencyUtils'; import DateUtils from './DateUtils'; import isReportMessageAttachment from './isReportMessageAttachment'; From 46d1bf52143d4647dd74737e05cc819b044c7980 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 4 Mar 2024 14:25:29 +0100 Subject: [PATCH 0327/1208] Improve typing --- desktop/main.ts | 49 +++++++++++++++++++----------------------------- desktop/start.ts | 4 +--- 2 files changed, 20 insertions(+), 33 deletions(-) diff --git a/desktop/main.ts b/desktop/main.ts index 071ddb7c7a41..cbc12d9d2608 100644 --- a/desktop/main.ts +++ b/desktop/main.ts @@ -1,5 +1,5 @@ -import {app, BrowserWindow, clipboard, dialog, ipcMain, Menu, MenuItem, shell} from 'electron'; -import type {BrowserView, MenuItemConstructorOptions, WebContents, WebviewTag} from 'electron'; +import {app, BrowserWindow, clipboard, dialog, ipcMain, Menu, shell} from 'electron'; +import type {BrowserView, MenuItem, MenuItemConstructorOptions, WebContents, WebviewTag} from 'electron'; import contextMenu from 'electron-context-menu'; import log from 'electron-log'; import type {ElectronLog} from 'electron-log'; @@ -62,22 +62,21 @@ function createContextMenu(preferredLocale: Locale = LOCALES.DEFAULT): () => voi paste: Localize.translate(preferredLocale, 'desktopApplicationMenu.paste'), copy: Localize.translate(preferredLocale, 'desktopApplicationMenu.copy'), }, - append: (defaultActions, parameters, browserWindow) => - [ - new MenuItem({ - // Only enable the menu item for Editable context which supports paste - visible: parameters.isEditable && parameters.editFlags.canPaste, - role: 'pasteAndMatchStyle', - accelerator: DESKTOP_SHORTCUT_ACCELERATOR.PASTE_AND_MATCH_STYLE, - label: Localize.translate(preferredLocale, 'desktopApplicationMenu.pasteAndMatchStyle'), - }), - new MenuItem({ - label: Localize.translate(preferredLocale, 'desktopApplicationMenu.pasteAsPlainText'), - visible: parameters.isEditable && parameters.editFlags.canPaste && clipboard.readText().length > 0, - accelerator: DESKTOP_SHORTCUT_ACCELERATOR.PASTE_AS_PLAIN_TEXT, - click: () => pasteAsPlainText(browserWindow), - }), - ] as unknown as MenuItemConstructorOptions[], + append: (defaultActions, parameters, browserWindow) => [ + { + // Only enable the menu item for Editable context which supports paste + visible: parameters.isEditable && parameters.editFlags.canPaste, + role: 'pasteAndMatchStyle', + accelerator: DESKTOP_SHORTCUT_ACCELERATOR.PASTE_AND_MATCH_STYLE, + label: Localize.translate(preferredLocale, 'desktopApplicationMenu.pasteAndMatchStyle'), + }, + { + label: Localize.translate(preferredLocale, 'desktopApplicationMenu.pasteAsPlainText'), + visible: parameters.isEditable && parameters.editFlags.canPaste && clipboard.readText().length > 0, + accelerator: DESKTOP_SHORTCUT_ACCELERATOR.PASTE_AS_PLAIN_TEXT, + click: () => pasteAsPlainText(browserWindow), + }, + ], }); } @@ -306,10 +305,6 @@ const mainWindow = (): Promise => { if (!__DEV__) { // Modify the origin and referer for requests sent to our API webRequest.onBeforeSendHeaders(validDestinationFilters, (details, callback) => { - // @ts-expect-error need to confirm if it's used - details.requestHeaders.origin = CONFIG.EXPENSIFY.URL_EXPENSIFY_CASH; - // @ts-expect-error need to confirm if it's used - details.requestHeaders.referer = CONFIG.EXPENSIFY.URL_EXPENSIFY_CASH; callback({requestHeaders: details.requestHeaders}); }); } @@ -418,16 +413,13 @@ const mainWindow = (): Promise => { submenu: [ { id: 'back', - // @ts-expect-error role doesn't exist but removing cause problems - role: 'back', accelerator: process.platform === 'darwin' ? 'Cmd+[' : 'Shift+[', click: () => { browserWindow.webContents.goBack(); }, }, { - // @ts-expect-error role doesn't exist but removing cause problems - role: 'back', + label: 'backWithKeyShortcut', visible: false, accelerator: process.platform === 'darwin' ? 'Cmd+Left' : 'Shift+Left', click: () => { @@ -436,16 +428,13 @@ const mainWindow = (): Promise => { }, { id: 'forward', - // @ts-expect-error role doesn't exist but removing cause problems - role: 'forward', accelerator: process.platform === 'darwin' ? 'Cmd+]' : 'Shift+]', click: () => { browserWindow.webContents.goForward(); }, }, { - // @ts-expect-error role doesn't exist but removing cause problems - role: 'forward', + label: 'forwardWithKeyShortcut', visible: false, accelerator: process.platform === 'darwin' ? 'Cmd+Right' : 'Shift+Right', click: () => { diff --git a/desktop/start.ts b/desktop/start.ts index 9efc7e04c9be..f92cd10dc5aa 100644 --- a/desktop/start.ts +++ b/desktop/start.ts @@ -42,8 +42,6 @@ portfinder }, ]; - // concurrently lib problem - // eslint-disable-next-line @typescript-eslint/no-unsafe-return return concurrently(processes, { inputStream: process.stdin, prefix: 'name', @@ -53,6 +51,6 @@ portfinder }).then( () => process.exit(0), () => process.exit(1), - ); + ) as never; }) .catch(() => process.exit(1)); From 0f1fb25ffd036dc02b3e7aba1730a2ee46baeb44 Mon Sep 17 00:00:00 2001 From: Jayesh Mangwani Date: Mon, 4 Mar 2024 20:17:26 +0530 Subject: [PATCH 0328/1208] removed unused onyx/Form --- src/types/onyx/Form.ts | 140 ---------------------------------------- src/types/onyx/index.ts | 6 -- 2 files changed, 146 deletions(-) delete mode 100644 src/types/onyx/Form.ts diff --git a/src/types/onyx/Form.ts b/src/types/onyx/Form.ts deleted file mode 100644 index 3743468b39b0..000000000000 --- a/src/types/onyx/Form.ts +++ /dev/null @@ -1,140 +0,0 @@ -import type * as OnyxCommon from './OnyxCommon'; -import type PersonalBankAccount from './PersonalBankAccount'; - -type FormValueType = string | boolean | Date | OnyxCommon.Errors; - -type BaseForm = { - /** Controls the loading state of the form */ - isLoading?: boolean; - - /** Server side errors keyed by microtime */ - errors?: OnyxCommon.Errors | null; - - /** Field-specific server side errors keyed by microtime */ - errorFields?: OnyxCommon.ErrorFields | null; -}; - -type Form = Record> = TFormValues & BaseForm; - -type AddDebitCardForm = Form<{ - /** Whether the form has been submitted */ - setupComplete: boolean; -}>; - -type DateOfBirthForm = Form<{ - /** Date of birth */ - dob?: string; -}>; - -type DisplayNameForm = Form<{ - firstName: string; - lastName: string; -}>; - -type NewRoomForm = Form<{ - roomName?: string; - reportDescription?: string; - policyID?: string; - writeCapability?: string; - visibility?: string; -}>; - -type IKnowATeacherForm = Form<{ - firstName: string; - lastName: string; - partnerUserID: string; -}>; - -type IntroSchoolPrincipalForm = Form<{ - firstName: string; - lastName: string; - partnerUserID: string; -}>; - -type PrivateNotesForm = Form<{ - privateNotes: string; -}>; - -type GetPhysicalCardForm = Form<{ - /** Address line 1 for delivery */ - addressLine1?: string; - - /** Address line 2 for delivery */ - addressLine2?: string; - - /** City for delivery */ - city?: string; - - /** Country for delivery */ - country?: string; - - /** First name for delivery */ - legalFirstName?: string; - - /** Last name for delivery */ - legalLastName?: string; - - /** Phone number for delivery */ - phoneNumber?: string; - - /** State for delivery */ - state?: string; - - /** Zip code for delivery */ - zipPostCode?: string; -}>; - -type PersonalBankAccountForm = Form; - -type WorkspaceSettingsForm = Form<{ - name: string; -}>; - -type ReportFieldEditForm = Form>; - -type CloseAccountForm = Form<{ - reasonForLeaving: string; - phoneOrEmail: string; -}>; - -type IdologyQuestionsForm = Form<{ - answer: string; -}>; - -type AdditionalDetailStepForm = Form<{ - legalFirstName: string; - legalLastName: string; - addressStreet: string; - addressCity: string; - addressZipCode: string; - phoneNumber: string; - dob: string; - ssn: string; - addressState: string; -}>; - -type RoomNameForm = Form<{ - roomName: string; -}>; - -export default Form; - -export type { - AddDebitCardForm, - DateOfBirthForm, - PrivateNotesForm, - DisplayNameForm, - FormValueType, - GetPhysicalCardForm, - NewRoomForm, - BaseForm, - IKnowATeacherForm, - IntroSchoolPrincipalForm, - PersonalBankAccountForm, - WorkspaceSettingsForm, - ReportFieldEditForm, - CloseAccountForm, - IdologyQuestionsForm, - AdditionalDetailStepForm, - RoomNameForm, -}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 277011602a17..6846fc302639 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -12,7 +12,6 @@ import type Currency from './Currency'; import type {CurrencyList} from './Currency'; import type CustomStatusDraft from './CustomStatusDraft'; import type Download from './Download'; -import type {AdditionalDetailStepForm, IdologyQuestionsForm, PrivateNotesForm, ReportFieldEditForm, RoomNameForm} from './Form'; import type FrequentlyUsedEmoji from './FrequentlyUsedEmoji'; import type {FundList} from './Fund'; import type Fund from './Fund'; @@ -154,10 +153,5 @@ export type { RecentlyUsedReportFields, LastPaymentMethod, InvitedEmailsToAccountIDs, - PrivateNotesForm, - ReportFieldEditForm, - IdologyQuestionsForm, - AdditionalDetailStepForm, - RoomNameForm, Log, }; From 9f7258c4c6ec7a127be94b704de6d7b95cada235 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 4 Mar 2024 15:50:28 +0100 Subject: [PATCH 0329/1208] Re-run checks From e0813e48574bc22e8a14844d8fae4afcd7c86f20 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Mon, 4 Mar 2024 14:09:18 +0100 Subject: [PATCH 0330/1208] 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 8a2b943aa3ab57c38d487f7de31307dadd0b28ca Mon Sep 17 00:00:00 2001 From: Jayesh Mangwani Date: Mon, 4 Mar 2024 22:09:08 +0530 Subject: [PATCH 0331/1208] removed IdologyQuestionsForm instead used AdditionalDetailStepForm --- src/ONYXKEYS.ts | 3 --- src/libs/actions/Wallet.ts | 4 ++-- src/pages/EnablePayments/IdologyQuestions.tsx | 4 ++-- src/types/form/AdditionalDetailStepForm.ts | 2 ++ src/types/form/IdologyQuestionsForm.ts | 18 ------------------ src/types/form/index.ts | 1 - 6 files changed, 6 insertions(+), 26 deletions(-) delete mode 100644 src/types/form/IdologyQuestionsForm.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 389c270f7510..6834fcaa13ce 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -397,8 +397,6 @@ const ONYXKEYS = { EXIT_SURVEY_REASON_FORM_DRAFT: 'exitSurveyReasonFormDraft', EXIT_SURVEY_RESPONSE_FORM: 'exitSurveyResponseForm', EXIT_SURVEY_RESPONSE_FORM_DRAFT: 'exitSurveyResponseFormDraft', - IDOLOGY_QUESTIONS_FORM: 'idologyQuestionsForm', - IDOLOGY_QUESTIONS_FORM_DRAFT: 'idologyQuestionsFormDraft', WALLET_ADDITIONAL_DETAILS: 'walletAdditionalDetails', WALLET_ADDITIONAL_DETAILS_DRAFT: 'walletAdditionalDetailsDraft', }, @@ -446,7 +444,6 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT]: FormTypes.PersonalBankAccountForm; [ONYXKEYS.FORMS.WORKSPACE_DESCRIPTION_FORM]: FormTypes.WorkspaceDescriptionForm; [ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS]: FormTypes.AdditionalDetailStepForm; - [ONYXKEYS.FORMS.IDOLOGY_QUESTIONS_FORM]: FormTypes.IdologyQuestionsForm; }; type OnyxFormDraftValuesMapping = { diff --git a/src/libs/actions/Wallet.ts b/src/libs/actions/Wallet.ts index ffc68c562c4c..9cb4b28bef20 100644 --- a/src/libs/actions/Wallet.ts +++ b/src/libs/actions/Wallet.ts @@ -233,7 +233,7 @@ function answerQuestionsForWallet(answers: WalletQuestionAnswer[], idNumber: str const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.IDOLOGY_QUESTIONS_FORM, + key: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, value: { isLoading: true, }, @@ -243,7 +243,7 @@ function answerQuestionsForWallet(answers: WalletQuestionAnswer[], idNumber: str const finallyData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.IDOLOGY_QUESTIONS_FORM, + key: ONYXKEYS.FORMS.WALLET_ADDITIONAL_DETAILS, value: { isLoading: false, }, diff --git a/src/pages/EnablePayments/IdologyQuestions.tsx b/src/pages/EnablePayments/IdologyQuestions.tsx index 13c71f7005a9..6baea2158613 100644 --- a/src/pages/EnablePayments/IdologyQuestions.tsx +++ b/src/pages/EnablePayments/IdologyQuestions.tsx @@ -92,7 +92,7 @@ function IdologyQuestions({questions, idNumber}: IdologyQuestionsProps) { } }; - const validate = (values: FormOnyxValues): FormInputErrors => { + const validate = (values: FormOnyxValues): FormInputErrors => { const errors: Errors = {}; if (!values.answer) { errors.answer = translate('additionalDetailsStep.selectAnswer'); @@ -112,7 +112,7 @@ function IdologyQuestions({questions, idNumber}: IdologyQuestionsProps) { ; @@ -27,6 +28,7 @@ type AdditionalDetailStepForm = Form< [INPUT_IDS.ADDRESS_STATE]: string; [INPUT_IDS.DOB]: string; [INPUT_IDS.SSN]: string; + [INPUT_IDS.ANSWER]: string; } >; diff --git a/src/types/form/IdologyQuestionsForm.ts b/src/types/form/IdologyQuestionsForm.ts deleted file mode 100644 index 250e8c6f7766..000000000000 --- a/src/types/form/IdologyQuestionsForm.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type {ValueOf} from 'type-fest'; -import type Form from './Form'; - -const INPUT_IDS = { - ANSWER: 'answer', -} as const; - -type InputID = ValueOf; - -type IdologyQuestionsForm = Form< - InputID, - { - [INPUT_IDS.ANSWER]: string; - } ->; - -export type {IdologyQuestionsForm}; -export default INPUT_IDS; diff --git a/src/types/form/index.ts b/src/types/form/index.ts index 498265848e37..0ab53ba9ec0e 100644 --- a/src/types/form/index.ts +++ b/src/types/form/index.ts @@ -38,5 +38,4 @@ export type {WorkspaceSettingsForm} from './WorkspaceSettingsForm'; export type {ReportPhysicalCardForm} from './ReportPhysicalCardForm'; export type {WorkspaceDescriptionForm} from './WorkspaceDescriptionForm'; export type {AdditionalDetailStepForm} from './AdditionalDetailStepForm'; -export type {IdologyQuestionsForm} from './IdologyQuestionsForm'; export type {default as Form} from './Form'; From 20a754b379b66894ec09165b49bd7e5d5716198e Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Mon, 4 Mar 2024 22:06:46 +0300 Subject: [PATCH 0332/1208] 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 0333/1208] 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 563ed6a0ae9b1247432810b8872293be6f86ed97 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Tue, 5 Mar 2024 11:49:09 +0800 Subject: [PATCH 0334/1208] moving code around --- src/pages/EditReportFieldDropdownPage.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/pages/EditReportFieldDropdownPage.tsx b/src/pages/EditReportFieldDropdownPage.tsx index 8831cca12197..97688c41750b 100644 --- a/src/pages/EditReportFieldDropdownPage.tsx +++ b/src/pages/EditReportFieldDropdownPage.tsx @@ -62,15 +62,11 @@ function EditReportFieldDropdownPage({fieldName, onSubmit, fieldID, fieldValue, ]; } - const selectedValue = fieldValue; - setHeaderMessage(!fieldOptions.length && !recentlyUsedOptions.length ? translate('common.noResultsFound') : ''); const newSections = []; - const filteredRecentlyUsedOptions = recentlyUsedOptions.filter((option) => option !== selectedValue); - const filteredFieldOptions = fieldOptions.filter((option) => option !== selectedValue); - + const selectedValue = fieldValue; if (selectedValue) { newSections.push({ shouldShow: false, @@ -85,6 +81,7 @@ function EditReportFieldDropdownPage({fieldName, onSubmit, fieldID, fieldValue, }); } + const filteredRecentlyUsedOptions = recentlyUsedOptions.filter((option) => option !== selectedValue); if (filteredRecentlyUsedOptions.length > 0) { newSections.push({ title: translate('common.recents'), @@ -98,6 +95,7 @@ function EditReportFieldDropdownPage({fieldName, onSubmit, fieldID, fieldValue, }); } + const filteredFieldOptions = fieldOptions.filter((option) => option !== selectedValue); if (filteredFieldOptions.length > 0) { newSections.push({ title: translate('common.all'), From 5e5289ba2f1724241b0f97ba0f669a1d27492392 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 5 Mar 2024 10:40:52 +0100 Subject: [PATCH 0335/1208] fix: typecheck --- src/libs/SidebarUtils.ts | 2 +- src/pages/home/sidebar/SidebarLinksData.tsx | 47 ++++++++++----------- 2 files changed, 24 insertions(+), 25 deletions(-) diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 6be95b393472..81d8f5a46d53 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -74,7 +74,7 @@ function getOrderedReportIDs( betas: OnyxEntry, policies: OnyxCollection, priorityMode: OnyxEntry>, - allReportActions: OnyxCollection, + allReportActions: OnyxCollection, transactionViolations: OnyxCollection, currentPolicyID = '', policyMemberAccountIDs: number[] = [], diff --git a/src/pages/home/sidebar/SidebarLinksData.tsx b/src/pages/home/sidebar/SidebarLinksData.tsx index 042f3b392d8a..1d0aa9e996ac 100644 --- a/src/pages/home/sidebar/SidebarLinksData.tsx +++ b/src/pages/home/sidebar/SidebarLinksData.tsx @@ -26,9 +26,13 @@ import type * as OnyxTypes from '@src/types/onyx'; import type {Message} from '@src/types/onyx/ReportAction'; import SidebarLinks from './SidebarLinks'; -type ChatReportSelector = ReturnType & {isUnreadWithMention: boolean}; -type PolicySelector = ReturnType; -type ReportActionsSelector = ReturnType; +type ChatReportSelector = OnyxTypes.Report & {isUnreadWithMention: boolean}; +type PolicySelector = Pick; +type TransactionSelector = Pick< + OnyxTypes.Transaction, + 'reportID' | 'iouRequestType' | 'comment' | 'receipt' | 'merchant' | 'modifiedMerchant' | 'created' | 'modifiedCreated' | 'amount' | 'modifiedAmount' +>; +type ReportActionsSelector = Array>; type SidebarLinksDataOnyxProps = { /** List of reports */ @@ -44,7 +48,7 @@ type SidebarLinksDataOnyxProps = { betas: OnyxEntry; /** All transactions f */ - allTransactions: OnyxEntry; + allTransactions: OnyxCollection; /** All report actions for all reports */ allReportActions: OnyxEntry; @@ -82,6 +86,7 @@ function SidebarLinksData({ transactionViolations, currentReportID, }: SidebarLinksDataProps) { + console.log(allReportActions); const {accountID} = useCurrentUserPersonalDetails(); const network = useNetwork(); const isFocused = useIsFocused(); @@ -101,7 +106,7 @@ function SidebarLinksData({ return reportKeys.reduce((errorsMap, reportKey) => { const report = chatReports?.[reportKey] ?? null; const allReportsActions = allReportActions?.[reportKey.replace(ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.COLLECTION.REPORT_ACTIONS)]; - const errors = OptionsListUtils.getAllReportErrors(report, allReportsActions, allTransactions) || {}; + const errors = OptionsListUtils.getAllReportErrors(report, allReportsActions, allTransactions as OnyxCollection) || {}; if (Object.keys(errors).length === 0) { return errorsMap; } @@ -118,7 +123,7 @@ function SidebarLinksData({ betas, policies as OnyxEntry>, priorityMode, - allReportActions, + allReportActions as OnyxCollection, transactionViolations, activeWorkspaceID, policyMemberAccountIDs, @@ -166,7 +171,7 @@ function SidebarLinksData({ betas, policies as OnyxEntry>, priorityMode, - allReportActions, + allReportActions as OnyxCollection, transactionViolations, activeWorkspaceID, policyMemberAccountIDs, @@ -222,15 +227,14 @@ SidebarLinksData.displayName = 'SidebarLinksData'; * This function (and the few below it), narrow down the data from Onyx to just the properties that we want to trigger a re-render of the component. This helps minimize re-rendering * and makes the entire component more performant because it's not re-rendering when a bunch of properties change which aren't ever used in the UI. */ -const chatReportSelector = (report: OnyxEntry) => - report && { +const chatReportSelector = (report: OnyxEntry): ChatReportSelector => + (report && { reportID: report.reportID, participantAccountIDs: report.participantAccountIDs, hasDraft: report.hasDraft, isPinned: report.isPinned, isHidden: report.isHidden, notificationPreference: report.notificationPreference, - errors: report.errors, errorFields: { addWorkspaceRoom: report.errorFields?.addWorkspaceRoom, }, @@ -252,9 +256,6 @@ const chatReportSelector = (report: OnyxEntry) => reportName: report.reportName, policyName: report.policyName, oldPolicyName: report.oldPolicyName, - isPolicyExpenseChat: report.isPolicyExpenseChat, - isOwnPolicyExpenseChat: report.isOwnPolicyExpenseChat, - isCancelledIOU: report.isCancelledIOU, // Other less obvious properites considered for sorting: ownerAccountID: report.ownerAccountID, currency: report.currency, @@ -264,7 +265,7 @@ const chatReportSelector = (report: OnyxEntry) => parentReportID: report.parentReportID, isDeletedParentAction: report.isDeletedParentAction, isUnreadWithMention: ReportUtils.isUnreadWithMention(report), - }; + }) as ChatReportSelector; const reportActionsSelector = (reportActions: OnyxEntry) => reportActions && @@ -285,15 +286,15 @@ const reportActionsSelector = (reportActions: OnyxEntry }; }); -const policySelector = (policy: OnyxEntry) => - policy && { +const policySelector = (policy: OnyxEntry): PolicySelector => + (policy && { type: policy.type, name: policy.name, avatar: policy.avatar, - }; + }) as PolicySelector; -const transactionSelector = (transaction) => - transaction && { +const transactionSelector = (transaction: OnyxEntry): TransactionSelector => + (transaction && { reportID: transaction.reportID, iouRequestType: transaction.iouRequestType, comment: transaction.comment, @@ -304,14 +305,13 @@ const transactionSelector = (transaction) => modifiedAmount: transaction.modifiedAmount, created: transaction.created, modifiedCreated: transaction.modifiedCreated, - }; + }) as TransactionSelector; export default withCurrentReportID( withOnyx({ chatReports: { key: ONYXKEYS.COLLECTION.REPORT, - // This assertion is needed because the selector in withOnyx expects that the return type will be the same as type in ONYXKEYS but for collection keys the selector is executed for each collection item. This is a bug in withOnyx typings that we don't have a solution yet, when useOnyx hook is introduced it will be fixed. - selector: chatReportSelector as unknown as (report: OnyxEntry) => OnyxCollection, + selector: chatReportSelector, initialValue: {}, }, isLoadingApp: { @@ -337,8 +337,7 @@ export default withCurrentReportID( }, policies: { key: ONYXKEYS.COLLECTION.POLICY, - // This assertion is needed because the selector in withOnyx expects that the return type will be the same as type in ONYXKEYS but for collection keys the selector is executed for each collection item. This is a bug in withOnyx typings that we don't have a solution yet, when useOnyx hook is introduced it will be fixed. - selector: policySelector as unknown as (policy: OnyxEntry) => OnyxCollection, + selector: policySelector, initialValue: {}, }, policyMembers: { From ac55d2f98c493345f0064dc3f6014155aef57a37 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 5 Mar 2024 11:33:16 +0100 Subject: [PATCH 0336/1208] fix: typecheck --- src/libs/SidebarUtils.ts | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 81d8f5a46d53..8d53e992cb2d 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -70,11 +70,11 @@ function filterDisplayName(displayName: string): string { */ function getOrderedReportIDs( currentReportId: string | null, - allReports: OnyxCollection, - betas: OnyxEntry, - policies: OnyxCollection, - priorityMode: OnyxEntry>, - allReportActions: OnyxCollection, + allReports: Record, + betas: Beta[], + policies: Record, + priorityMode: ValueOf, + allReportActions: OnyxCollection, transactionViolations: OnyxCollection, currentPolicyID = '', policyMemberAccountIDs: number[] = [], @@ -83,17 +83,17 @@ function getOrderedReportIDs( ): string[] { const isInGSDMode = priorityMode === CONST.PRIORITY_MODE.GSD; const isInDefaultMode = !isInGSDMode; - const allReportsDictValues = Object.values(allReports ?? {}); + const allReportsDictValues = Object.values(allReports); const reportIDsWithViolations = new Set(); // Filter out all the reports that shouldn't be displayed let reportsToDisplay = allReportsDictValues.filter((report) => { const parentReportActionsKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.parentReportID}`; - const parentReportAction = allReportActions?.[parentReportActionsKey]?.[report?.parentReportActionID ?? '']; + const parentReportAction = allReportActions?.[parentReportActionsKey]?.[report.parentReportActionID ?? '']; const doesReportHaveViolations = canUseViolations && !!parentReportAction && ReportUtils.doesTransactionThreadHaveViolations(report, transactionViolations, parentReportAction); if (doesReportHaveViolations) { - reportIDsWithViolations.add(report?.reportID ?? ''); + reportIDsWithViolations.add(report.reportID); } return ReportUtils.shouldReportBeInOptionList({ report, @@ -124,14 +124,14 @@ function getOrderedReportIDs( // 4. Archived reports // - Sorted by lastVisibleActionCreated in default (most recent) view mode // - Sorted by reportDisplayName in GSD (focus) view mode - const pinnedAndBrickRoadReports: Array> = []; + const pinnedAndBrickRoadReports: Report[] = []; const draftReports: Report[] = []; - const nonArchivedReports: Array> = []; + const nonArchivedReports: Report[] = []; const archivedReports: Report[] = []; if (currentPolicyID || policyMemberAccountIDs.length > 0) { reportsToDisplay = reportsToDisplay.filter( - (report) => report?.reportID === currentReportId || ReportUtils.doesReportBelongToWorkspace(report, policyMemberAccountIDs, currentPolicyID), + (report) => report.reportID === currentReportId || ReportUtils.doesReportBelongToWorkspace(report, policyMemberAccountIDs, currentPolicyID), ); } // There are a few properties that need to be calculated for the report which are used when sorting reports. @@ -140,20 +140,17 @@ function getOrderedReportIDs( // However, this code needs to be very performant to handle thousands of reports, so in the interest of speed, we're just going to disable this lint rule and add // the reportDisplayName property to the report object directly. // eslint-disable-next-line no-param-reassign - if (report) { - // eslint-disable-next-line no-param-reassign - report.displayName = filterDisplayName(ReportUtils.getReportName(report)); - } + report.displayName = filterDisplayName(ReportUtils.getReportName(report)); - const hasRBR = (!!report && report.reportID in reportIDsWithErrors) || reportIDsWithViolations.has(report?.reportID ?? ''); + const hasRBR = report.reportID in reportIDsWithErrors || reportIDsWithViolations.has(report.reportID); - const isPinned = report?.isPinned ?? false; - const reportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '', report?.parentReportActionID ?? ''); + const isPinned = report.isPinned ?? false; + const reportAction = ReportActionsUtils.getReportAction(report.parentReportID ?? '', report.parentReportActionID ?? ''); if (isPinned || hasRBR || ReportUtils.requiresAttentionFromCurrentUser(report, reportAction)) { pinnedAndBrickRoadReports.push(report); - } else if (report?.hasDraft) { + } else if (report.hasDraft) { draftReports.push(report); - } else if (report && ReportUtils.isArchivedRoom(report)) { + } else if (ReportUtils.isArchivedRoom(report)) { archivedReports.push(report); } else { nonArchivedReports.push(report); @@ -179,7 +176,7 @@ function getOrderedReportIDs( // Now that we have all the reports grouped and sorted, they must be flattened into an array and only return the reportID. // The order the arrays are concatenated in matters and will determine the order that the groups are displayed in the sidebar. - const LHNReports = [...pinnedAndBrickRoadReports, ...draftReports, ...nonArchivedReports, ...archivedReports].map((report) => report?.reportID ?? ''); + const LHNReports = [...pinnedAndBrickRoadReports, ...draftReports, ...nonArchivedReports, ...archivedReports].map((report) => report.reportID); return LHNReports; } From ab1369805213034680390bd4d572b50047972612 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 5 Mar 2024 11:56:42 +0100 Subject: [PATCH 0337/1208] Update concurrently lib, minor code improvements --- desktop/contextBridge.ts | 4 +- desktop/start.ts | 17 +- package-lock.json | 621 +++++++++------------------------------ package.json | 2 +- 4 files changed, 145 insertions(+), 499 deletions(-) diff --git a/desktop/contextBridge.ts b/desktop/contextBridge.ts index f2693259e51a..689c69de0cc8 100644 --- a/desktop/contextBridge.ts +++ b/desktop/contextBridge.ts @@ -16,9 +16,9 @@ const WHITELIST_CHANNELS_RENDERER_TO_MAIN = [ ELECTRON_EVENTS.REQUEST_VISIBILITY, ELECTRON_EVENTS.START_UPDATE, ELECTRON_EVENTS.LOCALE_UPDATED, -]; +] as const; -const WHITELIST_CHANNELS_MAIN_TO_RENDERER = [ELECTRON_EVENTS.KEYBOARD_SHORTCUTS_PAGE, ELECTRON_EVENTS.UPDATE_DOWNLOADED, ELECTRON_EVENTS.FOCUS, ELECTRON_EVENTS.BLUR]; +const WHITELIST_CHANNELS_MAIN_TO_RENDERER = [ELECTRON_EVENTS.KEYBOARD_SHORTCUTS_PAGE, ELECTRON_EVENTS.UPDATE_DOWNLOADED, ELECTRON_EVENTS.FOCUS, ELECTRON_EVENTS.BLUR] as const; const getErrorMessage = (channel: string): string => `Electron context bridge cannot be used with channel '${channel}'`; diff --git a/desktop/start.ts b/desktop/start.ts index f92cd10dc5aa..17ec690507ed 100644 --- a/desktop/start.ts +++ b/desktop/start.ts @@ -1,10 +1,9 @@ #!/usr/bin/env node -import {config} from 'dotenv'; +import concurrently from 'concurrently'; +import {config as configDotenv} from 'dotenv'; import portfinder from 'portfinder'; -const concurrently = require('concurrently'); - -config(); +configDotenv(); const basePort = 8082; @@ -12,7 +11,7 @@ portfinder .getPortPromise({ port: basePort, }) - .then((port) => { + .then((port): Promise => { const devServer = `webpack-dev-server --config config/webpack/webpack.dev.js --port ${port} --env platform=desktop`; const buildMain = 'webpack watch --config config/webpack/webpack.desktop.js --config-name desktop-main --mode=development'; @@ -42,15 +41,17 @@ portfinder }, ]; - return concurrently(processes, { + const {result} = concurrently(processes, { inputStream: process.stdin, prefix: 'name', // Like Harry Potter and he-who-must-not-be-named, "neither can live while the other survives" killOthers: ['success', 'failure'], - }).then( + }); + + return result.then( () => process.exit(0), () => process.exit(1), - ) as never; + ); }) .catch(() => process.exit(1)); diff --git a/package-lock.json b/package-lock.json index 80d3d1c6e911..db601a4ac37b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -197,7 +197,7 @@ "babel-plugin-transform-class-properties": "^6.24.1", "babel-plugin-transform-remove-console": "^6.9.4", "clean-webpack-plugin": "^3.0.0", - "concurrently": "^5.3.0", + "concurrently": "^8.2.2", "copy-webpack-plugin": "^6.4.1", "css-loader": "^6.7.2", "diff-so-fancy": "^1.3.0", @@ -2709,20 +2709,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@callstack/reassure-cli/node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/@callstack/reassure-cli/node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2762,42 +2748,6 @@ "node": ">=8" } }, - "node_modules/@callstack/reassure-cli/node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/@callstack/reassure-cli/node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@callstack/reassure-cli/node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "engines": { - "node": ">=12" - } - }, "node_modules/@callstack/reassure-compare": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@callstack/reassure-compare/-/reassure-compare-0.6.0.tgz", @@ -26041,75 +25991,16 @@ } }, "node_modules/cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - } - }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true, - "license": "MIT" - }, - "node_modules/cliui/node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/cliui/node_modules/string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^4.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "dev": true, - "license": "MIT", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dependencies": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" }, "engines": { - "node": ">=6" + "node": ">=12" } }, "node_modules/clone": { @@ -26457,40 +26348,115 @@ } }, "node_modules/concurrently": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-5.3.0.tgz", - "integrity": "sha512-8MhqOB6PWlBfA2vJ8a0bSFKATOdWlHiQlk11IfmQBPaHVP8oP2gsh2MObE6UR3hqDHqvaIvLTyceNW6obVuFHQ==", + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", + "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==", "dev": true, - "license": "MIT", "dependencies": { - "chalk": "^2.4.2", - "date-fns": "^2.0.1", - "lodash": "^4.17.15", - "read-pkg": "^4.0.1", - "rxjs": "^6.5.2", - "spawn-command": "^0.0.2-1", - "supports-color": "^6.1.0", + "chalk": "^4.1.2", + "date-fns": "^2.30.0", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "spawn-command": "0.0.2", + "supports-color": "^8.1.1", "tree-kill": "^1.2.2", - "yargs": "^13.3.0" + "yargs": "^17.7.2" }, "bin": { - "concurrently": "bin/concurrently.js" + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" }, "engines": { - "node": ">=6.0.0" + "node": "^14.13.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/concurrently/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/concurrently/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concurrently/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" } }, "node_modules/concurrently/node_modules/supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, - "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=6" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, "node_modules/config-file-ts": { @@ -28722,20 +28688,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/electron-builder/node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/electron-builder/node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -28794,42 +28746,6 @@ "node": ">=8" } }, - "node_modules/electron-builder/node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/electron-builder/node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/electron-builder/node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, - "engines": { - "node": ">=12" - } - }, "node_modules/electron-publish": { "version": "24.5.0", "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-24.5.0.tgz", @@ -35634,20 +35550,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/jest-cli/node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/jest-cli/node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -35687,41 +35589,6 @@ "node": ">=8" } }, - "node_modules/jest-cli/node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/jest-cli/node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/jest-cli/node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, "node_modules/jest-config": { "version": "29.4.1", "license": "MIT", @@ -39736,16 +39603,6 @@ "tslib": "2" } }, - "node_modules/memfs/node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", - "dev": true, - "peer": true, - "dependencies": { - "tslib": "^2.1.0" - } - }, "node_modules/memoize-one": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", @@ -40462,19 +40319,6 @@ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==" }, - "node_modules/metro/node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/metro/node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -40600,39 +40444,6 @@ } } }, - "node_modules/metro/node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "engines": { - "node": ">=10" - } - }, - "node_modules/metro/node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/metro/node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "engines": { - "node": ">=12" - } - }, "node_modules/microevent.ts": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/microevent.ts/-/microevent.ts-0.1.1.tgz", @@ -44871,19 +44682,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/react-native/node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/react-native/node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -44975,39 +44773,6 @@ "async-limiter": "~1.0.0" } }, - "node_modules/react-native/node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "engines": { - "node": ">=10" - } - }, - "node_modules/react-native/node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/react-native/node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "engines": { - "node": ">=12" - } - }, "node_modules/react-pdf": { "version": "7.3.3", "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-7.3.3.tgz", @@ -46791,25 +46556,14 @@ "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" }, "node_modules/rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", "dev": true, - "license": "Apache-2.0", "dependencies": { - "tslib": "^1.9.0" - }, - "engines": { - "npm": ">=2.0.0" + "tslib": "^2.1.0" } }, - "node_modules/rxjs/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true, - "license": "0BSD" - }, "node_modules/safe-array-concat": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.0.tgz", @@ -48008,11 +47762,10 @@ } }, "node_modules/spawn-command": { - "version": "0.0.2-1", - "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz", - "integrity": "sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg==", - "dev": true, - "license": "MIT" + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", + "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", + "dev": true }, "node_modules/spdx-correct": { "version": "3.1.1", @@ -52692,22 +52445,20 @@ } }, "node_modules/yargs": { - "version": "13.3.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", - "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", - "dev": true, - "license": "MIT", + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dependencies": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.2" + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" } }, "node_modules/yargs-parser": { @@ -52720,126 +52471,20 @@ "node": ">=10" } }, - "node_modules/yargs/node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true, - "license": "MIT" - }, - "node_modules/yargs/node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/yargs/node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yargs/node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^4.1.0" - }, + "node_modules/yargs/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "engines": { - "node": ">=6" + "node": ">=10" } }, "node_modules/yargs/node_modules/yargs-parser": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", - "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", - "dev": true, - "license": "ISC", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" } }, "node_modules/yauzl": { diff --git a/package.json b/package.json index 92afaafc85e7..7f5847d50608 100644 --- a/package.json +++ b/package.json @@ -246,7 +246,7 @@ "babel-plugin-transform-class-properties": "^6.24.1", "babel-plugin-transform-remove-console": "^6.9.4", "clean-webpack-plugin": "^3.0.0", - "concurrently": "^5.3.0", + "concurrently": "^8.2.2", "copy-webpack-plugin": "^6.4.1", "css-loader": "^6.7.2", "diff-so-fancy": "^1.3.0", From bb932cbed6602f6c6c527e504d0a67e0a429e761 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 5 Mar 2024 12:08:19 +0100 Subject: [PATCH 0338/1208] Update files extension in the README --- desktop/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/desktop/README.md b/desktop/README.md index 4493196b5ed4..2fc074153d87 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -37,9 +37,9 @@ The desktop app is organized in three pieces: - This is the webpack-bundled version of our react-native-web app (except using `index.desktop.js` files instead of `index.website.js`, where applicable) - This is _very_ similar to our web app, and code in this process should assume it will be run in the context of a browser (no access to `require`, Electron, or Node.js APis) 3. The context bridge - - Implemented in https://github.com/Expensify/App/blob/main/desktop/contextBridge.js + - Implemented in https://github.com/Expensify/App/blob/main/desktop/contextBridge.ts - The context bridge enables communication between the main and renderer processes. For example, if the renderer process needs to make use of a Node.js or Electron API it must: - 1. Define an event in https://github.com/Expensify/App/blob/main/desktop/ELECTRON_EVENTS.js + 1. Define an event in https://github.com/Expensify/App/blob/main/desktop/ELECTRON_EVENTS.ts 2. Add that event to the whitelist defined in the context bridge 3. Set up a handler for the event in the main process that can respond to the renderer process back through the bridge, if necessary. From 018516241e2691db789e7e7ff67f171950c2dcb6 Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 5 Mar 2024 19:37:54 +0700 Subject: [PATCH 0339/1208] fix: image shrinks while loading in carousel --- src/components/Lightbox/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Lightbox/index.tsx b/src/components/Lightbox/index.tsx index a7ed6946fb28..1a69e1f8958b 100644 --- a/src/components/Lightbox/index.tsx +++ b/src/components/Lightbox/index.tsx @@ -137,7 +137,7 @@ function Lightbox({isAuthTokenRequired = false, uri, onScaleChanged: onScaleChan const [isFallbackImageLoaded, setFallbackImageLoaded] = useState(false); const fallbackSize = useMemo(() => { if (!hasSiblingCarouselItems || !contentSize || isCanvasLoading) { - return DEFAULT_IMAGE_DIMENSION; + return undefined; } const {minScale} = getCanvasFitScale({canvasSize, contentSize}); @@ -217,7 +217,7 @@ function Lightbox({isAuthTokenRequired = false, uri, onScaleChanged: onScaleChan > setFallbackImageLoaded(true)} From 379485eaa374db1612b022d35aab75e67e8f818b Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 5 Mar 2024 15:13:04 +0100 Subject: [PATCH 0340/1208] fix: typecheck --- src/libs/OptionsListUtils.ts | 6 ++- src/libs/SidebarUtils.ts | 39 ++++++++-------- src/pages/home/sidebar/SidebarLinksData.tsx | 51 ++++++++++----------- 3 files changed, 51 insertions(+), 45 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 07f0df962455..1a7ea79e5d59 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -481,7 +481,11 @@ function getSearchText( /** * Get an object of error messages keyed by microtime by combining all error objects related to the report. */ -function getAllReportErrors(report: OnyxEntry, reportActions: OnyxEntry, transactions: OnyxCollection = allTransactions): OnyxCommon.Errors { +function getAllReportErrors( + report: OnyxEntry, + reportActions: OnyxEntry | ReportAction[] | undefined, + transactions: OnyxCollection = allTransactions, +): OnyxCommon.Errors { const reportErrors = report?.errors ?? {}; const reportErrorFields = report?.errorFields ?? {}; const reportActionErrors: OnyxCommon.ErrorFields = Object.values(reportActions ?? {}).reduce( diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 8d53e992cb2d..c22226553ffc 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -70,10 +70,10 @@ function filterDisplayName(displayName: string): string { */ function getOrderedReportIDs( currentReportId: string | null, - allReports: Record, - betas: Beta[], - policies: Record, - priorityMode: ValueOf, + allReports: OnyxCollection, + betas: OnyxEntry, + policies: OnyxCollection, + priorityMode: OnyxEntry>, allReportActions: OnyxCollection, transactionViolations: OnyxCollection, currentPolicyID = '', @@ -83,17 +83,17 @@ function getOrderedReportIDs( ): string[] { const isInGSDMode = priorityMode === CONST.PRIORITY_MODE.GSD; const isInDefaultMode = !isInGSDMode; - const allReportsDictValues = Object.values(allReports); + const allReportsDictValues = Object.values(allReports ?? {}); const reportIDsWithViolations = new Set(); // Filter out all the reports that shouldn't be displayed let reportsToDisplay = allReportsDictValues.filter((report) => { const parentReportActionsKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.parentReportID}`; - const parentReportAction = allReportActions?.[parentReportActionsKey]?.[report.parentReportActionID ?? '']; + const parentReportAction = allReportActions?.[parentReportActionsKey]?.[report?.parentReportActionID ?? '']; const doesReportHaveViolations = canUseViolations && !!parentReportAction && ReportUtils.doesTransactionThreadHaveViolations(report, transactionViolations, parentReportAction); if (doesReportHaveViolations) { - reportIDsWithViolations.add(report.reportID); + reportIDsWithViolations.add(report?.reportID ?? ''); } return ReportUtils.shouldReportBeInOptionList({ report, @@ -124,14 +124,14 @@ function getOrderedReportIDs( // 4. Archived reports // - Sorted by lastVisibleActionCreated in default (most recent) view mode // - Sorted by reportDisplayName in GSD (focus) view mode - const pinnedAndBrickRoadReports: Report[] = []; - const draftReports: Report[] = []; - const nonArchivedReports: Report[] = []; - const archivedReports: Report[] = []; + const pinnedAndBrickRoadReports: Array> = []; + const draftReports: Array> = []; + const nonArchivedReports: Array> = []; + const archivedReports: Array> = []; if (currentPolicyID || policyMemberAccountIDs.length > 0) { reportsToDisplay = reportsToDisplay.filter( - (report) => report.reportID === currentReportId || ReportUtils.doesReportBelongToWorkspace(report, policyMemberAccountIDs, currentPolicyID), + (report) => report?.reportID === currentReportId || ReportUtils.doesReportBelongToWorkspace(report, policyMemberAccountIDs, currentPolicyID), ); } // There are a few properties that need to be calculated for the report which are used when sorting reports. @@ -140,15 +140,18 @@ function getOrderedReportIDs( // However, this code needs to be very performant to handle thousands of reports, so in the interest of speed, we're just going to disable this lint rule and add // the reportDisplayName property to the report object directly. // eslint-disable-next-line no-param-reassign - report.displayName = filterDisplayName(ReportUtils.getReportName(report)); + if (report) { + // eslint-disable-next-line no-param-reassign + report.displayName = filterDisplayName(ReportUtils.getReportName(report)); + } - const hasRBR = report.reportID in reportIDsWithErrors || reportIDsWithViolations.has(report.reportID); + const hasRBR = (!!report && report?.reportID in reportIDsWithErrors) || reportIDsWithViolations.has(report?.reportID ?? ''); - const isPinned = report.isPinned ?? false; - const reportAction = ReportActionsUtils.getReportAction(report.parentReportID ?? '', report.parentReportActionID ?? ''); + const isPinned = report?.isPinned ?? false; + const reportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '', report?.parentReportActionID ?? ''); if (isPinned || hasRBR || ReportUtils.requiresAttentionFromCurrentUser(report, reportAction)) { pinnedAndBrickRoadReports.push(report); - } else if (report.hasDraft) { + } else if (report?.hasDraft) { draftReports.push(report); } else if (ReportUtils.isArchivedRoom(report)) { archivedReports.push(report); @@ -176,7 +179,7 @@ function getOrderedReportIDs( // Now that we have all the reports grouped and sorted, they must be flattened into an array and only return the reportID. // The order the arrays are concatenated in matters and will determine the order that the groups are displayed in the sidebar. - const LHNReports = [...pinnedAndBrickRoadReports, ...draftReports, ...nonArchivedReports, ...archivedReports].map((report) => report.reportID); + const LHNReports = [...pinnedAndBrickRoadReports, ...draftReports, ...nonArchivedReports, ...archivedReports].map((report) => report?.reportID ?? ''); return LHNReports; } diff --git a/src/pages/home/sidebar/SidebarLinksData.tsx b/src/pages/home/sidebar/SidebarLinksData.tsx index 1d0aa9e996ac..f9368bac86d2 100644 --- a/src/pages/home/sidebar/SidebarLinksData.tsx +++ b/src/pages/home/sidebar/SidebarLinksData.tsx @@ -51,7 +51,7 @@ type SidebarLinksDataOnyxProps = { allTransactions: OnyxCollection; /** All report actions for all reports */ - allReportActions: OnyxEntry; + allReportActions: OnyxCollection; /** The policies which the user has access to */ policies: OnyxCollection; @@ -86,7 +86,6 @@ function SidebarLinksData({ transactionViolations, currentReportID, }: SidebarLinksDataProps) { - console.log(allReportActions); const {accountID} = useCurrentUserPersonalDetails(); const network = useNetwork(); const isFocused = useIsFocused(); @@ -105,8 +104,8 @@ function SidebarLinksData({ const reportKeys = Object.keys(chatReports ?? {}); return reportKeys.reduce((errorsMap, reportKey) => { const report = chatReports?.[reportKey] ?? null; - const allReportsActions = allReportActions?.[reportKey.replace(ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.COLLECTION.REPORT_ACTIONS)]; - const errors = OptionsListUtils.getAllReportErrors(report, allReportsActions, allTransactions as OnyxCollection) || {}; + const allReportsActions = allReportActions?.[reportKey.replace(ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.COLLECTION.REPORT_ACTIONS) ?? '']; + const errors = OptionsListUtils.getAllReportErrors(report, allReportsActions as OnyxTypes.ReportAction[], allTransactions as OnyxCollection) || {}; if (Object.keys(errors).length === 0) { return errorsMap; } @@ -119,11 +118,11 @@ function SidebarLinksData({ const optionListItems: string[] = useMemo(() => { const reportIDs = SidebarUtils.getOrderedReportIDs( null, - chatReports as OnyxEntry>, + chatReports, betas, - policies as OnyxEntry>, + policies as OnyxCollection, priorityMode, - allReportActions as OnyxCollection, + allReportActions as OnyxCollection, transactionViolations, activeWorkspaceID, policyMemberAccountIDs, @@ -167,11 +166,11 @@ function SidebarLinksData({ if (currentReportID && !optionListItems?.includes(currentReportID)) { return SidebarUtils.getOrderedReportIDs( currentReportID, - chatReports as OnyxEntry>, + chatReports as OnyxCollection, betas, - policies as OnyxEntry>, + policies as OnyxCollection, priorityMode, - allReportActions as OnyxCollection, + allReportActions as OnyxCollection, transactionViolations, activeWorkspaceID, policyMemberAccountIDs, @@ -268,23 +267,23 @@ const chatReportSelector = (report: OnyxEntry): ChatReportSele }) as ChatReportSelector; const reportActionsSelector = (reportActions: OnyxEntry) => - reportActions && - Object.values(reportActions).map((reportAction) => { - const {reportActionID, actionName, errors, originalMessage} = reportAction; - const decision = reportAction.message?.[0].moderationDecision?.decision; + (reportActions && + Object.values(reportActions).map((reportAction) => { + const {reportActionID, actionName, errors, originalMessage} = reportAction; + const decision = reportAction.message?.[0].moderationDecision?.decision; - return { - reportActionID, - actionName, - errors, - message: [ - { - moderationDecision: {decision}, - } as Message, - ], - originalMessage, - }; - }); + return { + reportActionID, + actionName, + errors, + message: [ + { + moderationDecision: {decision}, + } as Message, + ], + originalMessage, + }; + })) as ReportActionsSelector; const policySelector = (policy: OnyxEntry): PolicySelector => (policy && { From b9d5cffd1b10ab088a5b4813711b57f57a1e460c Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 5 Mar 2024 15:17:47 +0100 Subject: [PATCH 0341/1208] implement processReportIDDeeplink --- .../processReportIDDeeplink/getReportIDfromUrl.ts | 9 +++++++++ src/libs/processReportIDDeeplink/index.ts | 5 +++++ src/libs/processReportIDDeeplink/index.website.ts | 14 ++++++++++++++ 3 files changed, 28 insertions(+) create mode 100644 src/libs/processReportIDDeeplink/getReportIDfromUrl.ts create mode 100644 src/libs/processReportIDDeeplink/index.ts create mode 100644 src/libs/processReportIDDeeplink/index.website.ts diff --git a/src/libs/processReportIDDeeplink/getReportIDfromUrl.ts b/src/libs/processReportIDDeeplink/getReportIDfromUrl.ts new file mode 100644 index 000000000000..96334f5615fb --- /dev/null +++ b/src/libs/processReportIDDeeplink/getReportIDfromUrl.ts @@ -0,0 +1,9 @@ +import * as ReportUtils from '@libs/ReportUtils'; + +export default function getReportIDfromUrl(url: string): string { + const currentParams = new URLSearchParams(url); + const currentExitToRoute = currentParams.get('exitTo') ?? ''; + const {reportID} = ReportUtils.parseReportRouteParams(currentExitToRoute); + + return reportID; +} diff --git a/src/libs/processReportIDDeeplink/index.ts b/src/libs/processReportIDDeeplink/index.ts new file mode 100644 index 000000000000..72f017367c5e --- /dev/null +++ b/src/libs/processReportIDDeeplink/index.ts @@ -0,0 +1,5 @@ +import getReportIDfromUrl from './getReportIDfromUrl'; + +export default function processReportIDDeeplink(url: string) { + return getReportIDfromUrl(url); +} diff --git a/src/libs/processReportIDDeeplink/index.website.ts b/src/libs/processReportIDDeeplink/index.website.ts new file mode 100644 index 000000000000..ad03ca759cbb --- /dev/null +++ b/src/libs/processReportIDDeeplink/index.website.ts @@ -0,0 +1,14 @@ +import CONST from '@src/CONST'; +import getReportIDfromUrl from './getReportIDfromUrl'; + +export default function processReportIDDeeplink(url: string) { + const prevUrl = sessionStorage.getItem(CONST.SESSION_STORAGE_KEYS.INITIAL_URL); + const prevReportID = getReportIDfromUrl(prevUrl ?? ''); + const currentReportID = getReportIDfromUrl(url); + + if (currentReportID && url) { + sessionStorage.setItem(CONST.SESSION_STORAGE_KEYS.INITIAL_URL, url); + } + + return currentReportID || prevReportID; +} From da072b51117e2ee63fc20af0290fdfc8d8d83f58 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 5 Mar 2024 15:28:23 +0100 Subject: [PATCH 0342/1208] integrate processReportIDDeeplink --- src/libs/actions/Report.ts | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 7ad12cf3e1ed..cdfb186e504d 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -48,7 +48,6 @@ import DateUtils from '@libs/DateUtils'; import * as EmojiUtils from '@libs/EmojiUtils'; import * as Environment from '@libs/Environment/Environment'; import * as ErrorUtils from '@libs/ErrorUtils'; -import getPlatform from '@libs/getPlatform'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import LocalNotification from '@libs/Notification/LocalNotification'; @@ -56,6 +55,7 @@ import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PhoneNumber from '@libs/PhoneNumber'; import getPolicyMemberAccountIDs from '@libs/PolicyMembersUtils'; import {extractPolicyIDFromPath} from '@libs/PolicyUtils'; +import processReportIDDeeplink from '@libs/processReportIDDeeplink'; import * as Pusher from '@libs/Pusher/pusher'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -178,27 +178,7 @@ const typingWatchTimers: Record = {}; let reportIDDeeplinkedFromOldDot: string | undefined; Linking.getInitialURL().then((url) => { - const isWeb = ([CONST.PLATFORM.WEB] as unknown as string).includes(getPlatform()); - const currentParams = new URLSearchParams(url ?? ''); - const currentExitToRoute = currentParams.get('exitTo') ?? ''; - const {reportID: currentReportID} = ReportUtils.parseReportRouteParams(currentExitToRoute); - - if (!isWeb) { - reportIDDeeplinkedFromOldDot = currentReportID; - - return; - } - - const prevUrl = sessionStorage.getItem(CONST.SESSION_STORAGE_KEYS.INITIAL_URL); - const prevParams = new URLSearchParams(prevUrl ?? ''); - const prevExitToRoute = prevParams.get('exitTo') ?? ''; - const {reportID: prevReportID} = ReportUtils.parseReportRouteParams(prevExitToRoute); - - reportIDDeeplinkedFromOldDot = currentReportID || prevReportID; - - if (currentReportID && url) { - sessionStorage.setItem(CONST.SESSION_STORAGE_KEYS.INITIAL_URL, url); - } + reportIDDeeplinkedFromOldDot = processReportIDDeeplink(url ?? ''); }); let lastVisitedPath: string | undefined; From 88b215a3fb82f5df46f620369973cce09eea1c66 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 5 Mar 2024 15:57:11 +0100 Subject: [PATCH 0343/1208] add return types --- src/libs/processReportIDDeeplink/index.ts | 2 +- src/libs/processReportIDDeeplink/index.website.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/processReportIDDeeplink/index.ts b/src/libs/processReportIDDeeplink/index.ts index 72f017367c5e..228b4f28e864 100644 --- a/src/libs/processReportIDDeeplink/index.ts +++ b/src/libs/processReportIDDeeplink/index.ts @@ -1,5 +1,5 @@ import getReportIDfromUrl from './getReportIDfromUrl'; -export default function processReportIDDeeplink(url: string) { +export default function processReportIDDeeplink(url: string): string { return getReportIDfromUrl(url); } diff --git a/src/libs/processReportIDDeeplink/index.website.ts b/src/libs/processReportIDDeeplink/index.website.ts index ad03ca759cbb..1fa3cd58d39b 100644 --- a/src/libs/processReportIDDeeplink/index.website.ts +++ b/src/libs/processReportIDDeeplink/index.website.ts @@ -1,7 +1,7 @@ import CONST from '@src/CONST'; import getReportIDfromUrl from './getReportIDfromUrl'; -export default function processReportIDDeeplink(url: string) { +export default function processReportIDDeeplink(url: string): string { const prevUrl = sessionStorage.getItem(CONST.SESSION_STORAGE_KEYS.INITIAL_URL); const prevReportID = getReportIDfromUrl(prevUrl ?? ''); const currentReportID = getReportIDfromUrl(url); From 1310235410228389887a0dfa8cf60d0016687b61 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 5 Mar 2024 16:01:17 +0100 Subject: [PATCH 0344/1208] rename a file --- .../{getReportIDfromUrl.ts => getReportIDFromUrl.ts} | 2 +- src/libs/processReportIDDeeplink/index.ts | 4 ++-- src/libs/processReportIDDeeplink/index.website.ts | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) rename src/libs/processReportIDDeeplink/{getReportIDfromUrl.ts => getReportIDFromUrl.ts} (81%) diff --git a/src/libs/processReportIDDeeplink/getReportIDfromUrl.ts b/src/libs/processReportIDDeeplink/getReportIDFromUrl.ts similarity index 81% rename from src/libs/processReportIDDeeplink/getReportIDfromUrl.ts rename to src/libs/processReportIDDeeplink/getReportIDFromUrl.ts index 96334f5615fb..6763b5a45085 100644 --- a/src/libs/processReportIDDeeplink/getReportIDfromUrl.ts +++ b/src/libs/processReportIDDeeplink/getReportIDFromUrl.ts @@ -1,6 +1,6 @@ import * as ReportUtils from '@libs/ReportUtils'; -export default function getReportIDfromUrl(url: string): string { +export default function getReportIDFromUrl(url: string): string { const currentParams = new URLSearchParams(url); const currentExitToRoute = currentParams.get('exitTo') ?? ''; const {reportID} = ReportUtils.parseReportRouteParams(currentExitToRoute); diff --git a/src/libs/processReportIDDeeplink/index.ts b/src/libs/processReportIDDeeplink/index.ts index 228b4f28e864..7970556735b8 100644 --- a/src/libs/processReportIDDeeplink/index.ts +++ b/src/libs/processReportIDDeeplink/index.ts @@ -1,5 +1,5 @@ -import getReportIDfromUrl from './getReportIDfromUrl'; +import getReportIDFromUrl from './getReportIDFromUrl'; export default function processReportIDDeeplink(url: string): string { - return getReportIDfromUrl(url); + return getReportIDFromUrl(url); } diff --git a/src/libs/processReportIDDeeplink/index.website.ts b/src/libs/processReportIDDeeplink/index.website.ts index 1fa3cd58d39b..02fcfe75f968 100644 --- a/src/libs/processReportIDDeeplink/index.website.ts +++ b/src/libs/processReportIDDeeplink/index.website.ts @@ -1,10 +1,10 @@ import CONST from '@src/CONST'; -import getReportIDfromUrl from './getReportIDfromUrl'; +import getReportIDFromUrl from './getReportIDFromUrl'; export default function processReportIDDeeplink(url: string): string { const prevUrl = sessionStorage.getItem(CONST.SESSION_STORAGE_KEYS.INITIAL_URL); - const prevReportID = getReportIDfromUrl(prevUrl ?? ''); - const currentReportID = getReportIDfromUrl(url); + const prevReportID = getReportIDFromUrl(prevUrl ?? ''); + const currentReportID = getReportIDFromUrl(url); if (currentReportID && url) { sessionStorage.setItem(CONST.SESSION_STORAGE_KEYS.INITIAL_URL, url); From dc64a57d4501a1fc99631e73b226f660875e8f24 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 5 Mar 2024 16:43:51 +0100 Subject: [PATCH 0345/1208] fix: types --- src/libs/OptionsListUtils.ts | 6 +-- src/pages/home/sidebar/SidebarLinksData.tsx | 45 ++++++++++++--------- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 1a7ea79e5d59..2b3125db47ce 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -481,11 +481,7 @@ function getSearchText( /** * Get an object of error messages keyed by microtime by combining all error objects related to the report. */ -function getAllReportErrors( - report: OnyxEntry, - reportActions: OnyxEntry | ReportAction[] | undefined, - transactions: OnyxCollection = allTransactions, -): OnyxCommon.Errors { +function getAllReportErrors(report: OnyxEntry, reportActions: OnyxEntry | undefined, transactions: OnyxCollection = allTransactions): OnyxCommon.Errors { const reportErrors = report?.errors ?? {}; const reportErrorFields = report?.errorFields ?? {}; const reportActionErrors: OnyxCommon.ErrorFields = Object.values(reportActions ?? {}).reduce( diff --git a/src/pages/home/sidebar/SidebarLinksData.tsx b/src/pages/home/sidebar/SidebarLinksData.tsx index f9368bac86d2..0f4daa6f5f76 100644 --- a/src/pages/home/sidebar/SidebarLinksData.tsx +++ b/src/pages/home/sidebar/SidebarLinksData.tsx @@ -23,7 +23,6 @@ import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; -import type {Message} from '@src/types/onyx/ReportAction'; import SidebarLinks from './SidebarLinks'; type ChatReportSelector = OnyxTypes.Report & {isUnreadWithMention: boolean}; @@ -32,7 +31,7 @@ type TransactionSelector = Pick< OnyxTypes.Transaction, 'reportID' | 'iouRequestType' | 'comment' | 'receipt' | 'merchant' | 'modifiedMerchant' | 'created' | 'modifiedCreated' | 'amount' | 'modifiedAmount' >; -type ReportActionsSelector = Array>; +type ReportActionsSelector = Record>; type SidebarLinksDataOnyxProps = { /** List of reports */ @@ -105,7 +104,9 @@ function SidebarLinksData({ return reportKeys.reduce((errorsMap, reportKey) => { const report = chatReports?.[reportKey] ?? null; const allReportsActions = allReportActions?.[reportKey.replace(ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.COLLECTION.REPORT_ACTIONS) ?? '']; - const errors = OptionsListUtils.getAllReportErrors(report, allReportsActions as OnyxTypes.ReportAction[], allTransactions as OnyxCollection) || {}; + + const errors = + OptionsListUtils.getAllReportErrors(report, allReportsActions as OnyxEntry, allTransactions as OnyxCollection) || {}; if (Object.keys(errors).length === 0) { return errorsMap; } @@ -255,6 +256,9 @@ const chatReportSelector = (report: OnyxEntry): ChatReportSele reportName: report.reportName, policyName: report.policyName, oldPolicyName: report.oldPolicyName, + isPolicyExpenseChat: report.isPolicyExpenseChat, + isOwnPolicyExpenseChat: report.isOwnPolicyExpenseChat, + isCancelledIOU: report.isCancelledIOU, // Other less obvious properites considered for sorting: ownerAccountID: report.ownerAccountID, currency: report.currency, @@ -266,24 +270,29 @@ const chatReportSelector = (report: OnyxEntry): ChatReportSele isUnreadWithMention: ReportUtils.isUnreadWithMention(report), }) as ChatReportSelector; -const reportActionsSelector = (reportActions: OnyxEntry) => +const reportActionsSelector = (reportActions: OnyxEntry): ReportActionsSelector => (reportActions && - Object.values(reportActions).map((reportAction) => { - const {reportActionID, actionName, errors, originalMessage} = reportAction; - const decision = reportAction.message?.[0].moderationDecision?.decision; + Object.fromEntries( + Object.entries(reportActions).map(([key, reportAction]) => { + const {reportActionID, actionName, errors, originalMessage} = reportAction; + const decision = reportAction.message?.[0].moderationDecision?.decision; - return { - reportActionID, - actionName, - errors, - message: [ + return [ + key, { - moderationDecision: {decision}, - } as Message, - ], - originalMessage, - }; - })) as ReportActionsSelector; + reportActionID, + actionName, + errors, + message: [ + { + moderationDecision: {decision}, + }, + ], + originalMessage, + }, + ]; + }), + )) as ReportActionsSelector; const policySelector = (policy: OnyxEntry): PolicySelector => (policy && { From 926701c1e653fed38fbcdd49f7020108ce53daad Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Tue, 5 Mar 2024 11:18:38 -0500 Subject: [PATCH 0346/1208] 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 0347/1208] 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 e9140e1aafc1e0e9cb836c34294a7fdbcfa9b14d Mon Sep 17 00:00:00 2001 From: Jayesh Mangwani Date: Tue, 5 Mar 2024 22:02:32 +0530 Subject: [PATCH 0348/1208] updated setAdditionalDetailsQuestions prop to handle null and added missed addressZipCode INPUT_ID to STEP_FIELDS --- src/libs/actions/Wallet.ts | 3 +-- src/pages/EnablePayments/AdditionalDetailsStep.tsx | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/Wallet.ts b/src/libs/actions/Wallet.ts index 9cb4b28bef20..097d9ee0419a 100644 --- a/src/libs/actions/Wallet.ts +++ b/src/libs/actions/Wallet.ts @@ -52,7 +52,7 @@ function openOnfidoFlow() { API.read(READ_COMMANDS.OPEN_ONFIDO_FLOW, {}, {optimisticData, finallyData}); } -function setAdditionalDetailsQuestions(questions: WalletAdditionalQuestionDetails[], idNumber: string) { +function setAdditionalDetailsQuestions(questions: WalletAdditionalQuestionDetails[] | null, idNumber?: string) { Onyx.merge(ONYXKEYS.WALLET_ADDITIONAL_DETAILS, {questions, idNumber}); } @@ -75,7 +75,6 @@ function setKYCWallSource(source?: ValueOf, chatRe /** * Validates a user's provided details against a series of checks */ - function updatePersonalDetails(personalDetails: UpdatePersonalDetailsForWalletParams) { const optimisticData: OnyxUpdate[] = [ { diff --git a/src/pages/EnablePayments/AdditionalDetailsStep.tsx b/src/pages/EnablePayments/AdditionalDetailsStep.tsx index 0b663261865f..57b9c7c6ade4 100644 --- a/src/pages/EnablePayments/AdditionalDetailsStep.tsx +++ b/src/pages/EnablePayments/AdditionalDetailsStep.tsx @@ -57,6 +57,7 @@ const STEP_FIELDS = [ INPUT_IDS.LEGAL_LAST_NAME, INPUT_IDS.ADDRESS_STREET, INPUT_IDS.ADDRESS_CITY, + INPUT_IDS.ADDRESS_ZIP_CODE, INPUT_IDS.PHONE_NUMBER, INPUT_IDS.DOB, INPUT_IDS.ADDRESS_STATE, @@ -132,7 +133,7 @@ function AdditionalDetailsStep({walletAdditionalDetails = DEFAULT_WALLET_ADDITIO > Wallet.setAdditionalDetailsQuestions([], walletAdditionalDetails?.idNumber ?? '')} + onBackButtonPress={() => Wallet.setAdditionalDetailsQuestions(null)} /> Date: Tue, 5 Mar 2024 19:40:42 +0300 Subject: [PATCH 0349/1208] 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 aaca30d1fe0d527fee1b8317ecb63ad00b92e8c3 Mon Sep 17 00:00:00 2001 From: John Lee Date: Tue, 5 Mar 2024 14:11:05 -0500 Subject: [PATCH 0350/1208] Change API to new schema --- package-lock.json | 6 +++--- package.json | 2 +- src/libs/ApiUtils.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 81e5b036a5be..c416a788e7c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,7 +52,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#45d3b61bb38b4f9a19ddf573ce1e212369b242db", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#8dd750aee6a5403365007861cc6b6e6e1fddb8ff", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.10.1", @@ -30751,8 +30751,8 @@ }, "node_modules/expensify-common": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#45d3b61bb38b4f9a19ddf573ce1e212369b242db", - "integrity": "sha512-R1ykTwH3Pdp2sFqE6AL3ihmo4OjLMDEc8mEqQwD9W+yoIDIScT6Wi5ewO5vZUNsyCiKnD+xvNU7I1d9VNRJkXw==", + "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#8dd750aee6a5403365007861cc6b6e6e1fddb8ff", + "integrity": "sha512-hsP+sd3jc752y402gAoJbmk8eTkAIbWBhV2nXuAsRl86jt79qQvgkfDheG0TWmcYrO+7RApqkok0OfZJuyI5nA==", "license": "MIT", "dependencies": { "classnames": "2.5.0", diff --git a/package.json b/package.json index 47ea222bfb91..c028c884e243 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,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#45d3b61bb38b4f9a19ddf573ce1e212369b242db", + "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#8dd750aee6a5403365007861cc6b6e6e1fddb8ff", "expo": "^50.0.3", "expo-av": "~13.10.4", "expo-image": "1.10.1", diff --git a/src/libs/ApiUtils.ts b/src/libs/ApiUtils.ts index 67feb18b36fa..78bcc000318c 100644 --- a/src/libs/ApiUtils.ts +++ b/src/libs/ApiUtils.ts @@ -52,7 +52,7 @@ function getApiRoot(request?: Request): string { * @param - the name of the API command */ function getCommandURL(request: Request): string { - return `${getApiRoot(request)}api?command=${request.command}`; + return `${getApiRoot(request)}api/${request.command}?`; } /** From a2ada45f0e5ee307f0d8b6073b6dacecabe47256 Mon Sep 17 00:00:00 2001 From: Ionatan Wiznia Date: Tue, 5 Mar 2024 16:36:54 -0300 Subject: [PATCH 0351/1208] 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 0352/1208] 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 0353/1208] 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 0354/1208] 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 0355/1208] 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 175556071115a7732be109d64241adf732851cf7 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Wed, 6 Mar 2024 14:20:49 +0500 Subject: [PATCH 0356/1208] refactor: use dedicated map for each locale --- src/libs/Localize/index.ts | 39 +++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/src/libs/Localize/index.ts b/src/libs/Localize/index.ts index ad5e93af24ee..f64d39d5e24a 100644 --- a/src/libs/Localize/index.ts +++ b/src/libs/Localize/index.ts @@ -12,6 +12,7 @@ import type {Locale} from '@src/types/onyx'; import type {ReceiptError} from '@src/types/onyx/Transaction'; import LocaleListener from './LocaleListener'; import BaseLocaleListener from './LocaleListener/BaseLocaleListener'; +import type BaseLocale from './LocaleListener/types'; // Current user mail is needed for handling missing translations let userEmail = ''; @@ -94,33 +95,55 @@ function translate(desiredLanguage: 'en' | 'es' | } /** - * Map to store translated values for each locale + * Map to store translated values for each locale. * This is used to avoid translating the same phrase multiple times. * * The data is stored in the following format: * * { - * "name_en": "Name", - * "name_es": "Nombre", + * "name": "Name", * } * * Note: We are not storing any translated values for phrases with variables, * as they have higher chance of being unique, so we'll end up wasting space * in our cache. */ -const translatedValues = new Map(); +const TRANSLATED_VALUES_EN = new Map(); +const TRANSLATED_VALUES_ES = new Map(); +const TRANSLATED_VALUES_ES_ES = new Map(); +const TRANSLATED_VALUES_ES_ONFIDO = new Map(); + +/** + * Returns the map for the given locale. + */ +function getTranslatedValuesMap(locale: BaseLocale) { + switch (locale) { + case CONST.LOCALES.ES_ES: + return TRANSLATED_VALUES_ES_ES; + case CONST.LOCALES.ES_ES_ONFIDO: + return TRANSLATED_VALUES_ES_ONFIDO; + case CONST.LOCALES.ES: + return TRANSLATED_VALUES_ES; + case CONST.LOCALES.DEFAULT: + default: + return TRANSLATED_VALUES_EN; + } +} /** * Uses the locale in this file updated by the Onyx subscriber. */ function translateLocal(phrase: TKey, ...variables: PhraseParameters>) { const preferredLocale = BaseLocaleListener.getPreferredLocale(); - const key = `${phrase}_${preferredLocale}`; + const key = `${phrase}`; const isVariablesEmpty = variables.length === 0; + // Get the map for the preferred locale + const map = getTranslatedValuesMap(preferredLocale); + // Directly access and assign the translated value from the cache, instead of // going through map.has() and map.get() to avoid multiple lookups. - const valueFromCache = translatedValues.get(key); + const valueFromCache = map.get(key); // If the phrase is already translated, return the translated value if (valueFromCache) { @@ -130,7 +153,9 @@ function translateLocal(phrase: TKey, ...variable // We don't want to store translated values for phrases with variables if (isVariablesEmpty) { - translatedValues.set(key, translatedText); + // We set the translated value in the cache in the next iteration + // of the event loop to make this operation asynchronous. + setImmediate(() => map.set(key, translatedText)); } return translatedText; } From 99dd34cfdf973681a713100ea345ced47911e206 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Wed, 6 Mar 2024 14:32:23 +0500 Subject: [PATCH 0357/1208] refactor: remove setImmediate --- src/libs/Localize/index.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/libs/Localize/index.ts b/src/libs/Localize/index.ts index f64d39d5e24a..72bb4dd89bb1 100644 --- a/src/libs/Localize/index.ts +++ b/src/libs/Localize/index.ts @@ -153,9 +153,8 @@ function translateLocal(phrase: TKey, ...variable // We don't want to store translated values for phrases with variables if (isVariablesEmpty) { - // We set the translated value in the cache in the next iteration - // of the event loop to make this operation asynchronous. - setImmediate(() => map.set(key, translatedText)); + // We set the translated value in the cache + map.set(key, translatedText); } return translatedText; } From 7b3c4134c5f03cca3b8f17c5e140d77ea5db1a83 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Wed, 6 Mar 2024 11:20:48 +0100 Subject: [PATCH 0358/1208] 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 f86fbdb470d32bfd32b95675d807661cbc914faa Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Wed, 6 Mar 2024 11:35:06 +0100 Subject: [PATCH 0359/1208] Add return type and change comment --- src/libs/actions/ReimbursementAccount/index.ts | 2 +- src/libs/actions/ReimbursementAccount/navigation.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/ReimbursementAccount/index.ts b/src/libs/actions/ReimbursementAccount/index.ts index dd1c784d2218..9edf1d9dbcd9 100644 --- a/src/libs/actions/ReimbursementAccount/index.ts +++ b/src/libs/actions/ReimbursementAccount/index.ts @@ -14,7 +14,7 @@ export {setBankAccountFormValidationErrors, setPersonalBankAccountFormValidation * - CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL to ask them to enter their accountNumber and routingNumber * - CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID to ask them to login to their bank via Plaid */ -function setBankAccountSubStep(subStep: BankAccountSubStep | null) { +function setBankAccountSubStep(subStep: BankAccountSubStep | null): Promise { return Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {achData: {subStep}}); } diff --git a/src/libs/actions/ReimbursementAccount/navigation.ts b/src/libs/actions/ReimbursementAccount/navigation.ts index 2c3eb7cf0384..49cf17fcc5bf 100644 --- a/src/libs/actions/ReimbursementAccount/navigation.ts +++ b/src/libs/actions/ReimbursementAccount/navigation.ts @@ -15,7 +15,7 @@ function goToWithdrawalAccountSetupStep(stepID: BankAccountStep) { * Navigate to the correct bank account route based on the bank account state and type * * @param policyID - The policy ID associated with the bank account. - * @param [backTo=''] - An optional return path. If provided, it will be URL-encoded and appended to the resulting URL. + * @param [backTo] - An optional return path. If provided, it will be URL-encoded and appended to the resulting URL. */ function navigateToBankAccountRoute(policyID: string, backTo?: string) { Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute('', policyID, backTo)); From fe7c953e9fd0206dd5dd308a5cd5e2d8c4309613 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Wed, 6 Mar 2024 11:43:30 +0100 Subject: [PATCH 0360/1208] 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 01ca51592899313913c58ad31e5fa072615b0083 Mon Sep 17 00:00:00 2001 From: Jayesh Mangwani Date: Wed, 6 Mar 2024 21:54:40 +0530 Subject: [PATCH 0361/1208] deleted unused walletTermsPropTypes --- .../EnablePayments/walletTermsPropTypes.js | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 src/pages/EnablePayments/walletTermsPropTypes.js diff --git a/src/pages/EnablePayments/walletTermsPropTypes.js b/src/pages/EnablePayments/walletTermsPropTypes.js deleted file mode 100644 index 4420a2dd0861..000000000000 --- a/src/pages/EnablePayments/walletTermsPropTypes.js +++ /dev/null @@ -1,18 +0,0 @@ -import PropTypes from 'prop-types'; -import _ from 'underscore'; -import CONST from '@src/CONST'; - -/** Prop types related to the Terms step of KYC flow */ -export default PropTypes.shape({ - /** Any error message to show */ - errors: PropTypes.objectOf(PropTypes.string), - - /** The source that triggered the KYC wall */ - source: PropTypes.oneOf(_.values(CONST.KYC_WALL_SOURCE)), - - /** When the user accepts the Wallet's terms in order to pay an IOU, this is the ID of the chatReport the IOU is linked to */ - chatReportID: PropTypes.string, - - /** Boolean to indicate whether the submission of wallet terms is being processed */ - isLoading: PropTypes.bool, -}); From 54c7a4cb0d2dae6e9f56761c10f67d6040c43b4c Mon Sep 17 00:00:00 2001 From: Ionatan Wiznia Date: Wed, 6 Mar 2024 13:34:50 -0300 Subject: [PATCH 0362/1208] 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 0363/1208] 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 0364/1208] 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 0365/1208] 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 0366/1208] 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 1f945a2ac05cd735ce461f357184143df8a09911 Mon Sep 17 00:00:00 2001 From: Rohan Sasne Date: Wed, 6 Mar 2024 23:42:39 +0530 Subject: [PATCH 0367/1208] use shouldSaveDraft --- src/hooks/useReimbursementAccountStepFormSubmit.ts | 5 ++++- .../BusinessInfo/substeps/PhoneNumberBusiness.tsx | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/hooks/useReimbursementAccountStepFormSubmit.ts b/src/hooks/useReimbursementAccountStepFormSubmit.ts index f3a17447c7d7..6cf1cc077c99 100644 --- a/src/hooks/useReimbursementAccountStepFormSubmit.ts +++ b/src/hooks/useReimbursementAccountStepFormSubmit.ts @@ -8,6 +8,7 @@ import type {SubStepProps} from './useSubStep/types'; type UseReimbursementAccountStepFormSubmitParams = Pick & { formId?: OnyxFormKey; fieldIds: Array>; + shouldSaveDraft?: boolean; }; /** @@ -17,16 +18,18 @@ type UseReimbursementAccountStepFormSubmitParams = Pick) => { - if (isEditing) { + if (isEditing || shouldSaveDraft) { const stepValues = fieldIds.reduce( (acc, key) => ({ ...acc, diff --git a/src/pages/ReimbursementAccount/BusinessInfo/substeps/PhoneNumberBusiness.tsx b/src/pages/ReimbursementAccount/BusinessInfo/substeps/PhoneNumberBusiness.tsx index e2746dbab59f..a000d1e067a6 100644 --- a/src/pages/ReimbursementAccount/BusinessInfo/substeps/PhoneNumberBusiness.tsx +++ b/src/pages/ReimbursementAccount/BusinessInfo/substeps/PhoneNumberBusiness.tsx @@ -43,8 +43,9 @@ function PhoneNumberBusiness({reimbursementAccount, onNext, isEditing}: PhoneNum const handleSubmit = useReimbursementAccountStepFormSubmit({ fieldIds: STEP_FIELDS, - isEditing: true, + isEditing, onNext, + shouldSaveDraft: true, }); return ( From 2b73c75b2c975f2639d8e9b5b59d7a822ee3af82 Mon Sep 17 00:00:00 2001 From: Rohan Sasne Date: Thu, 7 Mar 2024 00:02:28 +0530 Subject: [PATCH 0368/1208] Add useCallback dependency --- src/hooks/useReimbursementAccountStepFormSubmit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useReimbursementAccountStepFormSubmit.ts b/src/hooks/useReimbursementAccountStepFormSubmit.ts index 6cf1cc077c99..8f506b8ba127 100644 --- a/src/hooks/useReimbursementAccountStepFormSubmit.ts +++ b/src/hooks/useReimbursementAccountStepFormSubmit.ts @@ -43,6 +43,6 @@ export default function useReimbursementAccountStepFormSubmit({ onNext(); }, - [isEditing, onNext, formId, fieldIds], + [isEditing, onNext, formId, fieldIds, shouldSaveDraft], ); } From be58c4f67eaf2c6075da772f4554cc8693c99d3b Mon Sep 17 00:00:00 2001 From: Ionatan Wiznia Date: Wed, 6 Mar 2024 20:44:41 +0100 Subject: [PATCH 0369/1208] 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 89418e8070cbcd569de4a039148fba593efd369a Mon Sep 17 00:00:00 2001 From: Toby Sullivan Date: Wed, 6 Mar 2024 13:33:17 -0800 Subject: [PATCH 0370/1208] Add and update comments for report action params --- src/libs/ReportUtils.ts | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 350a08b43821..1508f5362406 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2331,18 +2331,21 @@ function getTransactionReportName(reportAction: OnyxEntry | EmptyObject, - reportAction: OnyxEntry | EmptyObject = {}, + iouReportAction: OnyxEntry | EmptyObject = {}, shouldConsiderScanningReceiptOrPendingRoute = false, isPreviewMessageForParentChatReport = false, policy: OnyxEntry = null, isForListPreview = false, - originalReportAction: OnyxEntry | EmptyObject = reportAction, + originalReportAction: OnyxEntry | EmptyObject = iouReportAction, ): string { - const reportActionMessage = reportAction?.message?.[0].html ?? ''; + const reportActionMessage = iouReportAction?.message?.[0].html ?? ''; if (isEmptyObject(report) || !report?.reportID) { // The iouReport is not found locally after SignIn because the OpenApp API won't return iouReports if they're settled @@ -2350,9 +2353,9 @@ function getReportPreviewMessage( return reportActionMessage; } - if (!isEmptyObject(reportAction) && !isIOUReport(report) && reportAction && ReportActionsUtils.isSplitBillAction(reportAction)) { + if (!isEmptyObject(iouReportAction) && !isIOUReport(report) && iouReportAction && ReportActionsUtils.isSplitBillAction(iouReportAction)) { // This covers group chats where the last action is a split bill action - const linkedTransaction = TransactionUtils.getLinkedTransaction(reportAction); + const linkedTransaction = TransactionUtils.getLinkedTransaction(iouReportAction); if (isEmptyObject(linkedTransaction)) { return reportActionMessage; } @@ -2387,8 +2390,8 @@ function getReportPreviewMessage( } let linkedTransaction; - if (!isEmptyObject(reportAction) && shouldConsiderScanningReceiptOrPendingRoute && reportAction && ReportActionsUtils.isMoneyRequestAction(reportAction)) { - linkedTransaction = TransactionUtils.getLinkedTransaction(reportAction); + if (!isEmptyObject(iouReportAction) && shouldConsiderScanningReceiptOrPendingRoute && iouReportAction && ReportActionsUtils.isMoneyRequestAction(iouReportAction)) { + linkedTransaction = TransactionUtils.getLinkedTransaction(iouReportAction); } if (!isEmptyObject(linkedTransaction) && TransactionUtils.hasReceipt(linkedTransaction) && TransactionUtils.isReceiptBeingScanned(linkedTransaction)) { @@ -2399,7 +2402,7 @@ function getReportPreviewMessage( return Localize.translateLocal('iou.routePending'); } - const originalMessage = reportAction?.originalMessage as IOUMessage | undefined; + const originalMessage = iouReportAction?.originalMessage as IOUMessage | undefined; // Show Paid preview message if it's settled or if the amount is paid & stuck at receivers end for only chat reports. if (isSettled(report.reportID) || (report.isWaitingOnBankAccount && isPreviewMessageForParentChatReport)) { @@ -2427,7 +2430,7 @@ function getReportPreviewMessage( return Localize.translateLocal('iou.waitingOnBankAccount', {submitterDisplayName}); } - const lastActorID = reportAction?.actorAccountID; + const lastActorID = iouReportAction?.actorAccountID; let amount = originalMessage?.amount; let currency = originalMessage?.currency ? originalMessage?.currency : report.currency; @@ -2436,8 +2439,8 @@ function getReportPreviewMessage( currency = TransactionUtils.getCurrency(linkedTransaction); } - if (isEmptyObject(linkedTransaction) && !isEmptyObject(reportAction)) { - linkedTransaction = TransactionUtils.getLinkedTransaction(reportAction); + if (isEmptyObject(linkedTransaction) && !isEmptyObject(iouReportAction)) { + linkedTransaction = TransactionUtils.getLinkedTransaction(iouReportAction); } let comment = !isEmptyObject(linkedTransaction) ? TransactionUtils.getDescription(linkedTransaction) : undefined; From 57d96be016668818515c567588ce6ce67c0a2974 Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 7 Mar 2024 15:41:52 +0700 Subject: [PATCH 0371/1208] 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 0372/1208] 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 0373/1208] 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 0374/1208] 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 0375/1208] 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 0376/1208] 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 0377/1208] 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 0378/1208] 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 0379/1208] 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 5770a2f2a4cfe66a91f5d2ec39e7d7a09099d053 Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Thu, 7 Mar 2024 15:10:22 +0100 Subject: [PATCH 0380/1208] Remove unused functions --- src/libs/actions/BankAccounts.ts | 1 - .../deleteFromBankAccountList.ts | 16 -------- .../actions/ReimbursementAccount/errors.ts | 21 +--------- .../actions/ReimbursementAccount/index.ts | 18 +------- .../actions/ReimbursementAccount/store.ts | 41 +------------------ 5 files changed, 4 insertions(+), 93 deletions(-) delete mode 100644 src/libs/actions/ReimbursementAccount/deleteFromBankAccountList.ts diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index 0f4e1aed36a7..878e1a443181 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -29,7 +29,6 @@ export { resetReimbursementAccount, resetFreePlanBankAccount, hideBankAccountErrors, - setWorkspaceIDForReimbursementAccount, setBankAccountSubStep, updateReimbursementAccountDraft, requestResetFreePlanBankAccount, diff --git a/src/libs/actions/ReimbursementAccount/deleteFromBankAccountList.ts b/src/libs/actions/ReimbursementAccount/deleteFromBankAccountList.ts deleted file mode 100644 index d9a2dd130d62..000000000000 --- a/src/libs/actions/ReimbursementAccount/deleteFromBankAccountList.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Onyx from 'react-native-onyx'; -import ONYXKEYS from '@src/ONYXKEYS'; -import * as store from './store'; - -/** - * Deletes a bank account from bankAccountList - */ -function deleteFromBankAccountList(bankAccountID: number) { - // We should delete the bankAccountID key from the bankAccountList object before setting it in Onyx - const bankAccountList = store.getBankAccountList(); - delete bankAccountList?.[bankAccountID]; - - Onyx.merge(ONYXKEYS.BANK_ACCOUNT_LIST, bankAccountList); -} - -export default deleteFromBankAccountList; diff --git a/src/libs/actions/ReimbursementAccount/errors.ts b/src/libs/actions/ReimbursementAccount/errors.ts index 05c375364329..2ab99c847291 100644 --- a/src/libs/actions/ReimbursementAccount/errors.ts +++ b/src/libs/actions/ReimbursementAccount/errors.ts @@ -1,17 +1,7 @@ import Onyx from 'react-native-onyx'; -import * as ErrorUtils from '@libs/ErrorUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import type {ErrorFields} from '@src/types/onyx/OnyxCommon'; -/** - * Set the current fields with errors. - */ -function setPersonalBankAccountFormValidationErrorFields(errorFields: ErrorFields) { - // We set 'errorFields' to null first because we don't have a way yet to replace a specific property without merging it - Onyx.merge(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {errorFields: null}); - Onyx.merge(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {errorFields}); -} - /** * Set the current fields with errors. @@ -32,13 +22,4 @@ function resetReimbursementAccount() { }); } -/** - * Set the current error message. - */ -function showBankAccountFormValidationError(error: string | null) { - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, { - errors: ErrorUtils.getMicroSecondOnyxError(error), - }); -} - -export {setBankAccountFormValidationErrors, setPersonalBankAccountFormValidationErrorFields, showBankAccountFormValidationError, resetReimbursementAccount}; +export {setBankAccountFormValidationErrors, resetReimbursementAccount}; diff --git a/src/libs/actions/ReimbursementAccount/index.ts b/src/libs/actions/ReimbursementAccount/index.ts index 9edf1d9dbcd9..57d785c5d1c9 100644 --- a/src/libs/actions/ReimbursementAccount/index.ts +++ b/src/libs/actions/ReimbursementAccount/index.ts @@ -2,11 +2,10 @@ import Onyx from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; import type {ReimbursementAccountForm} from '@src/types/form'; import type {BankAccountSubStep} from '@src/types/onyx/ReimbursementAccount'; -import deleteFromBankAccountList from './deleteFromBankAccountList'; import resetFreePlanBankAccount from './resetFreePlanBankAccount'; export {goToWithdrawalAccountSetupStep, navigateToBankAccountRoute} from './navigation'; -export {setBankAccountFormValidationErrors, setPersonalBankAccountFormValidationErrorFields, resetReimbursementAccount, showBankAccountFormValidationError} from './errors'; +export {setBankAccountFormValidationErrors, resetReimbursementAccount} from './errors'; /** * Set the current sub step in first step of adding withdrawal bank account: @@ -22,10 +21,6 @@ function hideBankAccountErrors() { Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {error: '', errors: null}); } -function setWorkspaceIDForReimbursementAccount(workspaceID: string | null) { - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT_WORKSPACE_ID, workspaceID); -} - function updateReimbursementAccountDraft(bankAccountData: Partial) { Onyx.merge(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT, bankAccountData); Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {draftStep: undefined}); @@ -45,13 +40,4 @@ function cancelResetFreePlanBankAccount() { Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {shouldShowResetModal: false}); } -export { - resetFreePlanBankAccount, - setBankAccountSubStep, - hideBankAccountErrors, - setWorkspaceIDForReimbursementAccount, - updateReimbursementAccountDraft, - requestResetFreePlanBankAccount, - cancelResetFreePlanBankAccount, - deleteFromBankAccountList, -}; +export {resetFreePlanBankAccount, setBankAccountSubStep, hideBankAccountErrors, updateReimbursementAccountDraft, requestResetFreePlanBankAccount, cancelResetFreePlanBankAccount}; diff --git a/src/libs/actions/ReimbursementAccount/store.ts b/src/libs/actions/ReimbursementAccount/store.ts index 30005b6fdc04..6e14cf72a569 100644 --- a/src/libs/actions/ReimbursementAccount/store.ts +++ b/src/libs/actions/ReimbursementAccount/store.ts @@ -3,25 +3,6 @@ import Onyx from 'react-native-onyx'; import BankAccount from '@libs/models/BankAccount'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; -import type {ACHData} from '@src/types/onyx/ReimbursementAccount'; -import type {EmptyObject} from '@src/types/utils/EmptyObject'; - -/** Reimbursement account actively being set up */ -let reimbursementAccountInSetup: ACHData | EmptyObject = {}; -Onyx.connect({ - key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, - callback: (val) => { - reimbursementAccountInSetup = val?.achData ?? {}; - }, -}); - -let reimbursementAccountWorkspaceID: OnyxEntry = null; -Onyx.connect({ - key: ONYXKEYS.REIMBURSEMENT_ACCOUNT_WORKSPACE_ID, - callback: (val) => { - reimbursementAccountWorkspaceID = val; - }, -}); let bankAccountList: OnyxEntry = null; Onyx.connect({ @@ -31,18 +12,6 @@ Onyx.connect({ }, }); -let credentials: OnyxEntry = null; -Onyx.connect({ - key: ONYXKEYS.CREDENTIALS, - callback: (val) => { - credentials = val; - }, -}); - -function getReimbursementAccountInSetup() { - return reimbursementAccountInSetup; -} - function getBankAccountList() { return bankAccountList; } @@ -58,12 +27,4 @@ function hasCreditBankAccount() { }); } -function getCredentials() { - return credentials; -} - -function getReimbursementAccountWorkspaceID() { - return reimbursementAccountWorkspaceID; -} - -export {getReimbursementAccountInSetup, getBankAccountList, getCredentials, getReimbursementAccountWorkspaceID, hasCreditBankAccount}; +export {getBankAccountList, hasCreditBankAccount}; From c14fbf85cbf91608e7b7f4ed7b2a6426a3bc57ed Mon Sep 17 00:00:00 2001 From: Michal Muzyk Date: Thu, 7 Mar 2024 15:14:11 +0100 Subject: [PATCH 0381/1208] feat: PolicyNewDistanceRatePage --- src/ONYXKEYS.ts | 3 + src/ROUTES.ts | 4 + src/SCREENS.ts | 1 + .../CreatePolicyDistanceRateParams.ts | 9 ++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 + .../AppNavigator/ModalStackNavigators.tsx | 1 + .../CENTRAL_PANE_TO_RHP_MAPPING.ts | 1 + src/libs/Navigation/linkingConfig/config.ts | 3 + src/libs/Navigation/types.ts | 3 + src/libs/actions/Policy.ts | 51 +++++++++++ .../distanceRates/PolicyDistanceRatesPage.tsx | 4 +- .../PolicyNewDistanceRatePage.tsx | 86 +++++++++++++++++++ .../form/PolicyCreateDistanceRateForm.ts | 18 ++++ src/types/form/index.ts | 1 + 15 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 src/libs/API/parameters/CreatePolicyDistanceRateParams.ts create mode 100644 src/pages/workspace/distanceRates/PolicyNewDistanceRatePage.tsx create mode 100644 src/types/form/PolicyCreateDistanceRateForm.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index f6b5c635e4ae..ce2a2be262e9 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -331,6 +331,8 @@ const ONYXKEYS = { WORKSPACE_DESCRIPTION_FORM_DRAFT: 'workspaceDescriptionFormDraft', WORKSPACE_RATE_AND_UNIT_FORM: 'workspaceRateAndUnitForm', WORKSPACE_RATE_AND_UNIT_FORM_DRAFT: 'workspaceRateAndUnitFormDraft', + POLICY_CREATE_DISTANCE_RATE_FORM: 'policyCreateDistanceRateForm', + POLICY_CREATE_DISTANCE_RATE_FORM_DRAFT: 'policyCreateDistanceRateFormDraft', CLOSE_ACCOUNT_FORM: 'closeAccount', CLOSE_ACCOUNT_FORM_DRAFT: 'closeAccountDraft', PROFILE_SETTINGS_FORM: 'profileSettingsForm', @@ -443,6 +445,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM]: FormTypes.ReimbursementAccountForm; [ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT]: FormTypes.PersonalBankAccountForm; [ONYXKEYS.FORMS.WORKSPACE_DESCRIPTION_FORM]: FormTypes.WorkspaceDescriptionForm; + [ONYXKEYS.FORMS.POLICY_CREATE_DISTANCE_RATE_FORM]: FormTypes.PolicyCreateDistanceRateForm; }; type OnyxFormDraftValuesMapping = { diff --git a/src/ROUTES.ts b/src/ROUTES.ts index fee69b9d785a..61dc44ac0432 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -554,6 +554,10 @@ const ROUTES = { route: 'workspace/:policyID/distance-rates', getRoute: (policyID: string) => `workspace/${policyID}/distance-rates` as const, }, + WORKSPACE_CREATE_DISTANCE_RATE: { + route: 'workspace/:policyID/distance-rates/new', + getRoute: (policyID: string) => `workspace/${policyID}/distance-rates/new` as const, + }, // Referral program promotion REFERRAL_DETAILS_MODAL: { diff --git a/src/SCREENS.ts b/src/SCREENS.ts index caedab241349..4d572a65e24f 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -226,6 +226,7 @@ const SCREENS = { CATEGORY_SETTINGS: 'Category_Settings', CATEGORIES_SETTINGS: 'Categories_Settings', DISTANCE_RATES: 'Distance_Rates', + CREATE_DISTANCE_RATE: 'Create_Distance_Rate', }, EDIT_REQUEST: { diff --git a/src/libs/API/parameters/CreatePolicyDistanceRateParams.ts b/src/libs/API/parameters/CreatePolicyDistanceRateParams.ts new file mode 100644 index 000000000000..ea06408d4402 --- /dev/null +++ b/src/libs/API/parameters/CreatePolicyDistanceRateParams.ts @@ -0,0 +1,9 @@ +import type {Rate} from '@src/types/onyx/Policy'; + +type CreatePolicyDistanceRateParams = { + policyID: string; + customUnitID: string; + customUnitRate: Rate; +}; + +export default CreatePolicyDistanceRateParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 7c4d592fe48d..34cc473b5d50 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -156,3 +156,4 @@ export type {default as SetWorkspaceAutoReportingMonthlyOffsetParams} from './Se export type {default as SetWorkspaceApprovalModeParams} from './SetWorkspaceApprovalModeParams'; export type {default as SwitchToOldDotParams} from './SwitchToOldDotParams'; export type {default as OpenPolicyDistanceRatesPageParams} from './OpenPolicyDistanceRatesPageParams'; +export type {default as CreatePolicyDistanceRateParams} from './CreatePolicyDistanceRateParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index a4cec18ae646..dce2592eae08 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -156,6 +156,7 @@ const WRITE_COMMANDS = { CANCEL_PAYMENT: 'CancelPayment', ACCEPT_ACH_CONTRACT_FOR_BANK_ACCOUNT: 'AcceptACHContractForBankAccount', SWITCH_TO_OLD_DOT: 'SwitchToOldDot', + CREATE_POLICY_DISTANCE_RATE: 'CreatePolicyDistanceRate', } as const; type WriteCommand = ValueOf; @@ -310,6 +311,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.CREATE_POLICY_DISTANCE_RATE]: Parameters.CreatePolicyDistanceRateParams; }; const READ_COMMANDS = { diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx index 545641957c9a..c6cf3d515336 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx @@ -251,6 +251,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../pages/workspace/WorkspaceProfileCurrencyPage').default as React.ComponentType, [SCREENS.WORKSPACE.CATEGORY_SETTINGS]: () => require('../../../pages/workspace/categories/CategorySettingsPage').default as React.ComponentType, [SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: () => require('../../../pages/workspace/categories/WorkspaceCategoriesSettingsPage').default as React.ComponentType, + [SCREENS.WORKSPACE.CREATE_DISTANCE_RATE]: () => require('../../../pages/workspace/distanceRates/PolicyNewDistanceRatePage').default as React.ComponentType, [SCREENS.REIMBURSEMENT_ACCOUNT]: () => require('../../../pages/ReimbursementAccount/ReimbursementAccountPage').default as React.ComponentType, [SCREENS.GET_ASSISTANCE]: () => require('../../../pages/GetAssistancePage').default as React.ComponentType, [SCREENS.SETTINGS.TWO_FACTOR_AUTH]: () => require('../../../pages/settings/Security/TwoFactorAuth/TwoFactorAuthPage').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 7959999ee813..1d2cad900ff1 100755 --- a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts @@ -7,6 +7,7 @@ const CENTRAL_PANE_TO_RHP_MAPPING: Partial> = [SCREENS.WORKSPACE.MEMBERS]: [SCREENS.WORKSPACE.INVITE, SCREENS.WORKSPACE.INVITE_MESSAGE], [SCREENS.WORKSPACE.WORKFLOWS]: [SCREENS.WORKSPACE.WORKFLOWS_APPROVER, SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY, SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET], [SCREENS.WORKSPACE.CATEGORIES]: [SCREENS.WORKSPACE.CATEGORY_SETTINGS, SCREENS.WORKSPACE.CATEGORIES_SETTINGS], + [SCREENS.WORKSPACE.DISTANCE_RATES]: [SCREENS.WORKSPACE.CREATE_DISTANCE_RATE], }; export default CENTRAL_PANE_TO_RHP_MAPPING; diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 927b5b509277..a99d3e9cb695 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -283,6 +283,9 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: { path: ROUTES.WORKSPACE_CATEGORIES_SETTINGS.route, }, + [SCREENS.WORKSPACE.CREATE_DISTANCE_RATE]: { + path: ROUTES.WORKSPACE_CREATE_DISTANCE_RATE.route, + }, [SCREENS.REIMBURSEMENT_ACCOUNT]: { path: ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.route, exact: true, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index f9b70b4b7ac8..73debd9d3897 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -207,6 +207,9 @@ type SettingsNavigatorParamList = { [SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: { policyID: string; }; + [SCREENS.WORKSPACE.CREATE_DISTANCE_RATE]: { + policyID: string; + }; [SCREENS.GET_ASSISTANCE]: { backTo: Routes; }; diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index b94ae68b04ac..2f9005633b48 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -10,6 +10,7 @@ import type {ValueOf} from 'type-fest'; import * as API from '@libs/API'; import type { AddMembersToWorkspaceParams, + CreatePolicyDistanceRateParams, CreateWorkspaceFromIOUPaymentParams, CreateWorkspaceParams, DeleteMembersFromWorkspaceParams, @@ -2514,6 +2515,54 @@ function openPolicyDistanceRatesPage(policyID?: string) { API.read(READ_COMMANDS.OPEN_POLICY_DISTANCE_RATES_PAGE, params); } +function createPolicyDistanceRate(policyID: string, customUnitID: string, customUnitRate: Rate) { + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + customUnits: { + [customUnitID]: { + rates: { + [customUnitRate.customUnitRateID ?? '']: { + ...customUnitRate, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + }, + }, + }, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + customUnits: { + [customUnitID]: { + rates: { + [customUnitRate.customUnitRateID ?? '']: { + errors: ErrorUtils.getMicroSecondOnyxError('workspace.distanceRates.errors.createRateGenericFailureMessage'), + pendingAction: null, + }, + }, + }, + }, + }, + }, + ]; + + const params: CreatePolicyDistanceRateParams = { + policyID, + customUnitID, + customUnitRate, + }; + + API.write(WRITE_COMMANDS.CREATE_POLICY_DISTANCE_RATE, params, {optimisticData, failureData}); +} + export { removeMembers, updateWorkspaceMembersRole, @@ -2565,4 +2614,6 @@ export { setWorkspaceRequiresCategory, clearCategoryErrors, openPolicyDistanceRatesPage, + generateCustomUnitID, + createPolicyDistanceRate, }; diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx index 08cd3dffe709..8c81ff7765b5 100644 --- a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx +++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx @@ -19,6 +19,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as CurrencyUtils from '@libs/CurrencyUtils'; +import Navigation from '@libs/Navigation/Navigation'; import type {CentralPaneNavigatorParamList} from '@navigation/types'; import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; @@ -26,6 +27,7 @@ import {openPolicyDistanceRatesPage} from '@userActions/Policy'; import ButtonWithDropdownMenu from '@src/components/ButtonWithDropdownMenu'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type Policy from '@src/types/onyx/Policy'; import type {CustomUnit, Rate} from '@src/types/onyx/Policy'; @@ -91,7 +93,7 @@ function PolicyDistanceRatesPage({policy, route}: PolicyDistanceRatesPageProps) ); const addRate = () => { - // Navigation.navigate(ROUTES.WORKSPACE_CREATE_DISTANCE_RATE.getRoute(route.params.policyID)); + Navigation.navigate(ROUTES.WORKSPACE_CREATE_DISTANCE_RATE.getRoute(route.params.policyID)); }; const openSettings = () => { diff --git a/src/pages/workspace/distanceRates/PolicyNewDistanceRatePage.tsx b/src/pages/workspace/distanceRates/PolicyNewDistanceRatePage.tsx new file mode 100644 index 000000000000..8e794e51674d --- /dev/null +++ b/src/pages/workspace/distanceRates/PolicyNewDistanceRatePage.tsx @@ -0,0 +1,86 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import AmountForm from '@components/AmountForm'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormOnyxValues} from '@components/Form/types'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@navigation/Navigation'; +import type {SettingsNavigatorParamList} from '@navigation/types'; +import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; +import {createPolicyDistanceRate, generateCustomUnitID} from '@userActions/Policy'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; +import INPUT_IDS from '@src/types/form/PolicyCreateDistanceRateForm'; +import type {Rate} from '@src/types/onyx/Policy'; +import type Policy from '@src/types/onyx/Policy'; + +type PolicyNewDistanceRatePageOnyxProps = { + policy: OnyxEntry; +}; + +type PolicyDistanceRatePageProps = PolicyNewDistanceRatePageOnyxProps & StackScreenProps; + +function PolicyNewDistanceRatePage({policy, route}: PolicyDistanceRatePageProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const currency = policy !== null ? policy?.outputCurrency : CONST.CURRENCY.USD; + const customUnits = policy?.customUnits ?? {}; + const customUnitID = customUnits[Object.keys(customUnits)[0]].customUnitID; + const customUnitRateID = generateCustomUnitID(); + + const submit = (values: FormOnyxValues) => { + const newRate: Rate = { + currency, + name: CONST.CUSTOM_UNITS.DEFAULT_RATE, + rate: Number(values.rate), + customUnitRateID, + }; + + createPolicyDistanceRate(route.params.policyID, customUnitID, newRate); + Navigation.goBack(); + }; + + return ( + + + + + + + + + + + ); +} + +PolicyNewDistanceRatePage.displayName = 'CreateDistanceRatePage'; + +export default withOnyx({ + policy: { + key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY}${route.params.policyID}`, + }, +})(PolicyNewDistanceRatePage); diff --git a/src/types/form/PolicyCreateDistanceRateForm.ts b/src/types/form/PolicyCreateDistanceRateForm.ts new file mode 100644 index 000000000000..11bb9f4aa83c --- /dev/null +++ b/src/types/form/PolicyCreateDistanceRateForm.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 PolicyCreateDistanceRateForm = Form< + InputID, + { + [INPUT_IDS.RATE]: string; + } +>; + +export type {PolicyCreateDistanceRateForm}; +export default INPUT_IDS; diff --git a/src/types/form/index.ts b/src/types/form/index.ts index 1ff8d0df2031..bf58c78d0b90 100644 --- a/src/types/form/index.ts +++ b/src/types/form/index.ts @@ -37,4 +37,5 @@ export type {WorkspaceRateAndUnitForm} from './WorkspaceRateAndUnitForm'; export type {WorkspaceSettingsForm} from './WorkspaceSettingsForm'; export type {ReportPhysicalCardForm} from './ReportPhysicalCardForm'; export type {WorkspaceDescriptionForm} from './WorkspaceDescriptionForm'; +export type {PolicyCreateDistanceRateForm} from './PolicyCreateDistanceRateForm'; export type {default as Form} from './Form'; From b8065489197501e2b65d40dd42ee882211dd4a57 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 7 Mar 2024 22:23:01 +0800 Subject: [PATCH 0382/1208] don't clear the optimistic personal detail if fail when inviting new user --- src/libs/actions/Policy.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index 38cc11756068..db62c68291a0 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -948,7 +948,6 @@ function addMembersToWorkspace(invitedEmailsToAccountIDs: InvitedEmailsToAccount // need to remove the members since that is handled by onClose of OfflineWithFeedback. value: failureMembersState, }, - ...newPersonalDetailsOnyxData.finallyData, ...membersChats.onyxFailureData, ...announceRoomMembers.onyxFailureData, ]; From b7cdc1b51d9ae04112c81c7c468de55dd29fe372 Mon Sep 17 00:00:00 2001 From: Jakub Kosmydel <104823336+kosmydel@users.noreply.github.com> Date: Thu, 7 Mar 2024 15:26:24 +0100 Subject: [PATCH 0383/1208] add initial new tax page --- src/CONST.ts | 4 + src/ONYXKEYS.ts | 2 + src/ROUTES.ts | 4 + src/SCREENS.ts | 1 + src/languages/en.ts | 2 + .../AppNavigator/ModalStackNavigators.tsx | 1 + src/libs/Navigation/linkingConfig/config.ts | 3 + src/libs/Navigation/types.ts | 3 + .../workspace/taxes/WorkspaceNewTaxPage.tsx | 96 +++++++++++++++++++ .../workspace/taxes/WorkspaceTaxesPage.tsx | 6 +- src/types/form/WorkspaceNewTaxForm.ts | 22 +++++ src/types/form/index.ts | 1 + src/types/onyx/Policy.ts | 2 +- 13 files changed, 144 insertions(+), 3 deletions(-) create mode 100644 src/pages/workspace/taxes/WorkspaceNewTaxPage.tsx create mode 100644 src/types/form/WorkspaceNewTaxForm.ts diff --git a/src/CONST.ts b/src/CONST.ts index 6861fe174ffe..b981d10e5be3 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -4021,6 +4021,10 @@ const CONST = { SESSION_STORAGE_KEYS: { INITIAL_URL: 'INITIAL_URL', }, + + TAX_RATES: { + NAME_MAX_LENGTH: 50, + }, } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 188ab5646d30..05b1e1bec1f9 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -404,6 +404,7 @@ const ONYXKEYS = { EXIT_SURVEY_REASON_FORM_DRAFT: 'exitSurveyReasonFormDraft', EXIT_SURVEY_RESPONSE_FORM: 'exitSurveyResponseForm', EXIT_SURVEY_RESPONSE_FORM_DRAFT: 'exitSurveyResponseFormDraft', + WORKSPACE_NEW_TAX_FORM: 'workspaceNewTaxForm', }, } as const; @@ -449,6 +450,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM]: FormTypes.ReimbursementAccountForm; [ONYXKEYS.FORMS.PERSONAL_BANK_ACCOUNT]: FormTypes.PersonalBankAccountForm; [ONYXKEYS.FORMS.WORKSPACE_DESCRIPTION_FORM]: FormTypes.WorkspaceDescriptionForm; + [ONYXKEYS.FORMS.WORKSPACE_NEW_TAX_FORM]: FormTypes.WorkspaceNewTaxForm; }; type OnyxFormDraftValuesMapping = { diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 9704bf047315..f0f6fed658f9 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -558,6 +558,10 @@ const ROUTES = { route: 'workspace/:policyID/taxes', getRoute: (policyID: string) => `workspace/${policyID}/taxes` as const, }, + WORKSPACE_TAXES_NEW: { + route: 'workspace/:policyID/taxes/new', + getRoute: (policyID: string) => `workspace/${policyID}/taxes/new` as const, + }, // Referral program promotion REFERRAL_DETAILS_MODAL: { route: 'referral/:contentType', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 27909319b724..d48cc9c19b83 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -216,6 +216,7 @@ const SCREENS = { CATEGORIES: 'Workspace_Categories', TAGS: 'Workspace_Tags', TAXES: 'Workspace_Taxes', + TAXES_NEW: 'Workspace_Taxes_New', CURRENCY: 'Workspace_Profile_Currency', WORKFLOWS: 'Workspace_Workflows', WORKFLOWS_APPROVER: 'Workspace_Workflows_Approver', diff --git a/src/languages/en.ts b/src/languages/en.ts index 715640bbb200..56c983ace282 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1787,6 +1787,8 @@ export default { }, taxes: { subtitle: 'Add tax names, rates, and set defaults.', + value: 'Value', + name: 'Name', }, emptyWorkspace: { title: 'Create a workspace', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx index cee9e31cedea..001891805bb2 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx @@ -262,6 +262,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../pages/settings/ExitSurvey/ExitSurveyConfirmPage').default as React.ComponentType, [SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY]: () => 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, }); const EnablePaymentsStackNavigator = createModalStackNavigator({ diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 0cae5e34e958..76f605c648b8 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -309,6 +309,9 @@ const config: LinkingOptions['config'] = { [SCREENS.SETTINGS.EXIT_SURVEY.CONFIRM]: { path: ROUTES.SETTINGS_EXIT_SURVEY_CONFIRM.route, }, + [SCREENS.WORKSPACE.TAXES_NEW]: { + path: ROUTES.WORKSPACE_TAXES_NEW.route, + }, }, }, [SCREENS.RIGHT_MODAL.PRIVATE_NOTES]: { diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 5c64dd62fd43..fcd69c49a135 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -225,6 +225,9 @@ type SettingsNavigatorParamList = { [SCREENS.SETTINGS.EXIT_SURVEY.CONFIRM]: { backTo: Routes; }; + [SCREENS.WORKSPACE.TAXES_NEW]: { + policyID: string; + }; } & ReimbursementAccountNavigatorParamList; type NewChatNavigatorParamList = { diff --git a/src/pages/workspace/taxes/WorkspaceNewTaxPage.tsx b/src/pages/workspace/taxes/WorkspaceNewTaxPage.tsx new file mode 100644 index 000000000000..a23b8d3061de --- /dev/null +++ b/src/pages/workspace/taxes/WorkspaceNewTaxPage.tsx @@ -0,0 +1,96 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useCallback} from 'react'; +import {View} from 'react-native'; +import AmountPicker from '@components/AmountPicker'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormOnyxValues} from '@components/Form/types'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import TextPicker from '@components/TextPicker'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {createWorkspaceTax} from '@libs/actions/TaxRate'; +import Navigation from '@libs/Navigation/Navigation'; +import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import * as ValidationUtils from '@libs/ValidationUtils'; +import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; +import INPUT_IDS from '@src/types/form/WorkspaceNewTaxForm'; +import type {TaxRate} from '@src/types/onyx'; + +type WorkspaceNewTaxPageProps = StackScreenProps; + +function WorkspaceNewTaxPage({ + route: { + params: {policyID}, + }, +}: WorkspaceNewTaxPageProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const validate = useCallback((values: FormOnyxValues): Partial> => { + const errors = ValidationUtils.getFieldRequiredErrors(values, [INPUT_IDS.VALUE, INPUT_IDS.NAME]); + + const value = Number(values[INPUT_IDS.VALUE]); + if (value > 100 || value < 0) { + errors[INPUT_IDS.VALUE] = 'workspace.taxes.errors.value.percentageRange'; + } + + return errors; + }, []); + + const submitForm = useCallback( + (values: FormOnyxValues) => { + // TODO: Add proper code generation + const taxRate = { + ...values, + code: `tax_${Date.now()}`, + } satisfies TaxRate; + createWorkspaceTax({policyID, taxRate}); + Navigation.goBack(); + }, + [policyID], + ); + + return ( + + + + + + + + + + ); +} + +WorkspaceNewTaxPage.displayName = 'WorkspaceNewTaxPage'; + +export default WorkspaceNewTaxPage; diff --git a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx index b19a69adebe2..0f4340b97eb8 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx @@ -15,12 +15,14 @@ import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import Navigation from '@libs/Navigation/Navigation'; import type {CentralPaneNavigatorParamList} from '@navigation/types'; import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; -import type SCREENS from '@src/SCREENS'; +import ROUTES from '@src/ROUTES'; +import SCREENS from '@src/SCREENS'; type WorkspaceTaxesPageProps = WithPolicyAndFullscreenLoadingProps & StackScreenProps; @@ -83,7 +85,7 @@ function WorkspaceTaxesPage({policy}: WorkspaceTaxesPageProps) {