From 6ffc1055ca26d026c7ec3a67ff91756a7a220293 Mon Sep 17 00:00:00 2001 From: Kacper Falat Date: Tue, 3 Oct 2023 11:10:34 +0200 Subject: [PATCH 001/463] Migrated User.js lib to TypeScript. --- src/libs/Navigation/Navigation.js | 2 +- src/libs/actions/{User.js => User.ts} | 148 +++++++++++--------------- 2 files changed, 62 insertions(+), 88 deletions(-) rename src/libs/actions/{User.js => User.ts} (85%) diff --git a/src/libs/Navigation/Navigation.js b/src/libs/Navigation/Navigation.js index de6162685079..5cbd8e9b0af6 100644 --- a/src/libs/Navigation/Navigation.js +++ b/src/libs/Navigation/Navigation.js @@ -77,7 +77,7 @@ const getActiveRouteIndex = function (route, index) { /** * Main navigation method for redirecting to a route. * @param {String} route - * @param {String} type - Type of action to perform. Currently UP is supported. + * @param {String} [type] - Type of action to perform. Currently UP is supported. */ function navigate(route = ROUTES.HOME, type) { if (!canNavigate('navigate', {route})) { diff --git a/src/libs/actions/User.js b/src/libs/actions/User.ts similarity index 85% rename from src/libs/actions/User.js rename to src/libs/actions/User.ts index 1830d1e51f6f..7f03ff7a2231 100644 --- a/src/libs/actions/User.js +++ b/src/libs/actions/User.ts @@ -1,6 +1,4 @@ -import _ from 'underscore'; -import lodashGet from 'lodash/get'; -import Onyx from 'react-native-onyx'; +import Onyx, {OnyxUpdate} from 'react-native-onyx'; import moment from 'moment'; import ONYXKEYS from '../../ONYXKEYS'; import * as API from '../API'; @@ -18,18 +16,21 @@ import * as Session from './Session'; import * as PersonalDetails from './PersonalDetails'; import * as OnyxUpdates from './OnyxUpdates'; import redirectToSignIn from './SignInRedirect'; +import type Login from '../../types/onyx/Login'; +import type OnyxPersonalDetails from '../../types/onyx/PersonalDetails'; +import type {OnyxUpdatesFromServer} from '../../types/onyx'; -let currentUserAccountID = ''; +let currentUserAccountID = -1; let currentEmail = ''; Onyx.connect({ key: ONYXKEYS.SESSION, callback: (val) => { - currentUserAccountID = lodashGet(val, 'accountID', -1); - currentEmail = lodashGet(val, 'email', ''); + currentUserAccountID = val?.accountID ?? -1; + currentEmail = val?.email ?? ''; }, }); -let myPersonalDetails = {}; +let myPersonalDetails: Partial = {}; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, callback: (val) => { @@ -44,9 +45,9 @@ Onyx.connect({ /** * Attempt to close the user's account * - * @param {String} message optional reason for closing account + * @param message optional reason for closing account */ -function closeAccount(message) { +function closeAccount(message: string) { // Note: successData does not need to set isLoading to false because if the CloseAccount // command succeeds, a Pusher response will clear all Onyx data. API.write( @@ -75,20 +76,17 @@ function closeAccount(message) { /** * Resends a validation link to a given login - * - * @param {String} login - * @param {Boolean} isPasswordless - temporary param to trigger passwordless flow in backend */ -function resendValidateCode(login) { +function resendValidateCode(login: string) { Session.resendValidateCode(login); } /** * Requests a new validate code be sent for the passed contact method * - * @param {String} contactMethod - the new contact method that the user is trying to verify + * @param contactMethod - the new contact method that the user is trying to verify */ -function requestContactMethodValidateCode(contactMethod) { +function requestContactMethodValidateCode(contactMethod: string) { const optimisticData = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -149,11 +147,9 @@ function requestContactMethodValidateCode(contactMethod) { } /** - * Sets whether or not the user is subscribed to Expensify news - * - * @param {Boolean} isSubscribed + * Sets whether the user is subscribed to Expensify news */ -function updateNewsletterSubscription(isSubscribed) { +function updateNewsletterSubscription(isSubscribed: boolean) { API.write( 'UpdateNewsletterSubscription', { @@ -181,10 +177,9 @@ function updateNewsletterSubscription(isSubscribed) { /** * Delete a specific contact method * - * @param {String} contactMethod - the contact method being deleted - * @param {Array} loginList + * @param contactMethod - the contact method being deleted */ -function deleteContactMethod(contactMethod, loginList) { +function deleteContactMethod(contactMethod: string, loginList: Record) { const oldLoginData = loginList[contactMethod]; const optimisticData = [ @@ -243,11 +238,8 @@ function deleteContactMethod(contactMethod, loginList) { /** * Clears any possible stored errors for a specific field on a contact method - * - * @param {String} contactMethod - * @param {String} fieldName */ -function clearContactMethodErrors(contactMethod, fieldName) { +function clearContactMethodErrors(contactMethod: string, fieldName: string) { Onyx.merge(ONYXKEYS.LOGIN_LIST, { [contactMethod]: { errorFields: { @@ -263,9 +255,9 @@ function clearContactMethodErrors(contactMethod, fieldName) { /** * Resets the state indicating whether a validation code has been sent to a specific contact method. * - * @param {String} contactMethod - The identifier of the contact method to reset. + * @param contactMethod - The identifier of the contact method to reset. */ -function resetContactMethodValidateCodeSentState(contactMethod) { +function resetContactMethodValidateCodeSentState(contactMethod: string) { Onyx.merge(ONYXKEYS.LOGIN_LIST, { [contactMethod]: { validateCodeSent: false, @@ -275,10 +267,8 @@ function resetContactMethodValidateCodeSentState(contactMethod) { /** * Adds a secondary login to a user's account - * - * @param {String} contactMethod */ -function addNewContactMethodAndNavigate(contactMethod) { +function addNewContactMethodAndNavigate(contactMethod: string) { const optimisticData = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -333,11 +323,8 @@ function addNewContactMethodAndNavigate(contactMethod) { /** * Validates a login given an accountID and validation code - * - * @param {Number} accountID - * @param {String} validateCode */ -function validateLogin(accountID, validateCode) { +function validateLogin(accountID: number, validateCode: string) { Onyx.merge(ONYXKEYS.ACCOUNT, {...CONST.DEFAULT_ACCOUNT_DATA, isLoading: true}); const optimisticData = [ @@ -363,10 +350,9 @@ function validateLogin(accountID, validateCode) { /** * Validates a secondary login / contact method * - * @param {String} contactMethod - The contact method the user is trying to verify - * @param {String} validateCode + * @param contactMethod - The contact method the user is trying to verify */ -function validateSecondaryLogin(contactMethod, validateCode) { +function validateSecondaryLogin(contactMethod: string, validateCode: string) { const optimisticData = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -448,11 +434,9 @@ function validateSecondaryLogin(contactMethod, validateCode) { * Checks the blockedFromConcierge object to see if it has an expiresAt key, * and if so whether the expiresAt date of a user's ban is before right now * - * @param {Object} blockedFromConciergeNVP - * @returns {Boolean} */ -function isBlockedFromConcierge(blockedFromConciergeNVP) { - if (_.isEmpty(blockedFromConciergeNVP)) { +function isBlockedFromConcierge(blockedFromConciergeNVP: {expiresAt: number}) { + if (!blockedFromConciergeNVP || Object.keys(blockedFromConciergeNVP).length === 0) { return false; } @@ -463,18 +447,17 @@ function isBlockedFromConcierge(blockedFromConciergeNVP) { return moment().isBefore(moment(blockedFromConciergeNVP.expiresAt), 'day'); } -function triggerNotifications(onyxUpdates) { - _.each(onyxUpdates, (update) => { +function triggerNotifications(onyxUpdates: any) { + onyxUpdates.forEach((update) => { if (!update.shouldNotify) { return; } const reportID = update.key.replace(ONYXKEYS.COLLECTION.REPORT_ACTIONS, ''); - const reportActions = _.values(update.value); + const reportActions = Object.values(update.value); - // eslint-disable-next-line rulesdir/no-negated-variables - const notifiableActions = _.filter(reportActions, (action) => ReportActionsUtils.isNotifiableReportAction(action)); - _.each(notifiableActions, (action) => Report.showReportActionNotification(reportID, action)); + const notifiableActions = reportActions.filter((action) => ReportActionsUtils.isNotifiableReportAction(action)); + notifiableActions.forEach((action) => Report.showReportActionNotification(reportID, action)); }); } @@ -490,7 +473,7 @@ function subscribeToUserEvents() { // Handles the mega multipleEvents from Pusher which contains an array of single events. // Each single event is passed to PusherUtils in order to trigger the callbacks for that event - PusherUtils.subscribeToPrivateUserChannelEvent(Pusher.TYPE.MULTIPLE_EVENTS, currentUserAccountID, (pushJSON) => { + PusherUtils.subscribeToPrivateUserChannelEvent(Pusher.TYPE.MULTIPLE_EVENTS, currentUserAccountID.toString(), (pushJSON: OnyxUpdatesFromServer) => { // The data for this push event comes in two different formats: // 1. Original format - this is what was sent before the RELIABLE_UPDATES project and will go away once RELIABLE_UPDATES is fully complete // - The data is an array of objects, where each object is an onyx update @@ -498,8 +481,8 @@ function subscribeToUserEvents() { // 1. Reliable updates format - this is what was sent with the RELIABLE_UPDATES project and will be the format from now on // - The data is an object, containing updateIDs from the server and an array of onyx updates (this array is the same format as the original format above) // Example: {lastUpdateID: 1, previousUpdateID: 0, updates: [{onyxMethod: 'whatever', key: 'foo', value: 'bar'}]} - if (_.isArray(pushJSON)) { - _.each(pushJSON, (multipleEvent) => { + if (Array.isArray(pushJSON)) { + pushJSON.forEach((multipleEvent) => { PusherUtils.triggerMultiEventHandler(multipleEvent.eventType, multipleEvent.data); }); return; @@ -512,7 +495,7 @@ function subscribeToUserEvents() { previousUpdateID: Number(pushJSON.previousUpdateID || 0), }; if (!OnyxUpdates.doesClientNeedToBeUpdated(Number(pushJSON.previousUpdateID || 0))) { - OnyxUpdates.apply(updates); + OnyxUpdates.apply(updates as any); return; } @@ -522,7 +505,7 @@ function subscribeToUserEvents() { }); // Handles Onyx updates coming from Pusher through the mega multipleEvents. - PusherUtils.subscribeToMultiEvent(Pusher.TYPE.MULTIPLE_EVENT_TYPE.ONYX_API_UPDATE, (pushJSON) => + PusherUtils.subscribeToMultiEvent(Pusher.TYPE.MULTIPLE_EVENT_TYPE.ONYX_API_UPDATE, (pushJSON: OnyxUpdate[]) => SequentialQueue.getCurrentRequest().then(() => { // If we don't have the currentUserAccountID (user is logged out) we don't want to update Onyx with data from Pusher if (!currentUserAccountID) { @@ -541,9 +524,8 @@ function subscribeToUserEvents() { /** * Sync preferredSkinTone with Onyx and Server - * @param {String} skinTone */ -function updatePreferredSkinTone(skinTone) { +function updatePreferredSkinTone(skinTone: string) { const optimisticData = [ { onyxMethod: Onyx.METHOD.SET, @@ -562,9 +544,8 @@ function updatePreferredSkinTone(skinTone) { /** * Sync frequentlyUsedEmojis with Onyx and Server - * @param {Object[]} frequentlyUsedEmojis */ -function updateFrequentlyUsedEmojis(frequentlyUsedEmojis) { +function updateFrequentlyUsedEmojis(frequentlyUsedEmojis: string[]) { const optimisticData = [ { onyxMethod: Onyx.METHOD.SET, @@ -583,9 +564,8 @@ function updateFrequentlyUsedEmojis(frequentlyUsedEmojis) { /** * Sync user chat priority mode with Onyx and Server - * @param {String} mode */ -function updateChatPriorityMode(mode) { +function updateChatPriorityMode(mode: string) { const optimisticData = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -603,10 +583,7 @@ function updateChatPriorityMode(mode) { Navigation.goBack(ROUTES.SETTINGS_PREFERENCES); } -/** - * @param {Boolean} shouldUseStagingServer - */ -function setShouldUseStagingServer(shouldUseStagingServer) { +function setShouldUseStagingServer(shouldUseStagingServer: boolean) { Onyx.merge(ONYXKEYS.USER, {shouldUseStagingServer}); } @@ -623,19 +600,19 @@ function clearScreenShareRequest() { /** * Open an OldDot tab linking to a screen share request. - * @param {String} accessToken Access token required to join a screen share room, generated by the backend - * @param {String} roomName Name of the screen share room to join + * @param accessToken Access token required to join a screen share room, generated by the backend + * @param roomName Name of the screen share room to join */ -function joinScreenShare(accessToken, roomName) { +function joinScreenShare(accessToken: string, roomName: string) { Link.openOldDotLink(`inbox?action=screenShare&accessToken=${accessToken}&name=${roomName}`); clearScreenShareRequest(); } /** * Downloads the statement PDF for the provided period - * @param {String} period YYYYMM format + * @param period YYYYMM format */ -function generateStatementPDF(period) { +function generateStatementPDF(period: string) { API.read( 'GetStatementPDF', {period}, @@ -673,10 +650,8 @@ function generateStatementPDF(period) { /** * Sets a contact method / secondary login as the user's "Default" contact method. - * - * @param {String} newDefaultContactMethod */ -function setContactMethodAsDefault(newDefaultContactMethod) { +function setContactMethodAsDefault(newDefaultContactMethod: string) { const oldDefaultContactMethod = currentEmail; const optimisticData = [ { @@ -754,14 +729,19 @@ function setContactMethodAsDefault(newDefaultContactMethod) { }, }, ]; - API.write('SetContactMethodAsDefault', {partnerUserID: newDefaultContactMethod}, {optimisticData, successData, failureData}); + API.write( + 'SetContactMethodAsDefault', + {partnerUserID: newDefaultContactMethod}, + { + optimisticData, + successData, + failureData, + }, + ); Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS); } -/** - * @param {String} theme - */ -function updateTheme(theme) { +function updateTheme(theme: string) { const optimisticData = [ { onyxMethod: Onyx.METHOD.SET, @@ -783,12 +763,10 @@ function updateTheme(theme) { /** * Sets a custom status - * - * @param {Object} status - * @param {String} status.text - * @param {String} status.emojiCode */ -function updateCustomStatus(status) { +type CustomStatus = {text: string; emojiCode: string; clearAfter?: string}; + +function updateCustomStatus(status: CustomStatus) { API.write('UpdateStatus', status, { optimisticData: [ { @@ -826,18 +804,14 @@ function clearCustomStatus() { /** * Sets a custom status * - * @param {Object} status - * @param {String} status.text - * @param {String} status.emojiCode - * @param {String} status.clearAfter - ISO 8601 format string, which represents the time when the status should be cleared + * @param status.clearAfter - ISO 8601 format string, which represents the time when the status should be cleared */ -function updateDraftCustomStatus(status) { +function updateDraftCustomStatus(status: CustomStatus) { Onyx.merge(ONYXKEYS.CUSTOM_STATUS_DRAFT, status); } /** * Clear the custom draft status - * */ function clearDraftCustomStatus() { Onyx.merge(ONYXKEYS.CUSTOM_STATUS_DRAFT, {text: '', emojiCode: '', clearAfter: ''}); From 7dbca33e8f5f69a5094e474a8e59365397efab2d Mon Sep 17 00:00:00 2001 From: Kacper Falat Date: Tue, 3 Oct 2023 11:13:34 +0200 Subject: [PATCH 002/463] Uninlined types. --- src/libs/actions/User.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 7f03ff7a2231..2ab91f6d59cd 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -20,6 +20,9 @@ import type Login from '../../types/onyx/Login'; import type OnyxPersonalDetails from '../../types/onyx/PersonalDetails'; import type {OnyxUpdatesFromServer} from '../../types/onyx'; +type CustomStatus = {text: string; emojiCode: string; clearAfter?: string}; +type BlockedFromConciergeNVP = {expiresAt: number}; + let currentUserAccountID = -1; let currentEmail = ''; Onyx.connect({ @@ -435,7 +438,7 @@ function validateSecondaryLogin(contactMethod: string, validateCode: string) { * and if so whether the expiresAt date of a user's ban is before right now * */ -function isBlockedFromConcierge(blockedFromConciergeNVP: {expiresAt: number}) { +function isBlockedFromConcierge(blockedFromConciergeNVP: BlockedFromConciergeNVP) { if (!blockedFromConciergeNVP || Object.keys(blockedFromConciergeNVP).length === 0) { return false; } @@ -764,8 +767,6 @@ function updateTheme(theme: string) { /** * Sets a custom status */ -type CustomStatus = {text: string; emojiCode: string; clearAfter?: string}; - function updateCustomStatus(status: CustomStatus) { API.write('UpdateStatus', status, { optimisticData: [ From 45b40db6bd5e28135feb34f1722501bffb41da00 Mon Sep 17 00:00:00 2001 From: Kacper Falat Date: Thu, 5 Oct 2023 10:59:43 +0200 Subject: [PATCH 003/463] WIP: Work sync --- src/libs/actions/User.ts | 398 +++++++++++++----------- src/types/onyx/OnyxUpdatesFromServer.ts | 6 +- src/types/onyx/PersonalDetails.ts | 3 + 3 files changed, 218 insertions(+), 189 deletions(-) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 2ab91f6d59cd..993559ffc2ec 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -1,4 +1,4 @@ -import Onyx, {OnyxUpdate} from 'react-native-onyx'; +import Onyx, {OnyxCollection, OnyxUpdate} from 'react-native-onyx'; import moment from 'moment'; import ONYXKEYS from '../../ONYXKEYS'; import * as API from '../API'; @@ -18,7 +18,9 @@ import * as OnyxUpdates from './OnyxUpdates'; import redirectToSignIn from './SignInRedirect'; import type Login from '../../types/onyx/Login'; import type OnyxPersonalDetails from '../../types/onyx/PersonalDetails'; -import type {OnyxUpdatesFromServer} from '../../types/onyx'; +import type {FrequentlyUsedEmoji, OnyxUpdatesFromServer} from '../../types/onyx'; +import {OnyxServerUpdate} from '../../types/onyx/OnyxUpdatesFromServer'; +import ReportAction from '../../types/onyx/ReportAction'; type CustomStatus = {text: string; emojiCode: string; clearAfter?: string}; type BlockedFromConciergeNVP = {expiresAt: number}; @@ -27,21 +29,21 @@ let currentUserAccountID = -1; let currentEmail = ''; Onyx.connect({ key: ONYXKEYS.SESSION, - callback: (val) => { - currentUserAccountID = val?.accountID ?? -1; - currentEmail = val?.email ?? ''; + callback: (value) => { + currentUserAccountID = value?.accountID ?? -1; + currentEmail = value?.email ?? ''; }, }); let myPersonalDetails: Partial = {}; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, - callback: (val) => { - if (!val || !currentUserAccountID) { + callback: (value) => { + if (!value || !currentUserAccountID) { return; } - myPersonalDetails = val[currentUserAccountID]; + myPersonalDetails = value[currentUserAccountID]; }, }); @@ -53,32 +55,38 @@ Onyx.connect({ function closeAccount(message: string) { // Note: successData does not need to set isLoading to false because if the CloseAccount // command succeeds, a Pusher response will clear all Onyx data. - API.write( - 'CloseAccount', - {message}, - { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM, - value: {isLoading: true}, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM, - value: {isLoading: false}, - }, - ], + + type CloseAccountParam = {message: string}; + + const parameters: CloseAccountParam = {message}; + + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM, + value: {isLoading: true}, }, - ); + ]; + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM, + value: {isLoading: false}, + }, + ]; + + API.write('CloseAccount', parameters, { + optimisticData, + failureData, + }); // Run cleanup actions to prevent reconnection callbacks from blocking logging in again redirectToSignIn(); } /** * Resends a validation link to a given login + * @param login + * @param isPasswordless - temporary param to trigger passwordless flow in backend */ function resendValidateCode(login: string) { Session.resendValidateCode(login); @@ -90,7 +98,7 @@ function resendValidateCode(login: string) { * @param contactMethod - the new contact method that the user is trying to verify */ function requestContactMethodValidateCode(contactMethod: string) { - const optimisticData = [ + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.LOGIN_LIST, @@ -108,7 +116,7 @@ function requestContactMethodValidateCode(contactMethod: string) { }, }, ]; - const successData = [ + const successData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.LOGIN_LIST, @@ -122,7 +130,7 @@ function requestContactMethodValidateCode(contactMethod: string) { }, }, ]; - const failureData = [ + const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.LOGIN_LIST, @@ -140,52 +148,51 @@ function requestContactMethodValidateCode(contactMethod: string) { }, ]; - API.write( - 'RequestContactMethodValidateCode', - { - email: contactMethod, - }, - {optimisticData, successData, failureData}, - ); + type RequestContactMethodValidateCodeParam = {email: string}; + + const parameters: RequestContactMethodValidateCodeParam = {email: contactMethod}; + + API.write('RequestContactMethodValidateCode', parameters, {optimisticData, successData, failureData}); } /** * Sets whether the user is subscribed to Expensify news */ function updateNewsletterSubscription(isSubscribed: boolean) { - API.write( - 'UpdateNewsletterSubscription', + type UpdateNewsletterSubscriptionParam = {isSubscribed: boolean}; + + const parameters: UpdateNewsletterSubscriptionParam = {isSubscribed}; + + const optimisticData: OnyxUpdate[] = [ { - isSubscribed, + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.USER, + value: {isSubscribedToNewsletter: isSubscribed}, }, + ]; + const failureData: OnyxUpdate[] = [ { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.USER, - value: {isSubscribedToNewsletter: isSubscribed}, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.USER, - value: {isSubscribedToNewsletter: !isSubscribed}, - }, - ], + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.USER, + value: {isSubscribedToNewsletter: !isSubscribed}, }, - ); + ]; + + API.write('UpdateNewsletterSubscription', parameters, { + optimisticData, + failureData, + }); } /** * Delete a specific contact method - * * @param contactMethod - the contact method being deleted + * @param loginList */ function deleteContactMethod(contactMethod: string, loginList: Record) { const oldLoginData = loginList[contactMethod]; - const optimisticData = [ + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.LOGIN_LIST, @@ -202,7 +209,7 @@ function deleteContactMethod(contactMethod: string, loginList: Record { if (!update.shouldNotify) { return; } const reportID = update.key.replace(ONYXKEYS.COLLECTION.REPORT_ACTIONS, ''); - const reportActions = Object.values(update.value); + const reportActions = Object.values((update.value as OnyxCollection) ?? {}); - const notifiableActions = reportActions.filter((action) => ReportActionsUtils.isNotifiableReportAction(action)); - notifiableActions.forEach((action) => Report.showReportActionNotification(reportID, action)); + const actions = reportActions.filter((action) => ReportActionsUtils.isNotifiableReportAction(action)) as ReportAction[]; + actions.forEach((action) => Report.showReportActionNotification(reportID, action)); }); } @@ -498,7 +506,7 @@ function subscribeToUserEvents() { previousUpdateID: Number(pushJSON.previousUpdateID || 0), }; if (!OnyxUpdates.doesClientNeedToBeUpdated(Number(pushJSON.previousUpdateID || 0))) { - OnyxUpdates.apply(updates as any); + OnyxUpdates.apply(updates); return; } @@ -529,60 +537,62 @@ function subscribeToUserEvents() { * Sync preferredSkinTone with Onyx and Server */ function updatePreferredSkinTone(skinTone: string) { - const optimisticData = [ + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.SET, key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, value: skinTone, }, ]; - API.write( - 'UpdatePreferredEmojiSkinTone', - { - value: skinTone, - }, - {optimisticData}, - ); + + type UpdatePreferredEmojiSkinTone = { + value: string; + }; + + const parameters: UpdatePreferredEmojiSkinTone = {value: skinTone}; + + API.write('UpdatePreferredEmojiSkinTone', parameters, {optimisticData}); } /** * Sync frequentlyUsedEmojis with Onyx and Server */ -function updateFrequentlyUsedEmojis(frequentlyUsedEmojis: string[]) { - const optimisticData = [ +function updateFrequentlyUsedEmojis(frequentlyUsedEmojis: FrequentlyUsedEmoji[]) { + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.SET, key: ONYXKEYS.FREQUENTLY_USED_EMOJIS, value: frequentlyUsedEmojis, }, ]; - API.write( - 'UpdateFrequentlyUsedEmojis', - { - value: JSON.stringify(frequentlyUsedEmojis), - }, - {optimisticData}, - ); + type UpdateFrequentlyUsedEmojisParam = {value: string}; + + const parameters: UpdateFrequentlyUsedEmojisParam = {value: JSON.stringify(frequentlyUsedEmojis)}; + + API.write('UpdateFrequentlyUsedEmojis', parameters, {optimisticData}); } /** * Sync user chat priority mode with Onyx and Server */ function updateChatPriorityMode(mode: string) { - const optimisticData = [ + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.NVP_PRIORITY_MODE, value: mode, }, ]; - API.write( - 'UpdateChatPriorityMode', - { - value: mode, - }, - {optimisticData}, - ); + + type UpdateChatPriorityModeParam = { + value: string; + }; + + const parameters: UpdateChatPriorityModeParam = { + value: mode, + }; + + API.write('UpdateChatPriorityMode', parameters, {optimisticData}); Navigation.goBack(ROUTES.SETTINGS_PREFERENCES); } @@ -616,37 +626,40 @@ function joinScreenShare(accessToken: string, roomName: string) { * @param period YYYYMM format */ function generateStatementPDF(period: string) { + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.WALLET_STATEMENT, + value: { + isGenerating: true, + }, + }, + ]; + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.WALLET_STATEMENT, + value: { + isGenerating: false, + }, + }, + ]; + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.WALLET_STATEMENT, + value: { + isGenerating: false, + }, + }, + ]; API.read( 'GetStatementPDF', {period}, { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.WALLET_STATEMENT, - value: { - isGenerating: true, - }, - }, - ], - successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.WALLET_STATEMENT, - value: { - isGenerating: false, - }, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.WALLET_STATEMENT, - value: { - isGenerating: false, - }, - }, - ], + optimisticData, + successData, + failureData, }, ); } @@ -656,7 +669,7 @@ function generateStatementPDF(period: string) { */ function setContactMethodAsDefault(newDefaultContactMethod: string) { const oldDefaultContactMethod = currentEmail; - const optimisticData = [ + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.SESSION, @@ -689,7 +702,7 @@ function setContactMethodAsDefault(newDefaultContactMethod: string) { }, }, ]; - const successData = [ + const successData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.LOGIN_LIST, @@ -702,7 +715,7 @@ function setContactMethodAsDefault(newDefaultContactMethod: string) { }, }, ]; - const failureData = [ + const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.SESSION, @@ -732,20 +745,25 @@ function setContactMethodAsDefault(newDefaultContactMethod: string) { }, }, ]; - API.write( - 'SetContactMethodAsDefault', - {partnerUserID: newDefaultContactMethod}, - { - optimisticData, - successData, - failureData, - }, - ); + + type SetContactMethodAsDefaultParam = { + partnerUserID: string; + }; + + const parameters: SetContactMethodAsDefaultParam = { + partnerUserID: newDefaultContactMethod, + }; + + API.write('SetContactMethodAsDefault', parameters, { + optimisticData, + successData, + failureData, + }); Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS); } function updateTheme(theme: string) { - const optimisticData = [ + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.SET, key: ONYXKEYS.PREFERRED_THEME, @@ -753,13 +771,15 @@ function updateTheme(theme: string) { }, ]; - API.write( - 'UpdateTheme', - { - value: theme, - }, - {optimisticData}, - ); + type UpdateThemeParam = { + value: string; + }; + + const parameters: UpdateThemeParam = { + value: theme, + }; + + API.write('UpdateTheme', parameters, {optimisticData}); Navigation.navigate(ROUTES.SETTINGS_PREFERENCES); } @@ -768,18 +788,19 @@ function updateTheme(theme: string) { * Sets a custom status */ function updateCustomStatus(status: CustomStatus) { - API.write('UpdateStatus', status, { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - value: { - [currentUserAccountID]: { - status, - }, + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + value: { + [currentUserAccountID]: { + status, }, }, - ], + }, + ]; + API.write('UpdateStatus', status, { + optimisticData, }); } @@ -787,24 +808,27 @@ function updateCustomStatus(status: CustomStatus) { * Clears the custom status */ function clearCustomStatus() { - API.write('ClearStatus', undefined, { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - value: { - [currentUserAccountID]: { - status: null, // Clearing the field - }, + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + value: { + [currentUserAccountID]: { + status: null, // Clearing the field }, }, - ], + }, + ]; + API.write('ClearStatus', undefined, { + optimisticData, }); } /** * Sets a custom status * + * @param status.text + * @param status.emojiCode * @param status.clearAfter - ISO 8601 format string, which represents the time when the status should be cleared */ function updateDraftCustomStatus(status: CustomStatus) { diff --git a/src/types/onyx/OnyxUpdatesFromServer.ts b/src/types/onyx/OnyxUpdatesFromServer.ts index 50b1503b90bd..843d3ae86e46 100644 --- a/src/types/onyx/OnyxUpdatesFromServer.ts +++ b/src/types/onyx/OnyxUpdatesFromServer.ts @@ -2,9 +2,11 @@ import {OnyxUpdate} from 'react-native-onyx'; import Request from './Request'; import Response from './Response'; +type OnyxServerUpdate = OnyxUpdate & {shouldNotify?: boolean}; + type OnyxUpdateEvent = { eventType: string; - data: OnyxUpdate[]; + data: OnyxServerUpdate[]; }; type OnyxUpdatesFromServer = { @@ -16,4 +18,4 @@ type OnyxUpdatesFromServer = { updates?: OnyxUpdateEvent[]; }; -export type {OnyxUpdatesFromServer, OnyxUpdateEvent}; +export type {OnyxUpdatesFromServer, OnyxUpdateEvent, OnyxServerUpdate}; diff --git a/src/types/onyx/PersonalDetails.ts b/src/types/onyx/PersonalDetails.ts index 201273beac63..6bb41849b0b6 100644 --- a/src/types/onyx/PersonalDetails.ts +++ b/src/types/onyx/PersonalDetails.ts @@ -37,6 +37,9 @@ type PersonalDetails = { /** Pronouns of the current user from their personal details */ pronouns?: string; + /** User status */ + status: {text: string; emojiCode: string; clearAfter?: string} | null; + /** Local currency for the user */ localCurrencyCode?: string; From a1021fd8719b2ea48b1b9f714b928bbe294bee31 Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 9 Nov 2023 00:57:26 +0700 Subject: [PATCH 004/463] fix for timezone abbreviations --- src/libs/IntlPolyfill/index.native.ts | 3 +- src/libs/IntlPolyfill/index.ts | 6 +- .../IntlPolyfill/polyfillDateTimeFormat.ts | 77 +++++++++++++++++++ 3 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 src/libs/IntlPolyfill/polyfillDateTimeFormat.ts diff --git a/src/libs/IntlPolyfill/index.native.ts b/src/libs/IntlPolyfill/index.native.ts index a044b4c52f0d..138d57621405 100644 --- a/src/libs/IntlPolyfill/index.native.ts +++ b/src/libs/IntlPolyfill/index.native.ts @@ -1,3 +1,4 @@ +import polyfillDateTimeFormat from '@libs/IntlPolyfill/polyfillDateTimeFormat'; import polyfillListFormat from './polyfillListFormat'; import polyfillNumberFormat from './polyfillNumberFormat'; import IntlPolyfill from './types'; @@ -10,8 +11,8 @@ const intlPolyfill: IntlPolyfill = () => { require('@formatjs/intl-getcanonicallocales/polyfill'); require('@formatjs/intl-locale/polyfill'); require('@formatjs/intl-pluralrules/polyfill'); - require('@formatjs/intl-datetimeformat'); polyfillNumberFormat(); + polyfillDateTimeFormat(); polyfillListFormat(); }; diff --git a/src/libs/IntlPolyfill/index.ts b/src/libs/IntlPolyfill/index.ts index bef12ef093e2..866cff7fe1ef 100644 --- a/src/libs/IntlPolyfill/index.ts +++ b/src/libs/IntlPolyfill/index.ts @@ -1,4 +1,5 @@ -import polyfillNumberFormat from './polyfillNumberFormat'; +import polyfillDateTimeFormat from '@libs/IntlPolyfill/polyfillDateTimeFormat'; +import polyfillNumberFormat from '@libs/IntlPolyfill/polyfillNumberFormat'; import IntlPolyfill from './types'; /** @@ -6,8 +7,7 @@ import IntlPolyfill from './types'; * This ensures that the currency data is consistent across platforms and browsers. */ const intlPolyfill: IntlPolyfill = () => { - // Just need to polyfill Intl.NumberFormat for web based platforms polyfillNumberFormat(); - require('@formatjs/intl-datetimeformat'); + polyfillDateTimeFormat(); }; export default intlPolyfill; diff --git a/src/libs/IntlPolyfill/polyfillDateTimeFormat.ts b/src/libs/IntlPolyfill/polyfillDateTimeFormat.ts new file mode 100644 index 000000000000..39ac9e78d794 --- /dev/null +++ b/src/libs/IntlPolyfill/polyfillDateTimeFormat.ts @@ -0,0 +1,77 @@ +import Onyx from 'react-native-onyx'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import {Timezone} from '@src/types/onyx/PersonalDetails'; + +/* eslint-disable @typescript-eslint/naming-convention */ +const tzLinks: Record = { + "Africa/Abidjan": "Africa/Accra", + "CET": "Europe/Paris", + "CST6CDT": "America/Chicago", + "EET": "Europe/Sofia", + "EST": "America/Cancun", + "EST5EDT": "America/New_York", + "Etc/GMT": "UTC", + "Etc/UTC": "UTC", + "Factory": "UTC", + "GMT": "UTC", + "HST": "Pacific/Honolulu", + "MET": "Europe/Paris", + "MST": "America/Phoenix", + "MST7MDT": "America/Denver", + "PST8PDT": "America/Los_Angeles", + "WET": "Europe/Lisbon" +} +/* eslint-enable @typescript-eslint/naming-convention */ + +let currentUserAccountID: number | undefined; +Onyx.connect({ + key: ONYXKEYS.SESSION, + callback: (val) => { + // When signed out, val is undefined + if (!val) { + return; + } + + currentUserAccountID = val.accountID; + }, +}); + +let timezone: Required = CONST.DEFAULT_TIME_ZONE; +Onyx.connect({ + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + callback: (value) => { + if (!currentUserAccountID) { + return; + } + + const personalDetailsTimezone = value?.[currentUserAccountID]?.timezone; + + timezone = { + selected: personalDetailsTimezone?.selected ?? CONST.DEFAULT_TIME_ZONE.selected, + automatic: personalDetailsTimezone?.automatic ?? CONST.DEFAULT_TIME_ZONE.automatic, + }; + }, +}); + +export default function () { + // Because JS Engines do not expose default timezone, the polyfill cannot detect local timezone that a browser is in. + // We must manually do this by getting the local timezone before adding polyfill. + let currentTimezone = timezone.automatic ? Intl.DateTimeFormat().resolvedOptions().timeZone : timezone.selected; + console.log(currentTimezone) + if (currentTimezone in tzLinks) { + currentTimezone = tzLinks[currentTimezone]; + } + + require('@formatjs/intl-datetimeformat/polyfill-force'); + require('@formatjs/intl-datetimeformat/locale-data/en'); + require('@formatjs/intl-datetimeformat/locale-data/es'); + require('@formatjs/intl-datetimeformat/add-all-tz'); + + if ('__setDefaultTimeZone' in Intl.DateTimeFormat) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + // eslint-disable-next-line no-underscore-dangle + Intl.DateTimeFormat.__setDefaultTimeZone(currentTimezone); + } +} From 4653beb09bd1ecc15edf690cdc87b0df367f3593 Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 9 Nov 2023 00:59:44 +0700 Subject: [PATCH 005/463] remove console --- src/libs/IntlPolyfill/polyfillDateTimeFormat.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/IntlPolyfill/polyfillDateTimeFormat.ts b/src/libs/IntlPolyfill/polyfillDateTimeFormat.ts index 39ac9e78d794..a8b5bafb0fa9 100644 --- a/src/libs/IntlPolyfill/polyfillDateTimeFormat.ts +++ b/src/libs/IntlPolyfill/polyfillDateTimeFormat.ts @@ -58,7 +58,6 @@ export default function () { // Because JS Engines do not expose default timezone, the polyfill cannot detect local timezone that a browser is in. // We must manually do this by getting the local timezone before adding polyfill. let currentTimezone = timezone.automatic ? Intl.DateTimeFormat().resolvedOptions().timeZone : timezone.selected; - console.log(currentTimezone) if (currentTimezone in tzLinks) { currentTimezone = tzLinks[currentTimezone]; } From 671cd2dc47a8a08a1ee08f850a6fcf2bdf5e935b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Wed, 8 Nov 2023 18:18:34 -0600 Subject: [PATCH 006/463] Update TEACHERS_UNITE constants --- src/CONST.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index ce9329d909ae..f619dcb9fc7e 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -2717,8 +2717,10 @@ const CONST = { ATTACHMENT: 'common.attachment', }, TEACHERS_UNITE: { - PUBLIC_ROOM_ID: '7470147100835202', - POLICY_ID: 'B795B6319125BDF2', + PROD_PUBLIC_ROOM_ID: '7470147100835202', + PROD_POLICY_ID: 'B795B6319125BDF2', + TEST_PUBLIC_ROOM_ID: '207591744844000', + TEST_POLICY_ID: 'ABD1345ED7293535', POLICY_NAME: 'Expensify.org / Teachers Unite!', PUBLIC_ROOM_NAME: '#teachers-unite', }, From 3f45a72b7a09c0f0f5361c5ed27d5567ae53d268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Wed, 8 Nov 2023 18:18:48 -0600 Subject: [PATCH 007/463] Add policyID and publicRoomReportID parameters to referTeachersUniteVolunteer and addSchoolPrincipal functions --- src/libs/actions/TeachersUnite.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/libs/actions/TeachersUnite.js b/src/libs/actions/TeachersUnite.js index 98b1f82629a4..8bcd0ea1e42f 100644 --- a/src/libs/actions/TeachersUnite.js +++ b/src/libs/actions/TeachersUnite.js @@ -28,9 +28,11 @@ Onyx.connect({ * @param {String} partnerUserID * @param {String} firstName * @param {String} lastName + * @param {String} policyID + * @param {String} publicRoomReportID */ -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); +function referTeachersUniteVolunteer(partnerUserID, firstName, lastName, policyID, publicRoomReportID) { + const optimisticPublicRoom = ReportUtils.buildOptimisticChatReport([], CONST.TEACHERS_UNITE.PUBLIC_ROOM_NAME, CONST.REPORT.CHAT_TYPE.POLICY_ROOM, policyID); const optimisticData = [ { onyxMethod: Onyx.METHOD.SET, @@ -52,7 +54,7 @@ function referTeachersUniteVolunteer(partnerUserID, firstName, lastName) { }, {optimisticData}, ); - Navigation.dismissModal(CONST.TEACHERS_UNITE.PUBLIC_ROOM_ID); + Navigation.dismissModal(publicRoomReportID); } /** @@ -60,10 +62,10 @@ function referTeachersUniteVolunteer(partnerUserID, firstName, lastName) { * @param {String} firstName * @param {String} partnerUserID * @param {String} lastName + * @param {String} policyID */ -function addSchoolPrincipal(firstName, partnerUserID, lastName) { +function addSchoolPrincipal(firstName, partnerUserID, lastName, policyID) { const policyName = CONST.TEACHERS_UNITE.POLICY_NAME; - const policyID = CONST.TEACHERS_UNITE.POLICY_ID; const loggedInEmail = OptionsListUtils.addSMSDomainIfPhoneNumber(sessionEmail); const reportCreationData = {}; From e2665723040d62dd0cecbb090ebaefa450efa821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Wed, 8 Nov 2023 18:19:02 -0600 Subject: [PATCH 008/463] Add useEnvironment hook to IntroSchoolPrincipalPage --- src/pages/TeachersUnite/IntroSchoolPrincipalPage.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/TeachersUnite/IntroSchoolPrincipalPage.js b/src/pages/TeachersUnite/IntroSchoolPrincipalPage.js index 16389d69053d..a2b658238523 100644 --- a/src/pages/TeachersUnite/IntroSchoolPrincipalPage.js +++ b/src/pages/TeachersUnite/IntroSchoolPrincipalPage.js @@ -5,6 +5,7 @@ import React, {useCallback} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; +import useEnvironment from '@hooks/useEnvironment'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -35,6 +36,7 @@ const defaultProps = { function IntroSchoolPrincipalPage(props) { const {translate} = useLocalize(); + const {environment} = useEnvironment(); /** * @param {Object} values @@ -43,7 +45,8 @@ function IntroSchoolPrincipalPage(props) { * @param {String} values.lastName */ const onSubmit = (values) => { - TeachersUnite.addSchoolPrincipal(values.firstName.trim(), values.partnerUserID.trim(), values.lastName.trim()); + const policyID = environment === CONST.ENVIRONMENT.PRODUCTION ? CONST.TEACHERS_UNITE.PROD_POLICY_ID : CONST.TEACHERS_UNITE.TEST_POLICY_ID; + TeachersUnite.addSchoolPrincipal(values.firstName.trim(), values.partnerUserID.trim(), values.lastName.trim(), policyID); }; /** From 0903d8f096a2efb74ae668c8e1d2f12028ea7fe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Wed, 8 Nov 2023 18:19:12 -0600 Subject: [PATCH 009/463] Add useEnvironment hook to KnowATeacherPage --- src/pages/TeachersUnite/KnowATeacherPage.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/pages/TeachersUnite/KnowATeacherPage.js b/src/pages/TeachersUnite/KnowATeacherPage.js index 696a9ef8b704..015ed095b7ac 100644 --- a/src/pages/TeachersUnite/KnowATeacherPage.js +++ b/src/pages/TeachersUnite/KnowATeacherPage.js @@ -11,6 +11,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; +import useEnvironment from '@hooks/useEnvironment'; import useLocalize from '@hooks/useLocalize'; import * as ErrorUtils from '@libs/ErrorUtils'; import * as LoginUtils from '@libs/LoginUtils'; @@ -36,6 +37,7 @@ const defaultProps = { function KnowATeacherPage(props) { const {translate} = useLocalize(); + const {environment} = useEnvironment(); /** * Submit form to pass firstName, partnerUserID and lastName @@ -51,7 +53,10 @@ function KnowATeacherPage(props) { const firstName = values.firstName.trim(); const lastName = values.lastName.trim(); - TeachersUnite.referTeachersUniteVolunteer(contactMethod, firstName, lastName); + + const policyID = environment === CONST.ENVIRONMENT.PRODUCTION ? CONST.TEACHERS_UNITE.PROD_POLICY_ID : CONST.TEACHERS_UNITE.TEST_POLICY_ID; + const publicRoomReportID = environment === CONST.ENVIRONMENT.PRODUCTION ? CONST.TEACHERS_UNITE.PROD_PUBLIC_ROOM_ID : CONST.TEACHERS_UNITE.TEST_PUBLIC_ROOM_ID; + TeachersUnite.referTeachersUniteVolunteer(contactMethod, firstName, lastName, policyID, publicRoomReportID); }; /** From 30f0fccb98f0836ea524aa34d33fc99a21730de4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Wed, 8 Nov 2023 18:26:45 -0600 Subject: [PATCH 010/463] Show "I'm a teacher" button always --- src/pages/TeachersUnite/SaveTheWorldPage.js | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/pages/TeachersUnite/SaveTheWorldPage.js b/src/pages/TeachersUnite/SaveTheWorldPage.js index 76e4c42294c1..940ac0432db3 100644 --- a/src/pages/TeachersUnite/SaveTheWorldPage.js +++ b/src/pages/TeachersUnite/SaveTheWorldPage.js @@ -28,9 +28,8 @@ const defaultProps = { policy: {}, }; -function SaveTheWorldPage(props) { +function SaveTheWorldPage() { const {translate} = useLocalize(); - const isTeacherAlreadyInvited = !_.isUndefined(props.policy) && props.policy.role === CONST.POLICY.ROLE.USER; return ( Navigation.navigate(ROUTES.I_KNOW_A_TEACHER)} /> - {!isTeacherAlreadyInvited && ( - Navigation.navigate(ROUTES.I_AM_A_TEACHER)} - /> - )} + Navigation.navigate(ROUTES.I_AM_A_TEACHER)} + /> ); } @@ -66,8 +63,4 @@ SaveTheWorldPage.propTypes = propTypes; SaveTheWorldPage.defaultProps = defaultProps; SaveTheWorldPage.displayName = 'SaveTheWorldPage'; -export default withOnyx({ - policy: { - key: () => `${ONYXKEYS.COLLECTION.POLICY}${CONST.TEACHERS_UNITE.POLICY_ID}`, - }, -})(SaveTheWorldPage); +export default SaveTheWorldPage; From 880a00a3cbea3a157b4b1522e805871c496e1f78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Wed, 8 Nov 2023 19:13:31 -0600 Subject: [PATCH 011/463] Remove unused imports from SaveTheWorldPage.js --- src/pages/TeachersUnite/SaveTheWorldPage.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/pages/TeachersUnite/SaveTheWorldPage.js b/src/pages/TeachersUnite/SaveTheWorldPage.js index 940ac0432db3..f825b0760408 100644 --- a/src/pages/TeachersUnite/SaveTheWorldPage.js +++ b/src/pages/TeachersUnite/SaveTheWorldPage.js @@ -1,8 +1,6 @@ -import _ from 'lodash'; import PropTypes from 'prop-types'; import React from 'react'; import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; import IllustratedHeaderPageLayout from '@components/IllustratedHeaderPageLayout'; import * as LottieAnimations from '@components/LottieAnimations'; import MenuItem from '@components/MenuItem'; @@ -11,8 +9,6 @@ import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; import styles from '@styles/styles'; import themeColors from '@styles/themes/default'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; From 8c9a58795cde0369d5e7eb8e84169cc7b6f0657e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Wed, 8 Nov 2023 19:13:56 -0600 Subject: [PATCH 012/463] Refactor environment variable to isProduction in IntroSchoolPrincipalPage --- src/pages/TeachersUnite/IntroSchoolPrincipalPage.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/TeachersUnite/IntroSchoolPrincipalPage.js b/src/pages/TeachersUnite/IntroSchoolPrincipalPage.js index a2b658238523..1e02281cff0e 100644 --- a/src/pages/TeachersUnite/IntroSchoolPrincipalPage.js +++ b/src/pages/TeachersUnite/IntroSchoolPrincipalPage.js @@ -36,7 +36,7 @@ const defaultProps = { function IntroSchoolPrincipalPage(props) { const {translate} = useLocalize(); - const {environment} = useEnvironment(); + const {isProduction} = useEnvironment(); /** * @param {Object} values @@ -45,7 +45,7 @@ function IntroSchoolPrincipalPage(props) { * @param {String} values.lastName */ const onSubmit = (values) => { - const policyID = environment === CONST.ENVIRONMENT.PRODUCTION ? CONST.TEACHERS_UNITE.PROD_POLICY_ID : CONST.TEACHERS_UNITE.TEST_POLICY_ID; + const policyID = isProduction ? CONST.TEACHERS_UNITE.PROD_POLICY_ID : CONST.TEACHERS_UNITE.TEST_POLICY_ID; TeachersUnite.addSchoolPrincipal(values.firstName.trim(), values.partnerUserID.trim(), values.lastName.trim(), policyID); }; From 1fb005d3c0243a6fcbffd671bfaf3937e1b9b1bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Wed, 8 Nov 2023 19:14:12 -0600 Subject: [PATCH 013/463] Refactor useEnvironment hook to use isProduction instead of environment --- src/pages/TeachersUnite/KnowATeacherPage.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/TeachersUnite/KnowATeacherPage.js b/src/pages/TeachersUnite/KnowATeacherPage.js index 015ed095b7ac..9bf68a5dbb28 100644 --- a/src/pages/TeachersUnite/KnowATeacherPage.js +++ b/src/pages/TeachersUnite/KnowATeacherPage.js @@ -37,7 +37,7 @@ const defaultProps = { function KnowATeacherPage(props) { const {translate} = useLocalize(); - const {environment} = useEnvironment(); + const {isProduction} = useEnvironment(); /** * Submit form to pass firstName, partnerUserID and lastName @@ -54,8 +54,8 @@ function KnowATeacherPage(props) { const lastName = values.lastName.trim(); - const policyID = environment === CONST.ENVIRONMENT.PRODUCTION ? CONST.TEACHERS_UNITE.PROD_POLICY_ID : CONST.TEACHERS_UNITE.TEST_POLICY_ID; - const publicRoomReportID = environment === CONST.ENVIRONMENT.PRODUCTION ? CONST.TEACHERS_UNITE.PROD_PUBLIC_ROOM_ID : CONST.TEACHERS_UNITE.TEST_PUBLIC_ROOM_ID; + const policyID = isProduction ? CONST.TEACHERS_UNITE.PROD_POLICY_ID : CONST.TEACHERS_UNITE.TEST_POLICY_ID; + const publicRoomReportID = isProduction ? CONST.TEACHERS_UNITE.PROD_PUBLIC_ROOM_ID : CONST.TEACHERS_UNITE.TEST_PUBLIC_ROOM_ID; TeachersUnite.referTeachersUniteVolunteer(contactMethod, firstName, lastName, policyID, publicRoomReportID); }; From 5ff73dcc41f30fcd08bd4939228cd85dec6da1c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Wed, 8 Nov 2023 19:57:40 -0600 Subject: [PATCH 014/463] Fix import statement and remove unnecessary whitespace --- src/pages/TeachersUnite/IntroSchoolPrincipalPage.js | 2 +- src/pages/TeachersUnite/KnowATeacherPage.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/TeachersUnite/IntroSchoolPrincipalPage.js b/src/pages/TeachersUnite/IntroSchoolPrincipalPage.js index 1e02281cff0e..a677d6802391 100644 --- a/src/pages/TeachersUnite/IntroSchoolPrincipalPage.js +++ b/src/pages/TeachersUnite/IntroSchoolPrincipalPage.js @@ -5,13 +5,13 @@ import React, {useCallback} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; -import useEnvironment from '@hooks/useEnvironment'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; +import useEnvironment from '@hooks/useEnvironment'; import useLocalize from '@hooks/useLocalize'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; diff --git a/src/pages/TeachersUnite/KnowATeacherPage.js b/src/pages/TeachersUnite/KnowATeacherPage.js index 9bf68a5dbb28..d4e0f6ea7957 100644 --- a/src/pages/TeachersUnite/KnowATeacherPage.js +++ b/src/pages/TeachersUnite/KnowATeacherPage.js @@ -53,7 +53,6 @@ function KnowATeacherPage(props) { const firstName = values.firstName.trim(); const lastName = values.lastName.trim(); - const policyID = isProduction ? CONST.TEACHERS_UNITE.PROD_POLICY_ID : CONST.TEACHERS_UNITE.TEST_POLICY_ID; const publicRoomReportID = isProduction ? CONST.TEACHERS_UNITE.PROD_PUBLIC_ROOM_ID : CONST.TEACHERS_UNITE.TEST_PUBLIC_ROOM_ID; TeachersUnite.referTeachersUniteVolunteer(contactMethod, firstName, lastName, policyID, publicRoomReportID); From f781c0659694912ca247a86c5715404735c4272f Mon Sep 17 00:00:00 2001 From: Hans Date: Mon, 13 Nov 2023 14:07:22 +0700 Subject: [PATCH 015/463] fix showing notfound page when offline --- .../home/report/withReportAndPrivateNotesOrNotFound.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/pages/home/report/withReportAndPrivateNotesOrNotFound.js b/src/pages/home/report/withReportAndPrivateNotesOrNotFound.js index 3982dd5ab542..7394c5900e13 100644 --- a/src/pages/home/report/withReportAndPrivateNotesOrNotFound.js +++ b/src/pages/home/report/withReportAndPrivateNotesOrNotFound.js @@ -6,6 +6,7 @@ import _ from 'underscore'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import networkPropTypes from '@components/networkPropTypes'; import {withNetwork} from '@components/OnyxProvider'; +import usePrevious from '@hooks/usePrevious'; import * as Report from '@libs/actions/Report'; import compose from '@libs/compose'; import getComponentDisplayName from '@libs/getComponentDisplayName'; @@ -56,6 +57,8 @@ export default function (WrappedComponent) { const {route, report, network, session} = props; const accountID = route.params.accountID; const isPrivateNotesFetchTriggered = !_.isUndefined(report.isLoadingPrivateNotes); + const prevIsOffline = usePrevious(network.isOffline); + const isReconnecting = prevIsOffline && !network.isOffline; useEffect(() => { // Do not fetch private notes if isLoadingPrivateNotes is already defined, or if network is offline. @@ -67,7 +70,7 @@ export default function (WrappedComponent) { // eslint-disable-next-line react-hooks/exhaustive-deps -- do not add report.isLoadingPrivateNotes to dependencies }, [report.reportID, network.isOffline, isPrivateNotesFetchTriggered]); - const isPrivateNotesEmpty = accountID ? _.isEmpty(lodashGet(report, ['privateNotes', accountID, 'note'], '')) : _.isEmpty(report.privateNotes); + const isPrivateNotesEmpty = accountID ? _.has(lodashGet(report, ['privateNotes', accountID, 'note'], '')) : _.isEmpty(report.privateNotes); const shouldShowFullScreenLoadingIndicator = !isPrivateNotesFetchTriggered || (isPrivateNotesEmpty && report.isLoadingPrivateNotes); // eslint-disable-next-line rulesdir/no-negated-variables @@ -78,13 +81,13 @@ export default function (WrappedComponent) { } // Don't show not found view if the notes are still loading, or if the notes are non-empty. - if (shouldShowFullScreenLoadingIndicator || !isPrivateNotesEmpty) { + if (shouldShowFullScreenLoadingIndicator || !isPrivateNotesEmpty || isReconnecting) { return false; } // As notes being empty and not loading is a valid case, show not found view only in offline mode. return network.isOffline; - }, [report, network.isOffline, accountID, session.accountID, isPrivateNotesEmpty, shouldShowFullScreenLoadingIndicator]); + }, [report, network.isOffline, accountID, session.accountID, isPrivateNotesEmpty, shouldShowFullScreenLoadingIndicator, isReconnecting]); if (shouldShowFullScreenLoadingIndicator) { return ; From 5de34fba8a874c66abed081a6e94509d1bb2d3bf Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 14 Nov 2023 17:50:11 +0500 Subject: [PATCH 016/463] 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: Tue, 14 Nov 2023 17:39:00 +0100 Subject: [PATCH 017/463] User.ts remigrated. --- src/libs/Network/SequentialQueue.ts | 2 +- src/libs/actions/User.ts | 52 +++++++++-------------------- 2 files changed, 16 insertions(+), 38 deletions(-) diff --git a/src/libs/Network/SequentialQueue.ts b/src/libs/Network/SequentialQueue.ts index d4aee4a221e5..4ce97f349194 100644 --- a/src/libs/Network/SequentialQueue.ts +++ b/src/libs/Network/SequentialQueue.ts @@ -176,7 +176,7 @@ function push(request: OnyxRequest) { flush(); } -function getCurrentRequest(): OnyxRequest | Promise { +function getCurrentRequest(): Promise { if (currentRequest === null) { return Promise.resolve(); } diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index e58612210485..7c959b390838 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -1,7 +1,6 @@ import {isBefore} from 'date-fns'; -import lodashGet from 'lodash/get'; -import Onyx from 'react-native-onyx'; -import _ from 'underscore'; +import Onyx, {OnyxCollection, OnyxUpdate} from 'react-native-onyx'; +import {ValueOf} from 'type-fest'; import * as API from '@libs/API'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; @@ -12,25 +11,17 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import Onyx, {OnyxCollection, OnyxUpdate} from 'react-native-onyx'; -import moment from 'moment'; -import ONYXKEYS from '../../ONYXKEYS'; -import * as API from '../API'; -import CONST from '../../CONST'; -import Navigation from '../Navigation/Navigation'; -import ROUTES from '../../ROUTES'; -import * as Pusher from '../Pusher/pusher'; +import type {FrequentlyUsedEmoji} from '@src/types/onyx'; +import type Login from '@src/types/onyx/Login'; +import {OnyxServerUpdate} from '@src/types/onyx/OnyxUpdatesFromServer'; +import type OnyxPersonalDetails from '@src/types/onyx/PersonalDetails'; +import ReportAction from '@src/types/onyx/ReportAction'; import * as Link from './Link'; import * as OnyxUpdates from './OnyxUpdates'; import * as PersonalDetails from './PersonalDetails'; import * as Report from './Report'; import * as Session from './Session'; import redirectToSignIn from './SignInRedirect'; -import type Login from '../../types/onyx/Login'; -import type OnyxPersonalDetails from '../../types/onyx/PersonalDetails'; -import type {FrequentlyUsedEmoji, OnyxUpdatesFromServer} from '../../types/onyx'; -import {OnyxServerUpdate} from '../../types/onyx/OnyxUpdatesFromServer'; -import ReportAction from '../../types/onyx/ReportAction'; type CustomStatus = {text: string; emojiCode: string; clearAfter?: string}; type BlockedFromConciergeNVP = {expiresAt: number}; @@ -96,7 +87,6 @@ function closeAccount(message: string) { /** * Resends a validation link to a given login * @param login - * @param isPasswordless - temporary param to trigger passwordless flow in backend */ function resendValidateCode(login: string) { Session.resendValidateCode(login); @@ -114,7 +104,6 @@ function requestContactMethodValidateCode(contactMethod: string) { key: ONYXKEYS.LOGIN_LIST, value: { [contactMethod]: { - validateCodeSent: false, errorFields: { validateCodeSent: null, validateLogin: null, @@ -132,7 +121,6 @@ function requestContactMethodValidateCode(contactMethod: string) { key: ONYXKEYS.LOGIN_LIST, value: { [contactMethod]: { - validateCodeSent: true, pendingFields: { validateCodeSent: null, }, @@ -146,7 +134,6 @@ function requestContactMethodValidateCode(contactMethod: string) { key: ONYXKEYS.LOGIN_LIST, value: { [contactMethod]: { - validateCodeSent: false, errorFields: { validateCodeSent: ErrorUtils.getMicroSecondOnyxError('contacts.genericFailureMessages.requestContactMethodValidateCode'), }, @@ -250,15 +237,8 @@ function deleteContactMethod(contactMethod: string, loginList: Record { + PusherUtils.subscribeToPrivateUserChannelEvent(Pusher.TYPE.MULTIPLE_EVENTS, currentUserAccountID.toString(), (pushJSON) => { // The data for this push event comes in two different formats: // 1. Original format - this is what was sent before the RELIABLE_UPDATES project and will go away once RELIABLE_UPDATES is fully complete // - The data is an array of objects, where each object is an onyx update @@ -520,7 +498,7 @@ function subscribeToUserEvents() { const updates = { type: CONST.ONYX_UPDATE_TYPES.PUSHER, lastUpdateID: Number(pushJSON.lastUpdateID || 0), - updates: pushJSON.updates, + updates: pushJSON.updates ?? [], previousUpdateID: Number(pushJSON.previousUpdateID || 0), }; if (!OnyxUpdates.doesClientNeedToBeUpdated(Number(pushJSON.previousUpdateID || 0))) { @@ -593,7 +571,7 @@ function updateFrequentlyUsedEmojis(frequentlyUsedEmojis: FrequentlyUsedEmoji[]) /** * Sync user chat priority mode with Onyx and Server */ -function updateChatPriorityMode(mode: string) { +function updateChatPriorityMode(mode: ValueOf) { const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -777,10 +755,10 @@ function setContactMethodAsDefault(newDefaultContactMethod: string) { successData, failureData, }); - Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS); + Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.getRoute()); } -function updateTheme(theme: string) { +function updateTheme(theme: ValueOf) { const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.SET, From c2f91cefea0d9ea4c1ac43d0f5b231a3ec88e4e7 Mon Sep 17 00:00:00 2001 From: Kacper Falat Date: Tue, 14 Nov 2023 17:47:49 +0100 Subject: [PATCH 018/463] User.ts remigrated. --- src/libs/actions/User.ts | 11 +++++++++-- src/types/onyx/PersonalDetails.ts | 3 --- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 7c959b390838..21b5ce811e48 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -783,7 +783,7 @@ function updateTheme(theme: ValueOf) { /** * Sets a custom status */ -function updateCustomStatus(status: CustomStatus) { +function updateCustomStatus(status: string) { const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -795,7 +795,14 @@ function updateCustomStatus(status: CustomStatus) { }, }, ]; - API.write('UpdateStatus', status, { + + type UpdateStatusParam = { + status: string; + }; + + const params: UpdateStatusParam = {status}; + + API.write('UpdateStatus', params, { optimisticData, }); } diff --git a/src/types/onyx/PersonalDetails.ts b/src/types/onyx/PersonalDetails.ts index 92204bf7bd28..8fc627158495 100644 --- a/src/types/onyx/PersonalDetails.ts +++ b/src/types/onyx/PersonalDetails.ts @@ -47,9 +47,6 @@ type PersonalDetails = { /** Pronouns of the current user from their personal details */ pronouns?: string; - /** User status */ - status: {text: string; emojiCode: string; clearAfter?: string} | null; - /** Local currency for the user */ localCurrencyCode?: string; From cba3f8cd93bdfbdc0776a1ac18aeecabd83f7a9e Mon Sep 17 00:00:00 2001 From: Kacper Falat Date: Tue, 14 Nov 2023 18:43:05 +0100 Subject: [PATCH 019/463] Small alignments. --- src/libs/actions/User.ts | 24 ++++++++++++++++-------- src/types/onyx/Login.ts | 3 +++ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 21b5ce811e48..a76474541364 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -86,7 +86,6 @@ function closeAccount(message: string) { /** * Resends a validation link to a given login - * @param login */ function resendValidateCode(login: string) { Session.resendValidateCode(login); @@ -104,6 +103,7 @@ function requestContactMethodValidateCode(contactMethod: string) { key: ONYXKEYS.LOGIN_LIST, value: { [contactMethod]: { + validateCodeSent: false, errorFields: { validateCodeSent: null, validateLogin: null, @@ -121,6 +121,7 @@ function requestContactMethodValidateCode(contactMethod: string) { key: ONYXKEYS.LOGIN_LIST, value: { [contactMethod]: { + validateCodeSent: true, pendingFields: { validateCodeSent: null, }, @@ -128,12 +129,14 @@ function requestContactMethodValidateCode(contactMethod: string) { }, }, ]; + const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.LOGIN_LIST, value: { [contactMethod]: { + validateCodeSent: false, errorFields: { validateCodeSent: ErrorUtils.getMicroSecondOnyxError('contacts.genericFailureMessages.requestContactMethodValidateCode'), }, @@ -265,7 +268,9 @@ function clearContactMethodErrors(contactMethod: string, fieldName: string) { */ function resetContactMethodValidateCodeSentState(contactMethod: string) { Onyx.merge(ONYXKEYS.LOGIN_LIST, { - [contactMethod]: {}, + [contactMethod]: { + validateCodeSent: false, + }, }); } @@ -408,6 +413,7 @@ function validateSecondaryLogin(contactMethod: string, validateCode: string) { value: {isLoading: false}, }, ]; + const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -512,7 +518,7 @@ function subscribeToUserEvents() { }); // Handles Onyx updates coming from Pusher through the mega multipleEvents. - PusherUtils.subscribeToMultiEvent(Pusher.TYPE.MULTIPLE_EVENT_TYPE.ONYX_API_UPDATE, (pushJSON: OnyxUpdate[]) => + PusherUtils.subscribeToMultiEvent(Pusher.TYPE.MULTIPLE_EVENT_TYPE.ONYX_API_UPDATE, (pushJSON: OnyxServerUpdate[]) => SequentialQueue.getCurrentRequest().then(() => { // If we don't have the currentUserAccountID (user is logged out) we don't want to update Onyx with data from Pusher if (!currentUserAccountID) { @@ -783,26 +789,28 @@ function updateTheme(theme: ValueOf) { /** * Sets a custom status */ -function updateCustomStatus(status: string) { +function updateCustomStatus(status: CustomStatus) { const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.PERSONAL_DETAILS_LIST, value: { [currentUserAccountID]: { - status, + status: status.text, }, }, }, ]; type UpdateStatusParam = { - status: string; + text: string; + emojiCode: string; + clearAfter?: string; }; - const params: UpdateStatusParam = {status}; + const parameters: UpdateStatusParam = {text: status.text, emojiCode: status.emojiCode, clearAfter: status.clearAfter}; - API.write('UpdateStatus', params, { + API.write('UpdateStatus', parameters, { optimisticData, }); } diff --git a/src/types/onyx/Login.ts b/src/types/onyx/Login.ts index c770e2f81f90..deedb1b71af9 100644 --- a/src/types/onyx/Login.ts +++ b/src/types/onyx/Login.ts @@ -10,6 +10,9 @@ type Login = { /** Date login was validated, used to show info indicator status */ validatedDate?: string; + /** Whether the user validation code was sent */ + validateCodeSent?: boolean; + /** Field-specific server side errors keyed by microtime */ errorFields?: OnyxCommon.ErrorFields; From 85a2c939757f931e6888987f2bceb0d94c1b8b00 Mon Sep 17 00:00:00 2001 From: Kacper Falat Date: Wed, 15 Nov 2023 10:00:08 +0100 Subject: [PATCH 020/463] Changes after review. --- src/libs/actions/User.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index a76474541364..b208c3bb0989 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -40,7 +40,7 @@ let myPersonalDetails: Partial = {}; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, callback: (value) => { - if (!value || !currentUserAccountID) { + if (!value || currentUserAccountID === -1) { return; } @@ -240,7 +240,6 @@ function deleteContactMethod(contactMethod: string, loginList: Record SequentialQueue.getCurrentRequest().then(() => { // If we don't have the currentUserAccountID (user is logged out) we don't want to update Onyx with data from Pusher - if (!currentUserAccountID) { + if (currentUserAccountID === -1) { return; } @@ -761,7 +760,7 @@ function setContactMethodAsDefault(newDefaultContactMethod: string) { successData, failureData, }); - Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.getRoute()); + Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.route); } function updateTheme(theme: ValueOf) { From 60be0181c8bf9b372e2a4551ea43ca4432a8a7d7 Mon Sep 17 00:00:00 2001 From: Kacper Falat Date: Wed, 15 Nov 2023 10:46:35 +0100 Subject: [PATCH 021/463] Changes after review. --- src/libs/actions/User.ts | 52 ++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index b208c3bb0989..238e73a4bf83 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -57,9 +57,9 @@ function closeAccount(message: string) { // Note: successData does not need to set isLoading to false because if the CloseAccount // command succeeds, a Pusher response will clear all Onyx data. - type CloseAccountParam = {message: string}; + type CloseAccountParams = {message: string}; - const parameters: CloseAccountParam = {message}; + const parameters: CloseAccountParams = {message}; const optimisticData: OnyxUpdate[] = [ { @@ -148,9 +148,9 @@ function requestContactMethodValidateCode(contactMethod: string) { }, ]; - type RequestContactMethodValidateCodeParam = {email: string}; + type RequestContactMethodValidateCodeParams = {email: string}; - const parameters: RequestContactMethodValidateCodeParam = {email: contactMethod}; + const parameters: RequestContactMethodValidateCodeParams = {email: contactMethod}; API.write('RequestContactMethodValidateCode', parameters, {optimisticData, successData, failureData}); } @@ -159,9 +159,9 @@ function requestContactMethodValidateCode(contactMethod: string) { * Sets whether the user is subscribed to Expensify news */ function updateNewsletterSubscription(isSubscribed: boolean) { - type UpdateNewsletterSubscriptionParam = {isSubscribed: boolean}; + type UpdateNewsletterSubscriptionParams = {isSubscribed: boolean}; - const parameters: UpdateNewsletterSubscriptionParam = {isSubscribed}; + const parameters: UpdateNewsletterSubscriptionParams = {isSubscribed}; const optimisticData: OnyxUpdate[] = [ { @@ -236,9 +236,9 @@ function deleteContactMethod(contactMethod: string, loginList: Record) { }, ]; - type UpdateChatPriorityModeParam = { + type UpdateChatPriorityModeParams = { value: string; }; - const parameters: UpdateChatPriorityModeParam = { + const parameters: UpdateChatPriorityModeParams = { value: mode, }; @@ -747,11 +747,11 @@ function setContactMethodAsDefault(newDefaultContactMethod: string) { }, ]; - type SetContactMethodAsDefaultParam = { + type SetContactMethodAsDefaultParams = { partnerUserID: string; }; - const parameters: SetContactMethodAsDefaultParam = { + const parameters: SetContactMethodAsDefaultParams = { partnerUserID: newDefaultContactMethod, }; @@ -772,11 +772,11 @@ function updateTheme(theme: ValueOf) { }, ]; - type UpdateThemeParam = { + type UpdateThemeParams = { value: string; }; - const parameters: UpdateThemeParam = { + const parameters: UpdateThemeParams = { value: theme, }; @@ -801,13 +801,13 @@ function updateCustomStatus(status: CustomStatus) { }, ]; - type UpdateStatusParam = { + type UpdateStatusParams = { text: string; emojiCode: string; clearAfter?: string; }; - const parameters: UpdateStatusParam = {text: status.text, emojiCode: status.emojiCode, clearAfter: status.clearAfter}; + const parameters: UpdateStatusParams = {text: status.text, emojiCode: status.emojiCode, clearAfter: status.clearAfter}; API.write('UpdateStatus', parameters, { optimisticData, From ee33de2fa620b55ac06ba52ba69ae2689b0cf0c3 Mon Sep 17 00:00:00 2001 From: Kacper Falat Date: Wed, 15 Nov 2023 10:50:37 +0100 Subject: [PATCH 022/463] Changes after review. --- src/libs/actions/User.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 238e73a4bf83..6290275ed3bb 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -36,7 +36,7 @@ Onyx.connect({ }, }); -let myPersonalDetails: Partial = {}; +let myPersonalDetails: OnyxPersonalDetails | Record = {}; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, callback: (value) => { From 3ebfc506f4a3116d2649774d110672f54c025637 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Wed, 15 Nov 2023 15:05:32 +0500 Subject: [PATCH 023/463] 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 024/463] 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 025/463] 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 6ce19e4a4aca1d0a1f08d6b5d23c6242aa985cf6 Mon Sep 17 00:00:00 2001 From: Kacper Falat Date: Wed, 15 Nov 2023 11:33:04 +0100 Subject: [PATCH 026/463] Changes after review. --- src/libs/actions/User.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 6290275ed3bb..4e417192e751 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -16,6 +16,7 @@ import type Login from '@src/types/onyx/Login'; import {OnyxServerUpdate} from '@src/types/onyx/OnyxUpdatesFromServer'; import type OnyxPersonalDetails from '@src/types/onyx/PersonalDetails'; import ReportAction from '@src/types/onyx/ReportAction'; +import {OnyxEntry} from "react-native-onyx/lib/types"; import * as Link from './Link'; import * as OnyxUpdates from './OnyxUpdates'; import * as PersonalDetails from './PersonalDetails'; @@ -447,7 +448,7 @@ function validateSecondaryLogin(contactMethod: string, validateCode: string) { * and if so whether the expiresAt date of a user's ban is before right now * */ -function isBlockedFromConcierge(blockedFromConciergeNVP: BlockedFromConciergeNVP): boolean { +function isBlockedFromConcierge(blockedFromConciergeNVP: OnyxEntry): boolean { if (!blockedFromConciergeNVP || Object.keys(blockedFromConciergeNVP).length === 0) { return false; } From 15e62f92aa9eb07d2ed3ac9ff9b71717395fca47 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Wed, 15 Nov 2023 17:31:22 +0500 Subject: [PATCH 027/463] 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 028/463] 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 4edce16177063d54aab7ce219bc31931ce73cd18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=27fvlvte=27=20Fa=C5=82at?= Date: Mon, 27 Nov 2023 10:32:51 +0100 Subject: [PATCH 029/463] Prettier fix. --- src/libs/actions/User.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 4e417192e751..a75975e07ebe 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -1,5 +1,6 @@ import {isBefore} from 'date-fns'; import Onyx, {OnyxCollection, OnyxUpdate} from 'react-native-onyx'; +import {OnyxEntry} from 'react-native-onyx/lib/types'; import {ValueOf} from 'type-fest'; import * as API from '@libs/API'; import * as ErrorUtils from '@libs/ErrorUtils'; @@ -16,7 +17,6 @@ import type Login from '@src/types/onyx/Login'; import {OnyxServerUpdate} from '@src/types/onyx/OnyxUpdatesFromServer'; import type OnyxPersonalDetails from '@src/types/onyx/PersonalDetails'; import ReportAction from '@src/types/onyx/ReportAction'; -import {OnyxEntry} from "react-native-onyx/lib/types"; import * as Link from './Link'; import * as OnyxUpdates from './OnyxUpdates'; import * as PersonalDetails from './PersonalDetails'; From 0e2833010b3be51885059123e6275d6341b3d4d8 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 28 Nov 2023 13:07:10 +0500 Subject: [PATCH 030/463] 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 d624dd3421c17f54c8ce0706ac27c56122ac2eeb Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 28 Nov 2023 11:34:06 +0100 Subject: [PATCH 031/463] create MVCPFlatList --- src/components/FlatList/MVCPFlatList.js | 206 ++++++++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 src/components/FlatList/MVCPFlatList.js diff --git a/src/components/FlatList/MVCPFlatList.js b/src/components/FlatList/MVCPFlatList.js new file mode 100644 index 000000000000..733ec575ac08 --- /dev/null +++ b/src/components/FlatList/MVCPFlatList.js @@ -0,0 +1,206 @@ +/* eslint-disable es/no-optional-chaining, es/no-nullish-coalescing-operators, react/prop-types */ +import PropTypes from 'prop-types'; +import React from 'react'; +import {FlatList} from 'react-native'; + +function mergeRefs(...args) { + return function forwardRef(node) { + args.forEach((ref) => { + if (ref == null) { + return; + } + if (typeof ref === 'function') { + ref(node); + return; + } + if (typeof ref === 'object') { + // eslint-disable-next-line no-param-reassign + ref.current = node; + return; + } + console.error(`mergeRefs cannot handle Refs of type boolean, number or string, received ref ${String(ref)}`); + }); + }; +} + +function useMergeRefs(...args) { + return React.useMemo( + () => mergeRefs(...args), + // eslint-disable-next-line + [...args], + ); +} + +const MVCPFlatList = React.forwardRef(({maintainVisibleContentPosition, horizontal, inverted, onScroll, ...props}, forwardedRef) => { + const {minIndexForVisible: mvcpMinIndexForVisible, autoscrollToTopThreshold: mvcpAutoscrollToTopThreshold} = maintainVisibleContentPosition ?? {}; + const scrollRef = React.useRef(null); + const prevFirstVisibleOffsetRef = React.useRef(null); + const firstVisibleViewRef = React.useRef(null); + const mutationObserverRef = React.useRef(null); + const lastScrollOffsetRef = React.useRef(0); + + const getScrollOffset = React.useCallback(() => { + if (scrollRef.current == null) { + return 0; + } + return horizontal ? scrollRef.current.getScrollableNode().scrollLeft : scrollRef.current.getScrollableNode().scrollTop; + }, [horizontal]); + + const getContentView = React.useCallback(() => scrollRef.current?.getScrollableNode().childNodes[0], []); + + const scrollToOffset = React.useCallback( + (offset, animated) => { + const behavior = animated ? 'smooth' : 'instant'; + scrollRef.current?.getScrollableNode().scroll(horizontal ? {left: offset, behavior} : {top: offset, behavior}); + }, + [horizontal], + ); + + const prepareForMaintainVisibleContentPosition = React.useCallback(() => { + if (mvcpMinIndexForVisible == null) { + return; + } + + const contentView = getContentView(); + if (contentView == null) { + return; + } + + const scrollOffset = getScrollOffset(); + + const contentViewLength = contentView.childNodes.length; + for (let i = mvcpMinIndexForVisible; i < contentViewLength; i++) { + const subview = contentView.childNodes[inverted ? contentViewLength - i - 1 : i]; + const subviewOffset = horizontal ? subview.offsetLeft : subview.offsetTop; + if (subviewOffset > scrollOffset || i === contentViewLength - 1) { + prevFirstVisibleOffsetRef.current = subviewOffset; + firstVisibleViewRef.current = subview; + break; + } + } + }, [getContentView, getScrollOffset, mvcpMinIndexForVisible, horizontal, inverted]); + + const adjustForMaintainVisibleContentPosition = React.useCallback(() => { + if (mvcpMinIndexForVisible == null) { + return; + } + + const firstVisibleView = firstVisibleViewRef.current; + const prevFirstVisibleOffset = prevFirstVisibleOffsetRef.current; + if (firstVisibleView == null || prevFirstVisibleOffset == null) { + return; + } + + const firstVisibleViewOffset = horizontal ? firstVisibleView.offsetLeft : firstVisibleView.offsetTop; + const delta = firstVisibleViewOffset - prevFirstVisibleOffset; + if (Math.abs(delta) > 0.5) { + const scrollOffset = getScrollOffset(); + prevFirstVisibleOffsetRef.current = firstVisibleViewOffset; + scrollToOffset(scrollOffset + delta, false); + if (mvcpAutoscrollToTopThreshold != null && scrollOffset <= mvcpAutoscrollToTopThreshold) { + scrollToOffset(0, true); + } + } + }, [getScrollOffset, scrollToOffset, mvcpMinIndexForVisible, mvcpAutoscrollToTopThreshold, horizontal]); + + const setupMutationObserver = React.useCallback(() => { + const contentView = getContentView(); + if (contentView == null) { + return; + } + + mutationObserverRef.current?.disconnect(); + + const mutationObserver = new MutationObserver(() => { + // Chrome adjusts scroll position when elements are added at the top of the + // view. We want to have the same behavior as react-native / Safari so we + // reset the scroll position to the last value we got from an event. + const lastScrollOffset = lastScrollOffsetRef.current; + const scrollOffset = getScrollOffset(); + if (lastScrollOffset !== scrollOffset) { + scrollToOffset(lastScrollOffset, false); + } + + // This needs to execute after scroll events are dispatched, but + // in the same tick to avoid flickering. rAF provides the right timing. + requestAnimationFrame(() => { + adjustForMaintainVisibleContentPosition(); + }); + }); + mutationObserver.observe(contentView, { + attributes: true, + childList: true, + subtree: true, + }); + + mutationObserverRef.current = mutationObserver; + }, [adjustForMaintainVisibleContentPosition, getContentView, getScrollOffset, scrollToOffset]); + + React.useEffect(() => { + prepareForMaintainVisibleContentPosition(); + setupMutationObserver(); + }, [prepareForMaintainVisibleContentPosition, setupMutationObserver]); + + const setMergedRef = useMergeRefs(scrollRef, forwardedRef); + + const onRef = React.useCallback( + (newRef) => { + // Make sure to only call refs and re-attach listeners if the node changed. + if (newRef == null || newRef === scrollRef.current) { + return; + } + + setMergedRef(newRef); + prepareForMaintainVisibleContentPosition(); + setupMutationObserver(); + }, + [prepareForMaintainVisibleContentPosition, setMergedRef, setupMutationObserver], + ); + + React.useEffect(() => { + const mutationObserver = mutationObserverRef.current; + return () => { + mutationObserver?.disconnect(); + }; + }, []); + + const onScrollInternal = React.useCallback( + (ev) => { + lastScrollOffsetRef.current = getScrollOffset(); + + prepareForMaintainVisibleContentPosition(); + + onScroll?.(ev); + }, + [getScrollOffset, prepareForMaintainVisibleContentPosition, onScroll], + ); + + return ( + + ); +}); + +MVCPFlatList.displayName = 'MVCPFlatList'; +MVCPFlatList.propTypes = { + maintainVisibleContentPosition: PropTypes.shape({ + minIndexForVisible: PropTypes.number.isRequired, + autoscrollToTopThreshold: PropTypes.number, + }), + horizontal: PropTypes.bool, +}; + +MVCPFlatList.defaultProps = { + maintainVisibleContentPosition: null, + horizontal: false, +}; + +export default MVCPFlatList; From a34fed2c6191c49fa96be0b5f5577ccda245a1a9 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 28 Nov 2023 11:34:18 +0100 Subject: [PATCH 032/463] use MVCPFlatList --- src/components/FlatList/index.web.js | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/components/FlatList/index.web.js diff --git a/src/components/FlatList/index.web.js b/src/components/FlatList/index.web.js new file mode 100644 index 000000000000..7299776db9bc --- /dev/null +++ b/src/components/FlatList/index.web.js @@ -0,0 +1,3 @@ +import MVCPFlatList from './MVCPFlatList'; + +export default MVCPFlatList; From e951bb7747210447028f350e5935f0bedb6e83e6 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Tue, 28 Nov 2023 17:24:39 +0100 Subject: [PATCH 033/463] [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 5d67d029365477c02a7e3ac70bf91c1ed79f1ab1 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Tue, 28 Nov 2023 17:42:15 +0100 Subject: [PATCH 034/463] move scrollToOffset into requestAnimationFrame --- src/components/FlatList/MVCPFlatList.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/FlatList/MVCPFlatList.js b/src/components/FlatList/MVCPFlatList.js index 733ec575ac08..c9ec3c6a95c1 100644 --- a/src/components/FlatList/MVCPFlatList.js +++ b/src/components/FlatList/MVCPFlatList.js @@ -112,18 +112,18 @@ const MVCPFlatList = React.forwardRef(({maintainVisibleContentPosition, horizont mutationObserverRef.current?.disconnect(); const mutationObserver = new MutationObserver(() => { - // Chrome adjusts scroll position when elements are added at the top of the - // view. We want to have the same behavior as react-native / Safari so we - // reset the scroll position to the last value we got from an event. - const lastScrollOffset = lastScrollOffsetRef.current; - const scrollOffset = getScrollOffset(); - if (lastScrollOffset !== scrollOffset) { - scrollToOffset(lastScrollOffset, false); - } - // This needs to execute after scroll events are dispatched, but // in the same tick to avoid flickering. rAF provides the right timing. requestAnimationFrame(() => { + // Chrome adjusts scroll position when elements are added at the top of the + // view. We want to have the same behavior as react-native / Safari so we + // reset the scroll position to the last value we got from an event. + const lastScrollOffset = lastScrollOffsetRef.current; + const scrollOffset = getScrollOffset(); + if (lastScrollOffset !== scrollOffset) { + scrollToOffset(lastScrollOffset, false); + } + adjustForMaintainVisibleContentPosition(); }); }); From 76c1781273e5dd7829d8aa92e87de8e7a0503d4b Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Tue, 28 Nov 2023 17:58:49 +0100 Subject: [PATCH 035/463] [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 036/463] [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 037/463] 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 038/463] [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 039/463] 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 040/463] [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 041/463] [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 042/463] [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 043/463] 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 044/463] 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 f205afe637eab9117f07b3c493fe831eba0d32b1 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Thu, 30 Nov 2023 09:49:29 +0100 Subject: [PATCH 045/463] 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 046/463] 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 047/463] 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 44cc8fe2bdbe7bca7920d4ca032a891bdae9d8b7 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Mon, 4 Dec 2023 16:48:38 +0500 Subject: [PATCH 048/463] refactor: remove unnecessary useMemo --- src/pages/home/sidebar/SidebarLinks.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index 52dbe879d218..09d6c1f2de62 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -148,8 +148,6 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority const viewMode = priorityMode === CONST.PRIORITY_MODE.GSD ? CONST.OPTION_MODE.COMPACT : CONST.OPTION_MODE.DEFAULT; // eslint-disable-next-line react-hooks/exhaustive-deps - const listStyle = useMemo(() => [isLoading ? styles.flexShrink1 : styles.flex1], [isLoading]); - // eslint-disable-next-line react-hooks/exhaustive-deps const contentContainerStyles = useMemo(() => [styles.sidebarListContainer, {paddingBottom: StyleUtils.getSafeAreaMargins(insets).marginBottom}], [insets]); return ( @@ -183,7 +181,7 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority Date: Mon, 4 Dec 2023 16:15:40 +0100 Subject: [PATCH 049/463] Put onyx update data into separate variables --- src/libs/actions/Card.ts | 190 +++++++++++++++--------------- src/libs/actions/TeachersUnite.ts | 174 +++++++++++++-------------- 2 files changed, 186 insertions(+), 178 deletions(-) diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts index 6e1753fbd591..82b9cbc47e7c 100644 --- a/src/libs/actions/Card.ts +++ b/src/libs/actions/Card.ts @@ -1,4 +1,4 @@ -import Onyx from 'react-native-onyx'; +import Onyx, {OnyxUpdate} from 'react-native-onyx'; import * as API from '@libs/API'; import * as Localize from '@libs/Localize'; import CONST from '@src/CONST'; @@ -8,6 +8,36 @@ import type {Response} from '@src/types/onyx'; type Reason = 'damaged' | 'stolen'; function reportVirtualExpensifyCardFraud(cardID: number) { + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD, + value: { + isLoading: true, + }, + }, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD, + value: { + isLoading: false, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD, + value: { + isLoading: false, + }, + }, + ]; + type ReportVirtualExpensifyCardFraudParams = { cardID: number; }; @@ -16,35 +46,7 @@ function reportVirtualExpensifyCardFraud(cardID: number) { cardID, }; - API.write('ReportVirtualExpensifyCardFraud', parameters, { - 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, - }, - }, - ], - }); + API.write('ReportVirtualExpensifyCardFraud', parameters, {optimisticData, successData, failureData}); } /** @@ -53,6 +55,37 @@ function reportVirtualExpensifyCardFraud(cardID: number) { * @param reason - reason for replacement */ function requestReplacementExpensifyCard(cardId: number, reason: Reason) { + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM, + value: { + isLoading: true, + errors: null, + }, + }, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM, + value: { + isLoading: false, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM, + value: { + isLoading: false, + }, + }, + ]; + type RequestReplacementExpensifyCardParams = { cardId: number; reason: string; @@ -63,42 +96,50 @@ function requestReplacementExpensifyCard(cardId: number, reason: Reason) { reason, }; - API.write('RequestReplacementExpensifyCard', parameters, { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM, - value: { - isLoading: true, + API.write('RequestReplacementExpensifyCard', parameters, {optimisticData, successData, failureData}); +} + +/** + * Activates the physical Expensify card based on the last four digits of the card number + */ +function activatePhysicalExpensifyCard(cardLastFourDigits: string, cardID: number) { + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.CARD_LIST, + value: { + [cardID]: { errors: null, + isLoading: true, }, }, - ], - successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM, - value: { + }, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.CARD_LIST, + value: { + [cardID]: { isLoading: false, }, }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM, - value: { + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.CARD_LIST, + value: { + [cardID]: { isLoading: false, }, }, - ], - }); -} + }, + ]; -/** - * Activates the physical Expensify card based on the last four digits of the card number - */ -function activatePhysicalExpensifyCard(cardLastFourDigits: string, cardID: number) { type ActivatePhysicalExpensifyCardParams = { cardLastFourDigits: string; cardID: number; @@ -109,42 +150,7 @@ function activatePhysicalExpensifyCard(cardLastFourDigits: string, cardID: numbe cardID, }; - API.write('ActivatePhysicalExpensifyCard', parameters, { - 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, - }, - }, - }, - ], - }); + API.write('ActivatePhysicalExpensifyCard', parameters, {optimisticData, successData, failureData}); } /** diff --git a/src/libs/actions/TeachersUnite.ts b/src/libs/actions/TeachersUnite.ts index f264d81f33d4..4768794c39f2 100644 --- a/src/libs/actions/TeachersUnite.ts +++ b/src/libs/actions/TeachersUnite.ts @@ -86,6 +86,93 @@ function addSchoolPrincipal(firstName: string, partnerUserID: string, lastName: reportActionID: expenseReportCreatedAction.reportActionID, }; + const optimisticData: OnyxUpdate[] = [ + { + 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, + }, + ]; + + const successData: OnyxUpdate[] = [ + { + 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, + }, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + 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, + }, + ]; + type AddSchoolPrincipalParams = { firstName: string; lastName: string; @@ -100,92 +187,7 @@ function addSchoolPrincipal(firstName: string, partnerUserID: string, lastName: 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, - }, - ], - }); + API.write('AddSchoolPrincipal', parameters, {optimisticData, successData, failureData}); Navigation.dismissModal(expenseChatReportID); } From 97fbe8453a7e6f7ac7b749fc8bf6dfbba7f8e5b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20M=C3=B3rawski?= Date: Tue, 5 Dec 2023 14:59:43 +0100 Subject: [PATCH 050/463] refactor to functional component --- src/pages/SearchPage.js | 313 +++++++++++++++++------------------- src/pages/SearchPage.old.js | 243 ++++++++++++++++++++++++++++ 2 files changed, 389 insertions(+), 167 deletions(-) mode change 100755 => 100644 src/pages/SearchPage.js create mode 100755 src/pages/SearchPage.old.js diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js old mode 100755 new mode 100644 index 5d111e7c181f..77a9ddf930ca --- a/src/pages/SearchPage.js +++ b/src/pages/SearchPage.js @@ -1,21 +1,18 @@ +import _ from 'lodash'; import PropTypes from 'prop-types'; -import React, {Component} from 'react'; +import React, {useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import networkPropTypes from '@components/networkPropTypes'; -import {withNetwork} from '@components/OnyxProvider'; import OptionsSelector from '@components/OptionsSelector'; import ScreenWrapper from '@components/ScreenWrapper'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import withThemeStyles, {withThemeStylesPropTypes} from '@components/withThemeStyles'; -import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; -import compose from '@libs/compose'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import Performance from '@libs/Performance'; import * as ReportUtils from '@libs/ReportUtils'; +import useThemeStyles from '@styles/useThemeStyles'; import * as Report from '@userActions/Report'; import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; @@ -35,209 +32,191 @@ const propTypes = { /** All reports shared with the user */ reports: PropTypes.objectOf(reportPropTypes), - /** Window Dimensions Props */ - ...windowDimensionsPropTypes, - - ...withLocalizePropTypes, - - /** Network info */ - network: networkPropTypes, - /** Whether we are searching for reports in the server */ isSearchingForReports: PropTypes.bool, - ...withThemeStylesPropTypes, }; const defaultProps = { betas: [], personalDetails: {}, reports: {}, - network: {}, isSearchingForReports: false, }; -class SearchPage extends Component { - constructor(props) { - super(props); +// custom hook that handles debouncing the search value using lodash debounce +function useDebouncedState(initialValue, delay) { + const [value, setValue] = useState(initialValue); + const [debouncedValue, setDebouncedValue] = useState(initialValue); + const debouncedSetDebouncedValue = useRef(_.debounce(setDebouncedValue, delay)).current; + + useEffect(() => debouncedSetDebouncedValue.cancel, [debouncedSetDebouncedValue]); + + const handleSetValue = (newValue) => { + setValue(newValue); + debouncedSetDebouncedValue(newValue); + }; + + return [value, debouncedValue, handleSetValue]; +} + +function SearchPage({betas, personalDetails, reports, isSearchingForReports}) { + const [isScreenTransitionEnd, setIsScreenTransitionEnd] = useState(false); + const {translate} = useLocalize(); + const network = useNetwork(); + const themeStyles = useThemeStyles(); + const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState('', 75); + + useEffect(() => { Timing.start(CONST.TIMING.SEARCH_RENDER); Performance.markStart(CONST.TIMING.SEARCH_RENDER); - - this.searchRendered = this.searchRendered.bind(this); - this.selectReport = this.selectReport.bind(this); - this.onChangeText = this.onChangeText.bind(this); - this.updateOptions = this.updateOptions.bind(this); - this.debouncedUpdateOptions = _.debounce(this.updateOptions.bind(this), 75); - this.state = { - searchValue: '', - recentReports: {}, - personalDetails: {}, - userToInvite: {}, - }; - } - - componentDidUpdate(prevProps) { - if (_.isEqual(prevProps.reports, this.props.reports) && _.isEqual(prevProps.personalDetails, this.props.personalDetails)) { - return; + }, []); + + const onChangeText = (text = '') => { + Report.searchInServer(text); + setSearchValue(text); + }; + + const { + recentReports, + personalDetails: localPersonalDetails, + userToInvite, + headerMessage, + } = useMemo(() => { + console.log('updateOptions', isScreenTransitionEnd); + if (!isScreenTransitionEnd) { + return { + recentReports: {}, + personalDetails: {}, + userToInvite: {}, + headerMessage: '', + }; } - this.updateOptions(); - } - - onChangeText(searchValue = '') { - Report.searchInServer(searchValue); - this.setState({searchValue}, this.debouncedUpdateOptions); - } - - /** - * Returns the sections needed for the OptionsSelector - * - * @returns {Array} - */ - getSections() { - const sections = []; + const options = OptionsListUtils.getSearchOptions(reports, personalDetails, debouncedSearchValue.trim(), betas); + const header = OptionsListUtils.getHeaderMessage(options.recentReports.length + options.personalDetails.length !== 0, Boolean(options.userToInvite), debouncedSearchValue); + return {...options, headerMessage: header}; + }, [debouncedSearchValue, reports, personalDetails, betas, isScreenTransitionEnd]); + + const sections = useMemo(() => { + console.log('updateSections'); + const newSections = []; let indexOffset = 0; - if (this.state.recentReports.length > 0) { - sections.push({ - data: this.state.recentReports, + if (recentReports.length > 0) { + newSections.push({ + data: recentReports, shouldShow: true, indexOffset, }); - indexOffset += this.state.recentReports.length; + indexOffset += recentReports.length; } - if (this.state.personalDetails.length > 0) { - sections.push({ - data: this.state.personalDetails, + if (localPersonalDetails.length > 0) { + newSections.push({ + data: localPersonalDetails, shouldShow: true, indexOffset, }); - indexOffset += this.state.recentReports.length; + indexOffset += recentReports.length; } - if (this.state.userToInvite) { - sections.push({ - data: [this.state.userToInvite], + if (userToInvite) { + newSections.push({ + data: [userToInvite], shouldShow: true, indexOffset, }); } - return sections; - } + return newSections; + }, [localPersonalDetails, recentReports, userToInvite]); - searchRendered() { - Timing.end(CONST.TIMING.SEARCH_RENDER); - Performance.markEnd(CONST.TIMING.SEARCH_RENDER); - } - - 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, - }); - } - - /** - * Reset the search value and redirect to the selected report - * - * @param {Object} option - */ - selectReport(option) { + const selectReport = (option) => { if (!option) { return; } if (option.reportID) { - this.setState( - { - searchValue: '', - }, - () => { - Navigation.dismissModal(option.reportID); - }, - ); + setSearchValue(''); + Navigation.dismissModal(option.reportID); } else { Report.navigateToAndOpenReport([option.login]); } - } - - render() { - const sections = this.getSections(); - const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(this.props.personalDetails); - const headerMessage = OptionsListUtils.getHeaderMessage( - this.state.recentReports.length + this.state.personalDetails.length !== 0, - Boolean(this.state.userToInvite), - this.state.searchValue, - ); - - return ( - - {({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => ( - <> - - - - - - )} - - ); - } + }; + + const searchRendered = () => { + Timing.end(CONST.TIMING.SEARCH_RENDER); + Performance.markEnd(CONST.TIMING.SEARCH_RENDER); + }; + + const handleScreenTransitionEnd = () => { + console.log('handleScreenTransitionEnd'); + setIsScreenTransitionEnd(true); + }; + + const isOptionsDataReady = useMemo(() => ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(personalDetails), [personalDetails]); + + console.log('render', { + isScreenTransitionEnd, + isOptionsDataReady, + sections: sections.length, + recentReports, + localPersonalDetails, + userToInvite, + headerMessage, + }); + + return ( + + {({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => ( + <> + + + + + + )} + + ); } SearchPage.propTypes = propTypes; SearchPage.defaultProps = defaultProps; SearchPage.displayName = 'SearchPage'; -export default compose( - withLocalize, - withWindowDimensions, - withNetwork(), - withOnyx({ - reports: { - key: ONYXKEYS.COLLECTION.REPORT, - }, - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, - betas: { - key: ONYXKEYS.BETAS, - }, - isSearchingForReports: { - key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS, - initWithStoredValues: false, - }, - }), - withThemeStyles, -)(SearchPage); +export default withOnyx({ + reports: { + key: ONYXKEYS.COLLECTION.REPORT, + }, + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, + betas: { + key: ONYXKEYS.BETAS, + }, + isSearchingForReports: { + key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS, + initWithStoredValues: false, + }, +})(SearchPage); diff --git a/src/pages/SearchPage.old.js b/src/pages/SearchPage.old.js new file mode 100755 index 000000000000..5d111e7c181f --- /dev/null +++ b/src/pages/SearchPage.old.js @@ -0,0 +1,243 @@ +import PropTypes from 'prop-types'; +import React, {Component} from 'react'; +import {View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import _ from 'underscore'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import networkPropTypes from '@components/networkPropTypes'; +import {withNetwork} from '@components/OnyxProvider'; +import OptionsSelector from '@components/OptionsSelector'; +import ScreenWrapper from '@components/ScreenWrapper'; +import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import withThemeStyles, {withThemeStylesPropTypes} from '@components/withThemeStyles'; +import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; +import compose from '@libs/compose'; +import Navigation from '@libs/Navigation/Navigation'; +import * as OptionsListUtils from '@libs/OptionsListUtils'; +import Performance from '@libs/Performance'; +import * as ReportUtils from '@libs/ReportUtils'; +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 = { + /* Onyx Props */ + + /** 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), + + /** Window Dimensions Props */ + ...windowDimensionsPropTypes, + + ...withLocalizePropTypes, + + /** Network info */ + network: networkPropTypes, + + /** Whether we are searching for reports in the server */ + isSearchingForReports: PropTypes.bool, + ...withThemeStylesPropTypes, +}; + +const defaultProps = { + betas: [], + personalDetails: {}, + reports: {}, + network: {}, + isSearchingForReports: false, +}; + +class SearchPage extends Component { + constructor(props) { + super(props); + + Timing.start(CONST.TIMING.SEARCH_RENDER); + Performance.markStart(CONST.TIMING.SEARCH_RENDER); + + this.searchRendered = this.searchRendered.bind(this); + this.selectReport = this.selectReport.bind(this); + this.onChangeText = this.onChangeText.bind(this); + this.updateOptions = this.updateOptions.bind(this); + this.debouncedUpdateOptions = _.debounce(this.updateOptions.bind(this), 75); + this.state = { + searchValue: '', + recentReports: {}, + personalDetails: {}, + userToInvite: {}, + }; + } + + componentDidUpdate(prevProps) { + if (_.isEqual(prevProps.reports, this.props.reports) && _.isEqual(prevProps.personalDetails, this.props.personalDetails)) { + return; + } + this.updateOptions(); + } + + onChangeText(searchValue = '') { + Report.searchInServer(searchValue); + this.setState({searchValue}, this.debouncedUpdateOptions); + } + + /** + * Returns the sections needed for the OptionsSelector + * + * @returns {Array} + */ + getSections() { + const sections = []; + let indexOffset = 0; + + if (this.state.recentReports.length > 0) { + sections.push({ + data: this.state.recentReports, + shouldShow: true, + indexOffset, + }); + indexOffset += this.state.recentReports.length; + } + + if (this.state.personalDetails.length > 0) { + sections.push({ + data: this.state.personalDetails, + shouldShow: true, + indexOffset, + }); + indexOffset += this.state.recentReports.length; + } + + if (this.state.userToInvite) { + sections.push({ + data: [this.state.userToInvite], + shouldShow: true, + indexOffset, + }); + } + + return sections; + } + + searchRendered() { + Timing.end(CONST.TIMING.SEARCH_RENDER); + Performance.markEnd(CONST.TIMING.SEARCH_RENDER); + } + + 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, + }); + } + + /** + * Reset the search value and redirect to the selected report + * + * @param {Object} option + */ + selectReport(option) { + if (!option) { + return; + } + + if (option.reportID) { + this.setState( + { + searchValue: '', + }, + () => { + Navigation.dismissModal(option.reportID); + }, + ); + } else { + Report.navigateToAndOpenReport([option.login]); + } + } + + render() { + const sections = this.getSections(); + const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(this.props.personalDetails); + const headerMessage = OptionsListUtils.getHeaderMessage( + this.state.recentReports.length + this.state.personalDetails.length !== 0, + Boolean(this.state.userToInvite), + this.state.searchValue, + ); + + return ( + + {({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => ( + <> + + + + + + )} + + ); + } +} + +SearchPage.propTypes = propTypes; +SearchPage.defaultProps = defaultProps; +SearchPage.displayName = 'SearchPage'; + +export default compose( + withLocalize, + withWindowDimensions, + withNetwork(), + withOnyx({ + reports: { + key: ONYXKEYS.COLLECTION.REPORT, + }, + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, + betas: { + key: ONYXKEYS.BETAS, + }, + isSearchingForReports: { + key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS, + initWithStoredValues: false, + }, + }), + withThemeStyles, +)(SearchPage); From abb5fc6ec20cc047e0c31618b478e4174e7f8901 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Wed, 6 Dec 2023 17:06:50 +0500 Subject: [PATCH 051/463] fix: personalDetails not updating --- src/libs/PersonalDetailsUtils.js | 10 ---------- src/pages/SearchPage.js | 15 +++++++++++---- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/libs/PersonalDetailsUtils.js b/src/libs/PersonalDetailsUtils.js index bbe4df529ade..560480dcec9d 100644 --- a/src/libs/PersonalDetailsUtils.js +++ b/src/libs/PersonalDetailsUtils.js @@ -197,17 +197,7 @@ function getFormattedAddress(privatePersonalDetails) { return formattedAddress.trim().replace(/,$/, ''); } -/** - * get personal details - * - * @returns {Object} - */ -function getPersonalDetails() { - return allPersonalDetails || {}; -} - export { - getPersonalDetails, getDisplayNameOrDefault, getPersonalDetailsByIDs, getAccountIDsByLogins, diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js index 34afe5e3c3b5..5dfa3a0cacc4 100755 --- a/src/pages/SearchPage.js +++ b/src/pages/SearchPage.js @@ -15,12 +15,12 @@ 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 * 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,6 +29,9 @@ 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), @@ -47,6 +50,7 @@ const propTypes = { const defaultProps = { betas: [], + personalDetails: {}, reports: {}, network: {}, isSearchingForReports: false, @@ -85,7 +89,7 @@ class SearchPage extends Component { } componentDidUpdate(prevProps) { - if (_.isEqual(prevProps.reports, this.props.reports)) { + if (_.isEqual(prevProps.reports, this.props.reports) && _.isEqual(prevProps.personalDetails, this.props.personalDetails)) { return; } this.updateOptions(); @@ -155,7 +159,7 @@ class SearchPage extends Component { this.interactionTask = InteractionManager.runAfterInteractions(() => { const {recentReports, personalDetails, userToInvite} = OptionsListUtils.getSearchOptions( this.props.reports, - PersonalDetailsUtils.getPersonalDetails(), + this.props.personalDetails, this.state.searchValue.trim(), this.props.betas, ); @@ -193,7 +197,7 @@ class SearchPage extends Component { render() { const sections = this.getSections(); - const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(PersonalDetailsUtils.getPersonalDetails()); + const isOptionsDataReady = ReportUtils.isReportDataReady() && OptionsListUtils.isPersonalDetailsReady(this.props.personalDetails); const headerMessage = OptionsListUtils.getHeaderMessage( this.state.recentReports.length + this.state.personalDetails.length !== 0, Boolean(this.state.userToInvite), @@ -257,6 +261,9 @@ export default compose( key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS, initWithStoredValues: false, }, + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, }), withThemeStyles, )(SearchPage); From e51ee0bdbaaa43cecd74ebca8bd051fac4ac514d Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 6 Dec 2023 16:21:58 +0100 Subject: [PATCH 052/463] Fix TS issue --- src/types/onyx/DemoInfo.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/onyx/DemoInfo.ts b/src/types/onyx/DemoInfo.ts index dcd7efc44d8d..300846fb1a0e 100644 --- a/src/types/onyx/DemoInfo.ts +++ b/src/types/onyx/DemoInfo.ts @@ -1,5 +1,5 @@ type DemoInfo = { - money2020: { + money2020?: { /** If the beginning demo should be shown */ isBeginningDemo?: boolean; }; From 588ac688083ca9db8bb25b7e2e3d8a4763a1c103 Mon Sep 17 00:00:00 2001 From: Roji Philip Date: Fri, 8 Dec 2023 00:03:25 +0530 Subject: [PATCH 053/463] 1 - show report preview in offline on mr deletion --- src/libs/actions/IOU.js | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index ed43569c360a..8b20eb84c69a 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -2004,6 +2004,7 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView const updatedReportAction = { [reportAction.reportActionID]: { pendingAction: shouldShowDeletedRequestMessage ? CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE : CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + childReportID: shouldDeleteTransactionThread ? null : reportAction.childReportID, previousMessage: reportAction.message, message: [ { @@ -2027,9 +2028,9 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView iouReportLastMessageText.length === 0 && !ReportActionsUtils.isDeletedParentAction(lastVisibleAction) && (!transactionThreadID || shouldDeleteTransactionThread); // STEP 4: Update the iouReport and reportPreview with new totals and messages if it wasn't deleted - let updatedIOUReport = null; - let updatedReportPreviewAction = null; - if (!shouldDeleteIOUReport) { + let updatedIOUReport = {...iouReport}; + let updatedReportPreviewAction = {...reportPreviewAction}; + updatedReportPreviewAction.pendingAction = shouldDeleteIOUReport ? CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE : CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE; if (ReportUtils.isExpenseReport(iouReport)) { updatedIOUReport = {...iouReport}; @@ -2048,7 +2049,6 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView updatedIOUReport.lastMessageText = iouReportLastMessageText; updatedIOUReport.lastVisibleActionCreated = lodashGet(lastVisibleAction, 'created'); - updatedReportPreviewAction = {...reportPreviewAction}; const hasNonReimbursableTransactions = ReportUtils.hasNonReimbursableTransactions(iouReport); const messageText = Localize.translateLocal(hasNonReimbursableTransactions ? 'iou.payerSpentAmount' : 'iou.payerOwesAmount', { payer: ReportUtils.getPersonalDetailsForAccountID(updatedIOUReport.managerID).login || '', @@ -2059,7 +2059,6 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView if (reportPreviewAction.childMoneyRequestCount > 0) { updatedReportPreviewAction.childMoneyRequestCount = reportPreviewAction.childMoneyRequestCount - 1; } - } // STEP 5: Build Onyx data const optimisticData = [ @@ -2083,12 +2082,12 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView ] : []), { - onyxMethod: shouldDeleteIOUReport ? Onyx.METHOD.SET : Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`, - value: shouldDeleteIOUReport ? null : updatedReportAction, + value: updatedReportAction, }, { - onyxMethod: shouldDeleteIOUReport ? Onyx.METHOD.SET : Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, value: updatedIOUReport, }, @@ -2107,7 +2106,6 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView value: { hasOutstandingIOU: false, hasOutstandingChildRequest: false, - iouReportID: null, lastMessageText: ReportActionsUtils.getLastVisibleMessage(iouReport.chatReportID, {[reportPreviewAction.reportActionID]: null}).lastMessageText, lastVisibleActionCreated: lodashGet(ReportActionsUtils.getLastVisibleAction(iouReport.chatReportID, {[reportPreviewAction.reportActionID]: null}), 'created'), }, @@ -2124,6 +2122,25 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView [reportAction.reportActionID]: {pendingAction: null}, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport.reportID}`, + value: { + [reportPreviewAction.reportActionID]: shouldDeleteIOUReport ? null : { + pendingAction: null, + errors: null, + }, + }, + }, + ...(shouldDeleteIOUReport + ? [ + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, + value: null, + }, + ] + : []), ]; const failureData = [ From 84b623310a0e4ba5beab632f471911315e42f562 Mon Sep 17 00:00:00 2001 From: Roji Philip Date: Fri, 8 Dec 2023 00:53:02 +0530 Subject: [PATCH 054/463] 2 - fix for TBD on last MR deletion with comments --- src/libs/ReportUtils.ts | 3 +-- src/libs/TransactionUtils.ts | 7 +++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 1266f145de30..f50ad7780ec4 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -898,8 +898,7 @@ function hasSingleParticipant(report: OnyxEntry): boolean { * */ function hasOnlyDistanceRequestTransactions(iouReportID: string | undefined): boolean { - const allTransactions = TransactionUtils.getAllReportTransactions(iouReportID); - return allTransactions.every((transaction) => TransactionUtils.isDistanceRequest(transaction)); + return TransactionUtils.areAllDistanceRequestTransactions(iouReportID); } /** diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index d03235a637c7..74c79430e3d1 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -404,6 +404,12 @@ function getAllReportTransactions(reportID?: string): Transaction[] { return transactions.filter((transaction) => `${transaction.reportID}` === `${reportID}`); } +function areAllDistanceRequestTransactions(reportID?: string): boolean { + const reportTransactions: Transaction[] = getAllReportTransactions(reportID); + const areAllDistanceRequestTransactions = reportTransactions.every((transaction) => isDistanceRequest(transaction)); + return reportTransactions.length > 0 && areAllDistanceRequestTransactions; +} + function waypointHasValidAddress(waypoint: RecentWaypoint | Waypoint): boolean { return !!waypoint?.address?.trim(); } @@ -480,6 +486,7 @@ export { getTag, getLinkedTransaction, getAllReportTransactions, + areAllDistanceRequestTransactions, hasReceipt, hasEReceipt, hasRoute, From 337813bbe70bcf6bd4f84aaa04f65b747f6164db Mon Sep 17 00:00:00 2001 From: Roji Philip Date: Fri, 8 Dec 2023 01:23:04 +0530 Subject: [PATCH 055/463] 3 - display correct total amount when MR exist --- src/libs/IOUUtils.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts index afbbcc2684a0..51c131281676 100644 --- a/src/libs/IOUUtils.ts +++ b/src/libs/IOUUtils.ts @@ -44,7 +44,6 @@ function updateIOUOwnerAndTotal(iouReport: OnyxEntry, actorAccountID: nu // Make a copy so we don't mutate the original object const iouReportUpdate: Report = {...iouReport}; - if (iouReportUpdate.total) { if (actorAccountID === iouReport.ownerAccountID) { iouReportUpdate.total += isDeleting ? -amount : amount; } else { @@ -59,7 +58,6 @@ function updateIOUOwnerAndTotal(iouReport: OnyxEntry, actorAccountID: nu } iouReportUpdate.hasOutstandingIOU = iouReportUpdate.total !== 0; - } return iouReportUpdate; } From 0a839489da5d858ace97a77987063133c7f986d4 Mon Sep 17 00:00:00 2001 From: Roji Philip Date: Fri, 8 Dec 2023 01:43:34 +0530 Subject: [PATCH 056/463] 4 - reply link in report preview opens iou report --- src/libs/ReportUtils.ts | 2 ++ src/pages/home/report/ReportActionItem.js | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index ac712c50e641..5e8a36515230 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -150,6 +150,7 @@ type OptimisticReportPreview = Pick< | 'childLastMoneyRequestComment' | 'childRecentReceiptTransactionIDs' | 'whisperedToAccountIDs' + | 'childReportID' > & {reportID?: string; accountID?: number}; type UpdateReportPreview = Pick< @@ -2919,6 +2920,7 @@ function buildOptimisticReportPreview(chatReport: OnyxEntry, iouReport: accountID: iouReport?.managerID ?? 0, // The preview is initially whispered if created with a receipt, so the actor is the current user as well actorAccountID: hasReceipt ? currentUserAccountID : iouReport?.managerID ?? 0, + childReportID: iouReport?.reportID, childMoneyRequestCount: 1, childLastMoneyRequestComment: comment, childRecentReceiptTransactionIDs: hasReceipt && isNotEmptyObject(transaction) ? {[transaction?.transactionID ?? '']: created} : undefined, diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index f850daaa1ffb..e4219220c464 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -524,7 +524,7 @@ function ReportActionItem(props) { {shouldDisplayThreadReplies && ( Date: Fri, 8 Dec 2023 02:06:07 +0530 Subject: [PATCH 057/463] safe logic for issue-3 as per proposal --- src/libs/IOUUtils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts index 51c131281676..f8611bdf671e 100644 --- a/src/libs/IOUUtils.ts +++ b/src/libs/IOUUtils.ts @@ -43,7 +43,7 @@ function updateIOUOwnerAndTotal(iouReport: OnyxEntry, actorAccountID: nu // Make a copy so we don't mutate the original object const iouReportUpdate: Report = {...iouReport}; - + if (typeof iouReportUpdate.total !== 'undefined') { if (actorAccountID === iouReport.ownerAccountID) { iouReportUpdate.total += isDeleting ? -amount : amount; } else { @@ -58,6 +58,7 @@ function updateIOUOwnerAndTotal(iouReport: OnyxEntry, actorAccountID: nu } iouReportUpdate.hasOutstandingIOU = iouReportUpdate.total !== 0; + } return iouReportUpdate; } From efbe915914189e9cb337b27d071dff8e14211d5b Mon Sep 17 00:00:00 2001 From: Roji Philip Date: Fri, 8 Dec 2023 02:26:44 +0530 Subject: [PATCH 058/463] lint fixes --- src/libs/TransactionUtils.ts | 4 ++-- src/libs/actions/IOU.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 74c79430e3d1..664d9bcd8a23 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -406,8 +406,8 @@ function getAllReportTransactions(reportID?: string): Transaction[] { function areAllDistanceRequestTransactions(reportID?: string): boolean { const reportTransactions: Transaction[] = getAllReportTransactions(reportID); - const areAllDistanceRequestTransactions = reportTransactions.every((transaction) => isDistanceRequest(transaction)); - return reportTransactions.length > 0 && areAllDistanceRequestTransactions; + const areAllDistanceRequests = reportTransactions.every((transaction) => isDistanceRequest(transaction)); + return reportTransactions.length > 0 && areAllDistanceRequests; } function waypointHasValidAddress(waypoint: RecentWaypoint | Waypoint): boolean { diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 8b20eb84c69a..b2637e618fde 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -2029,7 +2029,7 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView // STEP 4: Update the iouReport and reportPreview with new totals and messages if it wasn't deleted let updatedIOUReport = {...iouReport}; - let updatedReportPreviewAction = {...reportPreviewAction}; + const updatedReportPreviewAction = {...reportPreviewAction}; updatedReportPreviewAction.pendingAction = shouldDeleteIOUReport ? CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE : CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE; if (ReportUtils.isExpenseReport(iouReport)) { updatedIOUReport = {...iouReport}; From 92bdf81d53509aa454c152c3696634f8bafbf091 Mon Sep 17 00:00:00 2001 From: Roji Philip Date: Fri, 8 Dec 2023 13:00:43 +0530 Subject: [PATCH 059/463] jest fixes --- src/libs/actions/IOU.js | 6 ++++-- tests/actions/IOUTest.js | 11 +++++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index b2637e618fde..0b58b1f78de4 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -2116,10 +2116,12 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView const successData = [ { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: shouldDeleteIOUReport ? Onyx.METHOD.SET : Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`, value: { - [reportAction.reportActionID]: {pendingAction: null}, + [reportAction.reportActionID]: shouldDeleteIOUReport ? null : { + pendingAction: null + }, }, }, { diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index 18793e88d624..eb373c653148 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -2003,7 +2003,8 @@ describe('actions/IOU', () => { }); createIOUAction = _.find(reportActionsForReport, (reportAction) => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU); - expect(createIOUAction).toBeFalsy(); + // Then the IOU Action should be truthy for offline support. + expect(createIOUAction).toBeTruthy(); // Then we check if the transaction is removed from the transactions collection const t = await new Promise((resolve) => { @@ -2021,6 +2022,7 @@ describe('actions/IOU', () => { // Given fetch operations are resumed fetch.resume(); + await waitForBatchedUpdates(); // Then we recheck the IOU report action from the report actions collection reportActionsForReport = await new Promise((resolve) => { @@ -2071,11 +2073,12 @@ describe('actions/IOU', () => { }); }); - // Then the report should be falsy (indicating deletion) - expect(report).toBeFalsy(); + // Then the report should be truthy for offline support + expect(report).toBeTruthy(); // Given the resumed fetch state fetch.resume(); + await waitForBatchedUpdates(); report = await new Promise((resolve) => { const connectionID = Onyx.connect({ @@ -2088,7 +2091,7 @@ describe('actions/IOU', () => { }); }); - // Then the report should still be falsy (confirming deletion persisted) + // Then the report should be falsy so that there is no trace of the money request. expect(report).toBeFalsy(); }); From 6b954ec0e152b205e735ebc00bafc2c474003c9e Mon Sep 17 00:00:00 2001 From: Roji Philip Date: Fri, 8 Dec 2023 15:22:46 +0530 Subject: [PATCH 060/463] added comments and minor fixes --- src/libs/IOUUtils.ts | 7 +++++-- src/libs/TransactionUtils.ts | 4 ++++ src/libs/actions/IOU.js | 1 - 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts index f8611bdf671e..6efe7df2fc2f 100644 --- a/src/libs/IOUUtils.ts +++ b/src/libs/IOUUtils.ts @@ -43,7 +43,11 @@ function updateIOUOwnerAndTotal(iouReport: OnyxEntry, actorAccountID: nu // Make a copy so we don't mutate the original object const iouReportUpdate: Report = {...iouReport}; - if (typeof iouReportUpdate.total !== 'undefined') { + + // Let us ensure a valid value before updating the total amount. + if (!iouReportUpdate.total) { + iouReportUpdate.total = 0; + } if (actorAccountID === iouReport.ownerAccountID) { iouReportUpdate.total += isDeleting ? -amount : amount; } else { @@ -58,7 +62,6 @@ function updateIOUOwnerAndTotal(iouReport: OnyxEntry, actorAccountID: nu } iouReportUpdate.hasOutstandingIOU = iouReportUpdate.total !== 0; - } return iouReportUpdate; } diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 664d9bcd8a23..a44e1b6a47ef 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -404,6 +404,10 @@ function getAllReportTransactions(reportID?: string): Transaction[] { return transactions.filter((transaction) => `${transaction.reportID}` === `${reportID}`); } +/** + * Check if all the transactions in the iou report are distance requests. If so, return true. Else, return false. + * + */ function areAllDistanceRequestTransactions(reportID?: string): boolean { const reportTransactions: Transaction[] = getAllReportTransactions(reportID); const areAllDistanceRequests = reportTransactions.every((transaction) => isDistanceRequest(transaction)); diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 0b58b1f78de4..7b86fa1f8cb6 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -2004,7 +2004,6 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView const updatedReportAction = { [reportAction.reportActionID]: { pendingAction: shouldShowDeletedRequestMessage ? CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE : CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, - childReportID: shouldDeleteTransactionThread ? null : reportAction.childReportID, previousMessage: reportAction.message, message: [ { From 8e55cca513ff56b47031a8332a4cba849e46722f Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Fri, 8 Dec 2023 11:25:27 +0100 Subject: [PATCH 061/463] Move updates to global.d.ts file --- src/types/global.d.ts | 6 ++++++ src/types/modules/window.d.ts | 10 ---------- 2 files changed, 6 insertions(+), 10 deletions(-) delete mode 100644 src/types/modules/window.d.ts diff --git a/src/types/global.d.ts b/src/types/global.d.ts index a807b4328d50..f131eb5ef849 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -22,3 +22,9 @@ declare module '*.lottie' { } declare module 'react-native-device-info/jest/react-native-device-info-mock'; + +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +interface Window { + enableMemoryOnlyKeys: () => void; + disableMemoryOnlyKeys: () => void; +} diff --git a/src/types/modules/window.d.ts b/src/types/modules/window.d.ts deleted file mode 100644 index 1910c26768f5..000000000000 --- a/src/types/modules/window.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -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 43d403c13041d05d2dc6bc36f31387999784197c Mon Sep 17 00:00:00 2001 From: Roji Philip Date: Fri, 8 Dec 2023 16:26:13 +0530 Subject: [PATCH 062/463] prettier fix --- src/libs/IOUUtils.ts | 26 ++++++------ src/libs/TransactionUtils.ts | 2 +- src/libs/actions/IOU.js | 80 +++++++++++++++++++----------------- 3 files changed, 56 insertions(+), 52 deletions(-) diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts index 6efe7df2fc2f..9671808e5c7d 100644 --- a/src/libs/IOUUtils.ts +++ b/src/libs/IOUUtils.ts @@ -45,23 +45,23 @@ function updateIOUOwnerAndTotal(iouReport: OnyxEntry, actorAccountID: nu const iouReportUpdate: Report = {...iouReport}; // Let us ensure a valid value before updating the total amount. - if (!iouReportUpdate.total) { + if (!iouReportUpdate.total) { iouReportUpdate.total = 0; } - if (actorAccountID === iouReport.ownerAccountID) { - iouReportUpdate.total += isDeleting ? -amount : amount; - } else { - iouReportUpdate.total += isDeleting ? amount : -amount; - } + if (actorAccountID === iouReport.ownerAccountID) { + iouReportUpdate.total += isDeleting ? -amount : amount; + } else { + iouReportUpdate.total += isDeleting ? amount : -amount; + } - if (iouReportUpdate.total < 0) { - // The total sign has changed and hence we need to flip the manager and owner of the report. - iouReportUpdate.ownerAccountID = iouReport.managerID; - iouReportUpdate.managerID = iouReport.ownerAccountID; - iouReportUpdate.total = -iouReportUpdate.total; - } + if (iouReportUpdate.total < 0) { + // The total sign has changed and hence we need to flip the manager and owner of the report. + iouReportUpdate.ownerAccountID = iouReport.managerID; + iouReportUpdate.managerID = iouReport.ownerAccountID; + iouReportUpdate.total = -iouReportUpdate.total; + } - iouReportUpdate.hasOutstandingIOU = iouReportUpdate.total !== 0; + iouReportUpdate.hasOutstandingIOU = iouReportUpdate.total !== 0; return iouReportUpdate; } diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index a44e1b6a47ef..60104a030370 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -408,7 +408,7 @@ function getAllReportTransactions(reportID?: string): Transaction[] { * Check if all the transactions in the iou report are distance requests. If so, return true. Else, return false. * */ -function areAllDistanceRequestTransactions(reportID?: string): boolean { +function areAllDistanceRequestTransactions(reportID?: string): boolean { const reportTransactions: Transaction[] = getAllReportTransactions(reportID); const areAllDistanceRequests = reportTransactions.every((transaction) => isDistanceRequest(transaction)); return reportTransactions.length > 0 && areAllDistanceRequests; diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 7b86fa1f8cb6..a55933ac9318 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -2030,34 +2030,34 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView let updatedIOUReport = {...iouReport}; const updatedReportPreviewAction = {...reportPreviewAction}; updatedReportPreviewAction.pendingAction = shouldDeleteIOUReport ? CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE : CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE; - if (ReportUtils.isExpenseReport(iouReport)) { - updatedIOUReport = {...iouReport}; + if (ReportUtils.isExpenseReport(iouReport)) { + updatedIOUReport = {...iouReport}; - // Because of the Expense reports are stored as negative values, we add the total from the amount - updatedIOUReport.total += TransactionUtils.getAmount(transaction, true); - } else { - updatedIOUReport = IOUUtils.updateIOUOwnerAndTotal( - iouReport, - reportAction.actorAccountID, - TransactionUtils.getAmount(transaction, false), - TransactionUtils.getCurrency(transaction), - true, - ); - } + // Because of the Expense reports are stored as negative values, we add the total from the amount + updatedIOUReport.total += TransactionUtils.getAmount(transaction, true); + } else { + updatedIOUReport = IOUUtils.updateIOUOwnerAndTotal( + iouReport, + reportAction.actorAccountID, + TransactionUtils.getAmount(transaction, false), + TransactionUtils.getCurrency(transaction), + true, + ); + } - updatedIOUReport.lastMessageText = iouReportLastMessageText; - updatedIOUReport.lastVisibleActionCreated = lodashGet(lastVisibleAction, 'created'); + updatedIOUReport.lastMessageText = iouReportLastMessageText; + updatedIOUReport.lastVisibleActionCreated = lodashGet(lastVisibleAction, 'created'); - const hasNonReimbursableTransactions = ReportUtils.hasNonReimbursableTransactions(iouReport); - const messageText = Localize.translateLocal(hasNonReimbursableTransactions ? 'iou.payerSpentAmount' : 'iou.payerOwesAmount', { - payer: ReportUtils.getPersonalDetailsForAccountID(updatedIOUReport.managerID).login || '', - amount: CurrencyUtils.convertToDisplayString(updatedIOUReport.total, updatedIOUReport.currency), - }); - updatedReportPreviewAction.message[0].text = messageText; - updatedReportPreviewAction.message[0].html = messageText; - if (reportPreviewAction.childMoneyRequestCount > 0) { - updatedReportPreviewAction.childMoneyRequestCount = reportPreviewAction.childMoneyRequestCount - 1; - } + const hasNonReimbursableTransactions = ReportUtils.hasNonReimbursableTransactions(iouReport); + const messageText = Localize.translateLocal(hasNonReimbursableTransactions ? 'iou.payerSpentAmount' : 'iou.payerOwesAmount', { + payer: ReportUtils.getPersonalDetailsForAccountID(updatedIOUReport.managerID).login || '', + amount: CurrencyUtils.convertToDisplayString(updatedIOUReport.total, updatedIOUReport.currency), + }); + updatedReportPreviewAction.message[0].text = messageText; + updatedReportPreviewAction.message[0].html = messageText; + if (reportPreviewAction.childMoneyRequestCount > 0) { + updatedReportPreviewAction.childMoneyRequestCount = reportPreviewAction.childMoneyRequestCount - 1; + } // STEP 5: Build Onyx data const optimisticData = [ @@ -2118,29 +2118,33 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView onyxMethod: shouldDeleteIOUReport ? Onyx.METHOD.SET : Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`, value: { - [reportAction.reportActionID]: shouldDeleteIOUReport ? null : { - pendingAction: null - }, + [reportAction.reportActionID]: shouldDeleteIOUReport + ? null + : { + pendingAction: null, + }, }, }, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport.reportID}`, value: { - [reportPreviewAction.reportActionID]: shouldDeleteIOUReport ? null : { - pendingAction: null, - errors: null, - }, + [reportPreviewAction.reportActionID]: shouldDeleteIOUReport + ? null + : { + pendingAction: null, + errors: null, + }, }, }, ...(shouldDeleteIOUReport ? [ - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, - value: null, - }, - ] + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, + value: null, + }, + ] : []), ]; From dbf945ab18cb9085d5f1184c0ffcd870e003a07e Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Fri, 8 Dec 2023 14:39:26 -0700 Subject: [PATCH 063/463] Remove old receipt selector files --- src/pages/EditRequestReceiptPage.js | 2 +- src/pages/iou/MoneyRequestSelectorPage.js | 2 +- .../CameraPermission/index.android.js | 12 - .../CameraPermission/index.ios.js | 11 - .../ReceiptSelector/CameraPermission/index.js | 5 - .../NavigationAwareCamera/index.js | 80 ----- .../NavigationAwareCamera/index.native.js | 28 -- src/pages/iou/ReceiptSelector/index.js | 336 ------------------ src/pages/iou/ReceiptSelector/index.native.js | 298 ---------------- 9 files changed, 2 insertions(+), 772 deletions(-) delete mode 100644 src/pages/iou/ReceiptSelector/CameraPermission/index.android.js delete mode 100644 src/pages/iou/ReceiptSelector/CameraPermission/index.ios.js delete mode 100644 src/pages/iou/ReceiptSelector/CameraPermission/index.js delete mode 100644 src/pages/iou/ReceiptSelector/NavigationAwareCamera/index.js delete mode 100644 src/pages/iou/ReceiptSelector/NavigationAwareCamera/index.native.js delete mode 100644 src/pages/iou/ReceiptSelector/index.js delete mode 100644 src/pages/iou/ReceiptSelector/index.native.js diff --git a/src/pages/EditRequestReceiptPage.js b/src/pages/EditRequestReceiptPage.js index 03c0aa777d5e..a271f15a91b0 100644 --- a/src/pages/EditRequestReceiptPage.js +++ b/src/pages/EditRequestReceiptPage.js @@ -7,7 +7,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; import useThemeStyles from '@styles/useThemeStyles'; -import ReceiptSelector from './iou/ReceiptSelector'; +import ReceiptSelector from './iou/request/step/IOURequestStepScan'; const propTypes = { /** React Navigation route */ diff --git a/src/pages/iou/MoneyRequestSelectorPage.js b/src/pages/iou/MoneyRequestSelectorPage.js index af52ea1222ed..845625c13176 100644 --- a/src/pages/iou/MoneyRequestSelectorPage.js +++ b/src/pages/iou/MoneyRequestSelectorPage.js @@ -24,7 +24,7 @@ import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import NewDistanceRequestPage from './NewDistanceRequestPage'; -import ReceiptSelector from './ReceiptSelector'; +import ReceiptSelector from './request/step/IOURequestStepScan'; import NewRequestAmountPage from './steps/NewRequestAmountPage'; const propTypes = { diff --git a/src/pages/iou/ReceiptSelector/CameraPermission/index.android.js b/src/pages/iou/ReceiptSelector/CameraPermission/index.android.js deleted file mode 100644 index 3eb9ef4eea5a..000000000000 --- a/src/pages/iou/ReceiptSelector/CameraPermission/index.android.js +++ /dev/null @@ -1,12 +0,0 @@ -import {check, PERMISSIONS, request} from 'react-native-permissions'; - -function requestCameraPermission() { - return request(PERMISSIONS.ANDROID.CAMERA); -} - -// Android will never return blocked after a check, you have to request the permission to get the info. -function getCameraPermissionStatus() { - return check(PERMISSIONS.ANDROID.CAMERA); -} - -export {requestCameraPermission, getCameraPermissionStatus}; diff --git a/src/pages/iou/ReceiptSelector/CameraPermission/index.ios.js b/src/pages/iou/ReceiptSelector/CameraPermission/index.ios.js deleted file mode 100644 index 3c24bfa10d6f..000000000000 --- a/src/pages/iou/ReceiptSelector/CameraPermission/index.ios.js +++ /dev/null @@ -1,11 +0,0 @@ -import {check, PERMISSIONS, request} from 'react-native-permissions'; - -function requestCameraPermission() { - return request(PERMISSIONS.IOS.CAMERA); -} - -function getCameraPermissionStatus() { - return check(PERMISSIONS.IOS.CAMERA); -} - -export {requestCameraPermission, getCameraPermissionStatus}; diff --git a/src/pages/iou/ReceiptSelector/CameraPermission/index.js b/src/pages/iou/ReceiptSelector/CameraPermission/index.js deleted file mode 100644 index 4357b592d7ef..000000000000 --- a/src/pages/iou/ReceiptSelector/CameraPermission/index.js +++ /dev/null @@ -1,5 +0,0 @@ -function requestCameraPermission() {} - -function getCameraPermissionStatus() {} - -export {requestCameraPermission, getCameraPermissionStatus}; diff --git a/src/pages/iou/ReceiptSelector/NavigationAwareCamera/index.js b/src/pages/iou/ReceiptSelector/NavigationAwareCamera/index.js deleted file mode 100644 index 10b16da13b6e..000000000000 --- a/src/pages/iou/ReceiptSelector/NavigationAwareCamera/index.js +++ /dev/null @@ -1,80 +0,0 @@ -import PropTypes from 'prop-types'; -import React, {useEffect, useRef} from 'react'; -import {View} from 'react-native'; -import Webcam from 'react-webcam'; -import useTabNavigatorFocus from '@hooks/useTabNavigatorFocus'; - -const propTypes = { - /** Flag to turn on/off the torch/flashlight - if available */ - torchOn: PropTypes.bool, - - /** The index of the tab that contains this camera */ - cameraTabIndex: PropTypes.number.isRequired, - - /** Callback function when media stream becomes available - user granted camera permissions and camera starts to work */ - onUserMedia: PropTypes.func, - - /** Callback function passing torch/flashlight capability as bool param of the browser */ - onTorchAvailability: PropTypes.func, -}; - -const defaultProps = { - onUserMedia: undefined, - onTorchAvailability: undefined, - torchOn: false, -}; - -// Wraps a camera that will only be active when the tab is focused or as soon as it starts to become focused. -const NavigationAwareCamera = React.forwardRef(({torchOn, onTorchAvailability, cameraTabIndex, ...props}, ref) => { - const trackRef = useRef(null); - const shouldShowCamera = useTabNavigatorFocus({ - tabIndex: cameraTabIndex, - }); - - const handleOnUserMedia = (stream) => { - if (props.onUserMedia) { - props.onUserMedia(stream); - } - - const [track] = stream.getVideoTracks(); - const capabilities = track.getCapabilities(); - if (capabilities.torch) { - trackRef.current = track; - } - if (onTorchAvailability) { - onTorchAvailability(!!capabilities.torch); - } - }; - - useEffect(() => { - if (!trackRef.current) { - return; - } - - trackRef.current.applyConstraints({ - advanced: [{torch: torchOn}], - }); - }, [torchOn]); - - if (!shouldShowCamera) { - return null; - } - return ( - - - - ); -}); - -NavigationAwareCamera.propTypes = propTypes; -NavigationAwareCamera.displayName = 'NavigationAwareCamera'; -NavigationAwareCamera.defaultProps = defaultProps; - -export default NavigationAwareCamera; diff --git a/src/pages/iou/ReceiptSelector/NavigationAwareCamera/index.native.js b/src/pages/iou/ReceiptSelector/NavigationAwareCamera/index.native.js deleted file mode 100644 index 65c17d3cb7ab..000000000000 --- a/src/pages/iou/ReceiptSelector/NavigationAwareCamera/index.native.js +++ /dev/null @@ -1,28 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import {Camera} from 'react-native-vision-camera'; -import useTabNavigatorFocus from '@hooks/useTabNavigatorFocus'; - -const propTypes = { - /* The index of the tab that contains this camera */ - cameraTabIndex: PropTypes.number.isRequired, -}; - -// Wraps a camera that will only be active when the tab is focused or as soon as it starts to become focused. -const NavigationAwareCamera = React.forwardRef(({cameraTabIndex, ...props}, ref) => { - const isCameraActive = useTabNavigatorFocus({tabIndex: cameraTabIndex}); - - return ( - - ); -}); - -NavigationAwareCamera.propTypes = propTypes; -NavigationAwareCamera.displayName = 'NavigationAwareCamera'; - -export default NavigationAwareCamera; diff --git a/src/pages/iou/ReceiptSelector/index.js b/src/pages/iou/ReceiptSelector/index.js deleted file mode 100644 index dd7c2e3a104e..000000000000 --- a/src/pages/iou/ReceiptSelector/index.js +++ /dev/null @@ -1,336 +0,0 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; -import React, {useCallback, useContext, useReducer, useRef, useState} from 'react'; -import {ActivityIndicator, PanResponder, PixelRatio, Text, View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import Hand from '@assets/images/hand.svg'; -import ReceiptUpload from '@assets/images/receipt-upload.svg'; -import Shutter from '@assets/images/shutter.svg'; -import AttachmentPicker from '@components/AttachmentPicker'; -import Button from '@components/Button'; -import ConfirmModal from '@components/ConfirmModal'; -import CopyTextToClipboard from '@components/CopyTextToClipboard'; -import {DragAndDropContext} from '@components/DragAndDrop/Provider'; -import Icon from '@components/Icon'; -import * as Expensicons from '@components/Icon/Expensicons'; -import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; -import useLocalize from '@hooks/useLocalize'; -import useWindowDimensions from '@hooks/useWindowDimensions'; -import * as Browser from '@libs/Browser'; -import * as FileUtils from '@libs/fileDownload/FileUtils'; -import Navigation from '@libs/Navigation/Navigation'; -import {iouDefaultProps, iouPropTypes} from '@pages/iou/propTypes'; -import ReceiptDropUI from '@pages/iou/ReceiptDropUI'; -import reportPropTypes from '@pages/reportPropTypes'; -import useTheme from '@styles/themes/useTheme'; -import useThemeStyles from '@styles/useThemeStyles'; -import * as IOU from '@userActions/IOU'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import NavigationAwareCamera from './NavigationAwareCamera'; - -const propTypes = { - /** The report on which the request is initiated on */ - report: reportPropTypes, - - /** React Navigation route */ - route: PropTypes.shape({ - /** Params from the route */ - params: PropTypes.shape({ - /** The type of IOU report, i.e. bill, request, send */ - iouType: PropTypes.string, - - /** The report ID of the IOU */ - reportID: PropTypes.string, - }), - - /** The current route path */ - path: PropTypes.string, - }).isRequired, - - /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ - iou: iouPropTypes, - - /** The id of the transaction we're editing */ - transactionID: PropTypes.string, -}; - -const defaultProps = { - report: {}, - iou: iouDefaultProps, - transactionID: '', -}; - -function ReceiptSelector({route, transactionID, iou, report}) { - const theme = useTheme(); - const styles = useThemeStyles(); - const iouType = lodashGet(route, 'params.iouType', ''); - const pageIndex = lodashGet(route, 'params.pageIndex', 1); - - // Grouping related states - const [isAttachmentInvalid, setIsAttachmentInvalid] = useState(false); - const [attachmentInvalidReasonTitle, setAttachmentInvalidReasonTitle] = useState(''); - const [attachmentInvalidReason, setAttachmentValidReason] = useState(''); - - const [receiptImageTopPosition, setReceiptImageTopPosition] = useState(0); - const {isSmallScreenWidth} = useWindowDimensions(); - const {translate} = useLocalize(); - const {isDraggingOver} = useContext(DragAndDropContext); - - const [cameraPermissionState, setCameraPermissionState] = useState('prompt'); - const [isFlashLightOn, toggleFlashlight] = useReducer((state) => !state, false); - const [isTorchAvailable, setIsTorchAvailable] = useState(false); - const cameraRef = useRef(null); - - const hideReciptModal = () => { - setIsAttachmentInvalid(false); - }; - - /** - * Sets the upload receipt error modal content when an invalid receipt is uploaded - * @param {*} isInvalid - * @param {*} title - * @param {*} reason - */ - const setUploadReceiptError = (isInvalid, title, reason) => { - setIsAttachmentInvalid(isInvalid); - setAttachmentInvalidReasonTitle(title); - setAttachmentValidReason(reason); - }; - - function validateReceipt(file) { - const {fileExtension} = FileUtils.splitExtensionFromFileName(lodashGet(file, 'name', '')); - if (!CONST.API_ATTACHMENT_VALIDATIONS.ALLOWED_RECEIPT_EXTENSIONS.includes(fileExtension.toLowerCase())) { - setUploadReceiptError(true, 'attachmentPicker.wrongFileType', 'attachmentPicker.notAllowedExtension'); - return false; - } - - if (lodashGet(file, 'size', 0) > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) { - setUploadReceiptError(true, 'attachmentPicker.attachmentTooLarge', 'attachmentPicker.sizeExceeded'); - return false; - } - - if (lodashGet(file, 'size', 0) < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) { - setUploadReceiptError(true, 'attachmentPicker.attachmentTooSmall', 'attachmentPicker.sizeNotMet'); - return false; - } - - return true; - } - - /** - * Sets the Receipt objects and navigates the user to the next page - * @param {Object} file - * @param {Object} iouObject - * @param {Object} reportObject - */ - const setReceiptAndNavigate = (file, iouObject, reportObject) => { - if (!validateReceipt(file)) { - return; - } - - const filePath = URL.createObjectURL(file); - IOU.setMoneyRequestReceipt(filePath, file.name); - - if (transactionID) { - IOU.replaceReceipt(transactionID, file, filePath); - Navigation.dismissModal(); - return; - } - - IOU.navigateToNextPage(iouObject, iouType, reportObject, route.path); - }; - - const capturePhoto = useCallback(() => { - if (!cameraRef.current.getScreenshot) { - return; - } - const imageBase64 = cameraRef.current.getScreenshot(); - const filename = `receipt_${Date.now()}.png`; - const imageFile = FileUtils.base64ToFile(imageBase64, filename); - const filePath = URL.createObjectURL(imageFile); - IOU.setMoneyRequestReceipt(filePath, imageFile.name); - - if (transactionID) { - IOU.replaceReceipt(transactionID, imageFile, filePath); - Navigation.dismissModal(); - return; - } - - IOU.navigateToNextPage(iou, iouType, report, route.path); - }, [cameraRef, iou, report, iouType, transactionID, route.path]); - - const panResponder = useRef( - PanResponder.create({ - onPanResponderTerminationRequest: () => false, - }), - ).current; - - const mobileCameraView = () => ( - <> - - {(cameraPermissionState === 'prompt' || !cameraPermissionState) && ( - - )} - - {cameraPermissionState === 'denied' && ( - - - {translate('receipt.takePhoto')} - {translate('receipt.cameraAccess')} - - )} - setCameraPermissionState('granted')} - onUserMediaError={() => setCameraPermissionState('denied')} - style={{...styles.videoContainer, display: cameraPermissionState !== 'granted' ? 'none' : 'block'}} - ref={cameraRef} - screenshotFormat="image/png" - videoConstraints={{facingMode: {exact: 'environment'}}} - torchOn={isFlashLightOn} - onTorchAvailability={setIsTorchAvailable} - forceScreenshotSourceSize - cameraTabIndex={pageIndex} - /> - - - - - {({openPicker}) => ( - { - openPicker({ - onPicked: (file) => { - setReceiptAndNavigate(file, iou, report); - }, - }); - }} - > - - - )} - - - - - - - - - - ); - - const desktopUploadView = () => ( - <> - setReceiptImageTopPosition(PixelRatio.roundToNearestPixel(nativeEvent.layout.top))}> - - - - - {translate('receipt.upload')} - - {isSmallScreenWidth ? translate('receipt.chooseReceipt') : translate('receipt.dragReceiptBeforeEmail')} - - {isSmallScreenWidth ? null : translate('receipt.dragReceiptAfterEmail')} - - - - - {({openPicker}) => ( -