From fd857e72213f76804c836e1a8901f705de907b5d Mon Sep 17 00:00:00 2001 From: Getabalew Tesfaye Date: Wed, 11 Oct 2023 20:11:05 +0300 Subject: [PATCH 001/319] Feat: update the report when a payment is refunded --- src/CONST.ts | 4 ++++ src/components/ReportActionItem/MoneyRequestPreview.js | 2 ++ src/languages/en.ts | 3 +++ src/languages/es.ts | 3 +++ src/languages/types.ts | 3 +++ src/pages/home/report/ReportActionItem.js | 6 ++++++ 6 files changed, 21 insertions(+) diff --git a/src/CONST.ts b/src/CONST.ts index 23957827d140..91942313fa47 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -486,6 +486,7 @@ const CONST = { IOU: 'IOU', MODIFIEDEXPENSE: 'MODIFIEDEXPENSE', REIMBURSEMENTQUEUED: 'REIMBURSEMENTQUEUED', + REIMBURSEMENTDEQUEUED: 'REIMBURSEMENTDEQUEUED', RENAMED: 'RENAMED', REPORTPREVIEW: 'REPORTPREVIEW', SUBMITTED: 'SUBMITTED', @@ -1118,6 +1119,9 @@ const CONST = { DOCX: 'docx', SVG: 'svg', }, + CANCEL_REASON: { + PAYMENT_EXPIRED: 'CANCEL_REASON_PAYMENT_EXPIRED' + }, }, GROWL: { diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index 35215cadd15d..783c52c0553b 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -206,6 +206,8 @@ function MoneyRequestPreview(props) { message += ` • ${props.translate('iou.pending')}`; } else if (ReportUtils.isSettled(props.iouReport.reportID)) { message += ` • ${props.translate('iou.settledExpensify')}`; + } else if(props.iouReport.originalMessage.reason === CONST.IOU.CANCEL_REASON.PAYMENT_EXPIRED) { + message += ` • ${props.translate('iou.canceled')}`; } return message; }; diff --git a/src/languages/en.ts b/src/languages/en.ts index 7133ed88579e..3f47db2ecffd 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -76,6 +76,7 @@ import type { TagSelectionParams, TranslationBase, WalletProgramParams, + CanceledRequestParams, } from './types'; import * as ReportActionsUtils from '../libs/ReportActionsUtils'; @@ -522,6 +523,7 @@ export default { pay: 'Pay', viewDetails: 'View details', pending: 'Pending', + canceled: 'Canceled', deleteReceipt: 'Delete receipt', receiptScanning: 'Receipt scan in progress…', receiptMissingDetails: 'Receipt missing details', @@ -545,6 +547,7 @@ export default { managerApproved: ({manager}: ManagerApprovedParams) => `${manager} approved:`, payerSettled: ({amount}: PayerSettledParams) => `paid ${amount}`, waitingOnBankAccount: ({submitterDisplayName}: WaitingOnBankAccountParams) => `started settling up, payment is held until ${submitterDisplayName} adds a bank account`, + canceledRequest: ({amount, submitterDisplayName}: CanceledRequestParams) => `Canceled the ${amount} payment, because ${submitterDisplayName} did not enable their Expensify Wallet within 30 days`, settledAfterAddedBankAccount: ({submitterDisplayName, amount}: SettledAfterAddedBankAccountParams) => `${submitterDisplayName} added a bank account. The ${amount} payment has been made.`, paidElsewhereWithAmount: ({payer, amount}: PaidElsewhereWithAmountParams) => `${payer} paid ${amount} elsewhere`, diff --git a/src/languages/es.ts b/src/languages/es.ts index a98ddfaff7d0..db8e0d7c43c7 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -76,6 +76,7 @@ import type { TagSelectionParams, EnglishTranslation, WalletProgramParams, + CanceledRequestParams, } from './types'; /* eslint-disable max-len */ @@ -514,6 +515,7 @@ export default { pay: 'Pagar', viewDetails: 'Ver detalles', pending: 'Pendiente', + canceled: 'Canceled', deleteReceipt: 'Eliminar recibo', receiptScanning: 'Escaneo de recibo en curso…', receiptMissingDetails: 'Recibo con campos vacíos', @@ -537,6 +539,7 @@ export default { managerApproved: ({manager}: ManagerApprovedParams) => `${manager} aprobó:`, payerSettled: ({amount}: PayerSettledParams) => `pagó ${amount}`, waitingOnBankAccount: ({submitterDisplayName}: WaitingOnBankAccountParams) => `inicio el pago, pero no se procesará hasta que ${submitterDisplayName} añada una cuenta bancaria`, + canceledRequest: ({amount, submitterDisplayName}: CanceledRequestParams) => `Canceled the ${amount} payment, because ${submitterDisplayName} did not enable their Expensify Wallet within 30 days`, settledAfterAddedBankAccount: ({submitterDisplayName, amount}: SettledAfterAddedBankAccountParams) => `${submitterDisplayName} añadió una cuenta bancaria. El pago de ${amount} se ha realizado.`, paidElsewhereWithAmount: ({payer, amount}: PaidElsewhereWithAmountParams) => `${payer} pagó ${amount} de otra forma`, diff --git a/src/languages/types.ts b/src/languages/types.ts index 3ee504ccddd7..b3ce20500f85 100644 --- a/src/languages/types.ts +++ b/src/languages/types.ts @@ -122,6 +122,8 @@ type PayerSettledParams = {amount: number}; type WaitingOnBankAccountParams = {submitterDisplayName: string}; +type CanceledRequestParams = {amount: string, submitterDisplayName: string} + type SettledAfterAddedBankAccountParams = {submitterDisplayName: string; amount: string}; type PaidElsewhereWithAmountParams = {payer: string; amount: string}; @@ -277,6 +279,7 @@ export type { ManagerApprovedParams, PayerSettledParams, WaitingOnBankAccountParams, + CanceledRequestParams, SettledAfterAddedBankAccountParams, PaidElsewhereWithAmountParams, PaidWithExpensifyWithAmountParams, diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index d0e84499a443..36c8476df976 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -70,6 +70,7 @@ import themeColors from '../../../styles/themes/default'; import ReportActionItemBasicMessage from './ReportActionItemBasicMessage'; import RenderHTML from '../../../components/RenderHTML'; import ReportAttachmentsContext from './ReportAttachmentsContext'; +import * as CurrencyUtils from "../../../libs/CurrencyUtils"; const propTypes = { ...windowDimensionsPropTypes, @@ -361,6 +362,11 @@ function ReportActionItem(props) { ) : null} ); + } else if(props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTDEQUEUED) { + const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(props.personalDetailsList, [props.iouReport.ownerAccountID, 'displayName'], props.iouReport.ownerEmail); + const amount = CurrencyUtils.convertToDisplayString(props.iouReport.total, props.iouReport.currency); + + children = ; } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE) { children = ; } else { From 2b33b90d6da6399626bcaafac99dbaee4592a1b3 Mon Sep 17 00:00:00 2001 From: Getabalew Tesfaye Date: Wed, 11 Oct 2023 20:49:27 +0300 Subject: [PATCH 002/319] run prettier --- src/CONST.ts | 2 +- src/components/ReportActionItem/MoneyRequestPreview.js | 2 +- src/languages/en.ts | 3 ++- src/languages/es.ts | 3 ++- src/languages/types.ts | 2 +- src/pages/home/report/ReportActionItem.js | 4 ++-- 6 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 91942313fa47..308d83766d93 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1120,7 +1120,7 @@ const CONST = { SVG: 'svg', }, CANCEL_REASON: { - PAYMENT_EXPIRED: 'CANCEL_REASON_PAYMENT_EXPIRED' + PAYMENT_EXPIRED: 'CANCEL_REASON_PAYMENT_EXPIRED', }, }, diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index 783c52c0553b..a408041dbf5c 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -206,7 +206,7 @@ function MoneyRequestPreview(props) { message += ` • ${props.translate('iou.pending')}`; } else if (ReportUtils.isSettled(props.iouReport.reportID)) { message += ` • ${props.translate('iou.settledExpensify')}`; - } else if(props.iouReport.originalMessage.reason === CONST.IOU.CANCEL_REASON.PAYMENT_EXPIRED) { + } else if (props.iouReport.originalMessage.reason === CONST.IOU.CANCEL_REASON.PAYMENT_EXPIRED) { message += ` • ${props.translate('iou.canceled')}`; } return message; diff --git a/src/languages/en.ts b/src/languages/en.ts index 3f47db2ecffd..e5ab33014b4a 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -547,7 +547,8 @@ export default { managerApproved: ({manager}: ManagerApprovedParams) => `${manager} approved:`, payerSettled: ({amount}: PayerSettledParams) => `paid ${amount}`, waitingOnBankAccount: ({submitterDisplayName}: WaitingOnBankAccountParams) => `started settling up, payment is held until ${submitterDisplayName} adds a bank account`, - canceledRequest: ({amount, submitterDisplayName}: CanceledRequestParams) => `Canceled the ${amount} payment, because ${submitterDisplayName} did not enable their Expensify Wallet within 30 days`, + canceledRequest: ({amount, submitterDisplayName}: CanceledRequestParams) => + `Canceled the ${amount} payment, because ${submitterDisplayName} did not enable their Expensify Wallet within 30 days`, settledAfterAddedBankAccount: ({submitterDisplayName, amount}: SettledAfterAddedBankAccountParams) => `${submitterDisplayName} added a bank account. The ${amount} payment has been made.`, paidElsewhereWithAmount: ({payer, amount}: PaidElsewhereWithAmountParams) => `${payer} paid ${amount} elsewhere`, diff --git a/src/languages/es.ts b/src/languages/es.ts index db8e0d7c43c7..c88d6fb290ac 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -539,7 +539,8 @@ export default { managerApproved: ({manager}: ManagerApprovedParams) => `${manager} aprobó:`, payerSettled: ({amount}: PayerSettledParams) => `pagó ${amount}`, waitingOnBankAccount: ({submitterDisplayName}: WaitingOnBankAccountParams) => `inicio el pago, pero no se procesará hasta que ${submitterDisplayName} añada una cuenta bancaria`, - canceledRequest: ({amount, submitterDisplayName}: CanceledRequestParams) => `Canceled the ${amount} payment, because ${submitterDisplayName} did not enable their Expensify Wallet within 30 days`, + canceledRequest: ({amount, submitterDisplayName}: CanceledRequestParams) => + `Canceled the ${amount} payment, because ${submitterDisplayName} did not enable their Expensify Wallet within 30 days`, settledAfterAddedBankAccount: ({submitterDisplayName, amount}: SettledAfterAddedBankAccountParams) => `${submitterDisplayName} añadió una cuenta bancaria. El pago de ${amount} se ha realizado.`, paidElsewhereWithAmount: ({payer, amount}: PaidElsewhereWithAmountParams) => `${payer} pagó ${amount} de otra forma`, diff --git a/src/languages/types.ts b/src/languages/types.ts index b3ce20500f85..4ba148a7869d 100644 --- a/src/languages/types.ts +++ b/src/languages/types.ts @@ -122,7 +122,7 @@ type PayerSettledParams = {amount: number}; type WaitingOnBankAccountParams = {submitterDisplayName: string}; -type CanceledRequestParams = {amount: string, submitterDisplayName: string} +type CanceledRequestParams = {amount: string; submitterDisplayName: string}; type SettledAfterAddedBankAccountParams = {submitterDisplayName: string; amount: string}; diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 36c8476df976..5c52670fd37e 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -70,7 +70,7 @@ import themeColors from '../../../styles/themes/default'; import ReportActionItemBasicMessage from './ReportActionItemBasicMessage'; import RenderHTML from '../../../components/RenderHTML'; import ReportAttachmentsContext from './ReportAttachmentsContext'; -import * as CurrencyUtils from "../../../libs/CurrencyUtils"; +import * as CurrencyUtils from '../../../libs/CurrencyUtils'; const propTypes = { ...windowDimensionsPropTypes, @@ -362,7 +362,7 @@ function ReportActionItem(props) { ) : null} ); - } else if(props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTDEQUEUED) { + } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTDEQUEUED) { const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(props.personalDetailsList, [props.iouReport.ownerAccountID, 'displayName'], props.iouReport.ownerEmail); const amount = CurrencyUtils.convertToDisplayString(props.iouReport.total, props.iouReport.currency); From 0aac629c63aede00570c19eef26e3275812c0b35 Mon Sep 17 00:00:00 2001 From: Getabalew Tesfaye Date: Wed, 11 Oct 2023 23:44:42 +0300 Subject: [PATCH 003/319] prevent crash by checking key existence --- src/components/ReportActionItem/MoneyRequestPreview.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index a408041dbf5c..fc294c3ffeda 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -206,7 +206,7 @@ function MoneyRequestPreview(props) { message += ` • ${props.translate('iou.pending')}`; } else if (ReportUtils.isSettled(props.iouReport.reportID)) { message += ` • ${props.translate('iou.settledExpensify')}`; - } else if (props.iouReport.originalMessage.reason === CONST.IOU.CANCEL_REASON.PAYMENT_EXPIRED) { + } else if (props.iouReport.originalMessage && props.iouReport.originalMessage.reason === CONST.IOU.CANCEL_REASON.PAYMENT_EXPIRED) { message += ` • ${props.translate('iou.canceled')}`; } return message; From b7986d4975e834a3642655634bf9884b66285fcb Mon Sep 17 00:00:00 2001 From: Getabalew Tesfaye Date: Wed, 11 Oct 2023 23:57:24 +0300 Subject: [PATCH 004/319] Add es translations --- src/languages/es.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 500cc529304c..d0e5efa00c12 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -519,7 +519,7 @@ export default { pay: 'Pagar', viewDetails: 'Ver detalles', pending: 'Pendiente', - canceled: 'Canceled', + canceled: 'Canceló', posted: 'Contabilizado', deleteReceipt: 'Eliminar recibo', receiptScanning: 'Escaneo de recibo en curso…', @@ -545,7 +545,7 @@ export default { payerSettled: ({amount}: PayerSettledParams) => `pagó ${amount}`, waitingOnBankAccount: ({submitterDisplayName}: WaitingOnBankAccountParams) => `inicio el pago, pero no se procesará hasta que ${submitterDisplayName} añada una cuenta bancaria`, canceledRequest: ({amount, submitterDisplayName}: CanceledRequestParams) => - `Canceled the ${amount} payment, because ${submitterDisplayName} did not enable their Expensify Wallet within 30 days`, + `Canceló el pago ${amount}, porque ${submitterDisplayName} no habilitó su billetera Expensify en un plazo de 30 días.`, settledAfterAddedBankAccount: ({submitterDisplayName, amount}: SettledAfterAddedBankAccountParams) => `${submitterDisplayName} añadió una cuenta bancaria. El pago de ${amount} se ha realizado.`, paidElsewhereWithAmount: ({payer, amount}: PaidElsewhereWithAmountParams) => `${payer} pagó ${amount} de otra forma`, From 444b732a645942f6fbc91d94ea2ec58f83d49f5a Mon Sep 17 00:00:00 2001 From: Getabalew Tesfaye Date: Thu, 12 Oct 2023 00:17:24 +0300 Subject: [PATCH 005/319] use report action value --- src/components/ReportActionItem/MoneyRequestPreview.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index aa19c8536446..b557325e8505 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -216,7 +216,7 @@ function MoneyRequestPreview(props) { message += ` • ${props.translate('iou.approved')}`; } else if (props.iouReport.isWaitingOnBankAccount) { message += ` • ${props.translate('iou.pending')}`; - } else if (props.iouReport.originalMessage && props.iouReport.originalMessage.reason === CONST.IOU.CANCEL_REASON.PAYMENT_EXPIRED) { + } else if (props.action.originalMessage && props.action.originalMessage.reason === CONST.IOU.CANCEL_REASON.PAYMENT_EXPIRED) { message += ` • ${props.translate('iou.canceled')}`; } return message; From 99d4202ea5fd271146a47f77b4459b1498a9a885 Mon Sep 17 00:00:00 2001 From: Getabalew Tesfaye Date: Tue, 17 Oct 2023 17:44:47 +0300 Subject: [PATCH 006/319] use props.report value for amount and name --- src/pages/home/report/ReportActionItem.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index f51b0645cd7f..45621297ed98 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -363,8 +363,8 @@ function ReportActionItem(props) { ); } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTDEQUEUED) { - const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(props.personalDetailsList, [props.iouReport.ownerAccountID, 'displayName'], props.iouReport.ownerEmail); - const amount = CurrencyUtils.convertToDisplayString(props.iouReport.total, props.iouReport.currency); + const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(props.personalDetailsList, [props.report.ownerAccountID, 'displayName'], props.report.ownerEmail); + const amount = CurrencyUtils.convertToDisplayString(props.report.total, props.report.currency); children = ; } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE) { From b389a83bf0ddf0379780722a5360afb4e64c4b89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Tue, 24 Oct 2023 08:43:03 -0300 Subject: [PATCH 007/319] Rename AvatarWithIndicator to TSX --- .../{AvatarWithIndicator.js => AvatarWithIndicator.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/components/{AvatarWithIndicator.js => AvatarWithIndicator.tsx} (100%) diff --git a/src/components/AvatarWithIndicator.js b/src/components/AvatarWithIndicator.tsx similarity index 100% rename from src/components/AvatarWithIndicator.js rename to src/components/AvatarWithIndicator.tsx From 18b8449e060243355c22510694531a57cc64d0dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Tue, 24 Oct 2023 08:47:02 -0300 Subject: [PATCH 008/319] Migrate AvatarWithIndicator to TS --- src/components/AvatarWithIndicator.tsx | 36 ++++++++++++-------------- src/libs/UserUtils.ts | 6 ++--- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/src/components/AvatarWithIndicator.tsx b/src/components/AvatarWithIndicator.tsx index 5e7b8d1ee632..c29507b9dcc5 100644 --- a/src/components/AvatarWithIndicator.tsx +++ b/src/components/AvatarWithIndicator.tsx @@ -1,36 +1,34 @@ import React from 'react'; import {View} from 'react-native'; -import PropTypes from 'prop-types'; -import Avatar from './Avatar'; -import styles from '../styles/styles'; -import Tooltip from './Tooltip'; +import {SvgProps} from 'react-native-svg'; import * as UserUtils from '../libs/UserUtils'; -import Indicator from './Indicator'; +import styles from '../styles/styles'; +import Avatar from './Avatar'; import * as Expensicons from './Icon/Expensicons'; +import Indicator from './Indicator'; +import Tooltip from './Tooltip'; -const propTypes = { +type AvatarWithIndicatorProps = { /** URL for the avatar */ - source: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired, + source: string | React.FC; /** To show a tooltip on hover */ - tooltipText: PropTypes.string, + tooltipText?: string; /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ - fallbackIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), -}; - -const defaultProps = { - tooltipText: '', - fallbackIcon: Expensicons.FallbackAvatar, + fallbackIcon?: string | React.FC; }; -function AvatarWithIndicator(props) { +function AvatarWithIndicator({source, tooltipText = '', fallbackIcon = Expensicons.FallbackAvatar}: AvatarWithIndicatorProps) { return ( - + @@ -38,8 +36,6 @@ function AvatarWithIndicator(props) { ); } -AvatarWithIndicator.defaultProps = defaultProps; -AvatarWithIndicator.propTypes = propTypes; AvatarWithIndicator.displayName = 'AvatarWithIndicator'; export default AvatarWithIndicator; diff --git a/src/libs/UserUtils.ts b/src/libs/UserUtils.ts index 15bf3c0f1029..572c4597b514 100644 --- a/src/libs/UserUtils.ts +++ b/src/libs/UserUtils.ts @@ -104,7 +104,7 @@ function getDefaultAvatarURL(accountID: string | number = '', isNewDot = false): * Given a user's avatar path, returns true if user doesn't have an avatar or if URL points to a default avatar * @param [avatarURL] - the avatar source from user's personalDetails */ -function isDefaultAvatar(avatarURL?: string): boolean { +function isDefaultAvatar(avatarURL?: React.FC | string): boolean { if (typeof avatarURL === 'string') { if (avatarURL.includes('images/avatars/avatar_') || avatarURL.includes('images/avatars/default-avatar_') || avatarURL.includes('images/avatars/user/default')) { return true; @@ -131,7 +131,7 @@ function isDefaultAvatar(avatarURL?: string): boolean { * @param avatarURL - the avatar source from user's personalDetails * @param accountID - the accountID of the user */ -function getAvatar(avatarURL: string, accountID: number): React.FC | string { +function getAvatar(avatarURL: React.FC | string, accountID?: number): React.FC | string { return isDefaultAvatar(avatarURL) ? getDefaultAvatar(accountID) : avatarURL; } @@ -162,7 +162,7 @@ function getFullSizeAvatar(avatarURL: string, accountID: number): React.FC. This adds the _128 at the end of the * source URL (before the file type) if it doesn't exist there already. */ -function getSmallSizeAvatar(avatarURL: string, accountID: number): React.FC | string { +function getSmallSizeAvatar(avatarURL: React.FC | string, accountID?: number): React.FC | string { const source = getAvatar(avatarURL, accountID); if (typeof source !== 'string') { return source; From cebcf98619b38ebeabcf3a75c23c171a8e75836f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Wed, 25 Oct 2023 06:23:36 -0300 Subject: [PATCH 009/319] Rename params to better name --- src/libs/UserUtils.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/libs/UserUtils.ts b/src/libs/UserUtils.ts index 572c4597b514..50fc95c8416a 100644 --- a/src/libs/UserUtils.ts +++ b/src/libs/UserUtils.ts @@ -102,21 +102,21 @@ function getDefaultAvatarURL(accountID: string | number = '', isNewDot = false): /** * Given a user's avatar path, returns true if user doesn't have an avatar or if URL points to a default avatar - * @param [avatarURL] - the avatar source from user's personalDetails + * @param [avatarSource] - the avatar source from user's personalDetails */ -function isDefaultAvatar(avatarURL?: React.FC | string): boolean { - if (typeof avatarURL === 'string') { - if (avatarURL.includes('images/avatars/avatar_') || avatarURL.includes('images/avatars/default-avatar_') || avatarURL.includes('images/avatars/user/default')) { +function isDefaultAvatar(avatarSource?: React.FC | string): boolean { + if (typeof avatarSource === 'string') { + if (avatarSource.includes('images/avatars/avatar_') || avatarSource.includes('images/avatars/default-avatar_') || avatarSource.includes('images/avatars/user/default')) { return true; } // We use a hardcoded "default" Concierge avatar - if (avatarURL === CONST.CONCIERGE_ICON_URL_2021 || avatarURL === CONST.CONCIERGE_ICON_URL) { + if (avatarSource === CONST.CONCIERGE_ICON_URL_2021 || avatarSource === CONST.CONCIERGE_ICON_URL) { return true; } } - if (!avatarURL) { + if (!avatarSource) { // If null URL, we should also use a default avatar return true; } @@ -128,11 +128,11 @@ function isDefaultAvatar(avatarURL?: React.FC | string): boolean { * Provided a source URL, if source is a default avatar, return the associated SVG. * Otherwise, return the URL pointing to a user-uploaded avatar. * - * @param avatarURL - the avatar source from user's personalDetails + * @param avatarSource - the avatar source from user's personalDetails * @param accountID - the accountID of the user */ -function getAvatar(avatarURL: React.FC | string, accountID?: number): React.FC | string { - return isDefaultAvatar(avatarURL) ? getDefaultAvatar(accountID) : avatarURL; +function getAvatar(avatarSource: React.FC | string, accountID?: number): React.FC | string { + return isDefaultAvatar(avatarSource) ? getDefaultAvatar(accountID) : avatarSource; } /** @@ -150,8 +150,8 @@ function getAvatarUrl(avatarURL: string, accountID: number): string { * Avatars uploaded by users will have a _128 appended so that the asset server returns a small version. * This removes that part of the URL so the full version of the image can load. */ -function getFullSizeAvatar(avatarURL: string, accountID: number): React.FC | string { - const source = getAvatar(avatarURL, accountID); +function getFullSizeAvatar(avatarSource: React.FC | string, accountID: number): React.FC | string { + const source = getAvatar(avatarSource, accountID); if (typeof source !== 'string') { return source; } @@ -162,8 +162,8 @@ function getFullSizeAvatar(avatarURL: string, accountID: number): React.FC. This adds the _128 at the end of the * source URL (before the file type) if it doesn't exist there already. */ -function getSmallSizeAvatar(avatarURL: React.FC | string, accountID?: number): React.FC | string { - const source = getAvatar(avatarURL, accountID); +function getSmallSizeAvatar(avatarSource: React.FC | string, accountID?: number): React.FC | string { + const source = getAvatar(avatarSource, accountID); if (typeof source !== 'string') { return source; } From 8e34982d458f2d6a7f115a1dc58c1960c6093ad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Wed, 25 Oct 2023 10:38:52 -0300 Subject: [PATCH 010/319] Create AvatarSource type --- src/components/AvatarWithIndicator.tsx | 6 +++--- src/libs/UserUtils.ts | 11 +++++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/components/AvatarWithIndicator.tsx b/src/components/AvatarWithIndicator.tsx index c29507b9dcc5..e5100f084bd0 100644 --- a/src/components/AvatarWithIndicator.tsx +++ b/src/components/AvatarWithIndicator.tsx @@ -1,7 +1,7 @@ import React from 'react'; import {View} from 'react-native'; -import {SvgProps} from 'react-native-svg'; import * as UserUtils from '../libs/UserUtils'; +import {AvatarSource} from '../libs/UserUtils'; import styles from '../styles/styles'; import Avatar from './Avatar'; import * as Expensicons from './Icon/Expensicons'; @@ -10,13 +10,13 @@ import Tooltip from './Tooltip'; type AvatarWithIndicatorProps = { /** URL for the avatar */ - source: string | React.FC; + source: AvatarSource; /** To show a tooltip on hover */ tooltipText?: string; /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ - fallbackIcon?: string | React.FC; + fallbackIcon?: AvatarSource; }; function AvatarWithIndicator({source, tooltipText = '', fallbackIcon = Expensicons.FallbackAvatar}: AvatarWithIndicatorProps) { diff --git a/src/libs/UserUtils.ts b/src/libs/UserUtils.ts index 50fc95c8416a..6d068b89305f 100644 --- a/src/libs/UserUtils.ts +++ b/src/libs/UserUtils.ts @@ -8,6 +8,8 @@ import Login from '../types/onyx/Login'; type AvatarRange = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24; +type AvatarSource = React.FC | string; + type LoginListIndicator = ValueOf | ''; /** @@ -104,7 +106,7 @@ function getDefaultAvatarURL(accountID: string | number = '', isNewDot = false): * Given a user's avatar path, returns true if user doesn't have an avatar or if URL points to a default avatar * @param [avatarSource] - the avatar source from user's personalDetails */ -function isDefaultAvatar(avatarSource?: React.FC | string): boolean { +function isDefaultAvatar(avatarSource?: AvatarSource): boolean { if (typeof avatarSource === 'string') { if (avatarSource.includes('images/avatars/avatar_') || avatarSource.includes('images/avatars/default-avatar_') || avatarSource.includes('images/avatars/user/default')) { return true; @@ -131,7 +133,7 @@ function isDefaultAvatar(avatarSource?: React.FC | string): boolean { * @param avatarSource - the avatar source from user's personalDetails * @param accountID - the accountID of the user */ -function getAvatar(avatarSource: React.FC | string, accountID?: number): React.FC | string { +function getAvatar(avatarSource: AvatarSource, accountID?: number): AvatarSource { return isDefaultAvatar(avatarSource) ? getDefaultAvatar(accountID) : avatarSource; } @@ -150,7 +152,7 @@ function getAvatarUrl(avatarURL: string, accountID: number): string { * Avatars uploaded by users will have a _128 appended so that the asset server returns a small version. * This removes that part of the URL so the full version of the image can load. */ -function getFullSizeAvatar(avatarSource: React.FC | string, accountID: number): React.FC | string { +function getFullSizeAvatar(avatarSource: AvatarSource, accountID: number): AvatarSource { const source = getAvatar(avatarSource, accountID); if (typeof source !== 'string') { return source; @@ -162,7 +164,7 @@ function getFullSizeAvatar(avatarSource: React.FC | string, accountID: * Small sized avatars end with _128.. This adds the _128 at the end of the * source URL (before the file type) if it doesn't exist there already. */ -function getSmallSizeAvatar(avatarSource: React.FC | string, accountID?: number): React.FC | string { +function getSmallSizeAvatar(avatarSource: AvatarSource, accountID?: number): AvatarSource { const source = getAvatar(avatarSource, accountID); if (typeof source !== 'string') { return source; @@ -202,3 +204,4 @@ export { getFullSizeAvatar, generateAccountID, }; +export type {AvatarSource}; From bb077b6853ac71cb443529a0f5423477c91dcc99 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 25 Oct 2023 16:44:49 +0200 Subject: [PATCH 011/319] Rename files --- src/components/{TestToolMenu.js => TestToolMenu.tsx} | 0 src/components/{TestToolsModal.js => TestToolsModal.tsx} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/components/{TestToolMenu.js => TestToolMenu.tsx} (100%) rename src/components/{TestToolsModal.js => TestToolsModal.tsx} (100%) diff --git a/src/components/TestToolMenu.js b/src/components/TestToolMenu.tsx similarity index 100% rename from src/components/TestToolMenu.js rename to src/components/TestToolMenu.tsx diff --git a/src/components/TestToolsModal.js b/src/components/TestToolsModal.tsx similarity index 100% rename from src/components/TestToolsModal.js rename to src/components/TestToolsModal.tsx From 8bff041e052fa5fd5e449a3857b8c963b16d03e6 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 25 Oct 2023 17:11:12 +0200 Subject: [PATCH 012/319] Migrate TestToolMenu and TestToolsModal --- src/components/TestToolMenu.tsx | 55 ++++++++++++------------------- src/components/TestToolsModal.tsx | 29 ++++------------ 2 files changed, 28 insertions(+), 56 deletions(-) diff --git a/src/components/TestToolMenu.tsx b/src/components/TestToolMenu.tsx index 474e4c9bb10c..496c90fe9341 100644 --- a/src/components/TestToolMenu.tsx +++ b/src/components/TestToolMenu.tsx @@ -1,7 +1,5 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import {withOnyx} from 'react-native-onyx'; -import lodashGet from 'lodash/get'; +import {OnyxEntry, withOnyx} from 'react-native-onyx'; import styles from '../styles/styles'; import Switch from './Switch'; import Text from './Text'; @@ -11,40 +9,31 @@ import * as Session from '../libs/actions/Session'; import ONYXKEYS from '../ONYXKEYS'; import Button from './Button'; import TestToolRow from './TestToolRow'; -import networkPropTypes from './networkPropTypes'; import compose from '../libs/compose'; import {withNetwork} from './OnyxProvider'; import * as ApiUtils from '../libs/ApiUtils'; import CONFIG from '../CONFIG'; +import NetworkOnyx from '../types/onyx/Network'; +import UserOnyx from '../types/onyx/User'; -const propTypes = { +type TestToolMenuOnyxProps = { /** User object in Onyx */ - user: PropTypes.shape({ - /** Whether we should use the staging version of the secure API server */ - shouldUseStagingServer: PropTypes.bool, - }), + user: OnyxEntry; +}; +type TestToolMenuProps = TestToolMenuOnyxProps & { /** Network object in Onyx */ - network: networkPropTypes.isRequired, + network: OnyxEntry; }; -const defaultProps = { - user: { - // The default value is environment specific and can't be set with `defaultProps` (ENV is not resolved yet) - // When undefined (during render) STAGING defaults to `true`, other envs default to `false` - shouldUseStagingServer: undefined, - }, -}; +const USER_DEFAULT = {shouldUseStagingServer: undefined, isSubscribedToNewsletter: false, validated: false, isFromPublicDomain: false, isUsingExpensifyCard: false}; + +function TestToolMenu({user = USER_DEFAULT, network}: TestToolMenuProps) { + const shouldUseStagingServer = user?.shouldUseStagingServer ?? ApiUtils.isUsingStagingApi(); -function TestToolMenu(props) { return ( <> - - Test Preferences - + Test Preferences {/* Option to switch between staging and default api endpoints. This enables QA, internal testers and external devs to take advantage of sandbox environments for 3rd party services like Plaid and Onfido. @@ -53,8 +42,8 @@ function TestToolMenu(props) { User.setShouldUseStagingServer(!lodashGet(props, 'user.shouldUseStagingServer', ApiUtils.isUsingStagingApi()))} + isOn={shouldUseStagingServer} + onToggle={() => User.setShouldUseStagingServer(!shouldUseStagingServer)} /> )} @@ -63,8 +52,8 @@ function TestToolMenu(props) { Network.setShouldForceOffline(!props.network.shouldForceOffline)} + isOn={Boolean(network?.shouldForceOffline)} + onToggle={() => Network.setShouldForceOffline(!network?.shouldForceOffline)} /> @@ -72,8 +61,8 @@ function TestToolMenu(props) { Network.setShouldFailAllRequests(!props.network.shouldFailAllRequests)} + isOn={Boolean(network?.shouldFailAllRequests)} + onToggle={() => Network.setShouldFailAllRequests(!network?.shouldFailAllRequests)} /> @@ -98,15 +87,13 @@ function TestToolMenu(props) { ); } -TestToolMenu.propTypes = propTypes; -TestToolMenu.defaultProps = defaultProps; TestToolMenu.displayName = 'TestToolMenu'; export default compose( - withNetwork(), - withOnyx({ + withOnyx({ user: { key: ONYXKEYS.USER, }, }), + withNetwork(), )(TestToolMenu); diff --git a/src/components/TestToolsModal.tsx b/src/components/TestToolsModal.tsx index 43a74e48df5d..238db2379f01 100644 --- a/src/components/TestToolsModal.tsx +++ b/src/components/TestToolsModal.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import {withOnyx} from 'react-native-onyx'; +import {OnyxEntry, withOnyx} from 'react-native-onyx'; import {View} from 'react-native'; import ONYXKEYS from '../ONYXKEYS'; import Modal from './Modal'; @@ -9,26 +8,17 @@ import toggleTestToolsModal from '../libs/actions/TestTool'; import TestToolMenu from './TestToolMenu'; import styles from '../styles/styles'; -const propTypes = { - /** Details about modal */ - modal: PropTypes.shape({ - /** Indicates when an Alert modal is about to be visible */ - willAlertModalBecomeVisible: PropTypes.bool, - }), - +type TestToolsModalOnyxProps = { /** Whether the test tools modal is open */ - isTestToolsModalOpen: PropTypes.bool, + isTestToolsModalOpen: OnyxEntry; }; -const defaultProps = { - modal: {}, - isTestToolsModalOpen: false, -}; +type TestToolsModalProps = TestToolsModalOnyxProps; -function TestToolsModal(props) { +function TestToolsModal({isTestToolsModalOpen = false}: TestToolsModalProps) { return ( @@ -39,14 +29,9 @@ function TestToolsModal(props) { ); } -TestToolsModal.propTypes = propTypes; -TestToolsModal.defaultProps = defaultProps; TestToolsModal.displayName = 'TestToolsModal'; -export default withOnyx({ - modal: { - key: ONYXKEYS.MODAL, - }, +export default withOnyx({ isTestToolsModalOpen: { key: ONYXKEYS.IS_TEST_TOOLS_MODAL_OPEN, }, From 586c1d17d0496a0d0a3a34c6fc695b0f09ca0046 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Fri, 27 Oct 2023 12:31:18 +0200 Subject: [PATCH 013/319] Type Text more accurate --- src/components/TestToolMenu.tsx | 7 ++++++- src/components/Text.tsx | 19 +++---------------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/components/TestToolMenu.tsx b/src/components/TestToolMenu.tsx index 496c90fe9341..369ba5bb65b9 100644 --- a/src/components/TestToolMenu.tsx +++ b/src/components/TestToolMenu.tsx @@ -33,7 +33,12 @@ function TestToolMenu({user = USER_DEFAULT, network}: TestToolMenuProps) { return ( <> - Test Preferences + + Test Preferences + {/* Option to switch between staging and default api endpoints. This enables QA, internal testers and external devs to take advantage of sandbox environments for 3rd party services like Plaid and Onfido. diff --git a/src/components/Text.tsx b/src/components/Text.tsx index 60a59aae1520..598a01c6a46b 100644 --- a/src/components/Text.tsx +++ b/src/components/Text.tsx @@ -1,12 +1,12 @@ import React, {ForwardedRef} from 'react'; // eslint-disable-next-line no-restricted-imports -import {Text as RNText} from 'react-native'; +import {Text as RNText, TextProps as RNTextProps, StyleSheet} from 'react-native'; import type {TextStyle} from 'react-native'; import fontFamily from '../styles/fontFamily'; import themeColors from '../styles/themes/default'; import variables from '../styles/variables'; -type TextProps = { +type TextProps = RNTextProps & { /** The color of the text */ color?: string; @@ -21,31 +21,18 @@ type TextProps = { /** The family of the font to use */ family?: keyof typeof fontFamily; - - /** Any additional styles to apply */ - style?: TextStyle | TextStyle[]; }; function Text( {color = themeColors.text, fontSize = variables.fontSizeNormal, textAlign = 'left', children = null, family = 'EXP_NEUE', style = {}, ...props}: TextProps, ref: ForwardedRef, ) { - // If the style prop is an array of styles, we need to mix them all together - const mergedStyles = !Array.isArray(style) - ? style - : style.reduce( - (finalStyles, s) => ({ - ...finalStyles, - ...s, - }), - {}, - ); const componentStyle: TextStyle = { color, fontSize, textAlign, fontFamily: fontFamily[family], - ...mergedStyles, + ...StyleSheet.flatten(style), }; if (!componentStyle.lineHeight && componentStyle.fontSize === variables.fontSizeNormal) { From 98380131c8ef28ce6fff549cd370b6937209c2ba Mon Sep 17 00:00:00 2001 From: John Schuster Date: Fri, 27 Oct 2023 15:11:31 -0500 Subject: [PATCH 014/319] Delete docs/articles/new-expensify/getting-started/Expensify-Lounge.md --- .../getting-started/Expensify-Lounge.md | 67 ------------------- 1 file changed, 67 deletions(-) delete mode 100644 docs/articles/new-expensify/getting-started/Expensify-Lounge.md diff --git a/docs/articles/new-expensify/getting-started/Expensify-Lounge.md b/docs/articles/new-expensify/getting-started/Expensify-Lounge.md deleted file mode 100644 index bdccbe927769..000000000000 --- a/docs/articles/new-expensify/getting-started/Expensify-Lounge.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -title: Welcome to the Expensify Lounge! -description: How to get the most out of the Expensify Lounge. -redirect_from: articles/other/Expensify-Lounge/ ---- - - -# What is the Expensify Lounge? -The Expensify Lounge is a place where people go to Get Shit Done. It's a beautiful environment with great coffee and a group of people to collaborate with. Check out this guide on how to best utilize the Expensify Lounge! - -# The Two Rules -### Rule #1 - Get Shit Done - -The Lounge is a space for people to get work done. It is optimized to be the perfect environment for you to focus on your work, collaborate with others, and advance your most wild and creative ideas. To make this a reality, we ask our members to keep the following in mind: - -- **#focus** - Use the space for how it was designed and do not distract from others' focus. The space is beautiful, social, and collaborative, but it was created to help our members work effectively. -- **#urgency** - Working remotely is great, but there's nothing like real-time collaboration with your colleagues. Use the lounge to meet with co-workers IRL to continue the progress on whatever it is you're working on. -- **#results** - Don't mistake time for effort or effort for output. Upon arrival, visualize what you want to accomplish, and don't leave until it's done. - -## Rule #2 - Don’t Ruin it for Everyone Else - -We want this place to be incredible, innovative, and always elvoving. To achieve that, we have some general guidelines: - -- **#writeitdown** - If you can help others learn from you, do so. Write a blog post, a document, or a post in Expensify Chat to share with others. This includes making the Expensify Lounge a better space. Feel free to write down any improvements so we can make it better. -- **#showup** - If you are in the lounge, be fully present. Meet others, and collaborate in social rooms. The point is to build a community of people who are focused on getting shit done; you’ll get out what you put in. -- **#oneteam** - Providing an inclusive community is our priority, and we do not tolerate any form of discrimination. Aim to go out of your way to include people who want to be included. -- **#nocreeps** - Do not make people feel uncomfortable with your words or actions. If you are made to feel uncomfortable or notice this happening to someone else, you can use the escalation process outlined in the FAQ section. - -# How to Use the Expensify Lounge -Keeping those two rules in mind, below is a guide on how our members can get the most out of the lounge. - -### Rule #1 - Getting Shit Done -- **Order drinks from Concierge** - [Write Concierge here](https://new.expensify.com/concierge) to ask lounge questions or order beverages. Concierge will bring your order directly to you! -- **Using an office** - Offices are first come, first serve. If an office is open, feel free to use it! Please keep office use to under an hour. We currently do not allow reserving offices. -- **Lounge hours** - The lounge will be open from 8am-6pm PT, Monday through Friday and closed on some major holidays. You can review our Google Maps profile to check our holiday hours. -- **Make the lounge better** - Make any suggestions to improve the lounge experience in [#announce - Expensify Lounge](https://new.expensify.com/r/8292963527436014). - -## Rule #2 - Not Ruining it for Everyone Else -- **Offices are for calls** - Please do not occupy an office unless you have a call or collaborative meeting happening, and don't stay in an office for longer than an hour. -- **Respect other people** - Please do not be too loud or distracting while others are trying to work. While collaborating in Expensify Chat, be respectful of others’ viewpoints and keep a positive environment. -- **Stay home if you’re sick** - If you feel sick, please do not visit the lounge, or consider wearing a mask in public areas. -- **If you see something, say something** - If you are made to feel uncomfortable or witness others being made uncomfortable, let Concierge know. If this is happening in Expensify Chat, use our moderation tools (outlined below in the FAQ) to apply the applicable level of moderation. - -We’re so happy you are here to live rich, have fun, and save the world with us. Now, go enjoy the Expensify Lounge, and let's Get Shit Done! - -# FAQs - -#### What is Concierge? - -Concierge is our automated system that answers member questions in real-time. Questions regarding the local lounge will be routed directly to the lounge's Concierge. You can send Concierge a message if you have a drink request or general questions. They’ll take care of everything for you! - -#### Who is invited to the Expensify Lounge? - -Everyone is invited to the Expensify Lounge! Whether you're an existing customer, or you're someone looking for a great space to Get Shit Done, we'd love to have you. - -#### How do I escalate something that's making me or someone else uncomfortable? - -If you see something in Expensify Chat that should be escalated, you can use the escalation feature to mark a chat as: -- **Spam or Inconsiderate**: This will send a whisper to the sender of the message warning them of the violation, and the message will have a flag applied to it which will be visible to all users. Concierge will not review these flags. -- **Intimidating or Bullying**: The message will be immediately hidden, and the content will be reviewed by our team. After reviewing the message, and it's confirmed intimidation or bullying, the message will be permanently hidden and we'll communicate the violation to the sender of the message. -- **Harassment or Assault**: The message will be immediately hidden and reviewed by our team. The user will be sent a message to warning them of the violation, and Concierge can block the user if that's deemed necessary. - -If you witness something in-person, please write to Concierge referencing which lounge you are in, and they will escalate the issue appropriately. - -#### Where are other Expensify Lounge locations? - -Right now, we only have the San Francisco Lounge, but be on the lookout for more coming soon! From 84f47089566e6961b6324bcb4d7293367996bd70 Mon Sep 17 00:00:00 2001 From: John Schuster Date: Fri, 27 Oct 2023 15:14:14 -0500 Subject: [PATCH 015/319] Update Expensify-Card-Perks.md --- .../expensify-card/Expensify-Card-Perks.md | 36 +------------------ 1 file changed, 1 insertion(+), 35 deletions(-) diff --git a/docs/articles/expensify-classic/expensify-card/Expensify-Card-Perks.md b/docs/articles/expensify-classic/expensify-card/Expensify-Card-Perks.md index 5c9761b7ff1d..8bcc11fbf167 100644 --- a/docs/articles/expensify-classic/expensify-card/Expensify-Card-Perks.md +++ b/docs/articles/expensify-classic/expensify-card/Expensify-Card-Perks.md @@ -6,22 +6,12 @@ description: Get the most out of your Expensify Card with exclusive perks! # Overview The Expensify Card is packed with perks, both native to our Card program and through exclusive discounts with partnering solutions. The Expensify Card’s primary perks include: -- Access to our premiere Expensify Lounge (with more locations coming soon) - Swipe to Win, where every swipe has a chance to win fun personalized gifts for you and your closest friends and family members -- And unbeatable cash back incentive with each swipe +- Unbeatable cash back incentive with each swipe Below, we’ll cover all of our exclusive offers in more detail and how to claim discounts with our partners. # Expensify Card Perks -## Access to the Expensify Lounge -Our [world-class lounge](https://use.expensify.com/lounge) is now open for Expensify members and guests to enjoy! - -We invite you to visit our sleek San Francisco lounge, where sweeping city views provide the perfect backdrop for a morning coffee to start your day. - -Enjoy complimentary cocktails and snacks in a vibrant atmosphere with blazing-fast WiFi. Whether you want a place to focus on work, socialize with other members, or simply kick back and relax – our lounge is ready and waiting to welcome you. - -You can sign up for free [here](https://use.expensify.com) if you’re not an Expensify member. If you have any questions, reach out to concierge@expensify.com and [check this out](https://use.expensify.com/lounge) for more info. - ## Swipe to Win Swipe to Win is a new [Expensify Card](https://use.expensify.com/company-credit-card) perk that gives cardholders the chance to send a gift to a friend, family member, or essential worker on the frontlines! @@ -221,27 +211,3 @@ Stripe Atlas helps removes obstacles typically associated with starting a busine **Receive $100 off Stripe Atlas and get access to a startup toolkit and special offers on additional Strip Atlas services.** **How to redeem:** Sign up with your Expensify Card. - -# FAQ - -## Where is the Expensify Lounge? -The Expensify Lounge is located on the 16th floor of 88 Kearny Street in San Francisco, California, 94108. This is currently our only lounge location, but keep an eye out for more work lounges popping up soon! - -## When is the Expensify Lounge open? -The lounge is open 8 a.m. to 6 p.m. from Monday through Friday, except for national holidays. Capacity is limited, and we are admitting loungers on a first-come, first-served basis, so make sure to get there early! - -## Who can use the lounge workplace? -Customers with an Expensify subscription can use Expensify’s lounge workplace, and any partner who has completed [ExpensifyApproved! University!](https://university.expensify.com/users/sign_in?next=%2Fdashboard) - - - - -# FAQ -This section covers the useful but not as vital information, it should capture commonly queried elements which do not organically form part of the About or How-to sections. - -- What's idiosyncratic or potentially confusing about this feature? -- Is there anything unique about how this feature relates to billing/activity? -- If this feature is released, are there any common confusions that can't be solved by improvements to the product itself? -- Similarly, if this feature hasn't been released, can you predict and pre-empt any potential confusion? -- Is there any general troubleshooting for this feature? - - Note: troubleshooting should generally go in the FAQ, but if there is extensive troubleshooting, such as with integrations, that will be housed in a separate page, stored with and linked from the main page for that feature. From 9a6375a8ed3a0cfabab3efaa7184ab0f50ad8bf8 Mon Sep 17 00:00:00 2001 From: tienifr Date: Sat, 28 Oct 2023 16:30:31 +0700 Subject: [PATCH 016/319] fix: 30254 --- .../ComposerWithSuggestions.js | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js index 3972b21b91a7..d01d91f206bd 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js @@ -124,6 +124,22 @@ function ComposerWithSuggestions({ const isEmptyChat = useMemo(() => _.size(reportActions) === 1, [reportActions]); const parentAction = ReportActionsUtils.getParentReportAction(report); const shouldAutoFocus = !modal.isVisible && (shouldFocusInputOnScreenFocus || (isEmptyChat && !ReportActionsUtils.isTransactionThread(parentAction))) && shouldShowComposeInput; + const textInputRef = useRef(null); + const shouldAutoFocusRef = useRef(false); + + shouldAutoFocusRef.current = shouldAutoFocus; + + const prevIsFocused = usePrevious(isFocused); + + useEffect(() => { + if (!prevIsFocused && isFocused) { + setTimeout(() => { + if (shouldAutoFocusRef.current) { + textInputRef.current.focus(); + } + }, CONST.ANIMATED_TRANSITION); + } + }, [isFocused, prevIsFocused]); const valueRef = useRef(value); valueRef.current = value; @@ -134,8 +150,7 @@ function ComposerWithSuggestions({ })); const [composerHeight, setComposerHeight] = useState(0); - - const textInputRef = useRef(null); + const insertedEmojisRef = useRef([]); // A flag to indicate whether the onScroll callback is likely triggered by a layout change (caused by text change) or not @@ -478,7 +493,7 @@ function ComposerWithSuggestions({ }, [focusComposerOnKeyPress, navigation, setUpComposeFocusManager]); const prevIsModalVisible = usePrevious(modal.isVisible); - const prevIsFocused = usePrevious(isFocused); + useEffect(() => { if (modal.isVisible && !prevIsModalVisible) { // eslint-disable-next-line no-param-reassign From 8687ed3d7d256eead17234d75eea4e63bb5791ff Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Tue, 31 Oct 2023 16:32:06 +0100 Subject: [PATCH 017/319] migrate UnorderedList to TypeScript --- .../{UnorderedList.js => UnorderedList.tsx} | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) rename src/components/{UnorderedList.js => UnorderedList.tsx} (64%) diff --git a/src/components/UnorderedList.js b/src/components/UnorderedList.tsx similarity index 64% rename from src/components/UnorderedList.js rename to src/components/UnorderedList.tsx index de8cb4abd7f8..a16f52545c3b 100644 --- a/src/components/UnorderedList.js +++ b/src/components/UnorderedList.tsx @@ -1,22 +1,17 @@ -import PropTypes from 'prop-types'; import React from 'react'; import {View} from 'react-native'; -import _ from 'underscore'; import styles from '@styles/styles'; import Text from './Text'; -const propTypes = { +type UnorderedListProps = { /** An array of strings to display as an unordered list */ - items: PropTypes.arrayOf(PropTypes.string), -}; -const defaultProps = { - items: [], + items: string[]; }; -function UnorderedList(props) { +function UnorderedList({items = []}: UnorderedListProps) { return ( <> - {_.map(props.items, (itemText) => ( + {items.map((itemText) => ( Date: Tue, 31 Oct 2023 17:16:03 +0100 Subject: [PATCH 018/319] minor fixes --- src/components/UnorderedList.tsx | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/components/UnorderedList.tsx b/src/components/UnorderedList.tsx index a16f52545c3b..6437a0114776 100644 --- a/src/components/UnorderedList.tsx +++ b/src/components/UnorderedList.tsx @@ -5,23 +5,19 @@ import Text from './Text'; type UnorderedListProps = { /** An array of strings to display as an unordered list */ - items: string[]; + items?: string[]; }; function UnorderedList({items = []}: UnorderedListProps) { - return ( - <> - {items.map((itemText) => ( - - {'\u2022'} - {itemText} - - ))} - - ); + return items.map((itemText) => ( + + {'\u2022'} + {itemText} + + )); } UnorderedList.displayName = 'UnorderedList'; From a8888b7171e6701859ff7f4560c6f8ad4a589593 Mon Sep 17 00:00:00 2001 From: VH Date: Fri, 3 Nov 2023 20:27:56 +0700 Subject: [PATCH 019/319] Do not call API EditTask if title and description are not changed --- src/libs/actions/Task.js | 4 ++-- src/pages/tasks/TaskDescriptionPage.js | 10 +++++++--- src/pages/tasks/TaskTitlePage.js | 10 +++++++--- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/libs/actions/Task.js b/src/libs/actions/Task.js index 959710967881..38d84504afa3 100644 --- a/src/libs/actions/Task.js +++ b/src/libs/actions/Task.js @@ -357,7 +357,7 @@ function reopenTask(taskReport) { * @param {object} report * @param {Object} editedTask */ -function editTaskAndNavigate(report, {title, description}) { +function editTask(report, {title, description}) { // Create the EditedReportAction on the task const editTaskReportAction = ReportUtils.buildOptimisticEditedTaskReportAction(currentUserEmail); @@ -917,7 +917,7 @@ function getTaskReportActionMessage(actionName, reportID, isCreateTaskAction) { export { createTaskAndNavigate, - editTaskAndNavigate, + editTask, editTaskAssigneeAndNavigate, setTitleValue, setDescriptionValue, diff --git a/src/pages/tasks/TaskDescriptionPage.js b/src/pages/tasks/TaskDescriptionPage.js index 5d496fbca6c1..d0aeb3835734 100644 --- a/src/pages/tasks/TaskDescriptionPage.js +++ b/src/pages/tasks/TaskDescriptionPage.js @@ -39,9 +39,13 @@ function TaskDescriptionPage(props) { const submit = useCallback( (values) => { - // Set the description of the report in the store and then call Task.editTaskReport - // to update the description of the report on the server - Task.editTaskAndNavigate(props.report, {description: values.description}); + if (values.description !== props.report.description) { + // Set the description of the report in the store and then call EditTask API + // to update the description of the report on the server + Task.editTask(props.report, {description: values.description}); + } + + Navigation.dismissModal(props.report.reportID); }, [props], ); diff --git a/src/pages/tasks/TaskTitlePage.js b/src/pages/tasks/TaskTitlePage.js index 375a23cc3012..4c111667e094 100644 --- a/src/pages/tasks/TaskTitlePage.js +++ b/src/pages/tasks/TaskTitlePage.js @@ -50,9 +50,13 @@ function TaskTitlePage(props) { const submit = useCallback( (values) => { - // Set the title of the report in the store and then call Task.editTaskReport - // to update the title of the report on the server - Task.editTaskAndNavigate(props.report, {title: values.title}); + if (values.title !== props.report.reportName) { + // Set the title of the report in the store and then call EditTask API + // to update the title of the report on the server + Task.editTask(props.report, {title: values.title}); + } + + Navigation.dismissModal(props.report.reportID); }, [props], ); From 70f661f0b73f41327dc67a85c33c280308185b05 Mon Sep 17 00:00:00 2001 From: VH Date: Fri, 3 Nov 2023 20:29:01 +0700 Subject: [PATCH 020/319] Update description --- src/libs/actions/Task.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/Task.js b/src/libs/actions/Task.js index 38d84504afa3..eb22d436abec 100644 --- a/src/libs/actions/Task.js +++ b/src/libs/actions/Task.js @@ -616,9 +616,9 @@ function setAssigneeValue(assigneeEmail, assigneeAccountID, shareDestination, is // This is only needed for creation of a new task and so it should only be stored locally Onyx.merge(ONYXKEYS.TASK, {assignee: assigneeEmail, assigneeAccountID}); - // When we're editing the assignee, we immediately call EditTaskAndNavigate. Since setting the assignee is async, - // the chatReport is not yet set when EditTaskAndNavigate is called. So we return the chatReport here so that - // EditTaskAndNavigate can use it. + // When we're editing the assignee, we immediately call editTaskAssigneeAndNavigate. Since setting the assignee is async, + // the chatReport is not yet set when editTaskAssigneeAndNavigate is called. So we return the chatReport here so that + // editTaskAssigneeAndNavigate can use it. return chatReport; } From 8b91fa45e7e62c656691fbb413b67f592531962e Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Mon, 6 Nov 2023 17:47:58 +0700 Subject: [PATCH 021/319] fix: 30801 --- src/libs/actions/Policy.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index 9b33ff9b086e..a2f4ec79f0ff 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -976,7 +976,7 @@ function generateCustomUnitID() { /** * @returns {Object} */ -function buildOptimisticCustomUnits() { +function buildOptimisticCustomUnits(currency) { const customUnitID = generateCustomUnitID(); const customUnitRateID = generateCustomUnitID(); const customUnits = { @@ -991,6 +991,7 @@ function buildOptimisticCustomUnits() { customUnitRateID, name: CONST.CUSTOM_UNITS.DEFAULT_RATE, rate: CONST.CUSTOM_UNITS.MILEAGE_IRS_RATE * CONST.POLICY.CUSTOM_UNIT_RATE_BASE_OFFSET, + currency, }, }, }, @@ -1013,7 +1014,8 @@ function buildOptimisticCustomUnits() { */ function createDraftInitialWorkspace(policyOwnerEmail = '', policyName = '', policyID = generatePolicyID(), makeMeAdmin = false) { const workspaceName = policyName || generateDefaultWorkspaceName(policyOwnerEmail); - const {customUnits} = buildOptimisticCustomUnits(); + const outputCurrency = lodashGet(allPersonalDetails, [sessionAccountID, 'localCurrencyCode'], CONST.CURRENCY.USD) + const {customUnits} = buildOptimisticCustomUnits(outputCurrency); const optimisticData = [ { @@ -1026,7 +1028,7 @@ function createDraftInitialWorkspace(policyOwnerEmail = '', policyName = '', pol role: CONST.POLICY.ROLE.ADMIN, owner: sessionEmail, isPolicyExpenseChatEnabled: true, - outputCurrency: lodashGet(allPersonalDetails, [sessionAccountID, 'localCurrencyCode'], CONST.CURRENCY.USD), + outputCurrency, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, customUnits, makeMeAdmin, @@ -1059,7 +1061,8 @@ function createDraftInitialWorkspace(policyOwnerEmail = '', policyName = '', pol function createWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policyName = '', policyID = generatePolicyID()) { const workspaceName = policyName || generateDefaultWorkspaceName(policyOwnerEmail); - const {customUnits, customUnitID, customUnitRateID} = buildOptimisticCustomUnits(); + const outputCurrency = lodashGet(allPersonalDetails, [sessionAccountID, 'localCurrencyCode'], CONST.CURRENCY.USD); + const {customUnits, customUnitID, customUnitRateID} = buildOptimisticCustomUnits(outputCurrency); const { announceChatReportID, @@ -1105,7 +1108,7 @@ function createWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policyName role: CONST.POLICY.ROLE.ADMIN, owner: sessionEmail, isPolicyExpenseChatEnabled: true, - outputCurrency: lodashGet(allPersonalDetails, [sessionAccountID, 'localCurrencyCode'], CONST.CURRENCY.USD), + outputCurrency, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, customUnits, }, From f2302a6f962c32835d96d4632a82cb0fa98be8ea Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Mon, 6 Nov 2023 17:15:00 +0100 Subject: [PATCH 022/319] Revert "Revert "[Form Provider Refactor] RoomNameInput"" This reverts commit c01af10e --- src/components/Form/FormProvider.js | 24 ++++--- src/components/Form/InputWrapper.js | 2 + src/components/RoomNameInput/index.js | 50 ++++---------- src/components/RoomNameInput/index.native.js | 66 ------------------- .../RoomNameInput/roomNameInputPropTypes.js | 6 +- src/pages/settings/Report/RoomNamePage.js | 8 +-- src/pages/workspace/WorkspaceNewRoomPage.js | 21 +++--- 7 files changed, 49 insertions(+), 128 deletions(-) delete mode 100644 src/components/RoomNameInput/index.native.js diff --git a/src/components/Form/FormProvider.js b/src/components/Form/FormProvider.js index 92baa9727832..87bdd5b5c72a 100644 --- a/src/components/Form/FormProvider.js +++ b/src/components/Form/FormProvider.js @@ -229,6 +229,8 @@ function FormProvider({validate, formID, shouldValidateOnBlur, shouldValidateOnC .first() .value() || ''; + const value = !_.isUndefined(inputValues[`${inputID}ToDisplay`]) ? inputValues[`${inputID}ToDisplay`] : inputValues[inputID]; + return { ...propsToParse, ref: @@ -241,7 +243,7 @@ function FormProvider({validate, formID, shouldValidateOnBlur, shouldValidateOnC inputID, key: propsToParse.key || inputID, errorText: errors[inputID] || fieldErrorMessage, - value: inputValues[inputID], + value, // As the text input is controlled, we never set the defaultValue prop // as this is already happening by the value prop. defaultValue: undefined, @@ -281,13 +283,19 @@ function FormProvider({validate, formID, shouldValidateOnBlur, shouldValidateOnC propsToParse.onBlur(event); } }, - onInputChange: (value, key) => { + onInputChange: (inputValue, key) => { const inputKey = key || inputID; setInputValues((prevState) => { - const newState = { - ...prevState, - [inputKey]: value, - }; + const newState = _.isFunction(propsToParse.valueParser) + ? { + ...prevState, + [inputKey]: propsToParse.valueParser(inputValue), + [`${inputKey}ToDisplay`]: inputValue, + } + : { + ...prevState, + [inputKey]: inputValue, + }; if (shouldValidateOnChange) { onValidate(newState); @@ -296,11 +304,11 @@ function FormProvider({validate, formID, shouldValidateOnBlur, shouldValidateOnC }); if (propsToParse.shouldSaveDraft) { - FormActions.setDraftValues(propsToParse.formID, {[inputKey]: value}); + FormActions.setDraftValues(propsToParse.formID, {[inputKey]: inputValue}); } if (_.isFunction(propsToParse.onValueChange)) { - propsToParse.onValueChange(value, inputKey); + propsToParse.onValueChange(inputValue, inputKey); } }, }; diff --git a/src/components/Form/InputWrapper.js b/src/components/Form/InputWrapper.js index 99237fd8db43..6569fd315e23 100644 --- a/src/components/Form/InputWrapper.js +++ b/src/components/Form/InputWrapper.js @@ -7,11 +7,13 @@ const propTypes = { inputID: PropTypes.string.isRequired, valueType: PropTypes.string, forwardedRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({current: PropTypes.instanceOf(React.Component)})]), + valueParser: PropTypes.func, }; const defaultProps = { forwardedRef: undefined, valueType: 'string', + valueParser: undefined, }; function InputWrapper(props) { diff --git a/src/components/RoomNameInput/index.js b/src/components/RoomNameInput/index.js index 3f23a47d5f00..e659f92384b1 100644 --- a/src/components/RoomNameInput/index.js +++ b/src/components/RoomNameInput/index.js @@ -1,49 +1,23 @@ -import React, {useState} from 'react'; -import _ from 'underscore'; +import React from 'react'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; import * as RoomNameInputUtils from '@libs/RoomNameInputUtils'; import CONST from '@src/CONST'; import * as roomNameInputPropTypes from './roomNameInputPropTypes'; +import InputWrapper from "@components/Form/InputWrapper"; +import getOperatingSystem from "@libs/getOperatingSystem"; -function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, value, onBlur, onChangeText, onInputChange, shouldDelayFocus}) { +function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, onBlur, shouldDelayFocus, inputID}) { const {translate} = useLocalize(); - const [selection, setSelection] = useState(); + const keyboardType = getOperatingSystem() === CONST.OS.IOS ? CONST.KEYBOARD_TYPE.ASCII_CAPABLE : CONST.KEYBOARD_TYPE.VISIBLE_PASSWORD; - /** - * Calls the onChangeText callback with a modified room name - * @param {Event} event - */ - const setModifiedRoomName = (event) => { - const roomName = event.nativeEvent.text; - const modifiedRoomName = RoomNameInputUtils.modifyRoomName(roomName); - onChangeText(modifiedRoomName); - - // if custom component has onInputChange, use it to trigger changes (Form input) - if (_.isFunction(onInputChange)) { - onInputChange(modifiedRoomName); - } - - // Prevent cursor jump behaviour: - // Check if newRoomNameWithHash is the same as modifiedRoomName - // If it is then the room name is valid (does not contain unallowed characters); no action required - // If not then the room name contains unvalid characters and we must adjust the cursor position manually - // Read more: https://github.com/Expensify/App/issues/12741 - const oldRoomNameWithHash = value || ''; - const newRoomNameWithHash = `${CONST.POLICY.ROOM_PREFIX}${roomName}`; - if (modifiedRoomName !== newRoomNameWithHash) { - const offset = modifiedRoomName.length - oldRoomNameWithHash.length; - const newSelection = { - start: selection.start + offset, - end: selection.end + offset, - }; - setSelection(newSelection); - } - }; + const valueParser = (roomName) => RoomNameInputUtils.modifyRoomName(roomName); return ( - setSelection(event.nativeEvent.selection)} errorText={errorText} + valueParser={valueParser} autoCapitalize="none" onBlur={() => isFocused && onBlur()} shouldDelayFocus={shouldDelayFocus} @@ -63,6 +34,7 @@ function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, maxLength={CONST.REPORT.MAX_ROOM_NAME_LENGTH} spellCheck={false} shouldInterceptSwipe + keyboardType={keyboardType} // this is a bit hacky solution to a RN issue https://github.com/facebook/react-native/issues/27449 /> ); } diff --git a/src/components/RoomNameInput/index.native.js b/src/components/RoomNameInput/index.native.js deleted file mode 100644 index d9b592b1537d..000000000000 --- a/src/components/RoomNameInput/index.native.js +++ /dev/null @@ -1,66 +0,0 @@ -import React from 'react'; -import _ from 'underscore'; -import TextInput from '@components/TextInput'; -import useLocalize from '@hooks/useLocalize'; -import getOperatingSystem from '@libs/getOperatingSystem'; -import * as RoomNameInputUtils from '@libs/RoomNameInputUtils'; -import CONST from '@src/CONST'; -import * as roomNameInputPropTypes from './roomNameInputPropTypes'; - -function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, value, onBlur, onChangeText, onInputChange, shouldDelayFocus}) { - const {translate} = useLocalize(); - - /** - * Calls the onChangeText callback with a modified room name - * @param {Event} event - */ - const setModifiedRoomName = (event) => { - const roomName = event.nativeEvent.text; - const modifiedRoomName = RoomNameInputUtils.modifyRoomName(roomName); - onChangeText(modifiedRoomName); - - // if custom component has onInputChange, use it to trigger changes (Form input) - if (_.isFunction(onInputChange)) { - onInputChange(modifiedRoomName); - } - }; - - const keyboardType = getOperatingSystem() === CONST.OS.IOS ? CONST.KEYBOARD_TYPE.ASCII_CAPABLE : CONST.KEYBOARD_TYPE.VISIBLE_PASSWORD; - - return ( - isFocused && onBlur()} - autoFocus={isFocused && autoFocus} - autoCapitalize="none" - shouldDelayFocus={shouldDelayFocus} - /> - ); -} - -RoomNameInput.propTypes = roomNameInputPropTypes.propTypes; -RoomNameInput.defaultProps = roomNameInputPropTypes.defaultProps; -RoomNameInput.displayName = 'RoomNameInput'; - -const RoomNameInputWithRef = React.forwardRef((props, ref) => ( - -)); - -RoomNameInputWithRef.displayName = 'RoomNameInputWithRef'; - -export default RoomNameInputWithRef; diff --git a/src/components/RoomNameInput/roomNameInputPropTypes.js b/src/components/RoomNameInput/roomNameInputPropTypes.js index 7f8292f0123e..3d1ad18d27b3 100644 --- a/src/components/RoomNameInput/roomNameInputPropTypes.js +++ b/src/components/RoomNameInput/roomNameInputPropTypes.js @@ -1,4 +1,5 @@ import PropTypes from 'prop-types'; +import refPropTypes from '../refPropTypes'; const propTypes = { /** Callback to execute when the text input is modified correctly */ @@ -14,10 +15,10 @@ const propTypes = { errorText: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), /** A ref forwarded to the TextInput */ - forwardedRef: PropTypes.func, + forwardedRef: refPropTypes, /** The ID used to uniquely identify the input in a Form */ - inputID: PropTypes.string, + inputID: PropTypes.string.isRequired, /** Callback that is called when the text input is blurred */ onBlur: PropTypes.func, @@ -39,7 +40,6 @@ const defaultProps = { errorText: '', forwardedRef: () => {}, - inputID: undefined, onBlur: () => {}, autoFocus: false, shouldDelayFocus: false, diff --git a/src/pages/settings/Report/RoomNamePage.js b/src/pages/settings/Report/RoomNamePage.js index 2ff9b1f1108f..937da4565604 100644 --- a/src/pages/settings/Report/RoomNamePage.js +++ b/src/pages/settings/Report/RoomNamePage.js @@ -4,7 +4,6 @@ import React, {useCallback, useRef} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; -import Form from '@components/Form'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import RoomNameInput from '@components/RoomNameInput'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -21,6 +20,7 @@ import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import FormProvider from "@components/Form/FormProvider"; const propTypes = { ...withLocalizePropTypes, @@ -90,7 +90,7 @@ function RoomNamePage(props) { title={translate('newRoomPage.roomName')} onBackButtonPress={() => Navigation.goBack(ROUTES.REPORT_SETTINGS.getRoute(report.reportID))} /> -
Report.updatePolicyRoomNameAndNavigate(report, values.roomName)} @@ -100,13 +100,13 @@ function RoomNamePage(props) { > (roomNameInputRef.current = ref)} + ref={roomNameInputRef} inputID="roomName" defaultValue={report.reportName} isFocused={isFocused} /> -
+ ); diff --git a/src/pages/workspace/WorkspaceNewRoomPage.js b/src/pages/workspace/WorkspaceNewRoomPage.js index 271dc45026c7..05cc8df6a547 100644 --- a/src/pages/workspace/WorkspaceNewRoomPage.js +++ b/src/pages/workspace/WorkspaceNewRoomPage.js @@ -4,7 +4,6 @@ import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; -import Form from '@components/Form'; import KeyboardAvoidingView from '@components/KeyboardAvoidingView'; import OfflineIndicator from '@components/OfflineIndicator'; import RoomNameInput from '@components/RoomNameInput'; @@ -30,6 +29,8 @@ import * as App from '@userActions/App'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import FormProvider from "@components/Form/FormProvider"; +import InputWrapper from "@components/Form/InputWrapper"; const propTypes = { /** All reports shared with the user */ @@ -189,7 +190,7 @@ function WorkspaceNewRoomPage(props) { // This is because when wrapping whole screen the screen was freezing when changing Tabs. keyboardVerticalOffset={variables.contentHeaderHeight + variables.tabSelectorButtonHeight + variables.tabSelectorButtonPadding + insets.top} > -
(roomNameInputRef.current = el)} + ref={roomNameInputRef} inputID="roomName" isFocused={props.isFocused} shouldDelayFocus @@ -207,7 +208,8 @@ function WorkspaceNewRoomPage(props) { /> - - {isPolicyAdmin && ( - )} - {visibilityDescription} - + {isSmallScreenWidth && } )} From 331e2a80c1be738b3382aa8a1030c15a2e6397cc Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 7 Nov 2023 17:12:27 +0700 Subject: [PATCH 023/319] fix lint --- .../ComposerWithSuggestions/ComposerWithSuggestions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index 7f2ef5a0fb4a..64414f1d89e5 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -153,7 +153,7 @@ function ComposerWithSuggestions({ })); const [composerHeight, setComposerHeight] = useState(0); - + const insertedEmojisRef = useRef([]); // A flag to indicate whether the onScroll callback is likely triggered by a layout change (caused by text change) or not From 2b7be0a1c964084bf6143fb47eef948a8ae7b067 Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 7 Nov 2023 17:34:46 +0700 Subject: [PATCH 024/319] refactor code --- .../ComposerWithSuggestions.js | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index 64414f1d89e5..f6e97a838cf2 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -127,22 +127,6 @@ function ComposerWithSuggestions({ const isEmptyChat = useMemo(() => _.size(reportActions) === 1, [reportActions]); const parentAction = ReportActionsUtils.getParentReportAction(report); const shouldAutoFocus = !modal.isVisible && (shouldFocusInputOnScreenFocus || (isEmptyChat && !ReportActionsUtils.isTransactionThread(parentAction))) && shouldShowComposeInput; - const textInputRef = useRef(null); - const shouldAutoFocusRef = useRef(false); - - shouldAutoFocusRef.current = shouldAutoFocus; - - const prevIsFocused = usePrevious(isFocused); - - useEffect(() => { - if (!prevIsFocused && isFocused) { - setTimeout(() => { - if (shouldAutoFocusRef.current) { - textInputRef.current.focus(); - } - }, CONST.ANIMATED_TRANSITION); - } - }, [isFocused, prevIsFocused]); const valueRef = useRef(value); valueRef.current = value; @@ -154,6 +138,7 @@ function ComposerWithSuggestions({ const [composerHeight, setComposerHeight] = useState(0); + const textInputRef = useRef(null); const insertedEmojisRef = useRef([]); // A flag to indicate whether the onScroll callback is likely triggered by a layout change (caused by text change) or not @@ -547,6 +532,7 @@ function ComposerWithSuggestions({ }, [focusComposerOnKeyPress, navigation, setUpComposeFocusManager]); const prevIsModalVisible = usePrevious(modal.isVisible); + const prevIsFocused = usePrevious(isFocused); useEffect(() => { if (modal.isVisible && !prevIsModalVisible) { @@ -566,6 +552,24 @@ function ComposerWithSuggestions({ } focus(); }, [focus, prevIsFocused, editFocused, prevIsModalVisible, isFocused, modal.isVisible, isNextModalWillOpenRef]); + + const shouldAutoFocusRef = useRef(false); + shouldAutoFocusRef.current = shouldAutoFocus; + + useEffect(() => { + if (prevIsFocused || !isFocused) { + return; + } + // Focus composer when navigating from another screen + const focusTimeout = setTimeout(() => { + if (!shouldAutoFocusRef.current) { + return; + } + textInputRef.current.focus(); + }, CONST.ANIMATED_TRANSITION); + return () => clearTimeout(focusTimeout); + }, [isFocused, prevIsFocused]); + useEffect(() => { // Scrolls the composer to the bottom and sets the selection to the end, so that longer drafts are easier to edit updateMultilineInputRange(textInputRef.current, shouldAutoFocus); From 4294da8d50d5a1ce762695e8b9273934e135cb18 Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 7 Nov 2023 17:35:36 +0700 Subject: [PATCH 025/319] fix lint --- .../ComposerWithSuggestions/ComposerWithSuggestions.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index f6e97a838cf2..0b572bce2d81 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -533,7 +533,6 @@ function ComposerWithSuggestions({ const prevIsModalVisible = usePrevious(modal.isVisible); const prevIsFocused = usePrevious(isFocused); - useEffect(() => { if (modal.isVisible && !prevIsModalVisible) { // eslint-disable-next-line no-param-reassign @@ -555,7 +554,6 @@ function ComposerWithSuggestions({ const shouldAutoFocusRef = useRef(false); shouldAutoFocusRef.current = shouldAutoFocus; - useEffect(() => { if (prevIsFocused || !isFocused) { return; From 1b637a6cdcedf2cc7abdac829459ec677274bd70 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Tue, 7 Nov 2023 15:32:28 +0100 Subject: [PATCH 026/319] Fix room name parsing and not displaying room name in room name editor --- src/components/Form/FormProvider.js | 4 ++-- src/components/Form/InputWrapper.js | 5 ++++- src/components/RoomNameInput/index.js | 6 +++++- .../RoomNameInput/roomNameInputPropTypes.js | 3 +++ src/libs/RoomNameInputUtils.ts | 4 ++-- src/pages/settings/Report/RoomNamePage.js | 11 ++++------- 6 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/components/Form/FormProvider.js b/src/components/Form/FormProvider.js index 87bdd5b5c72a..96c575a76e17 100644 --- a/src/components/Form/FormProvider.js +++ b/src/components/Form/FormProvider.js @@ -2,7 +2,7 @@ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {createRef, useCallback, useMemo, useRef, useState} from 'react'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; + import _ from 'underscore'; import networkPropTypes from '@components/networkPropTypes'; import {withNetwork} from '@components/OnyxProvider'; import compose from '@libs/compose'; @@ -290,7 +290,7 @@ function FormProvider({validate, formID, shouldValidateOnBlur, shouldValidateOnC ? { ...prevState, [inputKey]: propsToParse.valueParser(inputValue), - [`${inputKey}ToDisplay`]: inputValue, + [`${inputKey}ToDisplay`]: _.isFunction(propsToParse.displayParser) ? propsToParse.displayParser(inputValue) : inputValue, } : { ...prevState, diff --git a/src/components/Form/InputWrapper.js b/src/components/Form/InputWrapper.js index 6569fd315e23..aef6f5cdc8b4 100644 --- a/src/components/Form/InputWrapper.js +++ b/src/components/Form/InputWrapper.js @@ -1,19 +1,22 @@ import PropTypes from 'prop-types'; import React, {forwardRef, useContext} from 'react'; import FormContext from './FormContext'; +import refPropTypes from "@components/refPropTypes"; const propTypes = { InputComponent: PropTypes.oneOfType([PropTypes.func, PropTypes.elementType]).isRequired, inputID: PropTypes.string.isRequired, valueType: PropTypes.string, - forwardedRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({current: PropTypes.instanceOf(React.Component)})]), + forwardedRef: refPropTypes, valueParser: PropTypes.func, + displayParser: PropTypes.func }; const defaultProps = { forwardedRef: undefined, valueType: 'string', valueParser: undefined, + displayParser: undefined, }; function InputWrapper(props) { diff --git a/src/components/RoomNameInput/index.js b/src/components/RoomNameInput/index.js index e659f92384b1..8801605db1c7 100644 --- a/src/components/RoomNameInput/index.js +++ b/src/components/RoomNameInput/index.js @@ -3,16 +3,18 @@ import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; import * as RoomNameInputUtils from '@libs/RoomNameInputUtils'; import CONST from '@src/CONST'; +import _ from 'underscore'; import * as roomNameInputPropTypes from './roomNameInputPropTypes'; import InputWrapper from "@components/Form/InputWrapper"; import getOperatingSystem from "@libs/getOperatingSystem"; -function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, onBlur, shouldDelayFocus, inputID}) { +function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, onBlur, shouldDelayFocus, inputID, roomName}) { const {translate} = useLocalize(); const keyboardType = getOperatingSystem() === CONST.OS.IOS ? CONST.KEYBOARD_TYPE.ASCII_CAPABLE : CONST.KEYBOARD_TYPE.VISIBLE_PASSWORD; const valueParser = (roomName) => RoomNameInputUtils.modifyRoomName(roomName); + const displayParser = (roomName) => RoomNameInputUtils.modifyRoomName(roomName, true); return ( isFocused && onBlur()} shouldDelayFocus={shouldDelayFocus} autoFocus={isFocused && autoFocus} maxLength={CONST.REPORT.MAX_ROOM_NAME_LENGTH} + defaultValue={roomName} spellCheck={false} shouldInterceptSwipe keyboardType={keyboardType} // this is a bit hacky solution to a RN issue https://github.com/facebook/react-native/issues/27449 diff --git a/src/components/RoomNameInput/roomNameInputPropTypes.js b/src/components/RoomNameInput/roomNameInputPropTypes.js index 3d1ad18d27b3..c9c5db7f74c0 100644 --- a/src/components/RoomNameInput/roomNameInputPropTypes.js +++ b/src/components/RoomNameInput/roomNameInputPropTypes.js @@ -31,6 +31,8 @@ const propTypes = { /** Whether navigation is focused */ isFocused: PropTypes.bool.isRequired, + + roomName: PropTypes.string }; const defaultProps = { @@ -43,6 +45,7 @@ const defaultProps = { onBlur: () => {}, autoFocus: false, shouldDelayFocus: false, + roomName: '', }; export {propTypes, defaultProps}; diff --git a/src/libs/RoomNameInputUtils.ts b/src/libs/RoomNameInputUtils.ts index cff0bbc30274..e6f7d420bf59 100644 --- a/src/libs/RoomNameInputUtils.ts +++ b/src/libs/RoomNameInputUtils.ts @@ -3,14 +3,14 @@ import CONST from '@src/CONST'; /** * Replaces spaces with dashes */ -function modifyRoomName(roomName: string): string { +function modifyRoomName(roomName: string, skipPolicyPrefix?: boolean): string { const modifiedRoomNameWithoutHash = roomName .replace(/ /g, '-') // Replaces the smart dash on iOS devices with two hyphens .replace(/—/g, '--'); - return `${CONST.POLICY.ROOM_PREFIX}${modifiedRoomNameWithoutHash}`; + return skipPolicyPrefix ? modifiedRoomNameWithoutHash : `${CONST.POLICY.ROOM_PREFIX}${modifiedRoomNameWithoutHash}`; } export { diff --git a/src/pages/settings/Report/RoomNamePage.js b/src/pages/settings/Report/RoomNamePage.js index 937da4565604..d3a3c6058f62 100644 --- a/src/pages/settings/Report/RoomNamePage.js +++ b/src/pages/settings/Report/RoomNamePage.js @@ -21,6 +21,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import FormProvider from "@components/Form/FormProvider"; +import _ from "underscore"; const propTypes = { ...withLocalizePropTypes, @@ -42,13 +43,8 @@ const defaultProps = { policy: {}, }; -function RoomNamePage(props) { - const policy = props.policy; - const report = props.report; - const reports = props.reports; - const translate = props.translate; - - const roomNameInputRef = useRef(null); +function RoomNamePage({policy, report, reports, translate}) { + const roomNameInputRef = useRef(null); const isFocused = useIsFocused(); const validate = useCallback( @@ -104,6 +100,7 @@ function RoomNamePage(props) { inputID="roomName" defaultValue={report.reportName} isFocused={isFocused} + roomName={report.reportName.slice(1)} /> From f0a6fd151a5d070ac75df6f7015c413eccd5c85b Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Tue, 7 Nov 2023 16:27:15 +0100 Subject: [PATCH 027/319] Lint fixes --- src/components/Form/FormProvider.js | 2 +- src/components/Form/InputWrapper.js | 4 ++-- src/components/RoomNameInput/index.js | 9 ++++----- src/components/RoomNameInput/roomNameInputPropTypes.js | 2 +- src/pages/settings/Report/RoomNamePage.js | 5 ++--- src/pages/workspace/WorkspaceNewRoomPage.js | 4 ++-- 6 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/components/Form/FormProvider.js b/src/components/Form/FormProvider.js index 96c575a76e17..48a381ac041e 100644 --- a/src/components/Form/FormProvider.js +++ b/src/components/Form/FormProvider.js @@ -2,7 +2,7 @@ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {createRef, useCallback, useMemo, useRef, useState} from 'react'; import {withOnyx} from 'react-native-onyx'; - import _ from 'underscore'; +import _ from 'underscore'; import networkPropTypes from '@components/networkPropTypes'; import {withNetwork} from '@components/OnyxProvider'; import compose from '@libs/compose'; diff --git a/src/components/Form/InputWrapper.js b/src/components/Form/InputWrapper.js index aef6f5cdc8b4..36dca6ffee61 100644 --- a/src/components/Form/InputWrapper.js +++ b/src/components/Form/InputWrapper.js @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import React, {forwardRef, useContext} from 'react'; +import refPropTypes from '@components/refPropTypes'; import FormContext from './FormContext'; -import refPropTypes from "@components/refPropTypes"; const propTypes = { InputComponent: PropTypes.oneOfType([PropTypes.func, PropTypes.elementType]).isRequired, @@ -9,7 +9,7 @@ const propTypes = { valueType: PropTypes.string, forwardedRef: refPropTypes, valueParser: PropTypes.func, - displayParser: PropTypes.func + displayParser: PropTypes.func, }; const defaultProps = { diff --git a/src/components/RoomNameInput/index.js b/src/components/RoomNameInput/index.js index 60d6181bcb81..39cf22bc98f3 100644 --- a/src/components/RoomNameInput/index.js +++ b/src/components/RoomNameInput/index.js @@ -1,20 +1,19 @@ import React from 'react'; +import InputWrapper from '@components/Form/InputWrapper'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; +import getOperatingSystem from '@libs/getOperatingSystem'; import * as RoomNameInputUtils from '@libs/RoomNameInputUtils'; import CONST from '@src/CONST'; -import _ from 'underscore'; import * as roomNameInputPropTypes from './roomNameInputPropTypes'; -import InputWrapper from "@components/Form/InputWrapper"; -import getOperatingSystem from "@libs/getOperatingSystem"; function RoomNameInput({isFocused, autoFocus, disabled, errorText, forwardedRef, onBlur, shouldDelayFocus, inputID, roomName}) { const {translate} = useLocalize(); const keyboardType = getOperatingSystem() === CONST.OS.IOS ? CONST.KEYBOARD_TYPE.ASCII_CAPABLE : CONST.KEYBOARD_TYPE.VISIBLE_PASSWORD; - const valueParser = (roomName) => RoomNameInputUtils.modifyRoomName(roomName); - const displayParser = (roomName) => RoomNameInputUtils.modifyRoomName(roomName, true); + const valueParser = (innerRoomName) => RoomNameInputUtils.modifyRoomName(innerRoomName); + const displayParser = (innerRoomName) => RoomNameInputUtils.modifyRoomName(innerRoomName, true); return ( Date: Tue, 7 Nov 2023 16:29:05 +0100 Subject: [PATCH 028/319] Fix lint issues --- src/components/Form/InputWrapper.js | 2 +- src/components/RoomNameInput/roomNameInputPropTypes.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Form/InputWrapper.js b/src/components/Form/InputWrapper.js index 36dca6ffee61..9fe2eddf3d64 100644 --- a/src/components/Form/InputWrapper.js +++ b/src/components/Form/InputWrapper.js @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import React, {forwardRef, useContext} from 'react'; import refPropTypes from '@components/refPropTypes'; -import FormContext from './FormContext'; +import FormContext from "@components/Form/FormContext"; const propTypes = { InputComponent: PropTypes.oneOfType([PropTypes.func, PropTypes.elementType]).isRequired, diff --git a/src/components/RoomNameInput/roomNameInputPropTypes.js b/src/components/RoomNameInput/roomNameInputPropTypes.js index 476464c7835d..29633f4dfedd 100644 --- a/src/components/RoomNameInput/roomNameInputPropTypes.js +++ b/src/components/RoomNameInput/roomNameInputPropTypes.js @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import refPropTypes from '../refPropTypes'; +import refPropTypes from "@components/refPropTypes"; const propTypes = { /** Callback to execute when the text input is modified correctly */ From 9e7cac008910931c40769635d196a79f444930d1 Mon Sep 17 00:00:00 2001 From: VH Date: Wed, 8 Nov 2023 09:51:20 +0700 Subject: [PATCH 029/319] Remove redudant navigation --- src/libs/actions/Task.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libs/actions/Task.js b/src/libs/actions/Task.js index afa1d2ebc891..b2a9ebd7866c 100644 --- a/src/libs/actions/Task.js +++ b/src/libs/actions/Task.js @@ -433,8 +433,6 @@ function editTask(report, {title, description}) { }, {optimisticData, successData, failureData}, ); - - Navigation.dismissModal(report.reportID); } function editTaskAssigneeAndNavigate(report, ownerAccountID, assigneeEmail, assigneeAccountID = 0, assigneeChatReport = null) { From 5464fc524817ebf58b06e5079a0961a652823fa6 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 8 Nov 2023 16:24:25 +0700 Subject: [PATCH 030/319] remove clearTimeout --- .../ComposerWithSuggestions/ComposerWithSuggestions.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index 224dae018204..a38d88c1c5ec 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -507,13 +507,12 @@ function ComposerWithSuggestions({ return; } // Focus composer when navigating from another screen - const focusTimeout = setTimeout(() => { + setTimeout(() => { if (!shouldAutoFocusRef.current) { return; } textInputRef.current.focus(); }, CONST.ANIMATED_TRANSITION); - return () => clearTimeout(focusTimeout); }, [isFocused, prevIsFocused]); useEffect(() => { From 580c6895f5f12bf902059942591a1ef34a07baf4 Mon Sep 17 00:00:00 2001 From: Kamil Owczarz Date: Wed, 8 Nov 2023 11:12:57 +0100 Subject: [PATCH 031/319] Fix lint --- src/components/Form/InputWrapper.js | 2 +- src/components/RoomNameInput/roomNameInputPropTypes.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Form/InputWrapper.js b/src/components/Form/InputWrapper.js index 9fe2eddf3d64..36dca6ffee61 100644 --- a/src/components/Form/InputWrapper.js +++ b/src/components/Form/InputWrapper.js @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import React, {forwardRef, useContext} from 'react'; import refPropTypes from '@components/refPropTypes'; -import FormContext from "@components/Form/FormContext"; +import FormContext from './FormContext'; const propTypes = { InputComponent: PropTypes.oneOfType([PropTypes.func, PropTypes.elementType]).isRequired, diff --git a/src/components/RoomNameInput/roomNameInputPropTypes.js b/src/components/RoomNameInput/roomNameInputPropTypes.js index 29633f4dfedd..f457e4e2a494 100644 --- a/src/components/RoomNameInput/roomNameInputPropTypes.js +++ b/src/components/RoomNameInput/roomNameInputPropTypes.js @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import refPropTypes from "@components/refPropTypes"; +import refPropTypes from '@components/refPropTypes'; const propTypes = { /** Callback to execute when the text input is modified correctly */ From 46d6a38a827b38878badca7aaec39e84bcf7b260 Mon Sep 17 00:00:00 2001 From: Abdelrahman Khattab Date: Wed, 8 Nov 2023 20:45:05 +0200 Subject: [PATCH 032/319] Introducing usePersonalDetails hoc in places that use withCurrentUserPersonalDetails --- .../HTMLRenderers/MentionUserRenderer.js | 19 ++++--------------- src/components/ReportActionItem/TaskView.js | 4 +++- src/pages/RoomMembersPage.js | 14 ++++---------- .../ReportActionCompose.js | 13 ++----------- src/pages/home/report/ReportActionsList.js | 1 - .../iou/steps/MoneyRequestConfirmPage.js | 15 +++++---------- src/pages/tasks/TaskAssigneeSelectorModal.js | 14 ++++---------- 7 files changed, 22 insertions(+), 58 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js index b7b7c43e7b58..ee2e5ff34915 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.js @@ -1,14 +1,13 @@ import lodashGet from 'lodash/get'; import React from 'react'; -import {withOnyx} from 'react-native-onyx'; import {TNodeChildrenRenderer} from 'react-native-render-html'; import _ from 'underscore'; +import {usePersonalDetails} from '@components/OnyxProvider'; import {ShowContextMenuContext, showContextMenuForReport} from '@components/ShowContextMenuContext'; import Text from '@components/Text'; import UserDetailsTooltip from '@components/UserDetailsTooltip'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; -import compose from '@libs/compose'; import * as LocalePhoneNumber from '@libs/LocalePhoneNumber'; import Navigation from '@libs/Navigation/Navigation'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; @@ -17,7 +16,6 @@ import personalDetailsPropType from '@pages/personalDetailsPropType'; import styles from '@styles/styles'; import * as StyleUtils from '@styles/StyleUtils'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import htmlRendererPropTypes from './htmlRendererPropTypes'; @@ -26,22 +24,20 @@ const propTypes = { /** Current user personal details */ currentUserPersonalDetails: personalDetailsPropType.isRequired, - - /** Personal details of all users */ - personalDetails: personalDetailsPropType.isRequired, }; function MentionUserRenderer(props) { const {translate} = useLocalize(); const defaultRendererProps = _.omit(props, ['TDefaultRenderer', 'style']); const htmlAttribAccountID = lodashGet(props.tnode.attributes, 'accountid'); + const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; let accountID; let displayNameOrLogin; let navigationRoute; if (!_.isEmpty(htmlAttribAccountID)) { - const user = lodashGet(props.personalDetails, htmlAttribAccountID); + const user = lodashGet(personalDetails, htmlAttribAccountID); accountID = parseInt(htmlAttribAccountID, 10); displayNameOrLogin = LocalePhoneNumber.formatPhoneNumber(lodashGet(user, 'login', '')) || lodashGet(user, 'displayName', '') || translate('common.hidden'); navigationRoute = ROUTES.PROFILE.getRoute(htmlAttribAccountID); @@ -97,11 +93,4 @@ function MentionUserRenderer(props) { MentionUserRenderer.propTypes = propTypes; MentionUserRenderer.displayName = 'MentionUserRenderer'; -export default compose( - withCurrentUserPersonalDetails, - withOnyx({ - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, - }), -)(MentionUserRenderer); +export default withCurrentUserPersonalDetails(MentionUserRenderer); diff --git a/src/components/ReportActionItem/TaskView.js b/src/components/ReportActionItem/TaskView.js index b12d6ae32128..5bd0523ca694 100644 --- a/src/components/ReportActionItem/TaskView.js +++ b/src/components/ReportActionItem/TaskView.js @@ -9,6 +9,7 @@ import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import {usePersonalDetails} from '@components/OnyxProvider'; import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction'; import SpacerView from '@components/SpacerView'; import Text from '@components/Text'; @@ -53,6 +54,7 @@ function TaskView(props) { const canModifyTask = Task.canModifyTask(props.report, props.currentUserPersonalDetails.accountID); const disableState = !canModifyTask; const isDisableInteractive = !canModifyTask || !isOpen; + const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; return ( @@ -147,7 +149,7 @@ function TaskView(props) { { - const details = props.personalDetails[accountID]; + const details = personalDetails[accountID]; if (!details) { Log.hmmm(`[RoomMembersPage] no personal details found for room member with accountID: ${accountID}`); @@ -298,7 +295,7 @@ function RoomMembersPage(props) { headerMessage={headerMessage} onSelectRow={(item) => toggleUser(item.keyForList)} onSelectAll={() => toggleAllUsers(data)} - showLoadingPlaceholder={!OptionsListUtils.isPersonalDetailsReady(props.personalDetails) || !didLoadRoomMembers} + showLoadingPlaceholder={!OptionsListUtils.isPersonalDetailsReady(personalDetails) || !didLoadRoomMembers} showScrollIndicator shouldPreventDefaultFocusOnSelectRow={!Browser.isMobile()} /> @@ -318,9 +315,6 @@ export default compose( withWindowDimensions, withReportOrNotFound(), withOnyx({ - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, session: { key: ONYXKEYS.SESSION, }, diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js index c0a1151f0202..77ad2ff12308 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js @@ -11,8 +11,7 @@ import EmojiPickerButton from '@components/EmojiPicker/EmojiPickerButton'; import ExceededCommentLength from '@components/ExceededCommentLength'; import OfflineIndicator from '@components/OfflineIndicator'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import {withNetwork} from '@components/OnyxProvider'; -import participantPropTypes from '@components/participantPropTypes'; +import {usePersonalDetails, withNetwork} from '@components/OnyxProvider'; import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -49,9 +48,6 @@ const propTypes = { /** Array of report actions for this report */ reportActions: PropTypes.arrayOf(PropTypes.shape(reportActionPropTypes)), - /** Personal details of all the users */ - personalDetails: PropTypes.objectOf(participantPropTypes), - /** The report currently being looked at */ report: reportPropTypes, @@ -84,7 +80,6 @@ const propTypes = { const defaultProps = { report: {}, blockedFromConcierge: {}, - personalDetails: {}, preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE, isComposerFullSize: false, pendingAction: null, @@ -108,7 +103,6 @@ function ReportActionCompose({ network, onSubmit, pendingAction, - personalDetails, report, reportID, reportActions, @@ -120,7 +114,7 @@ function ReportActionCompose({ const {isMediumScreenWidth, isSmallScreenWidth} = useWindowDimensions(); const animatedRef = useAnimatedRef(); const actionButtonRef = useRef(null); - + const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; /** * Updates the Highlight state of the composer */ @@ -473,9 +467,6 @@ export default compose( blockedFromConcierge: { key: ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE, }, - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, shouldShowComposeInput: { key: ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT, }, diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 759e73aa90e5..3240df2f89e3 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -70,7 +70,6 @@ const propTypes = { }; const defaultProps = { - personalDetails: {}, onScroll: () => {}, mostRecentIOUReportActionID: '', isLoadingInitialReportActions: false, diff --git a/src/pages/iou/steps/MoneyRequestConfirmPage.js b/src/pages/iou/steps/MoneyRequestConfirmPage.js index 6b570ee872c3..ad3500326ca0 100644 --- a/src/pages/iou/steps/MoneyRequestConfirmPage.js +++ b/src/pages/iou/steps/MoneyRequestConfirmPage.js @@ -7,6 +7,7 @@ import _ from 'underscore'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; import MoneyRequestConfirmationList from '@components/MoneyRequestConfirmationList'; +import {usePersonalDetails} from '@components/OnyxProvider'; import ScreenWrapper from '@components/ScreenWrapper'; import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; import withLocalize from '@components/withLocalize'; @@ -20,7 +21,6 @@ import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; import {iouDefaultProps, iouPropTypes} from '@pages/iou/propTypes'; -import personalDetailsPropType from '@pages/personalDetailsPropType'; import reportPropTypes from '@pages/reportPropTypes'; import styles from '@styles/styles'; import * as IOU from '@userActions/IOU'; @@ -47,15 +47,11 @@ const propTypes = { /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ iou: iouPropTypes, - /** Personal details of all users */ - personalDetails: personalDetailsPropType, - ...withCurrentUserPersonalDetailsPropTypes, }; const defaultProps = { report: {}, - personalDetails: {}, iou: iouDefaultProps, ...withCurrentUserPersonalDetailsDefaultProps, }; @@ -69,16 +65,18 @@ function MoneyRequestConfirmPage(props) { const isDistanceRequest = MoneyRequestUtils.isDistanceRequest(iouType, props.selectedTab); const isScanRequest = MoneyRequestUtils.isScanRequest(props.selectedTab); const [receiptFile, setReceiptFile] = useState(); + const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; + const participants = useMemo( () => _.chain(props.iou.participants) .map((participant) => { const isPolicyExpenseChat = lodashGet(participant, 'isPolicyExpenseChat', false); - return isPolicyExpenseChat ? OptionsListUtils.getPolicyExpenseReportOption(participant) : OptionsListUtils.getParticipantsOption(participant, props.personalDetails); + return isPolicyExpenseChat ? OptionsListUtils.getPolicyExpenseReportOption(participant) : OptionsListUtils.getParticipantsOption(participant, personalDetails); }) .filter((participant) => !!participant.login || !!participant.text) .value(), - [props.iou.participants, props.personalDetails], + [props.iou.participants, personalDetails], ); const isPolicyExpenseChat = useMemo(() => ReportUtils.isPolicyExpenseChat(ReportUtils.getRootParentReport(props.report)), [props.report]); const isManualRequestDM = props.selectedTab === CONST.TAB.MANUAL && iouType === CONST.IOU.TYPE.REQUEST; @@ -421,9 +419,6 @@ export default compose( return `${ONYXKEYS.COLLECTION.REPORT}${reportID}`; }, }, - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, selectedTab: { key: `${ONYXKEYS.COLLECTION.SELECTED_TAB}${CONST.TAB.RECEIPT_TAB_ID}`, }, diff --git a/src/pages/tasks/TaskAssigneeSelectorModal.js b/src/pages/tasks/TaskAssigneeSelectorModal.js index 131755e02dfa..69d365f6b89c 100644 --- a/src/pages/tasks/TaskAssigneeSelectorModal.js +++ b/src/pages/tasks/TaskAssigneeSelectorModal.js @@ -7,6 +7,7 @@ import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import {usePersonalDetails} from '@components/OnyxProvider'; import OptionsSelector from '@components/OptionsSelector'; import ScreenWrapper from '@components/ScreenWrapper'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; @@ -16,7 +17,6 @@ import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; -import personalDetailsPropType from '@pages/personalDetailsPropType'; import reportPropTypes from '@pages/reportPropTypes'; import styles from '@styles/styles'; import * as Task from '@userActions/Task'; @@ -28,9 +28,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), @@ -65,7 +62,6 @@ const propTypes = { const defaultProps = { betas: [], - personalDetails: {}, reports: {}, session: {}, route: {}, @@ -80,13 +76,14 @@ function TaskAssigneeSelectorModal(props) { const [filteredUserToInvite, setFilteredUserToInvite] = useState(null); const [filteredCurrentUserOption, setFilteredCurrentUserOption] = useState(null); const [isLoading, setIsLoading] = React.useState(true); + const allPersonalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; const {inputCallbackRef} = useAutoFocusInput(); const updateOptions = useCallback(() => { const {recentReports, personalDetails, userToInvite, currentUserOption} = OptionsListUtils.getFilteredOptions( props.reports, - props.personalDetails, + allPersonalDetails, props.betas, searchValue.trim(), [], @@ -111,7 +108,7 @@ function TaskAssigneeSelectorModal(props) { if (isLoading) { setIsLoading(false); } - }, [props, searchValue, isLoading]); + }, [props, searchValue, allPersonalDetails, isLoading]); useEffect(() => { const debouncedSearch = _.debounce(updateOptions, 200); @@ -249,9 +246,6 @@ export default compose( reports: { key: ONYXKEYS.COLLECTION.REPORT, }, - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, betas: { key: ONYXKEYS.BETAS, }, From a567b1cf98466435c9053acfe77c9d8f9b7695a8 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 10 Nov 2023 15:47:28 +0700 Subject: [PATCH 033/319] build optimistic custom unit when create workspace --- src/libs/actions/Policy.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index a2f4ec79f0ff..f1a17f8b898e 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -974,6 +974,7 @@ function generateCustomUnitID() { } /** + * @param {String} currency * @returns {Object} */ function buildOptimisticCustomUnits(currency) { @@ -1014,7 +1015,7 @@ function buildOptimisticCustomUnits(currency) { */ function createDraftInitialWorkspace(policyOwnerEmail = '', policyName = '', policyID = generatePolicyID(), makeMeAdmin = false) { const workspaceName = policyName || generateDefaultWorkspaceName(policyOwnerEmail); - const outputCurrency = lodashGet(allPersonalDetails, [sessionAccountID, 'localCurrencyCode'], CONST.CURRENCY.USD) + const outputCurrency = lodashGet(allPersonalDetails, [sessionAccountID, 'localCurrencyCode'], CONST.CURRENCY.USD); const {customUnits} = buildOptimisticCustomUnits(outputCurrency); const optimisticData = [ From e5fb0a1651a797ff7e7912efa70d3661285eaafe Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 10 Nov 2023 16:30:16 +0700 Subject: [PATCH 034/319] fix test --- src/libs/actions/Policy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index 091f2930a5b9..55a734bda72e 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -1049,7 +1049,7 @@ function generateCustomUnitID() { * @returns {Object} */ function buildOptimisticCustomUnits(currency) { - const customUnitID = generateCustomUnitID(); + const customUnitID = generateCustomUnitID(); const customUnitRateID = generateCustomUnitID(); const customUnits = { [customUnitID]: { From 87c7e12a28e93c1be58974ece82f79fbb72590b4 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 10 Nov 2023 16:30:47 +0700 Subject: [PATCH 035/319] fix lint --- src/libs/actions/Policy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index 55a734bda72e..091f2930a5b9 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -1049,7 +1049,7 @@ function generateCustomUnitID() { * @returns {Object} */ function buildOptimisticCustomUnits(currency) { - const customUnitID = generateCustomUnitID(); + const customUnitID = generateCustomUnitID(); const customUnitRateID = generateCustomUnitID(); const customUnits = { [customUnitID]: { From 2b581aa94578977be79c249126cf47c27a321b66 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 10 Nov 2023 16:54:03 +0700 Subject: [PATCH 036/319] fix test --- src/libs/actions/Policy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index 091f2930a5b9..961181612a81 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -1048,7 +1048,7 @@ function generateCustomUnitID() { * @param {String} currency * @returns {Object} */ -function buildOptimisticCustomUnits(currency) { +function buildOptimisticCustomUnits(currency) { const customUnitID = generateCustomUnitID(); const customUnitRateID = generateCustomUnitID(); const customUnits = { From 697adae78bcb04e2480e3f8f5aba6dbe5cbf50b9 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 10 Nov 2023 16:54:25 +0700 Subject: [PATCH 037/319] fix lint --- src/libs/actions/Policy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index 961181612a81..091f2930a5b9 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -1048,7 +1048,7 @@ function generateCustomUnitID() { * @param {String} currency * @returns {Object} */ -function buildOptimisticCustomUnits(currency) { +function buildOptimisticCustomUnits(currency) { const customUnitID = generateCustomUnitID(); const customUnitRateID = generateCustomUnitID(); const customUnits = { From 839326e399ff4753f5c026ff96d81715c5ce826c Mon Sep 17 00:00:00 2001 From: Getabalew Tesfaye Date: Fri, 10 Nov 2023 19:08:07 +0300 Subject: [PATCH 038/319] update 'reason' to 'cancellationReason' --- src/components/ReportActionItem/MoneyRequestPreview.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index f7c647fb4c86..c22b204dd80f 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -219,7 +219,7 @@ function MoneyRequestPreview(props) { message += ` • ${props.translate('iou.approved')}`; } else if (props.iouReport.isWaitingOnBankAccount) { message += ` • ${props.translate('iou.pending')}`; - } else if (props.action.originalMessage && props.action.originalMessage.reason === CONST.IOU.CANCEL_REASON.PAYMENT_EXPIRED) { + } else if (props.action.originalMessage && props.action.originalMessage.cancellationReason === CONST.IOU.CANCEL_REASON.PAYMENT_EXPIRED) { message += ` • ${props.translate('iou.canceled')}`; } return message; From f2c59f35b7f2e99005951a96e2f2c2b545fe518d Mon Sep 17 00:00:00 2001 From: Getabalew Tesfaye Date: Fri, 10 Nov 2023 19:15:11 +0300 Subject: [PATCH 039/319] remove usage of report.ownerEmail and add condition for canceled case --- src/components/ReportActionItem/MoneyRequestView.js | 6 ++++-- src/pages/home/report/ReportActionItem.js | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js index 4b69f14213a2..7130e3d41928 100644 --- a/src/components/ReportActionItem/MoneyRequestView.js +++ b/src/components/ReportActionItem/MoneyRequestView.js @@ -77,7 +77,7 @@ const defaultProps = { policyTags: {}, }; -function MoneyRequestView({report, betas, parentReport, policyCategories, shouldShowHorizontalRule, transaction, policyTags, policy}) { +function MoneyRequestView({report, betas, parentReport, policyCategories, shouldShowHorizontalRule, transaction, policyTags, policy, action}) { const {isSmallScreenWidth} = useWindowDimensions(); const {translate} = useLocalize(); const parentReportAction = ReportActionsUtils.getParentReportAction(report); @@ -136,7 +136,9 @@ function MoneyRequestView({report, betas, parentReport, policyCategories, should if (!isDistanceRequest) { amountDescription += ` • ${translate('iou.cash')}`; } - if (isSettled) { + if(props.action && props.action.originalMessage.cancellationReason === CONST.IOU.CANCEL_REASON.PAYMENT_EXPIRED) { + amountDescription += ` • ${translate('iou.canceled')}`; + } else if (isSettled) { amountDescription += ` • ${translate('iou.settledExpensify')}`; } else if (report.isWaitingOnBankAccount) { amountDescription += ` • ${translate('iou.pending')}`; diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index df56d38dc2dd..448e0ae166c9 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -415,7 +415,7 @@ function ReportActionItem(props) {
); } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTDEQUEUED) { - const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(props.personalDetailsList, [props.report.ownerAccountID, 'displayName'], props.report.ownerEmail); + const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails, [props.report.ownerAccountID, 'displayName']); const amount = CurrencyUtils.convertToDisplayString(props.report.total, props.report.currency); children = ; @@ -572,6 +572,7 @@ function ReportActionItem(props) { content = ( From 0b1f90b76f37a60097f3ea0f4139cca3c86f91d2 Mon Sep 17 00:00:00 2001 From: Getabalew Tesfaye Date: Fri, 10 Nov 2023 19:20:33 +0300 Subject: [PATCH 040/319] fix lint --- src/components/ReportActionItem/MoneyRequestView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js index 7130e3d41928..68656c6f40fe 100644 --- a/src/components/ReportActionItem/MoneyRequestView.js +++ b/src/components/ReportActionItem/MoneyRequestView.js @@ -136,7 +136,7 @@ function MoneyRequestView({report, betas, parentReport, policyCategories, should if (!isDistanceRequest) { amountDescription += ` • ${translate('iou.cash')}`; } - if(props.action && props.action.originalMessage.cancellationReason === CONST.IOU.CANCEL_REASON.PAYMENT_EXPIRED) { + if(action && action.originalMessage.cancellationReason === CONST.IOU.CANCEL_REASON.PAYMENT_EXPIRED) { amountDescription += ` • ${translate('iou.canceled')}`; } else if (isSettled) { amountDescription += ` • ${translate('iou.settledExpensify')}`; From a4013f10ce3981cf83583103006f05748ad1bfe5 Mon Sep 17 00:00:00 2001 From: Getabalew Tesfaye Date: Fri, 10 Nov 2023 19:21:47 +0300 Subject: [PATCH 041/319] run prettier --- src/components/ReportActionItem/MoneyRequestView.js | 2 +- src/pages/home/report/ReportActionItem.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js index 68656c6f40fe..0df83a6c36c9 100644 --- a/src/components/ReportActionItem/MoneyRequestView.js +++ b/src/components/ReportActionItem/MoneyRequestView.js @@ -136,7 +136,7 @@ function MoneyRequestView({report, betas, parentReport, policyCategories, should if (!isDistanceRequest) { amountDescription += ` • ${translate('iou.cash')}`; } - if(action && action.originalMessage.cancellationReason === CONST.IOU.CANCEL_REASON.PAYMENT_EXPIRED) { + if (action && action.originalMessage.cancellationReason === CONST.IOU.CANCEL_REASON.PAYMENT_EXPIRED) { amountDescription += ` • ${translate('iou.canceled')}`; } else if (isSettled) { amountDescription += ` • ${translate('iou.settledExpensify')}`; diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 448e0ae166c9..43526d7ee5c4 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -74,7 +74,7 @@ import ReportActionItemSingle from './ReportActionItemSingle'; import ReportActionItemThread from './ReportActionItemThread'; import reportActionPropTypes from './reportActionPropTypes'; import ReportAttachmentsContext from './ReportAttachmentsContext'; -import * as CurrencyUtils from '../../../libs/CurrencyUtils'; +import * as CurrencyUtils from '@libs/CurrencyUtils'; const propTypes = { ...windowDimensionsPropTypes, From 97d8c5eb14f6bf902535d23e9074dd29a44582b9 Mon Sep 17 00:00:00 2001 From: Getabalew Tesfaye Date: Fri, 10 Nov 2023 19:26:18 +0300 Subject: [PATCH 042/319] lint! --- src/pages/home/report/ReportActionItem.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 43526d7ee5c4..bbd368fa6734 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -58,6 +58,7 @@ import * as User from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import * as CurrencyUtils from '@libs/CurrencyUtils'; import AnimatedEmptyStateBackground from './AnimatedEmptyStateBackground'; import * as ContextMenuActions from './ContextMenu/ContextMenuActions'; import MiniReportActionContextMenu from './ContextMenu/MiniReportActionContextMenu'; @@ -74,7 +75,6 @@ import ReportActionItemSingle from './ReportActionItemSingle'; import ReportActionItemThread from './ReportActionItemThread'; import reportActionPropTypes from './reportActionPropTypes'; import ReportAttachmentsContext from './ReportAttachmentsContext'; -import * as CurrencyUtils from '@libs/CurrencyUtils'; const propTypes = { ...windowDimensionsPropTypes, From 2f0af0c67c0a664d363bcb25559bf6c211f215fd Mon Sep 17 00:00:00 2001 From: Getabalew Tesfaye Date: Mon, 13 Nov 2023 12:51:30 +0300 Subject: [PATCH 043/319] fix crash --- src/components/ReportActionItem/MoneyRequestView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js index 0df83a6c36c9..b51a55082e0a 100644 --- a/src/components/ReportActionItem/MoneyRequestView.js +++ b/src/components/ReportActionItem/MoneyRequestView.js @@ -136,7 +136,7 @@ function MoneyRequestView({report, betas, parentReport, policyCategories, should if (!isDistanceRequest) { amountDescription += ` • ${translate('iou.cash')}`; } - if (action && action.originalMessage.cancellationReason === CONST.IOU.CANCEL_REASON.PAYMENT_EXPIRED) { + if (action && action.originalMessage && action.originalMessage.cancellationReason === CONST.IOU.CANCEL_REASON.PAYMENT_EXPIRED) { amountDescription += ` • ${translate('iou.canceled')}`; } else if (isSettled) { amountDescription += ` • ${translate('iou.settledExpensify')}`; From ef72ec73c244a94611a892ed91edfab9b73669d0 Mon Sep 17 00:00:00 2001 From: Getabalew Tesfaye Date: Mon, 13 Nov 2023 12:51:51 +0300 Subject: [PATCH 044/319] run prettier --- src/languages/en.ts | 2 +- src/languages/es.ts | 2 +- src/pages/home/report/ReportActionItem.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 32143a9ae73a..2dcf373a3182 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -9,6 +9,7 @@ import type { BeginningOfChatHistoryAnnounceRoomPartOneParams, BeginningOfChatHistoryAnnounceRoomPartTwo, BeginningOfChatHistoryDomainRoomPartOneParams, + CanceledRequestParams, CharacterLimitParams, ConfirmThatParams, DateShouldBeAfterParams, @@ -78,7 +79,6 @@ import type { WelcomeToRoomParams, WeSentYouMagicSignInLinkParams, ZipCodeExampleFormatParams, - CanceledRequestParams, } from './types'; type StateValue = { diff --git a/src/languages/es.ts b/src/languages/es.ts index 7e3e7d89fdb5..5486898ca63b 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -8,6 +8,7 @@ import type { BeginningOfChatHistoryAnnounceRoomPartOneParams, BeginningOfChatHistoryAnnounceRoomPartTwo, BeginningOfChatHistoryDomainRoomPartOneParams, + CanceledRequestParams, CharacterLimitParams, ConfirmThatParams, DateShouldBeAfterParams, @@ -77,7 +78,6 @@ import type { WelcomeToRoomParams, WeSentYouMagicSignInLinkParams, ZipCodeExampleFormatParams, - CanceledRequestParams, } from './types'; /* eslint-disable max-len */ diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index bbd368fa6734..031aebdb3512 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -34,6 +34,7 @@ import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withW import usePrevious from '@hooks/usePrevious'; import compose from '@libs/compose'; import ControlSelection from '@libs/ControlSelection'; +import * as CurrencyUtils from '@libs/CurrencyUtils'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import focusTextInputAfterAnimation from '@libs/focusTextInputAfterAnimation'; import Navigation from '@libs/Navigation/Navigation'; @@ -58,7 +59,6 @@ import * as User from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import * as CurrencyUtils from '@libs/CurrencyUtils'; import AnimatedEmptyStateBackground from './AnimatedEmptyStateBackground'; import * as ContextMenuActions from './ContextMenu/ContextMenuActions'; import MiniReportActionContextMenu from './ContextMenu/MiniReportActionContextMenu'; From 17f7b490f324e9dc399fce46d32b05ae411e8331 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Tue, 14 Nov 2023 12:14:15 +0700 Subject: [PATCH 045/319] move output currency --- src/libs/actions/Policy.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index 5d5c937e46f6..b11df545fc64 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -1045,12 +1045,14 @@ function generateCustomUnitID() { } /** - * @param {String} currency + * * @returns {Object} */ -function buildOptimisticCustomUnits(currency) { +function buildOptimisticCustomUnits() { + const currency = lodashGet(allPersonalDetails, [sessionAccountID, 'localCurrencyCode'], CONST.CURRENCY.USD); const customUnitID = generateCustomUnitID(); const customUnitRateID = generateCustomUnitID(); + const customUnits = { [customUnitID]: { customUnitID, @@ -1073,6 +1075,7 @@ function buildOptimisticCustomUnits(currency) { customUnits, customUnitID, customUnitRateID, + outputCurrency: currency, }; } @@ -1086,8 +1089,7 @@ function buildOptimisticCustomUnits(currency) { */ function createDraftInitialWorkspace(policyOwnerEmail = '', policyName = '', policyID = generatePolicyID(), makeMeAdmin = false) { const workspaceName = policyName || generateDefaultWorkspaceName(policyOwnerEmail); - const outputCurrency = lodashGet(allPersonalDetails, [sessionAccountID, 'localCurrencyCode'], CONST.CURRENCY.USD); - const {customUnits} = buildOptimisticCustomUnits(outputCurrency); + const {customUnits, outputCurrency} = buildOptimisticCustomUnits(); const optimisticData = [ { @@ -1133,8 +1135,7 @@ function createDraftInitialWorkspace(policyOwnerEmail = '', policyName = '', pol function createWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policyName = '', policyID = generatePolicyID()) { const workspaceName = policyName || generateDefaultWorkspaceName(policyOwnerEmail); - const outputCurrency = lodashGet(allPersonalDetails, [sessionAccountID, 'localCurrencyCode'], CONST.CURRENCY.USD); - const {customUnits, customUnitID, customUnitRateID} = buildOptimisticCustomUnits(outputCurrency); + const {customUnits, customUnitID, customUnitRateID, outputCurrency} = buildOptimisticCustomUnits(); const { announceChatReportID, From 33b048d57857d06d689fa072463e56dad163bc03 Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 14 Nov 2023 17:39:24 +0700 Subject: [PATCH 046/319] fix: empty transaction data on open app --- src/libs/ReportUtils.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index a18594905b55..a3d613be8543 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1757,9 +1757,10 @@ function hasMissingSmartscanFields(iouReportID) { * Given a parent IOU report action get report name for the LHN. * * @param {Object} reportAction + * @param {String} parentReportActionMessage * @returns {String} */ -function getTransactionReportName(reportAction) { +function getTransactionReportName(reportAction, parentReportActionMessage) { if (ReportActionsUtils.isReversedTransaction(reportAction)) { return Localize.translateLocal('parentReportAction.reversedTransaction'); } @@ -1769,6 +1770,9 @@ function getTransactionReportName(reportAction) { } const transaction = TransactionUtils.getLinkedTransaction(reportAction); + if (_.isEmpty(transaction)) { + return parentReportActionMessage; + } if (TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction)) { return Localize.translateLocal('iou.receiptScanning'); } @@ -2077,12 +2081,16 @@ function getReportName(report, policy = undefined) { let formattedName; const parentReportAction = ReportActionsUtils.getParentReportAction(report); if (isChatThread(report)) { + if (_.isEmpty(parentReportAction)) { + return Localize.translateLocal('iou.request'); + } + + const parentReportActionMessage = lodashGet(parentReportAction, ['message', 0, 'text'], '').replace(/(\r\n|\n|\r)/gm, ' '); if (ReportActionsUtils.isTransactionThread(parentReportAction)) { - return getTransactionReportName(parentReportAction); + return getTransactionReportName(parentReportAction, parentReportActionMessage); } const isAttachment = ReportActionsUtils.isReportActionAttachment(parentReportAction); - const parentReportActionMessage = lodashGet(parentReportAction, ['message', 0, 'text'], '').replace(/(\r\n|\n|\r)/gm, ' '); if (isAttachment && parentReportActionMessage) { return `[${Localize.translateLocal('common.attachment')}]`; } From bbe5c5295a742edf817ac2b83a5c85e8152530e0 Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 14 Nov 2023 19:35:21 +0700 Subject: [PATCH 047/319] get message from parent report action --- src/libs/ReportUtils.js | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index a3d613be8543..bdef6b070dfb 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1757,10 +1757,9 @@ function hasMissingSmartscanFields(iouReportID) { * Given a parent IOU report action get report name for the LHN. * * @param {Object} reportAction - * @param {String} parentReportActionMessage * @returns {String} */ -function getTransactionReportName(reportAction, parentReportActionMessage) { +function getTransactionReportName(reportAction) { if (ReportActionsUtils.isReversedTransaction(reportAction)) { return Localize.translateLocal('parentReportAction.reversedTransaction'); } @@ -1771,7 +1770,7 @@ function getTransactionReportName(reportAction, parentReportActionMessage) { const transaction = TransactionUtils.getLinkedTransaction(reportAction); if (_.isEmpty(transaction)) { - return parentReportActionMessage; + return lodashGet(reportAction, ['message', 0, 'text'], '').replace(/(\r\n|\n|\r)/gm, ' '); } if (TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction)) { return Localize.translateLocal('iou.receiptScanning'); @@ -2081,16 +2080,15 @@ function getReportName(report, policy = undefined) { let formattedName; const parentReportAction = ReportActionsUtils.getParentReportAction(report); if (isChatThread(report)) { - if (_.isEmpty(parentReportAction)) { - return Localize.translateLocal('iou.request'); - } - - const parentReportActionMessage = lodashGet(parentReportAction, ['message', 0, 'text'], '').replace(/(\r\n|\n|\r)/gm, ' '); if (ReportActionsUtils.isTransactionThread(parentReportAction)) { - return getTransactionReportName(parentReportAction, parentReportActionMessage); + if (_.isEmpty(parentReportAction)) { + return Localize.translateLocal('iou.request'); + } + return getTransactionReportName(parentReportAction); } const isAttachment = ReportActionsUtils.isReportActionAttachment(parentReportAction); + const parentReportActionMessage = lodashGet(parentReportAction, ['message', 0, 'text'], '').replace(/(\r\n|\n|\r)/gm, ' '); if (isAttachment && parentReportActionMessage) { return `[${Localize.translateLocal('common.attachment')}]`; } From c2ebb368ea9aacd44108cfa48b1f4e9c1f417106 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Tue, 14 Nov 2023 15:46:06 +0100 Subject: [PATCH 048/319] [TS migration] Migrate 'OfflineIndicator.js' component --- src/components/OfflineIndicator.js | 67 ----------------------------- src/components/OfflineIndicator.tsx | 63 +++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 67 deletions(-) delete mode 100644 src/components/OfflineIndicator.js create mode 100644 src/components/OfflineIndicator.tsx diff --git a/src/components/OfflineIndicator.js b/src/components/OfflineIndicator.js deleted file mode 100644 index 9cacc3621790..000000000000 --- a/src/components/OfflineIndicator.js +++ /dev/null @@ -1,67 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import {View} from 'react-native'; -import compose from '@libs/compose'; -import stylePropTypes from '@styles/stylePropTypes'; -import styles from '@styles/styles'; -import * as StyleUtils from '@styles/StyleUtils'; -import variables from '@styles/variables'; -import Icon from './Icon'; -import * as Expensicons from './Icon/Expensicons'; -import networkPropTypes from './networkPropTypes'; -import {withNetwork} from './OnyxProvider'; -import Text from './Text'; -import withLocalize, {withLocalizePropTypes} from './withLocalize'; -import withWindowDimensions from './withWindowDimensions'; - -const propTypes = { - /** Information about the network */ - network: networkPropTypes.isRequired, - - /** Optional styles for container element that will override the default styling for the offline indicator */ - // eslint-disable-next-line react/forbid-prop-types - containerStyles: PropTypes.arrayOf(PropTypes.object), - - /** Optional styles for the container */ - style: stylePropTypes, - - /** Is the window width narrow, like on a mobile device */ - isSmallScreenWidth: PropTypes.bool.isRequired, - - ...withLocalizePropTypes, -}; - -const defaultProps = { - containerStyles: [], - style: [], -}; - -const setStyles = (containerStyles, isSmallScreenWidth) => { - if (containerStyles.length) { - return containerStyles; - } - return isSmallScreenWidth ? styles.offlineIndicatorMobile : styles.offlineIndicator; -}; - -function OfflineIndicator(props) { - if (!props.network.isOffline) { - return null; - } - - return ( - - - {props.translate('common.youAppearToBeOffline')} - - ); -} - -OfflineIndicator.propTypes = propTypes; -OfflineIndicator.defaultProps = defaultProps; -OfflineIndicator.displayName = 'OfflineIndicator'; - -export default compose(withWindowDimensions, withLocalize, withNetwork())(OfflineIndicator); diff --git a/src/components/OfflineIndicator.tsx b/src/components/OfflineIndicator.tsx new file mode 100644 index 000000000000..197f63ff17aa --- /dev/null +++ b/src/components/OfflineIndicator.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import {StyleProp, View, ViewStyle} from 'react-native'; +import {OnyxEntry} from 'react-native-onyx/lib/types'; +import compose from '@libs/compose'; +import styles from '@styles/styles'; +import variables from '@styles/variables'; +import type {Network} from '@src/types/onyx'; +import Icon from './Icon'; +import * as Expensicons from './Icon/Expensicons'; +import {withNetwork} from './OnyxProvider'; +import Text from './Text'; +import withLocalize from './withLocalize'; +import withWindowDimensions from './withWindowDimensions'; + +type OfflineIndicatorProps = { + /** Information about the network */ + network: OnyxEntry; + + /** Optional styles for container element that will override the default styling for the offline indicator */ + containerStyles?: StyleProp; + + /** Optional styles for the container */ + style?: StyleProp; + + /** Is the window width narrow, like on a mobile device */ + isSmallScreenWidth: boolean; + + // TODO: remove after withLocalize migrated + /** Returns translated string for given locale and phrase */ + translate: (value: string) => string; +}; + +const setStyles = (containerStyles: StyleProp, isSmallScreenWidth: boolean): StyleProp => { + if (!!containerStyles && ((Array.isArray(containerStyles) && containerStyles.length) || Object.keys(containerStyles).length)) { + return containerStyles; + } + + return isSmallScreenWidth ? styles.offlineIndicatorMobile : styles.offlineIndicator; +}; + +function OfflineIndicator({network, isSmallScreenWidth, translate, style = [], containerStyles = []}: OfflineIndicatorProps) { + if (!network?.isOffline) { + return null; + } + + return ( + + + {translate('common.youAppearToBeOffline')} + + ); +} + +OfflineIndicator.displayName = 'OfflineIndicator'; + +// TODO: remove when withWindowDimensions, withLocalize are migrated +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +export default compose(withWindowDimensions, withLocalize, withNetwork())(OfflineIndicator); From 72113b4295d5881adf612dcefbb68af5874f7c34 Mon Sep 17 00:00:00 2001 From: Dylan Date: Wed, 15 Nov 2023 11:00:18 +0700 Subject: [PATCH 049/319] using SET to save updated transaction --- src/libs/actions/IOU.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index aa07f0e7ca34..d3fa2f4915ba 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -765,7 +765,7 @@ function updateDistanceRequest(transactionID, transactionThreadReportID, transac // Optimistically modify the transaction optimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, value: { ...updatedTransaction, From dae4c62c0ffbe41b2fa7295818b9c0732fe7d30e Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Wed, 15 Nov 2023 15:10:55 +0700 Subject: [PATCH 050/319] fix improve private notes flow --- .../PrivateNotes/PrivateNotesEditPage.js | 1 - .../PrivateNotes/PrivateNotesListPage.js | 30 +++++++++++-------- src/pages/ReportDetailsPage.js | 16 ++++++++-- 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/src/pages/PrivateNotes/PrivateNotesEditPage.js b/src/pages/PrivateNotes/PrivateNotesEditPage.js index f38dabee9183..cb31a15023d1 100644 --- a/src/pages/PrivateNotes/PrivateNotesEditPage.js +++ b/src/pages/PrivateNotes/PrivateNotesEditPage.js @@ -116,7 +116,6 @@ function PrivateNotesEditPage({route, personalDetailsList, report}) { > Navigation.goBack(ROUTES.PRIVATE_NOTES_VIEW.getRoute(report.reportID, route.params.accountID))} shouldShowBackButton onCloseButtonPress={() => Navigation.dismissModal()} diff --git a/src/pages/PrivateNotes/PrivateNotesListPage.js b/src/pages/PrivateNotes/PrivateNotesListPage.js index 4d5b348c4b9f..6fdd96d77eb9 100644 --- a/src/pages/PrivateNotes/PrivateNotesListPage.js +++ b/src/pages/PrivateNotes/PrivateNotesListPage.js @@ -1,3 +1,4 @@ +import Str from 'expensify-common/lib/str'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useMemo} from 'react'; @@ -5,15 +6,15 @@ import {ScrollView} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import MenuItem from '@components/MenuItem'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import {withNetwork} from '@components/OnyxProvider'; import ScreenWrapper from '@components/ScreenWrapper'; +import Text from '@components/Text'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useLocalize from '@hooks/useLocalize'; import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; -import * as UserUtils from '@libs/UserUtils'; import withReportAndPrivateNotesOrNotFound from '@pages/home/report/withReportAndPrivateNotesOrNotFound'; import personalDetailsPropType from '@pages/personalDetailsPropType'; import reportPropTypes from '@pages/reportPropTypes'; @@ -66,7 +67,6 @@ function PrivateNotesListPage({report, personalDetailsList, session}) { */ function getMenuItem(item, index) { const keyTitle = item.translationKey ? translate(item.translationKey) : item.title; - return ( - ); @@ -97,10 +99,10 @@ function PrivateNotesListPage({report, personalDetailsList, session}) { return _.chain(lodashGet(report, 'privateNotes', {})) .map((privateNote, accountID) => ({ title: Number(lodashGet(session, 'accountID', null)) === Number(accountID) ? translate('privateNotes.myNote') : lodashGet(personalDetailsList, [accountID, 'login'], ''), - icon: UserUtils.getAvatar(lodashGet(personalDetailsList, [accountID, 'avatar'], UserUtils.getDefaultAvatar(accountID)), accountID), - iconType: CONST.ICON_TYPE_AVATAR, - action: () => Navigation.navigate(ROUTES.PRIVATE_NOTES_VIEW.getRoute(report.reportID, accountID)), + action: () => Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, accountID)), brickRoadIndicator: privateNoteBrickRoadIndicator(accountID), + note: lodashGet(privateNote, 'note', ''), + disabled: Number(session.accountID) !== Number(accountID), })) .value(); }, [report, personalDetailsList, session, translate]); @@ -115,6 +117,7 @@ function PrivateNotesListPage({report, personalDetailsList, session}) { shouldShowBackButton onCloseButtonPress={() => Navigation.dismissModal()} /> + {translate('privateNotes.personalNoteMessage')} {_.map(privateNotes, (item, index) => getMenuItem(item, index))} ); @@ -131,6 +134,9 @@ export default compose( personalDetailsList: { key: ONYXKEYS.PERSONAL_DETAILS_LIST, }, + session: { + key: ONYXKEYS.SESSION, + }, }), withNetwork(), )(PrivateNotesListPage); diff --git a/src/pages/ReportDetailsPage.js b/src/pages/ReportDetailsPage.js index de25fdc3a081..3272e15dac7f 100644 --- a/src/pages/ReportDetailsPage.js +++ b/src/pages/ReportDetailsPage.js @@ -1,3 +1,4 @@ +import {isEmpty} from 'lodash'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useMemo} from 'react'; @@ -139,7 +140,15 @@ function ReportDetailsPage(props) { translationKey: 'privateNotes.title', icon: Expensicons.Pencil, isAnonymousAction: false, - action: () => Navigation.navigate(ROUTES.PRIVATE_NOTES_LIST.getRoute(props.report.reportID)), + action: () => { + const currentUserPrivateNote = lodashGet(props.report, ['privateNotes', props.session.accountID, 'note'], ''); + if (isEmpty(currentUserPrivateNote)) { + Report.getReportPrivateNote(props.report.reportID); + Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(props.report.reportID, props.session.accountID)); + return; + } + Navigation.navigate(ROUTES.PRIVATE_NOTES_LIST.getRoute(props.report.reportID)); + }, brickRoadIndicator: Report.hasErrorInPrivateNotes(props.report) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : '', }); } @@ -156,7 +165,7 @@ function ReportDetailsPage(props) { } return items; - }, [props.report, isMoneyRequestReport, participants.length, isArchivedRoom, isThread, isUserCreatedPolicyRoom, canLeaveRoom, isGroupDMChat, isPolicyMember]); + }, [props.report, isMoneyRequestReport, participants.length, isArchivedRoom, isThread, isUserCreatedPolicyRoom, canLeaveRoom, isGroupDMChat, isPolicyMember, props.session.accountID]); const displayNamesWithTooltips = useMemo(() => { const hasMultipleParticipants = participants.length > 1; @@ -271,5 +280,8 @@ export default compose( policies: { key: ONYXKEYS.COLLECTION.POLICY, }, + session: { + key: ONYXKEYS.SESSION, + }, }), )(ReportDetailsPage); From c24c65425bb7cf433938aac819615d5452983e5e Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Wed, 15 Nov 2023 15:57:41 +0700 Subject: [PATCH 051/319] fix access private notes by deeplink --- src/pages/PrivateNotes/PrivateNotesListPage.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/pages/PrivateNotes/PrivateNotesListPage.js b/src/pages/PrivateNotes/PrivateNotesListPage.js index 6fdd96d77eb9..ef74bb838cee 100644 --- a/src/pages/PrivateNotes/PrivateNotesListPage.js +++ b/src/pages/PrivateNotes/PrivateNotesListPage.js @@ -1,7 +1,7 @@ -import Str from 'expensify-common/lib/str'; +import {some} from 'lodash'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useMemo} from 'react'; +import React, {useEffect, useMemo} from 'react'; import {ScrollView} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -107,6 +107,12 @@ function PrivateNotesListPage({report, personalDetailsList, session}) { .value(); }, [report, personalDetailsList, session, translate]); + useEffect(() => { + if (some(privateNotes, (item) => item.note)) { + return; + } + Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, session.accountID)); + }, [privateNotes, report.reportID, session.accountID]); return ( Date: Wed, 15 Nov 2023 16:17:24 +0700 Subject: [PATCH 052/319] refactor to handle focus in the same useEffect --- .../ComposerWithSuggestions.js | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index 288df7775f65..c3981fabb15d 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -489,7 +489,7 @@ function ComposerWithSuggestions({ // We want to focus or refocus the input when a modal has been closed or the underlying screen is refocused. // We avoid doing this on native platforms since the software keyboard popping // open creates a jarring and broken UX. - if (!(willBlurTextInputOnTapOutside && !isNextModalWillOpenRef.current && !modal.isVisible && isFocused && (prevIsModalVisible || !prevIsFocused))) { + if (!((willBlurTextInputOnTapOutside || shouldAutoFocus) && !isNextModalWillOpenRef.current && !modal.isVisible && isFocused && (prevIsModalVisible || !prevIsFocused))) { return; } @@ -500,21 +500,6 @@ function ComposerWithSuggestions({ focus(); }, [focus, prevIsFocused, editFocused, prevIsModalVisible, isFocused, modal.isVisible, isNextModalWillOpenRef]); - const shouldAutoFocusRef = useRef(false); - shouldAutoFocusRef.current = shouldAutoFocus; - useEffect(() => { - if (prevIsFocused || !isFocused) { - return; - } - // Focus composer when navigating from another screen - setTimeout(() => { - if (!shouldAutoFocusRef.current) { - return; - } - textInputRef.current.focus(); - }, CONST.ANIMATED_TRANSITION); - }, [isFocused, prevIsFocused]); - useEffect(() => { // Scrolls the composer to the bottom and sets the selection to the end, so that longer drafts are easier to edit updateMultilineInputRange(textInputRef.current, shouldAutoFocus); From 0edc335669b5f9562b4f9df36e1a96e7527ef69f Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Wed, 15 Nov 2023 16:56:12 +0700 Subject: [PATCH 053/319] fix goback after save --- src/pages/PrivateNotes/PrivateNotesEditPage.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/PrivateNotes/PrivateNotesEditPage.js b/src/pages/PrivateNotes/PrivateNotesEditPage.js index cb31a15023d1..8d8769763eed 100644 --- a/src/pages/PrivateNotes/PrivateNotesEditPage.js +++ b/src/pages/PrivateNotes/PrivateNotesEditPage.js @@ -105,7 +105,8 @@ function PrivateNotesEditPage({route, personalDetailsList, report}) { Keyboard.dismiss(); // Take user back to the PrivateNotesView page - Navigation.goBack(ROUTES.PRIVATE_NOTES_VIEW.getRoute(report.reportID, route.params.accountID)); + + Navigation.goBack(); }; return ( From f671ed37edf01b6c64ca85f92b377c15b3303132 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 15 Nov 2023 17:44:51 +0700 Subject: [PATCH 054/319] fix lint --- .../ComposerWithSuggestions/ComposerWithSuggestions.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index c3981fabb15d..3e8dd7372bc3 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -498,6 +498,7 @@ function ComposerWithSuggestions({ return; } focus(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [focus, prevIsFocused, editFocused, prevIsModalVisible, isFocused, modal.isVisible, isNextModalWillOpenRef]); useEffect(() => { From fd079d20023d3811bd332288a8e35ebf25f92f67 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Wed, 15 Nov 2023 18:33:33 +0700 Subject: [PATCH 055/319] fix use underscore some --- src/pages/PrivateNotes/PrivateNotesListPage.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/PrivateNotes/PrivateNotesListPage.js b/src/pages/PrivateNotes/PrivateNotesListPage.js index ef74bb838cee..4133bc0c5629 100644 --- a/src/pages/PrivateNotes/PrivateNotesListPage.js +++ b/src/pages/PrivateNotes/PrivateNotesListPage.js @@ -1,4 +1,3 @@ -import {some} from 'lodash'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useEffect, useMemo} from 'react'; @@ -108,7 +107,7 @@ function PrivateNotesListPage({report, personalDetailsList, session}) { }, [report, personalDetailsList, session, translate]); useEffect(() => { - if (some(privateNotes, (item) => item.note)) { + if (_.some(privateNotes, (item) => item.note)) { return; } Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, session.accountID)); From 55d26142fb4176fcfc5df5bb873f0ea714b57052 Mon Sep 17 00:00:00 2001 From: artus9033 Date: Thu, 16 Nov 2023 17:33:45 +0100 Subject: [PATCH 056/319] Fix green line being displayed chaotically in chat --- src/pages/home/report/ReportActionsList.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index dd537959c91f..00da92270455 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -338,8 +338,9 @@ function ReportActionsList({ const nextMessage = sortedReportActions[index + 1]; const isCurrentMessageUnread = isMessageUnread(reportAction, lastReadTimeRef.current); shouldDisplay = isCurrentMessageUnread && (!nextMessage || !isMessageUnread(nextMessage, lastReadTimeRef.current)); - if (!messageManuallyMarkedUnread) { - shouldDisplay = shouldDisplay && reportAction.actorAccountID !== Report.getCurrentUserAccountID(); + if (shouldDisplay && !messageManuallyMarkedUnread) { + const isWithinVisibleThreshold = scrollingVerticalOffset.current < MSG_VISIBLE_THRESHOLD ? reportAction.created < userActiveSince.current : true; + shouldDisplay = reportAction.actorAccountID !== Report.getCurrentUserAccountID() && isWithinVisibleThreshold; } if (shouldDisplay) { cacheUnreadMarkers.set(report.reportID, reportAction.reportActionID); From 9e0d4d8454a832662617d9737417b2cdb5236198 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 17 Nov 2023 12:47:17 +0100 Subject: [PATCH 057/319] disable building android from source --- android/settings.gradle | 9 - ...eact-native+0.72.4+002+NumberOfLines.patch | 978 ------------------ 2 files changed, 987 deletions(-) delete mode 100644 patches/react-native+0.72.4+002+NumberOfLines.patch diff --git a/android/settings.gradle b/android/settings.gradle index c2bb3db7845a..680dfbc32521 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -16,12 +16,3 @@ project(':react-native-dev-menu').projectDir = new File(rootProject.projectDir, apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) include ':app' includeBuild('../node_modules/@react-native/gradle-plugin') - -includeBuild('../node_modules/react-native') { - dependencySubstitution { - substitute(module("com.facebook.react:react-android")).using(project(":packages:react-native:ReactAndroid")) - substitute(module("com.facebook.react:react-native")).using(project(":packages:react-native:ReactAndroid")) - substitute(module("com.facebook.react:hermes-android")).using(project(":packages:react-native:ReactAndroid:hermes-engine")) - substitute(module("com.facebook.react:hermes-engine")).using(project(":packages:react-native:ReactAndroid:hermes-engine")) - } -} diff --git a/patches/react-native+0.72.4+002+NumberOfLines.patch b/patches/react-native+0.72.4+002+NumberOfLines.patch deleted file mode 100644 index 75422f84708e..000000000000 --- a/patches/react-native+0.72.4+002+NumberOfLines.patch +++ /dev/null @@ -1,978 +0,0 @@ -diff --git a/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js b/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js -index 55b770d..4073836 100644 ---- a/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js -+++ b/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js -@@ -179,6 +179,13 @@ export type NativeProps = $ReadOnly<{| - */ - numberOfLines?: ?Int32, - -+ /** -+ * Sets the maximum number of lines for a `TextInput`. Use it with multiline set to -+ * `true` to be able to fill the lines. -+ * @platform android -+ */ -+ maximumNumberOfLines?: ?Int32, -+ - /** - * When `false`, if there is a small amount of space available around a text input - * (e.g. landscape orientation on a phone), the OS may choose to have the user edit -diff --git a/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js b/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js -index 6f69329..d531bee 100644 ---- a/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js -+++ b/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js -@@ -144,6 +144,8 @@ const RCTTextInputViewConfig = { - placeholder: true, - autoCorrect: true, - multiline: true, -+ numberOfLines: true, -+ maximumNumberOfLines: true, - textContentType: true, - maxLength: true, - autoCapitalize: true, -diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts b/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts -index 8badb2a..b19f197 100644 ---- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts -+++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts -@@ -347,12 +347,6 @@ export interface TextInputAndroidProps { - */ - inlineImagePadding?: number | undefined; - -- /** -- * Sets the number of lines for a TextInput. -- * Use it with multiline set to true to be able to fill the lines. -- */ -- numberOfLines?: number | undefined; -- - /** - * Sets the return key to the label. Use it instead of `returnKeyType`. - * @platform android -@@ -663,11 +657,30 @@ export interface TextInputProps - */ - maxLength?: number | undefined; - -+ /** -+ * Sets the maximum number of lines for a TextInput. -+ * Use it with multiline set to true to be able to fill the lines. -+ */ -+ maxNumberOfLines?: number | undefined; -+ - /** - * If true, the text input can be multiple lines. The default value is false. - */ - multiline?: boolean | undefined; - -+ /** -+ * Sets the number of lines for a TextInput. -+ * Use it with multiline set to true to be able to fill the lines. -+ */ -+ numberOfLines?: number | undefined; -+ -+ /** -+ * Sets the number of rows for a TextInput. -+ * Use it with multiline set to true to be able to fill the lines. -+ */ -+ rows?: number | undefined; -+ -+ - /** - * Callback that is called when the text input is blurred - */ -diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js b/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js -index 7ed4579..b1d994e 100644 ---- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js -+++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js -@@ -343,26 +343,12 @@ type AndroidProps = $ReadOnly<{| - */ - inlineImagePadding?: ?number, - -- /** -- * Sets the number of lines for a `TextInput`. Use it with multiline set to -- * `true` to be able to fill the lines. -- * @platform android -- */ -- numberOfLines?: ?number, -- - /** - * Sets the return key to the label. Use it instead of `returnKeyType`. - * @platform android - */ - returnKeyLabel?: ?string, - -- /** -- * Sets the number of rows for a `TextInput`. Use it with multiline set to -- * `true` to be able to fill the lines. -- * @platform android -- */ -- rows?: ?number, -- - /** - * When `false`, it will prevent the soft keyboard from showing when the field is focused. - * Defaults to `true`. -@@ -632,6 +618,12 @@ export type Props = $ReadOnly<{| - */ - keyboardType?: ?KeyboardType, - -+ /** -+ * Sets the maximum number of lines for a `TextInput`. Use it with multiline set to -+ * `true` to be able to fill the lines. -+ */ -+ maxNumberOfLines?: ?number, -+ - /** - * Specifies largest possible scale a font can reach when `allowFontScaling` is enabled. - * Possible values: -@@ -653,6 +645,12 @@ export type Props = $ReadOnly<{| - */ - multiline?: ?boolean, - -+ /** -+ * Sets the number of lines for a `TextInput`. Use it with multiline set to -+ * `true` to be able to fill the lines. -+ */ -+ numberOfLines?: ?number, -+ - /** - * Callback that is called when the text input is blurred. - */ -@@ -814,6 +812,12 @@ export type Props = $ReadOnly<{| - */ - returnKeyType?: ?ReturnKeyType, - -+ /** -+ * Sets the number of rows for a `TextInput`. Use it with multiline set to -+ * `true` to be able to fill the lines. -+ */ -+ rows?: ?number, -+ - /** - * If `true`, the text input obscures the text entered so that sensitive text - * like passwords stay secure. The default value is `false`. Does not work with 'multiline={true}'. -diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.js b/node_modules/react-native/Libraries/Components/TextInput/TextInput.js -index 2127191..542fc06 100644 ---- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.js -+++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.js -@@ -390,7 +390,6 @@ type AndroidProps = $ReadOnly<{| - /** - * Sets the number of lines for a `TextInput`. Use it with multiline set to - * `true` to be able to fill the lines. -- * @platform android - */ - numberOfLines?: ?number, - -@@ -403,10 +402,14 @@ type AndroidProps = $ReadOnly<{| - /** - * Sets the number of rows for a `TextInput`. Use it with multiline set to - * `true` to be able to fill the lines. -- * @platform android - */ - rows?: ?number, - -+ /** -+ * Sets the maximum number of lines the TextInput can have. -+ */ -+ maxNumberOfLines?: ?number, -+ - /** - * When `false`, it will prevent the soft keyboard from showing when the field is focused. - * Defaults to `true`. -@@ -1069,6 +1072,9 @@ function InternalTextInput(props: Props): React.Node { - accessibilityState, - id, - tabIndex, -+ rows, -+ numberOfLines, -+ maxNumberOfLines, - selection: propsSelection, - ...otherProps - } = props; -@@ -1427,6 +1433,8 @@ function InternalTextInput(props: Props): React.Node { - focusable={tabIndex !== undefined ? !tabIndex : focusable} - mostRecentEventCount={mostRecentEventCount} - nativeID={id ?? props.nativeID} -+ numberOfLines={props.rows ?? props.numberOfLines} -+ maximumNumberOfLines={maxNumberOfLines} - onBlur={_onBlur} - onKeyPressSync={props.unstable_onKeyPressSync} - onChange={_onChange} -@@ -1482,6 +1490,7 @@ function InternalTextInput(props: Props): React.Node { - mostRecentEventCount={mostRecentEventCount} - nativeID={id ?? props.nativeID} - numberOfLines={props.rows ?? props.numberOfLines} -+ maximumNumberOfLines={maxNumberOfLines} - onBlur={_onBlur} - onChange={_onChange} - onFocus={_onFocus} -diff --git a/node_modules/react-native/Libraries/Text/Text.js b/node_modules/react-native/Libraries/Text/Text.js -index df548af..e02f5da 100644 ---- a/node_modules/react-native/Libraries/Text/Text.js -+++ b/node_modules/react-native/Libraries/Text/Text.js -@@ -18,7 +18,11 @@ import processColor from '../StyleSheet/processColor'; - import {getAccessibilityRoleFromRole} from '../Utilities/AcessibilityMapping'; - import Platform from '../Utilities/Platform'; - import TextAncestor from './TextAncestor'; --import {NativeText, NativeVirtualText} from './TextNativeComponent'; -+import { -+ CONTAINS_MAX_NUMBER_OF_LINES_RENAME, -+ NativeText, -+ NativeVirtualText, -+} from './TextNativeComponent'; - import * as React from 'react'; - import {useContext, useMemo, useState} from 'react'; - -@@ -59,6 +63,7 @@ const Text: React.AbstractComponent< - pressRetentionOffset, - role, - suppressHighlighting, -+ numberOfLines, - ...restProps - } = props; - -@@ -192,14 +197,33 @@ const Text: React.AbstractComponent< - } - } - -- let numberOfLines = restProps.numberOfLines; -+ let numberOfLinesValue = numberOfLines; - if (numberOfLines != null && !(numberOfLines >= 0)) { - console.error( - `'numberOfLines' in must be a non-negative number, received: ${numberOfLines}. The value will be set to 0.`, - ); -- numberOfLines = 0; -+ numberOfLinesValue = 0; - } - -+ const numberOfLinesProps = useMemo((): { -+ maximumNumberOfLines?: ?number, -+ numberOfLines?: ?number, -+ } => { -+ // FIXME: Current logic is breaking all Text components. -+ // if (CONTAINS_MAX_NUMBER_OF_LINES_RENAME) { -+ // return { -+ // maximumNumberOfLines: numberOfLinesValue, -+ // }; -+ // } else { -+ // return { -+ // numberOfLines: numberOfLinesValue, -+ // }; -+ // } -+ return { -+ maximumNumberOfLines: numberOfLinesValue, -+ }; -+ }, [numberOfLinesValue]); -+ - const hasTextAncestor = useContext(TextAncestor); - - const _accessible = Platform.select({ -@@ -241,7 +265,6 @@ const Text: React.AbstractComponent< - isHighlighted={isHighlighted} - isPressable={isPressable} - nativeID={id ?? nativeID} -- numberOfLines={numberOfLines} - ref={forwardedRef} - selectable={_selectable} - selectionColor={selectionColor} -@@ -252,6 +275,7 @@ const Text: React.AbstractComponent< - - #import -+#import -+#import - - @implementation RCTMultilineTextInputViewManager - -@@ -17,8 +19,21 @@ - (UIView *)view - return [[RCTMultilineTextInputView alloc] initWithBridge:self.bridge]; - } - -+- (RCTShadowView *)shadowView -+{ -+ RCTBaseTextInputShadowView *shadowView = (RCTBaseTextInputShadowView *)[super shadowView]; -+ -+ shadowView.maximumNumberOfLines = 0; -+ shadowView.exactNumberOfLines = 0; -+ -+ return shadowView; -+} -+ - #pragma mark - Multiline (aka TextView) specific properties - - RCT_REMAP_VIEW_PROPERTY(dataDetectorTypes, backedTextInputView.dataDetectorTypes, UIDataDetectorTypes) - -+RCT_EXPORT_SHADOW_PROPERTY(maximumNumberOfLines, NSInteger) -+RCT_REMAP_SHADOW_PROPERTY(numberOfLines, exactNumberOfLines, NSInteger) -+ - @end -diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.h b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.h -index 8f4cf7e..6238ebc 100644 ---- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.h -+++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.h -@@ -16,6 +16,7 @@ NS_ASSUME_NONNULL_BEGIN - @property (nonatomic, copy, nullable) NSString *text; - @property (nonatomic, copy, nullable) NSString *placeholder; - @property (nonatomic, assign) NSInteger maximumNumberOfLines; -+@property (nonatomic, assign) NSInteger exactNumberOfLines; - @property (nonatomic, copy, nullable) RCTDirectEventBlock onContentSizeChange; - - - (void)uiManagerWillPerformMounting; -diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.m b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.m -index 04d2446..9d77743 100644 ---- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.m -+++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputShadowView.m -@@ -218,7 +218,22 @@ - (NSAttributedString *)measurableAttributedText - - - (CGSize)sizeThatFitsMinimumSize:(CGSize)minimumSize maximumSize:(CGSize)maximumSize - { -- NSAttributedString *attributedText = [self measurableAttributedText]; -+ NSMutableAttributedString *attributedText = [[self measurableAttributedText] mutableCopy]; -+ -+ /* -+ * The block below is responsible for setting the exact height of the view in lines -+ * Unfortunatelly, iOS doesn't export any easy way to do it. So we set maximumNumberOfLines -+ * prop and then add random lines at the front. However, they are only used for layout -+ * so they are not visible on the screen. -+ */ -+ if (self.exactNumberOfLines) { -+ NSMutableString *newLines = [NSMutableString stringWithCapacity:self.exactNumberOfLines]; -+ for (NSUInteger i = 0UL; i < self.exactNumberOfLines; ++i) { -+ [newLines appendString:@"\n"]; -+ } -+ [attributedText insertAttributedString:[[NSAttributedString alloc] initWithString:newLines attributes:self.textAttributes.effectiveTextAttributes] atIndex:0]; -+ _maximumNumberOfLines = self.exactNumberOfLines; -+ } - - if (!_textStorage) { - _textContainer = [NSTextContainer new]; -diff --git a/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTSinglelineTextInputViewManager.m b/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTSinglelineTextInputViewManager.m -index 413ac42..56d039c 100644 ---- a/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTSinglelineTextInputViewManager.m -+++ b/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTSinglelineTextInputViewManager.m -@@ -19,6 +19,7 @@ - (RCTShadowView *)shadowView - RCTBaseTextInputShadowView *shadowView = (RCTBaseTextInputShadowView *)[super shadowView]; - - shadowView.maximumNumberOfLines = 1; -+ shadowView.exactNumberOfLines = 0; - - return shadowView; - } -diff --git a/node_modules/react-native/Libraries/Text/TextNativeComponent.js b/node_modules/react-native/Libraries/Text/TextNativeComponent.js -index 0d59904..3216e43 100644 ---- a/node_modules/react-native/Libraries/Text/TextNativeComponent.js -+++ b/node_modules/react-native/Libraries/Text/TextNativeComponent.js -@@ -9,6 +9,7 @@ - */ - - import {createViewConfig} from '../NativeComponent/ViewConfig'; -+import getNativeComponentAttributes from '../ReactNative/getNativeComponentAttributes'; - import UIManager from '../ReactNative/UIManager'; - import createReactNativeComponentClass from '../Renderer/shims/createReactNativeComponentClass'; - import {type HostComponent} from '../Renderer/shims/ReactNativeTypes'; -@@ -18,6 +19,7 @@ import {type TextProps} from './TextProps'; - - type NativeTextProps = $ReadOnly<{ - ...TextProps, -+ maximumNumberOfLines?: ?number, - isHighlighted?: ?boolean, - selectionColor?: ?ProcessedColorValue, - onClick?: ?(event: PressEvent) => mixed, -@@ -31,7 +33,7 @@ const textViewConfig = { - validAttributes: { - isHighlighted: true, - isPressable: true, -- numberOfLines: true, -+ maximumNumberOfLines: true, - ellipsizeMode: true, - allowFontScaling: true, - dynamicTypeRamp: true, -@@ -73,6 +75,12 @@ export const NativeText: HostComponent = - createViewConfig(textViewConfig), - ): any); - -+const jestIsDefined = typeof jest !== 'undefined'; -+export const CONTAINS_MAX_NUMBER_OF_LINES_RENAME: boolean = jestIsDefined -+ ? true -+ : getNativeComponentAttributes('RCTText')?.NativeProps -+ ?.maximumNumberOfLines === 'number'; -+ - export const NativeVirtualText: HostComponent = - !global.RN$Bridgeless && !UIManager.hasViewManagerConfig('RCTVirtualText') - ? NativeText -diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewDefaults.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewDefaults.java -index 8cab407..ad5fa96 100644 ---- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewDefaults.java -+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewDefaults.java -@@ -12,5 +12,6 @@ public class ViewDefaults { - - public static final float FONT_SIZE_SP = 14.0f; - public static final int LINE_HEIGHT = 0; -- public static final int NUMBER_OF_LINES = Integer.MAX_VALUE; -+ public static final int NUMBER_OF_LINES = -1; -+ public static final int MAXIMUM_NUMBER_OF_LINES = Integer.MAX_VALUE; - } -diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java -index 3f76fa7..7a5d096 100644 ---- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java -+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java -@@ -96,6 +96,7 @@ public class ViewProps { - public static final String LETTER_SPACING = "letterSpacing"; - public static final String NEEDS_OFFSCREEN_ALPHA_COMPOSITING = "needsOffscreenAlphaCompositing"; - public static final String NUMBER_OF_LINES = "numberOfLines"; -+ public static final String MAXIMUM_NUMBER_OF_LINES = "maximumNumberOfLines"; - public static final String ELLIPSIZE_MODE = "ellipsizeMode"; - public static final String ADJUSTS_FONT_SIZE_TO_FIT = "adjustsFontSizeToFit"; - public static final String MINIMUM_FONT_SCALE = "minimumFontScale"; -diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java -index b5811c7..96eef96 100644 ---- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java -+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactBaseTextShadowNode.java -@@ -303,6 +303,7 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode { - protected boolean mIsAccessibilityLink = false; - - protected int mNumberOfLines = UNSET; -+ protected int mMaxNumberOfLines = UNSET; - protected int mTextAlign = Gravity.NO_GRAVITY; - protected int mTextBreakStrategy = - (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) ? 0 : Layout.BREAK_STRATEGY_HIGH_QUALITY; -@@ -387,6 +388,12 @@ public abstract class ReactBaseTextShadowNode extends LayoutShadowNode { - markUpdated(); - } - -+ @ReactProp(name = ViewProps.MAXIMUM_NUMBER_OF_LINES, defaultInt = UNSET) -+ public void setMaxNumberOfLines(int numberOfLines) { -+ mMaxNumberOfLines = numberOfLines == 0 ? UNSET : numberOfLines; -+ markUpdated(); -+ } -+ - @ReactProp(name = ViewProps.LINE_HEIGHT, defaultFloat = Float.NaN) - public void setLineHeight(float lineHeight) { - mTextAttributes.setLineHeight(lineHeight); -diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextAnchorViewManager.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextAnchorViewManager.java -index 7b5d0c1..c3032eb 100644 ---- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextAnchorViewManager.java -+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextAnchorViewManager.java -@@ -49,8 +49,8 @@ public abstract class ReactTextAnchorViewManager minimumFontSize -- && (mNumberOfLines != UNSET && layout.getLineCount() > mNumberOfLines -+ && (mMaxNumberOfLines != UNSET && layout.getLineCount() > mMaxNumberOfLines - || heightMode != YogaMeasureMode.UNDEFINED && layout.getHeight() > height)) { - // TODO: We could probably use a smarter algorithm here. This will require 0(n) - // measurements -@@ -124,9 +124,9 @@ public class ReactTextShadowNode extends ReactBaseTextShadowNode { - } - - final int lineCount = -- mNumberOfLines == UNSET -+ mMaxNumberOfLines == UNSET - ? layout.getLineCount() -- : Math.min(mNumberOfLines, layout.getLineCount()); -+ : Math.min(mMaxNumberOfLines, layout.getLineCount()); - - // Instead of using `layout.getWidth()` (which may yield a significantly larger width for - // text that is wrapping), compute width using the longest line. -diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java -index 190bc27..c2bcdc1 100644 ---- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java -+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextView.java -@@ -87,7 +87,7 @@ public class ReactTextView extends AppCompatTextView implements ReactCompoundVie - - mReactBackgroundManager = new ReactViewBackgroundManager(this); - -- mNumberOfLines = ViewDefaults.NUMBER_OF_LINES; -+ mNumberOfLines = ViewDefaults.MAXIMUM_NUMBER_OF_LINES; - mAdjustsFontSizeToFit = false; - mLinkifyMaskType = 0; - mNotifyOnInlineViewLayout = false; -@@ -576,7 +576,7 @@ public class ReactTextView extends AppCompatTextView implements ReactCompoundVie - } - - public void setNumberOfLines(int numberOfLines) { -- mNumberOfLines = numberOfLines == 0 ? ViewDefaults.NUMBER_OF_LINES : numberOfLines; -+ mNumberOfLines = numberOfLines == 0 ? ViewDefaults.MAXIMUM_NUMBER_OF_LINES : numberOfLines; - setSingleLine(mNumberOfLines == 1); - setMaxLines(mNumberOfLines); - } -@@ -596,7 +596,7 @@ public class ReactTextView extends AppCompatTextView implements ReactCompoundVie - public void updateView() { - @Nullable - TextUtils.TruncateAt ellipsizeLocation = -- mNumberOfLines == ViewDefaults.NUMBER_OF_LINES || mAdjustsFontSizeToFit -+ mNumberOfLines == ViewDefaults.MAXIMUM_NUMBER_OF_LINES || mAdjustsFontSizeToFit - ? null - : mEllipsizeLocation; - setEllipsize(ellipsizeLocation); -diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java -index 561a2d0..9409cfc 100644 ---- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java -+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java -@@ -18,6 +18,7 @@ import android.text.SpannableStringBuilder; - import android.text.Spanned; - import android.text.StaticLayout; - import android.text.TextPaint; -+import android.text.TextUtils; - import android.util.LayoutDirection; - import android.util.LruCache; - import android.view.View; -@@ -65,6 +66,7 @@ public class TextLayoutManager { - private static final String TEXT_BREAK_STRATEGY_KEY = "textBreakStrategy"; - private static final String HYPHENATION_FREQUENCY_KEY = "android_hyphenationFrequency"; - private static final String MAXIMUM_NUMBER_OF_LINES_KEY = "maximumNumberOfLines"; -+ private static final String NUMBER_OF_LINES_KEY = "numberOfLines"; - private static final LruCache sSpannableCache = - new LruCache<>(spannableCacheSize); - private static final ConcurrentHashMap sTagToSpannableCache = -@@ -385,6 +387,48 @@ public class TextLayoutManager { - ? paragraphAttributes.getInt(MAXIMUM_NUMBER_OF_LINES_KEY) - : UNSET; - -+ int numberOfLines = -+ paragraphAttributes.hasKey(NUMBER_OF_LINES_KEY) -+ ? paragraphAttributes.getInt(NUMBER_OF_LINES_KEY) -+ : UNSET; -+ -+ int lines = layout.getLineCount(); -+ if (numberOfLines != UNSET && numberOfLines != 0 && numberOfLines >= lines && text.length() > 0) { -+ int numberOfEmptyLines = numberOfLines - lines; -+ SpannableStringBuilder ssb = new SpannableStringBuilder(); -+ -+ // for some reason a newline on end causes issues with computing height so we add a character -+ if (text.toString().endsWith("\n")) { -+ ssb.append("A"); -+ } -+ -+ for (int i = 0; i < numberOfEmptyLines; ++i) { -+ ssb.append("\nA"); -+ } -+ -+ Object[] spans = text.getSpans(0, 0, Object.class); -+ for (Object span : spans) { // It's possible we need to set exl-exl -+ ssb.setSpan(span, 0, ssb.length(), text.getSpanFlags(span)); -+ }; -+ -+ text = new SpannableStringBuilder(TextUtils.concat(text, ssb)); -+ boring = null; -+ layout = createLayout( -+ text, -+ boring, -+ width, -+ widthYogaMeasureMode, -+ includeFontPadding, -+ textBreakStrategy, -+ hyphenationFrequency); -+ } -+ -+ -+ if (numberOfLines != UNSET && numberOfLines != 0) { -+ maximumNumberOfLines = numberOfLines; -+ } -+ -+ - int calculatedLineCount = - maximumNumberOfLines == UNSET || maximumNumberOfLines == 0 - ? layout.getLineCount() -diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java -index 0d118f0..0ae44b7 100644 ---- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java -+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java -@@ -18,6 +18,7 @@ import android.text.SpannableStringBuilder; - import android.text.Spanned; - import android.text.StaticLayout; - import android.text.TextPaint; -+import android.text.TextUtils; - import android.util.LayoutDirection; - import android.util.LruCache; - import android.view.View; -@@ -61,6 +62,7 @@ public class TextLayoutManagerMapBuffer { - public static final short PA_KEY_ADJUST_FONT_SIZE_TO_FIT = 3; - public static final short PA_KEY_INCLUDE_FONT_PADDING = 4; - public static final short PA_KEY_HYPHENATION_FREQUENCY = 5; -+ public static final short PA_KEY_NUMBER_OF_LINES = 6; - - private static final boolean ENABLE_MEASURE_LOGGING = ReactBuildConfig.DEBUG && false; - -@@ -399,6 +401,47 @@ public class TextLayoutManagerMapBuffer { - ? paragraphAttributes.getInt(PA_KEY_MAX_NUMBER_OF_LINES) - : UNSET; - -+ int numberOfLines = -+ paragraphAttributes.contains(PA_KEY_NUMBER_OF_LINES) -+ ? paragraphAttributes.getInt(PA_KEY_NUMBER_OF_LINES) -+ : UNSET; -+ -+ int lines = layout.getLineCount(); -+ if (numberOfLines != UNSET && numberOfLines != 0 && numberOfLines > lines && text.length() > 0) { -+ int numberOfEmptyLines = numberOfLines - lines; -+ SpannableStringBuilder ssb = new SpannableStringBuilder(); -+ -+ // for some reason a newline on end causes issues with computing height so we add a character -+ if (text.toString().endsWith("\n")) { -+ ssb.append("A"); -+ } -+ -+ for (int i = 0; i < numberOfEmptyLines; ++i) { -+ ssb.append("\nA"); -+ } -+ -+ Object[] spans = text.getSpans(0, 0, Object.class); -+ for (Object span : spans) { // It's possible we need to set exl-exl -+ ssb.setSpan(span, 0, ssb.length(), text.getSpanFlags(span)); -+ }; -+ -+ text = new SpannableStringBuilder(TextUtils.concat(text, ssb)); -+ boring = null; -+ layout = createLayout( -+ text, -+ boring, -+ width, -+ widthYogaMeasureMode, -+ includeFontPadding, -+ textBreakStrategy, -+ hyphenationFrequency); -+ } -+ -+ if (numberOfLines != UNSET && numberOfLines != 0) { -+ maximumNumberOfLines = numberOfLines; -+ } -+ -+ - int calculatedLineCount = - maximumNumberOfLines == UNSET || maximumNumberOfLines == 0 - ? layout.getLineCount() -diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java -index ced37be..ef2f321 100644 ---- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java -+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.java -@@ -548,7 +548,13 @@ public class ReactEditText extends AppCompatEditText - * href='https://android.googlesource.com/platform/frameworks/base/+/jb-release/core/java/android/widget/TextView.java'>TextView.java} - */ - if (isMultiline()) { -+ // we save max lines as setSingleLines overwrites it -+ // https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/widget/TextView.java#10671 -+ int maxLines = getMaxLines(); - setSingleLine(false); -+ if (maxLines != -1) { -+ setMaxLines(maxLines); -+ } - } - - // We override the KeyListener so that all keys on the soft input keyboard as well as hardware -diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputLocalData.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputLocalData.java -index a850510..c59be1d 100644 ---- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputLocalData.java -+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputLocalData.java -@@ -41,9 +41,9 @@ public final class ReactTextInputLocalData { - public void apply(EditText editText) { - editText.setText(mText); - editText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize); -+ editText.setInputType(mInputType); - editText.setMinLines(mMinLines); - editText.setMaxLines(mMaxLines); -- editText.setInputType(mInputType); - editText.setHint(mPlaceholder); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - editText.setBreakStrategy(mBreakStrategy); -diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java -index b27ace4..c6a2d63 100644 ---- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java -+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java -@@ -737,9 +737,18 @@ public class ReactTextInputManager extends BaseViewManager= Build.VERSION_CODES.M -diff --git a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.cpp b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.cpp -index 2994aca..fff0d5e 100644 ---- a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.cpp -+++ b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.cpp -@@ -16,6 +16,7 @@ namespace facebook::react { - - bool ParagraphAttributes::operator==(const ParagraphAttributes &rhs) const { - return std::tie( -+ numberOfLines, - maximumNumberOfLines, - ellipsizeMode, - textBreakStrategy, -@@ -23,6 +24,7 @@ bool ParagraphAttributes::operator==(const ParagraphAttributes &rhs) const { - includeFontPadding, - android_hyphenationFrequency) == - std::tie( -+ rhs.numberOfLines, - rhs.maximumNumberOfLines, - rhs.ellipsizeMode, - rhs.textBreakStrategy, -@@ -42,6 +44,7 @@ bool ParagraphAttributes::operator!=(const ParagraphAttributes &rhs) const { - #if RN_DEBUG_STRING_CONVERTIBLE - SharedDebugStringConvertibleList ParagraphAttributes::getDebugProps() const { - return { -+ debugStringConvertibleItem("numberOfLines", numberOfLines), - debugStringConvertibleItem("maximumNumberOfLines", maximumNumberOfLines), - debugStringConvertibleItem("ellipsizeMode", ellipsizeMode), - debugStringConvertibleItem("textBreakStrategy", textBreakStrategy), -diff --git a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.h b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.h -index f5f87c6..b7d1e90 100644 ---- a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.h -+++ b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/ParagraphAttributes.h -@@ -30,6 +30,11 @@ class ParagraphAttributes : public DebugStringConvertible { - public: - #pragma mark - Fields - -+ /* -+ * Number of lines which paragraph takes. -+ */ -+ int numberOfLines{}; -+ - /* - * Maximum number of lines which paragraph can take. - * Zero value represents "no limit". -@@ -92,6 +97,7 @@ struct hash { - const facebook::react::ParagraphAttributes &attributes) const { - return folly::hash::hash_combine( - 0, -+ attributes.numberOfLines, - attributes.maximumNumberOfLines, - attributes.ellipsizeMode, - attributes.textBreakStrategy, -diff --git a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/conversions.h b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/conversions.h -index 8687b89..eab75f4 100644 ---- a/node_modules/react-native/ReactCommon/react/renderer/attributedstring/conversions.h -+++ b/node_modules/react-native/ReactCommon/react/renderer/attributedstring/conversions.h -@@ -835,10 +835,16 @@ inline ParagraphAttributes convertRawProp( - ParagraphAttributes const &defaultParagraphAttributes) { - auto paragraphAttributes = ParagraphAttributes{}; - -- paragraphAttributes.maximumNumberOfLines = convertRawProp( -+ paragraphAttributes.numberOfLines = convertRawProp( - context, - rawProps, - "numberOfLines", -+ sourceParagraphAttributes.numberOfLines, -+ defaultParagraphAttributes.numberOfLines); -+ paragraphAttributes.maximumNumberOfLines = convertRawProp( -+ context, -+ rawProps, -+ "maximumNumberOfLines", - sourceParagraphAttributes.maximumNumberOfLines, - defaultParagraphAttributes.maximumNumberOfLines); - paragraphAttributes.ellipsizeMode = convertRawProp( -@@ -913,6 +919,7 @@ inline std::string toString(AttributedString::Range const &range) { - inline folly::dynamic toDynamic( - const ParagraphAttributes ¶graphAttributes) { - auto values = folly::dynamic::object(); -+ values("numberOfLines", paragraphAttributes.numberOfLines); - values("maximumNumberOfLines", paragraphAttributes.maximumNumberOfLines); - values("ellipsizeMode", toString(paragraphAttributes.ellipsizeMode)); - values("textBreakStrategy", toString(paragraphAttributes.textBreakStrategy)); -@@ -1118,6 +1125,7 @@ constexpr static MapBuffer::Key PA_KEY_TEXT_BREAK_STRATEGY = 2; - constexpr static MapBuffer::Key PA_KEY_ADJUST_FONT_SIZE_TO_FIT = 3; - constexpr static MapBuffer::Key PA_KEY_INCLUDE_FONT_PADDING = 4; - constexpr static MapBuffer::Key PA_KEY_HYPHENATION_FREQUENCY = 5; -+constexpr static MapBuffer::Key PA_KEY_NUMBER_OF_LINES = 6; - - inline MapBuffer toMapBuffer(const ParagraphAttributes ¶graphAttributes) { - auto builder = MapBufferBuilder(); -@@ -1135,6 +1143,8 @@ inline MapBuffer toMapBuffer(const ParagraphAttributes ¶graphAttributes) { - builder.putString( - PA_KEY_HYPHENATION_FREQUENCY, - toString(paragraphAttributes.android_hyphenationFrequency)); -+ builder.putInt( -+ PA_KEY_NUMBER_OF_LINES, paragraphAttributes.numberOfLines); - - return builder.build(); - } -diff --git a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp -index 9953e22..98eb3da 100644 ---- a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp -+++ b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.cpp -@@ -56,6 +56,10 @@ AndroidTextInputProps::AndroidTextInputProps( - "numberOfLines", - sourceProps.numberOfLines, - {0})), -+ maximumNumberOfLines(CoreFeatures::enablePropIteratorSetter? sourceProps.maximumNumberOfLines : convertRawProp(context, rawProps, -+ "maximumNumberOfLines", -+ sourceProps.maximumNumberOfLines, -+ {0})), - disableFullscreenUI(CoreFeatures::enablePropIteratorSetter? sourceProps.disableFullscreenUI : convertRawProp(context, rawProps, - "disableFullscreenUI", - sourceProps.disableFullscreenUI, -@@ -281,6 +285,12 @@ void AndroidTextInputProps::setProp( - value, - paragraphAttributes, - maximumNumberOfLines, -+ "maximumNumberOfLines"); -+ REBUILD_FIELD_SWITCH_CASE( -+ paDefaults, -+ value, -+ paragraphAttributes, -+ numberOfLines, - "numberOfLines"); - REBUILD_FIELD_SWITCH_CASE( - paDefaults, value, paragraphAttributes, ellipsizeMode, "ellipsizeMode"); -@@ -323,6 +333,7 @@ void AndroidTextInputProps::setProp( - } - - switch (hash) { -+ RAW_SET_PROP_SWITCH_CASE_BASIC(maximumNumberOfLines); - RAW_SET_PROP_SWITCH_CASE_BASIC(autoComplete); - RAW_SET_PROP_SWITCH_CASE_BASIC(returnKeyLabel); - RAW_SET_PROP_SWITCH_CASE_BASIC(numberOfLines); -@@ -422,6 +433,7 @@ void AndroidTextInputProps::setProp( - // TODO T53300085: support this in codegen; this was hand-written - folly::dynamic AndroidTextInputProps::getDynamic() const { - folly::dynamic props = folly::dynamic::object(); -+ props["maximumNumberOfLines"] = maximumNumberOfLines; - props["autoComplete"] = autoComplete; - props["returnKeyLabel"] = returnKeyLabel; - props["numberOfLines"] = numberOfLines; -diff --git a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.h b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.h -index ba39ebb..ead28e3 100644 ---- a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.h -+++ b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputProps.h -@@ -84,6 +84,7 @@ class AndroidTextInputProps final : public ViewProps, public BaseTextProps { - std::string autoComplete{}; - std::string returnKeyLabel{}; - int numberOfLines{0}; -+ int maximumNumberOfLines{0}; - bool disableFullscreenUI{false}; - std::string textBreakStrategy{}; - SharedColor underlineColorAndroid{}; -diff --git a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm -index 368c334..a1bb33e 100644 ---- a/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm -+++ b/node_modules/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm -@@ -244,26 +244,51 @@ - (void)getRectWithAttributedString:(AttributedString)attributedString - - #pragma mark - Private - --- (NSTextStorage *)_textStorageForNSAttributesString:(NSAttributedString *)attributedString -++- (NSTextStorage *)_textStorageForNSAttributesString:(NSAttributedString *)inputAttributedString - paragraphAttributes:(ParagraphAttributes)paragraphAttributes - size:(CGSize)size - { -- NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:size]; -+ NSMutableAttributedString *attributedString = [ inputAttributedString mutableCopy]; -+ -+ /* -+ * The block below is responsible for setting the exact height of the view in lines -+ * Unfortunatelly, iOS doesn't export any easy way to do it. So we set maximumNumberOfLines -+ * prop and then add random lines at the front. However, they are only used for layout -+ * so they are not visible on the screen. This method is used for drawing only for Paragraph component -+ * but we set exact height in lines only on TextInput that doesn't use it. -+ */ -+ if (paragraphAttributes.numberOfLines) { -+ paragraphAttributes.maximumNumberOfLines = paragraphAttributes.numberOfLines; -+ NSMutableString *newLines = [NSMutableString stringWithCapacity: paragraphAttributes.numberOfLines]; -+ for (NSUInteger i = 0UL; i < paragraphAttributes.numberOfLines; ++i) { -+ // K is added on purpose. New line seems to be not enough for NTtextContainer -+ [newLines appendString:@"K\n"]; -+ } -+ NSDictionary * attributesOfFirstCharacter = [inputAttributedString attributesAtIndex:0 effectiveRange:NULL]; - -- textContainer.lineFragmentPadding = 0.0; // Note, the default value is 5. -- textContainer.lineBreakMode = paragraphAttributes.maximumNumberOfLines > 0 -- ? RCTNSLineBreakModeFromEllipsizeMode(paragraphAttributes.ellipsizeMode) -- : NSLineBreakByClipping; -- textContainer.maximumNumberOfLines = paragraphAttributes.maximumNumberOfLines; -+ [attributedString insertAttributedString:[[NSAttributedString alloc] initWithString:newLines attributes:attributesOfFirstCharacter] atIndex:0]; -+ } -+ -+ NSTextContainer *textContainer = [NSTextContainer new]; - - NSLayoutManager *layoutManager = [NSLayoutManager new]; - layoutManager.usesFontLeading = NO; - [layoutManager addTextContainer:textContainer]; - -- NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedString]; -+ NSTextStorage *textStorage = [NSTextStorage new]; - - [textStorage addLayoutManager:layoutManager]; - -+ textContainer.lineFragmentPadding = 0.0; // Note, the default value is 5. -+ textContainer.lineBreakMode = paragraphAttributes.maximumNumberOfLines > 0 -+ ? RCTNSLineBreakModeFromEllipsizeMode(paragraphAttributes.ellipsizeMode) -+ : NSLineBreakByClipping; -+ textContainer.size = size; -+ textContainer.maximumNumberOfLines = paragraphAttributes.maximumNumberOfLines; -+ -+ [textStorage replaceCharactersInRange:(NSRange){0, textStorage.length} withAttributedString:attributedString]; -+ -+ - if (paragraphAttributes.adjustsFontSizeToFit) { - CGFloat minimumFontSize = !isnan(paragraphAttributes.minimumFontSize) ? paragraphAttributes.minimumFontSize : 4.0; - CGFloat maximumFontSize = !isnan(paragraphAttributes.maximumFontSize) ? paragraphAttributes.maximumFontSize : 96.0; From 0c3ba38b6c3e5bdcac15eba3aaefb289196958be Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 17 Nov 2023 12:47:35 +0100 Subject: [PATCH 058/319] add numberOfLines workaround --- src/components/Composer/index.android.js | 35 +++++++++---------- src/components/Composer/index.ios.js | 30 ++++++++-------- .../updateNumberOfLines/index.native.ts | 2 ++ 3 files changed, 35 insertions(+), 32 deletions(-) diff --git a/src/components/Composer/index.android.js b/src/components/Composer/index.android.js index 278965cbc81a..be5511b70005 100644 --- a/src/components/Composer/index.android.js +++ b/src/components/Composer/index.android.js @@ -1,11 +1,14 @@ import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useRef} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {StyleSheet} from 'react-native'; import _ from 'underscore'; import RNTextInput from '@components/RNTextInput'; import * as ComposerUtils from '@libs/ComposerUtils'; +import styles from '@styles/styles'; import themeColors from '@styles/themes/default'; +const COMPOSER_LINE_HEIGHT = styles.textInputCompose.lineHeight || 0; + const propTypes = { /** Maximum number of lines in the text input */ maxLines: PropTypes.number, @@ -92,16 +95,17 @@ function Composer({shouldClear, onClear, isDisabled, maxLines, forwardedRef, isC onClear(); }, [shouldClear, onClear]); - /** - * Set maximum number of lines - * @return {Number} - */ - const maxNumberOfLines = useMemo(() => { - if (isComposerFullSize) { - return 1000000; - } - return maxLines; - }, [isComposerFullSize, maxLines]); + const [numberOfLines, setNumberOfLines] = useState(1); + const heightStyle = useMemo( + () => + StyleSheet.create({ + composerHeight: { + height: numberOfLines * COMPOSER_LINE_HEIGHT, + maxHeight: maxLines * COMPOSER_LINE_HEIGHT, + }, + }).composerHeight, + [maxLines, numberOfLines], + ); const composerStyles = useMemo(() => { StyleSheet.flatten(props.style); @@ -112,15 +116,10 @@ function Composer({shouldClear, onClear, isDisabled, maxLines, forwardedRef, isC autoComplete="off" placeholderTextColor={themeColors.placeholderText} ref={setTextInputRef} - onContentSizeChange={(e) => ComposerUtils.updateNumberOfLines({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e)} + onContentSizeChange={(e) => setNumberOfLines(ComposerUtils.updateNumberOfLines({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e))} rejectResponderTermination={false} - // Setting a really high number here fixes an issue with the `maxNumberOfLines` prop on TextInput, where on Android the text input would collapse to only one line, - // when it should actually expand to the container (https://github.com/Expensify/App/issues/11694#issuecomment-1560520670) - // @Szymon20000 is working on fixing this (android-only) issue in the in the upstream PR (https://github.com/facebook/react-native/pulls?q=is%3Apr+is%3Aopen+maxNumberOfLines) - // TODO: remove this comment once upstream PR is merged and available in a future release - maxNumberOfLines={maxNumberOfLines} textAlignVertical="center" - style={[composerStyles]} + style={[composerStyles, heightStyle]} /* eslint-disable-next-line react/jsx-props-no-spreading */ {...props} readOnly={isDisabled} diff --git a/src/components/Composer/index.ios.js b/src/components/Composer/index.ios.js index a1b8c1a4ffe6..980989ff6412 100644 --- a/src/components/Composer/index.ios.js +++ b/src/components/Composer/index.ios.js @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useRef} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {StyleSheet} from 'react-native'; import _ from 'underscore'; import RNTextInput from '@components/RNTextInput'; @@ -7,6 +7,8 @@ import * as ComposerUtils from '@libs/ComposerUtils'; import styles from '@styles/styles'; import themeColors from '@styles/themes/default'; +const COMPOSER_LINE_HEIGHT = styles.textInputCompose.lineHeight || 0; + const propTypes = { /** If the input should clear, it actually gets intercepted instead of .clear() */ shouldClear: PropTypes.bool, @@ -93,16 +95,17 @@ function Composer({shouldClear, onClear, isDisabled, maxLines, forwardedRef, isC onClear(); }, [shouldClear, onClear]); - /** - * Set maximum number of lines - * @return {Number} - */ - const maxNumberOfLines = useMemo(() => { - if (isComposerFullSize) { - return; - } - return maxLines; - }, [isComposerFullSize, maxLines]); + const [numberOfLines, setNumberOfLines] = useState(1); + const heightStyle = useMemo( + () => + StyleSheet.create({ + composerHeight: { + height: numberOfLines * COMPOSER_LINE_HEIGHT, + maxHeight: maxLines * COMPOSER_LINE_HEIGHT, + }, + }).composerHeight, + [maxLines, numberOfLines], + ); const composerStyles = useMemo(() => { StyleSheet.flatten(props.style); @@ -117,11 +120,10 @@ function Composer({shouldClear, onClear, isDisabled, maxLines, forwardedRef, isC autoComplete="off" placeholderTextColor={themeColors.placeholderText} ref={setTextInputRef} - onContentSizeChange={(e) => ComposerUtils.updateNumberOfLines({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e)} + onContentSizeChange={(e) => setNumberOfLines(ComposerUtils.updateNumberOfLines({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e))} rejectResponderTermination={false} smartInsertDelete={false} - maxNumberOfLines={maxNumberOfLines} - style={[composerStyles, styles.verticalAlignMiddle]} + style={[composerStyles, styles.verticalAlignMiddle, heightStyle]} /* eslint-disable-next-line react/jsx-props-no-spreading */ {...propsToPass} readOnly={isDisabled} diff --git a/src/libs/ComposerUtils/updateNumberOfLines/index.native.ts b/src/libs/ComposerUtils/updateNumberOfLines/index.native.ts index df9292ecd690..ef89ef2dc0d8 100644 --- a/src/libs/ComposerUtils/updateNumberOfLines/index.native.ts +++ b/src/libs/ComposerUtils/updateNumberOfLines/index.native.ts @@ -16,6 +16,8 @@ const updateNumberOfLines: UpdateNumberOfLines = (props, event) => { } const numberOfLines = getNumberOfLines(lineHeight, paddingTopAndBottom, inputHeight); updateIsFullComposerAvailable(props, numberOfLines); + + return numberOfLines; }; export default updateNumberOfLines; From 8f830ab1d06e2b506d7aee35e20e866c67ba5565 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 17 Nov 2023 13:57:04 +0100 Subject: [PATCH 059/319] fix: workaround for iOS --- src/components/Composer/index.ios.js | 30 ++++++++++------------------ 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/src/components/Composer/index.ios.js b/src/components/Composer/index.ios.js index 980989ff6412..090e67827ccd 100644 --- a/src/components/Composer/index.ios.js +++ b/src/components/Composer/index.ios.js @@ -1,6 +1,5 @@ import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {StyleSheet} from 'react-native'; +import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import _ from 'underscore'; import RNTextInput from '@components/RNTextInput'; import * as ComposerUtils from '@libs/ComposerUtils'; @@ -95,37 +94,28 @@ function Composer({shouldClear, onClear, isDisabled, maxLines, forwardedRef, isC onClear(); }, [shouldClear, onClear]); - const [numberOfLines, setNumberOfLines] = useState(1); - const heightStyle = useMemo( - () => - StyleSheet.create({ - composerHeight: { - height: numberOfLines * COMPOSER_LINE_HEIGHT, - maxHeight: maxLines * COMPOSER_LINE_HEIGHT, - }, - }).composerHeight, - [maxLines, numberOfLines], + const maxHeightStyle = useMemo( + () => ({ + maxHeight: maxLines * COMPOSER_LINE_HEIGHT, + }), + [maxLines], ); - const composerStyles = useMemo(() => { - StyleSheet.flatten(props.style); - }, [props.style]); - // On native layers we like to have the Text Input not focused so the // user can read new chats without the keyboard in the way of the view. // On Android the selection prop is required on the TextInput but this prop has issues on IOS const propsToPass = _.omit(props, 'selection'); return ( setNumberOfLines(ComposerUtils.updateNumberOfLines({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e))} + onContentSizeChange={(e) => ComposerUtils.updateNumberOfLines({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e)} rejectResponderTermination={false} smartInsertDelete={false} - style={[composerStyles, styles.verticalAlignMiddle, heightStyle]} - /* eslint-disable-next-line react/jsx-props-no-spreading */ - {...propsToPass} + style={[...props.style, isComposerFullSize ? undefined : maxHeightStyle]} readOnly={isDisabled} /> ); From 569c388e19a59040447b530e317fedd26e3084c1 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 17 Nov 2023 14:08:28 +0100 Subject: [PATCH 060/319] fix: android implementation --- src/components/Composer/index.android.js | 25 ++++++++---------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/src/components/Composer/index.android.js b/src/components/Composer/index.android.js index be5511b70005..9e27c5bb9b60 100644 --- a/src/components/Composer/index.android.js +++ b/src/components/Composer/index.android.js @@ -1,6 +1,5 @@ import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {StyleSheet} from 'react-native'; import _ from 'underscore'; import RNTextInput from '@components/RNTextInput'; import * as ComposerUtils from '@libs/ComposerUtils'; @@ -96,32 +95,24 @@ function Composer({shouldClear, onClear, isDisabled, maxLines, forwardedRef, isC }, [shouldClear, onClear]); const [numberOfLines, setNumberOfLines] = useState(1); - const heightStyle = useMemo( - () => - StyleSheet.create({ - composerHeight: { - height: numberOfLines * COMPOSER_LINE_HEIGHT, - maxHeight: maxLines * COMPOSER_LINE_HEIGHT, - }, - }).composerHeight, - [maxLines, numberOfLines], + const maxHeightStyle = useMemo( + () => ({ + maxHeight: maxLines * COMPOSER_LINE_HEIGHT, + }), + [maxLines], ); - const composerStyles = useMemo(() => { - StyleSheet.flatten(props.style); - }, [props.style]); - return ( setNumberOfLines(ComposerUtils.updateNumberOfLines({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e))} rejectResponderTermination={false} textAlignVertical="center" - style={[composerStyles, heightStyle]} - /* eslint-disable-next-line react/jsx-props-no-spreading */ - {...props} + style={[...props.style, maxHeightStyle]} readOnly={isDisabled} /> ); From d8acbd9b11864de410df5772475af88cf79888bd Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Fri, 17 Nov 2023 14:44:31 +0100 Subject: [PATCH 061/319] Fix TS issues --- src/components/OfflineIndicator.tsx | 35 ++++++++++++----------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/src/components/OfflineIndicator.tsx b/src/components/OfflineIndicator.tsx index 197f63ff17aa..7b980b663d04 100644 --- a/src/components/OfflineIndicator.tsx +++ b/src/components/OfflineIndicator.tsx @@ -1,34 +1,29 @@ import React from 'react'; import {StyleProp, View, ViewStyle} from 'react-native'; import {OnyxEntry} from 'react-native-onyx/lib/types'; -import compose from '@libs/compose'; import styles from '@styles/styles'; import variables from '@styles/variables'; import type {Network} from '@src/types/onyx'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; +import type {LocaleContextProps} from './LocaleContextProvider'; import {withNetwork} from './OnyxProvider'; import Text from './Text'; import withLocalize from './withLocalize'; import withWindowDimensions from './withWindowDimensions'; +import type {WindowDimensionsProps} from './withWindowDimensions/types'; -type OfflineIndicatorProps = { - /** Information about the network */ - network: OnyxEntry; +type OfflineIndicatorProps = LocaleContextProps & + WindowDimensionsProps & { + /** Information about the network */ + network: OnyxEntry; - /** Optional styles for container element that will override the default styling for the offline indicator */ - containerStyles?: StyleProp; + /** Optional styles for container element that will override the default styling for the offline indicator */ + containerStyles?: StyleProp; - /** Optional styles for the container */ - style?: StyleProp; - - /** Is the window width narrow, like on a mobile device */ - isSmallScreenWidth: boolean; - - // TODO: remove after withLocalize migrated - /** Returns translated string for given locale and phrase */ - translate: (value: string) => string; -}; + /** Optional styles for the container */ + style?: StyleProp; + }; const setStyles = (containerStyles: StyleProp, isSmallScreenWidth: boolean): StyleProp => { if (!!containerStyles && ((Array.isArray(containerStyles) && containerStyles.length) || Object.keys(containerStyles).length)) { @@ -38,7 +33,7 @@ const setStyles = (containerStyles: StyleProp, isSmallScreenWidth: bo return isSmallScreenWidth ? styles.offlineIndicatorMobile : styles.offlineIndicator; }; -function OfflineIndicator({network, isSmallScreenWidth, translate, style = [], containerStyles = []}: OfflineIndicatorProps) { +function OfflineIndicator({network, isSmallScreenWidth, translate, style, containerStyles}: OfflineIndicatorProps) { if (!network?.isOffline) { return null; } @@ -57,7 +52,5 @@ function OfflineIndicator({network, isSmallScreenWidth, translate, style = [], c OfflineIndicator.displayName = 'OfflineIndicator'; -// TODO: remove when withWindowDimensions, withLocalize are migrated -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore -export default compose(withWindowDimensions, withLocalize, withNetwork())(OfflineIndicator); +// TODO: use `compose` function for HOCs composing once TypeScript issues are resolved. +export default withNetwork()(withLocalize(withWindowDimensions(OfflineIndicator))); From ae5d0aa530067aa5b79de63a1c02ebd27c7c1d61 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 17 Nov 2023 16:21:54 +0100 Subject: [PATCH 062/319] fix: android --- src/components/Composer/index.android.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/Composer/index.android.js b/src/components/Composer/index.android.js index 9e27c5bb9b60..477e8b2f88a3 100644 --- a/src/components/Composer/index.android.js +++ b/src/components/Composer/index.android.js @@ -94,7 +94,6 @@ function Composer({shouldClear, onClear, isDisabled, maxLines, forwardedRef, isC onClear(); }, [shouldClear, onClear]); - const [numberOfLines, setNumberOfLines] = useState(1); const maxHeightStyle = useMemo( () => ({ maxHeight: maxLines * COMPOSER_LINE_HEIGHT, @@ -109,10 +108,10 @@ function Composer({shouldClear, onClear, isDisabled, maxLines, forwardedRef, isC autoComplete="off" placeholderTextColor={themeColors.placeholderText} ref={setTextInputRef} - onContentSizeChange={(e) => setNumberOfLines(ComposerUtils.updateNumberOfLines({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e))} + onContentSizeChange={(e) => ComposerUtils.updateNumberOfLines({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e)} rejectResponderTermination={false} textAlignVertical="center" - style={[...props.style, maxHeightStyle]} + style={[...props.style, isComposerFullSize ? undefined : maxHeightStyle]} readOnly={isDisabled} /> ); From a6c6602a0cb89ae18ad9e56d78f0de11427b845c Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 17 Nov 2023 16:26:10 +0100 Subject: [PATCH 063/319] fix: extract style --- src/components/Composer/index.android.js | 15 ++++----------- src/components/Composer/index.ios.js | 13 +++---------- src/styles/StyleUtils.ts | 14 ++++++++++++++ 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/components/Composer/index.android.js b/src/components/Composer/index.android.js index 477e8b2f88a3..7b72e17ae5fe 100644 --- a/src/components/Composer/index.android.js +++ b/src/components/Composer/index.android.js @@ -1,13 +1,11 @@ import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import _ from 'underscore'; import RNTextInput from '@components/RNTextInput'; import * as ComposerUtils from '@libs/ComposerUtils'; -import styles from '@styles/styles'; +import {getComposerMaxHeightStyle} from '@styles/StyleUtils'; import themeColors from '@styles/themes/default'; -const COMPOSER_LINE_HEIGHT = styles.textInputCompose.lineHeight || 0; - const propTypes = { /** Maximum number of lines in the text input */ maxLines: PropTypes.number, @@ -94,12 +92,7 @@ function Composer({shouldClear, onClear, isDisabled, maxLines, forwardedRef, isC onClear(); }, [shouldClear, onClear]); - const maxHeightStyle = useMemo( - () => ({ - maxHeight: maxLines * COMPOSER_LINE_HEIGHT, - }), - [maxLines], - ); + const maxHeightStyle = useMemo(() => getComposerMaxHeightStyle(maxLines, isComposerFullSize), [isComposerFullSize, maxLines]); return ( ComposerUtils.updateNumberOfLines({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e)} rejectResponderTermination={false} textAlignVertical="center" - style={[...props.style, isComposerFullSize ? undefined : maxHeightStyle]} + style={[...props.style, maxHeightStyle]} readOnly={isDisabled} /> ); diff --git a/src/components/Composer/index.ios.js b/src/components/Composer/index.ios.js index 090e67827ccd..efbda8cae2ad 100644 --- a/src/components/Composer/index.ios.js +++ b/src/components/Composer/index.ios.js @@ -3,11 +3,9 @@ import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import _ from 'underscore'; import RNTextInput from '@components/RNTextInput'; import * as ComposerUtils from '@libs/ComposerUtils'; -import styles from '@styles/styles'; +import {getComposerMaxHeightStyle} from '@styles/StyleUtils'; import themeColors from '@styles/themes/default'; -const COMPOSER_LINE_HEIGHT = styles.textInputCompose.lineHeight || 0; - const propTypes = { /** If the input should clear, it actually gets intercepted instead of .clear() */ shouldClear: PropTypes.bool, @@ -94,12 +92,7 @@ function Composer({shouldClear, onClear, isDisabled, maxLines, forwardedRef, isC onClear(); }, [shouldClear, onClear]); - const maxHeightStyle = useMemo( - () => ({ - maxHeight: maxLines * COMPOSER_LINE_HEIGHT, - }), - [maxLines], - ); + const maxHeightStyle = useMemo(() => getComposerMaxHeightStyle(maxLines, isComposerFullSize), [isComposerFullSize, maxLines]); // On native layers we like to have the Text Input not focused so the // user can read new chats without the keyboard in the way of the view. @@ -115,7 +108,7 @@ function Composer({shouldClear, onClear, isDisabled, maxLines, forwardedRef, isC onContentSizeChange={(e) => ComposerUtils.updateNumberOfLines({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e)} rejectResponderTermination={false} smartInsertDelete={false} - style={[...props.style, isComposerFullSize ? undefined : maxHeightStyle]} + style={[...props.style, maxHeightStyle]} readOnly={isDisabled} /> ); diff --git a/src/styles/StyleUtils.ts b/src/styles/StyleUtils.ts index a99d292e9dc6..b9b0347c9d7c 100644 --- a/src/styles/StyleUtils.ts +++ b/src/styles/StyleUtils.ts @@ -1389,6 +1389,19 @@ function getDotIndicatorTextStyles(isErrorText = true): TextStyle { return isErrorText ? {...styles.offlineFeedback.text, color: styles.formError.color} : {...styles.offlineFeedback.text}; } +/** + * Returns container styles for showing the icons in MultipleAvatars/SubscriptAvatar + */ +function getComposerMaxHeightStyle(maxLines: number, isComposerFullSize: boolean): ViewStyle | undefined { + const composerLineHeight = styles.textInputCompose.lineHeight ?? 0; + + return isComposerFullSize + ? undefined + : { + maxHeight: maxLines * composerLineHeight, + }; +} + export { combineStyles, displayIfTrue, @@ -1473,4 +1486,5 @@ export { getContainerStyles, getEReceiptColorStyles, getEReceiptColorCode, + getComposerMaxHeightStyle, }; From abbd499e46b5509e6fbcac9b23830e1728f023b5 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 17 Nov 2023 16:53:01 +0100 Subject: [PATCH 064/319] remove unused style --- src/styles/styles.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/styles/styles.ts b/src/styles/styles.ts index 63e219d7e953..a0c17d01a7a8 100644 --- a/src/styles/styles.ts +++ b/src/styles/styles.ts @@ -328,10 +328,6 @@ const styles = (theme: ThemeColors) => textAlign: 'left', }, - verticalAlignMiddle: { - verticalAlign: 'middle', - }, - verticalAlignTop: { verticalAlign: 'top', }, From 43a03239629f22af6360349889f973d1c875588e Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 17 Nov 2023 16:53:43 +0100 Subject: [PATCH 065/319] remove unused return --- src/libs/ComposerUtils/updateNumberOfLines/index.native.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libs/ComposerUtils/updateNumberOfLines/index.native.ts b/src/libs/ComposerUtils/updateNumberOfLines/index.native.ts index ef89ef2dc0d8..df9292ecd690 100644 --- a/src/libs/ComposerUtils/updateNumberOfLines/index.native.ts +++ b/src/libs/ComposerUtils/updateNumberOfLines/index.native.ts @@ -16,8 +16,6 @@ const updateNumberOfLines: UpdateNumberOfLines = (props, event) => { } const numberOfLines = getNumberOfLines(lineHeight, paddingTopAndBottom, inputHeight); updateIsFullComposerAvailable(props, numberOfLines); - - return numberOfLines; }; export default updateNumberOfLines; From e2678855271fc25e320e9292b70d575c71485c7a Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Fri, 17 Nov 2023 17:30:44 +0100 Subject: [PATCH 066/319] Show top skeleton when offline; Show animation always. --- .../ListBoundaryLoader/ListBoundaryLoader.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/pages/home/report/ListBoundaryLoader/ListBoundaryLoader.js b/src/pages/home/report/ListBoundaryLoader/ListBoundaryLoader.js index 6dd56471af07..7184944d5902 100644 --- a/src/pages/home/report/ListBoundaryLoader/ListBoundaryLoader.js +++ b/src/pages/home/report/ListBoundaryLoader/ListBoundaryLoader.js @@ -36,22 +36,20 @@ function ListBoundaryLoader({type, isLoadingOlderReportActions, isLoadingInitial const styles = useThemeStyles(); const {isOffline} = useNetwork(); - // we use two different loading components for header and footer to reduce the jumping effect when you scrolling to the newer reports + // We use two different loading components for the header and footer + // to reduce the jumping effect when the user is scrolling to the newer report actions if (type === CONST.LIST_COMPONENTS.FOOTER) { if (isLoadingOlderReportActions) { return ; } - // Make sure the oldest report action loaded is not the first. This is so we do not show the - // skeleton view above the created action in a newly generated optimistic chat or one with not + // Make sure the report chat is not loaded till the beginning. This is so we do not show the + // skeleton view above the "created" action in a newly generated optimistic chat or one with not // that many comments. - if (isLoadingInitialReportActions && lastReportActionName !== CONST.REPORT.ACTIONS.TYPE.CREATED) { - return ( - - ); + // Also, if we are offline and the report is not yet loaded till the beginning, we assume there are more actions to load, + // therefore show the skeleton view, even though the actions are not loading. + if (lastReportActionName !== CONST.REPORT.ACTIONS.TYPE.CREATED && (isLoadingInitialReportActions || isOffline)) { + return ; } } if (type === CONST.LIST_COMPONENTS.HEADER && isLoadingNewerReportActions) { From bbe702f59339b9bb039c126c2f019700228c43d9 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Fri, 17 Nov 2023 18:11:07 +0100 Subject: [PATCH 067/319] Update styles check --- src/components/OfflineIndicator.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/OfflineIndicator.tsx b/src/components/OfflineIndicator.tsx index 7b980b663d04..b333fb5bb1bb 100644 --- a/src/components/OfflineIndicator.tsx +++ b/src/components/OfflineIndicator.tsx @@ -26,7 +26,7 @@ type OfflineIndicatorProps = LocaleContextProps & }; const setStyles = (containerStyles: StyleProp, isSmallScreenWidth: boolean): StyleProp => { - if (!!containerStyles && ((Array.isArray(containerStyles) && containerStyles.length) || Object.keys(containerStyles).length)) { + if (containerStyles) { return containerStyles; } From ae7dde6e3d35edd3be35a2aa3bf1f4f8b02a38f2 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Sat, 18 Nov 2023 08:31:53 +0700 Subject: [PATCH 068/319] fix remove private note navigate user to detail page --- src/pages/PrivateNotes/PrivateNotesEditPage.js | 10 +++++++--- src/pages/PrivateNotes/PrivateNotesListPage.js | 7 ++++--- src/pages/ProfilePage.js | 11 ++++++++++- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/pages/PrivateNotes/PrivateNotesEditPage.js b/src/pages/PrivateNotes/PrivateNotesEditPage.js index cf4dd7beded4..bf0905c2d052 100644 --- a/src/pages/PrivateNotes/PrivateNotesEditPage.js +++ b/src/pages/PrivateNotes/PrivateNotesEditPage.js @@ -1,6 +1,7 @@ import {useFocusEffect} from '@react-navigation/native'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import Str from 'expensify-common/lib/str'; +import {isEmpty} from 'lodash'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useMemo, useRef, useState} from 'react'; @@ -18,6 +19,7 @@ import withLocalize from '@components/withLocalize'; import useLocalize from '@hooks/useLocalize'; import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; +import * as ReportUtils from '@libs/ReportUtils'; import updateMultilineInputRange from '@libs/UpdateMultilineInputRange'; import withReportAndPrivateNotesOrNotFound from '@pages/home/report/withReportAndPrivateNotesOrNotFound'; import personalDetailsPropType from '@pages/personalDetailsPropType'; @@ -105,9 +107,11 @@ function PrivateNotesEditPage({route, personalDetailsList, report}) { Keyboard.dismiss(); - // Take user back to the PrivateNotesView page - - Navigation.goBack(); + if (isEmpty(privateNote.trim())) { + ReportUtils.navigateToDetailsPage(report, true); + } else { + Navigation.goBack(ROUTES.PRIVATE_NOTES_LIST.getRoute(report.reportID)); + } }; return ( diff --git a/src/pages/PrivateNotes/PrivateNotesListPage.js b/src/pages/PrivateNotes/PrivateNotesListPage.js index 7a339dd5102b..b7f1032bfa9d 100644 --- a/src/pages/PrivateNotes/PrivateNotesListPage.js +++ b/src/pages/PrivateNotes/PrivateNotesListPage.js @@ -1,3 +1,4 @@ +import {useIsFocused} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useEffect, useMemo} from 'react'; @@ -106,13 +107,13 @@ function PrivateNotesListPage({report, personalDetailsList, session}) { })) .value(); }, [report, personalDetailsList, session, translate]); - + const isFocused = useIsFocused(); useEffect(() => { - if (_.some(privateNotes, (item) => item.note)) { + if (_.some(privateNotes, (item) => item.note) || !isFocused) { return; } Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, session.accountID)); - }, [privateNotes, report.reportID, session.accountID]); + }, [privateNotes, report.reportID, session.accountID, isFocused]); return ( Navigation.navigate(ROUTES.PRIVATE_NOTES_LIST.getRoute(props.report.reportID))} + onPress={() => { + const currentUserPrivateNote = lodashGet(props.report, ['privateNotes', props.session.accountID, 'note'], ''); + if (isEmpty(currentUserPrivateNote)) { + Report.getReportPrivateNote(props.report.reportID); + Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(props.report.reportID, props.session.accountID)); + return; + } + Navigation.navigate(ROUTES.PRIVATE_NOTES_LIST.getRoute(props.report.reportID)); + }} wrapperStyle={styles.breakAll} shouldShowRightIcon brickRoadIndicator={Report.hasErrorInPrivateNotes(props.report) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''} From dcb9c2f6cd0cc3e3afe201e78bef589f8abad5d2 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Sat, 18 Nov 2023 09:12:15 +0700 Subject: [PATCH 069/319] fix logic --- src/libs/ReportUtils.js | 15 +++++++++++++++ src/pages/PrivateNotes/PrivateNotesEditPage.js | 9 ++++----- src/pages/PrivateNotes/PrivateNotesListPage.js | 4 ++-- src/pages/ProfilePage.js | 8 +------- src/pages/ReportDetailsPage.js | 8 +------- 5 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 673cb09232de..c8da692d8682 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -2,6 +2,7 @@ import {format} from 'date-fns'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import Str from 'expensify-common/lib/str'; +import {isEmpty} from 'lodash'; import lodashGet from 'lodash/get'; import lodashIntersection from 'lodash/intersection'; import Onyx from 'react-native-onyx'; @@ -4274,6 +4275,19 @@ function shouldDisableWelcomeMessage(report, policy) { return isMoneyRequestReport(report) || isArchivedRoom(report) || !isChatRoom(report) || isChatThread(report) || !PolicyUtils.isPolicyAdmin(policy); } +/** + * @param {Object} report + * @param {Object} session + */ +function navigateToPrivateNotes(report, session) { + const currentUserPrivateNote = lodashGet(report, ['privateNotes', session.accountID, 'note'], ''); + if (isEmpty(currentUserPrivateNote)) { + Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, session.accountID)); + return; + } + Navigation.navigate(ROUTES.PRIVATE_NOTES_LIST.getRoute(report.reportID)); +} + export { getReportParticipantsTitle, isReportMessageAttachment, @@ -4437,4 +4451,5 @@ export { getChannelLogMemberMessage, getRoom, shouldDisableWelcomeMessage, + navigateToPrivateNotes, }; diff --git a/src/pages/PrivateNotes/PrivateNotesEditPage.js b/src/pages/PrivateNotes/PrivateNotesEditPage.js index bf0905c2d052..c89f51a3fa24 100644 --- a/src/pages/PrivateNotes/PrivateNotesEditPage.js +++ b/src/pages/PrivateNotes/PrivateNotesEditPage.js @@ -96,9 +96,9 @@ function PrivateNotesEditPage({route, personalDetailsList, report}) { const savePrivateNote = () => { const originalNote = lodashGet(report, ['privateNotes', route.params.accountID, 'note'], ''); - + let editedNote = ''; if (privateNote.trim() !== originalNote.trim()) { - const editedNote = Report.handleUserDeletedLinksInHtml(privateNote.trim(), parser.htmlToMarkdown(originalNote).trim()); + editedNote = Report.handleUserDeletedLinksInHtml(privateNote.trim(), parser.htmlToMarkdown(originalNote).trim()); Report.updatePrivateNotes(report.reportID, route.params.accountID, editedNote); } @@ -106,9 +106,8 @@ function PrivateNotesEditPage({route, personalDetailsList, report}) { debouncedSavePrivateNote(''); Keyboard.dismiss(); - - if (isEmpty(privateNote.trim())) { - ReportUtils.navigateToDetailsPage(report, true); + if (!_.some({...report.privateNotes, [route.params.accountID]: {note: editedNote}}, (item) => item.note)) { + ReportUtils.navigateToDetailsPage(report); } else { Navigation.goBack(ROUTES.PRIVATE_NOTES_LIST.getRoute(report.reportID)); } diff --git a/src/pages/PrivateNotes/PrivateNotesListPage.js b/src/pages/PrivateNotes/PrivateNotesListPage.js index b7f1032bfa9d..c8dc98d912dc 100644 --- a/src/pages/PrivateNotes/PrivateNotesListPage.js +++ b/src/pages/PrivateNotes/PrivateNotesListPage.js @@ -109,11 +109,11 @@ function PrivateNotesListPage({report, personalDetailsList, session}) { }, [report, personalDetailsList, session, translate]); const isFocused = useIsFocused(); useEffect(() => { - if (_.some(privateNotes, (item) => item.note) || !isFocused) { + if (_.some(report.privateNotes, (item) => item.note) || !isFocused) { return; } Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, session.accountID)); - }, [privateNotes, report.reportID, session.accountID, isFocused]); + }, [report.privateNotes, report.reportID, session.accountID, isFocused]); return ( { - const currentUserPrivateNote = lodashGet(props.report, ['privateNotes', props.session.accountID, 'note'], ''); - if (isEmpty(currentUserPrivateNote)) { - Report.getReportPrivateNote(props.report.reportID); - Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(props.report.reportID, props.session.accountID)); - return; - } - Navigation.navigate(ROUTES.PRIVATE_NOTES_LIST.getRoute(props.report.reportID)); + ReportUtils.navigateToPrivateNotes(props.report, props.session); }} wrapperStyle={styles.breakAll} shouldShowRightIcon diff --git a/src/pages/ReportDetailsPage.js b/src/pages/ReportDetailsPage.js index 7770e62bd30c..cae300469f70 100644 --- a/src/pages/ReportDetailsPage.js +++ b/src/pages/ReportDetailsPage.js @@ -142,13 +142,7 @@ function ReportDetailsPage(props) { icon: Expensicons.Pencil, isAnonymousAction: false, action: () => { - const currentUserPrivateNote = lodashGet(props.report, ['privateNotes', props.session.accountID, 'note'], ''); - if (isEmpty(currentUserPrivateNote)) { - Report.getReportPrivateNote(props.report.reportID); - Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(props.report.reportID, props.session.accountID)); - return; - } - Navigation.navigate(ROUTES.PRIVATE_NOTES_LIST.getRoute(props.report.reportID)); + ReportUtils.navigateToPrivateNotes(props.report, props.session); }, brickRoadIndicator: Report.hasErrorInPrivateNotes(props.report) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : '', }); From 393f62f06abb6067829d22a6b6795bf42a82d4e7 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Sat, 18 Nov 2023 09:18:22 +0700 Subject: [PATCH 070/319] fix merge main conflict --- src/pages/ReportDetailsPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/ReportDetailsPage.js b/src/pages/ReportDetailsPage.js index c39960d019e1..904137ecfbe0 100644 --- a/src/pages/ReportDetailsPage.js +++ b/src/pages/ReportDetailsPage.js @@ -148,7 +148,7 @@ function ReportDetailsPage(props) { } return items; - }, [isArchivedRoom, participants.length, isThread, isMoneyRequestReport, props.report, isGroupDMChat, isPolicyMember, isUserCreatedPolicyRoom]); + }, [isArchivedRoom, participants.length, isThread, isMoneyRequestReport, props.report, isGroupDMChat, isPolicyMember, isUserCreatedPolicyRoom, props.session]); const displayNamesWithTooltips = useMemo(() => { const hasMultipleParticipants = participants.length > 1; From 5b069ef1bec43d8c7b5f1a69320c3acf06270941 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Sat, 18 Nov 2023 09:22:15 +0700 Subject: [PATCH 071/319] fix lint --- src/pages/PrivateNotes/PrivateNotesEditPage.js | 1 - src/pages/ProfilePage.js | 1 - src/pages/ReportDetailsPage.js | 2 -- 3 files changed, 4 deletions(-) diff --git a/src/pages/PrivateNotes/PrivateNotesEditPage.js b/src/pages/PrivateNotes/PrivateNotesEditPage.js index c89f51a3fa24..2c3064d199ef 100644 --- a/src/pages/PrivateNotes/PrivateNotesEditPage.js +++ b/src/pages/PrivateNotes/PrivateNotesEditPage.js @@ -1,7 +1,6 @@ import {useFocusEffect} from '@react-navigation/native'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import Str from 'expensify-common/lib/str'; -import {isEmpty} from 'lodash'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useMemo, useRef, useState} from 'react'; diff --git a/src/pages/ProfilePage.js b/src/pages/ProfilePage.js index 6b6494dd72db..ab86789a99cc 100755 --- a/src/pages/ProfilePage.js +++ b/src/pages/ProfilePage.js @@ -1,6 +1,5 @@ import {parsePhoneNumber} from 'awesome-phonenumber'; import Str from 'expensify-common/lib/str'; -import {isEmpty} from 'lodash'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useEffect} from 'react'; diff --git a/src/pages/ReportDetailsPage.js b/src/pages/ReportDetailsPage.js index 904137ecfbe0..5095074e281e 100644 --- a/src/pages/ReportDetailsPage.js +++ b/src/pages/ReportDetailsPage.js @@ -1,5 +1,3 @@ -import {isEmpty} from 'lodash'; -import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useMemo} from 'react'; import {ScrollView, View} from 'react-native'; From 5504cc0767b3489ea7dfb5b244a0562925bf649d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Sat, 18 Nov 2023 15:34:33 +0100 Subject: [PATCH 072/319] split the Hoverable into Active and Disabled parts --- src/components/Hoverable/ActiveHoverable.tsx | 70 ++++++ src/components/Hoverable/index.tsx | 216 ++----------------- 2 files changed, 82 insertions(+), 204 deletions(-) create mode 100644 src/components/Hoverable/ActiveHoverable.tsx diff --git a/src/components/Hoverable/ActiveHoverable.tsx b/src/components/Hoverable/ActiveHoverable.tsx new file mode 100644 index 000000000000..b77b2a60a2fb --- /dev/null +++ b/src/components/Hoverable/ActiveHoverable.tsx @@ -0,0 +1,70 @@ +import {cloneElement, forwardRef, Ref, useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import {DeviceEventEmitter} from 'react-native'; +import CONST from '@src/CONST'; +import HoverableProps from './types'; + +type ActiveHoverableProps = Omit; + +// TODO: Do we really need distinction between onHover* and onMouse*? + +function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, children, ...props}: ActiveHoverableProps, ref: Ref) { + const [isHovered, setIsHovered] = useState(false); + const isScrolling = useRef(false); + const isHoveredRef = useRef(false); + + const updateIsHovered = useCallback( + (hovered: boolean) => { + isHoveredRef.current = hovered; + if (shouldHandleScroll && isScrolling.current) { + return; + } + setIsHovered(hovered); + }, + [shouldHandleScroll], + ); + + useEffect(() => (isHovered ? onHoverIn?.() : onHoverOut?.()), [isHovered, onHoverIn, onHoverOut]); + + useEffect(() => { + if (!shouldHandleScroll) { + return; + } + + const scrollingListener = DeviceEventEmitter.addListener(CONST.EVENTS.SCROLLING, (scrolling) => { + isScrolling.current = scrolling; + if (!isScrolling.current) { + setIsHovered(isHoveredRef.current); + } + }); + + return () => scrollingListener.remove(); + }, [shouldHandleScroll]); + + const child = useMemo(() => (typeof children === 'function' ? children(!isScrolling.current && isHovered) : children), [children, isHovered]); + + const onMouseEnter = useCallback( + (e: MouseEvent) => { + updateIsHovered(true); + props.onMouseEnter?.(e); + child.props.onMouseEnter?.(e); + }, + [updateIsHovered, props, child.props], + ); + + const onMouseLeave = useCallback( + (e: MouseEvent) => { + updateIsHovered(false); + props.onMouseLeave?.(e); + child.props.onMouseLeave?.(e); + }, + [updateIsHovered, props, child.props], + ); + + return cloneElement(child, { + ref, + onMouseEnter, + onMouseLeave, + }); +} + +export default forwardRef(ActiveHoverable); diff --git a/src/components/Hoverable/index.tsx b/src/components/Hoverable/index.tsx index 9c641cfc19be..e99522019c3c 100644 --- a/src/components/Hoverable/index.tsx +++ b/src/components/Hoverable/index.tsx @@ -1,212 +1,20 @@ -import React, {ForwardedRef, forwardRef, MutableRefObject, ReactElement, RefAttributes, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; -import {DeviceEventEmitter} from 'react-native'; -import * as DeviceCapabilities from '@libs/DeviceCapabilities'; -import CONST from '@src/CONST'; +import React, {cloneElement, forwardRef, Ref} from 'react'; +import ActiveHoverable from './ActiveHoverable'; import HoverableProps from './types'; -/** - * Maps the children of a Hoverable component to - * - a function that is called with the parameter - * - the child itself if it is the only child - * @param children The children to map. - * @param callbackParam The parameter to pass to the children function. - * @returns The mapped children. - */ -function mapChildren(children: ((isHovered: boolean) => ReactElement) | ReactElement | ReactElement[], callbackParam: boolean): ReactElement & RefAttributes { - if (Array.isArray(children)) { - return children[0]; +function Hoverable({disabled, ...props}: HoverableProps, ref: Ref) { + // If Hoverable is disabled, just render the child without additional logic or event listeners. + if (disabled) { + return cloneElement(typeof props.children === 'function' ? props.children(false) : props.children, {ref}); } - if (typeof children === 'function') { - return children(callbackParam); - } - - return children; -} - -/** - * Assigns a ref to an element, either by setting the current property of the ref object or by calling the ref function - * @param ref The ref object or function. - * @param element The element to assign the ref to. - */ -function assignRef(ref: ((instance: HTMLElement | null) => void) | MutableRefObject, element: HTMLElement) { - if (!ref) { - return; - } - if (typeof ref === 'function') { - ref(element); - } else if ('current' in ref) { - // eslint-disable-next-line no-param-reassign - ref.current = element; - } -} - -/** - * It is necessary to create a Hoverable component instead of relying solely on Pressable support for hover state, - * because nesting Pressables causes issues where the hovered state of the child cannot be easily propagated to the - * parent. https://github.com/necolas/react-native-web/issues/1875 - */ -function Hoverable( - {disabled = false, onHoverIn = () => {}, onHoverOut = () => {}, onMouseEnter = () => {}, onMouseLeave = () => {}, children, shouldHandleScroll = false}: HoverableProps, - outerRef: ForwardedRef, -) { - const [isHovered, setIsHovered] = useState(false); - - const isScrolling = useRef(false); - const isHoveredRef = useRef(false); - const ref = useRef(null); - - const updateIsHoveredOnScrolling = useCallback( - (hovered: boolean) => { - if (disabled) { - return; - } - - isHoveredRef.current = hovered; - - if (shouldHandleScroll && isScrolling.current) { - return; - } - setIsHovered(hovered); - }, - [disabled, shouldHandleScroll], + return ( + ); - - useEffect(() => { - const unsetHoveredWhenDocumentIsHidden = () => document.visibilityState === 'hidden' && setIsHovered(false); - - document.addEventListener('visibilitychange', unsetHoveredWhenDocumentIsHidden); - - return () => document.removeEventListener('visibilitychange', unsetHoveredWhenDocumentIsHidden); - }, []); - - useEffect(() => { - if (!shouldHandleScroll) { - return; - } - - const scrollingListener = DeviceEventEmitter.addListener(CONST.EVENTS.SCROLLING, (scrolling) => { - isScrolling.current = scrolling; - if (!scrolling) { - setIsHovered(isHoveredRef.current); - } - }); - - return () => scrollingListener.remove(); - }, [shouldHandleScroll]); - - useEffect(() => { - if (!DeviceCapabilities.hasHoverSupport()) { - return; - } - - /** - * Checks the hover state of a component and updates it based on the event target. - * This is necessary to handle cases where the hover state might get stuck due to an unreliable mouseleave trigger, - * such as when an element is removed before the mouseleave event is triggered. - * @param event The hover event object. - */ - const unsetHoveredIfOutside = (event: MouseEvent) => { - if (!ref.current || !isHovered) { - return; - } - - if (ref.current.contains(event.target as Node)) { - return; - } - - setIsHovered(false); - }; - - document.addEventListener('mouseover', unsetHoveredIfOutside); - - return () => document.removeEventListener('mouseover', unsetHoveredIfOutside); - }, [isHovered]); - - useEffect(() => { - if (!disabled || !isHovered) { - return; - } - setIsHovered(false); - }, [disabled, isHovered]); - - useEffect(() => { - if (disabled) { - return; - } - if (onHoverIn && isHovered) { - return onHoverIn(); - } - if (onHoverOut && !isHovered) { - return onHoverOut(); - } - }, [disabled, isHovered, onHoverIn, onHoverOut]); - - // Expose inner ref to parent through outerRef. This enable us to use ref both in parent and child. - useImperativeHandle(outerRef, () => ref.current, []); - - const child = useMemo(() => React.Children.only(mapChildren(children, isHovered)), [children, isHovered]); - - const enableHoveredOnMouseEnter = useCallback( - (event: MouseEvent) => { - updateIsHoveredOnScrolling(true); - onMouseEnter(event); - - if (typeof child.props.onMouseEnter === 'function') { - child.props.onMouseEnter(event); - } - }, - [child.props, onMouseEnter, updateIsHoveredOnScrolling], - ); - - const disableHoveredOnMouseLeave = useCallback( - (event: MouseEvent) => { - updateIsHoveredOnScrolling(false); - onMouseLeave(event); - - if (typeof child.props.onMouseLeave === 'function') { - child.props.onMouseLeave(event); - } - }, - [child.props, onMouseLeave, updateIsHoveredOnScrolling], - ); - - const disableHoveredOnBlur = useCallback( - (event: MouseEvent) => { - // Check if the blur event occurred due to clicking outside the element - // and the wrapperView contains the element that caused the blur and reset isHovered - if (!ref.current?.contains(event.target as Node) && !ref.current?.contains(event.relatedTarget as Node)) { - setIsHovered(false); - } - - if (typeof child.props.onBlur === 'function') { - child.props.onBlur(event); - } - }, - [child.props], - ); - - // We need to access the ref of a children from both parent and current component - // So we pass it to current ref and assign it once again to the child ref prop - const hijackRef = (el: HTMLElement) => { - ref.current = el; - if (child.ref) { - assignRef(child.ref, el); - } - }; - - if (!DeviceCapabilities.hasHoverSupport()) { - return React.cloneElement(child, { - ref: hijackRef, - }); - } - - return React.cloneElement(child, { - ref: hijackRef, - onMouseEnter: enableHoveredOnMouseEnter, - onMouseLeave: disableHoveredOnMouseLeave, - onBlur: disableHoveredOnBlur, - }); } export default forwardRef(Hoverable); From 895605ea41cd58d98a895590f797056ea4bcc1e2 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Mon, 20 Nov 2023 10:11:56 +0100 Subject: [PATCH 073/319] Replace HOCs with hooks usage --- src/components/OfflineIndicator.tsx | 37 +++++++++++++---------------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/src/components/OfflineIndicator.tsx b/src/components/OfflineIndicator.tsx index b333fb5bb1bb..ba0cf1860915 100644 --- a/src/components/OfflineIndicator.tsx +++ b/src/components/OfflineIndicator.tsx @@ -1,29 +1,21 @@ import React from 'react'; import {StyleProp, View, ViewStyle} from 'react-native'; -import {OnyxEntry} from 'react-native-onyx/lib/types'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import styles from '@styles/styles'; import variables from '@styles/variables'; -import type {Network} from '@src/types/onyx'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; -import type {LocaleContextProps} from './LocaleContextProvider'; -import {withNetwork} from './OnyxProvider'; import Text from './Text'; -import withLocalize from './withLocalize'; -import withWindowDimensions from './withWindowDimensions'; -import type {WindowDimensionsProps} from './withWindowDimensions/types'; -type OfflineIndicatorProps = LocaleContextProps & - WindowDimensionsProps & { - /** Information about the network */ - network: OnyxEntry; +type OfflineIndicatorProps = { + /** Optional styles for container element that will override the default styling for the offline indicator */ + containerStyles?: StyleProp; - /** Optional styles for container element that will override the default styling for the offline indicator */ - containerStyles?: StyleProp; - - /** Optional styles for the container */ - style?: StyleProp; - }; + /** Optional styles for the container */ + style?: StyleProp; +}; const setStyles = (containerStyles: StyleProp, isSmallScreenWidth: boolean): StyleProp => { if (containerStyles) { @@ -33,8 +25,12 @@ const setStyles = (containerStyles: StyleProp, isSmallScreenWidth: bo return isSmallScreenWidth ? styles.offlineIndicatorMobile : styles.offlineIndicator; }; -function OfflineIndicator({network, isSmallScreenWidth, translate, style, containerStyles}: OfflineIndicatorProps) { - if (!network?.isOffline) { +function OfflineIndicator({style, containerStyles}: OfflineIndicatorProps) { + const {translate} = useLocalize(); + const {isOffline} = useNetwork(); + const {isSmallScreenWidth} = useWindowDimensions(); + + if (!isOffline) { return null; } @@ -52,5 +48,4 @@ function OfflineIndicator({network, isSmallScreenWidth, translate, style, contai OfflineIndicator.displayName = 'OfflineIndicator'; -// TODO: use `compose` function for HOCs composing once TypeScript issues are resolved. -export default withNetwork()(withLocalize(withWindowDimensions(OfflineIndicator))); +export default OfflineIndicator; From cb8a310f0a5ca422302bc8eae672373ff2f94388 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Mon, 20 Nov 2023 16:14:44 +0100 Subject: [PATCH 074/319] unset Hovered when document visibility has changed --- src/components/Hoverable/ActiveHoverable.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/Hoverable/ActiveHoverable.tsx b/src/components/Hoverable/ActiveHoverable.tsx index b77b2a60a2fb..7e592a666905 100644 --- a/src/components/Hoverable/ActiveHoverable.tsx +++ b/src/components/Hoverable/ActiveHoverable.tsx @@ -5,8 +5,6 @@ import HoverableProps from './types'; type ActiveHoverableProps = Omit; -// TODO: Do we really need distinction between onHover* and onMouse*? - function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, children, ...props}: ActiveHoverableProps, ref: Ref) { const [isHovered, setIsHovered] = useState(false); const isScrolling = useRef(false); @@ -40,6 +38,14 @@ function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, children return () => scrollingListener.remove(); }, [shouldHandleScroll]); + useEffect(() => { + const unsetHoveredWhenDocumentIsHidden = () => document.visibilityState === 'hidden' && setIsHovered(false); + + document.addEventListener('visibilitychange', unsetHoveredWhenDocumentIsHidden); + + return () => document.removeEventListener('visibilitychange', unsetHoveredWhenDocumentIsHidden); + }, []); + const child = useMemo(() => (typeof children === 'function' ? children(!isScrolling.current && isHovered) : children), [children, isHovered]); const onMouseEnter = useCallback( From 86699394ac42de84ec7ed7447108512d469d1505 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Mon, 20 Nov 2023 16:19:43 +0100 Subject: [PATCH 075/319] Add missing clarifying comment --- src/components/Hoverable/index.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/Hoverable/index.tsx b/src/components/Hoverable/index.tsx index e99522019c3c..456f19e1e524 100644 --- a/src/components/Hoverable/index.tsx +++ b/src/components/Hoverable/index.tsx @@ -2,6 +2,11 @@ import React, {cloneElement, forwardRef, Ref} from 'react'; import ActiveHoverable from './ActiveHoverable'; import HoverableProps from './types'; +/** + * It is necessary to create a Hoverable component instead of relying solely on Pressable support for hover state, + * because nesting Pressables causes issues where the hovered state of the child cannot be easily propagated to the + * parent. https://github.com/necolas/react-native-web/issues/1875 + */ function Hoverable({disabled, ...props}: HoverableProps, ref: Ref) { // If Hoverable is disabled, just render the child without additional logic or event listeners. if (disabled) { From e4ccfa9e83a585b9f018981ad4de02cd387cef77 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Tue, 21 Nov 2023 11:55:57 +0700 Subject: [PATCH 076/319] fix open private notes using deeplink --- src/pages/PrivateNotes/PrivateNotesListPage.js | 18 ++++++++++++++---- src/pages/ProfilePage.js | 4 +--- src/pages/ReportDetailsPage.js | 4 +--- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/pages/PrivateNotes/PrivateNotesListPage.js b/src/pages/PrivateNotes/PrivateNotesListPage.js index c8dc98d912dc..7757dda487bf 100644 --- a/src/pages/PrivateNotes/PrivateNotesListPage.js +++ b/src/pages/PrivateNotes/PrivateNotesListPage.js @@ -108,11 +108,21 @@ function PrivateNotesListPage({report, personalDetailsList, session}) { .value(); }, [report, personalDetailsList, session, translate]); const isFocused = useIsFocused(); + + const navigateToEditPageTimeoutRef = React.useRef(null); useEffect(() => { - if (_.some(report.privateNotes, (item) => item.note) || !isFocused) { - return; - } - Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, session.accountID)); + navigateToEditPageTimeoutRef.current = setTimeout(() => { + if (_.some(report.privateNotes, (item) => item.note) || !isFocused) { + return; + } + Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, session.accountID)); + }, CONST.ANIMATED_TRANSITION); + return () => { + if (!navigateToEditPageTimeoutRef.current) { + return; + } + clearTimeout(navigateToEditPageTimeoutRef.current); + }; }, [report.privateNotes, report.reportID, session.accountID, isFocused]); return ( { - ReportUtils.navigateToPrivateNotes(props.report, props.session); - }} + onPress={() => ReportUtils.navigateToPrivateNotes(props.report, props.session)} wrapperStyle={styles.breakAll} shouldShowRightIcon brickRoadIndicator={Report.hasErrorInPrivateNotes(props.report) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''} diff --git a/src/pages/ReportDetailsPage.js b/src/pages/ReportDetailsPage.js index 54a01cd25b28..da3d2a6beb05 100644 --- a/src/pages/ReportDetailsPage.js +++ b/src/pages/ReportDetailsPage.js @@ -138,9 +138,7 @@ function ReportDetailsPage(props) { translationKey: 'privateNotes.title', icon: Expensicons.Pencil, isAnonymousAction: false, - action: () => { - ReportUtils.navigateToPrivateNotes(props.report, props.session); - }, + action: () => ReportUtils.navigateToPrivateNotes(props.report, props.session), brickRoadIndicator: Report.hasErrorInPrivateNotes(props.report) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : '', }); } From 4ede2250f8e4defa0a546cb9af4eb5af215fff2b Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Tue, 21 Nov 2023 15:36:49 +0700 Subject: [PATCH 077/319] disable auto correct for title task input --- src/pages/tasks/NewTaskDetailsPage.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/tasks/NewTaskDetailsPage.js b/src/pages/tasks/NewTaskDetailsPage.js index 4d0040e01107..d35439ad4d40 100644 --- a/src/pages/tasks/NewTaskDetailsPage.js +++ b/src/pages/tasks/NewTaskDetailsPage.js @@ -109,6 +109,7 @@ function NewTaskDetailsPage(props) { accessibilityLabel={props.translate('task.title')} value={taskTitle} onValueChange={(value) => setTaskTitle(value)} + autoCorrect={false} /> From 87d8979687fd19a16eb685a342231ca60a006728 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Tue, 21 Nov 2023 13:36:40 +0100 Subject: [PATCH 078/319] remove onMouse* props from Hoverable --- src/components/Hoverable/ActiveHoverable.tsx | 6 ++---- src/components/Hoverable/types.ts | 6 ------ src/components/Tooltip/BaseTooltip.js | 14 +++++++++++++- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/components/Hoverable/ActiveHoverable.tsx b/src/components/Hoverable/ActiveHoverable.tsx index 7e592a666905..cb504412953b 100644 --- a/src/components/Hoverable/ActiveHoverable.tsx +++ b/src/components/Hoverable/ActiveHoverable.tsx @@ -51,19 +51,17 @@ function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, children const onMouseEnter = useCallback( (e: MouseEvent) => { updateIsHovered(true); - props.onMouseEnter?.(e); child.props.onMouseEnter?.(e); }, - [updateIsHovered, props, child.props], + [updateIsHovered, child.props], ); const onMouseLeave = useCallback( (e: MouseEvent) => { updateIsHovered(false); - props.onMouseLeave?.(e); child.props.onMouseLeave?.(e); }, - [updateIsHovered, props, child.props], + [updateIsHovered, child.props], ); return cloneElement(child, { diff --git a/src/components/Hoverable/types.ts b/src/components/Hoverable/types.ts index 430b865f50c5..c1c055afe9c7 100644 --- a/src/components/Hoverable/types.ts +++ b/src/components/Hoverable/types.ts @@ -13,12 +13,6 @@ type HoverableProps = { /** Function that executes when the mouse leaves the children. */ onHoverOut?: () => void; - /** Direct pass-through of React's onMouseEnter event. */ - onMouseEnter?: (event: MouseEvent) => void; - - /** Direct pass-through of React's onMouseLeave event. */ - onMouseLeave?: (event: MouseEvent) => void; - /** Decides whether to handle the scroll behaviour to show hover once the scroll ends */ shouldHandleScroll?: boolean; }; diff --git a/src/components/Tooltip/BaseTooltip.js b/src/components/Tooltip/BaseTooltip.js index 3eb905e7a3e5..1aa5fa81e0a4 100644 --- a/src/components/Tooltip/BaseTooltip.js +++ b/src/components/Tooltip/BaseTooltip.js @@ -167,6 +167,16 @@ function Tooltip({children, numberOfLines, maxWidth, text, renderTooltipContent, setIsVisible(false); }, []); + const updateTargetPositionOnMouseEnter = useCallback( + (e) => { + updateTargetAndMousePosition(e); + if (children.props.onMouseEnter) { + children.props.onMouseEnter(e); + } + }, + [children.props, updateTargetAndMousePosition], + ); + // Skip the tooltip and return the children if the text is empty, // we don't have a render function or the device does not support hovering if ((_.isEmpty(text) && renderTooltipContent == null) || !hasHoverSupport) { @@ -205,7 +215,9 @@ function Tooltip({children, numberOfLines, maxWidth, text, renderTooltipContent, onHoverOut={hideTooltip} shouldHandleScroll={shouldHandleScroll} > - {children} + {React.cloneElement(children, { + onMouseEnter: updateTargetPositionOnMouseEnter, + })} From a7e49733c3786d06fbd85f7600c71d069958bb2c Mon Sep 17 00:00:00 2001 From: VH Date: Tue, 21 Nov 2023 20:00:59 +0700 Subject: [PATCH 079/319] Do not call API EditTaskAssignee if assignee is not changed --- src/libs/actions/Task.js | 12 +++++------- src/pages/tasks/TaskAssigneeSelectorModal.js | 9 ++++++--- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/libs/actions/Task.js b/src/libs/actions/Task.js index 225737ccbf6b..c7426bb470c4 100644 --- a/src/libs/actions/Task.js +++ b/src/libs/actions/Task.js @@ -435,7 +435,7 @@ function editTask(report, {title, description}) { ); } -function editTaskAssigneeAndNavigate(report, ownerAccountID, assigneeEmail, assigneeAccountID = 0, assigneeChatReport = null) { +function editTaskAssignee(report, ownerAccountID, assigneeEmail, assigneeAccountID = 0, assigneeChatReport = null) { // Create the EditedReportAction on the task const editTaskReportAction = ReportUtils.buildOptimisticEditedTaskReportAction(currentUserEmail); const reportName = report.reportName.trim(); @@ -520,8 +520,6 @@ function editTaskAssigneeAndNavigate(report, ownerAccountID, assigneeEmail, assi }, {optimisticData, successData, failureData}, ); - - Navigation.dismissModal(report.reportID); } /** @@ -623,9 +621,9 @@ function setAssigneeValue(assigneeEmail, assigneeAccountID, shareDestination, is // This is only needed for creation of a new task and so it should only be stored locally Onyx.merge(ONYXKEYS.TASK, {assignee: assigneeEmail, assigneeAccountID}); - // When we're editing the assignee, we immediately call editTaskAssigneeAndNavigate. Since setting the assignee is async, - // the chatReport is not yet set when editTaskAssigneeAndNavigate is called. So we return the chatReport here so that - // editTaskAssigneeAndNavigate can use it. + // When we're editing the assignee, we immediately call editTaskAssignee. Since setting the assignee is async, + // the chatReport is not yet set when editTaskAssignee is called. So we return the chatReport here so that + // editTaskAssignee can use it. return chatReport; } @@ -937,7 +935,7 @@ function getTaskReportActionMessage(actionName, reportID, isCreateTaskAction) { export { createTaskAndNavigate, editTask, - editTaskAssigneeAndNavigate, + editTaskAssignee, setTitleValue, setDescriptionValue, setTaskReport, diff --git a/src/pages/tasks/TaskAssigneeSelectorModal.js b/src/pages/tasks/TaskAssigneeSelectorModal.js index a817b4f94e9b..6ad08e36be7c 100644 --- a/src/pages/tasks/TaskAssigneeSelectorModal.js +++ b/src/pages/tasks/TaskAssigneeSelectorModal.js @@ -196,10 +196,13 @@ function TaskAssigneeSelectorModal(props) { // Check to see if we're editing a task and if so, update the assignee if (report) { - const assigneeChatReport = Task.setAssigneeValue(option.login, option.accountID, props.route.params.reportID, OptionsListUtils.isCurrentUser(option)); + if (option.accountID !== report.managerID) { + const assigneeChatReport = Task.setAssigneeValue(option.login, option.accountID, props.route.params.reportID, OptionsListUtils.isCurrentUser(option)); - // Pass through the selected assignee - Task.editTaskAssigneeAndNavigate(report, props.session.accountID, option.login, option.accountID, assigneeChatReport); + // Pass through the selected assignee + Task.editTaskAssignee(report, props.session.accountID, option.login, option.accountID, assigneeChatReport); + } + return Navigation.dismissModal(report.reportID); } }; From 4204fc21bf7d70cbe5cb5254bbd487f8a3e11de7 Mon Sep 17 00:00:00 2001 From: VH Date: Tue, 21 Nov 2023 20:08:34 +0700 Subject: [PATCH 080/319] Fix prettier --- src/pages/tasks/TaskAssigneeSelectorModal.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/tasks/TaskAssigneeSelectorModal.js b/src/pages/tasks/TaskAssigneeSelectorModal.js index 6ad08e36be7c..0dd2b65480a0 100644 --- a/src/pages/tasks/TaskAssigneeSelectorModal.js +++ b/src/pages/tasks/TaskAssigneeSelectorModal.js @@ -200,7 +200,7 @@ function TaskAssigneeSelectorModal(props) { const assigneeChatReport = Task.setAssigneeValue(option.login, option.accountID, props.route.params.reportID, OptionsListUtils.isCurrentUser(option)); // Pass through the selected assignee - Task.editTaskAssignee(report, props.session.accountID, option.login, option.accountID, assigneeChatReport); + Task.editTaskAssignee(report, props.session.accountID, option.login, option.accountID, assigneeChatReport); } return Navigation.dismissModal(report.reportID); } From a78b9f8c2d0c94455e68bd5d2b8487a7c41c32c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Tue, 21 Nov 2023 14:09:31 +0100 Subject: [PATCH 081/319] remove unused props --- src/components/Hoverable/ActiveHoverable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Hoverable/ActiveHoverable.tsx b/src/components/Hoverable/ActiveHoverable.tsx index cb504412953b..c03aed5b3d11 100644 --- a/src/components/Hoverable/ActiveHoverable.tsx +++ b/src/components/Hoverable/ActiveHoverable.tsx @@ -5,7 +5,7 @@ import HoverableProps from './types'; type ActiveHoverableProps = Omit; -function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, children, ...props}: ActiveHoverableProps, ref: Ref) { +function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, children}: ActiveHoverableProps, ref: Ref) { const [isHovered, setIsHovered] = useState(false); const isScrolling = useRef(false); const isHoveredRef = useRef(false); From 34ecf9874d1d22cedb716c0f60936e56462a51aa Mon Sep 17 00:00:00 2001 From: dukenv0307 <129500732+dukenv0307@users.noreply.github.com> Date: Tue, 21 Nov 2023 23:44:32 +0700 Subject: [PATCH 082/319] Update src/libs/actions/Policy.js Co-authored-by: Eugene Voloshchak --- src/libs/actions/Policy.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index b11df545fc64..de6afe5a3dd1 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -1045,7 +1045,6 @@ function generateCustomUnitID() { } /** - * * @returns {Object} */ function buildOptimisticCustomUnits() { From 46c7ce22ba4568d9b07d6054d56d2f67decd1a2e Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 21 Nov 2023 17:50:30 +0100 Subject: [PATCH 083/319] create buildOptimisticPolicyRecentlyUsedTags --- src/libs/actions/Policy.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index 4df510d44db7..e374b6727551 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -88,6 +88,13 @@ Onyx.connect({ callback: (val) => (allRecentlyUsedCategories = val), }); +let allRecentlyUsedTags = {}; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS, + waitForCollectionCallback: true, + callback: (val) => (allRecentlyUsedTags = val), +}); + let networkStatus = {}; Onyx.connect({ key: ONYXKEYS.NETWORK, @@ -1458,6 +1465,21 @@ function buildOptimisticPolicyRecentlyUsedCategories(policyID, category) { return lodashUnion([category], policyRecentlyUsedCategories); } +/** + * @param {String} policyID + * @param {String} tag + * @returns {Object} + */ +function buildOptimisticPolicyRecentlyUsedTags(policyID, tag) { + if (!policyID || !tag) { + return []; + } + + const policyRecentlyUsedTags = lodashGet(allRecentlyUsedTags, `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS}${policyID}`, []); + + return lodashUnion([tag], policyRecentlyUsedTags); +} + export { removeMembers, addMembersToWorkspace, @@ -1489,5 +1511,6 @@ export { dismissAddedWithPrimaryLoginMessages, openDraftWorkspaceRequest, buildOptimisticPolicyRecentlyUsedCategories, + buildOptimisticPolicyRecentlyUsedTags, createDraftInitialWorkspace, }; From a8350b9fd7aaeeea002bc595b5e9f19f59d2b6b1 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 21 Nov 2023 17:50:49 +0100 Subject: [PATCH 084/319] prepare create split bill methods --- src/libs/actions/IOU.js | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 6347f45549c7..cc98a05127b7 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -909,11 +909,12 @@ function requestMoney( * @param {String} comment * @param {String} currency * @param {String} category + * @param {String} tag * @param {String} existingSplitChatReportID - the report ID where the split bill happens, could be a group chat or a workspace chat * * @return {Object} */ -function createSplitsAndOnyxData(participants, currentUserLogin, currentUserAccountID, amount, comment, currency, category, existingSplitChatReportID = '') { +function createSplitsAndOnyxData(participants, currentUserLogin, currentUserAccountID, amount, comment, currency, category, tag, existingSplitChatReportID = '') { const currentUserEmailForIOUSplit = OptionsListUtils.addSMSDomainIfPhoneNumber(currentUserLogin); const participantAccountIDs = _.map(participants, (participant) => Number(participant.accountID)); const existingSplitChatReport = @@ -941,6 +942,7 @@ function createSplitsAndOnyxData(participants, currentUserLogin, currentUserAcco undefined, undefined, category, + tag, ); // Note: The created action must be optimistically generated before the IOU action so there's no chance that the created action appears after the IOU action in the chat @@ -1131,6 +1133,7 @@ function createSplitsAndOnyxData(participants, currentUserLogin, currentUserAcco undefined, undefined, category, + tag, ); // STEP 4: Build optimistic reportActions. We need: @@ -1184,6 +1187,13 @@ function createSplitsAndOnyxData(participants, currentUserLogin, currentUserAcco optimisticPolicyRecentlyUsedCategories = Policy.buildOptimisticPolicyRecentlyUsedCategories(participant.policyID, category); } + // Add tag to optimistic policy recently used tags when a participant is a workspace + let optimisticPolicyRecentlyUsedTags = []; + if (isPolicyExpenseChat) { + // TODO: Implement + optimisticPolicyRecentlyUsedTags = Policy.buildOptimisticPolicyRecentlyUsedTags(participant.policyID, category); + } + // STEP 5: Build Onyx Data const [oneOnOneOptimisticData, oneOnOneSuccessData, oneOnOneFailureData] = buildOnyxDataForMoneyRequest( oneOnOneChatReport, @@ -1195,7 +1205,7 @@ function createSplitsAndOnyxData(participants, currentUserLogin, currentUserAcco oneOnOnePersonalDetailListAction, oneOnOneReportPreviewAction, optimisticPolicyRecentlyUsedCategories, - {}, + optimisticPolicyRecentlyUsedTags, isNewOneOnOneChatReport, shouldCreateNewOneOnOneIOUReport, ); @@ -1245,10 +1255,11 @@ function createSplitsAndOnyxData(participants, currentUserLogin, currentUserAcco * @param {String} comment * @param {String} currency * @param {String} category + * @param {String} tag * @param {String} existingSplitChatReportID - Either a group DM or a workspace chat */ -function splitBill(participants, currentUserLogin, currentUserAccountID, amount, comment, currency, category, existingSplitChatReportID = '') { - const {splitData, splits, onyxData} = createSplitsAndOnyxData(participants, currentUserLogin, currentUserAccountID, amount, comment, currency, category, existingSplitChatReportID); +function splitBill(participants, currentUserLogin, currentUserAccountID, amount, comment, currency, category, tag, existingSplitChatReportID = '') { + const {splitData, splits, onyxData} = createSplitsAndOnyxData(participants, currentUserLogin, currentUserAccountID, amount, comment, currency, category, tag, existingSplitChatReportID); API.write( 'SplitBill', { @@ -1258,6 +1269,7 @@ function splitBill(participants, currentUserLogin, currentUserAccountID, amount, currency, comment, category, + tag, transactionID: splitData.transactionID, reportActionID: splitData.reportActionID, createdReportActionID: splitData.createdReportActionID, @@ -1279,9 +1291,10 @@ function splitBill(participants, currentUserLogin, currentUserAccountID, amount, * @param {String} comment * @param {String} currency * @param {String} category + * @param {String} tag */ -function splitBillAndOpenReport(participants, currentUserLogin, currentUserAccountID, amount, comment, currency, category) { - const {splitData, splits, onyxData} = createSplitsAndOnyxData(participants, currentUserLogin, currentUserAccountID, amount, comment, currency, category); +function splitBillAndOpenReport(participants, currentUserLogin, currentUserAccountID, amount, comment, currency, category, tag) { + const {splitData, splits, onyxData} = createSplitsAndOnyxData(participants, currentUserLogin, currentUserAccountID, amount, comment, currency, category, tag); API.write( 'SplitBillAndOpenReport', @@ -1292,6 +1305,7 @@ function splitBillAndOpenReport(participants, currentUserLogin, currentUserAccou currency, comment, category, + tag, transactionID: splitData.transactionID, reportActionID: splitData.reportActionID, createdReportActionID: splitData.createdReportActionID, From 13bad5bfb1515a6ae663629f7a05480350e4d1a1 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 21 Nov 2023 17:51:10 +0100 Subject: [PATCH 085/319] add tags within components --- src/components/MoneyRequestConfirmationList.js | 2 +- src/pages/iou/SplitBillDetailsPage.js | 2 ++ src/pages/iou/steps/MoneyRequestConfirmPage.js | 3 +++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index 6cf1b7e6cef1..d0ce641a1a8c 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -240,7 +240,7 @@ function MoneyRequestConfirmationList(props) { const policyTagList = lodashGet(policyTag, 'tags', {}); const policyTagListName = lodashGet(policyTag, 'name', translate('common.tag')); // A flag for showing the tags field - const shouldShowTags = props.isPolicyExpenseChat && OptionsListUtils.hasEnabledOptions(_.values(policyTagList)); + const shouldShowTags = props.isPolicyExpenseChat && (props.iouTag || OptionsListUtils.hasEnabledOptions(_.values(policyTagList))); // A flag for showing the billable field const shouldShowBillable = !lodashGet(props.policy, 'disabledFields.defaultBillable', true); diff --git a/src/pages/iou/SplitBillDetailsPage.js b/src/pages/iou/SplitBillDetailsPage.js index 2ebe96d60ed8..b582ae2b1396 100644 --- a/src/pages/iou/SplitBillDetailsPage.js +++ b/src/pages/iou/SplitBillDetailsPage.js @@ -101,6 +101,7 @@ function SplitBillDetailsPage(props) { merchant: splitMerchant, created: splitCreated, category: splitCategory, + tag: splitTag, } = isEditingSplitBill && props.draftTransaction ? ReportUtils.getTransactionDetails(props.draftTransaction) : ReportUtils.getTransactionDetails(props.transaction); const onConfirm = useCallback( @@ -134,6 +135,7 @@ function SplitBillDetailsPage(props) { iouCreated={splitCreated} iouMerchant={splitMerchant} iouCategory={splitCategory} + iouTag={splitTag} iouType={CONST.IOU.TYPE.SPLIT} isReadOnly={!isEditingSplitBill} shouldShowSmartScanFields diff --git a/src/pages/iou/steps/MoneyRequestConfirmPage.js b/src/pages/iou/steps/MoneyRequestConfirmPage.js index a69abeb94089..aae288155409 100644 --- a/src/pages/iou/steps/MoneyRequestConfirmPage.js +++ b/src/pages/iou/steps/MoneyRequestConfirmPage.js @@ -239,6 +239,7 @@ function MoneyRequestConfirmPage(props) { trimmedComment, props.iou.currency, props.iou.category, + props.iou.tag, reportID, ); return; @@ -254,6 +255,7 @@ function MoneyRequestConfirmPage(props) { trimmedComment, props.iou.currency, props.iou.category, + props.iou.tag, ); return; } @@ -277,6 +279,7 @@ function MoneyRequestConfirmPage(props) { props.currentUserPersonalDetails.accountID, props.iou.currency, props.iou.category, + props.iou.tag, props.iou.receiptPath, props.iou.receiptFilename, isDistanceRequest, From 77fceacf7d38f4767626b8466fb8e41093d2405b Mon Sep 17 00:00:00 2001 From: someone-here Date: Wed, 22 Nov 2023 02:15:07 +0530 Subject: [PATCH 086/319] Duplicate room name error when room created --- src/libs/actions/Report.js | 45 +++++++++---------- src/pages/workspace/WorkspaceNewRoomPage.js | 49 ++++++++++++++++++++- 2 files changed, 68 insertions(+), 26 deletions(-) diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index ac45a1e3f3be..a95c41bdab76 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -1550,26 +1550,7 @@ function navigateToConciergeChat(ignoreConciergeReportID = false) { * @param {String} writeCapability * @param {String} welcomeMessage */ -function addPolicyReport(policyID, reportName, visibility, writeCapability = CONST.REPORT.WRITE_CAPABILITIES.ALL, welcomeMessage = '') { - const participants = [currentUserAccountID]; - const parsedWelcomeMessage = ReportUtils.getParsedComment(welcomeMessage); - const policyReport = ReportUtils.buildOptimisticChatReport( - participants, - reportName, - CONST.REPORT.CHAT_TYPE.POLICY_ROOM, - policyID, - CONST.REPORT.OWNER_ACCOUNT_ID_FAKE, - false, - '', - visibility, - writeCapability, - - // The room might contain all policy members so notifying always should be opt-in only. - CONST.REPORT.NOTIFICATION_PREFERENCE.DAILY, - '', - '', - parsedWelcomeMessage, - ); +function addPolicyReport(policyReport) { const createdReportAction = ReportUtils.buildOptimisticCreatedReportAction(CONST.POLICY.OWNER_EMAIL_FAKE); // Onyx.set is used on the optimistic data so that it is present before navigating to the workspace room. With Onyx.merge the workspace room reportID is not present when @@ -1591,6 +1572,11 @@ function addPolicyReport(policyID, reportName, visibility, writeCapability = CON key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${policyReport.reportID}`, value: {[createdReportAction.reportActionID]: createdReportAction}, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.NEW_ROOM_FORM, + value: {isLoading: true}, + }, ]; const successData = [ { @@ -1611,6 +1597,11 @@ function addPolicyReport(policyID, reportName, visibility, writeCapability = CON }, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.NEW_ROOM_FORM, + value: {isLoading: false}, + }, ]; const failureData = [ { @@ -1622,22 +1613,26 @@ function addPolicyReport(policyID, reportName, visibility, writeCapability = CON }, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.NEW_ROOM_FORM, + value: {isLoading: false}, + }, ]; API.write( 'AddWorkspaceRoom', { policyID: policyReport.policyID, - reportName, - visibility, + reportName: policyReport.reportName, + visibility: policyReport.visibility, reportID: policyReport.reportID, createdReportActionID: createdReportAction.reportActionID, - writeCapability, - welcomeMessage: parsedWelcomeMessage, + writeCapability: policyReport.writeCapability, + welcomeMessage: policyReport.welcomeMessage, }, {optimisticData, successData, failureData}, ); - Navigation.dismissModal(policyReport.reportID); } /** diff --git a/src/pages/workspace/WorkspaceNewRoomPage.js b/src/pages/workspace/WorkspaceNewRoomPage.js index df78539bf665..6bac70202657 100644 --- a/src/pages/workspace/WorkspaceNewRoomPage.js +++ b/src/pages/workspace/WorkspaceNewRoomPage.js @@ -27,8 +27,10 @@ import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import * as App from '@userActions/App'; import * as Report from '@userActions/Report'; +import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import usePrevious from '@hooks/usePrevious'; const propTypes = { /** All reports shared with the user */ @@ -69,6 +71,13 @@ const defaultProps = { policies: {}, }; +function clearNewRoomFormError() { + Onyx.set(ONYXKEYS.FORMS.NEW_ROOM_FORM, { + isLoading: false, + errorFields: {}, + }); +} + function WorkspaceNewRoomPage(props) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -77,6 +86,7 @@ function WorkspaceNewRoomPage(props) { const [visibility, setVisibility] = useState(CONST.REPORT.VISIBILITY.RESTRICTED); const [policyID, setPolicyID] = useState(null); const [writeCapability, setWriteCapability] = useState(CONST.REPORT.WRITE_CAPABILITIES.ALL); + const wasLoading = usePrevious(props.formData.isLoading); const visibilityDescription = useMemo(() => translate(`newRoomPage.${visibility}Description`), [translate, visibility]); const isPolicyAdmin = useMemo(() => { if (!policyID) { @@ -85,14 +95,45 @@ function WorkspaceNewRoomPage(props) { return ReportUtils.isPolicyAdmin(policyID, props.policies); }, [policyID, props.policies]); + const [newRoomReportID, setNewRoomReportID] = useState(undefined); /** * @param {Object} values - form input values passed by the Form component */ const submit = (values) => { - Report.addPolicyReport(policyID, values.roomName, visibility, writeCapability, values.welcomeMessage); + const participants = [props.session.accountID]; + const parsedWelcomeMessage = ReportUtils.getParsedComment(values.welcomeMessage); + const policyReport = ReportUtils.buildOptimisticChatReport( + participants, + values.roomName, + CONST.REPORT.CHAT_TYPE.POLICY_ROOM, + policyID, + CONST.REPORT.OWNER_ACCOUNT_ID_FAKE, + false, + '', + visibility, + writeCapability || CONST.REPORT.WRITE_CAPABILITIES.ALL, + + // The room might contain all policy members so notifying always should be opt-in only. + CONST.REPORT.NOTIFICATION_PREFERENCE.DAILY, + '', + '', + parsedWelcomeMessage, + ); + setNewRoomReportID(policyReport.reportID); + Report.addPolicyReport(policyReport); }; + useEffect(() => { + return clearNewRoomFormError; + }, []); + + useEffect(() => { + if (((wasLoading && !props.formData.isLoading) || (isOffline && props.formData.isLoading)) && _.isEmpty(props.formData.errorFields)) { + Navigation.dismissModal(newRoomReportID); + } + }, [props.formData]); + useEffect(() => { if (isPolicyAdmin) { return; @@ -265,5 +306,11 @@ export default compose( reports: { key: ONYXKEYS.COLLECTION.REPORT, }, + formData: { + key: ONYXKEYS.FORMS.NEW_ROOM_FORM + }, + session: { + key: ONYXKEYS.SESSION, + } }), )(WorkspaceNewRoomPage); From b91ca76ea8c653855d572b9ddfef3e638d151b27 Mon Sep 17 00:00:00 2001 From: someone-here Date: Wed, 22 Nov 2023 02:56:01 +0530 Subject: [PATCH 087/319] Fix lint --- src/libs/actions/Report.js | 14 ++++-- src/pages/workspace/WorkspaceNewRoomPage.js | 51 ++++++++++++++------- 2 files changed, 43 insertions(+), 22 deletions(-) diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index a95c41bdab76..8267f1518797 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -1544,11 +1544,7 @@ function navigateToConciergeChat(ignoreConciergeReportID = false) { /** * Add a policy report (workspace room) optimistically and navigate to it. * - * @param {String} policyID - * @param {String} reportName - * @param {String} visibility - * @param {String} writeCapability - * @param {String} welcomeMessage + * @param {Object} policyReport */ function addPolicyReport(policyReport) { const createdReportAction = ReportUtils.buildOptimisticCreatedReportAction(CONST.POLICY.OWNER_EMAIL_FAKE); @@ -2501,6 +2497,13 @@ function searchInServer(searchInput) { debouncedSearchInServer(searchInput); } +function clearNewRoomFormError() { + Onyx.set(ONYXKEYS.FORMS.NEW_ROOM_FORM, { + isLoading: false, + errorFields: {}, + }); +} + export { searchInServer, addComment, @@ -2562,4 +2565,5 @@ export { openRoomMembersPage, savePrivateNotesDraft, getDraftPrivateNote, + clearNewRoomFormError, }; diff --git a/src/pages/workspace/WorkspaceNewRoomPage.js b/src/pages/workspace/WorkspaceNewRoomPage.js index 6bac70202657..6f30942cf825 100644 --- a/src/pages/workspace/WorkspaceNewRoomPage.js +++ b/src/pages/workspace/WorkspaceNewRoomPage.js @@ -16,9 +16,11 @@ import withNavigationFocus from '@components/withNavigationFocus'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import usePrevious from '@hooks/usePrevious'; import useWindowDimensions from '@hooks/useWindowDimensions'; import compose from '@libs/compose'; import * as ErrorUtils from '@libs/ErrorUtils'; +import Navigation from '@libs/Navigation/Navigation'; import Permissions from '@libs/Permissions'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -27,10 +29,8 @@ import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import * as App from '@userActions/App'; import * as Report from '@userActions/Report'; -import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import usePrevious from '@hooks/usePrevious'; const propTypes = { /** All reports shared with the user */ @@ -64,19 +64,34 @@ const propTypes = { /** Whether navigation is focused */ isFocused: PropTypes.bool.isRequired, + + /** Form state for NEW_ROOM_FORM */ + formState: PropTypes.shape({ + /** Loading state for the form */ + isLoading: PropTypes.bool, + + /** Field errors in the form */ + errorFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), + }), + + /** Session details for the user */ + session: PropTypes.shape({ + /** accountID of current user */ + accountID: PropTypes.number, + }), }; const defaultProps = { betas: [], reports: {}, policies: {}, -}; - -function clearNewRoomFormError() { - Onyx.set(ONYXKEYS.FORMS.NEW_ROOM_FORM, { + formState: { isLoading: false, errorFields: {}, - }); -} + }, + session: { + accountID: 0, + }, +}; function WorkspaceNewRoomPage(props) { const styles = useThemeStyles(); @@ -86,7 +101,7 @@ function WorkspaceNewRoomPage(props) { const [visibility, setVisibility] = useState(CONST.REPORT.VISIBILITY.RESTRICTED); const [policyID, setPolicyID] = useState(null); const [writeCapability, setWriteCapability] = useState(CONST.REPORT.WRITE_CAPABILITIES.ALL); - const wasLoading = usePrevious(props.formData.isLoading); + const wasLoading = usePrevious(props.formState.isLoading); const visibilityDescription = useMemo(() => translate(`newRoomPage.${visibility}Description`), [translate, visibility]); const isPolicyAdmin = useMemo(() => { if (!policyID) { @@ -113,7 +128,7 @@ function WorkspaceNewRoomPage(props) { '', visibility, writeCapability || CONST.REPORT.WRITE_CAPABILITIES.ALL, - + // The room might contain all policy members so notifying always should be opt-in only. CONST.REPORT.NOTIFICATION_PREFERENCE.DAILY, '', @@ -125,14 +140,16 @@ function WorkspaceNewRoomPage(props) { }; useEffect(() => { - return clearNewRoomFormError; + Report.clearNewRoomFormError(); }, []); useEffect(() => { - if (((wasLoading && !props.formData.isLoading) || (isOffline && props.formData.isLoading)) && _.isEmpty(props.formData.errorFields)) { - Navigation.dismissModal(newRoomReportID); + if (!(((wasLoading && !props.formState.isLoading) || (isOffline && props.formState.isLoading)) && _.isEmpty(props.formState.errorFields))) { + return; } - }, [props.formData]); + Navigation.dismissModal(newRoomReportID); + // eslint-disable-next-line react-hooks/exhaustive-deps -- we just want this to update on changing the form State + }, [props.formState]); useEffect(() => { if (isPolicyAdmin) { @@ -306,11 +323,11 @@ export default compose( reports: { key: ONYXKEYS.COLLECTION.REPORT, }, - formData: { - key: ONYXKEYS.FORMS.NEW_ROOM_FORM + formState: { + key: ONYXKEYS.FORMS.NEW_ROOM_FORM, }, session: { key: ONYXKEYS.SESSION, - } + }, }), )(WorkspaceNewRoomPage); From 8d53b8f53b988350545a269383bfd0d5cd8edea9 Mon Sep 17 00:00:00 2001 From: Sasha Kluger Date: Tue, 21 Nov 2023 17:21:20 -0800 Subject: [PATCH 088/319] Create Distance-Requests.md New help page for distance requests. It's pretty short and sweet for now, we can add more details and FAQs once new features are added and after we learn what questions people have. --- .../get-paid-back/Distance-Requests.md | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 docs/articles/new-expensify/get-paid-back/Distance-Requests.md diff --git a/docs/articles/new-expensify/get-paid-back/Distance-Requests.md b/docs/articles/new-expensify/get-paid-back/Distance-Requests.md new file mode 100644 index 000000000000..5217e8b08969 --- /dev/null +++ b/docs/articles/new-expensify/get-paid-back/Distance-Requests.md @@ -0,0 +1,30 @@ +--- +title: Distance Requests +description: How to create a distance request and request reimbursement for mileage +--- + + +# Overview + +Expensify allows you to request reimbursement for mileage by creating a distance request from a map. You can send a distance request in Expensify's mobile app, desktop app, or web app. + + +# How to create and send a distance request + +1. Click the green '+' button and select Request Money. +2. Select Distance along the top row of the Request Money window. +3. Enter the Start and Finish addresses then click Next. If there are multiple stops, you can add them before clicking Next. +4. Select your organization's workspace from the list of recent workspaces. +5. On the confirmation page, confirm the amount, Date, and Distance, and optionally enter a Description and Category. Click the Request button. +6. A workspace admin will receive your request and can reimburse you through Expensify or elsewhere! + + + +# FAQs + + +## How can I change the mileage rate used on my distance requests or add an additional rate? + +If you use a group workspace, then you can change the rate in your Workspace settings. In Expensify Classic, go to Settings > Workspaces > [Your Workspace] > Expenses > Distance > Add a Mileage Rate. If you submit mileage expenses on a group workspace, you must be a workspace admin to make changes. + +Individual workspaces offer a single default mileage rate that is aligned with the current IRS mileage reimbursement rate. From 4deb7a4b46b266988dda8639f5c3f995c20f01b6 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 22 Nov 2023 16:25:58 +0700 Subject: [PATCH 089/319] add comment --- src/libs/ReportUtils.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 39afb8a5a99e..281547d6ec03 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1815,6 +1815,7 @@ function getTransactionReportName(reportAction) { } const transaction = TransactionUtils.getLinkedTransaction(reportAction); + // Transaction data might be empty on app's first load, if so we fallback to parent report action's message if (_.isEmpty(transaction)) { return lodashGet(reportAction, ['message', 0, 'text'], '').replace(/(\r\n|\n|\r)/gm, ' '); } From 772fae10d4976968f49dcab977be9b5ab9e79787 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Wed, 22 Nov 2023 10:51:16 +0100 Subject: [PATCH 090/319] add has hover support in condition --- src/components/Hoverable/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/Hoverable/index.tsx b/src/components/Hoverable/index.tsx index 456f19e1e524..dc32c01ce3ab 100644 --- a/src/components/Hoverable/index.tsx +++ b/src/components/Hoverable/index.tsx @@ -1,4 +1,5 @@ import React, {cloneElement, forwardRef, Ref} from 'react'; +import {hasHoverSupport} from '@libs/DeviceCapabilities'; import ActiveHoverable from './ActiveHoverable'; import HoverableProps from './types'; @@ -9,7 +10,8 @@ import HoverableProps from './types'; */ function Hoverable({disabled, ...props}: HoverableProps, ref: Ref) { // If Hoverable is disabled, just render the child without additional logic or event listeners. - if (disabled) { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + if (disabled || !hasHoverSupport()) { return cloneElement(typeof props.children === 'function' ? props.children(false) : props.children, {ref}); } From 82b7c423949f79336507af57599ee2333d4d52df Mon Sep 17 00:00:00 2001 From: Yauheni Date: Wed, 22 Nov 2023 14:10:30 +0100 Subject: [PATCH 091/319] =?UTF-8?q?Fix=20bug=20with=20Login=E2=80=93Auto-f?= =?UTF-8?q?ill=20list=20appears=20if=20click=20on=20Sign=20in=20here?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/signin/LoginForm/BaseLoginForm.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/signin/LoginForm/BaseLoginForm.js b/src/pages/signin/LoginForm/BaseLoginForm.js index e48446360605..103bd4258f7c 100644 --- a/src/pages/signin/LoginForm/BaseLoginForm.js +++ b/src/pages/signin/LoginForm/BaseLoginForm.js @@ -1,3 +1,4 @@ +import {useNavigation} from '@react-navigation/native'; import {parsePhoneNumber} from 'awesome-phonenumber'; import Str from 'expensify-common/lib/str'; import PropTypes from 'prop-types'; @@ -101,6 +102,7 @@ function LoginForm(props) { const [formError, setFormError] = useState(false); const prevIsVisible = usePrevious(props.isVisible); const firstBlurred = useRef(false); + const navigation = useNavigation(); const {translate} = props; @@ -207,7 +209,7 @@ function LoginForm(props) { if (props.isFocused && props.isVisible) { Session.clearAccountMessages(); } - if (!canFocusInputOnScreenFocus() || !input.current || !props.isVisible) { + if (!canFocusInputOnScreenFocus() || !input.current || !props.isVisible || !navigation.isFocused()) { return; } let focusTimeout; From 5082a84302cacef454e5f6a83dbafa6f1dc0bd9f Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Wed, 22 Nov 2023 15:53:43 +0100 Subject: [PATCH 092/319] handle expense chat by one more property --- 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 db836549234d..fa214c6403b3 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -394,7 +394,7 @@ function isUserCreatedPolicyRoom(report) { * @returns {Boolean} */ function isPolicyExpenseChat(report) { - return getChatType(report) === CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT; + return getChatType(report) === CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT || report.isPolicyExpenseChat; } /** Wether the provided report belongs to a Control policy and is an epxense chat From 8c5f20b4df58cbc8b17826e899a07cd5ef2ae2a5 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Wed, 22 Nov 2023 15:57:47 +0100 Subject: [PATCH 093/319] implement buildOptimisticPolicyRecentlyUsedTags properly --- src/libs/actions/Policy.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index e374b6727551..f645d8dfc053 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -1475,9 +1475,14 @@ function buildOptimisticPolicyRecentlyUsedTags(policyID, tag) { return []; } - const policyRecentlyUsedTags = lodashGet(allRecentlyUsedTags, `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS}${policyID}`, []); + const policyRecentlyUsedTags = lodashGet(allRecentlyUsedTags, `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS}${policyID}`, {}); + // For now it only uses the first tag of the policy, since multi-tags are not yet supported + const tagListKey = _.first(_.keys(policyRecentlyUsedTags)); - return lodashUnion([tag], policyRecentlyUsedTags); + return { + ...policyRecentlyUsedTags, + [tagListKey]: lodashUnion([tag], lodashGet(policyRecentlyUsedTags, [tagListKey], [])), + }; } export { From e8cc3ae102c61c9addbbb8d9cd42b8ae60f2ba36 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Wed, 22 Nov 2023 15:58:13 +0100 Subject: [PATCH 094/319] integrate buildOptimisticPolicyRecentlyUsedTags --- src/libs/actions/IOU.js | 56 ++++++----------------------------------- 1 file changed, 7 insertions(+), 49 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 833f1a28982c..4d5335ac8b31 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -63,34 +63,6 @@ Onyx.connect({ }, }); -let allRecentlyUsedTags = {}; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS, - waitForCollectionCallback: true, - callback: (value) => { - if (!value) { - allRecentlyUsedTags = {}; - return; - } - - allRecentlyUsedTags = value; - }, -}); - -let allPolicyTags = {}; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.POLICY_TAGS, - waitForCollectionCallback: true, - callback: (value) => { - if (!value) { - allPolicyTags = {}; - return; - } - - allPolicyTags = value; - }, -}); - let userAccountID = ''; let currentUserEmail = ''; Onyx.connect({ @@ -516,15 +488,9 @@ function getMoneyRequestInformation( optimisticPolicyRecentlyUsedCategories = Policy.buildOptimisticPolicyRecentlyUsedCategories(iouReport.policyID, category); } - const optimisticPolicyRecentlyUsedTags = {}; - const policyTags = allPolicyTags[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${iouReport.policyID}`]; - const recentlyUsedPolicyTags = allRecentlyUsedTags[`${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS}${iouReport.policyID}`]; - - if (policyTags) { - // For now it only uses the first tag of the policy, since multi-tags are not yet supported - const tagListKey = _.first(_.keys(policyTags)); - const uniquePolicyRecentlyUsedTags = recentlyUsedPolicyTags ? _.filter(recentlyUsedPolicyTags[tagListKey], (recentlyUsedPolicyTag) => recentlyUsedPolicyTag !== tag) : []; - optimisticPolicyRecentlyUsedTags[tagListKey] = [tag, ...uniquePolicyRecentlyUsedTags]; + let optimisticPolicyRecentlyUsedTags = {}; + if (tag) { + optimisticPolicyRecentlyUsedTags = Policy.buildOptimisticPolicyRecentlyUsedTags(iouReport.policyID, tag); } // If there is an existing transaction (which is the case for distance requests), then the data from the existing transaction @@ -1200,10 +1166,9 @@ function createSplitsAndOnyxData(participants, currentUserLogin, currentUserAcco } // Add tag to optimistic policy recently used tags when a participant is a workspace - let optimisticPolicyRecentlyUsedTags = []; + let optimisticPolicyRecentlyUsedTags = {}; if (isPolicyExpenseChat) { - // TODO: Implement - optimisticPolicyRecentlyUsedTags = Policy.buildOptimisticPolicyRecentlyUsedTags(participant.policyID, category); + optimisticPolicyRecentlyUsedTags = Policy.buildOptimisticPolicyRecentlyUsedTags(participant.policyID, tag); } // STEP 5: Build Onyx Data @@ -1307,7 +1272,6 @@ function splitBill(participants, currentUserLogin, currentUserAccountID, amount, */ function splitBillAndOpenReport(participants, currentUserLogin, currentUserAccountID, amount, comment, currency, category, tag) { const {splitData, splits, onyxData} = createSplitsAndOnyxData(participants, currentUserLogin, currentUserAccountID, amount, comment, currency, category, tag); - API.write( 'SplitBillAndOpenReport', { @@ -1847,15 +1811,9 @@ function editRegularMoneyRequest(transactionID, transactionThreadReportID, trans updatedChatReport.lastMessageHtml = messageText; } - const optimisticPolicyRecentlyUsedTags = {}; + let optimisticPolicyRecentlyUsedTags = {}; if (_.has(transactionChanges, 'tag')) { - const tagListName = transactionChanges.tagListName; - const recentlyUsedPolicyTags = allRecentlyUsedTags[`${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS}${iouReport.policyID}`]; - - const uniquePolicyRecentlyUsedTags = recentlyUsedPolicyTags - ? _.filter(recentlyUsedPolicyTags[tagListName], (recentlyUsedPolicyTag) => recentlyUsedPolicyTag !== transactionChanges.tag) - : []; - optimisticPolicyRecentlyUsedTags[tagListName] = [transactionChanges.tag, ...uniquePolicyRecentlyUsedTags]; + optimisticPolicyRecentlyUsedTags = Policy.buildOptimisticPolicyRecentlyUsedTags(iouReport.policyID, transactionChanges.tag); } const isScanning = TransactionUtils.hasReceipt(updatedTransaction) && TransactionUtils.isReceiptBeingScanned(updatedTransaction); From d55f2ef886b0360b6c67ae5da6c1778c832f0190 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Wed, 22 Nov 2023 16:03:42 +0100 Subject: [PATCH 095/319] check isPolicyExpenseChat safely --- 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 fa214c6403b3..675349712f6a 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -394,7 +394,7 @@ function isUserCreatedPolicyRoom(report) { * @returns {Boolean} */ function isPolicyExpenseChat(report) { - return getChatType(report) === CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT || report.isPolicyExpenseChat; + return getChatType(report) === CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT || lodashGet(report, 'isPolicyExpenseChat', false); } /** Wether the provided report belongs to a Control policy and is an epxense chat From ec204b54e30a92413a406f438a3f878999b81cc7 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Wed, 22 Nov 2023 16:34:27 +0100 Subject: [PATCH 096/319] fix taking tag list name --- src/libs/actions/Policy.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index f645d8dfc053..d37b208be16f 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -88,6 +88,20 @@ Onyx.connect({ callback: (val) => (allRecentlyUsedCategories = val), }); +let allPolicyTags = {}; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.POLICY_TAGS, + waitForCollectionCallback: true, + callback: (value) => { + if (!value) { + allPolicyTags = {}; + return; + } + + allPolicyTags = value; + }, +}); + let allRecentlyUsedTags = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS, @@ -1475,9 +1489,10 @@ function buildOptimisticPolicyRecentlyUsedTags(policyID, tag) { return []; } - const policyRecentlyUsedTags = lodashGet(allRecentlyUsedTags, `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS}${policyID}`, {}); + const policyTags = lodashGet(allPolicyTags, `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {}); // For now it only uses the first tag of the policy, since multi-tags are not yet supported - const tagListKey = _.first(_.keys(policyRecentlyUsedTags)); + const tagListKey = _.first(_.keys(policyTags)); + const policyRecentlyUsedTags = lodashGet(allRecentlyUsedTags, `${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS}${policyID}`, {}); return { ...policyRecentlyUsedTags, From 9650b81e607196a213111cc5df6803da223935d9 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Wed, 22 Nov 2023 22:41:50 +0700 Subject: [PATCH 097/319] fix remove useRef in settimeout --- src/libs/ReportUtils.js | 1 + .../PrivateNotes/PrivateNotesListPage.js | 30 +++++++++---------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 195630dcbd32..26b461bf80ae 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -4317,6 +4317,7 @@ function shouldDisableWelcomeMessage(report, policy) { } /** + * Navigates to the appropriate screen based on the presence of a private note for the current user. * @param {Object} report * @param {Object} session */ diff --git a/src/pages/PrivateNotes/PrivateNotesListPage.js b/src/pages/PrivateNotes/PrivateNotesListPage.js index 7757dda487bf..e9a4f11a7202 100644 --- a/src/pages/PrivateNotes/PrivateNotesListPage.js +++ b/src/pages/PrivateNotes/PrivateNotesListPage.js @@ -58,6 +58,20 @@ const defaultProps = { function PrivateNotesListPage({report, personalDetailsList, session}) { const styles = useThemeStyles(); const {translate} = useLocalize(); + const isFocused = useIsFocused(); + + useEffect(() => { + const navigateToEditPageTimeout = setTimeout(() => { + if (_.some(report.privateNotes, (item) => item.note) || !isFocused) { + return; + } + Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, session.accountID)); + }, CONST.ANIMATED_TRANSITION); + + return () => { + clearTimeout(navigateToEditPageTimeout); + }; + }, [report.privateNotes, report.reportID, session.accountID, isFocused]); /** * Gets the menu item for each workspace @@ -107,23 +121,7 @@ function PrivateNotesListPage({report, personalDetailsList, session}) { })) .value(); }, [report, personalDetailsList, session, translate]); - const isFocused = useIsFocused(); - const navigateToEditPageTimeoutRef = React.useRef(null); - useEffect(() => { - navigateToEditPageTimeoutRef.current = setTimeout(() => { - if (_.some(report.privateNotes, (item) => item.note) || !isFocused) { - return; - } - Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, session.accountID)); - }, CONST.ANIMATED_TRANSITION); - return () => { - if (!navigateToEditPageTimeoutRef.current) { - return; - } - clearTimeout(navigateToEditPageTimeoutRef.current); - }; - }, [report.privateNotes, report.reportID, session.accountID, isFocused]); return ( Date: Wed, 22 Nov 2023 20:09:01 +0100 Subject: [PATCH 098/319] add unsetHoveredIfOutside functionality --- src/components/Hoverable/ActiveHoverable.tsx | 61 +++++++++++++++++++- src/components/Hoverable/types.ts | 4 +- 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/src/components/Hoverable/ActiveHoverable.tsx b/src/components/Hoverable/ActiveHoverable.tsx index c03aed5b3d11..2a6351f88022 100644 --- a/src/components/Hoverable/ActiveHoverable.tsx +++ b/src/components/Hoverable/ActiveHoverable.tsx @@ -1,12 +1,31 @@ -import {cloneElement, forwardRef, Ref, useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import {cloneElement, forwardRef, MutableRefObject, Ref, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import {DeviceEventEmitter} from 'react-native'; import CONST from '@src/CONST'; import HoverableProps from './types'; +/** + * Assigns a ref to an element, either by setting the current property of the ref object or by calling the ref function + * @param ref The ref object or function. + * @param element The element to assign the ref to. + */ +function assignRef(ref: ((instance: HTMLElement | null) => void) | MutableRefObject, element: HTMLElement) { + if (!ref) { + return; + } + if (typeof ref === 'function') { + ref(element); + } else if ('current' in ref) { + // eslint-disable-next-line no-param-reassign + ref.current = element; + } +} + type ActiveHoverableProps = Omit; -function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, children}: ActiveHoverableProps, ref: Ref) { +function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, children}: ActiveHoverableProps, outerRef: Ref) { const [isHovered, setIsHovered] = useState(false); + + const ref = useRef(null); const isScrolling = useRef(false); const isHoveredRef = useRef(false); @@ -21,6 +40,9 @@ function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, children [shouldHandleScroll], ); + // Expose inner ref to parent through outerRef. This enable us to use ref both in parent and child. + useImperativeHandle(outerRef, () => ref.current, []); + useEffect(() => (isHovered ? onHoverIn?.() : onHoverOut?.()), [isHovered, onHoverIn, onHoverOut]); useEffect(() => { @@ -38,6 +60,30 @@ function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, children return () => scrollingListener.remove(); }, [shouldHandleScroll]); + useEffect(() => { + /** + * Checks the hover state of a component and updates it based on the event target. + * This is necessary to handle cases where the hover state might get stuck due to an unreliable mouseleave trigger, + * such as when an element is removed before the mouseleave event is triggered. + * @param event The hover event object. + */ + const unsetHoveredIfOutside = (event: MouseEvent) => { + if (!ref.current || !isHovered) { + return; + } + + if (ref.current.contains(event.target as Node)) { + return; + } + + setIsHovered(false); + }; + + document.addEventListener('mouseover', unsetHoveredIfOutside); + + return () => document.removeEventListener('mouseover', unsetHoveredIfOutside); + }, [isHovered, ref]); + useEffect(() => { const unsetHoveredWhenDocumentIsHidden = () => document.visibilityState === 'hidden' && setIsHovered(false); @@ -64,8 +110,17 @@ function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, children [updateIsHovered, child.props], ); + // We need to access the ref of a children from both parent and current component + // So we pass it to current ref and assign it once again to the child ref prop + const hijackRef = (el: HTMLElement) => { + ref.current = el; + if (child.ref) { + assignRef(child.ref, el); + } + }; + return cloneElement(child, { - ref, + ref: hijackRef, onMouseEnter, onMouseLeave, }); diff --git a/src/components/Hoverable/types.ts b/src/components/Hoverable/types.ts index c1c055afe9c7..8008c5dae0cf 100644 --- a/src/components/Hoverable/types.ts +++ b/src/components/Hoverable/types.ts @@ -1,8 +1,8 @@ -import {ReactElement} from 'react'; +import {ReactElement, RefAttributes} from 'react'; type HoverableProps = { /** Children to wrap with Hoverable. */ - children: ((isHovered: boolean) => ReactElement) | ReactElement; + children: ((isHovered: boolean) => ReactElement & RefAttributes) | (ReactElement & RefAttributes); /** Whether to disable the hover action */ disabled?: boolean; From 1f910d22fc7154b4172b47274b374dd99389fb8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Wed, 22 Nov 2023 20:10:55 +0100 Subject: [PATCH 099/319] add onBlur handler --- src/components/Hoverable/ActiveHoverable.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/components/Hoverable/ActiveHoverable.tsx b/src/components/Hoverable/ActiveHoverable.tsx index 2a6351f88022..741e012f4601 100644 --- a/src/components/Hoverable/ActiveHoverable.tsx +++ b/src/components/Hoverable/ActiveHoverable.tsx @@ -110,6 +110,19 @@ function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, children}: [updateIsHovered, child.props], ); + const disableHoveredOnBlur = useCallback( + (event: MouseEvent) => { + // Check if the blur event occurred due to clicking outside the element + // and the wrapperView contains the element that caused the blur and reset isHovered + if (!ref.current?.contains(event.target as Node) && !ref.current?.contains(event.relatedTarget as Node)) { + setIsHovered(false); + } + + child.props.onBlur?.(event); + }, + [child.props], + ); + // We need to access the ref of a children from both parent and current component // So we pass it to current ref and assign it once again to the child ref prop const hijackRef = (el: HTMLElement) => { @@ -123,6 +136,7 @@ function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, children}: ref: hijackRef, onMouseEnter, onMouseLeave, + onBlur: disableHoveredOnBlur, }); } From 3823435ee2870e4392b5d088321695e1d3f9a272 Mon Sep 17 00:00:00 2001 From: Sasha Kluger Date: Wed, 22 Nov 2023 13:27:57 -0800 Subject: [PATCH 100/319] Update Distance-Requests.md Updated doc based on @neil-marcellini's comments. --- .../new-expensify/get-paid-back/Distance-Requests.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/articles/new-expensify/get-paid-back/Distance-Requests.md b/docs/articles/new-expensify/get-paid-back/Distance-Requests.md index 5217e8b08969..c8060be13cea 100644 --- a/docs/articles/new-expensify/get-paid-back/Distance-Requests.md +++ b/docs/articles/new-expensify/get-paid-back/Distance-Requests.md @@ -6,7 +6,7 @@ description: How to create a distance request and request reimbursement for mile # Overview -Expensify allows you to request reimbursement for mileage by creating a distance request from a map. You can send a distance request in Expensify's mobile app, desktop app, or web app. +Expensify allows you to request reimbursement for mileage by creating a distance request from a map. You can send a distance request in Expensify's mobile, desktop, or web app. # How to create and send a distance request @@ -14,8 +14,8 @@ Expensify allows you to request reimbursement for mileage by creating a distance 1. Click the green '+' button and select Request Money. 2. Select Distance along the top row of the Request Money window. 3. Enter the Start and Finish addresses then click Next. If there are multiple stops, you can add them before clicking Next. -4. Select your organization's workspace from the list of recent workspaces. -5. On the confirmation page, confirm the amount, Date, and Distance, and optionally enter a Description and Category. Click the Request button. +4. Choose who to send the request to by selecting your organization's workspace from the list of recent workspaces. +5. On the confirmation page, confirm the amount, date, and distance, and optionally add additional information such as a description or category. Click the Request button. 6. A workspace admin will receive your request and can reimburse you through Expensify or elsewhere! @@ -25,6 +25,4 @@ Expensify allows you to request reimbursement for mileage by creating a distance ## How can I change the mileage rate used on my distance requests or add an additional rate? -If you use a group workspace, then you can change the rate in your Workspace settings. In Expensify Classic, go to Settings > Workspaces > [Your Workspace] > Expenses > Distance > Add a Mileage Rate. If you submit mileage expenses on a group workspace, you must be a workspace admin to make changes. - -Individual workspaces offer a single default mileage rate that is aligned with the current IRS mileage reimbursement rate. +If you use a group workspace, then you can edit the default distance rate or add additional rates in your Workspace settings. In Expensify Classic, go to Settings > Workspaces > [Your Workspace] > Expenses and scroll down to the Distance section to update or add mileage rates. You must be a workspace admin to modify or add rates. From 4e7f09ad359d492937cfa9598985ae5758485808 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 23 Nov 2023 09:41:00 +0100 Subject: [PATCH 101/319] add lazy imports for AuthScreens --- src/libs/Navigation/AppNavigator/AuthScreens.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.js b/src/libs/Navigation/AppNavigator/AuthScreens.js index aedb2fa8d741..0fc78f0cb324 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.js +++ b/src/libs/Navigation/AppNavigator/AuthScreens.js @@ -14,7 +14,6 @@ import PusherConnectionManager from '@libs/PusherConnectionManager'; import * as SessionUtils from '@libs/SessionUtils'; import DemoSetupPage from '@pages/DemoSetupPage'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; -import DesktopSignInRedirectPage from '@pages/signin/DesktopSignInRedirectPage'; import useThemeStyles from '@styles/useThemeStyles'; import * as App from '@userActions/App'; import * as Download from '@userActions/Download'; @@ -34,13 +33,14 @@ import createCustomStackNavigator from './createCustomStackNavigator'; import defaultScreenOptions from './defaultScreenOptions'; import getRootNavigatorScreenOptions from './getRootNavigatorScreenOptions'; import CentralPaneNavigator from './Navigators/CentralPaneNavigator'; -import RightModalNavigator from './Navigators/RightModalNavigator'; const loadReportAttachments = () => require('../../../pages/home/report/ReportAttachments').default; +const loadRightModalNavigator = () => require('./Navigators/RightModalNavigator').default; const loadSidebarScreen = () => require('../../../pages/home/sidebar/SidebarScreen').default; const loadValidateLoginPage = () => require('../../../pages/ValidateLoginPage').default; const loadLogOutPreviousUserPage = () => require('../../../pages/LogOutPreviousUserPage').default; const loadConciergePage = () => require('../../../pages/ConciergePage').default; +const loadDesktopSignInRedirectPage = () => require('../../../pages/signin/DesktopSignInRedirectPage').default; let timezone; let currentAccountID; @@ -332,13 +332,13 @@ function AuthScreens({isUsingMemoryOnlyKeys, lastUpdateIDAppliedToClient, sessio From 9a958eb812bf006aa185bc2062e4b1beb9f39dff Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 23 Nov 2023 11:32:13 +0100 Subject: [PATCH 102/319] return RightModalNavigator --- src/libs/Navigation/AppNavigator/AuthScreens.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.js b/src/libs/Navigation/AppNavigator/AuthScreens.js index 0fc78f0cb324..1a4873db0a42 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.js +++ b/src/libs/Navigation/AppNavigator/AuthScreens.js @@ -33,9 +33,9 @@ import createCustomStackNavigator from './createCustomStackNavigator'; import defaultScreenOptions from './defaultScreenOptions'; import getRootNavigatorScreenOptions from './getRootNavigatorScreenOptions'; import CentralPaneNavigator from './Navigators/CentralPaneNavigator'; +import RightModalNavigator from './Navigators/RightModalNavigator'; const loadReportAttachments = () => require('../../../pages/home/report/ReportAttachments').default; -const loadRightModalNavigator = () => require('./Navigators/RightModalNavigator').default; const loadSidebarScreen = () => require('../../../pages/home/sidebar/SidebarScreen').default; const loadValidateLoginPage = () => require('../../../pages/ValidateLoginPage').default; const loadLogOutPreviousUserPage = () => require('../../../pages/LogOutPreviousUserPage').default; @@ -332,7 +332,7 @@ function AuthScreens({isUsingMemoryOnlyKeys, lastUpdateIDAppliedToClient, sessio Date: Thu, 23 Nov 2023 12:00:51 +0100 Subject: [PATCH 103/319] export SupportedLanguage --- src/libs/EmojiTrie.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/EmojiTrie.ts b/src/libs/EmojiTrie.ts index a45d1bc45b33..100d24a01546 100644 --- a/src/libs/EmojiTrie.ts +++ b/src/libs/EmojiTrie.ts @@ -128,4 +128,4 @@ const emojiTrie: EmojiTrie = supportedLanguages.reduce((prev, cur) => ({...prev, Timing.end(CONST.TIMING.TRIE_INITIALIZATION); export default emojiTrie; -export type {SimpleEmoji}; +export type {SimpleEmoji, SupportedLanguage}; From dad435b6c607376c3efc138dced1fdfd40c4ce84 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 23 Nov 2023 12:01:31 +0100 Subject: [PATCH 104/319] lazy import of emojisTrie --- src/libs/EmojiUtils.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index 22e84921b1ee..db515f7d7703 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -8,7 +8,7 @@ import {Emoji, HeaderEmoji, PickerEmojis} from '@assets/emojis/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {FrequentlyUsedEmoji} from '@src/types/onyx'; -import emojisTrie from './EmojiTrie'; +import {SupportedLanguage} from './EmojiTrie'; type HeaderIndice = {code: string; index: number; icon: React.FC}; type EmojiSpacer = {code: string; spacer: boolean}; @@ -322,6 +322,9 @@ function getAddedEmojis(currentEmojis: Emoji[], formerEmojis: Emoji[]): Emoji[] * If we're on mobile, we also add a space after the emoji granted there's no text after it. */ function replaceEmojis(text: string, preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE, lang: 'en' | 'es' = CONST.LOCALES.DEFAULT): ReplacedEmoji { + // emojisTrie importing the emoji json file on app starting and we want to avoid it + const emojisTrie = require('./EmojiTrie').default; + const trie = emojisTrie[lang]; if (!trie) { return {text, emojis: []}; @@ -396,7 +399,10 @@ function replaceAndExtractEmojis(text: string, preferredSkinTone = CONST.EMOJI_D * Suggest emojis when typing emojis prefix after colon * @param [limit] - matching emojis limit */ -function suggestEmojis(text: string, lang: keyof typeof emojisTrie, limit = CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_SUGGESTIONS): Emoji[] | undefined { +function suggestEmojis(text: string, lang: keyof SupportedLanguage, limit = CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_SUGGESTIONS): Emoji[] | undefined { + // emojisTrie importing the emoji json file on app starting and we want to avoid it + const emojisTrie = require('./EmojiTrie').default; + const trie = emojisTrie[lang]; if (!trie) { return []; From fa945daa32f38ac9631c2343c3f27c37fc7b4f39 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 23 Nov 2023 14:25:05 +0100 Subject: [PATCH 105/319] lazy BankIcons --- src/components/Icon/BankIcons.ts | 179 +++++++++++-------------------- 1 file changed, 62 insertions(+), 117 deletions(-) diff --git a/src/components/Icon/BankIcons.ts b/src/components/Icon/BankIcons.ts index 5d17e7f0c991..eaf08b93ddd1 100644 --- a/src/components/Icon/BankIcons.ts +++ b/src/components/Icon/BankIcons.ts @@ -1,46 +1,8 @@ import {CSSProperties} from 'react'; import {ViewStyle} from 'react-native'; import {SvgProps} from 'react-native-svg'; -import AmericanExpress from '@assets/images/bankicons/american-express.svg'; -import BankOfAmerica from '@assets/images/bankicons/bank-of-america.svg'; -import BB_T from '@assets/images/bankicons/bb-t.svg'; -import CapitalOne from '@assets/images/bankicons/capital-one.svg'; -import CharlesSchwab from '@assets/images/bankicons/charles-schwab.svg'; -import Chase from '@assets/images/bankicons/chase.svg'; -import CitiBank from '@assets/images/bankicons/citibank.svg'; -import CitizensBank from '@assets/images/bankicons/citizens-bank.svg'; -import Discover from '@assets/images/bankicons/discover.svg'; -import Fidelity from '@assets/images/bankicons/fidelity.svg'; import GenericBank from '@assets/images/bankicons/generic-bank-account.svg'; -import HuntingtonBank from '@assets/images/bankicons/huntington-bank.svg'; -import NavyFederalCreditUnion from '@assets/images/bankicons/navy-federal-credit-union.svg'; -import PNC from '@assets/images/bankicons/pnc.svg'; -import RegionsBank from '@assets/images/bankicons/regions-bank.svg'; -import SunTrust from '@assets/images/bankicons/suntrust.svg'; -import TdBank from '@assets/images/bankicons/td-bank.svg'; -import USBank from '@assets/images/bankicons/us-bank.svg'; -import USAA from '@assets/images/bankicons/usaa.svg'; -// Card Icons -import AmericanExpressCard from '@assets/images/cardicons/american-express.svg'; -import BankOfAmericaCard from '@assets/images/cardicons/bank-of-america.svg'; -import BB_TCard from '@assets/images/cardicons/bb-t.svg'; -import CapitalOneCard from '@assets/images/cardicons/capital-one.svg'; -import CharlesSchwabCard from '@assets/images/cardicons/charles-schwab.svg'; -import ChaseCard from '@assets/images/cardicons/chase.svg'; -import CitiBankCard from '@assets/images/cardicons/citibank.svg'; -import CitizensBankCard from '@assets/images/cardicons/citizens.svg'; -import DiscoverCard from '@assets/images/cardicons/discover.svg'; -import ExpensifyCardImage from '@assets/images/cardicons/expensify-card-dark.svg'; -import FidelityCard from '@assets/images/cardicons/fidelity.svg'; import GenericBankCard from '@assets/images/cardicons/generic-bank-card.svg'; -import HuntingtonBankCard from '@assets/images/cardicons/huntington-bank.svg'; -import NavyFederalCreditUnionCard from '@assets/images/cardicons/navy-federal-credit-union.svg'; -import PNCCard from '@assets/images/cardicons/pnc.svg'; -import RegionsBankCard from '@assets/images/cardicons/regions-bank.svg'; -import SunTrustCard from '@assets/images/cardicons/suntrust.svg'; -import TdBankCard from '@assets/images/cardicons/td-bank.svg'; -import USBankCard from '@assets/images/cardicons/us-bank.svg'; -import USAACard from '@assets/images/cardicons/usaa.svg'; import styles from '@styles/styles'; import variables from '@styles/variables'; @@ -52,101 +14,84 @@ type BankIcon = { iconStyles?: Array; }; +const BANK_NAMES = { + EXPENSIFY: 'expensify', + AMERICAN_EXPRESS: 'americanexpress', + BANK_OF_AMERICA: 'bank of america', + BB_T: 'bbt', + CAPITAL_ONE: 'capital one', + CHASE: 'chase', + CHARLES_SCHWAB: 'charles schwab', + CITIBANK: 'citibank', + CITIZENS_BANK: 'citizens bank', + DISCOVER: 'discover', + FIDELITY: 'fidelity', + GENERIC_BANK: 'generic bank', + HUNTINGTON_BANK: 'huntington bank', + NAVY_FEDERAL_CREDIT_UNION: 'navy federal credit union', + PNC: 'pnc', + REGIONS_BANK: 'regions bank', + SUNTRUST: 'suntrust', + TD_BANK: 'td bank', + US_BANK: 'us bank', + USAA: 'usaa', +}; + +type BankName = keyof typeof BANK_NAMES; + /** * Returns matching asset icon for bankName */ -function getAssetIcon(bankName: string, isCard: boolean): React.FC { - if (bankName.includes('expensify')) { - return ExpensifyCardImage; - } - - if (bankName.includes('americanexpress')) { - return isCard ? AmericanExpressCard : AmericanExpress; - } - - if (bankName.includes('bank of america') || bankName.includes('bankofamerica')) { - return isCard ? BankOfAmericaCard : BankOfAmerica; - } - - if (bankName.startsWith('bbt')) { - return isCard ? BB_TCard : BB_T; - } - - if (bankName.startsWith('capital one') || bankName.includes('capitalone')) { - return isCard ? CapitalOneCard : CapitalOne; - } - - if (bankName.startsWith('chase') || bankName.includes('chase')) { - return isCard ? ChaseCard : Chase; - } - - if (bankName.includes('charles schwab') || bankName.includes('charlesschwab')) { - return isCard ? CharlesSchwabCard : CharlesSchwab; - } - - if (bankName.startsWith('citibank') || bankName.includes('citibank')) { - return isCard ? CitiBankCard : CitiBank; - } - - if (bankName.startsWith('citizens bank') || bankName.includes('citizensbank')) { - return isCard ? CitizensBankCard : CitizensBank; - } - - if (bankName.startsWith('discover ') || bankName.includes('discover.') || bankName === 'discover') { - return isCard ? DiscoverCard : Discover; - } - - if (bankName.startsWith('fidelity')) { - return isCard ? FidelityCard : Fidelity; - } - - if (bankName.startsWith('huntington bank') || bankName.includes('huntingtonnational') || bankName.includes('huntington national')) { - return isCard ? HuntingtonBankCard : HuntingtonBank; - } - - if (bankName.startsWith('navy federal credit union') || bankName.includes('navy federal credit union')) { - return isCard ? NavyFederalCreditUnionCard : NavyFederalCreditUnion; - } - - if (bankName.startsWith('pnc') || bankName.includes('pnc')) { - return isCard ? PNCCard : PNC; - } - - if (bankName.startsWith('regions bank') || bankName.includes('regionsbank')) { - return isCard ? RegionsBankCard : RegionsBank; - } - - if (bankName.startsWith('suntrust') || bankName.includes('suntrust')) { - return isCard ? SunTrustCard : SunTrust; - } - - if (bankName.startsWith('td bank') || bankName.startsWith('tdbank') || bankName.includes('tdbank')) { - return isCard ? TdBankCard : TdBank; - } - - if (bankName.startsWith('us bank') || bankName.startsWith('usbank')) { - return isCard ? USBankCard : USBank; - } +function getAssetIcon(bankName: BankName, isCard: boolean): React.FC { + // Mapping bank names to their respective icon paths + const iconMappings = { + [BANK_NAMES.EXPENSIFY]: isCard ? require('@assets/images/cardicons/expensify-card-dark.svg') : require('@assets/images/bankicons/expensify.svg'), + [BANK_NAMES.AMERICAN_EXPRESS]: isCard ? require('@assets/images/cardicons/american-express.svg') : require('@assets/images/bankicons/american-express.svg'), + [BANK_NAMES.BANK_OF_AMERICA]: isCard ? require('@assets/images/cardicons/bank-of-america.svg') : require('@assets/images/bankicons/bank-of-america.svg'), + [BANK_NAMES.BB_T]: isCard ? require('@assets/images/cardicons/bb-t.svg') : require('@assets/images/bankicons/bb-t.svg'), + [BANK_NAMES.CAPITAL_ONE]: isCard ? require('@assets/images/cardicons/capital-one.svg') : require('@assets/images/bankicons/capital-one.svg'), + [BANK_NAMES.CHASE]: isCard ? require('@assets/images/cardicons/chase.svg') : require('@assets/images/bankicons/chase.svg'), + [BANK_NAMES.CHARLES_SCHWAB]: isCard ? require('@assets/images/cardicons/charles-schwab.svg') : require('@assets/images/bankicons/charles-schwab.svg'), + [BANK_NAMES.CITIBANK]: isCard ? require('@assets/images/cardicons/citibank.svg') : require('@assets/images/bankicons/citibank.svg'), + [BANK_NAMES.CITIZENS_BANK]: isCard ? require('@assets/images/cardicons/citizens.svg') : require('@assets/images/bankicons/citizens-bank.svg'), + [BANK_NAMES.DISCOVER]: isCard ? require('@assets/images/cardicons/discover.svg') : require('@assets/images/bankicons/discover.svg'), + [BANK_NAMES.FIDELITY]: isCard ? require('@assets/images/cardicons/fidelity.svg') : require('@assets/images/bankicons/fidelity.svg'), + [BANK_NAMES.GENERIC_BANK]: isCard ? require('@assets/images/cardicons/generic-bank-card.svg') : require('@assets/images/bankicons/generic-bank-account.svg'), + [BANK_NAMES.HUNTINGTON_BANK]: isCard ? require('@assets/images/cardicons/huntington-bank.svg') : require('@assets/images/bankicons/huntington-bank.svg'), + [BANK_NAMES.NAVY_FEDERAL_CREDIT_UNION]: isCard + ? require('@assets/images/cardicons/navy-federal-credit-union.svg') + : require('@assets/images/bankicons/navy-federal-credit-union.svg'), + [BANK_NAMES.PNC]: isCard ? require('@assets/images/cardicons/pnc.svg') : require('@assets/images/bankicons/pnc.svg'), + [BANK_NAMES.REGIONS_BANK]: isCard ? require('@assets/images/cardicons/regions-bank.svg') : require('@assets/images/bankicons/regions-bank.svg'), + [BANK_NAMES.SUNTRUST]: isCard ? require('@assets/images/cardicons/suntrust.svg') : require('@assets/images/bankicons/suntrust.svg'), + [BANK_NAMES.TD_BANK]: isCard ? require('@assets/images/cardicons/td-bank.svg') : require('@assets/images/bankicons/td-bank.svg'), + [BANK_NAMES.US_BANK]: isCard ? require('@assets/images/cardicons/us-bank.svg') : require('@assets/images/bankicons/us-bank.svg'), + [BANK_NAMES.USAA]: isCard ? require('@assets/images/cardicons/usaa.svg') : require('@assets/images/bankicons/usaa.svg'), + }; - if (bankName.includes('usaa')) { - return isCard ? USAACard : USAA; - } + // Fallback to generic bank/card icon + const iconModule = iconMappings[bankName] || (isCard ? '@assets/images/cardicons/generic-bank-card.svg' : '@assets/images/bankicons/generic-bank-account.svg'); + return iconModule as React.FC; +} - return isCard ? GenericBankCard : GenericBank; +function getBankNameKey(bankName: string): BankName | undefined { + return Object.keys(BANK_NAMES).find((key) => BANK_NAMES[key as BankName].toLowerCase() === bankName) as BankName | undefined; } /** * Returns Bank Icon Object that matches to existing bank icons or default icons */ -export default function getBankIcon(bankName: string, isCard = false): BankIcon { +export default function getBankIcon(bankName: BankName, isCard = false): BankIcon { + const bankNameKey = getBankNameKey(bankName.toLowerCase()); + const bankIcon: BankIcon = { icon: isCard ? GenericBankCard : GenericBank, }; - if (bankName) { - bankIcon.icon = getAssetIcon(bankName.toLowerCase(), isCard); + if (bankNameKey && Object.keys(BANK_NAMES).includes(bankNameKey)) { + bankIcon.icon = getAssetIcon(bankNameKey, isCard); } // For default Credit Card icon the icon size should not be set. From 221f30e250f79ac22935d7a063f9206468c50709 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 23 Nov 2023 14:40:15 +0100 Subject: [PATCH 106/319] rename patch --- ....patch => react-native+0.72.4+002+ModalKeyboardFlashing.patch} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename patches/{react-native+0.72.4+004+ModalKeyboardFlashing.patch => react-native+0.72.4+002+ModalKeyboardFlashing.patch} (100%) diff --git a/patches/react-native+0.72.4+004+ModalKeyboardFlashing.patch b/patches/react-native+0.72.4+002+ModalKeyboardFlashing.patch similarity index 100% rename from patches/react-native+0.72.4+004+ModalKeyboardFlashing.patch rename to patches/react-native+0.72.4+002+ModalKeyboardFlashing.patch From a83117b9b407cd4cc2d6ec9a710d8efcfa22ea7e Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 23 Nov 2023 16:11:04 +0100 Subject: [PATCH 107/319] add BankName type --- src/components/Icon/BankIcons.ts | 11 +++++++---- src/types/onyx/AccountData.ts | 3 ++- src/types/onyx/Fund.ts | 3 ++- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/components/Icon/BankIcons.ts b/src/components/Icon/BankIcons.ts index eaf08b93ddd1..9087582b3997 100644 --- a/src/components/Icon/BankIcons.ts +++ b/src/components/Icon/BankIcons.ts @@ -37,13 +37,14 @@ const BANK_NAMES = { USAA: 'usaa', }; +type BankNameAndEmptyString = keyof typeof BANK_NAMES | ''; type BankName = keyof typeof BANK_NAMES; /** * Returns matching asset icon for bankName */ -function getAssetIcon(bankName: BankName, isCard: boolean): React.FC { +function getAssetIcon(bankName: BankNameAndEmptyString, isCard: boolean): React.FC { // Mapping bank names to their respective icon paths const iconMappings = { [BANK_NAMES.EXPENSIFY]: isCard ? require('@assets/images/cardicons/expensify-card-dark.svg') : require('@assets/images/bankicons/expensify.svg'), @@ -75,15 +76,15 @@ function getAssetIcon(bankName: BankName, isCard: boolean): React.FC { return iconModule as React.FC; } -function getBankNameKey(bankName: string): BankName | undefined { - return Object.keys(BANK_NAMES).find((key) => BANK_NAMES[key as BankName].toLowerCase() === bankName) as BankName | undefined; +function getBankNameKey(bankName: string): BankNameAndEmptyString | undefined { + return Object.keys(BANK_NAMES).find((key) => BANK_NAMES[key as BankName].toLowerCase() === bankName) as BankNameAndEmptyString | undefined; } /** * Returns Bank Icon Object that matches to existing bank icons or default icons */ -export default function getBankIcon(bankName: BankName, isCard = false): BankIcon { +export default function getBankIcon(bankName: BankNameAndEmptyString, isCard = false): BankIcon { const bankNameKey = getBankNameKey(bankName.toLowerCase()); const bankIcon: BankIcon = { @@ -106,3 +107,5 @@ export default function getBankIcon(bankName: BankName, isCard = false): BankIco return bankIcon; } + +export type {BankName}; diff --git a/src/types/onyx/AccountData.ts b/src/types/onyx/AccountData.ts index 79484e7886af..c9ffcc34c659 100644 --- a/src/types/onyx/AccountData.ts +++ b/src/types/onyx/AccountData.ts @@ -1,10 +1,11 @@ +import {BankName} from '@components/Icon/BankIcons'; import * as OnyxCommon from './OnyxCommon'; type AdditionalData = { isP2PDebitCard?: boolean; beneficialOwners?: string[]; currency?: string; - bankName?: string; + bankName?: BankName; fieldsType?: string; country?: string; }; diff --git a/src/types/onyx/Fund.ts b/src/types/onyx/Fund.ts index 4e6cbc695a8d..d1d27cce5d71 100644 --- a/src/types/onyx/Fund.ts +++ b/src/types/onyx/Fund.ts @@ -1,3 +1,4 @@ +import {BankName} from '@components/Icon/BankIcons'; import CONST from '@src/CONST'; import * as OnyxCommon from './OnyxCommon'; @@ -21,7 +22,7 @@ type AccountData = { created?: string; currency?: string; fundID?: number; - bank?: string; + bank?: BankName; }; type Fund = { From e962db8fee674697d8291804fe16de8503c0b1f7 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 23 Nov 2023 16:43:16 +0100 Subject: [PATCH 108/319] fix warning --- src/libs/Navigation/AppNavigator/AuthScreens.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.js b/src/libs/Navigation/AppNavigator/AuthScreens.js index 1a4873db0a42..50a70cd70c56 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.js +++ b/src/libs/Navigation/AppNavigator/AuthScreens.js @@ -40,7 +40,7 @@ const loadSidebarScreen = () => require('../../../pages/home/sidebar/SidebarScre const loadValidateLoginPage = () => require('../../../pages/ValidateLoginPage').default; const loadLogOutPreviousUserPage = () => require('../../../pages/LogOutPreviousUserPage').default; const loadConciergePage = () => require('../../../pages/ConciergePage').default; -const loadDesktopSignInRedirectPage = () => require('../../../pages/signin/DesktopSignInRedirectPage').default; +const LoadDesktopSignInRedirectPage = () => require('../../../pages/signin/DesktopSignInRedirectPage').default; let timezone; let currentAccountID; @@ -338,7 +338,7 @@ function AuthScreens({isUsingMemoryOnlyKeys, lastUpdateIDAppliedToClient, sessio From 1e9d3af2b81437d7458f6f3a19524351602e4017 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Thu, 23 Nov 2023 17:11:50 +0100 Subject: [PATCH 109/319] clean --- src/libs/EmojiUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index db515f7d7703..49d08fcc1b2c 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -322,7 +322,7 @@ function getAddedEmojis(currentEmojis: Emoji[], formerEmojis: Emoji[]): Emoji[] * If we're on mobile, we also add a space after the emoji granted there's no text after it. */ function replaceEmojis(text: string, preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE, lang: 'en' | 'es' = CONST.LOCALES.DEFAULT): ReplacedEmoji { - // emojisTrie importing the emoji json file on app starting and we want to avoid it + // emojisTrie importing the emoji JSON file on the app starting and we want to avoid it const emojisTrie = require('./EmojiTrie').default; const trie = emojisTrie[lang]; @@ -400,7 +400,7 @@ function replaceAndExtractEmojis(text: string, preferredSkinTone = CONST.EMOJI_D * @param [limit] - matching emojis limit */ function suggestEmojis(text: string, lang: keyof SupportedLanguage, limit = CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_SUGGESTIONS): Emoji[] | undefined { - // emojisTrie importing the emoji json file on app starting and we want to avoid it + // emojisTrie importing the emoji JSON file on the app starting and we want to avoid it const emojisTrie = require('./EmojiTrie').default; const trie = emojisTrie[lang]; From 33c787d5e260c12d0eab65aea52a94e89148bc75 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 24 Nov 2023 11:43:03 +0100 Subject: [PATCH 110/319] update types --- src/components/Icon/BankIcons.ts | 83 +++++++++++++++++++++----------- 1 file changed, 54 insertions(+), 29 deletions(-) diff --git a/src/components/Icon/BankIcons.ts b/src/components/Icon/BankIcons.ts index 9087582b3997..a1711ac24955 100644 --- a/src/components/Icon/BankIcons.ts +++ b/src/components/Icon/BankIcons.ts @@ -1,6 +1,7 @@ import {CSSProperties} from 'react'; import {ViewStyle} from 'react-native'; import {SvgProps} from 'react-native-svg'; +import {ValueOf} from 'type-fest'; import GenericBank from '@assets/images/bankicons/generic-bank-account.svg'; import GenericBankCard from '@assets/images/cardicons/generic-bank-card.svg'; import styles from '@styles/styles'; @@ -35,49 +36,73 @@ const BANK_NAMES = { TD_BANK: 'td bank', US_BANK: 'us bank', USAA: 'usaa', -}; +} as const; type BankNameAndEmptyString = keyof typeof BANK_NAMES | ''; -type BankName = keyof typeof BANK_NAMES; +type BankName = ValueOf; +type BankNameKey = keyof typeof BANK_NAMES; /** * Returns matching asset icon for bankName */ -function getAssetIcon(bankName: BankNameAndEmptyString, isCard: boolean): React.FC { +function getAssetIcon(bankNameKey: BankNameKey, isCard: boolean): React.FC { + const bankValue = BANK_NAMES[bankNameKey]; + // Mapping bank names to their respective icon paths const iconMappings = { - [BANK_NAMES.EXPENSIFY]: isCard ? require('@assets/images/cardicons/expensify-card-dark.svg') : require('@assets/images/bankicons/expensify.svg'), - [BANK_NAMES.AMERICAN_EXPRESS]: isCard ? require('@assets/images/cardicons/american-express.svg') : require('@assets/images/bankicons/american-express.svg'), - [BANK_NAMES.BANK_OF_AMERICA]: isCard ? require('@assets/images/cardicons/bank-of-america.svg') : require('@assets/images/bankicons/bank-of-america.svg'), - [BANK_NAMES.BB_T]: isCard ? require('@assets/images/cardicons/bb-t.svg') : require('@assets/images/bankicons/bb-t.svg'), - [BANK_NAMES.CAPITAL_ONE]: isCard ? require('@assets/images/cardicons/capital-one.svg') : require('@assets/images/bankicons/capital-one.svg'), - [BANK_NAMES.CHASE]: isCard ? require('@assets/images/cardicons/chase.svg') : require('@assets/images/bankicons/chase.svg'), - [BANK_NAMES.CHARLES_SCHWAB]: isCard ? require('@assets/images/cardicons/charles-schwab.svg') : require('@assets/images/bankicons/charles-schwab.svg'), - [BANK_NAMES.CITIBANK]: isCard ? require('@assets/images/cardicons/citibank.svg') : require('@assets/images/bankicons/citibank.svg'), - [BANK_NAMES.CITIZENS_BANK]: isCard ? require('@assets/images/cardicons/citizens.svg') : require('@assets/images/bankicons/citizens-bank.svg'), - [BANK_NAMES.DISCOVER]: isCard ? require('@assets/images/cardicons/discover.svg') : require('@assets/images/bankicons/discover.svg'), - [BANK_NAMES.FIDELITY]: isCard ? require('@assets/images/cardicons/fidelity.svg') : require('@assets/images/bankicons/fidelity.svg'), - [BANK_NAMES.GENERIC_BANK]: isCard ? require('@assets/images/cardicons/generic-bank-card.svg') : require('@assets/images/bankicons/generic-bank-account.svg'), - [BANK_NAMES.HUNTINGTON_BANK]: isCard ? require('@assets/images/cardicons/huntington-bank.svg') : require('@assets/images/bankicons/huntington-bank.svg'), + [BANK_NAMES.EXPENSIFY]: isCard + ? (require('@assets/images/cardicons/expensify-card-dark.svg') as React.FC) + : (require('@assets/images/bankicons/expensify.svg') as React.FC), + [BANK_NAMES.AMERICAN_EXPRESS]: isCard + ? (require('@assets/images/cardicons/american-express.svg') as React.FC) + : (require('@assets/images/bankicons/american-express.svg') as React.FC), + [BANK_NAMES.BANK_OF_AMERICA]: isCard + ? (require('@assets/images/cardicons/bank-of-america.svg') as React.FC) + : (require('@assets/images/bankicons/bank-of-america.svg') as React.FC), + [BANK_NAMES.BB_T]: isCard ? (require('@assets/images/cardicons/bb-t.svg') as React.FC) : (require('@assets/images/bankicons/bb-t.svg') as React.FC), + [BANK_NAMES.CAPITAL_ONE]: isCard + ? (require('@assets/images/cardicons/capital-one.svg') as React.FC) + : (require('@assets/images/bankicons/capital-one.svg') as React.FC), + [BANK_NAMES.CHASE]: isCard ? (require('@assets/images/cardicons/chase.svg') as React.FC) : (require('@assets/images/bankicons/chase.svg') as React.FC), + [BANK_NAMES.CHARLES_SCHWAB]: isCard + ? (require('@assets/images/cardicons/charles-schwab.svg') as React.FC) + : (require('@assets/images/bankicons/charles-schwab.svg') as React.FC), + [BANK_NAMES.CITIBANK]: isCard ? (require('@assets/images/cardicons/citibank.svg') as React.FC) : (require('@assets/images/bankicons/citibank.svg') as React.FC), + [BANK_NAMES.CITIZENS_BANK]: isCard + ? (require('@assets/images/cardicons/citizens.svg') as React.FC) + : (require('@assets/images/bankicons/citizens-bank.svg') as React.FC), + [BANK_NAMES.DISCOVER]: isCard ? (require('@assets/images/cardicons/discover.svg') as React.FC) : (require('@assets/images/bankicons/discover.svg') as React.FC), + [BANK_NAMES.FIDELITY]: isCard ? (require('@assets/images/cardicons/fidelity.svg') as React.FC) : (require('@assets/images/bankicons/fidelity.svg') as React.FC), + [BANK_NAMES.GENERIC_BANK]: isCard + ? (require('@assets/images/cardicons/generic-bank-card.svg') as React.FC) + : (require('@assets/images/bankicons/generic-bank-account.svg') as React.FC), + [BANK_NAMES.HUNTINGTON_BANK]: isCard + ? (require('@assets/images/cardicons/huntington-bank.svg') as React.FC) + : (require('@assets/images/bankicons/huntington-bank.svg') as React.FC), [BANK_NAMES.NAVY_FEDERAL_CREDIT_UNION]: isCard - ? require('@assets/images/cardicons/navy-federal-credit-union.svg') - : require('@assets/images/bankicons/navy-federal-credit-union.svg'), - [BANK_NAMES.PNC]: isCard ? require('@assets/images/cardicons/pnc.svg') : require('@assets/images/bankicons/pnc.svg'), - [BANK_NAMES.REGIONS_BANK]: isCard ? require('@assets/images/cardicons/regions-bank.svg') : require('@assets/images/bankicons/regions-bank.svg'), - [BANK_NAMES.SUNTRUST]: isCard ? require('@assets/images/cardicons/suntrust.svg') : require('@assets/images/bankicons/suntrust.svg'), - [BANK_NAMES.TD_BANK]: isCard ? require('@assets/images/cardicons/td-bank.svg') : require('@assets/images/bankicons/td-bank.svg'), - [BANK_NAMES.US_BANK]: isCard ? require('@assets/images/cardicons/us-bank.svg') : require('@assets/images/bankicons/us-bank.svg'), - [BANK_NAMES.USAA]: isCard ? require('@assets/images/cardicons/usaa.svg') : require('@assets/images/bankicons/usaa.svg'), - }; + ? (require('@assets/images/cardicons/navy-federal-credit-union.svg') as React.FC) + : (require('@assets/images/bankicons/navy-federal-credit-union.svg') as React.FC), + [BANK_NAMES.PNC]: isCard ? (require('@assets/images/cardicons/pnc.svg') as React.FC) : (require('@assets/images/bankicons/pnc.svg') as React.FC), + [BANK_NAMES.REGIONS_BANK]: isCard + ? (require('@assets/images/cardicons/regions-bank.svg') as React.FC) + : (require('@assets/images/bankicons/regions-bank.svg') as React.FC), + [BANK_NAMES.SUNTRUST]: isCard ? (require('@assets/images/cardicons/suntrust.svg') as React.FC) : (require('@assets/images/bankicons/suntrust.svg') as React.FC), + [BANK_NAMES.TD_BANK]: isCard ? (require('@assets/images/cardicons/td-bank.svg') as React.FC) : (require('@assets/images/bankicons/td-bank.svg') as React.FC), + [BANK_NAMES.US_BANK]: isCard ? (require('@assets/images/cardicons/us-bank.svg') as React.FC) : (require('@assets/images/bankicons/us-bank.svg') as React.FC), + [BANK_NAMES.USAA]: isCard ? (require('@assets/images/cardicons/usaa.svg') as React.FC) : (require('@assets/images/bankicons/usaa.svg') as React.FC), + } as const; // Fallback to generic bank/card icon - const iconModule = iconMappings[bankName] || (isCard ? '@assets/images/cardicons/generic-bank-card.svg' : '@assets/images/bankicons/generic-bank-account.svg'); - return iconModule as React.FC; + const iconModule = + iconMappings[bankValue] || + (isCard ? (require('@assets/images/cardicons/generic-bank-card.svg') as React.FC) : (require('@assets/images/bankicons/generic-bank-account.svg') as React.FC)); + return iconModule; } -function getBankNameKey(bankName: string): BankNameAndEmptyString | undefined { - return Object.keys(BANK_NAMES).find((key) => BANK_NAMES[key as BankName].toLowerCase() === bankName) as BankNameAndEmptyString | undefined; +function getBankNameKey(bankName: string): BankNameKey | '' { + const bank = Object.entries(BANK_NAMES).find(([, value]) => value?.toLowerCase() === bankName); + return (bank?.[0] as BankNameKey) ?? ''; } /** From f72c49c2432d362ec9a96e3183fda5350dae3de0 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 24 Nov 2023 11:47:57 +0100 Subject: [PATCH 111/319] create Bank type --- src/types/onyx/Bank.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/types/onyx/Bank.ts diff --git a/src/types/onyx/Bank.ts b/src/types/onyx/Bank.ts new file mode 100644 index 000000000000..a490198f2650 --- /dev/null +++ b/src/types/onyx/Bank.ts @@ -0,0 +1,19 @@ +import {CSSProperties} from 'react'; +import {ViewStyle} from 'react-native'; +import {SvgProps} from 'react-native-svg'; +import {ValueOf} from 'type-fest'; +import CONST from '@src/CONST'; + +type BankIcon = { + icon: React.FC; + iconSize?: number; + iconHeight?: number; + iconWidth?: number; + iconStyles?: Array; +}; + +type BankNameAndEmptyString = keyof typeof CONST.BANK_NAMES | ''; +type BankName = ValueOf; +type BankNameKey = keyof typeof CONST.BANK_NAMES; + +export type {BankIcon, BankNameAndEmptyString, BankName, BankNameKey}; From 61a67018d1066a8fa8480e25942e6efc9fc020a2 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 24 Nov 2023 11:48:17 +0100 Subject: [PATCH 112/319] clean BankIcon component --- src/components/Icon/BankIcons.ts | 98 ++++++++++++-------------------- 1 file changed, 37 insertions(+), 61 deletions(-) diff --git a/src/components/Icon/BankIcons.ts b/src/components/Icon/BankIcons.ts index a1711ac24955..9d843856c4d1 100644 --- a/src/components/Icon/BankIcons.ts +++ b/src/components/Icon/BankIcons.ts @@ -1,96 +1,72 @@ -import {CSSProperties} from 'react'; -import {ViewStyle} from 'react-native'; import {SvgProps} from 'react-native-svg'; -import {ValueOf} from 'type-fest'; import GenericBank from '@assets/images/bankicons/generic-bank-account.svg'; import GenericBankCard from '@assets/images/cardicons/generic-bank-card.svg'; import styles from '@styles/styles'; import variables from '@styles/variables'; - -type BankIcon = { - icon: React.FC; - iconSize?: number; - iconHeight?: number; - iconWidth?: number; - iconStyles?: Array; -}; - -const BANK_NAMES = { - EXPENSIFY: 'expensify', - AMERICAN_EXPRESS: 'americanexpress', - BANK_OF_AMERICA: 'bank of america', - BB_T: 'bbt', - CAPITAL_ONE: 'capital one', - CHASE: 'chase', - CHARLES_SCHWAB: 'charles schwab', - CITIBANK: 'citibank', - CITIZENS_BANK: 'citizens bank', - DISCOVER: 'discover', - FIDELITY: 'fidelity', - GENERIC_BANK: 'generic bank', - HUNTINGTON_BANK: 'huntington bank', - NAVY_FEDERAL_CREDIT_UNION: 'navy federal credit union', - PNC: 'pnc', - REGIONS_BANK: 'regions bank', - SUNTRUST: 'suntrust', - TD_BANK: 'td bank', - US_BANK: 'us bank', - USAA: 'usaa', -} as const; - -type BankNameAndEmptyString = keyof typeof BANK_NAMES | ''; -type BankName = ValueOf; -type BankNameKey = keyof typeof BANK_NAMES; +import CONST from '@src/CONST'; +import {BankIcon, BankName, BankNameAndEmptyString, BankNameKey} from '@src/types/onyx/Bank'; /** * Returns matching asset icon for bankName */ function getAssetIcon(bankNameKey: BankNameKey, isCard: boolean): React.FC { - const bankValue = BANK_NAMES[bankNameKey]; + const bankValue = CONST.BANK_NAMES[bankNameKey]; // Mapping bank names to their respective icon paths const iconMappings = { - [BANK_NAMES.EXPENSIFY]: isCard + [CONST.BANK_NAMES.EXPENSIFY]: isCard ? (require('@assets/images/cardicons/expensify-card-dark.svg') as React.FC) : (require('@assets/images/bankicons/expensify.svg') as React.FC), - [BANK_NAMES.AMERICAN_EXPRESS]: isCard + [CONST.BANK_NAMES.AMERICAN_EXPRESS]: isCard ? (require('@assets/images/cardicons/american-express.svg') as React.FC) : (require('@assets/images/bankicons/american-express.svg') as React.FC), - [BANK_NAMES.BANK_OF_AMERICA]: isCard + [CONST.BANK_NAMES.BANK_OF_AMERICA]: isCard ? (require('@assets/images/cardicons/bank-of-america.svg') as React.FC) : (require('@assets/images/bankicons/bank-of-america.svg') as React.FC), - [BANK_NAMES.BB_T]: isCard ? (require('@assets/images/cardicons/bb-t.svg') as React.FC) : (require('@assets/images/bankicons/bb-t.svg') as React.FC), - [BANK_NAMES.CAPITAL_ONE]: isCard + [CONST.BANK_NAMES.BB_T]: isCard ? (require('@assets/images/cardicons/bb-t.svg') as React.FC) : (require('@assets/images/bankicons/bb-t.svg') as React.FC), + [CONST.BANK_NAMES.CAPITAL_ONE]: isCard ? (require('@assets/images/cardicons/capital-one.svg') as React.FC) : (require('@assets/images/bankicons/capital-one.svg') as React.FC), - [BANK_NAMES.CHASE]: isCard ? (require('@assets/images/cardicons/chase.svg') as React.FC) : (require('@assets/images/bankicons/chase.svg') as React.FC), - [BANK_NAMES.CHARLES_SCHWAB]: isCard + [CONST.BANK_NAMES.CHASE]: isCard ? (require('@assets/images/cardicons/chase.svg') as React.FC) : (require('@assets/images/bankicons/chase.svg') as React.FC), + [CONST.BANK_NAMES.CHARLES_SCHWAB]: isCard ? (require('@assets/images/cardicons/charles-schwab.svg') as React.FC) : (require('@assets/images/bankicons/charles-schwab.svg') as React.FC), - [BANK_NAMES.CITIBANK]: isCard ? (require('@assets/images/cardicons/citibank.svg') as React.FC) : (require('@assets/images/bankicons/citibank.svg') as React.FC), - [BANK_NAMES.CITIZENS_BANK]: isCard + [CONST.BANK_NAMES.CITIBANK]: isCard + ? (require('@assets/images/cardicons/citibank.svg') as React.FC) + : (require('@assets/images/bankicons/citibank.svg') as React.FC), + [CONST.BANK_NAMES.CITIZENS_BANK]: isCard ? (require('@assets/images/cardicons/citizens.svg') as React.FC) : (require('@assets/images/bankicons/citizens-bank.svg') as React.FC), - [BANK_NAMES.DISCOVER]: isCard ? (require('@assets/images/cardicons/discover.svg') as React.FC) : (require('@assets/images/bankicons/discover.svg') as React.FC), - [BANK_NAMES.FIDELITY]: isCard ? (require('@assets/images/cardicons/fidelity.svg') as React.FC) : (require('@assets/images/bankicons/fidelity.svg') as React.FC), - [BANK_NAMES.GENERIC_BANK]: isCard + [CONST.BANK_NAMES.DISCOVER]: isCard + ? (require('@assets/images/cardicons/discover.svg') as React.FC) + : (require('@assets/images/bankicons/discover.svg') as React.FC), + [CONST.BANK_NAMES.FIDELITY]: isCard + ? (require('@assets/images/cardicons/fidelity.svg') as React.FC) + : (require('@assets/images/bankicons/fidelity.svg') as React.FC), + [CONST.BANK_NAMES.GENERIC_BANK]: isCard ? (require('@assets/images/cardicons/generic-bank-card.svg') as React.FC) : (require('@assets/images/bankicons/generic-bank-account.svg') as React.FC), - [BANK_NAMES.HUNTINGTON_BANK]: isCard + [CONST.BANK_NAMES.HUNTINGTON_BANK]: isCard ? (require('@assets/images/cardicons/huntington-bank.svg') as React.FC) : (require('@assets/images/bankicons/huntington-bank.svg') as React.FC), - [BANK_NAMES.NAVY_FEDERAL_CREDIT_UNION]: isCard + [CONST.BANK_NAMES.NAVY_FEDERAL_CREDIT_UNION]: isCard ? (require('@assets/images/cardicons/navy-federal-credit-union.svg') as React.FC) : (require('@assets/images/bankicons/navy-federal-credit-union.svg') as React.FC), - [BANK_NAMES.PNC]: isCard ? (require('@assets/images/cardicons/pnc.svg') as React.FC) : (require('@assets/images/bankicons/pnc.svg') as React.FC), - [BANK_NAMES.REGIONS_BANK]: isCard + [CONST.BANK_NAMES.PNC]: isCard ? (require('@assets/images/cardicons/pnc.svg') as React.FC) : (require('@assets/images/bankicons/pnc.svg') as React.FC), + [CONST.BANK_NAMES.REGIONS_BANK]: isCard ? (require('@assets/images/cardicons/regions-bank.svg') as React.FC) : (require('@assets/images/bankicons/regions-bank.svg') as React.FC), - [BANK_NAMES.SUNTRUST]: isCard ? (require('@assets/images/cardicons/suntrust.svg') as React.FC) : (require('@assets/images/bankicons/suntrust.svg') as React.FC), - [BANK_NAMES.TD_BANK]: isCard ? (require('@assets/images/cardicons/td-bank.svg') as React.FC) : (require('@assets/images/bankicons/td-bank.svg') as React.FC), - [BANK_NAMES.US_BANK]: isCard ? (require('@assets/images/cardicons/us-bank.svg') as React.FC) : (require('@assets/images/bankicons/us-bank.svg') as React.FC), - [BANK_NAMES.USAA]: isCard ? (require('@assets/images/cardicons/usaa.svg') as React.FC) : (require('@assets/images/bankicons/usaa.svg') as React.FC), + [CONST.BANK_NAMES.SUNTRUST]: isCard + ? (require('@assets/images/cardicons/suntrust.svg') as React.FC) + : (require('@assets/images/bankicons/suntrust.svg') as React.FC), + [CONST.BANK_NAMES.TD_BANK]: isCard + ? (require('@assets/images/cardicons/td-bank.svg') as React.FC) + : (require('@assets/images/bankicons/td-bank.svg') as React.FC), + [CONST.BANK_NAMES.US_BANK]: isCard + ? (require('@assets/images/cardicons/us-bank.svg') as React.FC) + : (require('@assets/images/bankicons/us-bank.svg') as React.FC), + [CONST.BANK_NAMES.USAA]: isCard ? (require('@assets/images/cardicons/usaa.svg') as React.FC) : (require('@assets/images/bankicons/usaa.svg') as React.FC), } as const; // Fallback to generic bank/card icon @@ -101,7 +77,7 @@ function getAssetIcon(bankNameKey: BankNameKey, isCard: boolean): React.FC value?.toLowerCase() === bankName); + const bank = Object.entries(CONST.BANK_NAMES).find(([, value]) => value?.toLowerCase() === bankName); return (bank?.[0] as BankNameKey) ?? ''; } @@ -116,7 +92,7 @@ export default function getBankIcon(bankName: BankNameAndEmptyString, isCard = f icon: isCard ? GenericBankCard : GenericBank, }; - if (bankNameKey && Object.keys(BANK_NAMES).includes(bankNameKey)) { + if (bankNameKey && Object.keys(CONST.BANK_NAMES).includes(bankNameKey)) { bankIcon.icon = getAssetIcon(bankNameKey, isCard); } From b87507a4a40562e00b8d8af3d0dd339f3efc6afd Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 24 Nov 2023 11:48:32 +0100 Subject: [PATCH 113/319] add BANK_NAMES const --- src/CONST.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/CONST.ts b/src/CONST.ts index 436ac4ebbc31..599fabaeac9f 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -2914,6 +2914,29 @@ const CONST = { PERFORMANCE_TESTS: { RUNS: 20, }, + + BANK_NAMES: { + EXPENSIFY: 'expensify', + AMERICAN_EXPRESS: 'americanexpress', + BANK_OF_AMERICA: 'bank of america', + BB_T: 'bbt', + CAPITAL_ONE: 'capital one', + CHASE: 'chase', + CHARLES_SCHWAB: 'charles schwab', + CITIBANK: 'citibank', + CITIZENS_BANK: 'citizens bank', + DISCOVER: 'discover', + FIDELITY: 'fidelity', + GENERIC_BANK: 'generic bank', + HUNTINGTON_BANK: 'huntington bank', + NAVY_FEDERAL_CREDIT_UNION: 'navy federal credit union', + PNC: 'pnc', + REGIONS_BANK: 'regions bank', + SUNTRUST: 'suntrust', + TD_BANK: 'td bank', + US_BANK: 'us bank', + USAA: 'usaa', + } } as const; export default CONST; From 9f2c4efaf12be8e9652eeb48cc39738a784ff360 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 24 Nov 2023 12:03:01 +0100 Subject: [PATCH 114/319] spaces --- src/CONST.ts | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 599fabaeac9f..4913869af838 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -2916,27 +2916,27 @@ const CONST = { }, BANK_NAMES: { - EXPENSIFY: 'expensify', - AMERICAN_EXPRESS: 'americanexpress', - BANK_OF_AMERICA: 'bank of america', - BB_T: 'bbt', - CAPITAL_ONE: 'capital one', - CHASE: 'chase', - CHARLES_SCHWAB: 'charles schwab', - CITIBANK: 'citibank', - CITIZENS_BANK: 'citizens bank', - DISCOVER: 'discover', - FIDELITY: 'fidelity', - GENERIC_BANK: 'generic bank', - HUNTINGTON_BANK: 'huntington bank', - NAVY_FEDERAL_CREDIT_UNION: 'navy federal credit union', - PNC: 'pnc', - REGIONS_BANK: 'regions bank', - SUNTRUST: 'suntrust', - TD_BANK: 'td bank', - US_BANK: 'us bank', - USAA: 'usaa', - } + EXPENSIFY: 'expensify', + AMERICAN_EXPRESS: 'americanexpress', + BANK_OF_AMERICA: 'bank of america', + BB_T: 'bbt', + CAPITAL_ONE: 'capital one', + CHASE: 'chase', + CHARLES_SCHWAB: 'charles schwab', + CITIBANK: 'citibank', + CITIZENS_BANK: 'citizens bank', + DISCOVER: 'discover', + FIDELITY: 'fidelity', + GENERIC_BANK: 'generic bank', + HUNTINGTON_BANK: 'huntington bank', + NAVY_FEDERAL_CREDIT_UNION: 'navy federal credit union', + PNC: 'pnc', + REGIONS_BANK: 'regions bank', + SUNTRUST: 'suntrust', + TD_BANK: 'td bank', + US_BANK: 'us bank', + USAA: 'usaa', + }, } as const; export default CONST; From 11083015f4abd1574846c44b49a1385c50946c81 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Fri, 24 Nov 2023 12:38:11 +0100 Subject: [PATCH 115/319] prettier --- src/components/Icon/BankIcons.ts | 4 ++-- src/types/onyx/AccountData.ts | 2 +- src/types/onyx/Bank.ts | 3 +-- src/types/onyx/Fund.ts | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/Icon/BankIcons.ts b/src/components/Icon/BankIcons.ts index 9d843856c4d1..0afd4e304680 100644 --- a/src/components/Icon/BankIcons.ts +++ b/src/components/Icon/BankIcons.ts @@ -4,7 +4,7 @@ import GenericBankCard from '@assets/images/cardicons/generic-bank-card.svg'; import styles from '@styles/styles'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -import {BankIcon, BankName, BankNameAndEmptyString, BankNameKey} from '@src/types/onyx/Bank'; +import {BankIcon, BankName, BankNameKey} from '@src/types/onyx/Bank'; /** * Returns matching asset icon for bankName @@ -85,7 +85,7 @@ function getBankNameKey(bankName: string): BankNameKey | '' { * Returns Bank Icon Object that matches to existing bank icons or default icons */ -export default function getBankIcon(bankName: BankNameAndEmptyString, isCard = false): BankIcon { +export default function getBankIcon(bankName: BankName | '', isCard = false): BankIcon { const bankNameKey = getBankNameKey(bankName.toLowerCase()); const bankIcon: BankIcon = { diff --git a/src/types/onyx/AccountData.ts b/src/types/onyx/AccountData.ts index c9ffcc34c659..601e82cde836 100644 --- a/src/types/onyx/AccountData.ts +++ b/src/types/onyx/AccountData.ts @@ -1,4 +1,4 @@ -import {BankName} from '@components/Icon/BankIcons'; +import {BankName} from './Bank'; import * as OnyxCommon from './OnyxCommon'; type AdditionalData = { diff --git a/src/types/onyx/Bank.ts b/src/types/onyx/Bank.ts index a490198f2650..b6312e039079 100644 --- a/src/types/onyx/Bank.ts +++ b/src/types/onyx/Bank.ts @@ -12,8 +12,7 @@ type BankIcon = { iconStyles?: Array; }; -type BankNameAndEmptyString = keyof typeof CONST.BANK_NAMES | ''; type BankName = ValueOf; type BankNameKey = keyof typeof CONST.BANK_NAMES; -export type {BankIcon, BankNameAndEmptyString, BankName, BankNameKey}; +export type {BankIcon, BankName, BankNameKey}; diff --git a/src/types/onyx/Fund.ts b/src/types/onyx/Fund.ts index d1d27cce5d71..82c15c5089d7 100644 --- a/src/types/onyx/Fund.ts +++ b/src/types/onyx/Fund.ts @@ -1,5 +1,5 @@ -import {BankName} from '@components/Icon/BankIcons'; import CONST from '@src/CONST'; +import {BankName} from './Bank'; import * as OnyxCommon from './OnyxCommon'; type AdditionalData = { From 0129ddbb42f1e0fbf388ae8c43ead616f81e73fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Fri, 24 Nov 2023 13:54:01 +0100 Subject: [PATCH 116/319] change generic type to HTMLElement --- src/components/Hoverable/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Hoverable/index.tsx b/src/components/Hoverable/index.tsx index dc32c01ce3ab..d6b81e102805 100644 --- a/src/components/Hoverable/index.tsx +++ b/src/components/Hoverable/index.tsx @@ -8,7 +8,7 @@ import HoverableProps from './types'; * because nesting Pressables causes issues where the hovered state of the child cannot be easily propagated to the * parent. https://github.com/necolas/react-native-web/issues/1875 */ -function Hoverable({disabled, ...props}: HoverableProps, ref: Ref) { +function Hoverable({disabled, ...props}: HoverableProps, ref: Ref) { // If Hoverable is disabled, just render the child without additional logic or event listeners. // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing if (disabled || !hasHoverSupport()) { From 6e644a796716db20befee6a83df90e00148aaa47 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Fri, 24 Nov 2023 16:23:37 +0100 Subject: [PATCH 117/319] [TS migration] Migrate 'WalletStatementModal' component --- .../WalletStatementModalPropTypes.js | 21 ----------- .../{index.native.js => index.native.tsx} | 26 ++++++-------- .../{index.js => index.tsx} | 35 +++++++------------ src/components/WalletStatementModal/types.ts | 14 ++++++++ 4 files changed, 37 insertions(+), 59 deletions(-) delete mode 100644 src/components/WalletStatementModal/WalletStatementModalPropTypes.js rename src/components/WalletStatementModal/{index.native.js => index.native.tsx} (74%) rename src/components/WalletStatementModal/{index.js => index.tsx} (64%) create mode 100644 src/components/WalletStatementModal/types.ts diff --git a/src/components/WalletStatementModal/WalletStatementModalPropTypes.js b/src/components/WalletStatementModal/WalletStatementModalPropTypes.js deleted file mode 100644 index 06a2248151f6..000000000000 --- a/src/components/WalletStatementModal/WalletStatementModalPropTypes.js +++ /dev/null @@ -1,21 +0,0 @@ -import PropTypes from 'prop-types'; - -const walletStatementPropTypes = { - /* Onyx Props */ - /** Session info for the currently logged in user. */ - session: PropTypes.shape({ - /** Currently logged in user authToken */ - authToken: PropTypes.string, - }), - - /** URL for oldDot (expensify.com) statements page to display */ - statementPageURL: PropTypes.string, -}; - -const walletStatementDefaultProps = { - session: { - authToken: null, - }, -}; - -export {walletStatementPropTypes, walletStatementDefaultProps}; diff --git a/src/components/WalletStatementModal/index.native.js b/src/components/WalletStatementModal/index.native.tsx similarity index 74% rename from src/components/WalletStatementModal/index.native.js rename to src/components/WalletStatementModal/index.native.tsx index 9c2aea8e8ec4..896861e7898f 100644 --- a/src/components/WalletStatementModal/index.native.js +++ b/src/components/WalletStatementModal/index.native.tsx @@ -1,31 +1,29 @@ -import lodashGet from 'lodash/get'; import React, {useCallback, useRef} from 'react'; import {withOnyx} from 'react-native-onyx'; -import {WebView} from 'react-native-webview'; -import _ from 'underscore'; +import {WebView, WebViewNavigation} from 'react-native-webview'; +import {ValueOf} from 'type-fest'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import Navigation from '@libs/Navigation/Navigation'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import {walletStatementDefaultProps, walletStatementPropTypes} from './WalletStatementModalPropTypes'; +import type {WalletStatementOnyxProps, WalletStatementProps} from './types'; + +type WebViewNavigationEvent = WebViewNavigation & {type?: ValueOf}; const IOU_ROUTES = [ROUTES.IOU_REQUEST, ROUTES.IOU_SEND]; const renderLoading = () => ; -function WalletStatementModal({statementPageURL, session}) { - const webViewRef = useRef(); - const authToken = lodashGet(session, 'authToken', null); +function WalletStatementModal({statementPageURL, session}: WalletStatementProps) { + const webViewRef = useRef(null); + const authToken = session?.authToken ?? null; /** * Handles in-app navigation for webview links - * - * @param {String} params.type - * @param {String} params.url */ const handleNavigationStateChange = useCallback( - ({type, url}) => { + ({type, url}: WebViewNavigationEvent) => { if (!webViewRef.current || (type !== CONST.WALLET.WEB_MESSAGE_TYPE.STATEMENT && type !== CONST.WALLET.WEB_MESSAGE_TYPE.CONCIERGE)) { return; } @@ -36,7 +34,7 @@ function WalletStatementModal({statementPageURL, session}) { } if (type === CONST.WALLET.WEB_MESSAGE_TYPE.STATEMENT && url) { - const iouRoute = _.find(IOU_ROUTES, (item) => url.includes(item)); + const iouRoute = IOU_ROUTES.find((item) => url.includes(item)); if (iouRoute) { webViewRef.current.stopLoading(); @@ -66,10 +64,8 @@ function WalletStatementModal({statementPageURL, session}) { } WalletStatementModal.displayName = 'WalletStatementModal'; -WalletStatementModal.propTypes = walletStatementPropTypes; -WalletStatementModal.defaultProps = walletStatementDefaultProps; -export default withOnyx({ +export default withOnyx({ session: { key: ONYXKEYS.SESSION, }, diff --git a/src/components/WalletStatementModal/index.js b/src/components/WalletStatementModal/index.tsx similarity index 64% rename from src/components/WalletStatementModal/index.js rename to src/components/WalletStatementModal/index.tsx index 8ce4ee01ed30..0f1d1804df60 100644 --- a/src/components/WalletStatementModal/index.js +++ b/src/components/WalletStatementModal/index.tsx @@ -1,31 +1,25 @@ -import lodashGet from 'lodash/get'; import React, {useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; -import withLocalize from '@components/withLocalize'; -import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import useThemeStyles from '@styles/useThemeStyles'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import {walletStatementDefaultProps, walletStatementPropTypes} from './WalletStatementModalPropTypes'; +import type {WalletStatementOnyxProps, WalletStatementProps} from './types'; -function WalletStatementModal({statementPageURL, session}) { +function WalletStatementModal({statementPageURL, session}: WalletStatementProps) { const styles = useThemeStyles(); const [isLoading, setIsLoading] = useState(true); - const authToken = lodashGet(session, 'authToken', null); + const authToken = session?.authToken ?? null; /** * Handles in-app navigation for iframe links - * - * @param {MessageEvent} event */ - const navigate = (event) => { - if (!event.data || !event.data.type || (event.data.type !== CONST.WALLET.WEB_MESSAGE_TYPE.STATEMENT && event.data.type !== CONST.WALLET.WEB_MESSAGE_TYPE.CONCIERGE)) { + const navigate = (event: MessageEvent) => { + if (!event.data?.type || (event.data.type !== CONST.WALLET.WEB_MESSAGE_TYPE.STATEMENT && event.data.type !== CONST.WALLET.WEB_MESSAGE_TYPE.CONCIERGE)) { return; } @@ -35,7 +29,7 @@ function WalletStatementModal({statementPageURL, session}) { if (event.data.type === CONST.WALLET.WEB_MESSAGE_TYPE.STATEMENT && event.data.url) { const iouRoutes = [ROUTES.IOU_REQUEST, ROUTES.IOU_SEND]; - const navigateToIOURoute = _.find(iouRoutes, (iouRoute) => event.data.url.includes(iouRoute)); + const navigateToIOURoute = iouRoutes.find((iouRoute) => event.data.url.includes(iouRoute)); if (navigateToIOURoute) { Navigation.navigate(navigateToIOURoute); } @@ -51,7 +45,7 @@ function WalletStatementModal({statementPageURL, session}) { title="Statements" height="100%" width="100%" - seamless="seamless" + seamless frameBorder="0" onLoad={() => { setIsLoading(false); @@ -66,15 +60,10 @@ function WalletStatementModal({statementPageURL, session}) { ); } -WalletStatementModal.propTypes = walletStatementPropTypes; -WalletStatementModal.defaultProps = walletStatementDefaultProps; WalletStatementModal.displayName = 'WalletStatementModal'; -export default compose( - withLocalize, - withOnyx({ - session: { - key: ONYXKEYS.SESSION, - }, - }), -)(WalletStatementModal); +export default withOnyx({ + session: { + key: ONYXKEYS.SESSION, + }, +})(WalletStatementModal); diff --git a/src/components/WalletStatementModal/types.ts b/src/components/WalletStatementModal/types.ts new file mode 100644 index 000000000000..bb926b8250ae --- /dev/null +++ b/src/components/WalletStatementModal/types.ts @@ -0,0 +1,14 @@ +import {OnyxEntry} from 'react-native-onyx/lib/types'; +import type {Session} from '@src/types/onyx'; + +type WalletStatementOnyxProps = { + /** Session info for the currently logged in user. */ + session: OnyxEntry; +}; + +type WalletStatementProps = WalletStatementOnyxProps & { + /** URL for oldDot (expensify.com) statements page to display */ + statementPageURL: string; +}; + +export type {WalletStatementProps, WalletStatementOnyxProps}; From 3a5cbacf68a2cbbc9f7922ed999ef1ae1732e797 Mon Sep 17 00:00:00 2001 From: situchan Date: Sun, 26 Nov 2023 05:03:34 +0600 Subject: [PATCH 118/319] update transaction comment prop type --- src/components/transactionPropTypes.js | 31 ++++++++++++++------------ 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/components/transactionPropTypes.js b/src/components/transactionPropTypes.js index 049fe60630e5..a26cf9a76cf5 100644 --- a/src/components/transactionPropTypes.js +++ b/src/components/transactionPropTypes.js @@ -31,25 +31,28 @@ export default PropTypes.shape({ modifiedMerchant: PropTypes.string, /** The comment object on the transaction */ - comment: PropTypes.shape({ - /** The text of the comment */ - comment: PropTypes.string, + comment: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.shape({ + /** The text of the comment */ + comment: PropTypes.string, - /** The waypoints defining the distance request */ - waypoints: PropTypes.shape({ - /** The latitude of the waypoint */ - lat: PropTypes.number, + /** The waypoints defining the distance request */ + waypoints: PropTypes.shape({ + /** The latitude of the waypoint */ + lat: PropTypes.number, - /** The longitude of the waypoint */ - lng: PropTypes.number, + /** The longitude of the waypoint */ + lng: PropTypes.number, - /** The address of the waypoint */ - address: PropTypes.string, + /** The address of the waypoint */ + address: PropTypes.string, - /** The name of the waypoint */ - name: PropTypes.string, + /** The name of the waypoint */ + name: PropTypes.string, + }), }), - }), + ]), /** The type of transaction */ type: PropTypes.oneOf(_.values(CONST.TRANSACTION.TYPE)), From a2bf1a5ff5329ef2341422e659c2646a7e543574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Sun, 26 Nov 2023 10:08:46 +0100 Subject: [PATCH 119/319] simplify unsetHoveredIfOutside listener --- src/components/Hoverable/ActiveHoverable.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/Hoverable/ActiveHoverable.tsx b/src/components/Hoverable/ActiveHoverable.tsx index 741e012f4601..dc56d8e74f59 100644 --- a/src/components/Hoverable/ActiveHoverable.tsx +++ b/src/components/Hoverable/ActiveHoverable.tsx @@ -61,6 +61,10 @@ function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, children}: }, [shouldHandleScroll]); useEffect(() => { + // Do not mount a listener if the component is not hovered + if (!isHovered) { + return; + } /** * Checks the hover state of a component and updates it based on the event target. * This is necessary to handle cases where the hover state might get stuck due to an unreliable mouseleave trigger, @@ -68,11 +72,7 @@ function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, children}: * @param event The hover event object. */ const unsetHoveredIfOutside = (event: MouseEvent) => { - if (!ref.current || !isHovered) { - return; - } - - if (ref.current.contains(event.target as Node)) { + if (!ref.current || ref.current.contains(event.target as Node)) { return; } From 745f12ea1f3e2282dcc1ab70ecaa2413b98d2b6a Mon Sep 17 00:00:00 2001 From: VH Date: Sun, 26 Nov 2023 16:40:54 +0700 Subject: [PATCH 120/319] Normalize CRLF before compare description --- src/libs/StringUtils.ts | 11 ++++++++++- src/pages/tasks/TaskDescriptionPage.js | 4 +++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/libs/StringUtils.ts b/src/libs/StringUtils.ts index f1dd5a06e828..b80bb4be2c42 100644 --- a/src/libs/StringUtils.ts +++ b/src/libs/StringUtils.ts @@ -63,4 +63,13 @@ function removeInvisibleCharacters(value: string): string { return result.trim(); } -export default {sanitizeString, isEmptyString, removeInvisibleCharacters}; +/** + * Replace all CRLF with LF + * @param value - The input string + * @returns The string with all CRLF replaced with LF + */ +function normalizeCRLF(value: string | null): string { + return value?.replace(/\r\n/g, '\n'); +} + +export default {sanitizeString, isEmptyString, removeInvisibleCharacters, normalizeCRLF}; diff --git a/src/pages/tasks/TaskDescriptionPage.js b/src/pages/tasks/TaskDescriptionPage.js index c123d3c5b98c..6901f2562ee3 100644 --- a/src/pages/tasks/TaskDescriptionPage.js +++ b/src/pages/tasks/TaskDescriptionPage.js @@ -15,6 +15,7 @@ import * as Browser from '@libs/Browser'; import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; +import StringUtils from '@libs/StringUtils'; import updateMultilineInputRange from '@libs/UpdateMultilineInputRange'; import withReportOrNotFound from '@pages/home/report/withReportOrNotFound'; import reportPropTypes from '@pages/reportPropTypes'; @@ -42,7 +43,8 @@ function TaskDescriptionPage(props) { const submit = useCallback( (values) => { - if (values.description !== props.report.description) { + // props.report.description might contain CRLF from the server + if (StringUtils.normalizeCRLF(values.description) !== StringUtils.normalizeCRLF(props.report.description)) { // Set the description of the report in the store and then call EditTask API // to update the description of the report on the server Task.editTask(props.report, {description: values.description}); From bfe61c247ef35e8f9e9571304a33b2555ad534c6 Mon Sep 17 00:00:00 2001 From: VH Date: Sun, 26 Nov 2023 16:45:11 +0700 Subject: [PATCH 121/319] Fix typescript --- src/libs/StringUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/StringUtils.ts b/src/libs/StringUtils.ts index b80bb4be2c42..2fb918c7a233 100644 --- a/src/libs/StringUtils.ts +++ b/src/libs/StringUtils.ts @@ -68,7 +68,7 @@ function removeInvisibleCharacters(value: string): string { * @param value - The input string * @returns The string with all CRLF replaced with LF */ -function normalizeCRLF(value: string | null): string { +function normalizeCRLF(value?: string): string | undefined { return value?.replace(/\r\n/g, '\n'); } From f185e0cd52483728434c3b8b808af3ba8e42613a Mon Sep 17 00:00:00 2001 From: maddylewis <38016013+maddylewis@users.noreply.github.com> Date: Sun, 26 Nov 2023 11:33:02 -0500 Subject: [PATCH 122/319] Delete docs/articles/expensify-classic/billing-and-subscriptions/Payment-Card.md this file isn't needed - delting --- .../billing-and-subscriptions/Payment-Card.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 docs/articles/expensify-classic/billing-and-subscriptions/Payment-Card.md diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Payment-Card.md b/docs/articles/expensify-classic/billing-and-subscriptions/Payment-Card.md deleted file mode 100644 index 41a1fb96f56f..000000000000 --- a/docs/articles/expensify-classic/billing-and-subscriptions/Payment-Card.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Payment Card -description: Payment Card ---- -## Resource Coming Soon! From 06d785c661a8400aeab33943da4b9d8c09bbd82a Mon Sep 17 00:00:00 2001 From: maddylewis <38016013+maddylewis@users.noreply.github.com> Date: Sun, 26 Nov 2023 11:35:08 -0500 Subject: [PATCH 123/319] Delete docs/articles/new-expensify/workspace-and-domain-settings/Coming-Soon.md deleting file - it's not needed --- .../workspace-and-domain-settings/Coming-Soon.md | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 docs/articles/new-expensify/workspace-and-domain-settings/Coming-Soon.md diff --git a/docs/articles/new-expensify/workspace-and-domain-settings/Coming-Soon.md b/docs/articles/new-expensify/workspace-and-domain-settings/Coming-Soon.md deleted file mode 100644 index 6b85bb0364b5..000000000000 --- a/docs/articles/new-expensify/workspace-and-domain-settings/Coming-Soon.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Coming Soon -description: Coming Soon ---- From e17c27c332f439b96d33f997b9ddb8633e9ec7c4 Mon Sep 17 00:00:00 2001 From: maddylewis <38016013+maddylewis@users.noreply.github.com> Date: Sun, 26 Nov 2023 11:36:18 -0500 Subject: [PATCH 124/319] Delete docs/articles/expensify-classic/expensify-partner-program/Coming-Soon.md not needed - deleting --- .../expensify-partner-program/Coming-Soon.md | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 docs/articles/expensify-classic/expensify-partner-program/Coming-Soon.md diff --git a/docs/articles/expensify-classic/expensify-partner-program/Coming-Soon.md b/docs/articles/expensify-classic/expensify-partner-program/Coming-Soon.md deleted file mode 100644 index 6b85bb0364b5..000000000000 --- a/docs/articles/expensify-classic/expensify-partner-program/Coming-Soon.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Coming Soon -description: Coming Soon ---- From a75178aa3746f04e9b71d6e6e44574f7def6337e Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Sun, 26 Nov 2023 18:21:59 +0100 Subject: [PATCH 125/319] Change Boolean to '!!' --- src/components/TestToolMenu.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/TestToolMenu.tsx b/src/components/TestToolMenu.tsx index 2dfc060171f1..6a3efb0c26ce 100644 --- a/src/components/TestToolMenu.tsx +++ b/src/components/TestToolMenu.tsx @@ -57,7 +57,7 @@ function TestToolMenu({user = USER_DEFAULT, network}: TestToolMenuProps) { Network.setShouldForceOffline(!network?.shouldForceOffline)} /> @@ -66,7 +66,7 @@ function TestToolMenu({user = USER_DEFAULT, network}: TestToolMenuProps) { Network.setShouldFailAllRequests(!network?.shouldFailAllRequests)} /> From 6ec7fbc71fa4e4e9aa00089b5baab2a071a9c8ba Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Sun, 26 Nov 2023 18:33:42 +0100 Subject: [PATCH 126/319] Import react in testmenu --- src/components/TestToolMenu.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/TestToolMenu.tsx b/src/components/TestToolMenu.tsx index 6a3efb0c26ce..e40d8f473cdf 100644 --- a/src/components/TestToolMenu.tsx +++ b/src/components/TestToolMenu.tsx @@ -1,3 +1,4 @@ +import React from 'react'; import {OnyxEntry, withOnyx} from 'react-native-onyx'; import * as ApiUtils from '@libs/ApiUtils'; import compose from '@libs/compose'; From b9c56f4aefe3be7619a2d2a4e398252dfcf49cdf Mon Sep 17 00:00:00 2001 From: c3024 Date: Mon, 27 Nov 2023 07:47:35 +0530 Subject: [PATCH 127/319] remove red dot for settled requests --- src/components/ReportActionItem/MoneyRequestPreview.js | 2 +- src/components/ReportActionItem/ReportPreview.js | 2 +- src/libs/OptionsListUtils.js | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index 07aba132be0e..ea0e2d3a489b 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -282,7 +282,7 @@ function MoneyRequestPreview(props) { {getPreviewHeaderText() + (isSettled ? ` • ${getSettledMessage()}` : '')} - {hasFieldErrors && ( + {!isSettled && hasFieldErrors && ( {getPreviewMessage()} - {hasErrors && ( + {!iouSettled && hasErrors && ( Date: Mon, 27 Nov 2023 18:13:47 +0700 Subject: [PATCH 128/319] strip line break in task title --- src/components/TextInput/BaseTextInput/index.native.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/index.native.js b/src/components/TextInput/BaseTextInput/index.native.js index 60863cfb5771..867bc21152cc 100644 --- a/src/components/TextInput/BaseTextInput/index.native.js +++ b/src/components/TextInput/BaseTextInput/index.native.js @@ -172,10 +172,12 @@ function BaseTextInput(props) { /** * Set Value & activateLabel * - * @param {String} value + * @param {String} val * @memberof BaseTextInput */ - const setValue = (value) => { + const setValue = (val) => { + const value = val.replace(/\n/g, ' '); + if (props.onInputChange) { props.onInputChange(value); } From 3182e9bfc5dc5bf1ddd0f93fe235d21a96c06160 Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Mon, 27 Nov 2023 15:31:29 +0100 Subject: [PATCH 129/319] simplify optimistic tag syntax --- src/libs/actions/IOU.js | 15 +++------------ src/libs/actions/Policy.js | 2 +- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 4d5335ac8b31..4212199c5620 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -488,10 +488,7 @@ function getMoneyRequestInformation( optimisticPolicyRecentlyUsedCategories = Policy.buildOptimisticPolicyRecentlyUsedCategories(iouReport.policyID, category); } - let optimisticPolicyRecentlyUsedTags = {}; - if (tag) { - optimisticPolicyRecentlyUsedTags = Policy.buildOptimisticPolicyRecentlyUsedTags(iouReport.policyID, tag); - } + const optimisticPolicyRecentlyUsedTags = Policy.buildOptimisticPolicyRecentlyUsedTags(iouReport.policyID, tag); // If there is an existing transaction (which is the case for distance requests), then the data from the existing transaction // needs to be manually merged into the optimistic transaction. This is because buildOnyxDataForMoneyRequest() uses `Onyx.set()` for the transaction @@ -1166,10 +1163,7 @@ function createSplitsAndOnyxData(participants, currentUserLogin, currentUserAcco } // Add tag to optimistic policy recently used tags when a participant is a workspace - let optimisticPolicyRecentlyUsedTags = {}; - if (isPolicyExpenseChat) { - optimisticPolicyRecentlyUsedTags = Policy.buildOptimisticPolicyRecentlyUsedTags(participant.policyID, tag); - } + const optimisticPolicyRecentlyUsedTags = isPolicyExpenseChat ? Policy.buildOptimisticPolicyRecentlyUsedTags(participant.policyID, tag) : {}; // STEP 5: Build Onyx Data const [oneOnOneOptimisticData, oneOnOneSuccessData, oneOnOneFailureData] = buildOnyxDataForMoneyRequest( @@ -1811,10 +1805,7 @@ function editRegularMoneyRequest(transactionID, transactionThreadReportID, trans updatedChatReport.lastMessageHtml = messageText; } - let optimisticPolicyRecentlyUsedTags = {}; - if (_.has(transactionChanges, 'tag')) { - optimisticPolicyRecentlyUsedTags = Policy.buildOptimisticPolicyRecentlyUsedTags(iouReport.policyID, transactionChanges.tag); - } + const optimisticPolicyRecentlyUsedTags = Policy.buildOptimisticPolicyRecentlyUsedTags(iouReport.policyID, transactionChanges.tag); const isScanning = TransactionUtils.hasReceipt(updatedTransaction) && TransactionUtils.isReceiptBeingScanned(updatedTransaction); diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index 69035804bee7..7268f5690ecc 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -1491,7 +1491,7 @@ function buildOptimisticPolicyRecentlyUsedCategories(policyID, category) { */ function buildOptimisticPolicyRecentlyUsedTags(policyID, tag) { if (!policyID || !tag) { - return []; + return {}; } const policyTags = lodashGet(allPolicyTags, `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {}); From 26b7ba9db255bb25d902cc8c61624d4180d930ab Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Mon, 27 Nov 2023 15:34:08 +0100 Subject: [PATCH 130/319] simplify optimistic category syntax --- src/libs/actions/IOU.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 4212199c5620..79cf4cc57381 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -483,10 +483,7 @@ function getMoneyRequestInformation( billable, ); - let optimisticPolicyRecentlyUsedCategories = []; - if (category) { - optimisticPolicyRecentlyUsedCategories = Policy.buildOptimisticPolicyRecentlyUsedCategories(iouReport.policyID, category); - } + const optimisticPolicyRecentlyUsedCategories = Policy.buildOptimisticPolicyRecentlyUsedCategories(iouReport.policyID, category); const optimisticPolicyRecentlyUsedTags = Policy.buildOptimisticPolicyRecentlyUsedTags(iouReport.policyID, tag); @@ -1157,10 +1154,7 @@ function createSplitsAndOnyxData(participants, currentUserLogin, currentUserAcco } // Add category to optimistic policy recently used categories when a participant is a workspace - let optimisticPolicyRecentlyUsedCategories = []; - if (isPolicyExpenseChat) { - optimisticPolicyRecentlyUsedCategories = Policy.buildOptimisticPolicyRecentlyUsedCategories(participant.policyID, category); - } + const optimisticPolicyRecentlyUsedCategories = isPolicyExpenseChat ? Policy.buildOptimisticPolicyRecentlyUsedCategories(participant.policyID, category) : []; // Add tag to optimistic policy recently used tags when a participant is a workspace const optimisticPolicyRecentlyUsedTags = isPolicyExpenseChat ? Policy.buildOptimisticPolicyRecentlyUsedTags(participant.policyID, tag) : {}; From 706dd0087b484c5d92e2622d5bb8a13dd6c1d340 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 27 Nov 2023 15:40:01 +0100 Subject: [PATCH 131/319] fix typo --- src/libs/EmojiUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index 49d08fcc1b2c..22bfc83218ac 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -322,7 +322,7 @@ function getAddedEmojis(currentEmojis: Emoji[], formerEmojis: Emoji[]): Emoji[] * If we're on mobile, we also add a space after the emoji granted there's no text after it. */ function replaceEmojis(text: string, preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE, lang: 'en' | 'es' = CONST.LOCALES.DEFAULT): ReplacedEmoji { - // emojisTrie importing the emoji JSON file on the app starting and we want to avoid it + // emojisTrie is importing the emoji JSON file on the app starting and we want to avoid it const emojisTrie = require('./EmojiTrie').default; const trie = emojisTrie[lang]; @@ -400,7 +400,7 @@ function replaceAndExtractEmojis(text: string, preferredSkinTone = CONST.EMOJI_D * @param [limit] - matching emojis limit */ function suggestEmojis(text: string, lang: keyof SupportedLanguage, limit = CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_SUGGESTIONS): Emoji[] | undefined { - // emojisTrie importing the emoji JSON file on the app starting and we want to avoid it + // emojisTrie is importing the emoji JSON file on the app starting and we want to avoid it const emojisTrie = require('./EmojiTrie').default; const trie = emojisTrie[lang]; From 8141e5097b92590d9132cf3c64831b6b6727e90a Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 27 Nov 2023 15:40:15 +0100 Subject: [PATCH 132/319] add comments --- src/components/Icon/BankIcons.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/Icon/BankIcons.ts b/src/components/Icon/BankIcons.ts index 0afd4e304680..cc9730b5232b 100644 --- a/src/components/Icon/BankIcons.ts +++ b/src/components/Icon/BankIcons.ts @@ -13,7 +13,9 @@ import {BankIcon, BankName, BankNameKey} from '@src/types/onyx/Bank'; function getAssetIcon(bankNameKey: BankNameKey, isCard: boolean): React.FC { const bankValue = CONST.BANK_NAMES[bankNameKey]; - // Mapping bank names to their respective icon paths + // This maps bank names to their respective icon paths. + // The purpose is to avoid importing these at the app startup stage. + // Depending on whether 'isCard' is true, it selects either a card icon or a bank icon. const iconMappings = { [CONST.BANK_NAMES.EXPENSIFY]: isCard ? (require('@assets/images/cardicons/expensify-card-dark.svg') as React.FC) From 90d68384cf3b537efe6cd99e5cf0af09186b1bf1 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Mon, 27 Nov 2023 16:31:40 +0100 Subject: [PATCH 133/319] [TS migration] Migrate 'AnonymousReportFooter.js' component --- ...ortFooter.js => AnonymousReportFooter.tsx} | 49 ++++++++----------- 1 file changed, 21 insertions(+), 28 deletions(-) rename src/components/{AnonymousReportFooter.js => AnonymousReportFooter.tsx} (51%) diff --git a/src/components/AnonymousReportFooter.js b/src/components/AnonymousReportFooter.tsx similarity index 51% rename from src/components/AnonymousReportFooter.js rename to src/components/AnonymousReportFooter.tsx index 387e2ab01930..65dc813a829d 100644 --- a/src/components/AnonymousReportFooter.js +++ b/src/components/AnonymousReportFooter.tsx @@ -1,57 +1,52 @@ -import PropTypes from 'prop-types'; import React from 'react'; import {Text, View} from 'react-native'; -import reportPropTypes from '@pages/reportPropTypes'; +import {OnyxCollection} from 'react-native-onyx'; +import {OnyxEntry} from 'react-native-onyx/lib/types'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@styles/useThemeStyles'; import * as Session from '@userActions/Session'; +import {PersonalDetails, Report} from '@src/types/onyx'; import AvatarWithDisplayName from './AvatarWithDisplayName'; import Button from './Button'; import ExpensifyWordmark from './ExpensifyWordmark'; -import participantPropTypes from './participantPropTypes'; -import withLocalize, {withLocalizePropTypes} from './withLocalize'; -const propTypes = { +type AnonymousReportFooterProps = { /** The report currently being looked at */ - report: reportPropTypes, + report: OnyxEntry; - isSmallSizeLayout: PropTypes.bool, + /** Whether the small screen size layout should be used */ + isSmallSizeLayout?: boolean; /** Personal details of all the users */ - personalDetails: PropTypes.objectOf(participantPropTypes), - - ...withLocalizePropTypes, -}; - -const defaultProps = { - report: {}, - isSmallSizeLayout: false, - personalDetails: {}, + personalDetails: OnyxCollection; }; -function AnonymousReportFooter(props) { +function AnonymousReportFooter({isSmallSizeLayout = false, personalDetails, report}: AnonymousReportFooterProps) { const styles = useThemeStyles(); + const {translate} = useLocalize(); + return ( - + - - - + + + - {props.translate('anonymousReportFooter.logoTagline')} + {translate('anonymousReportFooter.logoTagline')}