diff --git a/.claude/skills/app-coding-standards/SKILL.md b/.claude/skills/app-coding-standards/SKILL.md
index bd904c38e077..3daa07beadb4 100644
--- a/.claude/skills/app-coding-standards/SKILL.md
+++ b/.claude/skills/app-coding-standards/SKILL.md
@@ -65,7 +65,6 @@ Coding standards for the Expensify App. Each standard is a standalone file in `r
- [CONSISTENCY-20](rules/consistency-20-function-names.md) — Name functions with a verb that says what they do
### Clean React Patterns
-- [CLEAN-REACT-PATTERNS-0](rules/clean-react-0-compiler.md) — React Compiler compliance
- [CLEAN-REACT-PATTERNS-1](rules/clean-react-1-composition-over-config.md) — Composition over configuration
- [CLEAN-REACT-PATTERNS-2](rules/clean-react-2-own-behavior.md) — Components own their behavior
- [CLEAN-REACT-PATTERNS-3](rules/clean-react-3-context-free-contracts.md) — Context-free component contracts
diff --git a/.claude/skills/app-coding-standards/rules/clean-react-0-compiler.md b/.claude/skills/app-coding-standards/rules/clean-react-0-compiler.md
deleted file mode 100644
index e43d5c7fb94a..000000000000
--- a/.claude/skills/app-coding-standards/rules/clean-react-0-compiler.md
+++ /dev/null
@@ -1,129 +0,0 @@
----
-ruleId: CLEAN-REACT-PATTERNS-0
-title: React Compiler compliance
----
-
-## [CLEAN-REACT-PATTERNS-0] React Compiler compliance
-
-### Reasoning
-
-React Compiler is enabled in this codebase (`babel-plugin-react-compiler` runs first in both Rspack and metro configs). It automatically memoizes components and hooks at the AST level — analyzing data flow, tracking dependencies, and inserting fine-grained caching that is more precise than any hand-written `useMemo`, `useCallback`, or `React.memo`.
-
-Manual memoization is therefore:
-
-1. **Redundant** — the compiler already handles it, so the manual wrapper adds zero value
-2. **Harmful** — it interferes with the compiler's optimization model, potentially preventing it from applying its own caching strategy or causing double-wrapping
-3. **Noisy** — it clutters the codebase with dependency arrays that must be maintained, reviewed, and debugged
-
-The codebase enforces this via:
-- **Babel plugin**: `babel-plugin-react-compiler` in `babel.config.js`
-- **Lint post-processor**: `scripts/lint/processors/ReactCompilerFilter.ts` suppresses redundant lint rules when both React Compilers memoize the file
-- **CI compliance check**: `scripts/react-compiler-compliance-check.ts` enforces that new components/hooks compile and that existing compiled files don't regress
-
-Reference: [React Compiler documentation](https://react.dev/learn/react-compiler)
-
-### Incorrect
-
-#### Incorrect (useCallback)
-
-```tsx
-function ReportScreen({reportID}: {reportID: string}) {
- const handlePress = useCallback(() => {
- Navigation.navigate(ROUTES.REPORT_DETAILS.getRoute(reportID));
- }, [reportID]);
-
- return ;
-}
-```
-
-#### Incorrect (useMemo)
-
-```tsx
-function PolicyList({policies}: {policies: Policy[]}) {
- const sortedPolicies = useMemo(
- () => policies.sort((a, b) => a.name.localeCompare(b.name)),
- [policies],
- );
-
- return ;
-}
-```
-
-#### Incorrect (React.memo)
-
-```tsx
-const Avatar = React.memo(function Avatar({source, size}: AvatarProps) {
- return ;
-});
-```
-
-### Correct
-
-#### Correct (plain function — compiler memoizes automatically)
-
-```tsx
-function ReportScreen({reportID}: {reportID: string}) {
- const handlePress = () => {
- Navigation.navigate(ROUTES.REPORT_DETAILS.getRoute(reportID));
- };
-
- return ;
-}
-```
-
-#### Correct (plain expression — compiler memoizes automatically)
-
-```tsx
-function PolicyList({policies}: {policies: Policy[]}) {
- const sortedPolicies = policies.sort((a, b) => a.name.localeCompare(b.name));
-
- return ;
-}
-```
-
-#### Correct (plain component — compiler memoizes automatically)
-
-```tsx
-function Avatar({source, size}: AvatarProps) {
- return ;
-}
-```
-
----
-
-### Review Metadata
-
-#### Verification
-
-Before flagging, verify that the file actually compiles with React Compiler:
-
-```bash
-check-compiler.sh
-```
-
-**IMPORTANT:** Run `check-compiler.sh` exactly as shown above - by name only, without an absolute path or `bash` prefix. The script is already on `$PATH`.
-
-If the output contains **"Failed to compile"** for the file under review, the rule **does not apply** — the author may have no alternative to manual memoization until the compilation issue is resolved.
-
-#### Condition
-
-The verification step above is a prerequisite. Only flag when the file compiles successfully AND any of these are true in new or modified code:
-
-1. **`useCallback`** — A function is wrapped in `useCallback`. The compiler automatically memoizes closures based on their captured variables.
-2. **`useMemo`** — A value is wrapped in `useMemo`. The compiler automatically caches derived values.
-3. **`React.memo`** — A component is wrapped in `React.memo` (or `memo` imported from React). The compiler automatically skips re-rendering components whose props haven't changed.
-
-**Response:** Challenge the author: "React Compiler is enabled — remove the manual memoization and restructure the code so the compiler can handle it."
-
-The goal is to fix the root cause (make code compiler-friendly) rather than slap on manual memoization as a workaround.
-
-**Search Patterns:**
-- `useCallback\s*\(` — manual callback memoization
-- `useMemo\s*\(` — manual value memoization
-- `React\.memo\s*\(` or `memo\s*\(` — manual component memoization
-- Import statements: `useCallback`, `useMemo` from `react`
-
-**DO NOT flag if:**
-- The file does not compile with React Compiler (verified by the compliance check in the Verification section above)
-- The code is inside `node_modules/`, `patches/`, or test fixtures
-- The manual memoization exists in unchanged lines (pre-existing code not touched by the diff)
diff --git a/.github/scripts/buildActions.ts b/.github/scripts/buildActions.ts
index dc5fb667b4dd..f9343de26bea 100755
--- a/.github/scripts/buildActions.ts
+++ b/.github/scripts/buildActions.ts
@@ -53,6 +53,8 @@ async function buildAction(actionPath: string): Promise {
entryPoints: [actionPath],
outfile,
bundle: true,
+ // Shared worktree dependencies must use the same module paths as a clean CI install.
+ preserveSymlinks: true,
platform: 'node',
target: 'node24',
format: 'esm',
diff --git a/config/eslint/eslint.config.mjs b/config/eslint/eslint.config.mjs
index f71f861de418..ba61f3b2ac04 100644
--- a/config/eslint/eslint.config.mjs
+++ b/config/eslint/eslint.config.mjs
@@ -553,6 +553,8 @@ const config = defineConfig([
{
files: ['src/**/*.ts', 'src/**/*.tsx'],
rules: {
+ // The lint pipeline keeps this diagnostic only when both compilers memoize the file.
+ 'rulesdir/no-manual-memoization': 'error',
'rulesdir/prefer-locale-compare-from-context': 'error',
'rulesdir/no-object-keys-includes': 'error',
},
diff --git a/contributingGuides/LINTING.md b/contributingGuides/LINTING.md
index 86be44432903..bf3fb6cf5fe9 100644
--- a/contributingGuides/LINTING.md
+++ b/contributingGuides/LINTING.md
@@ -153,6 +153,6 @@ The env-var names and TSV format are unchanged from the previous `eslint-seatbel
- [Ratcheting errors](https://www.notion.com/blog/how-we-evolved-our-code-notions-ratcheting-system-using-custom-eslint-rules).
- [`CONSISTENCY-5`: Justify ESLint rule disables](../.claude/skills/app-coding-standards/rules/consistency-5-justify-eslint-disable.md) — when `eslint-disable` is acceptable, and how to document it.
-- [`CLEAN-REACT-PATTERNS-0`: React Compiler compliance](../.claude/skills/app-coding-standards/rules/clean-react-0-compiler.md) — why some hook-related rules are suppressed per-file.
+- `rulesdir/no-manual-memoization` forbids `React.memo`, `useMemo`, and `useCallback` when both React Compilers memoize the file. Callbacks passed to `useFocusEffect` may retain `useCallback`, which React Navigation requires.
- [`STYLE.md`](./STYLE.md) — the coding style rules many of our ESLint rules enforce.
- [`REACT_COMPILER.md`](./REACT_COMPILER.md) — separate compiler-compliance CI check that runs alongside lint.
diff --git a/contributingGuides/REACT_COMPILER.md b/contributingGuides/REACT_COMPILER.md
index cf721fb6804c..15dfe546b1fb 100644
--- a/contributingGuides/REACT_COMPILER.md
+++ b/contributingGuides/REACT_COMPILER.md
@@ -6,6 +6,12 @@
React Compiler is enabled in the web build pipeline via `oxc-transform-react` (the Rust React Compiler) and in the Metro (mobile) build pipeline via `babel-plugin-react-compiler`. Because these are two different implementations, they don't always memoize a file the same way: a file can be auto-memoized on native but not on web, or vice versa. To catch that, both the CI check and the ESLint processor run BOTH compilers, via the shared helpers in `config/reactCompiler/` (`checkWithBabel.mjs`, `checkWithOxc.mjs`, and `checkBoth.mjs`).
+## Manual memoization
+
+`rulesdir/no-manual-memoization` disallows `React.memo`, `useMemo`, and `useCallback` in files fully compiled and memoized by both compilers. Derive values directly in render and use plain functions instead of maintaining dependency arrays. The lint pipeline allows manual memoization when either compiler cannot memoize the file.
+
+Callbacks passed to React Navigation's `useFocusEffect` are the exception: keep their `useCallback` wrapper because React Navigation requires a stable callback.
+
## React Compiler CI check
A CI check runs on every PR that modifies `.ts` or `.tsx` files. For each changed file it runs both `babel-plugin-react-compiler` and `oxc-transform-react` and reports, for each, whether the file compiles and whether it is actually memoized.
@@ -86,7 +92,7 @@ If you added a modification to `SharedValue`, you'll likely encounter this error
### Existing manual memoization could not be preserved. [...]
-These types of errors usually occur when the calls to `useMemo` that were made manually are too complex for react-compiler to understand. React compiler is still experimental so unfortunately this can happen.
+Remove the manual memoization and derive the value directly in render. This lets React Compiler infer dependencies and choose its own memoization boundaries. The examples below explain the historical errors in files that still need manual memoization because they do not compile on both platforms.
Some specific cases of this error are described below.
diff --git a/eslint-plugin-local-rules/no-manual-memoization.js b/eslint-plugin-local-rules/no-manual-memoization.js
new file mode 100644
index 000000000000..b332591e16f7
--- /dev/null
+++ b/eslint-plugin-local-rules/no-manual-memoization.js
@@ -0,0 +1,80 @@
+const name = 'no-manual-memoization';
+
+const REACT_NAVIGATION_MODULES = ['@react-navigation/native', '@react-navigation/core'];
+
+const meta = {
+ type: 'problem',
+ docs: {
+ description: 'Disallow manual memoization when both React Compilers memoize the file.',
+ recommended: 'error',
+ },
+ schema: [],
+ messages: {
+ redundant: 'React Compiler memoizes this file. Remove {{method}} and derive values directly in render instead. useCallback is only allowed for useFocusEffect callbacks.',
+ },
+};
+
+/** @param {import('eslint').Rule.RuleContext} context */
+function create(context) {
+ const {sourceCode} = context;
+
+ function findVariable(node) {
+ let scope = sourceCode.getScope(node);
+ while (scope) {
+ const variable = scope.set.get(node.name);
+ if (variable) {
+ return variable;
+ }
+ scope = scope.upper;
+ }
+ return undefined;
+ }
+
+ function importedMethod(node, moduleName) {
+ if (node.type === 'Identifier') {
+ const definition = findVariable(node)?.defs[0];
+ if (definition?.type !== 'ImportBinding' || definition.parent.source.value !== moduleName || definition.node.type !== 'ImportSpecifier') {
+ return undefined;
+ }
+ return definition.node.imported.name ?? definition.node.imported.value;
+ }
+ if (node.type !== 'MemberExpression' || node.object.type !== 'Identifier') {
+ return undefined;
+ }
+ const definition = findVariable(node.object)?.defs[0];
+ if (definition?.type !== 'ImportBinding' || definition.parent.source.value !== moduleName || definition.node.type === 'ImportSpecifier') {
+ return undefined;
+ }
+ return node.computed ? node.property.value : node.property.name;
+ }
+
+ function isFocusEffectArgument(node) {
+ return (
+ node.parent.type === 'CallExpression' &&
+ node.parent.arguments[0] === node &&
+ REACT_NAVIGATION_MODULES.some((moduleName) => importedMethod(node.parent.callee, moduleName) === 'useFocusEffect')
+ );
+ }
+
+ function isFocusCallback(node) {
+ if (isFocusEffectArgument(node)) {
+ return true;
+ }
+ if (node.parent.type !== 'VariableDeclarator' || node.parent.id.type !== 'Identifier') {
+ return false;
+ }
+ return sourceCode.getDeclaredVariables(node.parent).some((variable) => variable.references.some((reference) => reference.isRead() && isFocusEffectArgument(reference.identifier)));
+ }
+
+ return {
+ CallExpression(node) {
+ const method = importedMethod(node.callee, 'react');
+ if (!['memo', 'useMemo', 'useCallback'].includes(method) || (method === 'useCallback' && isFocusCallback(node))) {
+ return;
+ }
+ context.report({node, messageId: 'redundant', data: {method}});
+ },
+ };
+}
+
+export {name, meta, create};
diff --git a/scripts/lint/processors/ReactCompilerFilter.ts b/scripts/lint/processors/ReactCompilerFilter.ts
index 334a9ce92a4e..ceb9a0626235 100644
--- a/scripts/lint/processors/ReactCompilerFilter.ts
+++ b/scripts/lint/processors/ReactCompilerFilter.ts
@@ -7,6 +7,7 @@ import WorkerPool from '../../utils/WorkerPool';
import Processor from '../Processor';
const RULES_SUPPRESSED_BY_REACT_COMPILER = new Set(['react/jsx-no-constructed-context-values', 'rulesdir/no-inline-useOnyx-selector']);
+const MANUAL_MEMOIZATION_RULE = 'rulesdir/no-manual-memoization';
const EXHAUSTIVE_DEPS_USECALLBACK_USEMEMO_PATTERN = /\buseCallback\(\) Hook\b|\buseMemo\(\) Hook\b/;
const CACHE_DIR = 'node_modules/.cache/react-compiler';
const REACT_COMPILER_FINGERPRINT_FILES = [
@@ -126,8 +127,8 @@ async function checkCandidatesWithPool(candidates: Candidate[], checkBoth: Compi
}
/**
- * Drop suppressible React-Compiler-redundant messages, but only after both
- * compilers memoize the file. Files with no suppressible message are skipped
+ * Drop redundant messages and enforce no-manual-memoization only after both
+ * compilers memoize the file. Files with no compiler-dependent message are skipped
* entirely (~60× fewer compiler invocations than the ESLint processor).
*
* Cache is one file per compiler fingerprint and content hash so concurrent
@@ -166,13 +167,13 @@ async function filterReactCompilerMessages(
if (shouldSkipCompiler(filename)) {
continue;
}
- if (fileMessages.some(isSuppressibleMessage)) {
+ if (fileMessages.some((message) => isSuppressibleMessage(message) || message.ruleID === MANUAL_MEMOIZATION_RULE)) {
candidateNames.push(filename);
}
}
if (candidateNames.length === 0) {
- return messages;
+ return messages.filter((message) => message.ruleID !== MANUAL_MEMOIZATION_RULE);
}
const uncached: Candidate[] = [];
@@ -220,6 +221,9 @@ async function filterReactCompilerMessages(
);
return messages.filter((message) => {
+ if (message.ruleID === MANUAL_MEMOIZATION_RULE) {
+ return memoized.get(message.filePath) === true;
+ }
if (!memoized.get(message.filePath)) {
return true;
}
diff --git a/src/DeepLinkHandler.tsx b/src/DeepLinkHandler.tsx
index 406275578711..79cf34992274 100644
--- a/src/DeepLinkHandler.tsx
+++ b/src/DeepLinkHandler.tsx
@@ -1,6 +1,6 @@
import type {NativeEventSubscription} from 'react-native';
-import {useCallback, useEffect, useRef} from 'react';
+import {useEffect, useRef} from 'react';
import {Linking} from 'react-native';
import CONST from './CONST';
@@ -61,14 +61,14 @@ function DeepLinkHandler({onInitialUrl}: DeepLinkHandlerProps) {
// An anonymous deep link into a public room needs to be re-fetched after OpenApp settles (see the effect
// below). Track the pending reportID so both the initial-URL and the url-change paths stay in sync.
- const trackPendingPublicRoomFromDeepLink = useCallback((url: string, isCurrentlyAuthenticated: boolean) => {
+ const trackPendingPublicRoomFromDeepLink = (url: string, isCurrentlyAuthenticated: boolean) => {
const deepLinkReportID = getReportIDFromLink(url);
if (!deepLinkReportID || isCurrentlyAuthenticated) {
return;
}
pendingPublicRoomReportID.current = deepLinkReportID;
hasRefetchedPublicRoom.current = false;
- }, []);
+ };
useEffect(() => {
if (
diff --git a/src/components/AccountSwitcherButton.tsx b/src/components/AccountSwitcherButton.tsx
index 618d2116f0f7..46f5c3ea5ad4 100644
--- a/src/components/AccountSwitcherButton.tsx
+++ b/src/components/AccountSwitcherButton.tsx
@@ -31,7 +31,7 @@ import type {ComponentRef} from 'react';
import {canSwitchAccountsSelector} from '@selectors/Account';
import {Str} from 'expensify-common';
-import React, {useCallback, useLayoutEffect, useRef, useState} from 'react';
+import React, {useLayoutEffect, useRef, useState} from 'react';
import {View} from 'react-native';
import type {PopoverMenuItem} from './PopoverMenu';
@@ -139,14 +139,11 @@ function AccountSwitcherButton({isScreenFocused}: AccountSwitcherButtonProps) {
};
// Anchor the menu to the bottom-left of the Switch button so it opens directly below it.
- const measureDelegatorMenuPosition = useCallback(
- () =>
- calculatePopoverPosition(buttonRef, {
- horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT,
- vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP,
- }),
- [calculatePopoverPosition],
- );
+ const measureDelegatorMenuPosition = () =>
+ calculatePopoverPosition(buttonRef, {
+ horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT,
+ vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP,
+ });
// Keep the menu anchored to the button if the window is resized while it is open.
useLayoutEffect(() => {
@@ -265,36 +262,29 @@ function AccountSwitcherButton({isScreenFocused}: AccountSwitcherButtonProps) {
const isButtonTrailingItemOfRow = shouldUseNarrowLayout && isInLandscapeMode;
const TooltipToRender = shouldShowProductTrainingTooltip ? EducationalTooltip : Tooltip;
- const tooltipProps = shouldShowProductTrainingTooltip
- ? {
- shouldRender: shouldShowProductTrainingTooltip,
- renderTooltipContent: renderProductTrainingTooltip,
- anchorAlignment: {
- // Center on the button so the pointer lands on the button's center rather than off by its right edge.
- // The tooltip is wider than the room left of the window gutter, so the body still gets pulled left and
- // only the pointer ends up truly centered.
- horizontal: isButtonTrailingItemOfRow ? CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT : CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.CENTER,
- vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP,
- },
- shiftVertical: variables.accountSwitcherTooltipShiftVertical,
- // The nudge squares the right-aligned tooltip up against the gutter. Centered alignment subtracts it back
- // out of the pointer offset, which would drag the pointer off the button's center, so drop it.
- shiftHorizontal: isButtonTrailingItemOfRow ? variables.accountSwitcherTooltipShiftHorizontal : 0,
- // Native ignores the keep-on-screen clamp unless we opt in.
- computeHorizontalShiftForNative: true,
- wrapperStyle: styles.productTrainingTooltipWrapper,
- onTooltipPress: onPressSwitcher,
- // The switcher lives in the settings sidebar, which isn't the navigation-focused screen on wide layouts.
- // Without this the educational tooltip is suppressed (it relies on the screen being focused), so keep it shown until dismissed.
- shouldHideOnNavigate: false,
- // On narrow layouts the switcher scrolls away with the settings list, so the tooltip has to follow it or get out of the way.
- // On wide layouts it sits in the fixed top bar, where scrolling never moves the anchor.
- shouldHideOnScroll: shouldUseNarrowLayout,
- }
- : {
- text: translate('delegate.copilotAccess'),
- shouldRender: canSwitchAccounts || isWaitingForAccountData,
- };
+ let tooltipProps;
+ if (shouldShowProductTrainingTooltip) {
+ tooltipProps = {
+ shouldRender: shouldShowProductTrainingTooltip,
+ renderTooltipContent: renderProductTrainingTooltip,
+ anchorAlignment: {
+ horizontal: isButtonTrailingItemOfRow ? CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT : CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.CENTER,
+ vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP,
+ },
+ shiftVertical: variables.accountSwitcherTooltipShiftVertical,
+ shiftHorizontal: isButtonTrailingItemOfRow ? variables.accountSwitcherTooltipShiftHorizontal : 0,
+ computeHorizontalShiftForNative: true,
+ wrapperStyle: styles.productTrainingTooltipWrapper,
+ onTooltipPress: onPressSwitcher,
+ shouldHideOnNavigate: false,
+ shouldHideOnScroll: shouldUseNarrowLayout,
+ };
+ } else {
+ tooltipProps = {
+ text: translate('delegate.copilotAccess'),
+ shouldRender: canSwitchAccounts || isWaitingForAccountData,
+ };
+ }
if (!canSwitchAccounts && !isWaitingForAccountData) {
return null;
diff --git a/src/components/ActionSheetAwareScrollView/ActionSheetAwareScrollViewContext.tsx b/src/components/ActionSheetAwareScrollView/ActionSheetAwareScrollViewContext.tsx
index d99e1d233d3c..bbc7df8d4b64 100644
--- a/src/components/ActionSheetAwareScrollView/ActionSheetAwareScrollViewContext.tsx
+++ b/src/components/ActionSheetAwareScrollView/ActionSheetAwareScrollViewContext.tsx
@@ -6,7 +6,7 @@ import createDummySharedValue from '@src/utils/createDummySharedValue';
import type {PropsWithChildren} from 'react';
import type {ValueOf} from 'type-fest';
-import React, {createContext, useContext, useMemo} from 'react';
+import React, {createContext, useContext} from 'react';
import type {ActionSheetAwareScrollViewActionsContextValue, ActionSheetAwareScrollViewMeasurements, ActionSheetAwareScrollViewStateContextValue} from './types';
@@ -114,21 +114,15 @@ function ActionSheetAwareScrollViewProvider(props: PropsWithChildren) {
INITIAL_ACTION_SHEET_STATE,
);
- const stateValue = useMemo(
- () => ({
- currentActionSheetState: currentState,
- }),
- [currentState],
- );
+ const stateValue: ActionSheetAwareScrollViewStateContextValue = {
+ currentActionSheetState: currentState,
+ };
- const actionsValue = useMemo(
- () => ({
- transitionActionSheetState: transition,
- transitionActionSheetStateWorklet: transitionWorklet,
- resetStateMachine: reset,
- }),
- [reset, transition, transitionWorklet],
- );
+ const actionsValue: ActionSheetAwareScrollViewActionsContextValue = {
+ transitionActionSheetState: transition,
+ transitionActionSheetStateWorklet: transitionWorklet,
+ resetStateMachine: reset,
+ };
return (
diff --git a/src/components/ActiveElementRoleProvider/index.native.tsx b/src/components/ActiveElementRoleProvider/index.native.tsx
index 9b1aac63a9a9..a8ae2fbeebd1 100644
--- a/src/components/ActiveElementRoleProvider/index.native.tsx
+++ b/src/components/ActiveElementRoleProvider/index.native.tsx
@@ -7,12 +7,9 @@ const ActiveElementRoleContext = React.createContext ({
- role: null,
- }),
- [],
- );
+ const value = {
+ role: null,
+ };
return {children};
}
diff --git a/src/components/ActiveElementRoleProvider/index.tsx b/src/components/ActiveElementRoleProvider/index.tsx
index b153e30d7622..d3cc0d781094 100644
--- a/src/components/ActiveElementRoleProvider/index.tsx
+++ b/src/components/ActiveElementRoleProvider/index.tsx
@@ -27,12 +27,9 @@ function ActiveElementRoleProvider({children}: ActiveElementRoleProps) {
};
}, []);
- const value = React.useMemo(
- () => ({
- role: activeRoleRef,
- }),
- [activeRoleRef],
- );
+ const value = {
+ role: activeRoleRef,
+ };
return {children};
}
diff --git a/src/components/AddPaymentCard/PaymentCardForm.tsx b/src/components/AddPaymentCard/PaymentCardForm.tsx
index 1af3e11778f1..ab0eeef2dabb 100644
--- a/src/components/AddPaymentCard/PaymentCardForm.tsx
+++ b/src/components/AddPaymentCard/PaymentCardForm.tsx
@@ -27,7 +27,7 @@ import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
import type {ReactNode} from 'react';
import type {ValueOf} from 'type-fest';
-import React, {useCallback, useRef, useState} from 'react';
+import React, {useRef, useState} from 'react';
import {View} from 'react-native';
type PaymentCardFormProps = {
@@ -147,7 +147,7 @@ function PaymentCardForm({
// Formats user input into a valid expiration date (MM/YY) and automatically adds slash after the month.
// Ensures the month is always between 01 and 12 by correcting invalid value to match the proper format.
- const onChangeExpirationDate = useCallback((newValue: string) => {
+ const onChangeExpirationDate = (newValue: string) => {
if (typeof newValue !== 'string') {
return;
}
@@ -182,7 +182,7 @@ function PaymentCardForm({
previousValueRef.current = formattedValue;
setExpirationDate(formattedValue);
- }, []);
+ };
const [cardNumber, setCardNumber] = useState('');
@@ -228,7 +228,7 @@ function PaymentCardForm({
return errors;
};
- const onChangeCardNumber = useCallback((newValue: string) => {
+ const onChangeCardNumber = (newValue: string) => {
// Replace all characters that are not spaces or digits
let validCardNumber = newValue.replaceAll(/[^\d ]/g, '');
@@ -251,7 +251,7 @@ function PaymentCardForm({
}
setCardNumber(validCardNumber);
- }, []);
+ };
if (!shouldShowPaymentCardForm || isLoadingOnyxValue(metadata) || !hasClearedDraftCurrency) {
return null;
diff --git a/src/components/AddPlaidBankAccount.tsx b/src/components/AddPlaidBankAccount.tsx
index 20ea864b66d7..195b24eca7d0 100644
--- a/src/components/AddPlaidBankAccount.tsx
+++ b/src/components/AddPlaidBankAccount.tsx
@@ -17,7 +17,7 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type {OnyxEntry} from 'react-native-onyx';
-import React, {useCallback, useEffect, useRef, useState} from 'react';
+import React, {useEffect, useRef, useState} from 'react';
import {View} from 'react-native';
import ActivityIndicator from './ActivityIndicator';
@@ -99,16 +99,7 @@ function AddPlaidBankAccount({
}
};
- /**
- * @returns {Boolean}
- * I'm using useCallback so the useEffect which uses this function doesn't run on every render.
- */
- const isAuthenticatedWithPlaid = useCallback(
- () => (!!receivedRedirectURI && !!plaidLinkOAuthToken) || !!plaidData?.bankAccounts?.length || !isEmptyObject(plaidData?.errors),
- [plaidData?.bankAccounts?.length, plaidData?.errors, plaidLinkOAuthToken, receivedRedirectURI],
- );
-
- const isAuthenticated = isAuthenticatedWithPlaid();
+ const isAuthenticated = (!!receivedRedirectURI && !!plaidLinkOAuthToken) || !!plaidData?.bankAccounts?.length || !isEmptyObject(plaidData?.errors);
const prevIsAuthenticated = usePrevious(isAuthenticated);
/**
@@ -143,7 +134,7 @@ function AddPlaidBankAccount({
subscribeToNavigationShortcuts();
// If we're coming from Plaid OAuth flow then we need to reuse the existing plaidLinkToken
- if (isAuthenticatedWithPlaid()) {
+ if (isAuthenticated) {
return unsubscribeToNavigationShortcuts;
}
openPlaidBankLogin(allowDebit, bankAccountID);
@@ -156,11 +147,11 @@ function AddPlaidBankAccount({
useEffect(() => {
// If we are coming back from offline and we haven't authenticated with Plaid yet, we need to re-run our call to kick off Plaid
// previousNetworkState.current also makes sure that this doesn't run on the first render.
- if (previousNetworkState.current && !isOffline && !isAuthenticatedWithPlaid()) {
+ if (previousNetworkState.current && !isOffline && !isAuthenticated) {
openPlaidBankLogin(allowDebit, bankAccountID);
}
previousNetworkState.current = isOffline;
- }, [allowDebit, bankAccountID, isAuthenticatedWithPlaid, isOffline]);
+ }, [allowDebit, bankAccountID, isAuthenticated, isOffline]);
useEffect(() => {
// We had a Plaid session and lost it (e.g. openReimbursementAccountPage reset plaidData on reconnect).
@@ -193,9 +184,9 @@ function AddPlaidBankAccount({
onInputChange(plaidAccountID);
};
- const handlePlaidLinkError = useCallback((error: ErrorEvent | null) => {
+ const handlePlaidLinkError = (error: ErrorEvent | null) => {
Log.hmmm('[PlaidLink] Error: ', error?.message);
- }, []);
+ };
if (isPlaidDisabled) {
return (
diff --git a/src/components/AddToWalletButton/useIsCardInWallet.ts b/src/components/AddToWalletButton/useIsCardInWallet.ts
index 8fbce3166628..b70b74d7692d 100644
--- a/src/components/AddToWalletButton/useIsCardInWallet.ts
+++ b/src/components/AddToWalletButton/useIsCardInWallet.ts
@@ -5,7 +5,7 @@ import {checkIfWalletIsAvailable, isCardInWallet} from '@libs/Wallet';
import CONST from '@src/CONST';
import type {Card} from '@src/types/onyx';
-import {useCallback, useEffect, useState} from 'react';
+import {useEffect, useState} from 'react';
type UseIsCardInWalletType = {
isInWallet: boolean | null;
@@ -19,7 +19,7 @@ function useIsCardInWallet(card: Card): UseIsCardInWalletType {
const [isWalletAvailable, setIsWalletAvailable] = useState(false);
const isCardAvailable = card.state === CONST.EXPENSIFY_CARD.STATE.OPEN;
const [isLoading, setIsLoading] = useState(false);
- const checkIfCardIsInWallet = useCallback(() => {
+ const checkIfCardIsInWallet = () => {
isCardInWallet(card)
.then((result) => {
setIsInWallet(result);
@@ -30,7 +30,7 @@ function useIsCardInWallet(card: Card): UseIsCardInWalletType {
.finally(() => {
setIsLoading(false);
});
- }, [card]);
+ };
useEffect(() => {
if (!isCardAvailable) {
@@ -55,14 +55,12 @@ function useIsCardInWallet(card: Card): UseIsCardInWalletType {
}, [isCardAvailable]);
// Recheck card status when app regains focus in case user manually adds card to wallet outside the app
- useAppFocusEvent(
- useCallback(() => {
- if (!isCardAvailable) {
- return;
- }
- checkIfCardIsInWallet();
- }, [checkIfCardIsInWallet, isCardAvailable]),
- );
+ useAppFocusEvent(() => {
+ if (!isCardAvailable) {
+ return;
+ }
+ checkIfCardIsInWallet();
+ });
return {
isInWallet,
diff --git a/src/components/AddressForm.tsx b/src/components/AddressForm.tsx
index a707afe9f413..37e96175c01b 100644
--- a/src/components/AddressForm.tsx
+++ b/src/components/AddressForm.tsx
@@ -11,7 +11,7 @@ import INPUT_IDS from '@src/types/form/HomeAddressForm';
import type {ReactNode} from 'react';
import {CONST as COMMON_CONST} from 'expensify-common';
-import React, {createContext, useCallback, useContext} from 'react';
+import React, {createContext, useContext} from 'react';
import {View} from 'react-native';
import type {FormInputErrors, FormOnyxValues} from './Form/types';
@@ -258,86 +258,83 @@ function AddressForm({
const styles = useThemeStyles();
const {translate} = useLocalize();
- const validator = useCallback(
- (rawValues: FormOnyxValues): FormInputErrors => {
- // Every form rendered here has the home address fields, so they are validated the same way
- const addressValues: FormOnyxValues = rawValues;
+ const validator = (rawValues: FormOnyxValues): FormInputErrors => {
+ // Every form rendered here has the home address fields, so they are validated the same way
+ const addressValues: FormOnyxValues = rawValues;
- // When hidden, the country input is unregistered so fall back to the country prop.
- const values = shouldHideCountrySelector ? {...addressValues, country: addressValues.country || country} : addressValues;
+ // When hidden, the country input is unregistered so fall back to the country prop.
+ const values = shouldHideCountrySelector ? {...addressValues, country: addressValues.country || country} : addressValues;
- const errors: Record = {};
- const baseRequiredFields = shouldHideCountrySelector ? (['addressLine1', 'city', 'state'] as const) : (['addressLine1', 'city', 'country', 'state'] as const);
- const requiredFields = shouldRequireZip ? ([...baseRequiredFields, 'zipPostCode'] as const) : baseRequiredFields;
+ const errors: Record = {};
+ const baseRequiredFields = shouldHideCountrySelector ? (['addressLine1', 'city', 'state'] as const) : (['addressLine1', 'city', 'country', 'state'] as const);
+ const requiredFields = shouldRequireZip ? ([...baseRequiredFields, 'zipPostCode'] as const) : baseRequiredFields;
- // Check "State" dropdown is a valid state if selected Country is USA
- if (values.country === CONST.COUNTRY.US && !values.state) {
- errors.state = translate('common.error.fieldRequired');
+ // Check "State" dropdown is a valid state if selected Country is USA
+ if (values.country === CONST.COUNTRY.US && !values.state) {
+ errors.state = translate('common.error.fieldRequired');
+ }
+
+ // Add "Field required" errors if any required field is empty
+ for (const fieldKey of requiredFields) {
+ const fieldValue = values[fieldKey] ?? '';
+ if (isRequiredFulfilled(fieldValue)) {
+ continue;
}
- // Add "Field required" errors if any required field is empty
- for (const fieldKey of requiredFields) {
- const fieldValue = values[fieldKey] ?? '';
- if (isRequiredFulfilled(fieldValue)) {
- continue;
- }
+ errors[fieldKey] = translate('common.error.fieldRequired');
+ }
- errors[fieldKey] = translate('common.error.fieldRequired');
- }
+ if (values.addressLine1.length > CONST.FORM_CHARACTER_LIMIT) {
+ errors.addressLine1 = translate('common.error.characterLimitExceedCounter', values.addressLine1.length, CONST.FORM_CHARACTER_LIMIT);
+ }
- if (values.addressLine1.length > CONST.FORM_CHARACTER_LIMIT) {
- errors.addressLine1 = translate('common.error.characterLimitExceedCounter', values.addressLine1.length, CONST.FORM_CHARACTER_LIMIT);
- }
+ if (values.addressLine2.length > CONST.FORM_CHARACTER_LIMIT) {
+ errors.addressLine2 = translate('common.error.characterLimitExceedCounter', values.addressLine2.length, CONST.FORM_CHARACTER_LIMIT);
+ }
- if (values.addressLine2.length > CONST.FORM_CHARACTER_LIMIT) {
- errors.addressLine2 = translate('common.error.characterLimitExceedCounter', values.addressLine2.length, CONST.FORM_CHARACTER_LIMIT);
- }
+ if (values.city.length > CONST.FORM_CHARACTER_LIMIT) {
+ errors.city = translate('common.error.characterLimitExceedCounter', values.city.length, CONST.FORM_CHARACTER_LIMIT);
+ }
- if (values.city.length > CONST.FORM_CHARACTER_LIMIT) {
- errors.city = translate('common.error.characterLimitExceedCounter', values.city.length, CONST.FORM_CHARACTER_LIMIT);
- }
+ if (values.country !== CONST.COUNTRY.US && values.state.length > CONST.STATE_CHARACTER_LIMIT) {
+ errors.state = translate('common.error.characterLimitExceedCounter', values.state.length, CONST.STATE_CHARACTER_LIMIT);
+ }
- if (values.country !== CONST.COUNTRY.US && values.state.length > CONST.STATE_CHARACTER_LIMIT) {
- errors.state = translate('common.error.characterLimitExceedCounter', values.state.length, CONST.STATE_CHARACTER_LIMIT);
+ if (shouldValidatePhysicalAddress) {
+ const addressLine1Error = getInvalidAddressErrorTranslationPath(values.addressLine1);
+ if (values.addressLine1 && addressLine1Error) {
+ errors.addressLine1 = translate(addressLine1Error);
}
- if (shouldValidatePhysicalAddress) {
- const addressLine1Error = getInvalidAddressErrorTranslationPath(values.addressLine1);
- if (values.addressLine1 && addressLine1Error) {
- errors.addressLine1 = translate(addressLine1Error);
- }
-
- const addressLine2Error = getInvalidAddressErrorTranslationPath(values.addressLine2);
- if (values.addressLine2 && addressLine2Error) {
- errors.addressLine2 = translate(addressLine2Error);
- }
+ const addressLine2Error = getInvalidAddressErrorTranslationPath(values.addressLine2);
+ if (values.addressLine2 && addressLine2Error) {
+ errors.addressLine2 = translate(addressLine2Error);
}
+ }
- // If no country is selected, default value is an empty string and there's no related regex data so we default to an empty object
- const countryRegexDetails = (values.country ? COMMON_CONST.COUNTRY_ZIP_REGEX_DATA?.[values.country] : {}) as CountryZipRegex;
+ // If no country is selected, default value is an empty string and there's no related regex data so we default to an empty object
+ const countryRegexDetails = (values.country ? COMMON_CONST.COUNTRY_ZIP_REGEX_DATA?.[values.country] : {}) as CountryZipRegex;
- // The postal code system might not exist for a country, so no regex either for them.
- const countrySpecificZipRegex = countryRegexDetails?.regex;
- const countryZipFormat = countryRegexDetails?.samples ?? '';
+ // The postal code system might not exist for a country, so no regex either for them.
+ const countrySpecificZipRegex = countryRegexDetails?.regex;
+ const countryZipFormat = countryRegexDetails?.samples ?? '';
- if (countrySpecificZipRegex) {
- if (!countrySpecificZipRegex.test(values.zipPostCode?.trim().toUpperCase())) {
- if (isRequiredFulfilled(values.zipPostCode?.trim())) {
- errors.zipPostCode = translate('privatePersonalDetails.error.incorrectZipFormat', countryZipFormat);
- } else {
- errors.zipPostCode = translate('common.error.fieldRequired');
- }
+ if (countrySpecificZipRegex) {
+ if (!countrySpecificZipRegex.test(values.zipPostCode?.trim().toUpperCase())) {
+ if (isRequiredFulfilled(values.zipPostCode?.trim())) {
+ errors.zipPostCode = translate('privatePersonalDetails.error.incorrectZipFormat', countryZipFormat);
+ } else {
+ errors.zipPostCode = translate('common.error.fieldRequired');
}
- } else if (!COMMON_CONST.GENERIC_ZIP_CODE_REGEX.test(values?.zipPostCode?.trim()?.toUpperCase() ?? '')) {
- errors.zipPostCode = translate('privatePersonalDetails.error.incorrectZipFormat');
}
-
- // TypeScript can't tell that the address input IDs are keys of a generic form, though every form in AddressFormID has them
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
- return (validate ? {...errors, ...validate(rawValues)} : errors) as FormInputErrors;
- },
- [translate, shouldHideCountrySelector, country, shouldRequireZip, shouldValidatePhysicalAddress, validate],
- );
+ } else if (!COMMON_CONST.GENERIC_ZIP_CODE_REGEX.test(values?.zipPostCode?.trim()?.toUpperCase() ?? '')) {
+ errors.zipPostCode = translate('privatePersonalDetails.error.incorrectZipFormat');
+ }
+
+ // TypeScript can't tell that the address input IDs are keys of a generic form, though every form in AddressFormID has them
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
+ return (validate ? {...errors, ...validate(rawValues)} : errors) as FormInputErrors;
+ };
return (
({
- language: preferredLocale,
- types: resultTypes,
- components: limitSearchesToCountry ? `country:${limitSearchesToCountry.toLocaleLowerCase()}` : undefined,
- ...(locationBias && {locationbias: locationBias}),
- }),
- [preferredLocale, resultTypes, limitSearchesToCountry, locationBias],
- );
+ const query = {
+ language: preferredLocale,
+ types: resultTypes,
+ components: limitSearchesToCountry ? `country:${limitSearchesToCountry.toLocaleLowerCase()}` : undefined,
+ ...(locationBias && {locationbias: locationBias}),
+ };
const shouldShowCurrentLocationButton = canUseCurrentLocation && searchValue.trim().length === 0 && isFocused;
const saveLocationDetails = (autocompleteData: GooglePlaceData, details: GooglePlaceDetail | null) => {
const addressComponents = details?.address_components;
@@ -371,15 +368,12 @@ function AddressSearch({
};
}, []);
- const filteredPredefinedPlaces = useMemo(() => {
- if (!searchValue) {
- return predefinedPlaces ?? [];
- }
- if (shouldHidePredefinedPlaces) {
- return [];
- }
- return predefinedPlaces?.filter((predefinedPlace) => isPlaceMatchForSearch(searchValue, predefinedPlace)) ?? [];
- }, [predefinedPlaces, searchValue, shouldHidePredefinedPlaces]);
+ let filteredPredefinedPlaces;
+ if (!searchValue) {
+ filteredPredefinedPlaces = predefinedPlaces ?? [];
+ } else {
+ filteredPredefinedPlaces = shouldHidePredefinedPlaces ? [] : (predefinedPlaces?.filter((predefinedPlace) => isPlaceMatchForSearch(searchValue, predefinedPlace)) ?? []);
+ }
const listEmptyComponent = isTyping ? (
) : undefined;
- const listLoader = useMemo(() => , []);
+ const listLoader = ;
return (
/*
diff --git a/src/components/AgentPromotionalBanner.tsx b/src/components/AgentPromotionalBanner.tsx
index 368192021385..eaef88947acc 100644
--- a/src/components/AgentPromotionalBanner.tsx
+++ b/src/components/AgentPromotionalBanner.tsx
@@ -9,7 +9,7 @@ import CONST from '@src/CONST';
import type {StyleProp, ViewStyle} from 'react-native';
-import React, {useMemo} from 'react';
+import React from 'react';
import {View} from 'react-native';
import Badge from './Badge';
@@ -51,30 +51,27 @@ function AgentPromotionalBanner({title, subtitle, onDismiss, dismissSentryLabel,
const hasCta = !!ctaText && !!onCtaPress;
- const titleNode = useMemo(
- () => (
-
-
- {title}
-
-
-
- ),
- [title, styles, translate],
+ const titleNode = (
+
+
+ {title}
+
+
+
);
- const rightComponent = useMemo(() => {
- if (!hasCta) {
- return null;
- }
- if (shouldUseNarrowLayout && !isInLandscapeMode) {
- return (
+ let rightComponent;
+ if (!hasCta) {
+ rightComponent = null;
+ } else {
+ rightComponent =
+ shouldUseNarrowLayout && !isInLandscapeMode ? (
+ ) : (
+
);
- }
- return (
-
- );
- }, [hasCta, shouldUseNarrowLayout, isInLandscapeMode, ctaText, onCtaPress, ctaSentryLabel, styles]);
+ }
return (
diff --git a/src/components/AmountWithoutCurrencyInput.tsx b/src/components/AmountWithoutCurrencyInput.tsx
index dceadfd0f354..003949e300d9 100644
--- a/src/components/AmountWithoutCurrencyInput.tsx
+++ b/src/components/AmountWithoutCurrencyInput.tsx
@@ -5,7 +5,7 @@ import {handleNegativeAmountFlipping, replaceAllDigits, replaceCommasWithPeriod,
import CONST from '@src/CONST';
-import React, {useCallback, useMemo} from 'react';
+import React from 'react';
import type {BaseTextInputProps} from './TextInput/BaseTextInput/types';
@@ -41,29 +41,22 @@ function AmountWithoutCurrencyInput({
...rest
}: AmountFormProps) {
const {toLocaleDigit} = useLocalize();
- const separator = useMemo(
- () =>
- replaceAllDigits('1.1', toLocaleDigit)
- .split('')
- .filter((char) => char !== '1')
- .join(''),
- [toLocaleDigit],
- );
+ const separator = replaceAllDigits('1.1', toLocaleDigit)
+ .split('')
+ .filter((char) => char !== '1')
+ .join('');
/**
* Sets the selection and the amount accordingly to the value passed to the input
* @param newAmount - Changed amount from user input
*/
- const setNewAmount = useCallback(
- (newAmount: string) => {
- // Remove spaces from the newAmount value because Safari on iOS adds spaces when pasting a copied value
- // More info: https://github.com/Expensify/App/issues/16974
- const newAmountWithoutSpaces = stripSpacesFromAmount(newAmount);
- const processedAmount = handleNegativeAmountFlipping(newAmountWithoutSpaces, allowFlippingAmount ?? false, toggleNegative);
- const replacedCommasAmount = replaceCommasWithPeriod(processedAmount);
- onInputChange?.(replacedCommasAmount);
- },
- [onInputChange, allowFlippingAmount, toggleNegative],
- );
+ const setNewAmount = (newAmount: string) => {
+ // Remove spaces from the newAmount value because Safari on iOS adds spaces when pasting a copied value
+ // More info: https://github.com/Expensify/App/issues/16974
+ const newAmountWithoutSpaces = stripSpacesFromAmount(newAmount);
+ const processedAmount = handleNegativeAmountFlipping(newAmountWithoutSpaces, allowFlippingAmount ?? false, toggleNegative);
+ const replacedCommasAmount = replaceCommasWithPeriod(processedAmount);
+ onInputChange?.(replacedCommasAmount);
+ };
// Add custom notation for using '-' character in the mask.
// If we only use '-' for characterSet instead of '0123456789.-'
diff --git a/src/components/ApproverSelectionList.tsx b/src/components/ApproverSelectionList.tsx
index 784eab25857d..2688a6b6bb6d 100644
--- a/src/components/ApproverSelectionList.tsx
+++ b/src/components/ApproverSelectionList.tsx
@@ -20,7 +20,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
import type {Policy} from '@src/types/onyx';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
-import React, {useCallback, useMemo} from 'react';
+import React from 'react';
import type {ListItem} from './SelectionList/types';
@@ -89,18 +89,15 @@ function ApproverSelectionList({
const lazyIllustrations = useMemoizedLazyIllustrations(['TurtleInShell']);
const shouldFooterBeInsideList = useShouldFooterBeInsideList();
- const handleSearchChange = useCallback(
- (term: string) => {
- setSearchTerm(term);
- if (onSearchChange) {
- onSearchChange(term);
- }
- },
- [onSearchChange, setSearchTerm],
- );
+ const handleSearchChange = (term: string) => {
+ setSearchTerm(term);
+ if (onSearchChange) {
+ onSearchChange(term);
+ }
+ };
- const selectedMembers = useMemo(() => allApprovers.filter((approver) => approver.isSelected), [allApprovers]);
- const selectedApproverKeys = useMemo(() => selectedMembers.map((approver) => approver.value?.toString() ?? '').filter(Boolean), [selectedMembers]);
+ const selectedMembers = allApprovers.filter((approver) => approver.isSelected);
+ const selectedApproverKeys = selectedMembers.map((approver) => approver.value?.toString() ?? '').filter(Boolean);
const initialSelectedApproverKeys = useInitialSelection(selectedApproverKeys, {isVisible: !shouldShowLoadingPlaceholder && allApprovers.length > 0, resetOnFocus: true});
const initiallyFocusedApproverKey = allApprovers.find((approver) => initialSelectedApproverKeys.includes(String(approver.value)))?.keyForList;
const selectionListKey = initialSelectedApproverKeys.join(',');
@@ -108,12 +105,9 @@ function ApproverSelectionList({
const shouldShowNotFoundView =
(isEmptyObject(policy) && !isLoadingReportData) || (shouldRequirePolicyAdmin && !isPolicyAdmin(policy)) || isPendingDeletePolicy(policy) || shouldShowNotFoundViewProp;
- const data = useMemo(() => {
- const sortedApprovers = sortAlphabetically(allApprovers, 'text', localeCompare);
- const orderedApprovers = moveInitialSelectionToTop(sortedApprovers, initialSelectedApproverKeys);
-
- return tokenizedSearch(orderedApprovers, getSearchValueForPhoneOrEmail(debouncedSearchTerm, countryCode), (option) => [option.text ?? '', option.login ?? '']);
- }, [allApprovers, debouncedSearchTerm, countryCode, localeCompare, initialSelectedApproverKeys]);
+ const sortedApprovers = sortAlphabetically(allApprovers, 'text', localeCompare);
+ const orderedApprovers = moveInitialSelectionToTop(sortedApprovers, initialSelectedApproverKeys);
+ const data = tokenizedSearch(orderedApprovers, getSearchValueForPhoneOrEmail(debouncedSearchTerm, countryCode), (option) => [option.text ?? '', option.login ?? '']);
const shouldShowListEmptyContent = !debouncedSearchTerm && !data.length && shouldShowListEmptyContentProp;
@@ -132,31 +126,25 @@ function ApproverSelectionList({
}
};
- const listEmptyContent = useMemo(
- () => (
-
- ),
- [translate, listEmptyContentSubtitle, styles.textSupporting, styles.pb10, lazyIllustrations.TurtleInShell],
+ const listEmptyContent = (
+
);
- const textInputOptions = useMemo(
- () => ({
- label: shouldShowListEmptyContent ? undefined : translate('selectionList.findMember'),
- value: searchTerm,
- onChangeText: handleSearchChange,
- headerMessage: searchTerm && !data?.length ? translate('common.noResultsFound') : '',
- }),
- [shouldShowListEmptyContent, translate, searchTerm, handleSearchChange, data?.length],
- );
+ const textInputOptions = {
+ label: shouldShowListEmptyContent ? undefined : translate('selectionList.findMember'),
+ value: searchTerm,
+ onChangeText: handleSearchChange,
+ headerMessage: searchTerm && !data?.length ? translate('common.noResultsFound') : '',
+ };
return (
{
- const rawFileName = getFileName(source);
- return cleanFileName(rawFileName);
- }, [source]);
+ const rawFileName = getFileName(source);
+ const fileName = cleanFileName(rawFileName);
const isVideo = typeof source === 'string' && Str.isVideo(source);
const isFileImage = checkIsFileImage(source, fileName);
diff --git a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.native.tsx b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.native.tsx
index e461ea57ddef..4bb33a0b08da 100644
--- a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.native.tsx
+++ b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.native.tsx
@@ -12,7 +12,7 @@ import {canUseTouchScreen as canUseTouchScreenUtil} from '@libs/DeviceCapabiliti
import variables from '@styles/variables';
-import React, {useCallback, useRef, useState} from 'react';
+import React, {useRef, useState} from 'react';
import {Keyboard, View} from 'react-native';
import type AttachmentCarouselViewProps from './types';
@@ -41,41 +41,35 @@ function AttachmentCarouselView({
const pagerRef = useRef(null);
/** Updates the page state when the user navigates between attachments */
- const updatePage = useCallback(
- (newPageIndex: number) => {
- Keyboard.dismiss();
- setShouldShowArrows(true);
+ const updatePage = (newPageIndex: number) => {
+ Keyboard.dismiss();
+ setShouldShowArrows(true);
- const item = attachments.at(newPageIndex);
+ const item = attachments.at(newPageIndex);
- setPage(newPageIndex);
- if (newPageIndex >= 0 && item) {
- setActiveAttachmentID(item.attachmentID ?? item.source);
- if (onNavigate) {
- onNavigate(item);
- }
+ setPage(newPageIndex);
+ if (newPageIndex >= 0 && item) {
+ setActiveAttachmentID(item.attachmentID ?? item.source);
+ if (onNavigate) {
+ onNavigate(item);
}
- },
- [setShouldShowArrows, attachments, setPage, onNavigate],
- );
+ }
+ };
/**
* Increments or decrements the index to get another selected item
- * @param {Number} deltaSlide
+ * @param deltaSlide
*/
- const cycleThroughAttachments = useCallback(
- (deltaSlide: number) => {
- if (page === undefined) {
- return;
- }
- const nextPageIndex = page + deltaSlide;
- updatePage(nextPageIndex);
- pagerRef.current?.setPage(nextPageIndex);
+ const cycleThroughAttachments = (deltaSlide: number) => {
+ if (page === undefined) {
+ return;
+ }
+ const nextPageIndex = page + deltaSlide;
+ updatePage(nextPageIndex);
+ pagerRef.current?.setPage(nextPageIndex);
- autoHideArrows();
- },
- [autoHideArrows, page, updatePage],
- );
+ autoHideArrows();
+ };
return (
PixelRatio.roundToNearestPixel(windowWidth - (modalStyles.marginHorizontal + modalStyles.borderWidth) * 2),
- [modalStyles.borderWidth, modalStyles.marginHorizontal, windowWidth],
- );
+ const cellWidth = PixelRatio.roundToNearestPixel(windowWidth - (modalStyles.marginHorizontal + modalStyles.borderWidth) * 2);
/** Updates the page state when the user navigates between attachments */
- const updatePage = useCallback(
- ({viewableItems}: UpdatePageProps) => {
- if (isFullScreenRef.current) {
- return;
- }
+ const updatePage = ({viewableItems}: UpdatePageProps) => {
+ if (isFullScreenRef.current) {
+ return;
+ }
- Keyboard.dismiss();
+ Keyboard.dismiss();
- // Since we can have only one item in view at a time, we can use the first item in the array
- // to get the index of the current page
- const entry = viewableItems.at(0);
- if (!entry) {
- setActiveAttachmentID(null);
- return;
- }
+ // Since we can have only one item in view at a time, we can use the first item in the array
+ // to get the index of the current page
+ const entry = viewableItems.at(0);
+ if (!entry) {
+ setActiveAttachmentID(null);
+ return;
+ }
- const item = entry.item;
- if (entry.index != null) {
- setPage(entry.index);
- setActiveAttachmentID(item.attachmentID ?? item.source);
- }
+ const item = entry.item;
+ if (entry.index != null) {
+ setPage(entry.index);
+ setActiveAttachmentID(item.attachmentID ?? item.source);
+ }
- if (onNavigate) {
- onNavigate(item);
- }
- },
- [isFullScreenRef, onNavigate, setPage, setActiveAttachmentID],
- );
+ if (onNavigate) {
+ onNavigate(item);
+ }
+ };
/** Increments or decrements the index to get another selected item, returning whether the carousel moved */
- const cycleThroughAttachments = useCallback(
- (deltaSlide: number) => {
- if (isFullScreen) {
- return false;
- }
+ const cycleThroughAttachments = (deltaSlide: number) => {
+ if (isFullScreen) {
+ return false;
+ }
- const nextIndex = page + deltaSlide;
- const nextItem = attachments.at(nextIndex);
+ const nextIndex = page + deltaSlide;
+ const nextItem = attachments.at(nextIndex);
- if (!nextItem || nextIndex < 0 || !scrollRef.current) {
- return false;
- }
+ if (!nextItem || nextIndex < 0 || !scrollRef.current) {
+ return false;
+ }
- scrollRef.current.scrollToIndex({index: nextIndex, animated: canUseTouchScreen});
- return true;
- },
- [attachments, canUseTouchScreen, isFullScreen, page, scrollRef],
- );
+ scrollRef.current.scrollToIndex({index: nextIndex, animated: canUseTouchScreen});
+ return true;
+ };
- const extractItemKey = useCallback(
- (item: Attachment) =>
- !!item.attachmentID || (typeof item.source !== 'string' && typeof item.source !== 'number')
- ? `attachmentID-${item.attachmentID}`
- : `source-${item.source}|${item.attachmentLink}`,
- [],
- );
+ const extractItemKey = (item: Attachment) =>
+ !!item.attachmentID || (typeof item.source !== 'string' && typeof item.source !== 'number') ? `attachmentID-${item.attachmentID}` : `source-${item.source}|${item.attachmentLink}`;
/** Calculate items layout information to optimize scrolling performance */
- const getItemLayout = useCallback(
- (data: ArrayLike | null | undefined, index: number) => ({
- length: cellWidth,
- offset: cellWidth * index,
- index,
- }),
- [cellWidth],
- );
+ const getItemLayout = (data: ArrayLike | null | undefined, index: number) => ({
+ length: cellWidth,
+ offset: cellWidth * index,
+ index,
+ });
/** Pan gesture handling swiping through attachments on touch screen devices */
const pan = usePanGesture({
@@ -193,42 +176,33 @@ function AttachmentCarouselView({
},
});
- const stateValue = useMemo(
- () => ({
- pagerItems: [{source, index: 0, isActive: true}],
- activePage: 0,
- pagerRef: scrollRef,
- pagerGesture: pan,
- isPagerScrolling,
- isScrollEnabled,
- }),
- [source, scrollRef, pan, isPagerScrolling, isScrollEnabled],
- );
-
- const actionsValue = useMemo(
- () => ({
- onTap: handleTap,
- onScaleChanged: handleScaleChange,
- onSwipeDown,
- onAttachmentError,
- }),
- [handleTap, handleScaleChange, onSwipeDown, onAttachmentError],
- );
+ const stateValue: AttachmentCarouselPagerStateContextType = {
+ pagerItems: [{source, index: 0, isActive: true}],
+ activePage: 0,
+ pagerRef: scrollRef,
+ pagerGesture: pan,
+ isPagerScrolling,
+ isScrollEnabled,
+ };
+
+ const actionsValue: AttachmentCarouselPagerActionsContextType = {
+ onTap: handleTap,
+ onScaleChanged: handleScaleChange,
+ onSwipeDown,
+ onAttachmentError,
+ };
/** Defines how a single attachment should be rendered */
- const renderItem = useCallback(
- ({item}: ListRenderItemInfo) => (
-
-
-
- ),
- [activeAttachmentID, canUseTouchScreen, cellWidth, handleTap, report?.reportID, shouldShowArrows, styles.h100],
+ const renderItem = ({item}: ListRenderItemInfo) => (
+
+
+
);
// Scroll position is affected when window width is resized, so we readjust it on width changes
diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx
index f7dbe5043d2f..ff2eea92284b 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/index.tsx
+++ b/src/components/Attachments/AttachmentCarousel/Pager/index.tsx
@@ -7,7 +7,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import type {ForwardedRef, SetStateAction} from 'react';
import type {NativeSyntheticEvent} from 'react-native';
-import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
+import React, {useEffect, useImperativeHandle, useRef, useState} from 'react';
import {View} from 'react-native';
import {GestureDetector, useNativeGesture} from 'react-native-gesture-handler';
import PagerView from 'react-native-pager-view';
@@ -78,36 +78,27 @@ function AttachmentCarouselPager({items, activeAttachmentID, initialPage, setSho
}, [activePage, initialPage]);
/** The `pagerItems` object that passed down to the context. Later used to detect current page, whether it's a single image gallery etc. */
- const pagerItems = useMemo(
- () => items.map((item, index) => ({source: item.source, previewSource: item.previewSource, index, isActive: index === activePageIndex, attachmentID: item.attachmentID})),
- [activePageIndex, items],
- );
+ const pagerItems = items.map((item, index) => ({source: item.source, previewSource: item.previewSource, index, isActive: index === activePageIndex, attachmentID: item.attachmentID}));
- const extractItemKey = useCallback((item: Attachment, index: number) => `attachmentID-${item.attachmentID}-${index}`, []);
+ const extractItemKey = (item: Attachment, index: number) => `attachmentID-${item.attachmentID}-${index}`;
const nativeGestureHandler = useNativeGesture();
- const stateValue = useMemo(
- () => ({
- pagerItems,
- activePage: activePageIndex,
- isPagerScrolling,
- isScrollEnabled,
- pagerRef,
- externalGestureHandler: nativeGestureHandler,
- }),
- [pagerItems, activePageIndex, isPagerScrolling, isScrollEnabled, nativeGestureHandler],
- );
-
- const actionsValue = useMemo(
- () => ({
- onTap: handleTap,
- onSwipeDown,
- onScaleChanged: handleScaleChange,
- onAttachmentError,
- }),
- [handleTap, onSwipeDown, handleScaleChange, onAttachmentError],
- );
+ const stateValue: AttachmentCarouselPagerStateContextType = {
+ pagerItems,
+ activePage: activePageIndex,
+ isPagerScrolling,
+ isScrollEnabled,
+ pagerRef,
+ externalGestureHandler: nativeGestureHandler,
+ };
+
+ const actionsValue: AttachmentCarouselPagerActionsContextType = {
+ onTap: handleTap,
+ onSwipeDown,
+ onScaleChanged: handleScaleChange,
+ onAttachmentError,
+ };
const animatedProps = useAnimatedProps(() => ({
scrollEnabled: isScrollEnabled.get(),
diff --git a/src/components/Attachments/AttachmentCarousel/index.tsx b/src/components/Attachments/AttachmentCarousel/index.tsx
index 427f2328a319..abb1c2611721 100644
--- a/src/components/Attachments/AttachmentCarousel/index.tsx
+++ b/src/components/Attachments/AttachmentCarousel/index.tsx
@@ -12,7 +12,7 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import {deepEqual} from 'fast-equals';
-import React, {useCallback, useEffect, useState} from 'react';
+import React, {useEffect, useState} from 'react';
import {View} from 'react-native';
import type {AttachmentCarouselProps} from './types';
@@ -50,11 +50,8 @@ function AttachmentCarousel({
setShouldShowArrows(true);
}, [canUseTouchScreen, page, setShouldShowArrows]);
- const compareImage = useCallback(
- (attachment: Attachment) =>
- (attachmentID ? attachment.attachmentID === attachmentID : attachment.source === source) && (!attachmentLink || attachment.attachmentLink === attachmentLink),
- [attachmentLink, attachmentID, source],
- );
+ const compareImage = (attachment: Attachment) =>
+ (attachmentID ? attachment.attachmentID === attachmentID : attachment.source === source) && (!attachmentLink || attachment.attachmentLink === attachmentLink);
useEffect(() => {
const parentReportAction = report.parentReportActionID && parentReportActions ? parentReportActions[report.parentReportActionID] : undefined;
diff --git a/src/components/Attachments/AttachmentCarousel/useCarouselArrows.ts b/src/components/Attachments/AttachmentCarousel/useCarouselArrows.ts
index 7645e9e15a5e..44feaa711245 100644
--- a/src/components/Attachments/AttachmentCarousel/useCarouselArrows.ts
+++ b/src/components/Attachments/AttachmentCarousel/useCarouselArrows.ts
@@ -4,7 +4,7 @@ import CONST from '@src/CONST';
import type {SetStateAction} from 'react';
-import {useCallback, useEffect, useRef, useState} from 'react';
+import {useEffect, useRef, useState} from 'react';
function useCarouselArrows() {
const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen();
@@ -14,17 +14,17 @@ function useCarouselArrows() {
/**
* Cancels the automatic hiding of the arrows.
*/
- const cancelAutoHideArrows = useCallback(() => {
+ const cancelAutoHideArrows = () => {
if (!autoHideArrowTimeout.current) {
return;
}
clearTimeout(autoHideArrowTimeout.current);
- }, []);
+ };
/**
* Automatically hide the arrows if there is no interaction for 3 seconds.
*/
- const autoHideArrows = useCallback(() => {
+ const autoHideArrows = () => {
if (!canUseTouchScreen) {
return;
}
@@ -33,18 +33,15 @@ function useCarouselArrows() {
autoHideArrowTimeout.current = setTimeout(() => {
setShouldShowArrowsInternal(false);
}, CONST.ARROW_HIDE_DELAY);
- }, [canUseTouchScreen, cancelAutoHideArrows]);
+ };
/**
* Sets the visibility of the arrows.
*/
- const setShouldShowArrows = useCallback(
- (show: SetStateAction = true) => {
- setShouldShowArrowsInternal(show);
- autoHideArrows();
- },
- [autoHideArrows],
- );
+ const setShouldShowArrows = (show: SetStateAction = true) => {
+ setShouldShowArrowsInternal(show);
+ autoHideArrows();
+ };
useEffect(() => {
autoHideArrows();
diff --git a/src/components/Attachments/AttachmentCarousel/useCarouselContextEvents.ts b/src/components/Attachments/AttachmentCarousel/useCarouselContextEvents.ts
index fd3c37bcb3a4..ccb07c3ab280 100644
--- a/src/components/Attachments/AttachmentCarousel/useCarouselContextEvents.ts
+++ b/src/components/Attachments/AttachmentCarousel/useCarouselContextEvents.ts
@@ -1,6 +1,6 @@
import type {SetStateAction} from 'react';
-import {useCallback, useRef} from 'react';
+import {useRef} from 'react';
import {useSharedValue} from 'react-native-reanimated';
function useCarouselContextEvents(setShouldShowArrows?: (show?: SetStateAction) => void) {
@@ -10,56 +10,47 @@ function useCarouselContextEvents(setShouldShowArrows?: (show?: SetStateAction {
- if (showArrows === undefined) {
- setShouldShowArrows?.((prevShouldShowArrows) => !prevShouldShowArrows);
- return;
- }
+ const onRequestToggleArrows = (showArrows?: boolean) => {
+ if (showArrows === undefined) {
+ setShouldShowArrows?.((prevShouldShowArrows) => !prevShouldShowArrows);
+ return;
+ }
- setShouldShowArrows?.(showArrows);
- },
- [setShouldShowArrows],
- );
+ setShouldShowArrows?.(showArrows);
+ };
/**
* This callback is passed to the MultiGestureCanvas/Lightbox through the AttachmentCarouselPagerContext.
* It is used to react to zooming/pinching and (mostly) enabling/disabling scrolling on the pager,
* as well as enabling/disabling the carousel buttons.
*/
- const handleScaleChange = useCallback(
- (newScale: number) => {
- if (newScale === scale.current) {
- return;
- }
+ const handleScaleChange = (newScale: number) => {
+ if (newScale === scale.current) {
+ return;
+ }
- scale.current = newScale;
+ scale.current = newScale;
- const newIsScrollEnabled = newScale === 1;
- if (isScrollEnabled.get() === newIsScrollEnabled) {
- return;
- }
+ const newIsScrollEnabled = newScale === 1;
+ if (isScrollEnabled.get() === newIsScrollEnabled) {
+ return;
+ }
- isScrollEnabled.set(newIsScrollEnabled);
- onRequestToggleArrows(newIsScrollEnabled);
- },
- [isScrollEnabled, onRequestToggleArrows],
- );
+ isScrollEnabled.set(newIsScrollEnabled);
+ onRequestToggleArrows(newIsScrollEnabled);
+ };
/**
* This callback is passed to the MultiGestureCanvas/Lightbox through the AttachmentCarouselPagerContext.
* It is used to trigger touch events on the pager when the user taps on the MultiGestureCanvas/Lightbox.
*/
- const handleTap = useCallback(
- (shouldShowArrows?: boolean) => {
- if (!isScrollEnabled.get()) {
- return;
- }
+ const handleTap = (shouldShowArrows?: boolean) => {
+ if (!isScrollEnabled.get()) {
+ return;
+ }
- onRequestToggleArrows(shouldShowArrows);
- },
- [isScrollEnabled, onRequestToggleArrows],
- );
+ onRequestToggleArrows(shouldShowArrows);
+ };
return {handleTap, handleScaleChange, scale, isScrollEnabled};
}
diff --git a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.tsx b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.tsx
index 96b2b908945a..337f75f2d713 100644
--- a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.tsx
+++ b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.tsx
@@ -6,7 +6,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
-import React, {memo} from 'react';
+import React from 'react';
import type {AttachmentViewProps} from '..';
@@ -48,4 +48,4 @@ function AttachmentViewImage({attachmentID, url, file, isAuthTokenRequired, load
);
}
-export default memo(AttachmentViewImage);
+export default AttachmentViewImage;
diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.tsx b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.tsx
index 6a927f31acdd..9e8b24645688 100644
--- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.tsx
+++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.tsx
@@ -3,7 +3,7 @@ import PDFView from '@components/PDFView';
import type {GestureResponderEvent} from 'react-native';
-import React, {memo, useCallback, useEffect} from 'react';
+import React, {useEffect} from 'react';
import type AttachmentViewPdfProps from './types';
@@ -37,19 +37,16 @@ function BaseAttachmentViewPdf({
* because we want to disable the pager scroll when the pdf is zoomed in,
* as well as call the onScaleChanged prop of the AttachmentViewPdf component if defined.
*/
- const onScaleChanged = useCallback(
- (newScale: number) => {
- if (onScaleChangedProp !== undefined) {
- onScaleChangedProp(newScale);
- }
+ const onScaleChanged = (newScale: number) => {
+ if (onScaleChangedProp !== undefined) {
+ onScaleChangedProp(newScale);
+ }
- // When a pdf is shown in a carousel, we want to disable the pager scroll when the pdf is zoomed in
- if (state?.pagerRef && actions) {
- actions.onScaleChanged?.(newScale);
- }
- },
- [state?.pagerRef, actions, onScaleChangedProp],
- );
+ // When a pdf is shown in a carousel, we want to disable the pager scroll when the pdf is zoomed in
+ if (state?.pagerRef && actions) {
+ actions.onScaleChanged?.(newScale);
+ }
+ };
/**
* This callback is used to pass-through the onPress event from the AttachmentViewPdf's props
@@ -57,18 +54,15 @@ function BaseAttachmentViewPdf({
* The onTap event should only be triggered, if the pager is currently scrollable.
* Otherwise it means that the PDF is currently zoomed in, therefore the onTap callback should be ignored
*/
- const onPress = useCallback(
- (event?: GestureResponderEvent | KeyboardEvent) => {
- if (onPressProp !== undefined) {
- onPressProp(event);
- }
+ const onPress = (event?: GestureResponderEvent | KeyboardEvent) => {
+ if (onPressProp !== undefined) {
+ onPressProp(event);
+ }
- if (state !== null && actions && isScrollEnabled?.get()) {
- actions.onTap?.();
- }
- },
- [state, actions, isScrollEnabled, onPressProp],
- );
+ if (state !== null && actions && isScrollEnabled?.get()) {
+ actions.onTap?.();
+ }
+ };
return (
(
- {
- // The react-native-pdf's onScaleChanged event will sometimes give us scale values of e.g. 0.99... instead of 1,
- // even though we're not pinching/zooming
- // Rounding the scale value to 2 decimal place fixes this issue, since pinching will still be possible but very small pinches are ignored.
- scale.set(Math.round(newScale * 1e2) / 1e2);
- }}
- />
- ),
- [props, scale],
+ const Content = (
+ {
+ // The react-native-pdf's onScaleChanged event will sometimes give us scale values of e.g. 0.99... instead of 1,
+ // even though we're not pinching/zooming
+ // Rounding the scale value to 2 decimal place fixes this issue, since pinching will still be possible but very small pinches are ignored.
+ scale.set(Math.round(newScale * 1e2) / 1e2);
+ }}
+ />
);
return (
@@ -100,4 +97,4 @@ function AttachmentViewPdf(props: AttachmentViewPdfProps) {
);
}
-export default memo(AttachmentViewPdf);
+export default AttachmentViewPdf;
diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.ios.tsx b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.ios.tsx
index 392ddc79f55c..51b4eb04a136 100644
--- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.ios.tsx
+++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.ios.tsx
@@ -1,4 +1,4 @@
-import React, {memo} from 'react';
+import React from 'react';
import type AttachmentViewPdfProps from './types';
@@ -8,4 +8,4 @@ function AttachmentViewPdf(props: AttachmentViewPdfProps) {
return ;
}
-export default memo(AttachmentViewPdf);
+export default AttachmentViewPdf;
diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx
index f6797a571c9f..f51536c94d13 100644
--- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx
+++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx
@@ -1,6 +1,6 @@
import PDFView from '@components/PDFView';
-import React, {memo} from 'react';
+import React from 'react';
import type AttachmentViewPdfProps from './types';
@@ -36,4 +36,4 @@ function AttachmentViewPdf({
);
}
-export default memo(AttachmentViewPdf);
+export default AttachmentViewPdf;
diff --git a/src/components/Attachments/AttachmentView/AttachmentViewVideo/index.tsx b/src/components/Attachments/AttachmentView/AttachmentViewVideo/index.tsx
index d920dccc1c4e..10d2d6ef3a69 100644
--- a/src/components/Attachments/AttachmentView/AttachmentViewVideo/index.tsx
+++ b/src/components/Attachments/AttachmentView/AttachmentViewVideo/index.tsx
@@ -43,4 +43,4 @@ function AttachmentViewVideo({source, fileName, isHovered = false, shouldUseShar
AttachmentViewVideo.displayName = 'AttachmentViewVideo';
-export default React.memo(AttachmentViewVideo);
+export default AttachmentViewVideo;
diff --git a/src/components/Attachments/AttachmentView/index.tsx b/src/components/Attachments/AttachmentView/index.tsx
index 45f937f24f9f..7c685f449d7a 100644
--- a/src/components/Attachments/AttachmentView/index.tsx
+++ b/src/components/Attachments/AttachmentView/index.tsx
@@ -42,7 +42,7 @@ import type {GestureResponderEvent, ImageURISource, StyleProp, ViewStyle} from '
import type {OnyxEntry} from 'react-native-onyx';
import {SafeString, Str} from 'expensify-common';
-import React, {memo, useEffect, useState} from 'react';
+import React, {useEffect, useState} from 'react';
import {View} from 'react-native';
import AttachmentViewImage from './AttachmentViewImage';
@@ -433,7 +433,7 @@ function AttachmentView({
);
}
-export default memo(AttachmentView);
+export default AttachmentView;
export {checkIsFileImage};
export type {AttachmentViewProps};
diff --git a/src/components/Attachments/AttachmentView/useAttachmentErrors.ts b/src/components/Attachments/AttachmentView/useAttachmentErrors.ts
index 44a634754663..99ca2eb26015 100644
--- a/src/components/Attachments/AttachmentView/useAttachmentErrors.ts
+++ b/src/components/Attachments/AttachmentView/useAttachmentErrors.ts
@@ -1,6 +1,6 @@
import type {AttachmentSource} from '@components/Attachments/types';
-import {useCallback, useState} from 'react';
+import {useState} from 'react';
function convertSourceToString(source: AttachmentSource) {
if (typeof source === 'string' || typeof source === 'number') {
@@ -26,7 +26,7 @@ function convertSourceToString(source: AttachmentSource) {
function useAttachmentErrors() {
const [attachmentErrors, setAttachmentErrors] = useState>({});
- const setAttachmentError = useCallback((key: AttachmentSource, state = true) => {
+ const setAttachmentError = (key: AttachmentSource, state = true) => {
const url = convertSourceToString(key);
if (!url) {
return;
@@ -35,13 +35,13 @@ function useAttachmentErrors() {
...prevState,
[url]: state,
}));
- }, []);
+ };
- const clearAttachmentErrors = useCallback(() => {
+ const clearAttachmentErrors = () => {
setAttachmentErrors({});
- }, []);
+ };
- const isErrorInAttachment = useCallback((key: AttachmentSource) => attachmentErrors?.[convertSourceToString(key)], [attachmentErrors]);
+ const isErrorInAttachment = (key: AttachmentSource) => attachmentErrors?.[convertSourceToString(key)];
return {setAttachmentError, clearAttachmentErrors, isErrorInAttachment};
}
diff --git a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx
index 142fefc881bb..25483fc773a9 100644
--- a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx
+++ b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx
@@ -10,7 +10,7 @@ import CONST from '@src/CONST';
import type {ReactElement} from 'react';
-import React, {useCallback, useEffect, useRef} from 'react';
+import React, {useEffect, useRef} from 'react';
import {FlatList} from 'react-native-gesture-handler';
import Animated, {Easing, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
@@ -41,22 +41,19 @@ function BaseAutoCompleteSuggestionsImpl({
/**
* Render a suggestion menu item component.
*/
- const renderItem = useCallback(
- ({item, index}: RenderSuggestionMenuItemProps): ReactElement => (
- StyleUtils.getAutoCompleteSuggestionItemStyle(highlightedSuggestionIndex, CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT, hovered, index)}
- hoverDimmingValue={1}
- onMouseDown={(e) => e.preventDefault()}
- onPress={() => onSelect(index)}
- onLongPress={() => {}}
- accessibilityLabel={accessibilityLabelExtractor(item, index)}
- role={CONST.ROLE.MENUITEM}
- sentryLabel={CONST.SENTRY_LABEL.BASE_AUTO_COMPLETE_SUGGESTIONS.MENU_ITEM}
- >
- {renderSuggestionMenuItem(item, index)}
-
- ),
- [accessibilityLabelExtractor, renderSuggestionMenuItem, StyleUtils, highlightedSuggestionIndex, onSelect],
+ const renderItem = ({item, index}: RenderSuggestionMenuItemProps): ReactElement => (
+ StyleUtils.getAutoCompleteSuggestionItemStyle(highlightedSuggestionIndex, CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT, hovered, index)}
+ hoverDimmingValue={1}
+ onMouseDown={(e) => e.preventDefault()}
+ onPress={() => onSelect(index)}
+ onLongPress={() => {}}
+ accessibilityLabel={accessibilityLabelExtractor(item, index)}
+ role={CONST.ROLE.MENUITEM}
+ sentryLabel={CONST.SENTRY_LABEL.BASE_AUTO_COMPLETE_SUGGESTIONS.MENU_ITEM}
+ >
+ {renderSuggestionMenuItem(item, index)}
+
);
const innerHeight = CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT * suggestions.length;
diff --git a/src/components/AutoSubmitModal.tsx b/src/components/AutoSubmitModal.tsx
index 14f23760e0d0..254e9822e836 100644
--- a/src/components/AutoSubmitModal.tsx
+++ b/src/components/AutoSubmitModal.tsx
@@ -18,7 +18,7 @@ import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
import type IconAsset from '@src/types/utils/IconAsset';
-import React, {useMemo, useRef} from 'react';
+import React, {useRef} from 'react';
import {View} from 'react-native';
import CenteredModalLayout from './CenteredModalLayout';
@@ -32,26 +32,22 @@ function AutoSubmitModal() {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const illustrations = useMemoizedLazyIllustrations(['PaperAirplane', 'Pencil', 'ReceiptsStackedOnPin']);
- const menuSections = useMemo(
- () =>
- [
- {
- icon: illustrations.PaperAirplane,
- titleTranslationKey: 'autoSubmitModal.submittedExpensesTitle',
- descriptionTranslationKey: 'autoSubmitModal.submittedExpensesDescription',
- },
- {
- icon: illustrations.Pencil,
- titleTranslationKey: 'autoSubmitModal.pendingExpensesTitle',
- descriptionTranslationKey: 'autoSubmitModal.pendingExpensesDescription',
- },
- ] satisfies Array<{
- icon: IconAsset;
- titleTranslationKey: TranslationPaths;
- descriptionTranslationKey: TranslationPaths;
- }>,
- [illustrations.PaperAirplane, illustrations.Pencil],
- );
+ const menuSections = [
+ {
+ icon: illustrations.PaperAirplane,
+ titleTranslationKey: 'autoSubmitModal.submittedExpensesTitle',
+ descriptionTranslationKey: 'autoSubmitModal.submittedExpensesDescription',
+ },
+ {
+ icon: illustrations.Pencil,
+ titleTranslationKey: 'autoSubmitModal.pendingExpensesTitle',
+ descriptionTranslationKey: 'autoSubmitModal.pendingExpensesDescription',
+ },
+ ] satisfies Array<{
+ icon: IconAsset;
+ titleTranslationKey: TranslationPaths;
+ descriptionTranslationKey: TranslationPaths;
+ }>;
const willShowAgainRef = useRef(null);
diff --git a/src/components/AvatarCropModal/AvatarCropView.tsx b/src/components/AvatarCropModal/AvatarCropView.tsx
index 28c67a1d1e35..2ef9e0f22828 100644
--- a/src/components/AvatarCropModal/AvatarCropView.tsx
+++ b/src/components/AvatarCropModal/AvatarCropView.tsx
@@ -23,7 +23,7 @@ import type IconAsset from '@src/types/utils/IconAsset';
import type {LayoutChangeEvent} from 'react-native';
import type {PanGestureActiveEvent} from 'react-native-gesture-handler';
-import React, {useCallback, useEffect, useState} from 'react';
+import React, {useEffect, useState} from 'react';
import {View} from 'react-native';
import {GestureHandlerRootView, usePanGesture} from 'react-native-gesture-handler';
import ImageSize from 'react-native-image-size';
@@ -90,19 +90,19 @@ function AvatarCropView({imageUri = '', imageName = '', imageType = '', onClose,
const [isImageInitialized, setIsImageInitialized] = useState(false);
// An onLayout callback, that initializes the image container, for proper render of an image
- const initializeImageContainer = useCallback((event: LayoutChangeEvent) => {
+ const initializeImageContainer = (event: LayoutChangeEvent) => {
setIsImageContainerInitialized(true);
const {height, width} = event.nativeEvent.layout;
// Even if the browser height is reduced too much, the relative height should not be negative
const relativeHeight = Math.max(height - CONST.AVATAR_CROP_MODAL.CONTAINER_VERTICAL_MARGIN, CONST.AVATAR_CROP_MODAL.INITIAL_SIZE);
setImageContainerSize(Math.floor(Math.min(relativeHeight, width)));
- }, []);
+ };
// An onLayout callback, that initializes the slider container size, for proper render of a slider
- const initializeSliderContainer = useCallback((event: LayoutChangeEvent) => {
+ const initializeSliderContainer = (event: LayoutChangeEvent) => {
setSliderContainerSize(event.nativeEvent.layout.width);
- }, []);
+ };
// In order to calculate proper image position/size/animation, we have to know its size.
// And we have to update image size if image url changes.
@@ -134,16 +134,16 @@ function AvatarCropView({imageUri = '', imageName = '', imageType = '', onClose,
/**
* Validates that value is within the provided mix/max range.
*/
- const clamp = useCallback((value: number, [min, max]: [number, number]) => {
+ const clamp = (value: number, [min, max]: [number, number]) => {
'worklet';
return interpolate(value, [min, max], [min, max], 'clamp');
- }, []);
+ };
/**
* Returns current image size taking into account scale and rotation.
*/
- const getDisplayedImageSize = useCallback(() => {
+ const getDisplayedImageSize = () => {
'worklet';
let height = imageContainerSize * scale.get();
@@ -158,32 +158,29 @@ function AvatarCropView({imageUri = '', imageName = '', imageType = '', onClose,
}
return {height, width};
- }, [imageContainerSize, originalImageHeight, originalImageWidth, scale]);
+ };
/**
* Validates the offset to prevent overflow, and updates the image offset.
*/
- const updateImageOffset = useCallback(
- (offsetX: number, offsetY: number) => {
- 'worklet';
+ const updateImageOffset = (offsetX: number, offsetY: number) => {
+ 'worklet';
- const {height, width} = getDisplayedImageSize();
- const maxOffsetX = (width - imageContainerSize) / 2;
- const maxOffsetY = (height - imageContainerSize) / 2;
- translateX.set(clamp(offsetX, [maxOffsetX * -1, maxOffsetX]));
- translateY.set(clamp(offsetY, [maxOffsetY * -1, maxOffsetY]));
- prevMaxOffsetX.set(maxOffsetX);
- prevMaxOffsetY.set(maxOffsetY);
- },
- [getDisplayedImageSize, imageContainerSize, translateX, clamp, translateY, prevMaxOffsetX, prevMaxOffsetY],
- );
+ const {height, width} = getDisplayedImageSize();
+ const maxOffsetX = (width - imageContainerSize) / 2;
+ const maxOffsetY = (height - imageContainerSize) / 2;
+ translateX.set(clamp(offsetX, [maxOffsetX * -1, maxOffsetX]));
+ translateY.set(clamp(offsetY, [maxOffsetY * -1, maxOffsetY]));
+ prevMaxOffsetX.set(maxOffsetX);
+ prevMaxOffsetY.set(maxOffsetY);
+ };
- const newScaleValue = useCallback((newSliderValue: number, containerSize: number) => {
+ const newScaleValue = (newSliderValue: number, containerSize: number) => {
'worklet';
const {MAX_SCALE, MIN_SCALE} = CONST.AVATAR_CROP_MODAL;
return (newSliderValue / containerSize) * (MAX_SCALE - MIN_SCALE) + MIN_SCALE;
- }, []);
+ };
/**
* Calculates new x & y image translate value on image panning
@@ -262,7 +259,7 @@ function AvatarCropView({imageUri = '', imageName = '', imageType = '', onClose,
// Rotates the image by changing the rotation value by 90 degrees
// and updating the position so the image remains in the same place after rotation
- const rotateImage = useCallback(() => {
+ const rotateImage = () => {
rotation.set((value) => value - 90);
const oldTranslateX = translateX.get();
@@ -272,11 +269,11 @@ function AvatarCropView({imageUri = '', imageName = '', imageType = '', onClose,
const oldOriginalImageHeight = originalImageHeight.get();
originalImageHeight.set(originalImageWidth.get());
originalImageWidth.set(oldOriginalImageHeight);
- }, [originalImageHeight, originalImageWidth, rotation, translateX, translateY]);
+ };
// Crops an image that was provided in the imageUri prop, using the current position/scale
// then calls onSave callback
- const cropAndSaveImage = useCallback(() => {
+ const cropAndSaveImage = () => {
if (isLoading.get()) {
return;
}
@@ -312,7 +309,7 @@ function AvatarCropView({imageUri = '', imageName = '', imageType = '', onClose,
.catch(() => {
isLoading.set(false);
});
- }, [isLoading, originalImageHeight, originalImageWidth, scale, translateX, imageContainerSize, translateY, imageType, imageName, imageUri, rotation, onSave]);
+ };
const sliderOnPress = (locationX: number) => {
// We are using the worklet directive here and running on the UI thread to ensure the Reanimated
diff --git a/src/components/AvatarCropModal/ImageCropView.tsx b/src/components/AvatarCropModal/ImageCropView.tsx
index f53f9d932d63..d775278a86e5 100644
--- a/src/components/AvatarCropModal/ImageCropView.tsx
+++ b/src/components/AvatarCropModal/ImageCropView.tsx
@@ -102,6 +102,5 @@ function ImageCropView({imageUri = '', containerSize = 0, panGesture, maskImage,
ImageCropView.displayName = 'ImageCropView';
-// React.memo is needed here to prevent styles recompilation
// which sometimes may cause glitches during rerender of the modal
-export default React.memo(ImageCropView);
+export default ImageCropView;
diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx
index 6b479928c6e9..504bea79b22e 100644
--- a/src/components/Badge.tsx
+++ b/src/components/Badge.tsx
@@ -8,7 +8,7 @@ import type IconAsset from '@src/types/utils/IconAsset';
import type {GestureResponderEvent, PressableStateCallbackType, StyleProp, TextStyle, ViewStyle} from 'react-native';
-import React, {useCallback} from 'react';
+import React from 'react';
import {View} from 'react-native';
import Icon from './Icon';
@@ -83,17 +83,14 @@ function Badge({
const iconSize = isCondensed || shouldUseXXSmallIcon ? variables.iconSizeXXSmall : variables.iconSizeExtraSmall;
- const wrapperStyles: (state: PressableStateCallbackType) => StyleProp = useCallback(
- ({pressed}) => [
- styles.defaultBadge,
- isCondensed && styles.condensedBadge,
- styles.alignSelfCenter,
- styles.ml2,
- StyleUtils.getBadgeColorStyle(success, error, pressed, environment === CONST.ENVIRONMENT.ADHOC, isStrong),
- badgeStyles,
- ],
- [styles.defaultBadge, styles.condensedBadge, styles.alignSelfCenter, styles.ml2, StyleUtils, success, error, environment, badgeStyles, isCondensed, isStrong],
- );
+ const wrapperStyles: (state: PressableStateCallbackType) => StyleProp = ({pressed}) => [
+ styles.defaultBadge,
+ isCondensed && styles.condensedBadge,
+ styles.alignSelfCenter,
+ styles.ml2,
+ StyleUtils.getBadgeColorStyle(success, error, pressed, environment === CONST.ENVIRONMENT.ADHOC, isStrong),
+ badgeStyles,
+ ];
if (!text && !icon) {
return null;
diff --git a/src/components/Banner.tsx b/src/components/Banner.tsx
index 817bfb01e14a..94d0bdfd0587 100644
--- a/src/components/Banner.tsx
+++ b/src/components/Banner.tsx
@@ -11,7 +11,7 @@ import type IconAsset from '@src/types/utils/IconAsset';
import type {StyleProp, TextStyle, ViewStyle} from 'react-native';
-import React, {memo} from 'react';
+import React from 'react';
import {View} from 'react-native';
import Hoverable from './Hoverable';
@@ -140,6 +140,6 @@ function Banner({
);
}
-export default memo(Banner);
+export default Banner;
export type {BannerProps};
diff --git a/src/components/BlockingViews/BlockingView.tsx b/src/components/BlockingViews/BlockingView.tsx
index c7e4d921c95d..c0b6fd64e11a 100644
--- a/src/components/BlockingViews/BlockingView.tsx
+++ b/src/components/BlockingViews/BlockingView.tsx
@@ -23,7 +23,7 @@ import type {SvgProps} from 'react-native-svg';
import type {WebStyle} from 'react-native-web';
import type {MergeExclusive} from 'type-fest';
-import React, {useMemo} from 'react';
+import React from 'react';
import {View} from 'react-native';
import BlockingViewSubtitle from './BlockingViewSubtitle';
@@ -123,10 +123,7 @@ function BlockingView({
const styles = useThemeStyles();
const {translate} = useLocalize();
const SubtitleWrapper = shouldEmbedLinkWithSubtitle ? Text : View;
- const subtitleWrapperStyle = useMemo(
- () => (shouldEmbedLinkWithSubtitle ? [styles.textAlignCenter] : [styles.alignItemsCenter, styles.justifyContentCenter]),
- [shouldEmbedLinkWithSubtitle, styles],
- );
+ const subtitleWrapperStyle = shouldEmbedLinkWithSubtitle ? [styles.textAlignCenter] : [styles.alignItemsCenter, styles.justifyContentCenter];
const containerStyle = useBottomSafeSafeAreaPaddingStyle({addBottomSafeAreaPadding, addOfflineIndicatorBottomSafeAreaPadding, style: containerStyleProp});
useAbsentPageSpan();
diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx
index 5d5bd3bc762f..b14ba1ce3fef 100644
--- a/src/components/Button/Button.tsx
+++ b/src/components/Button/Button.tsx
@@ -13,7 +13,7 @@ import CONST from '@src/CONST';
import type {StyleProp, ViewStyle} from 'react-native';
import type {ValueOf} from 'type-fest';
-import React, {useMemo, useState} from 'react';
+import React, {useState} from 'react';
import {StyleSheet, View} from 'react-native';
import type {ButtonProps} from './types';
@@ -59,73 +59,58 @@ function Button({
const StyleUtils = useStyleUtils();
const [isHovered, setIsHovered] = useState(false);
- const contextValue = useMemo(
- () => ({
- isHovered,
- variant,
- size,
- onPress,
- isDisabled,
- isLoading,
- }),
- [isHovered, variant, size, onPress, isDisabled, isLoading],
- );
-
- const buttonVariantStyles = useMemo(() => {
- const shouldUseDisabledStyles = isDisabled && !stayNormalOnDisable;
- if (!variant) {
- return shouldUseDisabledStyles ? [styles.buttonOpacityDisabled, styles.buttonDisabled] : undefined;
- }
-
+ const contextValue = {
+ isHovered,
+ variant,
+ size,
+ onPress,
+ isDisabled,
+ isLoading,
+ };
+
+ let buttonVariantStyles;
+ const shouldUseDisabledStyles = isDisabled && !stayNormalOnDisable;
+ if (!variant) {
+ buttonVariantStyles = shouldUseDisabledStyles ? [styles.buttonOpacityDisabled, styles.buttonDisabled] : undefined;
+ } else {
const {normal: defaultStyles, disabled: disabledStyles} = StyleUtils.getButtonVariantStyles(styles);
- return [defaultStyles[variant], shouldUseDisabledStyles && disabledStyles[variant]];
- }, [isDisabled, stayNormalOnDisable, styles, variant, StyleUtils]);
-
- const borderRadiusStyles = useMemo, StyleProp>>(
- () => ({
- [CONST.BUTTON_REMOVE_BORDER_RADIUS.RIGHT]: styles.noRightBorderRadius,
- [CONST.BUTTON_REMOVE_BORDER_RADIUS.LEFT]: styles.noLeftBorderRadius,
- [CONST.BUTTON_REMOVE_BORDER_RADIUS.ALL]: [styles.noRightBorderRadius, styles.noLeftBorderRadius],
- }),
- [styles.noRightBorderRadius, styles.noLeftBorderRadius],
- );
-
- const horizontalPaddingBySize = useMemo, ViewStyle>>(
- () => ({
- [CONST.BUTTON_SIZE.SMALL]: styles.ph2,
- [CONST.BUTTON_SIZE.MEDIUM]: styles.ph3,
- [CONST.BUTTON_SIZE.LARGE]: styles.ph4,
- }),
- [styles.ph2, styles.ph3, styles.ph4],
- );
-
- const buttonStyles = useMemo>(
- () => [
- styles.button,
- StyleUtils.getButtonSizeStyle(styles, size),
- horizontalPaddingBySize[size],
- buttonVariantStyles,
- removeBorderRadius ? borderRadiusStyles[removeBorderRadius] : undefined,
- styles.alignItemsStretch,
- innerStyles,
- ],
- [styles, StyleUtils, size, horizontalPaddingBySize, buttonVariantStyles, removeBorderRadius, borderRadiusStyles, innerStyles],
- );
-
- const buttonContainerStyles = useMemo>(() => [buttonStyles, blendOpacity && styles.buttonBlendContainer], [buttonStyles, blendOpacity, styles.buttonBlendContainer]);
-
- const buttonBlendForegroundStyle = useMemo>(() => {
- if (!blendOpacity) {
- return undefined;
- }
+ buttonVariantStyles = [defaultStyles[variant], shouldUseDisabledStyles && disabledStyles[variant]];
+ }
+ const borderRadiusStyles: Record, StyleProp> = {
+ [CONST.BUTTON_REMOVE_BORDER_RADIUS.RIGHT]: styles.noRightBorderRadius,
+ [CONST.BUTTON_REMOVE_BORDER_RADIUS.LEFT]: styles.noLeftBorderRadius,
+ [CONST.BUTTON_REMOVE_BORDER_RADIUS.ALL]: [styles.noRightBorderRadius, styles.noLeftBorderRadius],
+ };
+
+ const horizontalPaddingBySize: Record, ViewStyle> = {
+ [CONST.BUTTON_SIZE.SMALL]: styles.ph2,
+ [CONST.BUTTON_SIZE.MEDIUM]: styles.ph3,
+ [CONST.BUTTON_SIZE.LARGE]: styles.ph4,
+ };
+
+ const buttonStyles: StyleProp = [
+ styles.button,
+ StyleUtils.getButtonSizeStyle(styles, size),
+ horizontalPaddingBySize[size],
+ buttonVariantStyles,
+ removeBorderRadius ? borderRadiusStyles[removeBorderRadius] : undefined,
+ styles.alignItemsStretch,
+ innerStyles,
+ ];
+
+ const buttonContainerStyles: StyleProp = [buttonStyles, blendOpacity && styles.buttonBlendContainer];
+
+ let buttonBlendForegroundStyle: StyleProp;
+ if (!blendOpacity) {
+ buttonBlendForegroundStyle = undefined;
+ } else {
const {backgroundColor, opacity} = StyleSheet.flatten(buttonStyles);
-
- return {
+ buttonBlendForegroundStyle = {
backgroundColor,
opacity,
};
- }, [buttonStyles, blendOpacity]);
+ }
let loadingIndicatorColor = theme.text;
if (variant === CONST.BUTTON_VARIANT.DANGER) {
diff --git a/src/components/Button/primitives/ButtonKeyboardShortcut/index.tsx b/src/components/Button/primitives/ButtonKeyboardShortcut/index.tsx
index 2597e0f2669e..e2472eae5621 100644
--- a/src/components/Button/primitives/ButtonKeyboardShortcut/index.tsx
+++ b/src/components/Button/primitives/ButtonKeyboardShortcut/index.tsx
@@ -10,7 +10,6 @@ import CONST from '@src/CONST';
import type {GestureResponderEvent} from 'react-native';
import {useIsFocused} from '@react-navigation/native';
-import {useCallback, useMemo} from 'react';
import validateSubmitShortcut from './validateSubmitShortcut';
@@ -34,27 +33,21 @@ function ButtonKeyboardShortcut({allowBubble, enterKeyEventListenerPriority, isP
const isFocused = useIsFocused();
const activeElementRole = useActiveElementRole();
- const shouldDisableEnterShortcut = useMemo(() => accessibilityRoles.includes(activeElementRole ?? '') && activeElementRole !== CONST.ROLE.PRESENTATION, [activeElementRole]);
-
- const keyboardShortcutCallback = useCallback(
- (event?: GestureResponderEvent | KeyboardEvent) => {
- if (!validateSubmitShortcut(isDisabled, isLoading, event)) {
- return;
- }
- onPress();
- },
- [isDisabled, isLoading, onPress],
- );
-
- const config = useMemo(
- () => ({
- isActive: !shouldDisableEnterShortcut && (isFocused || isPressOnEnterActive),
- shouldBubble: allowBubble,
- priority: enterKeyEventListenerPriority,
- shouldPreventDefault: false,
- }),
- [shouldDisableEnterShortcut, isFocused, isPressOnEnterActive, allowBubble, enterKeyEventListenerPriority],
- );
+ const shouldDisableEnterShortcut = accessibilityRoles.includes(activeElementRole ?? '') && activeElementRole !== CONST.ROLE.PRESENTATION;
+
+ const keyboardShortcutCallback = (event?: GestureResponderEvent | KeyboardEvent) => {
+ if (!validateSubmitShortcut(isDisabled, isLoading, event)) {
+ return;
+ }
+ onPress();
+ };
+
+ const config = {
+ isActive: !shouldDisableEnterShortcut && (isFocused || isPressOnEnterActive),
+ shouldBubble: allowBubble,
+ priority: enterKeyEventListenerPriority,
+ shouldPreventDefault: false,
+ };
useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ENTER, keyboardShortcutCallback, config);
diff --git a/src/components/Charts/hooks/useChartInteractions.ts b/src/components/Charts/hooks/useChartInteractions.ts
index ad5566750b70..02b23c582563 100644
--- a/src/components/Charts/hooks/useChartInteractions.ts
+++ b/src/components/Charts/hooks/useChartInteractions.ts
@@ -1,6 +1,5 @@
import type {SharedValue} from 'react-native-reanimated';
-import {useCallback} from 'react';
import {PointerType, useCompetingGestures, useHoverGesture, useTapGesture} from 'react-native-gesture-handler';
import {useDerivedValue, useSharedValue} from 'react-native-reanimated';
import {scheduleOnRN, scheduleOnUI} from 'react-native-worklets';
@@ -207,13 +206,10 @@ function useChartInteractions({
* Called by chart content from handleScaleChange to populate canvas positions.
* Must be called with the positions derived from the current d3 scale.
*/
- const setPointPositions = useCallback(
- (ox: number[], oy: number[]) => {
- pointOX.set(ox);
- pointOY.set(oy);
- },
- [pointOX, pointOY],
- );
+ const setPointPositions = (ox: number[], oy: number[]) => {
+ pointOX.set(ox);
+ pointOY.set(oy);
+ };
const getHitTestArgs = (targetIndex: number, cursorX: number, cursorY: number, targetX: number, targetY: number, currentChartBottom: number): HitTestArgs => {
'worklet';
diff --git a/src/components/Charts/hooks/useDynamicYDomain.ts b/src/components/Charts/hooks/useDynamicYDomain.ts
index 41ba6aae3340..fbf226f5bcc3 100644
--- a/src/components/Charts/hooks/useDynamicYDomain.ts
+++ b/src/components/Charts/hooks/useDynamicYDomain.ts
@@ -1,13 +1,9 @@
import type {ChartDataPoint} from '@components/Charts/types';
-import {useMemo} from 'react';
-
/** Keep zero inside the Y-axis domain so bars always grow from a visible baseline, also when all values are negative */
function useDynamicYDomain(data: ChartDataPoint[]): [number, number] {
- return useMemo((): [number, number] => {
- const values = data.flatMap((point) => Object.values(point.values).filter((value) => value !== undefined));
- return [Math.min(0, ...values), Math.max(0, ...values)];
- }, [data]);
+ const values = data.flatMap((point) => Object.values(point.values).filter((value) => value !== undefined));
+ return [Math.min(0, ...values), Math.max(0, ...values)];
}
export default useDynamicYDomain;
diff --git a/src/components/Composer/implementation/index.native.tsx b/src/components/Composer/implementation/index.native.tsx
index 008e6cb17f4e..bff16a6127b5 100644
--- a/src/components/Composer/implementation/index.native.tsx
+++ b/src/components/Composer/implementation/index.native.tsx
@@ -22,7 +22,7 @@ import type {FileObject} from '@src/types/utils/Attachment';
import type {MarkdownStyle, MarkdownTextInput} from '@expensify/react-native-live-markdown';
import type {NativeSyntheticEvent, TextInputChangeEvent} from 'react-native';
-import React, {useCallback, useEffect, useMemo, useRef} from 'react';
+import React, {useEffect, useRef} from 'react';
import {StyleSheet} from 'react-native';
type TextInputPasteEvent = NativeSyntheticEvent<{
@@ -52,7 +52,7 @@ function Composer({
...props
}: ComposerProps) {
const textInputRef = useRef(null);
- const textContainsOnlyEmojis = useMemo(() => containsOnlyEmojis(Parser.htmlToText(Parser.replace(value ?? ''))), [value]);
+ const textContainsOnlyEmojis = containsOnlyEmojis(Parser.htmlToText(Parser.replace(value ?? '')));
const theme = useTheme();
const markdownStyle = useMarkdownStyle(textContainsOnlyEmojis, !isGroupPolicyReport ? excludeReportMentionStyle : excludeNoStyles);
const styles = useThemeStyles();
@@ -83,58 +83,45 @@ function Composer({
/**
* Set the TextInput Ref
- * @param {Element} el
+ * @param el
*/
- const setTextInputRef = useCallback(
- (el: AnimatedMarkdownTextInputRef | null) => {
- textInputRef.current = isInLandscapeMode ? getLandscapeTextInputRefProxy(el) : el;
-
- if (typeof ref !== 'function' || textInputRef.current === null) {
- return;
- }
-
- // This callback prop is used by the parent component using the constructor to
- // get a ref to the inner textInput element e.g. if we do
- // this.textInput = el} /> this will not
- // return a ref to the component, but rather the HTML element by default
- ref(textInputRef.current);
- },
- // eslint-disable-next-line react-hooks/exhaustive-deps
- [isInLandscapeMode],
- );
+ const setTextInputRef = (el: AnimatedMarkdownTextInputRef | null) => {
+ textInputRef.current = isInLandscapeMode ? getLandscapeTextInputRefProxy(el) : el;
- const onClear = useCallback(
- ({nativeEvent}: TextInputChangeEvent) => {
- onClearProp(nativeEvent.text);
- },
- [onClearProp],
- );
+ if (typeof ref !== 'function' || textInputRef.current === null) {
+ return;
+ }
- const pasteFile = useCallback(
- (e: TextInputPasteEvent) => {
- const clipboardContent = e.nativeEvent.items.at(0);
- if (clipboardContent?.type === 'text/plain') {
- return;
- }
- const mimeType = clipboardContent?.type ?? '';
- const fileURI = clipboardContent?.data;
- const baseFileName = fileURI?.split('/').pop() ?? 'file';
- const {fileName: stem, fileExtension: originalFileExtension} = splitExtensionFromFileName(baseFileName);
- const fileExtension = originalFileExtension || (getExtensionFromMimeType(mimeType) ?? 'bin');
- const fileName = `${stem}.${fileExtension}`;
- let file: FileObject = {uri: fileURI, name: fileName, type: mimeType, size: 0};
- getFileSize(file.uri ?? '')
- .then((size) => (file = {...file, size}))
- .finally(() => onPasteFile(file));
- },
- [onPasteFile],
- );
+ // This callback prop is used by the parent component using the constructor to
+ // get a ref to the inner textInput element e.g. if we do
+ // this.textInput = el} /> this will not
+ // return a ref to the component, but rather the HTML element by default
+ ref(textInputRef.current);
+ };
- const maxHeightStyle = useMemo(
- () => StyleUtils.getComposerMaxHeightStyle(isInLandscapeMode ? CONST.COMPOSER.MAX_LINES_LANDSCAPE_MODE : maxLines, isComposerFullSize),
- [StyleUtils, isComposerFullSize, maxLines, isInLandscapeMode],
- );
- const composerStyle = useMemo(() => StyleSheet.flatten([style, textContainsOnlyEmojis ? styles.onlyEmojisTextLineHeight : {}]), [style, textContainsOnlyEmojis, styles]);
+ const onClear = ({nativeEvent}: TextInputChangeEvent) => {
+ onClearProp(nativeEvent.text);
+ };
+
+ const pasteFile = (e: TextInputPasteEvent) => {
+ const clipboardContent = e.nativeEvent.items.at(0);
+ if (clipboardContent?.type === 'text/plain') {
+ return;
+ }
+ const mimeType = clipboardContent?.type ?? '';
+ const fileURI = clipboardContent?.data;
+ const baseFileName = fileURI?.split('/').pop() ?? 'file';
+ const {fileName: stem, fileExtension: originalFileExtension} = splitExtensionFromFileName(baseFileName);
+ const fileExtension = originalFileExtension || (getExtensionFromMimeType(mimeType) ?? 'bin');
+ const fileName = `${stem}.${fileExtension}`;
+ let file: FileObject = {uri: fileURI, name: fileName, type: mimeType, size: 0};
+ getFileSize(file.uri ?? '')
+ .then((size) => (file = {...file, size}))
+ .finally(() => onPasteFile(file));
+ };
+
+ const maxHeightStyle = StyleUtils.getComposerMaxHeightStyle(isInLandscapeMode ? CONST.COMPOSER.MAX_LINES_LANDSCAPE_MODE : maxLines, isComposerFullSize);
+ const composerStyle = StyleSheet.flatten([style, textContainsOnlyEmojis ? styles.onlyEmojisTextLineHeight : {}]);
return (
{
+ const copyToClipboard = () => {
Clipboard.setString(urlToCopy || text || '');
- }, [text, urlToCopy]);
+ };
return (
- Object.keys(CONST.ALL_COUNTRIES).map((countryISO) => {
- const countryName = translate(`allCountries.${countryISO}` as TranslationPaths);
- return {
- value: countryISO,
- keyForList: countryISO,
- text: countryName,
- isSelected: currentCountry === countryISO,
- searchValue: StringUtils.sanitizeString(`${countryISO}${countryName}`),
- };
- }),
- [translate, currentCountry],
- );
+ const countries = Object.keys(CONST.ALL_COUNTRIES).map((countryISO) => {
+ const countryName = translate(`allCountries.${countryISO}` as TranslationPaths);
+ return {
+ value: countryISO,
+ keyForList: countryISO,
+ text: countryName,
+ isSelected: currentCountry === countryISO,
+ searchValue: StringUtils.sanitizeString(`${countryISO}${countryName}`),
+ };
+ });
const orderedCountries = moveInitialSelectionToTop(countries, initialSelectedValues);
const searchResults = searchOptions(debouncedSearchValue, debouncedSearchValue ? countries : orderedCountries);
const headerMessage = debouncedSearchValue.trim() && !searchResults.length ? translate('common.noResultsFound') : '';
const styles = useThemeStyles();
- const textInputOptions = useMemo(
- () => ({
- value: searchValue,
- label: translate('common.search'),
- onChangeText: setSearchValue,
- headerMessage,
- }),
- [headerMessage, searchValue, translate, setSearchValue],
- );
+ const textInputOptions = {
+ value: searchValue,
+ label: translate('common.search'),
+ onChangeText: setSearchValue,
+ headerMessage,
+ };
return (
()] = useOnyx(ONYXKEYS.CURRENCY_LIST);
const {preferredLocale} = useLocalize();
- const getCurrencySymbol = useCallback(
- (currencyCode: string): string | undefined => {
- return currencyList[currencyCode]?.symbol;
- },
- [currencyList],
- );
-
- const getCurrencyDecimals = useCallback(
- (currencyCode: string | undefined): number => {
- const decimals = currencyList[currencyCode ?? '']?.decimals;
- return decimals ?? CONST.DEFAULT_CURRENCY_DECIMALS;
- },
- [currencyList],
- );
-
- const convertToDisplayString = useCallback(
- (amountInCents: number | undefined, currencyCode: string | undefined): string => {
- const sanitizedCurrency = sanitizeCurrencyCode(currencyCode);
- const decimals = getCurrencyDecimals(sanitizedCurrency);
- const convertedAmount = convertToFrontendAmountAsInteger(amountInCents ?? 0, decimals);
- return format(preferredLocale, convertedAmount, {
- style: 'currency',
- currency: sanitizedCurrency,
-
- // We are forcing the number of decimals because we override the default number of decimals in the backend for some currencies
- // See: https://github.com/Expensify/PHP-Libs/pull/834
- minimumFractionDigits: decimals,
- // For currencies that have decimal places > 2, floor to 2 instead as we don't support more than 2 decimal places.
- maximumFractionDigits: 2,
- });
- },
- [getCurrencyDecimals, preferredLocale],
- );
-
- const convertToDisplayStringWithoutCurrency = useCallback(
- (amountInCents: number, currencyCode: string = CONST.CURRENCY.USD): string =>
- convertToDisplayStringWithoutCurrencyForLocale(preferredLocale, amountInCents, currencyCode, getCurrencyDecimals),
- [getCurrencyDecimals, preferredLocale],
- );
-
- const stateValue = useMemo(
- () => ({
- currencyList,
- }),
- [currencyList],
- );
-
- const actionsValue = useMemo(
- () => ({
- getCurrencySymbol,
- getCurrencyDecimals,
- convertToDisplayString,
- convertToDisplayStringWithoutCurrency,
- }),
- [getCurrencySymbol, getCurrencyDecimals, convertToDisplayString, convertToDisplayStringWithoutCurrency],
- );
+ const getCurrencySymbol = (currencyCode: string): string | undefined => {
+ return currencyList[currencyCode]?.symbol;
+ };
+
+ const getCurrencyDecimals = (currencyCode: string | undefined): number => {
+ const decimals = currencyList[currencyCode ?? '']?.decimals;
+ return decimals ?? CONST.DEFAULT_CURRENCY_DECIMALS;
+ };
+
+ const convertToDisplayString = (amountInCents: number | undefined, currencyCode: string | undefined): string => {
+ const sanitizedCurrency = sanitizeCurrencyCode(currencyCode);
+ const decimals = getCurrencyDecimals(sanitizedCurrency);
+ const convertedAmount = convertToFrontendAmountAsInteger(amountInCents ?? 0, decimals);
+ return format(preferredLocale, convertedAmount, {
+ style: 'currency',
+ currency: sanitizedCurrency,
+
+ // We are forcing the number of decimals because we override the default number of decimals in the backend for some currencies
+ // See: https://github.com/Expensify/PHP-Libs/pull/834
+ minimumFractionDigits: decimals,
+ // For currencies that have decimal places > 2, floor to 2 instead as we don't support more than 2 decimal places.
+ maximumFractionDigits: 2,
+ });
+ };
+
+ const convertToDisplayStringWithoutCurrency = (amountInCents: number, currencyCode: string = CONST.CURRENCY.USD): string =>
+ convertToDisplayStringWithoutCurrencyForLocale(preferredLocale, amountInCents, currencyCode, getCurrencyDecimals);
+
+ const stateValue: CurrencyListStateContextType = {
+ currencyList,
+ };
+
+ const actionsValue: CurrencyListActionsContextType = {
+ getCurrencySymbol,
+ getCurrencyDecimals,
+ convertToDisplayString,
+ convertToDisplayStringWithoutCurrency,
+ };
return (
diff --git a/src/components/CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContextProvider.tsx b/src/components/CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContextProvider.tsx
index c2645097ef5a..7727006e90d5 100644
--- a/src/components/CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContextProvider.tsx
+++ b/src/components/CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContextProvider.tsx
@@ -1,23 +1,17 @@
-import React, {useMemo, useState} from 'react';
+import React, {useState} from 'react';
import {CustomStatusBarAndBackgroundProvider} from './CustomStatusBarAndBackgroundContext';
function CustomStatusBarAndBackgroundContextProvider({children}: React.PropsWithChildren) {
const [isRootStatusBarEnabled, setRootStatusBarEnabled] = useState(true);
- const stateValue = useMemo(
- () => ({
- isRootStatusBarEnabled,
- }),
- [isRootStatusBarEnabled],
- );
+ const stateValue = {
+ isRootStatusBarEnabled,
+ };
- const actionsValue = useMemo(
- () => ({
- setRootStatusBarEnabled,
- }),
- [],
- );
+ const actionsValue = {
+ setRootStatusBarEnabled,
+ };
return (
{
- // Check if this function is either called through the current navigation listener
- // react-navigation library has a bug internally, where it can't keep track of the listeners, therefore, sometimes when the useEffect would re-render and we run navigationRef.removeListener the listener isn't removed and we end up with two or more listeners.
- // https://github.com/Expensify/App/issues/34154#issuecomment-1898519399
- if (listenerID !== undefined && listenerID !== listenerCount.current) {
- return;
- }
+ const updateStatusBarStyle = (listenerID?: number) => {
+ // Check if this function is either called through the current navigation listener
+ // react-navigation library has a bug internally, where it can't keep track of the listeners, therefore, sometimes when the useEffect would re-render and we run navigationRef.removeListener the listener isn't removed and we end up with two or more listeners.
+ // https://github.com/Expensify/App/issues/34154#issuecomment-1898519399
+ if (listenerID !== undefined && listenerID !== listenerCount.current) {
+ return;
+ }
- // Set the status bar color depending on the current route.
- // If we don't have any color defined for a route, fall back to
- // appBG color.
- let currentRoute: ReturnType | undefined;
- if (navigationRef.isReady()) {
- currentRoute = navigationRef.getCurrentRoute();
- }
+ // Set the status bar color depending on the current route.
+ // If we don't have any color defined for a route, fall back to
+ // appBG color.
+ let currentRoute: ReturnType | undefined;
+ if (navigationRef.isReady()) {
+ currentRoute = navigationRef.getCurrentRoute();
+ }
- let newStatusBarStyle = theme.statusBarStyle;
- let currentScreenBackgroundColor = theme.appBG;
- if (currentRoute && 'name' in currentRoute && currentRoute.name in theme.PAGE_THEMES) {
- const pageTheme = theme.PAGE_THEMES[currentRoute.name];
-
- newStatusBarStyle = pageTheme.statusBarStyle;
- const backgroundColorFromRoute =
- currentRoute?.params &&
- typeof currentRoute?.params === 'object' &&
- 'backgroundColor' in currentRoute.params &&
- typeof currentRoute.params.backgroundColor === 'string' &&
- currentRoute.params.backgroundColor;
-
- // It's possible for backgroundColorFromRoute to be empty string, so we must use "||" to fallback to backgroundColorFallback.
- currentScreenBackgroundColor = backgroundColorFromRoute || pageTheme.backgroundColor;
- }
+ let newStatusBarStyle = theme.statusBarStyle;
+ let currentScreenBackgroundColor = theme.appBG;
+ if (currentRoute && 'name' in currentRoute && currentRoute.name in theme.PAGE_THEMES) {
+ const pageTheme = theme.PAGE_THEMES[currentRoute.name];
+
+ newStatusBarStyle = pageTheme.statusBarStyle;
+ const backgroundColorFromRoute =
+ currentRoute?.params &&
+ typeof currentRoute?.params === 'object' &&
+ 'backgroundColor' in currentRoute.params &&
+ typeof currentRoute.params.backgroundColor === 'string' &&
+ currentRoute.params.backgroundColor;
+
+ // It's possible for backgroundColorFromRoute to be empty string, so we must use "||" to fallback to backgroundColorFallback.
+ currentScreenBackgroundColor = backgroundColorFromRoute || pageTheme.backgroundColor;
+ }
- prevStatusBarBackgroundColor.set(statusBarBackgroundColor.get());
- statusBarBackgroundColor.set(currentScreenBackgroundColor);
+ prevStatusBarBackgroundColor.set(statusBarBackgroundColor.get());
+ statusBarBackgroundColor.set(currentScreenBackgroundColor);
- const callUpdateStatusBarAppearance = () => {
- updateStatusBarAppearance({statusBarStyle: newStatusBarStyle});
- setStatusBarStyle(newStatusBarStyle);
- };
+ const callUpdateStatusBarAppearance = () => {
+ updateStatusBarAppearance({statusBarStyle: newStatusBarStyle});
+ setStatusBarStyle(newStatusBarStyle);
+ };
- const callUpdateStatusBarBackgroundColor = () => {
- statusBarAnimation.set(0);
- statusBarAnimation.set(withDelay(300, withTiming(1)));
- };
+ const callUpdateStatusBarBackgroundColor = () => {
+ statusBarAnimation.set(0);
+ statusBarAnimation.set(withDelay(300, withTiming(1)));
+ };
- // Don't update the status bar style if it's the same as the current one, to prevent flashing.
- // Force update if the root status bar is back on active or it won't overwrite the nested status bar style
- if (!didForceUpdateStatusBarRef.current && !prevIsRootStatusBarEnabled && isRootStatusBarEnabled) {
- callUpdateStatusBarAppearance();
- callUpdateStatusBarBackgroundColor();
+ // Don't update the status bar style if it's the same as the current one, to prevent flashing.
+ // Force update if the root status bar is back on active or it won't overwrite the nested status bar style
+ if (!didForceUpdateStatusBarRef.current && !prevIsRootStatusBarEnabled && isRootStatusBarEnabled) {
+ callUpdateStatusBarAppearance();
+ callUpdateStatusBarBackgroundColor();
- if (!prevIsRootStatusBarEnabled && isRootStatusBarEnabled) {
- didForceUpdateStatusBarRef.current = true;
- }
- return;
+ if (!prevIsRootStatusBarEnabled && isRootStatusBarEnabled) {
+ didForceUpdateStatusBarRef.current = true;
}
+ return;
+ }
- if (newStatusBarStyle !== statusBarStyle) {
- callUpdateStatusBarAppearance();
- }
+ if (newStatusBarStyle !== statusBarStyle) {
+ callUpdateStatusBarAppearance();
+ }
- if (currentScreenBackgroundColor !== theme.appBG || prevStatusBarBackgroundColor.get() !== theme.appBG) {
- callUpdateStatusBarBackgroundColor();
- }
- },
- [
- theme.statusBarStyle,
- theme.appBG,
- theme.PAGE_THEMES,
- prevStatusBarBackgroundColor,
- statusBarBackgroundColor,
- prevIsRootStatusBarEnabled,
- isRootStatusBarEnabled,
- statusBarStyle,
- statusBarAnimation,
- ],
- );
+ if (currentScreenBackgroundColor !== theme.appBG || prevStatusBarBackgroundColor.get() !== theme.appBG) {
+ callUpdateStatusBarBackgroundColor();
+ }
+ };
useEffect(() => {
didForceUpdateStatusBarRef.current = false;
diff --git a/src/components/CustomStylesForChildrenProvider.tsx b/src/components/CustomStylesForChildrenProvider.tsx
index 3fea548f22bf..3124856b58ce 100644
--- a/src/components/CustomStylesForChildrenProvider.tsx
+++ b/src/components/CustomStylesForChildrenProvider.tsx
@@ -1,6 +1,6 @@
import type {StyleProp, TextStyle, ViewStyle} from 'react-native';
-import React, {useMemo} from 'react';
+import React from 'react';
type CustomStylesForChildrenContextType = StyleProp | null;
@@ -11,7 +11,7 @@ type CustomStylesForChildrenProviderProps = React.PropsWithChildren & {
};
function CustomStylesForChildrenProvider({children, style}: CustomStylesForChildrenProviderProps) {
- const value = useMemo(() => style, [style]);
+ const value = style;
return {children};
}
diff --git a/src/components/DatePicker/CalendarPicker/index.tsx b/src/components/DatePicker/CalendarPicker/index.tsx
index 7c0c88543e14..8ed6d2008275 100644
--- a/src/components/DatePicker/CalendarPicker/index.tsx
+++ b/src/components/DatePicker/CalendarPicker/index.tsx
@@ -15,7 +15,7 @@ import type {StyleProp, ViewStyle} from 'react-native';
import {addMonths, addYears, clamp, isSameDay, isValid, setMonth, setYear, startOfDay, startOfMonth, subMonths, subYears} from 'date-fns';
import {Str} from 'expensify-common';
-import React, {useCallback, useEffect, useRef, useState} from 'react';
+import React, {useEffect, useRef, useState} from 'react';
import {View} from 'react-native';
import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
@@ -184,7 +184,7 @@ function CalendarPicker({
const webOnlyMarginStyle = isSmallScreenWidth ? {} : styles.mh1;
const headerPaddingStyle = headerContainerStyle ?? themeStyles.ph3;
- const getAccessibilityState = useCallback((isSelected: boolean) => ({selected: isSelected}), []);
+ const getAccessibilityState = (isSelected: boolean) => ({selected: isSelected});
const calendarWeeks = calendarDaysMatrix?.map((week) => (
{
+ const syncActiveLabelFromStack = () => {
const top = labelStackRef.current.at(-1);
setActiveLabel((prev) => {
const next = top?.text;
return prev === next ? prev : next;
});
- }, []);
-
- const pushLabel = useCallback(
- (text: string): number => {
- const id = nextIdRef.current++;
- labelStackRef.current = [...labelStackRef.current, {id, text}];
- initialFocusClaimedRef.current = false;
- syncActiveLabelFromStack();
- return id;
- },
- [syncActiveLabelFromStack],
- );
-
- const popLabel = useCallback(
- (id: number) => {
- labelStackRef.current = labelStackRef.current.filter((entry) => entry.id !== id);
- syncActiveLabelFromStack();
- },
- [syncActiveLabelFromStack],
- );
-
- const claimInitialFocus = useCallback((): boolean => {
+ };
+
+ const pushLabel = (text: string): number => {
+ const id = nextIdRef.current++;
+ labelStackRef.current = [...labelStackRef.current, {id, text}];
+ initialFocusClaimedRef.current = false;
+ syncActiveLabelFromStack();
+ return id;
+ };
+
+ const popLabel = (id: number) => {
+ labelStackRef.current = labelStackRef.current.filter((entry) => entry.id !== id);
+ syncActiveLabelFromStack();
+ };
+
+ const claimInitialFocus = (): boolean => {
if (initialFocusClaimedRef.current) {
return false;
}
initialFocusClaimedRef.current = true;
return true;
- }, []);
-
- const data: DialogLabelData = useMemo(
- () => ({
- containerRef,
- isInsideDialog: hasDialogSemantics,
- // Only expose a name when the container is a dialog (wide RHP).
- dialogAriaLabel: hasDialogSemantics ? activeLabel : undefined,
- }),
- [hasDialogSemantics, activeLabel],
- );
-
- const actions: DialogLabelActions = useMemo(
- () => ({
- pushLabel,
- popLabel,
- claimInitialFocus,
- }),
- [pushLabel, popLabel, claimInitialFocus],
- );
+ };
+
+ const data: DialogLabelData = {
+ containerRef,
+ isInsideDialog: hasDialogSemantics,
+ // Only expose a name when the container is a dialog (wide RHP).
+ dialogAriaLabel: hasDialogSemantics ? activeLabel : undefined,
+ };
+
+ const actions: DialogLabelActions = {
+ pushLabel,
+ popLabel,
+ claimInitialFocus,
+ };
return (
diff --git a/src/components/DisplayNames/DisplayNamesTooltipItem.tsx b/src/components/DisplayNames/DisplayNamesTooltipItem.tsx
index 12e99fdf4b84..fa1a21594ebf 100644
--- a/src/components/DisplayNames/DisplayNamesTooltipItem.tsx
+++ b/src/components/DisplayNames/DisplayNamesTooltipItem.tsx
@@ -11,7 +11,7 @@ import type {ComponentRef, RefObject} from 'react';
// eslint-disable-next-line no-restricted-imports
import type {Text as RNText, StyleProp, TextStyle} from 'react-native';
-import React, {useCallback} from 'react';
+import React from 'react';
type DisplayNamesTooltipItemProps = ForwardedFSClassProps & {
index?: number;
@@ -53,7 +53,7 @@ function DisplayNamesTooltipItem({
isSelectable = false,
}: DisplayNamesTooltipItemProps) {
const styles = useThemeStyles();
- const tooltipIndexBridge = useCallback(() => getTooltipShiftX(index), [getTooltipShiftX, index]);
+ const tooltipIndexBridge = () => getTooltipShiftX(index);
return (
containsCustomEmoji(fullTitle) && !containsOnlyCustomEmoji(fullTitle), [fullTitle]);
- const title = useMemo(() => {
- const processedTitle = shouldParseFullTitle ? Parser.htmlToText(fullTitle) : fullTitle;
- return StringUtils.lineBreaksToSpaces(processedTitle) || translate('common.hidden');
- }, [fullTitle, shouldParseFullTitle, translate]);
+ const titleContainsTextAndCustomEmoji = containsCustomEmoji(fullTitle) && !containsOnlyCustomEmoji(fullTitle);
+ const processedTitle = shouldParseFullTitle ? Parser.htmlToText(fullTitle) : fullTitle;
+ const title = StringUtils.lineBreaksToSpaces(processedTitle) || translate('common.hidden');
return (
{
- const processedTitle = shouldParseFullTitle ? Parser.htmlToText(fullTitle) : fullTitle;
- return StringUtils.lineBreaksToSpaces(processedTitle) || translate('common.hidden');
- }, [fullTitle, shouldParseFullTitle, translate]);
+ const processedTitle = shouldParseFullTitle ? Parser.htmlToText(fullTitle) : fullTitle;
+ const title = StringUtils.lineBreaksToSpaces(processedTitle) || translate('common.hidden');
if (!tooltipEnabled) {
return (
diff --git a/src/components/DistanceEReceiptPanel.tsx b/src/components/DistanceEReceiptPanel.tsx
index 53a8ef6ad82a..db4c6c291d40 100644
--- a/src/components/DistanceEReceiptPanel.tsx
+++ b/src/components/DistanceEReceiptPanel.tsx
@@ -15,7 +15,7 @@ import type {TranslationPaths} from '@src/languages/types';
import type {Transaction} from '@src/types/onyx';
import type {WaypointCollection} from '@src/types/onyx/Transaction';
-import React, {useMemo} from 'react';
+import React from 'react';
import {View} from 'react-native';
import Icon from './Icon';
@@ -46,16 +46,11 @@ function DistanceEReceiptPanel({transaction}: DistanceEReceiptPanelProps) {
// The thumbnail this card draws is the stored route map. An edit that makes the server rebuild the receipt
// invalidates its URL, and a route error means there is no trip to draw, so show the pending map for both.
const canShowStoredRouteMap = !hasPendingDistanceReceiptRegeneration(transaction) && !hasDistanceRouteErrors(transaction);
- const waypoints = useMemo(() => transaction?.comment?.waypoints ?? {}, [transaction?.comment?.waypoints]);
- const sortedWaypoints = useMemo(
- () =>
- // The waypoint keys are sometimes out of order
- Object.keys(waypoints)
- .sort((keyA, keyB) => getWaypointIndex(keyA) - getWaypointIndex(keyB))
- .map((key) => ({[key]: waypoints[key]}))
- .reduce((result, obj) => (obj ? Object.assign(result, obj) : result), {}),
- [waypoints],
- );
+ const waypoints = transaction?.comment?.waypoints ?? {};
+ const sortedWaypoints: WaypointCollection = Object.keys(waypoints)
+ .sort((keyA, keyB) => getWaypointIndex(keyA) - getWaypointIndex(keyB))
+ .map((key) => ({[key]: waypoints[key]}))
+ .reduce((result, obj) => (obj ? Object.assign(result, obj) : result), {});
return (
) => getAvailableReusableDistanceRoutes(reusableDistanceRoutes, transactions, isOffline).length > 0,
- [reusableDistanceRoutes, isOffline],
- );
+ const hasAvailableReusableRouteSelector = (transactions: OnyxCollection) => getAvailableReusableDistanceRoutes(reusableDistanceRoutes, transactions, isOffline).length > 0;
const [hasAvailableReusableRoute = false] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {
selector: hasAvailableReusableRouteSelector,
});
diff --git a/src/components/EditableCell/EditingCellContext.tsx b/src/components/EditableCell/EditingCellContext.tsx
index 75e0f4f6c682..5e986c7b8114 100644
--- a/src/components/EditableCell/EditingCellContext.tsx
+++ b/src/components/EditableCell/EditingCellContext.tsx
@@ -1,6 +1,6 @@
import type {ReactNode} from 'react';
-import React, {useCallback, useContext, useState} from 'react';
+import React, {useContext, useState} from 'react';
/**
* Context for managing inline editable cell state across the component tree.
@@ -40,7 +40,7 @@ function EditingCellProvider({children}: EditingCellProviderProps) {
const [wasRecentlyEditingCell, setWasRecentlyEditingCell] = useState(false);
const [focusedCellId, setFocusedCellId] = useState(null);
- const setIsEditingCell = useCallback((value: boolean) => {
+ const setIsEditingCell = (value: boolean) => {
setIsEditingCellState(value);
if (!value) {
@@ -51,7 +51,7 @@ function EditingCellProvider({children}: EditingCellProviderProps) {
setWasRecentlyEditingCell(false);
});
}
- }, []);
+ };
const stateValue = {
isEditingCell,
diff --git a/src/components/EditableCell/useInlineEditState.ts b/src/components/EditableCell/useInlineEditState.ts
index af43efb1d036..f91545d9c2d9 100644
--- a/src/components/EditableCell/useInlineEditState.ts
+++ b/src/components/EditableCell/useInlineEditState.ts
@@ -1,4 +1,4 @@
-import {useCallback, useEffect, useRef, useState} from 'react';
+import {useEffect, useRef, useState} from 'react';
import type {InlineEditSaveResult} from './types';
@@ -37,26 +37,26 @@ function useInlineEditState(
setLocalValue(value);
}
- const startEditing = useCallback(() => {
+ const startEditing = () => {
hasEndedRef.current = false;
setIsAwaitingConfirm(false);
setIsEditing(true);
- }, []);
+ };
- const closeEditing = useCallback(() => {
+ const closeEditing = () => {
setIsAwaitingConfirm(false);
// Reset to the source of truth when edit mode closes so a rejected save, such as an empty merchant, is not left on screen.
setLocalValue(value);
setIsEditing(false);
- }, [value]);
+ };
- const keepEditing = useCallback(() => {
+ const keepEditing = () => {
hasEndedRef.current = false;
setIsAwaitingConfirm(false);
onKeepEditing?.();
- }, [onKeepEditing]);
+ };
- const save = useCallback(() => {
+ const save = () => {
if (hasEndedRef.current) {
return;
}
@@ -90,15 +90,15 @@ function useInlineEditState(
keepEditing();
},
);
- }, [localValue, value, onSave, isEqual, closeEditing, keepEditing]);
+ };
- const cancelEditing = useCallback(() => {
+ const cancelEditing = () => {
if (hasEndedRef.current) {
return;
}
hasEndedRef.current = true;
closeEditing();
- }, [closeEditing]);
+ };
// Cancel editing when permission is revoked (e.g., transaction status changed). A pending save has already
// claimed the edit, so revocation is applied once the confirm modal releases it rather than under the modal.
diff --git a/src/components/EmojiPicker/CategoryShortcutButton.tsx b/src/components/EmojiPicker/CategoryShortcutButton.tsx
index 44790c1606cb..495839a9e96c 100644
--- a/src/components/EmojiPicker/CategoryShortcutButton.tsx
+++ b/src/components/EmojiPicker/CategoryShortcutButton.tsx
@@ -90,4 +90,4 @@ function CategoryShortcutButton({code, icon, onPress, isSelected = false}: Categ
CategoryShortcutButton.displayName = 'CategoryShortcutButton';
-export default React.memo(CategoryShortcutButton);
+export default CategoryShortcutButton;
diff --git a/src/components/EmojiPicker/EmojiPickerButton.tsx b/src/components/EmojiPicker/EmojiPickerButton.tsx
index eade7858788d..08625f6d0519 100644
--- a/src/components/EmojiPicker/EmojiPickerButton.tsx
+++ b/src/components/EmojiPicker/EmojiPickerButton.tsx
@@ -18,7 +18,7 @@ import CONST from '@src/CONST';
import KeyboardUtils from '@src/utils/keyboard';
import {useIsFocused} from '@react-navigation/native';
-import React, {memo, useEffect, useRef} from 'react';
+import React, {useEffect, useRef} from 'react';
type EmojiPickerButtonProps = {
/** Flag to disable the emoji picker button */
@@ -103,4 +103,4 @@ function EmojiPickerButton({isDisabled = false, emojiPickerID = '', shiftVertica
);
}
-export default memo(EmojiPickerButton);
+export default EmojiPickerButton;
diff --git a/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.ts b/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.ts
index cb3c2901ca9a..5fec3186b77a 100644
--- a/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.ts
+++ b/src/components/EmojiPicker/EmojiPickerMenu/useEmojiPickerMenu.ts
@@ -17,16 +17,16 @@ import calculateModalHeightInLandscapeMode from '@src/utils/calculateModalHeight
import type {FlashListRef} from '@shopify/flash-list';
-import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
+import {useEffect, useRef, useState} from 'react';
const useEmojiPickerMenu = () => {
const emojiListRef = useRef>(null);
const [frequentlyUsedEmojis] = useOnyx(ONYXKEYS.FREQUENTLY_USED_EMOJIS);
- const allEmojis = useMemo(() => mergeEmojisWithFrequentlyUsedEmojis(emojis, processFrequentlyUsedEmojis(frequentlyUsedEmojis)), [frequentlyUsedEmojis]);
- const headerEmojis = useMemo(() => getHeaderEmojis(allEmojis), [allEmojis]);
- const headerRowIndices = useMemo(() => headerEmojis.map((headerEmoji) => headerEmoji.index), [headerEmojis]);
- const spacersIndexes = useMemo(() => getSpacersIndexes(allEmojis), [allEmojis]);
+ const allEmojis = mergeEmojisWithFrequentlyUsedEmojis(emojis, processFrequentlyUsedEmojis(frequentlyUsedEmojis));
+ const headerEmojis = getHeaderEmojis(allEmojis);
+ const headerRowIndices = headerEmojis.map((headerEmoji) => headerEmoji.index);
+ const spacersIndexes = getSpacersIndexes(allEmojis);
const [filteredEmojis, setFilteredEmojis] = useState(allEmojis);
const [headerIndices, setHeaderIndices] = useState(headerRowIndices);
const isListFiltered = allEmojis.length !== filteredEmojis.length;
@@ -60,15 +60,12 @@ const useEmojiPickerMenu = () => {
/**
* Suggest emojis based on the search term
*/
- const suggestEmojisCallback = useCallback(
- (searchTerm: string) => {
- const normalizedSearchTerm = searchTerm.toLowerCase().trim().replaceAll(':', '');
- const emojisSuggestions = suggestEmojis(`:${normalizedSearchTerm}`, preferredLocale, allEmojis.length);
+ const suggestEmojisCallback = (searchTerm: string) => {
+ const normalizedSearchTerm = searchTerm.toLowerCase().trim().replaceAll(':', '');
+ const emojisSuggestions = suggestEmojis(`:${normalizedSearchTerm}`, preferredLocale, allEmojis.length);
- return [normalizedSearchTerm, emojisSuggestions] as const;
- },
- [allEmojis.length, preferredLocale],
- );
+ return [normalizedSearchTerm, emojisSuggestions] as const;
+ };
return {
allEmojis,
diff --git a/src/components/EmojiPicker/EmojiPickerMenuItem/index.native.tsx b/src/components/EmojiPicker/EmojiPickerMenuItem/index.native.tsx
index e36bd3f45ad8..582652eb63bc 100644
--- a/src/components/EmojiPicker/EmojiPickerMenuItem/index.native.tsx
+++ b/src/components/EmojiPicker/EmojiPickerMenuItem/index.native.tsx
@@ -66,8 +66,4 @@ EmojiPickerMenuItem.displayName = 'EmojiPickerMenuItem';
// Significantly speeds up re-renders of the EmojiPickerMenu's FlatList
// by only re-rendering at most two EmojiPickerMenuItems that are highlighted/un-highlighted per user action.
-export default React.memo(
- EmojiPickerMenuItem,
- (prevProps, nextProps) =>
- prevProps.isHighlighted === nextProps.isHighlighted && prevProps.emoji === nextProps.emoji && prevProps.isUsingKeyboardMovement === nextProps.isUsingKeyboardMovement,
-);
+export default EmojiPickerMenuItem;
diff --git a/src/components/EmojiPicker/EmojiPickerMenuItem/index.tsx b/src/components/EmojiPicker/EmojiPickerMenuItem/index.tsx
index 44467ff5b733..6ac5c0ea0421 100644
--- a/src/components/EmojiPicker/EmojiPickerMenuItem/index.tsx
+++ b/src/components/EmojiPicker/EmojiPickerMenuItem/index.tsx
@@ -92,7 +92,4 @@ EmojiPickerMenuItem.displayName = 'EmojiPickerMenuItem';
// Significantly speeds up re-renders of the EmojiPickerMenu's FlatList
// by only re-rendering at most two EmojiPickerMenuItems that are highlighted/un-highlighted per user action.
-export default React.memo(
- EmojiPickerMenuItem,
- (prevProps, nextProps) => prevProps.isFocused === nextProps.isFocused && prevProps.isHighlighted === nextProps.isHighlighted && prevProps.emoji === nextProps.emoji,
-);
+export default EmojiPickerMenuItem;
diff --git a/src/components/EmojiPicker/EmojiSkinToneList.tsx b/src/components/EmojiPicker/EmojiSkinToneList.tsx
index 3aba4dfe9a0b..d39c3e809a5b 100644
--- a/src/components/EmojiPicker/EmojiSkinToneList.tsx
+++ b/src/components/EmojiPicker/EmojiSkinToneList.tsx
@@ -9,7 +9,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
-import React, {useCallback, useEffect, useState} from 'react';
+import React, {useEffect, useState} from 'react';
import {View} from 'react-native';
import EmojiPickerMenuItem from './EmojiPickerMenuItem';
@@ -26,9 +26,9 @@ function EmojiSkinToneList() {
const {translate} = useLocalize();
const [preferredSkinTone, setPreferredSkinTone] = usePreferredEmojiSkinTone();
- const toggleIsSkinToneListVisible = useCallback(() => {
+ const toggleIsSkinToneListVisible = () => {
setIsSkinToneListVisible((prev) => !prev);
- }, []);
+ };
/**
* Set the preferred skin tone in Onyx and close the skin tone picker
diff --git a/src/components/EmojiWithTooltip/index.android.tsx b/src/components/EmojiWithTooltip/index.android.tsx
index 3de9c642d86a..1029a6b7d091 100644
--- a/src/components/EmojiWithTooltip/index.android.tsx
+++ b/src/components/EmojiWithTooltip/index.android.tsx
@@ -4,12 +4,10 @@ import useThemeStyles from '@hooks/useThemeStyles';
import {containsCustomEmoji} from '@libs/EmojiUtils';
-import {useMemo} from 'react';
-
import type EmojiWithTooltipProps from './types';
function EmojiWithTooltip({emojiCode, style = {}}: EmojiWithTooltipProps) {
- const isCustomEmoji = useMemo(() => containsCustomEmoji(emojiCode), [emojiCode]);
+ const isCustomEmoji = containsCustomEmoji(emojiCode);
const styles = useThemeStyles();
return {emojiCode};
}
diff --git a/src/components/EmojiWithTooltip/index.tsx b/src/components/EmojiWithTooltip/index.tsx
index 64ec51e11e34..ec9f688f88b8 100644
--- a/src/components/EmojiWithTooltip/index.tsx
+++ b/src/components/EmojiWithTooltip/index.tsx
@@ -6,7 +6,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import {findEmojiByCode, getLocalizedEmojiName} from '@libs/EmojiUtils';
-import React, {useCallback} from 'react';
+import React from 'react';
import {View} from 'react-native';
import type EmojiWithTooltipProps from './types';
@@ -17,21 +17,18 @@ function EmojiWithTooltip({emojiCode, style = {}}: EmojiWithTooltipProps) {
const emoji = findEmojiByCode(emojiCode);
const emojiName = getLocalizedEmojiName(emoji?.name, preferredLocale);
- const emojiTooltipContent = useCallback(
- () => (
-
-
-
- {emojiCode}
-
-
- {`:${emojiName}:`}
+ const emojiTooltipContent = () => (
+
+
+
+ {emojiCode}
+
- ),
- [emojiCode, emojiName, styles.alignItemsCenter, styles.ph2, styles.flexRow, styles.emojiTooltipWrapper, styles.fontColorReactionLabel, styles.onlyEmojisText, styles.textMicro],
+ {`:${emojiName}:`}
+
);
return (
diff --git a/src/components/EmptyStateComponent/index.tsx b/src/components/EmptyStateComponent/index.tsx
index 8f5d59db0e8d..6f4723711f66 100644
--- a/src/components/EmptyStateComponent/index.tsx
+++ b/src/components/EmptyStateComponent/index.tsx
@@ -12,7 +12,7 @@ import TextWithEmojiFragment from '@pages/inbox/report/comment/TextWithEmojiFrag
import CONST from '@src/CONST';
import isEmpty from 'lodash/isEmpty';
-import React, {useMemo} from 'react';
+import React from 'react';
import {StyleSheet, View} from 'react-native';
import type {EmptyStateComponentProps} from './types';
@@ -37,14 +37,11 @@ function EmptyStateComponent({
const styles = useThemeStyles();
const doesSubtitleContainCustomEmojiAndMore = containsCustomEmoji(subtitle ?? '') && !containsOnlyCustomEmoji(subtitle ?? '');
- const HeaderComponent = useMemo(
- () => (
-
- ),
- [headerMedia, headerContentStyles],
+ const HeaderComponent = (
+
);
return (
diff --git a/src/components/EnvironmentContextProvider/index.tsx b/src/components/EnvironmentContextProvider/index.tsx
index 60e968b3f845..20e7fc9f82eb 100644
--- a/src/components/EnvironmentContextProvider/index.tsx
+++ b/src/components/EnvironmentContextProvider/index.tsx
@@ -3,7 +3,7 @@ import getEnvironment from '@libs/Environment/getEnvironment';
import type {ReactElement, ReactNode} from 'react';
-import React, {createContext, useCallback, useContext, useEffect, useMemo, useState} from 'react';
+import React, {createContext, useContext, useEffect, useState} from 'react';
import type {EnvironmentActionsContextType, EnvironmentStateContextType, EnvironmentValue} from './types';
@@ -76,7 +76,7 @@ const EnvironmentActionsContext = createContext(d
function EnvironmentProvider({children}: EnvironmentProviderProps): ReactElement {
const [environment, setEnvironment] = useState(defaultEnvironmentStateContextValue.environment);
const [environmentURL, setEnvironmentURL] = useState(defaultEnvironmentStateContextValue.environmentURL);
- const environmentURLWithoutTrailingSlash = useMemo(() => environmentURL.replaceAll(/\/+$/g, ''), [environmentURL]);
+ const environmentURLWithoutTrailingSlash = environmentURL.replaceAll(/\/+$/g, '');
useEffect(() => {
getEnvironment().then(setEnvironment);
@@ -87,22 +87,16 @@ function EnvironmentProvider({children}: EnvironmentProviderProps): ReactElement
* Adjusts Expensify links in HTML content to use the current environment URL
* instead of the production URL (new.expensify.com).
*/
- const adjustExpensifyLinksForEnv = useCallback((html: string): string => rewriteProductionNewDotLinks(html, environmentURLWithoutTrailingSlash), [environmentURLWithoutTrailingSlash]);
+ const adjustExpensifyLinksForEnv = (html: string): string => rewriteProductionNewDotLinks(html, environmentURLWithoutTrailingSlash);
- const stateValue = useMemo(
- () => ({
- environment,
- environmentURL,
- }),
- [environment, environmentURL],
- );
+ const stateValue = {
+ environment,
+ environmentURL,
+ };
- const actionsValue = useMemo(
- () => ({
- adjustExpensifyLinksForEnv,
- }),
- [adjustExpensifyLinksForEnv],
- );
+ const actionsValue = {
+ adjustExpensifyLinksForEnv,
+ };
return (
diff --git a/src/components/FeatureTraining/FeatureTraining.tsx b/src/components/FeatureTraining/FeatureTraining.tsx
index 822755be5721..1ee3e2365486 100644
--- a/src/components/FeatureTraining/FeatureTraining.tsx
+++ b/src/components/FeatureTraining/FeatureTraining.tsx
@@ -1,6 +1,6 @@
import variables from '@styles/variables';
-import React, {useCallback, useMemo, useState} from 'react';
+import React, {useState} from 'react';
import type {FeatureTrainingActionsValue, FeatureTrainingStateValue} from './context';
import type {FeatureTrainingProps} from './types';
@@ -20,42 +20,36 @@ function FeatureTraining({
const [willShowAgain, setWillShowAgain] = useState(true);
const {Wrapper, wrapperProps} = useScrollableWrapper({shouldUseScrollView, width});
- const toggleWillShowAgain = useCallback(() => {
+ const toggleWillShowAgain = () => {
onWillShowAgainChange?.(!willShowAgain);
setWillShowAgain((prev) => !prev);
- }, [onWillShowAgainChange, willShowAgain]);
+ };
// Closing after confirm is the consumer's responsibility — call it inside onConfirm when desired.
- const handleConfirm = useCallback(() => {
+ const handleConfirm = () => {
onConfirm?.(willShowAgain);
- }, [onConfirm, willShowAgain]);
+ };
- const handleClose = useCallback(() => onClose?.(), [onClose]);
+ const handleClose = () => onClose?.();
- const stateValue = useMemo(
- () => ({
- willShowAgain,
- shouldShowLoadingImmediatelyOnPress: undefined,
- isCarousel: false,
- confirmSentryLabel,
- currentPage: undefined,
- pageCount: undefined,
- isLastPage: undefined,
- contentMinHeight: undefined,
- }),
- [willShowAgain, confirmSentryLabel],
- );
+ const stateValue: FeatureTrainingStateValue = {
+ willShowAgain,
+ shouldShowLoadingImmediatelyOnPress: undefined,
+ isCarousel: false,
+ confirmSentryLabel,
+ currentPage: undefined,
+ pageCount: undefined,
+ isLastPage: undefined,
+ contentMinHeight: undefined,
+ };
- const actionsValue = useMemo(
- () => ({
- toggleWillShowAgain,
- handleConfirm,
- handleClose,
- advance: undefined,
- goBack: undefined,
- }),
- [toggleWillShowAgain, handleConfirm, handleClose],
- );
+ const actionsValue: FeatureTrainingActionsValue = {
+ toggleWillShowAgain,
+ handleConfirm,
+ handleClose,
+ advance: undefined,
+ goBack: undefined,
+ };
return (
diff --git a/src/components/FeatureTraining/FeatureTrainingCarousel.tsx b/src/components/FeatureTraining/FeatureTrainingCarousel.tsx
index 5034899e92ca..26bc4c252061 100644
--- a/src/components/FeatureTraining/FeatureTrainingCarousel.tsx
+++ b/src/components/FeatureTraining/FeatureTrainingCarousel.tsx
@@ -7,7 +7,7 @@ import type {ViewabilityConfig, ViewToken} from '@src/types/utils/ReactNativeCom
import type {ReactElement, ReactNode} from 'react';
import type {LayoutChangeEvent, FlatList as RNFlatList, ViewStyle} from 'react-native';
-import React, {cloneElement, isValidElement, useCallback, useEffect, useMemo, useRef, useState} from 'react';
+import React, {cloneElement, isValidElement, useEffect, useRef, useState} from 'react';
import {FlatList, Platform, View} from 'react-native';
import type {FeatureTrainingActionsValue, FeatureTrainingStateValue} from './context';
@@ -95,23 +95,21 @@ function FeatureTrainingCarousel({
const {Wrapper, wrapperProps, setContainerHeight, shouldUseScrollView: usingScrollView, isInLandscapeMode} = useScrollableWrapper({shouldUseScrollView, width});
- const pages = useMemo(() => {
- const pageList: SplitPage[] = [];
- React.Children.forEach(children, (child) => {
- if (!isValidElement(child) || !isPageElement(child)) {
- return;
- }
- pageList.push(splitPageChildren(child.props.children));
- });
- return pageList;
- }, [children]);
+ const pageList: SplitPage[] = [];
+ React.Children.forEach(children, (child) => {
+ if (!isValidElement(child) || !isPageElement(child)) {
+ return;
+ }
+ pageList.push(splitPageChildren(child.props.children));
+ });
+ const pages = pageList;
const onPageChangeRef = useRef(onPageChange);
useEffect(() => {
onPageChangeRef.current = onPageChange;
}, [onPageChange]);
- const onViewableItemsChanged = useCallback(({viewableItems}: {viewableItems: ViewToken[]}) => {
+ const onViewableItemsChanged = ({viewableItems}: {viewableItems: ViewToken[]}) => {
const entry = viewableItems.at(0);
if (entry?.index == null || entry.index === lastReportedPage.current) {
return;
@@ -119,18 +117,18 @@ function FeatureTrainingCarousel({
lastReportedPage.current = entry.index;
setCurrentPage(entry.index);
onPageChangeRef.current?.(entry.index);
- }, []);
+ };
- const advance = useCallback(() => {
+ const advance = () => {
horizontalListRef.current?.scrollToIndex({index: Math.min(currentPage + 1, pages.length - 1), animated: true});
- }, [currentPage, pages.length]);
+ };
- const goBack = useCallback(() => {
+ const goBack = () => {
if (currentPage <= 0) {
return;
}
horizontalListRef.current?.scrollToIndex({index: Math.max(currentPage - 1, 0), animated: true});
- }, [currentPage]);
+ };
const pageCountRef = useRef(pages.length);
useEffect(() => {
@@ -147,7 +145,7 @@ function FeatureTrainingCarousel({
horizontalListRef.current?.scrollToOffset({offset: lastReportedPage.current * carouselViewportWidth, animated: false});
}, [carouselViewportWidth]);
- const recordPageHeight = useCallback((index: number, measured: number) => {
+ const recordPageHeight = (index: number, measured: number) => {
if (measuredHeightsRef.current[index] === measured) {
return;
}
@@ -156,59 +154,50 @@ function FeatureTrainingCarousel({
return;
}
setContentMinHeight(Math.max(...Object.values(measuredHeightsRef.current)));
- }, []);
+ };
const isLastPage = pages.length === 0 || currentPage >= pages.length - 1;
- const handleConfirm = useCallback(() => {
+ const handleConfirm = () => {
onConfirm?.(false);
- }, [onConfirm]);
-
- const handleClose = useCallback(() => onClose?.(), [onClose]);
-
- const stateValue = useMemo(
- () => ({
- willShowAgain: true,
- shouldShowLoadingImmediatelyOnPress: isLastPage ? undefined : false,
- isCarousel: true,
- confirmSentryLabel,
- currentPage,
- pageCount: pages.length,
- isLastPage,
- contentMinHeight,
- }),
- [isLastPage, confirmSentryLabel, currentPage, pages.length, contentMinHeight],
- );
-
- const actionsValue = useMemo(
- () => ({
- toggleWillShowAgain: () => {},
- handleConfirm,
- handleClose,
- advance,
- goBack,
- }),
- [handleConfirm, handleClose, advance, goBack],
- );
+ };
+
+ const handleClose = () => onClose?.();
+
+ const stateValue: FeatureTrainingStateValue = {
+ willShowAgain: true,
+ shouldShowLoadingImmediatelyOnPress: isLastPage ? undefined : false,
+ isCarousel: true,
+ confirmSentryLabel,
+ currentPage,
+ pageCount: pages.length,
+ isLastPage,
+ contentMinHeight,
+ };
+
+ const actionsValue: FeatureTrainingActionsValue = {
+ toggleWillShowAgain: () => {},
+ handleConfirm,
+ handleClose,
+ advance,
+ goBack,
+ };
const currentPageBody = pages.at(currentPage)?.body ?? null;
- const onWrapperLayout = useCallback(
- (e: LayoutChangeEvent) => {
- const newWidth = e.nativeEvent.layout.width;
- if (newWidth === carouselViewportWidth || newWidth <= 0) {
- return;
- }
- setCarouselViewportWidth(newWidth);
- if (!usingScrollView) {
- return;
- }
- setContainerHeight(e.nativeEvent.layout.height);
- },
- [carouselViewportWidth, usingScrollView, setContainerHeight],
- );
+ const onWrapperLayout = (e: LayoutChangeEvent) => {
+ const newWidth = e.nativeEvent.layout.width;
+ if (newWidth === carouselViewportWidth || newWidth <= 0) {
+ return;
+ }
+ setCarouselViewportWidth(newWidth);
+ if (!usingScrollView) {
+ return;
+ }
+ setContainerHeight(e.nativeEvent.layout.height);
+ };
- const probeStyle = useMemo(() => ({position: 'absolute', left: 0, top: 0, width: carouselViewportWidth, opacity: 0}), [carouselViewportWidth]);
+ const probeStyle: ViewStyle = {position: 'absolute', left: 0, top: 0, width: carouselViewportWidth, opacity: 0};
return (
@@ -289,7 +278,7 @@ type ProbePageProps = {
// Body's horizontal margins so the probe text wraps at the same width as the visible page.
function ProbePage({index, bodyText, onMeasure}: ProbePageProps) {
const styles = useThemeStyles();
- const onLayout = useCallback((event: LayoutChangeEvent) => onMeasure(index, event.nativeEvent.layout.height), [index, onMeasure]);
+ const onLayout = (event: LayoutChangeEvent) => onMeasure(index, event.nativeEvent.layout.height);
return {cloneElement(bodyText, {onLayout})};
}
diff --git a/src/components/FeedbackSurvey.tsx b/src/components/FeedbackSurvey.tsx
index f50a175456ed..c23d6c94dc44 100644
--- a/src/components/FeedbackSurvey.tsx
+++ b/src/components/FeedbackSurvey.tsx
@@ -11,7 +11,7 @@ import type ONYXKEYS from '@src/ONYXKEYS';
import INPUT_IDS from '@src/types/form/FeedbackSurveyForm';
import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
-import React, {useEffect, useMemo, useState} from 'react';
+import React, {useEffect, useState} from 'react';
import {View} from 'react-native';
import type {Choice} from './RadioButtons';
@@ -54,15 +54,12 @@ function FeedbackSurvey({title, description, onSubmit, footerText, isNoteRequire
const isLoadingDraft = isLoadingOnyxValue(draftResults);
- const options = useMemo(
- () => [
- {value: CONST.FEEDBACK_SURVEY_OPTIONS.TOO_LIMITED.ID, label: translate(CONST.FEEDBACK_SURVEY_OPTIONS.TOO_LIMITED.TRANSLATION_KEY)},
- {value: CONST.FEEDBACK_SURVEY_OPTIONS.TOO_EXPENSIVE.ID, label: translate(CONST.FEEDBACK_SURVEY_OPTIONS.TOO_EXPENSIVE.TRANSLATION_KEY)},
- {value: CONST.FEEDBACK_SURVEY_OPTIONS.INADEQUATE_SUPPORT.ID, label: translate(CONST.FEEDBACK_SURVEY_OPTIONS.INADEQUATE_SUPPORT.TRANSLATION_KEY)},
- {value: CONST.FEEDBACK_SURVEY_OPTIONS.BUSINESS_CLOSING.ID, label: translate(CONST.FEEDBACK_SURVEY_OPTIONS.BUSINESS_CLOSING.TRANSLATION_KEY)},
- ],
- [translate],
- );
+ const options: Choice[] = [
+ {value: CONST.FEEDBACK_SURVEY_OPTIONS.TOO_LIMITED.ID, label: translate(CONST.FEEDBACK_SURVEY_OPTIONS.TOO_LIMITED.TRANSLATION_KEY)},
+ {value: CONST.FEEDBACK_SURVEY_OPTIONS.TOO_EXPENSIVE.ID, label: translate(CONST.FEEDBACK_SURVEY_OPTIONS.TOO_EXPENSIVE.TRANSLATION_KEY)},
+ {value: CONST.FEEDBACK_SURVEY_OPTIONS.INADEQUATE_SUPPORT.ID, label: translate(CONST.FEEDBACK_SURVEY_OPTIONS.INADEQUATE_SUPPORT.TRANSLATION_KEY)},
+ {value: CONST.FEEDBACK_SURVEY_OPTIONS.BUSINESS_CLOSING.ID, label: translate(CONST.FEEDBACK_SURVEY_OPTIONS.BUSINESS_CLOSING.TRANSLATION_KEY)},
+ ];
useEffect(() => {
if (!draft?.reason || isLoadingDraft) {
diff --git a/src/components/FixedFooter.tsx b/src/components/FixedFooter.tsx
index ed7bb596088f..07d067a1250f 100644
--- a/src/components/FixedFooter.tsx
+++ b/src/components/FixedFooter.tsx
@@ -4,7 +4,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import type {ReactNode} from 'react';
import type {StyleProp, ViewStyle} from 'react-native';
-import React, {useMemo} from 'react';
+import React from 'react';
import {View} from 'react-native';
type FixedFooterProps = {
@@ -31,10 +31,7 @@ function FixedFooter({style, children, addBottomSafeAreaPadding, addOfflineIndic
styleProperty: shouldStickToBottom ? 'bottom' : 'paddingBottom',
});
- const footerStyle = useMemo>(
- () => [shouldStickToBottom && styles.stickToBottom, bottomSafeAreaPaddingStyle],
- [bottomSafeAreaPaddingStyle, shouldStickToBottom, styles.stickToBottom],
- );
+ const footerStyle: StyleProp = [shouldStickToBottom && styles.stickToBottom, bottomSafeAreaPaddingStyle];
if (!children) {
return null;
diff --git a/src/components/FlatList/FlatList/index.android.tsx b/src/components/FlatList/FlatList/index.android.tsx
index 6651c78ee7d3..abdc45f4412e 100644
--- a/src/components/FlatList/FlatList/index.android.tsx
+++ b/src/components/FlatList/FlatList/index.android.tsx
@@ -16,7 +16,7 @@ function CustomFlatList({ref, enableAnimatedKeyboardDismissal = false, onMome
const lastScrollOffsetRef = useRef(0);
const styles = useThemeStyles();
- const onScreenFocus = useCallback(() => {
+ const onScreenFocus = () => {
if (typeof ref === 'function') {
return;
}
@@ -26,15 +26,12 @@ function CustomFlatList({ref, enableAnimatedKeyboardDismissal = false, onMome
if (ref.current && lastScrollOffsetRef.current) {
ref.current.scrollToOffset({offset: lastScrollOffsetRef.current, animated: false});
}
- }, [ref]);
+ };
- const handleScrollEnd = useCallback(
- (event: NativeSyntheticEvent) => {
- onMomentumScrollEnd?.(event);
- lastScrollOffsetRef.current = event.nativeEvent.contentOffset.y;
- },
- [onMomentumScrollEnd],
- );
+ const handleScrollEnd = (event: NativeSyntheticEvent) => {
+ onMomentumScrollEnd?.(event);
+ lastScrollOffsetRef.current = event.nativeEvent.contentOffset.y;
+ };
const handleScroll = (event: NativeSyntheticEvent) => {
lastScrollOffsetRef.current = event.nativeEvent.contentOffset.y;
diff --git a/src/components/FlatList/FlatList/index.ios.tsx b/src/components/FlatList/FlatList/index.ios.tsx
index 5fc8e9b546a7..3b09f707ae03 100644
--- a/src/components/FlatList/FlatList/index.ios.tsx
+++ b/src/components/FlatList/FlatList/index.ios.tsx
@@ -7,7 +7,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import type {NativeScrollEvent, NativeSyntheticEvent} from 'react-native';
-import React, {useCallback, useRef, useState} from 'react';
+import React, {useRef, useState} from 'react';
import {FlatList} from 'react-native';
import type {CustomFlatListProps} from './types';
@@ -26,30 +26,21 @@ function CustomFlatList({
}: CustomFlatListProps) {
const [isScrolling, setIsScrolling] = useState(false);
const styles = useThemeStyles();
- const handleScrollBegin = useCallback(
- (event: NativeSyntheticEvent) => {
- onMomentumScrollBegin?.(event);
- setIsScrolling(true);
- },
- [onMomentumScrollBegin],
- );
+ const handleScrollBegin = (event: NativeSyntheticEvent) => {
+ onMomentumScrollBegin?.(event);
+ setIsScrolling(true);
+ };
- const handleScrollEnd = useCallback(
- (event: NativeSyntheticEvent) => {
- onMomentumScrollEnd?.(event);
- setIsScrolling(false);
- },
- [onMomentumScrollEnd],
- );
+ const handleScrollEnd = (event: NativeSyntheticEvent) => {
+ onMomentumScrollEnd?.(event);
+ setIsScrolling(false);
+ };
const emitComposerScrollEvents = useEmitComposerScrollEvents({enabled: !enableAnimatedKeyboardDismissal, inverted: restProps.inverted});
- const handleScroll = useCallback(
- (e: NativeSyntheticEvent) => {
- onScrollProp?.(e);
- emitComposerScrollEvents();
- },
- [emitComposerScrollEvents, onScrollProp],
- );
+ const handleScroll = (e: NativeSyntheticEvent) => {
+ onScrollProp?.(e);
+ emitComposerScrollEvents();
+ };
const listRef = useRef | null>(null);
useFlatListHandle({
diff --git a/src/components/FlatList/FlatList/index.tsx b/src/components/FlatList/FlatList/index.tsx
index fc35d965553e..c72a1ba4c91b 100644
--- a/src/components/FlatList/FlatList/index.tsx
+++ b/src/components/FlatList/FlatList/index.tsx
@@ -9,7 +9,7 @@ import mergeRefs from '@libs/mergeRefs';
import type {NativeScrollEvent, NativeSyntheticEvent} from 'react-native';
-import React, {useCallback, useEffect, useLayoutEffect, useRef} from 'react';
+import React, {useEffect, useLayoutEffect, useRef} from 'react';
import {FlatList} from 'react-native';
import type {CustomFlatListProps} from './types';
@@ -46,38 +46,35 @@ function MVCPFlatList({
mvcpAutoscrollToTopThresholdRef.current = mvcpAutoscrollToTopThreshold;
}, [mvcpAutoscrollToTopThreshold]);
- const getScrollOffset = useCallback((): number => {
+ const getScrollOffset = (): number => {
if (!listRef.current) {
return 0;
}
return horizontal ? (getScrollableNode(listRef.current)?.scrollLeft ?? 0) : (getScrollableNode(listRef.current)?.scrollTop ?? 0);
- }, [horizontal]);
+ };
- const getContentView = useCallback(() => getScrollableNode(listRef.current)?.childNodes[0], []);
+ const getContentView = () => getScrollableNode(listRef.current)?.childNodes[0];
- const scrollToOffset = useCallback(
- (offset: number, animated: boolean, interrupt: boolean) => {
- const behavior = animated ? 'smooth' : 'instant';
- const node = getScrollableNode(listRef.current);
- if (node == null) {
- return;
- }
+ const scrollToOffset = (offset: number, animated: boolean, interrupt: boolean) => {
+ const behavior = animated ? 'smooth' : 'instant';
+ const node = getScrollableNode(listRef.current);
+ if (node == null) {
+ return;
+ }
- const overflowProp = horizontal ? 'overflowX' : 'overflowY';
- // Stop momentum scrolling on mobile Safari otherwise the scroll position update
- // will not work.
- if (IS_MOBILE_SAFARI && interrupt) {
- node.style[overflowProp] = 'hidden';
- }
- node.scroll(horizontal ? {left: offset, behavior} : {top: offset, behavior});
- if (IS_MOBILE_SAFARI && interrupt) {
- node.style[overflowProp] = 'scroll';
- }
- },
- [horizontal],
- );
+ const overflowProp = horizontal ? 'overflowX' : 'overflowY';
+ // Stop momentum scrolling on mobile Safari otherwise the scroll position update
+ // will not work.
+ if (IS_MOBILE_SAFARI && interrupt) {
+ node.style[overflowProp] = 'hidden';
+ }
+ node.scroll(horizontal ? {left: offset, behavior} : {top: offset, behavior});
+ if (IS_MOBILE_SAFARI && interrupt) {
+ node.style[overflowProp] = 'scroll';
+ }
+ };
- const prepareForMaintainVisibleContentPosition = useCallback(() => {
+ const prepareForMaintainVisibleContentPosition = () => {
if (mvcpMinIndexForVisible == null) {
return;
}
@@ -103,35 +100,32 @@ function MVCPFlatList({
break;
}
}
- }, [getContentView, getScrollOffset, mvcpMinIndexForVisible, horizontal]);
+ };
- const adjustForMaintainVisibleContentPosition = useCallback(
- (animated = true) => {
- if (mvcpMinIndexForVisible == null) {
- return;
- }
+ const adjustForMaintainVisibleContentPosition = (animated = true) => {
+ if (mvcpMinIndexForVisible == null) {
+ return;
+ }
- const firstVisibleView = firstVisibleViewRef.current;
- const prevFirstVisibleOffset = prevFirstVisibleOffsetRef.current;
- if (firstVisibleView == null || !firstVisibleView.isConnected || prevFirstVisibleOffset == null) {
- return;
- }
+ const firstVisibleView = firstVisibleViewRef.current;
+ const prevFirstVisibleOffset = prevFirstVisibleOffsetRef.current;
+ if (firstVisibleView == null || !firstVisibleView.isConnected || prevFirstVisibleOffset == null) {
+ return;
+ }
- const firstVisibleViewOffset = horizontal ? firstVisibleView.offsetLeft : firstVisibleView.offsetTop;
- const delta = firstVisibleViewOffset - prevFirstVisibleOffset;
- if (Math.abs(delta) > (IS_MOBILE_SAFARI ? 100 : 0.5)) {
- const scrollOffset = lastScrollOffsetRef.current;
- prevFirstVisibleOffsetRef.current = firstVisibleViewOffset;
- scrollToOffset(scrollOffset + delta, false, true);
- if (mvcpAutoscrollToTopThresholdRef.current != null && scrollOffset <= mvcpAutoscrollToTopThresholdRef.current) {
- scrollToOffset(0, animated, false);
- }
+ const firstVisibleViewOffset = horizontal ? firstVisibleView.offsetLeft : firstVisibleView.offsetTop;
+ const delta = firstVisibleViewOffset - prevFirstVisibleOffset;
+ if (Math.abs(delta) > (IS_MOBILE_SAFARI ? 100 : 0.5)) {
+ const scrollOffset = lastScrollOffsetRef.current;
+ prevFirstVisibleOffsetRef.current = firstVisibleViewOffset;
+ scrollToOffset(scrollOffset + delta, false, true);
+ if (mvcpAutoscrollToTopThresholdRef.current != null && scrollOffset <= mvcpAutoscrollToTopThresholdRef.current) {
+ scrollToOffset(0, animated, false);
}
- },
- [scrollToOffset, mvcpMinIndexForVisible, horizontal],
- );
+ }
+ };
- const setupMutationObserver = useCallback(() => {
+ const setupMutationObserver = () => {
const contentView = getContentView();
if (!(contentView instanceof Node)) {
return;
@@ -178,7 +172,7 @@ function MVCPFlatList({
});
mutationObserverRef.current = mutationObserver;
- }, [adjustForMaintainVisibleContentPosition, prepareForMaintainVisibleContentPosition, getContentView]);
+ };
useEffect(() => {
if (!isListRenderedRef.current) {
@@ -193,19 +187,16 @@ function MVCPFlatList({
};
}, [prepareForMaintainVisibleContentPosition, setupMutationObserver]);
- const onRef = useCallback(
- (newRef: FlatList) => {
- // Make sure to only call refs and re-attach listeners if the node changed.
- if (newRef == null || newRef === listRef.current) {
- return;
- }
+ const onRef = (newRef: FlatList) => {
+ // Make sure to only call refs and re-attach listeners if the node changed.
+ if (newRef == null || newRef === listRef.current) {
+ return;
+ }
- mergeRefs(listRef, ref)(newRef);
- prepareForMaintainVisibleContentPosition();
- setupMutationObserver();
- },
- [prepareForMaintainVisibleContentPosition, ref, setupMutationObserver],
- );
+ mergeRefs(listRef, ref)(newRef);
+ prepareForMaintainVisibleContentPosition();
+ setupMutationObserver();
+ };
useFlatListHandle({
ref,
@@ -224,14 +215,11 @@ function MVCPFlatList({
}, []);
const emitComposerScrollEvents = useEmitComposerScrollEvents({enabled: true, inverted: restProps.inverted});
- const handleScroll = useCallback(
- (e: NativeSyntheticEvent) => {
- onScrollProp?.(e);
- prepareForMaintainVisibleContentPosition();
- emitComposerScrollEvents();
- },
- [emitComposerScrollEvents, onScrollProp, prepareForMaintainVisibleContentPosition],
- );
+ const handleScroll = (e: NativeSyntheticEvent) => {
+ onScrollProp?.(e);
+ prepareForMaintainVisibleContentPosition();
+ emitComposerScrollEvents();
+ };
return (
{
- if (typeof focusTrapSettings?.active !== 'undefined') {
- return focusTrapSettings.active;
- }
- // Focus trap can't be active on sidebar screens because it would block access to the tab bar.
- if (isSidebarScreenName(route.name)) {
- return false;
- }
-
- // in top tabs only focus trap for currently shown tab should be active
- if (TOP_TAB_SCREENS.find((screen) => screen === route.name)) {
- return isFocused;
- }
-
- // Focus trap can't be active on these screens if the layout is wide because they may be displayed side by side.
- if (WIDE_LAYOUT_INACTIVE_SCREENS.includes(route.name) && !shouldUseNarrowLayout) {
- return false;
- }
- return isFocused;
- }, [isFocused, shouldUseNarrowLayout, route.name, focusTrapSettings?.active]);
+ // Focus trap can't be active on sidebar screens because it would block access to the tab bar.
+ // in top tabs only focus trap for currently shown tab should be active
+ // Focus trap can't be active on these screens if the layout is wide because they may be displayed side by side.
+ let isActive;
+ if (typeof focusTrapSettings?.active !== 'undefined') {
+ isActive = focusTrapSettings.active;
+ } else {
+ isActive =
+ !isSidebarScreenName(route.name) &&
+ (TOP_TAB_SCREENS.find((screen) => screen === route.name) ? isFocused : !(WIDE_LAYOUT_INACTIVE_SCREENS.includes(route.name) && !shouldUseNarrowLayout) && isFocused);
+ }
return (
{
- if (!isError || typeof message !== 'string') {
- return '';
- }
+ let errorAnnouncementText;
+ if (!isError || typeof message !== 'string') {
+ errorAnnouncementText = '';
+ } else {
const trimmedMessage = message.trim();
if (!trimmedMessage) {
- return '';
+ errorAnnouncementText = '';
+ } else {
+ errorAnnouncementText = shouldRenderMessageAsHTML ? Parser.htmlToText(trimmedMessage) : trimmedMessage;
}
- return shouldRenderMessageAsHTML ? Parser.htmlToText(trimmedMessage) : trimmedMessage;
- }, [isError, message, shouldRenderMessageAsHTML]);
+ }
const hasError = errorAnnouncementText.length > 0;
@@ -102,21 +103,23 @@ function FormHelpMessage({
announcementKey: errorAnnouncementKey,
});
- const HTMLMessage = useMemo(() => {
- if (typeof message !== 'string' || !shouldRenderMessageAsHTML) {
- return '';
- }
-
+ let HTMLMessage;
+ if (typeof message !== 'string' || !shouldRenderMessageAsHTML) {
+ HTMLMessage = '';
+ } else {
// Messages that are already HTML (for example a saved category description hint) must not be parsed again,
// or ExpensiMark re-wraps their mentions and the mention renders blank.
- const replacedText = Parser.isHTML(message) ? message : Parser.replace(message, {shouldEscapeText: false});
-
+ const replacedText = Parser.isHTML(message)
+ ? message
+ : Parser.replace(message, {
+ shouldEscapeText: false,
+ });
if (isError) {
- return `${replacedText}`;
+ HTMLMessage = `${replacedText}`;
+ } else {
+ HTMLMessage = `${replacedText}`;
}
-
- return `${replacedText}`;
- }, [isError, message, shouldRenderMessageAsHTML]);
+ }
const errorIconLabel = isError && shouldShowRedDotIndicator ? CONST.ACCESSIBILITY_LABELS.ERROR : undefined;
diff --git a/src/components/FullScreenBlockingViewContextProvider.tsx b/src/components/FullScreenBlockingViewContextProvider.tsx
index fb98f8b3844a..f0a415008828 100644
--- a/src/components/FullScreenBlockingViewContextProvider.tsx
+++ b/src/components/FullScreenBlockingViewContextProvider.tsx
@@ -1,4 +1,4 @@
-import React, {createContext, useCallback, useContext, useMemo, useState} from 'react';
+import React, {createContext, useContext, useState} from 'react';
type FullScreenBlockingViewStateContextType = {
isBlockingViewVisible: boolean;
@@ -34,19 +34,19 @@ const FullScreenBlockingViewActionsContext = createContext>(new Set());
- const addRouteKey = useCallback((key: string) => {
+ const addRouteKey = (key: string) => {
setRouteKeys((prevKeys) => new Set(prevKeys).add(key));
- }, []);
+ };
- const removeRouteKey = useCallback((key: string) => {
+ const removeRouteKey = (key: string) => {
setRouteKeys((prevKeys) => {
const newKeys = new Set(prevKeys);
newKeys.delete(key);
return newKeys;
});
- }, []);
+ };
- const isBlockingViewVisible = useMemo(() => routeKeys.size > 0, [routeKeys.size]);
+ const isBlockingViewVisible = routeKeys.size > 0;
// Because of the React Compiler we don't need to memoize it manually
// eslint-disable-next-line react/jsx-no-constructed-context-values
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/AccountManagerLinkRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/AccountManagerLinkRenderer.tsx
index 1463550b8118..a6d0d4ff139c 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/AccountManagerLinkRenderer.tsx
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/AccountManagerLinkRenderer.tsx
@@ -14,7 +14,7 @@ import ROUTES from '@src/ROUTES';
import type {StyleProp, TextStyle} from 'react-native';
import type {CustomRendererProps, TPhrasing, TText} from 'react-native-render-html';
-import React, {useCallback} from 'react';
+import React from 'react';
import {TNodeChildrenRenderer} from 'react-native-render-html';
type AccountManagerLinkRendererProps = CustomRendererProps;
@@ -35,12 +35,12 @@ function AccountManagerLinkRenderer({tnode, style}: AccountManagerLinkRendererPr
];
}
- const navigateToAccountManager = useCallback(() => {
+ const navigateToAccountManager = () => {
if (!accountManagerReportID) {
return;
}
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(accountManagerReportID));
- }, [accountManagerReportID]);
+ };
const handleKeyDown = useEnterKeyHandler(navigateToAccountManager);
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx
index 6c66cd649a20..23ad03ecbc0d 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.tsx
@@ -20,7 +20,7 @@ import type {StyleProp, TextStyle} from 'react-native';
import type {CustomRendererProps, TPhrasing, TText} from 'react-native-render-html';
import {Str} from 'expensify-common';
-import React, {useContext, useMemo} from 'react';
+import React, {useContext} from 'react';
import {TNodeChildrenRenderer} from 'react-native-render-html';
import TableLinkColumnContext from './TableLinkColumnContext';
@@ -62,13 +62,7 @@ function AnchorRenderer({tnode, style, key}: AnchorRendererProps) {
// The `styles.link` used by the non-comment link path above already handles this, but AnchorForCommentsOnly does not use it.
const highContrastUnderlineStyle = theme.isHighContrast ? styles.underline : {};
- const onLinkPress = useMemo(() => {
- if (internalNewExpensifyPath || internalExpensifyPath) {
- return () => openLink(attrHref, environmentURL, isAttachment);
- }
-
- return undefined;
- }, [internalNewExpensifyPath, internalExpensifyPath, attrHref, environmentURL, isAttachment]);
+ const onLinkPress = internalNewExpensifyPath || internalExpensifyPath ? () => openLink(attrHref, environmentURL, isAttachment) : undefined;
// The table row already navigates to this link's destination, so the cell shows the link text as plain content
// rather than a second target styled as a link.
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx
index fa0fefe5b04d..c5d1f84b66ba 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx
@@ -21,7 +21,7 @@ import ROUTES from '@src/ROUTES';
import type {CustomRendererProps, TBlock} from 'react-native-render-html';
-import React, {memo} from 'react';
+import React from 'react';
function ImageRenderer({tnode}: CustomRendererProps) {
const icons = useMemoizedLazyExpensifyIcons(['Document', 'GalleryNotFound']);
@@ -147,4 +147,4 @@ function ImageRenderer({tnode}: CustomRendererProps) {
);
}
-export default memo(ImageRenderer, (prevProps, nextProps) => prevProps.tnode.attributes === nextProps.tnode.attributes);
+export default ImageRenderer;
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/index.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/index.tsx
index 10f274332392..d89cc180a128 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/index.tsx
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/index.tsx
@@ -19,7 +19,7 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type {TextStyle} from 'react-native';
import type {CustomRendererProps, TPhrasing, TText} from 'react-native-render-html';
-import React, {useContext, useMemo} from 'react';
+import React, {useContext} from 'react';
import {StyleSheet} from 'react-native';
import MentionReportContext from './MentionReportContext';
@@ -38,10 +38,7 @@ function MentionReportRenderer({style, tnode, TDefaultRenderer, ...defaultRender
const [currentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${currentReportIDValue}`);
// When we invite someone to a room they don't have the policy object, but we still want them to be able to see and click on report mentions, so we only check if the policyID in the report is from a workspace
- const isGroupPolicyReport = useMemo(
- () => (!!currentReport && !isEmptyObject(currentReport) && !!currentReport.policyID && currentReport.policyID !== CONST.POLICY.ID_FAKE) || !!policyID,
- [currentReport, policyID],
- );
+ const isGroupPolicyReport = (!!currentReport && !isEmptyObject(currentReport) && !!currentReport.policyID && currentReport.policyID !== CONST.POLICY.ID_FAKE) || !!policyID;
const mentionDetails = getReportMentionDetails(htmlAttributeReportID, currentReport, reports, tnode, policyID);
if (!mentionDetails) {
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/TableRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/TableRenderer.tsx
index 5561ca20fef2..fedd88079bfb 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/TableRenderer.tsx
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/TableRenderer.tsx
@@ -2,7 +2,7 @@ import variables from '@styles/variables';
import type {CustomRendererProps, TBlock, TNode} from 'react-native-render-html';
-import {useContext, useMemo} from 'react';
+import {useContext} from 'react';
import {useContentWidth} from 'react-native-render-html';
import type {CellHorizontalAlignment} from './TableColumnAlignmentContext';
@@ -32,8 +32,8 @@ function getColumnAlignments(tableNode: TNode): CellHorizontalAlignment[] {
}
function TableRenderer({tnode}: CustomRendererProps) {
- const columnAlignments = useMemo(() => getColumnAlignments(tnode), [tnode]);
- const linkColumnIndex = useMemo(() => getLinkColumnIndex(tnode), [tnode]);
+ const columnAlignments = getColumnAlignments(tnode);
+ const linkColumnIndex = getLinkColumnIndex(tnode);
// The comment-level width fills the message exactly; fall back to the HTML content width when the table is rendered
// outside a comment. A concrete number is required because the scroller needs a fixed viewport and content width.
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPie.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPie.tsx
index 84bf11d2cbff..e059d09f82e8 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPie.tsx
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPie.tsx
@@ -21,7 +21,7 @@ import useTheme from '@hooks/useTheme';
import type {TNode} from 'react-native-render-html';
-import React, {useMemo} from 'react';
+import React from 'react';
import {HTMLContentModel, useAmbientTRenderEngine} from 'react-native-render-html';
import {Pie} from 'victory-native';
@@ -90,11 +90,10 @@ function VictoryChartPie({tnode}: VictoryChartPieProps) {
sliceValues.push({label: dataLabel, value: polarEntry.value});
}
- const resolvedLabelLayout = useMemo(() => {
- if (!baseLabelItem || !effectiveLabelRadius) {
- return {};
- }
-
+ let resolvedLabelLayout;
+ if (!baseLabelItem || !effectiveLabelRadius) {
+ resolvedLabelLayout = {};
+ } else {
const slices = computeSliceAngles(sliceValues, START_ANGLE);
const rowHeight = computeLabelBlockHeight(baseLabelItem, typefaces);
const designHeight = typeof chartContentStyles.height === 'number' ? chartContentStyles.height : undefined;
@@ -107,8 +106,14 @@ function VictoryChartPie({tnode}: VictoryChartPieProps) {
const topFor = (columnTitleSafeTop: number) =>
designHeight ? Math.max(-Math.min(designHeight / 2, effectiveLabelRadius), columnTitleSafeTop + rowHeight / 2 - designHeight / 2) : -effectiveLabelRadius;
const plotBounds = {
- left: {top: topFor(scaledTitleSafeTop + scaledLeftColumnTopPadding), bottom},
- right: {top: topFor(scaledTitleSafeTop), bottom},
+ left: {
+ top: topFor(scaledTitleSafeTop + scaledLeftColumnTopPadding),
+ bottom,
+ },
+ right: {
+ top: topFor(scaledTitleSafeTop),
+ bottom,
+ },
};
const textRadius = computeTextRadiusBySide({
slices,
@@ -119,9 +124,14 @@ function VictoryChartPie({tnode}: VictoryChartPieProps) {
designWidth,
edgePadding,
});
-
- return computePieLabelLayout({slices, rowHeight, labelRadius: effectiveLabelRadius, textRadius, plotBounds});
- }, [sliceValues, baseLabelItem, effectiveLabelRadius, typefaces, chartContentStyles.height, chartContentStyles.width, customLabelByDataLabel, pixelScale]);
+ resolvedLabelLayout = computePieLabelLayout({
+ slices,
+ rowHeight,
+ labelRadius: effectiveLabelRadius,
+ textRadius,
+ plotBounds,
+ });
+ }
return (
{
- const isColorfulBackground = (backgroundColor ?? theme.appBG) !== theme.appBG && (backgroundColor ?? theme.highlightBG) !== theme.highlightBG;
- return {
- titleColor: isColorfulBackground ? theme.textColorfulBackground : undefined,
- iconFill: isColorfulBackground ? theme.iconColorfulBackground : undefined,
- };
- }, [backgroundColor, theme.appBG, theme.highlightBG, theme.iconColorfulBackground, theme.textColorfulBackground]);
+ const isColorfulBackground = (backgroundColor ?? theme.appBG) !== theme.appBG && (backgroundColor ?? theme.highlightBG) !== theme.highlightBG;
+ const {titleColor, iconFill} = {
+ titleColor: isColorfulBackground ? theme.textColorfulBackground : undefined,
+ iconFill: isColorfulBackground ? theme.iconColorfulBackground : undefined,
+ };
return (
{
- const stepCounterTranslation = stepCounter ? translate('stepCounter', stepCounter.step, stepCounter.total, stepCounter.text) : undefined;
- if (shouldShowReportAvatarWithDisplay) {
- return (
-
- );
- }
-
+ let middleContent;
+ const stepCounterTranslation = stepCounter ? translate('stepCounter', stepCounter.step, stepCounter.total, stepCounter.text) : undefined;
+ if (shouldShowReportAvatarWithDisplay) {
+ middleContent = (
+
+ );
+ } else {
const resolvedSubtitle = stepCounterTranslation ?? subtitle;
-
- return (
+ middleContent = (
{subTitleLink}}
);
- }, [
- StyleUtils,
- subTitleLink,
- shouldUseHeadlineHeader,
- report,
- shouldEnableDetailPageNavigation,
- shouldShowReportAvatarWithDisplay,
- stepCounter,
- styles.textHeadlineH2,
- subtitle,
- title,
- titleColor,
- titleStyles,
- numberOfTitleLines,
- translate,
- openParentReportInCurrentTab,
- shouldDisplayStatus,
- shouldSkipFocusAfterTransition,
- ]);
- const ThreeDotMenuButton = useMemo(() => {
- if (shouldShowThreeDotsButton) {
- return threeDotsMenuItems.length === 1 && shouldMinimizeMenuButton ? (
+ }
+ let ThreeDotMenuButton;
+ if (shouldShowThreeDotsButton) {
+ ThreeDotMenuButton =
+ threeDotsMenuItems.length === 1 && shouldMinimizeMenuButton ? (
);
- }
- return null;
- }, [
- shouldShowThreeDotsButton,
- threeDotsMenuItems,
- shouldMinimizeMenuButton,
- styles.touchableButtonImage,
- theme.icon,
- threeDotsMenuIcon,
- threeDotsMenuIconFill,
- shouldDisableThreeDotsButton,
- onThreeDotsButtonPress,
- shouldOverlayDots,
- threeDotsAnchorAlignment,
- shouldSetModalVisibility,
- ]);
+ } else {
+ ThreeDotMenuButton = null;
+ }
return (
{
- setAttachmentLoaded(source as AttachmentSource, true);
- if (!onLoad) {
- return;
- }
- if (isLoadedRef.current === true) {
- return;
- }
-
- // We override `onLoad`, so both web and native have the same signature
- const {width, height} = event.source;
- isLoadedRef.current = true;
- onLoad({nativeEvent: {width, height}});
- },
- [onLoad, setAttachmentLoaded, source],
- );
+ const imageLoadedSuccessfully = (event: ImageLoadEventData) => {
+ setAttachmentLoaded(source as AttachmentSource, true);
+ if (!onLoad) {
+ return;
+ }
+ if (isLoadedRef.current === true) {
+ return;
+ }
+
+ // We override `onLoad`, so both web and native have the same signature
+ const {width, height} = event.source;
+ isLoadedRef.current = true;
+ onLoad({nativeEvent: {width, height}});
+ };
return (
{
- setAttachmentLoaded?.(source as AttachmentSource, true);
- if (!onLoad) {
- return;
- }
+ const imageLoadedSuccessfully = (event: ImageLoadEventData) => {
+ setAttachmentLoaded?.(source as AttachmentSource, true);
+ if (!onLoad) {
+ return;
+ }
- // We override `onLoad`, so both web and native have the same signature
- const {width, height} = event.source;
- onLoad({nativeEvent: {width, height}});
- },
- [onLoad, source, setAttachmentLoaded],
- );
+ // We override `onLoad`, so both web and native have the same signature
+ const {width, height} = event.source;
+ onLoad({nativeEvent: {width, height}});
+ };
return (
(typeof url === 'string' ? {uri: url} : url), [url]);
+ const source = typeof url === 'string' ? {uri: url} : url;
const onError = () => {
Log.hmmm('Unable to fetch image to calculate size', {url});
@@ -111,4 +111,4 @@ function ImageWithSizeCalculation({
ImageWithSizeCalculation.displayName = 'ImageWithSizeCalculation';
-export default React.memo(ImageWithSizeCalculation);
+export default ImageWithSizeCalculation;
diff --git a/src/components/KeyboardAvoidingView/index.tsx b/src/components/KeyboardAvoidingView/index.tsx
index 039084bfb91a..c2f4096f7903 100644
--- a/src/components/KeyboardAvoidingView/index.tsx
+++ b/src/components/KeyboardAvoidingView/index.tsx
@@ -1,6 +1,6 @@
import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings';
-import React, {useMemo} from 'react';
+import React from 'react';
import type {KeyboardAvoidingViewProps} from './types';
@@ -9,10 +9,7 @@ import BaseKeyboardAvoidingView from './BaseKeyboardAvoidingView';
function KeyboardAvoidingView({shouldOffsetBottomSafeAreaPadding = false, keyboardVerticalOffset: keyboardVerticalOffsetProp, ...restProps}: KeyboardAvoidingViewProps) {
const {paddingBottom} = useSafeAreaPaddings(true);
- const keyboardVerticalOffset = useMemo(
- () => (keyboardVerticalOffsetProp ?? 0) + (shouldOffsetBottomSafeAreaPadding ? -paddingBottom : 0),
- [keyboardVerticalOffsetProp, paddingBottom, shouldOffsetBottomSafeAreaPadding],
- );
+ const keyboardVerticalOffset = (keyboardVerticalOffsetProp ?? 0) + (shouldOffsetBottomSafeAreaPadding ? -paddingBottom : 0);
return (
{
- // TODO: CONCIERGE_LHN_GBR tooltip will be replaced by a tooltip in the #admins room
- // https://github.com/Expensify/App/issues/57045#issuecomment-2701455668
- const tooltip = CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.CONCIERGE_LHN_GBR;
- const shouldTooltipBeVisible = shouldUseNarrowLayout ? isScreenFocused && isReportsSplitNavigatorLast : isReportsSplitNavigatorLast && !isFullscreenVisible;
-
- return {
- tooltipToRender: tooltip,
- shouldShowTooltip: shouldTooltipBeVisible,
- };
- }, [isScreenFocused, shouldUseNarrowLayout, isReportsSplitNavigatorLast, isFullscreenVisible]);
+ // TODO: CONCIERGE_LHN_GBR tooltip will be replaced by a tooltip in the #admins room
+ // https://github.com/Expensify/App/issues/57045#issuecomment-2701455668
+ const tooltip = CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.CONCIERGE_LHN_GBR;
+ const shouldTooltipBeVisible = shouldUseNarrowLayout ? isScreenFocused && isReportsSplitNavigatorLast : isReportsSplitNavigatorLast && !isFullscreenVisible;
+ const {tooltipToRender, shouldShowTooltip} = {
+ tooltipToRender: tooltip,
+ shouldShowTooltip: shouldTooltipBeVisible,
+ };
return useProductTrainingContext(tooltipToRender, shouldShowTooltip);
}
diff --git a/src/components/LocalePicker.tsx b/src/components/LocalePicker.tsx
index f8f82989805b..f21dc6238067 100644
--- a/src/components/LocalePicker.tsx
+++ b/src/components/LocalePicker.tsx
@@ -13,7 +13,7 @@ import {setLocale} from '@userActions/App';
import {LOCALE_TO_LANGUAGE_STRING, SORTED_LOCALES} from '@src/CONST/LOCALES';
import ONYXKEYS from '@src/ONYXKEYS';
-import React, {useMemo} from 'react';
+import React from 'react';
import {View} from 'react-native';
import type {PickerSize} from './Picker/types';
@@ -34,15 +34,13 @@ function LocalePicker({size = 'normal'}: LocalePickerProps) {
const [account] = useOnyx(ONYXKEYS.ACCOUNT);
const icons = useMemoizedLazyExpensifyIcons(['Globe']);
- const locales = useMemo(() => {
- const sortedLocales = SORTED_LOCALES;
- return sortedLocales.map((locale) => ({
- value: locale,
- label: LOCALE_TO_LANGUAGE_STRING[locale],
- keyForList: locale,
- isSelected: preferredLocale === locale,
- }));
- }, [preferredLocale]);
+ const sortedLocales = SORTED_LOCALES;
+ const locales = sortedLocales.map((locale) => ({
+ value: locale,
+ label: LOCALE_TO_LANGUAGE_STRING[locale],
+ keyForList: locale,
+ isSelected: preferredLocale === locale,
+ }));
const shouldDisablePicker = AccountUtils.isValidateCodeFormSubmitting(account);
diff --git a/src/components/MapView/MapView.tsx b/src/components/MapView/MapView.tsx
index 6157656c1374..38b53eb22a0d 100644
--- a/src/components/MapView/MapView.tsx
+++ b/src/components/MapView/MapView.tsx
@@ -23,7 +23,7 @@ import type {MapState} from '@rnmapbox/maps';
import {useFocusEffect} from '@react-navigation/native';
import Mapbox, {MarkerView} from '@rnmapbox/maps';
import {getForegroundPermissionsAsync} from 'expo-location';
-import {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react';
+import {useCallback, useEffect, useRef, useState} from 'react';
import {View} from 'react-native';
import {useSharedValue} from 'react-native-reanimated';
@@ -68,7 +68,7 @@ function MapView({
const cameraRef = useRef(null);
const pendingFramingRef = useRef(null);
const [isIdle, setIsIdle] = useState(false);
- const initialLocation = useMemo(() => initialState && {longitude: initialState.location[0], latitude: initialState.location[1]}, [initialState]);
+ const initialLocation = initialState && {longitude: initialState.location[0], latitude: initialState.location[1]};
const currentPosition = userLocation ?? initialLocation;
const currentPositionLongitude = currentPosition?.longitude;
const currentPositionLatitude = currentPosition?.latitude;
@@ -81,20 +81,14 @@ function MapView({
// location without bothering the user. It will return
// false if user has already started dragging the map or
// if there are one or more waypoints present.
- const shouldPanMapToCurrentPosition = useCallback(
- () => !userInteractedWithMap && shouldDisplayCurrentLocation && (!waypoints || waypoints.length === 0),
- [userInteractedWithMap, waypoints, shouldDisplayCurrentLocation],
- );
+ const shouldPanMapToCurrentPosition = () => !userInteractedWithMap && shouldDisplayCurrentLocation && (!waypoints || waypoints.length === 0);
- const setCurrentPositionToInitialState: GeolocationErrorCallback = useCallback(
- (error) => {
- if (error?.code !== GeolocationErrorCode.PERMISSION_DENIED || !initialLocation) {
- return;
- }
- clearUserLocation();
- },
- [initialLocation],
- );
+ const setCurrentPositionToInitialState: GeolocationErrorCallback = (error) => {
+ if (error?.code !== GeolocationErrorCode.PERMISSION_DENIED || !initialLocation) {
+ return;
+ }
+ clearUserLocation();
+ };
useFocusEffect(
useCallback(() => {
@@ -254,7 +248,7 @@ function MapView({
// The waypoints can disappear after the user centered on their location, so the button only offers them while they are still a target.
const shouldShowWaypointsCenterIcon = isWaypointsNextCenterTarget && hasWaypointsTarget;
- const centerMap = useCallback(() => {
+ const centerMap = () => {
pendingFramingRef.current?.cancel();
const waypointCoordinates = waypoints?.map((waypoint) => waypoint.coordinate) ?? [];
const shouldCenterOnWaypoints = hasWaypointsTarget && (isWaypointsNextCenterTarget || !canCenterOnCurrentPosition);
@@ -287,73 +281,70 @@ function MapView({
animationMode: CONST.MAPBOX.CAMERA_ANIMATION_MODE,
zoomLevel: CONST.MAPBOX.SINGLE_MARKER_ZOOM,
});
- }, [
- allDirectionCoordinates,
- canCenterOnCurrentPosition,
- currentPositionLongitude,
- currentPositionLatitude,
- hasWaypointsTarget,
- isWaypointsNextCenterTarget,
- mapPadding,
- singleWaypointCoordinate,
- waypoints,
- ]);
+ };
- const centerCoordinate = useMemo(() => (currentPosition ? [currentPosition.longitude, currentPosition.latitude] : initialState?.location), [currentPosition, initialState?.location]);
+ const centerCoordinate = currentPosition ? [currentPosition.longitude, currentPosition.latitude] : initialState?.location;
- const waypointsBounds = useMemo(() => {
- if (!waypoints) {
- return undefined;
- }
+ let waypointsBounds;
+ if (!waypoints) {
+ waypointsBounds = undefined;
+ } else {
const {northEast, southWest} = utils.getBounds(
waypoints.map((waypoint) => waypoint.coordinate),
allDirectionCoordinates,
);
- return {ne: northEast, sw: southWest};
- }, [waypoints, allDirectionCoordinates]);
+ waypointsBounds = {
+ ne: northEast,
+ sw: southWest,
+ };
+ }
const hasWaypoints = !!waypoints?.length;
- const defaultSettings: Mapbox.CameraStop | undefined = useMemo(() => {
- if (interactive) {
- // Start framed on the waypoints the same way the focus effect frames them, so the map does not fly in from the current location
- if (waypoints?.length === 1) {
- return {
- zoomLevel: CONST.MAPBOX.SINGLE_MARKER_ZOOM,
- centerCoordinate: waypoints.at(0)?.coordinate,
- };
- }
- if (waypoints?.length && waypointsBounds) {
- return {
- bounds: waypointsBounds,
- padding: utils.getCameraPadding(mapPadding),
- };
- }
- if (!centerCoordinate) {
- return undefined;
- }
- return {
- zoomLevel: initialState?.zoom,
- centerCoordinate,
+ let defaultSettings: Mapbox.CameraStop | undefined;
+ if (interactive) {
+ // Start framed on the waypoints the same way the focus effect frames them, so the map does not fly in from the current location
+ if (waypoints?.length === 1) {
+ defaultSettings = {
+ zoomLevel: CONST.MAPBOX.SINGLE_MARKER_ZOOM,
+ centerCoordinate: waypoints.at(0)?.coordinate,
};
+ } else if (waypoints?.length && waypointsBounds) {
+ defaultSettings = {
+ bounds: waypointsBounds,
+ padding: utils.getCameraPadding(mapPadding),
+ };
+ } else {
+ defaultSettings = !centerCoordinate
+ ? undefined
+ : {
+ zoomLevel: initialState?.zoom,
+ centerCoordinate,
+ };
}
- if (!waypointsBounds) {
- return undefined;
- }
- return {
- bounds: waypointsBounds,
- };
- }, [interactive, waypoints, waypointsBounds, mapPadding, centerCoordinate, initialState?.zoom]);
+ } else {
+ defaultSettings = !waypointsBounds
+ ? undefined
+ : {
+ bounds: waypointsBounds,
+ };
+ }
// The controlled centerCoordinate is applied after defaultSettings, so skip it while waypoints are shown or it would pull the camera back to the current location
- const initCenterCoordinate = useMemo(() => (interactive && !hasWaypoints ? centerCoordinate : undefined), [interactive, hasWaypoints, centerCoordinate]);
- const initBounds = useMemo(() => (interactive ? undefined : waypointsBounds), [interactive, waypointsBounds]);
+ const initCenterCoordinate = interactive && !hasWaypoints ? centerCoordinate : undefined;
+ const initBounds = interactive ? undefined : waypointsBounds;
// Localize the map labels to the user's preferred app locale so they match the rest of the app.
- const localizeLabels = useMemo<{locale: string} | undefined>(() => {
- const language = utils.getNativeMapboxLanguage(preferredLocale);
- return language ? {locale: language} : undefined;
- }, [preferredLocale]);
+ const language = utils.getNativeMapboxLanguage(preferredLocale);
+ const localizeLabels:
+ | {
+ locale: string;
+ }
+ | undefined = language
+ ? {
+ locale: language,
+ }
+ : undefined;
return !isOffline && isAccessTokenReady && !!defaultSettings ? (
@@ -462,4 +453,4 @@ function MapView({
);
}
-export default memo(MapView);
+export default MapView;
diff --git a/src/components/MapView/MapViewImpl.web.tsx b/src/components/MapView/MapViewImpl.web.tsx
index 30e349c05c06..5ba7ea23d62d 100644
--- a/src/components/MapView/MapViewImpl.web.tsx
+++ b/src/components/MapView/MapViewImpl.web.tsx
@@ -21,14 +21,14 @@ import useNetwork from '@src/hooks/useNetwork';
import getCurrentPosition from '@src/libs/getCurrentPosition';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {MapMouseEvent, MapRef, ViewState, ViewStateChangeEvent} from 'react-map-gl/mapbox';
+import type {MapMouseEvent, MapProps, MapRef, ViewState, ViewStateChangeEvent} from 'react-map-gl/mapbox';
// Explanation: Different Mapbox libraries are required for web and native mobile platforms.
// This is why we have separate components for web and native to handle the specific implementations.
// For the web version, we use the Mapbox Web library called react-map-gl, while for the native mobile version,
// we utilize a different Mapbox library @rnmapbox/maps tailored for mobile development.
import {useFocusEffect} from '@react-navigation/native';
-import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
+import React, {useCallback, useEffect, useRef, useState} from 'react';
import Map, {Marker} from 'react-map-gl/mapbox';
import {View} from 'react-native';
@@ -73,7 +73,7 @@ function MapViewImpl({
const expensifyIcons = useMemoizedLazyExpensifyIcons(['Crosshair', 'Expand', 'MapCurrentLocation']);
const [mapRef, setMapRef] = useState(null);
- const initialLocation = useMemo(() => ({longitude: initialState.location[0], latitude: initialState.location[1]}), [initialState]);
+ const initialLocation = {longitude: initialState.location[0], latitude: initialState.location[1]};
const currentPosition = userLocation ?? initialLocation;
const prevUserPosition = usePrevious(currentPosition);
const [userInteractedWithMap, setUserInteractedWithMap] = useState(false);
@@ -85,7 +85,7 @@ function MapViewImpl({
const [hasValidContainerSize, setHasValidContainerSize] = useState(false);
const containerRef = useRef(null);
const resetBoundariesRef = useRef<() => void>(() => {});
- const setRef = useCallback((newRef: MapRef | null) => setMapRef(newRef), []);
+ const setRef = (newRef: MapRef | null) => setMapRef(newRef);
const shouldInitializeCurrentPosition = useRef(true);
const [lastViewState, setLastViewState] = useState();
@@ -93,20 +93,14 @@ function MapViewImpl({
// location without bothering the user. It will return
// false if user has already started dragging the map or
// if there are one or more waypoints present.
- const shouldPanMapToCurrentPosition = useCallback(
- () => !userInteractedWithMap && shouldDisplayCurrentLocation && (!waypoints || waypoints.length === 0),
- [userInteractedWithMap, waypoints, shouldDisplayCurrentLocation],
- );
+ const shouldPanMapToCurrentPosition = () => !userInteractedWithMap && shouldDisplayCurrentLocation && (!waypoints || waypoints.length === 0);
- const setCurrentPositionToInitialState: GeolocationErrorCallback = useCallback(
- (error) => {
- if (error?.code !== GeolocationErrorCode.PERMISSION_DENIED || !initialLocation) {
- return;
- }
- clearUserLocation();
- },
- [initialLocation],
- );
+ const setCurrentPositionToInitialState: GeolocationErrorCallback = (error) => {
+ if (error?.code !== GeolocationErrorCode.PERMISSION_DENIED || !initialLocation) {
+ return;
+ }
+ clearUserLocation();
+ };
useFocusEffect(
useCallback(() => {
@@ -152,7 +146,7 @@ function MapViewImpl({
});
}, [currentPosition, mapRef, prevUserPosition.longitude, prevUserPosition.latitude, shouldPanMapToCurrentPosition]);
- const resetBoundaries = useCallback(() => {
+ const resetBoundaries = () => {
if (!waypoints || waypoints.length === 0) {
return;
}
@@ -188,7 +182,7 @@ function MapViewImpl({
allDirectionCoordinates,
);
map.fitBounds([northEast, southWest], {padding: mapPadding, speed: CONST.MAPBOX.FLY_TO_SPEED});
- }, [waypoints, mapRef, mapPadding, allDirectionCoordinates]);
+ };
// The ResizeObserver outlives renders, so it reads the latest reset logic through a ref.
useEffect(() => {
@@ -277,7 +271,7 @@ function MapViewImpl({
// The waypoints can disappear after the user centered on their location, so the button only offers them while they are still a target.
const shouldShowWaypointsCenterIcon = isWaypointsNextCenterTarget && hasWaypointsTarget;
- const centerMap = useCallback(() => {
+ const centerMap = () => {
if (!mapRef) {
return;
}
@@ -307,90 +301,78 @@ function MapViewImpl({
bearing: 0,
speed: CONST.MAPBOX.FLY_TO_SPEED,
});
- }, [
- allDirectionCoordinates,
- canCenterOnCurrentPosition,
- currentPosition?.longitude,
- currentPosition?.latitude,
- hasWaypointsTarget,
- isWaypointsNextCenterTarget,
- mapRef,
- mapPadding,
- singleWaypointCoordinate,
- waypoints,
- ]);
+ };
const saveViewState = (event: ViewStateChangeEvent) => {
setLastViewState(event.viewState);
};
- const initialViewState: Partial | undefined = useMemo(() => {
- if (!interactive) {
- // With no markers and no route geometry, getBounds returns infinite bounds that crash the map on init.
- // Fall back to a plain centered view state until route data arrives.
- if (!waypoints?.length && !allDirectionCoordinates.length) {
- return {
- longitude: currentPosition?.longitude,
- latitude: currentPosition?.latitude,
- zoom: initialState.zoom,
- };
- }
+ let initialViewState: MapProps['initialViewState'];
+ if (!interactive) {
+ // With no markers and no route geometry, getBounds returns infinite bounds that crash the map on init.
+ // Fall back to a plain centered view state until route data arrives.
+ if (!waypoints?.length && !allDirectionCoordinates.length) {
+ initialViewState = {
+ longitude: currentPosition?.longitude,
+ latitude: currentPosition?.latitude,
+ zoom: initialState.zoom,
+ };
+ } else {
const {northEast, southWest} = utils.getBounds(waypoints?.map((waypoint) => waypoint.coordinate) ?? [], allDirectionCoordinates);
- return {
+ initialViewState = {
zoom: initialState.zoom,
bounds: [northEast, southWest],
- fitBoundsOptions: {padding: mapPadding},
+ fitBoundsOptions: {
+ padding: mapPadding,
+ },
};
}
- if (lastViewState && waypoints?.length) {
- return lastViewState;
- }
+ } else if (lastViewState && waypoints?.length) {
+ initialViewState = lastViewState;
+ } else {
// Start framed on the waypoints the same way resetBoundaries frames them, so the map does not fly in from the current location
const firstWaypointCoordinate = waypoints?.length === 1 ? waypoints.at(0)?.coordinate : undefined;
if (firstWaypointCoordinate) {
- return {
+ initialViewState = {
longitude: firstWaypointCoordinate[0],
latitude: firstWaypointCoordinate[1],
zoom: CONST.MAPBOX.SINGLE_MARKER_ZOOM,
};
- }
- if (waypoints?.length) {
+ } else if (waypoints?.length) {
const {northEast, southWest} = utils.getBounds(
waypoints.map((waypoint) => waypoint.coordinate),
allDirectionCoordinates,
);
- return {
+ initialViewState = {
bounds: [northEast, southWest],
fitBoundsOptions: {padding: mapPadding},
};
+ } else {
+ initialViewState = {
+ longitude: currentPosition?.longitude,
+ latitude: currentPosition?.latitude,
+ zoom: initialState.zoom,
+ };
}
- return {
- longitude: currentPosition?.longitude,
- latitude: currentPosition?.latitude,
- zoom: initialState.zoom,
- };
- }, [waypoints, allDirectionCoordinates, interactive, currentPosition?.longitude, currentPosition?.latitude, initialState.zoom, mapPadding, lastViewState]);
+ }
// The route layers only need to be interactive when there is an alternate route to pick, so that clicking a route selects it.
- const interactiveLayerIds = useMemo(() => (interactive && hasAlternateDirection ? ALTERNATE_DIRECTIONS_LAYER_IDS : undefined), [interactive, hasAlternateDirection]);
+ const interactiveLayerIds = interactive && hasAlternateDirection ? ALTERNATE_DIRECTIONS_LAYER_IDS : undefined;
- const onDrag = useCallback(() => {
+ const onDrag = () => {
setUserInteractedWithMap(true);
// Dragging must keep the grabbing cursor even when it starts on top of a route.
setIsHoveringDirection(false);
setIsWaypointsNextCenterTarget(false);
- }, []);
+ };
- const selectClickedDirection = useCallback(
- (event: MapMouseEvent) => {
- const isAlternate: unknown = event.features?.at(0)?.properties?.isAlternate;
- if (typeof isAlternate !== 'boolean') {
- return;
- }
- setIsAlternateDirectionSelected?.(isAlternate);
- },
- [setIsAlternateDirectionSelected],
- );
+ const selectClickedDirection = (event: MapMouseEvent) => {
+ const isAlternate: unknown = event.features?.at(0)?.properties?.isAlternate;
+ if (typeof isAlternate !== 'boolean') {
+ return;
+ }
+ setIsAlternateDirectionSelected?.(isAlternate);
+ };
return !isOffline && !!accessToken && !!initialViewState ? (
{
+ const toggleDistanceUnit = () => {
userToggledUnit.current = true;
setDistanceUnit((currentUnit) =>
currentUnit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS ? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES : CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS,
);
- }, []);
+ };
return {distanceUnit, toggleDistanceUnit};
}
diff --git a/src/components/MenuItem/MenuItem.tsx b/src/components/MenuItem/MenuItem.tsx
index 80ee4da53299..1e7a06b837c7 100644
--- a/src/components/MenuItem/MenuItem.tsx
+++ b/src/components/MenuItem/MenuItem.tsx
@@ -65,7 +65,7 @@ import type {AccessibilityState, GestureResponderEvent, ImageStyle, Role, StyleP
import type {AnimatedStyle} from 'react-native-reanimated';
import type {ValueOf} from 'type-fest';
-import React, {useMemo, useRef} from 'react';
+import React, {useRef} from 'react';
import {View} from 'react-native';
import useRemoveNonInteractiveClickHandler from './hooks/useRemoveNonInteractiveClickHandler';
@@ -642,8 +642,12 @@ function MenuItem({
const shouldDimIconRight = iconRight === icons.ArrowRight || !iconRight;
const hasIcon = (!!icon || iconType === CONST.ICON_TYPE_WORKSPACE) && !Array.isArray(icon);
- // eslint-disable-next-line no-nested-ternary -- Selects ml2/ml3/empty based on icon presence and avatar size
- const iconLeftPadding = shouldPutLeftPaddingWhenNoIcon || hasIcon ? (avatarSize === CONST.AVATAR_SIZE.SMALL ? styles.ml2 : styles.ml3) : {};
+ let iconLeftPadding;
+ if (shouldPutLeftPaddingWhenNoIcon || hasIcon) {
+ iconLeftPadding = avatarSize === CONST.AVATAR_SIZE.SMALL ? styles.ml2 : styles.ml3;
+ } else {
+ iconLeftPadding = {};
+ }
const combinedTitleTextStyle = StyleUtils.combineStyles(
[
@@ -699,50 +703,46 @@ function MenuItem({
);
};
- const html = useMemo(() => {
- if (!title || !shouldParseTitle) {
- return '';
- }
- return Parser.replace(title, {shouldEscapeText, disabledRules: excludedMarkdownRules});
- }, [title, shouldParseTitle, shouldEscapeText, excludedMarkdownRules]);
-
- const helperHtml = useMemo(() => {
- if (!helperText || !shouldParseHelperText) {
- return '';
- }
- return Parser.replace(helperText, {shouldEscapeText});
- }, [helperText, shouldParseHelperText, shouldEscapeText]);
+ const html =
+ !title || !shouldParseTitle
+ ? ''
+ : Parser.replace(title, {
+ shouldEscapeText,
+ disabledRules: excludedMarkdownRules,
+ });
+
+ const helperHtml =
+ !helperText || !shouldParseHelperText
+ ? ''
+ : Parser.replace(helperText, {
+ shouldEscapeText,
+ });
const shouldRenderTitleAsHTML = shouldRenderAsHTML && !!title && Parser.isHTML(title);
const shouldEnableTextSelection = isTitleSelectable && getPlatform() === CONST.PLATFORM.WEB;
- const processedTitle = useMemo(() => {
- let titleToWrap = '';
- if (shouldRenderAsHTML) {
- titleToWrap = title ?? '';
- }
-
- if (shouldParseTitle) {
- titleToWrap = html;
- }
-
- if (shouldTruncateTitle) {
- titleToWrap = Parser.truncateHTML(`${titleToWrap}`, characterLimit, {ellipsis: '...'});
- return titleToWrap;
- }
-
- return titleToWrap ? `${titleToWrap}` : '';
- }, [title, shouldRenderAsHTML, shouldParseTitle, characterLimit, shouldTruncateTitle, html]);
-
- const processedHelperText = useMemo(() => {
- let textToWrap = '';
-
- if (shouldParseHelperText) {
- textToWrap = helperHtml;
- }
+ let processedTitle;
+ let titleToWrap = '';
+ if (shouldRenderAsHTML) {
+ titleToWrap = title ?? '';
+ }
+ if (shouldParseTitle) {
+ titleToWrap = html;
+ }
+ if (shouldTruncateTitle) {
+ titleToWrap = Parser.truncateHTML(`${titleToWrap}`, characterLimit, {
+ ellipsis: '...',
+ });
+ processedTitle = titleToWrap;
+ } else {
+ processedTitle = titleToWrap ? `${titleToWrap}` : '';
+ }
- return textToWrap ? `${textToWrap}` : '';
- }, [shouldParseHelperText, helperHtml]);
+ let textToWrap = '';
+ if (shouldParseHelperText) {
+ textToWrap = helperHtml;
+ }
+ const processedHelperText = textToWrap ? `${textToWrap}` : '';
const hasPressableRightComponent = (iconRight ?? icons.ArrowRight) || (shouldShowRightComponent && rightComponent);
diff --git a/src/components/MigratedUserWelcomeModal.tsx b/src/components/MigratedUserWelcomeModal.tsx
index 2186265546da..5ad51e5d06b1 100644
--- a/src/components/MigratedUserWelcomeModal.tsx
+++ b/src/components/MigratedUserWelcomeModal.tsx
@@ -19,7 +19,7 @@ import variables from '@styles/variables';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
-import React, {useMemo} from 'react';
+import React from 'react';
import {View} from 'react-native';
import type {FeatureListItem} from './FeatureList';
@@ -39,23 +39,20 @@ function MigratedUserWelcomeModal() {
const illustrations = useMemoizedLazyIllustrations(['ChatBubbles', 'ConciergeBot', 'PlanetWithMobileApp', 'MagnifyingGlassReceipt']);
const isCurrentUserPolicyAdmin = useIsPaidPolicyAdmin();
- const ExpensifyFeatures = useMemo(
- () => [
- {
- icon: illustrations.MagnifyingGlassReceipt,
- translationKey: 'migratedUserWelcomeModal.features.search',
- },
- {
- icon: illustrations.ConciergeBot,
- translationKey: 'migratedUserWelcomeModal.features.concierge',
- },
- {
- icon: illustrations.ChatBubbles,
- translationKey: 'migratedUserWelcomeModal.features.chat',
- },
- ],
- [illustrations.ChatBubbles, illustrations.ConciergeBot, illustrations.MagnifyingGlassReceipt],
- );
+ const ExpensifyFeatures: FeatureListItem[] = [
+ {
+ icon: illustrations.MagnifyingGlassReceipt,
+ translationKey: 'migratedUserWelcomeModal.features.search',
+ },
+ {
+ icon: illustrations.ConciergeBot,
+ translationKey: 'migratedUserWelcomeModal.features.concierge',
+ },
+ {
+ icon: illustrations.ChatBubbles,
+ translationKey: 'migratedUserWelcomeModal.features.chat',
+ },
+ ];
const persistDismissal = () => {
Log.hmmm('[MigratedUserWelcomeModal] dismissing product training');
diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx
index 4a8f82b2e46f..8df5cd8cd1de 100644
--- a/src/components/Modal/BaseModal.tsx
+++ b/src/components/Modal/BaseModal.tsx
@@ -30,7 +30,7 @@ import CONST from '@src/CONST';
import type {ComponentRef} from 'react';
import type {GestureResponderEvent, LayoutChangeEvent} from 'react-native';
-import React, {useCallback, useContext, useEffect, useRef, useState} from 'react';
+import React, {useContext, useEffect, useRef, useState} from 'react';
// Animated required for side panel navigation
// eslint-disable-next-line no-restricted-imports
import {Animated, View} from 'react-native';
@@ -137,27 +137,24 @@ function BaseModal({
* Hides modal
* @param callHideCallback - Should we call the onModalHide callback
*/
- const hideModal = useCallback(
- (callHideCallback = true) => {
- shouldCallHideModalOnUnmount.current = false;
- willAlertModalBecomeVisible(false);
- if (areAllModalsHidden()) {
- if (shouldSetModalVisibility && !Navigation.isTopmostRouteModalScreen()) {
- setModalVisibility(false);
- }
+ const hideModal = (callHideCallback = true) => {
+ shouldCallHideModalOnUnmount.current = false;
+ willAlertModalBecomeVisible(false);
+ if (areAllModalsHidden()) {
+ if (shouldSetModalVisibility && !Navigation.isTopmostRouteModalScreen()) {
+ setModalVisibility(false);
}
- if (callHideCallback) {
- onModalHide();
- }
- onModalDidClose();
- ComposerFocusManager.refocusAfterModalFullyClosed(uniqueModalId, restoreFocusType);
- },
- [shouldSetModalVisibility, onModalHide, restoreFocusType, uniqueModalId],
- );
+ }
+ if (callHideCallback) {
+ onModalHide();
+ }
+ onModalDidClose();
+ ComposerFocusManager.refocusAfterModalFullyClosed(uniqueModalId, restoreFocusType);
+ };
- const handleDismissModal = useCallback(() => {
+ const handleDismissModal = () => {
ComposerFocusManager.setReadyToFocus(uniqueModalId);
- }, [uniqueModalId]);
+ };
useEffect(() => {
let removeOnCloseListener: () => void;
@@ -206,12 +203,12 @@ function BaseModal({
[],
);
- const handleShowModal = useCallback(() => {
+ const handleShowModal = () => {
if (shouldSetModalVisibility) {
setModalVisibility(true, type);
}
onModalShow();
- }, [onModalShow, shouldSetModalVisibility, type]);
+ };
const handleBackdropPress = (e?: GestureResponderEvent | KeyboardEvent) => {
if (e && 'key' in e && e.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey) {
diff --git a/src/components/Modal/ReanimatedModal/Backdrop/index.web.tsx b/src/components/Modal/ReanimatedModal/Backdrop/index.web.tsx
index fff51efe87fc..eac68a69e7dd 100644
--- a/src/components/Modal/ReanimatedModal/Backdrop/index.web.tsx
+++ b/src/components/Modal/ReanimatedModal/Backdrop/index.web.tsx
@@ -12,7 +12,7 @@ import CONST from '@src/CONST';
import type {ViewStyle} from 'react-native';
import type {CSSStyle} from 'react-native-reanimated';
-import React, {useMemo} from 'react';
+import React from 'react';
import {View} from 'react-native';
import Animated, {Keyframe, ReduceMotion} from 'react-native-reanimated';
@@ -28,13 +28,13 @@ function Backdrop({
const styles = useThemeStyles();
const {translate} = useLocalize();
- const Exiting = useMemo(() => {
- if (!backdropOpacity) {
- return;
- }
+ let Exiting;
+ if (!backdropOpacity) {
+ Exiting = undefined;
+ } else {
const FadeOut = new Keyframe(getModalOutAnimation('fadeOut', backdropOpacity));
- return FadeOut.duration(animationOutTiming).reduceMotion(ReduceMotion.Never);
- }, [animationOutTiming, backdropOpacity]);
+ Exiting = FadeOut.duration(animationOutTiming).reduceMotion(ReduceMotion.Never);
+ }
// A CSS animation instead of an entering keyframe: on web, Reanimated hides an entering element until its keyframe starts and can
// delete the keyframe before the next animation frame (for example in a background tab), which leaves the backdrop hidden for good.
diff --git a/src/components/Modal/ReanimatedModal/Container/index.tsx b/src/components/Modal/ReanimatedModal/Container/index.tsx
index 4d0094954ba4..2e58d41f6b20 100644
--- a/src/components/Modal/ReanimatedModal/Container/index.tsx
+++ b/src/components/Modal/ReanimatedModal/Container/index.tsx
@@ -6,7 +6,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
-import React, {useMemo} from 'react';
+import React from 'react';
import {View} from 'react-native';
import Animated, {Keyframe} from 'react-native-reanimated';
import {scheduleOnRN} from 'react-native-worklets';
@@ -29,25 +29,19 @@ function Container({
}: Partial & ContainerProps) {
const styles = useThemeStyles();
- const Entering = useMemo(() => {
- const AnimationIn = new Keyframe(getModalInAnimation(animationIn));
+ const AnimationIn = new Keyframe(getModalInAnimation(animationIn));
+ const Entering = AnimationIn.duration(animationInTiming).withCallback(() => {
+ 'worklet';
- return AnimationIn.duration(animationInTiming).withCallback(() => {
- 'worklet';
+ scheduleOnRN(onOpenCallBack);
+ });
- scheduleOnRN(onOpenCallBack);
- });
- }, [animationIn, animationInTiming, onOpenCallBack]);
+ const AnimationOut = new Keyframe(getModalOutAnimation(animationOut));
+ const Exiting = AnimationOut.duration(animationOutTiming).withCallback(() => {
+ 'worklet';
- const Exiting = useMemo(() => {
- const AnimationOut = new Keyframe(getModalOutAnimation(animationOut));
-
- return AnimationOut.duration(animationOutTiming).withCallback(() => {
- 'worklet';
-
- scheduleOnRN(onCloseCallBack);
- });
- }, [animationOutTiming, onCloseCallBack, animationOut]);
+ scheduleOnRN(onCloseCallBack);
+ });
return (
{
+ const onBackButtonPressHandler = () => {
if (shouldIgnoreBackHandlerDuringTransition && isTransitioning) {
return false;
}
@@ -82,17 +82,14 @@ function ReanimatedModal({
return true;
}
return false;
- }, [isVisibleState, onBackButtonPress, isTransitioning, shouldIgnoreBackHandlerDuringTransition]);
+ };
- const handleEscape = useCallback(
- (e: KeyboardEvent) => {
- if (e.key !== 'Escape' || onBackButtonPressHandler() !== true) {
- return;
- }
- e.stopImmediatePropagation();
- },
- [onBackButtonPressHandler],
- );
+ const handleEscape = (e: KeyboardEvent) => {
+ if (e.key !== 'Escape' || onBackButtonPressHandler() !== true) {
+ return;
+ }
+ e.stopImmediatePropagation();
+ };
useEffect(() => {
if (getPlatform() === CONST.PLATFORM.WEB) {
@@ -148,11 +145,9 @@ function ReanimatedModal({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isVisible, isContainerOpen, isTransitioning]);
- const backdropStyle: ViewStyle = useMemo(() => {
- return {width: windowWidth, height: windowHeight, backgroundColor: backdropColor};
- }, [windowWidth, windowHeight, backdropColor]);
+ const backdropStyle: ViewStyle = {width: windowWidth, height: windowHeight, backgroundColor: backdropColor};
- const onOpenCallBack = useCallback(() => {
+ const onOpenCallBack = () => {
setIsTransitioning(false);
setIsContainerOpen(true);
if (transitionHandleRef.current) {
@@ -160,9 +155,9 @@ function ReanimatedModal({
transitionHandleRef.current = null;
}
onModalShow();
- }, [onModalShow]);
+ };
- const onCloseCallBack = useCallback(() => {
+ const onCloseCallBack = () => {
setIsTransitioning(false);
setIsContainerOpen(false);
if (transitionHandleRef.current) {
@@ -176,11 +171,9 @@ function ReanimatedModal({
if (getPlatform() === CONST.PLATFORM.ANDROID) {
onModalHide();
}
- }, [onModalHide]);
+ };
- const modalStyle = useMemo(() => {
- return {zIndex: StyleSheet.flatten(style)?.zIndex};
- }, [style]);
+ const modalStyle = {zIndex: StyleSheet.flatten(style)?.zIndex};
const containerView = (
) => {
- const parentReportAction = threadParentReportActionID ? parentReportActions?.[threadParentReportActionID] : undefined;
- return isMoneyRequestAction(parentReportAction) ? getOriginalMessage(parentReportAction)?.IOUTransactionID : undefined;
- },
- [threadParentReportActionID],
- );
+ const threadTransactionIDSelector = (parentReportActions: OnyxEntry) => {
+ const parentReportAction = threadParentReportActionID ? parentReportActions?.[threadParentReportActionID] : undefined;
+ return isMoneyRequestAction(parentReportAction) ? getOriginalMessage(parentReportAction)?.IOUTransactionID : undefined;
+ };
const [threadTransactionID] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(moneyRequestReport?.parentReportID)}`, {selector: threadTransactionIDSelector});
const anchorTransactionIDFromRoute = route.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT ? route.params.anchorTransactionID : undefined;
diff --git a/src/components/MoneyReportHeaderKYCDropdown.tsx b/src/components/MoneyReportHeaderKYCDropdown.tsx
index 697cd19a137c..a0b4189dc700 100644
--- a/src/components/MoneyReportHeaderKYCDropdown.tsx
+++ b/src/components/MoneyReportHeaderKYCDropdown.tsx
@@ -14,7 +14,7 @@ import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
import type {ValueOf} from 'type-fest';
-import React, {memo} from 'react';
+import React from 'react';
import type {ButtonWithDropdownMenuRef, DropdownOption} from './ButtonWithDropdownMenu/types';
import type {KYCWallProps} from './KYCWall/types';
@@ -130,4 +130,4 @@ function MoneyReportHeaderKYCDropdown({
);
}
-export default memo(MoneyReportHeaderKYCDropdown);
+export default MoneyReportHeaderKYCDropdown;
diff --git a/src/components/MoneyReportHeaderStatusBar.tsx b/src/components/MoneyReportHeaderStatusBar.tsx
index 652e6b035b9b..e4fe15a1e42d 100644
--- a/src/components/MoneyReportHeaderStatusBar.tsx
+++ b/src/components/MoneyReportHeaderStatusBar.tsx
@@ -14,7 +14,7 @@ import type IconAsset from '@src/types/utils/IconAsset';
import type {ValueOf} from 'type-fest';
-import React, {useMemo} from 'react';
+import React from 'react';
import {View} from 'react-native';
import Icon from './Icon';
@@ -32,25 +32,16 @@ function MoneyReportHeaderStatusBar({nextStep}: MoneyReportHeaderStatusBarProps)
const theme = useTheme();
const {translate, formatPhoneNumber, dateFnsLocale} = useLocalize();
const icons = useMemoizedLazyExpensifyIcons(['Hourglass', 'Checkmark', 'Stopwatch', 'Box']);
- const iconMap: IconMap = useMemo(
- () => ({
- [CONST.NEXT_STEP.ICONS.HOURGLASS]: icons.Hourglass,
- [CONST.NEXT_STEP.ICONS.CHECKMARK]: icons.Checkmark,
- [CONST.NEXT_STEP.ICONS.STOPWATCH]: icons.Stopwatch,
- [CONST.NEXT_STEP.ICONS.BOX]: icons.Box,
- }),
- [icons],
- );
+ const iconMap: IconMap = {
+ [CONST.NEXT_STEP.ICONS.HOURGLASS]: icons.Hourglass,
+ [CONST.NEXT_STEP.ICONS.CHECKMARK]: icons.Checkmark,
+ [CONST.NEXT_STEP.ICONS.STOPWATCH]: icons.Stopwatch,
+ [CONST.NEXT_STEP.ICONS.BOX]: icons.Box,
+ };
const currentUserPersonalDetails = useCurrentUserPersonalDetails();
const currentUserAccountID = currentUserPersonalDetails.accountID;
- const messageContent = useMemo(() => {
- if (!nextStep) {
- return '';
- }
-
- return buildNextStepMessage(nextStep, translate, dateFnsLocale, currentUserAccountID, formatPhoneNumber);
- }, [nextStep, translate, dateFnsLocale, currentUserAccountID, formatPhoneNumber]);
+ const messageContent = !nextStep ? '' : buildNextStepMessage(nextStep, translate, dateFnsLocale, currentUserAccountID, formatPhoneNumber);
return (
diff --git a/src/components/MoneyRequestConfirmationList/hooks/useConfirmationListData.ts b/src/components/MoneyRequestConfirmationList/hooks/useConfirmationListData.ts
index 14d4aaf327cb..a4b1e14e53e6 100644
--- a/src/components/MoneyRequestConfirmationList/hooks/useConfirmationListData.ts
+++ b/src/components/MoneyRequestConfirmationList/hooks/useConfirmationListData.ts
@@ -18,7 +18,7 @@ import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import {useIsFocused} from '@react-navigation/native';
-import {useCallback, useEffect, useRef, useState} from 'react';
+import {useEffect, useRef, useState} from 'react';
import type {ConfirmationDistanceState, ConfirmationListData, UseConfirmationListDataParams} from './types';
@@ -100,9 +100,9 @@ function useConfirmationListDataWithPolicy({
// In the new manual expense flow the inline fields live in the list footer, so they can be hidden behind the keyboard.
// We let those fields ask the list to scroll them into view when focused.
- const scrollFocusedInputIntoView = useCallback((input: MeasurableInput) => {
+ const scrollFocusedInputIntoView = (input: MeasurableInput) => {
listRef.current?.scrollInputIntoView(input);
- }, []);
+ };
const iouAmount = hasValidModifiedAmount(transaction) ? Number(transaction?.modifiedAmount) : (transaction?.amount ?? 0);
const iouCurrencyCode = getCurrency(transaction);
diff --git a/src/components/MoneyRequestConfirmationList/hooks/useParticipantSection.ts b/src/components/MoneyRequestConfirmationList/hooks/useParticipantSection.ts
index 181e38f40d4c..e595cbbc08d5 100644
--- a/src/components/MoneyRequestConfirmationList/hooks/useParticipantSection.ts
+++ b/src/components/MoneyRequestConfirmationList/hooks/useParticipantSection.ts
@@ -4,7 +4,7 @@ import usePreferredPolicy from '@hooks/usePreferredPolicy';
import CONST from '@src/CONST';
-import {useCallback, useEffect, useMemo} from 'react';
+import {useEffect} from 'react';
import type {UseParticipantSectionParams} from './types';
@@ -43,12 +43,12 @@ function useParticipantSection({
const selectedParticipants = selectedParticipantsProp.filter((participant) => participant.selected);
const payeePersonalDetails = payeePersonalDetailsProp ?? currentUserPersonalDetails;
- const participantRowErrors = useMemo(() => {
- if (formError !== 'iou.error.noParticipantSelected' && formError !== 'violations.missingAttendees') {
- return undefined;
- }
- return {participants: translate(formError)};
- }, [formError, translate]);
+ const participantRowErrors =
+ formError !== 'iou.error.noParticipantSelected' && formError !== 'violations.missingAttendees'
+ ? undefined
+ : {
+ participants: translate(formError),
+ };
useEffect(() => {
if (selectedParticipants.length === 0) {
@@ -57,9 +57,9 @@ function useParticipantSection({
clearFormErrors(['iou.error.noParticipantSelected']);
}, [selectedParticipants.length, clearFormErrors]);
- const dismissParticipantRowError = useCallback(() => {
+ const dismissParticipantRowError = () => {
clearFormErrors(['iou.error.noParticipantSelected', 'violations.missingAttendees']);
- }, [clearFormErrors]);
+ };
const {splitParticipants, getSplitSectionHeader} = useSplitParticipants({
isTypeSplit,
diff --git a/src/components/MoneyRequestHeaderSecondaryActions.tsx b/src/components/MoneyRequestHeaderSecondaryActions.tsx
index 9343c5caf75f..087a39dee2d0 100644
--- a/src/components/MoneyRequestHeaderSecondaryActions.tsx
+++ b/src/components/MoneyRequestHeaderSecondaryActions.tsx
@@ -94,7 +94,7 @@ import {hasSeenTourSelector} from '@selectors/Onboarding';
import {loginSelector} from '@selectors/PersonalDetails';
import {billingRestrictionPolicySelector, createFilteredPoliciesInfoSelector, createHasWorkspaceToSubmitToSelector} from '@selectors/Policy';
import {validTransactionDraftsSelector} from '@selectors/TransactionDraft';
-import React, {useMemo, useRef} from 'react';
+import React, {useRef} from 'react';
import type {ButtonWithDropdownMenuRef, DropdownOption} from './ButtonWithDropdownMenu/types';
@@ -195,10 +195,10 @@ function MoneyRequestHeaderSecondaryActions({reportID, onBackButtonPress}: Money
const activePolicy = useActivePolicy();
const lastWorkspaceNumber = useLastWorkspaceNumber();
const {isRestrictedToPreferredPolicy, preferredPolicyID} = usePreferredPolicy();
- const filteredPoliciesInfoSelector = useMemo(() => createFilteredPoliciesInfoSelector(currentUserEmail), [currentUserEmail]);
+ const filteredPoliciesInfoSelector = createFilteredPoliciesInfoSelector(currentUserEmail);
const [filteredPoliciesInfo] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: filteredPoliciesInfoSelector});
const [preferredPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${getNonEmptyStringOnyxID(preferredPolicyID)}`, {selector: billingRestrictionPolicySelector});
- const draftTransactionIDs = useMemo(() => Object.keys(transactionDrafts ?? {}), [transactionDrafts]);
+ const draftTransactionIDs = Object.keys(transactionDrafts ?? {});
// Custom hooks
const defaultExpensePolicy = useDefaultExpensePolicy();
@@ -212,7 +212,7 @@ function MoneyRequestHeaderSecondaryActions({reportID, onBackButtonPress}: Money
const {isBetaEnabled, isBetaEnabledOrUnknown} = usePermissions();
const isVendorMatchingBetaEnabled = isBetaEnabledOrUnknown(CONST.BETAS.VENDOR_MATCHING);
const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT);
- const hasWorkspaceToSubmitToSelector = useMemo(() => createHasWorkspaceToSubmitToSelector(currentUserLogin), [currentUserLogin]);
+ const hasWorkspaceToSubmitToSelector = createHasWorkspaceToSubmitToSelector(currentUserLogin);
const [hasWorkspaceToSubmitTo = false] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: hasWorkspaceToSubmitToSelector});
const {showConfirmModal} = useConfirmModal();
const {isDelegateAccessRestricted} = useDelegateNoAccessState();
diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx
index d328b2b08a9e..04ef24ced200 100644
--- a/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx
+++ b/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx
@@ -8,7 +8,7 @@ import {isSortableColumnName} from '@libs/ReportUtils';
import CONST from '@src/CONST';
-import React, {useCallback, useMemo} from 'react';
+import React from 'react';
import {View} from 'react-native';
type SearchTableHeaderProps = {
@@ -37,29 +37,24 @@ function MoneyRequestReportTableHeader({
}: SearchTableHeaderProps) {
const styles = useThemeStyles();
- const columnConfig = useMemo(
- () => [
- ...getExpenseHeaders().map((header) => ({
- ...header,
- isColumnSortable: isSortableColumnName(header.columnName),
- })),
- {
- columnName: CONST.SEARCH.TABLE_COLUMNS.COMMENTS,
- translationKey: undefined,
- isColumnSortable: false,
- },
- ],
- [],
- );
-
- const orderedColumnConfig = useMemo(() => {
- if (columns.length === 0) {
- return columnConfig;
- }
+ const columnConfig = [
+ ...getExpenseHeaders().map((header) => ({
+ ...header,
+ isColumnSortable: isSortableColumnName(header.columnName),
+ })),
+ {
+ columnName: CONST.SEARCH.TABLE_COLUMNS.COMMENTS,
+ translationKey: undefined,
+ isColumnSortable: false,
+ },
+ ];
+ let orderedColumnConfig;
+ if (columns.length === 0) {
+ orderedColumnConfig = columnConfig;
+ } else {
const configMap = new Map(columnConfig.map((config) => [config.columnName, config]));
const ordered: typeof columnConfig = [];
-
for (const columnName of columns) {
const config = configMap.get(columnName);
if (config) {
@@ -67,20 +62,15 @@ function MoneyRequestReportTableHeader({
configMap.delete(columnName);
}
}
-
for (const config of configMap.values()) {
ordered.push(config);
}
+ orderedColumnConfig = ordered;
+ }
- return ordered;
- }, [columns, columnConfig]);
-
- const shouldShowColumn = useCallback(
- (columnName: SearchColumnType) => {
- return columns.includes(columnName);
- },
- [columns],
- );
+ const shouldShowColumn = (columnName: SearchColumnType) => {
+ return columns.includes(columnName);
+ };
return (
diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionsNavigation.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionsNavigation.tsx
index 048bc58beab0..dd00ec83bb15 100644
--- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionsNavigation.tsx
+++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionsNavigation.tsx
@@ -33,7 +33,7 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import {findFocusedRoute, useIsFocused} from '@react-navigation/native';
import {guidedSetupAndTourStatusSelector} from '@selectors/Onboarding';
import {fullPolicySnapshotLastModifiedByPolicyIDSelector} from '@selectors/Policy';
-import React, {startTransition, useCallback, useEffect, useMemo, useRef} from 'react';
+import React, {startTransition, useEffect, useRef} from 'react';
import {View} from 'react-native';
const CAROUSEL_PRESERVING_SCREENS = [
@@ -105,25 +105,15 @@ function MoneyRequestReportTransactionsNavigation({currentTransactionID, isFromR
// A press made before the sibling's parent action has loaded is parked here and replayed once it arrives.
const pendingSiblingRef = useRef<{transactionID: string; originRoute: string} | null>(null);
- const {prevTransactionID, nextTransactionID} = useMemo(() => {
- if (transactionIDsList.length < 2 || currentTransactionIndex === -1) {
- return {prevTransactionID: undefined, nextTransactionID: undefined};
- }
+ const hasAdjacentTransactions = transactionIDsList.length >= 2 && currentTransactionIndex !== -1;
+ const prevTransactionID = hasAdjacentTransactions && currentTransactionIndex > 0 ? transactionIDsList.at(currentTransactionIndex - 1) : undefined;
+ const nextTransactionID = hasAdjacentTransactions ? transactionIDsList.at(currentTransactionIndex + 1) : undefined;
- return {
- prevTransactionID: currentTransactionIndex > 0 ? transactionIDsList.at(currentTransactionIndex - 1) : undefined,
- nextTransactionID: transactionIDsList.at(currentTransactionIndex + 1),
- };
- }, [currentTransactionIndex, transactionIDsList]);
-
- const prevNextTransactionsSelector = useCallback(
- (allTransactions: OnyxCollection) =>
- [currentTransactionID, prevTransactionID, nextTransactionID].map((transactionID) => {
- const key = `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}` as const;
- return allTransactions?.[key] ?? snapshot?.data?.[key];
- }),
- [currentTransactionID, nextTransactionID, prevTransactionID, snapshot],
- );
+ const prevNextTransactionsSelector = (allTransactions: OnyxCollection) =>
+ [currentTransactionID, prevTransactionID, nextTransactionID].map((transactionID) => {
+ const key = `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}` as const;
+ return allTransactions?.[key] ?? snapshot?.data?.[key];
+ });
const [[currentTransaction, prevTransaction, nextTransaction] = getEmptyArray()] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {
selector: prevNextTransactionsSelector,
@@ -134,32 +124,29 @@ function MoneyRequestReportTransactionsNavigation({currentTransactionID, isFromR
// Only the prev/next parent actions are ever read, so resolve them inside the selector instead of returning
// a Map of every money request action on the three parent reports (fast-equals compares Maps in O(n^2)).
- const parentReportActionsSelector = useCallback(
- (allReportActions: OnyxCollection) => {
- // An unreported (self-DM) sibling's IOU action lives in the self-DM, since report "0" holds no actions
- // of its own. Looking it up there is the only way such a sibling resolves from live data at all.
- const isPrevUnreported = prevTransactionReportID === CONST.REPORT.UNREPORTED_REPORT_ID;
- const isNextUnreported = nextTransactionReportID === CONST.REPORT.UNREPORTED_REPORT_ID;
- const selfDMReportID = isPrevUnreported || isNextUnreported ? findSelfDMReportID() : undefined;
- const prevActionsReportID = isPrevUnreported ? selfDMReportID : prevTransactionReportID;
- const nextActionsReportID = isNextUnreported ? selfDMReportID : nextTransactionReportID;
- // Whether the sibling's parent report has any actions at all, which is what separates "not fetched yet"
- // from "fetched, but this expense has no creation action". Only the former is worth waiting for.
- const hasPrevParentReportActions = !!Object.keys(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${prevActionsReportID}`] ?? {}).length;
- const hasNextParentReportActions = !!Object.keys(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${nextActionsReportID}`] ?? {}).length;
- const parentActions: PrevNextParentReportActions = {prevParentReportAction: undefined, nextParentReportAction: undefined};
- if (!prevTransactionID && !nextTransactionID) {
- return {...parentActions, hasPrevParentReportActions, hasNextParentReportActions};
- }
- const parentReportIDs = new Set([currentTransaction?.reportID, prevActionsReportID, nextActionsReportID]);
- for (const parentReportID of parentReportIDs) {
- const key = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}` as const;
- collectParentReportActions(allReportActions?.[key], prevTransactionID, nextTransactionID, parentActions);
- }
+ const parentReportActionsSelector = (allReportActions: OnyxCollection) => {
+ // An unreported (self-DM) sibling's IOU action lives in the self-DM, since report "0" holds no actions
+ // of its own. Looking it up there is the only way such a sibling resolves from live data at all.
+ const isPrevUnreported = prevTransactionReportID === CONST.REPORT.UNREPORTED_REPORT_ID;
+ const isNextUnreported = nextTransactionReportID === CONST.REPORT.UNREPORTED_REPORT_ID;
+ const selfDMReportID = isPrevUnreported || isNextUnreported ? findSelfDMReportID() : undefined;
+ const prevActionsReportID = isPrevUnreported ? selfDMReportID : prevTransactionReportID;
+ const nextActionsReportID = isNextUnreported ? selfDMReportID : nextTransactionReportID;
+ // Whether the sibling's parent report has any actions at all, which is what separates "not fetched yet"
+ // from "fetched, but this expense has no creation action". Only the former is worth waiting for.
+ const hasPrevParentReportActions = !!Object.keys(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${prevActionsReportID}`] ?? {}).length;
+ const hasNextParentReportActions = !!Object.keys(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${nextActionsReportID}`] ?? {}).length;
+ const parentActions: PrevNextParentReportActions = {prevParentReportAction: undefined, nextParentReportAction: undefined};
+ if (!prevTransactionID && !nextTransactionID) {
return {...parentActions, hasPrevParentReportActions, hasNextParentReportActions};
- },
- [currentTransaction?.reportID, nextTransactionReportID, nextTransactionID, prevTransactionReportID, prevTransactionID],
- );
+ }
+ const parentReportIDs = new Set([currentTransaction?.reportID, prevActionsReportID, nextActionsReportID]);
+ for (const parentReportID of parentReportIDs) {
+ const key = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}` as const;
+ collectParentReportActions(allReportActions?.[key], prevTransactionID, nextTransactionID, parentActions);
+ }
+ return {...parentActions, hasPrevParentReportActions, hasNextParentReportActions};
+ };
const [reportedParentReportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS, {
selector: parentReportActionsSelector,
@@ -169,17 +156,18 @@ function MoneyRequestReportTransactionsNavigation({currentTransactionID, isFromR
// one), so it only finds what has already been fetched into Onyx. Scanning the snapshot's report actions is
// what resolves a sibling whose parent actions were never loaded in this session.
const snapshotData = snapshot?.data;
- const snapshotParentReportActions = useMemo(() => {
- const parentActions: PrevNextParentReportActions = {prevParentReportAction: undefined, nextParentReportAction: undefined};
- if (snapshotData && (prevTransactionID ?? nextTransactionID)) {
- for (const [key, reportActionsForReport] of Object.entries(snapshotData)) {
- if (key.startsWith(ONYXKEYS.COLLECTION.REPORT_ACTIONS)) {
- collectParentReportActions(reportActionsForReport as OnyxTypes.ReportActions, prevTransactionID, nextTransactionID, parentActions);
- }
+ const parentActions: PrevNextParentReportActions = {
+ prevParentReportAction: undefined,
+ nextParentReportAction: undefined,
+ };
+ if (snapshotData && (prevTransactionID ?? nextTransactionID)) {
+ for (const [key, reportActionsForReport] of Object.entries(snapshotData)) {
+ if (key.startsWith(ONYXKEYS.COLLECTION.REPORT_ACTIONS)) {
+ collectParentReportActions(reportActionsForReport as OnyxTypes.ReportActions, prevTransactionID, nextTransactionID, parentActions);
}
}
- return parentActions;
- }, [nextTransactionID, prevTransactionID, snapshotData]);
+ }
+ const snapshotParentReportActions = parentActions;
// Live report actions win over the snapshot: the snapshot only fills in siblings the live pass couldn't resolve
// (i.e. unreported ones). A search snapshot is a point-in-time copy, so for a reported transaction it can hold an
diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportUnifiedList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportUnifiedList.tsx
index a1c1a466fc5c..cfb299fa9dbd 100644
--- a/src/components/MoneyRequestReportView/MoneyRequestReportUnifiedList.tsx
+++ b/src/components/MoneyRequestReportView/MoneyRequestReportUnifiedList.tsx
@@ -13,7 +13,7 @@ import type {ViewableItemsChanged, ViewToken} from '@src/types/utils/ReactNative
import type {FlashListProps, ListRenderItemInfo} from '@shopify/flash-list';
import type {LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent, StyleProp, ViewStyle} from 'react-native';
-import React, {memo, useEffect, useRef, useState} from 'react';
+import React, {useEffect, useRef, useState} from 'react';
import {View} from 'react-native';
import type {ExternalScrollFlashListTableHandle} from './ExternalScrollFlashListTable';
@@ -413,4 +413,4 @@ function MoneyRequestReportUnifiedList({
);
}
-export default memo(MoneyRequestReportUnifiedList);
+export default MoneyRequestReportUnifiedList;
diff --git a/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx b/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx
index f86f158685b8..f7862af150c7 100644
--- a/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx
+++ b/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx
@@ -34,7 +34,7 @@ import type {PendingAction} from '@src/types/onyx/OnyxCommon';
import type {StyleProp, ViewStyle} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
-import React, {useMemo} from 'react';
+import React from 'react';
import {View} from 'react-native';
import ReportFieldInlineInput from './ReportFieldInlineInput';
@@ -140,46 +140,39 @@ function MoneyRequestViewReportFields({report, policy, pendingAction, style, sho
deleteReportField(report.reportID, report.fieldList?.[reportField.fieldKey] ?? reportField);
};
- const sortedPolicyReportFields = useMemo((): EnrichedPolicyReportField[] => {
- const {fieldValues, fieldsByName} = getReportFieldMaps(report, policy?.fieldList ?? {}, reportNameValuePairs);
- const fields = Object.values(fieldsByName);
-
- return fields
- .filter((field) => isReportFieldTargetMatchingReport(report, field))
- .filter((reportField) => !shouldHideSingleReportField(reportField))
- .sort(({orderWeight: firstOrderWeight}, {orderWeight: secondOrderWeight}) => firstOrderWeight - secondOrderWeight)
- .map((field): EnrichedPolicyReportField => {
- const isFieldDisabled = isReportFieldDisabledForUser(report, field, policy, currentUserAccountID, rules);
- const isDeletedFormulaField = field.type === CONST.REPORT_FIELD_TYPES.FORMULA && field.deletable;
- const fieldKey = getReportFieldKey(field.fieldID);
- const isFieldOnPolicy = !!(policy?.fieldList?.[fieldKey] ?? policy?.fieldList?.[field.fieldID]);
- const isEditable = !isFieldDisabled || isDeletedFormulaField;
-
- // An input the user can type into is seeded with the raw value, the same as the report field editor page.
- // Seeding it with the computed value would save the resolved text over a `{field:X}` default the first
- // time it was edited, so the field would stop following the field it refers to.
- const shouldUseRawValue = isEditable && field.type !== CONST.REPORT_FIELD_TYPES.FORMULA;
- const fieldValue = shouldUseRawValue
- ? (field.value ?? field.defaultValue ?? '')
- : resolveReportFieldValue(field, report, policy, fieldValues, fieldsByName, getCurrencyDecimals);
-
- const violation = isFieldDisabled ? undefined : getFieldViolation(field);
- const violationTranslation = getFieldViolationTranslation(field, violation);
-
- return {
- ...field,
- fieldValue,
- isFieldDisabled: !isEditable,
- // Only fields that are no longer on the workspace can be deleted. Their `deletable` flag is not
- // checked: once the workspace drops a field, whether it was required no longer applies, and the
- // report's copy of a date field can still be flagged as not deletable.
- isFieldDeletable: field.fieldID !== CONST.REPORT_FIELD_TITLE_FIELD_ID && !isFieldOnPolicy && isEditable,
- fieldKey,
- violation,
- violationTranslation,
- };
- });
- }, [policy, report, currentUserAccountID, reportNameValuePairs, getCurrencyDecimals, rules]);
+ const {fieldValues, fieldsByName} = getReportFieldMaps(report, policy?.fieldList ?? {}, reportNameValuePairs);
+ const fields = Object.values(fieldsByName);
+ const sortedPolicyReportFields: EnrichedPolicyReportField[] = fields
+ .filter((field) => isReportFieldTargetMatchingReport(report, field))
+ .filter((reportField) => !shouldHideSingleReportField(reportField))
+ .sort(({orderWeight: firstOrderWeight}, {orderWeight: secondOrderWeight}) => firstOrderWeight - secondOrderWeight)
+ .map((field): EnrichedPolicyReportField => {
+ const isFieldDisabled = isReportFieldDisabledForUser(report, field, policy, currentUserAccountID, rules);
+ const isDeletedFormulaField = field.type === CONST.REPORT_FIELD_TYPES.FORMULA && field.deletable;
+ const fieldKey = getReportFieldKey(field.fieldID);
+ const isFieldOnPolicy = !!(policy?.fieldList?.[fieldKey] ?? policy?.fieldList?.[field.fieldID]);
+ const isEditable = !isFieldDisabled || isDeletedFormulaField;
+
+ // An input the user can type into is seeded with the raw value, the same as the report field editor page.
+ // Seeding it with the computed value would save the resolved text over a `{field:X}` default the first
+ // time it was edited, so the field would stop following the field it refers to.
+ const shouldUseRawValue = isEditable && field.type !== CONST.REPORT_FIELD_TYPES.FORMULA;
+ const fieldValue = shouldUseRawValue ? (field.value ?? field.defaultValue ?? '') : resolveReportFieldValue(field, report, policy, fieldValues, fieldsByName, getCurrencyDecimals);
+ const violation = isFieldDisabled ? undefined : getFieldViolation(field);
+ const violationTranslation = getFieldViolationTranslation(field, violation);
+ return {
+ ...field,
+ fieldValue,
+ isFieldDisabled: !isEditable,
+ // Only fields that are no longer on the workspace can be deleted. Their `deletable` flag is not
+ // checked: once the workspace drops a field, whether it was required no longer applies, and the
+ // report's copy of a date field can still be flagged as not deletable.
+ isFieldDeletable: field.fieldID !== CONST.REPORT_FIELD_TITLE_FIELD_ID && !isFieldOnPolicy && isEditable,
+ fieldKey,
+ violation,
+ violationTranslation,
+ };
+ });
// `sortedPolicyReportFields` already excludes fields hidden by `shouldHideSingleReportField`, including the title field.
// If no displayable custom fields remain, the early return below hides the section.
diff --git a/src/components/MultiGestureCanvas/index.tsx b/src/components/MultiGestureCanvas/index.tsx
index fa11a99b3da5..f2fa2b9da0d4 100644
--- a/src/components/MultiGestureCanvas/index.tsx
+++ b/src/components/MultiGestureCanvas/index.tsx
@@ -7,7 +7,7 @@ import type {Dimensions} from '@src/types/utils/Layout';
import type {NativeGesture, PanGesture} from 'react-native-gesture-handler';
import type {SharedValue} from 'react-native-reanimated';
-import React, {useCallback, useEffect, useMemo, useRef} from 'react';
+import React, {useEffect, useRef} from 'react';
import {View} from 'react-native';
import {GestureDetector, useCompetingGestures, useSimultaneousGestures} from 'react-native-gesture-handler';
import Animated, {cancelAnimation, useAnimatedReaction, useAnimatedStyle, useDerivedValue, useSharedValue, withSpring} from 'react-native-reanimated';
@@ -102,18 +102,15 @@ function MultiGestureCanvas({
const isTransformGestureActiveFallback = useSharedValue(false);
const isTransformGestureActive = isTransformGestureActiveProp ?? isTransformGestureActiveFallback;
- const zoomRange = useMemo(
- () => ({
- min: zoomRangeProp?.min ?? DEFAULT_ZOOM_RANGE.min,
- max: zoomRangeProp?.max ?? DEFAULT_ZOOM_RANGE.max,
- }),
- [zoomRangeProp?.max, zoomRangeProp?.min],
- );
+ const zoomRange = {
+ min: zoomRangeProp?.min ?? DEFAULT_ZOOM_RANGE.min,
+ max: zoomRangeProp?.max ?? DEFAULT_ZOOM_RANGE.max,
+ };
// Based on the (original) content size and the canvas size, we calculate the horizontal and vertical scale factors
// to fit the content inside the canvas
// We later use the lower of the two scale factors to fit the content inside the canvas
- const {minScale: minContentScale, maxScale: maxContentScale} = useMemo(() => MultiGestureCanvasUtils.getCanvasFitScale({canvasSize, contentSize}), [canvasSize, contentSize]);
+ const {minScale: minContentScale, maxScale: maxContentScale} = MultiGestureCanvasUtils.getCanvasFitScale({canvasSize, contentSize});
const zoomScale = useSharedValue(1);
@@ -148,50 +145,47 @@ function MultiGestureCanvas({
/**
* Stops any currently running decay animation from panning
*/
- const stopAnimation = useCallback(() => {
+ const stopAnimation = () => {
'worklet';
cancelAnimation(offsetX);
cancelAnimation(offsetY);
- }, [offsetX, offsetY]);
+ };
/**
* Resets the canvas to the initial state and animates back smoothly
*/
- const reset = useCallback(
- (animated: boolean, callback?: () => void) => {
- 'worklet';
+ const reset = (animated: boolean, callback?: () => void) => {
+ 'worklet';
- stopAnimation();
+ stopAnimation();
- offsetX.set(0);
- offsetY.set(0);
- pinchScale.set(1);
+ offsetX.set(0);
+ offsetY.set(0);
+ pinchScale.set(1);
- if (animated) {
- panTranslateX.set(withSpring(0, SPRING_CONFIG));
- panTranslateY.set(withSpring(0, SPRING_CONFIG));
- pinchTranslateX.set(withSpring(0, SPRING_CONFIG));
- pinchTranslateY.set(withSpring(0, SPRING_CONFIG));
- zoomScale.set(withSpring(1, SPRING_CONFIG, callback));
+ if (animated) {
+ panTranslateX.set(withSpring(0, SPRING_CONFIG));
+ panTranslateY.set(withSpring(0, SPRING_CONFIG));
+ pinchTranslateX.set(withSpring(0, SPRING_CONFIG));
+ pinchTranslateY.set(withSpring(0, SPRING_CONFIG));
+ zoomScale.set(withSpring(1, SPRING_CONFIG, callback));
- return;
- }
+ return;
+ }
- panTranslateX.set(0);
- panTranslateY.set(0);
- pinchTranslateX.set(0);
- pinchTranslateY.set(0);
- zoomScale.set(1);
+ panTranslateX.set(0);
+ panTranslateY.set(0);
+ pinchTranslateX.set(0);
+ pinchTranslateY.set(0);
+ zoomScale.set(1);
- if (callback === undefined) {
- return;
- }
+ if (callback === undefined) {
+ return;
+ }
- callback();
- },
- [offsetX, offsetY, panTranslateX, panTranslateY, pinchScale, pinchTranslateX, pinchTranslateY, stopAnimation, zoomScale],
- );
+ callback();
+ };
// Pinch must be created before pan: on web, handlers end in creation order, and pan has to end last
// to re-center the content using the offset that already includes the pinch translation.
@@ -287,7 +281,7 @@ function MultiGestureCanvas({
};
});
- const containerStyles = useMemo(() => [styles.flex1, StyleUtils.getMultiGestureCanvasContainerStyle(canvasSize.width)], [StyleUtils, canvasSize.width, styles.flex1]);
+ const containerStyles = [styles.flex1, StyleUtils.getMultiGestureCanvasContainerStyle(canvasSize.width)];
return (
{
+ const getBounds = () => {
'worklet';
let horizontalBoundary = 0;
@@ -110,12 +109,12 @@ const useCanvasPanGesture = ({
isInHorizontalBoundary,
isInVerticalBoundary,
};
- }, [canvasSize.width, canvasSize.height, zoomedContentWidth, zoomedContentHeight, offsetX, offsetY]);
+ };
// We want to smoothly decay/end the gesture by phasing out the pan animation
// In case the content is outside of the boundaries of the canvas,
// we need to move the content back into the boundaries
- const finishPanGesture = useCallback(() => {
+ const finishPanGesture = () => {
'worklet';
// If the content is centered within the canvas, we don't need to run any animations
@@ -183,7 +182,7 @@ const useCanvasPanGesture = ({
// Reset velocity variables after we finished the pan gesture
panVelocityX.set(0);
panVelocityY.set(0);
- }, [getBounds, isSwipingDownToClose, offsetX, offsetY, onSwipeDown, panTranslateX, panTranslateY, panVelocityX, panVelocityY, shouldDisableSwipeDownToClose, zoomScale]);
+ };
const panGesture = usePanGesture({
manualActivation: true,
diff --git a/src/components/MultiGestureCanvas/useCanvasPinchGesture.ts b/src/components/MultiGestureCanvas/useCanvasPinchGesture.ts
index 68de69fc6ead..3f2c652baa81 100644
--- a/src/components/MultiGestureCanvas/useCanvasPinchGesture.ts
+++ b/src/components/MultiGestureCanvas/useCanvasPinchGesture.ts
@@ -1,6 +1,6 @@
import type {PinchGesture} from 'react-native-gesture-handler';
-import {useCallback, useEffect, useState} from 'react';
+import {useEffect, useState} from 'react';
import {GestureStateManager, usePinchGesture} from 'react-native-gesture-handler';
import {useAnimatedReaction, useSharedValue, withSpring} from 'react-native-reanimated';
import {scheduleOnRN} from 'react-native-worklets';
@@ -91,17 +91,14 @@ const useCanvasPinchGesture = ({
* Calculates the adjusted focal point of the pinch gesture,
* based on the canvas size and the current offset
*/
- const getAdjustedFocal = useCallback(
- (focalX: number, focalY: number) => {
- 'worklet';
-
- return {
- x: focalX - (canvasSize.width / 2 + offsetX.get()),
- y: focalY - (canvasSize.height / 2 + offsetY.get()),
- };
- },
- [canvasSize.width, canvasSize.height, offsetX, offsetY],
- );
+ const getAdjustedFocal = (focalX: number, focalY: number) => {
+ 'worklet';
+
+ return {
+ x: focalX - (canvasSize.width / 2 + offsetX.get()),
+ y: focalY - (canvasSize.height / 2 + offsetY.get()),
+ };
+ };
// The pinch gesture is disabled when we release one of the fingers
// On the next render, we need to re-enable the pinch gesture
diff --git a/src/components/MultiGestureCanvas/useCanvasTapGestures.ts b/src/components/MultiGestureCanvas/useCanvasTapGestures.ts
index 89bfc18578a7..1aa17fa53096 100644
--- a/src/components/MultiGestureCanvas/useCanvasTapGestures.ts
+++ b/src/components/MultiGestureCanvas/useCanvasTapGestures.ts
@@ -1,6 +1,5 @@
import type {PanGesture, TapGesture} from 'react-native-gesture-handler';
-import {useCallback, useMemo} from 'react';
import {GestureStateManager, useTapGesture} from 'react-native-gesture-handler';
import {withSpring} from 'react-native-reanimated';
import {scheduleOnRN} from 'react-native-worklets';
@@ -51,90 +50,87 @@ const useCanvasTapGestures = ({
panGesture,
}: UseCanvasTapGesturesProps): {singleTapGesture: TapGesture; doubleTapGesture: TapGesture} => {
// The content size after scaling it with minimum scale to fit the content into the canvas
- const scaledContentWidth = useMemo(() => contentSize.width * minContentScale, [contentSize.width, minContentScale]);
- const scaledContentHeight = useMemo(() => contentSize.height * minContentScale, [contentSize.height, minContentScale]);
+ const scaledContentWidth = contentSize.width * minContentScale;
+ const scaledContentHeight = contentSize.height * minContentScale;
// On double tap the content should be zoomed to fill, but at least zoomed by DOUBLE_TAP_SCALE — never past the allowed zoom range
- const doubleTapScale = useMemo(() => Math.min(zoomRange.max, Math.max(DOUBLE_TAP_SCALE, maxContentScale / minContentScale)), [maxContentScale, minContentScale, zoomRange.max]);
-
- const zoomToCoordinates = useCallback(
- (focalX: number, focalY: number, callback: () => void) => {
- 'worklet';
-
- stopAnimation();
- isTransformGestureActive.set(true);
-
- // By how much the canvas is bigger than the content horizontally and vertically per side
- const horizontalCanvasOffset = Math.max(0, (canvasSize.width - scaledContentWidth) / 2);
- const verticalCanvasOffset = Math.max(0, (canvasSize.height - scaledContentHeight) / 2);
-
- // We need to adjust the focal point to take into account the canvas offset
- // The focal point cannot be outside of the content's bounds
- const adjustedFocalPoint = {
- x: MultiGestureCanvasUtils.clamp(focalX - horizontalCanvasOffset, 0, scaledContentWidth),
- y: MultiGestureCanvasUtils.clamp(focalY - verticalCanvasOffset, 0, scaledContentHeight),
- };
-
- // The center of the canvas
- const canvasCenter = {
- x: canvasSize.width / 2,
- y: canvasSize.height / 2,
- };
-
- // The center of the content before zooming
- const originalContentCenter = {
- x: scaledContentWidth / 2,
- y: scaledContentHeight / 2,
- };
-
- // The size of the content after zooming
- const zoomedContentSize = {
- width: scaledContentWidth * doubleTapScale,
- height: scaledContentHeight * doubleTapScale,
- };
-
- // The center of the zoomed content
- const zoomedContentCenter = {
- x: zoomedContentSize.width / 2,
- y: zoomedContentSize.height / 2,
- };
-
- // By how much the zoomed content is bigger/smaller than the canvas.
- const zoomedContentOffset = {
- x: zoomedContentCenter.x - canvasCenter.x,
- y: zoomedContentCenter.y - canvasCenter.y,
- };
-
- // How much the content needs to be shifted based on the focal point
- const shiftingFactor = {
- x: adjustedFocalPoint.x / originalContentCenter.x - 1,
- y: adjustedFocalPoint.y / originalContentCenter.y - 1,
- };
-
- // The offset after applying the focal point adjusted shift.
- // We need to invert the shift, because the content is moving in the opposite direction (* -1)
- const offsetAfterZooming = {
- x: zoomedContentOffset.x * (shiftingFactor.x * -1),
- y: zoomedContentOffset.y * (shiftingFactor.y * -1),
- };
-
- // If the zoomed content is less tall than the canvas, we need to reset the vertical offset
- if (zoomedContentSize.height < canvasSize.height) {
- offsetAfterZooming.y = 0;
- }
-
- offsetX.set(withSpring(offsetAfterZooming.x, SPRING_CONFIG));
- offsetY.set(withSpring(offsetAfterZooming.y, SPRING_CONFIG));
- zoomScale.set(
- withSpring(doubleTapScale, SPRING_CONFIG, () => {
- callback();
- isTransformGestureActive.set(false);
- }),
- );
- pinchScale.set(doubleTapScale);
- },
- [stopAnimation, isTransformGestureActive, canvasSize.width, canvasSize.height, scaledContentWidth, scaledContentHeight, doubleTapScale, offsetX, offsetY, zoomScale, pinchScale],
- );
+ const doubleTapScale = Math.min(zoomRange.max, Math.max(DOUBLE_TAP_SCALE, maxContentScale / minContentScale));
+
+ const zoomToCoordinates = (focalX: number, focalY: number, callback: () => void) => {
+ 'worklet';
+
+ stopAnimation();
+ isTransformGestureActive.set(true);
+
+ // By how much the canvas is bigger than the content horizontally and vertically per side
+ const horizontalCanvasOffset = Math.max(0, (canvasSize.width - scaledContentWidth) / 2);
+ const verticalCanvasOffset = Math.max(0, (canvasSize.height - scaledContentHeight) / 2);
+
+ // We need to adjust the focal point to take into account the canvas offset
+ // The focal point cannot be outside of the content's bounds
+ const adjustedFocalPoint = {
+ x: MultiGestureCanvasUtils.clamp(focalX - horizontalCanvasOffset, 0, scaledContentWidth),
+ y: MultiGestureCanvasUtils.clamp(focalY - verticalCanvasOffset, 0, scaledContentHeight),
+ };
+
+ // The center of the canvas
+ const canvasCenter = {
+ x: canvasSize.width / 2,
+ y: canvasSize.height / 2,
+ };
+
+ // The center of the content before zooming
+ const originalContentCenter = {
+ x: scaledContentWidth / 2,
+ y: scaledContentHeight / 2,
+ };
+
+ // The size of the content after zooming
+ const zoomedContentSize = {
+ width: scaledContentWidth * doubleTapScale,
+ height: scaledContentHeight * doubleTapScale,
+ };
+
+ // The center of the zoomed content
+ const zoomedContentCenter = {
+ x: zoomedContentSize.width / 2,
+ y: zoomedContentSize.height / 2,
+ };
+
+ // By how much the zoomed content is bigger/smaller than the canvas.
+ const zoomedContentOffset = {
+ x: zoomedContentCenter.x - canvasCenter.x,
+ y: zoomedContentCenter.y - canvasCenter.y,
+ };
+
+ // How much the content needs to be shifted based on the focal point
+ const shiftingFactor = {
+ x: adjustedFocalPoint.x / originalContentCenter.x - 1,
+ y: adjustedFocalPoint.y / originalContentCenter.y - 1,
+ };
+
+ // The offset after applying the focal point adjusted shift.
+ // We need to invert the shift, because the content is moving in the opposite direction (* -1)
+ const offsetAfterZooming = {
+ x: zoomedContentOffset.x * (shiftingFactor.x * -1),
+ y: zoomedContentOffset.y * (shiftingFactor.y * -1),
+ };
+
+ // If the zoomed content is less tall than the canvas, we need to reset the vertical offset
+ if (zoomedContentSize.height < canvasSize.height) {
+ offsetAfterZooming.y = 0;
+ }
+
+ offsetX.set(withSpring(offsetAfterZooming.x, SPRING_CONFIG));
+ offsetY.set(withSpring(offsetAfterZooming.y, SPRING_CONFIG));
+ zoomScale.set(
+ withSpring(doubleTapScale, SPRING_CONFIG, () => {
+ callback();
+ isTransformGestureActive.set(false);
+ }),
+ );
+ pinchScale.set(doubleTapScale);
+ };
const doubleTapGesture = useTapGesture({
numberOfTaps: 2,
diff --git a/src/components/MultifactorAuthentication/Context/MultifactorAuthenticationComposedContextProviders.tsx b/src/components/MultifactorAuthentication/Context/MultifactorAuthenticationComposedContextProviders.tsx
index bcebfbf8e4ac..76159537091a 100644
--- a/src/components/MultifactorAuthentication/Context/MultifactorAuthenticationComposedContextProviders.tsx
+++ b/src/components/MultifactorAuthentication/Context/MultifactorAuthenticationComposedContextProviders.tsx
@@ -1,6 +1,6 @@
import type {ReactNode} from 'react';
-import React, {useMemo, useReducer} from 'react';
+import React, {useReducer} from 'react';
import MultifactorAuthenticationActionsContext from './MultifactorAuthenticationActionsContext';
import {MultifactorAuthenticationContextProvider} from './MultifactorAuthenticationMainContext';
@@ -13,7 +13,7 @@ type MultifactorAuthenticationProviderProps = {
function MultifactorAuthenticationStateProvider({children}: MultifactorAuthenticationProviderProps) {
const [state, dispatch] = useReducer(stateReducer, DEFAULT_STATE);
- const actions = useMemo(() => ({dispatch}), [dispatch]);
+ const actions = {dispatch};
return (
diff --git a/src/components/MultifactorAuthentication/Context/MultifactorAuthenticationMainContext.tsx b/src/components/MultifactorAuthentication/Context/MultifactorAuthenticationMainContext.tsx
index ec7bceaf11d9..f1583076e436 100644
--- a/src/components/MultifactorAuthentication/Context/MultifactorAuthenticationMainContext.tsx
+++ b/src/components/MultifactorAuthentication/Context/MultifactorAuthenticationMainContext.tsx
@@ -30,7 +30,7 @@ import type {ReactNode} from 'react';
import type {OnyxEntry} from 'react-native-onyx';
import {CONST as COMMON_CONST} from 'expensify-common';
-import React, {createContext, useCallback, useContext, useEffect, useMemo, useRef} from 'react';
+import React, {createContext, useContext, useEffect, useRef} from 'react';
import Onyx from 'react-native-onyx';
import {useMultifactorAuthenticationActions} from './MultifactorAuthenticationActionsContext';
@@ -97,14 +97,14 @@ function MultifactorAuthenticationContextProvider({children}: MultifactorAuthent
const startStateRef = useRef(undefined);
- const captureCredentialsState = useCallback(async (): Promise => {
+ const captureCredentialsState = async (): Promise => {
const hasLocalCredentials = await biometrics.areLocalCredentialsKnownToServer();
return {
hasServerCredentials: biometrics.serverKnownCredentialIDs.length > 0,
hasLocalCredentials,
hasEverAcceptedSoftPrompt: !!deviceBiometricsState?.hasAcceptedSoftPrompt,
};
- }, [biometrics]);
+ };
/**
* Handles the completion of a multifactor authentication scenario.
@@ -117,68 +117,65 @@ function MultifactorAuthenticationContextProvider({children}: MultifactorAuthent
*
* @param isSuccessful - Whether the authentication scenario completed successfully
*/
- const handleCallback = useCallback(
- async (isSuccessful: boolean) => {
- const {error, scenario, scenarioResponse, payload} = state;
+ const handleCallback = async (isSuccessful: boolean) => {
+ const {error, scenario, scenarioResponse, payload} = state;
- if (!scenario) {
- return;
- }
+ if (!scenario) {
+ return;
+ }
- const scenarioConfig = scenario;
- const callbackInput: MultifactorAuthenticationCallbackInput = {
- httpStatusCode: scenarioResponse?.httpStatusCode,
- message: scenarioResponse?.reason ?? error?.reason,
- body: scenarioResponse?.body,
- };
-
- const callbackResponse = await scenarioConfig.callback?.(isSuccessful, callbackInput, payload);
-
- addMFABreadcrumb('Flow completed', {
- isSuccessful,
- callbackResponse: callbackResponse ?? 'none',
- httpStatusCode: scenarioResponse?.httpStatusCode ?? error?.httpStatusCode,
- reason: scenarioResponse?.reason ?? error?.reason,
- message: scenarioResponse?.message ?? error?.message,
- });
+ const scenarioConfig = scenario;
+ const callbackInput: MultifactorAuthenticationCallbackInput = {
+ httpStatusCode: scenarioResponse?.httpStatusCode,
+ message: scenarioResponse?.reason ?? error?.reason,
+ body: scenarioResponse?.body,
+ };
- const endState = await captureCredentialsState();
-
- trackMFAFlowOutcome({
- isSuccessful,
- scenario: state.scenarioName,
- scenarioResponse,
- error,
- authenticationMethod: state.authenticationMethod?.name,
- isRegistrationComplete: state.isRegistrationComplete,
- isAuthorizationComplete: state.isAuthorizationComplete,
- softPromptApproved: state.softPromptApproved,
- startState: startStateRef.current ?? endState,
- endState,
- });
+ const callbackResponse = await scenarioConfig.callback?.(isSuccessful, callbackInput, payload);
- dispatch({type: 'SET_FLOW_COMPLETE', payload: true});
+ addMFABreadcrumb('Flow completed', {
+ isSuccessful,
+ callbackResponse: callbackResponse ?? 'none',
+ httpStatusCode: scenarioResponse?.httpStatusCode ?? error?.httpStatusCode,
+ reason: scenarioResponse?.reason ?? error?.reason,
+ message: scenarioResponse?.message ?? error?.message,
+ });
- // If the callback returns SKIP_OUTCOME_SCREEN, the callback handles navigation itself.
- // Close the modal so the overlay plays its exit animation and then resets.
- if (callbackResponse === CONST.MULTIFACTOR_AUTHENTICATION.CALLBACK_RESPONSE.SKIP_OUTCOME_SCREEN) {
- dispatch({type: 'CLOSE_MODAL'});
- return;
- }
+ const endState = await captureCredentialsState();
- // Wait out any callback-initiated transition (e.g. RHP goBack) before pushing the outcome screen.
- // Android raced the two animations and the RHP slide-out leaked through the outgoing screen.
- const outcomeScreen = isSuccessful ? SCREENS.MULTIFACTOR_AUTHENTICATION.OUTCOME_SUCCESS : SCREENS.MULTIFACTOR_AUTHENTICATION.OUTCOME_FAILURE;
- Navigation.runAfterTransition(() => mfaNavigate(outcomeScreen));
- },
- [captureCredentialsState, dispatch, state],
- );
+ trackMFAFlowOutcome({
+ isSuccessful,
+ scenario: state.scenarioName,
+ scenarioResponse,
+ error,
+ authenticationMethod: state.authenticationMethod?.name,
+ isRegistrationComplete: state.isRegistrationComplete,
+ isAuthorizationComplete: state.isAuthorizationComplete,
+ softPromptApproved: state.softPromptApproved,
+ startState: startStateRef.current ?? endState,
+ endState,
+ });
+
+ dispatch({type: 'SET_FLOW_COMPLETE', payload: true});
+
+ // If the callback returns SKIP_OUTCOME_SCREEN, the callback handles navigation itself.
+ // Close the modal so the overlay plays its exit animation and then resets.
+ if (callbackResponse === CONST.MULTIFACTOR_AUTHENTICATION.CALLBACK_RESPONSE.SKIP_OUTCOME_SCREEN) {
+ dispatch({type: 'CLOSE_MODAL'});
+ return;
+ }
+
+ // Wait out any callback-initiated transition (e.g. RHP goBack) before pushing the outcome screen.
+ // Android raced the two animations and the RHP slide-out leaked through the outgoing screen.
+ const outcomeScreen = isSuccessful ? SCREENS.MULTIFACTOR_AUTHENTICATION.OUTCOME_SUCCESS : SCREENS.MULTIFACTOR_AUTHENTICATION.OUTCOME_FAILURE;
+ Navigation.runAfterTransition(() => mfaNavigate(outcomeScreen));
+ };
/**
* Internal process function that runs after each step.
* Uses if statements to determine and execute the next step in the flow.
*/
- const process = useCallback(async () => {
+ const process = async () => {
const {
error,
continuableError,
@@ -423,7 +420,7 @@ function MultifactorAuthenticationContextProvider({children}: MultifactorAuthent
// 5. All steps completed - invoke callback to determine whether to show the outcome screen
handleCallback(true);
- }, [biometrics, dispatch, handleCallback, isOffline, state, promptType]);
+ };
/**
* Drives the MFA state machine forward whenever relevant state changes occur.
@@ -483,54 +480,51 @@ function MultifactorAuthenticationContextProvider({children}: MultifactorAuthent
*
* @template T - The type of the multifactor authentication scenario
* @param scenario - The MFA scenario to process
- * @param {ExecuteScenarioParams} [params] - Optional parameters for the scenario
- * @returns {Promise} A promise that resolves when the scenario has been initialized
+ * @param params - Optional parameters for the scenario
+ * @returns A promise that resolves when the scenario has been initialized
*/
- const executeScenario = useCallback(
- async (scenario: T, params?: ExecuteScenarioParams): Promise => {
- // Rapid double-tap on the trigger can fire executeScenario twice. The reducer's INIT
- // case is the authoritative guard (it processes dispatches sequentially against the
- // latest state and drops the duplicate). This check exists only as a perf short-circuit
- // so the second tap doesn't pay for an extra captureCredentialsState() native call
- // and Sentry breadcrumb on the happy path.
- if (state.scenario) {
- return;
- }
+ const executeScenario = async (scenario: T, params?: ExecuteScenarioParams): Promise => {
+ // Rapid double-tap on the trigger can fire executeScenario twice. The reducer's INIT
+ // case is the authoritative guard (it processes dispatches sequentially against the
+ // latest state and drops the duplicate). This check exists only as a perf short-circuit
+ // so the second tap doesn't pay for an extra captureCredentialsState() native call
+ // and Sentry breadcrumb on the happy path.
+ if (state.scenario) {
+ return;
+ }
- startStateRef.current = await captureCredentialsState();
+ startStateRef.current = await captureCredentialsState();
- const breadcrumbData = {
- scenario,
- hasPayload: params !== undefined && Object.keys(params).length > 0,
- platform,
- isOffline,
- hasAcceptedSoftPrompt: startStateRef.current.hasEverAcceptedSoftPrompt,
- serverHasAnyCredentials: startStateRef.current.hasServerCredentials,
- };
-
- addMFABreadcrumb('Flow started', breadcrumbData);
- trackMFAFlowStart({
+ const breadcrumbData = {
+ scenario,
+ hasPayload: params !== undefined && Object.keys(params).length > 0,
+ platform,
+ isOffline,
+ hasAcceptedSoftPrompt: startStateRef.current.hasEverAcceptedSoftPrompt,
+ serverHasAnyCredentials: startStateRef.current.hasServerCredentials,
+ };
+
+ addMFABreadcrumb('Flow started', breadcrumbData);
+ trackMFAFlowStart({
+ scenario,
+ isOffline,
+ credentialsState: startStateRef.current,
+ });
+ dispatch({
+ type: 'INIT',
+ payload: {
scenario,
- isOffline,
- credentialsState: startStateRef.current,
- });
- dispatch({
- type: 'INIT',
- payload: {
- scenario,
- payload: params && Object.keys(params).length > 0 ? params : undefined,
- },
- });
- },
- [captureCredentialsState, dispatch, isOffline, platform, state.scenario],
- );
+ payload: params && Object.keys(params).length > 0 ? params : undefined,
+ },
+ });
+ };
/**
* Cancel the current authentication flow. Dispatches SET_ERROR which drives process() to handleCallback
* and the failure outcome. When the scenario or network is unavailable, process() short-circuits, so we
* close the modal directly instead to avoid getting stuck.
*/
- const cancel = useCallback(async () => {
+ const cancel = async () => {
actionControllerRef.current?.abort();
if (!state.scenario || isOffline) {
addMFABreadcrumb('Flow cancelled - closing directly', {hasScenario: !!state.scenario, isOffline}, 'warning');
@@ -550,7 +544,7 @@ function MultifactorAuthenticationContextProvider({children}: MultifactorAuthent
type: 'SET_ERROR',
payload: createLocalMFAError(CONST.MULTIFACTOR_AUTHENTICATION.REASON.LOCAL_ERRORS.CANCELED, 'User cancelled the MFA flow'),
});
- }, [dispatch, isOffline, state.scenario, state.payload]);
+ };
const requestCancel = () => {
if (state.isFlowComplete) {
@@ -573,15 +567,12 @@ function MultifactorAuthenticationContextProvider({children}: MultifactorAuthent
useSyncMfaModalNavigatorWithHistory(state.isModalOpen, requestCancel);
- const contextValue: MultifactorAuthenticationContextValue = useMemo(
- () => ({
- executeScenario,
- requestCancel,
- hideCancelConfirm,
- confirmCancel,
- }),
- [executeScenario, requestCancel, hideCancelConfirm, confirmCancel],
- );
+ const contextValue: MultifactorAuthenticationContextValue = {
+ executeScenario,
+ requestCancel,
+ hideCancelConfirm,
+ confirmCancel,
+ };
return {children};
}
diff --git a/src/components/MultifactorAuthentication/ValidateCodeResendButton.tsx b/src/components/MultifactorAuthentication/ValidateCodeResendButton.tsx
index 2fe4d804892c..56ae0ac4b6f7 100644
--- a/src/components/MultifactorAuthentication/ValidateCodeResendButton.tsx
+++ b/src/components/MultifactorAuthentication/ValidateCodeResendButton.tsx
@@ -11,7 +11,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
-import React, {useCallback, useImperativeHandle, useRef, useState} from 'react';
+import React, {useImperativeHandle, useRef, useState} from 'react';
import {View} from 'react-native';
type MultifactorAuthenticationValidateCodeResendButtonHandle = {
@@ -41,9 +41,9 @@ function MultifactorAuthenticationValidateCodeResendButton({
const [isCountdownRunning, setIsCountdownRunning] = useState(true);
const countdownRef = useRef(null);
- const handleCountdownFinish = useCallback(() => {
+ const handleCountdownFinish = () => {
setIsCountdownRunning(false);
- }, []);
+ };
useImperativeHandle(ref, () => ({
resetCountdown: () => {
diff --git a/src/components/Navigation/DebugTabView.tsx b/src/components/Navigation/DebugTabView.tsx
index f031578e1cce..e429b67bfef4 100644
--- a/src/components/Navigation/DebugTabView.tsx
+++ b/src/components/Navigation/DebugTabView.tsx
@@ -38,7 +38,7 @@ import type {NavigationState} from '@react-navigation/native';
import type {OnyxEntry} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
-import React, {useCallback, useMemo} from 'react';
+import React from 'react';
import {View} from 'react-native';
import NAVIGATION_TABS from './NavigationTabBar/NAVIGATION_TABS';
@@ -178,37 +178,22 @@ function DebugTabView({selectedTab}: Props) {
return FULL_WIDTH_TAB_ROOT_SCREENS.has(focusedLeaf);
});
- const message = useMemo((): TranslationPaths | undefined => {
- if (selectedTab === NAVIGATION_TABS.INBOX) {
- if (chatTabBrickRoad === CONST.BRICK_ROAD_INDICATOR_STATUS.INFO) {
- return 'debug.indicatorStatus.theresAReportAwaitingAction';
- }
- if (chatTabBrickRoad === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR) {
- return 'debug.indicatorStatus.theresAReportWithErrors';
- }
- }
- if (selectedTab === NAVIGATION_TABS.SETTINGS || selectedTab === NAVIGATION_TABS.WORKSPACES) {
- return getSettingsMessage(status);
- }
- }, [selectedTab, chatTabBrickRoad, status]);
-
- const indicator = useMemo(() => {
- if (selectedTab === NAVIGATION_TABS.INBOX) {
- if (chatTabBrickRoad === CONST.BRICK_ROAD_INDICATOR_STATUS.INFO) {
- return theme.success;
- }
- if (chatTabBrickRoad === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR) {
- return theme.danger;
- }
- }
- if (selectedTab === NAVIGATION_TABS.SETTINGS || selectedTab === NAVIGATION_TABS.WORKSPACES) {
- if (status) {
- return indicatorColor;
- }
+ let message: TranslationPaths | undefined;
+ let indicator;
+ if (selectedTab === NAVIGATION_TABS.INBOX) {
+ if (chatTabBrickRoad === CONST.BRICK_ROAD_INDICATOR_STATUS.INFO) {
+ message = 'debug.indicatorStatus.theresAReportAwaitingAction';
+ indicator = theme.success;
+ } else if (chatTabBrickRoad === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR) {
+ message = 'debug.indicatorStatus.theresAReportWithErrors';
+ indicator = theme.danger;
}
- }, [selectedTab, chatTabBrickRoad, theme.success, theme.danger, status, indicatorColor]);
+ } else if (selectedTab === NAVIGATION_TABS.SETTINGS || selectedTab === NAVIGATION_TABS.WORKSPACES) {
+ message = getSettingsMessage(status);
+ indicator = status ? indicatorColor : undefined;
+ }
- const navigateTo = useCallback(() => {
+ const navigateTo = () => {
if (selectedTab === NAVIGATION_TABS.INBOX && !!chatTabBrickRoad) {
const reportID = getChatTabBrickRoadReportID(orderedReportIDs, reportAttributes);
@@ -223,7 +208,7 @@ function DebugTabView({selectedTab}: Props) {
Navigation.navigate(route);
}
}
- }, [selectedTab, chatTabBrickRoad, orderedReportIDs, reportAttributes, status, reimbursementAccount, indicatorPolicyID]);
+ };
if (
(shouldUseNarrowLayout && !isAtRoot) ||
diff --git a/src/components/Navigation/QuickCreationActionsBar/index.tsx b/src/components/Navigation/QuickCreationActionsBar/index.tsx
index fa8f03f53e2f..3bad67078d9f 100644
--- a/src/components/Navigation/QuickCreationActionsBar/index.tsx
+++ b/src/components/Navigation/QuickCreationActionsBar/index.tsx
@@ -31,7 +31,7 @@ import {isTrackIntentUserSelector} from '@selectors/Onboarding';
import {emailSelector} from '@selectors/Session';
import {validTransactionDraftIDsSelector} from '@selectors/TransactionDraft';
import {Str} from 'expensify-common';
-import React, {useCallback, useMemo} from 'react';
+import React from 'react';
import {View} from 'react-native';
function QuickCreationActionsBar() {
@@ -57,49 +57,39 @@ function QuickCreationActionsBar() {
const [isTrackIntentUser] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {selector: isTrackIntentUserSelector});
const [rules] = useOnyx(ONYXKEYS.COLLECTION.RULE);
- const travelEnabledPolicy = useMemo(
- () => Object.values(allPolicies ?? {}).find((policy) => isWorkspaceProvisionedForTravel(policy?.travelSettings) && isPolicyAccessible(policy, email ?? '')),
- [allPolicies, email],
- );
+ const travelEnabledPolicy = Object.values(allPolicies ?? {}).find((policy) => isWorkspaceProvisionedForTravel(policy?.travelSettings) && isPolicyAccessible(policy, email ?? ''));
const shouldShowBookTravel = !!travelEnabledPolicy;
const isBlockedFromSpotnanaTravel = isBetaEnabled(CONST.BETAS.PREVENT_SPOTNANA_TRAVEL);
const primaryContactMethod = primaryLogin ?? session?.email ?? '';
- const isTravelReady = useMemo(() => {
- if (!!isBlockedFromSpotnanaTravel || !primaryContactMethod || Str.isSMSLogin(primaryContactMethod) || !isPaidGroupPolicy(travelEnabledPolicy)) {
- return false;
- }
-
- return hasAcceptedTravelTerms(travelEnabledPolicy, travelSettings);
- }, [travelEnabledPolicy, isBlockedFromSpotnanaTravel, primaryContactMethod, travelSettings]);
+ const isTravelReady =
+ !(!!isBlockedFromSpotnanaTravel || !primaryContactMethod || Str.isSMSLogin(primaryContactMethod) || !isPaidGroupPolicy(travelEnabledPolicy)) &&
+ hasAcceptedTravelTerms(travelEnabledPolicy, travelSettings);
- const handleCreateWorkspaceReport = useCallback(
- (policy: OnyxEntry, shouldDismissEmptyReportsConfirmation?: boolean) => {
- if (!policy?.id) {
- return;
- }
+ const handleCreateWorkspaceReport = (policy: OnyxEntry, shouldDismissEmptyReportsConfirmation?: boolean) => {
+ if (!policy?.id) {
+ return;
+ }
- const {reportID: createdReportID} = createNewReport(
- currentUserPersonalDetails,
- hasViolations,
- isASAPSubmitBetaEnabled,
- policy,
- isTrackIntentUser,
- getCurrencyDecimals,
- rules,
- false,
- shouldDismissEmptyReportsConfirmation,
- );
- // Navigate to the Reports page first so getCreateReportRoute() resolves against
- // the Search/Reports fullscreen context before opening the created report modal.
- Navigation.navigate(getReportsRootRoute());
- Navigation.setNavigationActionToMicrotaskQueue(() => {
- Navigation.navigate(getCreateReportRoute({reportID: createdReportID}));
- });
- },
- [currentUserPersonalDetails, hasViolations, isASAPSubmitBetaEnabled, isTrackIntentUser, getCurrencyDecimals, rules],
- );
+ const {reportID: createdReportID} = createNewReport(
+ currentUserPersonalDetails,
+ hasViolations,
+ isASAPSubmitBetaEnabled,
+ policy,
+ isTrackIntentUser,
+ getCurrencyDecimals,
+ rules,
+ false,
+ shouldDismissEmptyReportsConfirmation,
+ );
+ // Navigate to the Reports page first so getCreateReportRoute() resolves against
+ // the Search/Reports fullscreen context before opening the created report modal.
+ Navigation.navigate(getReportsRootRoute());
+ Navigation.setNavigationActionToMicrotaskQueue(() => {
+ Navigation.navigate(getCreateReportRoute({reportID: createdReportID}));
+ });
+ };
const {createReport, isVisible: isCreateReportReady} = useCreateReport({
onCreateReport: handleCreateWorkspaceReport,
@@ -107,36 +97,27 @@ function QuickCreationActionsBar() {
shouldHandleNavigationBack: false,
});
- const handleExpense = useCallback(
- () =>
- interceptAnonymousUser(() => {
- startMoneyRequest(CONST.IOU.TYPE.CREATE, generateReportID(), draftTransactionIDs);
- }),
- [draftTransactionIDs],
- );
+ const handleExpense = () =>
+ interceptAnonymousUser(() => {
+ startMoneyRequest(CONST.IOU.TYPE.CREATE, generateReportID(), draftTransactionIDs);
+ });
- const handleDistance = useCallback(
- () =>
- interceptAnonymousUser(() => {
- startDistanceRequest(CONST.IOU.TYPE.CREATE, generateReportID(), draftTransactionIDs, lastDistanceExpenseType);
- }),
- [draftTransactionIDs, lastDistanceExpenseType],
- );
+ const handleDistance = () =>
+ interceptAnonymousUser(() => {
+ startDistanceRequest(CONST.IOU.TYPE.CREATE, generateReportID(), draftTransactionIDs, lastDistanceExpenseType);
+ });
- const handleBookTravel = useCallback(
- () =>
- interceptAnonymousUser(() => {
- if (isTravelReady) {
- if (blockIfDefaultWorkspaceLacksTravel()) {
- return;
- }
- openTravelDotLink(travelEnabledPolicy?.id);
+ const handleBookTravel = () =>
+ interceptAnonymousUser(() => {
+ if (isTravelReady) {
+ if (blockIfDefaultWorkspaceLacksTravel()) {
return;
}
- Navigation.navigate(ROUTES.TRAVEL_MY_TRIPS.getRoute(travelEnabledPolicy?.id));
- }),
- [travelEnabledPolicy?.id, isTravelReady, blockIfDefaultWorkspaceLacksTravel],
- );
+ openTravelDotLink(travelEnabledPolicy?.id);
+ return;
+ }
+ Navigation.navigate(ROUTES.TRAVEL_MY_TRIPS.getRoute(travelEnabledPolicy?.id));
+ });
return (
diff --git a/src/components/Navigation/SearchSidebarCollapseStore.ts b/src/components/Navigation/SearchSidebarCollapseStore.ts
index 6a621c15e4c0..c33e6a9e1c83 100644
--- a/src/components/Navigation/SearchSidebarCollapseStore.ts
+++ b/src/components/Navigation/SearchSidebarCollapseStore.ts
@@ -8,7 +8,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
import type {ViewStyle} from 'react-native';
-import {useCallback, useMemo, useSyncExternalStore} from 'react';
+import {useSyncExternalStore} from 'react';
import {Platform} from 'react-native';
const SEARCH_SIDEBAR_COLLAPSE_ANIMATION_DURATION_MS = 220;
@@ -73,8 +73,8 @@ function useSearchSidebarCollapse() {
const [searchSidebarNVP] = useOnyx(ONYXKEYS.NVP_SEARCH_SIDEBAR);
const collapsed = searchSidebarNVP?.isCollapsed ?? false;
const peeking = useSyncExternalStore(subscribe, getPeekSnapshot, getPeekSnapshot);
- const toggleSidebar = useCallback(() => setSearchSidebarCollapsed(!collapsed), [collapsed]);
- const startSidebarPeek = useCallback(() => startPeek(collapsed), [collapsed]);
+ const toggleSidebar = () => setSearchSidebarCollapsed(!collapsed);
+ const startSidebarPeek = () => startPeek(collapsed);
return {
isCollapsed: collapsed,
@@ -89,44 +89,38 @@ function useSearchSidebarCollapse() {
function useSearchSidebarLayoutWidthStyle() {
const {isCollapsed: collapsed} = useSearchSidebarCollapse();
- return useMemo(() => ({...layoutTransitionStyle, height: '100%', width: getSearchSidebarWidth(collapsed ? 1 : 0)}), [collapsed]);
+ return {...layoutTransitionStyle, height: '100%', width: getSearchSidebarWidth(collapsed ? 1 : 0)};
}
function useSearchSidebarVisualWidthStyle() {
const {isCollapsed: collapsed, isPeeking: peeking} = useSearchSidebarCollapse();
- return useMemo(() => ({...layoutTransitionStyle, width: getSearchSidebarWidth(collapsed && !peeking ? 1 : 0)}), [collapsed, peeking]);
+ return {...layoutTransitionStyle, width: getSearchSidebarWidth(collapsed && !peeking ? 1 : 0)};
}
function useSearchSidebarContentOffsetStyle() {
const {isCollapsed: collapsed} = useSearchSidebarCollapse();
- return useMemo(() => ({...layoutTransitionStyle, marginLeft: getSearchSidebarWidth(collapsed ? 1 : 0)}), [collapsed]);
+ return {...layoutTransitionStyle, marginLeft: getSearchSidebarWidth(collapsed ? 1 : 0)};
}
function useSearchSidebarCollapseFadeStyle() {
const {isVisuallyCollapsed} = useSearchSidebarCollapse();
- return useMemo(
- () => ({
- ...fadeTransitionStyle,
- opacity: isVisuallyCollapsed ? 0 : 1,
- transform: [{translateX: isVisuallyCollapsed ? SEARCH_SIDEBAR_COLLAPSE_TRANSLATE_X : 0}],
- }),
- [isVisuallyCollapsed],
- );
+ return {
+ ...fadeTransitionStyle,
+ opacity: isVisuallyCollapsed ? 0 : 1,
+ transform: [{translateX: isVisuallyCollapsed ? SEARCH_SIDEBAR_COLLAPSE_TRANSLATE_X : 0}],
+ };
}
function useSearchSidebarToggleButtonStyle() {
const {isVisuallyCollapsed} = useSearchSidebarCollapse();
- return useMemo(
- () => ({
- ...fadeTransitionStyle,
- transform: [{translateX: isVisuallyCollapsed ? TOGGLE_BUTTON_COLLAPSED_TRANSLATE_X : 0}],
- }),
- [isVisuallyCollapsed],
- );
+ return {
+ ...fadeTransitionStyle,
+ transform: [{translateX: isVisuallyCollapsed ? TOGGLE_BUTTON_COLLAPSED_TRANSLATE_X : 0}],
+ };
}
export {
diff --git a/src/components/NavigationBar/index.android.tsx b/src/components/NavigationBar/index.android.tsx
index 79a38ba37143..f8c3e2918d1b 100644
--- a/src/components/NavigationBar/index.android.tsx
+++ b/src/components/NavigationBar/index.android.tsx
@@ -5,7 +5,6 @@ import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
-import {useMemo} from 'react';
import {View} from 'react-native';
/** NavigationBar renders a semi-translucent background behind the three-button navigation bar on Android. */
@@ -15,7 +14,7 @@ function NavigationBar() {
const {insets, paddingBottom} = useSafeAreaPaddings();
const {isOffline} = useNetwork();
- const navigationBarType = useMemo(() => StyleUtils.getNavigationBarType(insets), [StyleUtils, insets]);
+ const navigationBarType = StyleUtils.getNavigationBarType(insets);
const isSoftKeyNavigation = navigationBarType === CONST.NAVIGATION_BAR_TYPE.SOFT_KEYS;
return isSoftKeyNavigation ? : null;
diff --git a/src/components/NumberWithSymbolForm.tsx b/src/components/NumberWithSymbolForm.tsx
index 0d6f29060936..ad1ee6ddc005 100644
--- a/src/components/NumberWithSymbolForm.tsx
+++ b/src/components/NumberWithSymbolForm.tsx
@@ -27,7 +27,7 @@ import type {ForwardedRef} from 'react';
import type {KeyboardTypeOptions, NativeSyntheticEvent, StyleProp, TextStyle, ViewStyle} from 'react-native';
import {useIsFocused} from '@react-navigation/native';
-import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
+import React, {useEffect, useImperativeHandle, useRef, useState} from 'react';
import {View} from 'react-native';
import type {BaseTextInputRef} from './TextInput/BaseTextInput/types';
@@ -254,9 +254,9 @@ function NumberWithSymbolForm({
setMouseUp();
};
- const clearSelection = useCallback(() => {
+ const clearSelection = () => {
setSelection({start: selection.end, end: selection.end});
- }, [selection.end]);
+ };
/**
* Event occurs when a user presses a mouse button over an DOM element.
@@ -282,41 +282,38 @@ function NumberWithSymbolForm({
* Sets the selection and the number accordingly to the number passed to the input
* @param newNumber - Changed number from user input
*/
- const setNewNumber = useCallback(
- (newNumber: string) => {
- // Remove spaces from the newNumber number because Safari on iOS adds spaces when pasting a copied number
- // More info: https://github.com/Expensify/App/issues/16974
- const newNumberWithoutSpaces = stripSpacesFromAmount(newNumber);
- const rawFinalNumber = newNumberWithoutSpaces.includes('.') ? stripCommaFromAmount(newNumberWithoutSpaces) : replaceCommasWithPeriod(newNumberWithoutSpaces);
-
- // When allowNegativeInput is true, keep negative sign as-is (for split amounts)
- // When allowFlippingAmount is true, strip the negative sign and call toggleNegative
- const finalNumber = allowNegativeInput ? rawFinalNumber : handleNegativeAmountFlipping(rawFinalNumber, allowFlippingAmount, toggleNegative);
-
- // Use a shallow copy of selection to trigger setSelection
- // More info: https://github.com/Expensify/App/issues/16385
- if (!validateAmount(finalNumber, decimals, maxLength, allowNegativeInput)) {
- setSelection((prevSelection) => ({...prevSelection}));
- return;
- }
+ const setNewNumber = (newNumber: string) => {
+ // Remove spaces from the newNumber number because Safari on iOS adds spaces when pasting a copied number
+ // More info: https://github.com/Expensify/App/issues/16974
+ const newNumberWithoutSpaces = stripSpacesFromAmount(newNumber);
+ const rawFinalNumber = newNumberWithoutSpaces.includes('.') ? stripCommaFromAmount(newNumberWithoutSpaces) : replaceCommasWithPeriod(newNumberWithoutSpaces);
- willSelectionBeUpdatedManually.current = true;
- let hasSelectionBeenSet = false;
- const strippedNumber = stripCommaFromAmount(finalNumber);
- numberRef.current = strippedNumber;
- setCurrentNumber((prevNumber) => {
- const isForwardDelete = prevNumber.length > strippedNumber.length && forwardDeletePressedRef.current;
- if (!hasSelectionBeenSet) {
- hasSelectionBeenSet = true;
- setSelection((prevSelection) => getNewSelection(prevSelection, isForwardDelete ? strippedNumber.length : prevNumber.length, strippedNumber.length));
- willSelectionBeUpdatedManually.current = false;
- }
- return strippedNumber;
- });
- onInputChange?.(strippedNumber);
- },
- [decimals, maxLength, onInputChange, allowFlippingAmount, toggleNegative, allowNegativeInput],
- );
+ // When allowNegativeInput is true, keep negative sign as-is (for split amounts)
+ // When allowFlippingAmount is true, strip the negative sign and call toggleNegative
+ const finalNumber = allowNegativeInput ? rawFinalNumber : handleNegativeAmountFlipping(rawFinalNumber, allowFlippingAmount, toggleNegative);
+
+ // Use a shallow copy of selection to trigger setSelection
+ // More info: https://github.com/Expensify/App/issues/16385
+ if (!validateAmount(finalNumber, decimals, maxLength, allowNegativeInput)) {
+ setSelection((prevSelection) => ({...prevSelection}));
+ return;
+ }
+
+ willSelectionBeUpdatedManually.current = true;
+ let hasSelectionBeenSet = false;
+ const strippedNumber = stripCommaFromAmount(finalNumber);
+ numberRef.current = strippedNumber;
+ setCurrentNumber((prevNumber) => {
+ const isForwardDelete = prevNumber.length > strippedNumber.length && forwardDeletePressedRef.current;
+ if (!hasSelectionBeenSet) {
+ hasSelectionBeenSet = true;
+ setSelection((prevSelection) => getNewSelection(prevSelection, isForwardDelete ? strippedNumber.length : prevNumber.length, strippedNumber.length));
+ willSelectionBeUpdatedManually.current = false;
+ }
+ return strippedNumber;
+ });
+ onInputChange?.(strippedNumber);
+ };
/**
* Set a new number number properly formatted, used for the TextInput
@@ -375,37 +372,34 @@ function NumberWithSymbolForm({
* Update number with number or Backspace pressed for BigNumberPad.
* Validate new number with decimal number regex up to 6 digits and 2 decimal digit to enable Next button
*/
- const updateValueNumberPad = useCallback(
- (key: string) => {
- if (shouldUpdateSelection && !isTextInputFocused(textInput)) {
- textInput.current?.focus();
- }
- // Backspace button is pressed
- if (key === '<' || key === 'Backspace') {
- if (currentNumber.length > 0) {
- const selectionStart = selection.start === selection.end ? selection.start - 1 : selection.start;
- const newNumber = `${currentNumber.substring(0, selectionStart)}${currentNumber.substring(selection.end)}`;
- setNewNumber(addLeadingZero(newNumber, allowNegativeInput));
- }
- return;
+ const updateValueNumberPad = (key: string) => {
+ if (shouldUpdateSelection && !isTextInputFocused(textInput)) {
+ textInput.current?.focus();
+ }
+ // Backspace button is pressed
+ if (key === '<' || key === 'Backspace') {
+ if (currentNumber.length > 0) {
+ const selectionStart = selection.start === selection.end ? selection.start - 1 : selection.start;
+ const newNumber = `${currentNumber.substring(0, selectionStart)}${currentNumber.substring(selection.end)}`;
+ setNewNumber(addLeadingZero(newNumber, allowNegativeInput));
}
- const newNumber = addLeadingZero(`${currentNumber.substring(0, selection.start)}${key}${currentNumber.substring(selection.end)}`, allowNegativeInput);
- setNewNumber(newNumber);
- },
- [currentNumber, selection.start, selection.end, shouldUpdateSelection, setNewNumber, allowNegativeInput],
- );
+ return;
+ }
+ const newNumber = addLeadingZero(`${currentNumber.substring(0, selection.start)}${key}${currentNumber.substring(selection.end)}`, allowNegativeInput);
+ setNewNumber(newNumber);
+ };
/**
* Update long press number, to remove items pressing on <
*
* @param value - Changed text from user input
*/
- const updateLongPressHandlerState = useCallback((value: boolean) => {
+ const updateLongPressHandlerState = (value: boolean) => {
setShouldUpdateSelection(!value);
if (!value && !isTextInputFocused(textInput)) {
textInput.current?.focus();
}
- }, []);
+ };
/**
* Input handler to check for a forward-delete key (or keyboard shortcut) press.
@@ -460,16 +454,14 @@ function NumberWithSymbolForm({
};
// Calculate dynamic font size based on the total length of the amount display
- const dynamicAmountStyle = useMemo(() => {
- const totalLength = formattedNumber.length + (hideSymbol ? 0 : symbol.length) + (isNegative ? 1 : 0);
- return StyleUtils.getAmountInputFontSize(totalLength);
- }, [StyleUtils, formattedNumber.length, hideSymbol, symbol.length, isNegative]);
+ const totalLength = formattedNumber.length + (hideSymbol ? 0 : symbol.length) + (isNegative ? 1 : 0);
+ const dynamicAmountStyle = StyleUtils.getAmountInputFontSize(totalLength);
/**
* Handles pressing the flip button (+/-) to toggle negative sign
* Only available in displayAsTextInput mode for manual expense flow
*/
- const handleFlipPress = useCallback(() => {
+ const handleFlipPress = () => {
// Toggle the minus sign prefix in the value
const isRemovingSign = currentNumber.startsWith('-');
const newValue = isRemovingSign ? currentNumber.slice(1) : `-${currentNumber}`;
@@ -490,7 +482,7 @@ function NumberWithSymbolForm({
end: Math.max(prevSelection.end + offset, 0),
}));
onInputChange?.(newValue);
- }, [currentNumber, onInputChange]);
+ };
// Borderless buttons read as part of the field rather than as controls on top of it, so their labels take the
// supporting text color instead of the button's own, matching the icons beside them.
@@ -501,71 +493,54 @@ function NumberWithSymbolForm({
* Renders flip (+/-) button and/or currency selection button when enabled
* Only shown when clear button is not visible (see TextInput conditional rendering)
*/
- const textInputRightHandSideComponent = useMemo(() => {
- return (
-
- {leadingRightHandSideComponent}
- {shouldShowFlipButton && allowNegativeInput && canUseTouchScreen && (
-
+ )}
+ {shouldShowCurrencyButton && !!currencyOrUnitButtonText && (
+ e.preventDefault()}
+ contentContainerStyle={styles.justifyContentCenter}
+ accessibilityLabel={currencyButtonAccessibilityLabel ?? `${translate('common.selectCurrency')}, ${currencyOrUnitButtonText}`}
+ isDisabled={disabled}
+ >
+
+ {currencyOrUnitButtonText}
+
+ )}
+
+ );
if (displayAsTextInput) {
return (
diff --git a/src/components/OfflineIndicator.tsx b/src/components/OfflineIndicator.tsx
index cfecca387668..e033d1a288e5 100644
--- a/src/components/OfflineIndicator.tsx
+++ b/src/components/OfflineIndicator.tsx
@@ -9,7 +9,7 @@ import variables from '@styles/variables';
import type {StyleProp, ViewStyle} from 'react-native';
-import React, {useMemo} from 'react';
+import React from 'react';
import {View} from 'react-native';
import Icon from './Icon';
@@ -33,7 +33,7 @@ function OfflineIndicator({style, containerStyles: containerStylesProp, addBotto
const {isOffline} = useNetwork();
const icons = useMemoizedLazyExpensifyIcons(['OfflineCloud']);
- const fallbackStyle = useMemo(() => [styles.offlineIndicatorContainer, containerStylesProp], [styles.offlineIndicatorContainer, containerStylesProp]);
+ const fallbackStyle = [styles.offlineIndicatorContainer, containerStylesProp];
const containerStyles = useBottomSafeSafeAreaPaddingStyle({
addBottomSafeAreaPadding,
addOfflineIndicatorBottomSafeAreaPadding: false,
diff --git a/src/components/OptionRow.tsx b/src/components/OptionRow.tsx
index 9a446b6a6a65..2348b4138391 100644
--- a/src/components/OptionRow.tsx
+++ b/src/components/OptionRow.tsx
@@ -14,7 +14,6 @@ import CONST from '@src/CONST';
import type {ComponentRef} from 'react';
import type {StyleProp, TextStyle, ViewStyle} from 'react-native';
-import {deepEqual} from 'fast-equals';
import React, {useEffect, useRef, useState} from 'react';
import {StyleSheet, View} from 'react-native';
@@ -302,29 +301,6 @@ function OptionRow({
OptionRow.displayName = 'OptionRow';
-export default React.memo(
- OptionRow,
- (prevProps, nextProps) =>
- prevProps.isDisabled === nextProps.isDisabled &&
- prevProps.isMultilineSupported === nextProps.isMultilineSupported &&
- prevProps.isSelected === nextProps.isSelected &&
- prevProps.shouldHaveOptionSeparator === nextProps.shouldHaveOptionSeparator &&
- prevProps.showTitleTooltip === nextProps.showTitleTooltip &&
- // eslint-disable-next-line rulesdir/no-deep-equal-in-memo -- icons array is created inline in some usages (e.g., BaseReactionList) with unstable references
- deepEqual(prevProps.option.icons, nextProps.option.icons) &&
- prevProps.optionIsFocused === nextProps.optionIsFocused &&
- prevProps.option.text === nextProps.option.text &&
- prevProps.option.alternateText === nextProps.option.alternateText &&
- prevProps.option.descriptiveText === nextProps.option.descriptiveText &&
- prevProps.option.brickRoadIndicator === nextProps.option.brickRoadIndicator &&
- prevProps.option.shouldShowSubscript === nextProps.option.shouldShowSubscript &&
- prevProps.option.ownerAccountID === nextProps.option.ownerAccountID &&
- prevProps.option.subtitle === nextProps.option.subtitle &&
- prevProps.option.pendingAction === nextProps.option.pendingAction &&
- prevProps.option.customIcon === nextProps.option.customIcon &&
- prevProps.option.tabIndex === nextProps.option.tabIndex &&
- // eslint-disable-next-line rulesdir/no-deep-equal-in-memo -- amountInputProps origin and reference stability cannot be determined across all usages
- deepEqual(prevProps.option.amountInputProps, nextProps.option.amountInputProps),
-);
+export default OptionRow;
export type {OptionRowProps};
diff --git a/src/components/PDFThumbnail/index.native.tsx b/src/components/PDFThumbnail/index.native.tsx
index fd2f0cb10f3f..ffd51e09a99d 100644
--- a/src/components/PDFThumbnail/index.native.tsx
+++ b/src/components/PDFThumbnail/index.native.tsx
@@ -52,4 +52,4 @@ function PDFThumbnail({previewSourceURL, style, enabled = true, fitPolicy = 0, o
PDFThumbnail.displayName = 'PDFThumbnail';
-export default React.memo(PDFThumbnail);
+export default PDFThumbnail;
diff --git a/src/components/PDFThumbnail/index.tsx b/src/components/PDFThumbnail/index.tsx
index 28d8ad218ca0..5d5c8ac09b6e 100644
--- a/src/components/PDFThumbnail/index.tsx
+++ b/src/components/PDFThumbnail/index.tsx
@@ -6,7 +6,7 @@ import type {PDFDocumentProxy} from 'pdfjs-dist';
// eslint-disable-next-line import/extensions
import pdfWorkerSource from 'pdfjs-dist/legacy/build/pdf.worker.min.mjs';
-import React, {useMemo, useState} from 'react';
+import React, {useState} from 'react';
import {View} from 'react-native';
import {Document, pdfjs, Thumbnail} from 'react-pdf';
@@ -27,37 +27,34 @@ function PDFThumbnail({previewSourceURL, style, enabled = true, onPassword, onLo
const styles = useThemeStyles();
const [failedToLoad, setFailedToLoad] = useState(false);
- const thumbnail = useMemo(
- () => (
- }
- file={previewSourceURL}
- options={PDF_DOCUMENT_OPTIONS}
- externalLinkTarget="_blank"
- onPassword={onPassword}
- onLoad={() => {
- setFailedToLoad(false);
- }}
- onLoadSuccess={(pdf: PDFDocumentProxy) => {
- if (!onLoadSuccess) {
- return;
- }
- onLoadSuccess(pdf.numPages);
- }}
- onLoadError={() => {
- if (onLoadError) {
- onLoadError();
- }
- setFailedToLoad(true);
- }}
- error={() => null}
- >
-
-
-
-
- ),
- [previewSourceURL, onPassword, onLoadError, onLoadSuccess],
+ const thumbnail = (
+ }
+ file={previewSourceURL}
+ options={PDF_DOCUMENT_OPTIONS}
+ externalLinkTarget="_blank"
+ onPassword={onPassword}
+ onLoad={() => {
+ setFailedToLoad(false);
+ }}
+ onLoadSuccess={(pdf: PDFDocumentProxy) => {
+ if (!onLoadSuccess) {
+ return;
+ }
+ onLoadSuccess(pdf.numPages);
+ }}
+ onLoadError={() => {
+ if (onLoadError) {
+ onLoadError();
+ }
+ setFailedToLoad(true);
+ }}
+ error={() => null}
+ >
+
+
+
+
);
return (
@@ -81,7 +78,7 @@ function PDFThumbnail({previewSourceURL, style, enabled = true, onPassword, onLo
PDFThumbnail.displayName = 'PDFThumbnail';
-export default React.memo(PDFThumbnail);
+export default PDFThumbnail;
// Re-exported so other PDF-rendering components reuse this file's worker setup
// instead of importing pdfjs-dist/react-pdf directly.
diff --git a/src/components/PDFView/PDFPasswordForm.tsx b/src/components/PDFView/PDFPasswordForm.tsx
index d8c8192bd4ee..2a4eacdbb481 100644
--- a/src/components/PDFView/PDFPasswordForm.tsx
+++ b/src/components/PDFView/PDFPasswordForm.tsx
@@ -13,7 +13,7 @@ import {getBrowser} from '@libs/Browser';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
-import React, {useEffect, useMemo, useRef, useState} from 'react';
+import React, {useEffect, useRef, useState} from 'react';
import {View} from 'react-native';
import PDFInfoMessage from './PDFInfoMessage';
@@ -49,15 +49,12 @@ function PDFPasswordForm({isFocused, isPasswordInvalid = false, shouldShowLoadin
const focusTimeoutRef = useRef(undefined);
- const errorText = useMemo(() => {
- if (isPasswordInvalid) {
- return translate('attachmentView.passwordIncorrect');
- }
- if (validationErrorText) {
- return translate(validationErrorText);
- }
- return '';
- }, [isPasswordInvalid, validationErrorText, translate]);
+ let errorText;
+ if (isPasswordInvalid) {
+ errorText = translate('attachmentView.passwordIncorrect');
+ } else {
+ errorText = validationErrorText ? translate(validationErrorText) : '';
+ }
useEffect(() => {
if (!isFocused) {
diff --git a/src/components/PDFView/index.native.tsx b/src/components/PDFView/index.native.tsx
index 971fccd3e8b7..ee8712b174d2 100644
--- a/src/components/PDFView/index.native.tsx
+++ b/src/components/PDFView/index.native.tsx
@@ -19,7 +19,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
import type {StyleProp, ViewStyle} from 'react-native';
-import React, {useCallback, useEffect, useState} from 'react';
+import React, {useEffect, useState} from 'react';
import {Keyboard, Linking, View} from 'react-native';
import PDF from 'react-native-pdf';
@@ -77,7 +77,7 @@ function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused
* invalid.
*/
- const initiatePasswordChallenge = useCallback(() => {
+ const initiatePasswordChallenge = () => {
setShouldShowLoadingIndicator(false);
// Render password form, and don't render PDF and loading indicator.
@@ -90,7 +90,7 @@ function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused
if (password) {
setIsPasswordInvalid(true);
}
- }, [password]);
+ };
const handleFailureToLoadPDF = ((error: Error) => {
if (error.message.match(/password/i)) {
@@ -138,21 +138,18 @@ function PDFView({onToggleKeyboard, onLoadComplete, fileName, onPress, isFocused
/**
* Handle press link event on native apps.
*/
- const handlePressLink = useCallback(
- (url: string) => {
- if (!isAllowedPDFLink(url)) {
- return;
- }
-
- if (isTravelLink(url) && activePolicyID) {
- const postLoginPath = getRelativeUrl(url);
- openTravelDotLink(activePolicyID, postLoginPath);
- return;
- }
- Linking.openURL(url);
- },
- [activePolicyID],
- );
+ const handlePressLink = (url: string) => {
+ if (!isAllowedPDFLink(url)) {
+ return;
+ }
+
+ if (isTravelLink(url) && activePolicyID) {
+ const postLoginPath = getRelativeUrl(url);
+ openTravelDotLink(activePolicyID, postLoginPath);
+ return;
+ }
+ Linking.openURL(url);
+ };
function renderPDFView() {
// Insets are used to adjust the maxWidth of the PDF view in landscape mode
diff --git a/src/components/PDFView/index.tsx b/src/components/PDFView/index.tsx
index 1b3f2e53ac16..5d25863410aa 100644
--- a/src/components/PDFView/index.tsx
+++ b/src/components/PDFView/index.tsx
@@ -26,7 +26,7 @@ import type {Dimensions} from '@src/types/utils/Layout';
import type {CSSProperties, ReactNode} from 'react';
import type {LayoutChangeEvent} from 'react-native';
-import React, {memo, useCallback, useEffect, useRef, useState} from 'react';
+import React, {useEffect, useRef, useState} from 'react';
import {PDFPreviewer} from 'react-fast-pdf';
import {View} from 'react-native';
import {useAnimatedReaction, useSharedValue} from 'react-native-reanimated';
@@ -172,16 +172,13 @@ function PDFView({onToggleKeyboard, fileName, onPress, isFocused, onScaleChanged
*
* @param isKBOpen True if keyboard is open
*/
- const toggleKeyboardOnSmallScreens = useCallback(
- (isKBOpen: boolean) => {
- if (!shouldUseNarrowLayout) {
- return;
- }
- setIsKeyboardOpen(isKBOpen);
- onToggleKeyboard?.(isKBOpen);
- },
- [shouldUseNarrowLayout, onToggleKeyboard],
- );
+ const toggleKeyboardOnSmallScreens = (isKBOpen: boolean) => {
+ if (!shouldUseNarrowLayout) {
+ return;
+ }
+ setIsKeyboardOpen(isKBOpen);
+ onToggleKeyboard?.(isKBOpen);
+ };
/**
* Verify that the canvas limits have been calculated already, if not calculate them and put them in Onyx
@@ -320,4 +317,4 @@ function PDFView({onToggleKeyboard, fileName, onPress, isFocused, onScaleChanged
);
}
-export default memo(PDFView);
+export default PDFView;
diff --git a/src/components/PercentageForm.tsx b/src/components/PercentageForm.tsx
index 9cfe6a287acb..e816d560e49d 100644
--- a/src/components/PercentageForm.tsx
+++ b/src/components/PercentageForm.tsx
@@ -7,7 +7,7 @@ import CONST from '@src/CONST';
import type {ForwardedRef} from 'react';
-import React, {useCallback, useMemo, useRef} from 'react';
+import React, {useRef} from 'react';
import type {BaseTextInputProps, BaseTextInputRef} from './TextInput/BaseTextInput/types';
@@ -55,28 +55,25 @@ function PercentageForm({
const textInput = useRef(null);
- const currentAmount = useMemo(() => (typeof amount === 'string' ? amount : ''), [amount]);
+ const currentAmount = typeof amount === 'string' ? amount : '';
/**
* Sets the amount according to the value passed to the input
* @param newAmount - Changed amount from user input
*/
- const setNewAmount = useCallback(
- (newAmount: string) => {
- // Remove spaces from the newAmount value because Safari on iOS adds spaces when pasting a copied value
- // More info: https://github.com/Expensify/App/issues/16974
- const newAmountWithoutSpaces = stripSpacesFromAmount(newAmount);
- const withLeadingZero = addLeadingZero(newAmountWithoutSpaces, allowNegative);
- if (!validatePercentage(withLeadingZero, allowExceedingHundred, allowDecimal, allowNegative)) {
- return;
- }
-
- // Convert comma to period for internal representation (commas are used as decimal separators in some locales like Spanish)
- const normalizedAmount = replaceCommasWithPeriod(newAmountWithoutSpaces);
- onInputChange?.(normalizedAmount);
- },
- [allowExceedingHundred, allowDecimal, allowNegative, onInputChange],
- );
+ const setNewAmount = (newAmount: string) => {
+ // Remove spaces from the newAmount value because Safari on iOS adds spaces when pasting a copied value
+ // More info: https://github.com/Expensify/App/issues/16974
+ const newAmountWithoutSpaces = stripSpacesFromAmount(newAmount);
+ const withLeadingZero = addLeadingZero(newAmountWithoutSpaces, allowNegative);
+ if (!validatePercentage(withLeadingZero, allowExceedingHundred, allowDecimal, allowNegative)) {
+ return;
+ }
+
+ // Convert comma to period for internal representation (commas are used as decimal separators in some locales like Spanish)
+ const normalizedAmount = replaceCommasWithPeriod(newAmountWithoutSpaces);
+ onInputChange?.(normalizedAmount);
+ };
const formattedAmount = replaceAllDigits(currentAmount, toLocaleDigit);
diff --git a/src/components/Picker/BasePicker.tsx b/src/components/Picker/BasePicker.tsx
index 7122675e9914..f0278dc0760b 100644
--- a/src/components/Picker/BasePicker.tsx
+++ b/src/components/Picker/BasePicker.tsx
@@ -17,7 +17,7 @@ import type {ComponentRef, ReactElement, ReactNode, RefObject} from 'react';
import type {ScrollView} from 'react-native';
import lodashDefer from 'lodash/defer';
-import React, {useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
+import React, {useEffect, useImperativeHandle, useRef, useState} from 'react';
import {View} from 'react-native';
import RNPickerSelect from 'react-native-picker-select';
@@ -104,19 +104,23 @@ function BasePickerImpl({
setIsHighlighted(false);
};
- const iconToRender = useMemo((): IconToRender => {
- if (icon) {
- return () => icon(size);
- }
-
- return () => (
+ let iconToRender: IconToRender;
+ if (icon) {
+ iconToRender = () => icon(size);
+ } else {
+ iconToRender = () => (
);
- }, [icon, size, styles, theme.icon, icons.DownArrow]);
+ }
useImperativeHandle(ref, () => ({
/**
@@ -169,15 +173,14 @@ function BasePickerImpl({
const selectedItem = items.find((item) => item.value === value);
const selectedLabel = selectedItem?.label ?? '';
const defaultAccessibilityLabel = accessibilityLabel ?? label ?? selectedLabel;
- const enhancedAccessibilityLabel = useMemo(() => {
- if (!defaultAccessibilityLabel) {
- return selectedLabel || '';
- }
- if (selectedLabel) {
- return `${defaultAccessibilityLabel}${shouldAnnounceSelectedLabel ? `, ${selectedLabel}` : ''}, ${translate(isHighlighted ? 'common.expanded' : 'common.collapsed')}`;
- }
- return defaultAccessibilityLabel;
- }, [defaultAccessibilityLabel, selectedLabel, shouldAnnounceSelectedLabel, translate, isHighlighted]);
+ let enhancedAccessibilityLabel;
+ if (!defaultAccessibilityLabel) {
+ enhancedAccessibilityLabel = selectedLabel || '';
+ } else {
+ enhancedAccessibilityLabel = selectedLabel
+ ? `${defaultAccessibilityLabel}${shouldAnnounceSelectedLabel ? `, ${selectedLabel}` : ''}, ${translate(isHighlighted ? 'common.expanded' : 'common.collapsed')}`
+ : defaultAccessibilityLabel;
+ }
if (isDisabled && shouldShowOnlyTextWhenDisabled) {
return (
diff --git a/src/components/PlaidLink/index.tsx b/src/components/PlaidLink/index.tsx
index 83757fb2c20e..5604cf8b26c4 100644
--- a/src/components/PlaidLink/index.tsx
+++ b/src/components/PlaidLink/index.tsx
@@ -9,7 +9,7 @@ import CONST from '@src/CONST';
import type {PlaidLinkOnSuccessMetadata} from 'react-plaid-link';
-import React, {useCallback, useEffect, useState} from 'react';
+import React, {useEffect, useState} from 'react';
import {View} from 'react-native';
import {usePlaidLink} from 'react-plaid-link';
@@ -28,13 +28,10 @@ function clearSafariHistoryState() {
function PlaidLink({token, onSuccess = () => {}, onError = () => {}, onExit = () => {}, onEvent, receivedRedirectURI}: PlaidLinkProps) {
const [isPlaidLoaded, setIsPlaidLoaded] = useState(false);
const styles = useThemeStyles();
- const successCallback = useCallback(
- (publicToken: string, metadata: PlaidLinkOnSuccessMetadata) => {
- clearSafariHistoryState();
- onSuccess({publicToken, metadata});
- },
- [onSuccess],
- );
+ const successCallback = (publicToken: string, metadata: PlaidLinkOnSuccessMetadata) => {
+ clearSafariHistoryState();
+ onSuccess({publicToken, metadata});
+ };
const {open, ready, error} = usePlaidLink({
token,
diff --git a/src/components/PopoverMenu/index.tsx b/src/components/PopoverMenu/index.tsx
index b771d516a4bf..ebca5649f7f6 100644
--- a/src/components/PopoverMenu/index.tsx
+++ b/src/components/PopoverMenu/index.tsx
@@ -42,7 +42,7 @@ import type {ComponentRef, ReactNode, RefObject} from 'react';
import type {GestureResponderEvent, LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent, StyleProp, TextStyle, ViewStyle} from 'react-native';
import {deepEqual} from 'fast-equals';
-import React, {useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react';
+import React, {useEffect, useLayoutEffect, useRef, useState} from 'react';
import {StyleSheet, View} from 'react-native';
import usePopoverMenuFocusManagement from './usePopoverMenuFocusManagement';
@@ -453,25 +453,20 @@ function BasePopoverMenu({
});
const expensifyIcons = useMemoizedLazyExpensifyIcons(['BackArrow', 'ReceiptScan', 'MoneyCircle']);
const prevMenuItems = usePrevious(menuItems);
- const {currentHeaderText, shouldAlwaysShowHeaderText} = useMemo(() => {
- let currentItems = menuItems;
- let currentHeader = headerText ?? '';
- let shouldShow = true;
-
- if (enteredSubMenuIndexes.length > 0) {
- for (const index of enteredSubMenuIndexes) {
- const item = currentItems.at(index);
- if (!item?.subMenuItems) {
- break;
- }
- currentHeader = item.subMenuHeaderText ?? '';
- shouldShow = !!item.subMenuHeaderText;
- currentItems = item.subMenuItems;
+ let headerMenuItems = menuItems;
+ let currentHeaderText = headerText ?? '';
+ let shouldAlwaysShowHeaderText = true;
+ if (enteredSubMenuIndexes.length > 0) {
+ for (const index of enteredSubMenuIndexes) {
+ const item = headerMenuItems.at(index);
+ if (!item?.subMenuItems) {
+ break;
}
+ currentHeaderText = item.subMenuHeaderText ?? '';
+ shouldAlwaysShowHeaderText = !!item.subMenuHeaderText;
+ headerMenuItems = item.subMenuItems;
}
-
- return {currentHeaderText: currentHeader, shouldAlwaysShowHeaderText: shouldShow};
- }, [enteredSubMenuIndexes, headerText, menuItems]);
+ }
const [hasKeyBeenPressed, setHasKeyBeenPressed] = useState(false);
const {
@@ -778,21 +773,31 @@ function BasePopoverMenu({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [menuItems, setFocusedIndex, isSearchEnabled]);
- const menuContainerStyle = useMemo(() => {
- if (isSmallScreenWidth) {
- return shouldEnableMaxHeight && !isInLandscapeMode ? [{maxHeight: CONST.POPOVER_MENU_MAX_HEIGHT_MOBILE}] : [];
- }
-
- const stylesArray: ViewStyle[] = [StyleSheet.flatten(styles.createMenuContainer), {width: variables.compactPopoverMenuWidth}, styles.pv2];
-
+ let menuContainerStyle;
+ if (isSmallScreenWidth) {
+ menuContainerStyle =
+ shouldEnableMaxHeight && !isInLandscapeMode
+ ? [
+ {
+ maxHeight: CONST.POPOVER_MENU_MAX_HEIGHT_MOBILE,
+ },
+ ]
+ : [];
+ } else {
+ const stylesArray: ViewStyle[] = [
+ StyleSheet.flatten(styles.createMenuContainer),
+ {
+ width: variables.compactPopoverMenuWidth,
+ },
+ styles.pv2,
+ ];
if (shouldUseScrollView && shouldEnableMaxHeight && !isInLandscapeMode) {
stylesArray.push({
maxHeight: Math.max(windowHeight - variables.compactPopoverMenuVerticalMargin, CONST.POPOVER_MENU_MAX_HEIGHT),
});
}
-
- return stylesArray;
- }, [isSmallScreenWidth, shouldEnableMaxHeight, styles.createMenuContainer, styles.pv2, shouldUseScrollView, windowHeight, isInLandscapeMode]);
+ menuContainerStyle = stylesArray;
+ }
const {paddingTop, paddingBottom, paddingVertical, ...restScrollContainerStyle} =
(StyleSheet.flatten([isSmallScreenWidth ? styles.pv4 : styles.pv2, isSearchEnabled && styles.pt0, scrollContainerStyle]) as ViewStyle) ?? {};
@@ -810,24 +815,11 @@ function BasePopoverMenu({
...restContainerStyles
} = StyleSheet.flatten(containerStyles) ?? {};
- const scrollViewPaddingStyles = useMemo(
- () => ({
- paddingTop: paddingTop ?? containerPaddingTop ?? menuContainerPaddingTop,
- paddingBottom: paddingBottom ?? containerPaddingBottom ?? menuContainerPaddingBottom,
- paddingVertical: paddingVertical ?? containerPaddingVertical ?? menuContainerPaddingVertical ?? 0,
- }),
- [
- paddingTop,
- containerPaddingTop,
- menuContainerPaddingTop,
- paddingBottom,
- containerPaddingBottom,
- menuContainerPaddingBottom,
- paddingVertical,
- containerPaddingVertical,
- menuContainerPaddingVertical,
- ],
- );
+ const scrollViewPaddingStyles = {
+ paddingTop: paddingTop ?? containerPaddingTop ?? menuContainerPaddingTop,
+ paddingBottom: paddingBottom ?? containerPaddingBottom ?? menuContainerPaddingBottom,
+ paddingVertical: paddingVertical ?? containerPaddingVertical ?? menuContainerPaddingVertical ?? 0,
+ };
return (
- // eslint-disable-next-line rulesdir/no-deep-equal-in-memo -- menuItems array is created inline in most usages with unstable references
- deepEqual(prevProps.menuItems, nextProps.menuItems) &&
- prevProps.isVisible === nextProps.isVisible &&
- // eslint-disable-next-line rulesdir/no-deep-equal-in-memo -- anchorPosition object is created inline in most usages
- deepEqual(prevProps.anchorPosition, nextProps.anchorPosition) &&
- prevProps.anchorRef === nextProps.anchorRef &&
- prevProps.headerText === nextProps.headerText &&
- prevProps.searchInputOptions?.label === nextProps.searchInputOptions?.label &&
- prevProps.searchInputOptions?.value === nextProps.searchInputOptions?.value &&
- prevProps.searchInputOptions?.onChangeText === nextProps.searchInputOptions?.onChangeText &&
- prevProps.searchInputOptions?.shouldShowEmptyState === nextProps.searchInputOptions?.shouldShowEmptyState &&
- prevProps.searchInputOptions?.style === nextProps.searchInputOptions?.style &&
- prevProps.fromSidebarMediumScreen === nextProps.fromSidebarMediumScreen &&
- // eslint-disable-next-line rulesdir/no-deep-equal-in-memo -- anchorAlignment object is created inline in most usages
- deepEqual(prevProps.anchorAlignment, nextProps.anchorAlignment) &&
- prevProps.shouldSwitchPositionIfOverflow === nextProps.shouldSwitchPositionIfOverflow &&
- prevProps.animationIn === nextProps.animationIn &&
- prevProps.animationOut === nextProps.animationOut &&
- prevProps.animationInTiming === nextProps.animationInTiming &&
- prevProps.disableAnimation === nextProps.disableAnimation &&
- prevProps.withoutOverlay === nextProps.withoutOverlay &&
- prevProps.shouldEnableNewFocusManagement === nextProps.shouldEnableNewFocusManagement &&
- prevProps.shouldReturnFocus === nextProps.shouldReturnFocus &&
- prevProps.restoreFocusType === nextProps.restoreFocusType &&
- prevProps.shouldSetModalVisibility === nextProps.shouldSetModalVisibility &&
- prevProps.shouldPutHeaderTextAfterBackButton === nextProps.shouldPutHeaderTextAfterBackButton,
-);
+export default PopoverMenu;
export type {PopoverMenuItem, PopoverMenuProps};
export {getItemKey, buildKeyPathFromIndexPath, resolveIndexPathByKeyPath};
diff --git a/src/components/PopoverProvider/index.tsx b/src/components/PopoverProvider/index.tsx
index 297c64992540..33fdde5a47b8 100644
--- a/src/components/PopoverProvider/index.tsx
+++ b/src/components/PopoverProvider/index.tsx
@@ -1,6 +1,6 @@
import type {RefObject} from 'react';
-import React, {createContext, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
+import React, {createContext, useContext, useEffect, useRef, useState} from 'react';
import type {AnchorRef, AnchorType, PopoverContextProps} from './types';
@@ -44,7 +44,7 @@ function PopoverContextProvider(props: PopoverContextProps) {
const [activePopoverAnchor, setActivePopoverAnchor] = useState(null);
const [activePopoverExtraAnchorRefs, setActivePopoverExtraAnchorRefs] = useState([]);
- const closePopover = useCallback((anchorRef?: RefObject): boolean => {
+ const closePopover = (anchorRef?: RefObject): boolean => {
if (!activePopoverRef.current || (anchorRef && anchorRef !== activePopoverRef.current.anchorRef)) {
return false;
}
@@ -55,7 +55,7 @@ function PopoverContextProvider(props: PopoverContextProps) {
setIsOpen(false);
setActivePopoverAnchor(null);
return true;
- }, []);
+ };
useEffect(() => {
const listener = (e: Event) => {
@@ -131,21 +131,18 @@ function PopoverContextProvider(props: PopoverContextProps) {
};
}, [closePopover]);
- const onOpen = useCallback(
- (popoverParams: AnchorRef) => {
- if (activePopoverRef.current && activePopoverRef.current.ref !== popoverParams?.ref) {
- closePopover(activePopoverRef.current.anchorRef);
- }
- activePopoverRef.current = popoverParams;
- setActivePopover(popoverParams);
- setActivePopoverAnchor(popoverParams.anchorRef.current);
- setIsOpen(true);
- },
- [closePopover],
- );
+ const onOpen = (popoverParams: AnchorRef) => {
+ if (activePopoverRef.current && activePopoverRef.current.ref !== popoverParams?.ref) {
+ closePopover(activePopoverRef.current.anchorRef);
+ }
+ activePopoverRef.current = popoverParams;
+ setActivePopover(popoverParams);
+ setActivePopoverAnchor(popoverParams.anchorRef.current);
+ setIsOpen(true);
+ };
// To set the extra anchor refs for the popover when prop-drilling is not possible
- const setActivePopoverExtraAnchorRef = useCallback((extraAnchorRef?: RefObject) => {
+ const setActivePopoverExtraAnchorRef = (extraAnchorRef?: RefObject) => {
if (!extraAnchorRef) {
return;
}
@@ -159,25 +156,19 @@ function PopoverContextProvider(props: PopoverContextProps) {
}
return [...prev, extraAnchorRef];
});
- }, []);
-
- const actionsContextValue = useMemo(
- () => ({
- onOpen,
- close: closePopover,
- setActivePopoverExtraAnchorRef,
- }),
- [onOpen, closePopover, setActivePopoverExtraAnchorRef],
- );
-
- const stateContextValue = useMemo(
- () => ({
- isOpen,
- popover: activePopover,
- popoverAnchor: activePopoverAnchor,
- }),
- [isOpen, activePopover, activePopoverAnchor],
- );
+ };
+
+ const actionsContextValue: PopoverActionsContextType = {
+ onOpen,
+ close: closePopover,
+ setActivePopoverExtraAnchorRef,
+ };
+
+ const stateContextValue: PopoverStateContextType = {
+ isOpen,
+ popover: activePopover,
+ popoverAnchor: activePopoverAnchor,
+ };
return (
diff --git a/src/components/PopoverWithMeasuredContent/PopoverWithMeasuredContentBase.tsx b/src/components/PopoverWithMeasuredContent/PopoverWithMeasuredContentBase.tsx
index 9e32ffa57ae5..7a62ce15a88a 100644
--- a/src/components/PopoverWithMeasuredContent/PopoverWithMeasuredContentBase.tsx
+++ b/src/components/PopoverWithMeasuredContent/PopoverWithMeasuredContentBase.tsx
@@ -14,8 +14,8 @@ import CONST from '@src/CONST';
import type {LayoutChangeEvent} from 'react-native';
-import {circularDeepEqual, deepEqual} from 'fast-equals';
-import React, {useEffect, useMemo, useState} from 'react';
+import {deepEqual} from 'fast-equals';
+import React, {useEffect, useState} from 'react';
import {View} from 'react-native';
import type PopoverWithMeasuredContentProps from './types';
@@ -70,7 +70,7 @@ function PopoverWithMeasuredContentBase({
const prevWindowDimensions = usePrevious({windowWidth, windowHeight});
const hasStaticDimensions = popoverDimensions.width > 0 && popoverDimensions.height > 0;
- const modalId = useMemo(() => ComposerFocusManager.getId(), []);
+ const modalId = ComposerFocusManager.getId();
// Static dimensions skip `onLayout`, so sync height changes from the prop.
if (hasStaticDimensions && popoverHeight !== popoverDimensions.height) {
@@ -124,92 +124,82 @@ function PopoverWithMeasuredContentBase({
}
};
- const adjustedAnchorPosition = useMemo(() => {
- let horizontalConstraint;
- switch (anchorAlignment.horizontal) {
- case CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT:
- horizontalConstraint = {left: anchorPosition.horizontal - popoverWidth};
- break;
- case CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.CENTER:
- horizontalConstraint = {
- left: Math.floor(anchorPosition.horizontal - popoverWidth / 2),
- };
- break;
- case CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT:
- default:
- horizontalConstraint = {left: anchorPosition.horizontal};
- }
-
- let verticalConstraint;
- switch (anchorAlignment.vertical) {
- case CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM:
- verticalConstraint = {top: anchorPosition.vertical - popoverHeight};
- break;
- case CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.CENTER:
- verticalConstraint = {
- top: Math.floor(anchorPosition.vertical - popoverHeight / 2),
- };
- break;
- case CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP:
- default:
- verticalConstraint = {top: anchorPosition.vertical};
- }
- return {
- ...horizontalConstraint,
- ...verticalConstraint,
- };
- }, [anchorPosition.horizontal, anchorPosition.vertical, anchorAlignment.horizontal, anchorAlignment.vertical, popoverWidth, popoverHeight]);
-
- const positionCalculations = useMemo(() => {
- const horizontalShift = PopoverWithMeasuredContentUtils.computeHorizontalShift(adjustedAnchorPosition.left, popoverWidth, windowWidth);
- const verticalShift = PopoverWithMeasuredContentUtils.computeVerticalShift(
- adjustedAnchorPosition.top,
- popoverHeight,
- windowHeight,
- anchorDimensions.height,
- shouldSwitchPositionIfOverflow,
- );
- return {horizontalShift, verticalShift};
- }, [adjustedAnchorPosition.left, adjustedAnchorPosition.top, popoverWidth, popoverHeight, windowWidth, windowHeight, anchorDimensions.height, shouldSwitchPositionIfOverflow]);
-
- const shiftedAnchorPosition: PopoverAnchorPosition = useMemo(() => {
- const result: PopoverAnchorPosition = {
- left: adjustedAnchorPosition.left + positionCalculations.horizontalShift,
- };
-
- if (shouldMeasureAnchorPositionFromTop) {
- result.top = adjustedAnchorPosition.top + positionCalculations.verticalShift;
- }
-
- if (anchorAlignment.vertical === CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP) {
- const top = adjustedAnchorPosition.top + positionCalculations.verticalShift;
- const maxTop = windowHeight - popoverHeight - positionCalculations.verticalShift;
- let clampedTop = Math.min(Math.max(positionCalculations.verticalShift, top), maxTop);
- if (windowMargin > 0) {
- clampedTop = Math.min(Math.max(clampedTop, windowMargin), Math.max(windowHeight - popoverHeight - windowMargin, windowMargin));
- }
- result.top = clampedTop;
- }
-
- if (anchorAlignment.vertical === CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM) {
- const bottom = windowHeight - (adjustedAnchorPosition.top + popoverHeight) - positionCalculations.verticalShift;
- // Keep the top edge inside the window when the popover is taller than the space above the anchor.
- const maxBottom = windowHeight - popoverHeight - windowMargin;
- result.bottom = windowMargin > 0 ? Math.min(Math.max(bottom, windowMargin), Math.max(maxBottom, windowMargin)) : Math.min(bottom, maxBottom);
- }
+ let horizontalConstraint;
+ switch (anchorAlignment.horizontal) {
+ case CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT:
+ horizontalConstraint = {
+ left: anchorPosition.horizontal - popoverWidth,
+ };
+ break;
+ case CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.CENTER:
+ horizontalConstraint = {
+ left: Math.floor(anchorPosition.horizontal - popoverWidth / 2),
+ };
+ break;
+ case CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT:
+ default:
+ horizontalConstraint = {
+ left: anchorPosition.horizontal,
+ };
+ }
+ let verticalConstraint;
+ switch (anchorAlignment.vertical) {
+ case CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM:
+ verticalConstraint = {
+ top: anchorPosition.vertical - popoverHeight,
+ };
+ break;
+ case CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.CENTER:
+ verticalConstraint = {
+ top: Math.floor(anchorPosition.vertical - popoverHeight / 2),
+ };
+ break;
+ case CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP:
+ default:
+ verticalConstraint = {
+ top: anchorPosition.vertical,
+ };
+ }
+ const adjustedAnchorPosition = {
+ ...horizontalConstraint,
+ ...verticalConstraint,
+ };
- return result;
- }, [
- adjustedAnchorPosition.left,
+ const horizontalShift = PopoverWithMeasuredContentUtils.computeHorizontalShift(adjustedAnchorPosition.left, popoverWidth, windowWidth);
+ const verticalShift = PopoverWithMeasuredContentUtils.computeVerticalShift(
adjustedAnchorPosition.top,
- positionCalculations.horizontalShift,
- positionCalculations.verticalShift,
- anchorAlignment.vertical,
- windowHeight,
popoverHeight,
- shouldMeasureAnchorPositionFromTop,
- windowMargin,
- ]);
+ windowHeight,
+ anchorDimensions.height,
+ shouldSwitchPositionIfOverflow,
+ );
+ const positionCalculations = {
+ horizontalShift,
+ verticalShift,
+ };
+
+ const result: PopoverAnchorPosition = {
+ left: adjustedAnchorPosition.left + positionCalculations.horizontalShift,
+ };
+ if (shouldMeasureAnchorPositionFromTop) {
+ result.top = adjustedAnchorPosition.top + positionCalculations.verticalShift;
+ }
+ if (anchorAlignment.vertical === CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP) {
+ const top = adjustedAnchorPosition.top + positionCalculations.verticalShift;
+ const maxTop = windowHeight - popoverHeight - positionCalculations.verticalShift;
+ let clampedTop = Math.min(Math.max(positionCalculations.verticalShift, top), maxTop);
+ if (windowMargin > 0) {
+ clampedTop = Math.min(Math.max(clampedTop, windowMargin), Math.max(windowHeight - popoverHeight - windowMargin, windowMargin));
+ }
+ result.top = clampedTop;
+ }
+ if (anchorAlignment.vertical === CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM) {
+ const bottom = windowHeight - (adjustedAnchorPosition.top + popoverHeight) - positionCalculations.verticalShift;
+ // Keep the top edge inside the window when the popover is taller than the space above the anchor.
+ const maxBottom = windowHeight - popoverHeight - windowMargin;
+ result.bottom = windowMargin > 0 ? Math.min(Math.max(bottom, windowMargin), Math.max(maxBottom, windowMargin)) : Math.min(bottom, maxBottom);
+ }
+ const shiftedAnchorPosition: PopoverAnchorPosition = result;
return isContentMeasured ? (
{
- if (prevProps.isVisible === nextProps.isVisible && nextProps.isVisible === false) {
- return true;
- }
- return circularDeepEqual(prevProps, nextProps);
-});
+export default PopoverWithMeasuredContentBase;
diff --git a/src/components/PopoverWithMeasuredContent/index.tsx b/src/components/PopoverWithMeasuredContent/index.tsx
index bc2882f52c9f..aa8943166624 100644
--- a/src/components/PopoverWithMeasuredContent/index.tsx
+++ b/src/components/PopoverWithMeasuredContent/index.tsx
@@ -4,7 +4,6 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout';
import CONST from '@src/CONST';
-import {circularDeepEqual} from 'fast-equals';
import React, {useEffect, useState, useTransition} from 'react';
import type PopoverWithMeasuredContentProps from './types';
@@ -56,9 +55,4 @@ function PopoverWithMeasuredContent({shouldWrapModalChildrenInScrollViewIfBottom
PopoverWithMeasuredContent.displayName = 'PopoverWithMeasuredContent';
-export default React.memo(PopoverWithMeasuredContent, (prevProps, nextProps) => {
- if (prevProps.isVisible === nextProps.isVisible && nextProps.isVisible === false) {
- return true;
- }
- return circularDeepEqual(prevProps, nextProps);
-});
+export default PopoverWithMeasuredContent;
diff --git a/src/components/PopoverWithoutOverlay/index.tsx b/src/components/PopoverWithoutOverlay/index.tsx
index ea03d6b019de..e78a0d0b1c5c 100644
--- a/src/components/PopoverWithoutOverlay/index.tsx
+++ b/src/components/PopoverWithoutOverlay/index.tsx
@@ -12,7 +12,7 @@ import {onModalDidClose, setCloseModal, willAlertModalBecomeVisible} from '@libs
import CONST from '@src/CONST';
import viewRef from '@src/types/utils/viewRef';
-import React, {useEffect, useMemo} from 'react';
+import React, {useEffect} from 'react';
import {View} from 'react-native';
import type PopoverWithoutOverlayProps from './types';
@@ -81,27 +81,14 @@ function PopoverWithoutOverlay({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isVisible]);
- const modalPaddingStyles = useMemo(
- () =>
- StyleUtils.getModalPaddingStyles({
- shouldAddBottomSafeAreaMargin,
- shouldAddTopSafeAreaMargin,
- shouldAddBottomSafeAreaPadding: enableEdgeToEdgeBottomSafeAreaPadding === undefined && shouldAddBottomSafeAreaPadding,
- shouldAddTopSafeAreaPadding,
- modalContainerStyle,
- insets,
- }),
- [
- StyleUtils,
- enableEdgeToEdgeBottomSafeAreaPadding,
- insets,
- modalContainerStyle,
- shouldAddBottomSafeAreaMargin,
- shouldAddBottomSafeAreaPadding,
- shouldAddTopSafeAreaMargin,
- shouldAddTopSafeAreaPadding,
- ],
- );
+ const modalPaddingStyles = StyleUtils.getModalPaddingStyles({
+ shouldAddBottomSafeAreaMargin,
+ shouldAddTopSafeAreaMargin,
+ shouldAddBottomSafeAreaPadding: enableEdgeToEdgeBottomSafeAreaPadding === undefined && shouldAddBottomSafeAreaPadding,
+ shouldAddTopSafeAreaPadding,
+ modalContainerStyle,
+ insets,
+ });
if (!isVisible) {
return null;
diff --git a/src/components/PriorityModeController.tsx b/src/components/PriorityModeController.tsx
index 92b447b71099..7d8060d969b6 100644
--- a/src/components/PriorityModeController.tsx
+++ b/src/components/PriorityModeController.tsx
@@ -23,7 +23,7 @@ import type {OnyxEntry} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import {useNavigation} from '@react-navigation/native';
-import React, {useCallback, useEffect, useRef, useState} from 'react';
+import React, {useEffect, useRef, useState} from 'react';
import {View} from 'react-native';
import RenderHTML from './RenderHTML';
@@ -60,7 +60,7 @@ export default function PriorityModeController() {
const hasSwitched = useRef(false);
const priorityModePageUrl = `${environmentURL}/settings/preferences/priority-mode`;
- const handleShowModal = useCallback(() => {
+ const handleShowModal = () => {
if (isModalShown.current) {
return;
}
@@ -81,7 +81,7 @@ export default function PriorityModeController() {
}).then(() => {
isModalShown.current = false;
});
- }, [StyleUtils, illustrations.ThreeLeggedLaptopWoman, priorityModePageUrl, showConfirmModal, styles, translate]);
+ };
// Listen for state changes and trigger the #focus mode when appropriate
useEffect(() => {
diff --git a/src/components/ProductTrainingContext/index.tsx b/src/components/ProductTrainingContext/index.tsx
index b9db8eaef24b..f81e67e5e030 100644
--- a/src/components/ProductTrainingContext/index.tsx
+++ b/src/components/ProductTrainingContext/index.tsx
@@ -23,7 +23,7 @@ import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
import {hasCompletedGuidedSetupFlowSelector} from '@selectors/Onboarding';
import {emailSelector} from '@selectors/Session';
-import React, {createContext, useCallback, useContext, useEffect, useMemo, useState} from 'react';
+import React, {createContext, useContext, useEffect, useState} from 'react';
import {View} from 'react-native';
import type {ProductTrainingTooltipName} from './TOOLTIPS';
@@ -61,19 +61,11 @@ function ProductTrainingContextProvider({children}: ChildrenProps) {
const [currentUserLogin, currentUserLoginMetadata] = useOnyx(ONYXKEYS.SESSION, {selector: emailSelector});
const shouldSuppressPromotionalUI = useShouldSuppressPromotionalUI();
- const isUserPolicyEmployee = useMemo(() => {
- if (!allPolicies || !currentUserLogin || isLoadingOnyxValue(allPoliciesMetadata, currentUserLoginMetadata)) {
- return false;
- }
- return getActiveEmployeeWorkspaces(allPolicies, currentUserLogin).length > 0;
- }, [allPolicies, currentUserLogin, allPoliciesMetadata, currentUserLoginMetadata]);
+ const isUserPolicyEmployee =
+ !(!allPolicies || !currentUserLogin || isLoadingOnyxValue(allPoliciesMetadata, currentUserLoginMetadata)) && getActiveEmployeeWorkspaces(allPolicies, currentUserLogin).length > 0;
- const isUserPolicyAdmin = useMemo(() => {
- if (!allPolicies || !currentUserLogin || isLoadingOnyxValue(allPoliciesMetadata, currentUserLoginMetadata)) {
- return false;
- }
- return getActiveAdminWorkspaces(allPolicies, currentUserLogin).length > 0;
- }, [allPolicies, currentUserLogin, allPoliciesMetadata, currentUserLoginMetadata]);
+ const isUserPolicyAdmin =
+ !(!allPolicies || !currentUserLogin || isLoadingOnyxValue(allPoliciesMetadata, currentUserLoginMetadata)) && getActiveAdminWorkspaces(allPolicies, currentUserLogin).length > 0;
const [dismissedProductTraining] = useOnyx(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING);
@@ -84,18 +76,15 @@ function ProductTrainingContextProvider({children}: ChildrenProps) {
const [activeTooltips, setActiveTooltips] = useState>(new Set());
- const unregisterTooltip = useCallback(
- (tooltipName: ProductTrainingTooltipName) => {
- setActiveTooltips((prev) => {
- const next = new Set(prev);
- next.delete(tooltipName);
- return next;
- });
- },
- [setActiveTooltips],
- );
-
- const determineVisibleTooltip = useCallback(() => {
+ const unregisterTooltip = (tooltipName: ProductTrainingTooltipName) => {
+ setActiveTooltips((prev) => {
+ const next = new Set(prev);
+ next.delete(tooltipName);
+ return next;
+ });
+ };
+
+ const determineVisibleTooltip = () => {
if (activeTooltips.size === 0) {
return null;
}
@@ -114,122 +103,93 @@ function ProductTrainingContextProvider({children}: ChildrenProps) {
}
return highestPriorityTooltip.name;
- }, [activeTooltips]);
+ };
- const isUserInPaidPolicy = useMemo(() => {
- if (!allPolicies || !currentUserLogin || isLoadingOnyxValue(allPoliciesMetadata, currentUserLoginMetadata)) {
+ const isUserInPaidPolicy = !(!allPolicies || !currentUserLogin || isLoadingOnyxValue(allPoliciesMetadata, currentUserLoginMetadata)) && hasAnyPaidPolicy(allPolicies);
+
+ const shouldTooltipBeVisible = (tooltipName: ProductTrainingTooltipName) => {
+ if (isLoadingOnyxValue(isOnboardingCompletedMetadata) || isLoadingApp) {
return false;
}
- return hasAnyPaidPolicy(allPolicies);
- }, [allPolicies, currentUserLogin, allPoliciesMetadata, currentUserLoginMetadata]);
-
- const shouldTooltipBeVisible = useCallback(
- (tooltipName: ProductTrainingTooltipName) => {
- if (isLoadingOnyxValue(isOnboardingCompletedMetadata) || isLoadingApp) {
- return false;
- }
-
- const tooltipConfig = TOOLTIPS[tooltipName];
-
- const isDismissed = isProductTrainingElementDismissed(tooltipName, dismissedProductTraining);
-
- // Tooltips with a reappear window are shown again once that long has passed since the last dismissal.
- const canReappear = !!tooltipConfig.reappearsAfterMs && hasDismissalExpired(tooltipName, dismissedProductTraining, tooltipConfig.reappearsAfterMs);
-
- if (isDismissed && !canReappear) {
- return false;
- }
-
- // if hasBeenAddedToNudgeMigration is true, and welcome modal is not dismissed, don't show tooltip
- if (hasBeenAddedToNudgeMigration && !dismissedProductTraining?.[CONST.MIGRATED_USER_WELCOME_MODAL]) {
- return false;
- }
- if (isOnboardingCompleted === false) {
- return false;
- }
-
- // Mileage rate tooltip is exempt from the general "hide when modal visible" rule so it can show on the confirmation surface.
- // Hide it when a popover or bottom sheet opens (e.g. inline date picker) since those set modal.isPopover.
- if (tooltipName === CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.MILEAGE_RATE_AUTO_UPDATED && isModalVisible && modal?.isPopover) {
- return false;
- }
-
- // We need to make an exception for these tooltips because it is shown in a modal, otherwise it would be hidden if a modal is visible
- if (
- tooltipName !== CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.SCAN_TEST_DRIVE_CONFIRMATION &&
- tooltipName !== CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.GPS_TOOLTIP &&
- tooltipName !== CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.HAS_FILTER_NEGATION &&
- tooltipName !== CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.MILEAGE_RATE_AUTO_UPDATED &&
- isModalVisible
- ) {
- return false;
- }
-
- return tooltipConfig.shouldShow({
- shouldUseNarrowLayout,
- isUserPolicyEmployee,
- isUserPolicyAdmin,
- hasBeenAddedToNudgeMigration,
- isUserInPaidPolicy,
- });
- },
- [
- isOnboardingCompletedMetadata,
- isLoadingApp,
- dismissedProductTraining,
- hasBeenAddedToNudgeMigration,
- isOnboardingCompleted,
- isModalVisible,
- modal?.isPopover,
+
+ const tooltipConfig = TOOLTIPS[tooltipName];
+
+ const isDismissed = isProductTrainingElementDismissed(tooltipName, dismissedProductTraining);
+
+ // Tooltips with a reappear window are shown again once that long has passed since the last dismissal.
+ const canReappear = !!tooltipConfig.reappearsAfterMs && hasDismissalExpired(tooltipName, dismissedProductTraining, tooltipConfig.reappearsAfterMs);
+
+ if (isDismissed && !canReappear) {
+ return false;
+ }
+
+ // if hasBeenAddedToNudgeMigration is true, and welcome modal is not dismissed, don't show tooltip
+ if (hasBeenAddedToNudgeMigration && !dismissedProductTraining?.[CONST.MIGRATED_USER_WELCOME_MODAL]) {
+ return false;
+ }
+ if (isOnboardingCompleted === false) {
+ return false;
+ }
+
+ // Mileage rate tooltip is exempt from the general "hide when modal visible" rule so it can show on the confirmation surface.
+ // Hide it when a popover or bottom sheet opens (e.g. inline date picker) since those set modal.isPopover.
+ if (tooltipName === CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.MILEAGE_RATE_AUTO_UPDATED && isModalVisible && modal?.isPopover) {
+ return false;
+ }
+
+ // We need to make an exception for these tooltips because it is shown in a modal, otherwise it would be hidden if a modal is visible
+ if (
+ tooltipName !== CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.SCAN_TEST_DRIVE_CONFIRMATION &&
+ tooltipName !== CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.GPS_TOOLTIP &&
+ tooltipName !== CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.HAS_FILTER_NEGATION &&
+ tooltipName !== CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.MILEAGE_RATE_AUTO_UPDATED &&
+ isModalVisible
+ ) {
+ return false;
+ }
+
+ return tooltipConfig.shouldShow({
shouldUseNarrowLayout,
isUserPolicyEmployee,
isUserPolicyAdmin,
+ hasBeenAddedToNudgeMigration,
isUserInPaidPolicy,
- ],
- );
-
- const registerTooltip = useCallback(
- (tooltipName: ProductTrainingTooltipName) => {
- const shouldRegister = shouldTooltipBeVisible(tooltipName);
- if (!shouldRegister) {
- return;
- }
- setActiveTooltips((prev) => new Set([...prev, tooltipName]));
- },
- [shouldTooltipBeVisible],
- );
-
- const shouldRenderTooltip = useCallback(
- (tooltipName: ProductTrainingTooltipName) => {
- // Supportal agents and copilots should not see product-training tooltips on behalf of another account.
- if (shouldSuppressPromotionalUI) {
- return false;
- }
- // First check base conditions
- const shouldShow = shouldTooltipBeVisible(tooltipName);
- if (!shouldShow) {
- return false;
- }
- const visibleTooltip = determineVisibleTooltip();
-
- // If this is the highest priority visible tooltip, show it
- if (tooltipName === visibleTooltip) {
- return true;
- }
+ });
+ };
+
+ const registerTooltip = (tooltipName: ProductTrainingTooltipName) => {
+ const shouldRegister = shouldTooltipBeVisible(tooltipName);
+ if (!shouldRegister) {
+ return;
+ }
+ setActiveTooltips((prev) => new Set([...prev, tooltipName]));
+ };
+ const shouldRenderTooltip = (tooltipName: ProductTrainingTooltipName) => {
+ // Supportal agents and copilots should not see product-training tooltips on behalf of another account.
+ if (shouldSuppressPromotionalUI) {
return false;
- },
- [shouldSuppressPromotionalUI, shouldTooltipBeVisible, determineVisibleTooltip],
- );
-
- const contextValue = useMemo(
- () => ({
- shouldRenderTooltip,
- registerTooltip,
- unregisterTooltip,
- }),
- [shouldRenderTooltip, registerTooltip, unregisterTooltip],
- );
+ }
+ // First check base conditions
+ const shouldShow = shouldTooltipBeVisible(tooltipName);
+ if (!shouldShow) {
+ return false;
+ }
+ const visibleTooltip = determineVisibleTooltip();
+
+ // If this is the highest priority visible tooltip, show it
+ if (tooltipName === visibleTooltip) {
+ return true;
+ }
+
+ return false;
+ };
+
+ const contextValue = {
+ shouldRenderTooltip,
+ registerTooltip,
+ unregisterTooltip,
+ };
return {children};
}
@@ -258,23 +218,18 @@ const useProductTrainingContext = (tooltipName: ProductTrainingTooltipName, shou
};
}, [tooltipName, registerTooltip, unregisterTooltip, shouldShow]);
- const shouldShowProductTrainingTooltip = useMemo(() => {
- return shouldShow && shouldRenderTooltip(tooltipName) && !shouldHideToolTip;
- }, [shouldRenderTooltip, tooltipName, shouldShow, shouldHideToolTip]);
-
- const hideTooltip = useCallback(
- (isDismissedUsingCloseButton = false) => {
- if (!shouldShowProductTrainingTooltip) {
- return;
- }
- const tooltip = TOOLTIPS[tooltipName];
- tooltip.onHideTooltip(isDismissedUsingCloseButton);
- unregisterTooltip(tooltipName);
- },
- [tooltipName, shouldShowProductTrainingTooltip, unregisterTooltip],
- );
+ const shouldShowProductTrainingTooltip = shouldShow && shouldRenderTooltip(tooltipName) && !shouldHideToolTip;
- const renderProductTrainingTooltip = useCallback(() => {
+ const hideTooltip = (isDismissedUsingCloseButton = false) => {
+ if (!shouldShowProductTrainingTooltip) {
+ return;
+ }
+ const tooltip = TOOLTIPS[tooltipName];
+ tooltip.onHideTooltip(isDismissedUsingCloseButton);
+ unregisterTooltip(tooltipName);
+ };
+
+ const renderProductTrainingTooltip = () => {
const tooltip = TOOLTIPS[tooltipName];
const content = typeof tooltip.content === 'function' ? tooltip.content() : tooltip.content;
@@ -309,30 +264,11 @@ const useProductTrainingContext = (tooltipName: ProductTrainingTooltipName, shou
);
- }, [
- tooltipName,
- styles.alignItemsCenter,
- styles.flexRow,
- styles.justifyContentCenter,
- styles.textAlignCenter,
- styles.gap3,
- styles.pv2,
- styles.ph2,
- styles.renderHTML,
- styles.dFlex,
- styles.flexShrink1,
- theme.tooltipHighlightText,
- theme.icon,
- translate,
- config.onShown,
- hideTooltip,
- expensifyIcons.Close,
- expensifyIcons.Lightbulb,
- ]);
-
- const hideProductTrainingTooltip = useCallback(() => {
+ };
+
+ const hideProductTrainingTooltip = () => {
hideTooltip(false);
- }, [hideTooltip]);
+ };
return {
renderProductTrainingTooltip,
diff --git a/src/components/PushRowWithModal/PushRowModal.tsx b/src/components/PushRowWithModal/PushRowModal.tsx
index 098c21b91cde..00f833d1b6f7 100644
--- a/src/components/PushRowWithModal/PushRowModal.tsx
+++ b/src/components/PushRowWithModal/PushRowModal.tsx
@@ -15,7 +15,7 @@ import StringUtils from '@libs/StringUtils';
import CONST from '@src/CONST';
-import React, {useMemo} from 'react';
+import React from 'react';
type PushRowModalProps = {
isVisible: boolean;
@@ -52,17 +52,13 @@ function PushRowModal({isVisible, selectedOptions, canSelectMultiple = false, on
const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState('');
const initialSelectedValues = useInitialSelection(selectedOptions, {isVisible});
- const options = useMemo(
- () =>
- Object.entries(optionsList).map(([key, value]) => ({
- value: key,
- text: value,
- keyForList: key,
- isSelected: selectedOptions.includes(key),
- searchValue: StringUtils.sanitizeString(value),
- })),
- [optionsList, selectedOptions],
- );
+ const options = Object.entries(optionsList).map(([key, value]) => ({
+ value: key,
+ text: value,
+ keyForList: key,
+ isSelected: selectedOptions.includes(key),
+ searchValue: StringUtils.sanitizeString(value),
+ }));
const orderedOptions = moveInitialSelectionToTop(options, initialSelectedValues);
@@ -80,15 +76,12 @@ function PushRowModal({isVisible, selectedOptions, canSelectMultiple = false, on
const searchResults = searchOptions(debouncedSearchValue, debouncedSearchValue ? options : orderedOptions);
- const textInputOptions = useMemo(
- () => ({
- headerMessage: debouncedSearchValue.trim() && !searchResults.length ? translate('common.noResultsFound') : '',
- label: searchInputTitle,
- value: searchValue,
- onChangeText: setSearchValue,
- }),
- [debouncedSearchValue, searchInputTitle, searchResults.length, searchValue, setSearchValue, translate],
- );
+ const textInputOptions = {
+ headerMessage: debouncedSearchValue.trim() && !searchResults.length ? translate('common.noResultsFound') : '',
+ label: searchInputTitle,
+ value: searchValue,
+ onChangeText: setSearchValue,
+ };
return (
{
- 'worklet';
+ const parserWorklet = (text: string) => {
+ 'worklet';
- if (parser) {
- return parser(text);
- }
+ if (parser) {
+ return parser(text);
+ }
- return parseExpensiMarkWithShortMentions(text, mentionsSharedVal.get(), currentUserMentions);
- },
- [currentUserMentions, mentionsSharedVal, parser],
- );
+ return parseExpensiMarkWithShortMentions(text, mentionsSharedVal.get(), currentUserMentions);
+ };
useEffect(() => {
scheduleOnLiveMarkdownRuntime(() => {
diff --git a/src/components/Reactions/QuickEmojiReactions/BaseQuickEmojiReactions.tsx b/src/components/Reactions/QuickEmojiReactions/BaseQuickEmojiReactions.tsx
index 3109ab040be6..131b210be7e3 100644
--- a/src/components/Reactions/QuickEmojiReactions/BaseQuickEmojiReactions.tsx
+++ b/src/components/Reactions/QuickEmojiReactions/BaseQuickEmojiReactions.tsx
@@ -18,7 +18,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
import type {ReportActionReactions} from '@src/types/onyx';
import {getEmptyObject} from '@src/types/utils/EmptyObject';
-import React, {useCallback} from 'react';
+import React from 'react';
import {View} from 'react-native';
import type {BaseQuickEmojiReactionsProps} from './types';
@@ -37,12 +37,9 @@ function BaseQuickEmojiReactions({
const [preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE] = useOnyx(ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE);
const [emojiReactions = getEmptyObject()] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${reportActionID}`);
- const selectEmojiWithReaction = useCallback(
- (emoji: Emoji, skinTone: number) => {
- onEmojiSelected(emoji, emojiReactions, skinTone);
- },
- [onEmojiSelected, emojiReactions],
- );
+ const selectEmojiWithReaction = (emoji: Emoji, skinTone: number) => {
+ onEmojiSelected(emoji, emojiReactions, skinTone);
+ };
return (
diff --git a/src/components/Reactions/ReactionTooltipContent.tsx b/src/components/Reactions/ReactionTooltipContent.tsx
index 3e6763346326..6d8b09b8fe83 100644
--- a/src/components/Reactions/ReactionTooltipContent.tsx
+++ b/src/components/Reactions/ReactionTooltipContent.tsx
@@ -70,4 +70,4 @@ function ReactionTooltipContent({accountIDs, emojiCodes, emojiName, currentUserA
ReactionTooltipContent.displayName = 'ReactionTooltipContent';
-export default React.memo(ReactionTooltipContent);
+export default ReactionTooltipContent;
diff --git a/src/components/ReceiptCropView/index.tsx b/src/components/ReceiptCropView/index.tsx
index 3cbd5a6be7e0..8ed7dbf7cbd0 100644
--- a/src/components/ReceiptCropView/index.tsx
+++ b/src/components/ReceiptCropView/index.tsx
@@ -15,7 +15,7 @@ import type {ComponentRef} from 'react';
import type {LayoutChangeEvent} from 'react-native';
import type {PanGestureActiveEvent} from 'react-native-gesture-handler';
-import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
+import React, {useEffect, useRef, useState} from 'react';
import {View} from 'react-native';
import {GestureDetector, GestureHandlerRootView, usePanGesture} from 'react-native-gesture-handler';
import ImageSize from 'react-native-image-size';
@@ -93,18 +93,22 @@ function ReceiptCropView({imageUri, onCropChange, initialCrop, isAuthTokenRequir
}, [imageUri, isAuthTokenRequired]);
// Calculate scale factors to convert display coordinates to image coordinates
- const {scaleX, scaleY, displayWidth, displayHeight, imageOffsetX, imageOffsetY} = useMemo(() => {
- if (!containerSize.width || !containerSize.height || !imageSize.width || !imageSize.height) {
- return {scaleX: 1, scaleY: 1, displayWidth: 0, displayHeight: 0, imageOffsetX: 0, imageOffsetY: 0};
- }
-
+ let imageLayout;
+ if (!containerSize.width || !containerSize.height || !imageSize.width || !imageSize.height) {
+ imageLayout = {
+ scaleX: 1,
+ scaleY: 1,
+ displayWidth: 0,
+ displayHeight: 0,
+ imageOffsetX: 0,
+ imageOffsetY: 0,
+ };
+ } else {
// Calculate how the image is scaled to fit the container
const imageAspectRatio = imageSize.width / imageSize.height;
const containerAspectRatio = containerSize.width / containerSize.height;
-
let calculatedDisplayWidth: number;
let calculatedDisplayHeight: number;
-
if (imageAspectRatio > containerAspectRatio) {
// Image is wider - fit to width
calculatedDisplayWidth = containerSize.width;
@@ -128,8 +132,7 @@ function ReceiptCropView({imageUri, onCropChange, initialCrop, isAuthTokenRequir
displayHeightSV.set(calculatedDisplayHeight);
imageOffsetXSV.set(calculatedImageOffsetX);
imageOffsetYSV.set(calculatedImageOffsetY);
-
- return {
+ imageLayout = {
scaleX: calculatedScaleX,
scaleY: calculatedScaleY,
displayWidth: calculatedDisplayWidth,
@@ -137,7 +140,8 @@ function ReceiptCropView({imageUri, onCropChange, initialCrop, isAuthTokenRequir
imageOffsetX: calculatedImageOffsetX,
imageOffsetY: calculatedImageOffsetY,
};
- }, [containerSize.width, containerSize.height, imageSize.width, imageSize.height, displayWidthSV, displayHeightSV, imageOffsetXSV, imageOffsetYSV]);
+ }
+ const {scaleX, scaleY, displayWidth, displayHeight, imageOffsetX, imageOffsetY} = imageLayout;
// Initialize crop rectangle when dimensions are available
useEffect(() => {
@@ -246,80 +250,71 @@ function ReceiptCropView({imageUri, onCropChange, initialCrop, isAuthTokenRequir
onCropChange?.(crop);
}, [displayWidth, displayHeight, scaleX, scaleY, imageOffsetX, imageOffsetY, cropX, cropY, cropWidth, cropHeight, onCropChange]);
- const onContainerLayout = useCallback(
- (event: LayoutChangeEvent) => {
- const {width, height} = event.nativeEvent.layout;
- setContainerSize({width, height});
- containerWidthSV.set(width);
- containerHeightSV.set(height);
- },
- [containerWidthSV, containerHeightSV],
- );
+ const onContainerLayout = (event: LayoutChangeEvent) => {
+ const {width, height} = event.nativeEvent.layout;
+ setContainerSize({width, height});
+ containerWidthSV.set(width);
+ containerHeightSV.set(height);
+ };
- const onImageLoad = useCallback(
- (event: {nativeEvent: {width: number; height: number}}) => {
- const {width, height} = event.nativeEvent;
- if (!width || !height || (imageSize.width === width && imageSize.height === height)) {
- return;
- }
- if (!hasImageDimensions) {
- setHasImageDimensions(true);
- }
- if (!isImageInitialized.current) {
- setImageSize({width, height});
- }
- },
- [hasImageDimensions, imageSize.width, imageSize.height],
- );
+ const onImageLoad = (event: {nativeEvent: {width: number; height: number}}) => {
+ const {width, height} = event.nativeEvent;
+ if (!width || !height || (imageSize.width === width && imageSize.height === height)) {
+ return;
+ }
+ if (!hasImageDimensions) {
+ setHasImageDimensions(true);
+ }
+ if (!isImageInitialized.current) {
+ setImageSize({width, height});
+ }
+ };
/**
* Clamp a value between min and max
*/
- const clamp = useCallback((value: number, min: number, max: number) => {
+ const clamp = (value: number, min: number, max: number) => {
'worklet';
return Math.max(min, Math.min(max, value));
- }, []);
-
- const applyCropBoundsAndNotify = useCallback(
- (newX: number, newY: number, newWidth: number, newHeight: number, imageLeft: number, imageRight: number, imageTop: number, imageBottom: number) => {
- // clamp to image bounds
- let x = newX;
- let y = newY;
- let w = newWidth;
- let h = newHeight;
-
- if (x < imageLeft) {
- const diff = imageLeft - x;
- x = imageLeft;
- w -= diff;
- }
- if (x + w > imageRight) {
- w = imageRight - x;
- }
- if (y < imageTop) {
- const diff = imageTop - y;
- y = imageTop;
- h -= diff;
- }
- if (y + h > imageBottom) {
- h = imageBottom - y;
- }
+ };
- cropX.set(x);
- cropY.set(y);
- cropWidth.set(w);
- cropHeight.set(h);
-
- onCropChange?.({
- x: (x - imageOffsetX) * scaleX,
- y: (y - imageOffsetY) * scaleY,
- width: w * scaleX,
- height: h * scaleY,
- });
- },
- [cropX, cropY, cropWidth, cropHeight, imageOffsetX, scaleX, imageOffsetY, scaleY, onCropChange],
- );
+ const applyCropBoundsAndNotify = (newX: number, newY: number, newWidth: number, newHeight: number, imageLeft: number, imageRight: number, imageTop: number, imageBottom: number) => {
+ // clamp to image bounds
+ let x = newX;
+ let y = newY;
+ let w = newWidth;
+ let h = newHeight;
+
+ if (x < imageLeft) {
+ const diff = imageLeft - x;
+ x = imageLeft;
+ w -= diff;
+ }
+ if (x + w > imageRight) {
+ w = imageRight - x;
+ }
+ if (y < imageTop) {
+ const diff = imageTop - y;
+ y = imageTop;
+ h -= diff;
+ }
+ if (y + h > imageBottom) {
+ h = imageBottom - y;
+ }
+
+ cropX.set(x);
+ cropY.set(y);
+ cropWidth.set(w);
+ cropHeight.set(h);
+
+ onCropChange?.({
+ x: (x - imageOffsetX) * scaleX,
+ y: (y - imageOffsetY) * scaleY,
+ width: w * scaleX,
+ height: h * scaleY,
+ });
+ };
/**
* Resize the crop area when a handle is dragged. Edge handles move one edge, corner handles move the two edges that meet at the corner.
diff --git a/src/components/RenderHTML.tsx b/src/components/RenderHTML.tsx
index ea7d0aaa5388..acb574817125 100644
--- a/src/components/RenderHTML.tsx
+++ b/src/components/RenderHTML.tsx
@@ -6,7 +6,7 @@ import Parser from '@libs/Parser';
import type {RenderersProps} from 'react-native-render-html';
-import React, {useMemo} from 'react';
+import React from 'react';
import {RenderHTMLConfigProvider, RenderHTMLSource, useSharedProps} from 'react-native-render-html';
import htmlRenderers from './HTMLEngineProvider/HTMLRenderers';
@@ -71,30 +71,24 @@ function RenderHTML({html: htmlParam, onLinkPress, onConciergeLinkPress, isSelec
const styles = useThemeStyles();
const {windowWidth} = useWindowDimensions();
- const html = useMemo(() => {
- return (
- Parser.replace(htmlParam, {shouldEscapeText: false, filterRules: ['emoji']})
- // Escape brackets when pasting a link, since unescaped [] can break Markdown link syntax
- .replaceAll(RE_BRACKET_ESCAPE, (m) => (m.at(7) === '1' ? '[' : ']'))
- // Remove double tag if exists and keep the outermost tag (always the original tag).
- .replaceAll(RE_EMOJI_OPEN_OR_CLOSE, '$1$2')
- // Strip orphaned
tags inside and that would render as extra empty items
- .replaceAll(RE_BR_CLEANUP, '$1$2')
- );
- }, [htmlParam]);
-
- const renderersProps = useMemo(() => {
- return {
- a: {
- onPress: onLinkPress,
- },
- // Custom HTML renderer keys must use hyphenated tag names per react-native-render-html API
- /* eslint-disable @typescript-eslint/naming-convention */
- 'concierge-link': {
- onPress: onConciergeLinkPress,
- },
- };
- }, [onLinkPress, onConciergeLinkPress]);
+ const html = Parser.replace(htmlParam, {shouldEscapeText: false, filterRules: ['emoji']})
+ // Escape brackets when pasting a link, since unescaped [] can break Markdown link syntax
+ .replaceAll(RE_BRACKET_ESCAPE, (m) => (m.at(7) === '1' ? '[' : ']'))
+ // Remove double tag if exists and keep the outermost tag (always the original tag).
+ .replaceAll(RE_EMOJI_OPEN_OR_CLOSE, '$1$2')
+ // Strip orphaned
tags inside and that would render as extra empty items
+ .replaceAll(RE_BR_CLEANUP, '$1$2');
+
+ const renderersProps = {
+ a: {
+ onPress: onLinkPress,
+ },
+ // Custom HTML renderer keys must use hyphenated tag names per react-native-render-html API
+ /* eslint-disable @typescript-eslint/naming-convention */
+ 'concierge-link': {
+ onPress: onConciergeLinkPress,
+ },
+ };
const renderers = {
/* eslint-disable @typescript-eslint/naming-convention */
diff --git a/src/components/ReportActionAvatars/useReportPreviewSenderID.ts b/src/components/ReportActionAvatars/useReportPreviewSenderID.ts
index 91f9f43044f8..ccfadb35274a 100644
--- a/src/components/ReportActionAvatars/useReportPreviewSenderID.ts
+++ b/src/components/ReportActionAvatars/useReportPreviewSenderID.ts
@@ -19,8 +19,6 @@ import type {OriginalMessageIOU, Policy, Report, ReportAction, ReportActions, Tr
import type {OnyxEntry} from 'react-native-onyx';
-import {useMemo} from 'react';
-
function getSplitAuthor(transaction: Transaction, splits?: Array>) {
const {originalTransactionID, source} = transaction.comment ?? {};
@@ -309,10 +307,10 @@ function useReportPreviewSenderID({iouReport, action, chatReport}: {action: Onyx
const hasFinishedInitialReportActionsLoad = hasOnceLoadedReportActions === true || isLoadingInitialReportActions === false;
const {transactions: reportTransactions} = useTransactionsAndViolationsForReport(shouldFetchData ? action?.childReportID : undefined);
- const transactions = useMemo(() => {
- if (!shouldFetchData) {
- return undefined;
- }
+ let transactions;
+ if (!shouldFetchData) {
+ transactions = undefined;
+ } else {
const activeMoneyRequestCount = iouReport?.transactionCount ?? action?.childMoneyRequestCount ?? 0;
const allReportTransactions = Object.values(reportTransactions ?? {}).filter((transaction): transaction is Transaction => !!transaction);
const nonDeletedTransactionsIncludingOrphans = getAllNonDeletedTransactions(reportTransactions, iouActions ?? [], false, true);
@@ -320,13 +318,12 @@ function useReportPreviewSenderID({iouReport, action, chatReport}: {action: Onyx
nonDeletedTransactionsIncludingOrphans.length < allReportTransactions.length
? getAllNonDeletedTransactions(reportTransactions, iouActions ?? [])
: nonDeletedTransactionsIncludingOrphans;
-
if (filteredTransactions.length < allReportTransactions.length && filteredTransactions.length < activeMoneyRequestCount) {
- return nonDeletedTransactionsIncludingOrphans;
+ transactions = nonDeletedTransactionsIncludingOrphans;
+ } else {
+ transactions = filteredTransactions;
}
-
- return filteredTransactions;
- }, [reportTransactions, iouActions, shouldFetchData, iouReport?.transactionCount, action?.childMoneyRequestCount]);
+ }
// Only pending scans read the chat's last actor, so other previews skip a subscription that changes on every message.
const hasPendingScan = !!transactions?.some(hasPendingScanStateAndUnknownDirection);
diff --git a/src/components/ReportActionItem/ExportWithDropdownMenu.tsx b/src/components/ReportActionItem/ExportWithDropdownMenu.tsx
index 03089217468e..4ceeefb02a31 100644
--- a/src/components/ReportActionItem/ExportWithDropdownMenu.tsx
+++ b/src/components/ReportActionItem/ExportWithDropdownMenu.tsx
@@ -26,7 +26,7 @@ import type WithSentryLabel from '@src/types/utils/SentryLabel';
import type {StyleProp, ViewStyle} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
-import React, {useMemo} from 'react';
+import React from 'react';
import {StyleSheet} from 'react-native';
type ExportWithDropdownMenuProps = WithSentryLabel & {
@@ -81,35 +81,35 @@ function ExportWithDropdownMenu({
const flattenedWrapperStyle = StyleSheet.flatten([styles.flex1, wrapperStyle]);
const connectionNameFriendly = getAccountingIntegrationDisplayName(policy, connectionName, translate);
- const dropdownOptions: Array> = useMemo(() => {
- const optionTemplate = {
- icon: iconToDisplay,
- disabled: !canBeExported,
- displayInDefaultIconColor: true,
- iconWidth: variables.iconSizeMenuItem,
- iconHeight: variables.iconSizeMenuItem,
- additionalIconStyles: styles.integrationIcon,
- };
- const options = [
- {
- value: CONST.REPORT.EXPORT_OPTIONS.EXPORT_TO_INTEGRATION,
- text: translate('workspace.common.exportIntegrationSelected', {connectionName, connectionNameFriendly}),
- ...optionTemplate,
- },
- {
- value: CONST.REPORT.EXPORT_OPTIONS.MARK_AS_EXPORTED,
- text: translate('workspace.common.markAsEntered'),
- ...optionTemplate,
- },
- ];
- const exportMethod = report?.policyID ? exportMethods?.[report.policyID] : null;
- if (exportMethod) {
- options.sort((method) => (method.value === exportMethod ? -1 : 0));
- }
- return options;
- // We do not include exportMethods not to re-render the component when the preferred export method changes
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [canBeExported, iconToDisplay, connectionName, connectionNameFriendly, report?.policyID, translate]);
+ const optionTemplate = {
+ icon: iconToDisplay,
+ disabled: !canBeExported,
+ displayInDefaultIconColor: true,
+ iconWidth: variables.iconSizeMenuItem,
+ iconHeight: variables.iconSizeMenuItem,
+ additionalIconStyles: styles.integrationIcon,
+ };
+ const dropdownOptions: Array> = [
+ {
+ value: CONST.REPORT.EXPORT_OPTIONS.EXPORT_TO_INTEGRATION,
+ text: translate('workspace.common.exportIntegrationSelected', {
+ connectionName,
+ connectionNameFriendly,
+ }),
+ ...optionTemplate,
+ },
+ {
+ value: CONST.REPORT.EXPORT_OPTIONS.MARK_AS_EXPORTED,
+ text: translate('workspace.common.markAsEntered'),
+ ...optionTemplate,
+ },
+ ];
+ const exportMethod = report?.policyID ? exportMethods?.[report.policyID] : null;
+ // The split button tracks selection by index, so saving a preference must not reorder its options.
+ const defaultSelectedIndex = Math.max(
+ 0,
+ dropdownOptions.findIndex(({value}) => value === exportMethod),
+ );
const handleExport = (exportType: ReportExportType) => {
if (!reportID) {
@@ -158,6 +158,7 @@ function ExportWithDropdownMenu({
}}
onOptionSelected={({value}) => savePreferredExportMethod(value)}
options={dropdownOptions}
+ defaultSelectedIndex={defaultSelectedIndex}
style={[shouldUseNarrowLayout && styles.flexGrow1]}
wrapperStyle={flattenedWrapperStyle}
size={CONST.BUTTON_SIZE.MEDIUM}
diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx
index a3ab9bc5a161..4c6fb1b18e14 100644
--- a/src/components/ReportActionItem/MoneyReportView.tsx
+++ b/src/components/ReportActionItem/MoneyReportView.tsx
@@ -43,7 +43,7 @@ import type {PendingAction} from '@src/types/onyx/OnyxCommon';
import type {StyleProp, TextStyle} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
-import React, {useMemo} from 'react';
+import React from 'react';
import {View} from 'react-native';
type BreakdownRow = {
@@ -134,10 +134,8 @@ function MoneyReportView({
StyleUtils.getColorStyle(theme.textSupporting),
];
- const policyReportFields = useMemo(() => {
- const {fieldsByName} = getReportFieldMaps(report, policy?.fieldList ?? {}, reportNameValuePairs);
- return Object.values(fieldsByName).filter((field) => isReportFieldTargetMatchingReport(report, field));
- }, [policy?.fieldList, report, reportNameValuePairs]);
+ const {fieldsByName} = getReportFieldMaps(report, policy?.fieldList ?? {}, reportNameValuePairs);
+ const policyReportFields = Object.values(fieldsByName).filter((field) => isReportFieldTargetMatchingReport(report, field));
const isOnlyTitleFieldEnabled = policyReportFields.every(shouldHideSingleReportField);
const isClosedExpenseReportWithNoExpenses = isClosedExpenseReportWithNoExpensesReportUtils(report, transactions.length > 0);
@@ -146,20 +144,16 @@ function MoneyReportView({
const hasPendingAction = transactions.some(getTransactionPendingAction);
- const renderThreadDivider = useMemo(
- () =>
- shouldHideThreadDividerLine ? (
-
- ) : (
-
- ),
- [shouldHideThreadDividerLine, report?.reportID, styles.reportHorizontalRule],
+ const renderThreadDivider = shouldHideThreadDividerLine ? (
+
+ ) : (
+
);
const icons = useMemoizedLazyExpensifyIcons(['Checkmark']);
diff --git a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx
index ee660591a846..c3587be39616 100644
--- a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx
+++ b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx
@@ -101,7 +101,7 @@ import {hasSeenTourSelector} from '@selectors/Onboarding';
import {transactionThreadReportIDSelector} from '@selectors/ReportAction';
import {Str} from 'expensify-common';
import mapValues from 'lodash/mapValues';
-import React, {useEffect, useMemo, useRef, useState} from 'react';
+import React, {useEffect, useRef, useState} from 'react';
import {View} from 'react-native';
import HoveredDistanceEReceipt from './HoveredDistanceEReceipt';
@@ -179,13 +179,13 @@ function MoneyRequestReceiptView({
const iouReportTransactions = Object.values(iouReportTransactionsCollection);
const isTrackExpense = !mergeTransactionID && isTrackExpenseReportNew(report, parentReport, parentReportAction);
const moneyRequestReport = parentReport;
- const linkedTransactionID = useMemo(() => {
- if (!parentReportAction) {
- return undefined;
- }
+ let linkedTransactionID;
+ if (!parentReportAction) {
+ linkedTransactionID = undefined;
+ } else {
const originalMessage = parentReportAction && isMoneyRequestAction(parentReportAction) ? getOriginalMessage(parentReportAction) : undefined;
- return originalMessage?.IOUTransactionID;
- }, [parentReportAction]);
+ linkedTransactionID = originalMessage?.IOUTransactionID;
+ }
const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(linkedTransactionID)}`);
const [transactionReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transaction?.reportID}`);
@@ -307,16 +307,12 @@ function MoneyRequestReceiptView({
const {validateFiles, PDFValidationComponent} = useFilesValidation(onAttachmentFilesValidated);
- const iouType = useMemo(() => {
- if (isTrackExpense) {
- return CONST.IOU.TYPE.TRACK;
- }
- if (isInvoice) {
- return CONST.IOU.TYPE.INVOICE;
- }
-
- return CONST.IOU.TYPE.SUBMIT;
- }, [isTrackExpense, isInvoice]);
+ let iouType;
+ if (isTrackExpense) {
+ iouType = CONST.IOU.TYPE.TRACK;
+ } else {
+ iouType = isInvoice ? CONST.IOU.TYPE.INVOICE : CONST.IOU.TYPE.SUBMIT;
+ }
const receiptURIs = hasReceipt ? getThumbnailAndImageURIs(displayedTransaction) : undefined;
const isEReceiptTransaction = !!displayedTransaction && !hasReceiptSource(displayedTransaction) && hasEReceipt(displayedTransaction);
@@ -346,69 +342,48 @@ function MoneyRequestReceiptView({
const routeDistanceMeters = transaction?.comment?.customUnit?.routeDistanceMeters;
const distanceUnit = transaction?.comment?.customUnit?.distanceUnit;
- const [receiptImageViolations, receiptViolations] = useMemo(() => {
- const imageViolations = [];
- const allViolations = [];
- const filteredViolations = filterReceiptViolations(transactionViolations ?? []);
-
- for (const violation of filteredViolations) {
- const isReceiptFieldViolation = receiptFieldViolationNames.has(violation.name);
- const isReceiptImageViolation = receiptImageViolationNames.has(violation.name);
- const isRTERViolation = violation.name === CONST.VIOLATIONS.RTER;
- if (isReceiptFieldViolation || isReceiptImageViolation || isRTERViolation) {
- const cardID = violation.data?.cardID;
- const card = cardID ? cardList?.[cardID] : undefined;
- const violationMessage = ViolationsUtils.getViolationTranslation({
- dateFnsLocale,
- violation,
- translate,
- convertToDisplayString,
- canEdit: isActionTakenByCurrentUser && isEditable,
- companyCardPageURL,
- connectionLink,
- card,
- isMarkAsCash,
- routeDistanceMeters,
- distanceUnit,
- policyVendors,
- transactionCurrency: transaction?.currency,
- });
- allViolations.push(violationMessage);
- if (isReceiptImageViolation || isRTERViolation) {
- imageViolations.push(violationMessage);
- }
+ const imageViolations = [];
+ const allViolations = [];
+ const filteredViolations = filterReceiptViolations(transactionViolations ?? []);
+ for (const violation of filteredViolations) {
+ const isReceiptFieldViolation = receiptFieldViolationNames.has(violation.name);
+ const isReceiptImageViolation = receiptImageViolationNames.has(violation.name);
+ const isRTERViolation = violation.name === CONST.VIOLATIONS.RTER;
+ if (isReceiptFieldViolation || isReceiptImageViolation || isRTERViolation) {
+ const cardID = violation.data?.cardID;
+ const card = cardID ? cardList?.[cardID] : undefined;
+ const violationMessage = ViolationsUtils.getViolationTranslation({
+ dateFnsLocale,
+ violation,
+ translate,
+ convertToDisplayString,
+ canEdit: isActionTakenByCurrentUser && isEditable,
+ companyCardPageURL,
+ connectionLink,
+ card,
+ isMarkAsCash,
+ routeDistanceMeters,
+ distanceUnit,
+ policyVendors,
+ transactionCurrency: transaction?.currency,
+ });
+ allViolations.push(violationMessage);
+ if (isReceiptImageViolation || isRTERViolation) {
+ imageViolations.push(violationMessage);
}
}
- return [imageViolations, allViolations];
- }, [
- transactionViolations,
- translate,
- convertToDisplayString,
- isActionTakenByCurrentUser,
- isEditable,
- companyCardPageURL,
- connectionLink,
- cardList,
- isMarkAsCash,
- routeDistanceMeters,
- distanceUnit,
- policyVendors,
- transaction?.currency,
- dateFnsLocale,
- ]);
+ }
+ const [receiptImageViolations, receiptViolations] = [imageViolations, allViolations];
const receiptRequiredViolation = transactionViolations?.some((violation) => violation.name === CONST.VIOLATIONS.RECEIPT_REQUIRED);
const itemizedReceiptRequiredViolation = transactionViolations?.some((violation) => violation.name === CONST.VIOLATIONS.ITEMIZED_RECEIPT_REQUIRED);
const customRulesViolation = transactionViolations?.some((violation) => violation.name === CONST.VIOLATIONS.CUSTOM_RULES);
- const errorsWithoutReportCreation = useMemo(
- () => ({
- ...(transaction?.errorFields?.route ?? transaction?.errorFields?.waypoints ?? transaction?.errors),
- ...parentReportAction?.errors,
- }),
- [transaction?.errorFields?.route, transaction?.errorFields?.waypoints, transaction?.errors, parentReportAction?.errors],
- );
- const reportCreationError = useMemo(() => (getCreationReportErrors(report) ? getMicroSecondOnyxErrorWithTranslationKey('report.genericCreateReportFailureMessage') : {}), [report]);
+ const errorsWithoutReportCreation = {
+ ...(transaction?.errorFields?.route ?? transaction?.errorFields?.waypoints ?? transaction?.errors),
+ ...parentReportAction?.errors,
+ };
+ const reportCreationError = getCreationReportErrors(report) ? getMicroSecondOnyxErrorWithTranslationKey('report.genericCreateReportFailureMessage') : {};
const hasReceiptUploadError = Object.values(errorsWithoutReportCreation).some((error) => isReceiptError(error));
// Whether to show receipt audit result (e.g.`Verified`, `Issue Found`) and messages (e.g. `Receipt not verified. Please confirm accuracy.`)
@@ -420,31 +395,27 @@ function MoneyRequestReceiptView({
isGroupPolicyByType(policy?.type);
const shouldShowReceiptAudit = !isInvoice && (shouldShowReceiptEmptyState || hasReceipt || hasReceiptUploadError);
- const fallbackReceiptError = useMemo(() => {
- // Map/route distance expenses carry a generated map e-receipt (no real uploaded file), so they should not
- // surface a receipt-upload error on a create/report failure - the transaction's own generic error should be
- // shown instead. Odometer / pure-manual distance flows can have a real uploaded file, so the fallback is kept
- // for them (isMapBasedDistanceRequest is false for those).
- if (hasReceiptUploadError || isEmptyObject(reportCreationError) || !hasReceipt || !transaction?.receipt || isMapDistanceRequest) {
- return {};
- }
-
- return getMicroSecondOnyxErrorObject({
- error: CONST.IOU.RECEIPT_ERROR,
- source: transaction.receipt.source?.toString() ?? '',
- filename: transaction.receipt.filename ?? '',
- });
- }, [hasReceiptUploadError, reportCreationError, hasReceipt, transaction, isMapDistanceRequest]);
-
- const errors = useMemo(() => {
- if (hasReceiptUploadError) {
- return errorsWithoutReportCreation;
- }
- if (!isEmptyObject(fallbackReceiptError)) {
- return {...errorsWithoutReportCreation, ...fallbackReceiptError};
- }
- return isEmptyObject(errorsWithoutReportCreation) ? reportCreationError : errorsWithoutReportCreation;
- }, [hasReceiptUploadError, fallbackReceiptError, errorsWithoutReportCreation, reportCreationError]);
+ // Map/route distance expenses carry a generated map e-receipt (no real uploaded file), so they should not
+ // surface a receipt-upload error on a create/report failure - the transaction's own generic error should be
+ // shown instead. Odometer / pure-manual distance flows can have a real uploaded file, so the fallback is kept
+ // for them (isMapBasedDistanceRequest is false for those).
+ const fallbackReceiptError =
+ hasReceiptUploadError || isEmptyObject(reportCreationError) || !hasReceipt || !transaction?.receipt || isMapDistanceRequest
+ ? {}
+ : getMicroSecondOnyxErrorObject({
+ error: CONST.IOU.RECEIPT_ERROR,
+ source: transaction.receipt.source?.toString() ?? '',
+ filename: transaction.receipt.filename ?? '',
+ });
+
+ let errors;
+ if (hasReceiptUploadError) {
+ errors = errorsWithoutReportCreation;
+ } else if (!isEmptyObject(fallbackReceiptError)) {
+ errors = {...errorsWithoutReportCreation, ...fallbackReceiptError};
+ } else {
+ errors = isEmptyObject(errorsWithoutReportCreation) ? reportCreationError : errorsWithoutReportCreation;
+ }
const showReceiptErrorWithEmptyState = shouldShowReceiptEmptyState && !hasReceipt && !isEmptyObject(errors);
const {showConfirmModal} = useConfirmModal();
@@ -492,13 +463,10 @@ function MoneyRequestReceiptView({
).catch((error: unknown) => Log.alert('[ReceiptRetry] Retry failed unexpectedly', {error}));
};
- const transactionAndReportActionErrors = useMemo(
- () => ({
- ...transaction?.errors,
- ...parentReportAction?.errors,
- }),
- [transaction?.errors, parentReportAction?.errors],
- );
+ const transactionAndReportActionErrors = {
+ ...transaction?.errors,
+ ...parentReportAction?.errors,
+ };
const dismissReceiptError = () => {
if (!report?.reportID) {
@@ -613,7 +581,6 @@ function MoneyRequestReceiptView({
};
let receiptStyle: StyleProp;
-
if (fillSpace && shouldShowReceiptEmptyState) {
receiptStyle = styles.h100;
} else if (fillSpace) {
diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewProvider.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewProvider.tsx
index 280a4d2433e1..b1d613582f52 100644
--- a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewProvider.tsx
+++ b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewProvider.tsx
@@ -207,7 +207,7 @@ function MoneyRequestReportPreviewProvider({
onOrderedTransactionsChange,
});
- const openReportFromPreview = useCallback(() => {
+ const openReportFromPreview = () => {
if (!iouReportID) {
return;
}
@@ -232,13 +232,13 @@ function MoneyRequestReportPreviewProvider({
} else {
Navigation.navigate(ROUTES.EXPENSE_REPORT_RHP.getRoute({reportID: iouReportID, backTo}));
}
- }, [iouReportID, isSmallScreenWidth, onCancelPendingPress]);
+ };
// Only the pay flow opens this menu; approve surfaces its partial/full choice in the approve dropdown instead.
- const onHoldMenuOpen = useCallback((paymentType?: PaymentMethodType, canPay?: boolean, methodID?: number) => {
+ const onHoldMenuOpen = (paymentType?: PaymentMethodType, canPay?: boolean, methodID?: number) => {
setHoldMenu({paymentType, canPay: !!canPay, methodID});
- }, []);
- const onHoldMenuClose = useCallback(() => setHoldMenu(null), []);
+ };
+ const onHoldMenuClose = () => setHoldMenu(null);
const shouldShowCarouselArrows = !shouldUseNarrowLayout && !shouldShowAccessPlaceHolder && transactions.length > 2 && reportPreviewStyles.expenseCountVisible;
const buttonMaxWidth =
diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/ReportPreviewHeader.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/ReportPreviewHeader.tsx
index cba808917a03..81514556e501 100644
--- a/src/components/ReportActionItem/MoneyRequestReportPreview/ReportPreviewHeader.tsx
+++ b/src/components/ReportActionItem/MoneyRequestReportPreview/ReportPreviewHeader.tsx
@@ -23,7 +23,7 @@ import type {ReportAttributesDerivedValue} from '@src/types/onyx';
import type {OnyxEntry} from 'react-native-onyx';
import {reportNameSelector} from '@selectors/ReportAttributes';
-import React, {useCallback, useMemo} from 'react';
+import React from 'react';
import {View} from 'react-native';
import Animated from 'react-native-reanimated';
@@ -45,10 +45,10 @@ function ReportPreviewHeader() {
const {goToPrevious, goToNext} = useReportPreviewActions();
const numberOfRequests = transactions.length;
- const selectReportName = useCallback((attributes: OnyxEntry) => reportNameSelector(attributes, iouReportID), [iouReportID]);
+ const selectReportName = (attributes: OnyxEntry) => reportNameSelector(attributes, iouReportID);
const [derivedReportName] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, {selector: selectReportName});
const reportName = derivedReportName ?? iouReport?.reportName ?? '';
- const formattedReportName = useMemo(() => Parser.htmlToText(reportName || (action.childReportName ?? '')), [reportName, action.childReportName]);
+ const formattedReportName = Parser.htmlToText(reportName || (action.childReportName ?? ''));
/*
Show subtitle if at least one of the expenses is not being smart scanned, and either:
@@ -58,40 +58,28 @@ function ReportPreviewHeader() {
* There is an edge case when there is only one distance expense with a pending route and amount = 0.
In this case, we don't want to show the merchant or description because it says: "Pending route...", which is already displayed in the amount field.
*/
- const expenseCount = useMemo(
- () =>
- translate('iou.expenseCount', {
- count: numberOfRequests,
- }),
- [translate, numberOfRequests],
- );
+ const expenseCount = translate('iou.expenseCount', {
+ count: numberOfRequests,
+ });
const reportStateNum = iouReport?.stateNum ?? action?.childStateNum;
const reportStatusNum = iouReport?.statusNum ?? action?.childStatusNum;
- const reportStatus = useMemo(
- () =>
- getReportStatusTranslation({
- stateNum: reportStateNum,
- statusNum: reportStatusNum,
- translate,
- }),
- [reportStateNum, reportStatusNum, translate],
- );
+ const reportStatus = getReportStatusTranslation({
+ stateNum: reportStateNum,
+ statusNum: reportStatusNum,
+ translate,
+ });
const shouldShowReportStatus = !!reportStatus && !!expenseCount;
- const reportStatusColorStyle = useMemo(() => getReportStatusColorStyle(theme, reportStateNum, reportStatusNum), [reportStateNum, reportStatusNum, theme]);
+ const reportStatusColorStyle = getReportStatusColorStyle(theme, reportStateNum, reportStatusNum);
- const reportStatusTooltip = useMemo(
- () =>
- getReportStatusTooltipTranslation({
- stateNum: reportStateNum,
- statusNum: reportStatusNum,
- translate,
- }),
- [reportStateNum, reportStatusNum, translate],
- );
+ const reportStatusTooltip = getReportStatusTooltipTranslation({
+ stateNum: reportStateNum,
+ statusNum: reportStatusNum,
+ translate,
+ });
return (
diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/index.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/index.tsx
index 1482b2942598..523cafc82191 100644
--- a/src/components/ReportActionItem/MoneyRequestReportPreview/index.tsx
+++ b/src/components/ReportActionItem/MoneyRequestReportPreview/index.tsx
@@ -46,7 +46,7 @@ import type {LayoutChangeEvent} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {useIsFocused} from '@react-navigation/core';
-import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
+import React, {useEffect, useRef, useState} from 'react';
import type {MoneyRequestReportPreviewProps} from './types';
@@ -96,21 +96,21 @@ function MoneyRequestReportPreview({
const allReportTransactions = Object.values(reportTransactionsCollection ?? {}).filter((transaction): transaction is Transaction => !!transaction);
const transactions = allReportTransactions.filter((transaction) => isOffline || transaction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE);
const orderedTransactionsRef = useRef([]);
- const handleOrderedTransactionsChange = useCallback((orderedTransactions: Transaction[]) => {
+ const handleOrderedTransactionsChange = (orderedTransactions: Transaction[]) => {
orderedTransactionsRef.current = orderedTransactions;
- }, []);
+ };
const [hasIOUReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(iouReportID)}`, {
selector: hasReportActionsSelector,
});
const pendingExpenseTransactionRef = useRef<{transaction: Transaction; originRoute: string} | null>(null);
// Subscribing to the whole action list re-rendered every card on each write, and a split writes a burst of
// them. Narrow it to the one action a deferred press is waiting for, so nothing pending means nothing changes.
- const pendingPressActionCountSelector = useCallback((reportActions: OnyxEntry) => {
+ const pendingPressActionCountSelector = (reportActions: OnyxEntry) => {
if (!pendingExpenseTransactionRef.current) {
return undefined;
}
return Object.keys(reportActions ?? {}).length;
- }, []);
+ };
const [pendingPressActionCount] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(iouReportID)}`, {
selector: pendingPressActionCountSelector,
});
@@ -133,48 +133,34 @@ function MoneyRequestReportPreview({
const widthsRef = useRef<{currentWidth: number | null; currentWrapperWidth: number | null}>({currentWidth: null, currentWrapperWidth: null});
const [widths, setWidths] = useState({currentWidth: 0, currentWrapperWidth: 0});
- const updateWidths = useCallback(() => {
+ const updateWidths = () => {
const {currentWidth, currentWrapperWidth} = widthsRef.current;
if (currentWidth && currentWrapperWidth) {
setWidths({currentWidth, currentWrapperWidth});
}
- }, []);
-
- const onCarouselLayout = useCallback(
- (e: LayoutChangeEvent) => {
- const newWidth = e.nativeEvent.layout.width;
- if (widthsRef.current.currentWidth !== newWidth) {
- widthsRef.current.currentWidth = newWidth;
- updateWidths();
- }
- },
- [updateWidths],
- );
- const onWrapperLayout = useCallback(
- (e: LayoutChangeEvent) => {
- const newWrapperWidth = e.nativeEvent.layout.width;
- if (widthsRef.current.currentWrapperWidth !== newWrapperWidth) {
- widthsRef.current.currentWrapperWidth = newWrapperWidth;
- updateWidths();
- }
- },
- [updateWidths],
- );
+ };
- const reportPreviewStyles = useMemo(
- () => StyleUtils.getMoneyRequestReportPreviewStyle(shouldUseNarrowLayout, transactions.length, widths.currentWidth, widths.currentWrapperWidth),
- [StyleUtils, widths, shouldUseNarrowLayout, transactions.length],
- );
- const shouldShowPayerAndReceiver = useMemo(() => {
- if (!isIOUReport(iouReport) && action.childType !== CONST.REPORT.TYPE.IOU) {
- return false;
+ const onCarouselLayout = (e: LayoutChangeEvent) => {
+ const newWidth = e.nativeEvent.layout.width;
+ if (widthsRef.current.currentWidth !== newWidth) {
+ widthsRef.current.currentWidth = newWidth;
+ updateWidths();
+ }
+ };
+ const onWrapperLayout = (e: LayoutChangeEvent) => {
+ const newWrapperWidth = e.nativeEvent.layout.width;
+ if (widthsRef.current.currentWrapperWidth !== newWrapperWidth) {
+ widthsRef.current.currentWrapperWidth = newWrapperWidth;
+ updateWidths();
}
+ };
- return transactions.some((transaction) => (Number(transaction?.modifiedAmount) || transaction?.amount) < 0);
- }, [transactions, action.childType, iouReport]);
+ const reportPreviewStyles = StyleUtils.getMoneyRequestReportPreviewStyle(shouldUseNarrowLayout, transactions.length, widths.currentWidth, widths.currentWrapperWidth);
+ const shouldShowPayerAndReceiver =
+ !(!isIOUReport(iouReport) && action.childType !== CONST.REPORT.TYPE.IOU) && transactions.some((transaction) => (Number(transaction?.modifiedAmount) || transaction?.amount) < 0);
- const cancelPendingPress = useCallback(() => {
+ const cancelPendingPress = () => {
pendingExpenseTransactionRef.current = null;
if (!cascadeTimerRef.current) {
return;
@@ -182,9 +168,9 @@ function MoneyRequestReportPreview({
clearTimeout(cascadeTimerRef.current.timer);
cascadeTimerRef.current.release();
cascadeTimerRef.current = null;
- }, []);
+ };
- const openReportFromPreview = useCallback(() => {
+ const openReportFromPreview = () => {
if (!iouReportID || contextMenuRef.current?.isContextMenuOpening) {
return;
}
@@ -210,7 +196,7 @@ function MoneyRequestReportPreview({
} else {
Navigation.navigate(ROUTES.EXPENSE_REPORT_RHP.getRoute({reportID: iouReportID, backTo}));
}
- }, [cancelPendingPress, iouReportID, isSmallScreenWidth]);
+ };
const [hasOnceLoadedReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${chatReportID}`, {
selector: hasOnceLoadedReportActionsSelector,
});
@@ -237,181 +223,159 @@ function MoneyRequestReportPreview({
const transactionPreviewContainerStyles = [styles.h100, reportPreviewStyles.transactionPreviewCarouselStyle];
- const resolveChildReportID = useCallback(
- (transaction: Transaction) => {
- let transactionIOUAction = getIOUActionForReportID(transaction.reportID, transaction.transactionID);
- if (transactionIOUAction && !isLiveIOUAction(transactionIOUAction)) {
- const liveIOUAction = getIOUActionForTransactionID(Object.values(getAllReportActions(transaction.reportID) ?? {}).filter(isLiveIOUAction), transaction.transactionID);
- if (!liveIOUAction) {
- return undefined;
- }
- transactionIOUAction = liveIOUAction;
+ const resolveChildReportID = (transaction: Transaction) => {
+ let transactionIOUAction = getIOUActionForReportID(transaction.reportID, transaction.transactionID);
+ if (transactionIOUAction && !isLiveIOUAction(transactionIOUAction)) {
+ const liveIOUAction = getIOUActionForTransactionID(Object.values(getAllReportActions(transaction.reportID) ?? {}).filter(isLiveIOUAction), transaction.transactionID);
+ if (!liveIOUAction) {
+ return undefined;
}
- let childReportID = transactionIOUAction?.childReportID ?? transaction.transactionThreadReportID;
- if (childReportID) {
- const existingThread = getReportOrDraftReport(childReportID);
- if (existingThread && !existingThread.reportID) {
- return undefined;
- }
- setOptimisticTransactionThread(childReportID, iouReport?.reportID ?? transaction.reportID, transactionIOUAction?.reportActionID, iouReport?.policyID ?? policyID);
- } else if (transactionIOUAction?.reportActionID) {
- const transactionID = isMoneyRequestAction(transactionIOUAction) ? getOriginalMessage(transactionIOUAction)?.IOUTransactionID : undefined;
- if (transactionID) {
- childReportID = createTransactionThreadReport({
- introSelected,
- conciergeChat,
- currentUserLogin: currentUserEmail ?? '',
- currentUserAccountID,
- iouReport,
- iouReportAction: transactionIOUAction,
- personalDetails: personalDetailsList,
- fullPolicySnapshotLastModified,
- })?.reportID;
- }
+ transactionIOUAction = liveIOUAction;
+ }
+ let childReportID = transactionIOUAction?.childReportID ?? transaction.transactionThreadReportID;
+ if (childReportID) {
+ const existingThread = getReportOrDraftReport(childReportID);
+ if (existingThread && !existingThread.reportID) {
+ return undefined;
}
- return childReportID;
- },
- [conciergeChat, currentUserAccountID, currentUserEmail, fullPolicySnapshotLastModified, introSelected, iouReport, personalDetailsList, policyID],
- );
+ setOptimisticTransactionThread(childReportID, iouReport?.reportID ?? transaction.reportID, transactionIOUAction?.reportActionID, iouReport?.policyID ?? policyID);
+ } else if (transactionIOUAction?.reportActionID) {
+ const transactionID = isMoneyRequestAction(transactionIOUAction) ? getOriginalMessage(transactionIOUAction)?.IOUTransactionID : undefined;
+ if (transactionID) {
+ childReportID = createTransactionThreadReport({
+ introSelected,
+ conciergeChat,
+ currentUserLogin: currentUserEmail ?? '',
+ currentUserAccountID,
+ iouReport,
+ iouReportAction: transactionIOUAction,
+ personalDetails: personalDetailsList,
+ fullPolicySnapshotLastModified,
+ })?.reportID;
+ }
+ }
+ return childReportID;
+ };
// `routeAtPress` is captured when the user pressed, not read live: a second press inside the cascade window
// runs after the first press already navigated, so the live route is the report we opened and `backTo` would
// point at itself.
- const navigateToExpense = useCallback(
- (childReportID: string, routeAtPress: string) => {
- startSpan(`${CONST.TELEMETRY.SPAN_OPEN_REPORT}_${childReportID}`, {
- name: 'MoneyRequestReportPreview.Transaction',
- op: CONST.TELEMETRY.SPAN_OPEN_REPORT,
- });
-
- const openableTransactionIDs = (orderedTransactionsRef.current.length > 0 ? orderedTransactionsRef.current : transactions)
- .filter((pressedTransaction) => !isTransactionPendingDelete(pressedTransaction))
- .map((pressedTransaction) => pressedTransaction.transactionID);
-
- // This press owns the carousel it seeds. Without a source the list was unowned, so no screen could
- // release it and every other writer was free to overwrite it while the expense was still open.
- const carouselSource = CAROUSEL_SOURCE.reportPreview(iouReportID);
-
- if (isSmallScreenWidth && iouReportID) {
- const {wasPressedFromReport, backTo} = resolvePressOrigin(routeAtPress, `r/${iouReportID}`);
- const reportRoute = ROUTES.REPORT_WITH_ID.getRoute(iouReportID, undefined, undefined, backTo);
- if (!wasPressedFromReport) {
- Navigation.navigate(reportRoute);
- }
- setActiveTransactionIDs(openableTransactionIDs, {source: carouselSource});
- Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID: childReportID, backTo: reportRoute}));
- return;
- }
+ const navigateToExpense = (childReportID: string, routeAtPress: string) => {
+ startSpan(`${CONST.TELEMETRY.SPAN_OPEN_REPORT}_${childReportID}`, {
+ name: 'MoneyRequestReportPreview.Transaction',
+ op: CONST.TELEMETRY.SPAN_OPEN_REPORT,
+ });
- if (isSmallScreenWidth) {
- setActiveTransactionIDs(openableTransactionIDs, {source: carouselSource});
- Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID: childReportID, backTo: routeAtPress}));
- return;
+ const openableTransactionIDs = (orderedTransactionsRef.current.length > 0 ? orderedTransactionsRef.current : transactions)
+ .filter((pressedTransaction) => !isTransactionPendingDelete(pressedTransaction))
+ .map((pressedTransaction) => pressedTransaction.transactionID);
+
+ // This press owns the carousel it seeds. Without a source the list was unowned, so no screen could
+ // release it and every other writer was free to overwrite it while the expense was still open.
+ const carouselSource = CAROUSEL_SOURCE.reportPreview(iouReportID);
+
+ if (isSmallScreenWidth && iouReportID) {
+ const {wasPressedFromReport, backTo} = resolvePressOrigin(routeAtPress, `r/${iouReportID}`);
+ const reportRoute = ROUTES.REPORT_WITH_ID.getRoute(iouReportID, undefined, undefined, backTo);
+ if (!wasPressedFromReport) {
+ Navigation.navigate(reportRoute);
}
+ setActiveTransactionIDs(openableTransactionIDs, {source: carouselSource});
+ Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID: childReportID, backTo: reportRoute}));
+ return;
+ }
- if (iouReportID) {
- const {wasPressedFromReport, backTo} = resolvePressOrigin(routeAtPress, `e/${iouReportID}`);
- const reportRoute = ROUTES.EXPENSE_REPORT_RHP.getRoute({reportID: iouReportID, backTo});
- markReportRHPWidth(iouReportID, 'super-wide');
- if (!wasPressedFromReport) {
- Navigation.navigate(reportRoute);
- }
- const seeded = setActiveTransactionIDs(openableTransactionIDs, {source: carouselSource});
- markReportRHPWidth(childReportID, 'wide');
- const release = () => {
- unmarkReportRHPWidth(childReportID);
- // Only clears if the report still carries our hint, so it can't undo one set by the report itself.
- unmarkReportRHPWidth(iouReportID, 'super-wide');
- seeded.then(() => {
- if (getActiveTransactionIDs().ids !== openableTransactionIDs) {
- return;
- }
- clearActiveTransactionIDsForSource(carouselSource);
- });
- };
- const timer = setTimeout(() => {
- cascadeTimerRef.current = null;
- if (!Navigation.isActiveRoute(reportRoute)) {
- release();
+ if (isSmallScreenWidth) {
+ setActiveTransactionIDs(openableTransactionIDs, {source: carouselSource});
+ Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID: childReportID, backTo: routeAtPress}));
+ return;
+ }
+
+ if (iouReportID) {
+ const {wasPressedFromReport, backTo} = resolvePressOrigin(routeAtPress, `e/${iouReportID}`);
+ const reportRoute = ROUTES.EXPENSE_REPORT_RHP.getRoute({reportID: iouReportID, backTo});
+ markReportRHPWidth(iouReportID, 'super-wide');
+ if (!wasPressedFromReport) {
+ Navigation.navigate(reportRoute);
+ }
+ const seeded = setActiveTransactionIDs(openableTransactionIDs, {source: carouselSource});
+ markReportRHPWidth(childReportID, 'wide');
+ const release = () => {
+ unmarkReportRHPWidth(childReportID);
+ // Only clears if the report still carries our hint, so it can't undo one set by the report itself.
+ unmarkReportRHPWidth(iouReportID, 'super-wide');
+ seeded.then(() => {
+ if (getActiveTransactionIDs().ids !== openableTransactionIDs) {
return;
}
- Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID: childReportID, backTo: reportRoute}));
- }, PRESSED_EXPENSE_CASCADE_DELAY);
- cascadeTimerRef.current = {timer, release};
- return;
- }
+ clearActiveTransactionIDsForSource(carouselSource);
+ });
+ };
+ const timer = setTimeout(() => {
+ cascadeTimerRef.current = null;
+ if (!Navigation.isActiveRoute(reportRoute)) {
+ release();
+ return;
+ }
+ Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID: childReportID, backTo: reportRoute}));
+ }, PRESSED_EXPENSE_CASCADE_DELAY);
+ cascadeTimerRef.current = {timer, release};
+ return;
+ }
- setActiveTransactionIDs(openableTransactionIDs, {source: carouselSource}).then(() => {
- markReportRHPWidth(childReportID, 'wide');
- Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID: childReportID, backTo: routeAtPress}));
- });
- },
- [isSmallScreenWidth, iouReportID, markReportRHPWidth, unmarkReportRHPWidth, transactions],
- );
+ setActiveTransactionIDs(openableTransactionIDs, {source: carouselSource}).then(() => {
+ markReportRHPWidth(childReportID, 'wide');
+ Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID: childReportID, backTo: routeAtPress}));
+ });
+ };
- const openTransactionFromPreview = useCallback(
- (transaction: Transaction) => {
- if (contextMenuRef.current?.isContextMenuOpening) {
- return;
- }
+ const openTransactionFromPreview = (transaction: Transaction) => {
+ if (contextMenuRef.current?.isContextMenuOpening) {
+ return;
+ }
- const routeAtPress = Navigation.getActiveRoute();
+ const routeAtPress = Navigation.getActiveRoute();
- pendingExpenseTransactionRef.current = null;
- if (cascadeTimerRef.current) {
- clearTimeout(cascadeTimerRef.current.timer);
- cascadeTimerRef.current.release();
- cascadeTimerRef.current = null;
- }
+ pendingExpenseTransactionRef.current = null;
+ if (cascadeTimerRef.current) {
+ clearTimeout(cascadeTimerRef.current.timer);
+ cascadeTimerRef.current.release();
+ cascadeTimerRef.current = null;
+ }
- if (transactions.length <= 1) {
- openReportFromPreview();
- return;
- }
+ if (transactions.length <= 1) {
+ openReportFromPreview();
+ return;
+ }
- if (transaction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
- openReportFromPreview();
- return;
- }
+ if (transaction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) {
+ openReportFromPreview();
+ return;
+ }
- const isIOUActionLoaded = !!getIOUActionForReportID(transaction.reportID, transaction.transactionID);
- const childReportID = resolveChildReportID(transaction);
- const fullPolicySnapshot = policyID && fullPolicySnapshotLastModified ? {policyID, lastModified: fullPolicySnapshotLastModified} : undefined;
- if (childReportID) {
- if (!isIOUActionLoaded && iouReportID) {
- if (isOffline) {
- openReportFromPreview();
- return;
- }
- openReport({reportID: iouReportID, introSelected, conciergeChat, currentUserAccountID, hasReportActions: !!hasIOUReportActions, fullPolicySnapshot});
+ const isIOUActionLoaded = !!getIOUActionForReportID(transaction.reportID, transaction.transactionID);
+ const childReportID = resolveChildReportID(transaction);
+ const fullPolicySnapshot = policyID && fullPolicySnapshotLastModified ? {policyID, lastModified: fullPolicySnapshotLastModified} : undefined;
+ if (childReportID) {
+ if (!isIOUActionLoaded && iouReportID) {
+ if (isOffline) {
+ openReportFromPreview();
+ return;
}
- navigateToExpense(childReportID, routeAtPress);
- return;
- }
-
- if (!isIOUActionLoaded && iouReportID && !isOffline) {
- pendingExpenseTransactionRef.current = {transaction, originRoute: routeAtPress};
openReport({reportID: iouReportID, introSelected, conciergeChat, currentUserAccountID, hasReportActions: !!hasIOUReportActions, fullPolicySnapshot});
- return;
}
+ navigateToExpense(childReportID, routeAtPress);
+ return;
+ }
- openReportFromPreview();
- },
- [
- conciergeChat,
- currentUserAccountID,
- fullPolicySnapshotLastModified,
- hasIOUReportActions,
- introSelected,
- iouReportID,
- isOffline,
- navigateToExpense,
- openReportFromPreview,
- policyID,
- resolveChildReportID,
- transactions.length,
- ],
- );
+ if (!isIOUActionLoaded && iouReportID && !isOffline) {
+ pendingExpenseTransactionRef.current = {transaction, originRoute: routeAtPress};
+ openReport({reportID: iouReportID, introSelected, conciergeChat, currentUserAccountID, hasReportActions: !!hasIOUReportActions, fullPolicySnapshot});
+ return;
+ }
+
+ openReportFromPreview();
+ };
useEffect(() => {
const pendingPress = pendingExpenseTransactionRef.current;
diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/usePreviewMessageAnimation.ts b/src/components/ReportActionItem/MoneyRequestReportPreview/usePreviewMessageAnimation.ts
index b147d41a77e6..47756266f324 100644
--- a/src/components/ReportActionItem/MoneyRequestReportPreview/usePreviewMessageAnimation.ts
+++ b/src/components/ReportActionItem/MoneyRequestReportPreview/usePreviewMessageAnimation.ts
@@ -9,7 +9,7 @@ import type {PersonalDetails, Policy, Report} from '@src/types/onyx';
import type {OnyxEntry} from 'react-native-onyx';
-import {useEffect, useMemo} from 'react';
+import {useEffect} from 'react';
import {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
type UsePreviewMessageAnimationParams = {
@@ -97,68 +97,49 @@ function usePreviewMessageAnimation({
opacity: previewMessageOpacity.get(),
}));
- const previewMessage = useMemo(() => {
- if (isScanning) {
- return totalDisplaySpend ? `${translate('common.receipt')} ${CONST.DOT_SEPARATOR} ${translate('common.scanning')}` : `${translate('common.receipt')}`;
- }
- if (numberOfPendingRequests === 1 && numberOfRequests === 1) {
- return `${translate('common.receipt')} ${CONST.DOT_SEPARATOR} ${translate('iou.pending')}`;
- }
- if (shouldShowRTERViolationMessage) {
- return `${translate('common.receipt')} ${CONST.DOT_SEPARATOR} ${translate('iou.pendingMatch')}`;
- }
-
+ let previewMessage;
+ if (isScanning) {
+ previewMessage = totalDisplaySpend ? `${translate('common.receipt')} ${CONST.DOT_SEPARATOR} ${translate('common.scanning')}` : `${translate('common.receipt')}`;
+ } else if (numberOfPendingRequests === 1 && numberOfRequests === 1) {
+ previewMessage = `${translate('common.receipt')} ${CONST.DOT_SEPARATOR} ${translate('iou.pending')}`;
+ } else if (shouldShowRTERViolationMessage) {
+ previewMessage = `${translate('common.receipt')} ${CONST.DOT_SEPARATOR} ${translate('iou.pendingMatch')}`;
+ } else {
let payerOrApproverName;
if (isPolicyExpenseChat || isTripRoom) {
- payerOrApproverName = getPolicyName({report: chatReport, policy, unavailableTranslation: translate('workspace.common.unavailable')});
- } else if (isInvoiceRoom) {
- payerOrApproverName = getInvoicePayerName(chatReport, translate, formatPhoneNumber, invoiceReceiverPersonalDetail, invoiceReceiverPolicy);
- } else {
- payerOrApproverName = getDisplayNameForParticipant({
- accountID: managerID,
- shouldUseShortForm: true,
- formatPhoneNumber,
- hiddenTranslation: translate('common.hidden'),
+ payerOrApproverName = getPolicyName({
+ report: chatReport,
+ policy,
+ unavailableTranslation: translate('workspace.common.unavailable'),
});
+ } else {
+ payerOrApproverName = isInvoiceRoom
+ ? getInvoicePayerName(chatReport, translate, formatPhoneNumber, invoiceReceiverPersonalDetail, invoiceReceiverPolicy)
+ : getDisplayNameForParticipant({
+ accountID: managerID,
+ shouldUseShortForm: true,
+ formatPhoneNumber,
+ hiddenTranslation: translate('common.hidden'),
+ });
}
-
if (isApproved) {
- return translate('iou.managerApproved', payerOrApproverName);
- }
- let paymentVerb: TranslationPaths = 'iou.payerOwes';
- if (iouSettled || iouReport?.isWaitingOnBankAccount) {
- paymentVerb = 'iou.payerPaid';
- } else if (hasNonReimbursableTransactions) {
- paymentVerb = 'iou.payerSpent';
- payerOrApproverName = getDisplayNameForParticipant({
- accountID: chatReport?.ownerAccountID,
- shouldUseShortForm: true,
- formatPhoneNumber,
- hiddenTranslation: translate('common.hidden'),
- });
+ previewMessage = translate('iou.managerApproved', payerOrApproverName);
+ } else {
+ let paymentVerb: TranslationPaths = 'iou.payerOwes';
+ if (iouSettled || iouReport?.isWaitingOnBankAccount) {
+ paymentVerb = 'iou.payerPaid';
+ } else if (hasNonReimbursableTransactions) {
+ paymentVerb = 'iou.payerSpent';
+ payerOrApproverName = getDisplayNameForParticipant({
+ accountID: chatReport?.ownerAccountID,
+ shouldUseShortForm: true,
+ formatPhoneNumber,
+ hiddenTranslation: translate('common.hidden'),
+ });
+ }
+ previewMessage = translate(paymentVerb, payerOrApproverName);
}
- return translate(paymentVerb, payerOrApproverName);
- }, [
- isScanning,
- numberOfPendingRequests,
- numberOfRequests,
- shouldShowRTERViolationMessage,
- isPolicyExpenseChat,
- isTripRoom,
- isInvoiceRoom,
- isApproved,
- iouSettled,
- iouReport?.isWaitingOnBankAccount,
- hasNonReimbursableTransactions,
- translate,
- totalDisplaySpend,
- chatReport,
- policy,
- invoiceReceiverPolicy,
- invoiceReceiverPersonalDetail,
- managerID,
- formatPhoneNumber,
- ]);
+ }
useEffect(() => {
if (!isPaidAnimationRunning || isApprovedAnimationRunning || isSubmittingAnimationRunning) {
diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/useReportPreviewCarousel.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/useReportPreviewCarousel.tsx
index 62554eecdb87..b1dd181c3ff3 100644
--- a/src/components/ReportActionItem/MoneyRequestReportPreview/useReportPreviewCarousel.tsx
+++ b/src/components/ReportActionItem/MoneyRequestReportPreview/useReportPreviewCarousel.tsx
@@ -18,7 +18,7 @@ import type {ViewToken} from '@src/types/utils/ReactNativeCompat';
import type {FlashListRef, ListRenderItem, ListRenderItemInfo} from '@shopify/flash-list';
import type {OnyxEntry} from 'react-native-onyx';
-import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
+import React, {useEffect, useRef, useState} from 'react';
import {View} from 'react-native';
import type {MoneyRequestReportPreviewStyleType} from './types';
@@ -84,10 +84,10 @@ function useReportPreviewCarousel({
const [ownerLogin] = usePersonalDetail(iouReport?.ownerAccountID, loginSelector);
const isFocusedRef = useIsFocusedRef();
- const sortedTransactions = useMemo(() => {
- if (shouldShowAccessPlaceHolder) {
- return [];
- }
+ let sortedTransactions: Transaction[];
+ if (shouldShowAccessPlaceHolder) {
+ sortedTransactions = [];
+ } else {
const sorted = [...transactions].sort((a, b) => {
const rbrComparison = compareByRBR(
a,
@@ -105,10 +105,10 @@ function useReportPreviewCarousel({
// Tiebreak by date (ascending — oldest first) so position is stable across RBR state changes
return localeCompare(getCreated(a), getCreated(b));
});
- return sorted;
- }, [shouldShowAccessPlaceHolder, transactions, transactionViolations, currentUserDetails?.login, currentUserDetails?.accountID, iouReport, ownerLogin, policy, localeCompare]);
+ sortedTransactions = sorted;
+ }
- const carouselTransactions = useMemo(() => sortedTransactions.slice(0, MAX_PREVIEWS_NUMBER + 1), [sortedTransactions]);
+ const carouselTransactions = sortedTransactions.slice(0, MAX_PREVIEWS_NUMBER + 1);
const prevCarouselTransactionLength = useRef(0);
useEffect(() => {
@@ -129,9 +129,9 @@ function useReportPreviewCarousel({
// Expose a callback ref instead of the ref object so the ref does not flow through the hook's return value
// (React Compiler forbids reading/passing refs during render).
- const setCarouselRef = useCallback((node: FlashListRef | null) => {
+ const setCarouselRef = (node: FlashListRef | null) => {
carouselRef.current = node;
- }, []);
+ };
const prevTransactionCountForScroll = useRef(carouselTransactions.length);
const [carouselKey, setCarouselKey] = useState(0);
@@ -147,15 +147,11 @@ function useReportPreviewCarousel({
prevTransactionCountForScroll.current = carouselTransactions.length;
}, [carouselTransactions.length]);
- const visibleItemsOnEndCount = useMemo(() => {
- const lastItemWidth = transactions.length > MAX_PREVIEWS_NUMBER ? footerWidth : reportPreviewStyles.transactionPreviewCarouselStyle.width;
- const lastItemWithGap = lastItemWidth + styles.gap2.gap;
- const itemWithGap = reportPreviewStyles.transactionPreviewCarouselStyle.width + styles.gap2.gap;
- return Math.floor((currentWidth - 2 * styles.pl2.paddingLeft - lastItemWithGap) / itemWithGap) + 1;
- }, [transactions.length, footerWidth, reportPreviewStyles.transactionPreviewCarouselStyle.width, styles.gap2.gap, styles.pl2.paddingLeft, currentWidth]);
- const viewabilityConfig = useMemo(() => {
- return {itemVisiblePercentThreshold: 100};
- }, []);
+ const lastItemWidth = transactions.length > MAX_PREVIEWS_NUMBER ? footerWidth : reportPreviewStyles.transactionPreviewCarouselStyle.width;
+ const lastItemWithGap = lastItemWidth + styles.gap2.gap;
+ const itemWithGap = reportPreviewStyles.transactionPreviewCarouselStyle.width + styles.gap2.gap;
+ const visibleItemsOnEndCount = Math.floor((currentWidth - 2 * styles.pl2.paddingLeft - lastItemWithGap) / itemWithGap) + 1;
+ const viewabilityConfig = {itemVisiblePercentThreshold: 100};
const carouselTransactionsRef = useRef(carouselTransactions);
@@ -193,14 +189,14 @@ function useReportPreviewCarousel({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [newTransactionIDs]);
- const onViewableItemsChanged = useCallback(({viewableItems}: {viewableItems: ViewToken[]; changed: ViewToken[]}) => {
+ const onViewableItemsChanged = ({viewableItems}: {viewableItems: ViewToken[]; changed: ViewToken[]}) => {
const newIndex = viewableItems.at(0)?.index;
if (typeof newIndex === 'number') {
setCurrentIndex(newIndex);
}
const viewableItemsIndexes = viewableItems.map((item) => item.index).filter((item): item is number => typeof item === 'number');
setCurrentVisibleItems(viewableItemsIndexes);
- }, []);
+ };
const snapOffsets = carouselTransactions.map((_, index) => index * (reportPreviewStyles.transactionPreviewCarouselStyle.width + styles.transactionsCarouselGap.width));
@@ -266,7 +262,7 @@ function useReportPreviewCarousel({
setOptimisticIndex(undefined);
}, [carouselTransactions.length, currentIndex, currentVisibleItems, currentVisibleItems.length, optimisticIndex, visibleItemsOnEndCount]);
- const adjustScroll = useCallback(() => {
+ const adjustScroll = () => {
// Workaround for a known React Native bug on Android (https://github.com/facebook/react-native/issues/27504):
// When the FlatList is scrolled to the end and the last item is deleted, a blank space is left behind.
// To fix this, we detect when onEndReached is triggered due to an item deletion,
@@ -276,7 +272,7 @@ function useReportPreviewCarousel({
}
prevCarouselTransactionLength.current = carouselTransactions.length;
carouselRef.current?.scrollToEnd();
- }, [carouselTransactions.length]);
+ };
const renderSeparator = () => ;
diff --git a/src/components/ReportActionItem/TaskView.tsx b/src/components/ReportActionItem/TaskView.tsx
index 54d1510b76f8..686a50b3e92e 100644
--- a/src/components/ReportActionItem/TaskView.tsx
+++ b/src/components/ReportActionItem/TaskView.tsx
@@ -47,7 +47,7 @@ import type {OnyxEntry} from 'react-native-onyx';
import {delegateEmailSelector} from '@selectors/Account';
import {hasSeenTourSelector} from '@selectors/Onboarding';
-import React, {useEffect, useMemo} from 'react';
+import React, {useEffect} from 'react';
import {View} from 'react-native';
type TaskViewProps = {
@@ -106,27 +106,21 @@ function TaskView({report, parentReport, action}: TaskViewProps) {
const disableState = !isTaskModifiable;
const isDisableInteractive = disableState || !isOpen;
const accountID = currentUserPersonalDetails?.accountID ?? CONST.DEFAULT_NUMBER_ID;
- const contextMenuStateValue = useMemo(
- () => ({
- anchor: null,
- report,
- action,
- transactionThreadReport: undefined,
- isDisabled: true,
- shouldDisplayContextMenu: false,
- }),
- [report, action],
- );
+ const contextMenuStateValue = {
+ anchor: null,
+ report,
+ action,
+ transactionThreadReport: undefined,
+ isDisabled: true,
+ shouldDisplayContextMenu: false,
+ };
- const contextMenuActionsValue = useMemo(
- () => ({
- checkIfContextMenuActive: () => {},
- onShowContextMenu: (callback: () => void) => callback(),
- }),
- [],
- );
+ const contextMenuActionsValue = {
+ checkIfContextMenuActive: () => {},
+ onShowContextMenu: (callback: () => void) => callback(),
+ };
- const attachmentContextValue = useMemo(() => ({type: CONST.ATTACHMENT_TYPE.ONBOARDING, accountID}), [accountID]);
+ const attachmentContextValue = {type: CONST.ATTACHMENT_TYPE.ONBOARDING, accountID};
return (
diff --git a/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx b/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx
index 66b34ce94dd5..ab8b95e4b99b 100644
--- a/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx
+++ b/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx
@@ -47,7 +47,7 @@ import {cardByIdSelector} from '@src/selectors/Card';
import {getStableReportSelector} from '@src/selectors/Report';
import truncate from 'lodash/truncate';
-import React, {useMemo} from 'react';
+import React from 'react';
import {View} from 'react-native';
import Animated from 'react-native-reanimated';
@@ -85,10 +85,7 @@ function TransactionPreviewContent({
const {convertToDisplayString, getCurrencyDecimals} = useCurrencyListActions();
const {environmentURL} = useEnvironment();
const isParentPolicyExpenseChat = isPolicyExpenseChat(chatReport);
- const transactionDetails = useMemo>(
- () => getTransactionDetails(transaction, undefined, policy, isParentPolicyExpenseChat) ?? {},
- [transaction, policy, isParentPolicyExpenseChat],
- );
+ const transactionDetails: Partial = getTransactionDetails(transaction, undefined, policy, isParentPolicyExpenseChat) ?? {};
const {amount, comment: requestComment, merchant, category, currency: requestCurrency} = transactionDetails;
const [originalTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(transaction?.comment?.originalTransactionID)}`);
// Only when the expense is actually held: the hold is appended to the RBR message on its own, so it must not also be picked
@@ -108,32 +105,25 @@ function TransactionPreviewContent({
const currentUserEmail = currentUserDetails.email ?? '';
const currentUserLogin = currentUserDetails.login;
const currentUserAccountID = currentUserDetails.accountID;
- const transactionPreviewCommonArguments = useMemo(
- () => ({
- iouReport: report,
- iouReportOwnerLogin: ownerLogin,
- policy,
- transaction,
- action,
- isBillSplit,
- violations,
- transactionDetails,
- }),
- [action, report, ownerLogin, policy, isBillSplit, transaction, transactionDetails, violations],
- );
+ const transactionPreviewCommonArguments = {
+ iouReport: report,
+ iouReportOwnerLogin: ownerLogin,
+ policy,
+ transaction,
+ action,
+ isBillSplit,
+ violations,
+ transactionDetails,
+ };
- const conditionals = useMemo(
- () =>
- createTransactionPreviewConditionals({
- ...transactionPreviewCommonArguments,
- areThereDuplicates,
- isReportAPolicyExpenseChat: isParentPolicyExpenseChat,
- currentUserEmail,
- currentUserAccountID,
- reportActions,
- }),
- [areThereDuplicates, transactionPreviewCommonArguments, isParentPolicyExpenseChat, currentUserEmail, currentUserAccountID, reportActions],
- );
+ const conditionals = createTransactionPreviewConditionals({
+ ...transactionPreviewCommonArguments,
+ areThereDuplicates,
+ isReportAPolicyExpenseChat: isParentPolicyExpenseChat,
+ currentUserEmail,
+ currentUserAccountID,
+ reportActions,
+ });
const {shouldShowRBR, shouldShowMerchant, shouldShowSplitShare, shouldShowCategory, shouldShowSkeleton, shouldShowDescription} = conditionals;
@@ -163,19 +153,15 @@ function TransactionPreviewContent({
})
: undefined;
- const previewText = useMemo(
- () =>
- getTransactionPreviewTextAndTranslationPaths({
- dateFnsLocale,
- ...transactionPreviewCommonArguments,
- shouldShowRBR,
- violationMessage,
- reportActions,
- originalTransaction,
- convertToDisplayString,
- }),
- [transactionPreviewCommonArguments, shouldShowRBR, violationMessage, reportActions, originalTransaction, convertToDisplayString, dateFnsLocale],
- );
+ const previewText = getTransactionPreviewTextAndTranslationPaths({
+ dateFnsLocale,
+ ...transactionPreviewCommonArguments,
+ shouldShowRBR,
+ violationMessage,
+ reportActions,
+ originalTransaction,
+ convertToDisplayString,
+ });
const getTranslatedText = (item: TranslationPathOrText) => (item.translationPath ? translate(item.translationPath) : (item.text ?? ''));
// The hold comes last, after whatever else flagged the expense, e.g. "Category missing • This expense was put on hold".
@@ -210,71 +196,43 @@ function TransactionPreviewContent({
const participantAccountIDs = isMoneyRequestAction(action) && isBillSplit ? (getOriginalMessage(action)?.participantAccountIDs ?? []) : [managerID, ownerAccountID];
const isCardTransaction = isCardTransactionUtils(transaction);
- // Compute the from/to data only for IOU reports
- const {from, to} = useMemo(() => {
- if (!shouldShowPayerAndReceiver) {
- return {
- from: undefined,
- to: undefined,
- };
- }
-
- // For IOU or Split, we want the unprocessed amount because it is important whether the amount was positive or negative
- const payerAndReceiver = getIOUPayerAndReceiver(managerID, ownerAccountID, personalDetails, transactionRawAmount);
-
- return {
- from: payerAndReceiver.from,
- to: payerAndReceiver.to,
- };
- }, [managerID, ownerAccountID, personalDetails, shouldShowPayerAndReceiver, transactionRawAmount]);
+ // For IOU or Split, the raw amount determines which participant is paying.
+ const payerAndReceiver = shouldShowPayerAndReceiver ? getIOUPayerAndReceiver(managerID, ownerAccountID, personalDetails, transactionRawAmount) : undefined;
+ const from = payerAndReceiver?.from;
+ const to = payerAndReceiver?.to;
const shouldShowIOUHeader = !!from && !!to;
// If available, retrieve the split share from the splits object of the transaction, if not, display an even share.
const actorAccountID = action?.actorAccountID;
- const splitShare = useMemo(() => {
- if (!shouldShowSplitShare) {
- return 0;
- }
-
+ let splitShare;
+ if (!shouldShowSplitShare) {
+ splitShare = 0;
+ } else {
const splitAmount = transaction?.comment?.splits?.find((split) => split.accountID === sessionAccountID)?.amount;
if (splitAmount !== undefined) {
- return splitAmount;
- }
-
- let originalParticipantCount = participantAccountIDs.length;
-
- if (isBillSplit) {
- // Try to get the participant count from transaction splits data
- const transactionSplitsCount = transaction?.comment?.splits?.length;
- if (transactionSplitsCount && transactionSplitsCount > 0) {
- originalParticipantCount = transactionSplitsCount;
- } else if (isMoneyRequestAction(action)) {
- originalParticipantCount = getOriginalMessage(action)?.participantAccountIDs?.length ?? participantAccountIDs.length;
+ splitShare = splitAmount;
+ } else {
+ let originalParticipantCount = participantAccountIDs.length;
+ if (isBillSplit) {
+ // Try to get the participant count from transaction splits data
+ const transactionSplitsCount = transaction?.comment?.splits?.length;
+ if (transactionSplitsCount && transactionSplitsCount > 0) {
+ originalParticipantCount = transactionSplitsCount;
+ } else if (isMoneyRequestAction(action)) {
+ originalParticipantCount = getOriginalMessage(action)?.participantAccountIDs?.length ?? participantAccountIDs.length;
+ }
}
+ splitShare = calculateAmount(
+ isParentPolicyExpenseChat ? 1 : originalParticipantCount - 1,
+ amount ?? 0,
+ requestCurrency ?? '',
+ actorAccountID === sessionAccountID,
+ false,
+ getCurrencyDecimals,
+ );
}
-
- return calculateAmount(
- isParentPolicyExpenseChat ? 1 : originalParticipantCount - 1,
- amount ?? 0,
- requestCurrency ?? '',
- actorAccountID === sessionAccountID,
- false,
- getCurrencyDecimals,
- );
- }, [
- shouldShowSplitShare,
- isParentPolicyExpenseChat,
- participantAccountIDs.length,
- transaction?.comment?.splits,
- amount,
- requestCurrency,
- sessionAccountID,
- isBillSplit,
- action,
- actorAccountID,
- getCurrencyDecimals,
- ]);
+ }
const animatedHighlightStyle = useAnimatedHighlightStyle({
shouldHighlight,
diff --git a/src/components/ReportActionItem/TransactionPreview/index.tsx b/src/components/ReportActionItem/TransactionPreview/index.tsx
index b1850100660e..37ec736ed04b 100644
--- a/src/components/ReportActionItem/TransactionPreview/index.tsx
+++ b/src/components/ReportActionItem/TransactionPreview/index.tsx
@@ -32,7 +32,7 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type {GestureResponderEvent} from 'react-native';
import {useRoute} from '@react-navigation/native';
-import React, {memo, useCallback, useMemo} from 'react';
+import React from 'react';
import type {TransactionPreviewProps} from './types';
@@ -61,9 +61,9 @@ function TransactionPreview(props: TransactionPreviewProps) {
const personalDetails = usePersonalDetails();
// Get transaction violations for given transaction id from onyx, find duplicated transactions violations and get duplicates
- const allDuplicateIDs = useMemo(() => violations?.find((violation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION)?.data?.duplicates ?? [], [violations]);
+ const allDuplicateIDs = violations?.find((violation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION)?.data?.duplicates ?? [];
const [allDuplicates] = useTransactionsByID(allDuplicateIDs);
- const duplicates = useMemo(() => removeSettledAndApprovedTransactions(allDuplicates ?? []), [allDuplicates]);
+ const duplicates = removeSettledAndApprovedTransactions(allDuplicates ?? []);
const sessionAccountID = session?.accountID;
const areThereDuplicates = allDuplicateIDs.length > 0 && duplicates.length > 0 && allDuplicateIDs.length === duplicates.length;
@@ -80,10 +80,10 @@ function TransactionPreview(props: TransactionPreviewProps) {
showContextMenuForReport(event, contextMenuAnchorRef, contextMenuReportID, contextMenuAction, checkIfContextMenuActive, originalReportID);
};
- const offlineWithFeedbackOnClose = useCallback(() => {
+ const offlineWithFeedbackOnClose = () => {
clearWalletTermsError();
clearIOUError(chatReportID);
- }, [chatReportID]);
+ };
const navigateToReviewFields = () =>
Navigation.navigate(getReviewNavigationRoute(Navigation.getActiveRoute(), route.params?.reportID, transaction, duplicates, policy, policyCategories, policyTags ?? {}, report));
@@ -160,4 +160,4 @@ function TransactionPreview(props: TransactionPreviewProps) {
);
}
-export default memo(TransactionPreview);
+export default TransactionPreview;
diff --git a/src/components/ReportActionItem/TripDetailsView.tsx b/src/components/ReportActionItem/TripDetailsView.tsx
index 6aeea8dcdc94..3bd809ef353c 100644
--- a/src/components/ReportActionItem/TripDetailsView.tsx
+++ b/src/components/ReportActionItem/TripDetailsView.tsx
@@ -34,7 +34,7 @@ import type {OnyxEntry} from 'react-native-onyx';
import {differenceInCalendarDays} from 'date-fns';
import {Str} from 'expensify-common';
-import React, {useCallback, useMemo} from 'react';
+import React from 'react';
import {View} from 'react-native';
type ReservationViewProps = {
@@ -82,25 +82,22 @@ function ReservationView({reservation, transactionID, tripRoomReportID, sequence
const formattedDate = getFormattedDate();
- const bottomDescription = useMemo(() => {
- const code = getTripReservationCode(reservation);
- if (reservation.type === CONST.RESERVATION_TYPE.FLIGHT) {
- const longName = reservation.company?.longName ? `${reservation.company?.longName} • ` : '';
- const shortName = reservation?.company?.shortName ? `${reservation?.company?.shortName} ` : '';
- return `${code}${longName}${shortName}${reservation.route?.number}`;
- }
- if (reservation.type === CONST.RESERVATION_TYPE.HOTEL) {
- return `${code}${StringUtils.removeDoubleQuotes(reservation.start.address)}`;
- }
- if (reservation.type === CONST.RESERVATION_TYPE.CAR) {
- const vendor = reservation.vendor ? `${reservation.vendor} • ` : '';
- return `${vendor}${reservation.start.location}`;
- }
- if (reservation.type === CONST.RESERVATION_TYPE.TRAIN) {
- return reservation.route?.name;
- }
- return StringUtils.removeDoubleQuotes(reservation.start.address) ?? reservation.start.location;
- }, [reservation]);
+ let bottomDescription;
+ const code = getTripReservationCode(reservation);
+ if (reservation.type === CONST.RESERVATION_TYPE.FLIGHT) {
+ const longName = reservation.company?.longName ? `${reservation.company?.longName} • ` : '';
+ const shortName = reservation?.company?.shortName ? `${reservation?.company?.shortName} ` : '';
+ bottomDescription = `${code}${longName}${shortName}${reservation.route?.number}`;
+ } else if (reservation.type === CONST.RESERVATION_TYPE.HOTEL) {
+ bottomDescription = `${code}${StringUtils.removeDoubleQuotes(reservation.start.address)}`;
+ } else if (reservation.type === CONST.RESERVATION_TYPE.CAR) {
+ const vendor = reservation.vendor ? `${reservation.vendor} • ` : '';
+ bottomDescription = `${vendor}${reservation.start.location}`;
+ } else if (reservation.type === CONST.RESERVATION_TYPE.TRAIN) {
+ bottomDescription = reservation.route?.name;
+ } else {
+ bottomDescription = StringUtils.removeDoubleQuotes(reservation.start.address) ?? reservation.start.location;
+ }
const titleTextStyle = isCancelled ? [styles.textStrong, styles.textSupporting] : styles.textStrong;
@@ -193,59 +190,53 @@ function TripDetailsView({tripRoomReport, shouldShowHorizontalRule, tripTransact
const {convertToDisplayString} = useCurrencyListActions();
const {shouldUseNarrowLayout} = useResponsiveLayout();
- const getTripDescription = useCallback(
- (amount: number, currency: string, reservations: ReservationData[]) => {
- const trips = `${reservations.length} ${reservations.length === 1 ? translate('travel.trip') : translate('travel.trips')}`;
- return `${convertToDisplayString(amount, currency)} • ${trips.toLowerCase()}`;
- },
- [convertToDisplayString, translate],
- );
+ const getTripDescription = (amount: number, currency: string, reservations: ReservationData[]) => {
+ const trips = `${reservations.length} ${reservations.length === 1 ? translate('travel.trip') : translate('travel.trips')}`;
+ return `${convertToDisplayString(amount, currency)} • ${trips.toLowerCase()}`;
+ };
- const getTripTitle = useCallback(
- (reservations: ReservationData[]) => {
- if (reservations.length === 0) {
- return '';
- }
+ const getTripTitle = (reservations: ReservationData[]) => {
+ if (reservations.length === 0) {
+ return '';
+ }
- const firstReservation = reservations.at(0)?.reservation;
- const lastReservation = reservations.at(reservations.length - 1)?.reservation;
+ const firstReservation = reservations.at(0)?.reservation;
+ const lastReservation = reservations.at(reservations.length - 1)?.reservation;
- if (!lastReservation || !firstReservation) {
- return '';
- }
+ if (!lastReservation || !firstReservation) {
+ return '';
+ }
- switch (firstReservation?.type) {
- case CONST.RESERVATION_TYPE.FLIGHT: {
- const destinationReservation = reservations.findLast((reservation) => reservation.reservation.legId === firstReservation.legId);
- if (!destinationReservation) {
- return '';
- }
- return `${translate('travel.flightTo')} ${formatTransitLocationLabel(destinationReservation.reservation.end, true)}`;
+ switch (firstReservation?.type) {
+ case CONST.RESERVATION_TYPE.FLIGHT: {
+ const destinationReservation = reservations.findLast((reservation) => reservation.reservation.legId === firstReservation.legId);
+ if (!destinationReservation) {
+ return '';
}
- case CONST.RESERVATION_TYPE.TRAIN:
- if (reservations.length === 2 && firstReservation.start.shortName === lastReservation.end.shortName) {
- return `${translate('travel.trainTo')} ${Str.recapitalize(lastReservation.start.longName ?? '')}`;
- }
- return `${translate('travel.trainTo')} ${Str.recapitalize(lastReservation.end.longName ?? '')}`;
- case CONST.RESERVATION_TYPE.HOTEL: {
- const nights = differenceInCalendarDays(new Date(lastReservation?.end.date), new Date(firstReservation.start.date));
- return `${nights} ${nights > 1 ? translate('travel.nightsIn') : translate('travel.nightIn')} ${Str.recapitalize(firstReservation.start.longName ?? '')}`;
+ return `${translate('travel.flightTo')} ${formatTransitLocationLabel(destinationReservation.reservation.end, true)}`;
+ }
+ case CONST.RESERVATION_TYPE.TRAIN:
+ if (reservations.length === 2 && firstReservation.start.shortName === lastReservation.end.shortName) {
+ return `${translate('travel.trainTo')} ${Str.recapitalize(lastReservation.start.longName ?? '')}`;
}
- case CONST.RESERVATION_TYPE.CAR: {
- const days = differenceInCalendarDays(new Date(lastReservation.end.date), new Date(firstReservation.start.date));
-
- if (days > 0) {
- return `${days} ${days > 1 ? translate('common.days') : translate('common.day')}${translate('travel.carRental')}`;
- }
+ return `${translate('travel.trainTo')} ${Str.recapitalize(lastReservation.end.longName ?? '')}`;
+ case CONST.RESERVATION_TYPE.HOTEL: {
+ const nights = differenceInCalendarDays(new Date(lastReservation?.end.date), new Date(firstReservation.start.date));
+ return `${nights} ${nights > 1 ? translate('travel.nightsIn') : translate('travel.nightIn')} ${Str.recapitalize(firstReservation.start.longName ?? '')}`;
+ }
+ case CONST.RESERVATION_TYPE.CAR: {
+ const days = differenceInCalendarDays(new Date(lastReservation.end.date), new Date(firstReservation.start.date));
- return `${DateUtils.getFormattedDurationBetweenDates(translate, new Date(firstReservation.start.date), new Date(lastReservation.end.date))}${translate('travel.carRental')}`;
+ if (days > 0) {
+ return `${days} ${days > 1 ? translate('common.days') : translate('common.day')}${translate('travel.carRental')}`;
}
- default:
- return '';
+
+ return `${DateUtils.getFormattedDurationBetweenDates(translate, new Date(firstReservation.start.date), new Date(lastReservation.end.date))}${translate('travel.carRental')}`;
}
- },
- [translate],
- );
+ default:
+ return '';
+ }
+ };
const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${tripRoomReport?.reportID}`);
diff --git a/src/components/ReportActionItem/TripRoomPreview.tsx b/src/components/ReportActionItem/TripRoomPreview.tsx
index 2e892fa12f01..dd178dde2557 100644
--- a/src/components/ReportActionItem/TripRoomPreview.tsx
+++ b/src/components/ReportActionItem/TripRoomPreview.tsx
@@ -34,7 +34,7 @@ import type {ListRenderItemInfo, StyleProp, ViewStyle} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {Str} from 'expensify-common';
-import React, {useMemo} from 'react';
+import React from 'react';
import {FlatList, View} from 'react-native';
type TripRoomPreviewProps = {
@@ -142,16 +142,12 @@ function TripRoomPreview({action, containerStyles, isHovered = false}: TripRoomP
const {totalDisplaySpend = 0, currency = reportCurrency} = chatReport ? getTripTotal(chatReport, reportNameValuePairs) : {};
- const displayAmount = useMemo(() => {
- if (totalDisplaySpend) {
- return convertToDisplayString(totalDisplaySpend, currency);
- }
-
- return convertToDisplayString(
- tripTransactions?.reduce((acc, transaction) => acc + Math.abs(transaction.amount), 0),
- currency,
- );
- }, [convertToDisplayString, currency, totalDisplaySpend, tripTransactions]);
+ const displayAmount = totalDisplaySpend
+ ? convertToDisplayString(totalDisplaySpend, currency)
+ : convertToDisplayString(
+ tripTransactions?.reduce((acc, transaction) => acc + Math.abs(transaction.amount), 0),
+ currency,
+ );
const navigateToTrip = () => Navigation.navigate(getReportRouteForCurrentContext({reportID: chatReportID}));
const renderItem = ({item}: ListRenderItemInfo) => (
diff --git a/src/components/ReportSearchHeader/index.tsx b/src/components/ReportSearchHeader/index.tsx
index ca6aada11ab3..7470490a8fff 100755
--- a/src/components/ReportSearchHeader/index.tsx
+++ b/src/components/ReportSearchHeader/index.tsx
@@ -5,7 +5,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
-import React, {useMemo} from 'react';
+import React from 'react';
import {View} from 'react-native';
import type ReportSearchHeaderProps from './types';
@@ -14,40 +14,24 @@ function ReportSearchHeader({report, style, transactions, avatarBorderColor, isT
const styles = useThemeStyles();
const {isLargeScreenWidth} = useResponsiveLayout();
- const statusContainerStyle = useMemo(() => {
- return [isLargeScreenWidth ? styles.mt1 : styles.mt0Half, report?.shouldShowStatusAsPending && styles.offlineFeedbackPending];
- }, [isLargeScreenWidth, styles.mt1, styles.mt0Half, report?.shouldShowStatusAsPending, styles.offlineFeedbackPending]);
-
- const middleContent = useMemo(() => {
- return (
-
- );
- }, [
- report,
- transactions,
- avatarBorderColor,
- isTitleSelectable,
- styles.fontWeightNormal,
- styles.textLineHeightNormal,
- styles.minHeight4,
- styles.mt1,
- isLargeScreenWidth,
- styles.textMicro,
- statusContainerStyle,
- ]);
+ const statusContainerStyle = [isLargeScreenWidth ? styles.mt1 : styles.mt0Half, report?.shouldShowStatusAsPending && styles.offlineFeedbackPending];
+
+ const middleContent = (
+
+ );
return (
{
- host?.openReportSubmitToPopover(reportID, options);
- },
- [host],
- );
+ return (reportID: string | undefined, options?: ReportSubmitToPopoverOpenOptions) => {
+ host?.openReportSubmitToPopover(reportID, options);
+ };
}
type ReportSubmitToPopoverRootProps = {
@@ -97,7 +94,7 @@ function ReportSubmitToPopoverHost({children, anchorAlignment}: ReportSubmitToPo
const [activeReportID, setActiveReportID] = useState();
const pendingOpenRef = useRef<{reportID: string; options?: ReportSubmitToPopoverOpenOptions; anchorRef?: RefObject | null>} | null>(null);
- const registerAnchor = useCallback((reportID: string | undefined, anchorRef: RefObject | null>) => {
+ const registerAnchor = (reportID: string | undefined, anchorRef: RefObject | null>) => {
if (!reportID) {
return () => {};
}
@@ -120,9 +117,9 @@ function ReportSubmitToPopoverHost({children, anchorAlignment}: ReportSubmitToPo
anchorRegistryRef.current.delete(reportID);
}
};
- }, []);
+ };
- const getAnchorRef = useCallback(() => {
+ const getAnchorRef = () => {
if (activeAnchorRefRef.current) {
return activeAnchorRefRef.current;
}
@@ -137,7 +134,7 @@ function ReportSubmitToPopoverHost({children, anchorAlignment}: ReportSubmitToPo
}
return anchors.values().next().value ?? null;
- }, [activeReportID]);
+ };
const {
reportSubmitToPopover,
@@ -151,34 +148,28 @@ function ReportSubmitToPopoverHost({children, anchorAlignment}: ReportSubmitToPo
getAnchorRef,
});
- const searchSubmitPopoverGuard = useMemo(
- () => ({
- isReportSubmitToPopoverVisible,
- isReportSubmitToDismissGuardActive,
- shouldDisableSearchSubmitPress: isReportSubmitToPopoverVisible || isReportSubmitToDismissGuardActive,
- consumeIgnoreNextSearchSubmitPress,
- }),
- [isReportSubmitToPopoverVisible, isReportSubmitToDismissGuardActive, consumeIgnoreNextSearchSubmitPress],
- );
+ const searchSubmitPopoverGuard = {
+ isReportSubmitToPopoverVisible,
+ isReportSubmitToDismissGuardActive,
+ shouldDisableSearchSubmitPress: isReportSubmitToPopoverVisible || isReportSubmitToDismissGuardActive,
+ consumeIgnoreNextSearchSubmitPress,
+ };
- const openReportSubmitToPopoverForHost = useCallback(
- (reportID: string | undefined, options?: ReportSubmitToPopoverOpenOptions, anchorRef?: RefObject | null>) => {
- if (!reportID) {
- return;
- }
+ const openReportSubmitToPopoverForHost = (reportID: string | undefined, options?: ReportSubmitToPopoverOpenOptions, anchorRef?: RefObject | null>) => {
+ if (!reportID) {
+ return;
+ }
- activeAnchorRefRef.current = anchorRef ?? null;
+ activeAnchorRefRef.current = anchorRef ?? null;
- if (activeReportID === reportID) {
- openPopoverForActiveReport(options);
- return;
- }
+ if (activeReportID === reportID) {
+ openPopoverForActiveReport(options);
+ return;
+ }
- pendingOpenRef.current = {reportID, options, anchorRef};
- setActiveReportID(reportID);
- },
- [activeReportID, openPopoverForActiveReport],
- );
+ pendingOpenRef.current = {reportID, options, anchorRef};
+ setActiveReportID(reportID);
+ };
useLayoutEffect(() => {
const pending = pendingOpenRef.current;
@@ -191,13 +182,10 @@ function ReportSubmitToPopoverHost({children, anchorAlignment}: ReportSubmitToPo
openPopoverForActiveReport(pending.options);
}, [activeReportID, openPopoverForActiveReport]);
- const hostContextValue = useMemo(
- () => ({
- registerAnchor,
- openReportSubmitToPopover: openReportSubmitToPopoverForHost,
- }),
- [registerAnchor, openReportSubmitToPopoverForHost],
- );
+ const hostContextValue = {
+ registerAnchor,
+ openReportSubmitToPopover: openReportSubmitToPopoverForHost,
+ };
return (
@@ -240,12 +228,9 @@ function ReportSubmitToPopoverRootWithHost({reportID, host, children}: {reportID
useEffect(() => host.registerAnchor(reportID, anchorRef), [host, reportID]);
- const openReportSubmitToPopover = useCallback(
- (options?: ReportSubmitToPopoverOpenOptions) => {
- host.openReportSubmitToPopover(reportID, options, anchorRef);
- },
- [host, reportID, anchorRef],
- );
+ const openReportSubmitToPopover = (options?: ReportSubmitToPopoverOpenOptions) => {
+ host.openReportSubmitToPopover(reportID, options, anchorRef);
+ };
return (
@@ -262,15 +247,12 @@ function ReportSubmitToPopoverRootWithLocalPopover({reportID, onSubmitSuccess, a
anchorAlignment,
});
- const searchSubmitPopoverGuard = useMemo(
- () => ({
- isReportSubmitToPopoverVisible,
- isReportSubmitToDismissGuardActive,
- shouldDisableSearchSubmitPress: isReportSubmitToPopoverVisible || isReportSubmitToDismissGuardActive,
- consumeIgnoreNextSearchSubmitPress,
- }),
- [isReportSubmitToPopoverVisible, isReportSubmitToDismissGuardActive, consumeIgnoreNextSearchSubmitPress],
- );
+ const searchSubmitPopoverGuard = {
+ isReportSubmitToPopoverVisible,
+ isReportSubmitToDismissGuardActive,
+ shouldDisableSearchSubmitPress: isReportSubmitToPopoverVisible || isReportSubmitToDismissGuardActive,
+ consumeIgnoreNextSearchSubmitPress,
+ };
return (
diff --git a/src/components/RequireFieldsRules/FieldRequirementSettingRow.tsx b/src/components/RequireFieldsRules/FieldRequirementSettingRow.tsx
index dd851517f860..830fd4644363 100644
--- a/src/components/RequireFieldsRules/FieldRequirementSettingRow.tsx
+++ b/src/components/RequireFieldsRules/FieldRequirementSettingRow.tsx
@@ -28,7 +28,7 @@ import type {RequireFieldsRuleForm, RequireFieldsRuleSettingFieldKey} from '@src
import type {PolicyCategory} from '@src/types/onyx';
import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
-import React, {useCallback} from 'react';
+import React from 'react';
import {View} from 'react-native';
import FieldRequirementsDirectionToggle from './FieldRequirementsDirectionToggle';
@@ -78,18 +78,15 @@ function FieldRequirementSettingRow({
!isLoadingOnyxValue(dismissedProductTrainingMetadata) &&
!isProductTrainingElementDismissed(couplingTooltipName, dismissedProductTraining);
- const hideCouplingTooltip = useCallback(
- (isDismissedUsingCloseButton = false) => {
- if (!couplingTooltipName) {
- return;
- }
+ const hideCouplingTooltip = (isDismissedUsingCloseButton = false) => {
+ if (!couplingTooltipName) {
+ return;
+ }
- dismissProductTraining(couplingTooltipName, isDismissedUsingCloseButton);
- },
- [couplingTooltipName],
- );
+ dismissProductTraining(couplingTooltipName, isDismissedUsingCloseButton);
+ };
- const renderCouplingTooltipContent = useCallback(() => {
+ const renderCouplingTooltipContent = () => {
return (
@@ -119,24 +116,7 @@ function FieldRequirementSettingRow({
);
- }, [
- couplingTooltip,
- expensifyIcons.Close,
- expensifyIcons.Lightbulb,
- hideCouplingTooltip,
- styles.alignItemsCenter,
- styles.flexRow,
- styles.flexShrink1,
- styles.gap3,
- styles.justifyContentCenter,
- styles.ph2,
- styles.productTrainingTooltipText,
- styles.pv2,
- styles.textAlignCenter,
- theme.icon,
- theme.tooltipHighlightText,
- translate,
- ]);
+ };
const handleSelectSetting = (newSetting: FieldRequirementsDirection | undefined) => {
if (!canWriteRules || isCouplingDisabled) {
diff --git a/src/components/RoomHeaderAvatars.tsx b/src/components/RoomHeaderAvatars.tsx
index 054c482c5149..2212656ad809 100644
--- a/src/components/RoomHeaderAvatars.tsx
+++ b/src/components/RoomHeaderAvatars.tsx
@@ -18,7 +18,7 @@ import type {Icon} from '@src/types/onyx/OnyxCommon';
import type {OnyxEntry} from 'react-native-onyx';
-import React, {memo} from 'react';
+import React from 'react';
import {View} from 'react-native';
import AvatarFromIcon from './Avatar/AvatarFromIcon';
@@ -179,4 +179,4 @@ function RoomHeaderAvatars({icons, report, policy, participants, currentUserAcco
);
}
-export default memo(RoomHeaderAvatars);
+export default RoomHeaderAvatars;
diff --git a/src/components/ScreenWrapper/ScreenWrapperOfflineIndicators.tsx b/src/components/ScreenWrapper/ScreenWrapperOfflineIndicators.tsx
index 2b9945d9dae7..77d530e66402 100644
--- a/src/components/ScreenWrapper/ScreenWrapperOfflineIndicators.tsx
+++ b/src/components/ScreenWrapper/ScreenWrapperOfflineIndicators.tsx
@@ -12,7 +12,7 @@ import CONST from '@src/CONST';
import type {ReactNode} from 'react';
import type {StyleProp, ViewStyle} from 'react-native';
-import React, {useMemo} from 'react';
+import React from 'react';
import {View} from 'react-native';
type ScreenWrapperOfflineIndicatorsProps = {
@@ -51,7 +51,7 @@ function ScreenWrapperOfflineIndicators({
const {isOffline} = useNetwork();
const {insets} = useSafeAreaPaddings(true);
- const navigationBarType = useMemo(() => StyleUtils.getNavigationBarType(insets), [StyleUtils, insets]);
+ const navigationBarType = StyleUtils.getNavigationBarType(insets);
const isSoftKeyNavigation = navigationBarType === CONST.NAVIGATION_BAR_TYPE.SOFT_KEYS;
/**
@@ -61,13 +61,13 @@ function ScreenWrapperOfflineIndicators({
* By default, the background color of the small screen offline indicator is translucent.
* If `isOfflineIndicatorTranslucent` is set to true, an opaque background color is applied.
*/
- const smallScreenOfflineIndicatorBackgroundStyle = useMemo(() => {
- const showOfflineIndicatorBackground = !extraContent && (isSoftKeyNavigation || isOffline);
- if (!showOfflineIndicatorBackground) {
- return undefined;
- }
- return isOfflineIndicatorTranslucent ? styles.translucentNavigationBarBG : styles.appBG;
- }, [extraContent, isOffline, isOfflineIndicatorTranslucent, isSoftKeyNavigation, styles.appBG, styles.translucentNavigationBarBG]);
+ let smallScreenOfflineIndicatorBackgroundStyle;
+ const showOfflineIndicatorBackground = !extraContent && (isSoftKeyNavigation || isOffline);
+ if (!showOfflineIndicatorBackground) {
+ smallScreenOfflineIndicatorBackgroundStyle = undefined;
+ } else {
+ smallScreenOfflineIndicatorBackgroundStyle = isOfflineIndicatorTranslucent ? styles.translucentNavigationBarBG : styles.appBG;
+ }
/**
* This style includes the bottom safe area padding for the small screen offline indicator.
@@ -88,30 +88,18 @@ function ScreenWrapperOfflineIndicators({
* It always applies the bottom safe area padding as well as the background style, if the device has soft keys.
* In this case, we want the whole container (including the bottom safe area padding) to have translucent/opaque background.
*/
- const smallScreenOfflineIndicatorContainerStyle = useMemo(
- () => [
- smallScreenOfflineIndicatorBottomSafeAreaStyle,
- shouldSmallScreenOfflineIndicatorStickToBottom && styles.stickToBottom,
- !isSoftKeyNavigation && smallScreenOfflineIndicatorBackgroundStyle,
- ],
- [
- smallScreenOfflineIndicatorBottomSafeAreaStyle,
- shouldSmallScreenOfflineIndicatorStickToBottom,
- styles.stickToBottom,
- isSoftKeyNavigation,
- smallScreenOfflineIndicatorBackgroundStyle,
- ],
- );
+ const smallScreenOfflineIndicatorContainerStyle = [
+ smallScreenOfflineIndicatorBottomSafeAreaStyle,
+ shouldSmallScreenOfflineIndicatorStickToBottom && styles.stickToBottom,
+ !isSoftKeyNavigation && smallScreenOfflineIndicatorBackgroundStyle,
+ ];
/**
* This style includes the styles applied to the small screen offline indicator component.
* If the device has soft keys, we only want to apply the background style to the small screen offline indicator component,
* rather than the whole container, because otherwise the navigation bar would be extra opaque, since it already has a translucent background.
*/
- const smallScreenOfflineIndicatorStyle = useMemo(
- () => [styles.pl5, isSoftKeyNavigation && smallScreenOfflineIndicatorBackgroundStyle, offlineIndicatorStyle],
- [isSoftKeyNavigation, smallScreenOfflineIndicatorBackgroundStyle, offlineIndicatorStyle, styles.pl5],
- );
+ const smallScreenOfflineIndicatorStyle = [styles.pl5, isSoftKeyNavigation && smallScreenOfflineIndicatorBackgroundStyle, offlineIndicatorStyle];
return (
<>
diff --git a/src/components/ScreenWrapper/index.tsx b/src/components/ScreenWrapper/index.tsx
index a1644c687ecb..82481fca50ea 100644
--- a/src/components/ScreenWrapper/index.tsx
+++ b/src/components/ScreenWrapper/index.tsx
@@ -38,7 +38,7 @@ import type {EdgeInsets} from 'react-native-safe-area-context';
import {NavigationRouteContext, useFocusEffect, useIsFocused, useNavigation, usePreventRemove} from '@react-navigation/native';
import {isSingleNewDotEntrySelector} from '@selectors/HybridApp';
-import React, {createContext, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
+import React, {createContext, useCallback, useContext, useEffect, useRef, useState} from 'react';
import {DeviceEventEmitter, Keyboard} from 'react-native';
import type {ScreenWrapperContainerProps} from './ScreenWrapperContainer';
@@ -143,10 +143,7 @@ function ScreenWrapper({
// We disable legacy bottom safe area padding handling, if we are using edge-to-edge mode.
const includeSafeAreaPaddingBottom = isUsingEdgeToEdgeMode ? false : includeSafeAreaPaddingBottomProp;
const isSafeAreaTopPaddingApplied = includePaddingTop;
- const statusContextValue = useMemo(
- () => ({didScreenTransitionEnd, isSafeAreaTopPaddingApplied, isSafeAreaBottomPaddingApplied: includeSafeAreaPaddingBottom}),
- [didScreenTransitionEnd, includeSafeAreaPaddingBottom, isSafeAreaTopPaddingApplied],
- );
+ const statusContextValue = {didScreenTransitionEnd, isSafeAreaTopPaddingApplied, isSafeAreaBottomPaddingApplied: includeSafeAreaPaddingBottom};
// This context allows us to disable the safe area padding offsetting the offline indicator in scrollable components like 'ScrollView', 'SelectionList' or 'FormProvider'.
// This is useful e.g. for the RightModalNavigator, where we want to avoid the safe area padding offsetting the offline indicator because we only show the offline indicator on small screens.
@@ -155,17 +152,14 @@ function ScreenWrapper({
const shouldMoveAccessibilityFocus = isMobileWebNarrowLayout && isInNarrowPane;
const shouldHideFromAccessibility = isMobileWebNarrowLayout && !isFocused;
const {addSafeAreaPadding, showOnSmallScreens, showOnWideScreens} = useContext(ScreenWrapperOfflineIndicatorContext);
- const offlineIndicatorContextValue = useMemo(() => {
- const newAddSafeAreaPadding = isInNarrowPane ? isSmallScreenWidth : addSafeAreaPadding;
-
- return {
- // Allows for individual screens to disable the offline indicator safe area padding for the screen and all nested ScreenWrapper components.
- addSafeAreaPadding: disableOfflineIndicatorSafeAreaPadding === undefined ? (newAddSafeAreaPadding ?? true) : !disableOfflineIndicatorSafeAreaPadding,
- // Prevent any nested ScreenWrapper components from rendering another offline indicator.
- showOnSmallScreens: false,
- showOnWideScreens: false,
- };
- }, [addSafeAreaPadding, disableOfflineIndicatorSafeAreaPadding, isInNarrowPane, isSmallScreenWidth]);
+ const newAddSafeAreaPadding = isInNarrowPane ? isSmallScreenWidth : addSafeAreaPadding;
+ const offlineIndicatorContextValue = {
+ // Allows for individual screens to disable the offline indicator safe area padding for the screen and all nested ScreenWrapper components.
+ addSafeAreaPadding: disableOfflineIndicatorSafeAreaPadding === undefined ? (newAddSafeAreaPadding ?? true) : !disableOfflineIndicatorSafeAreaPadding,
+ // Prevent any nested ScreenWrapper components from rendering another offline indicator.
+ showOnSmallScreens: false,
+ showOnWideScreens: false,
+ };
/** If there is no bottom content, the mobile offline indicator will stick to the bottom of the screen by default. */
const displayStickySmallScreenOfflineIndicator = shouldSmallScreenOfflineIndicatorStickToBottom && !bottomContent;
@@ -279,12 +273,7 @@ function ScreenWrapper({
}, [navigation]),
);
- const ChildrenContent = useMemo(() => {
- return (
- // If props.children is a function, call it to provide the insets to the children.
- typeof children === 'function' ? children({insets, safeAreaPaddingBottomStyle, didScreenTransitionEnd}) : children
- );
- }, [children, insets, safeAreaPaddingBottomStyle, didScreenTransitionEnd]);
+ const ChildrenContent = typeof children === 'function' ? children({insets, safeAreaPaddingBottomStyle, didScreenTransitionEnd}) : children;
return (
diff --git a/src/components/ScrollOffsetContextProvider.tsx b/src/components/ScrollOffsetContextProvider.tsx
index 49792e8d05c5..9f385744da96 100644
--- a/src/components/ScrollOffsetContextProvider.tsx
+++ b/src/components/ScrollOffsetContextProvider.tsx
@@ -11,7 +11,7 @@ import SCREENS from '@src/SCREENS';
import type {ParamListBase} from '@react-navigation/native';
import {findFocusedRoute} from '@react-navigation/native';
-import React, {createContext, useCallback, useEffect, useMemo, useRef} from 'react';
+import React, {createContext, useEffect, useRef} from 'react';
type ScrollOffsetContextValue = {
/** Save scroll offset of FlashList on given screen */
@@ -82,18 +82,18 @@ function ScrollOffsetContextProvider({children}: ScrollOffsetContextProviderProp
}
}, [priorityMode, previousPriorityMode, inboxTab, previousInboxTab]);
- const saveScrollOffset: ScrollOffsetContextValue['saveScrollOffset'] = useCallback((route, scrollOffset) => {
+ const saveScrollOffset: ScrollOffsetContextValue['saveScrollOffset'] = (route, scrollOffset) => {
scrollOffsetsRef.current[getKey(route)] = scrollOffset;
- }, []);
+ };
- const getScrollOffset: ScrollOffsetContextValue['getScrollOffset'] = useCallback((route) => {
+ const getScrollOffset: ScrollOffsetContextValue['getScrollOffset'] = (route) => {
if (!scrollOffsetsRef.current) {
return;
}
return scrollOffsetsRef.current[getKey(route)];
- }, []);
+ };
- const cleanScrollOffsets = useCallback((keys: string[], shouldDelete: (key: string) => boolean) => {
+ const cleanScrollOffsets = (keys: string[], shouldDelete: (key: string) => boolean) => {
for (const key of keys) {
if (!shouldDelete(key)) {
continue;
@@ -101,53 +101,47 @@ function ScrollOffsetContextProvider({children}: ScrollOffsetContextProviderProp
delete scrollOffsetsRef.current[key];
}
- }, []);
+ };
- const cleanStaleScrollOffsets: ScrollOffsetContextValue['cleanStaleScrollOffsets'] = useCallback(
- (state) => {
- const sidebarRoutes = state.routes.filter((route) => isSidebarScreenName(route.name));
- const existingScreenKeys = new Set(sidebarRoutes.map(getKey));
+ const cleanStaleScrollOffsets: ScrollOffsetContextValue['cleanStaleScrollOffsets'] = (state) => {
+ const sidebarRoutes = state.routes.filter((route) => isSidebarScreenName(route.name));
+ const existingScreenKeys = new Set(sidebarRoutes.map(getKey));
- const focusedRoute = findFocusedRoute(state);
- const routeName = focusedRoute?.name;
+ const focusedRoute = findFocusedRoute(state);
+ const routeName = focusedRoute?.name;
- const isSearchScreen = routeName === SCREENS.SEARCH.ROOT;
- const isSearchMoneyRequestReport =
- routeName === SCREENS.RIGHT_MODAL.EXPENSE_REPORT || routeName === SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT || routeName === SCREENS.RIGHT_MODAL.SEARCH_REPORT;
+ const isSearchScreen = routeName === SCREENS.SEARCH.ROOT;
+ const isSearchMoneyRequestReport =
+ routeName === SCREENS.RIGHT_MODAL.EXPENSE_REPORT || routeName === SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT || routeName === SCREENS.RIGHT_MODAL.SEARCH_REPORT;
- const scrollOffsetKeys = Object.keys(scrollOffsetsRef.current);
+ const scrollOffsetKeys = Object.keys(scrollOffsetsRef.current);
- if (isSearchScreen || isSearchMoneyRequestReport) {
- const currentKey = focusedRoute ? getKey(focusedRoute) : null;
- cleanScrollOffsets(scrollOffsetKeys, (key) => key.startsWith(SCREENS.SEARCH.ROOT) && key !== currentKey && !isSearchMoneyRequestReport);
- return;
- }
- cleanScrollOffsets(scrollOffsetKeys, (key) => !existingScreenKeys.has(key));
- },
- [cleanScrollOffsets],
- );
+ if (isSearchScreen || isSearchMoneyRequestReport) {
+ const currentKey = focusedRoute ? getKey(focusedRoute) : null;
+ cleanScrollOffsets(scrollOffsetKeys, (key) => key.startsWith(SCREENS.SEARCH.ROOT) && key !== currentKey && !isSearchMoneyRequestReport);
+ return;
+ }
+ cleanScrollOffsets(scrollOffsetKeys, (key) => !existingScreenKeys.has(key));
+ };
- const saveScrollIndex: ScrollOffsetContextValue['saveScrollIndex'] = useCallback((route, scrollIndex) => {
+ const saveScrollIndex: ScrollOffsetContextValue['saveScrollIndex'] = (route, scrollIndex) => {
scrollOffsetsRef.current[getKey(route)] = scrollIndex;
- }, []);
+ };
- const getScrollIndex: ScrollOffsetContextValue['getScrollIndex'] = useCallback((route) => {
+ const getScrollIndex: ScrollOffsetContextValue['getScrollIndex'] = (route) => {
if (!scrollOffsetsRef.current) {
return;
}
return scrollOffsetsRef.current[getKey(route)];
- }, []);
-
- const contextValue = useMemo(
- (): ScrollOffsetContextValue => ({
- saveScrollOffset,
- getScrollOffset,
- cleanStaleScrollOffsets,
- saveScrollIndex,
- getScrollIndex,
- }),
- [saveScrollOffset, getScrollOffset, cleanStaleScrollOffsets, saveScrollIndex, getScrollIndex],
- );
+ };
+
+ const contextValue: ScrollOffsetContextValue = {
+ saveScrollOffset,
+ getScrollOffset,
+ cleanStaleScrollOffsets,
+ saveScrollIndex,
+ getScrollIndex,
+ };
return {children};
}
diff --git a/src/components/ScrollViewWithContext.tsx b/src/components/ScrollViewWithContext.tsx
index d44a833c643f..7d5b875b2d89 100644
--- a/src/components/ScrollViewWithContext.tsx
+++ b/src/components/ScrollViewWithContext.tsx
@@ -4,7 +4,7 @@ import type {ComponentRef, ForwardedRef, ReactNode} from 'react';
// eslint-disable-next-line no-restricted-imports
import type {NativeScrollEvent, NativeSyntheticEvent, ScrollView as RNScrollView} from 'react-native';
-import React, {createContext, useMemo, useRef, useState} from 'react';
+import React, {createContext, useRef, useState} from 'react';
import type {ScrollViewProps} from './ScrollView';
@@ -45,13 +45,10 @@ function ScrollViewWithContext({onScroll, scrollEventThrottle, children, ref, ..
setContentOffsetY(event.nativeEvent.contentOffset.y);
};
- const contextValue = useMemo(
- () => ({
- scrollViewRef,
- contentOffsetY,
- }),
- [scrollViewRef, contentOffsetY],
- );
+ const contextValue = {
+ scrollViewRef,
+ contentOffsetY,
+ };
return (
({...getEmptyDateValues(), ...defaultDateValues}), [defaultDateValues]);
+ const normalizedDefaultDateValues = {...getEmptyDateValues(), ...defaultDateValues};
const searchDatePresetFilterBaseRef = useRef(null);
const [selectedDateModifierState, setSelectedDateModifierState] = useState(null);
const [shouldShowRangeError, setShouldShowRangeError] = useState(false);
@@ -114,45 +114,36 @@ function DateFilterBase({
);
}, [normalizedDefaultDateValues, dateFnsLocale]);
- const handleDateValuesChange = useCallback(
- (values: SearchDateValues) => {
- setRangeDisplayText(
- getDateRangeDisplayValueFromFormValue(
- dateFnsLocale,
- values[CONST.SEARCH.DATE_MODIFIERS.RANGE],
- values[CONST.SEARCH.DATE_MODIFIERS.AFTER],
- values[CONST.SEARCH.DATE_MODIFIERS.BEFORE],
- ),
- );
- onDateValuesChange?.(values);
- },
- [onDateValuesChange, dateFnsLocale],
- );
+ const handleDateValuesChange = (values: SearchDateValues) => {
+ setRangeDisplayText(
+ getDateRangeDisplayValueFromFormValue(
+ dateFnsLocale,
+ values[CONST.SEARCH.DATE_MODIFIERS.RANGE],
+ values[CONST.SEARCH.DATE_MODIFIERS.AFTER],
+ values[CONST.SEARCH.DATE_MODIFIERS.BEFORE],
+ ),
+ );
+ onDateValuesChange?.(values);
+ };
const isDateModifierControlled = selectedDateModifierProp !== undefined;
const selectedDateModifier = isDateModifierControlled ? selectedDateModifierProp : selectedDateModifierState;
- const setSelectedDateModifier = useCallback(
- (dateModifier: SearchDateModifier | null) => {
- if (isDateModifierControlled) {
- onSelectDateModifier?.(dateModifier);
- return;
- }
- setSelectedDateModifierState(dateModifier);
- },
- [isDateModifierControlled, onSelectDateModifier],
- );
+ const setSelectedDateModifier = (dateModifier: SearchDateModifier | null) => {
+ if (isDateModifierControlled) {
+ onSelectDateModifier?.(dateModifier);
+ return;
+ }
+ setSelectedDateModifierState(dateModifier);
+ };
- const handleSelectDateModifier = useCallback(
- (dateModifier: SearchDateModifier | null) => {
- setSelectedDateModifier(dateModifier);
- onDateModifierChange?.(!!dateModifier);
- onDateValuesChange?.(searchDatePresetFilterBaseRef.current?.getDateValues() ?? getEmptyDateValues());
- },
- [onDateModifierChange, onDateValuesChange, setSelectedDateModifier],
- );
+ const handleSelectDateModifier = (dateModifier: SearchDateModifier | null) => {
+ setSelectedDateModifier(dateModifier);
+ onDateModifierChange?.(!!dateModifier);
+ onDateValuesChange?.(searchDatePresetFilterBaseRef.current?.getDateValues() ?? getEmptyDateValues());
+ };
- const goBack = useCallback(() => {
+ const goBack = () => {
if (selectedDateModifier) {
if (selectedDateModifier === CONST.SEARCH.DATE_MODIFIERS.RANGE) {
searchDatePresetFilterBaseRef.current?.restoreRangeToEntrySnapshot();
@@ -164,11 +155,11 @@ function DateFilterBase({
}
onBackButtonPress?.();
- }, [onBackButtonPress, onDateModifierChange, selectedDateModifier, setSelectedDateModifier]);
+ };
const computedTitle = getDateModifierTitle(selectedDateModifier, title ?? '', translate);
- const reset = useCallback(() => {
+ const reset = () => {
if (!searchDatePresetFilterBaseRef.current) {
return;
}
@@ -187,9 +178,9 @@ function DateFilterBase({
const dateValues = searchDatePresetFilterBaseRef.current.getDateValues();
setShouldShowRangeError(false);
onReset?.(dateValues);
- }, [onDateModifierChange, onReset, selectedDateModifier, setSelectedDateModifier]);
+ };
- const save = useCallback(() => {
+ const save = () => {
if (!searchDatePresetFilterBaseRef.current) {
return;
}
@@ -210,7 +201,7 @@ function DateFilterBase({
const dateValues = searchDatePresetFilterBaseRef.current.getDateValues();
startWithLoading(() => onSubmit(dateValues));
- }, [onDateModifierChange, onSubmit, selectedDateModifier, setSelectedDateModifier, startWithLoading]);
+ };
useImperativeHandle(
ref,
diff --git a/src/components/Search/FilterComponents/DatePresetFilterBase.tsx b/src/components/Search/FilterComponents/DatePresetFilterBase.tsx
index a3e8e2517eca..c26bd3c05e14 100644
--- a/src/components/Search/FilterComponents/DatePresetFilterBase.tsx
+++ b/src/components/Search/FilterComponents/DatePresetFilterBase.tsx
@@ -19,7 +19,7 @@ import CONST from '@src/CONST';
import type {Ref} from 'react';
import {format} from 'date-fns';
-import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
+import React, {useEffect, useImperativeHandle, useRef, useState} from 'react';
import {View} from 'react-native';
import RangeDatePicker from './RangeDatePicker';
@@ -133,50 +133,41 @@ function DatePresetFilterBase({
const shouldShowHorizontalRule = !!presets?.length;
const customDateTitle = translate('search.filters.date.customDate');
const customRangeTitle = translate('search.filters.date.customRange');
- const normalizedDefaultDateValues = useMemo(() => normalizeDateValues(defaultDateValues), [defaultDateValues]);
+ const normalizedDefaultDateValues = normalizeDateValues(defaultDateValues);
- const getRangeDisplayTextFromDateValues = useCallback(
- (dateValues: SearchDateValues) => {
- const rangeValue = dateValues[CONST.SEARCH.DATE_MODIFIERS.RANGE];
- if (!rangeValue) {
- return '';
- }
- return getDateRangeDisplayValueFromFormValue(dateFnsLocale, rangeValue, dateValues[CONST.SEARCH.DATE_MODIFIERS.AFTER], dateValues[CONST.SEARCH.DATE_MODIFIERS.BEFORE]);
- },
- [dateFnsLocale],
- );
+ const getRangeDisplayTextFromDateValues = (dateValues: SearchDateValues) => {
+ const rangeValue = dateValues[CONST.SEARCH.DATE_MODIFIERS.RANGE];
+ if (!rangeValue) {
+ return '';
+ }
+ return getDateRangeDisplayValueFromFormValue(dateFnsLocale, rangeValue, dateValues[CONST.SEARCH.DATE_MODIFIERS.AFTER], dateValues[CONST.SEARCH.DATE_MODIFIERS.BEFORE]);
+ };
- const getRangeEphemeralValuesFromDateValues = useCallback((dateValues: SearchDateValues) => {
+ const getRangeEphemeralValuesFromDateValues = (dateValues: SearchDateValues) => {
const rangeBoundaries = getRangeBoundariesFromFormValue(dateValues[CONST.SEARCH.DATE_MODIFIERS.RANGE]);
return {
from: rangeBoundaries.from,
to: rangeBoundaries.to,
};
- }, []);
+ };
- const notifyDateValuesChange = useCallback(
- (values: SearchDateValues) => {
- onDateValuesChange?.(values);
- },
- [onDateValuesChange],
- );
+ const notifyDateValuesChange = (values: SearchDateValues) => {
+ onDateValuesChange?.(values);
+ };
const [dateValues, setDateValues] = useState(normalizedDefaultDateValues);
const dateValuesRef = useRef(normalizedDefaultDateValues);
const [focusedPresetKey, setFocusedPresetKey] = useState(null);
- const updateDateValues = useCallback(
- (updater: SearchDateValues | ((prevDateValues: SearchDateValues) => SearchDateValues), shouldNotify = true) => {
- const nextDateValues = typeof updater === 'function' ? updater(dateValuesRef.current) : updater;
- dateValuesRef.current = nextDateValues;
- setDateValues(nextDateValues);
-
- if (shouldNotify) {
- notifyDateValuesChange(nextDateValues);
- }
- },
- [notifyDateValuesChange],
- );
+ const updateDateValues = (updater: SearchDateValues | ((prevDateValues: SearchDateValues) => SearchDateValues), shouldNotify = true) => {
+ const nextDateValues = typeof updater === 'function' ? updater(dateValuesRef.current) : updater;
+ dateValuesRef.current = nextDateValues;
+ setDateValues(nextDateValues);
+
+ if (shouldNotify) {
+ notifyDateValuesChange(nextDateValues);
+ }
+ };
useEffect(() => {
if (isSearchAdvancedFiltersFormLoading) {
@@ -199,58 +190,46 @@ function DatePresetFilterBase({
setDateValues(normalizedDefaultDateValues);
}, [isSearchAdvancedFiltersFormLoading, normalizedDefaultDateValues]);
- const setDateValue = useCallback(
- (dateModifier: SearchDateModifier, value: string | undefined) => {
- updateDateValues((prevDateValues) => {
- if (dateModifier === CONST.SEARCH.DATE_MODIFIERS.ON && isSearchDatePreset(value)) {
- return getExclusiveDateValues(dateModifier, value);
- }
-
- if (dateModifier === CONST.SEARCH.DATE_MODIFIERS.RANGE) {
- return {...prevDateValues, [dateModifier]: value};
- }
-
- if (dateModifier !== CONST.SEARCH.DATE_MODIFIERS.ON && isSearchDatePreset(prevDateValues[CONST.SEARCH.DATE_MODIFIERS.ON])) {
- return {
- ...prevDateValues,
- [dateModifier]: value,
- [CONST.SEARCH.DATE_MODIFIERS.ON]: undefined,
- };
- }
+ const setDateValue = (dateModifier: SearchDateModifier, value: string | undefined) => {
+ updateDateValues((prevDateValues) => {
+ if (dateModifier === CONST.SEARCH.DATE_MODIFIERS.ON && isSearchDatePreset(value)) {
+ return getExclusiveDateValues(dateModifier, value);
+ }
+ if (dateModifier === CONST.SEARCH.DATE_MODIFIERS.RANGE) {
return {...prevDateValues, [dateModifier]: value};
- });
- },
- [updateDateValues],
- );
+ }
- const setExclusiveDateValue = useCallback(
- (dateModifier: SearchDateModifier, value: string | undefined) => {
- updateDateValues(getExclusiveDateValues(dateModifier, value));
- },
- [updateDateValues],
- );
+ if (dateModifier !== CONST.SEARCH.DATE_MODIFIERS.ON && isSearchDatePreset(prevDateValues[CONST.SEARCH.DATE_MODIFIERS.ON])) {
+ return {
+ ...prevDateValues,
+ [dateModifier]: value,
+ [CONST.SEARCH.DATE_MODIFIERS.ON]: undefined,
+ };
+ }
- const dateDisplayValues = useMemo(() => {
- const dateOn = dateValues[CONST.SEARCH.DATE_MODIFIERS.ON];
- const dateAfter = dateValues[CONST.SEARCH.DATE_MODIFIERS.AFTER];
- const dateBefore = dateValues[CONST.SEARCH.DATE_MODIFIERS.BEFORE];
+ return {...prevDateValues, [dateModifier]: value};
+ });
+ };
- return {
- // dateOn could be a preset e.g. Last month which should not be displayed as the On field
- [CONST.SEARCH.DATE_MODIFIERS.ON]: isSearchDatePreset(dateOn) ? undefined : dateOn,
- [CONST.SEARCH.DATE_MODIFIERS.AFTER]: dateAfter,
- [CONST.SEARCH.DATE_MODIFIERS.BEFORE]: dateBefore,
- [CONST.SEARCH.DATE_MODIFIERS.RANGE]: dateValues[CONST.SEARCH.DATE_MODIFIERS.RANGE] ? 'range' : undefined,
- };
- }, [dateValues]);
+ const setExclusiveDateValue = (dateModifier: SearchDateModifier, value: string | undefined) => {
+ updateDateValues(getExclusiveDateValues(dateModifier, value));
+ };
- const getInitialEphemeralDateValue = useCallback((dateModifier: SearchDateModifier | null) => (dateModifier ? dateDisplayValues[dateModifier] : undefined), [dateDisplayValues]);
+ const dateOn = dateValues[CONST.SEARCH.DATE_MODIFIERS.ON];
+ const dateAfter = dateValues[CONST.SEARCH.DATE_MODIFIERS.AFTER];
+ const dateBefore = dateValues[CONST.SEARCH.DATE_MODIFIERS.BEFORE];
+ const dateDisplayValues: SearchDateValues = {
+ // dateOn could be a preset e.g. Last month which should not be displayed as the On field
+ [CONST.SEARCH.DATE_MODIFIERS.ON]: isSearchDatePreset(dateOn) ? undefined : dateOn,
+ [CONST.SEARCH.DATE_MODIFIERS.AFTER]: dateAfter,
+ [CONST.SEARCH.DATE_MODIFIERS.BEFORE]: dateBefore,
+ [CONST.SEARCH.DATE_MODIFIERS.RANGE]: dateValues[CONST.SEARCH.DATE_MODIFIERS.RANGE] ? 'range' : undefined,
+ };
+
+ const getInitialEphemeralDateValue = (dateModifier: SearchDateModifier | null) => (dateModifier ? dateDisplayValues[dateModifier] : undefined);
const [ephemeralDateValue, setEphemeralDateValue] = useState(() => getInitialEphemeralDateValue(selectedDateModifier));
- const resetEphemeralDateValue = useCallback(
- (dateModifier: SearchDateModifier | null) => setEphemeralDateValue(getInitialEphemeralDateValue(dateModifier)),
- [getInitialEphemeralDateValue],
- );
+ const resetEphemeralDateValue = (dateModifier: SearchDateModifier | null) => setEphemeralDateValue(getInitialEphemeralDateValue(dateModifier));
const [rangeEphemeralValues, setRangeEphemeralValues] = useState<{from?: string; to?: string}>(() => getRangeEphemeralValuesFromDateValues(normalizedDefaultDateValues));
@@ -268,27 +247,24 @@ function DatePresetFilterBase({
// Used to discard unsaved range picks when the user leaves Range mode without saving.
const rangeEntrySnapshotRef = useRef(undefined);
- const selectDateModifier = useCallback(
- (dateModifier: SearchDateModifier | null) => {
- const isSwitchingBetweenCustomDateModifiers = isCustomDateModifier(selectedDateModifier) && isCustomDateModifier(dateModifier);
- if (!isSwitchingBetweenCustomDateModifiers) {
- resetEphemeralDateValue(dateModifier);
- }
+ const selectDateModifier = (dateModifier: SearchDateModifier | null) => {
+ const isSwitchingBetweenCustomDateModifiers = isCustomDateModifier(selectedDateModifier) && isCustomDateModifier(dateModifier);
+ if (!isSwitchingBetweenCustomDateModifiers) {
+ resetEphemeralDateValue(dateModifier);
+ }
- onRangeValidationErrorChange?.(false);
+ onRangeValidationErrorChange?.(false);
- if (dateModifier === CONST.SEARCH.DATE_MODIFIERS.RANGE) {
- const currentDateValues = dateValuesRef.current;
- rangeEntrySnapshotRef.current = currentDateValues[CONST.SEARCH.DATE_MODIFIERS.RANGE];
- setRangeEphemeralValues(getRangeEphemeralValuesFromDateValues(currentDateValues));
- }
+ if (dateModifier === CONST.SEARCH.DATE_MODIFIERS.RANGE) {
+ const currentDateValues = dateValuesRef.current;
+ rangeEntrySnapshotRef.current = currentDateValues[CONST.SEARCH.DATE_MODIFIERS.RANGE];
+ setRangeEphemeralValues(getRangeEphemeralValuesFromDateValues(currentDateValues));
+ }
- onSelectDateModifier(dateModifier);
- },
- [getRangeEphemeralValuesFromDateValues, onRangeValidationErrorChange, onSelectDateModifier, resetEphemeralDateValue, selectedDateModifier],
- );
+ onSelectDateModifier(dateModifier);
+ };
- const validate = useCallback(() => {
+ const validate = () => {
if (selectedDateModifier !== CONST.SEARCH.DATE_MODIFIERS.RANGE) {
onRangeValidationErrorChange?.(false);
return true;
@@ -297,7 +273,7 @@ function DatePresetFilterBase({
const isValid = !!(rangeEphemeralValues.from && rangeEphemeralValues.to);
onRangeValidationErrorChange?.(!isValid);
return isValid;
- }, [onRangeValidationErrorChange, rangeEphemeralValues.from, rangeEphemeralValues.to, selectedDateModifier]);
+ };
useImperativeHandle(
ref,
@@ -385,18 +361,17 @@ function DatePresetFilterBase({
const rangeDescription = getRangeDisplayTextFromDateValues(dateValues) || undefined;
const customDateModifier = getCustomDateModifierFromDateValues(dateValues, allowedCustomDateModifiers);
- const customDateDescription = useMemo(() => {
- const customDateValue = dateDisplayValues[customDateModifier];
- if (!customDateValue) {
- return undefined;
- }
-
- return `${translate(`common.${customDateModifier.toLowerCase() as SearchDateModifierLower}`)} ${customDateValue}`;
- }, [customDateModifier, dateDisplayValues, translate]);
+ let customDateDescription;
+ const customDateValue = dateDisplayValues[customDateModifier];
+ if (!customDateValue) {
+ customDateDescription = undefined;
+ } else {
+ customDateDescription = `${translate(`common.${customDateModifier.toLowerCase() as SearchDateModifierLower}`)} ${customDateValue}`;
+ }
const handleSingleDateSelected = (date: Date) => {
setEphemeralDateValue(format(date, CONST.DATE.FNS_FORMAT_STRING));
};
- const selectCustomDateMode = useCallback(() => selectDateModifier(customDateModifier), [customDateModifier, selectDateModifier]);
+ const selectCustomDateMode = () => selectDateModifier(customDateModifier);
if (!selectedDateModifier) {
return (
diff --git a/src/components/Search/FilterDropdowns/DateSelectPopup/index.tsx b/src/components/Search/FilterDropdowns/DateSelectPopup/index.tsx
index 7b6ade5671d3..7504e221098a 100644
--- a/src/components/Search/FilterDropdowns/DateSelectPopup/index.tsx
+++ b/src/components/Search/FilterDropdowns/DateSelectPopup/index.tsx
@@ -20,7 +20,7 @@ import CONST from '@src/CONST';
import type {ComponentRef} from 'react';
import type {StyleProp, ViewStyle} from 'react-native';
-import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
+import React, {useEffect, useRef, useState} from 'react';
import {View} from 'react-native';
import SelectedDateModifierHeader from './SelectedDateModifierHeader';
@@ -54,22 +54,18 @@ function DateSelectPopup({label, value, presets, allowedCustomDateModifiers = CO
const [rangeText, setRangeText] = useState(() =>
getDateRangeDisplayValueFromFormValue(dateFnsLocale, value[CONST.SEARCH.DATE_MODIFIERS.RANGE], value[CONST.SEARCH.DATE_MODIFIERS.AFTER], value[CONST.SEARCH.DATE_MODIFIERS.BEFORE]),
);
- const syncedRangeText = useMemo(
- () =>
- getDateRangeDisplayValueFromFormValue(
- dateFnsLocale,
- value[CONST.SEARCH.DATE_MODIFIERS.RANGE],
- value[CONST.SEARCH.DATE_MODIFIERS.AFTER],
- value[CONST.SEARCH.DATE_MODIFIERS.BEFORE],
- ),
- [value, dateFnsLocale],
+ const syncedRangeText = getDateRangeDisplayValueFromFormValue(
+ dateFnsLocale,
+ value[CONST.SEARCH.DATE_MODIFIERS.RANGE],
+ value[CONST.SEARCH.DATE_MODIFIERS.AFTER],
+ value[CONST.SEARCH.DATE_MODIFIERS.BEFORE],
);
const displayedRangeText = selectedDateModifier ? rangeText : syncedRangeText;
const selectedDateModifierTitle = getDateModifierTitle(selectedDateModifier, '', translate);
- const updateRangeText = useCallback(() => {
+ const updateRangeText = () => {
setRangeText(searchDatePresetFilterBaseRef.current?.getRangeDisplayText() ?? '');
- }, []);
+ };
// Widen the popover when Range is selected, reset when not
useEffect(() => {
@@ -80,20 +76,20 @@ function DateSelectPopup({label, value, presets, allowedCustomDateModifiers = CO
}
}, [selectedDateModifier, setPopoverWidth]);
- const clearSelection = useCallback(() => {
+ const clearSelection = () => {
setSelectedDateModifier(null);
setShouldShowRangeError(false);
- }, []);
+ };
- const handleBackPress = useCallback(() => {
+ const handleBackPress = () => {
if (searchDatePresetFilterBaseRef.current && selectedDateModifier === CONST.SEARCH.DATE_MODIFIERS.RANGE) {
searchDatePresetFilterBaseRef.current.restoreRangeToEntrySnapshot();
updateRangeText();
}
clearSelection();
- }, [clearSelection, selectedDateModifier, updateRangeText]);
+ };
- const applyChanges = useCallback(() => {
+ const applyChanges = () => {
if (!searchDatePresetFilterBaseRef.current) {
return;
}
@@ -112,7 +108,7 @@ function DateSelectPopup({label, value, presets, allowedCustomDateModifiers = CO
const dateValues = searchDatePresetFilterBaseRef.current.getDateValues();
onChange(dateValues);
closeOverlay();
- }, [closeOverlay, onChange, selectedDateModifier]);
+ };
const maxPopupHeight = Math.round(windowHeight * 0.875);
diff --git a/src/components/Search/FilterDropdowns/GroupByPopup.tsx b/src/components/Search/FilterDropdowns/GroupByPopup.tsx
index c7f3e99fe4d7..0e3e0741116d 100644
--- a/src/components/Search/FilterDropdowns/GroupByPopup.tsx
+++ b/src/components/Search/FilterDropdowns/GroupByPopup.tsx
@@ -11,7 +11,7 @@ import type {GroupBySection} from '@libs/SearchUIUtils';
import CONST from '@src/CONST';
-import React, {useCallback, useMemo, useState} from 'react';
+import React, {useState} from 'react';
import {View} from 'react-native';
import BasePopup from './BasePopup';
@@ -34,44 +34,38 @@ function GroupByPopup({value, sections, onBackButtonPress, closeOverlay, onChang
const styles = useThemeStyles();
const [selectedItem, setSelectedItem] = useState(value);
- const allSelectableItems = useMemo(() => sections.flatMap((section) => section.options), [sections]);
-
- const listSections = useMemo(
- () =>
- sections.map((section) => ({
- sectionIndex: section.sectionIndex,
- ...(section.sectionIndex > 0 ? {customHeader: } : {}),
- data: section.options.map>((item) => ({
- text: item.text,
- keyForList: item.value,
- isSelected: item.value === selectedItem?.value,
- })),
- })),
- [sections, selectedItem?.value, styles.dividerLine],
- );
-
- const itemCount = Math.max(
- 1,
- listSections.reduce((count, section) => count + section.data.length + (section.data.length > 0 && !!section.customHeader ? 1 : 0), 0),
- );
-
- const updateSelectedItem = useCallback(
- (item: ListItem) => {
- const newItem = allSelectableItems.find((option) => option.value === item.keyForList) ?? null;
- setSelectedItem(newItem);
- },
- [allSelectableItems],
- );
-
- const applyChanges = useCallback(() => {
+ const allSelectableItems = sections.flatMap((section) => section.options);
+
+ const listSections = sections.map((section) => ({
+ sectionIndex: section.sectionIndex,
+ ...(section.sectionIndex > 0 ? {customHeader: } : {}),
+ data: section.options.map>((item) => ({
+ text: item.text,
+ keyForList: item.value,
+ isSelected: item.value === selectedItem?.value,
+ })),
+ }));
+
+ let sectionItemCount = 0;
+ for (const section of listSections) {
+ sectionItemCount += section.data.length + (section.data.length > 0 && !!section.customHeader ? 1 : 0);
+ }
+ const itemCount = Math.max(1, sectionItemCount);
+
+ const updateSelectedItem = (item: ListItem) => {
+ const newItem = allSelectableItems.find((option) => option.value === item.keyForList) ?? null;
+ setSelectedItem(newItem);
+ };
+
+ const applyChanges = () => {
onChange(selectedItem);
closeOverlay();
- }, [closeOverlay, onChange, selectedItem]);
+ };
- const resetChanges = useCallback(() => {
+ const resetChanges = () => {
onChange(null);
closeOverlay();
- }, [closeOverlay, onChange]);
+ };
return (
({
}: SingleSelectPopupProps) {
const [selectedItem, setSelectedItem] = useState(value);
- const applyChanges = useCallback(() => {
+ const applyChanges = () => {
onChange(selectedItem);
closeOverlay();
- }, [closeOverlay, onChange, selectedItem]);
+ };
- const resetChanges = useCallback(() => {
+ const resetChanges = () => {
onChange(items.find((item) => item.value === defaultValue));
closeOverlay();
- }, [closeOverlay, onChange, defaultValue, items]);
+ };
return (
getAllTaxRates(policies), [policies]);
+ const taxRates = getAllTaxRates(policies);
const [isTrackIntentUser] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {selector: isTrackIntentUserSelector});
const {
@@ -279,90 +279,63 @@ function SearchAutocompleteList({
}
}, [isLoadingOptions]);
- const searchOptions = useMemo(() => {
- if (listOptions === null) {
- return defaultListOptions;
- }
- return getSearchOptions({
- dateFnsLocale,
- convertToDisplayString,
- convertToDisplayStringWithoutCurrency,
- options: listOptions,
- draftComments,
- isDefaultRoomsBetaEnabled,
- isUsedInChatFinder: true,
- includeReadOnly: true,
- searchQuery: autocompleteQueryValue,
- maxResults: CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_SUGGESTIONS,
- includeUserToInvite: true,
- includeRecentReports: true,
- includeCurrentUser: true,
- countryCode,
- shouldShowGBR: false,
- shouldUnreadBeBold: true,
- loginList,
- visibleReportActionsData,
- currentUserAccountID,
- currentUserEmail,
- policyCollection: policies,
- personalDetails,
- reportAttributesDerived: reportAttributes,
- sortedActions,
- transactionThreadIDs,
- lastActions,
- currentUserLogin: currentUserEmail,
- localeCompare,
- formatPhoneNumber,
- conciergeReportID,
- allPolicyTags,
- isTrackIntentUser,
- translate,
- getReportByID,
- rules,
- }).options;
- }, [
- listOptions,
- draftComments,
- isDefaultRoomsBetaEnabled,
- autocompleteQueryValue,
- countryCode,
- loginList,
- visibleReportActionsData,
- currentUserAccountID,
- currentUserEmail,
- policies,
- allPolicyTags,
- personalDetails,
- reportAttributes,
- sortedActions,
- transactionThreadIDs,
- lastActions,
- localeCompare,
- formatPhoneNumber,
- conciergeReportID,
- isTrackIntentUser,
- translate,
- getReportByID,
- dateFnsLocale,
- convertToDisplayString,
- convertToDisplayStringWithoutCurrency,
- rules,
- ]);
+ const searchOptions =
+ listOptions === null
+ ? defaultListOptions
+ : getSearchOptions({
+ dateFnsLocale,
+ convertToDisplayString,
+ convertToDisplayStringWithoutCurrency,
+ options: listOptions,
+ draftComments,
+ isDefaultRoomsBetaEnabled,
+ isUsedInChatFinder: true,
+ includeReadOnly: true,
+ searchQuery: autocompleteQueryValue,
+ maxResults: CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_SUGGESTIONS,
+ includeUserToInvite: true,
+ includeRecentReports: true,
+ includeCurrentUser: true,
+ countryCode,
+ shouldShowGBR: false,
+ shouldUnreadBeBold: true,
+ loginList,
+ visibleReportActionsData,
+ currentUserAccountID,
+ currentUserEmail,
+ policyCollection: policies,
+ personalDetails,
+ reportAttributesDerived: reportAttributes,
+ sortedActions,
+ transactionThreadIDs,
+ lastActions,
+ currentUserLogin: currentUserEmail,
+ localeCompare,
+ formatPhoneNumber,
+ conciergeReportID,
+ allPolicyTags,
+ isTrackIntentUser,
+ translate,
+ getReportByID,
+ rules,
+ }).options;
// Deduped once and read everywhere the order is needed, so a repeated reportID ranks at its first position.
- const orderedSearchResultReportIDs = useMemo(() => (searchResultReportIDs?.length ? [...new Set(searchResultReportIDs)] : []), [searchResultReportIDs]);
-
- const serverReportsOptions = useMemo(() => {
- if (!hasActiveSearchResults || listOptions === null || orderedSearchResultReportIDs.length === 0) {
- return CONST.EMPTY_ARRAY;
- }
+ const orderedSearchResultReportIDs: string[] = searchResultReportIDs?.length ? [...new Set(searchResultReportIDs)] : [];
+ let serverReportsOptions;
+ if (!hasActiveSearchResults || listOptions === null || orderedSearchResultReportIDs.length === 0) {
+ serverReportsOptions = CONST.EMPTY_ARRAY;
+ } else {
const orderedReportIDs = orderedSearchResultReportIDs.slice(0, CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_SUGGESTIONS);
const reportIDs = new Set(orderedReportIDs);
const options = getSearchOptions({
dateFnsLocale,
convertToDisplayString,
- options: {reports: listOptions.reports.filter((option) => reportIDs.has(option.reportID)), personalDetails: []},
+ options: {
+ reports: listOptions.reports.filter((option) => reportIDs.has(option.reportID)),
+ personalDetails: [],
+ },
draftComments,
isDefaultRoomsBetaEnabled,
isUsedInChatFinder: true,
@@ -392,30 +365,8 @@ function SearchAutocompleteList({
formatPhoneNumber,
}).options;
const optionsByReportID = new Map(options.recentReports.map((option) => [option.reportID, option]));
- return orderedReportIDs.map((reportID) => optionsByReportID.get(reportID)).filter((option): option is OptionData => !!option && !option.isSelfDM);
- }, [
- hasActiveSearchResults,
- listOptions,
- orderedSearchResultReportIDs,
- dateFnsLocale,
- convertToDisplayString,
- draftComments,
- isDefaultRoomsBetaEnabled,
- countryCode,
- loginList,
- visibleReportActionsData,
- currentUserAccountID,
- currentUserEmail,
- policies,
- personalDetails,
- sortedActions,
- conciergeReportID,
- isTrackIntentUser,
- translate,
- getReportByID,
- rules,
- formatPhoneNumber,
- ]);
+ serverReportsOptions = orderedReportIDs.map((reportID) => optionsByReportID.get(reportID)).filter((option): option is OptionData => !!option && !option.isSelfDM);
+ }
const [isInitialRender, setIsInitialRender] = useState(true);
@@ -505,63 +456,43 @@ function SearchAutocompleteList({
const autocompleteQueryWithoutFilters = getQueryWithoutFilters(autocompleteQueryValue);
- const recentSearchesData = useMemo(() => {
- const sortedRecentSearches = Object.entries(recentSearches ?? {}).sort(([, firstRecentSearch], [, secondRecentSearch]) =>
- localeCompare(secondRecentSearch.timestamp, firstRecentSearch.timestamp),
- );
-
- return sortedRecentSearches.slice(0, 5).map(([recentSearchHash, {query}]) => {
- const searchQueryJSON = buildSearchQueryJSON(query);
- return {
- text: searchQueryJSON
- ? buildUserReadableQueryString({
- queryJSON: searchQueryJSON,
- PersonalDetails: personalDetails,
- reports,
- taxRates,
- cardList: allCards,
- cardFeeds: allFeeds,
- policies,
- currentUserAccountID,
- autoCompleteWithSpace: false,
- translate,
- formatPhoneNumber,
- feedKeysWithCards,
- reportAttributes,
- bankAccountList,
- })
- : query,
- singleIcon: expensifyIcons.History,
- searchQuery: query,
- keyForList: recentSearchHash,
- searchItemType: CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.SEARCH,
- };
- });
- }, [
- recentSearches,
- localeCompare,
- personalDetails,
- reports,
- taxRates,
- allCards,
- allFeeds,
- policies,
- currentUserAccountID,
- translate,
- formatPhoneNumber,
- feedKeysWithCards,
- reportAttributes,
- bankAccountList,
- expensifyIcons.History,
- ]);
+ const sortedRecentSearches = Object.entries(recentSearches ?? {}).sort(([, firstRecentSearch], [, secondRecentSearch]) =>
+ localeCompare(secondRecentSearch.timestamp, firstRecentSearch.timestamp),
+ );
+ const recentSearchesData = sortedRecentSearches.slice(0, 5).map(([recentSearchHash, {query}]) => {
+ const searchQueryJSON = buildSearchQueryJSON(query);
+ return {
+ text: searchQueryJSON
+ ? buildUserReadableQueryString({
+ queryJSON: searchQueryJSON,
+ PersonalDetails: personalDetails,
+ reports,
+ taxRates,
+ cardList: allCards,
+ cardFeeds: allFeeds,
+ policies,
+ currentUserAccountID,
+ autoCompleteWithSpace: false,
+ translate,
+ formatPhoneNumber,
+ feedKeysWithCards,
+ reportAttributes,
+ bankAccountList,
+ })
+ : query,
+ singleIcon: expensifyIcons.History,
+ searchQuery: query,
+ keyForList: recentSearchHash,
+ searchItemType: CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.SEARCH,
+ };
+ });
// The client's own matches, before Auth's order is merged in. Kept separate so the frozen rank below can snapshot
// the client's order: snapshotting the merged list would bake the previous query's server order into "Recent chats".
- const localReportsOptions = useMemo(() => {
- if (!hasActiveSearchResults) {
- return [];
- }
-
+ let localReportsOptions: OptionData[];
+ if (!hasActiveSearchResults) {
+ localReportsOptions = [];
+ } else {
// searchOptions/autocompleteQueryValue are debounced. For a query -> query change this still returns the
// previous query's matches during the debounce window (rows stay visible, preserving focus/Enter/arrow keys).
// For the empty -> query transition hasActiveSearchResults is false until the debounced query lands, so
@@ -570,26 +501,21 @@ function SearchAutocompleteList({
sortByReportTypeInSearch: true,
preferChatRoomsOverThreads: true,
});
-
const reportOptions: OptionData[] = [...orderedOptions.recentReports, ...orderedOptions.personalDetails];
if (searchOptions.userToInvite) {
reportOptions.push(searchOptions.userToInvite);
}
+ localReportsOptions = reportOptions;
+ }
- return reportOptions;
- }, [autocompleteQueryValue, activePolicyID, hasActiveSearchResults, searchOptions]);
-
- const recentReportsOptions = useMemo(() => {
- if (!hasActiveSearchResults) {
- return searchOptions.recentReports.slice(0, CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_SUGGESTIONS);
- }
-
+ let recentReportsOptions;
+ if (!hasActiveSearchResults) {
+ recentReportsOptions = searchOptions.recentReports.slice(0, CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_SUGGESTIONS);
+ } else {
const reportOptions: OptionData[] = [...localReportsOptions];
-
if (orderedSearchResultReportIDs.length > 0) {
const matchedReportIDs = new Set(reportOptions.map((option) => option.reportID).filter(Boolean));
reportOptions.push(...serverReportsOptions.filter((option) => !matchedReportIDs.has(option.reportID)));
-
const rankByReportID = new Map(orderedSearchResultReportIDs.map((reportID, index) => [reportID, index]));
const rankOf = (option: OptionData) => {
if (option.isSelfDM) {
@@ -603,10 +529,11 @@ function SearchAutocompleteList({
// Preserve locally matched rows first. Auth orders only the remaining slots, so a server response cannot
// remove chats, contacts, or invite options that were already visible locally. The combined list remains capped at 20.
if (orderedSearchResultReportIDs.length > 0) {
- return reportOptions;
+ recentReportsOptions = reportOptions;
+ } else {
+ recentReportsOptions = reportOptions.slice(0, CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_SUGGESTIONS);
}
- return reportOptions.slice(0, CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_SUGGESTIONS);
- }, [hasActiveSearchResults, localReportsOptions, orderedSearchResultReportIDs, searchOptions, serverReportsOptions]);
+ }
// Locked rank map (stable key -> originalIndex) capturing the order of the client's own matches at the moment the
// query settled, so reports merged into Onyx later do not shift the rows already visible in the top section.
@@ -675,154 +602,148 @@ function SearchAutocompleteList({
}, [autocompleteQueryWithoutFilters, debounceHandleSearch, handleSearch, hasUpstreamDebounce]);
/* Sections generation */
- const {sections, styledRecentReports, suggestionsCount} = useMemo(() => {
- const nextSections: Array> = [];
- let sectionIndex = 0;
- let nextSuggestionsCount = 0;
-
- const pushSection = (section: Section) => {
- nextSections.push(section);
- nextSuggestionsCount += section.data.filter((item) => item.keyForList !== CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.FIND_ITEM).length;
- };
-
- if (searchQueryItems && searchQueryItems.length > 0) {
- pushSection({data: searchQueryItems as AutocompleteListItem[], sectionIndex: sectionIndex++});
- }
-
- const additionalSections = getAdditionalSections?.(searchOptions, sectionIndex);
-
- if (additionalSections) {
- for (const section of additionalSections) {
- pushSection(section);
- sectionIndex++;
- }
- }
-
- if (!hasEffectiveInputQuery && recentSearchesData && recentSearchesData.length > 0) {
- pushSection({title: translate('search.recentSearches'), data: recentSearchesData as AutocompleteListItem[], sectionIndex: sectionIndex++});
+ const nextSections: Array> = [];
+ let sectionIndex = 0;
+ let nextSuggestionsCount = 0;
+ const pushSection = (section: Section) => {
+ nextSections.push(section);
+ nextSuggestionsCount += section.data.filter((item) => item.keyForList !== CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.FIND_ITEM).length;
+ };
+ if (searchQueryItems && searchQueryItems.length > 0) {
+ pushSection({
+ data: searchQueryItems as AutocompleteListItem[],
+ sectionIndex: sectionIndex++,
+ });
+ }
+ const additionalSections = getAdditionalSections?.(searchOptions, sectionIndex);
+ if (additionalSections) {
+ for (const section of additionalSections) {
+ pushSection(section);
+ sectionIndex++;
}
-
- const nextStyledRecentReports = recentReportsOptions.map((option) => {
- const report = getReportOrDraftReport(option.reportID, undefined, undefined, undefined, reports?.[`${ONYXKEYS.COLLECTION.REPORT}${option.reportID}`]);
- const reportAction = getReportAction(report?.parentReportID, report?.parentReportActionID);
- const shouldParserToHTML = !!reportAction && reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT;
- const keyForList = option.keyForList ?? option.reportID ?? (option.accountID ? String(option.accountID) : undefined);
- return {
- ...option,
- keyForList,
- pressableStyle: styles.br2,
- text: StringUtils.lineBreaksToSpaces(shouldParserToHTML ? Parser.htmlToText(option.text ?? '') : (option.text ?? '')),
- } as AutocompleteListItem;
+ }
+ if (!hasEffectiveInputQuery && recentSearchesData && recentSearchesData.length > 0) {
+ pushSection({
+ title: translate('search.recentSearches'),
+ data: recentSearchesData as AutocompleteListItem[],
+ sectionIndex: sectionIndex++,
});
-
- const skeletonHeader = (
-
- );
-
- if (!hasActiveSearchResults) {
- // No active (debounced) query yet: single "Recent chats" section. This also covers the debounce window
- // right after the user starts typing, so we keep recent chats visible instead of flashing search rows.
- if (!isLoadingOptions) {
- pushSection({title: translate('search.recentChats'), data: nextStyledRecentReports, sectionIndex: sectionIndex++});
- } else {
- pushSection({
- title: translate('search.recentChats'),
- data: [],
- sectionIndex: sectionIndex++,
- customHeader: skeletonHeader,
- });
- }
+ }
+ const nextStyledRecentReports = recentReportsOptions.map((option) => {
+ const report = getReportOrDraftReport(option.reportID, undefined, undefined, undefined, reports?.[`${ONYXKEYS.COLLECTION.REPORT}${option.reportID}`]);
+ const reportAction = getReportAction(report?.parentReportID, report?.parentReportActionID);
+ const shouldParserToHTML = !!reportAction && reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT;
+ const keyForList = option.keyForList ?? option.reportID ?? (option.accountID ? String(option.accountID) : undefined);
+ return {
+ ...option,
+ keyForList,
+ pressableStyle: styles.br2,
+ text: StringUtils.lineBreaksToSpaces(shouldParserToHTML ? Parser.htmlToText(option.text ?? '') : (option.text ?? '')),
+ } as AutocompleteListItem;
+ });
+ const skeletonHeader = (
+
+ );
+ if (!hasActiveSearchResults) {
+ // No active (debounced) query yet: single "Recent chats" section. This also covers the debounce window
+ // right after the user starts typing, so we keep recent chats visible instead of flashing search rows.
+ if (!isLoadingOptions) {
+ pushSection({
+ title: translate('search.recentChats'),
+ data: nextStyledRecentReports,
+ sectionIndex: sectionIndex++,
+ });
} else {
- // Active search uses one shared 20-row budget: preserve locally matched rows first, then fill remaining
- // slots with Auth-ranked server results. Auth orders the server section; it does not evict local rows.
- const localRows: AutocompleteListItem[] = [];
- const serverRows: AutocompleteListItem[] = [];
- for (const item of nextStyledRecentReports) {
- const stableKey = getStableRankKey(item);
- if (item.isSelfDM || (stableKey && frozenLocalRank.has(stableKey))) {
- localRows.push(item);
- } else {
- serverRows.push(item);
- }
- }
- // Sort the local section by the rank captured at query-change time so it cannot
- // reorder when the API returns.
- localRows.sort((a, b) => {
- if (a.isSelfDM !== b.isSelfDM) {
- return a.isSelfDM ? -1 : 1;
- }
- return (frozenLocalRank.get(getStableRankKey(a) ?? '') ?? 0) - (frozenLocalRank.get(getStableRankKey(b) ?? '') ?? 0);
+ pushSection({
+ title: translate('search.recentChats'),
+ data: [],
+ sectionIndex: sectionIndex++,
+ customHeader: skeletonHeader,
});
-
- if (localRows.length > 0 || !isLoadingOptions) {
- pushSection({title: translate('search.recentChats'), data: localRows, sectionIndex: sectionIndex++});
+ }
+ } else {
+ // Active search uses one shared 20-row budget: preserve locally matched rows first, then fill remaining
+ // slots with Auth-ranked server results. Auth orders the server section; it does not evict local rows.
+ const localRows: AutocompleteListItem[] = [];
+ const serverRows: AutocompleteListItem[] = [];
+ for (const item of nextStyledRecentReports) {
+ const stableKey = getStableRankKey(item);
+ if (item.isSelfDM || (stableKey && frozenLocalRank.has(stableKey))) {
+ localRows.push(item);
} else {
- // Options are still loading and no local results matched — show a skeleton so the
- // user sees feedback instead of a bare section header.
- pushSection({
- title: undefined,
- data: [],
- sectionIndex: sectionIndex++,
- customHeader: skeletonHeader,
- });
+ serverRows.push(item);
}
-
- const remainingServerRows = serverRows.slice(0, Math.max(CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_SUGGESTIONS - localRows.length, 0));
- if (remainingServerRows.length > 0) {
- pushSection({
- title: translate('search.serverResults'),
- data: remainingServerRows,
- sectionIndex: sectionIndex++,
- });
+ }
+ // Sort the local section by the rank captured at query-change time so it cannot
+ // reorder when the API returns.
+ localRows.sort((a, b) => {
+ if (a.isSelfDM !== b.isSelfDM) {
+ return a.isSelfDM ? -1 : 1;
}
+ return (frozenLocalRank.get(getStableRankKey(a) ?? '') ?? 0) - (frozenLocalRank.get(getStableRankKey(b) ?? '') ?? 0);
+ });
+ if (localRows.length > 0 || !isLoadingOptions) {
+ pushSection({
+ title: translate('search.recentChats'),
+ data: localRows,
+ sectionIndex: sectionIndex++,
+ });
+ } else {
+ // Options are still loading and no local results matched — show a skeleton so the
+ // user sees feedback instead of a bare section header.
+ pushSection({
+ title: undefined,
+ data: [],
+ sectionIndex: sectionIndex++,
+ customHeader: skeletonHeader,
+ });
}
-
- if (autocompleteSuggestions.length > 0) {
- const autocompleteData: AutocompleteListItem[] = autocompleteSuggestions.map(({filterKey, text, autocompleteID, mapKey, workspaceIcon}) => {
- return {
- text: getAutocompleteDisplayText(filterKey, text),
- mapKey: mapKey ? getSubstitutionMapKey(mapKey, text) : undefined,
- singleIcon: expensifyIcons.MagnifyingGlass,
- searchQuery: text,
- autocompleteID,
- keyForList: autocompleteID ?? text, // in case we have a unique identifier then use it because text might not be unique
- searchItemType: CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.AUTOCOMPLETE_SUGGESTION,
- // For report-backed `in:` suggestions, show the owning workspace on the right of the row so identically
- // named rooms (e.g. #admins) in different workspaces can be told apart.
- rightElement: workspaceIcon ? (
-
- ) : undefined,
- };
+ const remainingServerRows = serverRows.slice(0, Math.max(CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_SUGGESTIONS - localRows.length, 0));
+ if (remainingServerRows.length > 0) {
+ pushSection({
+ title: translate('search.serverResults'),
+ data: remainingServerRows,
+ sectionIndex: sectionIndex++,
});
-
- pushSection({title: translate('search.suggestions'), data: autocompleteData, sectionIndex: sectionIndex++});
}
-
- return {sections: nextSections, styledRecentReports: nextStyledRecentReports, suggestionsCount: nextSuggestionsCount};
- }, [
- hasEffectiveInputQuery,
- hasActiveSearchResults,
- autocompleteSuggestions,
- expensifyIcons,
- frozenLocalRank,
- getAdditionalSections,
- recentReportsOptions,
- recentSearchesData,
- searchOptions,
- searchQueryItems,
- styles,
- translate,
- isLoadingOptions,
- reports,
- ]);
+ }
+ if (autocompleteSuggestions.length > 0) {
+ const autocompleteData: AutocompleteListItem[] = autocompleteSuggestions.map(({filterKey, text, autocompleteID, mapKey, workspaceIcon}) => {
+ return {
+ text: getAutocompleteDisplayText(filterKey, text),
+ mapKey: mapKey ? getSubstitutionMapKey(mapKey, text) : undefined,
+ singleIcon: expensifyIcons.MagnifyingGlass,
+ searchQuery: text,
+ autocompleteID,
+ keyForList: autocompleteID ?? text,
+ // in case we have a unique identifier then use it because text might not be unique
+ searchItemType: CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.AUTOCOMPLETE_SUGGESTION,
+ // For report-backed `in:` suggestions, show the owning workspace on the right of the row so identically
+ // named rooms (e.g. #admins) in different workspaces can be told apart.
+ rightElement: workspaceIcon ? (
+
+ ) : undefined,
+ };
+ });
+ pushSection({
+ title: translate('search.suggestions'),
+ data: autocompleteData,
+ sectionIndex: sectionIndex++,
+ });
+ }
+ const {sections, styledRecentReports, suggestionsCount} = {
+ sections: nextSections,
+ styledRecentReports: nextStyledRecentReports,
+ suggestionsCount: nextSuggestionsCount,
+ };
const trimmedAutocompleteQueryValue = autocompleteQueryValue.trim();
@@ -1002,5 +923,5 @@ function SearchAutocompleteList({
SearchAutocompleteList.displayName = 'SearchAutocompleteList';
-export default React.memo(SearchAutocompleteList);
+export default SearchAutocompleteList;
export type {GetAdditionalSectionsCallback, SearchAutocompleteListProps};
diff --git a/src/components/Search/SearchBulkActionsButton.tsx b/src/components/Search/SearchBulkActionsButton.tsx
index 6e3351afc27a..e27ffc59fa1d 100644
--- a/src/components/Search/SearchBulkActionsButton.tsx
+++ b/src/components/Search/SearchBulkActionsButton.tsx
@@ -29,7 +29,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import {getEmptyObject} from '@src/types/utils/EmptyObject';
-import React, {useContext, useMemo, useRef} from 'react';
+import React, {useContext, useRef} from 'react';
import {View} from 'react-native';
import type {BulkPaySelectionData, SearchQueryJSON, SelectedTransactions} from './types';
@@ -99,32 +99,33 @@ function SearchBulkActionsButton({queryJSON}: SearchBulkActionsButtonProps) {
const isExpenseReportType = queryJSON.type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT;
const popoverUseScrollView = shouldPopoverUseScrollView(dropdownButtonsOptions);
- const {selectedItemsCount, excludedItemsCount} = useMemo(() => {
- const getItemsCount = (transactionsToCount: typeof selectedTransactions, cashBackCount: number) => {
- if (isExpenseReportType) {
- const reportIDs = new Set(
- Object.values(transactionsToCount)
- .map((transaction) => transaction?.reportID)
- .filter((reportID): reportID is string => !!reportID),
- );
- return reportIDs.size;
+ const getItemsCount = (transactionsToCount: typeof selectedTransactions, cashBackCount: number) => {
+ if (isExpenseReportType) {
+ const reportIDs = new Set(
+ Object.values(transactionsToCount)
+ .map((transaction) => transaction?.reportID)
+ .filter((reportID): reportID is string => !!reportID),
+ );
+ return reportIDs.size;
+ }
+ return Object.keys(transactionsToCount).reduce((count, key) => {
+ if (key.startsWith(CONST.SEARCH.GROUP_PREFIX)) {
+ const group = searchData?.[key as keyof typeof searchData] as
+ | {
+ count?: number;
+ isCashBack?: boolean;
+ }
+ | undefined;
+ return count + (group?.isCashBack ? cashBackCount : (group?.count ?? 0));
}
-
- return Object.keys(transactionsToCount).reduce((count, key) => {
- if (key.startsWith(CONST.SEARCH.GROUP_PREFIX)) {
- const group = searchData?.[key as keyof typeof searchData] as {count?: number; isCashBack?: boolean} | undefined;
- return count + (group?.isCashBack ? cashBackCount : (group?.count ?? 0));
- }
- return count + 1;
- }, 0);
- };
-
- return {
- selectedItemsCount: getItemsCount(selectedTransactions, 1),
- // Excluded items come off the server count, which only sums expenses, so a cash back row takes nothing off.
- excludedItemsCount: getItemsCount(excludedTransactions, 0),
- };
- }, [excludedTransactions, selectedTransactions, isExpenseReportType, searchData]);
+ return count + 1;
+ }, 0);
+ };
+ const {selectedItemsCount, excludedItemsCount} = {
+ selectedItemsCount: getItemsCount(selectedTransactions, 1),
+ // Excluded items come off the server count, which only sums expenses, so a cash back row takes nothing off.
+ excludedItemsCount: getItemsCount(excludedTransactions, 0),
+ };
const payBulkSelectedItem = (subItem: PopoverMenuItem, triggerKYCFlow: (kycParams: ContinueActionParams) => void) =>
handleBulkPayItemSelected({
@@ -161,10 +162,10 @@ function SearchBulkActionsButton({queryJSON}: SearchBulkActionsButtonProps) {
// The server count is the only source for how many items "select all" covers, so keep the button loading until it
// arrives. Offline or on error it never will, so fall back to the count of the items we do have selected.
const isAllMatchingItemsCountLoading = areAllMatchingItemsSelected && typeof relevantAllMatchingCount !== 'number' && !isOffline && !hasSearchErrors;
+ // Show the matching-report total minus excluded reports once it lands. Before then, or while offline, fall back
+ // to the loaded-page selected report count.
let selectedAllMatchingItemsCount: number;
if (isExpenseReportType) {
- // Show the matching-report total minus excluded reports once it lands. Before then, or while offline, fall back
- // to the loaded-page selected report count.
selectedAllMatchingItemsCount = typeof allMatchingReportsCount === 'number' ? Math.max(allMatchingReportsCount - excludedItemsCount, 0) : selectedItemsCount;
} else if (typeof allMatchingItemsCount !== 'number') {
selectedAllMatchingItemsCount = selectedItemsCount;
diff --git a/src/components/Search/SearchList/BaseSearchList/index.native.tsx b/src/components/Search/SearchList/BaseSearchList/index.native.tsx
index 6845ec83318c..80c02d552e43 100644
--- a/src/components/Search/SearchList/BaseSearchList/index.native.tsx
+++ b/src/components/Search/SearchList/BaseSearchList/index.native.tsx
@@ -2,7 +2,7 @@ import useTabRootScrollProps from '@components/Navigation/TabBarBottomContent/us
import type {SearchListItem} from '@components/Search/SearchList/ListItem/types';
import {FlashList} from '@shopify/flash-list';
-import React, {useCallback} from 'react';
+import React from 'react';
import Animated from 'react-native-reanimated';
import type BaseSearchListProps from './types';
@@ -25,12 +25,9 @@ function BaseSearchList({
getItemType,
}: BaseSearchListProps) {
const tabRootScrollProps = useTabRootScrollProps(contentContainerStyle);
- const renderItemWithoutKeyboardFocus = useCallback(
- ({item, index}: {item: SearchListItem; index: number}) => {
- return renderItem(item, index, undefined);
- },
- [renderItem],
- );
+ const renderItemWithoutKeyboardFocus = ({item, index}: {item: SearchListItem; index: number}) => {
+ return renderItem(item, index, undefined);
+ };
return (
{
+ const setHasKeyBeenPressed = () => {
if (hasKeyBeenPressed.current) {
return;
}
// We need to track whether a key has been pressed to enable focus syncing only if a key has been pressed.
// This is to avoid the default behavior of web showing blue border on click of items after a page refresh.
hasKeyBeenPressed.current = true;
- }, []);
+ };
const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({
initialFocusedIndex: -1,
@@ -140,31 +140,28 @@ function BaseSearchList({
const renderItemWithKeyboardFocus = ({item, index}: {item: SearchListItem; index: number}) => renderItem(item, index, getOnFocus(index));
- const selectFocusedOption = useCallback(
- (event?: GestureResponderEvent | KeyboardEvent) => {
- // Allow event propagation during cell editing so Enter can trigger TextInput.onSubmitEditing.
- // When not editing, stop propagation to prevent unintended button activation and handle row selection.
- if (isEditingCell) {
- return;
- }
+ const selectFocusedOption = (event?: GestureResponderEvent | KeyboardEvent) => {
+ // Allow event propagation during cell editing so Enter can trigger TextInput.onSubmitEditing.
+ // When not editing, stop propagation to prevent unintended button activation and handle row selection.
+ if (isEditingCell) {
+ return;
+ }
- // If a cell has keyboard focus (via Tab), let the Enter event propagate to trigger the cell's onPress
- if (focusedCellId) {
- return;
- }
+ // If a cell has keyboard focus (via Tab), let the Enter event propagate to trigger the cell's onPress
+ if (focusedCellId) {
+ return;
+ }
- event?.stopPropagation();
+ event?.stopPropagation();
- const focusedItem = data.at(focusedIndex);
+ const focusedItem = data.at(focusedIndex);
- if (!focusedItem) {
- return;
- }
+ if (!focusedItem) {
+ return;
+ }
- onSelectRow(focusedItem);
- },
- [data, focusedCellId, focusedIndex, isEditingCell, onSelectRow],
- );
+ onSelectRow(focusedItem);
+ };
useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ENTER, selectFocusedOption, {
captureOnInputs: true,
@@ -181,10 +178,7 @@ function BaseSearchList({
return () => removeKeyDownPressListener(setHasKeyBeenPressed);
}, [setHasKeyBeenPressed]);
- const extraData = useMemo(
- () => [columns, newTransactions, nonPersonalAndWorkspaceCards, isAttendeesEnabledForMovingPolicy],
- [columns, newTransactions, nonPersonalAndWorkspaceCards, isAttendeesEnabledForMovingPolicy],
- );
+ const extraData = [columns, newTransactions, nonPersonalAndWorkspaceCards, isAttendeesEnabledForMovingPolicy];
return (
({
const searchData = currentSearchResults?.data;
- const snapshotReport = useMemo(() => {
- return (searchData?.[`${ONYXKEYS.COLLECTION.REPORT}${reportItem.reportID}`] ?? {}) as Report;
- }, [searchData, reportItem.reportID]);
+ const snapshotReport = (searchData?.[`${ONYXKEYS.COLLECTION.REPORT}${reportItem.reportID}`] ?? {}) as Report;
const [parentChatReport] = useOnyxWithoutSnapshots(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(snapshotReport?.chatReportID ?? reportItem.parentReportID)}`);
- const snapshotChatReport = useMemo(() => {
- const chatReportID = snapshotReport?.chatReportID ?? reportItem.parentReportID;
- return chatReportID ? searchData?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`] : undefined;
- }, [searchData, snapshotReport?.chatReportID, reportItem.parentReportID]);
+ const chatReportID = snapshotReport?.chatReportID ?? reportItem.parentReportID;
+ const snapshotChatReport = chatReportID ? searchData?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`] : undefined;
const chatReport = parentChatReport ?? snapshotChatReport;
const [chatReportActions] = useOnyxWithoutSnapshots(
`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(chatReport?.reportID ?? snapshotReport?.chatReportID ?? snapshotReport.parentReportID)}`,
);
- const snapshotPolicy = useMemo(() => {
- return (searchData?.[`${ONYXKEYS.COLLECTION.POLICY}${reportItem.policyID}`] ?? {}) as Policy;
- }, [searchData, reportItem.policyID]);
+ const snapshotPolicy = (searchData?.[`${ONYXKEYS.COLLECTION.POLICY}${reportItem.policyID}`] ?? {}) as Policy;
- const reportActions = useMemo(() => {
- const actionsData = searchData?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportItem.reportID}`];
- return actionsData ? Object.values(actionsData) : [];
- }, [searchData, reportItem.reportID]);
+ const actionsData = searchData?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportItem.reportID}`];
+ const reportActions = actionsData ? Object.values(actionsData) : [];
const liveReportItem = useLiveRowCapabilities({
item: reportItem,
@@ -172,9 +164,7 @@ function ExpenseReportListItemInner({
enabled: !!searchData,
});
- const isDisabledCheckbox = useMemo(() => {
- return reportItem.isDisabled ?? reportItem.isDisabledCheckbox;
- }, [reportItem.isDisabled, reportItem.isDisabledCheckbox]);
+ const isDisabledCheckbox = reportItem.isDisabled ?? reportItem.isDisabledCheckbox;
// Prefer live Onyx data over stale search snapshot for pending delete check.
const reportForPendingDeleteCheck = parentReport ?? reportItem;
@@ -189,19 +179,17 @@ function ExpenseReportListItemInner({
// Sync missingAttendees violation at render time for each transaction in the report
// This ensures violations show immediately when category settings change, without needing to click the row
- const hasSyncedMissingAttendeesViolation = useMemo(() => {
- if (!isAttendeeTrackingEnabled(policyForViolations)) {
- return false;
- }
-
+ let hasSyncedMissingAttendeesViolation;
+ if (!isAttendeeTrackingEnabled(policyForViolations)) {
+ hasSyncedMissingAttendeesViolation = false;
+ } else {
const isInvoice = isInvoiceReport(reportItem) || reportItem.type === CONST.REPORT.TYPE.INVOICE;
- return reportItem?.transactions?.some((transaction) => {
+ hasSyncedMissingAttendeesViolation = reportItem?.transactions?.some((transaction) => {
const relevantViolations = (transaction.violations ?? []).filter(
(violation) =>
!isViolationDismissed(transaction, violation, currentUserDetails.email ?? '', currentUserDetails.accountID, reportForViolations, submitterLogin, policyForViolations) &&
shouldShowViolation(reportForViolations, policyForViolations, violation.name, currentUserDetails.email ?? '', currentUserDetails.accountID, false, transaction),
);
-
const violations = syncMissingAttendeesViolation(
relevantViolations,
policyCategories,
@@ -214,7 +202,7 @@ function ExpenseReportListItemInner({
);
return violations.some((violation) => violation.name === CONST.VIOLATIONS.MISSING_ATTENDEES);
});
- }, [reportItem, policyCategories, policyForViolations, reportForViolations, currentUserDetails, submitterLogin]);
+ }
const {isDelegateAccessRestricted} = useDelegateNoAccessState();
const {showDelegateNoAccessModal} = useDelegateNoAccessActions();
@@ -223,7 +211,7 @@ function ExpenseReportListItemInner({
const openReportSubmitToPopover = useOpenReportSubmitToPopover();
const {shouldDisableSearchSubmitPress, consumeIgnoreNextSearchSubmitPress} = useSearchSubmitPopoverGuard();
const {transactions: reportTransactions, violations: reportViolations} = useTransactionsAndViolationsForReport(reportItem.reportID);
- const liveReportTransactions = useMemo(() => Object.values(reportTransactions), [reportTransactions]);
+ const liveReportTransactions = Object.values(reportTransactions);
const confirmSubmitReportViolations = useConfirmSubmitReportViolations({
reportID: reportItem.reportID,
report: reportForViolations,
@@ -270,7 +258,7 @@ function ExpenseReportListItemInner({
chatReportPolicyID: chatReport?.policyID,
});
- const handleOnButtonPress = useCallback(() => {
+ const handleOnButtonPress = () => {
handleActionButtonPress({
isASAPSubmitBetaEnabled,
getCurrencyDecimals,
@@ -341,75 +329,25 @@ function ExpenseReportListItemInner({
conciergeChat,
bankAccountList,
});
- }, [
- currentSearchHash,
- reportItem,
- isASAPSubmitBetaEnabled,
- liveReportItem,
- onSelectRow,
- searchData,
- snapshotReport,
- chatReport,
- snapshotPolicy,
- submitterLogin,
- parentPolicy,
- parentReport,
- lastPaymentMethod,
- userBillingGracePeriodEnds,
- personalPolicyID,
- currentSearchKey,
- isDelegateAccessRestricted,
- showDelegateNoAccessModal,
- showHoldMenu,
- liveReportTransactions,
- ownerBillingGracePeriodEnd,
- amountOwed,
- openReportSubmitToPopover,
- shouldDisableSearchSubmitPress,
- consumeIgnoreNextSearchSubmitPress,
- showConfirmModal,
- translate,
- convertToDisplayString,
- getCurrencyDecimals,
- currentUserAccountID,
- currentUserLogin,
- introSelected,
- isSelfTourViewed,
- activePolicy,
- chatReportPolicy,
- chatReportActions,
- delegateEmail,
- delegateAccountID,
- isTrackIntentUser,
- liveViolationsForSnapshotTransactions,
- rules,
- getPersonalDetailsByLogin,
- conciergeChat,
- bankAccountList,
- shouldShowMarkAsDoneCopy,
- confirmSubmitReportViolations,
- ]);
+ };
const handleSelectionButtonPress = (shiftKey?: boolean) => {
onSelectionButtonPress?.(item, undefined, shiftKey);
};
- const listItemPressableStyle = useMemo(
- () => [
- styles.selectionListPressableItemWrapper,
- isLargeScreenWidth && styles.pv3,
- isLargeScreenWidth && styles.ph3,
- // Background is applied on the parent wrapper, so keep this transparent
- styles.bgTransparent,
- isSelected && styles.activeComponentBG,
- styles.mh0,
- isPendingDelete && styles.cursorDisabled,
- isLargeScreenWidth ? StyleUtils.getSearchTableRowPressableStyle(!!isLastItem, isSelected, {vertical: variables.tableRowPaddingVertical}) : styles.noBorderRadius,
- !isLargeScreenWidth && isFirstItem && [styles.tableTopRadius, styles.overflowHidden],
- !isLargeScreenWidth && isLastItem && [styles.tableBottomRadius, styles.overflowHidden],
- ],
- [styles, isSelected, isLargeScreenWidth, isFirstItem, isLastItem, isPendingDelete, StyleUtils],
- );
+ const listItemPressableStyle = [
+ styles.selectionListPressableItemWrapper,
+ isLargeScreenWidth && styles.pv3,
+ isLargeScreenWidth && styles.ph3,
+ // Background is applied on the parent wrapper, so keep this transparent
+ styles.bgTransparent,
+ isSelected && styles.activeComponentBG,
+ styles.mh0,
+ isPendingDelete && styles.cursorDisabled,
+ isLargeScreenWidth ? StyleUtils.getSearchTableRowPressableStyle(!!isLastItem, isSelected, {vertical: variables.tableRowPaddingVertical}) : styles.noBorderRadius,
+ !isLargeScreenWidth && isFirstItem && [styles.tableTopRadius, styles.overflowHidden],
+ !isLargeScreenWidth && isLastItem && [styles.tableBottomRadius, styles.overflowHidden],
+ ];
const shouldShowViolationDescription = isOpenExpenseReport(reportItem) || isProcessingReport(reportItem);
@@ -435,31 +373,14 @@ function ExpenseReportListItemInner({
const hasVisibleReportViolations = hasLiveTransactions ? liveHasVisibleViolations : fallbackHasVisibleViolations;
const hasAnyVisibleViolations = hasVisibleReportViolations || hasSyncedMissingAttendeesViolation;
- const getDescription = useMemo(() => {
- if (reportItem?.isRejectedReport) {
- return (
-
-
- {translate('iou.rejectReport.rejectedReportMessage')}
-
- );
- }
- if (!hasAnyVisibleViolations || !shouldShowViolationDescription) {
- return;
- }
- return (
+ let getDescription;
+ if (reportItem?.isRejectedReport) {
+ getDescription = (
({
width={12}
height={12}
/>
-
- {translate('reportViolations.reportContainsExpensesWithViolations')}
+
+ {translate('iou.rejectReport.rejectedReportMessage')}
);
- }, [
- reportItem?.isRejectedReport,
- hasAnyVisibleViolations,
- shouldShowViolationDescription,
- styles.flexRow,
- styles.alignItemsCenter,
- styles.mt2,
- styles.mr1,
- styles.textMicro,
- styles.mutedNormalTextLabel,
- styles.userSelectNone,
- isLargeScreenWidth,
- expensifyIcons.DotIndicator,
- theme.danger,
- theme.textError,
- translate,
- ]);
+ } else {
+ getDescription =
+ !hasAnyVisibleViolations || !shouldShowViolationDescription ? undefined : (
+
+
+
+ {translate('reportViolations.reportContainsExpensesWithViolations')}
+
+
+ );
+ }
// Full label for the button (its whole announcement); just a row identifier for the group, whose cells are reachable.
const rowAccessibilityLabel = canSelectMultiple ? liveReportItem.reportName : getExpenseReportRowAccessibilityLabel(liveReportItem, {translate, dateFnsLocale, convertToDisplayString});
diff --git a/src/components/Search/SearchList/ListItem/GroupChildrenContent.tsx b/src/components/Search/SearchList/ListItem/GroupChildrenContent.tsx
index ffbfec542e8c..2668ef77e289 100644
--- a/src/components/Search/SearchList/ListItem/GroupChildrenContent.tsx
+++ b/src/components/Search/SearchList/ListItem/GroupChildrenContent.tsx
@@ -11,7 +11,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
import type {Transaction, TransactionViolation, TransactionViolations} from '@src/types/onyx';
import {useIsFocused} from '@react-navigation/native';
-import React, {useEffect, useMemo, useState} from 'react';
+import React, {useEffect, useState} from 'react';
import type {GroupChildrenContentProps, TransactionListItemType} from './types';
@@ -108,18 +108,16 @@ function GroupChildrenContent({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isScreenFocused]);
- const filteredViolations: Record = useMemo(() => {
- if (!snapshotData) {
- return {};
- }
-
+ let filteredViolations: Record;
+ if (!snapshotData) {
+ filteredViolations = {};
+ } else {
const groupViolations: Record = {};
for (const [key, value] of Object.entries(snapshotData)) {
if (key.startsWith(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS)) {
groupViolations[key] = value as TransactionViolations;
}
}
-
const result: Record = {};
for (const key of Object.keys(snapshotData)) {
if (!key.startsWith(ONYXKEYS.COLLECTION.TRANSACTION)) {
@@ -129,10 +127,8 @@ function GroupChildrenContent({
if (!snapshotTransaction || typeof snapshotTransaction !== 'object' || !('transactionID' in snapshotTransaction) || !('reportID' in snapshotTransaction)) {
continue;
}
-
const report = snapshotData[`${ONYXKEYS.COLLECTION.REPORT}${snapshotTransaction.reportID}`];
const policy = snapshotData[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`];
-
if (report && policy) {
const transactionViolations = groupViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${snapshotTransaction.transactionID}`];
if (transactionViolations) {
@@ -151,8 +147,8 @@ function GroupChildrenContent({
}
}
}
- return result;
- }, [snapshotData, currentUserDetails.email, currentUserDetails.accountID]);
+ filteredViolations = result;
+ }
const onExpandedRowLongPress = (transaction: TransactionListItemType) => {
onLongPressRow?.(transaction);
diff --git a/src/components/Search/SearchList/ListItem/GroupHeader.tsx b/src/components/Search/SearchList/ListItem/GroupHeader.tsx
index 93d65989866c..cdca1fe922f4 100644
--- a/src/components/Search/SearchList/ListItem/GroupHeader.tsx
+++ b/src/components/Search/SearchList/ListItem/GroupHeader.tsx
@@ -39,7 +39,7 @@ import type {ComponentRef} from 'react';
import type {NativeSyntheticEvent} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
-import React, {useMemo, useRef} from 'react';
+import React, {useRef} from 'react';
import {View} from 'react-native';
// eslint-disable-next-line no-restricted-imports
import {useOnyx as useOnyxWithoutSnapshots} from 'react-native-onyx';
@@ -128,15 +128,12 @@ function GroupHeader({
const [parentReportAction] = useOnyxWithoutSnapshots(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${oneTransactionReportID}`, {
selector: (reportActions: OnyxEntry): OnyxEntry => reportActions?.[`${oneTransactionItem?.reportAction?.reportActionID}`],
});
- const transactionPreviewData: TransactionPreviewData = useMemo(
- () => ({
- hasParentReport: !!parentReport,
- hasTransaction: !!oneTransaction,
- hasParentReportAction: !!parentReportAction,
- hasTransactionThreadReport: !!oneTransactionThreadReport,
- }),
- [parentReport, oneTransaction, parentReportAction, oneTransactionThreadReport],
- );
+ const transactionPreviewData: TransactionPreviewData = {
+ hasParentReport: !!parentReport,
+ hasTransaction: !!oneTransaction,
+ hasParentReportAction: !!parentReportAction,
+ hasTransactionThreadReport: !!oneTransactionThreadReport,
+ };
const [transactionsSnapshot] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${groupItem.transactionsQueryJSON?.hash}`);
const snapshotData = transactionsSnapshot?.data;
diff --git a/src/components/Search/SearchList/ListItem/ReportListItemHeader.tsx b/src/components/Search/SearchList/ListItem/ReportListItemHeader.tsx
index 5ad38d5815aa..72cbd86be067 100644
--- a/src/components/Search/SearchList/ListItem/ReportListItemHeader.tsx
+++ b/src/components/Search/SearchList/ListItem/ReportListItemHeader.tsx
@@ -45,7 +45,7 @@ import type {OnyxEntry} from 'react-native-onyx';
import {isTrackIntentUserSelector} from '@selectors/Onboarding';
import {transactionViolationsByIDsSelector} from '@selectors/TransactionViolations';
-import React, {useMemo} from 'react';
+import React from 'react';
import {View} from 'react-native';
// Use the original useOnyx hook to get the real-time personal details list data from Onyx and not from the snapshot
// eslint-disable-next-line no-restricted-imports
@@ -266,16 +266,10 @@ function ReportListItemHeaderInner({
const {isLargeScreenWidth} = useResponsiveLayout();
const thereIsFromAndTo = !!reportItem?.from && !!reportItem?.to;
const showUserInfo = (reportItem.type === CONST.REPORT.TYPE.IOU && thereIsFromAndTo) || (reportItem.type === CONST.REPORT.TYPE.EXPENSE && !!reportItem?.from);
- const snapshotReport = useMemo(() => {
- return (snapshot?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${reportItem.reportID}`] ?? {}) as Report;
- }, [snapshot, reportItem.reportID]);
- const snapshotPolicy = useMemo(() => {
- return (snapshot?.data?.[`${ONYXKEYS.COLLECTION.POLICY}${reportItem.policyID}`] ?? {}) as Policy;
- }, [snapshot, reportItem.policyID]);
- const snapshotChatReport = useMemo(() => {
- const chatReportID = snapshotReport?.chatReportID ?? reportItem.parentReportID;
- return chatReportID ? snapshot?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`] : undefined;
- }, [snapshot, snapshotReport?.chatReportID, reportItem.parentReportID]);
+ const snapshotReport = (snapshot?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${reportItem.reportID}`] ?? {}) as Report;
+ const snapshotPolicy = (snapshot?.data?.[`${ONYXKEYS.COLLECTION.POLICY}${reportItem.policyID}`] ?? {}) as Policy;
+ const chatReportID = snapshotReport?.chatReportID ?? reportItem.parentReportID;
+ const snapshotChatReport = chatReportID ? snapshot?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`] : undefined;
const [parentPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${getNonEmptyStringOnyxID(snapshotReport?.policyID ?? reportItem.policyID)}`);
const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(snapshotReport?.reportID ?? reportItem.reportID)}`);
const [submitterLogin] = useAllPersonalDetailsWithoutSnapshots(personalDetailsLoginSelector(reportItem.ownerAccountID));
diff --git a/src/components/Search/SearchList/ListItem/TransactionGroupListExpanded.tsx b/src/components/Search/SearchList/ListItem/TransactionGroupListExpanded.tsx
index 0c73c5e2162f..8958f5107e68 100644
--- a/src/components/Search/SearchList/ListItem/TransactionGroupListExpanded.tsx
+++ b/src/components/Search/SearchList/ListItem/TransactionGroupListExpanded.tsx
@@ -50,7 +50,7 @@ import type * as OnyxTypes from '@src/types/onyx';
import type {OnyxCollection} from 'react-native-onyx';
-import React, {useCallback, useMemo} from 'react';
+import React from 'react';
import {View} from 'react-native';
import type {TransactionGroupListExpandedProps, TransactionListItemType} from './types';
@@ -111,31 +111,26 @@ function TransactionGroupListExpandedImpl({
const visibleTransactions = isExpenseReportType ? transactions.slice(0, transactionsVisibleLimit) : transactions;
- const neededReportIDs = useMemo(() => {
- const ids = new Set();
- for (const transaction of visibleTransactions) {
- if (transaction.reportID) {
- ids.add(`${ONYXKEYS.COLLECTION.REPORT}${transaction.reportID}`);
- }
- if (transaction.reportAction?.childReportID) {
- ids.add(`${ONYXKEYS.COLLECTION.REPORT}${transaction.reportAction.childReportID}`);
- }
+ const ids = new Set();
+ for (const transaction of visibleTransactions) {
+ if (transaction.reportID) {
+ ids.add(`${ONYXKEYS.COLLECTION.REPORT}${transaction.reportID}`);
}
- return ids;
- }, [visibleTransactions]);
-
- const reportsSelector = useCallback(
- (reports: OnyxCollection) => {
- const result: OnyxCollection = {};
- for (const key of Object.keys(reports ?? {})) {
- if (neededReportIDs.has(key)) {
- result[key] = reports?.[key];
- }
+ if (transaction.reportAction?.childReportID) {
+ ids.add(`${ONYXKEYS.COLLECTION.REPORT}${transaction.reportAction.childReportID}`);
+ }
+ }
+ const neededReportIDs = ids;
+
+ const reportsSelector = (reports: OnyxCollection) => {
+ const result: OnyxCollection = {};
+ for (const key of Object.keys(reports ?? {})) {
+ if (neededReportIDs.has(key)) {
+ result[key] = reports?.[key];
}
- return result;
- },
- [neededReportIDs],
- );
+ }
+ return result;
+ };
const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: reportsSelector});
const isLastTransaction = (index: number) => {
diff --git a/src/components/Search/SearchList/SearchSelectAllMenu.tsx b/src/components/Search/SearchList/SearchSelectAllMenu.tsx
index 705a38f1bc90..57e3fd71185f 100644
--- a/src/components/Search/SearchList/SearchSelectAllMenu.tsx
+++ b/src/components/Search/SearchList/SearchSelectAllMenu.tsx
@@ -15,7 +15,7 @@ import CONST from '@src/CONST';
import type {ComponentRef} from 'react';
import type {View} from 'react-native';
-import React, {useCallback, useMemo, useRef, useState} from 'react';
+import React, {useRef, useState} from 'react';
type SearchSelectAllMenuProps = {
isSelectAllChecked: boolean | undefined;
@@ -39,27 +39,24 @@ function SearchSelectAllMenu({isSelectAllChecked, isIndeterminate, selectedItems
const shouldOpenSelectAllMenu = selectedItemsLength === 0 && !!currentSearchResults?.search?.hasMoreResults;
- const closeSelectAllMenu = useCallback(() => setIsSelectAllMenuVisible(false), []);
+ const closeSelectAllMenu = () => setIsSelectAllMenuVisible(false);
- const selectAllOnPage = useCallback(() => {
+ const selectAllOnPage = () => {
closeSelectAllMenu();
onAllCheckboxPress();
- }, [closeSelectAllMenu, onAllCheckboxPress]);
+ };
- const selectAllMatching = useCallback(() => {
+ const selectAllMatching = () => {
closeSelectAllMenu();
selectAllMatchingItems(true);
- }, [closeSelectAllMenu, selectAllMatchingItems]);
-
- const selectAllMenuItems = useMemo(
- (): PopoverMenuItem[] => [
- {icon: expensifyIcons.Checkmark, text: translate('search.exportAll.selectAllOnThisPage'), onSelected: selectAllOnPage},
- {icon: expensifyIcons.CheckSquare, text: translate('workspace.people.selectAll'), onSelected: selectAllMatching},
- ],
- [expensifyIcons.CheckSquare, expensifyIcons.Checkmark, selectAllMatching, selectAllOnPage, translate],
- );
+ };
+
+ const selectAllMenuItems: PopoverMenuItem[] = [
+ {icon: expensifyIcons.Checkmark, text: translate('search.exportAll.selectAllOnThisPage'), onSelected: selectAllOnPage},
+ {icon: expensifyIcons.CheckSquare, text: translate('workspace.people.selectAll'), onSelected: selectAllMatching},
+ ];
- const showSelectAllMenu = useCallback(() => {
+ const showSelectAllMenu = () => {
if (!selectAllAnchorRef.current) {
return;
}
@@ -71,16 +68,16 @@ function SearchSelectAllMenu({isSelectAllChecked, isIndeterminate, selectedItems
setSelectAllMenuPosition(position);
setIsSelectAllMenuVisible(true);
});
- }, [calculatePopoverPosition]);
+ };
- const handleSelectAllPress = useCallback(() => {
+ const handleSelectAllPress = () => {
if (shouldOpenSelectAllMenu) {
showSelectAllMenu();
return;
}
onAllCheckboxPress();
- }, [onAllCheckboxPress, shouldOpenSelectAllMenu, showSelectAllMenu]);
+ };
return (
<>
diff --git a/src/components/Search/SearchPageFooter.tsx b/src/components/Search/SearchPageFooter.tsx
index 253a62ab3def..9638e0f56e09 100644
--- a/src/components/Search/SearchPageFooter.tsx
+++ b/src/components/Search/SearchPageFooter.tsx
@@ -18,7 +18,7 @@ import CONST from '@src/CONST';
import type {LayoutChangeEvent} from 'react-native';
-import React, {useMemo, useState} from 'react';
+import React, {useState} from 'react';
import {StyleSheet, View} from 'react-native';
import type {SingleSelectItem} from './FilterComponents/SingleSelect';
@@ -109,7 +109,7 @@ function SearchPageFooter({
setLoadedTotalHeight(height);
};
- const valueTextStyle = useMemo(() => (isOffline ? [styles.textLabelSupporting, styles.labelStrong] : [styles.labelStrong]), [isOffline, styles]);
+ const valueTextStyle = isOffline ? [styles.textLabelSupporting, styles.labelStrong] : [styles.labelStrong];
// The SearchList registers a global Enter shortcut that opens the focused expense. While the footer button is focused,
// claim Enter at top priority without bubbling so Enter only opens the footer's display menu instead of also opening the expense.
diff --git a/src/components/Search/SearchPageHeader/SearchActionsBarCreateButton.tsx b/src/components/Search/SearchPageHeader/SearchActionsBarCreateButton.tsx
index 8ab3aa1353fc..26500fe5355a 100644
--- a/src/components/Search/SearchPageHeader/SearchActionsBarCreateButton.tsx
+++ b/src/components/Search/SearchPageHeader/SearchActionsBarCreateButton.tsx
@@ -30,7 +30,7 @@ import type {OnyxEntry} from 'react-native-onyx';
import {isTrackIntentUserSelector} from '@selectors/Onboarding';
import {validTransactionDraftIDsSelector} from '@selectors/TransactionDraft';
-import React, {useCallback, useMemo, useRef, useState} from 'react';
+import React, {useRef, useState} from 'react';
import {View} from 'react-native';
function SearchActionsBarCreateButton() {
@@ -53,41 +53,38 @@ function SearchActionsBarCreateButton() {
const [isTrackIntentUser] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {selector: isTrackIntentUserSelector});
const [rules] = useOnyx(ONYXKEYS.COLLECTION.RULE);
- const handleCreateWorkspaceReport = useCallback(
- (policy: OnyxEntry, shouldDismissEmptyReportsConfirmation?: boolean) => {
- if (!policy?.id) {
- return;
- }
-
- const {reportID: createdReportID} = createNewReport(
- currentUserPersonalDetails,
- hasViolations,
- isASAPSubmitBetaEnabled,
- policy,
- isTrackIntentUser,
- getCurrencyDecimals,
- rules,
- false,
- shouldDismissEmptyReportsConfirmation,
+ const handleCreateWorkspaceReport = (policy: OnyxEntry, shouldDismissEmptyReportsConfirmation?: boolean) => {
+ if (!policy?.id) {
+ return;
+ }
+
+ const {reportID: createdReportID} = createNewReport(
+ currentUserPersonalDetails,
+ hasViolations,
+ isASAPSubmitBetaEnabled,
+ policy,
+ isTrackIntentUser,
+ getCurrencyDecimals,
+ rules,
+ false,
+ shouldDismissEmptyReportsConfirmation,
+ );
+ Navigation.setNavigationActionToMicrotaskQueue(() => {
+ Navigation.navigate(
+ isSearchTopmostFullScreenRoute()
+ ? ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID: createdReportID, backTo: Navigation.getActiveRoute()})
+ : ROUTES.REPORT_WITH_ID.getRoute(createdReportID, undefined, undefined, Navigation.getActiveRoute()),
);
- Navigation.setNavigationActionToMicrotaskQueue(() => {
- Navigation.navigate(
- isSearchTopmostFullScreenRoute()
- ? ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID: createdReportID, backTo: Navigation.getActiveRoute()})
- : ROUTES.REPORT_WITH_ID.getRoute(createdReportID, undefined, undefined, Navigation.getActiveRoute()),
- );
- });
- },
- [currentUserPersonalDetails, hasViolations, isASAPSubmitBetaEnabled, isTrackIntentUser, getCurrencyDecimals, rules],
- );
+ });
+ };
const {createReport, isVisible: isCreateReportVisible} = useCreateReport({
onCreateReport: handleCreateWorkspaceReport,
shouldSkipEmptyReportConfirmation: !isCreateMenuActive,
});
- const hideCreateMenu = useCallback(() => setIsCreateMenuActive(false), []);
- const showCreateMenu = useCallback(() => {
+ const hideCreateMenu = () => setIsCreateMenuActive(false);
+ const showCreateMenu = () => {
if (!createButtonRef.current) {
return;
}
@@ -98,38 +95,35 @@ function SearchActionsBarCreateButton() {
setCreateMenuPosition(position);
setIsCreateMenuActive(true);
});
- }, [calculatePopoverPosition]);
-
- const createMenuItems = useMemo(
- (): PopoverMenuItem[] => [
- {
- icon: getIconForAction(CONST.IOU.TYPE.CREATE, expensifyIcons),
- text: translate('iou.createExpense'),
- onSelected: () =>
- interceptAnonymousUser(() => {
- startMoneyRequest(CONST.IOU.TYPE.CREATE, generateReportID(), draftTransactionIDs);
- }),
- },
- {
- icon: expensifyIcons.Location,
- text: translate('iou.trackDistance'),
- onSelected: () =>
- interceptAnonymousUser(() => {
- startDistanceRequest(CONST.IOU.TYPE.CREATE, generateReportID(), draftTransactionIDs);
- }),
- },
- ...(isCreateReportVisible
- ? [
- {
- icon: expensifyIcons.Document,
- text: translate('report.newReport.createReport'),
- onSelected: createReport,
- },
- ]
- : []),
- ],
- [translate, expensifyIcons, draftTransactionIDs, createReport, isCreateReportVisible],
- );
+ };
+
+ const createMenuItems: PopoverMenuItem[] = [
+ {
+ icon: getIconForAction(CONST.IOU.TYPE.CREATE, expensifyIcons),
+ text: translate('iou.createExpense'),
+ onSelected: () =>
+ interceptAnonymousUser(() => {
+ startMoneyRequest(CONST.IOU.TYPE.CREATE, generateReportID(), draftTransactionIDs);
+ }),
+ },
+ {
+ icon: expensifyIcons.Location,
+ text: translate('iou.trackDistance'),
+ onSelected: () =>
+ interceptAnonymousUser(() => {
+ startDistanceRequest(CONST.IOU.TYPE.CREATE, generateReportID(), draftTransactionIDs);
+ }),
+ },
+ ...(isCreateReportVisible
+ ? [
+ {
+ icon: expensifyIcons.Document,
+ text: translate('report.newReport.createReport'),
+ onSelected: createReport,
+ },
+ ]
+ : []),
+ ];
return (
diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx
index f3c270348d14..93d941907e8b 100644
--- a/src/components/Search/SearchRouter/SearchRouter.tsx
+++ b/src/components/Search/SearchRouter/SearchRouter.tsx
@@ -69,7 +69,7 @@ import type {ValueOf} from 'type-fest';
import {guidedSetupAndTourStatusSelector} from '@selectors/Onboarding';
import {pendingDeleteMemberAccountIDsSelector} from '@selectors/ReportMetaData';
import {deepEqual} from 'fast-equals';
-import React, {useCallback, useEffect, useRef, useState} from 'react';
+import React, {useEffect, useRef, useState} from 'react';
import {View} from 'react-native';
import type {SubstitutionMap} from './getQueryWithSubstitutions';
@@ -253,155 +253,129 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla
return {[reportKey]: contextualReport};
})();
- const getAdditionalSections: GetAdditionalSectionsCallback = useCallback(
- ({recentReports}, sectionIndex) => {
- const getNavigationSuggestionsSection = () => (navigationSuggestions.length ? [{sectionIndex, data: navigationSuggestions}] : undefined);
+ const getAdditionalSections: GetAdditionalSectionsCallback = ({recentReports}, sectionIndex) => {
+ const getNavigationSuggestionsSection = () => (navigationSuggestions.length ? [{sectionIndex, data: navigationSuggestions}] : undefined);
- if (!contextualReportID) {
- return getNavigationSuggestionsSection();
- }
+ if (!contextualReportID) {
+ return getNavigationSuggestionsSection();
+ }
- // We will only show the contextual search suggestion if the user has not typed anything
- if (textInputValue) {
- return getNavigationSuggestionsSection();
- }
+ // We will only show the contextual search suggestion if the user has not typed anything
+ if (textInputValue) {
+ return getNavigationSuggestionsSection();
+ }
- if (!isSearchRouterDisplayed && !isSearchRouterScreen) {
+ if (!isSearchRouterDisplayed && !isSearchRouterScreen) {
+ return undefined;
+ }
+ let reportForContextualSearch = recentReports.find((option) => option.reportID === contextualReportID);
+ const reportAction = getReportAction(contextualReport?.parentReportID, contextualReport?.parentReportActionID);
+ const shouldParserToHTML = !!reportAction && reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT;
+ if (!reportForContextualSearch) {
+ if (!contextualReport || isHiddenForCurrentUser(contextualReport)) {
return undefined;
}
- let reportForContextualSearch = recentReports.find((option) => option.reportID === contextualReportID);
- const reportAction = getReportAction(contextualReport?.parentReportID, contextualReport?.parentReportActionID);
- const shouldParserToHTML = !!reportAction && reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT;
- if (!reportForContextualSearch) {
- if (!contextualReport || isHiddenForCurrentUser(contextualReport)) {
- return undefined;
- }
- const option = createOptionFromReport({
- dateFnsLocale,
- convertToDisplayString,
- convertToDisplayStringWithoutCurrency,
- report: contextualReport,
- personalDetails,
- privateIsArchived: contextualReportNVP,
- rules,
- policy: contextualReportPolicy,
- sortedActions,
- transactionThreadIDs,
- lastActions,
- currentUserAccountID,
- currentUserLogin: currentUserPersonalDetails.login,
- conciergeReportID,
- reportAttributesDerived: reportAttributes,
- config: {
- showPersonalDetails: isOneOnOneChat(contextualReport),
- },
- isTrackIntentUser,
- pendingDeleteMemberAccountIDs: contextualReportPendingDeleteMemberAccountIDs,
- formatPhoneNumber,
- });
- reportForContextualSearch = option;
- }
+ const option = createOptionFromReport({
+ dateFnsLocale,
+ convertToDisplayString,
+ convertToDisplayStringWithoutCurrency,
+ report: contextualReport,
+ personalDetails,
+ privateIsArchived: contextualReportNVP,
+ rules,
+ policy: contextualReportPolicy,
+ sortedActions,
+ transactionThreadIDs,
+ lastActions,
+ currentUserAccountID,
+ currentUserLogin: currentUserPersonalDetails.login,
+ conciergeReportID,
+ reportAttributesDerived: reportAttributes,
+ config: {
+ showPersonalDetails: isOneOnOneChat(contextualReport),
+ },
+ isTrackIntentUser,
+ pendingDeleteMemberAccountIDs: contextualReportPendingDeleteMemberAccountIDs,
+ formatPhoneNumber,
+ });
+ reportForContextualSearch = option;
+ }
- const reportQueryValue = reportForContextualSearch.text ?? reportForContextualSearch.alternateText ?? reportForContextualSearch.reportID;
+ const reportQueryValue = reportForContextualSearch.text ?? reportForContextualSearch.alternateText ?? reportForContextualSearch.reportID;
- let roomType: ValueOf = CONST.SEARCH.DATA_TYPES.CHAT;
- let autocompleteID: string | undefined = reportForContextualSearch.reportID;
+ let roomType: ValueOf = CONST.SEARCH.DATA_TYPES.CHAT;
+ let autocompleteID: string | undefined = reportForContextualSearch.reportID;
- if (reportForContextualSearch.isInvoiceRoom) {
- roomType = CONST.SEARCH.DATA_TYPES.INVOICE;
- const report = reportForContextualSearch as SearchOption;
- if (report.item?.invoiceReceiver && report.item.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL) {
- autocompleteID = report.item.invoiceReceiver.accountID.toString();
- } else {
- autocompleteID = '';
- }
+ if (reportForContextualSearch.isInvoiceRoom) {
+ roomType = CONST.SEARCH.DATA_TYPES.INVOICE;
+ const report = reportForContextualSearch as SearchOption;
+ if (report.item?.invoiceReceiver && report.item.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL) {
+ autocompleteID = report.item.invoiceReceiver.accountID.toString();
+ } else {
+ autocompleteID = '';
}
- if (reportForContextualSearch.isPolicyExpenseChat) {
- roomType = CONST.SEARCH.DATA_TYPES.EXPENSE;
- if (reportForContextualSearch.policyID) {
- autocompleteID = reportForContextualSearch.policyID;
- } else {
- autocompleteID = '';
- }
+ }
+ if (reportForContextualSearch.isPolicyExpenseChat) {
+ roomType = CONST.SEARCH.DATA_TYPES.EXPENSE;
+ if (reportForContextualSearch.policyID) {
+ autocompleteID = reportForContextualSearch.policyID;
+ } else {
+ autocompleteID = '';
}
+ }
- return [
- {
- sectionIndex,
- data: [
- {
- text: StringUtils.lineBreaksToSpaces(
- `${translate('search.searchIn')} ${
- shouldParserToHTML
- ? Parser.htmlToText(reportForContextualSearch.text ?? reportForContextualSearch.alternateText ?? '')
- : (reportForContextualSearch.text ?? reportForContextualSearch.alternateText ?? '')
- }`,
- ),
- singleIcon: expensifyIcons.MagnifyingGlass,
- searchQuery: reportQueryValue,
- autocompleteID,
- keyForList: 'contextualSearch',
- searchItemType: CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.CONTEXTUAL_SUGGESTION,
- roomType,
- policyID: reportForContextualSearch.policyID,
- },
- ],
- },
- ];
- },
- [
- contextualReportID,
- conciergeReportID,
- navigationSuggestions,
- textInputValue,
- isSearchRouterDisplayed,
- isSearchRouterScreen,
- translate,
- expensifyIcons.MagnifyingGlass,
- contextualReport,
- personalDetails,
- sortedActions,
- transactionThreadIDs,
- lastActions,
- currentUserPersonalDetails.login,
- contextualReportNVP,
- contextualReportPolicy,
- reportAttributes,
- isTrackIntentUser,
- dateFnsLocale,
- currentUserAccountID,
- convertToDisplayString,
- formatPhoneNumber,
- convertToDisplayStringWithoutCurrency,
- rules,
- contextualReportPendingDeleteMemberAccountIDs,
- ],
- );
-
- const searchQueryItems = textInputValue?.trim()
- ? [
- {
- text: textInputValue,
- singleIcon: expensifyIcons.MagnifyingGlass,
- searchQuery: textInputValue,
- keyForList: CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.FIND_ITEM,
- searchItemType: CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.SEARCH,
- },
- ...(shouldShowAskConcierge
- ? [
- {
- text: translate('search.askConcierge', textInputValue),
- singleIcon: expensifyIcons.ConciergeAvatar,
- shouldIconApplyFill: false,
- searchQuery: textInputValue,
- keyForList: CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.ASK_CONCIERGE,
- searchItemType: CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.ASK_CONCIERGE,
- },
- ]
- : []),
- ]
- : undefined;
+ return [
+ {
+ sectionIndex,
+ data: [
+ {
+ text: StringUtils.lineBreaksToSpaces(
+ `${translate('search.searchIn')} ${
+ shouldParserToHTML
+ ? Parser.htmlToText(reportForContextualSearch.text ?? reportForContextualSearch.alternateText ?? '')
+ : (reportForContextualSearch.text ?? reportForContextualSearch.alternateText ?? '')
+ }`,
+ ),
+ singleIcon: expensifyIcons.MagnifyingGlass,
+ searchQuery: reportQueryValue,
+ autocompleteID,
+ keyForList: 'contextualSearch',
+ searchItemType: CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.CONTEXTUAL_SUGGESTION,
+ roomType,
+ policyID: reportForContextualSearch.policyID,
+ },
+ ],
+ },
+ ];
+ };
+
+ let searchQueryItems;
+ if (textInputValue?.trim()) {
+ searchQueryItems = [
+ {
+ text: textInputValue,
+ singleIcon: expensifyIcons.MagnifyingGlass,
+ searchQuery: textInputValue,
+ keyForList: CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.FIND_ITEM,
+ searchItemType: CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.SEARCH,
+ },
+ ...(shouldShowAskConcierge
+ ? [
+ {
+ text: translate('search.askConcierge', textInputValue),
+ singleIcon: expensifyIcons.ConciergeAvatar,
+ shouldIconApplyFill: false,
+ searchQuery: textInputValue,
+ keyForList: CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.ASK_CONCIERGE,
+ searchItemType: CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.ASK_CONCIERGE,
+ },
+ ]
+ : []),
+ ];
+ } else {
+ searchQueryItems = undefined;
+ }
const shouldScrollRef = useRef(false);
// Trigger scrollToRight when input value changes and shouldScroll is true
@@ -414,171 +388,131 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla
shouldScrollRef.current = false;
}, [textInputValue]);
- const onSearchQueryChange = useCallback(
- // `isTypedInput` is false when the query is set without typing in the input, such as by picking a suggestion
- (userQuery: string, autoScrollToRight = false, inputEventTimeStamp?: number, isTypedInput = true) => {
- if (autoScrollToRight) {
- shouldScrollRef.current = true;
- }
- const singleLineUserQuery = StringUtils.lineBreaksToSpaces(userQuery, true);
- const updatedUserQuery = getAutocompleteQueryWithComma(textInputValue, singleLineUserQuery);
- setTextInputValue(updatedUserQuery);
- // A same-value update does not restart the debounce timer, so it must not move the span start either.
- if (updatedUserQuery !== lastAutocompleteQueryRef.current) {
- // The list may still be evaluating the previous debounced query, and that measurement would now include this input.
- cancelSearchRouterQuerySpan(
- updatedUserQuery.trim() ? CONST.TELEMETRY.SEARCH_ROUTER_QUERY_CANCEL_REASON.SUPERSEDED : CONST.TELEMETRY.SEARCH_ROUTER_QUERY_CANCEL_REASON.CLEARED,
- );
- lastAutocompleteQueryRef.current = updatedUserQuery;
- queryChangedAtRef.current = getSearchRouterQueryChangedAt(inputEventTimeStamp, performance.now());
- isQueryChangeTypedRef.current = isTypedInput;
- }
- setAutocompleteQueryValue(updatedUserQuery);
+ // `isTypedInput` is false when the query is set without typing in the input, such as by picking a suggestion
+ const onSearchQueryChange = (userQuery: string, autoScrollToRight = false, inputEventTimeStamp?: number, isTypedInput = true) => {
+ if (autoScrollToRight) {
+ shouldScrollRef.current = true;
+ }
+ const singleLineUserQuery = StringUtils.lineBreaksToSpaces(userQuery, true);
+ const updatedUserQuery = getAutocompleteQueryWithComma(textInputValue, singleLineUserQuery);
+ setTextInputValue(updatedUserQuery);
+ // A same-value update does not restart the debounce timer, so it must not move the span start either.
+ if (updatedUserQuery !== lastAutocompleteQueryRef.current) {
+ // The list may still be evaluating the previous debounced query, and that measurement would now include this input.
+ cancelSearchRouterQuerySpan(updatedUserQuery.trim() ? CONST.TELEMETRY.SEARCH_ROUTER_QUERY_CANCEL_REASON.SUPERSEDED : CONST.TELEMETRY.SEARCH_ROUTER_QUERY_CANCEL_REASON.CLEARED);
+ lastAutocompleteQueryRef.current = updatedUserQuery;
+ queryChangedAtRef.current = getSearchRouterQueryChangedAt(inputEventTimeStamp, performance.now());
+ isQueryChangeTypedRef.current = isTypedInput;
+ }
+ setAutocompleteQueryValue(updatedUserQuery);
- const updatedSubstitutionsMap = getUpdatedSubstitutionsMap(singleLineUserQuery, autocompleteSubstitutions);
- if (!deepEqual(autocompleteSubstitutions, updatedSubstitutionsMap)) {
- setAutocompleteSubstitutions(updatedSubstitutionsMap);
- }
- },
- [autocompleteSubstitutions, setAutocompleteQueryValue, setTextInputValue, textInputValue],
- );
+ const updatedSubstitutionsMap = getUpdatedSubstitutionsMap(singleLineUserQuery, autocompleteSubstitutions);
+ if (!deepEqual(autocompleteSubstitutions, updatedSubstitutionsMap)) {
+ setAutocompleteSubstitutions(updatedSubstitutionsMap);
+ }
+ };
+
+ const submitSearch = (queryString: SearchQueryString, shouldSkipAmountConversion = false) => {
+ const queryWithSubstitutions = getQueryWithSubstitutions(queryString, autocompleteSubstitutions, currentUserAccountID, getPersonalDetailsByLogin());
+ const updatedQuery = getQueryWithUpdatedValues(queryWithSubstitutions, shouldSkipAmountConversion, policies);
+ if (!updatedQuery) {
+ return;
+ }
+
+ // Reset the search query flag when performing a new search
+ setShouldResetSearchQuery(false);
+
+ onRouterClose();
+ setSearchContext(true);
+ const updatedQueryJSON = buildSearchQueryJSON(updatedQuery);
+ const searchKey = updatedQueryJSON?.hash === currentSearchHash ? currentSearchKey : getSearchKeyForQuery(updatedQueryJSON);
+ Navigation.navigate(
+ ROUTES.SEARCH_ROOT.getRoute({
+ query: updatedQuery,
+ rawQuery: shouldSkipAmountConversion || !isFromSearchPageSearchButton ? undefined : queryWithSubstitutions,
+ searchKey,
+ }),
+ );
+
+ setTextInputValue('');
+ lastAutocompleteQueryRef.current = '';
+ setAutocompleteQueryValue('');
+ };
- const submitSearch = useCallback(
- (queryString: SearchQueryString, shouldSkipAmountConversion = false) => {
- const queryWithSubstitutions = getQueryWithSubstitutions(queryString, autocompleteSubstitutions, currentUserAccountID, getPersonalDetailsByLogin());
- const updatedQuery = getQueryWithUpdatedValues(queryWithSubstitutions, shouldSkipAmountConversion, policies);
- if (!updatedQuery) {
+ const onListItemPress = (item: OptionData | SearchQueryItem) => {
+ const setFocusAndScrollToRight = () => {
+ if (!textInputRef.current) {
return;
}
+ textInputRef.current.focus();
+ scrollToRight(textInputRef.current);
+ };
- // Reset the search query flag when performing a new search
- setShouldResetSearchQuery(false);
-
- onRouterClose();
- setSearchContext(true);
- const updatedQueryJSON = buildSearchQueryJSON(updatedQuery);
- const searchKey = updatedQueryJSON?.hash === currentSearchHash ? currentSearchKey : getSearchKeyForQuery(updatedQueryJSON);
- Navigation.navigate(
- ROUTES.SEARCH_ROOT.getRoute({
- query: updatedQuery,
- rawQuery: shouldSkipAmountConversion || !isFromSearchPageSearchButton ? undefined : queryWithSubstitutions,
- searchKey,
- }),
- );
-
- setTextInputValue('');
- lastAutocompleteQueryRef.current = '';
- setAutocompleteQueryValue('');
- },
- [
- autocompleteSubstitutions,
- currentUserAccountID,
- getPersonalDetailsByLogin,
- onRouterClose,
- setAutocompleteQueryValue,
- setTextInputValue,
- setShouldResetSearchQuery,
- getSearchKeyForQuery,
- currentSearchHash,
- currentSearchKey,
- isFromSearchPageSearchButton,
- policies,
- ],
- );
+ if (isSearchQueryItem(item)) {
+ if (item.searchItemType === CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.NAVIGATE && item.action) {
+ onRouterClose(item.action);
+ return;
+ }
- const onListItemPress = useCallback(
- (item: OptionData | SearchQueryItem) => {
- const setFocusAndScrollToRight = () => {
- if (!textInputRef.current) {
- return;
- }
- textInputRef.current.focus();
- scrollToRight(textInputRef.current);
- };
-
- if (isSearchQueryItem(item)) {
- if (item.searchItemType === CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.NAVIGATE && item.action) {
- onRouterClose(item.action);
- return;
- }
+ if (!item.searchQuery) {
+ return;
+ }
- if (!item.searchQuery) {
- return;
- }
+ if (item.searchItemType === CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.CONTEXTUAL_SUGGESTION) {
+ const searchQuery = getContextualSearchQuery(item, contextualPoliciesMap, contextualReportsMap);
+ const newSearchQuery = `${searchQuery}\u00A0`;
+ onSearchQueryChange(newSearchQuery, true, undefined, false);
+ setSelection({start: newSearchQuery.length, end: newSearchQuery.length});
- if (item.searchItemType === CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.CONTEXTUAL_SUGGESTION) {
- const searchQuery = getContextualSearchQuery(item, contextualPoliciesMap, contextualReportsMap);
- const newSearchQuery = `${searchQuery}\u00A0`;
- onSearchQueryChange(newSearchQuery, true, undefined, false);
- setSelection({start: newSearchQuery.length, end: newSearchQuery.length});
-
- const autocompleteKey = getContextualSearchAutocompleteKey(item, contextualPoliciesMap, contextualReportsMap);
- if (autocompleteKey && item.autocompleteID) {
- const substitutions = {...autocompleteSubstitutions, [autocompleteKey]: item.autocompleteID};
- setAutocompleteSubstitutions(substitutions);
- }
- setFocusAndScrollToRight();
- } else if (item.searchItemType === CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.AUTOCOMPLETE_SUGGESTION && textInputValue) {
- const fieldKey = item.mapKey?.includes(':') ? item.mapKey.split(':').at(0) : item.mapKey;
- const trimmedUserSearchQuery = getTrimmedUserSearchQueryPreservingComma(textInputValue, fieldKey);
- const newSearchQuery = `${trimmedUserSearchQuery}${sanitizeSearchValue(item.searchQuery)}\u00A0`;
- onSearchQueryChange(newSearchQuery, true, undefined, false);
- setSelection({start: newSearchQuery.length, end: newSearchQuery.length});
-
- updateAutocompleteSubstitutionsForSelection({
- newSearchQuery,
- fieldKey,
- mapKey: item.mapKey,
- searchQuery: item.searchQuery,
- autocompleteID: item.autocompleteID,
- substitutions: autocompleteSubstitutions,
- setAutocompleteSubstitutions,
- });
- setFocusAndScrollToRight();
- } else if (item.searchItemType === CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.ASK_CONCIERGE) {
- const {searchQuery} = item;
- askConcierge(searchQuery);
- onRouterClose();
- } else {
- submitSearch(item.searchQuery, item.keyForList !== CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.FIND_ITEM);
- }
- } else {
- if (item?.reportID) {
- Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(item.reportID));
- } else if ('login' in item) {
- navigateToAndOpenReport({
- userLogins: item.login ? [item.login] : [],
- personalDetails,
- currentUserAccountID,
- introSelected,
- isSelfTourViewed: guidedSetupAndTourStatus?.isSelfTourViewed,
- hasCompletedGuidedSetupFlow: guidedSetupAndTourStatus?.hasCompletedGuidedSetupFlow,
- conciergeChat,
- isSupportalSession,
- shouldDismissModal: false,
- });
+ const autocompleteKey = getContextualSearchAutocompleteKey(item, contextualPoliciesMap, contextualReportsMap);
+ if (autocompleteKey && item.autocompleteID) {
+ const substitutions = {...autocompleteSubstitutions, [autocompleteKey]: item.autocompleteID};
+ setAutocompleteSubstitutions(substitutions);
}
+ setFocusAndScrollToRight();
+ } else if (item.searchItemType === CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.AUTOCOMPLETE_SUGGESTION && textInputValue) {
+ const fieldKey = item.mapKey?.includes(':') ? item.mapKey.split(':').at(0) : item.mapKey;
+ const trimmedUserSearchQuery = getTrimmedUserSearchQueryPreservingComma(textInputValue, fieldKey);
+ const newSearchQuery = `${trimmedUserSearchQuery}${sanitizeSearchValue(item.searchQuery)}\u00A0`;
+ onSearchQueryChange(newSearchQuery, true, undefined, false);
+ setSelection({start: newSearchQuery.length, end: newSearchQuery.length});
+
+ updateAutocompleteSubstitutionsForSelection({
+ newSearchQuery,
+ fieldKey,
+ mapKey: item.mapKey,
+ searchQuery: item.searchQuery,
+ autocompleteID: item.autocompleteID,
+ substitutions: autocompleteSubstitutions,
+ setAutocompleteSubstitutions,
+ });
+ setFocusAndScrollToRight();
+ } else if (item.searchItemType === CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.ASK_CONCIERGE) {
+ const {searchQuery} = item;
+ askConcierge(searchQuery);
onRouterClose();
+ } else {
+ submitSearch(item.searchQuery, item.keyForList !== CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.FIND_ITEM);
}
- },
- [
- autocompleteSubstitutions,
- onRouterClose,
- personalDetails,
- onSearchQueryChange,
- submitSearch,
- textInputValue,
- currentUserAccountID,
- introSelected,
- guidedSetupAndTourStatus?.isSelfTourViewed,
- guidedSetupAndTourStatus?.hasCompletedGuidedSetupFlow,
- conciergeChat,
- isSupportalSession,
- contextualPoliciesMap,
- contextualReportsMap,
- askConcierge,
- ],
- );
+ } else {
+ if (item?.reportID) {
+ Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(item.reportID));
+ } else if ('login' in item) {
+ navigateToAndOpenReport({
+ userLogins: item.login ? [item.login] : [],
+ personalDetails,
+ currentUserAccountID,
+ introSelected,
+ isSelfTourViewed: guidedSetupAndTourStatus?.isSelfTourViewed,
+ hasCompletedGuidedSetupFlow: guidedSetupAndTourStatus?.hasCompletedGuidedSetupFlow,
+ conciergeChat,
+ isSupportalSession,
+ shouldDismissModal: false,
+ });
+ }
+ onRouterClose();
+ }
+ };
useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ESCAPE, () => {
onRouterClose();
diff --git a/src/components/Search/SearchScopeProvider.tsx b/src/components/Search/SearchScopeProvider.tsx
index 9990ce9e7a43..c7819a25691a 100644
--- a/src/components/Search/SearchScopeProvider.tsx
+++ b/src/components/Search/SearchScopeProvider.tsx
@@ -1,6 +1,6 @@
import type ChildrenProps from '@src/types/utils/ChildrenProps';
-import React, {useContext, useMemo} from 'react';
+import React, {useContext} from 'react';
const defaultSearchContext = {
isOnSearch: false,
@@ -9,12 +9,9 @@ const defaultSearchContext = {
const SearchScopeContext = React.createContext(defaultSearchContext);
function SearchScopeProvider({children, isOnSearch = true}: ChildrenProps) {
- const searchContext = useMemo(
- () => ({
- isOnSearch,
- }),
- [isOnSearch],
- );
+ const searchContext = {
+ isOnSearch,
+ };
return {children};
}
diff --git a/src/components/Search/SearchSelectionFooter.tsx b/src/components/Search/SearchSelectionFooter.tsx
index 8a9c5378cc55..fedeeef53f1d 100644
--- a/src/components/Search/SearchSelectionFooter.tsx
+++ b/src/components/Search/SearchSelectionFooter.tsx
@@ -19,7 +19,7 @@ import type {StyleProp, ViewStyle} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {accountIDSelector} from '@selectors/Session';
-import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
+import React, {useEffect, useRef, useState} from 'react';
import {View} from 'react-native';
import type {SearchFooterCount, SearchFooterTotal, SelectedTransactionInfo, SelectedTransactions} from './types';
@@ -231,23 +231,17 @@ function SearchSelectionFooter({searchResults, style}: SearchSelectionFooterProp
const metadataReportCount = metadata?.reportCount;
const metadataCurrency = metadata?.currency;
const metadataTotal = isSnapshotTotalFromEarlierBreakdown ? answeredFooterTotals?.totals[requestedFooterTotal] : metadata?.total;
- const selectedTransactionsKeys = useMemo(() => Object.keys(selectedTransactions ?? {}), [selectedTransactions]);
- const excludedTransactionsKeys = useMemo(() => Object.keys(excludedTransactions), [excludedTransactions]);
+ const selectedTransactionsKeys = Object.keys(selectedTransactions ?? {});
+ const excludedTransactionsKeys = Object.keys(excludedTransactions);
const isExpenseType = currentSearchQueryJSON?.type === CONST.SEARCH.DATA_TYPES.EXPENSE;
- const selectedExpenseCount = useMemo(
- () => getTransactionCount(selectedTransactionsKeys, selectedTransactions, currentSearchResults?.data),
- [currentSearchResults?.data, selectedTransactions, selectedTransactionsKeys],
- );
+ const selectedExpenseCount = getTransactionCount(selectedTransactionsKeys, selectedTransactions, currentSearchResults?.data);
// The Reports footer is also expense-based, so excluding one report removes each of its child expenses here.
- const excludedExpenseCount = useMemo(
- () => (isExpenseType || isReportsSearch ? getTransactionCount(excludedTransactionsKeys, excludedTransactions, currentSearchResults?.data) : 0),
- [currentSearchResults?.data, excludedTransactions, excludedTransactionsKeys, isExpenseType, isReportsSearch],
- );
+ const excludedExpenseCount = isExpenseType || isReportsSearch ? getTransactionCount(excludedTransactionsKeys, excludedTransactions, currentSearchResults?.data) : 0;
- const excludedReportCount = useMemo(() => {
- if (!isReportsSearch || excludedTransactionsKeys.length === 0) {
- return 0;
- }
+ let excludedReportCount;
+ if (!isReportsSearch || excludedTransactionsKeys.length === 0) {
+ excludedReportCount = 0;
+ } else {
const excludedKeys = new Set(excludedTransactionsKeys);
let count = 0;
for (const [reportID, reportTransactions] of currentSearchTransactionsByReportID ?? []) {
@@ -264,135 +258,101 @@ function SearchSelectionFooter({searchResults, style}: SearchSelectionFooterProp
count += 1;
}
}
- return count;
- }, [currentSearchTransactionsByReportID, excludedTransactions, excludedTransactionsKeys, isReportsSearch]);
+ excludedReportCount = count;
+ }
// Individually-selected transactions (loose rows in a grouped view, or every row on a flat search).
- const selectedTransactionIDs = useMemo(
- () => selectedTransactionsKeys.map((key) => selectedTransactions[key]?.transaction?.transactionID).filter((transactionID): transactionID is string => !!transactionID),
- [selectedTransactions, selectedTransactionsKeys],
- );
- const excludedTransactionIDs = useMemo(
- () => excludedTransactionsKeys.map((key) => excludedTransactions[key]?.transaction?.transactionID).filter((transactionID): transactionID is string => !!transactionID),
- [excludedTransactions, excludedTransactionsKeys],
- );
- const selectedReportIDs = useMemo(() => {
- if (!isReportsSearch) {
- return EMPTY_REPORT_IDS;
- }
- return selectedReports.map((report) => report.reportID).filter((reportID): reportID is string => !!reportID);
- }, [isReportsSearch, selectedReports]);
- const excludedReportIDs = useMemo(() => {
- if (!isReportsSearch) {
- return EMPTY_REPORT_IDS;
- }
- return [
- ...new Set(
- Object.values(excludedTransactions)
- .map((transaction) => transaction.reportID)
- .filter((reportID): reportID is string => !!reportID),
- ),
- ];
- }, [excludedTransactions, isReportsSearch]);
- const selectedGroupKeys = useMemo(() => selectedTransactionsKeys.filter(isGroupEntry), [selectedTransactionsKeys]);
- const excludedGroupKeys = useMemo(() => excludedTransactionsKeys.filter(isGroupEntry), [excludedTransactionsKeys]);
+ const selectedTransactionIDs = selectedTransactionsKeys
+ .map((key) => selectedTransactions[key]?.transaction?.transactionID)
+ .filter((transactionID): transactionID is string => !!transactionID);
+ const excludedTransactionIDs = excludedTransactionsKeys
+ .map((key) => excludedTransactions[key]?.transaction?.transactionID)
+ .filter((transactionID): transactionID is string => !!transactionID);
+ const selectedReportIDs = !isReportsSearch ? EMPTY_REPORT_IDS : selectedReports.map((report) => report.reportID).filter((reportID): reportID is string => !!reportID);
+ const excludedReportIDs = !isReportsSearch
+ ? EMPTY_REPORT_IDS
+ : [
+ ...new Set(
+ Object.values(excludedTransactions)
+ .map((transaction) => transaction.reportID)
+ .filter((reportID): reportID is string => !!reportID),
+ ),
+ ];
+ const selectedGroupKeys = selectedTransactionsKeys.filter(isGroupEntry);
+ const excludedGroupKeys = excludedTransactionsKeys.filter(isGroupEntry);
// Live default-currency source figures, keyed the same way as the conversion cache, captured from the current
// selection/snapshot on every render. The freshness checks below compare these to the figures stamped when each
// conversion was requested.
- const transactionSourceByID = useMemo(() => {
- const sources: Record = {};
- for (const transactions of [selectedTransactions, excludedTransactions]) {
- for (const [key, transaction] of Object.entries(transactions)) {
- const transactionID = transaction.transaction?.transactionID;
- if (!isGroupEntry(key) && transactionID) {
- sources[transactionID] = getEntrySource(transaction);
- }
+ const transactionSourceByID: Record = {};
+ const groupSourceByKey: Record = {};
+ for (const transactions of [selectedTransactions, excludedTransactions]) {
+ for (const [key, transaction] of Object.entries(transactions)) {
+ const transactionID = transaction.transaction?.transactionID;
+ if (!isGroupEntry(key) && transactionID) {
+ transactionSourceByID[transactionID] = getEntrySource(transaction);
}
- }
- return sources;
- }, [excludedTransactions, selectedTransactions]);
- const groupSourceByKey = useMemo(() => {
- const sources: Record = {};
- for (const transactions of [selectedTransactions, excludedTransactions]) {
- for (const [key, transaction] of Object.entries(transactions)) {
- if (isGroupEntry(key)) {
- sources[key] = getEntrySource(transaction);
- }
+ if (isGroupEntry(key)) {
+ groupSourceByKey[key] = getEntrySource(transaction);
}
}
- return sources;
- }, [excludedTransactions, selectedTransactions]);
+ }
// Source figures for every loaded group, not just the selected ones. The grouped response caches every group's
// converted value, so stamping them all lets a later selection of another group reuse the cache instead of
// re-running the grouped query. Uses the same expense-signed figure as getEntrySource so a stamp always matches
// the live source the freshness checks compare against.
- const loadedGroupSourceByKey = useMemo(() => {
- const data = currentSearchResults?.data;
- if (!isGroupedSearch || !data) {
- return EMPTY_SOURCES;
- }
- const sources: Record = {};
+ let loadedGroupSourceByKey;
+ const data = currentSearchResults?.data;
+ if (!isGroupedSearch || !data) {
+ loadedGroupSourceByKey = EMPTY_SOURCES;
+ } else {
+ const loadedSources: Record = {};
for (const key of Object.keys(data)) {
if (!isGroupEntry(key)) {
continue;
}
const group: unknown = data[key];
if (group && typeof group === 'object' && 'total' in group && typeof group.total === 'number') {
- sources[key] = -group.total;
+ loadedSources[key] = -group.total;
}
}
- return sources;
- }, [currentSearchResults?.data, isGroupedSearch]);
- const reportSourceByID = useMemo(() => {
- if (!isReportsSearch) {
- return EMPTY_SOURCES;
- }
- const sources: Record = {};
+ loadedGroupSourceByKey = loadedSources;
+ }
+ let reportSourceByID;
+ if (!isReportsSearch) {
+ reportSourceByID = EMPTY_SOURCES;
+ } else {
+ const reportSources: Record = {};
for (const report of selectedReports) {
if (report.reportID) {
- sources[report.reportID] = report.total;
+ reportSources[report.reportID] = report.total;
}
}
for (const transaction of Object.values(excludedTransactions)) {
if (transaction.reportID) {
- sources[transaction.reportID] = (sources[transaction.reportID] ?? 0) + getEntrySource(transaction);
+ reportSources[transaction.reportID] = (reportSources[transaction.reportID] ?? 0) + getEntrySource(transaction);
}
}
- return sources;
- }, [excludedTransactions, isReportsSearch, selectedReports]);
+ reportSourceByID = reportSources;
+ }
// A conversion is fresh only when its converted figure is cached AND the source it was stamped against still equals
// the live source — so an inline edit that moves the live source makes it stale and triggers a refetch below.
- const isTransactionFresh = useCallback(
- (transactionID: string, currency: string) =>
- convertedTransactions?.[transactionID]?.[currency] !== undefined && conversionSources?.transactions?.[transactionID]?.[currency] === transactionSourceByID[transactionID],
- [conversionSources, convertedTransactions, transactionSourceByID],
- );
- const isGroupFresh = useCallback(
- (key: string, currency: string) => convertedGroups?.[key]?.[currency] !== undefined && conversionSources?.groups?.[key]?.[currency] === groupSourceByKey[key],
- [conversionSources, convertedGroups, groupSourceByKey],
- );
- const isReportFresh = useCallback(
- (reportID: string, currency: string) => convertedReports?.[reportID]?.[currency] !== undefined && conversionSources?.reports?.[reportID]?.[currency] === reportSourceByID[reportID],
- [conversionSources, convertedReports, reportSourceByID],
- );
+ const isTransactionFresh = (transactionID: string, currency: string) =>
+ convertedTransactions?.[transactionID]?.[currency] !== undefined && conversionSources?.transactions?.[transactionID]?.[currency] === transactionSourceByID[transactionID];
+ const isGroupFresh = (key: string, currency: string) => convertedGroups?.[key]?.[currency] !== undefined && conversionSources?.groups?.[key]?.[currency] === groupSourceByKey[key];
+ const isReportFresh = (reportID: string, currency: string) =>
+ convertedReports?.[reportID]?.[currency] !== undefined && conversionSources?.reports?.[reportID]?.[currency] === reportSourceByID[reportID];
// A conversion has been requested for the current live source when the stamp (written optimistically at request
// time) matches it — the converted value may still be in flight. The fetch effect fires on this weaker predicate,
// not on freshness: freshness also requires the value, so using it would fire a duplicate request when the
// effect re-runs off its own optimistic stamp merge.
- const wasTransactionRequested = useCallback(
- (transactionID: string, currency: string) => conversionSources?.transactions?.[transactionID]?.[currency] === transactionSourceByID[transactionID],
- [conversionSources, transactionSourceByID],
- );
- const wasGroupRequested = useCallback((key: string, currency: string) => conversionSources?.groups?.[key]?.[currency] === groupSourceByKey[key], [conversionSources, groupSourceByKey]);
- const wasReportRequested = useCallback(
- (reportID: string, currency: string) =>
- conversionSources?.reports?.[reportID]?.[currency] === reportSourceByID[reportID] || pendingReportConversionSources.current[reportID]?.[currency] === reportSourceByID[reportID],
- [conversionSources, reportSourceByID],
- );
+ const wasTransactionRequested = (transactionID: string, currency: string) => conversionSources?.transactions?.[transactionID]?.[currency] === transactionSourceByID[transactionID];
+ const wasGroupRequested = (key: string, currency: string) => conversionSources?.groups?.[key]?.[currency] === groupSourceByKey[key];
+ const wasReportRequested = (reportID: string, currency: string) =>
+ conversionSources?.reports?.[reportID]?.[currency] === reportSourceByID[reportID] || pendingReportConversionSources.current[reportID]?.[currency] === reportSourceByID[reportID];
// A cash back row holds no expenses, so leaving it out still matches the server count while the server total nets it.
// An unloaded page can still hold one, and matching counts mean that page has no expenses left to select.
@@ -464,18 +424,15 @@ function SearchSelectionFooter({searchResults, style}: SearchSelectionFooterProp
// groups and/or individual transactions.
const hasConvertibleSelection = isReportsSearch ? selectedReportIDs.length > 0 : selectedGroupKeys.length > 0 || selectedTransactionIDs.length > 0;
- const areAllSelectedConverted = useMemo(() => {
- if (!hasCustomFooterCurrency || !selectedCurrency) {
- return false;
- }
- return isReportsSearch
+ const areAllSelectedConverted =
+ !(!hasCustomFooterCurrency || !selectedCurrency) &&
+ (isReportsSearch
? areAllSelectedReportsConverted(selectedReportIDs, (reportID) => isReportFresh(reportID, selectedCurrency))
: areAllSelectedEntriesConverted(
selectedTransactions,
(key) => isGroupFresh(key, selectedCurrency),
(transactionID) => isTransactionFresh(transactionID, selectedCurrency),
- );
- }, [hasCustomFooterCurrency, isGroupFresh, isReportFresh, isReportsSearch, isTransactionFresh, selectedCurrency, selectedReportIDs, selectedTransactions]);
+ ));
const hasExcludedExpenses = isExpenseType && excludedTransactionsKeys.length > 0;
const hasExcludedReports = isReportsSearch && excludedReportIDs.length > 0;
@@ -485,19 +442,15 @@ function SearchSelectionFooter({searchResults, style}: SearchSelectionFooterProp
// A collapsed group is excluded as one entry with no expenses to classify, so nothing tells how much of it falls in
// the breakdown. No figure is shown rather than one that still counts the group.
const hasExcludedGroupUnderBreakdown = excludedGroupKeys.length > 0 && footerTotalBreakdown !== CONST.SEARCH.FOOTER_TOTAL.TOTAL;
- const areAllExcludedConverted = useMemo(() => {
- if (!hasCustomFooterCurrency || !selectedCurrency || (!hasExcludedExpenses && !hasExcludedReports)) {
- return false;
- }
- if (hasExcludedReports) {
- return areAllSelectedReportsConverted(excludedReportIDs, (reportID) => isReportFresh(reportID, selectedCurrency));
- }
- return areAllSelectedEntriesConverted(
- excludedTransactions,
- (key) => isGroupFresh(key, selectedCurrency),
- (transactionID) => isTransactionFresh(transactionID, selectedCurrency),
- );
- }, [excludedReportIDs, excludedTransactions, hasCustomFooterCurrency, hasExcludedExpenses, hasExcludedReports, isGroupFresh, isReportFresh, isTransactionFresh, selectedCurrency]);
+ const areAllExcludedConverted =
+ !(!hasCustomFooterCurrency || !selectedCurrency || (!hasExcludedExpenses && !hasExcludedReports)) &&
+ (hasExcludedReports
+ ? areAllSelectedReportsConverted(excludedReportIDs, (reportID) => isReportFresh(reportID, selectedCurrency))
+ : areAllSelectedEntriesConverted(
+ excludedTransactions,
+ (key) => isGroupFresh(key, selectedCurrency),
+ (transactionID) => isTransactionFresh(transactionID, selectedCurrency),
+ ));
// Show the loading skeleton only while a conversion can still arrive — there is something to convert, the request
// can be made (online) and hasn't failed. Otherwise the footer stays on the default-currency total instead of a
@@ -725,14 +678,16 @@ function SearchSelectionFooter({searchResults, style}: SearchSelectionFooterProp
applyFooterSelection({footerCount: nextCountType});
};
- const footerData = useMemo(() => {
- if (!shouldAllowFooterTotals && selectedTransactionsKeys.length === 0) {
- return {count: undefined, total: undefined, currency: undefined};
- }
-
+ let footerData;
+ if (!shouldAllowFooterTotals && selectedTransactionsKeys.length === 0) {
+ footerData = {
+ count: undefined,
+ total: undefined,
+ currency: undefined,
+ };
+ } else {
const selectedTransactionItems = Object.values(selectedTransactions);
const fallbackCurrency = effectiveDefaultCurrency ?? selectedTransactionItems.at(0)?.groupCurrency ?? selectedTransactionItems.at(0)?.currency;
-
if (shouldUseClientTotal) {
const shouldUseConvertedSelectedTotal = hasCustomFooterCurrency && areAllSelectedConverted && !hasConversionFailed && !!selectedCurrency;
@@ -744,109 +699,76 @@ function SearchSelectionFooter({searchResults, style}: SearchSelectionFooterProp
// from the reports' own expenses instead. A Reports search converts by report, so those expenses have no
// converted figures of their own, so the breakdown stays in the default currency and is labelled as such.
const shouldConvertSelectedTotal = shouldUseConvertedSelectedTotal && !(isReportsSearch && footerTotalBreakdown !== CONST.SEARCH.FOOTER_TOTAL.TOTAL);
-
- let total;
- if (shouldConvertSelectedTotal && isReportsSearch && selectedCurrency) {
- total = selectedReportIDs.reduce((acc, reportID) => acc - (convertedReports?.[reportID]?.[selectedCurrency] ?? 0), 0);
- } else {
- total = selectedTransactionsKeys.reduce((acc, key) => {
- const transaction = selectedTransactions[key];
- if (!doesTransactionMatchFooterTotal(transaction.transaction, footerTotalBreakdown)) {
- return acc;
- }
- let convertedAmount;
- if (shouldConvertSelectedTotal && selectedCurrency) {
- if (isGroupEntry(key)) {
- convertedAmount = convertedGroups?.[key]?.[selectedCurrency];
- } else if (transaction.transaction?.transactionID) {
- convertedAmount = convertedTransactions?.[transaction.transaction.transactionID]?.[selectedCurrency];
- }
- }
- return acc - (convertedAmount ?? getEntrySource(transaction));
- }, 0);
- }
-
- return {count: selectedExpenseCount, total, currency: shouldConvertSelectedTotal ? selectedCurrency : fallbackCurrency};
- }
-
- if (hasExcludedGroupUnderBreakdown) {
- return {
+ const total =
+ shouldConvertSelectedTotal && isReportsSearch && selectedCurrency
+ ? selectedReportIDs.reduce((acc, reportID) => acc - (convertedReports?.[reportID]?.[selectedCurrency] ?? 0), 0)
+ : selectedTransactionsKeys.reduce((acc, key) => {
+ const transaction = selectedTransactions[key];
+ if (!doesTransactionMatchFooterTotal(transaction.transaction, footerTotalBreakdown)) {
+ return acc;
+ }
+ let convertedAmount;
+ if (shouldConvertSelectedTotal && selectedCurrency) {
+ if (isGroupEntry(key)) {
+ convertedAmount = convertedGroups?.[key]?.[selectedCurrency];
+ } else if (transaction.transaction?.transactionID) {
+ convertedAmount = convertedTransactions?.[transaction.transaction.transactionID]?.[selectedCurrency];
+ }
+ }
+ return acc - (convertedAmount ?? getEntrySource(transaction));
+ }, 0);
+ footerData = {
+ count: selectedExpenseCount,
+ total,
+ currency: shouldConvertSelectedTotal ? selectedCurrency : fallbackCurrency,
+ };
+ } else if (hasExcludedGroupUnderBreakdown) {
+ footerData = {
count: metadataCount === undefined ? undefined : Math.max(metadataCount - excludedExpenseCount, 0),
total: undefined,
currency: effectiveDefaultCurrency ?? metadataCurrency,
};
- }
-
- const excludedKeysForTotal = excludedTransactionsKeys.filter((key) => doesTransactionMatchFooterTotal(excludedTransactions[key]?.transaction, footerTotalBreakdown));
- if (
- hasCustomFooterCurrency &&
- isSearchTotalFresh &&
- canConvertExcludedReports &&
- ((!hasExcludedExpenses && !hasExcludedReports) || areAllExcludedConverted) &&
- !hasConversionFailed &&
- selectedCurrencyConvertedTotal
- ) {
- let excludedConvertedTotal = 0;
- if (hasExcludedReports && selectedCurrency) {
- excludedConvertedTotal = excludedReportIDs.reduce((total, reportID) => total - (convertedReports?.[reportID]?.[selectedCurrency] ?? 0), 0);
- } else if (hasExcludedExpenses) {
- excludedConvertedTotal = excludedKeysForTotal.reduce((total, key) => {
- const transaction = excludedTransactions[key];
- const transactionID = transaction.transaction?.transactionID;
- let convertedAmount;
- if (isGroupEntry(key)) {
- convertedAmount = convertedGroups?.[key]?.[selectedCurrency];
- } else if (transactionID) {
- convertedAmount = convertedTransactions?.[transactionID]?.[selectedCurrency];
- }
- return total - (convertedAmount ?? getEntrySource(transaction));
- }, 0);
+ } else {
+ const excludedKeysForTotal = excludedTransactionsKeys.filter((key) => doesTransactionMatchFooterTotal(excludedTransactions[key]?.transaction, footerTotalBreakdown));
+ if (
+ hasCustomFooterCurrency &&
+ isSearchTotalFresh &&
+ canConvertExcludedReports &&
+ ((!hasExcludedExpenses && !hasExcludedReports) || areAllExcludedConverted) &&
+ !hasConversionFailed &&
+ selectedCurrencyConvertedTotal
+ ) {
+ let excludedConvertedTotal = 0;
+ if (hasExcludedReports && selectedCurrency) {
+ excludedConvertedTotal = excludedReportIDs.reduce((total, reportID) => total - (convertedReports?.[reportID]?.[selectedCurrency] ?? 0), 0);
+ } else if (hasExcludedExpenses) {
+ excludedConvertedTotal = excludedKeysForTotal.reduce((total, key) => {
+ const transaction = excludedTransactions[key];
+ const transactionID = transaction.transaction?.transactionID;
+ let convertedAmount;
+ if (isGroupEntry(key)) {
+ convertedAmount = convertedGroups?.[key]?.[selectedCurrency];
+ } else if (transactionID) {
+ convertedAmount = convertedTransactions?.[transactionID]?.[selectedCurrency];
+ }
+ return total - (convertedAmount ?? getEntrySource(transaction));
+ }, 0);
+ }
+ footerData = {
+ count: Math.max(selectedCurrencyConvertedTotal.count - excludedExpenseCount, 0),
+ total: selectedCurrencyConvertedTotal.total - excludedConvertedTotal,
+ currency: selectedCurrency,
+ };
+ } else {
+ const excludedTotal = hasExcludedExpenses || hasExcludedReports ? getTransactionTotal(excludedKeysForTotal.map((key) => excludedTransactions[key])) : 0;
+ footerData = {
+ count: metadataCount === undefined ? undefined : Math.max(metadataCount - excludedExpenseCount, 0),
+ total: metadataTotal === undefined ? undefined : metadataTotal - excludedTotal,
+ currency: effectiveDefaultCurrency ?? metadataCurrency,
+ };
}
- return {
- count: Math.max(selectedCurrencyConvertedTotal.count - excludedExpenseCount, 0),
- total: selectedCurrencyConvertedTotal.total - excludedConvertedTotal,
- currency: selectedCurrency,
- };
}
-
- const excludedTotal = hasExcludedExpenses || hasExcludedReports ? getTransactionTotal(excludedKeysForTotal.map((key) => excludedTransactions[key])) : 0;
- return {
- count: metadataCount === undefined ? undefined : Math.max(metadataCount - excludedExpenseCount, 0),
- total: metadataTotal === undefined ? undefined : metadataTotal - excludedTotal,
- currency: effectiveDefaultCurrency ?? metadataCurrency,
- };
- }, [
- areAllSelectedConverted,
- areAllExcludedConverted,
- canConvertExcludedReports,
- convertedGroups,
- convertedReports,
- convertedTransactions,
- effectiveDefaultCurrency,
- excludedExpenseCount,
- excludedReportIDs,
- excludedTransactions,
- excludedTransactionsKeys,
- hasConversionFailed,
- hasCustomFooterCurrency,
- hasExcludedExpenses,
- hasExcludedGroupUnderBreakdown,
- hasExcludedReports,
- isReportsSearch,
- isSearchTotalFresh,
- metadataCount,
- metadataCurrency,
- metadataTotal,
- selectedCurrency,
- selectedCurrencyConvertedTotal,
- selectedExpenseCount,
- selectedReportIDs,
- selectedTransactions,
- selectedTransactionsKeys,
- shouldAllowFooterTotals,
- shouldUseClientTotal,
- footerTotalBreakdown,
- ]);
+ }
if (!shouldShowFooter) {
return null;
diff --git a/src/components/Search/SearchTableHeader.tsx b/src/components/Search/SearchTableHeader.tsx
index b53ed1a71a76..e4db145179e4 100644
--- a/src/components/Search/SearchTableHeader.tsx
+++ b/src/components/Search/SearchTableHeader.tsx
@@ -11,7 +11,7 @@ import type IconAsset from '@src/types/utils/IconAsset';
import type {ValueOf} from 'type-fest';
-import React, {useCallback, useMemo} from 'react';
+import React from 'react';
import type {SearchColumnType, SearchGroupBy, SearchSortBy, SortOrder} from './types';
@@ -600,20 +600,16 @@ function SearchTableHeader({
// Only load Profile icon when it's needed for EXPENSE_REPORT type or grouped transactions
const icons = useMemoizedLazyExpensifyIcons(type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT || !!groupBy ? ['Profile', 'Bank', 'CreditCard'] : []) satisfies SearchHeaderIcons;
- const shouldShowColumn = useCallback(
- (columnName: SearchColumnType) => {
- return columns.includes(columnName);
- },
- [columns],
- );
-
- const columnConfig = useMemo(() => getSearchColumns(type, icons, groupBy, isExpenseReportView), [type, groupBy, icons, isExpenseReportView]);
+ const shouldShowColumn = (columnName: SearchColumnType) => {
+ return columns.includes(columnName);
+ };
- const orderedColumnConfig = useMemo(() => {
- if (!columnConfig) {
- return null;
- }
+ const columnConfig = getSearchColumns(type, icons, groupBy, isExpenseReportView);
+ let orderedColumnConfig;
+ if (!columnConfig) {
+ orderedColumnConfig = null;
+ } else {
const configMap = new Map(columnConfig.map((config) => [config.columnName, config]));
// Users can customize column order via the Search Columns page.
@@ -621,7 +617,6 @@ function SearchTableHeader({
// then appending any remaining columns (which will be filtered out by shouldShowColumn).
const orderedConfig: SearchColumnConfig[] = [];
const addedColumns = new Set();
-
for (const col of columns) {
const config = configMap.get(col);
if (config) {
@@ -629,15 +624,13 @@ function SearchTableHeader({
addedColumns.add(col);
}
}
-
for (const config of columnConfig) {
if (!addedColumns.has(config.columnName)) {
orderedConfig.push(config);
}
}
-
- return orderedConfig;
- }, [columnConfig, columns]);
+ orderedColumnConfig = orderedConfig;
+ }
if (displayNarrowVersion) {
return;
diff --git a/src/components/Search/hooks/useLiveFilteredReportActions.ts b/src/components/Search/hooks/useLiveFilteredReportActions.ts
index 6b3fae867e8e..aa0d4ec98b01 100644
--- a/src/components/Search/hooks/useLiveFilteredReportActions.ts
+++ b/src/components/Search/hooks/useLiveFilteredReportActions.ts
@@ -3,7 +3,6 @@ import {selectFilteredReportActions, selectFilteredReportActionsForReports} from
import ONYXKEYS from '@src/ONYXKEYS';
import type {ReportAction} from '@src/types/onyx';
-import {useCallback, useMemo} from 'react';
// Use the original useOnyx hook to get the real-time data from Onyx and not from the snapshot
// eslint-disable-next-line no-restricted-imports
import {useOnyx as useOnyxWithoutSnapshots} from 'react-native-onyx';
@@ -14,12 +13,9 @@ import {useOnyx as useOnyxWithoutSnapshots} from 'react-native-onyx';
* group renders, so the selected output and its equality check stay small.
*/
function useLiveFilteredReportActions(reportIDs?: string[]): Record | undefined {
- const reportActionsKeys = useMemo(() => [...new Set(reportIDs)].map((reportID) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`), [reportIDs]);
- const selector = useCallback(
- (reportActions: Parameters[0]) =>
- reportIDs ? selectFilteredReportActionsForReports(reportActions, reportActionsKeys) : selectFilteredReportActions(reportActions),
- [reportIDs, reportActionsKeys],
- );
+ const reportActionsKeys = [...new Set(reportIDs)].map((reportID) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`);
+ const selector = (reportActions: Parameters[0]) =>
+ reportIDs ? selectFilteredReportActionsForReports(reportActions, reportActionsKeys) : selectFilteredReportActions(reportActions);
const [liveReportActions] = useOnyxWithoutSnapshots