diff --git a/src/components/ContextMenuItem.tsx b/src/components/ContextMenuItem.tsx index 2cc6a0ecec44..fe7be45e1b57 100644 --- a/src/components/ContextMenuItem.tsx +++ b/src/components/ContextMenuItem.tsx @@ -55,6 +55,12 @@ type ContextMenuItemProps = { /** Handles what to do when the item loose focus */ onBlur?: () => void; + + /** Whether the menu item is disabled or not */ + disabled?: boolean; + + /** Whether the menu item should show loading icon */ + shouldShowLoadingSpinnerIcon?: boolean; }; type ContextMenuItemHandle = { @@ -78,6 +84,8 @@ function ContextMenuItem( buttonRef = {current: null}, onFocus = () => {}, onBlur = () => {}, + disabled = false, + shouldShowLoadingSpinnerIcon = false, }: ContextMenuItemProps, ref: ForwardedRef, ) { @@ -135,6 +143,8 @@ function ContextMenuItem( interactive={isThrottledButtonActive} onFocus={onFocus} onBlur={onBlur} + disabled={disabled} + shouldShowLoadingSpinnerIcon={shouldShowLoadingSpinnerIcon} /> ); } diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 6a211e452f9a..4865617ddb2e 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -2,7 +2,7 @@ import type {ImageContentFit} from 'expo-image'; import type {ReactElement, ReactNode} from 'react'; import React, {forwardRef, useContext, useMemo} from 'react'; import type {GestureResponderEvent, StyleProp, TextStyle, ViewStyle} from 'react-native'; -import {View} from 'react-native'; +import {ActivityIndicator, View} from 'react-native'; import type {AnimatedStyle} from 'react-native-reanimated'; import type {ValueOf} from 'type-fest'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -304,6 +304,8 @@ type MenuItemBaseProps = { /** Render custom content inside the tooltip. */ renderTooltipContent?: () => ReactNode; + + shouldShowLoadingSpinnerIcon?: boolean; }; type MenuItemProps = (IconProps | AvatarProps | NoIcon) & MenuItemBaseProps; @@ -376,6 +378,7 @@ function MenuItem( shouldEscapeText = undefined, shouldGreyOutWhenDisabled = true, shouldUseDefaultCursorWhenDisabled = false, + shouldShowLoadingSpinnerIcon = false, isAnonymousAction = false, shouldBlockSelection = false, shouldParseTitle = false, @@ -579,26 +582,33 @@ function MenuItem( )} {icon && !Array.isArray(icon) && ( - {typeof icon !== 'string' && iconType === CONST.ICON_TYPE_ICON && ( - - )} + {typeof icon !== 'string' && + iconType === CONST.ICON_TYPE_ICON && + (!shouldShowLoadingSpinnerIcon ? ( + + ) : ( + + ))} {icon && iconType === CONST.ICON_TYPE_WORKSPACE && ( { return Object.values(ReportConnection.getAllReports() ?? {}).find((report) => isPolicyExpenseChat(report) && report?.policyID === policyID); } +function getSourceIDFromReportAction(reportAction: OnyxEntry): string { + const message = Array.isArray(reportAction?.message) ? reportAction?.message?.at(-1) ?? null : reportAction?.message ?? null; + const html = message?.html ?? ''; + const {sourceURL} = getAttachmentDetails(html); + const sourceID = (sourceURL?.match(CONST.REGEX.ATTACHMENT_ID) ?? [])[1]; + return sourceID; +} + function getIntegrationIcon(connectionName?: ConnectionName) { if (connectionName === CONST.POLICY.CONNECTIONS.NAME.XERO) { return XeroSquare; @@ -7538,6 +7547,7 @@ export { isExported, hasOnlyNonReimbursableTransactions, getMostRecentlyVisitedReport, + getSourceIDFromReportAction, getReport, getReportNameValuePairs, hasReportViolations, diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx index 5ab38cbf2e7e..c675b056342f 100755 --- a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx @@ -5,7 +5,7 @@ import {InteractionManager, View} from 'react-native'; // eslint-disable-next-line no-restricted-imports import type {GestureResponderEvent, Text as RNText, View as ViewType} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx, withOnyx} from 'react-native-onyx'; import type {ContextMenuItemHandle} from '@components/ContextMenuItem'; import ContextMenuItem from '@components/ContextMenuItem'; import FocusTrapForModal from '@components/FocusTrap/FocusTrapForModal'; @@ -135,6 +135,10 @@ function BaseReportActionContextMenu({ return reportActions[reportActionID]; }, [reportActions, reportActionID]); + const sourceID = ReportUtils.getSourceIDFromReportAction(reportAction); + + const [download] = useOnyx(`${ONYXKEYS.COLLECTION.DOWNLOAD}${sourceID}`); + const originalReportID = useMemo(() => ReportUtils.getOriginalReportID(reportID, reportAction), [reportID, reportAction]); const shouldEnableArrowNavigation = !isMini && (isVisible || shouldKeepOpen); @@ -287,6 +291,8 @@ function BaseReportActionContextMenu({ shouldPreventDefaultFocusOnPress={contextAction.shouldPreventDefaultFocusOnPress} onFocus={() => setFocusedIndex(index)} onBlur={() => (index === filteredContextMenuActions.length - 1 || index === 1) && setFocusedIndex(-1)} + disabled={contextAction?.shouldDisable ? contextAction?.shouldDisable(download) : false} + shouldShowLoadingSpinnerIcon={contextAction?.shouldDisable ? contextAction?.shouldDisable(download) : false} /> ); })} diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index 27859cec4193..b0a4d3d59d09 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -31,7 +31,7 @@ import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {Beta, OnyxInputOrEntry, ReportAction, ReportActionReactions, Transaction} from '@src/types/onyx'; +import type {Beta, Download as DownloadOnyx, OnyxInputOrEntry, ReportAction, ReportActionReactions, Transaction} from '@src/types/onyx'; import type IconAsset from '@src/types/utils/IconAsset'; import type {ContextMenuAnchor} from './ReportActionContextMenu'; import {hideContextMenu, showDeleteModal} from './ReportActionContextMenu'; @@ -108,6 +108,7 @@ type ContextMenuAction = (ContextMenuActionWithContent | ContextMenuActionWithIc isAnonymousAction: boolean; shouldShow: ShouldShow; shouldPreventDefaultFocusOnPress?: boolean; + shouldDisable?: (download: OnyxEntry) => boolean; }; // A list of all the context actions in this menu. @@ -533,6 +534,7 @@ const ContextMenuActions: ContextMenuAction[] = [ } }, getDescription: () => {}, + shouldDisable: (download) => download?.isDownloading ?? false, }, { isAnonymousAction: true,