diff --git a/src/CONST.ts b/src/CONST.ts
index cec7cbc0b8a5..0c98645511d4 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -512,6 +512,7 @@ const CONST = {
CLOSED: 'CLOSED',
CREATED: 'CREATED',
IOU: 'IOU',
+ MARKEDREIMBURSED: 'MARKEDREIMBURSED',
MODIFIEDEXPENSE: 'MODIFIEDEXPENSE',
MOVED: 'MOVED',
REIMBURSEMENTQUEUED: 'REIMBURSEMENTQUEUED',
diff --git a/src/NAVIGATORS.ts b/src/NAVIGATORS.ts
index a3a041e65684..c68a950d3501 100644
--- a/src/NAVIGATORS.ts
+++ b/src/NAVIGATORS.ts
@@ -4,6 +4,7 @@
* */
export default {
CENTRAL_PANE_NAVIGATOR: 'CentralPaneNavigator',
+ LEFT_MODAL_NAVIGATOR: 'LeftModalNavigator',
RIGHT_MODAL_NAVIGATOR: 'RightModalNavigator',
FULL_SCREEN_NAVIGATOR: 'FullScreenNavigator',
} as const;
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index d86b7f893901..26a23e7efadc 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -81,10 +81,12 @@ const SCREENS = {
SAVE_THE_WORLD: {
ROOT: 'SaveTheWorld_Root',
},
+ LEFT_MODAL: {
+ SEARCH: 'Search',
+ },
RIGHT_MODAL: {
SETTINGS: 'Settings',
NEW_CHAT: 'NewChat',
- SEARCH: 'Search',
DETAILS: 'Details',
PROFILE: 'Profile',
REPORT_DETAILS: 'Report_Details',
diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js
index 51912c04eb31..d24d1e18907f 100755
--- a/src/components/AttachmentModal.js
+++ b/src/components/AttachmentModal.js
@@ -366,7 +366,7 @@ function AttachmentModal(props) {
setIsAuthTokenRequired(props.isAuthTokenRequired);
}, [props.isAuthTokenRequired]);
- const sourceForAttachmentView = props.source || source;
+ const sourceForAttachmentView = source || props.source;
const threeDotsMenuItems = useMemo(() => {
if (!props.isReceiptAttachment || !props.parentReport || !props.parentReportActions) {
diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx
index cdafd0b0b93b..c6f6cd619c09 100644
--- a/src/components/Button/index.tsx
+++ b/src/components/Button/index.tsx
@@ -221,7 +221,7 @@ function Button(
@@ -232,7 +232,7 @@ function Button(
diff --git a/src/components/CurrencySymbolButton.js b/src/components/CurrencySymbolButton.tsx
similarity index 83%
rename from src/components/CurrencySymbolButton.js
rename to src/components/CurrencySymbolButton.tsx
index d03834fc1fd6..18955bb0b391 100644
--- a/src/components/CurrencySymbolButton.js
+++ b/src/components/CurrencySymbolButton.tsx
@@ -1,4 +1,3 @@
-import PropTypes from 'prop-types';
import React from 'react';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -7,15 +6,15 @@ import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback';
import Text from './Text';
import Tooltip from './Tooltip';
-const propTypes = {
+type CurrencySymbolButtonProps = {
/** Currency symbol of selected currency */
- currencySymbol: PropTypes.string.isRequired,
+ currencySymbol: string;
/** Function to call when currency button is pressed */
- onCurrencyButtonPress: PropTypes.func.isRequired,
+ onCurrencyButtonPress: () => void;
};
-function CurrencySymbolButton({onCurrencyButtonPress, currencySymbol}) {
+function CurrencySymbolButton({onCurrencyButtonPress, currencySymbol}: CurrencySymbolButtonProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
return (
@@ -31,7 +30,6 @@ function CurrencySymbolButton({onCurrencyButtonPress, currencySymbol}) {
);
}
-CurrencySymbolButton.propTypes = propTypes;
CurrencySymbolButton.displayName = 'CurrencySymbolButton';
export default CurrencySymbolButton;
diff --git a/src/components/DotIndicatorMessage.tsx b/src/components/DotIndicatorMessage.tsx
index 65afe8c7e4eb..2787804c5f76 100644
--- a/src/components/DotIndicatorMessage.tsx
+++ b/src/components/DotIndicatorMessage.tsx
@@ -84,9 +84,9 @@ function DotIndicatorMessage({messages = {}, style, type, textStyles}: DotIndica
key={i}
style={styles.offlineFeedback.text}
>
- {Localize.translateLocal('iou.error.receiptFailureMessage')}
- {Localize.translateLocal('iou.error.saveFileMessage')}
- {Localize.translateLocal('iou.error.loseFileMessage')}
+ {Localize.translateLocal('iou.error.receiptFailureMessage')}
+ {Localize.translateLocal('iou.error.saveFileMessage')}
+ {Localize.translateLocal('iou.error.loseFileMessage')}
) : (
diff --git a/src/components/LocationErrorMessage/BaseLocationErrorMessage.js b/src/components/LocationErrorMessage/BaseLocationErrorMessage.tsx
similarity index 81%
rename from src/components/LocationErrorMessage/BaseLocationErrorMessage.js
rename to src/components/LocationErrorMessage/BaseLocationErrorMessage.tsx
index d90783d94ad5..ceeb33d5e6bc 100644
--- a/src/components/LocationErrorMessage/BaseLocationErrorMessage.js
+++ b/src/components/LocationErrorMessage/BaseLocationErrorMessage.tsx
@@ -1,4 +1,3 @@
-import PropTypes from 'prop-types';
import React from 'react';
import {View} from 'react-native';
import Icon from '@components/Icon';
@@ -7,29 +6,25 @@ import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeed
import Text from '@components/Text';
import TextLink from '@components/TextLink';
import Tooltip from '@components/Tooltip';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
+import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import colors from '@styles/theme/colors';
import CONST from '@src/CONST';
-import * as locationErrorMessagePropTypes from './locationErrorMessagePropTypes';
+import LocationErrorMessageProps from './types';
-const propTypes = {
+type BaseLocationErrorMessageProps = LocationErrorMessageProps & {
/** A callback that runs when 'allow location permission' link is pressed */
- onAllowLocationLinkPress: PropTypes.func.isRequired,
-
- // eslint-disable-next-line react/forbid-foreign-prop-types
- ...locationErrorMessagePropTypes.propTypes,
-
- /* Onyx Props */
- ...withLocalizePropTypes,
+ onAllowLocationLinkPress: () => void;
};
-function BaseLocationErrorMessage({onClose, onAllowLocationLinkPress, locationErrorCode, translate}) {
+function BaseLocationErrorMessage({onClose, onAllowLocationLinkPress, locationErrorCode}: BaseLocationErrorMessageProps) {
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
+ const {translate} = useLocalize();
+
if (!locationErrorCode) {
return null;
}
@@ -81,6 +76,5 @@ function BaseLocationErrorMessage({onClose, onAllowLocationLinkPress, locationEr
}
BaseLocationErrorMessage.displayName = 'BaseLocationErrorMessage';
-BaseLocationErrorMessage.propTypes = propTypes;
-BaseLocationErrorMessage.defaultProps = locationErrorMessagePropTypes.defaultProps;
-export default withLocalize(BaseLocationErrorMessage);
+
+export default BaseLocationErrorMessage;
diff --git a/src/components/LocationErrorMessage/index.native.js b/src/components/LocationErrorMessage/index.native.tsx
similarity index 67%
rename from src/components/LocationErrorMessage/index.native.js
rename to src/components/LocationErrorMessage/index.native.tsx
index 467018538b6e..3f3813084a47 100644
--- a/src/components/LocationErrorMessage/index.native.js
+++ b/src/components/LocationErrorMessage/index.native.tsx
@@ -1,14 +1,14 @@
import React from 'react';
import {Linking} from 'react-native';
import BaseLocationErrorMessage from './BaseLocationErrorMessage';
-import * as locationErrorMessagePropTypes from './locationErrorMessagePropTypes';
+import LocationErrorMessageProps from './types';
/** Opens app level settings from the native system settings */
const openAppSettings = () => {
Linking.openSettings();
};
-function LocationErrorMessage(props) {
+function LocationErrorMessage(props: LocationErrorMessageProps) {
return (
{
Linking.openURL(CONST.NEWHELP_URL);
};
-function LocationErrorMessage(props) {
+function LocationErrorMessage(props: LocationErrorMessageProps) {
return (
void;
/**
* The location error code from onyx
@@ -11,11 +9,7 @@ const propTypes = {
* - code 2 = location is unavailable or there is some connection issue
* - code 3 = location fetch timeout
*/
- locationErrorCode: PropTypes.oneOf([-1, 1, 2, 3]),
-};
-
-const defaultProps = {
- locationErrorCode: null,
+ locationErrorCode?: -1 | 1 | 2 | 3;
};
-export {propTypes, defaultProps};
+export default LocationErrorMessageProps;
diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx
index db150d55f0d2..9faabc403c75 100644
--- a/src/components/MenuItem.tsx
+++ b/src/components/MenuItem.tsx
@@ -49,14 +49,14 @@ type UnresponsiveProps = {
type IconProps = {
/** Flag to choose between avatar image or an icon */
- iconType: typeof CONST.ICON_TYPE_ICON;
+ iconType?: typeof CONST.ICON_TYPE_ICON;
/** Icon to display on the left side of component */
icon: IconAsset;
};
type AvatarProps = {
- iconType: typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_WORKSPACE;
+ iconType?: typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_WORKSPACE;
icon: AvatarSource;
};
@@ -85,7 +85,7 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) &
titleStyle?: ViewStyle;
/** Any adjustments to style when menu item is hovered or pressed */
- hoverAndPressStyle: StyleProp>;
+ hoverAndPressStyle?: StyleProp>;
/** Additional styles to style the description text below the title */
descriptionTextStyle?: StyleProp;
@@ -175,7 +175,7 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) &
isSelected?: boolean;
/** Prop to identify if we should load avatars vertically instead of diagonally */
- shouldStackHorizontally: boolean;
+ shouldStackHorizontally?: boolean;
/** Prop to represent the size of the avatar images to be shown */
avatarSize?: (typeof CONST.AVATAR_SIZE)[keyof typeof CONST.AVATAR_SIZE];
@@ -220,10 +220,10 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) &
furtherDetails?: string;
/** The function that should be called when this component is LongPressed or right-clicked. */
- onSecondaryInteraction: () => void;
+ onSecondaryInteraction?: () => void;
/** Array of objects that map display names to their corresponding tooltip */
- titleWithTooltips: DisplayNameWithTooltip[];
+ titleWithTooltips?: DisplayNameWithTooltip[];
/** Icon should be displayed in its own color */
displayInDefaultIconColor?: boolean;
diff --git a/src/components/Popover/popoverPropTypes.js b/src/components/Popover/popoverPropTypes.js
index c13fd8fa0b85..c758c4e6d311 100644
--- a/src/components/Popover/popoverPropTypes.js
+++ b/src/components/Popover/popoverPropTypes.js
@@ -26,6 +26,9 @@ const propTypes = {
/** The ref of the popover */
withoutOverlayRef: refPropTypes,
+
+ /** Whether we want to show the popover on the right side of the screen */
+ fromSidebarMediumScreen: PropTypes.bool,
};
const defaultProps = {
diff --git a/src/components/Popover/types.ts b/src/components/Popover/types.ts
index 7f7e2829770c..7890ce5555f0 100644
--- a/src/components/Popover/types.ts
+++ b/src/components/Popover/types.ts
@@ -1,7 +1,15 @@
+import type {ValueOf} from 'type-fest';
import BaseModalProps, {PopoverAnchorPosition} from '@components/Modal/types';
import {WindowDimensionsProps} from '@components/withWindowDimensions/types';
+import CONST from '@src/CONST';
-type AnchorAlignment = {horizontal: string; vertical: string};
+type AnchorAlignment = {
+ /** The horizontal anchor alignment of the popover */
+ horizontal: ValueOf;
+
+ /** The vertical anchor alignment of the popover */
+ vertical: ValueOf;
+};
type PopoverDimensions = {
width: number;
@@ -39,4 +47,4 @@ type PopoverProps = BaseModalProps & {
type PopoverWithWindowDimensionsProps = PopoverProps & WindowDimensionsProps;
-export type {PopoverProps, PopoverWithWindowDimensionsProps};
+export type {PopoverProps, PopoverWithWindowDimensionsProps, AnchorAlignment};
diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx
new file mode 100644
index 000000000000..2c85b80534ca
--- /dev/null
+++ b/src/components/PopoverMenu.tsx
@@ -0,0 +1,176 @@
+import type {ImageContentFit} from 'expo-image';
+import React, {RefObject, useRef} from 'react';
+import {View} from 'react-native';
+import type {ModalProps} from 'react-native-modal';
+import type {SvgProps} from 'react-native-svg';
+import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager';
+import useKeyboardShortcut from '@hooks/useKeyboardShortcut';
+import useThemeStyles from '@hooks/useThemeStyles';
+import useWindowDimensions from '@hooks/useWindowDimensions';
+import CONST from '@src/CONST';
+import type {AnchorPosition} from '@src/styles';
+import MenuItem from './MenuItem';
+import type {AnchorAlignment} from './Popover/types';
+import PopoverWithMeasuredContent from './PopoverWithMeasuredContent';
+import Text from './Text';
+
+type PopoverMenuItem = {
+ /** An icon element displayed on the left side */
+ icon: React.FC;
+
+ /** Text label */
+ text: string;
+
+ /** A callback triggered when this item is selected */
+ onSelected: () => void;
+
+ /** A description text to show under the title */
+ description?: string;
+
+ /** The fill color to pass into the icon. */
+ iconFill?: string;
+
+ /** Icon Width */
+ iconWidth?: number;
+
+ /** Icon Height */
+ iconHeight?: number;
+
+ /** Icon should be displayed in its own color */
+ displayInDefaultIconColor?: boolean;
+
+ /** Determines how the icon should be resized to fit its container */
+ contentFit?: ImageContentFit;
+};
+
+type PopoverModalProps = Pick;
+
+type PopoverMenuProps = PopoverModalProps & {
+ /** Callback method fired when the user requests to close the modal */
+ onClose: () => void;
+
+ /** State that determines whether to display the modal or not */
+ isVisible: boolean;
+
+ /** Callback to fire when a CreateMenu item is selected */
+ onItemSelected: (selectedItem: PopoverMenuItem, index: number) => void;
+
+ /** Menu items to be rendered on the list */
+ menuItems: PopoverMenuItem[];
+
+ /** Optional non-interactive text to display as a header for any create menu */
+ headerText?: string;
+
+ /** Whether disable the animations */
+ disableAnimation?: boolean;
+
+ /** The horizontal and vertical anchors points for the popover */
+ anchorPosition: AnchorPosition;
+
+ /** Ref of the anchor */
+ anchorRef: RefObject;
+
+ /** Where the popover should be positioned relative to the anchor points. */
+ anchorAlignment?: AnchorAlignment;
+
+ /** Whether we don't want to show overlay */
+ withoutOverlay?: boolean;
+
+ /** Should we announce the Modal visibility changes? */
+ shouldSetModalVisibility?: boolean;
+
+ /** Whether we want to show the popover on the right side of the screen */
+ fromSidebarMediumScreen?: boolean;
+};
+
+function PopoverMenu({
+ menuItems,
+ onItemSelected,
+ isVisible,
+ anchorPosition,
+ anchorRef,
+ onClose,
+ headerText,
+ fromSidebarMediumScreen,
+ anchorAlignment = {
+ horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT,
+ vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM,
+ },
+ animationIn = 'fadeIn',
+ animationOut = 'fadeOut',
+ animationInTiming = CONST.ANIMATED_TRANSITION,
+ disableAnimation = true,
+ withoutOverlay = false,
+ shouldSetModalVisibility = true,
+}: PopoverMenuProps) {
+ const styles = useThemeStyles();
+ const {isSmallScreenWidth} = useWindowDimensions();
+ const selectedItemIndex = useRef(null);
+ const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({initialFocusedIndex: -1, maxIndex: menuItems.length - 1, isActive: isVisible});
+
+ const selectItem = (index: number) => {
+ const selectedItem = menuItems[index];
+ onItemSelected(selectedItem, index);
+ selectedItemIndex.current = index;
+ };
+
+ useKeyboardShortcut(
+ CONST.KEYBOARD_SHORTCUTS.ENTER,
+ () => {
+ if (focusedIndex === -1) {
+ return;
+ }
+ selectItem(focusedIndex);
+ setFocusedIndex(-1); // Reset the focusedIndex on selecting any menu
+ },
+ {isActive: isVisible},
+ );
+
+ return (
+ {
+ setFocusedIndex(-1);
+ if (selectedItemIndex.current !== null) {
+ menuItems[selectedItemIndex.current].onSelected();
+ selectedItemIndex.current = null;
+ }
+ }}
+ animationIn={animationIn}
+ animationOut={animationOut}
+ animationInTiming={animationInTiming}
+ disableAnimation={disableAnimation}
+ fromSidebarMediumScreen={fromSidebarMediumScreen}
+ withoutOverlay={withoutOverlay}
+ shouldSetModalVisibility={shouldSetModalVisibility}
+ >
+
+ {!!headerText && {headerText}}
+ {menuItems.map((item, menuIndex) => (
+
+
+ );
+}
+
+PopoverMenu.displayName = 'PopoverMenu';
+
+export default React.memo(PopoverMenu);
diff --git a/src/components/PopoverMenu/index.js b/src/components/PopoverMenu/index.js
deleted file mode 100644
index 597105173b4c..000000000000
--- a/src/components/PopoverMenu/index.js
+++ /dev/null
@@ -1,114 +0,0 @@
-import PropTypes from 'prop-types';
-import React, {useRef} from 'react';
-import {View} from 'react-native';
-import _ from 'underscore';
-import MenuItem from '@components/MenuItem';
-import PopoverWithMeasuredContent from '@components/PopoverWithMeasuredContent';
-import refPropTypes from '@components/refPropTypes';
-import Text from '@components/Text';
-import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions';
-import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager';
-import useKeyboardShortcut from '@hooks/useKeyboardShortcut';
-import useThemeStyles from '@hooks/useThemeStyles';
-import useWindowDimensions from '@hooks/useWindowDimensions';
-import CONST from '@src/CONST';
-import {defaultProps as createMenuDefaultProps, propTypes as createMenuPropTypes} from './popoverMenuPropTypes';
-
-const propTypes = {
- ...createMenuPropTypes,
- ...windowDimensionsPropTypes,
-
- /** Ref of the anchor */
- anchorRef: refPropTypes,
-
- withoutOverlay: PropTypes.bool,
-
- /** Should we announce the Modal visibility changes? */
- shouldSetModalVisibility: PropTypes.bool,
-};
-
-const defaultProps = {
- ...createMenuDefaultProps,
- anchorAlignment: {
- horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT,
- vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM,
- },
- anchorRef: () => {},
- withoutOverlay: false,
- shouldSetModalVisibility: true,
-};
-
-function PopoverMenu(props) {
- const styles = useThemeStyles();
- const {isSmallScreenWidth} = useWindowDimensions();
- const selectedItemIndex = useRef(null);
- const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({initialFocusedIndex: -1, maxIndex: props.menuItems.length - 1, isActive: props.isVisible});
-
- const selectItem = (index) => {
- const selectedItem = props.menuItems[index];
- props.onItemSelected(selectedItem, index);
- selectedItemIndex.current = index;
- };
-
- useKeyboardShortcut(
- CONST.KEYBOARD_SHORTCUTS.ENTER,
- () => {
- if (focusedIndex === -1) {
- return;
- }
- selectItem(focusedIndex);
- setFocusedIndex(-1); // Reset the focusedIndex on selecting any menu
- },
- {isActive: props.isVisible},
- );
-
- return (
- {
- setFocusedIndex(-1);
- if (selectedItemIndex.current !== null) {
- props.menuItems[selectedItemIndex.current].onSelected();
- selectedItemIndex.current = null;
- }
- }}
- animationIn={props.animationIn}
- animationOut={props.animationOut}
- animationInTiming={props.animationInTiming}
- disableAnimation={props.disableAnimation}
- fromSidebarMediumScreen={props.fromSidebarMediumScreen}
- withoutOverlay={props.withoutOverlay}
- shouldSetModalVisibility={props.shouldSetModalVisibility}
- >
-
- {!_.isEmpty(props.headerText) && {props.headerText}}
- {_.map(props.menuItems, (item, menuIndex) => (
-
-
- );
-}
-
-PopoverMenu.propTypes = propTypes;
-PopoverMenu.defaultProps = defaultProps;
-PopoverMenu.displayName = 'PopoverMenu';
-
-export default React.memo(withWindowDimensions(PopoverMenu));
diff --git a/src/components/PopoverMenu/popoverMenuPropTypes.js b/src/components/PopoverMenu/popoverMenuPropTypes.js
deleted file mode 100644
index 53eeb63b05e7..000000000000
--- a/src/components/PopoverMenu/popoverMenuPropTypes.js
+++ /dev/null
@@ -1,71 +0,0 @@
-import PropTypes from 'prop-types';
-import _ from 'underscore';
-import sourcePropTypes from '@components/Image/sourcePropTypes';
-import CONST from '@src/CONST';
-
-const propTypes = {
- /** Callback method fired when the user requests to close the modal */
- onClose: PropTypes.func.isRequired,
-
- /** State that determines whether to display the modal or not */
- isVisible: PropTypes.bool.isRequired,
-
- /** Callback to fire when a CreateMenu item is selected */
- onItemSelected: PropTypes.func.isRequired,
-
- /** Menu items to be rendered on the list */
- menuItems: PropTypes.arrayOf(
- PropTypes.shape({
- /** An icon element displayed on the left side */
- icon: sourcePropTypes,
-
- /** Text label */
- text: PropTypes.string.isRequired,
-
- /** A callback triggered when this item is selected */
- onSelected: PropTypes.func.isRequired,
- }),
- ).isRequired,
-
- /** The anchor position of the CreateMenu popover */
- anchorPosition: PropTypes.shape({
- top: PropTypes.number,
- right: PropTypes.number,
- bottom: PropTypes.number,
- left: PropTypes.number,
- }).isRequired,
-
- /** Where the popover should be positioned relative to the anchor points. */
- anchorAlignment: PropTypes.shape({
- horizontal: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL)),
- vertical: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_VERTICAL)),
- }),
-
- /** The anchor reference of the CreateMenu popover */
- anchorRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]).isRequired,
-
- /** A react-native-animatable animation definition for the modal display animation. */
- animationIn: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
-
- /** A react-native-animatable animation definition for the modal hide animation. */
- animationOut: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
-
- /** A react-native-animatable animation timing for the modal display animation. */
- animationInTiming: PropTypes.number,
-
- /** Optional non-interactive text to display as a header for any create menu */
- headerText: PropTypes.string,
-
- /** Whether disable the animations */
- disableAnimation: PropTypes.bool,
-};
-
-const defaultProps = {
- animationIn: 'fadeIn',
- animationOut: 'fadeOut',
- animationInTiming: CONST.ANIMATED_TRANSITION,
- headerText: undefined,
- disableAnimation: true,
-};
-
-export {propTypes, defaultProps};
diff --git a/src/components/PopoverWithMeasuredContent.tsx b/src/components/PopoverWithMeasuredContent.tsx
index 9d10f7869f8a..206a33181605 100644
--- a/src/components/PopoverWithMeasuredContent.tsx
+++ b/src/components/PopoverWithMeasuredContent.tsx
@@ -8,8 +8,9 @@ import CONST from '@src/CONST';
import type {AnchorPosition} from '@src/styles';
import Popover from './Popover';
import {PopoverProps} from './Popover/types';
+import type {WindowDimensionsProps} from './withWindowDimensions/types';
-type PopoverWithMeasuredContentProps = Omit & {
+type PopoverWithMeasuredContentProps = Omit & {
/** The horizontal and vertical anchors points for the popover */
anchorPosition: AnchorPosition;
};
diff --git a/src/components/ReportActionItem/ReportActionItemImage.js b/src/components/ReportActionItem/ReportActionItemImage.js
index 2c5ef22b1b8e..4336a5eddd8a 100644
--- a/src/components/ReportActionItem/ReportActionItemImage.js
+++ b/src/components/ReportActionItem/ReportActionItemImage.js
@@ -1,3 +1,4 @@
+import Str from 'expensify-common/lib/str';
import PropTypes from 'prop-types';
import React from 'react';
import {View} from 'react-native';
@@ -60,7 +61,7 @@ function ReportActionItemImage({thumbnail, image, enablePreviewModal, transactio
);
- } else if (thumbnail && !isLocalFile) {
+ } else if (thumbnail && !isLocalFile && !Str.isPDF(imageSource)) {
receiptImageComponent = (
- {
- Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(props.iouReportID));
- }}
- onPressIn={() => DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()}
- onPressOut={() => ControlSelection.unblock()}
- onLongPress={(event) => showContextMenuForReport(event, props.contextMenuAnchor, props.chatReportID, props.action, props.checkIfContextMenuActive)}
- style={[styles.flexRow, styles.justifyContentBetween, styles.reportPreviewBox]}
- role="button"
- accessibilityLabel={props.translate('iou.viewDetails')}
- >
-
- {hasReceipts && (
-
- )}
-
-
-
- {getPreviewMessage()}
-
- {!iouSettled && hasErrors && (
-
- )}
-
-
-
- {getDisplayAmount()}
- {ReportUtils.isSettled(props.iouReportID) && (
-
-
-
+
+
+ {
+ Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(props.iouReportID));
+ }}
+ onPressIn={() => DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()}
+ onPressOut={() => ControlSelection.unblock()}
+ onLongPress={(event) => showContextMenuForReport(event, props.contextMenuAnchor, props.chatReportID, props.action, props.checkIfContextMenuActive)}
+ style={[styles.flexRow, styles.justifyContentBetween, styles.reportPreviewBox]}
+ role="button"
+ accessibilityLabel={props.translate('iou.viewDetails')}
+ >
+
+ {hasReceipts && (
+
+ )}
+
+
+
+ {getPreviewMessage()}
+
+ {!iouSettled && hasErrors && (
+
)}
-
- {!isScanning && (numberOfRequests > 1 || hasReceipts) && (
- {previewSubtitle || moneyRequestComment}
+ {getDisplayAmount()}
+ {ReportUtils.isSettled(props.iouReportID) && (
+
+
+
+ )}
- )}
- {shouldShowSettlementButton && (
- IOU.payMoneyRequest(paymentType, props.chatReport, props.iouReport)}
- enablePaymentsRoute={ROUTES.ENABLE_PAYMENTS}
- addBankAccountRoute={bankAccountRoute}
- shouldHidePaymentOptions={!shouldShowPayButton}
- shouldShowApproveButton={shouldShowApproveButton}
- style={[styles.mt3]}
- kycWallAnchorAlignment={{
- horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT,
- vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM,
- }}
- paymentMethodDropdownAnchorAlignment={{
- horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT,
- vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM,
- }}
- />
- )}
- {shouldShowSubmitButton && (
-
-
-
-
+
+
+
);
}
diff --git a/src/hooks/useDragAndDrop.ts b/src/hooks/useDragAndDrop.ts
index 0e82cb22505f..21f48921c187 100644
--- a/src/hooks/useDragAndDrop.ts
+++ b/src/hooks/useDragAndDrop.ts
@@ -1,5 +1,5 @@
import {useIsFocused} from '@react-navigation/native';
-import React, {useCallback, useContext, useEffect, useRef, useState} from 'react';
+import React, {useCallback, useContext, useEffect, useState} from 'react';
import {View} from 'react-native';
import {PopoverContext} from '@components/PopoverProvider';
@@ -30,18 +30,10 @@ export default function useDragAndDrop({dropZone, onDrop = () => {}, shouldAllow
const [isDraggingOver, setIsDraggingOver] = useState(false);
const {close: closePopover} = useContext(PopoverContext);
- // This solution is borrowed from this SO: https://stackoverflow.com/questions/7110353/html5-dragleave-fired-when-hovering-a-child-element
- // This is necessary because dragging over children will cause dragleave to execute on the parent.
- // You can think of this counter as a stack. When a child is hovered over we push an element onto the stack.
- // Then we only process the dragleave event if the count is 0, because it means that the last element (the parent) has been popped off the stack.
- const dragCounter = useRef(0);
-
- // If this component is out of focus or disabled, reset the drag state back to the default
useEffect(() => {
if (isFocused && !isDisabled) {
return;
}
- dragCounter.current = 0;
setIsDraggingOver(false);
}, [isFocused, isDisabled]);
@@ -82,7 +74,6 @@ export default function useDragAndDrop({dropZone, onDrop = () => {}, shouldAllow
handleDragEvent(event);
break;
case DRAG_ENTER_EVENT:
- dragCounter.current++;
handleDragEvent(event);
if (isDraggingOver) {
return;
@@ -90,15 +81,17 @@ export default function useDragAndDrop({dropZone, onDrop = () => {}, shouldAllow
setIsDraggingOver(true);
break;
case DRAG_LEAVE_EVENT:
- dragCounter.current--;
- if (!isDraggingOver || dragCounter.current > 0) {
+ if (!isDraggingOver) {
+ return;
+ }
+ // This is necessary because dragging over children will cause dragleave to execute on the parent.
+ if ((event.currentTarget as HTMLElement | null)?.contains(event.relatedTarget as HTMLElement | null)) {
return;
}
setIsDraggingOver(false);
break;
case DROP_EVENT:
- dragCounter.current = 0;
setIsDraggingOver(false);
onDrop(event);
break;
diff --git a/src/hooks/useHandleExceedMaxCommentLength.ts b/src/hooks/useHandleExceedMaxCommentLength.ts
index 9700999bb004..fea0793c9854 100644
--- a/src/hooks/useHandleExceedMaxCommentLength.ts
+++ b/src/hooks/useHandleExceedMaxCommentLength.ts
@@ -19,7 +19,7 @@ const useHandleExceedMaxCommentLength = () => {
[hasExceededMaxCommentLength],
);
- const validateCommentMaxLength = useMemo(() => _.debounce(handleValueChange, 1500), [handleValueChange]);
+ const validateCommentMaxLength = useMemo(() => _.debounce(handleValueChange, 1500, {leading: true}), [handleValueChange]);
return {hasExceededMaxCommentLength, validateCommentMaxLength};
};
diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
index b6304cb3b1b7..fdef49d71eae 100644
--- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx
+++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
@@ -35,6 +35,7 @@ import createCustomStackNavigator from './createCustomStackNavigator';
import defaultScreenOptions from './defaultScreenOptions';
import getRootNavigatorScreenOptions from './getRootNavigatorScreenOptions';
import CentralPaneNavigator from './Navigators/CentralPaneNavigator';
+import LeftModalNavigator from './Navigators/LeftModalNavigator';
import RightModalNavigator from './Navigators/RightModalNavigator';
type AuthScreensProps = {
@@ -295,6 +296,12 @@ function AuthScreens({lastUpdateIDAppliedToClient, session, lastOpenedPublicRoom
component={RightModalNavigator}
listeners={modalScreenListeners}
/>
+
({
+const ModalNavigatorScreenOptions = (themeStyles: ThemeStyles): StackNavigationOptions => ({
headerShown: false,
animationEnabled: true,
gestureDirection: 'horizontal',
@@ -14,4 +14,4 @@ const RHPScreenOptions = (themeStyles: ThemeStyles): StackNavigationOptions => (
cardStyleInterpolator: CardStyleInterpolators.forHorizontalIOS,
});
-export default RHPScreenOptions;
+export default ModalNavigatorScreenOptions;
diff --git a/src/libs/Navigation/AppNavigator/Navigators/LeftModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/LeftModalNavigator.tsx
new file mode 100644
index 000000000000..b7385c930e2c
--- /dev/null
+++ b/src/libs/Navigation/AppNavigator/Navigators/LeftModalNavigator.tsx
@@ -0,0 +1,45 @@
+import {createStackNavigator, StackScreenProps} from '@react-navigation/stack';
+import React, {useMemo} from 'react';
+import {View} from 'react-native';
+import NoDropZone from '@components/DragAndDrop/NoDropZone';
+import useThemeStyles from '@hooks/useThemeStyles';
+import useWindowDimensions from '@hooks/useWindowDimensions';
+import ModalNavigatorScreenOptions from '@libs/Navigation/AppNavigator/ModalNavigatorScreenOptions';
+import * as ModalStackNavigators from '@libs/Navigation/AppNavigator/ModalStackNavigators';
+import {AuthScreensParamList, LeftModalNavigatorParamList} from '@libs/Navigation/types';
+import NAVIGATORS from '@src/NAVIGATORS';
+import SCREENS from '@src/SCREENS';
+import Overlay from './Overlay';
+
+type LeftModalNavigatorProps = StackScreenProps;
+
+const Stack = createStackNavigator();
+
+function LeftModalNavigator({navigation}: LeftModalNavigatorProps) {
+ const styles = useThemeStyles();
+ const {isSmallScreenWidth} = useWindowDimensions();
+ const screenOptions = useMemo(() => ModalNavigatorScreenOptions(styles), [styles]);
+
+ return (
+
+ {!isSmallScreenWidth && (
+
+ )}
+
+
+
+
+
+
+ );
+}
+
+LeftModalNavigator.displayName = 'LeftModalNavigator';
+
+export default LeftModalNavigator;
diff --git a/src/libs/Navigation/AppNavigator/Navigators/Overlay.tsx b/src/libs/Navigation/AppNavigator/Navigators/Overlay.tsx
index 065de8da578b..a3fe1c657f34 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/Overlay.tsx
+++ b/src/libs/Navigation/AppNavigator/Navigators/Overlay.tsx
@@ -9,15 +9,18 @@ import CONST from '@src/CONST';
type OverlayProps = {
/* Callback to close the modal */
onPress: () => void;
+
+ /* Returns whether a modal is displayed on the left side of the screen. By default, the modal is displayed on the right */
+ isModalOnTheLeft?: boolean;
};
-function Overlay({onPress}: OverlayProps) {
+function Overlay({onPress, isModalOnTheLeft = false}: OverlayProps) {
const styles = useThemeStyles();
const {current} = useCardAnimation();
const {translate} = useLocalize();
return (
-
+
{/* In the latest Electron version buttons can't be both clickable and draggable.
That's why we added this workaround. Because of two Pressable components on the desktop app
diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx
index bd790589c8d1..d7c31bcae7d9 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx
+++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx
@@ -4,8 +4,8 @@ import {View} from 'react-native';
import NoDropZone from '@components/DragAndDrop/NoDropZone';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
+import ModalNavigatorScreenOptions from '@libs/Navigation/AppNavigator/ModalNavigatorScreenOptions';
import * as ModalStackNavigators from '@libs/Navigation/AppNavigator/ModalStackNavigators';
-import RHPScreenOptions from '@libs/Navigation/AppNavigator/RHPScreenOptions';
import type {AuthScreensParamList, RightModalNavigatorParamList} from '@navigation/types';
import NAVIGATORS from '@src/NAVIGATORS';
import SCREENS from '@src/SCREENS';
@@ -18,7 +18,7 @@ const Stack = createStackNavigator();
function RightModalNavigator({navigation}: RightModalNavigatorProps) {
const styles = useThemeStyles();
const {isSmallScreenWidth} = useWindowDimensions();
- const screenOptions = useMemo(() => RHPScreenOptions(styles), [styles]);
+ const screenOptions = useMemo(() => ModalNavigatorScreenOptions(styles), [styles]);
return (
@@ -33,10 +33,6 @@ function RightModalNavigator({navigation}: RightModalNavigatorProps) {
name={SCREENS.RIGHT_MODAL.NEW_CHAT}
component={ModalStackNavigators.NewChatModalStackNavigator}
/>
-
({
rightModalNavigator: {
...commonScreenOptions,
@@ -32,7 +34,23 @@ export default (isSmallScreenWidth: boolean, themeStyles: ThemeStyles): ScreenOp
right: 0,
},
},
+ leftModalNavigator: {
+ ...commonScreenOptions,
+ cardStyleInterpolator: (props) => modalCardStyleInterpolator(isSmallScreenWidth, false, props, SLIDE_LEFT_OUTPUT_RANGE_MULTIPLIER),
+ presentation: 'transparentModal',
+
+ // We want pop in LHP since there are some flows that would work weird otherwise
+ animationTypeForReplace: 'pop',
+ cardStyle: {
+ ...getNavigationModalCardStyle(),
+
+ // This is necessary to cover translated sidebar with overlay.
+ width: isSmallScreenWidth ? '100%' : '200%',
+ // LHP should be displayed in place of the sidebar
+ left: isSmallScreenWidth ? 0 : -variables.sideBarWidth,
+ },
+ },
homeScreen: {
title: CONFIG.SITE_TITLE,
...commonScreenOptions,
diff --git a/src/libs/Navigation/AppNavigator/modalCardStyleInterpolator.ts b/src/libs/Navigation/AppNavigator/modalCardStyleInterpolator.ts
index eff88422cc5c..fd59b02e724d 100644
--- a/src/libs/Navigation/AppNavigator/modalCardStyleInterpolator.ts
+++ b/src/libs/Navigation/AppNavigator/modalCardStyleInterpolator.ts
@@ -3,11 +3,16 @@ import {Animated} from 'react-native';
import getCardStyles from '@styles/utils/cardStyles';
import variables from '@styles/variables';
-export default (isSmallScreenWidth: boolean, isFullScreenModal: boolean, {current: {progress}, inverted, layouts: {screen}}: StackCardInterpolationProps): StackCardInterpolatedStyle => {
+export default (
+ isSmallScreenWidth: boolean,
+ isFullScreenModal: boolean,
+ {current: {progress}, inverted, layouts: {screen}}: StackCardInterpolationProps,
+ outputRangeMultiplier = 1,
+): StackCardInterpolatedStyle => {
const translateX = Animated.multiply(
progress.interpolate({
inputRange: [0, 1],
- outputRange: [isSmallScreenWidth ? screen.width : variables.sideBarWidth, 0],
+ outputRange: [outputRangeMultiplier * (isSmallScreenWidth ? screen.width : variables.sideBarWidth), 0],
extrapolate: 'clamp',
}),
inverted,
diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts
index cb9cc104285f..284e5703635a 100644
--- a/src/libs/Navigation/Navigation.ts
+++ b/src/libs/Navigation/Navigation.ts
@@ -1,18 +1,17 @@
-import {findFocusedRoute, getActionFromState} from '@react-navigation/core';
+import {findFocusedRoute} from '@react-navigation/core';
import {CommonActions, EventArg, getPathFromState, NavigationContainerEventMap, NavigationState, PartialState, StackActions} from '@react-navigation/native';
-import findLastIndex from 'lodash/findLastIndex';
import Log from '@libs/Log';
import CONST from '@src/CONST';
import NAVIGATORS from '@src/NAVIGATORS';
import ROUTES, {Route} from '@src/ROUTES';
-import SCREENS, {PROTECTED_SCREENS} from '@src/SCREENS';
-import getStateFromPath from './getStateFromPath';
+import {PROTECTED_SCREENS} from '@src/SCREENS';
+import originalDismissModal from './dismissModal';
import originalGetTopmostReportActionId from './getTopmostReportActionID';
import originalGetTopmostReportId from './getTopmostReportId';
import linkingConfig from './linkingConfig';
import linkTo from './linkTo';
import navigationRef from './navigationRef';
-import {StackNavigationAction, StateOrRoute} from './types';
+import {StateOrRoute} from './types';
let resolveNavigationIsReadyPromise: () => void;
const navigationIsReadyPromise = new Promise((resolve) => {
@@ -44,6 +43,9 @@ const getTopmostReportId = (state = navigationRef.getState()) => originalGetTopm
// Re-exporting the getTopmostReportActionID here to fill in default value for state. The getTopmostReportActionID isn't defined in this file to avoid cyclic dependencies.
const getTopmostReportActionId = (state = navigationRef.getState()) => originalGetTopmostReportActionId(state);
+// Re-exporting the dismissModal here to fill in default value for navigationRef. The dismissModal isn't defined in this file to avoid cyclic dependencies.
+const dismissModal = (targetReportId = '', ref = navigationRef) => originalDismissModal(targetReportId, ref);
+
/** Method for finding on which index in stack we are. */
function getActiveRouteIndex(stateOrRoute: StateOrRoute, index?: number): number | undefined {
if ('routes' in stateOrRoute && stateOrRoute.routes) {
@@ -56,7 +58,7 @@ function getActiveRouteIndex(stateOrRoute: StateOrRoute, index?: number): number
return getActiveRouteIndex(childActiveRoute, stateOrRoute.state.index ?? 0);
}
- if ('name' in stateOrRoute && stateOrRoute.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR) {
+ if ('name' in stateOrRoute && (stateOrRoute.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR || stateOrRoute.name === NAVIGATORS.LEFT_MODAL_NAVIGATOR)) {
return 0;
}
@@ -166,8 +168,8 @@ function goBack(fallbackRoute: Route, shouldEnforceFallback = false, shouldPopTo
if (isFirstRouteInNavigator) {
const rootState = navigationRef.getRootState();
const lastRoute = rootState.routes.at(-1);
- // If the user comes from a different flow (there is more than one route in RHP) we should go back to the previous flow on UP button press instead of using the fallbackRoute.
- if (lastRoute?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR && (lastRoute.state?.index ?? 0) > 0) {
+ // If the user comes from a different flow (there is more than one route in ModalNavigator) we should go back to the previous flow on UP button press instead of using the fallbackRoute.
+ if ((lastRoute?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR || lastRoute?.name === NAVIGATORS.LEFT_MODAL_NAVIGATOR) && (lastRoute.state?.index ?? 0) > 0) {
navigationRef.current.goBack();
return;
}
@@ -206,45 +208,6 @@ function setParams(params: Record, routeKey: string) {
});
}
-/**
- * Dismisses the last modal stack if there is any
- *
- * @param targetReportID - The reportID to navigate to after dismissing the modal
- */
-function dismissModal(targetReportID?: string) {
- if (!canNavigate('dismissModal')) {
- return;
- }
- const rootState = navigationRef.getRootState();
- const lastRoute = rootState.routes.at(-1);
- switch (lastRoute?.name) {
- case NAVIGATORS.RIGHT_MODAL_NAVIGATOR:
- case SCREENS.NOT_FOUND:
- case SCREENS.REPORT_ATTACHMENTS:
- // if we are not in the target report, we need to navigate to it after dismissing the modal
- if (targetReportID && targetReportID !== getTopmostReportId(rootState)) {
- const state = getStateFromPath(ROUTES.REPORT_WITH_ID.getRoute(targetReportID));
-
- const action: StackNavigationAction = getActionFromState(state, linkingConfig.config);
- if (action) {
- action.type = 'REPLACE';
- navigationRef.current?.dispatch(action);
- }
- // If not-found page is in the route stack, we need to close it
- } else if (targetReportID && rootState.routes.some((route) => route.name === SCREENS.NOT_FOUND)) {
- const lastRouteIndex = rootState.routes.length - 1;
- const centralRouteIndex = findLastIndex(rootState.routes, (route) => route.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR);
- navigationRef.current?.dispatch({...StackActions.pop(lastRouteIndex - centralRouteIndex), target: rootState.key});
- } else {
- navigationRef.current?.dispatch({...StackActions.pop(), target: rootState.key});
- }
- break;
- default: {
- Log.hmmm('[Navigation] dismissModal failed because there is no modal stack to dismiss');
- }
- }
-}
-
/**
* Returns the current active route without the URL params
*/
diff --git a/src/libs/Navigation/dismissModal.ts b/src/libs/Navigation/dismissModal.ts
new file mode 100644
index 000000000000..37b4c6d9b9e6
--- /dev/null
+++ b/src/libs/Navigation/dismissModal.ts
@@ -0,0 +1,56 @@
+import {getActionFromState} from '@react-navigation/core';
+import {NavigationContainerRef, StackActions} from '@react-navigation/native';
+import {findLastIndex} from 'lodash';
+import Log from '@libs/Log';
+import NAVIGATORS from '@src/NAVIGATORS';
+import ROUTES from '@src/ROUTES';
+import SCREENS from '@src/SCREENS';
+import getStateFromPath from './getStateFromPath';
+import getTopmostReportId from './getTopmostReportId';
+import linkingConfig from './linkingConfig';
+import {RootStackParamList, StackNavigationAction} from './types';
+
+// This function is in a separate file than Navigation.js to avoid cyclic dependency.
+
+/**
+ * Dismisses the last modal stack if there is any
+ *
+ * @param targetReportID - The reportID to navigate to after dismissing the modal
+ */
+function dismissModal(targetReportID: string, navigationRef: NavigationContainerRef) {
+ if (!navigationRef.isReady()) {
+ return;
+ }
+
+ const state = navigationRef.getState();
+ const lastRoute = state.routes.at(-1);
+ switch (lastRoute?.name) {
+ case NAVIGATORS.LEFT_MODAL_NAVIGATOR:
+ case NAVIGATORS.RIGHT_MODAL_NAVIGATOR:
+ case SCREENS.NOT_FOUND:
+ case SCREENS.REPORT_ATTACHMENTS:
+ // if we are not in the target report, we need to navigate to it after dismissing the modal
+ if (targetReportID && targetReportID !== getTopmostReportId(state)) {
+ const reportState = getStateFromPath(ROUTES.REPORT_WITH_ID.getRoute(targetReportID));
+
+ const action: StackNavigationAction = getActionFromState(reportState, linkingConfig.config);
+ if (action) {
+ action.type = 'REPLACE';
+ navigationRef.dispatch(action);
+ }
+ // If not-found page is in the route stack, we need to close it
+ } else if (targetReportID && state.routes.some((route) => route.name === SCREENS.NOT_FOUND)) {
+ const lastRouteIndex = state.routes.length - 1;
+ const centralRouteIndex = findLastIndex(state.routes, (route) => route.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR);
+ navigationRef.dispatch({...StackActions.pop(lastRouteIndex - centralRouteIndex), target: state.key});
+ } else {
+ navigationRef.dispatch({...StackActions.pop(), target: state.key});
+ }
+ break;
+ default: {
+ Log.hmmm('[Navigation] dismissModal failed because there is no modal stack to dismiss');
+ }
+ }
+}
+
+export default dismissModal;
diff --git a/src/libs/Navigation/linkTo.ts b/src/libs/Navigation/linkTo.ts
index 9694879f9aae..86558765a6e6 100644
--- a/src/libs/Navigation/linkTo.ts
+++ b/src/libs/Navigation/linkTo.ts
@@ -4,6 +4,7 @@ import {Writable} from 'type-fest';
import CONST from '@src/CONST';
import NAVIGATORS from '@src/NAVIGATORS';
import {Route} from '@src/ROUTES';
+import dismissModal from './dismissModal';
import getStateFromPath from './getStateFromPath';
import getTopmostReportId from './getTopmostReportId';
import linkingConfig from './linkingConfig';
@@ -55,6 +56,10 @@ function getMinimalAction(action: NavigationAction, state: NavigationState): Wri
return currentAction;
}
+function isModalNavigator(targetNavigator?: string) {
+ return targetNavigator === NAVIGATORS.LEFT_MODAL_NAVIGATOR || targetNavigator === NAVIGATORS.RIGHT_MODAL_NAVIGATOR;
+}
+
export default function linkTo(navigation: NavigationContainerRef | null, path: Route, type?: string, isActiveRoute?: boolean) {
if (!navigation) {
throw new Error("Couldn't find a navigation object. Is your component inside a screen in a navigator?");
@@ -75,6 +80,9 @@ export default function linkTo(navigation: NavigationContainerRef = {
},
},
[SCREENS.NOT_FOUND]: '*',
-
+ [NAVIGATORS.LEFT_MODAL_NAVIGATOR]: {
+ screens: {
+ [SCREENS.LEFT_MODAL.SEARCH]: {
+ screens: {
+ [SCREENS.SEARCH_ROOT]: ROUTES.SEARCH,
+ },
+ },
+ },
+ },
[NAVIGATORS.RIGHT_MODAL_NAVIGATOR]: {
screens: {
[SCREENS.RIGHT_MODAL.SETTINGS]: {
@@ -338,11 +346,6 @@ const linkingConfig: LinkingOptions = {
[SCREENS.I_AM_A_TEACHER]: ROUTES.I_AM_A_TEACHER,
},
},
- [SCREENS.RIGHT_MODAL.SEARCH]: {
- screens: {
- [SCREENS.SEARCH_ROOT]: ROUTES.SEARCH,
- },
- },
[SCREENS.RIGHT_MODAL.DETAILS]: {
screens: {
[SCREENS.DETAILS_ROOT]: ROUTES.DETAILS.route,
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index cb28b2314b60..4823b44b89cb 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -346,10 +346,13 @@ type PrivateNotesNavigatorParamList = {
};
};
+type LeftModalNavigatorParamList = {
+ [SCREENS.LEFT_MODAL.SEARCH]: NavigatorScreenParams;
+};
+
type RightModalNavigatorParamList = {
[SCREENS.RIGHT_MODAL.SETTINGS]: NavigatorScreenParams;
[SCREENS.RIGHT_MODAL.NEW_CHAT]: NavigatorScreenParams;
- [SCREENS.RIGHT_MODAL.SEARCH]: NavigatorScreenParams;
[SCREENS.RIGHT_MODAL.DETAILS]: NavigatorScreenParams;
[SCREENS.RIGHT_MODAL.PROFILE]: NavigatorScreenParams;
[SCREENS.RIGHT_MODAL.REPORT_DETAILS]: NavigatorScreenParams;
@@ -413,6 +416,7 @@ type AuthScreensParamList = {
source: string;
};
[SCREENS.NOT_FOUND]: undefined;
+ [NAVIGATORS.LEFT_MODAL_NAVIGATOR]: NavigatorScreenParams;
[NAVIGATORS.RIGHT_MODAL_NAVIGATOR]: NavigatorScreenParams;
[SCREENS.DESKTOP_SIGN_IN_REDIRECT]: undefined;
};
@@ -428,6 +432,7 @@ export type {
NavigationStateRoute,
NavigationRoot,
AuthScreensParamList,
+ LeftModalNavigatorParamList,
RightModalNavigatorParamList,
PublicScreensParamList,
MoneyRequestNavigatorParamList,
diff --git a/src/libs/ReceiptUtils.ts b/src/libs/ReceiptUtils.ts
index cb987724ee19..1151b559746d 100644
--- a/src/libs/ReceiptUtils.ts
+++ b/src/libs/ReceiptUtils.ts
@@ -66,7 +66,8 @@ function getThumbnailAndImageURIs(transaction: Transaction, receiptPath: string
image = ReceiptSVG;
}
- return {thumbnail: image, image: path, isLocalFile: true};
+ const isLocalFile = path.startsWith('blob:') || path.startsWith('file:');
+ return {thumbnail: image, image: path, isLocalFile};
}
// eslint-disable-next-line import/prefer-default-export
diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts
index 4847eee2c8c6..34477d7f71ef 100644
--- a/src/libs/ReportActionsUtils.ts
+++ b/src/libs/ReportActionsUtils.ts
@@ -640,6 +640,19 @@ function isTaskAction(reportAction: OnyxEntry): boolean {
);
}
+/**
+ * When we delete certain reports, we want to check whether there are any visible actions left to display.
+ * If there are no visible actions left (including system messages), we can hide the report from view entirely
+ */
+function doesReportHaveVisibleActions(reportID: string, actionsToMerge: ReportActions = {}): boolean {
+ const reportActions = Object.values(OnyxUtils.fastMerge(allReportActions?.[reportID] ?? {}, actionsToMerge));
+ const visibleReportActions = Object.values(reportActions ?? {}).filter((action) => shouldReportActionBeVisibleAsLastAction(action));
+
+ // Exclude the task system message and the created message
+ const visibleReportActionsWithoutTaskSystemMessage = visibleReportActions.filter((action) => !isTaskAction(action) && !isCreatedAction(action));
+ return visibleReportActionsWithoutTaskSystemMessage.length > 0;
+}
+
function getAllReportActions(reportID: string): ReportActions {
return allReportActions?.[reportID] ?? {};
}
@@ -751,6 +764,14 @@ function getMemberChangeMessageFragment(reportAction: OnyxEntry):
};
}
+/**
+ * MARKEDREIMBURSED reportActions come from marking a report as reimbursed in OldDot. For now, we just
+ * concat all of the text elements of the message to create the full message.
+ */
+function getMarkedReimbursedMessage(reportAction: OnyxEntry): string {
+ return reportAction?.message?.map((element) => element.text).join('') ?? '';
+}
+
function getMemberChangeMessagePlainText(reportAction: OnyxEntry): string {
const messageElements = getMemberChangeMessageElements(reportAction);
return messageElements.map((element) => element.content).join('');
@@ -811,6 +832,7 @@ export {
isSentMoneyReportAction,
isSplitBillAction,
isTaskAction,
+ doesReportHaveVisibleActions,
isThreadParentMessage,
isTransactionThread,
isWhisperAction,
@@ -820,6 +842,7 @@ export {
hasRequestFromCurrentAccount,
getFirstVisibleReportActionID,
isMemberChangeAction,
+ getMarkedReimbursedMessage,
getMemberChangeMessageFragment,
getMemberChangeMessagePlainText,
isReimbursementDeQueuedAction,
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index 9404d832564a..470d9f3392d3 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -432,6 +432,51 @@ function getChatType(report: OnyxEntry): ValueOf | EmptyObject {
+ /**
+ * Using typical string concatenation here due to performance issues
+ * with template literals.
+ */
+ if (!allReports) {
+ return {};
+ }
+
+ return allReports?.[ONYXKEYS.COLLECTION.REPORT + reportID] ?? {};
+}
+
+/**
+ * Returns the parentReport if the given report is a thread.
+ */
+function getParentReport(report: OnyxEntry): OnyxEntry | EmptyObject {
+ if (!report?.parentReportID) {
+ return {};
+ }
+ return allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`] ?? {};
+}
+
+/**
+ * Returns the root parentReport if the given report is nested.
+ * Uses recursion to iterate any depth of nested reports.
+ */
+function getRootParentReport(report: OnyxEntry | undefined | EmptyObject): OnyxEntry | EmptyObject {
+ if (!report) {
+ return {};
+ }
+
+ // Returns the current report as the root report, because it does not have a parentReportID
+ if (!report?.parentReportID) {
+ return report;
+ }
+
+ const parentReport = getReport(report?.parentReportID);
+
+ // Runs recursion to iterate a parent report
+ return getRootParentReport(isNotEmptyObject(parentReport) ? parentReport : null);
+}
+
function getPolicy(policyID: string): Policy | EmptyObject {
if (!allPolicies || !policyID) {
return {};
@@ -461,10 +506,12 @@ function getPolicyName(report: OnyxEntry | undefined | EmptyObject, retu
}
const finalPolicy = policy ?? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`];
+ const parentReport = getRootParentReport(report);
+
// Public rooms send back the policy name with the reportSummary,
// since they can also be accessed by people who aren't in the workspace
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- const policyName = finalPolicy?.name || report?.policyName || report?.oldPolicyName || noPolicyFound;
+ const policyName = finalPolicy?.name || report?.policyName || report?.oldPolicyName || parentReport?.oldPolicyName || noPolicyFound;
return policyName;
}
@@ -1019,21 +1066,6 @@ function isOneOnOneChat(report: OnyxEntry): boolean {
);
}
-/**
- * Get the report given a reportID
- */
-function getReport(reportID: string | undefined): OnyxEntry | EmptyObject {
- /**
- * Using typical string concatenation here due to performance issues
- * with template literals.
- */
- if (!allReports) {
- return {};
- }
-
- return allReports?.[ONYXKEYS.COLLECTION.REPORT + reportID] ?? {};
-}
-
/**
* Get the notification preference given a report
*/
@@ -2132,36 +2164,6 @@ function getModifiedExpenseOriginalMessage(oldTransaction: OnyxEntry): OnyxEntry | EmptyObject {
- if (!report?.parentReportID) {
- return {};
- }
- return allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`] ?? {};
-}
-
-/**
- * Returns the root parentReport if the given report is nested.
- * Uses recursion to iterate any depth of nested reports.
- */
-function getRootParentReport(report: OnyxEntry): OnyxEntry | EmptyObject {
- if (!report) {
- return {};
- }
-
- // Returns the current report as the root report, because it does not have a parentReportID
- if (!report?.parentReportID) {
- return report;
- }
-
- const parentReport = getReport(report?.parentReportID);
-
- // Runs recursion to iterate a parent report
- return getRootParentReport(isNotEmptyObject(parentReport) ? parentReport : null);
-}
-
/**
* Get the title for a report.
*/
@@ -2540,8 +2542,6 @@ function buildOptimisticExpenseReport(chatReportID: string, policyID: string, pa
const formattedTotal = CurrencyUtils.convertToDisplayString(storedTotal, currency);
const policy = getPolicy(policyID);
- // The expense report is always created with the policy's output currency
- const outputCurrency = policy?.outputCurrency ?? CONST.CURRENCY.USD;
const isFree = policy?.type === CONST.POLICY.TYPE.FREE;
// Define the state and status of the report based on whether the policy is free or paid
@@ -2555,7 +2555,7 @@ function buildOptimisticExpenseReport(chatReportID: string, policyID: string, pa
policyID,
type: CONST.REPORT.TYPE.EXPENSE,
ownerAccountID: payeeAccountID,
- currency: outputCurrency,
+ currency,
// We don't translate reportName because the server response is always in English
reportName: `${policyName} owes ${formattedTotal}`,
diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js
index 22d660bd60be..511c299dda54 100644
--- a/src/libs/actions/IOU.js
+++ b/src/libs/actions/IOU.js
@@ -300,6 +300,20 @@ function resetMoneyRequestInfo(id = '') {
});
}
+/**
+ * Helper function to get the receipt error for money requests, or the generic error if there's no receipt
+ *
+ * @param {Object} receipt
+ * @param {String} filename
+ * @param {Boolean} [isScanRequest]
+ * @returns {Object}
+ */
+function getReceiptError(receipt, filename, isScanRequest = true) {
+ return _.isEmpty(receipt) || !isScanRequest
+ ? ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage')
+ : ErrorUtils.getMicroSecondOnyxErrorObject({error: CONST.IOU.RECEIPT_ERROR, source: receipt.source, filename});
+}
+
function buildOnyxDataForMoneyRequest(
chatReport,
iouReport,
@@ -315,6 +329,7 @@ function buildOnyxDataForMoneyRequest(
isNewIOUReport,
hasOutstandingChildRequest = false,
) {
+ const isScanRequest = TransactionUtils.isScanRequest(transaction);
const optimisticData = [
{
// Use SET for new reports because it doesn't exist yet, is faster and we need the data to be available when we navigate to the chat page
@@ -336,7 +351,9 @@ function buildOnyxDataForMoneyRequest(
...iouReport,
lastMessageText: iouAction.message[0].text,
lastMessageHtml: iouAction.message[0].html,
- ...(isNewIOUReport ? {pendingFields: {createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}} : {}),
+ pendingFields: {
+ ...(isNewIOUReport ? {createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD} : {preview: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}),
+ },
},
},
{
@@ -406,18 +423,14 @@ function buildOnyxDataForMoneyRequest(
},
]
: []),
- ...(isNewIOUReport
- ? [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`,
- value: {
- pendingFields: null,
- errorFields: null,
- },
- },
- ]
- : []),
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`,
+ value: {
+ pendingFields: null,
+ errorFields: null,
+ },
+ },
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`,
@@ -481,20 +494,16 @@ function buildOnyxDataForMoneyRequest(
: {}),
},
},
- ...(isNewIOUReport
- ? [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`,
- value: {
- pendingFields: null,
- errorFields: {
- createChat: ErrorUtils.getMicroSecondOnyxError('report.genericCreateReportFailureMessage'),
- },
- },
- },
- ]
- : []),
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`,
+ value: {
+ pendingFields: null,
+ errorFields: {
+ ...(isNewIOUReport ? {createChat: ErrorUtils.getMicroSecondOnyxError('report.genericCreateReportFailureMessage')} : {}),
+ },
+ },
+ },
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`,
@@ -519,7 +528,7 @@ function buildOnyxDataForMoneyRequest(
...(isNewChatReport
? {
[chatCreatedAction.reportActionID]: {
- errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'),
+ errors: getReceiptError(transaction.receipt, transaction.filename || transaction.receipt.filename, isScanRequest),
},
[reportPreviewAction.reportActionID]: {
errors: ErrorUtils.getMicroSecondOnyxError(null),
@@ -528,7 +537,7 @@ function buildOnyxDataForMoneyRequest(
: {
[reportPreviewAction.reportActionID]: {
created: reportPreviewAction.created,
- errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'),
+ errors: getReceiptError(transaction.receipt, transaction.filename || transaction.receipt.filename, isScanRequest),
},
}),
},
@@ -540,7 +549,7 @@ function buildOnyxDataForMoneyRequest(
...(isNewIOUReport
? {
[iouCreatedAction.reportActionID]: {
- errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'),
+ errors: getReceiptError(transaction.receipt, transaction.filename || transaction.receipt.filename, isScanRequest),
},
[iouAction.reportActionID]: {
errors: ErrorUtils.getMicroSecondOnyxError(null),
@@ -548,7 +557,7 @@ function buildOnyxDataForMoneyRequest(
}
: {
[iouAction.reportActionID]: {
- errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'),
+ errors: getReceiptError(transaction.receipt, transaction.filename || transaction.receipt.filename, isScanRequest),
},
}),
},
@@ -653,9 +662,10 @@ function getMoneyRequestInformation(
if (iouReport) {
if (isPolicyExpenseChat) {
iouReport = {...iouReport};
-
- // Because of the Expense reports are stored as negative values, we substract the total from the amount
- iouReport.total -= amount;
+ if (lodashGet(iouReport, 'currency') === currency) {
+ // Because of the Expense reports are stored as negative values, we substract the total from the amount
+ iouReport.total -= amount;
+ }
} else {
iouReport = IOUUtils.updateIOUOwnerAndTotal(iouReport, payeeAccountID, amount, currency);
}
@@ -1713,7 +1723,7 @@ function startSplitBill(participants, currentUserLogin, currentUserAccountID, co
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${splitChatReport.reportID}`,
value: {
[splitIOUReportAction.reportActionID]: {
- errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'),
+ errors: getReceiptError(receipt, filename),
},
},
});
@@ -1736,7 +1746,7 @@ function startSplitBill(participants, currentUserLogin, currentUserAccountID, co
errors: ErrorUtils.getMicroSecondOnyxError('report.genericCreateReportFailureMessage'),
},
[splitIOUReportAction.reportActionID]: {
- errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'),
+ errors: getReceiptError(receipt, filename),
},
},
},
diff --git a/src/libs/actions/OnyxUpdates.ts b/src/libs/actions/OnyxUpdates.ts
index bc407625dc6a..6f9c98c91f9e 100644
--- a/src/libs/actions/OnyxUpdates.ts
+++ b/src/libs/actions/OnyxUpdates.ts
@@ -72,8 +72,8 @@ function apply({lastUpdateID, type, request, response, updates}: Merge | undefined {
Log.info(`[OnyxUpdateManager] Applying update type: ${type} with lastUpdateID: ${lastUpdateID}`, false, {command: request?.command});
- if (lastUpdateID && lastUpdateIDAppliedToClient && Number(lastUpdateID) < lastUpdateIDAppliedToClient) {
- Log.info('[OnyxUpdateManager] Update received was older than current state, returning without applying the updates', false);
+ if (lastUpdateID && lastUpdateIDAppliedToClient && Number(lastUpdateID) <= lastUpdateIDAppliedToClient) {
+ Log.info('[OnyxUpdateManager] Update received was older or the same than current state, returning without applying the updates', false);
return Promise.resolve();
}
if (lastUpdateID && (lastUpdateIDAppliedToClient === null || Number(lastUpdateID) > lastUpdateIDAppliedToClient)) {
diff --git a/src/libs/actions/Task.js b/src/libs/actions/Task.js
index 678e1fb967dd..31cfd96c0bd3 100644
--- a/src/libs/actions/Task.js
+++ b/src/libs/actions/Task.js
@@ -713,7 +713,7 @@ function getShareDestination(reportID, reports, personalDetails) {
* @param {number} originalStateNum
* @param {number} originalStatusNum
*/
-function cancelTask(taskReportID, taskTitle, originalStateNum, originalStatusNum) {
+function deleteTask(taskReportID, taskTitle, originalStateNum, originalStatusNum) {
const message = `deleted task: ${taskTitle}`;
const optimisticCancelReportAction = ReportUtils.buildOptimisticTaskReportAction(taskReportID, CONST.REPORT.ACTIONS.TYPE.TASKCANCELLED, message);
const optimisticReportActionID = optimisticCancelReportAction.reportActionID;
@@ -721,6 +721,9 @@ function cancelTask(taskReportID, taskTitle, originalStateNum, originalStatusNum
const parentReportAction = ReportActionsUtils.getParentReportAction(taskReport);
const parentReport = ReportUtils.getParentReport(taskReport);
+ // If the task report is the last visible action in the parent report, we should navigate back to the parent report
+ const shouldDeleteTaskReport = !ReportActionsUtils.doesReportHaveVisibleActions(taskReportID);
+
const optimisticReportActions = {
[parentReportAction.reportActionID]: {
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
@@ -822,6 +825,10 @@ function cancelTask(taskReportID, taskTitle, originalStateNum, originalStatusNum
];
API.write('CancelTask', {cancelledTaskReportActionID: optimisticReportActionID, taskReportID}, {optimisticData, successData, failureData});
+
+ if (shouldDeleteTaskReport) {
+ Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(parentReport.reportID));
+ }
}
/**
@@ -928,7 +935,7 @@ export {
clearOutTaskInfoAndNavigate,
getAssignee,
getShareDestination,
- cancelTask,
+ deleteTask,
dismissModalAndClearOutTaskInfo,
getTaskAssigneeAccountID,
clearTaskErrors,
diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js
index 6c5a3ac4843f..edf6b65b2f4a 100644
--- a/src/pages/home/HeaderView.js
+++ b/src/pages/home/HeaderView.js
@@ -136,8 +136,8 @@ function HeaderView(props) {
if (props.report.stateNum !== CONST.REPORT.STATE_NUM.SUBMITTED && props.report.statusNum !== CONST.REPORT.STATUS.CLOSED && canModifyTask) {
threeDotMenuItems.push({
icon: Expensicons.Trashcan,
- text: translate('common.cancel'),
- onSelected: Session.checkIfActionIsAllowed(() => Task.cancelTask(props.report.reportID, props.report.reportName, props.report.stateNum, props.report.statusNum)),
+ text: translate('common.delete'),
+ onSelected: Session.checkIfActionIsAllowed(() => Task.deleteTask(props.report.reportID, props.report.reportName, props.report.stateNum, props.report.statusNum)),
});
}
}
diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js
index 1a163fcc9bca..435c086d913f 100644
--- a/src/pages/home/report/ReportActionItem.js
+++ b/src/pages/home/report/ReportActionItem.js
@@ -422,6 +422,8 @@ function ReportActionItem(props) {
children = ;
} else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE) {
children = ;
+ } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED) {
+ children = ;
} else {
const hasBeenFlagged = !_.contains([CONST.MODERATION.MODERATOR_DECISION_APPROVED, CONST.MODERATION.MODERATOR_DECISION_PENDING], moderationDecision);
children = (
diff --git a/src/pages/signin/ChooseSSOOrMagicCode.js b/src/pages/signin/ChooseSSOOrMagicCode.js
index db985e525545..13d20e689128 100644
--- a/src/pages/signin/ChooseSSOOrMagicCode.js
+++ b/src/pages/signin/ChooseSSOOrMagicCode.js
@@ -1,11 +1,12 @@
import PropTypes from 'prop-types';
-import React from 'react';
-import {View} from 'react-native';
+import React, {useEffect} from 'react';
+import {Keyboard, View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
import Button from '@components/Button';
import FormHelpMessage from '@components/FormHelpMessage';
import Text from '@components/Text';
+import useKeyboardState from '@hooks/useKeyboardState';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
@@ -54,10 +55,19 @@ const defaultProps = {
function ChooseSSOOrMagicCode({credentials, account, setIsUsingMagicCode}) {
const styles = useThemeStyles();
+ const {isKeyboardShown} = useKeyboardState();
const {translate} = useLocalize();
const {isOffline} = useNetwork();
const {isSmallScreenWidth} = useWindowDimensions();
+ // This view doesn't have a field for user input, so dismiss the device keyboard if shown
+ useEffect(() => {
+ if (!isKeyboardShown) {
+ return;
+ }
+ Keyboard.dismiss();
+ }, [isKeyboardShown]);
+
return (
<>
diff --git a/src/pages/tasks/TaskAssigneeSelectorModal.js b/src/pages/tasks/TaskAssigneeSelectorModal.js
index 39b585438413..a4fc61910be2 100644
--- a/src/pages/tasks/TaskAssigneeSelectorModal.js
+++ b/src/pages/tasks/TaskAssigneeSelectorModal.js
@@ -104,7 +104,7 @@ function TaskAssigneeSelectorModal(props) {
false,
{},
[],
- false,
+ true,
);
setHeaderMessage(OptionsListUtils.getHeaderMessage(recentReports?.length + personalDetails?.length !== 0 || currentUserOption, Boolean(userToInvite), searchValue));
diff --git a/src/styles/index.ts b/src/styles/index.ts
index da3c2bc2608c..fb1919b9f5d3 100644
--- a/src/styles/index.ts
+++ b/src/styles/index.ts
@@ -1422,6 +1422,12 @@ const styles = (theme: ThemeColors) =>
height: variables.lineHeightSizeh1,
},
+ LHPNavigatorContainer: (isSmallScreenWidth: boolean) =>
+ ({
+ ...modalNavigatorContainer(isSmallScreenWidth),
+ left: 0,
+ } satisfies ViewStyle),
+
RHPNavigatorContainer: (isSmallScreenWidth: boolean) =>
({
...modalNavigatorContainer(isSmallScreenWidth),
@@ -1641,14 +1647,14 @@ const styles = (theme: ThemeColors) =>
marginBottom: 4,
},
- overlayStyles: (current: OverlayStylesParams) =>
+ overlayStyles: (current: OverlayStylesParams, isModalOnTheLeft: boolean) =>
({
...positioning.pFixed,
// We need to stretch the overlay to cover the sidebar and the translate animation distance.
- left: -2 * variables.sideBarWidth,
+ left: isModalOnTheLeft ? 0 : -2 * variables.sideBarWidth,
top: 0,
bottom: 0,
- right: 0,
+ right: isModalOnTheLeft ? -2 * variables.sideBarWidth : 0,
backgroundColor: theme.overlay,
opacity: current.progress.interpolate({
inputRange: [0, 1],