From f158e5c115472ed9751dff971389d79d03fcef7b Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Mon, 30 Oct 2023 20:18:33 +0100 Subject: [PATCH 001/276] fix delay --- src/libs/DateUtils.ts | 16 ++++++++++++++++ src/libs/HttpUtils.js | 22 ++++++++++++++++++++++ src/libs/ReportUtils.js | 2 +- src/libs/actions/Network.ts | 6 +++++- src/libs/actions/Report.js | 2 +- src/types/onyx/Network.ts | 3 +++ 6 files changed, 48 insertions(+), 3 deletions(-) diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts index 13853189ed26..a1e156a9b7cc 100644 --- a/src/libs/DateUtils.ts +++ b/src/libs/DateUtils.ts @@ -55,6 +55,12 @@ Onyx.connect({ }, }); +let networkTimeSkew: number = 0; +Onyx.connect({ + key: ONYXKEYS.NETWORK, + callback: (value) => (networkTimeSkew = value?.timeSkew), +}); + /** * Gets the locale string and setting default locale for date-fns */ @@ -304,6 +310,15 @@ function getDateStringFromISOTimestamp(isoTimestamp: string): string { return dateString; } +/** + * Returns the current time plus skew in milliseconds in the format expected by the database + * + * @returns {String} + */ +function getDBTimeWithSkew() { + return getDBTime(new Date().valueOf() + networkTimeSkew); +} + /** * receive date like 2020-05-16 05:34:14 and format it to show in string like "Until 05:34 PM" */ @@ -374,6 +389,7 @@ const DateUtils = { isTomorrow, isYesterday, formatWithUTCTimeZone, + getDBTimeWithSkew, }; export default DateUtils; diff --git a/src/libs/HttpUtils.js b/src/libs/HttpUtils.js index 2df7421ea91c..fca7fdf76bb9 100644 --- a/src/libs/HttpUtils.js +++ b/src/libs/HttpUtils.js @@ -22,6 +22,16 @@ Onyx.connect({ // We use the AbortController API to terminate pending request in `cancelPendingRequests` let cancellationController = new AbortController(); +/** + * The API commands that require the skew calculation + */ +const addSkewList = ['OpenReport', 'ReconnectApp', 'OpenApp']; + +/** + * Regex to get API command from the command + */ +const regex = /[?&]command=([^&]+)/; + /** * Send an HTTP request, and attempt to resolve the json response. * If there is a network error, we'll set the application offline. @@ -33,12 +43,24 @@ let cancellationController = new AbortController(); * @returns {Promise} */ function processHTTPRequest(url, method = 'get', body = null, canCancel = true) { + const startTime = new Date().valueOf(); return fetch(url, { // We hook requests to the same Controller signal, so we can cancel them all at once signal: canCancel ? cancellationController.signal : undefined, method, body, }) + .then((response) => { + const match = url.match(regex)[1]; + if (addSkewList.includes(match) && response.headers) { + const serverTime = new Date(response.headers.get('Date')).valueOf(); + const endTime = new Date().valueOf(); + const latency = (endTime - startTime) / 2; + const skew = serverTime - startTime + latency; + NetworkActions.setTimeSkew(skew); + } + return response; + }) .then((response) => { // Test mode where all requests will succeed in the server, but fail to return a response if (shouldFailAllRequests || shouldForceOffline) { diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 8cadc6dcc8ec..862ed1da4379 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -2206,7 +2206,7 @@ function buildOptimisticAddCommentReportAction(text, file) { ], automatic: false, avatar: lodashGet(allPersonalDetails, [currentUserAccountID, 'avatar'], UserUtils.getDefaultAvatarURL(currentUserAccountID)), - created: DateUtils.getDBTime(), + created: DateUtils.getDBTimeWithSkew(), message: [ { translationKey: isAttachment ? CONST.TRANSLATION_KEYS.ATTACHMENT : '', diff --git a/src/libs/actions/Network.ts b/src/libs/actions/Network.ts index 17580c214376..e71094eded05 100644 --- a/src/libs/actions/Network.ts +++ b/src/libs/actions/Network.ts @@ -5,6 +5,10 @@ function setIsOffline(isOffline: boolean) { Onyx.merge(ONYXKEYS.NETWORK, {isOffline}); } +function setTimeSkew(skew: number) { + Onyx.merge(ONYXKEYS.NETWORK, {timeSkew: skew}); +} + function setShouldForceOffline(shouldForceOffline: boolean) { Onyx.merge(ONYXKEYS.NETWORK, {shouldForceOffline}); } @@ -16,4 +20,4 @@ function setShouldFailAllRequests(shouldFailAllRequests: boolean) { Onyx.merge(ONYXKEYS.NETWORK, {shouldFailAllRequests}); } -export {setIsOffline, setShouldForceOffline, setShouldFailAllRequests}; +export {setIsOffline, setShouldForceOffline, setShouldFailAllRequests, setTimeSkew}; diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 3f7dc76b174d..363f159fd7f2 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -315,7 +315,7 @@ function addActions(reportID, text = '', file) { // Always prefer the file as the last action over text const lastAction = attachmentAction || reportCommentAction; - const currentTime = DateUtils.getDBTime(); + const currentTime = DateUtils.getDBTimeWithSkew(); const lastCommentText = ReportUtils.formatReportLastMessageText(lastAction.message[0].text); diff --git a/src/types/onyx/Network.ts b/src/types/onyx/Network.ts index 5af4c1170c3f..32b084bbf2f7 100644 --- a/src/types/onyx/Network.ts +++ b/src/types/onyx/Network.ts @@ -7,6 +7,9 @@ type Network = { /** Whether we should fail all network requests */ shouldFailAllRequests?: boolean; + + /** Skew between the client and server clocks */ + timeSkew?: number; }; export default Network; From 5704405e083297510919af0f44cc3a08319f43d2 Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Mon, 30 Oct 2023 20:21:34 +0100 Subject: [PATCH 002/276] added lib --- src/libs/HttpUtils.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libs/HttpUtils.js b/src/libs/HttpUtils.js index fca7fdf76bb9..f555991f19c6 100644 --- a/src/libs/HttpUtils.js +++ b/src/libs/HttpUtils.js @@ -3,6 +3,7 @@ import _ from 'underscore'; import alert from '@components/Alert'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import * as NetworkActions from './actions/Network'; import * as ApiUtils from './ApiUtils'; import HttpsError from './Errors/HttpsError'; @@ -44,6 +45,7 @@ const regex = /[?&]command=([^&]+)/; */ function processHTTPRequest(url, method = 'get', body = null, canCancel = true) { const startTime = new Date().valueOf(); + return fetch(url, { // We hook requests to the same Controller signal, so we can cancel them all at once signal: canCancel ? cancellationController.signal : undefined, From 9738d5231a98f3148a93b2eefb78e5163960b3ab Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Tue, 31 Oct 2023 15:35:09 +0100 Subject: [PATCH 003/276] lint fix --- src/libs/DateUtils.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts index a1e156a9b7cc..0b127009484e 100644 --- a/src/libs/DateUtils.ts +++ b/src/libs/DateUtils.ts @@ -55,7 +55,7 @@ Onyx.connect({ }, }); -let networkTimeSkew: number = 0; +let networkTimeSkew = 0; Onyx.connect({ key: ONYXKEYS.NETWORK, callback: (value) => (networkTimeSkew = value?.timeSkew), @@ -312,10 +312,8 @@ function getDateStringFromISOTimestamp(isoTimestamp: string): string { /** * Returns the current time plus skew in milliseconds in the format expected by the database - * - * @returns {String} */ -function getDBTimeWithSkew() { +function getDBTimeWithSkew(): string { return getDBTime(new Date().valueOf() + networkTimeSkew); } From e4722b1957949da556b846583b09a5291ce4acc2 Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Tue, 31 Oct 2023 15:44:36 +0100 Subject: [PATCH 004/276] ts check fix --- src/libs/DateUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts index 0b127009484e..f4346f6ff7c0 100644 --- a/src/libs/DateUtils.ts +++ b/src/libs/DateUtils.ts @@ -55,7 +55,7 @@ Onyx.connect({ }, }); -let networkTimeSkew = 0; +let networkTimeSkew: number | undefined = 0; Onyx.connect({ key: ONYXKEYS.NETWORK, callback: (value) => (networkTimeSkew = value?.timeSkew), From 10983780352ae87ffdb123d608169ca4dd4f5fbb Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Tue, 31 Oct 2023 15:49:50 +0100 Subject: [PATCH 005/276] ts check fix --- src/libs/DateUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts index f4346f6ff7c0..b87d5d6e8973 100644 --- a/src/libs/DateUtils.ts +++ b/src/libs/DateUtils.ts @@ -55,10 +55,10 @@ Onyx.connect({ }, }); -let networkTimeSkew: number | undefined = 0; +let networkTimeSkew = 0; Onyx.connect({ key: ONYXKEYS.NETWORK, - callback: (value) => (networkTimeSkew = value?.timeSkew), + callback: (value) => (networkTimeSkew = value?.timeSkew || 0), }); /** From 0428382bcf6640a77f8057791a823e9674c2170f Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Tue, 31 Oct 2023 18:20:20 +0100 Subject: [PATCH 006/276] lint fix --- src/libs/DateUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts index b87d5d6e8973..63f87a97eed2 100644 --- a/src/libs/DateUtils.ts +++ b/src/libs/DateUtils.ts @@ -58,7 +58,7 @@ Onyx.connect({ let networkTimeSkew = 0; Onyx.connect({ key: ONYXKEYS.NETWORK, - callback: (value) => (networkTimeSkew = value?.timeSkew || 0), + callback: (value) => (networkTimeSkew = value?.timeSkew ?? 0), }); /** From fbe9c2836315fc74b4170c345c32efe66a8d03bd Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Thu, 2 Nov 2023 16:08:22 +0100 Subject: [PATCH 007/276] fix --- src/libs/DateUtils.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts index 695dcd573291..62a760e8427a 100644 --- a/src/libs/DateUtils.ts +++ b/src/libs/DateUtils.ts @@ -348,7 +348,10 @@ function getDateStringFromISOTimestamp(isoTimestamp: string): string { * Returns the current time plus skew in milliseconds in the format expected by the database */ function getDBTimeWithSkew(): string { - return getDBTime(new Date().valueOf() + networkTimeSkew); + if (networkTimeSkew > 0) { + return getDBTime(new Date().valueOf() + networkTimeSkew); + } + return getDBTime(); } /** From 532a64d0a5a822b8fd0950d2b4b57d7644c596a1 Mon Sep 17 00:00:00 2001 From: Artem Makushov <39777589+waterim@users.noreply.github.com> Date: Mon, 13 Nov 2023 18:04:17 +0100 Subject: [PATCH 008/276] Update src/libs/HttpUtils.js Co-authored-by: Vit Horacek <36083550+mountiny@users.noreply.github.com> --- src/libs/HttpUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/HttpUtils.js b/src/libs/HttpUtils.js index f555991f19c6..b8fa11ae325f 100644 --- a/src/libs/HttpUtils.js +++ b/src/libs/HttpUtils.js @@ -31,7 +31,7 @@ const addSkewList = ['OpenReport', 'ReconnectApp', 'OpenApp']; /** * Regex to get API command from the command */ -const regex = /[?&]command=([^&]+)/; +const APICommandRegex = /[?&]command=([^&]+)/; /** * Send an HTTP request, and attempt to resolve the json response. From 405a8ad33a93acd30288054a2a2bc21810957ebe Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Mon, 13 Nov 2023 18:32:05 +0100 Subject: [PATCH 009/276] fix tests --- src/libs/HttpUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/HttpUtils.js b/src/libs/HttpUtils.js index b8fa11ae325f..93deaffe412f 100644 --- a/src/libs/HttpUtils.js +++ b/src/libs/HttpUtils.js @@ -53,7 +53,7 @@ function processHTTPRequest(url, method = 'get', body = null, canCancel = true) body, }) .then((response) => { - const match = url.match(regex)[1]; + const match = url.match(APICommandRegex)[1]; if (addSkewList.includes(match) && response.headers) { const serverTime = new Date(response.headers.get('Date')).valueOf(); const endTime = new Date().valueOf(); From 5de34fba8a874c66abed081a6e94509d1bb2d3bf Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 14 Nov 2023 17:50:11 +0500 Subject: [PATCH 010/276] perf: add memoization This memoizes relevant functions and values to not re-render LHNOptionsList and ReportActionsList when there's some update in react tree which is not relevant --- src/components/LHNOptionsList/LHNOptionsList.js | 4 ++-- src/pages/home/report/ReportActionsList.js | 4 ++-- src/pages/home/report/ReportActionsView.js | 14 +++++++------- src/pages/home/sidebar/SidebarLinks.js | 8 +++++--- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.js b/src/components/LHNOptionsList/LHNOptionsList.js index ef1954aeb948..ec031c041c0e 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.js +++ b/src/components/LHNOptionsList/LHNOptionsList.js @@ -1,6 +1,6 @@ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useCallback} from 'react'; +import React, {memo, useCallback} from 'react'; import {FlatList, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -211,4 +211,4 @@ export default compose( key: ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, }, }), -)(LHNOptionsList); +)(memo(LHNOptionsList)); diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 759e73aa90e5..51dce09610d4 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -1,7 +1,7 @@ import {useRoute} 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, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import _ from 'underscore'; import InvertedFlatList from '@components/InvertedFlatList'; @@ -443,4 +443,4 @@ ReportActionsList.propTypes = propTypes; ReportActionsList.defaultProps = defaultProps; ReportActionsList.displayName = 'ReportActionsList'; -export default compose(withWindowDimensions, withPersonalDetails(), withCurrentUserPersonalDetails)(ReportActionsList); +export default compose(withWindowDimensions, withPersonalDetails(), withCurrentUserPersonalDetails)(memo(ReportActionsList)); diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 01ec967d76b1..761c6933ff3f 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -1,7 +1,7 @@ import {useIsFocused} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useContext, useEffect, useMemo, useRef} from 'react'; +import React, {useCallback, useContext, useEffect, useMemo, useRef} from 'react'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import networkPropTypes from '@components/networkPropTypes'; @@ -172,25 +172,25 @@ function ReportActionsView(props) { } }, [props.report, didSubscribeToReportTypingEvents, reportID]); + const oldestReportAction = useMemo(() => _.last(props.reportActions), [props.reportActions]); + /** * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently * displaying. */ - const loadOlderChats = () => { + 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) { return; } - const oldestReportAction = _.last(props.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) { return; } // Retrieve the next REPORT.ACTIONS.LIMIT sized page of comments Report.getOlderActions(reportID, oldestReportAction.reportActionID); - }; + }, [props.network.isOffline, props.isLoadingOlderReportActions, oldestReportAction.actionName, oldestReportAction.reportActionID, reportID]); /** * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently @@ -227,7 +227,7 @@ function ReportActionsView(props) { /** * Runs when the FlatList finishes laying out */ - const recordTimeToMeasureItemLayout = () => { + const recordTimeToMeasureItemLayout = useCallback(() => { if (didLayout.current) { return; } @@ -242,7 +242,7 @@ function ReportActionsView(props) { } else { Performance.markEnd(CONST.TIMING.SWITCH_REPORT); } - }; + }, [hasCachedActions]); // Comments have not loaded at all yet do nothing if (!_.size(props.reportActions)) { diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index ad981a190a70..e6dee6f213d4 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -1,6 +1,6 @@ /* eslint-disable rulesdir/onyx-props-must-have-default */ import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useRef} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import {InteractionManager, View} from 'react-native'; import _ from 'underscore'; import LogoComponent from '@assets/images/expensify-wordmark.svg'; @@ -145,6 +145,8 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority ); const viewMode = priorityMode === CONST.PRIORITY_MODE.GSD ? CONST.OPTION_MODE.COMPACT : CONST.OPTION_MODE.DEFAULT; + const listStyle = useMemo(() => [isLoading ? styles.flexShrink1 : styles.flex1], [isLoading]); + const contentContainerStyles = useMemo(() => [styles.sidebarListContainer, {paddingBottom: StyleUtils.getSafeAreaMargins(insets).marginBottom}], [insets]); return ( @@ -177,8 +179,8 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority Date: Wed, 15 Nov 2023 15:05:32 +0500 Subject: [PATCH 011/276] perf: add memoization --- src/components/OptionsList/BaseOptionsList.js | 22 ++++++++----------- src/components/OptionsList/index.js | 4 ++-- src/components/OptionsList/index.native.js | 6 ++--- 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/components/OptionsList/BaseOptionsList.js b/src/components/OptionsList/BaseOptionsList.js index e0acc2534fbf..cecf983ff989 100644 --- a/src/components/OptionsList/BaseOptionsList.js +++ b/src/components/OptionsList/BaseOptionsList.js @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import React, {forwardRef, memo, useEffect, useRef} from 'react'; +import React, {forwardRef, memo, useEffect, useMemo, useRef} from 'react'; import {View} from 'react-native'; import _ from 'underscore'; import OptionRow from '@components/OptionRow'; @@ -35,7 +35,7 @@ const defaultProps = { ...optionsListDefaultProps, }; -function BaseOptionsList({ +const BaseOptionsList = forwardRef(({ keyboardDismissMode, onScrollBeginDrag, onScroll, @@ -65,16 +65,18 @@ function BaseOptionsList({ onSelectRow, boldStyle, isDisabled, - innerRef, isRowMultilineSupported, isLoadingNewOptions, nestedScrollEnabled, bounces, -}) { + safeAreaPaddingBottomStyle, +}, innerRef) => { const flattenedData = useRef(); const previousSections = usePrevious(sections); const didLayout = useRef(false); + const listContentContainerStyle = useMemo(() => [contentContainerStyles, safeAreaPaddingBottomStyle], [contentContainerStyles, safeAreaPaddingBottomStyle]) + /** * This helper function is used to memoize the computation needed for getItemLayout. It is run whenever section data changes. * @@ -270,7 +272,7 @@ function BaseOptionsList({ scrollEnabled={nestedScrollEnabled} onScrollBeginDrag={onScrollBeginDrag} onScroll={onScroll} - contentContainerStyle={contentContainerStyles} + contentContainerStyle={listContentContainerStyle} showsVerticalScrollIndicator={showScrollIndicator} sections={sections} keyExtractor={extractKey} @@ -290,7 +292,7 @@ function BaseOptionsList({ )} ); -} +}); BaseOptionsList.propTypes = propTypes; BaseOptionsList.defaultProps = defaultProps; @@ -298,13 +300,7 @@ BaseOptionsList.displayName = 'BaseOptionsList'; // using memo to avoid unnecessary rerenders when parents component rerenders (thus causing this component to rerender because shallow comparison is used for some props). export default memo( - forwardRef((props, ref) => ( - - )), + BaseOptionsList, (prevProps, nextProps) => nextProps.focusedIndex === prevProps.focusedIndex && nextProps.selectedOptions.length === prevProps.selectedOptions.length && diff --git a/src/components/OptionsList/index.js b/src/components/OptionsList/index.js index 36b8e7fccf12..6046a6124ccc 100644 --- a/src/components/OptionsList/index.js +++ b/src/components/OptionsList/index.js @@ -1,4 +1,4 @@ -import React, {forwardRef, useCallback, useEffect, useRef} from 'react'; +import React, {forwardRef, memo, useCallback, useEffect, useRef} from 'react'; import {Keyboard} from 'react-native'; import _ from 'underscore'; import withWindowDimensions from '@components/withWindowDimensions'; @@ -64,4 +64,4 @@ const OptionsListWithRef = forwardRef((props, ref) => ( OptionsListWithRef.displayName = 'OptionsListWithRef'; -export default withWindowDimensions(OptionsListWithRef); +export default withWindowDimensions(memo(OptionsListWithRef)); diff --git a/src/components/OptionsList/index.native.js b/src/components/OptionsList/index.native.js index ab2db4f20967..8a70e1e060b1 100644 --- a/src/components/OptionsList/index.native.js +++ b/src/components/OptionsList/index.native.js @@ -1,4 +1,4 @@ -import React, {forwardRef} from 'react'; +import React, {forwardRef, memo} from 'react'; import {Keyboard} from 'react-native'; import BaseOptionsList from './BaseOptionsList'; import {defaultProps, propTypes} from './optionsListPropTypes'; @@ -8,7 +8,7 @@ const OptionsList = forwardRef((props, ref) => ( // eslint-disable-next-line react/jsx-props-no-spreading {...props} ref={ref} - onScrollBeginDrag={() => Keyboard.dismiss()} + onScrollBeginDrag={Keyboard.dismiss} /> )); @@ -16,4 +16,4 @@ OptionsList.propTypes = propTypes; OptionsList.defaultProps = defaultProps; OptionsList.displayName = 'OptionsList'; -export default OptionsList; +export default memo(OptionsList); From bbb216dd728eb135709a7892b793247c5cdb820d Mon Sep 17 00:00:00 2001 From: hurali97 Date: Wed, 15 Nov 2023 15:07:28 +0500 Subject: [PATCH 012/276] perf: add navigation listeners and remove inline functions --- .../OptionsSelector/BaseOptionsSelector.js | 109 ++++++++++-------- 1 file changed, 62 insertions(+), 47 deletions(-) diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js index 8c480c27f20f..cdf2b83b6215 100755 --- a/src/components/OptionsSelector/BaseOptionsSelector.js +++ b/src/components/OptionsSelector/BaseOptionsSelector.js @@ -1,7 +1,7 @@ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {Component} from 'react'; -import {ScrollView, View} from 'react-native'; +import {InteractionManager, ScrollView, View} from 'react-native'; import _ from 'underscore'; import ArrowKeyFocusManager from '@components/ArrowKeyFocusManager'; import Button from '@components/Button'; @@ -10,7 +10,7 @@ import FormHelpMessage from '@components/FormHelpMessage'; import OptionsList from '@components/OptionsList'; import TextInput from '@components/TextInput'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import withNavigationFocus from '@components/withNavigationFocus'; +import withNavigation from '@components/withNavigation'; import compose from '@libs/compose'; import getPlatform from '@libs/getPlatform'; import KeyboardShortcut from '@libs/KeyboardShortcut'; @@ -32,9 +32,6 @@ const propTypes = { /** List styles for OptionsList */ listStyles: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), - /** Whether navigation is focused */ - isFocused: PropTypes.bool.isRequired, - ...optionsSelectorPropTypes, ...withLocalizePropTypes, }; @@ -58,49 +55,59 @@ class BaseOptionsSelector extends Component { this.selectFocusedOption = this.selectFocusedOption.bind(this); this.addToSelection = this.addToSelection.bind(this); this.updateSearchValue = this.updateSearchValue.bind(this); + this.onLayout = this.onLayout.bind(this); + this.setListRef = this.setListRef.bind(this); this.relatedTarget = null; - const allOptions = this.flattenSections(); - const focusedIndex = this.getInitiallyFocusedIndex(allOptions); - + this.focusListener = null; + this.blurListener = null; + this.isFocused = false; this.state = { - allOptions, - focusedIndex, + allOptions: [], + focusedIndex: 0, shouldDisableRowSelection: false, errorMessage: '', }; } componentDidMount() { - this.subscribeToKeyboardShortcut(); + this.focusListener = this.props.navigation.addListener('focus', () => { + this.subscribeToKeyboardShortcut(); + + // Screen coming back into focus, for example + // when doing Cmd+Shift+K, then Cmd+K, then Cmd+Shift+K. + // Only applies to platforms that support keyboard shortcuts + if ([CONST.PLATFORM.DESKTOP, CONST.PLATFORM.WEB].includes(getPlatform()) && this.props.autoFocus && this.textInput) { + this.focusTimeout = setTimeout(() => { + this.textInput.focus(); + }, CONST.ANIMATED_TRANSITION); + } - if (this.props.isFocused && this.props.autoFocus && this.textInput) { - this.focusTimeout = setTimeout(() => { - this.textInput.focus(); - }, CONST.ANIMATED_TRANSITION); - } + this.isFocused = true; + }); + this.blurListener = this.props.navigation.addListener('blur', () => { + this.unSubscribeFromKeyboardShortcut(); + this.isFocused = false; + }); this.scrollToIndex(this.props.selectedOptions.length ? 0 : this.state.focusedIndex, false); + + /** + * Execute the following code after all interactions have been completed. + * Which means once we are sure that all navigation animations are done, + * we will execute the callback passed to `runAfterInteractions`. + */ + this.interactionTask = InteractionManager.runAfterInteractions(() => { + const allOptions = this.flattenSections(); + const focusedIndex = this.getInitiallyFocusedIndex(allOptions); + this.setState({ + allOptions, + focusedIndex, + }); + }); } componentDidUpdate(prevProps) { - if (prevProps.isFocused !== this.props.isFocused) { - if (this.props.isFocused) { - this.subscribeToKeyboardShortcut(); - } else { - this.unSubscribeFromKeyboardShortcut(); - } - } - - // Screen coming back into focus, for example - // when doing Cmd+Shift+K, then Cmd+K, then Cmd+Shift+K. - // Only applies to platforms that support keyboard shortcuts - if ([CONST.PLATFORM.DESKTOP, CONST.PLATFORM.WEB].includes(getPlatform()) && !prevProps.isFocused && this.props.isFocused && this.props.autoFocus && this.textInput) { - setTimeout(() => { - this.textInput.focus(); - }, CONST.ANIMATED_TRANSITION); - } - if (_.isEqual(this.props.sections, prevProps.sections)) { return; } @@ -139,11 +146,22 @@ class BaseOptionsSelector extends Component { } componentWillUnmount() { + this.interactionTask.cancel(); + this.focusListener(); + this.blurListener(); if (this.focusTimeout) { clearTimeout(this.focusTimeout); } + } - this.unSubscribeFromKeyboardShortcut(); + onLayout() { + if (this.props.selectedOptions.length === 0) { + this.scrollToIndex(this.state.focusedIndex, false); + } + + if (this.props.onLayout) { + this.props.onLayout(); + } } /** @@ -172,6 +190,10 @@ class BaseOptionsSelector extends Component { return defaultIndex; } + setListRef(ref) { + this.list = ref; + } + updateSearchValue(value) { this.setState({ errorMessage: value.length > this.props.maxLength ? this.props.translate('common.error.characterLimitExceedCounter', {length: value.length, limit: this.props.maxLength}) : '', @@ -226,7 +248,7 @@ class BaseOptionsSelector extends Component { selectFocusedOption() { const focusedOption = this.state.allOptions[this.state.focusedIndex]; - if (!focusedOption || !this.props.isFocused) { + if (!focusedOption || !this.isFocused) { return; } @@ -400,7 +422,7 @@ class BaseOptionsSelector extends Component { ); const optionsList = ( (this.list = el)} + ref={this.setListRef} optionHoveredStyle={this.props.optionHoveredStyle} onSelectRow={this.props.onSelectRow ? this.selectRow : undefined} sections={this.props.sections} @@ -417,16 +439,9 @@ class BaseOptionsSelector extends Component { isDisabled={this.props.isDisabled} shouldHaveOptionSeparator={this.props.shouldHaveOptionSeparator} highlightSelectedOptions={this.props.highlightSelectedOptions} - onLayout={() => { - if (this.props.selectedOptions.length === 0) { - this.scrollToIndex(this.state.focusedIndex, false); - } - - if (this.props.onLayout) { - this.props.onLayout(); - } - }} - contentContainerStyles={[safeAreaPaddingBottomStyle, ...this.props.contentContainerStyles]} + onLayout={this.onLayout} + safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle} + contentContainerStyles={this.props.contentContainerStyles} sectionHeaderStyle={this.props.sectionHeaderStyle} listContainerStyles={this.props.listContainerStyles} listStyles={this.props.listStyles} @@ -518,4 +533,4 @@ class BaseOptionsSelector extends Component { BaseOptionsSelector.defaultProps = defaultProps; BaseOptionsSelector.propTypes = propTypes; -export default compose(withLocalize, withNavigationFocus)(BaseOptionsSelector); +export default compose(withLocalize, withNavigation)(BaseOptionsSelector); From 15147c6e7c6a09ca0e54a3c3351cfbebe9e18d6c Mon Sep 17 00:00:00 2001 From: hurali97 Date: Wed, 15 Nov 2023 15:09:45 +0500 Subject: [PATCH 013/276] refactor: use personalDetails from utils and add Interaction Manager --- src/libs/PersonalDetailsUtils.js | 11 ++++++- src/pages/SearchPage.js | 49 ++++++++++++++++++-------------- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/src/libs/PersonalDetailsUtils.js b/src/libs/PersonalDetailsUtils.js index c99adc32a56a..3a1038700537 100644 --- a/src/libs/PersonalDetailsUtils.js +++ b/src/libs/PersonalDetailsUtils.js @@ -177,4 +177,13 @@ function getFormattedAddress(privatePersonalDetails) { return formattedAddress.trim().replace(/,$/, ''); } -export {getDisplayNameOrDefault, getPersonalDetailsByIDs, getAccountIDsByLogins, getLoginsByAccountIDs, getNewPersonalDetailsOnyxData, getFormattedAddress}; +/** + * get personal details + * + * @returns {Object} + */ +function getPersonalDetails() { + return allPersonalDetails || {}; +} + +export {getPersonalDetails, getDisplayNameOrDefault, getPersonalDetailsByIDs, getAccountIDsByLogins, getLoginsByAccountIDs, getNewPersonalDetailsOnyxData, getFormattedAddress}; diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js index 3e7731efc7b2..7d9f9818c309 100755 --- a/src/pages/SearchPage.js +++ b/src/pages/SearchPage.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; import React, {Component} from 'react'; -import {View} from 'react-native'; +import {InteractionManager, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -14,13 +14,13 @@ import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import Performance from '@libs/Performance'; +import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import styles from '@styles/styles'; import * as Report from '@userActions/Report'; import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import personalDetailsPropType from './personalDetailsPropType'; import reportPropTypes from './reportPropTypes'; const propTypes = { @@ -29,9 +29,6 @@ const propTypes = { /** Beta features list */ betas: PropTypes.arrayOf(PropTypes.string), - /** All of the personal details for everyone */ - personalDetails: PropTypes.objectOf(personalDetailsPropType), - /** All reports shared with the user */ reports: PropTypes.objectOf(reportPropTypes), @@ -49,7 +46,6 @@ const propTypes = { const defaultProps = { betas: [], - personalDetails: {}, reports: {}, network: {}, isSearchingForReports: false, @@ -76,12 +72,16 @@ class SearchPage extends Component { } componentDidUpdate(prevProps) { - if (_.isEqual(prevProps.reports, this.props.reports) && _.isEqual(prevProps.personalDetails, this.props.personalDetails)) { + if (_.isEqual(prevProps.reports, this.props.reports)) { return; } this.updateOptions(); } + componentWillUnmount() { + this.interactionTask.cancel(); + } + onChangeText(searchValue = '') { if (searchValue.length) { Report.searchInServer(searchValue); @@ -134,16 +134,26 @@ class SearchPage extends Component { } updateOptions() { - const {recentReports, personalDetails, userToInvite} = OptionsListUtils.getSearchOptions( - this.props.reports, - this.props.personalDetails, - this.state.searchValue.trim(), - this.props.betas, - ); - this.setState({ - userToInvite, - recentReports, - personalDetails, + if (this.interactionTask) { + this.interactionTask.cancel(); + } + + /** + * Execute the callback after all interactions are done, which means + * after all animations have finished. + */ + this.interactionTask = InteractionManager.runAfterInteractions(() => { + const {recentReports, personalDetails, userToInvite} = OptionsListUtils.getSearchOptions( + this.props.reports, + PersonalDetailsUtils.getPersonalDetails(), + this.state.searchValue.trim(), + this.props.betas, + ); + this.setState({ + userToInvite, + recentReports, + personalDetails, + }); }); } @@ -173,7 +183,7 @@ class SearchPage extends Component { render() { const sections = this.getSections(); - const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(this.props.personalDetails); + const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(this.state.personalDetails); const headerMessage = OptionsListUtils.getHeaderMessage( this.state.recentReports.length + this.state.personalDetails.length !== 0, Boolean(this.state.userToInvite), @@ -228,9 +238,6 @@ export default compose( reports: { key: ONYXKEYS.COLLECTION.REPORT, }, - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, betas: { key: ONYXKEYS.BETAS, }, From 15e62f92aa9eb07d2ed3ac9ff9b71717395fca47 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Wed, 15 Nov 2023 17:31:22 +0500 Subject: [PATCH 014/276] fix: linting --- src/components/OptionsList/BaseOptionsList.js | 489 +++++++++--------- 1 file changed, 247 insertions(+), 242 deletions(-) diff --git a/src/components/OptionsList/BaseOptionsList.js b/src/components/OptionsList/BaseOptionsList.js index cecf983ff989..d303c6f58073 100644 --- a/src/components/OptionsList/BaseOptionsList.js +++ b/src/components/OptionsList/BaseOptionsList.js @@ -35,264 +35,269 @@ const defaultProps = { ...optionsListDefaultProps, }; -const BaseOptionsList = forwardRef(({ - keyboardDismissMode, - onScrollBeginDrag, - onScroll, - listStyles, - focusedIndex, - selectedOptions, - headerMessage, - isLoading, - sections, - onLayout, - hideSectionHeaders, - shouldHaveOptionSeparator, - showTitleTooltip, - optionHoveredStyle, - contentContainerStyles, - sectionHeaderStyle, - showScrollIndicator, - listContainerStyles, - shouldDisableRowInnerPadding, - shouldPreventDefaultFocusOnSelectRow, - disableFocusOptions, - canSelectMultipleOptions, - shouldShowMultipleOptionSelectorAsButton, - multipleOptionSelectorButtonText, - onAddToSelection, - highlightSelectedOptions, - onSelectRow, - boldStyle, - isDisabled, - isRowMultilineSupported, - isLoadingNewOptions, - nestedScrollEnabled, - bounces, - safeAreaPaddingBottomStyle, -}, innerRef) => { - const flattenedData = useRef(); - const previousSections = usePrevious(sections); - const didLayout = useRef(false); - - const listContentContainerStyle = useMemo(() => [contentContainerStyles, safeAreaPaddingBottomStyle], [contentContainerStyles, safeAreaPaddingBottomStyle]) - - /** - * This helper function is used to memoize the computation needed for getItemLayout. It is run whenever section data changes. - * - * @returns {Array} - */ - const buildFlatSectionArray = () => { - let offset = 0; - - // Start with just an empty list header - const flatArray = [{length: 0, offset}]; - - // Build the flat array - for (let sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) { - const section = sections[sectionIndex]; - - // Add the section header - const sectionHeaderHeight = section.title && !hideSectionHeaders ? variables.optionsListSectionHeaderHeight : 0; - flatArray.push({length: sectionHeaderHeight, offset}); - offset += sectionHeaderHeight; - - // Add section items - for (let i = 0; i < section.data.length; i++) { - let fullOptionHeight = variables.optionRowHeight; - if (i > 0 && shouldHaveOptionSeparator) { - fullOptionHeight += variables.borderTopWidth; +const BaseOptionsList = forwardRef( + ( + { + keyboardDismissMode, + onScrollBeginDrag, + onScroll, + listStyles, + focusedIndex, + selectedOptions, + headerMessage, + isLoading, + sections, + onLayout, + hideSectionHeaders, + shouldHaveOptionSeparator, + showTitleTooltip, + optionHoveredStyle, + contentContainerStyles, + sectionHeaderStyle, + showScrollIndicator, + listContainerStyles, + shouldDisableRowInnerPadding, + shouldPreventDefaultFocusOnSelectRow, + disableFocusOptions, + canSelectMultipleOptions, + shouldShowMultipleOptionSelectorAsButton, + multipleOptionSelectorButtonText, + onAddToSelection, + highlightSelectedOptions, + onSelectRow, + boldStyle, + isDisabled, + isRowMultilineSupported, + isLoadingNewOptions, + nestedScrollEnabled, + bounces, + safeAreaPaddingBottomStyle, + }, + innerRef, + ) => { + const flattenedData = useRef(); + const previousSections = usePrevious(sections); + const didLayout = useRef(false); + + const listContentContainerStyle = useMemo(() => [contentContainerStyles, safeAreaPaddingBottomStyle], [contentContainerStyles, safeAreaPaddingBottomStyle]); + + /** + * This helper function is used to memoize the computation needed for getItemLayout. It is run whenever section data changes. + * + * @returns {Array} + */ + const buildFlatSectionArray = () => { + let offset = 0; + + // Start with just an empty list header + const flatArray = [{length: 0, offset}]; + + // Build the flat array + for (let sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) { + const section = sections[sectionIndex]; + + // Add the section header + const sectionHeaderHeight = section.title && !hideSectionHeaders ? variables.optionsListSectionHeaderHeight : 0; + flatArray.push({length: sectionHeaderHeight, offset}); + offset += sectionHeaderHeight; + + // Add section items + for (let i = 0; i < section.data.length; i++) { + let fullOptionHeight = variables.optionRowHeight; + if (i > 0 && shouldHaveOptionSeparator) { + fullOptionHeight += variables.borderTopWidth; + } + flatArray.push({length: fullOptionHeight, offset}); + offset += fullOptionHeight; } - flatArray.push({length: fullOptionHeight, offset}); - offset += fullOptionHeight; + + // Add the section footer + flatArray.push({length: 0, offset}); } - // Add the section footer + // Then add the list footer flatArray.push({length: 0, offset}); - } - - // Then add the list footer - flatArray.push({length: 0, offset}); - return flatArray; - }; - - useEffect(() => { - if (_.isEqual(sections, previousSections)) { - return; - } - flattenedData.current = buildFlatSectionArray(); - }); - - const onViewableItemsChanged = () => { - if (didLayout.current || !onLayout) { - return; - } - - didLayout.current = true; - onLayout(); - }; - - /** - * This function is used to compute the layout of any given item in our list. - * We need to implement it so that we can programmatically scroll to items outside the virtual render window of the SectionList. - * - * @param {Array} data - This is the same as the data we pass into the component - * @param {Number} flatDataArrayIndex - This index is provided by React Native, and refers to a flat array with data from all the sections. This flat array has some quirks: - * - * 1. It ALWAYS includes a list header and a list footer, even if we don't provide/render those. - * 2. Each section includes a header, even if we don't provide/render one. - * - * For example, given a list with two sections, two items in each section, no header, no footer, and no section headers, the flat array might look something like this: - * - * [{header}, {sectionHeader}, {item}, {item}, {sectionHeader}, {item}, {item}, {footer}] - * - * @returns {Object} - */ - const getItemLayout = (data, flatDataArrayIndex) => { - if (!_.has(flattenedData.current, flatDataArrayIndex)) { + return flatArray; + }; + + useEffect(() => { + if (_.isEqual(sections, previousSections)) { + return; + } flattenedData.current = buildFlatSectionArray(); - } + }); + + const onViewableItemsChanged = () => { + if (didLayout.current || !onLayout) { + return; + } - const targetItem = flattenedData.current[flatDataArrayIndex]; - return { - length: targetItem.length, - offset: targetItem.offset, - index: flatDataArrayIndex, + didLayout.current = true; + onLayout(); }; - }; - - /** - * Returns the key used by the list - * @param {Object} option - * @return {String} - */ - const extractKey = (option) => option.keyForList; - - /** - * Function which renders a row in the list - * - * @param {Object} params - * @param {Object} params.item - * @param {Number} params.index - * @param {Object} params.section - * - * @return {Component} - */ - const renderItem = ({item, index, section}) => { - const isItemDisabled = isDisabled || section.isDisabled || !!item.isDisabled; - const isSelected = _.some(selectedOptions, (option) => { - if (option.accountID && option.accountID === item.accountID) { - return true; + + /** + * This function is used to compute the layout of any given item in our list. + * We need to implement it so that we can programmatically scroll to items outside the virtual render window of the SectionList. + * + * @param {Array} data - This is the same as the data we pass into the component + * @param {Number} flatDataArrayIndex - This index is provided by React Native, and refers to a flat array with data from all the sections. This flat array has some quirks: + * + * 1. It ALWAYS includes a list header and a list footer, even if we don't provide/render those. + * 2. Each section includes a header, even if we don't provide/render one. + * + * For example, given a list with two sections, two items in each section, no header, no footer, and no section headers, the flat array might look something like this: + * + * [{header}, {sectionHeader}, {item}, {item}, {sectionHeader}, {item}, {item}, {footer}] + * + * @returns {Object} + */ + const getItemLayout = (data, flatDataArrayIndex) => { + if (!_.has(flattenedData.current, flatDataArrayIndex)) { + flattenedData.current = buildFlatSectionArray(); } - if (option.reportID && option.reportID === item.reportID) { - return true; + const targetItem = flattenedData.current[flatDataArrayIndex]; + return { + length: targetItem.length, + offset: targetItem.offset, + index: flatDataArrayIndex, + }; + }; + + /** + * Returns the key used by the list + * @param {Object} option + * @return {String} + */ + const extractKey = (option) => option.keyForList; + + /** + * Function which renders a row in the list + * + * @param {Object} params + * @param {Object} params.item + * @param {Number} params.index + * @param {Object} params.section + * + * @return {Component} + */ + const renderItem = ({item, index, section}) => { + const isItemDisabled = isDisabled || section.isDisabled || !!item.isDisabled; + const isSelected = _.some(selectedOptions, (option) => { + if (option.accountID && option.accountID === item.accountID) { + return true; + } + + if (option.reportID && option.reportID === item.reportID) { + return true; + } + + if (_.isEmpty(option.name)) { + return false; + } + + return option.name === item.searchText; + }); + + return ( + 0 && shouldHaveOptionSeparator} + shouldDisableRowInnerPadding={shouldDisableRowInnerPadding} + shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow} + isMultilineSupported={isRowMultilineSupported} + /> + ); + }; + + /** + * Function which renders a section header component + * + * @param {Object} params + * @param {Object} params.section + * @param {String} params.section.title + * @param {Boolean} params.section.shouldShow + * + * @return {Component} + */ + const renderSectionHeader = ({section: {title, shouldShow}}) => { + if (!title && shouldShow && !hideSectionHeaders && sectionHeaderStyle) { + return ; } - if (_.isEmpty(option.name)) { - return false; + if (title && shouldShow && !hideSectionHeaders) { + return ( + // Note: The `optionsListSectionHeader` style provides an explicit height to section headers. + // We do this so that we can reference the height in `getItemLayout` – + // we need to know the heights of all list items up-front in order to synchronously compute the layout of any given list item. + // So be aware that if you adjust the content of the section header (for example, change the font size), you may need to adjust this explicit height as well. + + {title} + + ); } - return option.name === item.searchText; - }); + return ; + }; return ( - 0 && shouldHaveOptionSeparator} - shouldDisableRowInnerPadding={shouldDisableRowInnerPadding} - shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow} - isMultilineSupported={isRowMultilineSupported} - /> + + {isLoading ? ( + + ) : ( + <> + {/* If we are loading new options we will avoid showing any header message. This is mostly because one of the header messages says there are no options. */} + {/* This is misleading because we might be in the process of loading fresh options from the server. */} + {!isLoadingNewOptions && headerMessage ? ( + + {headerMessage} + + ) : null} + + + )} + ); - }; - - /** - * Function which renders a section header component - * - * @param {Object} params - * @param {Object} params.section - * @param {String} params.section.title - * @param {Boolean} params.section.shouldShow - * - * @return {Component} - */ - const renderSectionHeader = ({section: {title, shouldShow}}) => { - if (!title && shouldShow && !hideSectionHeaders && sectionHeaderStyle) { - return ; - } - - if (title && shouldShow && !hideSectionHeaders) { - return ( - // Note: The `optionsListSectionHeader` style provides an explicit height to section headers. - // We do this so that we can reference the height in `getItemLayout` – - // we need to know the heights of all list items up-front in order to synchronously compute the layout of any given list item. - // So be aware that if you adjust the content of the section header (for example, change the font size), you may need to adjust this explicit height as well. - - {title} - - ); - } - - return ; - }; - - return ( - - {isLoading ? ( - - ) : ( - <> - {/* If we are loading new options we will avoid showing any header message. This is mostly because one of the header messages says there are no options. */} - {/* This is misleading because we might be in the process of loading fresh options from the server. */} - {!isLoadingNewOptions && headerMessage ? ( - - {headerMessage} - - ) : null} - - - )} - - ); -}); + }, +); BaseOptionsList.propTypes = propTypes; BaseOptionsList.defaultProps = defaultProps; From aab633b9bb74d783f42d96c30e2d0242567e5f01 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Thu, 16 Nov 2023 12:14:34 +0500 Subject: [PATCH 015/276] refactor: focus text input --- .../OptionsSelector/BaseOptionsSelector.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js index cdf2b83b6215..682743ec7f01 100755 --- a/src/components/OptionsSelector/BaseOptionsSelector.js +++ b/src/components/OptionsSelector/BaseOptionsSelector.js @@ -72,12 +72,11 @@ class BaseOptionsSelector extends Component { componentDidMount() { this.focusListener = this.props.navigation.addListener('focus', () => { - this.subscribeToKeyboardShortcut(); + if ([CONST.PLATFORM.DESKTOP, CONST.PLATFORM.WEB].includes(getPlatform())) { + this.subscribeToKeyboardShortcut(); + } - // Screen coming back into focus, for example - // when doing Cmd+Shift+K, then Cmd+K, then Cmd+Shift+K. - // Only applies to platforms that support keyboard shortcuts - if ([CONST.PLATFORM.DESKTOP, CONST.PLATFORM.WEB].includes(getPlatform()) && this.props.autoFocus && this.textInput) { + if (this.props.autoFocus && this.textInput) { this.focusTimeout = setTimeout(() => { this.textInput.focus(); }, CONST.ANIMATED_TRANSITION); @@ -87,7 +86,9 @@ class BaseOptionsSelector extends Component { }); this.blurListener = this.props.navigation.addListener('blur', () => { - this.unSubscribeFromKeyboardShortcut(); + if ([CONST.PLATFORM.DESKTOP, CONST.PLATFORM.WEB].includes(getPlatform())) { + this.unSubscribeFromKeyboardShortcut(); + } this.isFocused = false; }); this.scrollToIndex(this.props.selectedOptions.length ? 0 : this.state.focusedIndex, false); From 0e2833010b3be51885059123e6275d6341b3d4d8 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 28 Nov 2023 13:07:10 +0500 Subject: [PATCH 016/276] fix: skeleton being shown when typing --- src/pages/SearchPage.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js index a6323729a86d..6759b08060d7 100755 --- a/src/pages/SearchPage.js +++ b/src/pages/SearchPage.js @@ -52,6 +52,18 @@ const defaultProps = { isSearchingForReports: false, }; +function isSectionsEmpty(sections) { + if (!sections.length) { + return true; + } + + if (!sections[0].data.length) { + return true; + } + + return _.isEmpty(sections[0].data[0]); +} + class SearchPage extends Component { constructor(props) { super(props); @@ -184,7 +196,7 @@ class SearchPage extends Component { render() { const sections = this.getSections(); - const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(this.state.personalDetails); + const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(PersonalDetailsUtils.getPersonalDetails()); const headerMessage = OptionsListUtils.getHeaderMessage( this.state.recentReports.length + this.state.personalDetails.length !== 0, Boolean(this.state.userToInvite), @@ -209,7 +221,7 @@ class SearchPage extends Component { headerMessage={headerMessage} hideSectionHeaders showTitleTooltip - shouldShowOptions={didScreenTransitionEnd && isOptionsDataReady} + shouldShowOptions={didScreenTransitionEnd && isOptionsDataReady && !isSectionsEmpty(sections)} textInputLabel={this.props.translate('optionsSelector.nameEmailOrPhoneNumber')} textInputAlert={ this.props.network.isOffline ? `${this.props.translate('common.youAppearToBeOffline')} ${this.props.translate('search.resultsAreLimited')}` : '' From e951bb7747210447028f350e5935f0bedb6e83e6 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Tue, 28 Nov 2023 17:24:39 +0100 Subject: [PATCH 017/276] [TS migration] Migrate 'TransactionEdit.js' lib --- src/ONYXKEYS.ts | 1 + .../{TransactionEdit.js => TransactionEdit.ts} | 17 ++++++++++------- 2 files changed, 11 insertions(+), 7 deletions(-) rename src/libs/actions/{TransactionEdit.js => TransactionEdit.ts} (76%) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 5576eb64736d..0cb3f67bd990 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -446,6 +446,7 @@ type OnyxValues = { [ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM]: boolean; [ONYXKEYS.COLLECTION.SECURITY_GROUP]: OnyxTypes.SecurityGroup; [ONYXKEYS.COLLECTION.TRANSACTION]: OnyxTypes.Transaction; + [ONYXKEYS.COLLECTION.TRANSACTION_DRAFT]: OnyxTypes.Transaction; [ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS]: OnyxTypes.RecentlyUsedTags; [ONYXKEYS.COLLECTION.SELECTED_TAB]: string; diff --git a/src/libs/actions/TransactionEdit.js b/src/libs/actions/TransactionEdit.ts similarity index 76% rename from src/libs/actions/TransactionEdit.js rename to src/libs/actions/TransactionEdit.ts index 2cb79ac387bd..387dacddbcdc 100644 --- a/src/libs/actions/TransactionEdit.js +++ b/src/libs/actions/TransactionEdit.ts @@ -1,28 +1,31 @@ -import Onyx from 'react-native-onyx'; +import Onyx, {OnyxEntry} from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; +import {Transaction} from '@src/types/onyx'; /** * Makes a backup copy of a transaction object that can be restored when the user cancels editing a transaction. - * - * @param {Object} transaction */ -function createBackupTransaction(transaction) { +function createBackupTransaction(transaction: OnyxEntry) { + if (!transaction) { + return; + } + const newTransaction = { ...transaction, }; + // Use set so that it will always fully overwrite any backup transaction that could have existed before Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transaction.transactionID}`, newTransaction); } /** * Removes a transaction from Onyx that was only used temporary in the edit flow - * @param {String} transactionID */ -function removeBackupTransaction(transactionID) { +function removeBackupTransaction(transactionID: string) { Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, null); } -function restoreOriginalTransactionFromBackup(transactionID) { +function restoreOriginalTransactionFromBackup(transactionID: string) { const connectionID = Onyx.connect({ key: `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, callback: (backupTransaction) => { From 76c1781273e5dd7829d8aa92e87de8e7a0503d4b Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Tue, 28 Nov 2023 17:58:49 +0100 Subject: [PATCH 018/276] [TS migration] Migrate 'MemoryOnlyKeys' lib --- .../{MemoryOnlyKeys.js => MemoryOnlyKeys.ts} | 3 ++- .../exposeGlobalMemoryOnlyKeysMethods/index.js | 12 ------------ .../index.native.js | 6 ------ .../index.native.ts | 8 ++++++++ .../exposeGlobalMemoryOnlyKeysMethods/index.ts | 18 ++++++++++++++++++ .../exposeGlobalMemoryOnlyKeysMethods/types.ts | 3 +++ 6 files changed, 31 insertions(+), 19 deletions(-) rename src/libs/actions/MemoryOnlyKeys/{MemoryOnlyKeys.js => MemoryOnlyKeys.ts} (72%) delete mode 100644 src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.js delete mode 100644 src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.native.js create mode 100644 src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.native.ts create mode 100644 src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.ts create mode 100644 src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/types.ts diff --git a/src/libs/actions/MemoryOnlyKeys/MemoryOnlyKeys.js b/src/libs/actions/MemoryOnlyKeys/MemoryOnlyKeys.ts similarity index 72% rename from src/libs/actions/MemoryOnlyKeys/MemoryOnlyKeys.js rename to src/libs/actions/MemoryOnlyKeys/MemoryOnlyKeys.ts index 028bce225909..79d1ec0f82d9 100644 --- a/src/libs/actions/MemoryOnlyKeys/MemoryOnlyKeys.js +++ b/src/libs/actions/MemoryOnlyKeys/MemoryOnlyKeys.ts @@ -1,8 +1,9 @@ import Onyx from 'react-native-onyx'; +import {OnyxKey} from 'react-native-onyx/lib/types'; import Log from '@libs/Log'; import ONYXKEYS from '@src/ONYXKEYS'; -const memoryOnlyKeys = [ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.COLLECTION.POLICY, ONYXKEYS.PERSONAL_DETAILS_LIST]; +const memoryOnlyKeys: OnyxKey[] = [ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.COLLECTION.POLICY, ONYXKEYS.PERSONAL_DETAILS_LIST]; const enable = () => { Log.info('[MemoryOnlyKeys] enabled'); diff --git a/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.js b/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.js deleted file mode 100644 index 1d039c8980a9..000000000000 --- a/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.js +++ /dev/null @@ -1,12 +0,0 @@ -import * as MemoryOnlyKeys from '@userActions/MemoryOnlyKeys/MemoryOnlyKeys'; - -const exposeGlobalMemoryOnlyKeysMethods = () => { - window.enableMemoryOnlyKeys = () => { - MemoryOnlyKeys.enable(); - }; - window.disableMemoryOnlyKeys = () => { - MemoryOnlyKeys.disable(); - }; -}; - -export default exposeGlobalMemoryOnlyKeysMethods; diff --git a/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.native.js b/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.native.js deleted file mode 100644 index 9d08b9db6aa4..000000000000 --- a/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.native.js +++ /dev/null @@ -1,6 +0,0 @@ -/** - * This is a no-op because the global methods will only work for web and desktop - */ -const exposeGlobalMemoryOnlyKeysMethods = () => {}; - -export default exposeGlobalMemoryOnlyKeysMethods; diff --git a/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.native.ts b/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.native.ts new file mode 100644 index 000000000000..b89e03bdefdc --- /dev/null +++ b/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.native.ts @@ -0,0 +1,8 @@ +import type ExposeGlobalMemoryOnlyKeysMethods from './types'; + +/** + * This is a no-op because the global methods will only work for web and desktop + */ +const exposeGlobalMemoryOnlyKeysMethods: ExposeGlobalMemoryOnlyKeysMethods = () => {}; + +export default exposeGlobalMemoryOnlyKeysMethods; diff --git a/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.ts b/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.ts new file mode 100644 index 000000000000..6d72188803d7 --- /dev/null +++ b/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.ts @@ -0,0 +1,18 @@ +import * as MemoryOnlyKeys from '@userActions/MemoryOnlyKeys/MemoryOnlyKeys'; +import type ExposeGlobalMemoryOnlyKeysMethods from './types'; + +type WindowWithMemoryOnlyKeys = Window & { + enableMemoryOnlyKeys?: () => void; + disableMemoryOnlyKeys?: () => void; +}; + +const exposeGlobalMemoryOnlyKeysMethods: ExposeGlobalMemoryOnlyKeysMethods = () => { + (window as WindowWithMemoryOnlyKeys).enableMemoryOnlyKeys = () => { + MemoryOnlyKeys.enable(); + }; + (window as WindowWithMemoryOnlyKeys).disableMemoryOnlyKeys = () => { + MemoryOnlyKeys.disable(); + }; +}; + +export default exposeGlobalMemoryOnlyKeysMethods; diff --git a/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/types.ts b/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/types.ts new file mode 100644 index 000000000000..4cb50041b627 --- /dev/null +++ b/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/types.ts @@ -0,0 +1,3 @@ +type ExposeGlobalMemoryOnlyKeysMethods = () => void; + +export default ExposeGlobalMemoryOnlyKeysMethods; From cf2d8e60ac26d35726312388187a4b9f495c3015 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Tue, 28 Nov 2023 18:10:36 +0100 Subject: [PATCH 019/276] [TS migration] Migrate 'CanvasSize.js' lib --- src/libs/actions/{CanvasSize.js => CanvasSize.ts} | 6 +++--- src/types/modules/canvas-size.d.ts | 6 ++++++ 2 files changed, 9 insertions(+), 3 deletions(-) rename src/libs/actions/{CanvasSize.js => CanvasSize.ts} (89%) create mode 100644 src/types/modules/canvas-size.d.ts diff --git a/src/libs/actions/CanvasSize.js b/src/libs/actions/CanvasSize.ts similarity index 89% rename from src/libs/actions/CanvasSize.js rename to src/libs/actions/CanvasSize.ts index b313763131b9..9de851aacae3 100644 --- a/src/libs/actions/CanvasSize.js +++ b/src/libs/actions/CanvasSize.ts @@ -16,7 +16,7 @@ function retrieveMaxCanvasArea() { useWorker: false, }) .then(() => ({ - onSuccess: (width, height) => { + onSuccess: (width: number, height: number) => { Onyx.merge(ONYXKEYS.MAX_CANVAS_AREA, width * height); }, })); @@ -27,7 +27,7 @@ function retrieveMaxCanvasArea() { */ function retrieveMaxCanvasHeight() { canvasSize.maxHeight({ - onSuccess: (width, height) => { + onSuccess: (width: number, height: number) => { Onyx.merge(ONYXKEYS.MAX_CANVAS_HEIGHT, height); }, }); @@ -38,7 +38,7 @@ function retrieveMaxCanvasHeight() { */ function retrieveMaxCanvasWidth() { canvasSize.maxWidth({ - onSuccess: (width) => { + onSuccess: (width: number) => { Onyx.merge(ONYXKEYS.MAX_CANVAS_WIDTH, width); }, }); diff --git a/src/types/modules/canvas-size.d.ts b/src/types/modules/canvas-size.d.ts new file mode 100644 index 000000000000..6e1243aa657a --- /dev/null +++ b/src/types/modules/canvas-size.d.ts @@ -0,0 +1,6 @@ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ +declare module 'canvas-size' { + import canvasSize from 'canvas-size'; + + export default canvasSize; +} From 819b77ab8f52e06c259d63ffa2877128237638c2 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Tue, 28 Nov 2023 18:19:21 +0100 Subject: [PATCH 020/276] Add window.d.ts file --- .../exposeGlobalMemoryOnlyKeysMethods/index.ts | 9 ++------- src/types/modules/window.d.ts | 10 ++++++++++ 2 files changed, 12 insertions(+), 7 deletions(-) create mode 100644 src/types/modules/window.d.ts diff --git a/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.ts b/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.ts index 6d72188803d7..4514edacb288 100644 --- a/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.ts +++ b/src/libs/actions/MemoryOnlyKeys/exposeGlobalMemoryOnlyKeysMethods/index.ts @@ -1,16 +1,11 @@ import * as MemoryOnlyKeys from '@userActions/MemoryOnlyKeys/MemoryOnlyKeys'; import type ExposeGlobalMemoryOnlyKeysMethods from './types'; -type WindowWithMemoryOnlyKeys = Window & { - enableMemoryOnlyKeys?: () => void; - disableMemoryOnlyKeys?: () => void; -}; - const exposeGlobalMemoryOnlyKeysMethods: ExposeGlobalMemoryOnlyKeysMethods = () => { - (window as WindowWithMemoryOnlyKeys).enableMemoryOnlyKeys = () => { + window.enableMemoryOnlyKeys = () => { MemoryOnlyKeys.enable(); }; - (window as WindowWithMemoryOnlyKeys).disableMemoryOnlyKeys = () => { + window.disableMemoryOnlyKeys = () => { MemoryOnlyKeys.disable(); }; }; diff --git a/src/types/modules/window.d.ts b/src/types/modules/window.d.ts new file mode 100644 index 000000000000..1910c26768f5 --- /dev/null +++ b/src/types/modules/window.d.ts @@ -0,0 +1,10 @@ +declare global { + // eslint-disable-next-line @typescript-eslint/consistent-type-definitions + interface Window { + enableMemoryOnlyKeys: () => void; + disableMemoryOnlyKeys: () => void; + } +} + +// We used the export {} line to mark this file as an external module +export {}; From 2b59ded20f16607387c43b37bfa85230c6f04e31 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 29 Nov 2023 09:25:51 +0100 Subject: [PATCH 021/276] [TS migration] Migrate 'Card.js' lib --- src/libs/actions/Card.js | 176 ------------------------------------- src/libs/actions/Card.ts | 184 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+), 176 deletions(-) delete mode 100644 src/libs/actions/Card.js create mode 100644 src/libs/actions/Card.ts diff --git a/src/libs/actions/Card.js b/src/libs/actions/Card.js deleted file mode 100644 index 9adcd3803766..000000000000 --- a/src/libs/actions/Card.js +++ /dev/null @@ -1,176 +0,0 @@ -import Onyx from 'react-native-onyx'; -import * as API from '@libs/API'; -import * as Localize from '@libs/Localize'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; - -/** - * @param {Number} cardID - */ -function reportVirtualExpensifyCardFraud(cardID) { - API.write( - 'ReportVirtualExpensifyCardFraud', - { - cardID, - }, - { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD, - value: { - isLoading: true, - }, - }, - ], - successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD, - value: { - isLoading: false, - }, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD, - value: { - isLoading: false, - }, - }, - ], - }, - ); -} - -/** - * Call the API to deactivate the card and request a new one - * @param {String} cardId - id of the card that is going to be replaced - * @param {String} reason - reason for replacement ('damaged' | 'stolen') - */ -function requestReplacementExpensifyCard(cardId, reason) { - API.write( - 'RequestReplacementExpensifyCard', - { - cardId, - reason, - }, - { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM, - value: { - isLoading: true, - errors: null, - }, - }, - ], - successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM, - value: { - isLoading: false, - }, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM, - value: { - isLoading: false, - }, - }, - ], - }, - ); -} - -/** - * Activates the physical Expensify card based on the last four digits of the card number - * - * @param {Number} cardLastFourDigits - * @param {Number} cardID - */ -function activatePhysicalExpensifyCard(cardLastFourDigits, cardID) { - API.write( - 'ActivatePhysicalExpensifyCard', - {cardLastFourDigits, cardID}, - { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.CARD_LIST, - value: { - [cardID]: { - errors: null, - isLoading: true, - }, - }, - }, - ], - successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.CARD_LIST, - value: { - [cardID]: { - isLoading: false, - }, - }, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.CARD_LIST, - value: { - [cardID]: { - isLoading: false, - }, - }, - }, - ], - }, - ); -} - -/** - * Clears errors for a specific cardID - * - * @param {Number} cardID - */ -function clearCardListErrors(cardID) { - Onyx.merge(ONYXKEYS.CARD_LIST, {[cardID]: {errors: null, isLoading: false}}); -} - -/** - * Makes an API call to get virtual card details (pan, cvv, expiration date, address) - * This function purposefully uses `makeRequestWithSideEffects` method. For security reason - * card details cannot be persisted in Onyx and have to be asked for each time a user want's to - * reveal them. - * - * @param {String} cardID - virtual card ID - * - * @returns {Promise} - promise with card details object - */ -function revealVirtualCardDetails(cardID) { - return new Promise((resolve, reject) => { - // eslint-disable-next-line rulesdir/no-api-side-effects-method - API.makeRequestWithSideEffects('RevealExpensifyCardDetails', {cardID}) - .then((response) => { - if (response.jsonCode !== CONST.JSON_CODE.SUCCESS) { - reject(Localize.translateLocal('cardPage.cardDetailsLoadingFailure')); - return; - } - resolve(response); - }) - .catch(() => reject(Localize.translateLocal('cardPage.cardDetailsLoadingFailure'))); - }); -} - -export {requestReplacementExpensifyCard, activatePhysicalExpensifyCard, clearCardListErrors, reportVirtualExpensifyCardFraud, revealVirtualCardDetails}; diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts new file mode 100644 index 000000000000..8dd049db1f30 --- /dev/null +++ b/src/libs/actions/Card.ts @@ -0,0 +1,184 @@ +import Onyx from 'react-native-onyx'; +import * as API from '@libs/API'; +import * as Localize from '@libs/Localize'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import {Response} from '@src/types/onyx'; + +function reportVirtualExpensifyCardFraud(cardID: number) { + type ReportVirtualExpensifyCardFraudParams = { + cardID: number; + }; + + const reportVirtualExpensifyCardFraudParams: ReportVirtualExpensifyCardFraudParams = { + cardID, + }; + + API.write('ReportVirtualExpensifyCardFraud', reportVirtualExpensifyCardFraudParams, { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD, + value: { + isLoading: true, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD, + value: { + isLoading: false, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD, + value: { + isLoading: false, + }, + }, + ], + }); +} + +/** + * Call the API to deactivate the card and request a new one + * @param cardId - id of the card that is going to be replaced + * @param reason - reason for replacement ('damaged' | 'stolen') + */ +function requestReplacementExpensifyCard(cardId: number, reason: string) { + type RequestReplacementExpensifyCardParams = { + cardId: number; + reason: string; + }; + + const requestReplacementExpensifyCardParams: RequestReplacementExpensifyCardParams = { + cardId, + reason, + }; + + API.write('RequestReplacementExpensifyCard', requestReplacementExpensifyCardParams, { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM, + value: { + isLoading: true, + errors: null, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM, + value: { + isLoading: false, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM, + value: { + isLoading: false, + }, + }, + ], + }); +} + +/** + * Activates the physical Expensify card based on the last four digits of the card number + */ +function activatePhysicalExpensifyCard(cardLastFourDigits: number, cardID: number) { + type ActivatePhysicalExpensifyCardParams = { + cardLastFourDigits: number; + cardID: number; + }; + + const activatePhysicalExpensifyCardParams: ActivatePhysicalExpensifyCardParams = { + cardLastFourDigits, + cardID, + }; + + API.write('ActivatePhysicalExpensifyCard', activatePhysicalExpensifyCardParams, { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.CARD_LIST, + value: { + [cardID]: { + errors: null, + isLoading: true, + }, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.CARD_LIST, + value: { + [cardID]: { + isLoading: false, + }, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.CARD_LIST, + value: { + [cardID]: { + isLoading: false, + }, + }, + }, + ], + }); +} + +/** + * Clears errors for a specific cardID + */ +function clearCardListErrors(cardID: number) { + Onyx.merge(ONYXKEYS.CARD_LIST, {[cardID]: {errors: null, isLoading: false}}); +} + +/** + * Makes an API call to get virtual card details (pan, cvv, expiration date, address) + * This function purposefully uses `makeRequestWithSideEffects` method. For security reason + * card details cannot be persisted in Onyx and have to be asked for each time a user want's to + * reveal them. + * + * @param cardID - virtual card ID + * + * @returns promise with card details object + */ +function revealVirtualCardDetails(cardID: number): Promise { + return new Promise((resolve, reject) => { + type RevealExpensifyCardDetailsParams = {cardID: number}; + + const revealExpensifyCardDetailsParams: RevealExpensifyCardDetailsParams = {cardID}; + + // eslint-disable-next-line rulesdir/no-api-side-effects-method + API.makeRequestWithSideEffects('RevealExpensifyCardDetails', revealExpensifyCardDetailsParams) + .then((response) => { + if (response?.jsonCode !== CONST.JSON_CODE.SUCCESS) { + reject(Localize.translateLocal('cardPage.cardDetailsLoadingFailure')); + return; + } + resolve(response); + }) + .catch(() => reject(Localize.translateLocal('cardPage.cardDetailsLoadingFailure'))); + }); +} + +export {requestReplacementExpensifyCard, activatePhysicalExpensifyCard, clearCardListErrors, reportVirtualExpensifyCardFraud, revealVirtualCardDetails}; From 3e15c67d4feb074f8bd6f949c75cda9bae8dc6e9 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 29 Nov 2023 09:31:46 +0100 Subject: [PATCH 022/276] TS update after main merging --- src/libs/actions/Card.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts index 8dd049db1f30..82137cc7c4cc 100644 --- a/src/libs/actions/Card.ts +++ b/src/libs/actions/Card.ts @@ -96,9 +96,9 @@ function requestReplacementExpensifyCard(cardId: number, reason: string) { /** * Activates the physical Expensify card based on the last four digits of the card number */ -function activatePhysicalExpensifyCard(cardLastFourDigits: number, cardID: number) { +function activatePhysicalExpensifyCard(cardLastFourDigits: string, cardID: number) { type ActivatePhysicalExpensifyCardParams = { - cardLastFourDigits: number; + cardLastFourDigits: string; cardID: number; }; From ad02a8c6f431f5a75fcebe3e1b27a13e97000f13 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 29 Nov 2023 09:55:10 +0100 Subject: [PATCH 023/276] [TS migration] Migrate 'OnyxUpdateManager.ts' lib --- src/libs/actions/App.ts | 4 ++-- ...xUpdateManager.js => OnyxUpdateManager.ts} | 24 +++++++++---------- src/libs/actions/OnyxUpdates.ts | 1 + 3 files changed, 15 insertions(+), 14 deletions(-) rename src/libs/actions/{OnyxUpdateManager.js => OnyxUpdateManager.ts} (85%) diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts index 4de8f1c1f171..ff4e798ba92a 100644 --- a/src/libs/actions/App.ts +++ b/src/libs/actions/App.ts @@ -293,12 +293,12 @@ function finalReconnectAppAfterActivatingReliableUpdates(): Promise { +function getMissingOnyxUpdates(updateIDFrom = 0, updateIDTo: number | string = 0): Promise { console.debug(`[OnyxUpdates] Fetching missing updates updateIDFrom: ${updateIDFrom} and updateIDTo: ${updateIDTo}`); type GetMissingOnyxMessagesParams = { updateIDFrom: number; - updateIDTo: number; + updateIDTo: number | string; }; const parameters: GetMissingOnyxMessagesParams = { diff --git a/src/libs/actions/OnyxUpdateManager.js b/src/libs/actions/OnyxUpdateManager.ts similarity index 85% rename from src/libs/actions/OnyxUpdateManager.js rename to src/libs/actions/OnyxUpdateManager.ts index 21cea452295b..b61c8eeae268 100644 --- a/src/libs/actions/OnyxUpdateManager.js +++ b/src/libs/actions/OnyxUpdateManager.ts @@ -1,5 +1,4 @@ import Onyx from 'react-native-onyx'; -import _ from 'underscore'; import Log from '@libs/Log'; import * as SequentialQueue from '@libs/Network/SequentialQueue'; import CONST from '@src/CONST'; @@ -22,27 +21,28 @@ import * as OnyxUpdates from './OnyxUpdates'; // The circular dependency happens because this file calls API.GetMissingOnyxUpdates() which uses the SaveResponseInOnyx.js file // (as a middleware). Therefore, SaveResponseInOnyx.js can't import and use this file directly. -let lastUpdateIDAppliedToClient = 0; +let lastUpdateIDAppliedToClient: number | null = 0; Onyx.connect({ key: ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, - callback: (val) => (lastUpdateIDAppliedToClient = val), + callback: (value) => (lastUpdateIDAppliedToClient = value), }); export default () => { console.debug('[OnyxUpdateManager] Listening for updates from the server'); Onyx.connect({ key: ONYXKEYS.ONYX_UPDATES_FROM_SERVER, - callback: (val) => { - if (!val) { + callback: (value) => { + if (!value) { return; } // Since we used the same key that used to store another object, let's confirm that the current object is // following the new format before we proceed. If it isn't, then let's clear the object in Onyx. if ( - !_.isObject(val) || - !_.has(val, 'type') || - (!(val.type === CONST.ONYX_UPDATE_TYPES.HTTPS && _.has(val, 'request') && _.has(val, 'response')) && !(val.type === CONST.ONYX_UPDATE_TYPES.PUSHER && _.has(val, 'updates'))) + value === null || + !Object.hasOwn(value, 'type') || + (!(value.type === CONST.ONYX_UPDATE_TYPES.HTTPS && Object.hasOwn(value, 'request') && Object.hasOwn(value, 'response')) && + !(value.type === CONST.ONYX_UPDATE_TYPES.PUSHER && Object.hasOwn(value, 'updates'))) ) { console.debug('[OnyxUpdateManager] Invalid format found for updates, cleaning and unpausing the queue'); Onyx.set(ONYXKEYS.ONYX_UPDATES_FROM_SERVER, null); @@ -50,9 +50,9 @@ export default () => { return; } - const updateParams = val; - const lastUpdateIDFromServer = val.lastUpdateID; - const previousUpdateIDFromServer = val.previousUpdateID; + const updateParams = value; + const lastUpdateIDFromServer = value.lastUpdateID; + const previousUpdateIDFromServer = value.previousUpdateID; // In cases where we received a previousUpdateID and it doesn't match our lastUpdateIDAppliedToClient // we need to perform one of the 2 possible cases: @@ -76,7 +76,7 @@ export default () => { canUnpauseQueuePromise = App.finalReconnectAppAfterActivatingReliableUpdates(); } else { // The flow below is setting the promise to a getMissingOnyxUpdates to address flow (2) explained above. - console.debug(`[OnyxUpdateManager] Client is behind the server by ${previousUpdateIDFromServer - lastUpdateIDAppliedToClient} so fetching incremental updates`); + console.debug(`[OnyxUpdateManager] Client is behind the server by ${Number(previousUpdateIDFromServer) - lastUpdateIDAppliedToClient} so fetching incremental updates`); Log.info('Gap detected in update IDs from server so fetching incremental updates', true, { lastUpdateIDFromServer, previousUpdateIDFromServer, diff --git a/src/libs/actions/OnyxUpdates.ts b/src/libs/actions/OnyxUpdates.ts index ce673fa6aaaf..af3a16cd3b54 100644 --- a/src/libs/actions/OnyxUpdates.ts +++ b/src/libs/actions/OnyxUpdates.ts @@ -68,6 +68,7 @@ function applyPusherOnyxUpdates(updates: OnyxUpdateEvent[]) { */ function apply({lastUpdateID, type, request, response, updates}: Merge): Promise; function apply({lastUpdateID, type, request, response, updates}: Merge): Promise; +function apply({lastUpdateID, type, request, response, updates}: OnyxUpdatesFromServer): Promise; function apply({lastUpdateID, type, request, response, updates}: OnyxUpdatesFromServer): Promise | undefined { console.debug(`[OnyxUpdateManager] Applying update type: ${type} with lastUpdateID: ${lastUpdateID}`, {request, response, updates}); From 21eac298f59a709433b1110633f796d4dd848528 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 29 Nov 2023 10:06:43 +0100 Subject: [PATCH 024/276] [TS migration] Migrate 'DemoActions.js' lib --- src/libs/Navigation/Navigation.ts | 4 ++-- .../{DemoActions.js => DemoActions.ts} | 24 ++++++++++++------- src/types/onyx/Response.ts | 1 + 3 files changed, 18 insertions(+), 11 deletions(-) rename src/libs/actions/{DemoActions.js => DemoActions.ts} (80%) diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index c2dd3e76e7ad..e90c092327fd 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -69,7 +69,7 @@ function getActiveRouteIndex(stateOrRoute: StateOrRoute, index?: number): number * @param path - Path that you are looking for. * @return - Returns distance to path or -1 if the path is not found in root navigator. */ -function getDistanceFromPathInRootNavigator(path: string): number { +function getDistanceFromPathInRootNavigator(path?: string): number { let currentState = navigationRef.getRootState(); for (let index = 0; index < 5; index++) { @@ -138,7 +138,7 @@ function navigate(route: Route = ROUTES.HOME, type?: string) { * @param shouldEnforceFallback - Enforces navigation to fallback route * @param shouldPopToTop - Should we navigate to LHN on back press */ -function goBack(fallbackRoute: Route, shouldEnforceFallback = false, shouldPopToTop = false) { +function goBack(fallbackRoute?: Route, shouldEnforceFallback = false, shouldPopToTop = false) { if (!canNavigate('goBack')) { return; } diff --git a/src/libs/actions/DemoActions.js b/src/libs/actions/DemoActions.ts similarity index 80% rename from src/libs/actions/DemoActions.js rename to src/libs/actions/DemoActions.ts index 245e475e7ca9..41f5a54977cb 100644 --- a/src/libs/actions/DemoActions.js +++ b/src/libs/actions/DemoActions.ts @@ -1,4 +1,3 @@ -import lodashGet from 'lodash/get'; import Config from 'react-native-config'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; @@ -7,17 +6,17 @@ import * as ReportUtils from '@libs/ReportUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -let currentUserEmail; +let currentUserEmail: string; Onyx.connect({ key: ONYXKEYS.SESSION, callback: (val) => { - currentUserEmail = lodashGet(val, 'email', ''); + currentUserEmail = val?.email ?? ''; }, }); function runMoney2020Demo() { // Try to navigate to existing demo chat if it exists in Onyx - const money2020AccountID = Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_MONEY2020', 15864555)); + const money2020AccountID = Number(Config.EXPENSIFY_ACCOUNT_ID_MONEY2020 ?? 15864555); const existingChatReport = ReportUtils.getChatByParticipants([money2020AccountID]); if (existingChatReport) { // We must call goBack() to remove the demo route from nav history @@ -26,12 +25,19 @@ function runMoney2020Demo() { return; } - // We use makeRequestWithSideEffects here because we need to get the chat report ID to navigate to it after it's created - // eslint-disable-next-line rulesdir/no-api-side-effects-method - API.makeRequestWithSideEffects('CreateChatReport', { + type CreateChatReportParams = { + emailList: string; + activationConference: string; + }; + + const createChatReportParams: CreateChatReportParams = { emailList: `${currentUserEmail},money2020@expensify.com`, activationConference: 'money2020', - }).then((response) => { + }; + + // We use makeRequestWithSideEffects here because we need to get the chat report ID to navigate to it after it's created + // eslint-disable-next-line rulesdir/no-api-side-effects-method + API.makeRequestWithSideEffects('CreateChatReport', createChatReportParams).then((response) => { // If there's no response or no reportID in the response, navigate the user home so user doesn't get stuck. if (!response || !response.reportID) { Navigation.goBack(); @@ -50,7 +56,7 @@ function runMoney2020Demo() { /** * Runs code for specific demos, based on the provided URL * - * @param {String} url - URL user is navigating to via deep link (or regular link in web) + * @param url - URL user is navigating to via deep link (or regular link in web) */ function runDemoByURL(url = '') { const cleanUrl = (url || '').toLowerCase(); diff --git a/src/types/onyx/Response.ts b/src/types/onyx/Response.ts index 66d5dcbdfd5b..c002c75ec075 100644 --- a/src/types/onyx/Response.ts +++ b/src/types/onyx/Response.ts @@ -11,6 +11,7 @@ type Response = { jsonCode?: number | string; onyxData?: OnyxUpdate[]; requestID?: string; + reportID?: string; shouldPauseQueue?: boolean; authToken?: string; encryptedAuthToken?: string; From 02d646d1c4d503639077a60d4eb97813446d6d82 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 29 Nov 2023 12:12:23 +0100 Subject: [PATCH 025/276] [TS migration] Migrate 'TeacherUnite.js' lib --- src/ONYXKEYS.ts | 2 +- src/libs/PolicyUtils.ts | 2 +- src/libs/ReportUtils.ts | 12 +- src/libs/actions/TeachersUnite.js | 180 ---------------------------- src/libs/actions/TeachersUnite.ts | 189 ++++++++++++++++++++++++++++++ src/types/onyx/OriginalMessage.ts | 4 +- src/types/onyx/PersonalDetails.ts | 4 +- src/types/onyx/Policy.ts | 10 +- src/types/onyx/Report.ts | 3 + src/types/onyx/ReportAction.ts | 2 +- src/types/onyx/index.ts | 3 +- 11 files changed, 213 insertions(+), 198 deletions(-) delete mode 100644 src/libs/actions/TeachersUnite.js create mode 100644 src/libs/actions/TeachersUnite.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 0cb3f67bd990..d9d6fb502e5e 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -366,7 +366,7 @@ type OnyxValues = { [ONYXKEYS.NETWORK]: OnyxTypes.Network; [ONYXKEYS.CUSTOM_STATUS_DRAFT]: OnyxTypes.CustomStatusDraft; [ONYXKEYS.INPUT_FOCUSED]: boolean; - [ONYXKEYS.PERSONAL_DETAILS_LIST]: Record; + [ONYXKEYS.PERSONAL_DETAILS_LIST]: OnyxTypes.PersonalDetailsList; [ONYXKEYS.PRIVATE_PERSONAL_DETAILS]: OnyxTypes.PrivatePersonalDetails; [ONYXKEYS.TASK]: OnyxTypes.Task; [ONYXKEYS.CURRENCY_LIST]: Record; diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 19129959d016..04bf08889870 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -16,7 +16,7 @@ type UnitRate = {rate: number}; function getActivePolicies(policies: OnyxCollection): Policy[] | undefined { return Object.values(policies ?? {}).filter( (policy): policy is Policy => - policy !== null && policy && (policy.isPolicyExpenseChatEnabled || policy.areChatRoomsEnabled) && policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + policy !== null && policy && (policy.isPolicyExpenseChatEnabled || !!policy.areChatRoomsEnabled) && policy.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, ); } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index d93661778b83..a97a24608d66 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -16,8 +16,8 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import {Beta, Login, PersonalDetails, Policy, PolicyTags, Report, ReportAction, Transaction} from '@src/types/onyx'; import {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; -import {ChangeLog, IOUMessage, OriginalMessageActionName} from '@src/types/onyx/OriginalMessage'; -import {Message, ReportActions} from '@src/types/onyx/ReportAction'; +import {ChangeLog, IOUMessage, OriginalMessageActionName, OriginalMessageCreated} from '@src/types/onyx/OriginalMessage'; +import {Message, ReportActionBase, ReportActions} from '@src/types/onyx/ReportAction'; import {Receipt, WaypointCollection} from '@src/types/onyx/Transaction'; import DeepValueOf from '@src/types/utils/DeepValueOf'; import {EmptyObject, isEmptyObject, isNotEmptyObject} from '@src/types/utils/EmptyObject'; @@ -184,9 +184,10 @@ type OptimisticClosedReportAction = Pick< >; type OptimisticCreatedReportAction = Pick< - ReportAction, - 'actionName' | 'actorAccountID' | 'automatic' | 'avatar' | 'created' | 'message' | 'person' | 'reportActionID' | 'shouldShow' | 'pendingAction' ->; + ReportActionBase, + 'actorAccountID' | 'automatic' | 'avatar' | 'created' | 'message' | 'person' | 'reportActionID' | 'shouldShow' | 'pendingAction' +> & + OriginalMessageCreated; type OptimisticChatReport = Pick< Report, @@ -311,7 +312,6 @@ type DisplayNameWithTooltips = Array { - sessionEmail = lodashGet(val, 'email', ''); - sessionAccountID = lodashGet(val, 'accountID', 0); - }, -}); - -let allPersonalDetails; -Onyx.connect({ - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - callback: (val) => (allPersonalDetails = val), -}); - -/** - * @param {String} partnerUserID - * @param {String} firstName - * @param {String} lastName - */ -function referTeachersUniteVolunteer(partnerUserID, firstName, lastName) { - const optimisticPublicRoom = ReportUtils.buildOptimisticChatReport([], CONST.TEACHERS_UNITE.PUBLIC_ROOM_NAME, CONST.REPORT.CHAT_TYPE.POLICY_ROOM, CONST.TEACHERS_UNITE.POLICY_ID); - const optimisticData = [ - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticPublicRoom.reportID}`, - value: { - ...optimisticPublicRoom, - reportID: optimisticPublicRoom.reportID, - policyName: CONST.TEACHERS_UNITE.POLICY_NAME, - }, - }, - ]; - API.write( - 'ReferTeachersUniteVolunteer', - { - publicRoomReportID: optimisticPublicRoom.reportID, - firstName, - lastName, - partnerUserID, - }, - {optimisticData}, - ); - Navigation.dismissModal(CONST.TEACHERS_UNITE.PUBLIC_ROOM_ID); -} - -/** - * Optimistically creates a policyExpenseChat for the school principal and passes data to AddSchoolPrincipal - * @param {String} firstName - * @param {String} partnerUserID - * @param {String} lastName - */ -function addSchoolPrincipal(firstName, partnerUserID, lastName) { - const policyName = CONST.TEACHERS_UNITE.POLICY_NAME; - const policyID = CONST.TEACHERS_UNITE.POLICY_ID; - const loggedInEmail = OptionsListUtils.addSMSDomainIfPhoneNumber(sessionEmail); - const reportCreationData = {}; - - const expenseChatData = ReportUtils.buildOptimisticChatReport([sessionAccountID], '', CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, policyID, sessionAccountID, true, policyName); - const expenseChatReportID = expenseChatData.reportID; - const expenseReportCreatedAction = ReportUtils.buildOptimisticCreatedReportAction(sessionEmail); - const expenseReportActionData = { - [expenseReportCreatedAction.reportActionID]: expenseReportCreatedAction, - }; - - reportCreationData[loggedInEmail] = { - reportID: expenseChatReportID, - reportActionID: expenseReportCreatedAction.reportActionID, - }; - - API.write( - 'AddSchoolPrincipal', - { - firstName, - lastName, - partnerUserID, - reportCreationData: JSON.stringify(reportCreationData), - }, - { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - value: { - id: policyID, - isPolicyExpenseChatEnabled: true, - type: CONST.POLICY.TYPE.CORPORATE, - name: policyName, - role: CONST.POLICY.ROLE.USER, - owner: sessionEmail, - outputCurrency: lodashGet(allPersonalDetails, [sessionAccountID, 'localCurrencyCode'], CONST.CURRENCY.USD), - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - }, - }, - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}`, - value: { - [sessionAccountID]: { - role: CONST.POLICY.ROLE.USER, - errors: {}, - }, - }, - }, - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.REPORT}${expenseChatReportID}`, - value: { - pendingFields: { - addWorkspaceRoom: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - }, - ...expenseChatData, - }, - }, - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseChatReportID}`, - value: expenseReportActionData, - }, - ], - successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - value: {pendingAction: null}, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${expenseChatReportID}`, - value: { - pendingFields: { - addWorkspaceRoom: null, - }, - pendingAction: null, - }, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseChatReportID}`, - value: { - [_.keys(expenseChatData)[0]]: { - pendingAction: null, - }, - }, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}`, - value: null, - }, - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.REPORT}${expenseChatReportID}`, - value: null, - }, - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseChatReportID}`, - value: null, - }, - ], - }, - ); - Navigation.dismissModal(expenseChatReportID); -} - -export default {referTeachersUniteVolunteer, addSchoolPrincipal}; diff --git a/src/libs/actions/TeachersUnite.ts b/src/libs/actions/TeachersUnite.ts new file mode 100644 index 000000000000..4b1438090312 --- /dev/null +++ b/src/libs/actions/TeachersUnite.ts @@ -0,0 +1,189 @@ +import Onyx, {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; +import * as API from '@libs/API'; +import Navigation from '@libs/Navigation/Navigation'; +import * as OptionsListUtils from '@libs/OptionsListUtils'; +import * as ReportUtils from '@libs/ReportUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import {PersonalDetailsList} from '@src/types/onyx'; + +type CreationData = { + reportID: string; + reportActionID: string; +}; + +type ReportCreationData = Record; + +let sessionEmail = ''; +let sessionAccountID = 0; +Onyx.connect({ + key: ONYXKEYS.SESSION, + callback: (value) => { + sessionEmail = value?.email ?? ''; + sessionAccountID = value?.accountID ?? 0; + }, +}); + +let allPersonalDetails: OnyxEntry; +Onyx.connect({ + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + callback: (val) => (allPersonalDetails = val), +}); + +function referTeachersUniteVolunteer(partnerUserID: string, firstName: string, lastName: string) { + const optimisticPublicRoom = ReportUtils.buildOptimisticChatReport([], CONST.TEACHERS_UNITE.PUBLIC_ROOM_NAME, CONST.REPORT.CHAT_TYPE.POLICY_ROOM, CONST.TEACHERS_UNITE.POLICY_ID); + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticPublicRoom.reportID}`, + value: { + ...optimisticPublicRoom, + reportID: optimisticPublicRoom.reportID, + policyName: CONST.TEACHERS_UNITE.POLICY_NAME, + }, + }, + ]; + + type ReferTeachersUniteVolunteerParams = { + publicRoomReportID: string; + firstName: string; + lastName: string; + partnerUserID: string; + }; + + const parameters: ReferTeachersUniteVolunteerParams = { + publicRoomReportID: optimisticPublicRoom.reportID, + firstName, + lastName, + partnerUserID, + }; + + API.write('ReferTeachersUniteVolunteer', parameters, {optimisticData}); + Navigation.dismissModal(CONST.TEACHERS_UNITE.PUBLIC_ROOM_ID); +} + +/** + * Optimistically creates a policyExpenseChat for the school principal and passes data to AddSchoolPrincipal + */ +function addSchoolPrincipal(firstName: string, partnerUserID: string, lastName: string) { + const policyName = CONST.TEACHERS_UNITE.POLICY_NAME; + const policyID = CONST.TEACHERS_UNITE.POLICY_ID; + const loggedInEmail = OptionsListUtils.addSMSDomainIfPhoneNumber(sessionEmail); + const reportCreationData: ReportCreationData = {}; + + const expenseChatData = ReportUtils.buildOptimisticChatReport([sessionAccountID], '', CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, policyID, sessionAccountID, true, policyName); + const expenseChatReportID = expenseChatData.reportID; + const expenseReportCreatedAction = ReportUtils.buildOptimisticCreatedReportAction(sessionEmail); + const expenseReportActionData = { + [expenseReportCreatedAction.reportActionID]: expenseReportCreatedAction, + }; + + reportCreationData[loggedInEmail] = { + reportID: expenseChatReportID, + reportActionID: expenseReportCreatedAction.reportActionID, + }; + + type AddSchoolPrincipalParams = { + firstName: string; + lastName: string; + partnerUserID: string; + reportCreationData: string; + }; + + const parameters: AddSchoolPrincipalParams = { + firstName, + lastName, + partnerUserID, + reportCreationData: JSON.stringify(reportCreationData), + }; + + API.write('AddSchoolPrincipal', parameters, { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + id: policyID, + isPolicyExpenseChatEnabled: true, + type: CONST.POLICY.TYPE.CORPORATE, + name: policyName, + role: CONST.POLICY.ROLE.USER, + owner: sessionEmail, + outputCurrency: allPersonalDetails?.[sessionAccountID]?.localCurrencyCode ?? CONST.CURRENCY.USD, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}`, + value: { + [sessionAccountID]: { + role: CONST.POLICY.ROLE.USER, + errors: {}, + }, + }, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT}${expenseChatReportID}`, + value: { + pendingFields: { + addWorkspaceRoom: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + ...expenseChatData, + }, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseChatReportID}`, + value: expenseReportActionData, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: {pendingAction: null}, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${expenseChatReportID}`, + value: { + pendingFields: { + addWorkspaceRoom: null, + }, + pendingAction: null, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseChatReportID}`, + value: { + [Object.keys(expenseChatData)[0]]: { + pendingAction: null, + }, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.POLICY_MEMBERS}${policyID}`, + value: null, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT}${expenseChatReportID}`, + value: null, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseChatReportID}`, + value: null, + }, + ], + }); + Navigation.dismissModal(expenseChatReportID); +} + +export default {referTeachersUniteVolunteer, addSchoolPrincipal}; diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index 0dc532ebeded..5e0b70831626 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -117,7 +117,7 @@ type OriginalMessageClosed = { type OriginalMessageCreated = { actionName: typeof CONST.REPORT.ACTIONS.TYPE.CREATED; - originalMessage: unknown; + originalMessage?: unknown; }; type OriginalMessageRenamed = { @@ -225,4 +225,4 @@ type OriginalMessage = | OriginalMessageMoved; export default OriginalMessage; -export type {ChronosOOOEvent, Decision, Reaction, ActionName, IOUMessage, Closed, OriginalMessageActionName, ChangeLog}; +export type {ChronosOOOEvent, Decision, Reaction, ActionName, IOUMessage, Closed, OriginalMessageActionName, OriginalMessageCreated, ChangeLog}; diff --git a/src/types/onyx/PersonalDetails.ts b/src/types/onyx/PersonalDetails.ts index af559eafd0a1..8f824272230e 100644 --- a/src/types/onyx/PersonalDetails.ts +++ b/src/types/onyx/PersonalDetails.ts @@ -76,6 +76,8 @@ type PersonalDetails = { payPalMeAddress?: string; }; +type PersonalDetailsList = Record; + export default PersonalDetails; -export type {Timezone, SelectedTimezone}; +export type {Timezone, SelectedTimezone, PersonalDetailsList}; diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index e6e3240d1b23..5bef0cf932b1 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -19,7 +19,7 @@ type Policy = { owner: string; /** The accountID of the policy owner */ - ownerAccountID: number; + ownerAccountID?: number; /** The output currency for the policy */ outputCurrency: string; @@ -34,7 +34,7 @@ type Policy = { pendingAction?: OnyxCommon.PendingAction; /** A list of errors keyed by microtime */ - errors: OnyxCommon.Errors; + errors?: OnyxCommon.Errors; /** Whether this policy was loaded from a policy summary, or loaded completely with all of its values */ isFromFullPolicy?: boolean; @@ -46,16 +46,16 @@ type Policy = { customUnits?: Record; /** Whether chat rooms can be created and used on this policy. Enabled manually by CQ/JS snippet. Always true for free policies. */ - areChatRoomsEnabled: boolean; + areChatRoomsEnabled?: boolean; /** Whether policy expense chats can be created and used on this policy. Enabled manually by CQ/JS snippet. Always true for free policies. */ isPolicyExpenseChatEnabled: boolean; /** Whether the scheduled submit is enabled */ - autoReporting: boolean; + autoReporting?: boolean; /** The scheduled submit frequency set up on the this policy */ - autoReportingFrequency: ValueOf; + autoReportingFrequency?: ValueOf; /** The employee list of the policy */ employeeList?: []; diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 81a92c4bf603..0f0ccdd0826e 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -130,6 +130,9 @@ type Report = { /** Pending fields for the report */ pendingFields?: Record; + /** Pending action for the report */ + pendingAction?: OnyxCommon.PendingAction | null; + /** The ID of the preexisting report (it is possible that we optimistically created a Report for which a report already exists) */ preexistingReportID?: string; diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index 891a0ffcb7b8..895ce793ad53 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -145,4 +145,4 @@ type ReportAction = ReportActionBase & OriginalMessage; type ReportActions = Record; export default ReportAction; -export type {ReportActions, Message}; +export type {ReportActions, Message, ReportActionBase, OriginalMessage}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index e7b9c7661c79..f4acef24cd18 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -18,7 +18,7 @@ import Modal from './Modal'; import Network from './Network'; import {OnyxUpdateEvent, OnyxUpdatesFromServer} from './OnyxUpdatesFromServer'; import PersonalBankAccount from './PersonalBankAccount'; -import PersonalDetails from './PersonalDetails'; +import PersonalDetails, {PersonalDetailsList} from './PersonalDetails'; import PlaidData from './PlaidData'; import Policy from './Policy'; import PolicyCategory from './PolicyCategory'; @@ -77,6 +77,7 @@ export type { OnyxUpdatesFromServer, PersonalBankAccount, PersonalDetails, + PersonalDetailsList, PlaidData, Policy, PolicyCategory, From 2857187b7c5831e2ddd436c4a4d4ab19a832fd2a Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 29 Nov 2023 12:42:17 +0100 Subject: [PATCH 026/276] Code improvements --- src/libs/ReportUtils.ts | 2 +- src/libs/actions/Card.ts | 18 +++++++++--------- src/libs/actions/DemoActions.ts | 4 ++-- src/libs/actions/TeachersUnite.ts | 9 ++++++--- src/libs/actions/TransactionEdit.ts | 2 +- 5 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index a97a24608d66..ae4c4217e6aa 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4383,4 +4383,4 @@ export { canEditWriteCapability, }; -export type {OptionData}; +export type {OptionData, OptimisticCreatedReportAction}; diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts index 82137cc7c4cc..8a2923d9c6fd 100644 --- a/src/libs/actions/Card.ts +++ b/src/libs/actions/Card.ts @@ -3,18 +3,18 @@ import * as API from '@libs/API'; import * as Localize from '@libs/Localize'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import {Response} from '@src/types/onyx'; +import type {Response} from '@src/types/onyx'; function reportVirtualExpensifyCardFraud(cardID: number) { type ReportVirtualExpensifyCardFraudParams = { cardID: number; }; - const reportVirtualExpensifyCardFraudParams: ReportVirtualExpensifyCardFraudParams = { + const parameters: ReportVirtualExpensifyCardFraudParams = { cardID, }; - API.write('ReportVirtualExpensifyCardFraud', reportVirtualExpensifyCardFraudParams, { + API.write('ReportVirtualExpensifyCardFraud', parameters, { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, @@ -56,12 +56,12 @@ function requestReplacementExpensifyCard(cardId: number, reason: string) { reason: string; }; - const requestReplacementExpensifyCardParams: RequestReplacementExpensifyCardParams = { + const parameters: RequestReplacementExpensifyCardParams = { cardId, reason, }; - API.write('RequestReplacementExpensifyCard', requestReplacementExpensifyCardParams, { + API.write('RequestReplacementExpensifyCard', parameters, { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, @@ -102,12 +102,12 @@ function activatePhysicalExpensifyCard(cardLastFourDigits: string, cardID: numbe cardID: number; }; - const activatePhysicalExpensifyCardParams: ActivatePhysicalExpensifyCardParams = { + const parameters: ActivatePhysicalExpensifyCardParams = { cardLastFourDigits, cardID, }; - API.write('ActivatePhysicalExpensifyCard', activatePhysicalExpensifyCardParams, { + API.write('ActivatePhysicalExpensifyCard', parameters, { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, @@ -166,10 +166,10 @@ function revealVirtualCardDetails(cardID: number): Promise { return new Promise((resolve, reject) => { type RevealExpensifyCardDetailsParams = {cardID: number}; - const revealExpensifyCardDetailsParams: RevealExpensifyCardDetailsParams = {cardID}; + const parameters: RevealExpensifyCardDetailsParams = {cardID}; // eslint-disable-next-line rulesdir/no-api-side-effects-method - API.makeRequestWithSideEffects('RevealExpensifyCardDetails', revealExpensifyCardDetailsParams) + API.makeRequestWithSideEffects('RevealExpensifyCardDetails', parameters) .then((response) => { if (response?.jsonCode !== CONST.JSON_CODE.SUCCESS) { reject(Localize.translateLocal('cardPage.cardDetailsLoadingFailure')); diff --git a/src/libs/actions/DemoActions.ts b/src/libs/actions/DemoActions.ts index 41f5a54977cb..79c7c1652b1c 100644 --- a/src/libs/actions/DemoActions.ts +++ b/src/libs/actions/DemoActions.ts @@ -30,14 +30,14 @@ function runMoney2020Demo() { activationConference: string; }; - const createChatReportParams: CreateChatReportParams = { + const parameters: CreateChatReportParams = { emailList: `${currentUserEmail},money2020@expensify.com`, activationConference: 'money2020', }; // We use makeRequestWithSideEffects here because we need to get the chat report ID to navigate to it after it's created // eslint-disable-next-line rulesdir/no-api-side-effects-method - API.makeRequestWithSideEffects('CreateChatReport', createChatReportParams).then((response) => { + API.makeRequestWithSideEffects('CreateChatReport', parameters).then((response) => { // If there's no response or no reportID in the response, navigate the user home so user doesn't get stuck. if (!response || !response.reportID) { Navigation.goBack(); diff --git a/src/libs/actions/TeachersUnite.ts b/src/libs/actions/TeachersUnite.ts index 4b1438090312..f264d81f33d4 100644 --- a/src/libs/actions/TeachersUnite.ts +++ b/src/libs/actions/TeachersUnite.ts @@ -3,9 +3,10 @@ import * as API from '@libs/API'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; +import type {OptimisticCreatedReportAction} from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import {PersonalDetailsList} from '@src/types/onyx'; +import type {PersonalDetailsList} from '@src/types/onyx'; type CreationData = { reportID: string; @@ -14,6 +15,8 @@ type CreationData = { type ReportCreationData = Record; +type ExpenseReportActionData = Record; + let sessionEmail = ''; let sessionAccountID = 0; Onyx.connect({ @@ -27,7 +30,7 @@ Onyx.connect({ let allPersonalDetails: OnyxEntry; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, - callback: (val) => (allPersonalDetails = val), + callback: (value) => (allPersonalDetails = value), }); function referTeachersUniteVolunteer(partnerUserID: string, firstName: string, lastName: string) { @@ -74,7 +77,7 @@ function addSchoolPrincipal(firstName: string, partnerUserID: string, lastName: const expenseChatData = ReportUtils.buildOptimisticChatReport([sessionAccountID], '', CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, policyID, sessionAccountID, true, policyName); const expenseChatReportID = expenseChatData.reportID; const expenseReportCreatedAction = ReportUtils.buildOptimisticCreatedReportAction(sessionEmail); - const expenseReportActionData = { + const expenseReportActionData: ExpenseReportActionData = { [expenseReportCreatedAction.reportActionID]: expenseReportCreatedAction, }; diff --git a/src/libs/actions/TransactionEdit.ts b/src/libs/actions/TransactionEdit.ts index 387dacddbcdc..3831ba8e437d 100644 --- a/src/libs/actions/TransactionEdit.ts +++ b/src/libs/actions/TransactionEdit.ts @@ -1,6 +1,6 @@ import Onyx, {OnyxEntry} from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; -import {Transaction} from '@src/types/onyx'; +import type {Transaction} from '@src/types/onyx'; /** * Makes a backup copy of a transaction object that can be restored when the user cancels editing a transaction. From 00cad5e62b9e96fb894c5b40a8c1da27e023aee6 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 29 Nov 2023 15:09:22 +0100 Subject: [PATCH 027/276] Fix crash --- src/libs/actions/DemoActions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/DemoActions.ts b/src/libs/actions/DemoActions.ts index 79c7c1652b1c..b764d8268482 100644 --- a/src/libs/actions/DemoActions.ts +++ b/src/libs/actions/DemoActions.ts @@ -16,7 +16,7 @@ Onyx.connect({ function runMoney2020Demo() { // Try to navigate to existing demo chat if it exists in Onyx - const money2020AccountID = Number(Config.EXPENSIFY_ACCOUNT_ID_MONEY2020 ?? 15864555); + const money2020AccountID = Number(Config?.EXPENSIFY_ACCOUNT_ID_MONEY2020 ?? 15864555); const existingChatReport = ReportUtils.getChatByParticipants([money2020AccountID]); if (existingChatReport) { // We must call goBack() to remove the demo route from nav history From ea2ba21257e96091e45da326a1cc5780cf281192 Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Wed, 29 Nov 2023 15:48:32 +0100 Subject: [PATCH 028/276] fix --- src/libs/HttpUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/HttpUtils.ts b/src/libs/HttpUtils.ts index dbb0717c571a..3fe92b5b1a89 100644 --- a/src/libs/HttpUtils.ts +++ b/src/libs/HttpUtils.ts @@ -56,7 +56,7 @@ function processHTTPRequest(url: string, method: RequestType = 'get', body: Form const endTime = new Date().valueOf(); const latency = (endTime - startTime) / 2; const skew = serverTime - startTime + latency; - NetworkActions.setTimeSkew(skew); + NetworkActions.setTimeSkew(dateHeaderValue ? skew : 0); } return response; }) From 5737385bbd685b78234b58c14ea1236628c841b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Wed, 29 Nov 2023 16:02:30 +0100 Subject: [PATCH 029/276] useEffects refactoring --- .../MoneyRequestParticipantsPage.js | 22 +++--- .../MoneyRequestParticipantsSelector.js | 72 +++++++++---------- 2 files changed, 44 insertions(+), 50 deletions(-) diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js index 9a114de98ea7..375904b3e44e 100644 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js @@ -1,7 +1,7 @@ import lodashGet from 'lodash/get'; import lodashSize from 'lodash/size'; import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, 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 HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -62,28 +62,28 @@ function MoneyRequestParticipantsPage({iou, selectedTab, route, transaction}) { const isSendRequest = iouType === CONST.IOU.TYPE.SEND; const isScanRequest = MoneyRequestUtils.isScanRequest(selectedTab); const isSplitRequest = iou.id === CONST.IOU.TYPE.SPLIT; - const [headerTitle, setHeaderTitle] = useState(); const waypoints = lodashGet(transaction, 'comment.waypoints', {}); const validatedWaypoints = TransactionUtils.getValidWaypoints(waypoints); const isInvalidWaypoint = lodashSize(validatedWaypoints) < 2; - useEffect(() => { + const headerTitle = useMemo(() => { if (isDistanceRequest) { - setHeaderTitle(translate('common.distance')); - return; + return translate('common.distance'); } if (isSendRequest) { - setHeaderTitle(translate('common.send')); - return; + return translate('common.send'); } if (isScanRequest) { - setHeaderTitle(translate('tabSelector.scan')); - return; + return translate('tabSelector.scan'); + } + + if (iou.splitRequest) { + return translate('iou.split'); } - setHeaderTitle(iou.isSplitRequest ? translate('iou.split') : translate('tabSelector.manual')); - }, [iou.isSplitRequest, isDistanceRequest, translate, isScanRequest, isSendRequest]); + return translate('tabSelector.manual'); + }, [iou, isDistanceRequest, translate, isScanRequest, isSendRequest]); const navigateToConfirmationStep = (moneyRequestType) => { IOU.setMoneyRequestId(moneyRequestType); diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index 61ca7853cc27..9f6b0244fa9a 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -1,6 +1,6 @@ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import React, {useCallback, useMemo, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -97,12 +97,39 @@ function MoneyRequestParticipantsSelector({ }) { const styles = useThemeStyles(); const [searchTerm, setSearchTerm] = useState(''); - const [newChatOptions, setNewChatOptions] = useState({ - recentReports: [], - personalDetails: [], - userToInvite: null, - }); const {isOffline} = useNetwork(); + const newChatOptions = useMemo(() => { + const chatOptions = OptionsListUtils.getFilteredOptions( + reports, + personalDetails, + betas, + searchTerm, + participants, + CONST.EXPENSIFY_EMAILS, + + // If we are using this component in the "Request money" flow then we pass the includeOwnedWorkspaceChats argument so that the current user + // sees the option to request money from their admin on their own Workspace Chat. + iouType === CONST.IOU.TYPE.REQUEST, + + // We don't want to include any P2P options like personal details or reports that are not workspace chats for certain features. + !isDistanceRequest, + false, + {}, + [], + false, + {}, + [], + // We don't want the user to be able to invite individuals when they are in the "Distance request" flow for now. + // This functionality is being built here: https://github.com/Expensify/App/issues/23291 + !isDistanceRequest, + true, + ); + return { + recentReports: chatOptions.recentReports, + personalDetails: chatOptions.personalDetails, + userToInvite: chatOptions.userToInvite, + }; + }, [betas, reports, participants, personalDetails, searchTerm, iouType, isDistanceRequest]); const maxParticipantsReached = participants.length === CONST.REPORT.MAXIMUM_PARTICIPANTS; @@ -224,39 +251,6 @@ function MoneyRequestParticipantsSelector({ ); const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails); - useEffect(() => { - const chatOptions = OptionsListUtils.getFilteredOptions( - reports, - personalDetails, - betas, - searchTerm, - participants, - CONST.EXPENSIFY_EMAILS, - - // If we are using this component in the "Request money" flow then we pass the includeOwnedWorkspaceChats argument so that the current user - // sees the option to request money from their admin on their own Workspace Chat. - iouType === CONST.IOU.TYPE.REQUEST, - - // We don't want to include any P2P options like personal details or reports that are not workspace chats for certain features. - !isDistanceRequest, - false, - {}, - [], - false, - {}, - [], - // We don't want the user to be able to invite individuals when they are in the "Distance request" flow for now. - // This functionality is being built here: https://github.com/Expensify/App/issues/23291 - !isDistanceRequest, - true, - ); - setNewChatOptions({ - recentReports: chatOptions.recentReports, - personalDetails: chatOptions.personalDetails, - userToInvite: chatOptions.userToInvite, - }); - }, [betas, reports, participants, personalDetails, translate, searchTerm, setNewChatOptions, iouType, isDistanceRequest]); - // When search term updates we will fetch any reports const setSearchTermAndSearchInServer = useCallback((text = '') => { if (text.length) { From f205afe637eab9117f07b3c493fe831eba0d32b1 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Thu, 30 Nov 2023 09:49:29 +0100 Subject: [PATCH 030/276] Add @types/canvas-size lib --- package-lock.json | 13 +++++++++++++ package.json | 1 + src/libs/actions/CanvasSize.ts | 6 +++--- src/libs/actions/Card.ts | 6 ++++-- src/types/modules/canvas-size.d.ts | 6 ------ 5 files changed, 21 insertions(+), 11 deletions(-) delete mode 100644 src/types/modules/canvas-size.d.ts diff --git a/package-lock.json b/package-lock.json index 32271f8dc743..4a333726f64f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -159,6 +159,7 @@ "@testing-library/jest-native": "5.4.1", "@testing-library/react-native": "11.5.1", "@trivago/prettier-plugin-sort-imports": "^4.2.0", + "@types/canvas-size": "^1.2.2", "@types/concurrently": "^7.0.0", "@types/jest": "^29.5.2", "@types/jest-when": "^3.5.2", @@ -19082,6 +19083,12 @@ "@types/responselike": "^1.0.0" } }, + "node_modules/@types/canvas-size": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@types/canvas-size/-/canvas-size-1.2.2.tgz", + "integrity": "sha512-yuTXFWC4tHV3lt5ZtbIP9VeeMNbDYm5mPyqaQnaMuSSx2mjsfZGXMNmHTnfdsR5qZdB6dtbaV5IP2PKv79vmKg==", + "dev": true + }, "node_modules/@types/concurrently": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@types/concurrently/-/concurrently-7.0.0.tgz", @@ -66435,6 +66442,12 @@ "@types/responselike": "^1.0.0" } }, + "@types/canvas-size": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@types/canvas-size/-/canvas-size-1.2.2.tgz", + "integrity": "sha512-yuTXFWC4tHV3lt5ZtbIP9VeeMNbDYm5mPyqaQnaMuSSx2mjsfZGXMNmHTnfdsR5qZdB6dtbaV5IP2PKv79vmKg==", + "dev": true + }, "@types/concurrently": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@types/concurrently/-/concurrently-7.0.0.tgz", diff --git a/package.json b/package.json index 7da3658e67b6..15d0f876d45d 100644 --- a/package.json +++ b/package.json @@ -206,6 +206,7 @@ "@testing-library/jest-native": "5.4.1", "@testing-library/react-native": "11.5.1", "@trivago/prettier-plugin-sort-imports": "^4.2.0", + "@types/canvas-size": "^1.2.2", "@types/concurrently": "^7.0.0", "@types/jest": "^29.5.2", "@types/jest-when": "^3.5.2", diff --git a/src/libs/actions/CanvasSize.ts b/src/libs/actions/CanvasSize.ts index 9de851aacae3..8e0a155f25eb 100644 --- a/src/libs/actions/CanvasSize.ts +++ b/src/libs/actions/CanvasSize.ts @@ -11,7 +11,7 @@ function retrieveMaxCanvasArea() { // More information at: https://github.com/jhildenbiddle/canvas-size/issues/13 canvasSize .maxArea({ - max: Browser.isMobile() ? 8192 : null, + max: Browser.isMobile() ? 8192 : undefined, usePromise: true, useWorker: false, }) @@ -27,7 +27,7 @@ function retrieveMaxCanvasArea() { */ function retrieveMaxCanvasHeight() { canvasSize.maxHeight({ - onSuccess: (width: number, height: number) => { + onSuccess: (width, height) => { Onyx.merge(ONYXKEYS.MAX_CANVAS_HEIGHT, height); }, }); @@ -38,7 +38,7 @@ function retrieveMaxCanvasHeight() { */ function retrieveMaxCanvasWidth() { canvasSize.maxWidth({ - onSuccess: (width: number) => { + onSuccess: (width) => { Onyx.merge(ONYXKEYS.MAX_CANVAS_WIDTH, width); }, }); diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts index 8a2923d9c6fd..6e1753fbd591 100644 --- a/src/libs/actions/Card.ts +++ b/src/libs/actions/Card.ts @@ -5,6 +5,8 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Response} from '@src/types/onyx'; +type Reason = 'damaged' | 'stolen'; + function reportVirtualExpensifyCardFraud(cardID: number) { type ReportVirtualExpensifyCardFraudParams = { cardID: number; @@ -48,9 +50,9 @@ function reportVirtualExpensifyCardFraud(cardID: number) { /** * Call the API to deactivate the card and request a new one * @param cardId - id of the card that is going to be replaced - * @param reason - reason for replacement ('damaged' | 'stolen') + * @param reason - reason for replacement */ -function requestReplacementExpensifyCard(cardId: number, reason: string) { +function requestReplacementExpensifyCard(cardId: number, reason: Reason) { type RequestReplacementExpensifyCardParams = { cardId: number; reason: string; diff --git a/src/types/modules/canvas-size.d.ts b/src/types/modules/canvas-size.d.ts deleted file mode 100644 index 6e1243aa657a..000000000000 --- a/src/types/modules/canvas-size.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -/* eslint-disable @typescript-eslint/consistent-type-definitions */ -declare module 'canvas-size' { - import canvasSize from 'canvas-size'; - - export default canvasSize; -} From 27c9dde4f846bcbe58187e53ea500618a748c77b Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Thu, 30 Nov 2023 10:04:28 +0100 Subject: [PATCH 031/276] Update code to use PersonalDetailsList type --- src/components/ArchivedReportFooter.tsx | 4 ++-- src/libs/GroupChatUtils.ts | 4 ++-- src/libs/PolicyUtils.ts | 3 +-- src/libs/ReportUtils.ts | 4 ++-- src/libs/actions/PersonalDetails.ts | 4 ++-- 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/components/ArchivedReportFooter.tsx b/src/components/ArchivedReportFooter.tsx index 3187bf3604e8..712ef6be769e 100644 --- a/src/components/ArchivedReportFooter.tsx +++ b/src/components/ArchivedReportFooter.tsx @@ -8,7 +8,7 @@ import * as ReportUtils from '@libs/ReportUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PersonalDetails, Report, ReportAction} from '@src/types/onyx'; +import type {PersonalDetailsList, Report, ReportAction} from '@src/types/onyx'; import Banner from './Banner'; type ArchivedReportFooterOnyxProps = { @@ -16,7 +16,7 @@ type ArchivedReportFooterOnyxProps = { reportClosedAction: OnyxEntry; /** Personal details of all users */ - personalDetails: OnyxEntry>; + personalDetails: OnyxEntry; }; type ArchivedReportFooterProps = ArchivedReportFooterOnyxProps & { diff --git a/src/libs/GroupChatUtils.ts b/src/libs/GroupChatUtils.ts index db64f6574824..862c50700c0c 100644 --- a/src/libs/GroupChatUtils.ts +++ b/src/libs/GroupChatUtils.ts @@ -1,10 +1,10 @@ import Onyx, {OnyxEntry} from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; -import {PersonalDetails, Report} from '@src/types/onyx'; +import {PersonalDetailsList, Report} from '@src/types/onyx'; import * as OptionsListUtils from './OptionsListUtils'; import * as ReportUtils from './ReportUtils'; -let allPersonalDetails: OnyxEntry> = {}; +let allPersonalDetails: OnyxEntry = {}; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, callback: (val) => (allPersonalDetails = val), diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 04bf08889870..d09fdbc892da 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -2,11 +2,10 @@ import Str from 'expensify-common/lib/str'; import {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import {PersonalDetails, Policy, PolicyMembers, PolicyTag, PolicyTags} from '@src/types/onyx'; +import {PersonalDetailsList, Policy, PolicyMembers, PolicyTag, PolicyTags} from '@src/types/onyx'; import {EmptyObject, isEmptyObject} from '@src/types/utils/EmptyObject'; type MemberEmailsToAccountIDs = Record; -type PersonalDetailsList = Record; type UnitRate = {rate: number}; /** diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index ae4c4217e6aa..fb452ce6f26a 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -14,7 +14,7 @@ import CONST from '@src/CONST'; import {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import {Beta, Login, PersonalDetails, Policy, PolicyTags, Report, ReportAction, Transaction} from '@src/types/onyx'; +import {Beta, Login, PersonalDetails, PersonalDetailsList, Policy, PolicyTags, Report, ReportAction, Transaction} from '@src/types/onyx'; import {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; import {ChangeLog, IOUMessage, OriginalMessageActionName, OriginalMessageCreated} from '@src/types/onyx/OriginalMessage'; import {Message, ReportActionBase, ReportActions} from '@src/types/onyx/ReportAction'; @@ -1396,7 +1396,7 @@ function getDisplayNameForParticipant(accountID?: number, shouldUseShortForm = f } function getDisplayNamesWithTooltips( - personalDetailsList: PersonalDetails[] | Record, + personalDetailsList: PersonalDetails[] | PersonalDetailsList, isMultipleParticipantReport: boolean, shouldFallbackToHidden = true, ): DisplayNameWithTooltips { diff --git a/src/libs/actions/PersonalDetails.ts b/src/libs/actions/PersonalDetails.ts index 29d18d543a11..02b5f70db285 100644 --- a/src/libs/actions/PersonalDetails.ts +++ b/src/libs/actions/PersonalDetails.ts @@ -9,7 +9,7 @@ import * as UserUtils from '@libs/UserUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import {DateOfBirthForm, PersonalDetails, PrivatePersonalDetails} from '@src/types/onyx'; +import {DateOfBirthForm, PersonalDetails, PersonalDetailsList, PrivatePersonalDetails} from '@src/types/onyx'; import {SelectedTimezone, Timezone} from '@src/types/onyx/PersonalDetails'; type FirstAndLastName = { @@ -27,7 +27,7 @@ Onyx.connect({ }, }); -let allPersonalDetails: OnyxEntry> = null; +let allPersonalDetails: OnyxEntry = null; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, callback: (val) => (allPersonalDetails = val), From 2dfbe5da9b46001579cdfca33f8fb4f16274f368 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Thu, 30 Nov 2023 10:28:35 +0100 Subject: [PATCH 032/276] Update invalid format check --- src/libs/actions/DemoActions.ts | 4 ++-- src/libs/actions/OnyxUpdateManager.ts | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/libs/actions/DemoActions.ts b/src/libs/actions/DemoActions.ts index b764d8268482..363b8434a2ce 100644 --- a/src/libs/actions/DemoActions.ts +++ b/src/libs/actions/DemoActions.ts @@ -9,8 +9,8 @@ import ROUTES from '@src/ROUTES'; let currentUserEmail: string; Onyx.connect({ key: ONYXKEYS.SESSION, - callback: (val) => { - currentUserEmail = val?.email ?? ''; + callback: (value) => { + currentUserEmail = value?.email ?? ''; }, }); diff --git a/src/libs/actions/OnyxUpdateManager.ts b/src/libs/actions/OnyxUpdateManager.ts index b61c8eeae268..ab0dea960b27 100644 --- a/src/libs/actions/OnyxUpdateManager.ts +++ b/src/libs/actions/OnyxUpdateManager.ts @@ -39,10 +39,9 @@ export default () => { // Since we used the same key that used to store another object, let's confirm that the current object is // following the new format before we proceed. If it isn't, then let's clear the object in Onyx. if ( - value === null || - !Object.hasOwn(value, 'type') || - (!(value.type === CONST.ONYX_UPDATE_TYPES.HTTPS && Object.hasOwn(value, 'request') && Object.hasOwn(value, 'response')) && - !(value.type === CONST.ONYX_UPDATE_TYPES.PUSHER && Object.hasOwn(value, 'updates'))) + !(typeof value === 'object' && !!value) || + !('type' in value) || + (!(value.type === CONST.ONYX_UPDATE_TYPES.HTTPS && value.request && value.response) && !(value.type === CONST.ONYX_UPDATE_TYPES.PUSHER && value.updates)) ) { console.debug('[OnyxUpdateManager] Invalid format found for updates, cleaning and unpausing the queue'); Onyx.set(ONYXKEYS.ONYX_UPDATES_FROM_SERVER, null); From fec08cfc5464774dfb7c39a775a7eb60f52b3da5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Thu, 30 Nov 2023 10:30:20 +0100 Subject: [PATCH 033/276] logic refactoring --- src/components/SelectionList/BaseListItem.js | 16 ++++- .../SelectionList/BaseSelectionList.js | 12 ++++ .../SelectionList/selectionListPropTypes.js | 3 + src/libs/OptionsListUtils.js | 2 + .../MoneyRequestParticipantsSelector.js | 67 +++++++++++-------- 5 files changed, 69 insertions(+), 31 deletions(-) diff --git a/src/components/SelectionList/BaseListItem.js b/src/components/SelectionList/BaseListItem.js index a37f2fe4ddc0..bef1a63fb3ed 100644 --- a/src/components/SelectionList/BaseListItem.js +++ b/src/components/SelectionList/BaseListItem.js @@ -23,6 +23,7 @@ function BaseListItem({ shouldPreventDefaultFocusOnSelectRow = false, canSelectMultiple = false, onSelectRow, + onRowPress, onDismissError = () => {}, }) { const theme = useTheme(); @@ -39,7 +40,7 @@ function BaseListItem({ errorRowStyles={styles.ph5} > onSelectRow(item)} + onPress={() => onRowPress(item)} disabled={isDisabled} accessibilityLabel={item.text} role={CONST.ACCESSIBILITY_ROLE.BUTTON} @@ -59,7 +60,16 @@ function BaseListItem({ ]} > {canSelectMultiple && ( - + onSelectRow(item)} + disabled={isDisabled} + hoverDimmingValue={1} + hoverStyle={styles.hoveredComponentBG} + dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}} + onMouseDown={shouldPreventDefaultFocusOnSelectRow ? (e) => e.preventDefault() : undefined} + > )} - + )} { + if (!onRowPress) { + selectRow(item, true); + return; + } + onRowPress(item); + }; + const renderItem = ({item, index, section}) => { const normalizedIndex = index + lodashGet(section, 'indexOffset', 0); const isDisabled = section.isDisabled || item.isDisabled; @@ -308,6 +318,7 @@ function BaseSelectionList({ showTooltip={showTooltip} canSelectMultiple={canSelectMultiple} onSelectRow={() => selectRow(item, true)} + onRowPress={handleRowPress} onDismissError={onDismissError} shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow} /> @@ -405,6 +416,7 @@ function BaseSelectionList({ }} label={textInputLabel} accessibilityLabel={textInputLabel} + hint={textInputHint} role={CONST.ACCESSIBILITY_ROLE.TEXT} value={textInputValue} placeholder={textInputPlaceholder} diff --git a/src/components/SelectionList/selectionListPropTypes.js b/src/components/SelectionList/selectionListPropTypes.js index 0c2fe83d025f..8ed8197476d2 100644 --- a/src/components/SelectionList/selectionListPropTypes.js +++ b/src/components/SelectionList/selectionListPropTypes.js @@ -16,6 +16,9 @@ const commonListItemPropTypes = { canSelectMultiple: PropTypes.bool, /** Callback to fire when the item is pressed */ + onRowPress: PropTypes.func, + + /** Callback to fire when the item is selected */ onSelectRow: PropTypes.func.isRequired, /** Callback to fire when an error is dismissed */ diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index c616587c3983..ebc791606844 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -219,6 +219,7 @@ function getParticipantsOption(participant, personalDetails) { ], phoneNumber: lodashGet(detail, 'phoneNumber', ''), selected: participant.selected, + isSelected: participant.selected, searchText: participant.searchText, }; } @@ -574,6 +575,7 @@ function getPolicyExpenseReportOption(report) { option.text = ReportUtils.getPolicyName(expenseReport); option.alternateText = Localize.translateLocal('workspace.common.workspace'); option.selected = report.selected; + option.isSelected = report.selected; return option; } diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index 9f6b0244fa9a..0967b4962723 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -8,6 +8,7 @@ import Button from '@components/Button'; import FormHelpMessage from '@components/FormHelpMessage'; import OptionsSelector from '@components/OptionsSelector'; import refPropTypes from '@components/refPropTypes'; +import SelectionList from '@components/SelectionList'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useNetwork from '@hooks/useNetwork'; import * as Report from '@libs/actions/Report'; @@ -274,6 +275,8 @@ function MoneyRequestParticipantsSelector({ navigateToSplit(); }, [shouldShowSplitBillErrorMessage, navigateToSplit]); + const shouldShowSplitButton = isAllowedToSplit && !shouldShowSplitBillErrorMessage && participants.length > 0; + const footerContent = ( {shouldShowSplitBillErrorMessage && ( @@ -283,44 +286,52 @@ function MoneyRequestParticipantsSelector({ message="iou.error.splitBillMultipleParticipantsErrorMessage" /> )} -