From 6ffc1055ca26d026c7ec3a67ff91756a7a220293 Mon Sep 17 00:00:00 2001 From: Kacper Falat Date: Tue, 3 Oct 2023 11:10:34 +0200 Subject: [PATCH 001/475] 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/475] 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/475] 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 229a4331aeddc934748757b76b226361f93ce6dc Mon Sep 17 00:00:00 2001 From: Srikar Parsi Date: Thu, 2 Nov 2023 21:37:02 -0400 Subject: [PATCH 004/475] update subsribe on leave --- src/libs/actions/Report.js | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 1de15c1184cb..5ddb2355f1c9 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -2059,6 +2059,32 @@ function leaveRoom(reportID, isWorkspaceMemberLeavingWorkspaceRoom = false) { }, ]; + const failureData = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: report, + }, + ]; + + if (report.parentReportID && report.parentReportActionID) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`, + value: {[report.parentReportActionID]: {childReportNotificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN}}, + }); + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`, + value: {[report.parentReportActionID]: {childReportNotificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN}}, + }); + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`, + value: {[parentReportActionID]: {childReportNotificationPreference: report.notificationPreference}}, + }); + } + API.write( 'LeaveRoom', { @@ -2067,13 +2093,7 @@ function leaveRoom(reportID, isWorkspaceMemberLeavingWorkspaceRoom = false) { { optimisticData, successData, - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: report, - }, - ], + failureData, }, ); From 26cfe6452e4a2464fbfc8f91868204dab8dca28d Mon Sep 17 00:00:00 2001 From: Rodrigo Lino da Costa Date: Fri, 3 Nov 2023 16:45:34 +0000 Subject: [PATCH 005/475] Using visibleChatMemberList instead of participants ids --- src/libs/ReportUtils.js | 24 ++++++++++++++++++++++++ src/pages/ReportDetailsPage.js | 2 +- src/pages/ReportParticipantsPage.js | 2 +- src/pages/ShareCodePage.js | 2 +- src/pages/reportPropTypes.js | 3 +++ 5 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 1ab715b66aeb..8c93f8b778ff 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -4048,6 +4048,29 @@ function getParticipantsIDs(report) { return participants; } +/** + * Returns an array of the visible member Ids of a report + * + * @param {Object} report + * @returns {Array} + */ +function getVisibleMembersIDs(report) { + if (!report) { + return []; + } + + const visibleChatMembers = report.visibleChatMemberList || []; + + // Build visibleChatMembers list for IOU/expense reports + if (isMoneyRequestReport(report)) { + return _.chain([report.managerID, report.ownerAccountID, ...visibleChatMembers]) + .compact() + .uniq() + .value(); + } + return visibleChatMembers; +} + /** * Return iou report action display message * @@ -4280,6 +4303,7 @@ export { getTransactionDetails, getTaskAssigneeChatOnyxData, getParticipantsIDs, + getVisibleMembersIDs, canEditMoneyRequest, canEditFieldOfMoneyRequest, buildTransactionThread, diff --git a/src/pages/ReportDetailsPage.js b/src/pages/ReportDetailsPage.js index ef28102cc144..a0fe8d6a35b7 100644 --- a/src/pages/ReportDetailsPage.js +++ b/src/pages/ReportDetailsPage.js @@ -74,7 +74,7 @@ function ReportDetailsPage(props) { const chatRoomSubtitle = useMemo(() => ReportUtils.getChatRoomSubtitle(props.report), [props.report, policy]); const parentNavigationSubtitleData = ReportUtils.getParentNavigationSubtitle(props.report); const canLeaveRoom = useMemo(() => ReportUtils.canLeaveRoom(props.report, !_.isEmpty(policy)), [policy, props.report]); - const participants = useMemo(() => ReportUtils.getParticipantsIDs(props.report), [props.report]); + const participants = useMemo(() => ReportUtils.getVisibleMembersIDs(props.report), [props.report]); const isGroupDMChat = useMemo(() => ReportUtils.isDM(props.report) && participants.length > 1, [props.report, participants.length]); diff --git a/src/pages/ReportParticipantsPage.js b/src/pages/ReportParticipantsPage.js index c2179c53126b..9601375f8cc3 100755 --- a/src/pages/ReportParticipantsPage.js +++ b/src/pages/ReportParticipantsPage.js @@ -55,7 +55,7 @@ const defaultProps = { * @return {Array} */ const getAllParticipants = (report, personalDetails, translate) => - _.chain(ReportUtils.getParticipantsIDs(report)) + _.chain(ReportUtils.getVisibleMembersIDs(report)) .map((accountID, index) => { const userPersonalDetail = lodashGet(personalDetails, accountID, {displayName: personalDetails.displayName || translate('common.hidden'), avatar: ''}); const userLogin = LocalePhoneNumber.formatPhoneNumber(userPersonalDetail.login || '') || translate('common.hidden'); diff --git a/src/pages/ShareCodePage.js b/src/pages/ShareCodePage.js index b2bc32b381ce..64629cdfd956 100644 --- a/src/pages/ShareCodePage.js +++ b/src/pages/ShareCodePage.js @@ -54,7 +54,7 @@ class ShareCodePage extends React.Component { } if (ReportUtils.isMoneyRequestReport(this.props.report)) { // generate subtitle from participants - return _.map(ReportUtils.getParticipantsIDs(this.props.report), (accountID) => ReportUtils.getDisplayNameForParticipant(accountID)).join(' & '); + return _.map(ReportUtils.getVisibleMembersIDs(this.props.report), (accountID) => ReportUtils.getDisplayNameForParticipant(accountID)).join(' & '); } if (isReport) { diff --git a/src/pages/reportPropTypes.js b/src/pages/reportPropTypes.js index a3bbbda5c0bf..e0e03d26bbf6 100644 --- a/src/pages/reportPropTypes.js +++ b/src/pages/reportPropTypes.js @@ -50,6 +50,9 @@ export default PropTypes.shape({ /** List of accountIDs of participants of the report */ participantAccountIDs: PropTypes.arrayOf(PropTypes.number), + /** List of accountIDs of visible members of the report */ + visibleChatMemberList: PropTypes.arrayOf(PropTypes.number), + /** Linked policy's ID */ policyID: PropTypes.string, From a1021fd8719b2ea48b1b9f714b928bbe294bee31 Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 9 Nov 2023 00:57:26 +0700 Subject: [PATCH 006/475] 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 007/475] 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 008/475] 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 009/475] 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 010/475] 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 011/475] 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 012/475] 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 013/475] 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 014/475] 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 015/475] 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 016/475] 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 1e060e2bc927673cb8988e9da5e97bb9c7a728f4 Mon Sep 17 00:00:00 2001 From: Srikar Parsi Date: Thu, 9 Nov 2023 04:35:12 -0500 Subject: [PATCH 017/475] fix variable names --- src/libs/actions/Report.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 27396c875d34..638c83862cfc 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -2085,7 +2085,7 @@ function leaveRoom(reportID, isWorkspaceMemberLeavingWorkspaceRoom = false) { failureData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`, - value: {[parentReportActionID]: {childReportNotificationPreference: report.notificationPreference}}, + value: {[report.parentReportActionID]: {childReportNotificationPreference: report.notificationPreference}}, }); } From f781c0659694912ca247a86c5715404735c4272f Mon Sep 17 00:00:00 2001 From: Hans Date: Mon, 13 Nov 2023 14:07:22 +0700 Subject: [PATCH 018/475] 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 9f42a57a6e6e91f521195f173e0c3caa9ec28342 Mon Sep 17 00:00:00 2001 From: Rodrigo Lino da Costa Date: Mon, 13 Nov 2023 12:14:44 +0000 Subject: [PATCH 019/475] Removing getParticipantsIDs since it is not being used anymore. --- src/libs/ReportUtils.js | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index c700827b5eb7..0feb24542e79 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -4056,29 +4056,6 @@ function getTaskAssigneeChatOnyxData(accountID, assigneeAccountID, taskReportID, }; } -/** - * Returns an array of the participants Ids of a report - * - * @param {Object} report - * @returns {Array} - */ -function getParticipantsIDs(report) { - if (!report) { - return []; - } - - const participants = report.participantAccountIDs || []; - - // Build participants list for IOU/expense reports - if (isMoneyRequestReport(report)) { - return _.chain([report.managerID, report.ownerAccountID, ...participants]) - .compact() - .uniq() - .value(); - } - return participants; -} - /** * Returns an array of the visible member Ids of a report * @@ -4345,7 +4322,6 @@ export { getTransactionReportName, getTransactionDetails, getTaskAssigneeChatOnyxData, - getParticipantsIDs, getVisibleMembersIDs, canEditMoneyRequest, canEditFieldOfMoneyRequest, From 5de34fba8a874c66abed081a6e94509d1bb2d3bf Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 14 Nov 2023 17:50:11 +0500 Subject: [PATCH 020/475] 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 021/475] 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 022/475] 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 023/475] 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 024/475] 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 025/475] 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 026/475] 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 027/475] 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 028/475] 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 029/475] 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 030/475] 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 031/475] 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 032/475] 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 b493348bb12dfb9813a97cb986acded6770cacf7 Mon Sep 17 00:00:00 2001 From: Rodrigo Lino da Costa Date: Sat, 18 Nov 2023 16:02:10 +0000 Subject: [PATCH 033/475] renaming visibleChatMembers variable --- src/libs/ReportUtils.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index dfcb97ab1469..18de01af288a 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -4107,16 +4107,16 @@ function getVisibleMembersIDs(report) { return []; } - const visibleChatMembers = report.visibleChatMemberList || []; + const visibleChatMembersIDs = report.visibleChatMemberList || []; // Build visibleChatMembers list for IOU/expense reports if (isMoneyRequestReport(report)) { - return _.chain([report.managerID, report.ownerAccountID, ...visibleChatMembers]) + return _.chain([report.managerID, report.ownerAccountID, ...visibleChatMembersIDs]) .compact() .uniq() .value(); } - return visibleChatMembers; + return visibleChatMembersIDs; } /** From 004a2225c4c600a8ce1eb69c1faf57b3bbda70e8 Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Mon, 20 Nov 2023 13:02:41 +0100 Subject: [PATCH 034/475] Remove unnecessary --- src/libs/updatePropsPaperWorklet/index.js | 3 --- src/libs/updatePropsPaperWorklet/index.native.js | 13 ------------- .../ReportActionCompose/ReportActionCompose.js | 9 +++------ 3 files changed, 3 insertions(+), 22 deletions(-) delete mode 100644 src/libs/updatePropsPaperWorklet/index.js delete mode 100644 src/libs/updatePropsPaperWorklet/index.native.js diff --git a/src/libs/updatePropsPaperWorklet/index.js b/src/libs/updatePropsPaperWorklet/index.js deleted file mode 100644 index 1bca6ea13cdc..000000000000 --- a/src/libs/updatePropsPaperWorklet/index.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function () { - 'worklet'; -} diff --git a/src/libs/updatePropsPaperWorklet/index.native.js b/src/libs/updatePropsPaperWorklet/index.native.js deleted file mode 100644 index ed79b38ffab5..000000000000 --- a/src/libs/updatePropsPaperWorklet/index.native.js +++ /dev/null @@ -1,13 +0,0 @@ -export default function (viewTag, viewName, updates) { - 'worklet'; - - // _updatePropsPaper is a function that is worklet function from react-native-reanimated which is not available on web - // eslint-disable-next-line no-undef - _updatePropsPaper([ - { - tag: viewTag, - name: viewName, - updates, - }, - ]); -} diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js index 7bce37dc3826..adab1b007843 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import {runOnJS, useAnimatedRef} from 'react-native-reanimated'; +import {runOnJS, setNativeProps, useAnimatedRef} from 'react-native-reanimated'; import _ from 'underscore'; import AttachmentModal from '@components/AttachmentModal'; import EmojiPickerButton from '@components/EmojiPicker/EmojiPickerButton'; @@ -22,7 +22,6 @@ import getDraftComment from '@libs/ComposerUtils/getDraftComment'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import getModalState from '@libs/getModalState'; import * as ReportUtils from '@libs/ReportUtils'; -import updatePropsPaperWorklet from '@libs/updatePropsPaperWorklet'; import willBlurTextInputOnTapOutsideFunc from '@libs/willBlurTextInputOnTapOutside'; import ParticipantLocalTime from '@pages/home/report/ParticipantLocalTime'; import reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; @@ -332,13 +331,11 @@ function ReportActionCompose({ return; } - const viewTag = animatedRef(); - const viewName = 'RCTMultilineTextInputView'; - const updates = {text: ''}; // We are setting the isCommentEmpty flag to true so the status of it will be in sync of the native text input state runOnJS(setIsCommentEmpty)(true); runOnJS(resetFullComposerSize)(); - updatePropsPaperWorklet(viewTag, viewName, updates); // clears native text input on the UI thread + setNativeProps(animatedRef, {text: ''}); + // updatePropsPaperWorklet(viewTag, viewName, updates); // clears native text input on the UI thread runOnJS(submitForm)(); }, [isSendDisabled, resetFullComposerSize, submitForm, animatedRef, isReportReadyForDisplay]); From e4135d11d975a2529da9c2ef39b8c5386058c74d Mon Sep 17 00:00:00 2001 From: Daniel Edwards Date: Tue, 21 Nov 2023 11:23:44 -0500 Subject: [PATCH 035/475] Possible solution to not passing down tags and categories --- src/libs/ReportUtils.js | 6 +++ src/libs/actions/IOU.js | 49 ++++++++++++++++++- .../iou/steps/MoneyRequestConfirmPage.js | 28 +++++++++++ 3 files changed, 82 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 673cb09232de..ebbc88278b03 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -100,6 +100,10 @@ function getPolicyTags(policyID) { return lodashGet(allPolicyTags, `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {}); } +function getPolicyCategories(policyID) { + return lodashGet(allPolicyTags, `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, []); +} + function getChatType(report) { return report ? report.chatType : ''; } @@ -4295,6 +4299,8 @@ export { getParentNavigationSubtitle, getPolicyName, getPolicyType, + getPolicyCategories, + getPolicyTags, isArchivedRoom, isExpensifyOnlyParticipantInReport, canCreateTaskInReport, diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 1f1cee166a0e..cccde7259cd6 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -19,6 +19,7 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import * as UserUtils from '@libs/UserUtils'; +import ViolationsUtils from '@libs/ViolationsUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -154,6 +155,7 @@ function buildOnyxDataForMoneyRequest( optimisticPolicyRecentlyUsedTags, isNewChatReport, isNewIOUReport, + policyID, ) { const optimisticData = [ { @@ -380,6 +382,28 @@ function buildOnyxDataForMoneyRequest( }, ]; + if (!policyID) { + return [optimisticData, successData, failureData]; + } + const policy = ReportUtils.getPolicy(policyID); + const policyTags = ReportUtils.getPolicyTags(policyID); + const policyCategories = ReportUtils.getPolicyCategories(policyID); + + const violationsOnyxData = ViolationsUtils.getViolationsOnyxData(transaction, [], policy.requiresTags, policyTags, policy.requiresCategory, policyCategories); + + if (violationsOnyxData) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`, + value: violationsOnyxData, + }); + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`, + value: [], + }); + } + return [optimisticData, successData, failureData]; } @@ -401,6 +425,7 @@ function buildOnyxDataForMoneyRequest( * @param {String} [category] * @param {String} [tag] * @param {Boolean} [billable] + * @param {String} [policyID] * @returns {Object} data * @returns {String} data.payerEmail * @returns {Object} data.iouReport @@ -430,6 +455,7 @@ function getMoneyRequestInformation( category = undefined, tag = undefined, billable = undefined, + policyID = undefined, ) { const payerEmail = OptionsListUtils.addSMSDomainIfPhoneNumber(participant.login); const payerAccountID = Number(participant.accountID); @@ -592,6 +618,7 @@ function getMoneyRequestInformation( optimisticPolicyRecentlyUsedTags, isNewChatReport, isNewIOUReport, + policyID, ); return { @@ -839,6 +866,9 @@ function updateDistanceRequest(transactionID, transactionThreadReportID, transac * @param {String} [category] * @param {String} [tag] * @param {Boolean} [billable] + * @param {String} [policyID] + * @param {Object} [policyTags] + * @param {Object} [policyCategories] */ function requestMoney( report, @@ -854,12 +884,29 @@ function requestMoney( category = undefined, tag = undefined, billable = undefined, + policyID = undefined, ) { // If the report is iou or expense report, we should get the linked chat report to be passed to the getMoneyRequestInformation function const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); const currentChatReport = isMoneyRequestReport ? ReportUtils.getReport(report.chatReportID) : report; const {payerAccountID, payerEmail, iouReport, chatReport, transaction, iouAction, createdChatReportActionID, createdIOUReportActionID, reportPreviewAction, onyxData} = - getMoneyRequestInformation(currentChatReport, participant, comment, amount, currency, created, merchant, payeeAccountID, payeeEmail, receipt, undefined, category, tag, billable); + getMoneyRequestInformation( + currentChatReport, + participant, + comment, + amount, + currency, + created, + merchant, + payeeAccountID, + payeeEmail, + receipt, + undefined, + category, + tag, + billable, + policyID, + ); API.write( 'RequestMoney', diff --git a/src/pages/iou/steps/MoneyRequestConfirmPage.js b/src/pages/iou/steps/MoneyRequestConfirmPage.js index ebb687b324e8..0d3810bb34d2 100644 --- a/src/pages/iou/steps/MoneyRequestConfirmPage.js +++ b/src/pages/iou/steps/MoneyRequestConfirmPage.js @@ -50,12 +50,29 @@ const propTypes = { /** Personal details of all users */ personalDetails: personalDetailsPropType, + /** The policy of the current report */ + policy: PropTypes.shape({ + /** Whether the policy requires a tag */ + requiresTag: PropTypes.bool, + + /** Whether the policy requires a category */ + requiresCategory: PropTypes.bool, + + /** Whether there is more than one list of tags */ + hasMultipleTagLists: PropTypes.bool, + + /** Whether the policy has enable tax tracking */ + isTrackingTaxEnabled: PropTypes.bool, + }), + ...withCurrentUserPersonalDetailsPropTypes, }; const defaultProps = { report: {}, personalDetails: {}, + policyCategories: {}, + policyTags: {}, iou: iouDefaultProps, ...withCurrentUserPersonalDetailsDefaultProps, }; @@ -173,6 +190,7 @@ function MoneyRequestConfirmPage(props) { props.iou.category, props.iou.tag, props.iou.billable, + props.policy.id, ); }, [ @@ -186,6 +204,7 @@ function MoneyRequestConfirmPage(props) { props.iou.category, props.iou.tag, props.iou.billable, + props.policy.id, ], ); @@ -430,6 +449,15 @@ export default compose( selectedTab: { key: `${ONYXKEYS.COLLECTION.SELECTED_TAB}${CONST.TAB.RECEIPT_TAB_ID}`, }, + policy: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`, + }, + policyCategories: { + key: ONYXKEYS.POLICY_CATEGORIES, + }, + policyTags: { + key: ONYXKEYS.POLICY_TAGS, + }, }), // eslint-disable-next-line rulesdir/no-multiple-onyx-in-file withOnyx({ From 6ea03da8d0bdc10dce31b8e4caef7a8682170f16 Mon Sep 17 00:00:00 2001 From: Rodrigo Lino da Costa <5201282+rlinoz@users.noreply.github.com> Date: Wed, 22 Nov 2023 10:10:08 -0300 Subject: [PATCH 036/475] more explicit comment Co-authored-by: Marc Glasser --- src/libs/ReportUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 18de01af288a..c616f2935c11 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -4097,7 +4097,7 @@ function getTaskAssigneeChatOnyxData(accountID, assigneeAccountID, taskReportID, } /** - * Returns an array of the visible member Ids of a report + * Returns an array of the visible member accountIDs for a report * * @param {Object} report * @returns {Array} From 0c92a82b896fe53541ab8ba167d71440166f11e6 Mon Sep 17 00:00:00 2001 From: Rodrigo Lino da Costa Date: Wed, 22 Nov 2023 11:40:56 -0300 Subject: [PATCH 037/475] renaming visibleChatMemberList --- src/libs/ReportUtils.js | 10 +++++----- src/pages/ReportDetailsPage.js | 2 +- src/pages/ReportParticipantsPage.js | 2 +- src/pages/ShareCodePage.js | 2 +- src/pages/reportPropTypes.js | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index bd6a5b94c8a3..dd227dc43ab5 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -4103,21 +4103,21 @@ function getTaskAssigneeChatOnyxData(accountID, assigneeAccountID, taskReportID, * @param {Object} report * @returns {Array} */ -function getVisibleMembersIDs(report) { +function getVisibleMemberIDs(report) { if (!report) { return []; } - const visibleChatMembersIDs = report.visibleChatMemberList || []; + const visibleChatMemberAccountIDs = report.visibleChatMemberAccountIDs || []; // Build visibleChatMembers list for IOU/expense reports if (isMoneyRequestReport(report)) { - return _.chain([report.managerID, report.ownerAccountID, ...visibleChatMembersIDs]) + return _.chain([report.managerID, report.ownerAccountID, ...visibleChatMemberAccountIDs]) .compact() .uniq() .value(); } - return visibleChatMembersIDs; + return visibleChatMemberAccountIDs; } /** @@ -4418,7 +4418,7 @@ export { getTransactionReportName, getTransactionDetails, getTaskAssigneeChatOnyxData, - getVisibleMembersIDs, + getVisibleMemberIDs, canEditMoneyRequest, canEditFieldOfMoneyRequest, buildTransactionThread, diff --git a/src/pages/ReportDetailsPage.js b/src/pages/ReportDetailsPage.js index 9b0c5f9a08e9..284f32a88aad 100644 --- a/src/pages/ReportDetailsPage.js +++ b/src/pages/ReportDetailsPage.js @@ -73,7 +73,7 @@ function ReportDetailsPage(props) { // eslint-disable-next-line react-hooks/exhaustive-deps -- policy is a dependency because `getChatRoomSubtitle` calls `getPolicyName` which in turn retrieves the value from the `policy` value stored in Onyx const chatRoomSubtitle = useMemo(() => ReportUtils.getChatRoomSubtitle(props.report), [props.report, policy]); const parentNavigationSubtitleData = ReportUtils.getParentNavigationSubtitle(props.report); - const participants = useMemo(() => ReportUtils.getVisibleMembersIDs(props.report), [props.report]); + const participants = useMemo(() => ReportUtils.getVisibleMemberIDs(props.report), [props.report]); const isGroupDMChat = useMemo(() => ReportUtils.isDM(props.report) && participants.length > 1, [props.report, participants.length]); diff --git a/src/pages/ReportParticipantsPage.js b/src/pages/ReportParticipantsPage.js index ec40783fc394..aa49140020a9 100755 --- a/src/pages/ReportParticipantsPage.js +++ b/src/pages/ReportParticipantsPage.js @@ -55,7 +55,7 @@ const defaultProps = { * @return {Array} */ const getAllParticipants = (report, personalDetails, translate) => - _.chain(ReportUtils.getVisibleMembersIDs(report)) + _.chain(ReportUtils.getVisibleMemberIDs(report)) .map((accountID, index) => { const userPersonalDetail = lodashGet(personalDetails, accountID, {displayName: personalDetails.displayName || translate('common.hidden'), avatar: ''}); const userLogin = LocalePhoneNumber.formatPhoneNumber(userPersonalDetail.login || '') || translate('common.hidden'); diff --git a/src/pages/ShareCodePage.js b/src/pages/ShareCodePage.js index 7039e524f1a6..fee70b464426 100644 --- a/src/pages/ShareCodePage.js +++ b/src/pages/ShareCodePage.js @@ -55,7 +55,7 @@ class ShareCodePage extends React.Component { } if (ReportUtils.isMoneyRequestReport(this.props.report)) { // generate subtitle from participants - return _.map(ReportUtils.getVisibleMembersIDs(this.props.report), (accountID) => ReportUtils.getDisplayNameForParticipant(accountID)).join(' & '); + return _.map(ReportUtils.getVisibleMemberIDs(this.props.report), (accountID) => ReportUtils.getDisplayNameForParticipant(accountID)).join(' & '); } if (isReport) { diff --git a/src/pages/reportPropTypes.js b/src/pages/reportPropTypes.js index e0e03d26bbf6..7a0b588af474 100644 --- a/src/pages/reportPropTypes.js +++ b/src/pages/reportPropTypes.js @@ -51,7 +51,7 @@ export default PropTypes.shape({ participantAccountIDs: PropTypes.arrayOf(PropTypes.number), /** List of accountIDs of visible members of the report */ - visibleChatMemberList: PropTypes.arrayOf(PropTypes.number), + visibleChatMemberAccountIDs: PropTypes.arrayOf(PropTypes.number), /** Linked policy's ID */ policyID: PropTypes.string, From 3abd2963dd40497d8dc477068295df5edceb2e47 Mon Sep 17 00:00:00 2001 From: Rodrigo Lino da Costa Date: Wed, 22 Nov 2023 14:58:13 -0300 Subject: [PATCH 038/475] adding getParticipantsIDs back since it might break forks/branches, and deprecating it --- src/libs/ReportUtils.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index dd227dc43ab5..1bef28cbc6ba 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -4097,6 +4097,31 @@ function getTaskAssigneeChatOnyxData(accountID, assigneeAccountID, taskReportID, }; } +/** + * Returns an array of the participants Ids of a report + * + * @param {Object} report + * @returns {Array} + * + * @deprecated Use getVisibleMemberIDs instead + */ +function getParticipantsIDs(report) { + if (!report) { + return []; + } + + const participants = report.participantAccountIDs || []; + + // Build participants list for IOU/expense reports + if (isMoneyRequestReport(report)) { + return _.chain([report.managerID, report.ownerAccountID, ...participants]) + .compact() + .uniq() + .value(); + } + return participants; +} + /** * Returns an array of the visible member accountIDs for a report * @@ -4418,6 +4443,7 @@ export { getTransactionReportName, getTransactionDetails, getTaskAssigneeChatOnyxData, + getParticipantsIDs, getVisibleMemberIDs, canEditMoneyRequest, canEditFieldOfMoneyRequest, From 6d5966f43a61ce45111e68a0ca6843401b679d96 Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Thu, 23 Nov 2023 13:05:38 +0100 Subject: [PATCH 039/475] remove unnecessary commented code --- .../home/report/ReportActionCompose/ReportActionCompose.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js index adab1b007843..670ee07a5bcb 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js @@ -334,8 +334,7 @@ function ReportActionCompose({ // We are setting the isCommentEmpty flag to true so the status of it will be in sync of the native text input state runOnJS(setIsCommentEmpty)(true); runOnJS(resetFullComposerSize)(); - setNativeProps(animatedRef, {text: ''}); - // updatePropsPaperWorklet(viewTag, viewName, updates); // clears native text input on the UI thread + setNativeProps(animatedRef, {text: ''}); // clears native text input on the UI thread runOnJS(submitForm)(); }, [isSendDisabled, resetFullComposerSize, submitForm, animatedRef, isReportReadyForDisplay]); 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 040/475] 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 041/475] 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 042/475] 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 043/475] 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 8fe667dcce7a3ef6500ea4d553b442ff700469a9 Mon Sep 17 00:00:00 2001 From: Daniel Edwards Date: Tue, 28 Nov 2023 09:34:24 -0500 Subject: [PATCH 044/475] Updated withPolicy with new properties and also passed through to IOU functions --- src/libs/actions/IOU.js | 60 +++++++++++-------- .../iou/steps/MoneyRequestConfirmPage.js | 28 +++++---- src/pages/workspace/withPolicy.tsx | 14 +++++ 3 files changed, 65 insertions(+), 37 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index cccde7259cd6..7469a4e57392 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -19,7 +19,7 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import * as UserUtils from '@libs/UserUtils'; -import ViolationsUtils from '@libs/ViolationsUtils'; +import ViolationsUtils from '@libs/Violations/ViolationsUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -78,19 +78,19 @@ Onyx.connect({ }, }); -let allPolicyTags = {}; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.POLICY_TAGS, - waitForCollectionCallback: true, - callback: (value) => { - if (!value) { - allPolicyTags = {}; - return; - } +// let allPolicyTags = {}; +// Onyx.connect({ +// key: ONYXKEYS.COLLECTION.POLICY_TAGS, +// waitForCollectionCallback: true, +// callback: (value) => { +// if (!value) { +// allPolicyTags = {}; +// return; +// } - allPolicyTags = value; - }, -}); +// allPolicyTags = value; +// }, +// }); let userAccountID = ''; let currentUserEmail = ''; @@ -155,7 +155,9 @@ function buildOnyxDataForMoneyRequest( optimisticPolicyRecentlyUsedTags, isNewChatReport, isNewIOUReport, - policyID, + policy, + policyTags, + policyCategories, ) { const optimisticData = [ { @@ -382,12 +384,9 @@ function buildOnyxDataForMoneyRequest( }, ]; - if (!policyID) { + if (!policy.id) { return [optimisticData, successData, failureData]; } - const policy = ReportUtils.getPolicy(policyID); - const policyTags = ReportUtils.getPolicyTags(policyID); - const policyCategories = ReportUtils.getPolicyCategories(policyID); const violationsOnyxData = ViolationsUtils.getViolationsOnyxData(transaction, [], policy.requiresTags, policyTags, policy.requiresCategory, policyCategories); @@ -425,7 +424,9 @@ function buildOnyxDataForMoneyRequest( * @param {String} [category] * @param {String} [tag] * @param {Boolean} [billable] - * @param {String} [policyID] + * @param {Object} [policy] + * @param {Object} [policyTags] + * @param {Object} [policyCategories] * @returns {Object} data * @returns {String} data.payerEmail * @returns {Object} data.iouReport @@ -455,7 +456,9 @@ function getMoneyRequestInformation( category = undefined, tag = undefined, billable = undefined, - policyID = undefined, + policy = undefined, + policyTags = undefined, + policyCategories = undefined, ) { const payerEmail = OptionsListUtils.addSMSDomainIfPhoneNumber(participant.login); const payerAccountID = Number(participant.accountID); @@ -531,7 +534,8 @@ function getMoneyRequestInformation( } const optimisticPolicyRecentlyUsedTags = {}; - const policyTags = allPolicyTags[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${iouReport.policyID}`]; + // TODO: Remove the following line once everything is tested + // const policyTags = allPolicyTags[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${iouReport.policyID}`]; const recentlyUsedPolicyTags = allRecentlyUsedTags[`${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS}${iouReport.policyID}`]; if (policyTags) { @@ -618,7 +622,9 @@ function getMoneyRequestInformation( optimisticPolicyRecentlyUsedTags, isNewChatReport, isNewIOUReport, - policyID, + policy, + policyTags, + policyCategories, ); return { @@ -866,7 +872,7 @@ function updateDistanceRequest(transactionID, transactionThreadReportID, transac * @param {String} [category] * @param {String} [tag] * @param {Boolean} [billable] - * @param {String} [policyID] + * @param {Object} [policy] * @param {Object} [policyTags] * @param {Object} [policyCategories] */ @@ -884,7 +890,9 @@ function requestMoney( category = undefined, tag = undefined, billable = undefined, - policyID = undefined, + policy = undefined, + policyTags = undefined, + policyCategories = undefined, ) { // If the report is iou or expense report, we should get the linked chat report to be passed to the getMoneyRequestInformation function const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); @@ -905,7 +913,9 @@ function requestMoney( category, tag, billable, - policyID, + policy, + policyTags, + policyCategories, ); API.write( diff --git a/src/pages/iou/steps/MoneyRequestConfirmPage.js b/src/pages/iou/steps/MoneyRequestConfirmPage.js index 0d3810bb34d2..4ccd47166634 100644 --- a/src/pages/iou/steps/MoneyRequestConfirmPage.js +++ b/src/pages/iou/steps/MoneyRequestConfirmPage.js @@ -22,6 +22,7 @@ import * as ReportUtils from '@libs/ReportUtils'; import {iouDefaultProps, iouPropTypes} from '@pages/iou/propTypes'; import personalDetailsPropType from '@pages/personalDetailsPropType'; import reportPropTypes from '@pages/reportPropTypes'; +import {policyDefaultProps, policyPropTypes} from '@pages/workspace/withPolicy'; import useThemeStyles from '@styles/useThemeStyles'; import * as IOU from '@userActions/IOU'; import * as Policy from '@userActions/Policy'; @@ -51,18 +52,16 @@ const propTypes = { personalDetails: personalDetailsPropType, /** The policy of the current report */ - policy: PropTypes.shape({ - /** Whether the policy requires a tag */ - requiresTag: PropTypes.bool, + policy: policyPropTypes, - /** Whether the policy requires a category */ - requiresCategory: PropTypes.bool, - - /** Whether there is more than one list of tags */ - hasMultipleTagLists: PropTypes.bool, + policyTags: PropTypes.shape({ + /** List of tags */ + tags: PropTypes.arrayOf(PropTypes.string), + }), - /** Whether the policy has enable tax tracking */ - isTrackingTaxEnabled: PropTypes.bool, + policyCategories: PropTypes.shape({ + /** List of categories */ + categories: PropTypes.arrayOf(PropTypes.string), }), ...withCurrentUserPersonalDetailsPropTypes, @@ -74,6 +73,7 @@ const defaultProps = { policyCategories: {}, policyTags: {}, iou: iouDefaultProps, + policy: policyDefaultProps, ...withCurrentUserPersonalDetailsDefaultProps, }; @@ -190,7 +190,9 @@ function MoneyRequestConfirmPage(props) { props.iou.category, props.iou.tag, props.iou.billable, - props.policy.id, + props.policy, + props.policyTags, + props.policyCategories, ); }, [ @@ -204,7 +206,9 @@ function MoneyRequestConfirmPage(props) { props.iou.category, props.iou.tag, props.iou.billable, - props.policy.id, + props.policy, + props.policyTags, + props.policyCategories, ], ); diff --git a/src/pages/workspace/withPolicy.tsx b/src/pages/workspace/withPolicy.tsx index 8db093ec24d0..96b89825fb6f 100644 --- a/src/pages/workspace/withPolicy.tsx +++ b/src/pages/workspace/withPolicy.tsx @@ -51,6 +51,20 @@ const policyPropTypes = { * } */ errorFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), + + /** Whether or not the policy requires tags */ + requiresTags: PropTypes.bool, + + /** Whether or not the policy requires categories */ + requiresCategories: PropTypes.bool, + + /** Whether or not the policy has multiple tag lists */ + hasMultipleTagLists: PropTypes.bool, + + /** Whether or not the policy has tax tracking enabled */ + isTrackingTaxEnabled: PropTypes.bool, + + /** */ }), /** The employee list of this policy */ From 1cd0e08aac80489901ddc5d177e4b83266becb25 Mon Sep 17 00:00:00 2001 From: Daniel Edwards Date: Tue, 28 Nov 2023 10:10:56 -0500 Subject: [PATCH 045/475] Added some logs for testing -- remove these --- src/libs/Violations/ViolationsUtils.ts | 2 ++ src/libs/actions/IOU.js | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts index 28f5aedf10b9..b5b31d789211 100644 --- a/src/libs/Violations/ViolationsUtils.ts +++ b/src/libs/Violations/ViolationsUtils.ts @@ -58,6 +58,8 @@ const ViolationsUtils = { } } + // TODO: Remove the following line once everything is tested + console.log('ViolationsUtils.getViolationsOnyxData', newTransactionViolations); return { onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`, diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 7469a4e57392..95d8725b39f8 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -388,8 +388,12 @@ function buildOnyxDataForMoneyRequest( return [optimisticData, successData, failureData]; } + // TODO: Remove the following line once everything is tested + console.log('POLICY: ', policy); const violationsOnyxData = ViolationsUtils.getViolationsOnyxData(transaction, [], policy.requiresTags, policyTags, policy.requiresCategory, policyCategories); + // TODO: Remove the following line once everything is tested + console.log('ONYXDATA', violationsOnyxData); if (violationsOnyxData) { optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, From e951bb7747210447028f350e5935f0bedb6e83e6 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Tue, 28 Nov 2023 17:24:39 +0100 Subject: [PATCH 046/475] [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 047/475] 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 048/475] [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 049/475] [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 050/475] 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 4a7cc0b8005c2bf4b4d1d531fe4c2bd1beffda72 Mon Sep 17 00:00:00 2001 From: Rodrigo Lino da Costa Date: Tue, 28 Nov 2023 14:28:00 -0300 Subject: [PATCH 051/475] fix lint --- src/libs/ReportUtils.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 0cf872ab7339..225e67bbaacf 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4087,17 +4087,14 @@ function getParticipantsIDs(report: OnyxEntry): number[] { } /** - * Returns an array of the visible member accountIDs for a report - * - * @param {Object} report - * @returns {Array} + * Returns an array of the visible member accountIDs for a report* */ function getVisibleMemberIDs(report: OnyxEntry): number[] { if (!report) { return []; } - const visibleChatMemberAccountIDs = report.visibleChatMemberAccountIDs || []; + const visibleChatMemberAccountIDs = report.visibleChatMemberAccountIDs ?? []; // Build participants list for IOU/expense reports if (isMoneyRequestReport(report)) { From 2b59ded20f16607387c43b37bfa85230c6f04e31 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 29 Nov 2023 09:25:51 +0100 Subject: [PATCH 052/475] [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 053/475] 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 054/475] [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 055/475] [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 056/475] [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 057/475] 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 329f4bed262b00671b58ec92479ed4de2896cb31 Mon Sep 17 00:00:00 2001 From: Rodrigo Lino da Costa Date: Wed, 29 Nov 2023 09:09:25 -0300 Subject: [PATCH 058/475] prettier --- src/libs/ReportUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 225e67bbaacf..4c508bb9798e 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4105,7 +4105,6 @@ function getVisibleMemberIDs(report: OnyxEntry): number[] { return visibleChatMemberAccountIDs; } - /** * Return iou report action display message */ From 00cad5e62b9e96fb894c5b40a8c1da27e023aee6 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 29 Nov 2023 15:09:22 +0100 Subject: [PATCH 059/475] 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 060/475] 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 061/475] 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 062/475] 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 063/475] 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 064/475] 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 6f84a871c1fd32d2b198270e824de611e36fa1c7 Mon Sep 17 00:00:00 2001 From: Daniel Edwards Date: Tue, 5 Dec 2023 11:57:59 -0500 Subject: [PATCH 065/475] remove unused export --- src/libs/ReportUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index dae93fd2c3c6..ec4e477a67de 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4279,7 +4279,6 @@ export { getParentNavigationSubtitle, getPolicyName, getPolicyType, - getPolicyCategories, getPolicyTags, isArchivedRoom, isExpensifyOnlyParticipantInReport, From f6f9cac5d14d5c79ca96faa9fc0d7ed2b5f0b8ca Mon Sep 17 00:00:00 2001 From: Daniel Edwards Date: Tue, 5 Dec 2023 16:08:19 -0500 Subject: [PATCH 066/475] Remove console.log statements and fix import path in IOU.js --- src/libs/ViolationsUtils.ts | 2 -- src/libs/actions/IOU.js | 8 ++------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/libs/ViolationsUtils.ts b/src/libs/ViolationsUtils.ts index bb834d5fcb1c..4cfa259c9e78 100644 --- a/src/libs/ViolationsUtils.ts +++ b/src/libs/ViolationsUtils.ts @@ -74,8 +74,6 @@ const ViolationsUtils = { } } - // TODO: Remove the following line once everything is tested - console.log('ViolationsUtils.getViolationsOnyxData', newTransactionViolations); return { onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`, diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index e06b863bb6b0..4a6e191e5fc9 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -19,7 +19,7 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import * as UserUtils from '@libs/UserUtils'; -import ViolationsUtils from '@libs/Violations/ViolationsUtils'; +import ViolationsUtils from '@libs/ViolationsUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -356,16 +356,12 @@ function buildOnyxDataForMoneyRequest( }, ]; - if (!policy.id) { + if (!policy || !policy.id) { return [optimisticData, successData, failureData]; } - // TODO: Remove the following line once everything is tested - console.log('POLICY: ', policy); const violationsOnyxData = ViolationsUtils.getViolationsOnyxData(transaction, [], policy.requiresTags, policyTags, policy.requiresCategory, policyCategories); - // TODO: Remove the following line once everything is tested - console.log('ONYXDATA', violationsOnyxData); if (violationsOnyxData) { optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, From 9a041ccb538c50aee24cc51ff578cc51c7cb232c Mon Sep 17 00:00:00 2001 From: Daniel Edwards Date: Tue, 5 Dec 2023 16:51:44 -0500 Subject: [PATCH 067/475] Remove unused function getPolicyTags --- src/libs/ReportUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 6ce2e68f3dbf..c444187cbd10 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4287,7 +4287,6 @@ export { getParentNavigationSubtitle, getPolicyName, getPolicyType, - getPolicyTags, isArchivedRoom, isExpensifyOnlyParticipantInReport, canCreateTaskInReport, From abb5fc6ec20cc047e0c31618b478e4174e7f8901 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Wed, 6 Dec 2023 17:06:50 +0500 Subject: [PATCH 068/475] 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 069/475] 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 150487e5453c7330abd0ebd9b65e49f4958bf857 Mon Sep 17 00:00:00 2001 From: Daniel Edwards Date: Wed, 6 Dec 2023 12:31:47 -0500 Subject: [PATCH 070/475] Fixed withOnyx call --- src/pages/iou/steps/MoneyRequestConfirmPage.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/pages/iou/steps/MoneyRequestConfirmPage.js b/src/pages/iou/steps/MoneyRequestConfirmPage.js index c34f4191c958..7bc865b6e667 100644 --- a/src/pages/iou/steps/MoneyRequestConfirmPage.js +++ b/src/pages/iou/steps/MoneyRequestConfirmPage.js @@ -450,16 +450,10 @@ export default compose( key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`, }, policyCategories: { - key: ONYXKEYS.POLICY_CATEGORIES, + key: ({policy}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policy ? policy.id : '0'}`, }, policyTags: { - key: ONYXKEYS.POLICY_TAGS, - }, - }), - // eslint-disable-next-line rulesdir/no-multiple-onyx-in-file - withOnyx({ - policy: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`, + key: ({policy}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policy ? policy.id : '0'}`, }, }), )(MoneyRequestConfirmPage); From 6f95d5b246c90b27b2ad16bb7bcb2c52208e7d45 Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Thu, 7 Dec 2023 14:39:44 +0100 Subject: [PATCH 071/475] add Educational Interstitial as popup and RHP --- .../simple-illustration__commentbubbles.svg | 22 ++++ .../simple-illustration__hourglass.svg | 56 +++++++++ .../simple-illustration__trashcan.svg | 52 +++++++++ src/ROUTES.ts | 1 + src/components/Icon/Illustrations.ts | 6 + src/components/ProcessMoneyRequestHoldMenu.js | 106 ++++++++++++++++++ src/languages/en.ts | 8 ++ .../AppNavigator/ModalStackNavigators.js | 5 + .../Navigators/RightModalNavigator.js | 4 + src/libs/Navigation/linkingConfig.js | 5 + src/pages/ProcessMoneyRequestHoldPage.js | 94 ++++++++++++++++ src/styles/styles.ts | 15 +++ src/styles/variables.ts | 1 + 13 files changed, 375 insertions(+) create mode 100644 assets/images/simple-illustrations/simple-illustration__commentbubbles.svg create mode 100644 assets/images/simple-illustrations/simple-illustration__hourglass.svg create mode 100644 assets/images/simple-illustrations/simple-illustration__trashcan.svg create mode 100644 src/components/ProcessMoneyRequestHoldMenu.js create mode 100644 src/pages/ProcessMoneyRequestHoldPage.js diff --git a/assets/images/simple-illustrations/simple-illustration__commentbubbles.svg b/assets/images/simple-illustrations/simple-illustration__commentbubbles.svg new file mode 100644 index 000000000000..829d3ee2e3fe --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__commentbubbles.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__hourglass.svg b/assets/images/simple-illustrations/simple-illustration__hourglass.svg new file mode 100644 index 000000000000..539e1e45b795 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__hourglass.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__trashcan.svg b/assets/images/simple-illustrations/simple-illustration__trashcan.svg new file mode 100644 index 000000000000..4e66efa0a67e --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__trashcan.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 26589a3db0e0..a4ce8e46df6c 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -379,6 +379,7 @@ export default { route: 'referral/:contentType', getRoute: (contentType: string) => `referral/${contentType}`, }, + PROCESS_MONEY_REQUEST_HOLD: 'hold-request-educational', // These are some one-off routes that will be removed once they're no longer needed (see GH issues for details) SAASTR: 'saastr', diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts index 440350895826..6b486c7407b4 100644 --- a/src/components/Icon/Illustrations.ts +++ b/src/components/Icon/Illustrations.ts @@ -30,11 +30,13 @@ import BankArrow from '@assets/images/simple-illustrations/simple-illustration__ import PinkBill from '@assets/images/simple-illustrations/simple-illustration__bill.svg'; import ChatBubbles from '@assets/images/simple-illustrations/simple-illustration__chatbubbles.svg'; import CoffeeMug from '@assets/images/simple-illustrations/simple-illustration__coffeemug.svg'; +import CommentBubbles from '@assets/images/simple-illustrations/simple-illustration__commentbubbles.svg'; import ConciergeBubble from '@assets/images/simple-illustrations/simple-illustration__concierge-bubble.svg'; import ConciergeNew from '@assets/images/simple-illustrations/simple-illustration__concierge.svg'; import CreditCardsNew from '@assets/images/simple-illustrations/simple-illustration__credit-cards.svg'; import EmailAddress from '@assets/images/simple-illustrations/simple-illustration__email-address.svg'; import HandEarth from '@assets/images/simple-illustrations/simple-illustration__handearth.svg'; +import Hourglass from '@assets/images/simple-illustrations/simple-illustration__hourglass.svg'; import InvoiceBlue from '@assets/images/simple-illustrations/simple-illustration__invoice.svg'; import LockOpen from '@assets/images/simple-illustrations/simple-illustration__lockopen.svg'; import Luggage from '@assets/images/simple-illustrations/simple-illustration__luggage.svg'; @@ -47,6 +49,7 @@ import SanFrancisco from '@assets/images/simple-illustrations/simple-illustratio import ShieldYellow from '@assets/images/simple-illustrations/simple-illustration__shield.svg'; import ThumbsUpStars from '@assets/images/simple-illustrations/simple-illustration__thumbsupstars.svg'; import TrackShoe from '@assets/images/simple-illustrations/simple-illustration__track-shoe.svg'; +import TrashCan from '@assets/images/simple-illustrations/simple-illustration__trashcan.svg'; import TreasureChest from '@assets/images/simple-illustrations/simple-illustration__treasurechest.svg'; export { @@ -100,4 +103,7 @@ export { Hands, HandEarth, SmartScan, + Hourglass, + CommentBubbles, + TrashCan, }; diff --git a/src/components/ProcessMoneyRequestHoldMenu.js b/src/components/ProcessMoneyRequestHoldMenu.js new file mode 100644 index 000000000000..78da0a5ba834 --- /dev/null +++ b/src/components/ProcessMoneyRequestHoldMenu.js @@ -0,0 +1,106 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, {useMemo} from 'react'; +import {View} from 'react-native'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@styles/useThemeStyles'; +import variables from '@styles/variables'; +import Button from './Button'; +import Icon from './Icon'; +import * as Illustrations from './Icon/Illustrations'; +import Popover from './Popover'; +import refPropTypes from './refPropTypes'; +import Text from './Text'; + +const propTypes = { + isVisible: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, + onConfirm: PropTypes.func.isRequired, + anchorPosition: PropTypes.shape({ + horizontal: PropTypes.number, + vertical: PropTypes.number, + }), + anchorRef: refPropTypes, +}; + +const defaultProps = { + anchorPosition: {}, + anchorRef: () => {}, +}; + +function ProcessMoneyRequestHoldMenu({isVisible, onClose, onConfirm, anchorPosition, anchorRef}) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + + const holdMenuSections = useMemo(() => { + const baseHoldMenuSections = [ + { + icon: Illustrations.Hourglass, + titleTranslationKey: 'iou.whatIsHoldTitle', + descriptionTranslationKey: 'iou.whatIsHoldExplain', + }, + { + icon: Illustrations.CommentBubbles, + titleTranslationKey: 'iou.holdIsTemporaryTitle', + descriptionTranslationKey: 'iou.holdIsTemporaryExplain', + }, + { + icon: Illustrations.TrashCan, + titleTranslationKey: 'iou.deleteHoldTitle', + descriptionTranslationKey: 'iou.deleteHoldExplain', + }, + ]; + + return _.map(baseHoldMenuSections, (section, index) => ( + + + + {translate(section.titleTranslationKey)} + + {translate(section.descriptionTranslationKey)} + + + + )); + }, [styles, translate]); + + return ( + + + + {translate('iou.holdEducationalTitle')} + {translate('iou.hold')} + + {holdMenuSections} +