From 2465982fbc624a011e53b0bb51ee302b213d58c7 Mon Sep 17 00:00:00 2001
From: nkdengineer <nkdengineer@outlook.com>
Date: Wed, 3 Jul 2024 10:36:39 +0700
Subject: [PATCH 1/3] fix: disabled download button when downloading

---
 src/components/ContextMenuItem.tsx                    |  4 ++++
 .../ContextMenu/BaseReportActionContextMenu.tsx       | 11 ++++++++++-
 .../home/report/ContextMenu/ContextMenuActions.tsx    |  4 +++-
 3 files changed, 17 insertions(+), 2 deletions(-)

diff --git a/src/components/ContextMenuItem.tsx b/src/components/ContextMenuItem.tsx
index 2cc6a0ecec44..2fc6dd72b545 100644
--- a/src/components/ContextMenuItem.tsx
+++ b/src/components/ContextMenuItem.tsx
@@ -55,6 +55,8 @@ type ContextMenuItemProps = {
 
     /** Handles what to do when the item loose focus */
     onBlur?: () => void;
+
+    disabled?: boolean;
 };
 
 type ContextMenuItemHandle = {
@@ -78,6 +80,7 @@ function ContextMenuItem(
         buttonRef = {current: null},
         onFocus = () => {},
         onBlur = () => {},
+        disabled = false,
     }: ContextMenuItemProps,
     ref: ForwardedRef<ContextMenuItemHandle>,
 ) {
@@ -135,6 +138,7 @@ function ContextMenuItem(
             interactive={isThrottledButtonActive}
             onFocus={onFocus}
             onBlur={onBlur}
+            disabled={disabled}
         />
     );
 }
diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx
index 136d8e5b59eb..16719f7b4a7f 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';
@@ -15,6 +15,7 @@ import useLocalize from '@hooks/useLocalize';
 import useNetwork from '@hooks/useNetwork';
 import useStyleUtils from '@hooks/useStyleUtils';
 import useWindowDimensions from '@hooks/useWindowDimensions';
+import getAttachmentDetails from '@libs/fileDownload/getAttachmentDetails';
 import * as ReportActionsUtils from '@libs/ReportActionsUtils';
 import * as ReportUtils from '@libs/ReportUtils';
 import * as Session from '@userActions/Session';
@@ -135,6 +136,13 @@ function BaseReportActionContextMenu({
         return reportActions[reportActionID];
     }, [reportActions, reportActionID]);
 
+    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];
+
+    const [download] = useOnyx(`${ONYXKEYS.COLLECTION.DOWNLOAD}${sourceID}`);
+
     const originalReportID = useMemo(() => ReportUtils.getOriginalReportID(reportID, reportAction), [reportID, reportAction]);
 
     const shouldEnableArrowNavigation = !isMini && (isVisible || shouldKeepOpen);
@@ -288,6 +296,7 @@ function BaseReportActionContextMenu({
                                 shouldPreventDefaultFocusOnPress={contextAction.shouldPreventDefaultFocusOnPress}
                                 onFocus={() => setFocusedIndex(index)}
                                 onBlur={() => (index === filteredContextMenuActions.length - 1 || index === 1) && setFocusedIndex(-1)}
+                                disabled={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 bf634b4ac8ae..68c3b1d1e89a 100644
--- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
+++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
@@ -28,7 +28,7 @@ import * as Report from '@userActions/Report';
 import CONST from '@src/CONST';
 import type {TranslationPaths} from '@src/languages/types';
 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';
@@ -105,6 +105,7 @@ type ContextMenuAction = (ContextMenuActionWithContent | ContextMenuActionWithIc
     isAnonymousAction: boolean;
     shouldShow: ShouldShow;
     shouldPreventDefaultFocusOnPress?: boolean;
+    shouldDisable?: (download: OnyxEntry<DownloadOnyx>) => boolean;
 };
 
 // A list of all the context actions in this menu.
@@ -512,6 +513,7 @@ const ContextMenuActions: ContextMenuAction[] = [
             }
         },
         getDescription: () => {},
+        shouldDisable: (download) => download?.isDownloading ?? false,
     },
     {
         isAnonymousAction: false,

From 4687fc61ce7f05c004efd967aeae7009220e7a0f Mon Sep 17 00:00:00 2001
From: nkdengineer <nkdengineer@outlook.com>
Date: Thu, 4 Jul 2024 11:33:00 +0700
Subject: [PATCH 2/3] fix: add loading spinner

---
 src/components/ContextMenuItem.tsx            |  6 +++
 src/components/MenuItem.tsx                   | 52 +++++++++++--------
 .../BaseReportActionContextMenu.tsx           |  1 +
 3 files changed, 38 insertions(+), 21 deletions(-)

diff --git a/src/components/ContextMenuItem.tsx b/src/components/ContextMenuItem.tsx
index 2fc6dd72b545..fe7be45e1b57 100644
--- a/src/components/ContextMenuItem.tsx
+++ b/src/components/ContextMenuItem.tsx
@@ -56,7 +56,11 @@ 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 = {
@@ -81,6 +85,7 @@ function ContextMenuItem(
         onFocus = () => {},
         onBlur = () => {},
         disabled = false,
+        shouldShowLoadingSpinnerIcon = false,
     }: ContextMenuItemProps,
     ref: ForwardedRef<ContextMenuItemHandle>,
 ) {
@@ -139,6 +144,7 @@ function ContextMenuItem(
             onFocus={onFocus}
             onBlur={onBlur}
             disabled={disabled}
+            shouldShowLoadingSpinnerIcon={shouldShowLoadingSpinnerIcon}
         />
     );
 }
diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx
index 9fd18524158d..c2b89d1dbfcc 100644
--- a/src/components/MenuItem.tsx
+++ b/src/components/MenuItem.tsx
@@ -3,7 +3,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';
@@ -301,6 +301,8 @@ type MenuItemBaseProps = {
 
     /** Render custom content inside the tooltip. */
     renderTooltipContent?: () => ReactNode;
+
+    shouldShowLoadingSpinnerIcon?: boolean;
 };
 
 type MenuItemProps = (IconProps | AvatarProps | NoIcon) & MenuItemBaseProps;
@@ -372,6 +374,7 @@ function MenuItem(
         shouldEscapeText = undefined,
         shouldGreyOutWhenDisabled = true,
         shouldUseDefaultCursorWhenDisabled = false,
+        shouldShowLoadingSpinnerIcon = false,
         isAnonymousAction = false,
         shouldBlockSelection = false,
         shouldParseTitle = false,
@@ -577,26 +580,33 @@ function MenuItem(
                                                     )}
                                                     {icon && !Array.isArray(icon) && (
                                                         <View style={[styles.popoverMenuIcon, iconStyles, StyleUtils.getAvatarWidthStyle(avatarSize)]}>
-                                                            {typeof icon !== 'string' && iconType === CONST.ICON_TYPE_ICON && (
-                                                                <Icon
-                                                                    contentFit={contentFit}
-                                                                    hovered={isHovered}
-                                                                    pressed={pressed}
-                                                                    src={icon}
-                                                                    width={iconWidth}
-                                                                    height={iconHeight}
-                                                                    fill={
-                                                                        displayInDefaultIconColor
-                                                                            ? undefined
-                                                                            : iconFill ??
-                                                                              StyleUtils.getIconFillColor(
-                                                                                  getButtonState(focused || isHovered, pressed, success, disabled, interactive),
-                                                                                  true,
-                                                                                  isPaneMenu,
-                                                                              )
-                                                                    }
-                                                                />
-                                                            )}
+                                                            {typeof icon !== 'string' &&
+                                                                iconType === CONST.ICON_TYPE_ICON &&
+                                                                (!shouldShowLoadingSpinnerIcon ? (
+                                                                    <Icon
+                                                                        contentFit={contentFit}
+                                                                        hovered={isHovered}
+                                                                        pressed={pressed}
+                                                                        src={icon}
+                                                                        width={iconWidth}
+                                                                        height={iconHeight}
+                                                                        fill={
+                                                                            displayInDefaultIconColor
+                                                                                ? undefined
+                                                                                : iconFill ??
+                                                                                  StyleUtils.getIconFillColor(
+                                                                                      getButtonState(focused || isHovered, pressed, success, disabled, interactive),
+                                                                                      true,
+                                                                                      isPaneMenu,
+                                                                                  )
+                                                                        }
+                                                                    />
+                                                                ) : (
+                                                                    <ActivityIndicator
+                                                                        size="small"
+                                                                        color={theme.textSupporting}
+                                                                    />
+                                                                ))}
                                                             {icon && iconType === CONST.ICON_TYPE_WORKSPACE && (
                                                                 <Avatar
                                                                     imageStyles={[styles.alignSelfCenter]}
diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx
index 16719f7b4a7f..1b55ff54bdb2 100755
--- a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx
+++ b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx
@@ -297,6 +297,7 @@ function BaseReportActionContextMenu({
                                 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}
                             />
                         );
                     })}

From 7dc244b94c232a8289d032e3b779d46d0aeb82ce Mon Sep 17 00:00:00 2001
From: nkdengineer <nkdengineer@outlook.com>
Date: Thu, 11 Jul 2024 11:09:11 +0700
Subject: [PATCH 3/3] create new utils func

---
 src/libs/ReportUtils.ts                                | 10 ++++++++++
 .../report/ContextMenu/BaseReportActionContextMenu.tsx |  6 +-----
 2 files changed, 11 insertions(+), 5 deletions(-)

diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index afe384b87531..e4c81cf6d45a 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -55,6 +55,7 @@ import * as store from './actions/ReimbursementAccount/store';
 import * as CurrencyUtils from './CurrencyUtils';
 import DateUtils from './DateUtils';
 import {hasValidDraftComment} from './DraftCommentUtils';
+import getAttachmentDetails from './fileDownload/getAttachmentDetails';
 import isReportMessageAttachment from './isReportMessageAttachment';
 import localeCompare from './LocaleCompare';
 import * as LocalePhoneNumber from './LocalePhoneNumber';
@@ -7089,6 +7090,14 @@ function findPolicyExpenseChatByPolicyID(policyID: string): OnyxEntry<Report> {
     return Object.values(ReportConnection.getAllReports() ?? {}).find((report) => isPolicyExpenseChat(report) && report?.policyID === policyID);
 }
 
+function getSourceIDFromReportAction(reportAction: OnyxEntry<ReportAction>): 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;
+}
+
 export {
     addDomainToShortMention,
     areAllRequestsBeingSmartScanned,
@@ -7370,6 +7379,7 @@ export {
     findPolicyExpenseChatByPolicyID,
     hasOnlyNonReimbursableTransactions,
     getMostRecentlyVisitedReport,
+    getSourceIDFromReportAction,
 };
 
 export type {
diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx
index 2cbb95d1bcc4..c675b056342f 100755
--- a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx
+++ b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx
@@ -15,7 +15,6 @@ import useLocalize from '@hooks/useLocalize';
 import useNetwork from '@hooks/useNetwork';
 import useStyleUtils from '@hooks/useStyleUtils';
 import useWindowDimensions from '@hooks/useWindowDimensions';
-import getAttachmentDetails from '@libs/fileDownload/getAttachmentDetails';
 import * as ReportActionsUtils from '@libs/ReportActionsUtils';
 import * as ReportUtils from '@libs/ReportUtils';
 import * as Session from '@userActions/Session';
@@ -136,10 +135,7 @@ function BaseReportActionContextMenu({
         return reportActions[reportActionID];
     }, [reportActions, reportActionID]);
 
-    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];
+    const sourceID = ReportUtils.getSourceIDFromReportAction(reportAction);
 
     const [download] = useOnyx(`${ONYXKEYS.COLLECTION.DOWNLOAD}${sourceID}`);
 
