diff --git a/android/app/build.gradle b/android/app/build.gradle
index dc72b8825ae6..0b3bad0d8676 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -98,8 +98,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1001044004
- versionName "1.4.40-4"
+ versionCode 1001044005
+ versionName "1.4.40-5"
}
flavorDimensions "default"
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 2c48680bf3bd..facd7194172a 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.4.40.4
+ 1.4.40.5
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index b8b2f7e2383b..2d7838cfcdec 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature
????
CFBundleVersion
- 1.4.40.4
+ 1.4.40.5
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index 897ee8140144..86503558bfe1 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -13,7 +13,7 @@
CFBundleShortVersionString
1.4.40
CFBundleVersion
- 1.4.40.4
+ 1.4.40.5
NSExtension
NSExtensionPointIdentifier
diff --git a/package-lock.json b/package-lock.json
index f37598a02ae0..09e89b9c484c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.4.40-4",
+ "version": "1.4.40-5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.4.40-4",
+ "version": "1.4.40-5",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index eb64e5b0c7ce..105db98bf9b1 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.4.40-4",
+ "version": "1.4.40-5",
"author": "Expensify, Inc.",
"homepage": "https://new.expensify.com",
"description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",
diff --git a/src/App.tsx b/src/App.tsx
index 7c1ead1d86d3..52baedc79421 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -6,6 +6,7 @@ import Onyx from 'react-native-onyx';
import {PickerStateProvider} from 'react-native-picker-select';
import {SafeAreaProvider} from 'react-native-safe-area-context';
import '../wdyr';
+import ActiveElementRoleProvider from './components/ActiveElementRoleProvider';
import ActiveWorkspaceContextProvider from './components/ActiveWorkspace/ActiveWorkspaceProvider';
import ColorSchemeWrapper from './components/ColorSchemeWrapper';
import ComposeProviders from './components/ComposeProviders';
@@ -78,6 +79,7 @@ function App({url}: AppProps) {
PickerStateProvider,
EnvironmentProvider,
CustomStatusBarAndBackgroundContextProvider,
+ ActiveElementRoleProvider,
ActiveWorkspaceContextProvider,
]}
>
diff --git a/src/CONST.ts b/src/CONST.ts
index f1b7e74bbde4..a939f0942839 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -1413,9 +1413,9 @@ const CONST = {
OWNER_EMAIL_FAKE: '_FAKE_',
OWNER_ACCOUNT_ID_FAKE: 0,
REIMBURSEMENT_CHOICES: {
- REIMBURSEMENT_YES: 'reimburseYes', // Direct
- REIMBURSEMENT_NO: 'reimburseNo', // None
- REIMBURSEMENT_MANUAL: 'reimburseManual', // Indirect
+ REIMBURSEMENT_YES: 'reimburseYes',
+ REIMBURSEMENT_NO: 'reimburseNo',
+ REIMBURSEMENT_MANUAL: 'reimburseManual',
},
ID_FAKE: '_FAKE_',
EMPTY: 'EMPTY',
diff --git a/src/components/ActiveElementRoleProvider/index.native.tsx b/src/components/ActiveElementRoleProvider/index.native.tsx
new file mode 100644
index 000000000000..4a9f2290b2b0
--- /dev/null
+++ b/src/components/ActiveElementRoleProvider/index.native.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import type {ActiveElementRoleContextValue, ActiveElementRoleProps} from './types';
+
+const ActiveElementRoleContext = React.createContext({
+ role: null,
+});
+
+function ActiveElementRoleProvider({children}: ActiveElementRoleProps) {
+ const value = React.useMemo(
+ () => ({
+ role: null,
+ }),
+ [],
+ );
+
+ return {children};
+}
+
+export default ActiveElementRoleProvider;
+export {ActiveElementRoleContext};
diff --git a/src/components/ActiveElementRoleProvider/index.tsx b/src/components/ActiveElementRoleProvider/index.tsx
new file mode 100644
index 000000000000..630af8618c08
--- /dev/null
+++ b/src/components/ActiveElementRoleProvider/index.tsx
@@ -0,0 +1,40 @@
+import React, {useEffect, useState} from 'react';
+import type {ActiveElementRoleContextValue, ActiveElementRoleProps} from './types';
+
+const ActiveElementRoleContext = React.createContext({
+ role: null,
+});
+
+function ActiveElementRoleProvider({children}: ActiveElementRoleProps) {
+ const [activeRoleRef, setRole] = useState(document?.activeElement?.role ?? null);
+
+ const handleFocusIn = () => {
+ setRole(document?.activeElement?.role ?? null);
+ };
+
+ const handleFocusOut = () => {
+ setRole(null);
+ };
+
+ useEffect(() => {
+ document.addEventListener('focusin', handleFocusIn);
+ document.addEventListener('focusout', handleFocusOut);
+
+ return () => {
+ document.removeEventListener('focusin', handleFocusIn);
+ document.removeEventListener('focusout', handleFocusOut);
+ };
+ }, []);
+
+ const value = React.useMemo(
+ () => ({
+ role: activeRoleRef,
+ }),
+ [activeRoleRef],
+ );
+
+ return {children};
+}
+
+export default ActiveElementRoleProvider;
+export {ActiveElementRoleContext};
diff --git a/src/components/ActiveElementRoleProvider/types.ts b/src/components/ActiveElementRoleProvider/types.ts
new file mode 100644
index 000000000000..f22343b12550
--- /dev/null
+++ b/src/components/ActiveElementRoleProvider/types.ts
@@ -0,0 +1,9 @@
+type ActiveElementRoleContextValue = {
+ role: string | null;
+};
+
+type ActiveElementRoleProps = {
+ children: React.ReactNode;
+};
+
+export type {ActiveElementRoleContextValue, ActiveElementRoleProps};
diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx
index f4b6e8b23ecf..1961829b6aa7 100644
--- a/src/components/Button/index.tsx
+++ b/src/components/Button/index.tsx
@@ -265,14 +265,16 @@ function Button(
return (
<>
-
+ {pressOnEnter && (
+
+ )}
{
diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx
index a55d329f39ee..27f424ad1b70 100644
--- a/src/components/LHNOptionsList/LHNOptionsList.tsx
+++ b/src/components/LHNOptionsList/LHNOptionsList.tsx
@@ -1,6 +1,6 @@
import {FlashList} from '@shopify/flash-list';
import type {ReactElement} from 'react';
-import React, {useCallback} from 'react';
+import React, {memo, useCallback} from 'react';
import {StyleSheet, View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import withCurrentReportID from '@components/withCurrentReportID';
@@ -158,7 +158,7 @@ export default withCurrentReportID(
transactionViolations: {
key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS,
},
- })(LHNOptionsList),
+ })(memo(LHNOptionsList)),
);
export type {LHNOptionsListProps};
diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index bfe5a7ff2a75..0e08aed214a6 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -57,10 +57,9 @@ function MoneyReportHeader({session, policy, chatReport, nextStep, report: money
const isAutoReimbursable = ReportUtils.canBeAutoReimbursed(moneyRequestReport, policy);
const isPaidGroupPolicy = ReportUtils.isPaidGroupPolicy(moneyRequestReport);
const isManager = ReportUtils.isMoneyRequestReport(moneyRequestReport) && session?.accountID === moneyRequestReport.managerID;
- const isReimburser = session?.email === policy?.reimburserEmail;
const isPayer = isPaidGroupPolicy
? // In a group policy, the admin approver can pay the report directly by skipping the approval step
- isReimburser && (isApproved || isManager)
+ isPolicyAdmin && (isApproved || isManager)
: isPolicyAdmin || (ReportUtils.isMoneyRequestReport(moneyRequestReport) && isManager);
const isDraft = ReportUtils.isDraftExpenseReport(moneyRequestReport);
const isOnInstantSubmitPolicy = PolicyUtils.isInstantSubmitEnabled(policy);
diff --git a/src/components/OnyxProvider.tsx b/src/components/OnyxProvider.tsx
index 124f3558df90..d14aec90fa10 100644
--- a/src/components/OnyxProvider.tsx
+++ b/src/components/OnyxProvider.tsx
@@ -10,11 +10,12 @@ const [withPersonalDetails, PersonalDetailsProvider, , usePersonalDetails] = cre
const [withCurrentDate, CurrentDateProvider] = createOnyxContext(ONYXKEYS.CURRENT_DATE);
const [withReportActionsDrafts, ReportActionsDraftsProvider] = createOnyxContext(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS);
const [withBlockedFromConcierge, BlockedFromConciergeProvider] = createOnyxContext(ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE);
-const [withBetas, BetasProvider, BetasContext] = createOnyxContext(ONYXKEYS.BETAS);
+const [withBetas, BetasProvider, BetasContext, useBetas] = createOnyxContext(ONYXKEYS.BETAS);
const [withReportCommentDrafts, ReportCommentDraftsProvider] = createOnyxContext(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT);
const [withPreferredTheme, PreferredThemeProvider, PreferredThemeContext] = createOnyxContext(ONYXKEYS.PREFERRED_THEME);
const [withFrequentlyUsedEmojis, FrequentlyUsedEmojisProvider, , useFrequentlyUsedEmojis] = createOnyxContext(ONYXKEYS.FREQUENTLY_USED_EMOJIS);
const [withPreferredEmojiSkinTone, PreferredEmojiSkinToneProvider, PreferredEmojiSkinToneContext] = createOnyxContext(ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE);
+const [, SessionProvider, , useSession] = createOnyxContext(ONYXKEYS.SESSION);
type OnyxProviderProps = {
/** Rendered child component */
@@ -35,6 +36,7 @@ function OnyxProvider(props: OnyxProviderProps) {
PreferredThemeProvider,
FrequentlyUsedEmojisProvider,
PreferredEmojiSkinToneProvider,
+ SessionProvider,
]}
>
{props.children}
@@ -59,8 +61,10 @@ export {
withReportCommentDrafts,
withPreferredTheme,
PreferredThemeContext,
+ useBetas,
withFrequentlyUsedEmojis,
useFrequentlyUsedEmojis,
withPreferredEmojiSkinTone,
PreferredEmojiSkinToneContext,
+ useSession,
};
diff --git a/src/components/OptionsList/BaseOptionsList.tsx b/src/components/OptionsList/BaseOptionsList.tsx
index 4a9df89ae644..575df128894a 100644
--- a/src/components/OptionsList/BaseOptionsList.tsx
+++ b/src/components/OptionsList/BaseOptionsList.tsx
@@ -1,6 +1,6 @@
import isEqual from 'lodash/isEqual';
import type {ForwardedRef} from 'react';
-import React, {forwardRef, memo, useEffect, useRef} from 'react';
+import React, {forwardRef, memo, useEffect, useMemo, useRef} from 'react';
import type {SectionListRenderItem} from 'react-native';
import {View} from 'react-native';
import OptionRow from '@components/OptionRow';
@@ -31,7 +31,7 @@ function BaseOptionsList(
shouldHaveOptionSeparator = false,
showTitleTooltip = false,
optionHoveredStyle,
- contentContainerStyles,
+ contentContainerStyles: contentContainerStylesProp,
sectionHeaderStyle,
showScrollIndicator = true,
listContainerStyles: listContainerStylesProp,
@@ -51,6 +51,7 @@ function BaseOptionsList(
nestedScrollEnabled = true,
bounces = true,
renderFooterContent,
+ safeAreaPaddingBottomStyle,
}: BaseOptionListProps,
ref: ForwardedRef,
) {
@@ -64,7 +65,8 @@ function BaseOptionsList(
const previousSections = usePrevious(sections);
const didLayout = useRef(false);
- const listContainerStyles = listContainerStylesProp ?? [styles.flex1];
+ const listContainerStyles = useMemo(() => listContainerStylesProp ?? [styles.flex1], [listContainerStylesProp, styles.flex1]);
+ const contentContainerStyles = useMemo(() => [safeAreaPaddingBottomStyle, contentContainerStylesProp], [contentContainerStylesProp, safeAreaPaddingBottomStyle]);
/**
* This helper function is used to memoize the computation needed for getItemLayout. It is run whenever section data changes.
diff --git a/src/components/OptionsList/index.native.tsx b/src/components/OptionsList/index.native.tsx
index bdcd0418a940..d4977f25f366 100644
--- a/src/components/OptionsList/index.native.tsx
+++ b/src/components/OptionsList/index.native.tsx
@@ -10,7 +10,7 @@ function OptionsList(props: OptionsListProps, ref: ForwardedRef
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
ref={ref}
- onScrollBeginDrag={() => Keyboard.dismiss()}
+ onScrollBeginDrag={Keyboard.dismiss}
/>
);
}
diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx
index 4066776711b1..cfe06b2c0a62 100644
--- a/src/components/ReportActionItem/ReportPreview.tsx
+++ b/src/components/ReportActionItem/ReportPreview.tsx
@@ -122,7 +122,6 @@ function ReportPreview({
const {totalDisplaySpend, reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(iouReport);
const policyType = policy?.type;
const isAutoReimbursable = ReportUtils.canBeAutoReimbursed(iouReport, policy);
- const isReimburser = session?.email === policy?.reimburserEmail;
const iouSettled = ReportUtils.isSettled(iouReportID);
const iouCanceled = ReportUtils.isArchivedRoom(chatReport);
@@ -213,7 +212,7 @@ function ReportPreview({
const isPolicyAdmin = policyType !== CONST.POLICY.TYPE.PERSONAL && policy?.role === CONST.POLICY.ROLE.ADMIN;
const isPayer = isPaidGroupPolicy
? // In a paid group policy, the admin approver can pay the report directly by skipping the approval step
- isReimburser && (isApproved || isCurrentUserManager)
+ isPolicyAdmin && (isApproved || isCurrentUserManager)
: isPolicyAdmin || (isMoneyRequestReport && isCurrentUserManager);
const isOnInstantSubmitPolicy = PolicyUtils.isInstantSubmitEnabled(policy);
const isOnSubmitAndClosePolicy = PolicyUtils.isSubmitAndClose(policy);
diff --git a/src/components/withCurrentUserPersonalDetails.tsx b/src/components/withCurrentUserPersonalDetails.tsx
index 9406c8634c1b..313bcad74f35 100644
--- a/src/components/withCurrentUserPersonalDetails.tsx
+++ b/src/components/withCurrentUserPersonalDetails.tsx
@@ -1,26 +1,17 @@
import type {ComponentType, ForwardedRef, RefAttributes} from 'react';
-import React, {useMemo} from 'react';
-import type {OnyxEntry} from 'react-native-onyx';
-import {withOnyx} from 'react-native-onyx';
+import React from 'react';
+import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import getComponentDisplayName from '@libs/getComponentDisplayName';
import personalDetailsPropType from '@pages/personalDetailsPropType';
-import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
-import type {PersonalDetails, Session} from '@src/types/onyx';
-import {usePersonalDetails} from './OnyxProvider';
+import type {PersonalDetails} from '@src/types/onyx';
type CurrentUserPersonalDetails = PersonalDetails | Record;
-type OnyxProps = {
- /** Session of the current user */
- session: OnyxEntry;
-};
-
type HOCProps = {
currentUserPersonalDetails: CurrentUserPersonalDetails;
};
-type WithCurrentUserPersonalDetailsProps = OnyxProps & HOCProps;
+type WithCurrentUserPersonalDetailsProps = HOCProps;
// TODO: remove when all components that use it will be migrated to TS
const withCurrentUserPersonalDetailsPropTypes = {
@@ -33,15 +24,9 @@ const withCurrentUserPersonalDetailsDefaultProps: HOCProps = {
export default function (
WrappedComponent: ComponentType>,
-): ComponentType & RefAttributes, keyof OnyxProps>> {
+): ComponentType & RefAttributes> {
function WithCurrentUserPersonalDetails(props: Omit, ref: ForwardedRef) {
- const personalDetails = usePersonalDetails() ?? CONST.EMPTY_OBJECT;
- const accountID = props.session?.accountID ?? 0;
- const accountPersonalDetails = personalDetails?.[accountID];
- const currentUserPersonalDetails: CurrentUserPersonalDetails = useMemo(
- () => (accountPersonalDetails ? {...accountPersonalDetails, accountID} : {}) as CurrentUserPersonalDetails,
- [accountPersonalDetails, accountID],
- );
+ const currentUserPersonalDetails = useCurrentUserPersonalDetails();
return (
& RefAttributes, OnyxProps>({
- session: {
- key: ONYXKEYS.SESSION,
- },
- })(withCurrentUserPersonalDetails);
+ return React.forwardRef(WithCurrentUserPersonalDetails);
}
export {withCurrentUserPersonalDetailsPropTypes, withCurrentUserPersonalDetailsDefaultProps};
diff --git a/src/hooks/useActiveElementRole/index.native.ts b/src/hooks/useActiveElementRole/index.native.ts
deleted file mode 100644
index 4278014f02a8..000000000000
--- a/src/hooks/useActiveElementRole/index.native.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import type UseActiveElementRole from './types';
-
-/**
- * Native doesn't have the DOM, so we just return null.
- */
-const useActiveElementRole: UseActiveElementRole = () => null;
-
-export default useActiveElementRole;
diff --git a/src/hooks/useActiveElementRole/index.ts b/src/hooks/useActiveElementRole/index.ts
index a98999105ac8..98ae285f92b0 100644
--- a/src/hooks/useActiveElementRole/index.ts
+++ b/src/hooks/useActiveElementRole/index.ts
@@ -1,4 +1,5 @@
-import {useEffect, useRef} from 'react';
+import {useContext} from 'react';
+import {ActiveElementRoleContext} from '@components/ActiveElementRoleProvider';
import type UseActiveElementRole from './types';
/**
@@ -6,27 +7,9 @@ import type UseActiveElementRole from './types';
* On native, we just return null.
*/
const useActiveElementRole: UseActiveElementRole = () => {
- const activeRoleRef = useRef(document?.activeElement?.role);
+ const {role} = useContext(ActiveElementRoleContext);
- const handleFocusIn = () => {
- activeRoleRef.current = document?.activeElement?.role;
- };
-
- const handleFocusOut = () => {
- activeRoleRef.current = null;
- };
-
- useEffect(() => {
- document.addEventListener('focusin', handleFocusIn);
- document.addEventListener('focusout', handleFocusOut);
-
- return () => {
- document.removeEventListener('focusin', handleFocusIn);
- document.removeEventListener('focusout', handleFocusOut);
- };
- }, []);
-
- return activeRoleRef.current;
+ return role;
};
export default useActiveElementRole;
diff --git a/src/hooks/useActiveElementRole/types.ts b/src/hooks/useActiveElementRole/types.ts
index c31b8ab7ddbf..f6884548785f 100644
--- a/src/hooks/useActiveElementRole/types.ts
+++ b/src/hooks/useActiveElementRole/types.ts
@@ -1,3 +1,3 @@
-type UseActiveElementRole = () => string | null | undefined;
+type UseActiveElementRole = () => string | null;
export default UseActiveElementRole;
diff --git a/src/hooks/useCurrentUserPersonalDetails.ts b/src/hooks/useCurrentUserPersonalDetails.ts
new file mode 100644
index 000000000000..da3c2b18bd83
--- /dev/null
+++ b/src/hooks/useCurrentUserPersonalDetails.ts
@@ -0,0 +1,21 @@
+import {useMemo} from 'react';
+import {usePersonalDetails, useSession} from '@components/OnyxProvider';
+import CONST from '@src/CONST';
+import type {PersonalDetails} from '@src/types/onyx';
+
+type CurrentUserPersonalDetails = PersonalDetails | Record;
+
+function useCurrentUserPersonalDetails() {
+ const session = useSession();
+ const personalDetails = usePersonalDetails() ?? CONST.EMPTY_OBJECT;
+ const accountID = session?.accountID ?? 0;
+ const accountPersonalDetails = personalDetails?.[accountID];
+ const currentUserPersonalDetails: CurrentUserPersonalDetails = useMemo(
+ () => (accountPersonalDetails ? {...accountPersonalDetails, accountID} : {}) as CurrentUserPersonalDetails,
+ [accountPersonalDetails, accountID],
+ );
+
+ return currentUserPersonalDetails;
+}
+
+export default useCurrentUserPersonalDetails;
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 5da1aa3a9dc6..6d853604aa20 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -1759,7 +1759,7 @@ export default {
trackDistanceCopy: 'Configura la tarifa y unidad usadas para medir distancias.',
trackDistanceRate: 'Tarifa',
trackDistanceUnit: 'Unidad',
- trackDistanceChooseUnit: 'Elija una unidad predeterminada para rastrear.',
+ trackDistanceChooseUnit: 'Elige una unidad predeterminada de medida.',
kilometers: 'Kilómetros',
miles: 'Millas',
unlockNextDayReimbursements: 'Desbloquea reembolsos diarios',
diff --git a/src/libs/focusEditAfterCancelDelete/index.native.ts b/src/libs/focusEditAfterCancelDelete/index.native.ts
new file mode 100755
index 000000000000..17bafabc5790
--- /dev/null
+++ b/src/libs/focusEditAfterCancelDelete/index.native.ts
@@ -0,0 +1,8 @@
+import {InteractionManager} from 'react-native';
+import type FocusEditAfterCancelDelete from './types';
+
+const focusEditAfterCancelDelete: FocusEditAfterCancelDelete = (textInputRef) => {
+ InteractionManager.runAfterInteractions(() => textInputRef?.focus());
+};
+
+export default focusEditAfterCancelDelete;
diff --git a/src/libs/focusEditAfterCancelDelete/index.ts b/src/libs/focusEditAfterCancelDelete/index.ts
new file mode 100755
index 000000000000..541c0ef1aaef
--- /dev/null
+++ b/src/libs/focusEditAfterCancelDelete/index.ts
@@ -0,0 +1,5 @@
+import type FocusEditAfterCancelDelete from './types';
+
+const focusEditAfterCancelDelete: FocusEditAfterCancelDelete = () => {};
+
+export default focusEditAfterCancelDelete;
diff --git a/src/libs/focusEditAfterCancelDelete/types.ts b/src/libs/focusEditAfterCancelDelete/types.ts
new file mode 100755
index 000000000000..ee479203f890
--- /dev/null
+++ b/src/libs/focusEditAfterCancelDelete/types.ts
@@ -0,0 +1,5 @@
+import type {TextInput} from 'react-native';
+
+type FocusEditAfterCancelDelete = (inputRef: TextInput | HTMLTextAreaElement | null) => void;
+
+export default FocusEditAfterCancelDelete;
diff --git a/src/pages/ShareCodePage.tsx b/src/pages/ShareCodePage.tsx
index 138aa729fcc2..e4e2a90c4157 100644
--- a/src/pages/ShareCodePage.tsx
+++ b/src/pages/ShareCodePage.tsx
@@ -12,8 +12,7 @@ import QRShareWithDownload from '@components/QRShare/QRShareWithDownload';
import type QRShareWithDownloadHandle from '@components/QRShare/QRShareWithDownload/types';
import ScreenWrapper from '@components/ScreenWrapper';
import Section from '@components/Section';
-import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails';
-import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails';
+import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useEnvironment from '@hooks/useEnvironment';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -28,9 +27,9 @@ import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import type {Report, Session} from '@src/types/onyx';
-type ShareCodePageOnyxProps = WithCurrentUserPersonalDetailsProps & {
+type ShareCodePageOnyxProps = {
/** Session info for the currently logged in user. */
- session: OnyxEntry;
+ session?: OnyxEntry;
/** The report currently being looked at */
report?: OnyxEntry;
@@ -38,12 +37,13 @@ type ShareCodePageOnyxProps = WithCurrentUserPersonalDetailsProps & {
type ShareCodePageProps = ShareCodePageOnyxProps;
-function ShareCodePage({report, session, currentUserPersonalDetails}: ShareCodePageProps) {
+function ShareCodePage({report, session}: ShareCodePageProps) {
const themeStyles = useThemeStyles();
const {translate} = useLocalize();
const {environmentURL} = useEnvironment();
const qrCodeRef = useRef(null);
const {isSmallScreenWidth} = useWindowDimensions();
+ const currentUserPersonalDetails = useCurrentUserPersonalDetails();
const isReport = !!report?.reportID;
@@ -145,4 +145,4 @@ function ShareCodePage({report, session, currentUserPersonalDetails}: ShareCodeP
ShareCodePage.displayName = 'ShareCodePage';
-export default withCurrentUserPersonalDetails(ShareCodePage);
+export default ShareCodePage;
diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx
index e97aa0338f90..427c6ccdbfc4 100644
--- a/src/pages/home/report/ReportActionItemMessageEdit.tsx
+++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx
@@ -3,7 +3,7 @@ import Str from 'expensify-common/lib/str';
import lodashDebounce from 'lodash/debounce';
import type {ForwardedRef} from 'react';
import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react';
-import {InteractionManager, Keyboard, View} from 'react-native';
+import {Keyboard, View} from 'react-native';
import type {NativeSyntheticEvent, TextInput, TextInputFocusEventData, TextInputKeyPressEventData} from 'react-native';
import type {Emoji} from '@assets/emojis/types';
import Composer from '@components/Composer';
@@ -25,6 +25,7 @@ import * as Browser from '@libs/Browser';
import * as ComposerUtils from '@libs/ComposerUtils';
import * as EmojiUtils from '@libs/EmojiUtils';
import focusComposerWithDelay from '@libs/focusComposerWithDelay';
+import focusEditAfterCancelDelete from '@libs/focusEditAfterCancelDelete';
import onyxSubscribe from '@libs/onyxSubscribe';
import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
@@ -317,14 +318,7 @@ function ReportActionItemMessageEdit(
// When user tries to save the empty message, it will delete it. Prompt the user to confirm deleting.
if (!trimmedNewDraft) {
textInputRef.current?.blur();
- ReportActionContextMenu.showDeleteModal(
- reportID,
- action,
- true,
- deleteDraft,
- // eslint-disable-next-line @typescript-eslint/no-misused-promises
- () => InteractionManager.runAfterInteractions(() => textInputRef.current?.focus()),
- );
+ ReportActionContextMenu.showDeleteModal(reportID, action, true, deleteDraft, () => focusEditAfterCancelDelete(textInputRef.current));
return;
}
Report.editReportComment(reportID, action, trimmedNewDraft);
diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js
index 2f9e3222206b..1da806b9c269 100644
--- a/src/pages/home/report/ReportActionsList.js
+++ b/src/pages/home/report/ReportActionsList.js
@@ -1,7 +1,7 @@
import {useRoute} from '@react-navigation/native';
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
-import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
+import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {DeviceEventEmitter, InteractionManager} from 'react-native';
import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
import _ from 'underscore';
@@ -515,4 +515,4 @@ ReportActionsList.propTypes = propTypes;
ReportActionsList.defaultProps = defaultProps;
ReportActionsList.displayName = 'ReportActionsList';
-export default compose(withWindowDimensions, withCurrentUserPersonalDetails)(ReportActionsList);
+export default compose(withWindowDimensions, withCurrentUserPersonalDetails)(memo(ReportActionsList));
diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js
index 62ff9426b4ae..064187855b57 100755
--- a/src/pages/home/report/ReportActionsView.js
+++ b/src/pages/home/report/ReportActionsView.js
@@ -1,7 +1,7 @@
import {useIsFocused} from '@react-navigation/native';
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
-import React, {useContext, useEffect, useMemo, useRef} from 'react';
+import React, {useCallback, useContext, useEffect, useMemo, useRef} from 'react';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
import networkPropTypes from '@components/networkPropTypes';
@@ -173,25 +173,25 @@ function ReportActionsView(props) {
}
}, [props.report.pendingFields, didSubscribeToReportTypingEvents, reportID]);
+ const oldestReportAction = useMemo(() => _.last(props.reportActions), [props.reportActions]);
+
/**
* Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently
* displaying.
*/
- const loadOlderChats = () => {
+ const loadOlderChats = useCallback(() => {
// Only fetch more if we are neither already fetching (so that we don't initiate duplicate requests) nor offline.
if (props.network.isOffline || props.isLoadingOlderReportActions) {
return;
}
- const oldestReportAction = _.last(props.reportActions);
-
// Don't load more chats if we're already at the beginning of the chat history
if (!oldestReportAction || oldestReportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) {
return;
}
// Retrieve the next REPORT.ACTIONS.LIMIT sized page of comments
Report.getOlderActions(reportID, oldestReportAction.reportActionID);
- };
+ }, [props.isLoadingOlderReportActions, props.network.isOffline, oldestReportAction, reportID]);
/**
* Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently
@@ -228,7 +228,7 @@ function ReportActionsView(props) {
/**
* Runs when the FlatList finishes laying out
*/
- const recordTimeToMeasureItemLayout = () => {
+ const recordTimeToMeasureItemLayout = useCallback(() => {
if (didLayout.current) {
return;
}
@@ -243,7 +243,7 @@ function ReportActionsView(props) {
} else {
Performance.markEnd(CONST.TIMING.SWITCH_REPORT);
}
- };
+ }, [hasCachedActions]);
// Comments have not loaded at all yet do nothing
if (!_.size(props.reportActions)) {
diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js
index 2fe44639e184..e78e656e0b7e 100644
--- a/src/pages/home/sidebar/SidebarLinks.js
+++ b/src/pages/home/sidebar/SidebarLinks.js
@@ -1,7 +1,7 @@
/* eslint-disable rulesdir/onyx-props-must-have-default */
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
-import React, {useCallback, useEffect, useRef} from 'react';
+import React, {useCallback, useEffect, useMemo, useRef} from 'react';
import {InteractionManager, StyleSheet, View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
@@ -131,6 +131,9 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority
const viewMode = priorityMode === CONST.PRIORITY_MODE.GSD ? CONST.OPTION_MODE.COMPACT : CONST.OPTION_MODE.DEFAULT;
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ const contentContainerStyles = useMemo(() => StyleSheet.flatten([styles.sidebarListContainer, {paddingBottom: StyleUtils.getSafeAreaMargins(insets).marginBottom}]), [insets]);
+
return (
{
+ const options = useMemo(() => {
const {recentReports, personalDetails, userToInvite, currentUserOption} = OptionsListUtils.getFilteredOptions(
- props.reports,
+ reports,
allPersonalDetails,
- props.betas,
- searchValue.trim(),
+ betas,
+ debouncedSearchValue.trim(),
[],
CONST.EXPENSIFY_EMAILS,
false,
@@ -100,50 +76,56 @@ function TaskAssigneeSelectorModal(props) {
true,
);
- setHeaderMessage(OptionsListUtils.getHeaderMessage(recentReports?.length + personalDetails?.length !== 0 || currentUserOption, Boolean(userToInvite), searchValue));
+ const headerMessage = OptionsListUtils.getHeaderMessage(recentReports?.length + personalDetails?.length !== 0 || currentUserOption, Boolean(userToInvite), debouncedSearchValue);
- setFilteredUserToInvite(userToInvite);
- setFilteredRecentReports(recentReports);
- setFilteredPersonalDetails(personalDetails);
- setFilteredCurrentUserOption(currentUserOption);
if (isLoading) {
setIsLoading(false);
}
- }, [props, searchValue, allPersonalDetails, isLoading]);
- useEffect(() => {
- const debouncedSearch = _.debounce(updateOptions, 200);
- debouncedSearch();
- return () => {
- debouncedSearch.cancel();
+ return {
+ userToInvite,
+ recentReports,
+ personalDetails,
+ currentUserOption,
+ headerMessage,
};
- }, [updateOptions]);
+ }, [debouncedSearchValue, allPersonalDetails, isLoading, betas, reports]);
+
+ return {...options, isLoading, searchValue, debouncedSearchValue, setSearchValue};
+}
+
+function TaskAssigneeSelectorModal({reports, task, rootParentReportPolicy}) {
+ const styles = useThemeStyles();
+ const route = useRoute();
+ const {translate} = useLocalize();
+ const session = useSession();
+ const currentUserPersonalDetails = useCurrentUserPersonalDetails();
+ const {userToInvite, recentReports, personalDetails, currentUserOption, isLoading, searchValue, setSearchValue, headerMessage} = useOptions({reports, task});
const onChangeText = (newSearchTerm = '') => {
setSearchValue(newSearchTerm);
};
const report = useMemo(() => {
- if (!props.route.params || !props.route.params.reportID) {
+ if (!route.params || !route.params.reportID) {
return null;
}
- return props.reports[`${ONYXKEYS.COLLECTION.REPORT}${props.route.params.reportID}`];
- }, [props.reports, props.route.params]);
-
- if (report && !ReportUtils.isTaskReport(report)) {
- Navigation.isNavigationReady().then(() => {
- Navigation.dismissModal(report.reportID);
- });
- }
+ if (report && !ReportUtils.isTaskReport(report)) {
+ Navigation.isNavigationReady().then(() => {
+ Navigation.dismissModal(report.reportID);
+ });
+ }
+ return reports[`${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID}`];
+ }, [reports, route]);
const sections = useMemo(() => {
const sectionsList = [];
let indexOffset = 0;
- if (filteredCurrentUserOption) {
+ if (currentUserOption) {
sectionsList.push({
- title: props.translate('newTaskPage.assignMe'),
- data: [filteredCurrentUserOption],
+ title: translate('newTaskPage.assignMe'),
+ data: [currentUserOption],
shouldShow: true,
indexOffset,
});
@@ -151,31 +133,31 @@ function TaskAssigneeSelectorModal(props) {
}
sectionsList.push({
- title: props.translate('common.recents'),
- data: filteredRecentReports,
- shouldShow: filteredRecentReports?.length > 0,
+ title: translate('common.recents'),
+ data: recentReports,
+ shouldShow: recentReports?.length > 0,
indexOffset,
});
- indexOffset += filteredRecentReports?.length;
+ indexOffset += recentReports?.length;
sectionsList.push({
- title: props.translate('common.contacts'),
- data: filteredPersonalDetails,
- shouldShow: filteredPersonalDetails?.length > 0,
+ title: translate('common.contacts'),
+ data: personalDetails,
+ shouldShow: personalDetails?.length > 0,
indexOffset,
});
- indexOffset += filteredPersonalDetails?.length;
+ indexOffset += personalDetails?.length;
- if (filteredUserToInvite) {
+ if (userToInvite) {
sectionsList.push({
- data: [filteredUserToInvite],
+ data: [userToInvite],
shouldShow: true,
indexOffset,
});
}
return sectionsList;
- }, [filteredCurrentUserOption, filteredPersonalDetails, filteredRecentReports, filteredUserToInvite, props]);
+ }, [currentUserOption, personalDetails, recentReports, userToInvite, translate]);
const selectReport = useCallback(
(option) => {
@@ -189,20 +171,22 @@ function TaskAssigneeSelectorModal(props) {
const assigneeChatReport = Task.setAssigneeValue(option.login, option.accountID, report.reportID, OptionsListUtils.isCurrentUser(option));
// Pass through the selected assignee
- Task.editTaskAssignee(report, props.session.accountID, option.login, option.accountID, assigneeChatReport);
+ Task.editTaskAssignee(report, session.accountID, option.login, option.accountID, assigneeChatReport);
}
- Navigation.dismissModalWithReport(report);
+ Navigation.dismissModal(report.reportID);
// If there's no report, we're creating a new task
} else if (option.accountID) {
- Task.setAssigneeValue(option.login, option.accountID, props.task.shareDestination, OptionsListUtils.isCurrentUser(option));
+ Task.setAssigneeValue(option.login, option.accountID, task.shareDestination, OptionsListUtils.isCurrentUser(option));
Navigation.goBack(ROUTES.NEW_TASK);
}
},
- [props.session.accountID, props.task.shareDestination, report],
+ [session.accountID, task.shareDestination, report],
);
+ const handleBackButtonPress = useCallback(() => (lodashGet(route.params, 'reportID') ? Navigation.dismissModal() : Navigation.goBack(ROUTES.NEW_TASK)), [route.params]);
+
const isOpen = ReportUtils.isOpenTaskReport(report);
- const canModifyTask = Task.canModifyTask(report, props.currentUserPersonalDetails.accountID);
+ const canModifyTask = Task.canModifyTask(report, currentUserPersonalDetails.accountID, lodashGet(rootParentReportPolicy, 'role', ''));
const isTaskNonEditable = ReportUtils.isTaskReport(report) && (!canModifyTask || !isOpen);
return (
@@ -213,21 +197,19 @@ function TaskAssigneeSelectorModal(props) {
{({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => (
(lodashGet(props.route.params, 'reportID') ? Navigation.dismissModal() : Navigation.goBack(ROUTES.NEW_TASK))}
+ title={translate('task.assignee')}
+ onBackButtonPress={handleBackButtonPress}
/>
-
@@ -241,20 +223,22 @@ TaskAssigneeSelectorModal.propTypes = propTypes;
TaskAssigneeSelectorModal.defaultProps = defaultProps;
export default compose(
- withLocalize,
- withCurrentUserPersonalDetails,
withOnyx({
reports: {
key: ONYXKEYS.COLLECTION.REPORT,
},
- betas: {
- key: ONYXKEYS.BETAS,
- },
task: {
key: ONYXKEYS.TASK,
},
- session: {
- key: ONYXKEYS.SESSION,
+ }),
+ withOnyx({
+ rootParentReportPolicy: {
+ key: ({reports, route}) => {
+ const report = reports[`${ONYXKEYS.COLLECTION.REPORT}${route.params?.reportID || '0'}`];
+ const rootParentReport = ReportUtils.getRootParentReport(report);
+ return `${ONYXKEYS.COLLECTION.POLICY}${rootParentReport ? rootParentReport.policyID : '0'}`;
+ },
+ selector: (policy) => lodashPick(policy, ['role']),
},
}),
)(TaskAssigneeSelectorModal);
diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx
index 86b3d2aae51f..461bfd7eb5b3 100755
--- a/src/pages/workspace/WorkspacesListPage.tsx
+++ b/src/pages/workspace/WorkspacesListPage.tsx
@@ -169,21 +169,21 @@ function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, r
}
return (
-
-
- {({hovered}) => (
+ {({hovered}) => (
+
- )}
-
-
+
+ )}
+
);
},
[isSmallScreenWidth, styles.mb3, styles.mh5, styles.ph5, styles.hoveredComponentBG, translate],
diff --git a/src/pages/workspace/WorkspacesListRow.tsx b/src/pages/workspace/WorkspacesListRow.tsx
index 2f74d265c004..f16dfd9f0aa1 100644
--- a/src/pages/workspace/WorkspacesListRow.tsx
+++ b/src/pages/workspace/WorkspacesListRow.tsx
@@ -44,11 +44,17 @@ type WorkspacesListRowProps = WithCurrentUserPersonalDetailsProps & {
* component will return null to prevent layout from jumping on initial render and when parent width changes. */
layoutWidth?: ValueOf;
- /** Additional styles applied to the row */
+ /** Custom styles applied to the row */
rowStyles?: StyleProp;
+ /** Additional styles from OfflineWithFeedback applied to the row */
+ style?: StyleProp;
+
/** The type of brick road indicator to show. */
brickRoadIndicator?: ValueOf;
+
+ /** Determines if three dots menu should be shown or not */
+ shouldDisableThreeDotsMenu?: boolean;
};
type BrickRoadIndicatorIconProps = {
@@ -89,7 +95,9 @@ function WorkspacesListRow({
currentUserPersonalDetails,
layoutWidth = CONST.LAYOUT_WIDTH.NONE,
rowStyles,
+ style,
brickRoadIndicator,
+ shouldDisableThreeDotsMenu,
}: WorkspacesListRowProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
@@ -120,8 +128,10 @@ function WorkspacesListRow({
const isWide = layoutWidth === CONST.LAYOUT_WIDTH.WIDE;
const isNarrow = layoutWidth === CONST.LAYOUT_WIDTH.NARROW;
+ const isDeleted = style && Array.isArray(style) ? style.includes(styles.offlineFeedback.deleted) : false;
+
return (
-
+
{title}
@@ -143,6 +153,7 @@ function WorkspacesListRow({
>
)}
@@ -158,13 +169,13 @@ function WorkspacesListRow({
{PersonalDetailsUtils.getDisplayNameOrDefault(ownerDetails)}
{ownerDetails.login}
@@ -182,13 +193,13 @@ function WorkspacesListRow({
{userFriendlyWorkspaceType}
{translate('workspace.common.plan')}
@@ -215,6 +226,7 @@ function WorkspacesListRow({
anchorAlignment={{horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP}}
iconStyles={[styles.mr2]}
shouldOverlay
+ disabled={shouldDisableThreeDotsMenu}
/>
>
diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts
index afdc236ca043..e63e87adfa2a 100644
--- a/src/types/onyx/Policy.ts
+++ b/src/types/onyx/Policy.ts
@@ -162,9 +162,6 @@ type Policy = {
/** When tax tracking is enabled */
isTaxTrackingEnabled?: boolean;
- /** The email of the reimburser set when reimbursement is direct */
- reimburserEmail?: string;
-
/** ReportID of the admins room for this workspace */
chatReportIDAdmins?: number;