From 0d7c42c9f3eda4525a47d5618e78450cd8d334a7 Mon Sep 17 00:00:00 2001 From: rory Date: Sat, 28 Feb 2026 00:19:01 -0800 Subject: [PATCH 01/88] refactor(contextmenu): consolidate popover state into single object Replace 7 refs + 8 useState calls with a single PopoverContextMenuState object. Keep separate isPopoverVisible boolean for animation control: hideContextMenu sets isPopoverVisible=false (triggering animation), onModalHide clears menuState=null (clearing data after animation). Consolidate popoverAnchorPosition + contextMenuDimensions into PopoverPosition within the state object. Eliminate reportActionRef entirely (latent staleness bug -- only set in showDeleteModal, never in showContextMenu). Store only reportActionID in consolidated state. Derive full action from reportActions[reportActionID]. Move onEmojiPickerToggle from ref to state to avoid accessing refs during render. Remove all useCallback wrappers and inline the logic. Made-with: Cursor --- .../PopoverReportActionContextMenu.tsx | 346 +++++++++--------- 1 file changed, 168 insertions(+), 178 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx index 1c8cf5467ccd..d8666c9cf83d 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -1,9 +1,8 @@ import type {ForwardedRef} from 'react'; -import React, {useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; +import React, {useEffect, useImperativeHandle, useRef, useState} from 'react'; /* eslint-disable no-restricted-imports */ import type {EmitterSubscription, GestureResponderEvent, NativeTouchEvent, View} from 'react-native'; import {DeviceEventEmitter, Dimensions, InteractionManager} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; import {Actions, useActionSheetAwareScrollViewActions} from '@components/ActionSheetAwareScrollView'; import ConfirmModal from '@components/ConfirmModal'; import PopoverWithMeasuredContent from '@components/PopoverWithMeasuredContent'; @@ -27,9 +26,6 @@ import {getOriginalMessage, isMoneyRequestAction, isReportPreviewAction, isTrack import {getOriginalReportID} from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {AnchorDimensions} from '@src/styles'; -import type {ReportAction} from '@src/types/onyx'; -import type {Location} from '@src/types/utils/Layout'; import BaseReportActionContextMenu from './BaseReportActionContextMenu'; import type {ContextMenuAction} from './ContextMenuActions'; import type {ContextMenuAnchor, ContextMenuType, ReportActionContextMenu} from './ReportActionContextMenu'; @@ -41,6 +37,33 @@ function extractPointerEvent(event: GestureResponderEvent | MouseEvent): MouseEv return event; } +type PopoverPosition = { + anchorHorizontal: number; + anchorVertical: number; + anchorWidth: number; + anchorHeight: number; +}; + +type PopoverContextMenuState = { + type: ContextMenuType; + reportID: string | undefined; + reportActionID: string | undefined; + originalReportID: string | undefined; + selection: string; + draftMessage: string | undefined; + isArchivedRoom: boolean; + isChronos: boolean; + isPinnedChat: boolean; + isUnreadChat: boolean; + isThreadReportParentAction: boolean; + disabledActions: ContextMenuAction[]; + isOverflowMenu: boolean; + withoutOverlay: boolean; + position: PopoverPosition; + contextMenuTargetNode: HTMLDivElement | null; + onEmojiPickerToggle: ((state: boolean) => void) | undefined; +}; + type PopoverReportActionContextMenuProps = { /** Reference to the outer element */ ref?: ForwardedRef; @@ -48,44 +71,29 @@ type PopoverReportActionContextMenuProps = { function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuProps) { const {translate} = useLocalize(); - const reportIDRef = useRef(undefined); - const typeRef = useRef(undefined); - const reportActionRef = useRef> | null>(null); - const reportActionIDRef = useRef(undefined); - const originalReportIDRef = useRef(undefined); - const selectionRef = useRef(''); - const reportActionDraftMessageRef = useRef(undefined); - const isReportArchived = useReportIsArchived(reportIDRef.current); - const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportIDRef.current}`); - const isOriginalReportArchived = useReportIsArchived(getOriginalReportID(reportIDRef.current, reportActionRef.current, reportActions)); - const {iouReport, chatReport, isChatIOUReportArchived} = useGetIOUReportFromReportAction(reportActionRef.current); const {transitionActionSheetState} = useActionSheetAwareScrollViewActions(); - const cursorRelativePosition = useRef({ - horizontal: 0, - vertical: 0, - }); + const [menuState, setMenuState] = useState(null); + const [isPopoverVisible, setIsPopoverVisible] = useState(false); + const [isContextMenuOpening, setIsContextMenuOpening] = useState(false); + const [composerToRefocusOnClose, setComposerToRefocusOnClose] = useState(); - // The horizontal and vertical position (relative to the screen) where the popover will display. - const popoverAnchorPosition = useRef({ - horizontal: 0, - vertical: 0, - }); + const reportID = menuState?.reportID; + const reportActionID = menuState?.reportActionID; + + const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`); + const reportAction = reportActions?.[reportActionID ?? '']; + + const isReportArchived = useReportIsArchived(reportID); + const isOriginalReportArchived = useReportIsArchived(getOriginalReportID(reportID, reportAction, reportActions)); + const {iouReport, chatReport, isChatIOUReportArchived} = useGetIOUReportFromReportAction(reportAction); + + const cursorRelativePosition = useRef({horizontal: 0, vertical: 0}); const instanceIDRef = useRef(''); const {email, accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); - const [isPopoverVisible, setIsPopoverVisible] = useState(false); const [isDeleteCommentConfirmModalVisible, setIsDeleteCommentConfirmModalVisible] = useState(false); const [shouldSetModalVisibilityForDeleteConfirmation, setShouldSetModalVisibilityForDeleteConfirmation] = useState(true); - - const [isRoomArchived, setIsRoomArchived] = useState(false); - const [isChronosReportEnabled, setIsChronosReportEnabled] = useState(false); - const [isChatPinned, setIsChatPinned] = useState(false); - const [hasUnreadMessages, setHasUnreadMessages] = useState(false); - const [isThreadReportParentAction, setIsThreadReportParentAction] = useState(false); - const [disabledActions, setDisabledActions] = useState([]); - const [shouldSwitchPositionIfOverflow, setShouldSwitchPositionIfOverflow] = useState(false); - const [isWithoutOverlay, setIsWithoutOverlay] = useState(true); const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS); @@ -93,39 +101,26 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro const anchorRef = useRef(null); const dimensionsEventListener = useRef(null); const contextMenuAnchorRef = useRef(null); - const contextMenuTargetNode = useRef(null); - const contextMenuDimensions = useRef({ - width: 0, - height: 0, - }); - - const [composerToRefocusOnClose, setComposerToRefocusOnClose] = useState(); const onPopoverShow = useRef(() => {}); - const [isContextMenuOpening, setIsContextMenuOpening] = useState(false); const onPopoverHide = useRef(() => {}); - const onEmojiPickerToggle = useRef void)>(undefined); const onCancelDeleteModal = useRef(() => {}); const onConfirmDeleteModal = useRef(() => {}); - const onPopoverHideActionCallback = useRef(() => {}); const callbackWhenDeleteModalHide = useRef(() => {}); /** Get the Context menu anchor position. We calculate the anchor coordinates from measureInWindow async method */ - const getContextMenuMeasuredLocation = useCallback( - () => - new Promise((resolve) => { - if (contextMenuAnchorRef.current && 'measureInWindow' in contextMenuAnchorRef.current && typeof contextMenuAnchorRef.current.measureInWindow === 'function') { - contextMenuAnchorRef.current.measureInWindow((x, y) => resolve({x, y})); - } else { - resolve({x: 0, y: 0}); - } - }), - [], - ); + const getContextMenuMeasuredLocation = () => + new Promise<{x: number; y: number}>((resolve) => { + if (contextMenuAnchorRef.current && 'measureInWindow' in contextMenuAnchorRef.current && typeof contextMenuAnchorRef.current.measureInWindow === 'function') { + contextMenuAnchorRef.current.measureInWindow((x, y) => resolve({x, y})); + } else { + resolve({x: 0, y: 0}); + } + }); /** This gets called on Dimensions change to find the anchor coordinates for the action context menu. */ - const measureContextMenuAnchorPosition = useCallback(() => { + const measureContextMenuAnchorPosition = () => { if (!isPopoverVisible) { return; } @@ -135,12 +130,21 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro return; } - popoverAnchorPosition.current = { - horizontal: cursorRelativePosition.current.horizontal + x, - vertical: cursorRelativePosition.current.vertical + y, - }; + setMenuState((prev) => { + if (!prev) { + return prev; + } + return { + ...prev, + position: { + ...prev.position, + anchorHorizontal: cursorRelativePosition.current.horizontal + x, + anchorVertical: cursorRelativePosition.current.vertical + y, + }, + }; + }); }); - }, [isPopoverVisible, getContextMenuMeasuredLocation]); + }; useEffect(() => { dimensionsEventListener.current = Dimensions.addEventListener('change', measureContextMenuAnchorPosition); @@ -151,34 +155,18 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro } dimensionsEventListener.current.remove(); }; - }, [measureContextMenuAnchorPosition]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isPopoverVisible]); /** Whether Context Menu is active for the Report Action. */ - const isActiveReportAction: ReportActionContextMenu['isActiveReportAction'] = (actionID) => - !!actionID && (reportActionIDRef.current === actionID || reportActionRef.current?.reportActionID === actionID); + const isActiveReportAction: ReportActionContextMenu['isActiveReportAction'] = (actionID) => !!actionID && reportActionID === String(actionID); const clearActiveReportAction = () => { - reportActionIDRef.current = undefined; - reportActionRef.current = null; + setMenuState(null); }; /** * Show the ReportActionContextMenu modal popover. - * - * @param type - context menu type [EMAIL, LINK, REPORT_ACTION] - * @param [event] - A press event. - * @param [selection] - Copied content. - * @param contextMenuAnchor - popoverAnchor - * @param reportID - Active Report Id - * @param reportActionID - ReportAction for ContextMenu - * @param originalReportID - The current Report Id of the reportAction - * @param draftMessage - ReportAction draft message - * @param [onShow] - Run a callback when Menu is shown - * @param [onHide] - Run a callback when Menu is hidden - * @param isArchivedRoom - Whether the provided report is an archived room - * @param isChronosReport - Flag to check if the chat participant is Chronos - * @param isPinnedChat - Flag to check if the chat is pinned in the LHN. Used for the Pin/Unpin action - * @param isUnreadChat - Flag to check if the chat is unread in the LHN. Used for the Mark as Read/Unread action */ const showContextMenu: ReportActionContextMenu['showContextMenu'] = (showContextMenuParams) => { const { @@ -187,7 +175,7 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro selection, contextMenuAnchor, report: currentReport = {}, - reportAction = {}, + reportAction: reportActionParam = {}, callbacks = {}, disabledOptions = [], shouldCloseOnTarget = false, @@ -200,30 +188,32 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro setComposerToRefocusOnClose('edit'); } - const {reportID, originalReportID, isArchivedRoom = false, isChronos = false, isPinnedChat = false, isUnreadChat = false} = currentReport; - const {reportActionID, draftMessage, isThreadReportParentAction: isThreadReportParentActionParam = false} = reportAction; + const {reportID: showReportID, originalReportID: showOriginalReportID, isArchivedRoom = false, isChronos = false, isPinnedChat = false, isUnreadChat = false} = currentReport; + const {reportActionID: showReportActionID, draftMessage, isThreadReportParentAction: isThreadReportParentActionParam = false} = reportActionParam; const {onShow = () => {}, onHide = () => {}, setIsEmojiPickerActive = () => {}} = callbacks; setIsContextMenuOpening(true); - setIsWithoutOverlay(withoutOverlay); + const {pageX = 0, pageY = 0} = extractPointerEvent(event); contextMenuAnchorRef.current = contextMenuAnchor; - contextMenuTargetNode.current = event.target as HTMLDivElement; + const targetNode = event.target as HTMLDivElement; if (shouldCloseOnTarget) { - anchorRef.current = event.target as HTMLDivElement; + anchorRef.current = targetNode; } else { anchorRef.current = null; } onPopoverShow.current = onShow; onPopoverHide.current = onHide; - onEmojiPickerToggle.current = setIsEmojiPickerActive; - new Promise((resolve) => { + new Promise((resolve) => { if (!!(!pageX && !pageY && contextMenuAnchorRef.current) || isOverflowMenu) { calculateAnchorPosition(contextMenuAnchorRef.current).then((position) => { - popoverAnchorPosition.current = {horizontal: position.horizontal, vertical: position.vertical}; - contextMenuDimensions.current = {width: position.vertical, height: position.height}; - resolve(); + resolve({ + anchorHorizontal: position.horizontal, + anchorVertical: position.vertical, + anchorWidth: position.vertical, + anchorHeight: position.height, + }); }); } else { getContextMenuMeasuredLocation().then(({x, y}) => { @@ -231,29 +221,36 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro horizontal: pageX - x, vertical: pageY - y, }; - popoverAnchorPosition.current = { - horizontal: pageX, - vertical: pageY, - }; - resolve(); + resolve({ + anchorHorizontal: pageX, + anchorVertical: pageY, + anchorWidth: 0, + anchorHeight: 0, + }); }); } - }).then(() => { - setDisabledActions(disabledOptions); - typeRef.current = type; - reportIDRef.current = reportID; - reportActionIDRef.current = reportActionID; - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - originalReportIDRef.current = originalReportID || undefined; - selectionRef.current = selection; + }).then((position) => { + setMenuState({ + type, + reportID: showReportID, + reportActionID: showReportActionID, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + originalReportID: showOriginalReportID || undefined, + selection, + draftMessage, + isArchivedRoom, + isChronos, + isPinnedChat, + isUnreadChat, + isThreadReportParentAction: isThreadReportParentActionParam, + disabledActions: disabledOptions, + isOverflowMenu, + withoutOverlay, + position, + contextMenuTargetNode: targetNode, + onEmojiPickerToggle: setIsEmojiPickerActive, + }); setIsPopoverVisible(true); - reportActionDraftMessageRef.current = draftMessage; - setIsRoomArchived(isArchivedRoom); - setIsChronosReportEnabled(isChronos); - setIsChatPinned(isPinnedChat); - setHasUnreadMessages(isUnreadChat); - setIsThreadReportParentAction(isThreadReportParentActionParam); - setShouldSwitchPositionIfOverflow(isOverflowMenu); }); }; @@ -262,7 +259,6 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro instanceIDRef.current = Math.random().toString(36).slice(2, 7); onPopoverShow.current(); - // After we have called the action, reset it. onPopoverShow.current = () => {}; // After the context menu opening animation ends reset isContextMenuOpening. @@ -279,11 +275,8 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro /** After Popover hides, call the registered onPopoverHide & onPopoverHideActionCallback callback and reset it */ const runAndResetOnPopoverHide = () => { - reportIDRef.current = undefined; - reportActionIDRef.current = undefined; - originalReportIDRef.current = undefined; + setMenuState(null); instanceIDRef.current = ''; - selectionRef.current = ''; onPopoverHide.current = runAndResetCallback(onPopoverHide.current); onPopoverHideActionCallback.current = runAndResetCallback(onPopoverHideActionCallback.current); @@ -300,8 +293,6 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro onPopoverHideActionCallback.current = callbacks.onHide; } - selectionRef.current = ''; - reportActionDraftMessageRef.current = undefined; setIsPopoverVisible(false); transitionActionSheetState({ @@ -314,16 +305,16 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro }; const transactionIDs: string[] = []; - if (isMoneyRequestAction(reportActionRef.current)) { - const originalMessage = getOriginalMessage(reportActionRef.current); + if (isMoneyRequestAction(reportAction)) { + const originalMessage = getOriginalMessage(reportAction); if (originalMessage && 'IOUTransactionID' in originalMessage && !!originalMessage.IOUTransactionID) { transactionIDs.push(originalMessage.IOUTransactionID); } } const {duplicateTransactions, duplicateTransactionViolations} = useDuplicateTransactionsAndViolations(transactionIDs); - const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportIDRef.current}`); - const [childReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportActionRef.current?.childReportID}`); + const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); + const [childReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportAction?.childReportID}`); const [selfDMReportID] = useOnyx(ONYXKEYS.SELF_DM_REPORT_ID); const [selfDMReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${selfDMReportID}`); const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`); @@ -331,11 +322,11 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro const {currentSearchHash} = useSearchStateContext(); const {deleteTransactions} = useDeleteTransactions({ report, - reportActions: reportActionRef.current ? [reportActionRef.current] : [], + reportActions: reportAction ? [reportAction] : [], policy, }); - const [originalReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getOriginalReportID(reportIDRef.current, reportActionRef.current, reportActions)}`); + const [originalReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getOriginalReportID(reportID, reportAction, reportActions)}`); const ancestorsRef = useRef([]); const ancestors = useAncestors(originalReport); const {transactions: reportTransactions} = useTransactionsAndViolationsForReport(originalReport?.iouReportID); @@ -345,14 +336,13 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro } ancestorsRef.current = ancestors; }, [originalReport, ancestors]); - const confirmDeleteAndHideModal = useCallback(() => { + const confirmDeleteAndHideModal = () => { callbackWhenDeleteModalHide.current = runAndResetCallback(onConfirmDeleteModal.current); - const reportAction = reportActionRef.current; if (isMoneyRequestAction(reportAction)) { const originalMessage = getOriginalMessage(reportAction); if (isTrackExpenseAction(reportAction)) { deleteTrackExpense({ - chatReportID: reportIDRef.current, + chatReportID: reportID, chatReport: report, transactionID: originalMessage?.IOUTransactionID, reportAction, @@ -378,46 +368,42 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro }); } - DeviceEventEmitter.emit(`deletedReportAction_${reportIDRef.current}`, reportAction?.reportActionID); + DeviceEventEmitter.emit(`deletedReportAction_${reportID}`, reportAction?.reportActionID); setIsDeleteCommentConfirmModalVisible(false); - }, [ - report, - childReport, - selfDMReport, - iouReport, - chatReport, - duplicateTransactions, - duplicateTransactionViolations, - isReportArchived, - isChatIOUReportArchived, - allTransactionViolations, - currentUserAccountID, - deleteTransactions, - currentSearchHash, - email, - reportTransactions, - bankAccountList, - isOriginalReportArchived, - visibleReportActionsData, - ]); + }; const hideDeleteModal = () => { callbackWhenDeleteModalHide.current = () => (onCancelDeleteModal.current = runAndResetCallback(onCancelDeleteModal.current)); setIsDeleteCommentConfirmModalVisible(false); setShouldSetModalVisibilityForDeleteConfirmation(true); - setIsRoomArchived(false); - setIsChronosReportEnabled(false); - setIsChatPinned(false); - setHasUnreadMessages(false); }; /** Opens the Confirm delete action modal */ - const showDeleteModal: ReportActionContextMenu['showDeleteModal'] = (reportID, reportAction, shouldSetModalVisibility = true, onConfirm = () => {}, onCancel = () => {}) => { + const showDeleteModal: ReportActionContextMenu['showDeleteModal'] = (showReportID, showReportAction, shouldSetModalVisibility = true, onConfirm = () => {}, onCancel = () => {}) => { onCancelDeleteModal.current = onCancel; - onConfirmDeleteModal.current = onConfirm; - reportIDRef.current = reportID; - reportActionRef.current = reportAction ?? null; + + setMenuState((prev) => ({ + ...(prev ?? { + type: CONST.CONTEXT_MENU_TYPES.REPORT_ACTION as ContextMenuType, + selection: '', + draftMessage: undefined, + isArchivedRoom: false, + isChronos: false, + isPinnedChat: false, + isUnreadChat: false, + isThreadReportParentAction: false, + disabledActions: [], + isOverflowMenu: false, + withoutOverlay: true, + position: {anchorHorizontal: 0, anchorVertical: 0, anchorWidth: 0, anchorHeight: 0}, + contextMenuTargetNode: null, + onEmojiPickerToggle: undefined, + }), + reportID: showReportID, + reportActionID: showReportAction?.reportActionID, + originalReportID: prev?.originalReportID, + })); setShouldSetModalVisibilityForDeleteConfirmation(shouldSetModalVisibility); setIsDeleteCommentConfirmModalVisible(true); @@ -437,8 +423,6 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro composerToRefocusOnCloseEmojiPicker: composerToRefocusOnClose, })); - const reportAction = reportActionRef.current; - return ( <> hideContextMenu()} onModalShow={runAndResetOnPopoverShow} onModalHide={runAndResetOnPopoverHide} - anchorPosition={popoverAnchorPosition.current} + anchorPosition={{ + horizontal: menuState?.position.anchorHorizontal ?? 0, + vertical: menuState?.position.anchorVertical ?? 0, + }} animationIn="fadeIn" disableAnimation={false} shouldSetModalVisibility={false} fullscreen - withoutOverlay={isWithoutOverlay} - anchorDimensions={contextMenuDimensions.current} + withoutOverlay={menuState?.withoutOverlay ?? true} + anchorDimensions={{ + width: menuState?.position.anchorWidth ?? 0, + height: menuState?.position.anchorHeight ?? 0, + }} anchorRef={anchorRef} - shouldSwitchPositionIfOverflow={shouldSwitchPositionIfOverflow} + shouldSwitchPositionIfOverflow={menuState?.isOverflowMenu ?? false} > Date: Sat, 28 Feb 2026 00:22:24 -0800 Subject: [PATCH 02/88] refactor(contextmenu): remove manual memoization from BaseReportActionContextMenu Remove memo(deepEqual) wrapper and all useMemo calls (5 total). React Compiler handles memoization automatically. Replace inline {current: null} with stable nullRef to avoid new object creation per render. Inline all computed values directly. Made-with: Cursor --- .../BaseReportActionContextMenu.tsx | 50 ++++++++----------- 1 file changed, 20 insertions(+), 30 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx index cdd2b0054036..11826c3f1318 100755 --- a/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx +++ b/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx @@ -1,6 +1,5 @@ -import {deepEqual} from 'fast-equals'; import type {RefObject} from 'react'; -import React, {memo, useMemo, useRef, useState} from 'react'; +import React, {useRef, useState} from 'react'; 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'; @@ -161,6 +160,7 @@ function BaseReportActionContextMenu({ const {isOffline} = useNetwork(); const {isProduction} = useEnvironment(); const threeDotRef = useRef(null); + const nullRef = useRef(null); const [betas] = useOnyx(ONYXKEYS.BETAS); const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, { canEvict: false, @@ -171,12 +171,8 @@ function BaseReportActionContextMenu({ selector: withDEWRoutedActionsObject, }); - const reportAction: OnyxEntry = useMemo(() => { - if (isEmptyObject(originalReportActions) || reportActionID === '0' || reportActionID === '-1' || !reportActionID) { - return; - } - return originalReportActions[reportActionID]; - }, [originalReportActions, reportActionID]); + const reportAction: OnyxEntry = + isEmptyObject(originalReportActions) || reportActionID === '0' || reportActionID === '-1' || !reportActionID ? undefined : originalReportActions[reportActionID]; const transactionID = getLinkedTransactionID(reportAction); const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(transactionID)}`); const [isDebugModeEnabled] = useOnyx(ONYXKEYS.IS_DEBUG_MODE_ENABLED); @@ -205,28 +201,23 @@ function BaseReportActionContextMenu({ const parentReportAction = getReportAction(childReport?.parentReportID, childReport?.parentReportActionID); const {reportActions: paginatedReportActions} = usePaginatedReportActions(childReport?.reportID); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); - const transactionThreadReportID = useMemo( - () => getOneTransactionThreadReportID(childReport, childChatReport, paginatedReportActions ?? [], isOffline), - [paginatedReportActions, isOffline, childReport, childChatReport], - ); + const transactionThreadReportID = getOneTransactionThreadReportID(childReport, childChatReport, paginatedReportActions ?? [], isOffline); const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(transactionThreadReportID)}`); - const isMoneyRequestReport = useMemo(() => ReportUtilsIsMoneyRequestReport(childReport), [childReport]); - const isInvoiceReport = useMemo(() => ReportUtilsIsInvoiceReport(childReport), [childReport]); - - const requestParentReportAction = useMemo(() => { - if (isMoneyRequestReport || isInvoiceReport) { - if (transactionThreadReportID === CONST.FAKE_REPORT_ID) { - return Object.values(childReportActions ?? {}).find((action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && !isDeletedAction(action)); - } - if (!paginatedReportActions || !transactionThreadReport?.parentReportActionID) { - return undefined; - } - return paginatedReportActions.find((action) => action.reportActionID === transactionThreadReport.parentReportActionID); + const isMoneyRequestReport = ReportUtilsIsMoneyRequestReport(childReport); + const isInvoiceReport = ReportUtilsIsInvoiceReport(childReport); + + let requestParentReportAction; + if (isMoneyRequestReport || isInvoiceReport) { + if (transactionThreadReportID === CONST.FAKE_REPORT_ID) { + requestParentReportAction = Object.values(childReportActions ?? {}).find((action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && !isDeletedAction(action)); + } else if (paginatedReportActions && transactionThreadReport?.parentReportActionID) { + requestParentReportAction = paginatedReportActions.find((action) => action.reportActionID === transactionThreadReport.parentReportActionID); } - return parentReportAction; - }, [parentReportAction, isMoneyRequestReport, isInvoiceReport, paginatedReportActions, transactionThreadReport?.parentReportActionID, transactionThreadReportID, childReportActions]); + } else { + requestParentReportAction = parentReportAction; + } const moneyRequestAction = transactionThreadReportID ? requestParentReportAction : parentReportAction; const isChildReportArchived = useReportIsArchived(childReport?.reportID); @@ -245,7 +236,7 @@ function BaseReportActionContextMenu({ const session = useSession(); const encryptedAuthToken = session?.encryptedAuthToken ?? ''; - const isMoneyRequest = useMemo(() => ReportUtilsIsMoneyRequest(childReport), [childReport]); + const isMoneyRequest = ReportUtilsIsMoneyRequest(childReport); const isTrackExpenseReport = ReportUtilsIsTrackExpenseReport(childReport); const isSingleTransactionView = isMoneyRequest || isTrackExpenseReport; const isMoneyRequestOrReport = isMoneyRequestReport || isSingleTransactionView; @@ -422,7 +413,7 @@ function BaseReportActionContextMenu({ return ( Date: Sat, 28 Feb 2026 00:27:49 -0800 Subject: [PATCH 03/88] refactor(contextmenu): extract ConfirmDeleteReportActionModal Extract delete confirmation flow from PopoverReportActionContextMenu into a standalone ConfirmDeleteReportActionModal component that mounts via the established global modal system (useModal/ModalProvider). The new component owns all ~16 delete-related Onyx subscriptions, which are only active when the delete modal is actually shown. This eliminates 5 duplicate subscriptions with BaseReportActionContextMenu and defers the remaining ~11 until actually needed. The promise-based API replaces 3 callback refs + 2 state vars from PopoverReportActionContextMenu. The shouldSetModalVisibility parameter is dropped as the global modal system manages visibility independently. Made-with: Cursor --- .../ConfirmDeleteReportActionModal.tsx | 141 +++++++++++ .../PopoverReportActionContextMenu.tsx | 234 +++++------------- 2 files changed, 206 insertions(+), 169 deletions(-) create mode 100644 src/pages/inbox/report/ContextMenu/ConfirmDeleteReportActionModal.tsx diff --git a/src/pages/inbox/report/ContextMenu/ConfirmDeleteReportActionModal.tsx b/src/pages/inbox/report/ContextMenu/ConfirmDeleteReportActionModal.tsx new file mode 100644 index 000000000000..b555df1e1b61 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/ConfirmDeleteReportActionModal.tsx @@ -0,0 +1,141 @@ +import React, {useEffect, useRef, useState} from 'react'; +import {DeviceEventEmitter, InteractionManager} from 'react-native'; +import ConfirmModal from '@components/ConfirmModal'; +import type {ModalProps} from '@components/Modal/Global/ModalContext'; +import {ModalActions} from '@components/Modal/Global/ModalContext'; +import {useSearchStateContext} from '@components/Search/SearchContext'; +import useAncestors from '@hooks/useAncestors'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useDeleteTransactions from '@hooks/useDeleteTransactions'; +import useDuplicateTransactionsAndViolations from '@hooks/useDuplicateTransactionsAndViolations'; +import useGetIOUReportFromReportAction from '@hooks/useGetIOUReportFromReportAction'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import useReportIsArchived from '@hooks/useReportIsArchived'; +import useTransactionsAndViolationsForReport from '@hooks/useTransactionsAndViolationsForReport'; +import {deleteTrackExpense} from '@libs/actions/IOU'; +import {deleteAppReport, deleteReportComment} from '@libs/actions/Report'; +import {getOriginalMessage, isMoneyRequestAction, isReportPreviewAction, isTrackExpenseAction} from '@libs/ReportActionsUtils'; +import {getOriginalReportID} from '@libs/ReportUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; + +type ConfirmDeleteReportActionModalProps = ModalProps & { + reportID: string; + reportActionID: string; +}; + +function ConfirmDeleteReportActionModal({closeModal, reportID, reportActionID}: ConfirmDeleteReportActionModalProps) { + const {translate} = useLocalize(); + const {email, accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); + const {currentSearchHash} = useSearchStateContext(); + + const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`); + const reportAction = reportActions?.[reportActionID]; + + const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); + const [childReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportAction?.childReportID}`); + const [selfDMReportID] = useOnyx(ONYXKEYS.SELF_DM_REPORT_ID); + const [selfDMReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${selfDMReportID}`); + const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`); + const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); + const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); + const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS); + + const isReportArchived = useReportIsArchived(reportID); + const [originalReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getOriginalReportID(reportID, reportAction, reportActions)}`); + const isOriginalReportArchived = useReportIsArchived(getOriginalReportID(reportID, reportAction, reportActions)); + const {iouReport, chatReport, isChatIOUReportArchived} = useGetIOUReportFromReportAction(reportAction); + + const transactionIDs: string[] = []; + if (isMoneyRequestAction(reportAction)) { + const originalMessage = getOriginalMessage(reportAction); + if (originalMessage && 'IOUTransactionID' in originalMessage && !!originalMessage.IOUTransactionID) { + transactionIDs.push(originalMessage.IOUTransactionID); + } + } + + const {duplicateTransactions, duplicateTransactionViolations} = useDuplicateTransactionsAndViolations(transactionIDs); + const {deleteTransactions} = useDeleteTransactions({ + report, + reportActions: reportAction ? [reportAction] : [], + policy, + }); + + const ancestorsRef = useRef>([]); + const ancestors = useAncestors(originalReport); + const {transactions: reportTransactions} = useTransactionsAndViolationsForReport(originalReport?.iouReportID); + + useEffect(() => { + if (!originalReport) { + return; + } + ancestorsRef.current = ancestors; + }, [originalReport, ancestors]); + + const [isVisible, setIsVisible] = useState(true); + const [closeAction, setCloseAction] = useState(ModalActions.CLOSE); + + const handleConfirm = () => { + if (isMoneyRequestAction(reportAction)) { + const originalMessage = getOriginalMessage(reportAction); + if (isTrackExpenseAction(reportAction)) { + deleteTrackExpense({ + chatReportID: reportID, + chatReport: report, + transactionID: originalMessage?.IOUTransactionID, + reportAction, + iouReport, + chatIOUReport: chatReport, + transactions: duplicateTransactions, + violations: duplicateTransactionViolations, + isSingleTransactionView: undefined, + isChatReportArchived: isReportArchived, + isChatIOUReportArchived, + allTransactionViolationsParam: allTransactionViolations, + currentUserAccountID, + }); + } else if (originalMessage?.IOUTransactionID) { + deleteTransactions([originalMessage.IOUTransactionID], duplicateTransactions, duplicateTransactionViolations, currentSearchHash); + } + } else if (isReportPreviewAction(reportAction)) { + deleteAppReport(childReport, selfDMReport, email ?? '', currentUserAccountID, reportTransactions, allTransactionViolations, bankAccountList, currentSearchHash); + } else if (reportAction) { + // eslint-disable-next-line @typescript-eslint/no-deprecated + InteractionManager.runAfterInteractions(() => { + deleteReportComment(report, reportAction, ancestorsRef.current, isReportArchived, isOriginalReportArchived, email ?? '', visibleReportActionsData ?? undefined); + }); + } + + DeviceEventEmitter.emit(`deletedReportAction_${reportID}`, reportAction?.reportActionID); + setCloseAction(ModalActions.CONFIRM); + setIsVisible(false); + }; + + const handleCancel = () => { + setCloseAction(ModalActions.CLOSE); + setIsVisible(false); + }; + + const handleModalHide = () => { + if (isVisible) { + return; + } + closeModal({action: closeAction}); + }; + + return ( + + ); +} + +export default ConfirmDeleteReportActionModal; diff --git a/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx index d8666c9cf83d..67fcea8394dd 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -2,31 +2,17 @@ import type {ForwardedRef} from 'react'; import React, {useEffect, useImperativeHandle, useRef, useState} from 'react'; /* eslint-disable no-restricted-imports */ import type {EmitterSubscription, GestureResponderEvent, NativeTouchEvent, View} from 'react-native'; -import {DeviceEventEmitter, Dimensions, InteractionManager} from 'react-native'; +import {Dimensions} from 'react-native'; import {Actions, useActionSheetAwareScrollViewActions} from '@components/ActionSheetAwareScrollView'; -import ConfirmModal from '@components/ConfirmModal'; +import {ModalActions, useModal} from '@components/Modal/Global/ModalContext'; import PopoverWithMeasuredContent from '@components/PopoverWithMeasuredContent'; -import {useSearchStateContext} from '@components/Search/SearchContext'; -import useAncestors from '@hooks/useAncestors'; -import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; -import useDeleteTransactions from '@hooks/useDeleteTransactions'; -import useDuplicateTransactionsAndViolations from '@hooks/useDuplicateTransactionsAndViolations'; -import useGetIOUReportFromReportAction from '@hooks/useGetIOUReportFromReportAction'; -import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; -import useReportIsArchived from '@hooks/useReportIsArchived'; -import useTransactionsAndViolationsForReport from '@hooks/useTransactionsAndViolationsForReport'; -import {deleteTrackExpense} from '@libs/actions/IOU'; -import {deleteAppReport, deleteReportComment} from '@libs/actions/Report'; import calculateAnchorPosition from '@libs/calculateAnchorPosition'; import refocusComposerAfterPreventFirstResponder from '@libs/refocusComposerAfterPreventFirstResponder'; import type {ComposerType} from '@libs/ReportActionComposeFocusManager'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import {getOriginalMessage, isMoneyRequestAction, isReportPreviewAction, isTrackExpenseAction} from '@libs/ReportActionsUtils'; -import {getOriginalReportID} from '@libs/ReportUtils'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import BaseReportActionContextMenu from './BaseReportActionContextMenu'; +import ConfirmDeleteReportActionModal from './ConfirmDeleteReportActionModal'; import type {ContextMenuAction} from './ContextMenuActions'; import type {ContextMenuAnchor, ContextMenuType, ReportActionContextMenu} from './ReportActionContextMenu'; @@ -70,32 +56,18 @@ type PopoverReportActionContextMenuProps = { }; function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuProps) { - const {translate} = useLocalize(); const {transitionActionSheetState} = useActionSheetAwareScrollViewActions(); + const modalContext = useModal(); const [menuState, setMenuState] = useState(null); const [isPopoverVisible, setIsPopoverVisible] = useState(false); const [isContextMenuOpening, setIsContextMenuOpening] = useState(false); const [composerToRefocusOnClose, setComposerToRefocusOnClose] = useState(); - const reportID = menuState?.reportID; const reportActionID = menuState?.reportActionID; - const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`); - const reportAction = reportActions?.[reportActionID ?? '']; - - const isReportArchived = useReportIsArchived(reportID); - const isOriginalReportArchived = useReportIsArchived(getOriginalReportID(reportID, reportAction, reportActions)); - const {iouReport, chatReport, isChatIOUReportArchived} = useGetIOUReportFromReportAction(reportAction); - const cursorRelativePosition = useRef({horizontal: 0, vertical: 0}); const instanceIDRef = useRef(''); - const {email, accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); - - const [isDeleteCommentConfirmModalVisible, setIsDeleteCommentConfirmModalVisible] = useState(false); - const [shouldSetModalVisibilityForDeleteConfirmation, setShouldSetModalVisibilityForDeleteConfirmation] = useState(true); - const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); - const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS); const contentRef = useRef(null); const anchorRef = useRef(null); @@ -104,10 +76,7 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro const onPopoverShow = useRef(() => {}); const onPopoverHide = useRef(() => {}); - const onCancelDeleteModal = useRef(() => {}); - const onConfirmDeleteModal = useRef(() => {}); const onPopoverHideActionCallback = useRef(() => {}); - const callbackWhenDeleteModalHide = useRef(() => {}); /** Get the Context menu anchor position. We calculate the anchor coordinates from measureInWindow async method */ const getContextMenuMeasuredLocation = () => @@ -284,7 +253,6 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro /** * Hide the ReportActionContextMenu modal popover. - * @param onHideActionCallback Callback to be called after popover is completely hidden */ const hideContextMenu: ReportActionContextMenu['hideContextMenu'] = (hideContextMenuParams) => { const {callbacks = {}} = hideContextMenuParams ?? {}; @@ -304,84 +272,15 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro }); }; - const transactionIDs: string[] = []; - if (isMoneyRequestAction(reportAction)) { - const originalMessage = getOriginalMessage(reportAction); - if (originalMessage && 'IOUTransactionID' in originalMessage && !!originalMessage.IOUTransactionID) { - transactionIDs.push(originalMessage.IOUTransactionID); - } - } - - const {duplicateTransactions, duplicateTransactionViolations} = useDuplicateTransactionsAndViolations(transactionIDs); - const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); - const [childReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportAction?.childReportID}`); - const [selfDMReportID] = useOnyx(ONYXKEYS.SELF_DM_REPORT_ID); - const [selfDMReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${selfDMReportID}`); - const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`); - const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); - const {currentSearchHash} = useSearchStateContext(); - const {deleteTransactions} = useDeleteTransactions({ - report, - reportActions: reportAction ? [reportAction] : [], - policy, - }); - - const [originalReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getOriginalReportID(reportID, reportAction, reportActions)}`); - const ancestorsRef = useRef([]); - const ancestors = useAncestors(originalReport); - const {transactions: reportTransactions} = useTransactionsAndViolationsForReport(originalReport?.iouReportID); - useEffect(() => { - if (!originalReport) { - return; - } - ancestorsRef.current = ancestors; - }, [originalReport, ancestors]); - const confirmDeleteAndHideModal = () => { - callbackWhenDeleteModalHide.current = runAndResetCallback(onConfirmDeleteModal.current); - if (isMoneyRequestAction(reportAction)) { - const originalMessage = getOriginalMessage(reportAction); - if (isTrackExpenseAction(reportAction)) { - deleteTrackExpense({ - chatReportID: reportID, - chatReport: report, - transactionID: originalMessage?.IOUTransactionID, - reportAction, - iouReport, - chatIOUReport: chatReport, - transactions: duplicateTransactions, - violations: duplicateTransactionViolations, - isSingleTransactionView: undefined, - isChatReportArchived: isReportArchived, - isChatIOUReportArchived, - allTransactionViolationsParam: allTransactionViolations, - currentUserAccountID, - }); - } else if (originalMessage?.IOUTransactionID) { - deleteTransactions([originalMessage.IOUTransactionID], duplicateTransactions, duplicateTransactionViolations, currentSearchHash); - } - } else if (isReportPreviewAction(reportAction)) { - deleteAppReport(childReport, selfDMReport, email ?? '', currentUserAccountID, reportTransactions, allTransactionViolations, bankAccountList, currentSearchHash); - } else if (reportAction) { - // eslint-disable-next-line @typescript-eslint/no-deprecated - InteractionManager.runAfterInteractions(() => { - deleteReportComment(report, reportAction, ancestorsRef.current, isReportArchived, isOriginalReportArchived, email ?? '', visibleReportActionsData ?? undefined); - }); - } - - DeviceEventEmitter.emit(`deletedReportAction_${reportID}`, reportAction?.reportActionID); - setIsDeleteCommentConfirmModalVisible(false); - }; - const hideDeleteModal = () => { - callbackWhenDeleteModalHide.current = () => (onCancelDeleteModal.current = runAndResetCallback(onCancelDeleteModal.current)); - setIsDeleteCommentConfirmModalVisible(false); - setShouldSetModalVisibilityForDeleteConfirmation(true); + // No-op: delete modal lifecycle is managed by the global modal system }; - /** Opens the Confirm delete action modal */ - const showDeleteModal: ReportActionContextMenu['showDeleteModal'] = (showReportID, showReportAction, shouldSetModalVisibility = true, onConfirm = () => {}, onCancel = () => {}) => { - onCancelDeleteModal.current = onCancel; - onConfirmDeleteModal.current = onConfirm; + /** Opens the Confirm delete action modal via the global modal system */ + const showDeleteModal: ReportActionContextMenu['showDeleteModal'] = (showReportID, showReportAction, _shouldSetModalVisibility, onConfirm = () => {}, onCancel = () => {}) => { + if (!showReportID || !showReportAction?.reportActionID) { + return; + } setMenuState((prev) => ({ ...(prev ?? { @@ -401,12 +300,26 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro onEmojiPickerToggle: undefined, }), reportID: showReportID, - reportActionID: showReportAction?.reportActionID, + reportActionID: showReportAction.reportActionID, originalReportID: prev?.originalReportID, })); - setShouldSetModalVisibilityForDeleteConfirmation(shouldSetModalVisibility); - setIsDeleteCommentConfirmModalVisible(true); + modalContext + .showModal({ + component: ConfirmDeleteReportActionModal, + props: { + reportID: showReportID, + reportActionID: showReportAction.reportActionID, + }, + }) + .then((result) => { + if (result.action === ModalActions.CONFIRM) { + onConfirm(); + } else { + onCancel(); + } + clearActiveReportAction(); + }); }; useImperativeHandle(ref, () => ({ @@ -424,63 +337,46 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro })); return ( - <> - hideContextMenu()} + onModalShow={runAndResetOnPopoverShow} + onModalHide={runAndResetOnPopoverHide} + anchorPosition={{ + horizontal: menuState?.position.anchorHorizontal ?? 0, + vertical: menuState?.position.anchorVertical ?? 0, + }} + animationIn="fadeIn" + disableAnimation={false} + shouldSetModalVisibility={false} + fullscreen + withoutOverlay={menuState?.withoutOverlay ?? true} + anchorDimensions={{ + width: menuState?.position.anchorWidth ?? 0, + height: menuState?.position.anchorHeight ?? 0, + }} + anchorRef={anchorRef} + shouldSwitchPositionIfOverflow={menuState?.isOverflowMenu ?? false} + > + hideContextMenu()} - onModalShow={runAndResetOnPopoverShow} - onModalHide={runAndResetOnPopoverHide} - anchorPosition={{ - horizontal: menuState?.position.anchorHorizontal ?? 0, - vertical: menuState?.position.anchorVertical ?? 0, - }} - animationIn="fadeIn" - disableAnimation={false} - shouldSetModalVisibility={false} - fullscreen - withoutOverlay={menuState?.withoutOverlay ?? true} - anchorDimensions={{ - width: menuState?.position.anchorWidth ?? 0, - height: menuState?.position.anchorHeight ?? 0, - }} - anchorRef={anchorRef} - shouldSwitchPositionIfOverflow={menuState?.isOverflowMenu ?? false} - > - - - { - clearActiveReportAction(); - callbackWhenDeleteModalHide.current(); - }} - prompt={translate('reportActionContextMenu.deleteConfirmation', {action: reportAction})} - confirmText={translate('common.delete')} - cancelText={translate('common.cancel')} - danger + type={menuState?.type} + reportID={menuState?.reportID} + reportActionID={menuState?.reportActionID} + draftMessage={menuState?.draftMessage} + selection={menuState?.selection ?? ''} + isArchivedRoom={menuState?.isArchivedRoom ?? false} + isChronosReport={menuState?.isChronos ?? false} + isPinnedChat={menuState?.isPinnedChat ?? false} + isUnreadChat={menuState?.isUnreadChat ?? false} + isThreadReportParentAction={menuState?.isThreadReportParentAction ?? false} + anchor={{current: menuState?.contextMenuTargetNode ?? null}} + contentRef={contentRef} + originalReportID={menuState?.originalReportID} + disabledActions={menuState?.disabledActions ?? []} + setIsEmojiPickerActive={menuState?.onEmojiPickerToggle} /> - + ); } From 183c3eace9ed77b8e8462826ed4cbbcbbec56adf Mon Sep 17 00:00:00 2001 From: rory Date: Sat, 28 Feb 2026 00:38:05 -0800 Subject: [PATCH 04/88] feat(contextmenu): hoist MiniReportActionContextMenu to singleton Create MiniContextMenuProvider with split contexts (Actions/State) to avoid unnecessary re-renders in list items. The provider manages show/hide with 120ms delay, shouldKeepOpen/pendingHide for emoji picker flow, and stable action references via useState lazy init. Rewrite MiniReportActionContextMenu as an animated singleton rendered via createPortal to document.body for reliable position:fixed. Uses Reanimated shared values for animated row-to-row slides with overshoot easing, and CSS transitions for opacity fade. PureReportActionItem now measures its row via getBoundingClientRect on hover and calls showMiniContextMenu instead of rendering its own MiniReportActionContextMenu instance. This eliminates ~1100 Onyx subscriptions (24 per visible item). Made-with: Cursor --- src/pages/inbox/ReportScreen.tsx | 88 ++++++------ .../ContextMenu/MiniContextMenuProvider.tsx | 130 ++++++++++++++++++ .../MiniReportActionContextMenu/index.tsx | 98 ++++++++++--- .../inbox/report/PureReportActionItem.tsx | 48 ++++--- 4 files changed, 287 insertions(+), 77 deletions(-) create mode 100644 src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx diff --git a/src/pages/inbox/ReportScreen.tsx b/src/pages/inbox/ReportScreen.tsx index b6b968974789..d5d5d7f2578a 100644 --- a/src/pages/inbox/ReportScreen.tsx +++ b/src/pages/inbox/ReportScreen.tsx @@ -106,6 +106,8 @@ import DeleteTransactionNavigateBackHandler from './DeleteTransactionNavigateBac import HeaderView from './HeaderView'; import useReportWasDeleted from './hooks/useReportWasDeleted'; import ReactionListWrapper from './ReactionListWrapper'; +import {MiniContextMenuProvider} from './report/ContextMenu/MiniContextMenuProvider'; +import MiniReportActionContextMenu from './report/ContextMenu/MiniReportActionContextMenu'; import ReportActionsView from './report/ReportActionsView'; import ReportFooter from './report/ReportFooter'; import {ActionListContext} from './ReportScreenContext'; @@ -1024,48 +1026,50 @@ function ReportScreen({route, navigation, isInSidePanel = false}: ReportScreenPr )} - - {(!report || shouldWaitForTransactions) && } - {!!report && !shouldDisplayMoneyRequestActionsList && !shouldWaitForTransactions ? ( - - ) : null} - {!!report && shouldDisplayMoneyRequestActionsList && !shouldWaitForTransactions ? ( - - ) : null} - {isCurrentReportLoadedFromOnyx ? ( - - ) : null} - + + + + {(!report || shouldWaitForTransactions) && } + {!!report && !shouldDisplayMoneyRequestActionsList && !shouldWaitForTransactions ? ( + + ) : null} + {!!report && shouldDisplayMoneyRequestActionsList && !shouldWaitForTransactions ? ( + + ) : null} + {isCurrentReportLoadedFromOnyx ? ( + + ) : null} + + diff --git a/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx b/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx new file mode 100644 index 000000000000..4509b144ad7b --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx @@ -0,0 +1,130 @@ +import type {ReactNode, RefObject} from 'react'; +import React, {createContext, useContext, useRef, useState} from 'react'; +import type {ContextMenuAction} from './ContextMenuActions'; +import type {ContextMenuAnchor} from './ReportActionContextMenu'; + +const HIDE_DELAY_MS = 120; + +type RowMeasurements = { + top: number; + height: number; + right: number; +}; + +type MiniContextMenuParams = { + reportID: string; + reportActionID: string; + originalReportID: string; + anchor: RefObject; + displayAsGroup: boolean; + isArchivedRoom: boolean; + isThreadReportParentAction: boolean; + draftMessage: string | undefined; + isChronosReport: boolean; + disabledActions: ContextMenuAction[]; + checkIfContextMenuActive: () => void; + setIsEmojiPickerActive: (state: boolean) => void; + rowMeasurements: RowMeasurements; +}; + +type MiniContextMenuState = MiniContextMenuParams & { + isVisible: boolean; +}; + +type MiniContextMenuActions = { + showMiniContextMenu: (params: MiniContextMenuParams) => void; + hideMiniContextMenu: () => void; + cancelHide: () => void; + keepOpen: () => void; + release: () => void; +}; + +const MiniContextMenuActionsContext = createContext({ + showMiniContextMenu: () => {}, + hideMiniContextMenu: () => {}, + cancelHide: () => {}, + keepOpen: () => {}, + release: () => {}, +}); + +const MiniContextMenuStateContext = createContext(null); + +type MiniContextMenuProviderProps = { + children: ReactNode; +}; + +function MiniContextMenuProvider({children}: MiniContextMenuProviderProps) { + const [state, setState] = useState(null); + const hideTimerRef = useRef | null>(null); + const shouldKeepOpenRef = useRef(false); + const pendingHideRef = useRef(false); + + const [actions] = useState(() => { + const clearHideTimer = () => { + if (hideTimerRef.current === null) { + return; + } + clearTimeout(hideTimerRef.current); + hideTimerRef.current = null; + }; + + const performHide = () => { + clearHideTimer(); + setState((prev) => { + if (!prev) { + return null; + } + return {...prev, isVisible: false}; + }); + }; + + return { + showMiniContextMenu: (params: MiniContextMenuParams) => { + clearHideTimer(); + pendingHideRef.current = false; + setState({...params, isVisible: true}); + }, + hideMiniContextMenu: () => { + if (shouldKeepOpenRef.current) { + pendingHideRef.current = true; + return; + } + clearHideTimer(); + hideTimerRef.current = setTimeout(performHide, HIDE_DELAY_MS); + }, + cancelHide: () => { + clearHideTimer(); + pendingHideRef.current = false; + }, + keepOpen: () => { + shouldKeepOpenRef.current = true; + clearHideTimer(); + pendingHideRef.current = false; + }, + release: () => { + shouldKeepOpenRef.current = false; + if (pendingHideRef.current) { + pendingHideRef.current = false; + performHide(); + } + }, + }; + }); + + return ( + + {children} + + ); +} + +function useMiniContextMenuActions(): MiniContextMenuActions { + return useContext(MiniContextMenuActionsContext); +} + +function useMiniContextMenuState(): MiniContextMenuState | null { + return useContext(MiniContextMenuStateContext); +} + +export {MiniContextMenuProvider, useMiniContextMenuActions, useMiniContextMenuState, MiniContextMenuActionsContext, MiniContextMenuStateContext}; +export type {MiniContextMenuParams, MiniContextMenuState, RowMeasurements, MiniContextMenuActions}; diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index 8c5ea5ad8581..bf986a39e9bc 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -1,24 +1,88 @@ -import React from 'react'; -import {View} from 'react-native'; -import useStyleUtils from '@hooks/useStyleUtils'; +import React, {useEffect, useRef} from 'react'; +import {createPortal} from 'react-dom'; +import Animated, {Easing, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import BaseReportActionContextMenu from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; -import CONST from '@src/CONST'; -import type MiniReportActionContextMenuProps from './types'; +import {useMiniContextMenuActions, useMiniContextMenuState} from '@pages/inbox/report/ContextMenu/MiniContextMenuProvider'; -function MiniReportActionContextMenu({displayAsGroup = false, ...rest}: MiniReportActionContextMenuProps) { - const StyleUtils = useStyleUtils(); +const SLIDE_DURATION = 200; +const OVERSHOOT_EASING = Easing.bezier(0.34, 1.56, 0.64, 1); - return ( - { + if (!state) { + return; + } + + if (state.isVisible) { + const targetY = state.rowMeasurements.top + (state.displayAsGroup ? -8 : -4); + const targetRight = window.innerWidth - state.rowMeasurements.right + 4; + + if (wasVisibleRef.current) { + baseTop.value = withTiming(targetY, {duration: SLIDE_DURATION, easing: OVERSHOOT_EASING}); + baseRight.value = withTiming(targetRight, {duration: SLIDE_DURATION}); + } else { + baseTop.value = targetY; + baseRight.value = targetRight; + } + } + wasVisibleRef.current = state.isVisible; + }); + + const positionStyle = useAnimatedStyle(() => ({ + top: baseTop.value, + right: baseRight.value, + })); + + if (!state) { + return null; + } + + return createPortal( + // eslint-disable-next-line jsx-a11y/mouse-events-have-key-events +
- - + + + +
, + document.body, ); } diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx index 44e5316191b0..ee510af435b0 100644 --- a/src/pages/inbox/report/PureReportActionItem.tsx +++ b/src/pages/inbox/report/PureReportActionItem.tsx @@ -248,7 +248,7 @@ import type {Errors} from '@src/types/onyx/OnyxCommon'; import type {JoinWorkspaceResolution} from '@src/types/onyx/OriginalMessage'; import {isEmptyObject, isEmptyValueObject} from '@src/types/utils/EmptyObject'; import {RestrictedReadOnlyContextMenuActions} from './ContextMenu/ContextMenuActions'; -import MiniReportActionContextMenu from './ContextMenu/MiniReportActionContextMenu'; +import {useMiniContextMenuActions} from './ContextMenu/MiniContextMenuProvider'; import type {ContextMenuAnchor} from './ContextMenu/ReportActionContextMenu'; import {hideContextMenu, hideDeleteModal, isActiveReportAction, showContextMenu} from './ContextMenu/ReportActionContextMenu'; import LinkPreviewer from './LinkPreviewer'; @@ -558,6 +558,7 @@ function PureReportActionItem({ const {transitionActionSheetState} = ActionSheetAwareScrollView.useActionSheetAwareScrollViewActions(); const {translate, formatPhoneNumber, localeCompare, formatTravelDate, getLocalDateFromDatetime, datetimeToCalendarTime} = useLocalize(); const {showConfirmModal} = useConfirmModal(); + const {showMiniContextMenu, hideMiniContextMenu} = useMiniContextMenuActions(); const personalDetail = useCurrentUserPersonalDetails(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const reportID = report?.reportID ?? action?.reportID; @@ -2058,31 +2059,42 @@ function PureReportActionItem({ shouldFreezeCapture={isPaymentMethodPopoverActive} onHoverIn={() => { setIsReportActionActive(false); + if (!shouldDisplayContextMenu || draftMessage !== undefined || hasErrors) { + return; + } + const node = popoverAnchorRef.current; + if (!node || !('getBoundingClientRect' in node)) { + return; + } + const rect = node.getBoundingClientRect(); + showMiniContextMenu({ + reportID: reportID ?? '', + reportActionID: action.reportActionID, + originalReportID: originalReportID ?? '', + anchor: popoverAnchorRef, + displayAsGroup: !!displayAsGroup, + isArchivedRoom: !!isArchivedRoom, + isThreadReportParentAction: !!isThreadReportParentAction, + draftMessage, + isChronosReport: !!isChronosReport, + disabledActions, + checkIfContextMenuActive: toggleContextMenuFromActiveReportAction, + setIsEmojiPickerActive, + rowMeasurements: { + top: rect.top, + height: rect.height, + right: rect.right, + }, + }); }} onHoverOut={() => { setIsReportActionActive(!!isReportActionLinked); + hideMiniContextMenu(); }} > {(hovered) => ( {shouldDisplayNewMarker && (!shouldUseThreadDividerLine || !isFirstVisibleReportAction) && } - {shouldDisplayContextMenu && ( - - )} Date: Sat, 28 Feb 2026 00:39:39 -0800 Subject: [PATCH 05/88] refactor(contextmenu): delegate shouldKeepOpen to provider for mini mode In mini mode, BaseReportActionContextMenu now uses the provider's keepOpen()/release() API for emoji picker and overflow menu flows. Non-mini (popover) mode retains local state. This ensures the singleton stays visible when submenus are open. Made-with: Cursor --- .../BaseReportActionContextMenu.tsx | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx index 11826c3f1318..0ef94436308d 100755 --- a/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx +++ b/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx @@ -44,6 +44,7 @@ import type {OriginalMessageIOU, ReportAction} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {ContextMenuAction, ContextMenuActionPayload} from './ContextMenuActions'; import ContextMenuActions from './ContextMenuActions'; +import {useMiniContextMenuActions} from './MiniContextMenuProvider'; import type {ContextMenuAnchor, ContextMenuType} from './ReportActionContextMenu'; import {hideContextMenu, showContextMenu} from './ReportActionContextMenu'; @@ -155,7 +156,9 @@ function BaseReportActionContextMenu({ const {translate, getLocalDateFromDatetime} = useLocalize(); // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); - const [shouldKeepOpen, setShouldKeepOpen] = useState(false); + const [localShouldKeepOpen, setLocalShouldKeepOpen] = useState(false); + const miniActions = useMiniContextMenuActions(); + const shouldKeepOpen = isMini ? false : localShouldKeepOpen; const wrapperStyle = StyleUtils.getReportActionContextMenuStyles(isMini, shouldUseNarrowLayout); const {isOffline} = useNetwork(); const {isProduction} = useEnvironment(); @@ -342,7 +345,11 @@ function BaseReportActionContextMenu({ onShow: checkIfContextMenuActive, onHide: () => { checkIfContextMenuActive?.(); - setShouldKeepOpen(false); + if (isMini) { + miniActions.release(); + } else { + setLocalShouldKeepOpen(false); + } }, }, disabledOptions: filteredContextMenuActions, @@ -372,9 +379,21 @@ function BaseReportActionContextMenu({ report, draftMessage, selection, - close: () => setShouldKeepOpen(false), + close: () => { + if (isMini) { + miniActions.release(); + } else { + setLocalShouldKeepOpen(false); + } + }, transitionActionSheetState, - openContextMenu: () => setShouldKeepOpen(true), + openContextMenu: () => { + if (isMini) { + miniActions.keepOpen(); + } else { + setLocalShouldKeepOpen(true); + } + }, interceptAnonymousUser, openOverflowMenu, setIsEmojiPickerActive, From 826f62e4ee6093fc4e4ac663eb43d046655769ff Mon Sep 17 00:00:00 2001 From: rory Date: Sat, 28 Feb 2026 00:40:35 -0800 Subject: [PATCH 06/88] feat(contextmenu): hide mini menu on scroll Add a scroll event listener (capture phase) to dismiss the mini context menu when the list scrolls. The menu reappears at the correct position on the next hover via fresh getBoundingClientRect. Made-with: Cursor --- .../MiniReportActionContextMenu/index.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index bf986a39e9bc..f4d6944ca0a0 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -37,6 +37,19 @@ function MiniReportActionContextMenu() { wasVisibleRef.current = state.isVisible; }); + useEffect(() => { + if (!isVisible) { + return; + } + const handleScroll = () => { + hideMiniContextMenu(); + }; + window.addEventListener('scroll', handleScroll, true); + return () => { + window.removeEventListener('scroll', handleScroll, true); + }; + }, [isVisible, hideMiniContextMenu]); + const positionStyle = useAnimatedStyle(() => ({ top: baseTop.value, right: baseRight.value, From 2d2fbc52d75d87b6e28d6c5de69249f2039bc5b1 Mon Sep 17 00:00:00 2001 From: rory Date: Sat, 28 Feb 2026 00:50:17 -0800 Subject: [PATCH 07/88] refactor(contextmenu): add composition infrastructure Add ContextMenuPayloadProvider (shared context for all action components), ContextMenuLayout (visibility evaluation, mini-mode truncation, arrow key focus), and actionConfig (shouldShow registry with ordered action IDs). Foundation for converting the config array into individual dot-notation components. Made-with: Cursor --- .../report/ContextMenu/ContextMenuLayout.tsx | 121 +++++++ .../ContextMenuPayloadProvider.tsx | 89 +++++ .../ContextMenu/actions/actionConfig.ts | 309 ++++++++++++++++++ 3 files changed, 519 insertions(+) create mode 100644 src/pages/inbox/report/ContextMenu/ContextMenuLayout.tsx create mode 100644 src/pages/inbox/report/ContextMenu/ContextMenuPayloadProvider.tsx create mode 100644 src/pages/inbox/report/ContextMenu/actions/actionConfig.ts diff --git a/src/pages/inbox/report/ContextMenu/ContextMenuLayout.tsx b/src/pages/inbox/report/ContextMenu/ContextMenuLayout.tsx new file mode 100644 index 000000000000..bdc993b42920 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/ContextMenuLayout.tsx @@ -0,0 +1,121 @@ +import type {ReactNode} from 'react'; +import {createContext, useContext} from 'react'; +import {View} from 'react-native'; +import FocusTrapForModal from '@components/FocusTrap/FocusTrapForModal'; +import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useStyleUtils from '@hooks/useStyleUtils'; +import CONST from '@src/CONST'; +import type {ActionId} from './actions/actionConfig'; +import {ORDERED_ACTION_SHOULD_SHOW} from './actions/actionConfig'; +import {useContextMenuPayload} from './ContextMenuPayloadProvider'; + +type ContextMenuVisibilityContextValue = { + visibleActionIds: ActionId[]; + focusedIndex: number; + setFocusedIndex: (index: number) => void; +}; + +const ContextMenuVisibilityContext = createContext({ + visibleActionIds: [], + focusedIndex: -1, + setFocusedIndex: () => {}, +}); + +function useContextMenuVisibility(): ContextMenuVisibilityContextValue { + return useContext(ContextMenuVisibilityContext); +} + +type ContextMenuLayoutProps = { + isMini: boolean; + isVisible: boolean; + shouldKeepOpen: boolean; + contentRef?: React.RefObject; + children: ReactNode; +}; + +function ContextMenuLayout({isMini, isVisible, shouldKeepOpen, contentRef, children}: ContextMenuLayoutProps) { + const StyleUtils = useStyleUtils(); + // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth + const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); + const payload = useContextMenuPayload(); + + const shouldShowArgs = { + type: payload.type, + reportAction: payload.reportAction, + childReportActions: payload.childReportActions, + isArchivedRoom: payload.isArchivedRoom, + betas: payload.betas, + menuTarget: payload.anchor, + isChronosReport: payload.isChronosReport, + reportID: payload.reportID, + isPinnedChat: payload.isPinnedChat, + isUnreadChat: payload.isUnreadChat, + isThreadReportParentAction: payload.isThreadReportParentAction, + isOffline: payload.isOffline, + isMini, + isProduction: payload.isProduction, + moneyRequestAction: payload.moneyRequestAction, + areHoldRequirementsMet: payload.areHoldRequirementsMet, + isDebugModeEnabled: payload.isDebugModeEnabled, + iouTransaction: payload.iouTransaction, + transactions: payload.transactions, + moneyRequestReport: payload.moneyRequestReport, + moneyRequestPolicy: payload.moneyRequestPolicy, + isHarvestReport: payload.isHarvestReport, + }; + + let visibleActionIds = ORDERED_ACTION_SHOULD_SHOW.filter((entry) => !payload.disabledActionIds.has(entry.id) && entry.shouldShow(shouldShowArgs)).map((entry) => entry.id); + + if (isMini) { + const overflowMenuId = visibleActionIds.at(-1); + const otherIds = visibleActionIds.slice(0, -1); + if (otherIds.length > CONST.MINI_CONTEXT_MENU_MAX_ITEMS && overflowMenuId) { + visibleActionIds = [...otherIds.slice(0, CONST.MINI_CONTEXT_MENU_MAX_ITEMS - 1), overflowMenuId]; + } else { + visibleActionIds = otherIds; + } + } + + const contentActionIndexes = visibleActionIds + .map((id, index) => { + const entry = ORDERED_ACTION_SHOULD_SHOW.find((e) => e.id === id); + return entry?.isContentAction ? index : undefined; + }) + .filter((index): index is number => index !== undefined); + + const shouldEnableArrowNavigation = !isMini && (isVisible || shouldKeepOpen); + + const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({ + initialFocusedIndex: -1, + disabledIndexes: contentActionIndexes, + maxIndex: visibleActionIds.length - 1, + isActive: shouldEnableArrowNavigation, + }); + + const wrapperStyle = StyleUtils.getReportActionContextMenuStyles(isMini, shouldUseNarrowLayout); + + const contextValue: ContextMenuVisibilityContextValue = { + visibleActionIds, + focusedIndex, + setFocusedIndex, + }; + + return ( + (isVisible || shouldKeepOpen || !isMini) && ( + + + + {children} + + + + ) + ); +} + +export default ContextMenuLayout; +export {useContextMenuVisibility}; diff --git a/src/pages/inbox/report/ContextMenu/ContextMenuPayloadProvider.tsx b/src/pages/inbox/report/ContextMenu/ContextMenuPayloadProvider.tsx new file mode 100644 index 000000000000..a55646af5d95 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/ContextMenuPayloadProvider.tsx @@ -0,0 +1,89 @@ +import type {RefObject} from 'react'; +import {createContext, useContext} from 'react'; +import type {GestureResponderEvent, View} from 'react-native'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {LocaleContextProps, LocalizedTranslate} from '@components/LocaleContextProvider'; +import type useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import type {Beta, Card, Download as DownloadOnyx, IntroSelected, Policy, PolicyTagLists, ReportAction, ReportActions, Report as ReportType, Transaction} from '@src/types/onyx'; +import type {ContextMenuAnchor, ContextMenuType} from './ReportActionContextMenu'; + +type ContextMenuPayloadContextValue = { + type: ContextMenuType; + reportID: string | undefined; + originalReportID: string | undefined; + + reportActions: OnyxEntry; + reportAction: ReportAction; + report: OnyxEntry; + originalReport: OnyxEntry; + childReport: OnyxEntry; + childReportActions: OnyxCollection; + + policy: OnyxEntry; + policyTags: OnyxEntry; + + moneyRequestAction: ReportAction | undefined; + moneyRequestReport: OnyxEntry; + moneyRequestPolicy: OnyxEntry; + iouTransaction: OnyxEntry; + transaction: OnyxEntry; + card: Card | undefined; + + currentUserAccountID: number; + currentUserPersonalDetails: ReturnType; + encryptedAuthToken: string; + + isArchivedRoom: boolean; + isChronosReport: boolean; + isPinnedChat: boolean; + isUnreadChat: boolean; + isThreadReportParentAction: boolean; + isOffline: boolean; + isMini: boolean; + isProduction: boolean; + isHarvestReport: boolean; + isTryNewDotNVPDismissed: boolean; + isDelegateAccessRestricted: boolean; + areHoldRequirementsMet: boolean; + isDebugModeEnabled: OnyxEntry; + + betas: OnyxEntry; + transactions: OnyxCollection; + introSelected: OnyxEntry; + draftMessage: string; + selection: string; + + movedFromReport: OnyxEntry; + movedToReport: OnyxEntry; + harvestReport: OnyxEntry; + + download: OnyxEntry; + + close: () => void; + transitionActionSheetState: (params: {type: string; payload?: Record}) => void; + openContextMenu: () => void; + interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; + openOverflowMenu: (event: GestureResponderEvent | MouseEvent, anchorRef: RefObject) => void; + setIsEmojiPickerActive: ((state: boolean) => void) | undefined; + showDelegateNoAccessModal: (() => void) | undefined; + + translate: LocalizedTranslate; + getLocalDateFromDatetime: LocaleContextProps['getLocalDateFromDatetime']; + + anchor: RefObject | undefined; + + disabledActionIds: Set; +}; + +const ContextMenuPayloadContext = createContext(null); + +function useContextMenuPayload(): ContextMenuPayloadContextValue { + const ctx = useContext(ContextMenuPayloadContext); + if (ctx === null) { + throw new Error('useContextMenuPayload must be used within a ContextMenuPayloadProvider'); + } + return ctx; +} + +export {ContextMenuPayloadContext, useContextMenuPayload}; +export type {ContextMenuPayloadContextValue}; diff --git a/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts b/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts new file mode 100644 index 000000000000..5adce1a794f5 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts @@ -0,0 +1,309 @@ +import type {RefObject} from 'react'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import { + getOriginalMessage, + getReportAction, + hasReasoning, + isActionableTrackExpense, + isActionOfType, + isCreatedAction, + isCreatedTaskReportAction, + isDeletedAction, + isMessageDeleted, + isMoneyRequestAction, + isReportActionAttachment, + isReportPreviewAction, + isTripPreview, + isWhisperAction, +} from '@libs/ReportActionsUtils'; +import { + canDeleteReportAction, + canEditReportAction, + canFlagReportAction, + canHoldUnholdReportAction, + getChildReportNotificationPreference, + shouldDisableThread, + shouldDisplayThreadReplies, +} from '@libs/ReportUtils'; +import CONST from '@src/CONST'; +import type {Beta, Policy, ReportAction, Report as ReportType, Transaction} from '@src/types/onyx'; +import type {ContextMenuAnchor} from '../ReportActionContextMenu'; + +const ACTION_IDS = { + EMOJI_REACTION: 'emojiReaction', + REPLY_IN_THREAD: 'replyInThread', + MARK_AS_UNREAD: 'markAsUnread', + EXPLAIN: 'explain', + MARK_AS_READ: 'markAsRead', + EDIT: 'edit', + UNHOLD: 'unhold', + HOLD: 'hold', + JOIN_THREAD: 'joinThread', + LEAVE_THREAD: 'leaveThread', + COPY_URL: 'copyUrl', + COPY_TO_CLIPBOARD: 'copyToClipboard', + COPY_EMAIL: 'copyEmail', + COPY_MESSAGE: 'copyMessage', + COPY_LINK: 'copyLink', + PIN: 'pin', + UNPIN: 'unpin', + FLAG_AS_OFFENSIVE: 'flagAsOffensive', + DOWNLOAD: 'download', + COPY_ONYX_DATA: 'copyOnyxData', + DEBUG: 'debug', + DELETE: 'delete', + OVERFLOW_MENU: 'overflowMenu', +} as const; + +type ActionId = (typeof ACTION_IDS)[keyof typeof ACTION_IDS]; + +type ShouldShowArgs = { + type: string; + reportAction: OnyxEntry; + childReportActions: OnyxCollection; + isArchivedRoom: boolean; + betas: OnyxEntry; + menuTarget: RefObject | undefined; + isChronosReport: boolean; + reportID?: string; + isPinnedChat: boolean; + isUnreadChat: boolean; + isThreadReportParentAction: boolean; + isOffline: boolean; + isMini: boolean; + isProduction: boolean; + moneyRequestAction: ReportAction | undefined; + areHoldRequirementsMet: boolean; + isDebugModeEnabled: OnyxEntry; + iouTransaction: OnyxEntry; + transactions?: OnyxCollection; + moneyRequestReport?: OnyxEntry; + moneyRequestPolicy?: OnyxEntry; + isHarvestReport?: boolean; +}; + +function getActionHtml(reportAction: OnyxEntry): string { + const message = Array.isArray(reportAction?.message) ? (reportAction?.message?.at(-1) ?? null) : (reportAction?.message ?? null); + return message?.html ?? ''; +} + +const ORDERED_ACTION_SHOULD_SHOW: Array<{id: ActionId; isContentAction: boolean; shouldShow: (args: ShouldShowArgs) => boolean}> = [ + { + id: ACTION_IDS.EMOJI_REACTION, + isContentAction: true, + shouldShow: ({type, reportAction}) => { + const isDynamicWorkflowRoutedAction = isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.DYNAMIC_EXTERNAL_WORKFLOW_ROUTED); + return type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && !!reportAction && 'message' in reportAction && !isMessageDeleted(reportAction) && !isDynamicWorkflowRoutedAction; + }, + }, + { + id: ACTION_IDS.REPLY_IN_THREAD, + isContentAction: false, + shouldShow: ({type, reportAction, reportID, isThreadReportParentAction, isArchivedRoom}) => { + if (type !== CONST.CONTEXT_MENU_TYPES.REPORT_ACTION || !reportID) { + return false; + } + return !shouldDisableThread(reportAction, isThreadReportParentAction, isArchivedRoom); + }, + }, + { + id: ACTION_IDS.MARK_AS_UNREAD, + isContentAction: false, + shouldShow: ({type, reportAction, isUnreadChat}) => { + const isDynamicWorkflowRoutedAction = isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.DYNAMIC_EXTERNAL_WORKFLOW_ROUTED); + return (type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && !isDynamicWorkflowRoutedAction) || (type === CONST.CONTEXT_MENU_TYPES.REPORT && !isUnreadChat); + }, + }, + { + id: ACTION_IDS.EXPLAIN, + isContentAction: false, + shouldShow: ({type, reportAction, isArchivedRoom}) => { + if (type !== CONST.CONTEXT_MENU_TYPES.REPORT_ACTION || isArchivedRoom || !reportAction) { + return false; + } + return hasReasoning(reportAction); + }, + }, + { + id: ACTION_IDS.MARK_AS_READ, + isContentAction: false, + shouldShow: ({type, isUnreadChat}) => type === CONST.CONTEXT_MENU_TYPES.REPORT && isUnreadChat, + }, + { + id: ACTION_IDS.EDIT, + isContentAction: false, + shouldShow: ({type, reportAction, isArchivedRoom, isChronosReport, moneyRequestAction}) => + type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && (canEditReportAction(reportAction) || canEditReportAction(moneyRequestAction)) && !isArchivedRoom && !isChronosReport, + }, + { + id: ACTION_IDS.UNHOLD, + isContentAction: false, + shouldShow: ({type, moneyRequestReport, moneyRequestAction, moneyRequestPolicy, areHoldRequirementsMet, iouTransaction}) => { + if (type !== CONST.CONTEXT_MENU_TYPES.REPORT_ACTION || !areHoldRequirementsMet) { + return false; + } + const holdReportAction = getReportAction(moneyRequestAction?.childReportID, `${iouTransaction?.comment?.hold ?? ''}`); + return canHoldUnholdReportAction(moneyRequestReport, moneyRequestAction, holdReportAction, iouTransaction, moneyRequestPolicy).canUnholdRequest; + }, + }, + { + id: ACTION_IDS.HOLD, + isContentAction: false, + shouldShow: ({type, moneyRequestReport, moneyRequestAction, moneyRequestPolicy, areHoldRequirementsMet, iouTransaction}) => { + if (type !== CONST.CONTEXT_MENU_TYPES.REPORT_ACTION || !areHoldRequirementsMet) { + return false; + } + const holdReportAction = getReportAction(moneyRequestAction?.childReportID, `${iouTransaction?.comment?.hold ?? ''}`); + return canHoldUnholdReportAction(moneyRequestReport, moneyRequestAction, holdReportAction, iouTransaction, moneyRequestPolicy).canHoldRequest; + }, + }, + { + id: ACTION_IDS.JOIN_THREAD, + isContentAction: false, + shouldShow: ({reportAction, isArchivedRoom, isThreadReportParentAction, isHarvestReport}) => { + const childReportNotificationPreference = getChildReportNotificationPreference(reportAction); + const isDeletedActionResult = isDeletedAction(reportAction); + const shouldDisplayReplies = shouldDisplayThreadReplies(reportAction, isThreadReportParentAction); + const subscribed = childReportNotificationPreference !== 'hidden'; + const isWhisper = isWhisperAction(reportAction) || isActionableTrackExpense(reportAction); + const isExpenseReportAction = isMoneyRequestAction(reportAction) || isReportPreviewAction(reportAction); + const isTaskAction = isCreatedTaskReportAction(reportAction); + const isHarvestCreatedExpenseReportAction = !!isHarvestReport && isCreatedAction(reportAction); + const shouldDisableJoin = shouldDisableThread(reportAction, isThreadReportParentAction, isArchivedRoom); + return ( + !subscribed && + !isWhisper && + !isTaskAction && + !isExpenseReportAction && + !isThreadReportParentAction && + !isHarvestCreatedExpenseReportAction && + !shouldDisableJoin && + (shouldDisplayReplies || (!isDeletedActionResult && !isArchivedRoom)) + ); + }, + }, + { + id: ACTION_IDS.LEAVE_THREAD, + isContentAction: false, + shouldShow: ({reportAction, isArchivedRoom, isThreadReportParentAction, isHarvestReport}) => { + const childReportNotificationPreference = getChildReportNotificationPreference(reportAction); + const isDeletedActionResult = isDeletedAction(reportAction); + const shouldDisplayReplies = shouldDisplayThreadReplies(reportAction, isThreadReportParentAction); + const subscribed = childReportNotificationPreference !== 'hidden'; + const isWhisper = isWhisperAction(reportAction) || isActionableTrackExpense(reportAction); + const isExpenseReportAction = isMoneyRequestAction(reportAction) || isReportPreviewAction(reportAction); + const isTaskAction = isCreatedTaskReportAction(reportAction); + const isHarvestCreatedExpenseReportAction = !!isHarvestReport && isCreatedAction(reportAction); + return ( + subscribed && + !isWhisper && + !isTaskAction && + !isExpenseReportAction && + !isThreadReportParentAction && + !isHarvestCreatedExpenseReportAction && + (shouldDisplayReplies || (!isDeletedActionResult && !isArchivedRoom)) + ); + }, + }, + { + id: ACTION_IDS.COPY_URL, + isContentAction: false, + shouldShow: ({type}) => type === CONST.CONTEXT_MENU_TYPES.LINK, + }, + { + id: ACTION_IDS.COPY_TO_CLIPBOARD, + isContentAction: false, + shouldShow: ({type}) => type === CONST.CONTEXT_MENU_TYPES.TEXT, + }, + { + id: ACTION_IDS.COPY_EMAIL, + isContentAction: false, + shouldShow: ({type}) => type === CONST.CONTEXT_MENU_TYPES.EMAIL, + }, + { + id: ACTION_IDS.COPY_MESSAGE, + isContentAction: false, + shouldShow: ({type, reportAction}) => + type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && !isReportActionAttachment(reportAction) && !isMessageDeleted(reportAction) && !isTripPreview(reportAction), + }, + { + id: ACTION_IDS.COPY_LINK, + isContentAction: false, + shouldShow: ({type, reportAction, menuTarget}) => { + const isAttachment = isReportActionAttachment(reportAction); + const isAttachmentTarget = menuTarget?.current && 'tagName' in menuTarget.current && menuTarget?.current.tagName === 'IMG' && isAttachment; + const isDynamicWorkflowRoutedAction = isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.DYNAMIC_EXTERNAL_WORKFLOW_ROUTED); + return type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && !isAttachmentTarget && !isMessageDeleted(reportAction) && !isDynamicWorkflowRoutedAction; + }, + }, + { + id: ACTION_IDS.PIN, + isContentAction: false, + shouldShow: ({type, isPinnedChat}) => type === CONST.CONTEXT_MENU_TYPES.REPORT && !isPinnedChat, + }, + { + id: ACTION_IDS.UNPIN, + isContentAction: false, + shouldShow: ({type, isPinnedChat}) => type === CONST.CONTEXT_MENU_TYPES.REPORT && isPinnedChat, + }, + { + id: ACTION_IDS.FLAG_AS_OFFENSIVE, + isContentAction: false, + shouldShow: ({type, reportAction, isArchivedRoom, isChronosReport, reportID}) => + type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && + canFlagReportAction(reportAction, reportID) && + !isArchivedRoom && + !isChronosReport && + reportAction?.actorAccountID !== CONST.ACCOUNT_ID.CONCIERGE, + }, + { + id: ACTION_IDS.DOWNLOAD, + isContentAction: false, + shouldShow: ({reportAction, isOffline}) => { + const isAttachment = isReportActionAttachment(reportAction); + const html = getActionHtml(reportAction); + const isUploading = html.includes(CONST.ATTACHMENT_OPTIMISTIC_SOURCE_ATTRIBUTE); + return isAttachment && !isUploading && !!reportAction?.reportActionID && !isMessageDeleted(reportAction) && !isOffline; + }, + }, + { + id: ACTION_IDS.COPY_ONYX_DATA, + isContentAction: false, + shouldShow: ({type, isProduction}) => type === CONST.CONTEXT_MENU_TYPES.REPORT && !isProduction, + }, + { + id: ACTION_IDS.DEBUG, + isContentAction: false, + shouldShow: ({type, isDebugModeEnabled}) => [CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, CONST.CONTEXT_MENU_TYPES.REPORT].some((value) => value === type) && !!isDebugModeEnabled, + }, + { + id: ACTION_IDS.DELETE, + isContentAction: false, + shouldShow: ({type, reportAction, isArchivedRoom, isChronosReport, reportID: reportIDParam, moneyRequestAction, iouTransaction, transactions, childReportActions}) => { + let reportID = reportIDParam; + if (isMoneyRequestAction(moneyRequestAction)) { + reportID = getOriginalMessage(moneyRequestAction)?.IOUReportID; + } else if (isReportPreviewAction(reportAction)) { + reportID = reportAction?.childReportID; + } + return ( + !!reportIDParam && + type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && + canDeleteReportAction(moneyRequestAction ?? reportAction, reportID, iouTransaction, transactions, childReportActions) && + !isArchivedRoom && + !isChronosReport && + !isMessageDeleted(reportAction) + ); + }, + }, + { + id: ACTION_IDS.OVERFLOW_MENU, + isContentAction: false, + shouldShow: ({isMini}) => isMini, + }, +]; + +const RESTRICTED_READONLY_ACTION_IDS = new Set([ACTION_IDS.REPLY_IN_THREAD, ACTION_IDS.EDIT, ACTION_IDS.JOIN_THREAD, ACTION_IDS.DELETE]); + +export {ACTION_IDS, ORDERED_ACTION_SHOULD_SHOW, RESTRICTED_READONLY_ACTION_IDS, getActionHtml}; +export type {ActionId, ShouldShowArgs}; From 183b120f9404ceddd9f12b76dbf18effb5d757d8 Mon Sep 17 00:00:00 2001 From: rory Date: Sat, 28 Feb 2026 01:11:10 -0800 Subject: [PATCH 08/88] refactor(contextmenu): wire composition architecture into Base Replace the config array .filter().map() loop in BaseReportActionContextMenu with ContextMenuPayloadProvider, ContextMenuLayout, and individual dot-notation action components. Convert disabledActions (ContextMenuAction[]) to disabledActionIds (Set) throughout the chain: PopoverReportActionContextMenu, MiniContextMenuProvider, ReportActionContextMenu, PureReportActionItem. Use Reanimated .get()/.set() API for shared values. Fix import alias in actionConfig. Made-with: Cursor --- .../BaseReportActionContextMenu.tsx | 355 ++++-------- .../ContextMenu/MiniContextMenuProvider.tsx | 7 +- .../MiniReportActionContextMenu/index.tsx | 14 +- .../PopoverReportActionContextMenu.tsx | 13 +- .../ContextMenu/ReportActionContextMenu.ts | 3 +- .../ContextMenu/actions/ContextMenuAction.ts | 51 ++ .../report/ContextMenu/actions/CopyEmail.tsx | 47 ++ .../report/ContextMenu/actions/CopyLink.tsx | 49 ++ .../ContextMenu/actions/CopyMessage.tsx | 540 ++++++++++++++++++ .../ContextMenu/actions/CopyOnyxData.tsx | 45 ++ .../ContextMenu/actions/CopyToClipboard.tsx | 45 ++ .../report/ContextMenu/actions/CopyURL.tsx | 46 ++ .../report/ContextMenu/actions/Debug.tsx | 48 ++ .../report/ContextMenu/actions/Delete.tsx | 48 ++ .../report/ContextMenu/actions/Download.tsx | 63 ++ .../inbox/report/ContextMenu/actions/Edit.tsx | 67 +++ .../ContextMenu/actions/EmojiReaction.tsx | 66 +++ .../report/ContextMenu/actions/Explain.tsx | 50 ++ .../ContextMenu/actions/FlagAsOffensive.tsx | 56 ++ .../inbox/report/ContextMenu/actions/Hold.tsx | 50 ++ .../report/ContextMenu/actions/JoinThread.tsx | 53 ++ .../ContextMenu/actions/LeaveThread.tsx | 53 ++ .../report/ContextMenu/actions/MarkAsRead.tsx | 44 ++ .../ContextMenu/actions/MarkAsUnread.tsx | 44 ++ .../ContextMenu/actions/OverflowMenu.tsx | 45 ++ .../inbox/report/ContextMenu/actions/Pin.tsx | 46 ++ .../ContextMenu/actions/ReplyInThread.tsx | 50 ++ .../report/ContextMenu/actions/Unhold.tsx | 50 ++ .../report/ContextMenu/actions/Unpin.tsx | 46 ++ .../ContextMenu/actions/actionConfig.ts | 2 +- .../inbox/report/PureReportActionItem.tsx | 16 +- 31 files changed, 1845 insertions(+), 267 deletions(-) create mode 100644 src/pages/inbox/report/ContextMenu/actions/ContextMenuAction.ts create mode 100644 src/pages/inbox/report/ContextMenu/actions/CopyEmail.tsx create mode 100644 src/pages/inbox/report/ContextMenu/actions/CopyLink.tsx create mode 100644 src/pages/inbox/report/ContextMenu/actions/CopyMessage.tsx create mode 100644 src/pages/inbox/report/ContextMenu/actions/CopyOnyxData.tsx create mode 100644 src/pages/inbox/report/ContextMenu/actions/CopyToClipboard.tsx create mode 100644 src/pages/inbox/report/ContextMenu/actions/CopyURL.tsx create mode 100644 src/pages/inbox/report/ContextMenu/actions/Debug.tsx create mode 100644 src/pages/inbox/report/ContextMenu/actions/Delete.tsx create mode 100644 src/pages/inbox/report/ContextMenu/actions/Download.tsx create mode 100644 src/pages/inbox/report/ContextMenu/actions/Edit.tsx create mode 100644 src/pages/inbox/report/ContextMenu/actions/EmojiReaction.tsx create mode 100644 src/pages/inbox/report/ContextMenu/actions/Explain.tsx create mode 100644 src/pages/inbox/report/ContextMenu/actions/FlagAsOffensive.tsx create mode 100644 src/pages/inbox/report/ContextMenu/actions/Hold.tsx create mode 100644 src/pages/inbox/report/ContextMenu/actions/JoinThread.tsx create mode 100644 src/pages/inbox/report/ContextMenu/actions/LeaveThread.tsx create mode 100644 src/pages/inbox/report/ContextMenu/actions/MarkAsRead.tsx create mode 100644 src/pages/inbox/report/ContextMenu/actions/MarkAsUnread.tsx create mode 100644 src/pages/inbox/report/ContextMenu/actions/OverflowMenu.tsx create mode 100644 src/pages/inbox/report/ContextMenu/actions/Pin.tsx create mode 100644 src/pages/inbox/report/ContextMenu/actions/ReplyInThread.tsx create mode 100644 src/pages/inbox/report/ContextMenu/actions/Unhold.tsx create mode 100644 src/pages/inbox/report/ContextMenu/actions/Unpin.tsx diff --git a/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx index 0ef94436308d..2889a4c8f6a0 100755 --- a/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx +++ b/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx @@ -1,27 +1,21 @@ import type {RefObject} from 'react'; -import React, {useRef, useState} from 'react'; -import {InteractionManager, View} from 'react-native'; +import React, {useState} from 'react'; +import {InteractionManager} 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 * as ActionSheetAwareScrollView from '@components/ActionSheetAwareScrollView'; -import ContextMenuItem from '@components/ContextMenuItem'; import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; -import FocusTrapForModal from '@components/FocusTrap/FocusTrapForModal'; import {useSession} from '@components/OnyxListItemProvider'; -import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useEnvironment from '@hooks/useEnvironment'; import useGetExpensifyCardFromReportAction from '@hooks/useGetExpensifyCardFromReportAction'; -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePaginatedReportActions from '@hooks/usePaginatedReportActions'; import useReportIsArchived from '@hooks/useReportIsArchived'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useRestoreInputFocus from '@hooks/useRestoreInputFocus'; -import useStyleUtils from '@hooks/useStyleUtils'; import useTransactionsAndViolationsForReport from '@hooks/useTransactionsAndViolationsForReport'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import {getMovedReportID} from '@libs/ModifiedExpenseMessage'; @@ -42,71 +36,32 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {OriginalMessageIOU, ReportAction} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import type {ContextMenuAction, ContextMenuActionPayload} from './ContextMenuActions'; -import ContextMenuActions from './ContextMenuActions'; +import ContextMenuAction from './actions/ContextMenuAction'; +import ContextMenuLayout from './ContextMenuLayout'; +import {ContextMenuPayloadContext} from './ContextMenuPayloadProvider'; +import type {ContextMenuPayloadContextValue} from './ContextMenuPayloadProvider'; import {useMiniContextMenuActions} from './MiniContextMenuProvider'; import type {ContextMenuAnchor, ContextMenuType} from './ReportActionContextMenu'; import {hideContextMenu, showContextMenu} from './ReportActionContextMenu'; type BaseReportActionContextMenuProps = { - /** The ID of the report this report action is attached to. */ reportID: string | undefined; - - /** The ID of the report action this context menu is attached to. */ reportActionID: string | undefined; - - /** The ID of the original report from which the given reportAction is first created. */ originalReportID: string | undefined; - - /** - * If true, this component will be a small, row-oriented menu that displays icons but not text. - * If false, this component will be a larger, column-oriented menu that displays icons alongside text in each row. - */ isMini?: boolean; - - /** Controls the visibility of this component. */ isVisible?: boolean; - - /** The copy selection. */ selection?: string; - - /** Draft message - if this is set the comment is in 'edit' mode */ draftMessage?: string; - - /** String representing the context menu type [LINK, REPORT_ACTION] which controls context menu choices */ type?: ContextMenuType; - - /** Target node which is the target of ContentMenu */ anchor?: RefObject; - - /** Flag to check if the chat participant is Chronos */ isChronosReport?: boolean; - - /** Whether the provided report is an archived room */ isArchivedRoom?: boolean; - - /** Flag to check if the chat is pinned in the LHN. Used for the Pin/Unpin action */ isPinnedChat?: boolean; - - /** Flag to check if the chat is unread in the LHN. Used for the Mark as Read/Unread action */ isUnreadChat?: boolean; - - /** - * Is the action a thread's parent reportAction viewed from within the thread report? - * It will be false if we're viewing the same parent report action from the report it belongs to rather than the thread. - */ isThreadReportParentAction?: boolean; - - /** Content Ref */ - contentRef?: RefObject; - - /** Function to check if context menu is active */ + contentRef?: RefObject; checkIfContextMenuActive?: () => void; - - /** List of disabled actions */ - disabledActions?: ContextMenuAction[]; - - /** Function to update emoji picker state */ + disabledActionIds?: Set; setIsEmojiPickerActive?: (state: boolean) => void; }; @@ -127,43 +82,19 @@ function BaseReportActionContextMenu({ reportID, originalReportID, checkIfContextMenuActive, - disabledActions = [], + disabledActionIds = new Set(), setIsEmojiPickerActive, }: BaseReportActionContextMenuProps) { const {transitionActionSheetState} = ActionSheetAwareScrollView.useActionSheetAwareScrollViewActions(); const {isDelegateAccessRestricted} = useDelegateNoAccessState(); const {showDelegateNoAccessModal} = useDelegateNoAccessActions(); - const icons = useMemoizedLazyExpensifyIcons([ - 'Bell', - 'Bug', - 'ChatBubbleReply', - 'ChatBubbleUnread', - 'Checkmark', - 'Concierge', - 'Copy', - 'Download', - 'Exit', - 'Flag', - 'LinkCopy', - 'Mail', - 'Pencil', - 'Pin', - 'Stopwatch', - 'ThreeDots', - 'Trashcan', - ] as const); - const StyleUtils = useStyleUtils(); - const {translate, getLocalDateFromDatetime} = useLocalize(); - // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth - const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); const [localShouldKeepOpen, setLocalShouldKeepOpen] = useState(false); const miniActions = useMiniContextMenuActions(); const shouldKeepOpen = isMini ? false : localShouldKeepOpen; - const wrapperStyle = StyleUtils.getReportActionContextMenuStyles(isMini, shouldUseNarrowLayout); + const {translate, getLocalDateFromDatetime} = useLocalize(); const {isOffline} = useNetwork(); const {isProduction} = useEnvironment(); - const threeDotRef = useRef(null); - const nullRef = useRef(null); + const [betas] = useOnyx(ONYXKEYS.BETAS); const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, { canEvict: false, @@ -249,70 +180,11 @@ function BaseReportActionContextMenu({ isMoneyRequestOrReport && !isArchivedNonExpenseReport(transactionThreadReportID ? childReport : parentReport, transactionThreadReportID ? isChildReportArchived : isParentReportArchived); - const shouldEnableArrowNavigation = !isMini && (isVisible || shouldKeepOpen); const isHarvestReport = isHarvestCreatedExpenseReport(reportNameValuePairs?.origin, reportNameValuePairs?.originalID); - let filteredContextMenuActions = ContextMenuActions.filter( - (contextAction) => - !disabledActions.includes(contextAction) && - contextAction.shouldShow({ - type, - reportAction, - childReportActions, - isArchivedRoom, - betas, - menuTarget: anchor, - isChronosReport, - reportID, - isPinnedChat, - isUnreadChat, - isThreadReportParentAction, - isOffline: !!isOffline, - isMini, - isProduction, - moneyRequestReport, - moneyRequestAction, - moneyRequestPolicy, - areHoldRequirementsMet, - isDebugModeEnabled, - iouTransaction, - transactions, - isHarvestReport, - }), - ); - - if (isMini) { - const menuAction = filteredContextMenuActions.at(-1); - const otherActions = filteredContextMenuActions.slice(0, -1); - if (otherActions.length > CONST.MINI_CONTEXT_MENU_MAX_ITEMS && menuAction) { - filteredContextMenuActions = otherActions.slice(0, CONST.MINI_CONTEXT_MENU_MAX_ITEMS - 1); - filteredContextMenuActions.push(menuAction); - } else { - filteredContextMenuActions = otherActions; - } - } - - // Context menu actions that are not rendered as menu items are excluded from arrow navigation - const nonMenuItemActionIndexes = filteredContextMenuActions.map((contextAction, index) => - 'renderContent' in contextAction && typeof contextAction.renderContent === 'function' ? index : undefined, - ); - const disabledIndexes = nonMenuItemActionIndexes.filter((index): index is number => index !== undefined); - - const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({ - initialFocusedIndex: -1, - disabledIndexes, - maxIndex: filteredContextMenuActions.length - 1, - isActive: shouldEnableArrowNavigation, - }); - - /** - * Checks if user is anonymous. If true and the action doesn't accept for anonymous user, hides the context menu and - * shows the sign in modal. Else, executes the callback. - */ const interceptAnonymousUser = (callback: () => void, isAnonymousAction = false) => { if (isAnonymousUser() && !isAnonymousAction) { hideContextMenu(false); - // eslint-disable-next-line @typescript-eslint/no-deprecated InteractionManager.runAfterInteractions(() => { signOutAndRedirectToSignIn(); @@ -324,7 +196,7 @@ function BaseReportActionContextMenu({ useRestoreInputFocus(isVisible); - const openOverflowMenu = (event: GestureResponderEvent | MouseEvent, anchorRef: RefObject) => { + const openOverflowMenu = (event: GestureResponderEvent | MouseEvent, anchorRef: RefObject) => { showContextMenu({ type: CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, event, @@ -352,7 +224,7 @@ function BaseReportActionContextMenu({ } }, }, - disabledOptions: filteredContextMenuActions, + disabledActionIds: new Set(), shouldCloseOnTarget: true, isOverflowMenu: true, }); @@ -361,108 +233,111 @@ function BaseReportActionContextMenu({ // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style const card = useGetExpensifyCardFromReportAction({reportAction: (reportAction ?? null) as ReportAction, policyID}); - return ( - (isVisible || shouldKeepOpen || !isMini) && ( - - - {filteredContextMenuActions.map((contextAction, index) => { - const closePopup = !isMini; - const payload: ContextMenuActionPayload = { - reportActions, - // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style - reportAction: (reportAction ?? null) as ReportAction, - reportID, - originalReportID, - report, - draftMessage, - selection, - close: () => { - if (isMini) { - miniActions.release(); - } else { - setLocalShouldKeepOpen(false); - } - }, - transitionActionSheetState, - openContextMenu: () => { - if (isMini) { - miniActions.keepOpen(); - } else { - setLocalShouldKeepOpen(true); - } - }, - interceptAnonymousUser, - openOverflowMenu, - setIsEmojiPickerActive, - isHarvestReport, - moneyRequestAction, - card, - originalReport, - isTryNewDotNVPDismissed, - childReport, - movedFromReport, - movedToReport, - getLocalDateFromDatetime, - policy, - policyTags, - translate, - harvestReport, - introSelected, - isDelegateAccessRestricted, - showDelegateNoAccessModal, - currentUserAccountID: currentUserPersonalDetails?.accountID, - currentUserPersonalDetails, - encryptedAuthToken, - }; - - if ('renderContent' in contextAction) { - return contextAction.renderContent(closePopup, payload); - } - - const {textTranslateKey} = contextAction; - const isKeyInActionUpdateKeys = textTranslateKey === 'reportActionContextMenu.editAction' || textTranslateKey === 'reportActionContextMenu.deleteConfirmation'; - const text = textTranslateKey && (isKeyInActionUpdateKeys ? translate(textTranslateKey, {action: moneyRequestAction ?? reportAction}) : translate(textTranslateKey)); - const transactionPayload = textTranslateKey === 'reportActionContextMenu.copyMessage' && transaction && {transaction}; - const isMenuAction = textTranslateKey === 'reportActionContextMenu.menu'; - const icon = typeof contextAction.icon === 'string' ? icons[contextAction.icon] : contextAction.icon; - const successIcon = typeof contextAction.successIcon === 'string' ? icons[contextAction.successIcon] : contextAction.successIcon; + // eslint-disable-next-line react/jsx-no-constructed-context-values + const payloadValue: ContextMenuPayloadContextValue = { + type, + reportID, + originalReportID, + reportActions, + // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style + reportAction: (reportAction ?? null) as ReportAction, + report, + originalReport, + childReport, + childReportActions, + policy, + policyTags, + moneyRequestAction, + moneyRequestReport, + moneyRequestPolicy, + iouTransaction, + transaction, + card, + currentUserAccountID: currentUserPersonalDetails?.accountID, + currentUserPersonalDetails, + encryptedAuthToken, + isArchivedRoom, + isChronosReport, + isPinnedChat, + isUnreadChat, + isThreadReportParentAction, + isOffline: !!isOffline, + isMini, + isProduction, + isHarvestReport, + isTryNewDotNVPDismissed, + isDelegateAccessRestricted: !!isDelegateAccessRestricted, + areHoldRequirementsMet, + isDebugModeEnabled, + betas, + transactions, + introSelected, + draftMessage, + selection, + movedFromReport, + movedToReport, + harvestReport, + download, + close: () => { + if (isMini) { + miniActions.release(); + } else { + setLocalShouldKeepOpen(false); + } + }, + transitionActionSheetState, + openContextMenu: () => { + if (isMini) { + miniActions.keepOpen(); + } else { + setLocalShouldKeepOpen(true); + } + }, + interceptAnonymousUser, + openOverflowMenu, + setIsEmojiPickerActive, + showDelegateNoAccessModal, + translate, + getLocalDateFromDatetime, + anchor, + disabledActionIds, + }; - return ( - - interceptAnonymousUser( - () => contextAction.onPress?.(closePopup, {...payload, ...transactionPayload, event, ...(isMenuAction ? {anchorRef: threeDotRef} : {})}), - contextAction.isAnonymousAction, - ) - } - description={contextAction.getDescription?.(selection) ?? ''} - isAnonymousAction={contextAction.isAnonymousAction} - isFocused={focusedIndex === index} - 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} - sentryLabel={contextAction.sentryLabel} - /> - ); - })} - - - ) + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + ); } export default BaseReportActionContextMenu; - export type {BaseReportActionContextMenuProps}; diff --git a/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx b/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx index 4509b144ad7b..70884bc96d54 100644 --- a/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx @@ -1,6 +1,5 @@ import type {ReactNode, RefObject} from 'react'; import React, {createContext, useContext, useRef, useState} from 'react'; -import type {ContextMenuAction} from './ContextMenuActions'; import type {ContextMenuAnchor} from './ReportActionContextMenu'; const HIDE_DELAY_MS = 120; @@ -12,16 +11,16 @@ type RowMeasurements = { }; type MiniContextMenuParams = { - reportID: string; + reportID: string | undefined; reportActionID: string; - originalReportID: string; + originalReportID: string | undefined; anchor: RefObject; displayAsGroup: boolean; isArchivedRoom: boolean; isThreadReportParentAction: boolean; draftMessage: string | undefined; isChronosReport: boolean; - disabledActions: ContextMenuAction[]; + disabledActionIds: Set; checkIfContextMenuActive: () => void; setIsEmojiPickerActive: (state: boolean) => void; rowMeasurements: RowMeasurements; diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index f4d6944ca0a0..8ceef9da62ba 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -27,11 +27,11 @@ function MiniReportActionContextMenu() { const targetRight = window.innerWidth - state.rowMeasurements.right + 4; if (wasVisibleRef.current) { - baseTop.value = withTiming(targetY, {duration: SLIDE_DURATION, easing: OVERSHOOT_EASING}); - baseRight.value = withTiming(targetRight, {duration: SLIDE_DURATION}); + baseTop.set(withTiming(targetY, {duration: SLIDE_DURATION, easing: OVERSHOOT_EASING})); + baseRight.set(withTiming(targetRight, {duration: SLIDE_DURATION})); } else { - baseTop.value = targetY; - baseRight.value = targetRight; + baseTop.set(targetY); + baseRight.set(targetRight); } } wasVisibleRef.current = state.isVisible; @@ -51,8 +51,8 @@ function MiniReportActionContextMenu() { }, [isVisible, hideMiniContextMenu]); const positionStyle = useAnimatedStyle(() => ({ - top: baseTop.value, - right: baseRight.value, + top: baseTop.get(), + right: baseRight.get(), })); if (!state) { @@ -88,7 +88,7 @@ function MiniReportActionContextMenu() { isThreadReportParentAction={state.isThreadReportParentAction} draftMessage={state.draftMessage} isChronosReport={state.isChronosReport} - disabledActions={state.disabledActions} + disabledActionIds={state.disabledActionIds} checkIfContextMenuActive={state.checkIfContextMenuActive} setIsEmojiPickerActive={state.setIsEmojiPickerActive} isVisible={state.isVisible} diff --git a/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx index 67fcea8394dd..91c2e189a2fb 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -13,9 +13,10 @@ import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManag import CONST from '@src/CONST'; import BaseReportActionContextMenu from './BaseReportActionContextMenu'; import ConfirmDeleteReportActionModal from './ConfirmDeleteReportActionModal'; -import type {ContextMenuAction} from './ContextMenuActions'; import type {ContextMenuAnchor, ContextMenuType, ReportActionContextMenu} from './ReportActionContextMenu'; +const EMPTY_DISABLED_ACTION_IDS = new Set(); + function extractPointerEvent(event: GestureResponderEvent | MouseEvent): MouseEvent | NativeTouchEvent { if ('nativeEvent' in event) { return event.nativeEvent; @@ -42,7 +43,7 @@ type PopoverContextMenuState = { isPinnedChat: boolean; isUnreadChat: boolean; isThreadReportParentAction: boolean; - disabledActions: ContextMenuAction[]; + disabledActionIds: Set; isOverflowMenu: boolean; withoutOverlay: boolean; position: PopoverPosition; @@ -146,7 +147,7 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro report: currentReport = {}, reportAction: reportActionParam = {}, callbacks = {}, - disabledOptions = [], + disabledActionIds = new Set(), shouldCloseOnTarget = false, isOverflowMenu = false, withoutOverlay = true, @@ -212,7 +213,7 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro isPinnedChat, isUnreadChat, isThreadReportParentAction: isThreadReportParentActionParam, - disabledActions: disabledOptions, + disabledActionIds, isOverflowMenu, withoutOverlay, position, @@ -292,7 +293,7 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro isPinnedChat: false, isUnreadChat: false, isThreadReportParentAction: false, - disabledActions: [], + disabledActionIds: EMPTY_DISABLED_ACTION_IDS, isOverflowMenu: false, withoutOverlay: true, position: {anchorHorizontal: 0, anchorVertical: 0, anchorWidth: 0, anchorHeight: 0}, @@ -373,7 +374,7 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro anchor={{current: menuState?.contextMenuTargetNode ?? null}} contentRef={contentRef} originalReportID={menuState?.originalReportID} - disabledActions={menuState?.disabledActions ?? []} + disabledActionIds={menuState?.disabledActionIds ?? EMPTY_DISABLED_ACTION_IDS} setIsEmojiPickerActive={menuState?.onEmojiPickerToggle} /> diff --git a/src/pages/inbox/report/ContextMenu/ReportActionContextMenu.ts b/src/pages/inbox/report/ContextMenu/ReportActionContextMenu.ts index 2da4ac2bbffc..012d61bc020b 100644 --- a/src/pages/inbox/report/ContextMenu/ReportActionContextMenu.ts +++ b/src/pages/inbox/report/ContextMenu/ReportActionContextMenu.ts @@ -7,7 +7,6 @@ import type {ValueOf} from 'type-fest'; import type {ComposerType} from '@libs/ReportActionComposeFocusManager'; import type CONST from '@src/CONST'; import type {ReportAction} from '@src/types/onyx'; -import type {ContextMenuAction} from './ContextMenuActions'; type OnConfirm = () => void; @@ -40,7 +39,7 @@ type ShowContextMenuParams = { onHide?: () => void; setIsEmojiPickerActive?: (state: boolean) => void; }; - disabledOptions?: ContextMenuAction[]; + disabledActionIds?: Set; shouldCloseOnTarget?: boolean; isOverflowMenu?: boolean; withoutOverlay?: boolean; diff --git a/src/pages/inbox/report/ContextMenu/actions/ContextMenuAction.ts b/src/pages/inbox/report/ContextMenu/actions/ContextMenuAction.ts new file mode 100644 index 000000000000..71887c17cc66 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/ContextMenuAction.ts @@ -0,0 +1,51 @@ +import CopyEmail from './CopyEmail'; +import CopyLink from './CopyLink'; +import CopyMessage from './CopyMessage'; +import CopyOnyxData from './CopyOnyxData'; +import CopyToClipboard from './CopyToClipboard'; +import CopyURL from './CopyURL'; +import Debug from './Debug'; +import Delete from './Delete'; +import Download from './Download'; +import Edit from './Edit'; +import EmojiReaction from './EmojiReaction'; +import Explain from './Explain'; +import FlagAsOffensive from './FlagAsOffensive'; +import Hold from './Hold'; +import JoinThread from './JoinThread'; +import LeaveThread from './LeaveThread'; +import MarkAsRead from './MarkAsRead'; +import MarkAsUnread from './MarkAsUnread'; +import OverflowMenu from './OverflowMenu'; +import Pin from './Pin'; +import ReplyInThread from './ReplyInThread'; +import Unhold from './Unhold'; +import Unpin from './Unpin'; + +const ContextMenuAction = { + EmojiReaction, + ReplyInThread, + MarkAsUnread, + Explain, + MarkAsRead, + Edit, + Unhold, + Hold, + JoinThread, + LeaveThread, + CopyURL, + CopyToClipboard, + CopyEmail, + CopyMessage, + CopyLink, + Pin, + Unpin, + FlagAsOffensive, + Download, + CopyOnyxData, + Debug, + Delete, + OverflowMenu, +}; + +export default ContextMenuAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/CopyEmail.tsx b/src/pages/inbox/report/ContextMenu/actions/CopyEmail.tsx new file mode 100644 index 000000000000..90b2cfb01120 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/CopyEmail.tsx @@ -0,0 +1,47 @@ +import ContextMenuItem from '@components/ContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import Clipboard from '@libs/Clipboard'; +import EmailUtils from '@libs/EmailUtils'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import CONST from '@src/CONST'; +import {useContextMenuVisibility} from '../ContextMenuLayout'; +import {useContextMenuPayload} from '../ContextMenuPayloadProvider'; +import {hideContextMenu} from '../ReportActionContextMenu'; +import {ACTION_IDS} from './actionConfig'; + +function CopyEmail() { + const {selection, isMini, interceptAnonymousUser} = useContextMenuPayload(); + const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); + const icons = useMemoizedLazyExpensifyIcons(['Copy', 'Checkmark'] as const); + const {translate} = useLocalize(); + + const actionIndex = visibleActionIds.indexOf(ACTION_IDS.COPY_EMAIL); + if (actionIndex === -1) { + return null; + } + + const handlePress = () => { + Clipboard.setString(EmailUtils.trimMailTo(selection)); + hideContextMenu(true, ReportActionComposeFocusManager.focus); + }; + + return ( + interceptAnonymousUser(handlePress, true)} + isFocused={focusedIndex === actionIndex} + onFocus={() => setFocusedIndex(actionIndex)} + onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_EMAIL} + /> + ); +} + +export default CopyEmail; diff --git a/src/pages/inbox/report/ContextMenu/actions/CopyLink.tsx b/src/pages/inbox/report/ContextMenu/actions/CopyLink.tsx new file mode 100644 index 000000000000..56bb8586ec3d --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/CopyLink.tsx @@ -0,0 +1,49 @@ +import ContextMenuItem from '@components/ContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import Clipboard from '@libs/Clipboard'; +import {getEnvironmentURL} from '@libs/Environment/Environment'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import CONST from '@src/CONST'; +import {useContextMenuVisibility} from '../ContextMenuLayout'; +import {useContextMenuPayload} from '../ContextMenuPayloadProvider'; +import {hideContextMenu} from '../ReportActionContextMenu'; +import {ACTION_IDS} from './actionConfig'; + +function CopyLink() { + const {reportAction, originalReportID, isMini, interceptAnonymousUser} = useContextMenuPayload(); + const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); + const icons = useMemoizedLazyExpensifyIcons(['LinkCopy', 'Checkmark'] as const); + const {translate} = useLocalize(); + + const actionIndex = visibleActionIds.indexOf(ACTION_IDS.COPY_LINK); + if (actionIndex === -1) { + return null; + } + + const handlePress = () => { + getEnvironmentURL().then((environmentURL) => { + const reportActionID = reportAction?.reportActionID; + Clipboard.setString(`${environmentURL}/r/${originalReportID}/${reportActionID}`); + }); + hideContextMenu(true, ReportActionComposeFocusManager.focus); + }; + + return ( + interceptAnonymousUser(handlePress, true)} + isFocused={focusedIndex === actionIndex} + onFocus={() => setFocusedIndex(actionIndex)} + onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_LINK} + /> + ); +} + +export default CopyLink; diff --git a/src/pages/inbox/report/ContextMenu/actions/CopyMessage.tsx b/src/pages/inbox/report/ContextMenu/actions/CopyMessage.tsx new file mode 100644 index 000000000000..a5a0f83fbb78 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/CopyMessage.tsx @@ -0,0 +1,540 @@ +import {Str} from 'expensify-common'; +import type {OnyxEntry} from 'react-native-onyx'; +import ContextMenuItem from '@components/ContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import Clipboard from '@libs/Clipboard'; +import getClipboardText from '@libs/Clipboard/getClipboardText'; +import {formatPhoneNumber as formatPhoneNumberPhoneUtils} from '@libs/LocalePhoneNumber'; +import {getForReportActionTemp} from '@libs/ModifiedExpenseMessage'; +import Parser from '@libs/Parser'; +import {getCleanedTagName, isPolicyAdmin} from '@libs/PolicyUtils'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import { + getActionableCardFraudAlertMessage, + getActionableMentionWhisperMessage, + getAddedApprovalRuleMessage, + getAddedBudgetMessage, + getAddedConnectionMessage, + getAutoPayApprovedReportsEnabledMessage, + getAutoReimbursementMessage, + getCardIssuedMessage, + getChangedApproverActionMessage, + getCompanyAddressUpdateMessage, + getCompanyCardConnectionBrokenMessage, + getCreatedReportForUnapprovedTransactionsMessage, + getCurrencyDefaultTaxUpdateMessage, + getCustomTaxNameUpdateMessage, + getDefaultApproverUpdateMessage, + getDeletedApprovalRuleMessage, + getDeletedBudgetMessage, + getDismissedViolationMessageText, + getDynamicExternalWorkflowRoutedMessage, + getExportIntegrationMessageHTML, + getForeignCurrencyDefaultTaxUpdateMessage, + getForwardsToUpdateMessage, + getHarvestCreatedExpenseReportMessage, + getIntegrationSyncFailedMessage, + getInvoiceCompanyNameUpdateMessage, + getInvoiceCompanyWebsiteUpdateMessage, + getIOUReportIDFromReportActionPreview, + getJoinRequestMessage, + getMarkedReimbursedMessage, + getMemberChangeMessageFragment, + getMessageOfOldDotReportAction, + getOriginalMessage, + getPlaidBalanceFailureMessage, + getPolicyChangeLogAddEmployeeMessage, + getPolicyChangeLogDefaultBillableMessage, + getPolicyChangeLogDefaultReimbursableMessage, + getPolicyChangeLogDefaultTitleEnforcedMessage, + getPolicyChangeLogDeleteMemberMessage, + getPolicyChangeLogMaxExpenseAgeMessage, + getPolicyChangeLogMaxExpenseAmountMessage, + getPolicyChangeLogMaxExpenseAmountNoReceiptMessage, + getPolicyChangeLogUpdateEmployee, + getReimburserUpdateMessage, + getRemovedConnectionMessage, + getRenamedAction, + getReportActionMessageText, + getRoomAvatarUpdatedMessage, + getSetAutoJoinMessage, + getSettlementAccountLockedMessage, + getSubmitsToUpdateMessage, + getTagListNameUpdatedMessage, + getTagListUpdatedMessage, + getTagListUpdatedRequiredMessage, + getTravelUpdateMessage, + getUpdateACHAccountMessage, + getUpdatedApprovalRuleMessage, + getUpdatedAuditRateMessage, + getUpdatedAutoHarvestingMessage, + getUpdatedBudgetMessage, + getUpdatedDefaultTitleMessage, + getUpdatedIndividualBudgetNotificationMessage, + getUpdatedManualApprovalThresholdMessage, + getUpdatedOwnershipMessage, + getUpdatedProhibitedExpensesMessage, + getUpdatedReimbursementChoiceMessage, + getUpdatedSharedBudgetNotificationMessage, + getUpdatedTimeEnabledMessage, + getUpdatedTimeRateMessage, + getUpdateRoomDescriptionMessage, + getWorkspaceAttendeeTrackingUpdateMessage, + getWorkspaceCategoriesUpdatedMessage, + getWorkspaceCategoryUpdateMessage, + getWorkspaceCurrencyUpdateMessage, + getWorkspaceCustomUnitRateAddedMessage, + getWorkspaceCustomUnitRateDeletedMessage, + getWorkspaceCustomUnitRateImportedMessage, + getWorkspaceCustomUnitRateUpdatedMessage, + getWorkspaceCustomUnitSubRateDeletedMessage, + getWorkspaceCustomUnitSubRateUpdatedMessage, + getWorkspaceCustomUnitUpdatedMessage, + getWorkspaceDescriptionUpdatedMessage, + getWorkspaceFeatureEnabledMessage, + getWorkspaceFrequencyUpdateMessage, + getWorkspaceReimbursementUpdateMessage, + getWorkspaceReportFieldAddMessage, + getWorkspaceReportFieldDeleteMessage, + getWorkspaceReportFieldUpdateMessage, + getWorkspaceTagUpdateMessage, + getWorkspaceTaxUpdateMessage, + getWorkspaceUpdateFieldMessage, + isActionableJoinRequest, + isActionableMentionWhisper, + isActionableTrackExpense, + isActionOfType, + isCardIssuedAction, + isCreatedTaskReportAction, + isMarkAsClosedAction, + isMemberChangeAction, + isModifiedExpenseAction, + isMoneyRequestAction, + isMovedAction, + isOldDotReportAction, + isReimbursementDeQueuedOrCanceledAction, + isReimbursementQueuedAction, + isRenamedAction, + isReportActionAttachment, + isReportPreviewAction as isReportPreviewActionReportActionsUtils, + isTagModificationAction, + isTaskAction as isTaskActionReportActionsUtils, + isUnapprovedAction, +} from '@libs/ReportActionsUtils'; +import {getReportName} from '@libs/ReportNameUtils'; +import { + getDeletedTransactionMessage, + getIOUReportActionDisplayMessage, + getMovedActionMessage, + getMovedTransactionMessage, + getPolicyChangeMessage, + getReimbursementDeQueuedOrCanceledActionMessage, + getReimbursementQueuedActionMessage, + getReportName as getReportNameDeprecated, + getReportOrDraftReport, + getReportPreviewMessage, + getUnreportedTransactionMessage, + getWorkspaceNameUpdatedMessage, + isExpenseReport, +} from '@libs/ReportUtils'; +import {getTaskCreatedMessage, getTaskReportActionMessage} from '@libs/TaskUtils'; +import CONST from '@src/CONST'; +import type {ReportAction} from '@src/types/onyx'; +import {useContextMenuVisibility} from '../ContextMenuLayout'; +import type {ContextMenuPayloadContextValue} from '../ContextMenuPayloadProvider'; +import {useContextMenuPayload} from '../ContextMenuPayloadProvider'; +import {hideContextMenu} from '../ReportActionContextMenu'; +import {ACTION_IDS, getActionHtml} from './actionConfig'; + +function setClipboardMessage(content: string | undefined) { + if (!content) { + return; + } + const clipboardText = getClipboardText(content); + if (!Clipboard.canSetHtml()) { + Clipboard.setString(clipboardText); + } else { + Clipboard.setHtml(content, clipboardText); + } +} + +function copyMessageToClipboard(payload: ContextMenuPayloadContextValue) { + const { + reportAction, + transaction, + selection, + report, + card, + originalReport, + isHarvestReport = false, + isTryNewDotNVPDismissed = false, + movedFromReport, + movedToReport, + childReport, + policy, + getLocalDateFromDatetime, + policyTags, + translate, + harvestReport, + currentUserPersonalDetails, + } = payload; + + const isReportPreviewAction = isReportPreviewActionReportActionsUtils(reportAction); + const messageHtml = getActionHtml(reportAction); + const messageText = getReportActionMessageText(reportAction); + const isAttachment = isReportActionAttachment(reportAction); + + if (!isAttachment) { + const content = selection || messageHtml; + if (isReportPreviewAction) { + const iouReportID = getIOUReportIDFromReportActionPreview(reportAction); + const displayMessage = getReportPreviewMessage(iouReportID, reportAction, undefined, undefined, undefined, undefined, undefined, true); + Clipboard.setString(displayMessage); + } else if (isTaskActionReportActionsUtils(reportAction)) { + const {text, html} = getTaskReportActionMessage(translate, reportAction); + const displayMessage = html ?? text; + setClipboardMessage(displayMessage); + } else if (isModifiedExpenseAction(reportAction)) { + const modifyExpenseMessage = getForReportActionTemp({ + translate, + reportAction, + policy, + movedFromReport, + movedToReport, + policyTags, + currentUserLogin: (currentUserPersonalDetails as {email?: string})?.email ?? (currentUserPersonalDetails as {login?: string})?.login ?? '', + }); + Clipboard.setString(modifyExpenseMessage); + } else if (isReimbursementDeQueuedOrCanceledAction(reportAction)) { + const displayMessage = getReimbursementDeQueuedOrCanceledActionMessage(translate, reportAction, report); + Clipboard.setString(displayMessage); + } else if (isMoneyRequestAction(reportAction)) { + const displayMessage = getIOUReportActionDisplayMessage(translate, reportAction, transaction, report); + if (displayMessage === Parser.htmlToText(displayMessage)) { + Clipboard.setString(displayMessage); + } else { + setClipboardMessage(displayMessage); + } + } else if (isCreatedTaskReportAction(reportAction)) { + const taskPreviewMessage = getTaskCreatedMessage(translate, reportAction, childReport, true); + Clipboard.setString(taskPreviewMessage); + } else if (isMemberChangeAction(reportAction)) { + const logMessage = getMemberChangeMessageFragment(translate, reportAction, getReportNameDeprecated).html ?? ''; + setClipboardMessage(logMessage); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_NAME) { + Clipboard.setString(Str.htmlDecode(getWorkspaceNameUpdatedMessage(translate, reportAction))); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_DESCRIPTION) { + Clipboard.setString(getWorkspaceDescriptionUpdatedMessage(translate, reportAction)); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_CURRENCY) { + Clipboard.setString(getWorkspaceCurrencyUpdateMessage(translate, reportAction)); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_AUTO_REPORTING_FREQUENCY) { + Clipboard.setString(getWorkspaceFrequencyUpdateMessage(translate, reportAction)); + } else if ( + reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_CATEGORY || + reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_CATEGORY || + reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_CATEGORY || + reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.SET_CATEGORY_NAME + ) { + Clipboard.setString(getWorkspaceCategoryUpdateMessage(translate, reportAction)); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_CATEGORIES) { + Clipboard.setString(getWorkspaceCategoriesUpdatedMessage(translate, reportAction)); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.IMPORT_TAGS) { + Clipboard.setString(translate('workspaceActions.importTags')); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_ALL_TAGS) { + Clipboard.setString(translate('workspaceActions.deletedAllTags')); + } else if ( + reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_TAX || + reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_TAX || + reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_TAX + ) { + Clipboard.setString(getWorkspaceTaxUpdateMessage(translate, reportAction)); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_CUSTOM_TAX_NAME) { + Clipboard.setString(getCustomTaxNameUpdateMessage(translate, reportAction)); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_CURRENCY_DEFAULT_TAX) { + Clipboard.setString(getCurrencyDefaultTaxUpdateMessage(translate, reportAction)); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_FOREIGN_CURRENCY_DEFAULT_TAX) { + Clipboard.setString(getForeignCurrencyDefaultTaxUpdateMessage(translate, reportAction)); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_TAG_LIST_NAME) { + Clipboard.setString(getCleanedTagName(getTagListNameUpdatedMessage(translate, reportAction))); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_TAG_LIST) { + Clipboard.setString(getCleanedTagName(getTagListUpdatedMessage(translate, reportAction))); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_TAG_LIST_REQUIRED) { + Clipboard.setString(getCleanedTagName(getTagListUpdatedRequiredMessage(translate, reportAction))); + } else if (isTagModificationAction(reportAction.actionName)) { + Clipboard.setString(getCleanedTagName(getWorkspaceTagUpdateMessage(translate, reportAction))); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_CUSTOM_UNIT) { + Clipboard.setString(getWorkspaceCustomUnitUpdatedMessage(translate, reportAction)); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.IMPORT_CUSTOM_UNIT_RATES) { + Clipboard.setString(getWorkspaceCustomUnitRateImportedMessage(translate, reportAction)); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_CUSTOM_UNIT_RATE) { + Clipboard.setString(getWorkspaceCustomUnitRateAddedMessage(translate, reportAction)); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_CUSTOM_UNIT_RATE) { + Clipboard.setString(getWorkspaceCustomUnitRateUpdatedMessage(translate, reportAction)); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_CUSTOM_UNIT_RATE) { + Clipboard.setString(getWorkspaceCustomUnitRateDeletedMessage(translate, reportAction)); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_CUSTOM_UNIT_SUB_RATE) { + Clipboard.setString(getWorkspaceCustomUnitSubRateUpdatedMessage(translate, reportAction)); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_CUSTOM_UNIT_SUB_RATE) { + Clipboard.setString(getWorkspaceCustomUnitSubRateDeletedMessage(translate, reportAction)); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_REPORT_FIELD) { + Clipboard.setString(getWorkspaceReportFieldAddMessage(translate, reportAction)); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_REPORT_FIELD) { + Clipboard.setString(getWorkspaceReportFieldUpdateMessage(translate, reportAction)); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_REPORT_FIELD) { + Clipboard.setString(getWorkspaceReportFieldDeleteMessage(translate, reportAction)); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_FIELD) { + setClipboardMessage(getWorkspaceUpdateFieldMessage(translate, reportAction)); + } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_FEATURE_ENABLED) { + Clipboard.setString(getWorkspaceFeatureEnabledMessage(translate, reportAction)); + } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_IS_ATTENDEE_TRACKING_ENABLED) { + Clipboard.setString(getWorkspaceAttendeeTrackingUpdateMessage(translate, reportAction)); + } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_DEFAULT_APPROVER) { + Clipboard.setString(getDefaultApproverUpdateMessage(translate, reportAction)); + } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_SUBMITS_TO) { + Clipboard.setString(getSubmitsToUpdateMessage(translate, reportAction)); + } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_FORWARDS_TO) { + Clipboard.setString(getForwardsToUpdateMessage(translate, reportAction)); + } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_AUTO_PAY_APPROVED_REPORTS_ENABLED) { + Clipboard.setString(getAutoPayApprovedReportsEnabledMessage(translate, reportAction)); + } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_AUTO_REIMBURSEMENT) { + Clipboard.setString(getAutoReimbursementMessage(translate, reportAction)); + } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_INVOICE_COMPANY_NAME) { + Clipboard.setString(getInvoiceCompanyNameUpdateMessage(translate, reportAction)); + } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_INVOICE_COMPANY_WEBSITE) { + Clipboard.setString(getInvoiceCompanyWebsiteUpdateMessage(translate, reportAction)); + } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_REIMBURSER) { + Clipboard.setString(getReimburserUpdateMessage(translate, reportAction)); + } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_REIMBURSEMENT_ENABLED) { + Clipboard.setString(getWorkspaceReimbursementUpdateMessage(translate, reportAction)); + } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_ACH_ACCOUNT) { + Clipboard.setString(getUpdateACHAccountMessage(translate, reportAction)); + } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_ADDRESS) { + Clipboard.setString(getCompanyAddressUpdateMessage(translate, reportAction)); + } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_MAX_EXPENSE_AMOUNT_NO_RECEIPT) { + Clipboard.setString(getPolicyChangeLogMaxExpenseAmountNoReceiptMessage(translate, reportAction)); + } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_MAX_EXPENSE_AMOUNT) { + Clipboard.setString(getPolicyChangeLogMaxExpenseAmountMessage(translate, reportAction)); + } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_MAX_EXPENSE_AGE) { + Clipboard.setString(getPolicyChangeLogMaxExpenseAgeMessage(translate, reportAction)); + } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_DEFAULT_BILLABLE) { + Clipboard.setString(getPolicyChangeLogDefaultBillableMessage(translate, reportAction)); + } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_DEFAULT_REIMBURSABLE) { + Clipboard.setString(getPolicyChangeLogDefaultReimbursableMessage(translate, reportAction)); + } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_DEFAULT_TITLE_ENFORCED) { + Clipboard.setString(getPolicyChangeLogDefaultTitleEnforcedMessage(translate, reportAction)); + } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_OWNERSHIP) { + setClipboardMessage(Parser.htmlToText(getUpdatedOwnershipMessage(translate, reportAction, policy) ?? '')); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.UNREPORTED_TRANSACTION)) { + setClipboardMessage(getUnreportedTransactionMessage(translate, reportAction)); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.MARKED_REIMBURSED)) { + Clipboard.setString(getMarkedReimbursedMessage(translate, reportAction)); + } else if (isReimbursementQueuedAction(reportAction)) { + Clipboard.setString(getReimbursementQueuedActionMessage({reportAction, translate, formatPhoneNumber: formatPhoneNumberPhoneUtils, report, shouldUseShortDisplayName: false})); + } else if (isActionableMentionWhisper(reportAction)) { + const mentionWhisperMessage = getActionableMentionWhisperMessage(translate, reportAction); + setClipboardMessage(mentionWhisperMessage); + } else if (isActionableTrackExpense(reportAction)) { + setClipboardMessage(CONST.ACTIONABLE_TRACK_EXPENSE_WHISPER_MESSAGE); + } else if (isRenamedAction(reportAction)) { + setClipboardMessage(getRenamedAction(translate, reportAction, isExpenseReport(report))); + } else if ( + isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.SUBMITTED) || + isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.SUBMITTED_AND_CLOSED) || + isMarkAsClosedAction(reportAction) + ) { + const harvesting = !isMarkAsClosedAction(reportAction) ? (getOriginalMessage(reportAction)?.harvesting ?? false) : false; + if (harvesting) { + setClipboardMessage(translate('iou.automaticallySubmitted')); + } else { + Clipboard.setString(translate('iou.submitted', getOriginalMessage(reportAction)?.message)); + } + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.APPROVED)) { + const {automaticAction} = getOriginalMessage(reportAction) ?? {}; + if (automaticAction) { + setClipboardMessage(translate('iou.automaticallyApproved')); + } else { + Clipboard.setString(translate('iou.approvedMessage')); + } + } else if (isUnapprovedAction(reportAction)) { + Clipboard.setString(translate('iou.unapproved')); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.FORWARDED)) { + const {automaticAction} = getOriginalMessage(reportAction) ?? {}; + if (automaticAction) { + setClipboardMessage(translate('iou.automaticallyForwarded')); + } else { + Clipboard.setString(translate('iou.forwarded')); + } + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REJECTED) { + Clipboard.setString(translate('iou.rejectedThisReport')); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.CORPORATE_UPGRADE) { + const displayMessage = translate('workspaceActions.upgradedWorkspace'); + Clipboard.setString(displayMessage); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.CORPORATE_FORCE_UPGRADE) { + const displayMessage = Parser.htmlToText(translate('workspaceActions.forcedCorporateUpgrade')); + Clipboard.setString(displayMessage); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.TEAM_DOWNGRADE) { + Clipboard.setString(translate('workspaceActions.downgradedWorkspace')); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.HOLD) { + Clipboard.setString(translate('iou.heldExpense')); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.UNHOLD) { + Clipboard.setString(translate('iou.unheldExpense')); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REJECTEDTRANSACTION_THREAD) { + Clipboard.setString(translate('iou.reject.reportActions.rejectedExpense')); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REJECTED_TRANSACTION_MARKASRESOLVED) { + Clipboard.setString(translate('iou.reject.reportActions.markedAsResolved')); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.RETRACTED) { + Clipboard.setString(translate('iou.retracted')); + } else if (isOldDotReportAction(reportAction)) { + const oldDotActionMessage = getMessageOfOldDotReportAction(translate, reportAction); + Clipboard.setString(oldDotActionMessage); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.DISMISSED_VIOLATION) { + const originalMessage = getOriginalMessage(reportAction) as ReportAction['originalMessage']; + Clipboard.setString(getDismissedViolationMessageText(translate, originalMessage)); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.RESOLVED_DUPLICATES) { + Clipboard.setString(translate('violations.resolvedDuplicates')); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.EXPORTED_TO_INTEGRATION) { + setClipboardMessage(getExportIntegrationMessageHTML(translate, reportAction)); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOM_CHANGE_LOG.UPDATE_ROOM_DESCRIPTION) { + setClipboardMessage(getUpdateRoomDescriptionMessage(translate, reportAction)); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOM_CHANGE_LOG.UPDATE_ROOM_AVATAR) { + setClipboardMessage(getRoomAvatarUpdatedMessage(translate, reportAction)); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EMPLOYEE) { + setClipboardMessage(getPolicyChangeLogAddEmployeeMessage(translate, reportAction)); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EMPLOYEE) { + setClipboardMessage(getPolicyChangeLogUpdateEmployee(translate, reportAction)); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_EMPLOYEE) { + setClipboardMessage(getPolicyChangeLogDeleteMemberMessage(translate, reportAction)); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.DELETED_TRANSACTION) { + setClipboardMessage(getDeletedTransactionMessage(translate, reportAction)); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REOPENED) { + setClipboardMessage(translate('iou.reopened')); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.INTEGRATION_SYNC_FAILED)) { + setClipboardMessage(getIntegrationSyncFailedMessage(translate, reportAction, report?.policyID, isTryNewDotNVPDismissed)); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.COMPANY_CARD_CONNECTION_BROKEN)) { + setClipboardMessage(getCompanyCardConnectionBrokenMessage(translate, reportAction)); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.PLAID_BALANCE_FAILURE)) { + setClipboardMessage(getPlaidBalanceFailureMessage(translate, reportAction)); + } else if (isCardIssuedAction(reportAction)) { + const shouldNavigateToCardDetails = isPolicyAdmin(policy, currentUserPersonalDetails.login); + setClipboardMessage(getCardIssuedMessage({reportAction, shouldRenderHTML: true, shouldNavigateToCardDetails, policyID: report?.policyID, expensifyCard: card, translate})); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_INTEGRATION)) { + setClipboardMessage(getAddedConnectionMessage(translate, reportAction)); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_INTEGRATION)) { + setClipboardMessage(getRemovedConnectionMessage(translate, reportAction)); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.TRAVEL_UPDATE)) { + setClipboardMessage(getTravelUpdateMessage(translate, reportAction)); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_AUDIT_RATE)) { + setClipboardMessage(getUpdatedAuditRateMessage(translate, reportAction)); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_APPROVER_RULE)) { + setClipboardMessage(getAddedApprovalRuleMessage(translate, reportAction)); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_APPROVER_RULE)) { + setClipboardMessage(getDeletedApprovalRuleMessage(translate, reportAction)); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_APPROVER_RULE)) { + setClipboardMessage(getUpdatedApprovalRuleMessage(translate, reportAction)); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_MANUAL_APPROVAL_THRESHOLD)) { + setClipboardMessage(getUpdatedManualApprovalThresholdMessage(translate, reportAction)); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_BUDGET)) { + setClipboardMessage(getAddedBudgetMessage(translate, reportAction, policy)); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_BUDGET)) { + setClipboardMessage(getUpdatedBudgetMessage(translate, reportAction, policy)); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_BUDGET)) { + setClipboardMessage(getDeletedBudgetMessage(translate, reportAction, policy)); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_TIME_ENABLED)) { + setClipboardMessage(getUpdatedTimeEnabledMessage(translate, reportAction)); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_TIME_RATE)) { + setClipboardMessage(getUpdatedTimeRateMessage(translate, reportAction)); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_PROHIBITED_EXPENSES)) { + setClipboardMessage(getUpdatedProhibitedExpensesMessage(translate, reportAction)); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_REIMBURSEMENT_CHOICE)) { + setClipboardMessage(getUpdatedReimbursementChoiceMessage(translate, reportAction)); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.SET_AUTO_JOIN)) { + setClipboardMessage(getSetAutoJoinMessage(translate, reportAction)); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_DEFAULT_TITLE)) { + setClipboardMessage(getUpdatedDefaultTitleMessage(translate, reportAction)); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_AUTO_HARVESTING)) { + setClipboardMessage(getUpdatedAutoHarvestingMessage(translate, reportAction)); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.INDIVIDUAL_BUDGET_NOTIFICATION)) { + setClipboardMessage(getUpdatedIndividualBudgetNotificationMessage(translate, reportAction)); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.SHARED_BUDGET_NOTIFICATION)) { + setClipboardMessage(getUpdatedSharedBudgetNotificationMessage(translate, reportAction)); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.TAKE_CONTROL) || isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.REROUTE)) { + setClipboardMessage(getChangedApproverActionMessage(translate, reportAction)); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.MOVED_TRANSACTION)) { + setClipboardMessage(getMovedTransactionMessage(translate, reportAction)); + } else if (isMovedAction(reportAction)) { + setClipboardMessage(getMovedActionMessage(translate, reportAction, originalReport)); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_CARD_FRAUD_ALERT)) { + setClipboardMessage(getActionableCardFraudAlertMessage(translate, reportAction, getLocalDateFromDatetime)); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CHANGE_POLICY) { + const displayMessage = getPolicyChangeMessage(translate, reportAction); + Clipboard.setString(displayMessage); + } else if (isActionableJoinRequest(reportAction)) { + const displayMessage = getJoinRequestMessage(translate, policy, reportAction); + Clipboard.setString(displayMessage); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOM_CHANGE_LOG.LEAVE_ROOM || reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.LEAVE_ROOM) { + Clipboard.setString(translate('report.actions.type.leftTheChat')); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.DYNAMIC_EXTERNAL_WORKFLOW_ROUTED)) { + setClipboardMessage(getDynamicExternalWorkflowRoutedMessage(reportAction, translate)); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.CREATED) && isHarvestReport) { + const harvestReportName = getReportName(harvestReport); + const displayMessage = getHarvestCreatedExpenseReportMessage(harvestReport?.reportID, harvestReportName, translate); + setClipboardMessage(displayMessage); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.CREATED_REPORT_FOR_UNAPPROVED_TRANSACTIONS)) { + const {originalID} = getOriginalMessage(reportAction) ?? {}; + const reportName = getReportName(getReportOrDraftReport(originalID)); + const displayMessage = getCreatedReportForUnapprovedTransactionsMessage(originalID, reportName, translate); + setClipboardMessage(displayMessage); + } else if (content) { + setClipboardMessage( + content.replaceAll(/()(.*?)(<\/mention-user>)/gi, (match, openTag: string, innerContent: string, closeTag: string): string => { + const modifiedContent = Str.removeSMSDomain(innerContent) || ''; + return openTag + modifiedContent + closeTag || ''; + }), + ); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.SETTLEMENT_ACCOUNT_LOCKED)) { + setClipboardMessage(getSettlementAccountLockedMessage(translate, reportAction)); + } else if (messageText) { + Clipboard.setString(messageText); + } + } +} + +function CopyMessage() { + const payload = useContextMenuPayload(); + const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); + const icons = useMemoizedLazyExpensifyIcons(['Copy', 'Checkmark'] as const); + const {translate} = useLocalize(); + + const actionIndex = visibleActionIds.indexOf(ACTION_IDS.COPY_MESSAGE); + if (actionIndex === -1) { + return null; + } + + const closePopover = !payload.isMini; + + const handlePress = () => { + copyMessageToClipboard(payload); + if (closePopover) { + hideContextMenu(true, ReportActionComposeFocusManager.focus); + } + }; + + return ( + payload.interceptAnonymousUser(handlePress, true)} + isFocused={focusedIndex === actionIndex} + onFocus={() => setFocusedIndex(actionIndex)} + onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_MESSAGE} + /> + ); +} + +export default CopyMessage; diff --git a/src/pages/inbox/report/ContextMenu/actions/CopyOnyxData.tsx b/src/pages/inbox/report/ContextMenu/actions/CopyOnyxData.tsx new file mode 100644 index 000000000000..60ca2db65097 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/CopyOnyxData.tsx @@ -0,0 +1,45 @@ +import ContextMenuItem from '@components/ContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import Clipboard from '@libs/Clipboard'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import CONST from '@src/CONST'; +import {useContextMenuVisibility} from '../ContextMenuLayout'; +import {useContextMenuPayload} from '../ContextMenuPayloadProvider'; +import {hideContextMenu} from '../ReportActionContextMenu'; +import {ACTION_IDS} from './actionConfig'; + +function CopyOnyxData() { + const {report, isMini, interceptAnonymousUser} = useContextMenuPayload(); + const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); + const icons = useMemoizedLazyExpensifyIcons(['Copy', 'Checkmark'] as const); + const {translate} = useLocalize(); + + const actionIndex = visibleActionIds.indexOf(ACTION_IDS.COPY_ONYX_DATA); + if (actionIndex === -1) { + return null; + } + + const handlePress = () => { + Clipboard.setString(JSON.stringify(report, null, 4)); + hideContextMenu(true, ReportActionComposeFocusManager.focus); + }; + + return ( + interceptAnonymousUser(handlePress, true)} + isFocused={focusedIndex === actionIndex} + onFocus={() => setFocusedIndex(actionIndex)} + onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_ONYX_DATA} + /> + ); +} + +export default CopyOnyxData; diff --git a/src/pages/inbox/report/ContextMenu/actions/CopyToClipboard.tsx b/src/pages/inbox/report/ContextMenu/actions/CopyToClipboard.tsx new file mode 100644 index 000000000000..ac2d42cd32f3 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/CopyToClipboard.tsx @@ -0,0 +1,45 @@ +import ContextMenuItem from '@components/ContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import Clipboard from '@libs/Clipboard'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import CONST from '@src/CONST'; +import {ACTION_IDS} from './actionConfig'; + +function CopyToClipboard() { + const {selection, isMini, interceptAnonymousUser} = useContextMenuPayload(); + const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); + const icons = useMemoizedLazyExpensifyIcons(['Copy', 'Checkmark'] as const); + const {translate} = useLocalize(); + + const actionIndex = visibleActionIds.indexOf(ACTION_IDS.COPY_TO_CLIPBOARD); + if (actionIndex === -1) { + return null; + } + + const handlePress = () => { + Clipboard.setString(selection); + hideContextMenu(true, ReportActionComposeFocusManager.focus); + }; + + return ( + interceptAnonymousUser(handlePress, true)} + isFocused={focusedIndex === actionIndex} + onFocus={() => setFocusedIndex(actionIndex)} + onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_TO_CLIPBOARD} + /> + ); +} + +export default CopyToClipboard; diff --git a/src/pages/inbox/report/ContextMenu/actions/CopyURL.tsx b/src/pages/inbox/report/ContextMenu/actions/CopyURL.tsx new file mode 100644 index 000000000000..3a8fdcbb2a86 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/CopyURL.tsx @@ -0,0 +1,46 @@ +import ContextMenuItem from '@components/ContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import Clipboard from '@libs/Clipboard'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import CONST from '@src/CONST'; +import {ACTION_IDS} from './actionConfig'; + +function CopyURL() { + const {selection, isMini, interceptAnonymousUser} = useContextMenuPayload(); + const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); + const icons = useMemoizedLazyExpensifyIcons(['Copy', 'Checkmark'] as const); + const {translate} = useLocalize(); + + const actionIndex = visibleActionIds.indexOf(ACTION_IDS.COPY_URL); + if (actionIndex === -1) { + return null; + } + + const handlePress = () => { + Clipboard.setString(selection); + hideContextMenu(true, ReportActionComposeFocusManager.focus); + }; + + return ( + interceptAnonymousUser(handlePress, true)} + isFocused={focusedIndex === actionIndex} + onFocus={() => setFocusedIndex(actionIndex)} + onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_URL} + /> + ); +} + +export default CopyURL; diff --git a/src/pages/inbox/report/ContextMenu/actions/Debug.tsx b/src/pages/inbox/report/ContextMenu/actions/Debug.tsx new file mode 100644 index 000000000000..f94596802e31 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/Debug.tsx @@ -0,0 +1,48 @@ +import ContextMenuItem from '@components/ContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import Navigation from '@libs/Navigation/Navigation'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; +import {useContextMenuVisibility} from '../ContextMenuLayout'; +import {useContextMenuPayload} from '../ContextMenuPayloadProvider'; +import {hideContextMenu} from '../ReportActionContextMenu'; +import {ACTION_IDS} from './actionConfig'; + +function Debug() { + const {reportID, reportAction, isMini, interceptAnonymousUser} = useContextMenuPayload(); + const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); + const icons = useMemoizedLazyExpensifyIcons(['Bug'] as const); + const {translate} = useLocalize(); + + const actionIndex = visibleActionIds.indexOf(ACTION_IDS.DEBUG); + if (actionIndex === -1) { + return null; + } + + const handlePress = () => { + if (reportAction) { + Navigation.navigate(ROUTES.DEBUG_REPORT_ACTION.getRoute(reportID ?? '', reportAction.reportActionID)); + } else { + Navigation.navigate(ROUTES.DEBUG_REPORT.getRoute(reportID ?? '')); + } + hideContextMenu(false, ReportActionComposeFocusManager.focus); + }; + + return ( + interceptAnonymousUser(handlePress, true)} + isFocused={focusedIndex === actionIndex} + onFocus={() => setFocusedIndex(actionIndex)} + onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.DEBUG} + /> + ); +} + +export default Debug; diff --git a/src/pages/inbox/report/ContextMenu/actions/Delete.tsx b/src/pages/inbox/report/ContextMenu/actions/Delete.tsx new file mode 100644 index 000000000000..e2b23e56c1c2 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/Delete.tsx @@ -0,0 +1,48 @@ +import ContextMenuItem from '@components/ContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils'; +import CONST from '@src/CONST'; +import {useContextMenuVisibility} from '../ContextMenuLayout'; +import {useContextMenuPayload} from '../ContextMenuPayloadProvider'; +import {hideContextMenu, showDeleteModal} from '../ReportActionContextMenu'; +import {ACTION_IDS} from './actionConfig'; + +function Delete() { + const {reportID, reportAction, moneyRequestAction, isMini} = useContextMenuPayload(); + const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); + const icons = useMemoizedLazyExpensifyIcons(['Trashcan'] as const); + const {translate} = useLocalize(); + + const actionIndex = visibleActionIds.indexOf(ACTION_IDS.DELETE); + if (actionIndex === -1) { + return null; + } + + const closePopover = !isMini; + + const handlePress = () => { + const iouReportID = isMoneyRequestAction(moneyRequestAction) ? getOriginalMessage(moneyRequestAction)?.IOUReportID : undefined; + const effectiveReportID = iouReportID && Number(iouReportID) !== 0 ? iouReportID : (reportID ?? ''); + if (closePopover) { + hideContextMenu(false, () => showDeleteModal(effectiveReportID, moneyRequestAction ?? reportAction)); + return; + } + showDeleteModal(effectiveReportID, moneyRequestAction ?? reportAction); + }; + + return ( + setFocusedIndex(actionIndex)} + onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.DELETE} + /> + ); +} + +export default Delete; diff --git a/src/pages/inbox/report/ContextMenu/actions/Download.tsx b/src/pages/inbox/report/ContextMenu/actions/Download.tsx new file mode 100644 index 000000000000..13946b429b3c --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/Download.tsx @@ -0,0 +1,63 @@ +import ContextMenuItem from '@components/ContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; +import {isMobileSafari} from '@libs/Browser'; +import fileDownload from '@libs/fileDownload'; +import getAttachmentDetails from '@libs/fileDownload/getAttachmentDetails'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {setDownload} from '@userActions/Download'; +import CONST from '@src/CONST'; +import {useContextMenuVisibility} from '../ContextMenuLayout'; +import {useContextMenuPayload} from '../ContextMenuPayloadProvider'; +import {hideContextMenu} from '../ReportActionContextMenu'; +import {ACTION_IDS, getActionHtml} from './actionConfig'; + +function Download() { + const {reportAction, encryptedAuthToken, isMini, interceptAnonymousUser, download} = useContextMenuPayload(); + const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); + const icons = useMemoizedLazyExpensifyIcons(['Download'] as const); + const {translate} = useLocalize(); + + const actionIndex = visibleActionIds.indexOf(ACTION_IDS.DOWNLOAD); + if (actionIndex === -1) { + return null; + } + + const closePopover = !isMini; + const isDownloading = download?.isDownloading ?? false; + + const handlePress = () => { + const html = getActionHtml(reportAction); + const {originalFileName, sourceURL} = getAttachmentDetails(html); + const sourceURLWithAuth = addEncryptedAuthTokenToURL(sourceURL ?? '', encryptedAuthToken); + const sourceID = (sourceURL?.match(CONST.REGEX.ATTACHMENT_ID) ?? [])[1]; + setDownload(sourceID, true); + const anchorRegex = CONST.REGEX_LINK_IN_ANCHOR; + const isAnchorTag = anchorRegex.test(html); + fileDownload(translate, sourceURLWithAuth, originalFileName ?? '', '', isAnchorTag && isMobileSafari()).then(() => setDownload(sourceID, false)); + if (closePopover) { + hideContextMenu(true, ReportActionComposeFocusManager.focus); + } + }; + + return ( + interceptAnonymousUser(handlePress, true)} + isFocused={focusedIndex === actionIndex} + onFocus={() => setFocusedIndex(actionIndex)} + onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.DOWNLOAD} + /> + ); +} + +export default Download; diff --git a/src/pages/inbox/report/ContextMenu/actions/Edit.tsx b/src/pages/inbox/report/ContextMenu/actions/Edit.tsx new file mode 100644 index 000000000000..995a1d348dbc --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/Edit.tsx @@ -0,0 +1,67 @@ +import ContextMenuItem from '@components/ContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import Navigation from '@libs/Navigation/Navigation'; +import Parser from '@libs/Parser'; +import {isMoneyRequestAction} from '@libs/ReportActionsUtils'; +import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import {deleteReportActionDraft, openReport, saveReportActionDraft} from '@userActions/Report'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; +import {ACTION_IDS, getActionHtml} from './actionConfig'; + +function Edit() { + const {reportID, reportAction, moneyRequestAction, draftMessage, introSelected, isMini} = useContextMenuPayload(); + const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Pencil'] as const); + + const actionIndex = visibleActionIds.indexOf(ACTION_IDS.EDIT); + if (actionIndex === -1) { + return null; + } + const closePopover = !isMini; + + return ( + { + if (isMoneyRequestAction(reportAction) || isMoneyRequestAction(moneyRequestAction)) { + const editExpense = () => { + const childReportID = reportAction?.childReportID; + openReport(childReportID, introSelected); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID)); + }; + if (closePopover) { + hideContextMenu(false, editExpense); + return; + } + editExpense(); + return; + } + const editAction = () => { + if (!draftMessage) { + saveReportActionDraft(reportID, reportAction, Parser.htmlToMarkdown(getActionHtml(reportAction))); + } else { + deleteReportActionDraft(reportID, reportAction); + } + }; + if (closePopover) { + hideContextMenu(false, editAction); + return; + } + editAction(); + }} + isFocused={focusedIndex === actionIndex} + onFocus={() => setFocusedIndex(actionIndex)} + onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.EDIT_COMMENT} + /> + ); +} + +export default Edit; diff --git a/src/pages/inbox/report/ContextMenu/actions/EmojiReaction.tsx b/src/pages/inbox/report/ContextMenu/actions/EmojiReaction.tsx new file mode 100644 index 000000000000..189e8941baeb --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/EmojiReaction.tsx @@ -0,0 +1,66 @@ +import type {OnyxEntry} from 'react-native-onyx'; +import type {Emoji} from '@assets/emojis/types'; +import MiniQuickEmojiReactions from '@components/Reactions/MiniQuickEmojiReactions'; +import QuickEmojiReactions from '@components/Reactions/QuickEmojiReactions'; +import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import {toggleEmojiReaction} from '@userActions/Report'; +import type {ReportActionReactions} from '@src/types/onyx'; +import {ACTION_IDS} from './actionConfig'; + +function EmojiReaction() { + const {reportID, reportAction, currentUserAccountID, close, openContextMenu, setIsEmojiPickerActive, isMini} = useContextMenuPayload(); + const {visibleActionIds} = useContextMenuVisibility(); + + if (!visibleActionIds.includes(ACTION_IDS.EMOJI_REACTION)) { + return null; + } + + const closeContextMenu = (onHideCallback?: () => void) => { + if (isMini) { + close(); + if (onHideCallback) { + onHideCallback(); + } + } else { + hideContextMenu(false, onHideCallback); + } + }; + + const toggleEmojiAndCloseMenu = (emoji: Emoji, existingReactions: OnyxEntry, preferredSkinTone: number) => { + toggleEmojiReaction(reportID, reportAction, emoji, existingReactions, preferredSkinTone, currentUserAccountID); + closeContextMenu(); + setIsEmojiPickerActive?.(false); + }; + + if (isMini) { + return ( + { + openContextMenu(); + setIsEmojiPickerActive?.(true); + }} + onEmojiPickerClosed={() => { + closeContextMenu(); + setIsEmojiPickerActive?.(false); + }} + reportActionID={reportAction?.reportActionID} + reportAction={reportAction} + /> + ); + } + + return ( + + ); +} + +export default EmojiReaction; diff --git a/src/pages/inbox/report/ContextMenu/actions/Explain.tsx b/src/pages/inbox/report/ContextMenu/actions/Explain.tsx new file mode 100644 index 000000000000..1a354e2d4f1c --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/Explain.tsx @@ -0,0 +1,50 @@ +import ContextMenuItem from '@components/ContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import {explain} from '@userActions/Report'; +import CONST from '@src/CONST'; +import KeyboardUtils from '@src/utils/keyboard'; +import {ACTION_IDS} from './actionConfig'; + +function Explain() { + const {childReport, originalReport, reportAction, currentUserPersonalDetails, isMini} = useContextMenuPayload(); + const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Concierge'] as const); + + const actionIndex = visibleActionIds.indexOf(ACTION_IDS.EXPLAIN); + if (actionIndex === -1) { + return null; + } + const closePopover = !isMini; + + return ( + { + if (!originalReport?.reportID) { + return; + } + const doExplain = () => explain(childReport, originalReport, reportAction, translate, currentUserPersonalDetails?.accountID ?? 0, currentUserPersonalDetails?.timezone); + if (closePopover) { + hideContextMenu(false, () => { + KeyboardUtils.dismiss().then(doExplain); + }); + return; + } + doExplain(); + }} + isFocused={focusedIndex === actionIndex} + onFocus={() => setFocusedIndex(actionIndex)} + onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.EXPLAIN} + /> + ); +} + +export default Explain; diff --git a/src/pages/inbox/report/ContextMenu/actions/FlagAsOffensive.tsx b/src/pages/inbox/report/ContextMenu/actions/FlagAsOffensive.tsx new file mode 100644 index 000000000000..5018a4839bed --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/FlagAsOffensive.tsx @@ -0,0 +1,56 @@ +import ContextMenuItem from '@components/ContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import Navigation from '@libs/Navigation/Navigation'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; +import KeyboardUtils from '@src/utils/keyboard'; +import {useContextMenuVisibility} from '../ContextMenuLayout'; +import {useContextMenuPayload} from '../ContextMenuPayloadProvider'; +import {hideContextMenu} from '../ReportActionContextMenu'; +import {ACTION_IDS} from './actionConfig'; + +function FlagAsOffensive() { + const {reportID, reportAction, isMini} = useContextMenuPayload(); + const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); + const icons = useMemoizedLazyExpensifyIcons(['Flag'] as const); + const {translate} = useLocalize(); + + const actionIndex = visibleActionIds.indexOf(ACTION_IDS.FLAG_AS_OFFENSIVE); + if (actionIndex === -1) { + return null; + } + + const closePopover = !isMini; + + const handlePress = () => { + if (!reportID) { + return; + } + const activeRoute = Navigation.getActiveRoute(); + if (closePopover) { + hideContextMenu(false, () => { + KeyboardUtils.dismiss().then(() => { + Navigation.navigate(ROUTES.FLAG_COMMENT.getRoute(reportID, reportAction?.reportActionID, activeRoute)); + }); + }); + return; + } + Navigation.navigate(ROUTES.FLAG_COMMENT.getRoute(reportID, reportAction?.reportActionID, activeRoute)); + }; + + return ( + setFocusedIndex(actionIndex)} + onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.FLAG_AS_OFFENSIVE} + /> + ); +} + +export default FlagAsOffensive; diff --git a/src/pages/inbox/report/ContextMenu/actions/Hold.tsx b/src/pages/inbox/report/ContextMenu/actions/Hold.tsx new file mode 100644 index 000000000000..f3fd73eae463 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/Hold.tsx @@ -0,0 +1,50 @@ +import ContextMenuItem from '@components/ContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import {changeMoneyRequestHoldStatus} from '@libs/ReportUtils'; +import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import CONST from '@src/CONST'; +import {ACTION_IDS} from './actionConfig'; + +function Hold() { + const {moneyRequestAction, isDelegateAccessRestricted, showDelegateNoAccessModal, isMini, interceptAnonymousUser} = useContextMenuPayload(); + const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); + const icons = useMemoizedLazyExpensifyIcons(['Stopwatch'] as const); + const {translate} = useLocalize(); + + const actionIndex = visibleActionIds.indexOf(ACTION_IDS.HOLD); + if (actionIndex === -1) { + return null; + } + + const closePopover = !isMini; + + const handlePress = () => { + if (isDelegateAccessRestricted) { + hideContextMenu(false, showDelegateNoAccessModal); + return; + } + if (closePopover) { + hideContextMenu(false, () => changeMoneyRequestHoldStatus(moneyRequestAction)); + return; + } + changeMoneyRequestHoldStatus(moneyRequestAction); + }; + + return ( + interceptAnonymousUser(handlePress, false)} + isFocused={focusedIndex === actionIndex} + onFocus={() => setFocusedIndex(actionIndex)} + onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.HOLD} + /> + ); +} + +export default Hold; diff --git a/src/pages/inbox/report/ContextMenu/actions/JoinThread.tsx b/src/pages/inbox/report/ContextMenu/actions/JoinThread.tsx new file mode 100644 index 000000000000..f0c30dd30e62 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/JoinThread.tsx @@ -0,0 +1,53 @@ +import ContextMenuItem from '@components/ContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {getChildReportNotificationPreference} from '@libs/ReportUtils'; +import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import {toggleSubscribeToChildReport} from '@userActions/Report'; +import CONST from '@src/CONST'; +import {ACTION_IDS} from './actionConfig'; + +function JoinThread() { + const {reportAction, originalReport, currentUserAccountID, isMini, interceptAnonymousUser} = useContextMenuPayload(); + const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); + const icons = useMemoizedLazyExpensifyIcons(['Bell'] as const); + const {translate} = useLocalize(); + + const actionIndex = visibleActionIds.indexOf(ACTION_IDS.JOIN_THREAD); + if (actionIndex === -1) { + return null; + } + + const closePopover = !isMini; + + const handlePress = () => { + const childReportNotificationPreference = getChildReportNotificationPreference(reportAction); + if (closePopover) { + hideContextMenu(false, () => { + ReportActionComposeFocusManager.focus(); + toggleSubscribeToChildReport(reportAction?.childReportID, currentUserAccountID, reportAction, originalReport, childReportNotificationPreference); + }); + return; + } + ReportActionComposeFocusManager.focus(); + toggleSubscribeToChildReport(reportAction?.childReportID, currentUserAccountID, reportAction, originalReport, childReportNotificationPreference); + }; + + return ( + interceptAnonymousUser(handlePress, false)} + isFocused={focusedIndex === actionIndex} + onFocus={() => setFocusedIndex(actionIndex)} + onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.JOIN_THREAD} + /> + ); +} + +export default JoinThread; diff --git a/src/pages/inbox/report/ContextMenu/actions/LeaveThread.tsx b/src/pages/inbox/report/ContextMenu/actions/LeaveThread.tsx new file mode 100644 index 000000000000..3cfad706fa23 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/LeaveThread.tsx @@ -0,0 +1,53 @@ +import ContextMenuItem from '@components/ContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {getChildReportNotificationPreference} from '@libs/ReportUtils'; +import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import {toggleSubscribeToChildReport} from '@userActions/Report'; +import CONST from '@src/CONST'; +import {ACTION_IDS} from './actionConfig'; + +function LeaveThread() { + const {reportAction, originalReport, currentUserAccountID, isMini, interceptAnonymousUser} = useContextMenuPayload(); + const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); + const icons = useMemoizedLazyExpensifyIcons(['Exit'] as const); + const {translate} = useLocalize(); + + const actionIndex = visibleActionIds.indexOf(ACTION_IDS.LEAVE_THREAD); + if (actionIndex === -1) { + return null; + } + + const closePopover = !isMini; + + const handlePress = () => { + const childReportNotificationPreference = getChildReportNotificationPreference(reportAction); + if (closePopover) { + hideContextMenu(false, () => { + ReportActionComposeFocusManager.focus(); + toggleSubscribeToChildReport(reportAction?.childReportID, currentUserAccountID, reportAction, originalReport, childReportNotificationPreference); + }); + return; + } + ReportActionComposeFocusManager.focus(); + toggleSubscribeToChildReport(reportAction?.childReportID, currentUserAccountID, reportAction, originalReport, childReportNotificationPreference); + }; + + return ( + interceptAnonymousUser(handlePress, false)} + isFocused={focusedIndex === actionIndex} + onFocus={() => setFocusedIndex(actionIndex)} + onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.LEAVE_THREAD} + /> + ); +} + +export default LeaveThread; diff --git a/src/pages/inbox/report/ContextMenu/actions/MarkAsRead.tsx b/src/pages/inbox/report/ContextMenu/actions/MarkAsRead.tsx new file mode 100644 index 000000000000..4ccb5600b896 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/MarkAsRead.tsx @@ -0,0 +1,44 @@ +import ContextMenuItem from '@components/ContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import {readNewestAction} from '@userActions/Report'; +import CONST from '@src/CONST'; +import {ACTION_IDS} from './actionConfig'; + +function MarkAsRead() { + const {reportID, isMini} = useContextMenuPayload(); + const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Mail', 'Checkmark'] as const); + + const actionIndex = visibleActionIds.indexOf(ACTION_IDS.MARK_AS_READ); + if (actionIndex === -1) { + return null; + } + const closePopover = !isMini; + + return ( + { + readNewestAction(reportID, true, true); + if (closePopover) { + hideContextMenu(true, ReportActionComposeFocusManager.focus); + } + }} + isFocused={focusedIndex === actionIndex} + onFocus={() => setFocusedIndex(actionIndex)} + onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.MARK_AS_READ} + /> + ); +} + +export default MarkAsRead; diff --git a/src/pages/inbox/report/ContextMenu/actions/MarkAsUnread.tsx b/src/pages/inbox/report/ContextMenu/actions/MarkAsUnread.tsx new file mode 100644 index 000000000000..163599e5a4ab --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/MarkAsUnread.tsx @@ -0,0 +1,44 @@ +import ContextMenuItem from '@components/ContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import {markCommentAsUnread} from '@userActions/Report'; +import CONST from '@src/CONST'; +import {ACTION_IDS} from './actionConfig'; + +function MarkAsUnread() { + const {reportID, reportActions, reportAction, currentUserAccountID, isMini} = useContextMenuPayload(); + const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['ChatBubbleUnread', 'Checkmark'] as const); + + const actionIndex = visibleActionIds.indexOf(ACTION_IDS.MARK_AS_UNREAD); + if (actionIndex === -1) { + return null; + } + const closePopover = !isMini; + + return ( + { + markCommentAsUnread(reportID, reportActions, reportAction, currentUserAccountID); + if (closePopover) { + hideContextMenu(true, ReportActionComposeFocusManager.focus); + } + }} + isFocused={focusedIndex === actionIndex} + onFocus={() => setFocusedIndex(actionIndex)} + onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.MARK_AS_UNREAD} + /> + ); +} + +export default MarkAsUnread; diff --git a/src/pages/inbox/report/ContextMenu/actions/OverflowMenu.tsx b/src/pages/inbox/report/ContextMenu/actions/OverflowMenu.tsx new file mode 100644 index 000000000000..e5b0d5367c4b --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/OverflowMenu.tsx @@ -0,0 +1,45 @@ +import {useRef} from 'react'; +import type {GestureResponderEvent, View} from 'react-native'; +import ContextMenuItem from '@components/ContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import CONST from '@src/CONST'; +import {useContextMenuVisibility} from '../ContextMenuLayout'; +import {useContextMenuPayload} from '../ContextMenuPayloadProvider'; +import {ACTION_IDS} from './actionConfig'; + +function OverflowMenu() { + const {openOverflowMenu, openContextMenu, isMini, interceptAnonymousUser} = useContextMenuPayload(); + const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); + const icons = useMemoizedLazyExpensifyIcons(['ThreeDots'] as const); + const {translate} = useLocalize(); + const threeDotRef = useRef(null); + + const actionIndex = visibleActionIds.indexOf(ACTION_IDS.OVERFLOW_MENU); + if (actionIndex === -1) { + return null; + } + + const handlePress = (event?: GestureResponderEvent | MouseEvent | KeyboardEvent) => { + openOverflowMenu(event as GestureResponderEvent | MouseEvent, threeDotRef); + openContextMenu(); + }; + + return ( + interceptAnonymousUser(() => handlePress(event), true)} + isAnonymousAction + isFocused={focusedIndex === actionIndex} + onFocus={() => setFocusedIndex(actionIndex)} + onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + shouldPreventDefaultFocusOnPress={false} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.MENU} + /> + ); +} + +export default OverflowMenu; diff --git a/src/pages/inbox/report/ContextMenu/actions/Pin.tsx b/src/pages/inbox/report/ContextMenu/actions/Pin.tsx new file mode 100644 index 000000000000..e1ed4e98c3f0 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/Pin.tsx @@ -0,0 +1,46 @@ +import ContextMenuItem from '@components/ContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {togglePinnedState} from '@userActions/Report'; +import CONST from '@src/CONST'; +import {useContextMenuVisibility} from '../ContextMenuLayout'; +import {useContextMenuPayload} from '../ContextMenuPayloadProvider'; +import {hideContextMenu} from '../ReportActionContextMenu'; +import {ACTION_IDS} from './actionConfig'; + +function Pin() { + const {reportID, isMini} = useContextMenuPayload(); + const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); + const icons = useMemoizedLazyExpensifyIcons(['Pin'] as const); + const {translate} = useLocalize(); + + const actionIndex = visibleActionIds.indexOf(ACTION_IDS.PIN); + if (actionIndex === -1) { + return null; + } + + const closePopover = !isMini; + + const handlePress = () => { + togglePinnedState(reportID, false); + if (closePopover) { + hideContextMenu(false, ReportActionComposeFocusManager.focus); + } + }; + + return ( + setFocusedIndex(actionIndex)} + onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.PIN} + /> + ); +} + +export default Pin; diff --git a/src/pages/inbox/report/ContextMenu/actions/ReplyInThread.tsx b/src/pages/inbox/report/ContextMenu/actions/ReplyInThread.tsx new file mode 100644 index 000000000000..01a77de5c91f --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/ReplyInThread.tsx @@ -0,0 +1,50 @@ +import ContextMenuItem from '@components/ContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import {navigateToAndOpenChildReport} from '@userActions/Report'; +import CONST from '@src/CONST'; +import KeyboardUtils from '@src/utils/keyboard'; +import {ACTION_IDS} from './actionConfig'; + +function ReplyInThread() { + const {childReport, reportAction, originalReport, currentUserAccountID, interceptAnonymousUser, isMini} = useContextMenuPayload(); + const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['ChatBubbleReply'] as const); + + const actionIndex = visibleActionIds.indexOf(ACTION_IDS.REPLY_IN_THREAD); + if (actionIndex === -1) { + return null; + } + const closePopover = !isMini; + + return ( + + interceptAnonymousUser(() => { + if (closePopover) { + hideContextMenu(false, () => { + KeyboardUtils.dismiss().then(() => { + navigateToAndOpenChildReport(childReport, reportAction, originalReport, currentUserAccountID); + }); + }); + return; + } + navigateToAndOpenChildReport(childReport, reportAction, originalReport, currentUserAccountID); + }, false) + } + isFocused={focusedIndex === actionIndex} + onFocus={() => setFocusedIndex(actionIndex)} + onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.REPLY_IN_THREAD} + /> + ); +} + +export default ReplyInThread; diff --git a/src/pages/inbox/report/ContextMenu/actions/Unhold.tsx b/src/pages/inbox/report/ContextMenu/actions/Unhold.tsx new file mode 100644 index 000000000000..bc94f04ce094 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/Unhold.tsx @@ -0,0 +1,50 @@ +import ContextMenuItem from '@components/ContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import {changeMoneyRequestHoldStatus} from '@libs/ReportUtils'; +import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import CONST from '@src/CONST'; +import {ACTION_IDS} from './actionConfig'; + +function Unhold() { + const {moneyRequestAction, isDelegateAccessRestricted, showDelegateNoAccessModal, isMini, interceptAnonymousUser} = useContextMenuPayload(); + const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); + const icons = useMemoizedLazyExpensifyIcons(['Stopwatch'] as const); + const {translate} = useLocalize(); + + const actionIndex = visibleActionIds.indexOf(ACTION_IDS.UNHOLD); + if (actionIndex === -1) { + return null; + } + + const closePopover = !isMini; + + const handlePress = () => { + if (isDelegateAccessRestricted) { + hideContextMenu(false, showDelegateNoAccessModal); + return; + } + if (closePopover) { + hideContextMenu(false, () => changeMoneyRequestHoldStatus(moneyRequestAction)); + return; + } + changeMoneyRequestHoldStatus(moneyRequestAction); + }; + + return ( + interceptAnonymousUser(handlePress, false)} + isFocused={focusedIndex === actionIndex} + onFocus={() => setFocusedIndex(actionIndex)} + onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.UNHOLD} + /> + ); +} + +export default Unhold; diff --git a/src/pages/inbox/report/ContextMenu/actions/Unpin.tsx b/src/pages/inbox/report/ContextMenu/actions/Unpin.tsx new file mode 100644 index 000000000000..e3694936308d --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/Unpin.tsx @@ -0,0 +1,46 @@ +import ContextMenuItem from '@components/ContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {togglePinnedState} from '@userActions/Report'; +import CONST from '@src/CONST'; +import {useContextMenuVisibility} from '../ContextMenuLayout'; +import {useContextMenuPayload} from '../ContextMenuPayloadProvider'; +import {hideContextMenu} from '../ReportActionContextMenu'; +import {ACTION_IDS} from './actionConfig'; + +function Unpin() { + const {reportID, isMini} = useContextMenuPayload(); + const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); + const icons = useMemoizedLazyExpensifyIcons(['Pin'] as const); + const {translate} = useLocalize(); + + const actionIndex = visibleActionIds.indexOf(ACTION_IDS.UNPIN); + if (actionIndex === -1) { + return null; + } + + const closePopover = !isMini; + + const handlePress = () => { + togglePinnedState(reportID, true); + if (closePopover) { + hideContextMenu(false, ReportActionComposeFocusManager.focus); + } + }; + + return ( + setFocusedIndex(actionIndex)} + onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.UNPIN} + /> + ); +} + +export default Unpin; diff --git a/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts b/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts index 5adce1a794f5..bc7316102599 100644 --- a/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts +++ b/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts @@ -25,9 +25,9 @@ import { shouldDisableThread, shouldDisplayThreadReplies, } from '@libs/ReportUtils'; +import type {ContextMenuAnchor} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import type {Beta, Policy, ReportAction, Report as ReportType, Transaction} from '@src/types/onyx'; -import type {ContextMenuAnchor} from '../ReportActionContextMenu'; const ACTION_IDS = { EMOJI_REACTION: 'emojiReaction', diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx index ee510af435b0..629d2f6a48e7 100644 --- a/src/pages/inbox/report/PureReportActionItem.tsx +++ b/src/pages/inbox/report/PureReportActionItem.tsx @@ -247,7 +247,7 @@ import type * as OnyxTypes from '@src/types/onyx'; import type {Errors} from '@src/types/onyx/OnyxCommon'; import type {JoinWorkspaceResolution} from '@src/types/onyx/OriginalMessage'; import {isEmptyObject, isEmptyValueObject} from '@src/types/utils/EmptyObject'; -import {RestrictedReadOnlyContextMenuActions} from './ContextMenu/ContextMenuActions'; +import {RESTRICTED_READONLY_ACTION_IDS} from './ContextMenu/actions/actionConfig'; import {useMiniContextMenuActions} from './ContextMenu/MiniContextMenuProvider'; import type {ContextMenuAnchor} from './ContextMenu/ReportActionContextMenu'; import {hideContextMenu, hideDeleteModal, isActiveReportAction, showContextMenu} from './ContextMenu/ReportActionContextMenu'; @@ -263,6 +263,8 @@ import ReportActionItemSingle from './ReportActionItemSingle'; import ReportActionItemThread from './ReportActionItemThread'; import TripSummary from './TripSummary'; +const EMPTY_SET = new Set(); + type PureReportActionItemProps = { /** All the data of the policy collection */ policies: OnyxCollection; @@ -771,7 +773,7 @@ function PureReportActionItem({ [transitionActionSheetState], ); - const disabledActions = useMemo(() => (!canWriteInReport(report) ? RestrictedReadOnlyContextMenuActions : []), [report]); + const disabledActionIds = !canWriteInReport(report) ? RESTRICTED_READONLY_ACTION_IDS : EMPTY_SET; /** * Show the ReportActionContextMenu modal popover. @@ -809,7 +811,7 @@ function PureReportActionItem({ onHide: toggleContextMenuFromActiveReportAction, setIsEmojiPickerActive: setIsEmojiPickerActive as () => void, }, - disabledOptions: disabledActions, + disabledActionIds, }); }); }, @@ -821,7 +823,7 @@ function PureReportActionItem({ toggleContextMenuFromActiveReportAction, originalReportID, shouldDisplayContextMenu, - disabledActions, + disabledActionIds, isArchivedRoom, isChronosReport, handleShowContextMenu, @@ -2068,16 +2070,16 @@ function PureReportActionItem({ } const rect = node.getBoundingClientRect(); showMiniContextMenu({ - reportID: reportID ?? '', + reportID, reportActionID: action.reportActionID, - originalReportID: originalReportID ?? '', + originalReportID, anchor: popoverAnchorRef, displayAsGroup: !!displayAsGroup, isArchivedRoom: !!isArchivedRoom, isThreadReportParentAction: !!isThreadReportParentAction, draftMessage, isChronosReport: !!isChronosReport, - disabledActions, + disabledActionIds, checkIfContextMenuActive: toggleContextMenuFromActiveReportAction, setIsEmojiPickerActive, rowMeasurements: { From e3e49ff1eb7ca088549cd2cbe8ffbd0e69ae2f18 Mon Sep 17 00:00:00 2001 From: rory Date: Sat, 28 Feb 2026 01:32:50 -0800 Subject: [PATCH 09/88] chore(contextmenu): polish and reduce lint warning budget Organize component internals: hooks, derived values, callbacks, effects, render. Fix deprecated getReportNameDeprecated usage (add eslint-disable), fix no-default-id-values errors in Debug, Delete, and Explain. Use Reanimated .get()/.set() API. Fix import aliases for @pages prefix. Clean up unused imports. Reduce lint warning budget from 383 to 353 (30 warnings eliminated). Made-with: Cursor --- package.json | 2 +- .../BaseReportActionContextMenu.tsx | 60 ++++++++----------- .../ConfirmDeleteReportActionModal.tsx | 2 +- .../report/ContextMenu/ContextMenuLayout.tsx | 4 +- .../ContextMenu/MiniContextMenuProvider.tsx | 9 +-- .../PopoverReportActionContextMenu.tsx | 28 ++------- .../report/ContextMenu/actions/CopyEmail.tsx | 6 +- .../report/ContextMenu/actions/CopyLink.tsx | 6 +- .../ContextMenu/actions/CopyMessage.tsx | 10 ++-- .../ContextMenu/actions/CopyOnyxData.tsx | 6 +- .../ContextMenu/actions/CopyToClipboard.tsx | 2 +- .../report/ContextMenu/actions/CopyURL.tsx | 2 +- .../report/ContextMenu/actions/Debug.tsx | 13 ++-- .../report/ContextMenu/actions/Delete.tsx | 8 +-- .../report/ContextMenu/actions/Download.tsx | 6 +- .../inbox/report/ContextMenu/actions/Edit.tsx | 56 ++++++++--------- .../report/ContextMenu/actions/Explain.tsx | 29 +++++---- .../ContextMenu/actions/FlagAsOffensive.tsx | 6 +- .../inbox/report/ContextMenu/actions/Hold.tsx | 3 +- .../report/ContextMenu/actions/JoinThread.tsx | 3 +- .../ContextMenu/actions/LeaveThread.tsx | 3 +- .../ContextMenu/actions/OverflowMenu.tsx | 4 +- .../inbox/report/ContextMenu/actions/Pin.tsx | 6 +- .../ContextMenu/actions/ReplyInThread.tsx | 27 +++++---- .../report/ContextMenu/actions/Unhold.tsx | 3 +- .../report/ContextMenu/actions/Unpin.tsx | 6 +- 26 files changed, 143 insertions(+), 167 deletions(-) diff --git a/package.json b/package.json index 36a55732331c..4b890c5de2f3 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "perf-test": "NODE_OPTIONS=--experimental-vm-modules npx reassure", "typecheck": "NODE_OPTIONS=--max_old_space_size=8192 tsc", "typecheck-tsgo": "tsgo --project tsconfig.tsgo.json", - "lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=383 --cache --cache-location=node_modules/.cache/eslint --cache-strategy content --concurrency=auto", + "lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=353 --cache --cache-location=node_modules/.cache/eslint --cache-strategy content --concurrency=auto", "lint-changed": "NODE_OPTIONS=--max_old_space_size=8192 ./scripts/lintChanged.sh", "check-lazy-loading": "ts-node scripts/checkLazyLoading.ts", "lint-watch": "npx eslint-watch --watch --changed", diff --git a/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx index 2889a4c8f6a0..e3e77c32c246 100755 --- a/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx +++ b/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx @@ -90,10 +90,11 @@ function BaseReportActionContextMenu({ const {showDelegateNoAccessModal} = useDelegateNoAccessActions(); const [localShouldKeepOpen, setLocalShouldKeepOpen] = useState(false); const miniActions = useMiniContextMenuActions(); - const shouldKeepOpen = isMini ? false : localShouldKeepOpen; const {translate, getLocalDateFromDatetime} = useLocalize(); const {isOffline} = useNetwork(); const {isProduction} = useEnvironment(); + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); + const encryptedAuthToken = useSession()?.encryptedAuthToken ?? ''; const [betas] = useOnyx(ONYXKEYS.BETAS); const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, { @@ -104,44 +105,44 @@ function BaseReportActionContextMenu({ canEvict: false, selector: withDEWRoutedActionsObject, }); + const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); + const [originalReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${originalReportID}`); + const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${getNonEmptyStringOnyxID(reportID)}`); + const [isDebugModeEnabled] = useOnyx(ONYXKEYS.IS_DEBUG_MODE_ENABLED); + const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT); + const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); + + const hasValidReportAction = !isEmptyObject(originalReportActions) && reportActionID && reportActionID !== '0' && reportActionID !== '-1'; + const reportAction: OnyxEntry = hasValidReportAction ? originalReportActions[reportActionID] : undefined; - const reportAction: OnyxEntry = - isEmptyObject(originalReportActions) || reportActionID === '0' || reportActionID === '-1' || !reportActionID ? undefined : originalReportActions[reportActionID]; const transactionID = getLinkedTransactionID(reportAction); const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(transactionID)}`); - const [isDebugModeEnabled] = useOnyx(ONYXKEYS.IS_DEBUG_MODE_ENABLED); - const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); - const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${getNonEmptyStringOnyxID(reportID)}`); const [harvestReport] = useOnyx( `${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(getHarvestOriginalReportID(reportNameValuePairs?.origin, reportNameValuePairs?.originalID))}`, {}, ); - const [originalReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${originalReportID}`); - const isOriginalReportArchived = useReportIsArchived(originalReportID); const policyID = report?.policyID; const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`); - const [movedFromReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(reportAction, CONST.REPORT.MOVE_TYPE.FROM)}`); const [movedToReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(reportAction, CONST.REPORT.MOVE_TYPE.TO)}`); - - const sourceID = getSourceIDFromReportAction(reportAction); - - const [download] = useOnyx(`${ONYXKEYS.COLLECTION.DOWNLOAD}${sourceID}`); + const [download] = useOnyx(`${ONYXKEYS.COLLECTION.DOWNLOAD}${getSourceIDFromReportAction(reportAction)}`); const [childReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportAction?.childReportID}`); const [childReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportAction?.childReportID}`); const [childChatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${childReport?.chatReportID}`); + const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${childReport?.parentReportID}`); const parentReportAction = getReportAction(childReport?.parentReportID, childReport?.parentReportActionID); const {reportActions: paginatedReportActions} = usePaginatedReportActions(childReport?.reportID); - const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const transactionThreadReportID = getOneTransactionThreadReportID(childReport, childChatReport, paginatedReportActions ?? [], isOffline); - const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(transactionThreadReportID)}`); + const isOriginalReportArchived = useReportIsArchived(originalReportID); + const isChildReportArchived = useReportIsArchived(childReport?.reportID); + const isParentReportArchived = useReportIsArchived(childReport?.parentReportID); + const isMoneyRequestReport = ReportUtilsIsMoneyRequestReport(childReport); const isInvoiceReport = ReportUtilsIsInvoiceReport(childReport); - let requestParentReportAction; if (isMoneyRequestReport || isInvoiceReport) { if (transactionThreadReportID === CONST.FAKE_REPORT_ID) { @@ -152,35 +153,28 @@ function BaseReportActionContextMenu({ } else { requestParentReportAction = parentReportAction; } - const moneyRequestAction = transactionThreadReportID ? requestParentReportAction : parentReportAction; - const isChildReportArchived = useReportIsArchived(childReport?.reportID); - const isParentReportArchived = useReportIsArchived(childReport?.parentReportID); - const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${childReport?.parentReportID}`); + const iouTransactionID = (getOriginalMessage(moneyRequestAction ?? reportAction) as OriginalMessageIOU)?.IOUTransactionID; - const [iouTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(iouTransactionID)}`); const iouReportID = (getOriginalMessage(moneyRequestAction ?? reportAction) as OriginalMessageIOU)?.IOUReportID; + const [iouTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(iouTransactionID)}`); const [moneyRequestReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`); const [moneyRequestPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${moneyRequestReport?.policyID}`); const {transactions} = useTransactionsAndViolationsForReport(childReport?.reportID); - const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT); - const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); - - const isTryNewDotNVPDismissed = !!tryNewDot?.classicRedirect?.dismissed; - const session = useSession(); - const encryptedAuthToken = session?.encryptedAuthToken ?? ''; const isMoneyRequest = ReportUtilsIsMoneyRequest(childReport); const isTrackExpenseReport = ReportUtilsIsTrackExpenseReport(childReport); const isSingleTransactionView = isMoneyRequest || isTrackExpenseReport; const isMoneyRequestOrReport = isMoneyRequestReport || isSingleTransactionView; - - const areHoldRequirementsMet = - !isInvoiceReport && - isMoneyRequestOrReport && - !isArchivedNonExpenseReport(transactionThreadReportID ? childReport : parentReport, transactionThreadReportID ? isChildReportArchived : isParentReportArchived); + const archivedReportForHold = transactionThreadReportID ? childReport : parentReport; + const isArchivedForHold = transactionThreadReportID ? isChildReportArchived : isParentReportArchived; + const areHoldRequirementsMet = !isInvoiceReport && isMoneyRequestOrReport && !isArchivedNonExpenseReport(archivedReportForHold, isArchivedForHold); const isHarvestReport = isHarvestCreatedExpenseReport(reportNameValuePairs?.origin, reportNameValuePairs?.originalID); + const isTryNewDotNVPDismissed = !!tryNewDot?.classicRedirect?.dismissed; + const shouldKeepOpen = isMini ? false : localShouldKeepOpen; + + useRestoreInputFocus(isVisible); const interceptAnonymousUser = (callback: () => void, isAnonymousAction = false) => { if (isAnonymousUser() && !isAnonymousAction) { @@ -194,8 +188,6 @@ function BaseReportActionContextMenu({ } }; - useRestoreInputFocus(isVisible); - const openOverflowMenu = (event: GestureResponderEvent | MouseEvent, anchorRef: RefObject) => { showContextMenu({ type: CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, diff --git a/src/pages/inbox/report/ContextMenu/ConfirmDeleteReportActionModal.tsx b/src/pages/inbox/report/ContextMenu/ConfirmDeleteReportActionModal.tsx index b555df1e1b61..94e510eadb73 100644 --- a/src/pages/inbox/report/ContextMenu/ConfirmDeleteReportActionModal.tsx +++ b/src/pages/inbox/report/ContextMenu/ConfirmDeleteReportActionModal.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useRef, useState} from 'react'; +import {useEffect, useRef, useState} from 'react'; import {DeviceEventEmitter, InteractionManager} from 'react-native'; import ConfirmModal from '@components/ConfirmModal'; import type {ModalProps} from '@components/Modal/Global/ModalContext'; diff --git a/src/pages/inbox/report/ContextMenu/ContextMenuLayout.tsx b/src/pages/inbox/report/ContextMenu/ContextMenuLayout.tsx index bdc993b42920..6224021b23bc 100644 --- a/src/pages/inbox/report/ContextMenu/ContextMenuLayout.tsx +++ b/src/pages/inbox/report/ContextMenu/ContextMenuLayout.tsx @@ -1,4 +1,4 @@ -import type {ReactNode} from 'react'; +import type {ReactNode, RefObject} from 'react'; import {createContext, useContext} from 'react'; import {View} from 'react-native'; import FocusTrapForModal from '@components/FocusTrap/FocusTrapForModal'; @@ -30,7 +30,7 @@ type ContextMenuLayoutProps = { isMini: boolean; isVisible: boolean; shouldKeepOpen: boolean; - contentRef?: React.RefObject; + contentRef?: RefObject; children: ReactNode; }; diff --git a/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx b/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx index 70884bc96d54..f205ec9ee923 100644 --- a/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx @@ -60,7 +60,7 @@ function MiniContextMenuProvider({children}: MiniContextMenuProviderProps) { const [actions] = useState(() => { const clearHideTimer = () => { - if (hideTimerRef.current === null) { + if (hideTimerRef.current == null) { return; } clearTimeout(hideTimerRef.current); @@ -69,12 +69,7 @@ function MiniContextMenuProvider({children}: MiniContextMenuProviderProps) { const performHide = () => { clearHideTimer(); - setState((prev) => { - if (!prev) { - return null; - } - return {...prev, isVisible: false}; - }); + setState((prev) => (prev ? {...prev, isVisible: false} : null)); }; return { diff --git a/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx index 91c2e189a2fb..ae454fbbe3af 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -79,7 +79,6 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro const onPopoverHide = useRef(() => {}); const onPopoverHideActionCallback = useRef(() => {}); - /** Get the Context menu anchor position. We calculate the anchor coordinates from measureInWindow async method */ const getContextMenuMeasuredLocation = () => new Promise<{x: number; y: number}>((resolve) => { if (contextMenuAnchorRef.current && 'measureInWindow' in contextMenuAnchorRef.current && typeof contextMenuAnchorRef.current.measureInWindow === 'function') { @@ -89,7 +88,6 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro } }); - /** This gets called on Dimensions change to find the anchor coordinates for the action context menu. */ const measureContextMenuAnchorPosition = () => { if (!isPopoverVisible) { return; @@ -128,16 +126,12 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro // eslint-disable-next-line react-hooks/exhaustive-deps }, [isPopoverVisible]); - /** Whether Context Menu is active for the Report Action. */ const isActiveReportAction: ReportActionContextMenu['isActiveReportAction'] = (actionID) => !!actionID && reportActionID === String(actionID); const clearActiveReportAction = () => { setMenuState(null); }; - /** - * Show the ReportActionContextMenu modal popover. - */ const showContextMenu: ReportActionContextMenu['showContextMenu'] = (showContextMenuParams) => { const { type, @@ -176,12 +170,14 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro onPopoverHide.current = onHide; new Promise((resolve) => { - if (!!(!pageX && !pageY && contextMenuAnchorRef.current) || isOverflowMenu) { - calculateAnchorPosition(contextMenuAnchorRef.current).then((position) => { + const anchor = contextMenuAnchorRef.current; + const useAnchorPosition = isOverflowMenu || (anchor != null && !pageX && !pageY); + if (useAnchorPosition && anchor) { + calculateAnchorPosition(anchor).then((position) => { resolve({ anchorHorizontal: position.horizontal, anchorVertical: position.vertical, - anchorWidth: position.vertical, + anchorWidth: position.width, anchorHeight: position.height, }); }); @@ -224,26 +220,20 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro }); }; - /** After Popover shows, call the registered onPopoverShow callback and reset it */ const runAndResetOnPopoverShow = () => { instanceIDRef.current = Math.random().toString(36).slice(2, 7); onPopoverShow.current(); - onPopoverShow.current = () => {}; - - // After the context menu opening animation ends reset isContextMenuOpening. setTimeout(() => { setIsContextMenuOpening(false); }, CONST.ANIMATED_TRANSITION); }; - /** Run the callback and return a noop function to reset it */ const runAndResetCallback = (callback: () => void) => { callback(); return () => {}; }; - /** After Popover hides, call the registered onPopoverHide & onPopoverHideActionCallback callback and reset it */ const runAndResetOnPopoverHide = () => { setMenuState(null); instanceIDRef.current = ''; @@ -252,9 +242,6 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro onPopoverHideActionCallback.current = runAndResetCallback(onPopoverHideActionCallback.current); }; - /** - * Hide the ReportActionContextMenu modal popover. - */ const hideContextMenu: ReportActionContextMenu['hideContextMenu'] = (hideContextMenuParams) => { const {callbacks = {}} = hideContextMenuParams ?? {}; @@ -273,11 +260,8 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro }); }; - const hideDeleteModal = () => { - // No-op: delete modal lifecycle is managed by the global modal system - }; + const hideDeleteModal = () => {}; - /** Opens the Confirm delete action modal via the global modal system */ const showDeleteModal: ReportActionContextMenu['showDeleteModal'] = (showReportID, showReportAction, _shouldSetModalVisibility, onConfirm = () => {}, onCancel = () => {}) => { if (!showReportID || !showReportAction?.reportActionID) { return; diff --git a/src/pages/inbox/report/ContextMenu/actions/CopyEmail.tsx b/src/pages/inbox/report/ContextMenu/actions/CopyEmail.tsx index 90b2cfb01120..ec6cba23f55e 100644 --- a/src/pages/inbox/report/ContextMenu/actions/CopyEmail.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/CopyEmail.tsx @@ -4,10 +4,10 @@ import useLocalize from '@hooks/useLocalize'; import Clipboard from '@libs/Clipboard'; import EmailUtils from '@libs/EmailUtils'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; -import {useContextMenuVisibility} from '../ContextMenuLayout'; -import {useContextMenuPayload} from '../ContextMenuPayloadProvider'; -import {hideContextMenu} from '../ReportActionContextMenu'; import {ACTION_IDS} from './actionConfig'; function CopyEmail() { diff --git a/src/pages/inbox/report/ContextMenu/actions/CopyLink.tsx b/src/pages/inbox/report/ContextMenu/actions/CopyLink.tsx index 56bb8586ec3d..8eae26dc718d 100644 --- a/src/pages/inbox/report/ContextMenu/actions/CopyLink.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/CopyLink.tsx @@ -4,10 +4,10 @@ import useLocalize from '@hooks/useLocalize'; import Clipboard from '@libs/Clipboard'; import {getEnvironmentURL} from '@libs/Environment/Environment'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; -import {useContextMenuVisibility} from '../ContextMenuLayout'; -import {useContextMenuPayload} from '../ContextMenuPayloadProvider'; -import {hideContextMenu} from '../ReportActionContextMenu'; import {ACTION_IDS} from './actionConfig'; function CopyLink() { diff --git a/src/pages/inbox/report/ContextMenu/actions/CopyMessage.tsx b/src/pages/inbox/report/ContextMenu/actions/CopyMessage.tsx index a5a0f83fbb78..732a51fabae5 100644 --- a/src/pages/inbox/report/ContextMenu/actions/CopyMessage.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/CopyMessage.tsx @@ -1,5 +1,4 @@ import {Str} from 'expensify-common'; -import type {OnyxEntry} from 'react-native-onyx'; import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; @@ -139,12 +138,12 @@ import { isExpenseReport, } from '@libs/ReportUtils'; import {getTaskCreatedMessage, getTaskReportActionMessage} from '@libs/TaskUtils'; +import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; +import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import type {ReportAction} from '@src/types/onyx'; -import {useContextMenuVisibility} from '../ContextMenuLayout'; -import type {ContextMenuPayloadContextValue} from '../ContextMenuPayloadProvider'; -import {useContextMenuPayload} from '../ContextMenuPayloadProvider'; -import {hideContextMenu} from '../ReportActionContextMenu'; import {ACTION_IDS, getActionHtml} from './actionConfig'; function setClipboardMessage(content: string | undefined) { @@ -220,6 +219,7 @@ function copyMessageToClipboard(payload: ContextMenuPayloadContextValue) { const taskPreviewMessage = getTaskCreatedMessage(translate, reportAction, childReport, true); Clipboard.setString(taskPreviewMessage); } else if (isMemberChangeAction(reportAction)) { + // eslint-disable-next-line @typescript-eslint/no-deprecated const logMessage = getMemberChangeMessageFragment(translate, reportAction, getReportNameDeprecated).html ?? ''; setClipboardMessage(logMessage); } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_NAME) { diff --git a/src/pages/inbox/report/ContextMenu/actions/CopyOnyxData.tsx b/src/pages/inbox/report/ContextMenu/actions/CopyOnyxData.tsx index 60ca2db65097..048bd585478e 100644 --- a/src/pages/inbox/report/ContextMenu/actions/CopyOnyxData.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/CopyOnyxData.tsx @@ -3,10 +3,10 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import Clipboard from '@libs/Clipboard'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; -import {useContextMenuVisibility} from '../ContextMenuLayout'; -import {useContextMenuPayload} from '../ContextMenuPayloadProvider'; -import {hideContextMenu} from '../ReportActionContextMenu'; import {ACTION_IDS} from './actionConfig'; function CopyOnyxData() { diff --git a/src/pages/inbox/report/ContextMenu/actions/CopyToClipboard.tsx b/src/pages/inbox/report/ContextMenu/actions/CopyToClipboard.tsx index ac2d42cd32f3..04238e53ca6a 100644 --- a/src/pages/inbox/report/ContextMenu/actions/CopyToClipboard.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/CopyToClipboard.tsx @@ -12,8 +12,8 @@ import {ACTION_IDS} from './actionConfig'; function CopyToClipboard() { const {selection, isMini, interceptAnonymousUser} = useContextMenuPayload(); const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); - const icons = useMemoizedLazyExpensifyIcons(['Copy', 'Checkmark'] as const); const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Copy', 'Checkmark'] as const); const actionIndex = visibleActionIds.indexOf(ACTION_IDS.COPY_TO_CLIPBOARD); if (actionIndex === -1) { diff --git a/src/pages/inbox/report/ContextMenu/actions/CopyURL.tsx b/src/pages/inbox/report/ContextMenu/actions/CopyURL.tsx index 3a8fdcbb2a86..547e750c2f3c 100644 --- a/src/pages/inbox/report/ContextMenu/actions/CopyURL.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/CopyURL.tsx @@ -12,8 +12,8 @@ import {ACTION_IDS} from './actionConfig'; function CopyURL() { const {selection, isMini, interceptAnonymousUser} = useContextMenuPayload(); const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); - const icons = useMemoizedLazyExpensifyIcons(['Copy', 'Checkmark'] as const); const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Copy', 'Checkmark'] as const); const actionIndex = visibleActionIds.indexOf(ACTION_IDS.COPY_URL); if (actionIndex === -1) { diff --git a/src/pages/inbox/report/ContextMenu/actions/Debug.tsx b/src/pages/inbox/report/ContextMenu/actions/Debug.tsx index f94596802e31..0d28718f1950 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Debug.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/Debug.tsx @@ -3,11 +3,11 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; -import {useContextMenuVisibility} from '../ContextMenuLayout'; -import {useContextMenuPayload} from '../ContextMenuPayloadProvider'; -import {hideContextMenu} from '../ReportActionContextMenu'; import {ACTION_IDS} from './actionConfig'; function Debug() { @@ -22,10 +22,13 @@ function Debug() { } const handlePress = () => { + if (!reportID) { + return; + } if (reportAction) { - Navigation.navigate(ROUTES.DEBUG_REPORT_ACTION.getRoute(reportID ?? '', reportAction.reportActionID)); + Navigation.navigate(ROUTES.DEBUG_REPORT_ACTION.getRoute(reportID, reportAction.reportActionID)); } else { - Navigation.navigate(ROUTES.DEBUG_REPORT.getRoute(reportID ?? '')); + Navigation.navigate(ROUTES.DEBUG_REPORT.getRoute(reportID)); } hideContextMenu(false, ReportActionComposeFocusManager.focus); }; diff --git a/src/pages/inbox/report/ContextMenu/actions/Delete.tsx b/src/pages/inbox/report/ContextMenu/actions/Delete.tsx index e2b23e56c1c2..69b9710dd574 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Delete.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/Delete.tsx @@ -2,10 +2,10 @@ import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils'; +import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; +import {hideContextMenu, showDeleteModal} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; -import {useContextMenuVisibility} from '../ContextMenuLayout'; -import {useContextMenuPayload} from '../ContextMenuPayloadProvider'; -import {hideContextMenu, showDeleteModal} from '../ReportActionContextMenu'; import {ACTION_IDS} from './actionConfig'; function Delete() { @@ -23,7 +23,7 @@ function Delete() { const handlePress = () => { const iouReportID = isMoneyRequestAction(moneyRequestAction) ? getOriginalMessage(moneyRequestAction)?.IOUReportID : undefined; - const effectiveReportID = iouReportID && Number(iouReportID) !== 0 ? iouReportID : (reportID ?? ''); + const effectiveReportID = iouReportID && Number(iouReportID) !== 0 ? iouReportID : reportID; if (closePopover) { hideContextMenu(false, () => showDeleteModal(effectiveReportID, moneyRequestAction ?? reportAction)); return; diff --git a/src/pages/inbox/report/ContextMenu/actions/Download.tsx b/src/pages/inbox/report/ContextMenu/actions/Download.tsx index 13946b429b3c..5234b4656d88 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Download.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/Download.tsx @@ -6,11 +6,11 @@ import {isMobileSafari} from '@libs/Browser'; import fileDownload from '@libs/fileDownload'; import getAttachmentDetails from '@libs/fileDownload/getAttachmentDetails'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {setDownload} from '@userActions/Download'; import CONST from '@src/CONST'; -import {useContextMenuVisibility} from '../ContextMenuLayout'; -import {useContextMenuPayload} from '../ContextMenuPayloadProvider'; -import {hideContextMenu} from '../ReportActionContextMenu'; import {ACTION_IDS, getActionHtml} from './actionConfig'; function Download() { diff --git a/src/pages/inbox/report/ContextMenu/actions/Edit.tsx b/src/pages/inbox/report/ContextMenu/actions/Edit.tsx index 995a1d348dbc..4ed1fad6fbcb 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Edit.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/Edit.tsx @@ -24,38 +24,40 @@ function Edit() { } const closePopover = !isMini; + const handlePress = () => { + if (isMoneyRequestAction(reportAction) || isMoneyRequestAction(moneyRequestAction)) { + const editExpense = () => { + const childReportID = reportAction?.childReportID; + openReport(childReportID, introSelected); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID)); + }; + if (closePopover) { + hideContextMenu(false, editExpense); + return; + } + editExpense(); + return; + } + const editAction = () => { + if (!draftMessage) { + saveReportActionDraft(reportID, reportAction, Parser.htmlToMarkdown(getActionHtml(reportAction))); + } else { + deleteReportActionDraft(reportID, reportAction); + } + }; + if (closePopover) { + hideContextMenu(false, editAction); + return; + } + editAction(); + }; + return ( { - if (isMoneyRequestAction(reportAction) || isMoneyRequestAction(moneyRequestAction)) { - const editExpense = () => { - const childReportID = reportAction?.childReportID; - openReport(childReportID, introSelected); - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID)); - }; - if (closePopover) { - hideContextMenu(false, editExpense); - return; - } - editExpense(); - return; - } - const editAction = () => { - if (!draftMessage) { - saveReportActionDraft(reportID, reportAction, Parser.htmlToMarkdown(getActionHtml(reportAction))); - } else { - deleteReportActionDraft(reportID, reportAction); - } - }; - if (closePopover) { - hideContextMenu(false, editAction); - return; - } - editAction(); - }} + onPress={handlePress} isFocused={focusedIndex === actionIndex} onFocus={() => setFocusedIndex(actionIndex)} onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} diff --git a/src/pages/inbox/report/ContextMenu/actions/Explain.tsx b/src/pages/inbox/report/ContextMenu/actions/Explain.tsx index 1a354e2d4f1c..236b631cce01 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Explain.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/Explain.tsx @@ -21,24 +21,27 @@ function Explain() { } const closePopover = !isMini; + const handlePress = () => { + if (!originalReport?.reportID) { + return; + } + const doExplain = () => + explain(childReport, originalReport, reportAction, translate, currentUserPersonalDetails?.accountID ?? CONST.DEFAULT_NUMBER_ID, currentUserPersonalDetails?.timezone); + if (closePopover) { + hideContextMenu(false, () => { + KeyboardUtils.dismiss().then(doExplain); + }); + return; + } + doExplain(); + }; + return ( { - if (!originalReport?.reportID) { - return; - } - const doExplain = () => explain(childReport, originalReport, reportAction, translate, currentUserPersonalDetails?.accountID ?? 0, currentUserPersonalDetails?.timezone); - if (closePopover) { - hideContextMenu(false, () => { - KeyboardUtils.dismiss().then(doExplain); - }); - return; - } - doExplain(); - }} + onPress={handlePress} isFocused={focusedIndex === actionIndex} onFocus={() => setFocusedIndex(actionIndex)} onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} diff --git a/src/pages/inbox/report/ContextMenu/actions/FlagAsOffensive.tsx b/src/pages/inbox/report/ContextMenu/actions/FlagAsOffensive.tsx index 5018a4839bed..cffc24bb5f04 100644 --- a/src/pages/inbox/report/ContextMenu/actions/FlagAsOffensive.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/FlagAsOffensive.tsx @@ -2,12 +2,12 @@ import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; +import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import KeyboardUtils from '@src/utils/keyboard'; -import {useContextMenuVisibility} from '../ContextMenuLayout'; -import {useContextMenuPayload} from '../ContextMenuPayloadProvider'; -import {hideContextMenu} from '../ReportActionContextMenu'; import {ACTION_IDS} from './actionConfig'; function FlagAsOffensive() { diff --git a/src/pages/inbox/report/ContextMenu/actions/Hold.tsx b/src/pages/inbox/report/ContextMenu/actions/Hold.tsx index f3fd73eae463..cac935f434c8 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Hold.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/Hold.tsx @@ -11,14 +11,13 @@ import {ACTION_IDS} from './actionConfig'; function Hold() { const {moneyRequestAction, isDelegateAccessRestricted, showDelegateNoAccessModal, isMini, interceptAnonymousUser} = useContextMenuPayload(); const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); - const icons = useMemoizedLazyExpensifyIcons(['Stopwatch'] as const); const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Stopwatch'] as const); const actionIndex = visibleActionIds.indexOf(ACTION_IDS.HOLD); if (actionIndex === -1) { return null; } - const closePopover = !isMini; const handlePress = () => { diff --git a/src/pages/inbox/report/ContextMenu/actions/JoinThread.tsx b/src/pages/inbox/report/ContextMenu/actions/JoinThread.tsx index f0c30dd30e62..cbcd92a7790a 100644 --- a/src/pages/inbox/report/ContextMenu/actions/JoinThread.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/JoinThread.tsx @@ -13,14 +13,13 @@ import {ACTION_IDS} from './actionConfig'; function JoinThread() { const {reportAction, originalReport, currentUserAccountID, isMini, interceptAnonymousUser} = useContextMenuPayload(); const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); - const icons = useMemoizedLazyExpensifyIcons(['Bell'] as const); const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Bell'] as const); const actionIndex = visibleActionIds.indexOf(ACTION_IDS.JOIN_THREAD); if (actionIndex === -1) { return null; } - const closePopover = !isMini; const handlePress = () => { diff --git a/src/pages/inbox/report/ContextMenu/actions/LeaveThread.tsx b/src/pages/inbox/report/ContextMenu/actions/LeaveThread.tsx index 3cfad706fa23..335f6180cc92 100644 --- a/src/pages/inbox/report/ContextMenu/actions/LeaveThread.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/LeaveThread.tsx @@ -13,14 +13,13 @@ import {ACTION_IDS} from './actionConfig'; function LeaveThread() { const {reportAction, originalReport, currentUserAccountID, isMini, interceptAnonymousUser} = useContextMenuPayload(); const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); - const icons = useMemoizedLazyExpensifyIcons(['Exit'] as const); const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Exit'] as const); const actionIndex = visibleActionIds.indexOf(ACTION_IDS.LEAVE_THREAD); if (actionIndex === -1) { return null; } - const closePopover = !isMini; const handlePress = () => { diff --git a/src/pages/inbox/report/ContextMenu/actions/OverflowMenu.tsx b/src/pages/inbox/report/ContextMenu/actions/OverflowMenu.tsx index e5b0d5367c4b..273a19ae978b 100644 --- a/src/pages/inbox/report/ContextMenu/actions/OverflowMenu.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/OverflowMenu.tsx @@ -3,9 +3,9 @@ import type {GestureResponderEvent, View} from 'react-native'; import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; +import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import CONST from '@src/CONST'; -import {useContextMenuVisibility} from '../ContextMenuLayout'; -import {useContextMenuPayload} from '../ContextMenuPayloadProvider'; import {ACTION_IDS} from './actionConfig'; function OverflowMenu() { diff --git a/src/pages/inbox/report/ContextMenu/actions/Pin.tsx b/src/pages/inbox/report/ContextMenu/actions/Pin.tsx index e1ed4e98c3f0..2e328bc2ddb8 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Pin.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/Pin.tsx @@ -2,11 +2,11 @@ import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {togglePinnedState} from '@userActions/Report'; import CONST from '@src/CONST'; -import {useContextMenuVisibility} from '../ContextMenuLayout'; -import {useContextMenuPayload} from '../ContextMenuPayloadProvider'; -import {hideContextMenu} from '../ReportActionContextMenu'; import {ACTION_IDS} from './actionConfig'; function Pin() { diff --git a/src/pages/inbox/report/ContextMenu/actions/ReplyInThread.tsx b/src/pages/inbox/report/ContextMenu/actions/ReplyInThread.tsx index 01a77de5c91f..f9686ccaae28 100644 --- a/src/pages/inbox/report/ContextMenu/actions/ReplyInThread.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/ReplyInThread.tsx @@ -21,24 +21,25 @@ function ReplyInThread() { } const closePopover = !isMini; + const handlePress = () => + interceptAnonymousUser(() => { + if (closePopover) { + hideContextMenu(false, () => { + KeyboardUtils.dismiss().then(() => { + navigateToAndOpenChildReport(childReport, reportAction, originalReport, currentUserAccountID); + }); + }); + return; + } + navigateToAndOpenChildReport(childReport, reportAction, originalReport, currentUserAccountID); + }, false); + return ( - interceptAnonymousUser(() => { - if (closePopover) { - hideContextMenu(false, () => { - KeyboardUtils.dismiss().then(() => { - navigateToAndOpenChildReport(childReport, reportAction, originalReport, currentUserAccountID); - }); - }); - return; - } - navigateToAndOpenChildReport(childReport, reportAction, originalReport, currentUserAccountID); - }, false) - } + onPress={handlePress} isFocused={focusedIndex === actionIndex} onFocus={() => setFocusedIndex(actionIndex)} onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} diff --git a/src/pages/inbox/report/ContextMenu/actions/Unhold.tsx b/src/pages/inbox/report/ContextMenu/actions/Unhold.tsx index bc94f04ce094..b5e01fd03332 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Unhold.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/Unhold.tsx @@ -11,14 +11,13 @@ import {ACTION_IDS} from './actionConfig'; function Unhold() { const {moneyRequestAction, isDelegateAccessRestricted, showDelegateNoAccessModal, isMini, interceptAnonymousUser} = useContextMenuPayload(); const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); - const icons = useMemoizedLazyExpensifyIcons(['Stopwatch'] as const); const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Stopwatch'] as const); const actionIndex = visibleActionIds.indexOf(ACTION_IDS.UNHOLD); if (actionIndex === -1) { return null; } - const closePopover = !isMini; const handlePress = () => { diff --git a/src/pages/inbox/report/ContextMenu/actions/Unpin.tsx b/src/pages/inbox/report/ContextMenu/actions/Unpin.tsx index e3694936308d..0a6b6665ea0a 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Unpin.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/Unpin.tsx @@ -2,11 +2,11 @@ import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {togglePinnedState} from '@userActions/Report'; import CONST from '@src/CONST'; -import {useContextMenuVisibility} from '../ContextMenuLayout'; -import {useContextMenuPayload} from '../ContextMenuPayloadProvider'; -import {hideContextMenu} from '../ReportActionContextMenu'; import {ACTION_IDS} from './actionConfig'; function Unpin() { From 8f653541474801d7b30c36001bd5a1b89806f67c Mon Sep 17 00:00:00 2001 From: rory Date: Sat, 28 Feb 2026 01:59:46 -0800 Subject: [PATCH 10/88] fix(contextmenu): restore hideDeleteModal to close global modal Wire hideDeleteModal to call modalContext.closeModal() so the delete confirmation modal is dismissed when a report action item unmounts while the modal is open (e.g., navigating away). Made-with: Cursor --- .../report/ContextMenu/PopoverReportActionContextMenu.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx index ae454fbbe3af..64666a793f49 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -260,7 +260,9 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro }); }; - const hideDeleteModal = () => {}; + const hideDeleteModal = () => { + modalContext.closeModal(); + }; const showDeleteModal: ReportActionContextMenu['showDeleteModal'] = (showReportID, showReportAction, _shouldSetModalVisibility, onConfirm = () => {}, onCancel = () => {}) => { if (!showReportID || !showReportAction?.reportActionID) { From d3c86828fc67f8c53b08ee7266968b837c9ce4a5 Mon Sep 17 00:00:00 2001 From: rory Date: Sat, 28 Feb 2026 02:24:41 -0800 Subject: [PATCH 11/88] fix(contextmenu): resolve delete action from source report collection When the delete target is a money request, the effectiveReportID can be the IOU report ID, but the action lives in the chat report's REPORT_ACTIONS collection. Pass actionSourceReportID to the modal and fall back to it when the action isn't found in the primary collection. Made-with: Cursor --- .../ContextMenu/ConfirmDeleteReportActionModal.tsx | 11 +++++++---- .../ContextMenu/PopoverReportActionContextMenu.tsx | 1 + 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/ConfirmDeleteReportActionModal.tsx b/src/pages/inbox/report/ContextMenu/ConfirmDeleteReportActionModal.tsx index 94e510eadb73..a704bcf1ad70 100644 --- a/src/pages/inbox/report/ContextMenu/ConfirmDeleteReportActionModal.tsx +++ b/src/pages/inbox/report/ContextMenu/ConfirmDeleteReportActionModal.tsx @@ -22,15 +22,18 @@ import ONYXKEYS from '@src/ONYXKEYS'; type ConfirmDeleteReportActionModalProps = ModalProps & { reportID: string; reportActionID: string; + actionSourceReportID?: string; }; -function ConfirmDeleteReportActionModal({closeModal, reportID, reportActionID}: ConfirmDeleteReportActionModalProps) { +function ConfirmDeleteReportActionModal({closeModal, reportID, reportActionID, actionSourceReportID}: ConfirmDeleteReportActionModalProps) { const {translate} = useLocalize(); const {email, accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); const {currentSearchHash} = useSearchStateContext(); const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`); - const reportAction = reportActions?.[reportActionID]; + const [sourceReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${actionSourceReportID}`); + const actionReportActions = reportActions?.[reportActionID] ? reportActions : sourceReportActions; + const reportAction = actionReportActions?.[reportActionID]; const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); const [childReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportAction?.childReportID}`); @@ -42,8 +45,8 @@ function ConfirmDeleteReportActionModal({closeModal, reportID, reportActionID}: const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS); const isReportArchived = useReportIsArchived(reportID); - const [originalReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getOriginalReportID(reportID, reportAction, reportActions)}`); - const isOriginalReportArchived = useReportIsArchived(getOriginalReportID(reportID, reportAction, reportActions)); + const [originalReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getOriginalReportID(reportID, reportAction, actionReportActions)}`); + const isOriginalReportArchived = useReportIsArchived(getOriginalReportID(reportID, reportAction, actionReportActions)); const {iouReport, chatReport, isChatIOUReportArchived} = useGetIOUReportFromReportAction(reportAction); const transactionIDs: string[] = []; diff --git a/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx index 64666a793f49..4a42c130f95b 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -297,6 +297,7 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro props: { reportID: showReportID, reportActionID: showReportAction.reportActionID, + actionSourceReportID: menuState?.reportID, }, }) .then((result) => { From 57af1e89e685a976e35cd2c8a607a7fe6cc4713e Mon Sep 17 00:00:00 2001 From: rory Date: Sat, 28 Feb 2026 02:24:49 -0800 Subject: [PATCH 12/88] perf(contextmenu): add dependency array to mini-menu position effect The positioning useEffect had no dependency array, causing it to run after every render. Scope it to state changes so animations only trigger when row measurements or visibility actually change. Made-with: Cursor --- .../report/ContextMenu/MiniReportActionContextMenu/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index 8ceef9da62ba..a6c7267bb214 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -35,7 +35,7 @@ function MiniReportActionContextMenu() { } } wasVisibleRef.current = state.isVisible; - }); + }, [state, baseTop, baseRight]); useEffect(() => { if (!isVisible) { From c05ac51c54031c329c6a8e61e9021f66ee08bd43 Mon Sep 17 00:00:00 2001 From: rory Date: Sat, 28 Feb 2026 02:47:19 -0800 Subject: [PATCH 13/88] fix(contextmenu): pass actionSourceReportID through showDeleteModal In mini-menu flows the popover menuState is unset, so deriving the source report from menuState?.reportID yields undefined. Pass the source report ID explicitly from Delete.tsx through the showDeleteModal interface so the modal can always resolve the action. Also guard hideDeleteModal with a ref so it only closes the modal when showDeleteModal actually opened one, preventing unrelated modals from being dismissed during navigation/unmount cleanup. Made-with: Cursor --- .../PopoverReportActionContextMenu.tsx | 18 +++++++++++++++-- .../ContextMenu/ReportActionContextMenu.ts | 20 ++++++++++++++++--- .../report/ContextMenu/actions/Delete.tsx | 5 +++-- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx index 4a42c130f95b..2e30a1e892c6 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -260,11 +260,23 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro }); }; + const isDeleteModalActiveRef = useRef(false); + const hideDeleteModal = () => { + if (!isDeleteModalActiveRef.current) { + return; + } modalContext.closeModal(); }; - const showDeleteModal: ReportActionContextMenu['showDeleteModal'] = (showReportID, showReportAction, _shouldSetModalVisibility, onConfirm = () => {}, onCancel = () => {}) => { + const showDeleteModal: ReportActionContextMenu['showDeleteModal'] = ( + showReportID, + showReportAction, + _shouldSetModalVisibility, + onConfirm = () => {}, + onCancel = () => {}, + actionSourceReportID = undefined, + ) => { if (!showReportID || !showReportAction?.reportActionID) { return; } @@ -291,16 +303,18 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro originalReportID: prev?.originalReportID, })); + isDeleteModalActiveRef.current = true; modalContext .showModal({ component: ConfirmDeleteReportActionModal, props: { reportID: showReportID, reportActionID: showReportAction.reportActionID, - actionSourceReportID: menuState?.reportID, + actionSourceReportID, }, }) .then((result) => { + isDeleteModalActiveRef.current = false; if (result.action === ModalActions.CONFIRM) { onConfirm(); } else { diff --git a/src/pages/inbox/report/ContextMenu/ReportActionContextMenu.ts b/src/pages/inbox/report/ContextMenu/ReportActionContextMenu.ts index 012d61bc020b..efff9e46e480 100644 --- a/src/pages/inbox/report/ContextMenu/ReportActionContextMenu.ts +++ b/src/pages/inbox/report/ContextMenu/ReportActionContextMenu.ts @@ -57,7 +57,14 @@ type HideContextMenu = (params?: HideContextMenuParams) => void; type ReportActionContextMenu = { showContextMenu: ShowContextMenu; hideContextMenu: HideContextMenu; - showDeleteModal: (reportID: string, reportAction: OnyxEntry, shouldSetModalVisibility?: boolean, onConfirm?: OnConfirm, onCancel?: OnCancel) => void; + showDeleteModal: ( + reportID: string, + reportAction: OnyxEntry, + shouldSetModalVisibility?: boolean, + onConfirm?: OnConfirm, + onCancel?: OnCancel, + actionSourceReportID?: string, + ) => void; hideDeleteModal: () => void; isActiveReportAction: (accountID: string | number) => boolean; instanceIDRef: RefObject; @@ -156,11 +163,18 @@ function hideDeleteModal() { /** * Opens the Confirm delete action modal */ -function showDeleteModal(reportID: string | undefined, reportAction: OnyxEntry, shouldSetModalVisibility?: boolean, onConfirm?: OnConfirm, onCancel?: OnCancel) { +function showDeleteModal( + reportID: string | undefined, + reportAction: OnyxEntry, + shouldSetModalVisibility?: boolean, + onConfirm?: OnConfirm, + onCancel?: OnCancel, + actionSourceReportID?: string, +) { if (!contextMenuRef.current || !reportID) { return; } - contextMenuRef.current.showDeleteModal(reportID, reportAction, shouldSetModalVisibility, onConfirm, onCancel); + contextMenuRef.current.showDeleteModal(reportID, reportAction, shouldSetModalVisibility, onConfirm, onCancel, actionSourceReportID); } /** diff --git a/src/pages/inbox/report/ContextMenu/actions/Delete.tsx b/src/pages/inbox/report/ContextMenu/actions/Delete.tsx index 69b9710dd574..73889bde03f1 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Delete.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/Delete.tsx @@ -24,11 +24,12 @@ function Delete() { const handlePress = () => { const iouReportID = isMoneyRequestAction(moneyRequestAction) ? getOriginalMessage(moneyRequestAction)?.IOUReportID : undefined; const effectiveReportID = iouReportID && Number(iouReportID) !== 0 ? iouReportID : reportID; + const actionSourceID = effectiveReportID !== reportID ? reportID : undefined; if (closePopover) { - hideContextMenu(false, () => showDeleteModal(effectiveReportID, moneyRequestAction ?? reportAction)); + hideContextMenu(false, () => showDeleteModal(effectiveReportID, moneyRequestAction ?? reportAction, undefined, undefined, undefined, actionSourceID)); return; } - showDeleteModal(effectiveReportID, moneyRequestAction ?? reportAction); + showDeleteModal(effectiveReportID, moneyRequestAction ?? reportAction, undefined, undefined, undefined, actionSourceID); }; return ( From f264fc4ac25ed991f4f49daa5ccf3c19487858c5 Mon Sep 17 00:00:00 2001 From: rory Date: Sat, 28 Feb 2026 03:13:02 -0800 Subject: [PATCH 14/88] fix(contextmenu): hide mini-menu immediately on scroll The scroll handler repeatedly called hideMiniContextMenu() which uses a 120ms debounced timer, causing the menu to stay visible while scrolling. Add an {immediate: true} option to bypass the timer for scroll-triggered hides. Made-with: Cursor --- .../report/ContextMenu/MiniContextMenuProvider.tsx | 10 +++++++--- .../ContextMenu/MiniReportActionContextMenu/index.tsx | 4 ++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx b/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx index f205ec9ee923..57eeb6055a4e 100644 --- a/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx @@ -32,7 +32,7 @@ type MiniContextMenuState = MiniContextMenuParams & { type MiniContextMenuActions = { showMiniContextMenu: (params: MiniContextMenuParams) => void; - hideMiniContextMenu: () => void; + hideMiniContextMenu: (options?: {immediate?: boolean}) => void; cancelHide: () => void; keepOpen: () => void; release: () => void; @@ -78,13 +78,17 @@ function MiniContextMenuProvider({children}: MiniContextMenuProviderProps) { pendingHideRef.current = false; setState({...params, isVisible: true}); }, - hideMiniContextMenu: () => { + hideMiniContextMenu: (options) => { if (shouldKeepOpenRef.current) { pendingHideRef.current = true; return; } clearHideTimer(); - hideTimerRef.current = setTimeout(performHide, HIDE_DELAY_MS); + if (options?.immediate) { + performHide(); + } else { + hideTimerRef.current = setTimeout(performHide, HIDE_DELAY_MS); + } }, cancelHide: () => { clearHideTimer(); diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index a6c7267bb214..a7d9807d8e40 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -42,7 +42,7 @@ function MiniReportActionContextMenu() { return; } const handleScroll = () => { - hideMiniContextMenu(); + hideMiniContextMenu({immediate: true}); }; window.addEventListener('scroll', handleScroll, true); return () => { @@ -63,7 +63,7 @@ function MiniReportActionContextMenu() { // eslint-disable-next-line jsx-a11y/mouse-events-have-key-events
hideMiniContextMenu()} data-selection-scraper-hidden-element={isVisible} style={{ position: 'fixed', From 3d7bbd39b55ab539339868f8c73d1336d175d887 Mon Sep 17 00:00:00 2001 From: rory Date: Sat, 28 Feb 2026 03:45:05 -0800 Subject: [PATCH 15/88] fix(contextmenu): pass mini visible actions as disabled in overflow The old code passed filteredContextMenuActions as disabledOptions when opening the overflow popover, hiding actions already visible in the mini row. Restore this behavior by passing the current visibleActionIds from ContextMenuLayout as disabledActionIds to the overflow menu. Made-with: Cursor --- .../inbox/report/ContextMenu/BaseReportActionContextMenu.tsx | 4 ++-- .../inbox/report/ContextMenu/ContextMenuPayloadProvider.tsx | 2 +- src/pages/inbox/report/ContextMenu/actions/OverflowMenu.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx index e3e77c32c246..1f500d278806 100755 --- a/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx +++ b/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx @@ -188,7 +188,7 @@ function BaseReportActionContextMenu({ } }; - const openOverflowMenu = (event: GestureResponderEvent | MouseEvent, anchorRef: RefObject) => { + const openOverflowMenu = (event: GestureResponderEvent | MouseEvent, anchorRef: RefObject, miniVisibleActionIds?: Set) => { showContextMenu({ type: CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, event, @@ -216,7 +216,7 @@ function BaseReportActionContextMenu({ } }, }, - disabledActionIds: new Set(), + disabledActionIds: miniVisibleActionIds, shouldCloseOnTarget: true, isOverflowMenu: true, }); diff --git a/src/pages/inbox/report/ContextMenu/ContextMenuPayloadProvider.tsx b/src/pages/inbox/report/ContextMenu/ContextMenuPayloadProvider.tsx index a55646af5d95..9bf6d48f9709 100644 --- a/src/pages/inbox/report/ContextMenu/ContextMenuPayloadProvider.tsx +++ b/src/pages/inbox/report/ContextMenu/ContextMenuPayloadProvider.tsx @@ -63,7 +63,7 @@ type ContextMenuPayloadContextValue = { transitionActionSheetState: (params: {type: string; payload?: Record}) => void; openContextMenu: () => void; interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; - openOverflowMenu: (event: GestureResponderEvent | MouseEvent, anchorRef: RefObject) => void; + openOverflowMenu: (event: GestureResponderEvent | MouseEvent, anchorRef: RefObject, miniVisibleActionIds?: Set) => void; setIsEmojiPickerActive: ((state: boolean) => void) | undefined; showDelegateNoAccessModal: (() => void) | undefined; diff --git a/src/pages/inbox/report/ContextMenu/actions/OverflowMenu.tsx b/src/pages/inbox/report/ContextMenu/actions/OverflowMenu.tsx index 273a19ae978b..94089b5d169a 100644 --- a/src/pages/inbox/report/ContextMenu/actions/OverflowMenu.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/OverflowMenu.tsx @@ -21,7 +21,7 @@ function OverflowMenu() { } const handlePress = (event?: GestureResponderEvent | MouseEvent | KeyboardEvent) => { - openOverflowMenu(event as GestureResponderEvent | MouseEvent, threeDotRef); + openOverflowMenu(event as GestureResponderEvent | MouseEvent, threeDotRef, new Set(visibleActionIds)); openContextMenu(); }; From 825eeb1087a33fec43c8921ef8cb5363a0e75ba4 Mon Sep 17 00:00:00 2001 From: rory Date: Sat, 28 Feb 2026 04:04:22 -0800 Subject: [PATCH 16/88] fix(contextmenu): add interceptAnonymousUser guard to non-anonymous actions The old code centrally wrapped all action onPress handlers with interceptAnonymousUser in BaseReportActionContextMenu. The composition refactor moved onPress to individual components but some non-anonymous actions (EmojiReaction, MarkAsUnread, Explain, MarkAsRead, Edit, Pin, Unpin) lost this guard, allowing anonymous users to execute restricted handlers instead of being redirected to sign in. Made-with: Cursor --- .../inbox/report/ContextMenu/actions/Edit.tsx | 4 ++-- .../report/ContextMenu/actions/EmojiReaction.tsx | 6 +++--- .../inbox/report/ContextMenu/actions/Explain.tsx | 4 ++-- .../report/ContextMenu/actions/MarkAsRead.tsx | 16 +++++++++------- .../report/ContextMenu/actions/MarkAsUnread.tsx | 16 +++++++++------- .../inbox/report/ContextMenu/actions/Pin.tsx | 4 ++-- .../inbox/report/ContextMenu/actions/Unpin.tsx | 4 ++-- 7 files changed, 29 insertions(+), 25 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/actions/Edit.tsx b/src/pages/inbox/report/ContextMenu/actions/Edit.tsx index 4ed1fad6fbcb..121852380aa9 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Edit.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/Edit.tsx @@ -13,7 +13,7 @@ import ROUTES from '@src/ROUTES'; import {ACTION_IDS, getActionHtml} from './actionConfig'; function Edit() { - const {reportID, reportAction, moneyRequestAction, draftMessage, introSelected, isMini} = useContextMenuPayload(); + const {reportID, reportAction, moneyRequestAction, draftMessage, introSelected, isMini, interceptAnonymousUser} = useContextMenuPayload(); const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Pencil'] as const); @@ -57,7 +57,7 @@ function Edit() { icon={icons.Pencil} text={translate('reportActionContextMenu.editAction', {action: moneyRequestAction ?? reportAction})} isMini={isMini} - onPress={handlePress} + onPress={() => interceptAnonymousUser(handlePress)} isFocused={focusedIndex === actionIndex} onFocus={() => setFocusedIndex(actionIndex)} onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} diff --git a/src/pages/inbox/report/ContextMenu/actions/EmojiReaction.tsx b/src/pages/inbox/report/ContextMenu/actions/EmojiReaction.tsx index 189e8941baeb..e0d8e60d136f 100644 --- a/src/pages/inbox/report/ContextMenu/actions/EmojiReaction.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/EmojiReaction.tsx @@ -10,7 +10,7 @@ import type {ReportActionReactions} from '@src/types/onyx'; import {ACTION_IDS} from './actionConfig'; function EmojiReaction() { - const {reportID, reportAction, currentUserAccountID, close, openContextMenu, setIsEmojiPickerActive, isMini} = useContextMenuPayload(); + const {reportID, reportAction, currentUserAccountID, close, openContextMenu, setIsEmojiPickerActive, isMini, interceptAnonymousUser} = useContextMenuPayload(); const {visibleActionIds} = useContextMenuVisibility(); if (!visibleActionIds.includes(ACTION_IDS.EMOJI_REACTION)) { @@ -37,7 +37,7 @@ function EmojiReaction() { if (isMini) { return ( interceptAnonymousUser(() => toggleEmojiAndCloseMenu(emoji, existingReactions, preferredSkinTone))} onPressOpenPicker={() => { openContextMenu(); setIsEmojiPickerActive?.(true); @@ -55,7 +55,7 @@ function EmojiReaction() { return ( interceptAnonymousUser(() => toggleEmojiAndCloseMenu(emoji, existingReactions, preferredSkinTone))} reportActionID={reportAction?.reportActionID} reportAction={reportAction} setIsEmojiPickerActive={setIsEmojiPickerActive} diff --git a/src/pages/inbox/report/ContextMenu/actions/Explain.tsx b/src/pages/inbox/report/ContextMenu/actions/Explain.tsx index 236b631cce01..33646053cbbe 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Explain.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/Explain.tsx @@ -10,7 +10,7 @@ import KeyboardUtils from '@src/utils/keyboard'; import {ACTION_IDS} from './actionConfig'; function Explain() { - const {childReport, originalReport, reportAction, currentUserPersonalDetails, isMini} = useContextMenuPayload(); + const {childReport, originalReport, reportAction, currentUserPersonalDetails, isMini, interceptAnonymousUser} = useContextMenuPayload(); const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Concierge'] as const); @@ -41,7 +41,7 @@ function Explain() { icon={icons.Concierge} text={translate('reportActionContextMenu.explain')} isMini={isMini} - onPress={handlePress} + onPress={() => interceptAnonymousUser(handlePress)} isFocused={focusedIndex === actionIndex} onFocus={() => setFocusedIndex(actionIndex)} onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} diff --git a/src/pages/inbox/report/ContextMenu/actions/MarkAsRead.tsx b/src/pages/inbox/report/ContextMenu/actions/MarkAsRead.tsx index 4ccb5600b896..b969ab4bb016 100644 --- a/src/pages/inbox/report/ContextMenu/actions/MarkAsRead.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/MarkAsRead.tsx @@ -10,7 +10,7 @@ import CONST from '@src/CONST'; import {ACTION_IDS} from './actionConfig'; function MarkAsRead() { - const {reportID, isMini} = useContextMenuPayload(); + const {reportID, isMini, interceptAnonymousUser} = useContextMenuPayload(); const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Mail', 'Checkmark'] as const); @@ -21,18 +21,20 @@ function MarkAsRead() { } const closePopover = !isMini; + const handlePress = () => { + readNewestAction(reportID, true, true); + if (closePopover) { + hideContextMenu(true, ReportActionComposeFocusManager.focus); + } + }; + return ( { - readNewestAction(reportID, true, true); - if (closePopover) { - hideContextMenu(true, ReportActionComposeFocusManager.focus); - } - }} + onPress={() => interceptAnonymousUser(handlePress)} isFocused={focusedIndex === actionIndex} onFocus={() => setFocusedIndex(actionIndex)} onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} diff --git a/src/pages/inbox/report/ContextMenu/actions/MarkAsUnread.tsx b/src/pages/inbox/report/ContextMenu/actions/MarkAsUnread.tsx index 163599e5a4ab..5d1533c3acaa 100644 --- a/src/pages/inbox/report/ContextMenu/actions/MarkAsUnread.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/MarkAsUnread.tsx @@ -10,7 +10,7 @@ import CONST from '@src/CONST'; import {ACTION_IDS} from './actionConfig'; function MarkAsUnread() { - const {reportID, reportActions, reportAction, currentUserAccountID, isMini} = useContextMenuPayload(); + const {reportID, reportActions, reportAction, currentUserAccountID, isMini, interceptAnonymousUser} = useContextMenuPayload(); const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['ChatBubbleUnread', 'Checkmark'] as const); @@ -21,18 +21,20 @@ function MarkAsUnread() { } const closePopover = !isMini; + const handlePress = () => { + markCommentAsUnread(reportID, reportActions, reportAction, currentUserAccountID); + if (closePopover) { + hideContextMenu(true, ReportActionComposeFocusManager.focus); + } + }; + return ( { - markCommentAsUnread(reportID, reportActions, reportAction, currentUserAccountID); - if (closePopover) { - hideContextMenu(true, ReportActionComposeFocusManager.focus); - } - }} + onPress={() => interceptAnonymousUser(handlePress)} isFocused={focusedIndex === actionIndex} onFocus={() => setFocusedIndex(actionIndex)} onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} diff --git a/src/pages/inbox/report/ContextMenu/actions/Pin.tsx b/src/pages/inbox/report/ContextMenu/actions/Pin.tsx index 2e328bc2ddb8..1ea43eef3de4 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Pin.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/Pin.tsx @@ -10,7 +10,7 @@ import CONST from '@src/CONST'; import {ACTION_IDS} from './actionConfig'; function Pin() { - const {reportID, isMini} = useContextMenuPayload(); + const {reportID, isMini, interceptAnonymousUser} = useContextMenuPayload(); const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); const icons = useMemoizedLazyExpensifyIcons(['Pin'] as const); const {translate} = useLocalize(); @@ -34,7 +34,7 @@ function Pin() { icon={icons.Pin} text={translate('common.pin')} isMini={isMini} - onPress={handlePress} + onPress={() => interceptAnonymousUser(handlePress)} isFocused={focusedIndex === actionIndex} onFocus={() => setFocusedIndex(actionIndex)} onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} diff --git a/src/pages/inbox/report/ContextMenu/actions/Unpin.tsx b/src/pages/inbox/report/ContextMenu/actions/Unpin.tsx index 0a6b6665ea0a..a54817b3ef7b 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Unpin.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/Unpin.tsx @@ -10,7 +10,7 @@ import CONST from '@src/CONST'; import {ACTION_IDS} from './actionConfig'; function Unpin() { - const {reportID, isMini} = useContextMenuPayload(); + const {reportID, isMini, interceptAnonymousUser} = useContextMenuPayload(); const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); const icons = useMemoizedLazyExpensifyIcons(['Pin'] as const); const {translate} = useLocalize(); @@ -34,7 +34,7 @@ function Unpin() { icon={icons.Pin} text={translate('common.unPin')} isMini={isMini} - onPress={handlePress} + onPress={() => interceptAnonymousUser(handlePress)} isFocused={focusedIndex === actionIndex} onFocus={() => setFocusedIndex(actionIndex)} onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} From 4eb4e8da0e2ced1f62ceef5bd4c1ee3c454150ca Mon Sep 17 00:00:00 2001 From: rory Date: Sat, 28 Feb 2026 07:34:49 -0800 Subject: [PATCH 17/88] docs(contextmenu): restore JSDoc comments on BaseReportActionContextMenu props Made-with: Cursor --- .../BaseReportActionContextMenu.tsx | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx index 1f500d278806..de35403a7e79 100755 --- a/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx +++ b/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx @@ -45,23 +45,64 @@ import type {ContextMenuAnchor, ContextMenuType} from './ReportActionContextMenu import {hideContextMenu, showContextMenu} from './ReportActionContextMenu'; type BaseReportActionContextMenuProps = { + /** The ID of the report this report action is attached to. */ reportID: string | undefined; + + /** The ID of the report action this context menu is attached to. */ reportActionID: string | undefined; + + /** The ID of the original report from which the given reportAction is first created. */ originalReportID: string | undefined; + + /** + * If true, this component will be a small, row-oriented menu that displays icons but not text. + * If false, this component will be a larger, column-oriented menu that displays icons alongside text in each row. + */ isMini?: boolean; + + /** Controls the visibility of this component. */ isVisible?: boolean; + + /** The copy selection. */ selection?: string; + + /** Draft message - if this is set the comment is in 'edit' mode */ draftMessage?: string; + + /** String representing the context menu type [LINK, REPORT_ACTION] which controls context menu choices */ type?: ContextMenuType; + + /** Target node which is the target of ContentMenu */ anchor?: RefObject; + + /** Flag to check if the chat participant is Chronos */ isChronosReport?: boolean; + + /** Whether the provided report is an archived room */ isArchivedRoom?: boolean; + + /** Flag to check if the chat is pinned in the LHN. Used for the Pin/Unpin action */ isPinnedChat?: boolean; + + /** Flag to check if the chat is unread in the LHN. Used for the Mark as Read/Unread action */ isUnreadChat?: boolean; + + /** + * Is the action a thread's parent reportAction viewed from within the thread report? + * It will be false if we're viewing the same parent report action from the report it belongs to rather than the thread. + */ isThreadReportParentAction?: boolean; + + /** Content Ref */ contentRef?: RefObject; + + /** Function to check if context menu is active */ checkIfContextMenuActive?: () => void; + + /** List of disabled action IDs */ disabledActionIds?: Set; + + /** Function to update emoji picker state */ setIsEmojiPickerActive?: (state: boolean) => void; }; @@ -176,6 +217,10 @@ function BaseReportActionContextMenu({ useRestoreInputFocus(isVisible); + /** + * Checks if user is anonymous. If true and the action doesn't accept for anonymous user, hides the context menu and + * shows the sign in modal. Else, executes the callback. + */ const interceptAnonymousUser = (callback: () => void, isAnonymousAction = false) => { if (isAnonymousUser() && !isAnonymousAction) { hideContextMenu(false); From 47132bde9997126f74c2fe37191d577696156393 Mon Sep 17 00:00:00 2001 From: rory Date: Sat, 28 Feb 2026 07:52:51 -0800 Subject: [PATCH 18/88] refactor(contextmenu): eliminate ContextMenuVisibilityContext Move visibility evaluation and focus management from ContextMenuLayout into BaseReportActionContextMenu. The parent now conditionally renders each action and passes isFocused/onFocus/onBlur as explicit props. ContextMenuLayout is simplified to a pure wrapper (FocusTrap + View). Made-with: Cursor --- .../BaseReportActionContextMenu.tsx | 145 +++++++++++++++--- .../report/ContextMenu/ContextMenuLayout.tsx | 101 +----------- .../report/ContextMenu/actions/CopyEmail.tsx | 17 +- .../report/ContextMenu/actions/CopyLink.tsx | 17 +- .../ContextMenu/actions/CopyMessage.tsx | 18 +-- .../ContextMenu/actions/CopyOnyxData.tsx | 17 +- .../ContextMenu/actions/CopyToClipboard.tsx | 17 +- .../report/ContextMenu/actions/CopyURL.tsx | 17 +- .../report/ContextMenu/actions/Debug.tsx | 17 +- .../report/ContextMenu/actions/Delete.tsx | 17 +- .../report/ContextMenu/actions/Download.tsx | 18 +-- .../inbox/report/ContextMenu/actions/Edit.tsx | 17 +- .../ContextMenu/actions/EmojiReaction.tsx | 7 - .../report/ContextMenu/actions/Explain.tsx | 16 +- .../ContextMenu/actions/FlagAsOffensive.tsx | 17 +- .../inbox/report/ContextMenu/actions/Hold.tsx | 16 +- .../report/ContextMenu/actions/JoinThread.tsx | 16 +- .../ContextMenu/actions/LeaveThread.tsx | 16 +- .../report/ContextMenu/actions/MarkAsRead.tsx | 16 +- .../ContextMenu/actions/MarkAsUnread.tsx | 16 +- .../ContextMenu/actions/OverflowMenu.tsx | 22 ++- .../inbox/report/ContextMenu/actions/Pin.tsx | 17 +- .../ContextMenu/actions/ReplyInThread.tsx | 16 +- .../report/ContextMenu/actions/Unhold.tsx | 16 +- .../report/ContextMenu/actions/Unpin.tsx | 17 +- 25 files changed, 247 insertions(+), 379 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx index de35403a7e79..8e40a7786598 100755 --- a/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx +++ b/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx @@ -7,6 +7,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import * as ActionSheetAwareScrollView from '@components/ActionSheetAwareScrollView'; import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; import {useSession} from '@components/OnyxListItemProvider'; +import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useEnvironment from '@hooks/useEnvironment'; import useGetExpensifyCardFromReportAction from '@hooks/useGetExpensifyCardFromReportAction'; @@ -36,6 +37,8 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {OriginalMessageIOU, ReportAction} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import type {ActionId} from './actions/actionConfig'; +import {ORDERED_ACTION_SHOULD_SHOW} from './actions/actionConfig'; import ContextMenuAction from './actions/ContextMenuAction'; import ContextMenuLayout from './ContextMenuLayout'; import {ContextMenuPayloadContext} from './ContextMenuPayloadProvider'; @@ -44,6 +47,12 @@ import {useMiniContextMenuActions} from './MiniContextMenuProvider'; import type {ContextMenuAnchor, ContextMenuType} from './ReportActionContextMenu'; import {hideContextMenu, showContextMenu} from './ReportActionContextMenu'; +type ContextMenuActionFocusProps = { + isFocused: boolean; + onFocus: () => void; + onBlur: () => void; +}; + type BaseReportActionContextMenuProps = { /** The ID of the report this report action is attached to. */ reportID: string | undefined; @@ -217,6 +226,94 @@ function BaseReportActionContextMenu({ useRestoreInputFocus(isVisible); + // Evaluate which actions are visible + const shouldShowArgs = { + type, + reportAction, + childReportActions, + isArchivedRoom, + betas, + menuTarget: anchor, + isChronosReport, + reportID, + isPinnedChat, + isUnreadChat, + isThreadReportParentAction, + isOffline: !!isOffline, + isMini, + isProduction, + moneyRequestAction, + areHoldRequirementsMet, + isDebugModeEnabled, + iouTransaction, + transactions, + moneyRequestReport, + moneyRequestPolicy, + isHarvestReport, + }; + + let visibleActionIds = ORDERED_ACTION_SHOULD_SHOW.filter((entry) => !disabledActionIds.has(entry.id) && entry.shouldShow(shouldShowArgs)).map((entry) => entry.id); + + if (isMini) { + const overflowMenuId = visibleActionIds.at(-1); + const otherIds = visibleActionIds.slice(0, -1); + if (otherIds.length > CONST.MINI_CONTEXT_MENU_MAX_ITEMS && overflowMenuId) { + visibleActionIds = [...otherIds.slice(0, CONST.MINI_CONTEXT_MENU_MAX_ITEMS - 1), overflowMenuId]; + } else { + visibleActionIds = otherIds; + } + } + + const visibleSet = new Set(visibleActionIds); + + const contentActionIndexes = visibleActionIds + .map((id, index) => { + const entry = ORDERED_ACTION_SHOULD_SHOW.find((e) => e.id === id); + return entry?.isContentAction ? index : undefined; + }) + .filter((index): index is number => index !== undefined); + + const shouldEnableArrowNavigation = !isMini && (isVisible || shouldKeepOpen); + + const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({ + initialFocusedIndex: -1, + disabledIndexes: contentActionIndexes, + maxIndex: visibleActionIds.length - 1, + isActive: shouldEnableArrowNavigation, + }); + + const getFocusProps = (id: ActionId): ContextMenuActionFocusProps => { + const index = visibleActionIds.indexOf(id); + return { + isFocused: focusedIndex === index, + onFocus: () => setFocusedIndex(index), + onBlur: () => (index === visibleActionIds.length - 1 || index === 1) && setFocusedIndex(-1), + }; + }; + + const renderAction = (id: ActionId, Component: React.ComponentType) => { + const {isFocused, onFocus, onBlur} = getFocusProps(id); + return ( + + ); + }; + + const renderOverflowMenu = () => { + const {isFocused, onFocus, onBlur} = getFocusProps('overflowMenu'); + return ( + + ); + }; + /** * Checks if user is anonymous. If true and the action doesn't accept for anonymous user, hides the context menu and * shows the sign in modal. Else, executes the callback. @@ -348,33 +445,33 @@ function BaseReportActionContextMenu({ shouldKeepOpen={shouldKeepOpen} contentRef={contentRef} > - - - - - - - - - - - - - - - - - - - - - - - + {visibleSet.has('emojiReaction') && } + {visibleSet.has('replyInThread') && renderAction('replyInThread', ContextMenuAction.ReplyInThread)} + {visibleSet.has('markAsUnread') && renderAction('markAsUnread', ContextMenuAction.MarkAsUnread)} + {visibleSet.has('explain') && renderAction('explain', ContextMenuAction.Explain)} + {visibleSet.has('markAsRead') && renderAction('markAsRead', ContextMenuAction.MarkAsRead)} + {visibleSet.has('edit') && renderAction('edit', ContextMenuAction.Edit)} + {visibleSet.has('unhold') && renderAction('unhold', ContextMenuAction.Unhold)} + {visibleSet.has('hold') && renderAction('hold', ContextMenuAction.Hold)} + {visibleSet.has('joinThread') && renderAction('joinThread', ContextMenuAction.JoinThread)} + {visibleSet.has('leaveThread') && renderAction('leaveThread', ContextMenuAction.LeaveThread)} + {visibleSet.has('copyUrl') && renderAction('copyUrl', ContextMenuAction.CopyURL)} + {visibleSet.has('copyToClipboard') && renderAction('copyToClipboard', ContextMenuAction.CopyToClipboard)} + {visibleSet.has('copyEmail') && renderAction('copyEmail', ContextMenuAction.CopyEmail)} + {visibleSet.has('copyMessage') && renderAction('copyMessage', ContextMenuAction.CopyMessage)} + {visibleSet.has('copyLink') && renderAction('copyLink', ContextMenuAction.CopyLink)} + {visibleSet.has('pin') && renderAction('pin', ContextMenuAction.Pin)} + {visibleSet.has('unpin') && renderAction('unpin', ContextMenuAction.Unpin)} + {visibleSet.has('flagAsOffensive') && renderAction('flagAsOffensive', ContextMenuAction.FlagAsOffensive)} + {visibleSet.has('download') && renderAction('download', ContextMenuAction.Download)} + {visibleSet.has('copyOnyxData') && renderAction('copyOnyxData', ContextMenuAction.CopyOnyxData)} + {visibleSet.has('debug') && renderAction('debug', ContextMenuAction.Debug)} + {visibleSet.has('delete') && renderAction('delete', ContextMenuAction.Delete)} + {visibleSet.has('overflowMenu') && renderOverflowMenu()} ); } export default BaseReportActionContextMenu; -export type {BaseReportActionContextMenuProps}; +export type {BaseReportActionContextMenuProps, ContextMenuActionFocusProps}; diff --git a/src/pages/inbox/report/ContextMenu/ContextMenuLayout.tsx b/src/pages/inbox/report/ContextMenu/ContextMenuLayout.tsx index 6224021b23bc..77f3ca514a2b 100644 --- a/src/pages/inbox/report/ContextMenu/ContextMenuLayout.tsx +++ b/src/pages/inbox/report/ContextMenu/ContextMenuLayout.tsx @@ -1,30 +1,8 @@ import type {ReactNode, RefObject} from 'react'; -import {createContext, useContext} from 'react'; import {View} from 'react-native'; import FocusTrapForModal from '@components/FocusTrap/FocusTrapForModal'; -import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; -import CONST from '@src/CONST'; -import type {ActionId} from './actions/actionConfig'; -import {ORDERED_ACTION_SHOULD_SHOW} from './actions/actionConfig'; -import {useContextMenuPayload} from './ContextMenuPayloadProvider'; - -type ContextMenuVisibilityContextValue = { - visibleActionIds: ActionId[]; - focusedIndex: number; - setFocusedIndex: (index: number) => void; -}; - -const ContextMenuVisibilityContext = createContext({ - visibleActionIds: [], - focusedIndex: -1, - setFocusedIndex: () => {}, -}); - -function useContextMenuVisibility(): ContextMenuVisibilityContextValue { - return useContext(ContextMenuVisibilityContext); -} type ContextMenuLayoutProps = { isMini: boolean; @@ -38,84 +16,21 @@ function ContextMenuLayout({isMini, isVisible, shouldKeepOpen, contentRef, child const StyleUtils = useStyleUtils(); // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); - const payload = useContextMenuPayload(); - - const shouldShowArgs = { - type: payload.type, - reportAction: payload.reportAction, - childReportActions: payload.childReportActions, - isArchivedRoom: payload.isArchivedRoom, - betas: payload.betas, - menuTarget: payload.anchor, - isChronosReport: payload.isChronosReport, - reportID: payload.reportID, - isPinnedChat: payload.isPinnedChat, - isUnreadChat: payload.isUnreadChat, - isThreadReportParentAction: payload.isThreadReportParentAction, - isOffline: payload.isOffline, - isMini, - isProduction: payload.isProduction, - moneyRequestAction: payload.moneyRequestAction, - areHoldRequirementsMet: payload.areHoldRequirementsMet, - isDebugModeEnabled: payload.isDebugModeEnabled, - iouTransaction: payload.iouTransaction, - transactions: payload.transactions, - moneyRequestReport: payload.moneyRequestReport, - moneyRequestPolicy: payload.moneyRequestPolicy, - isHarvestReport: payload.isHarvestReport, - }; - - let visibleActionIds = ORDERED_ACTION_SHOULD_SHOW.filter((entry) => !payload.disabledActionIds.has(entry.id) && entry.shouldShow(shouldShowArgs)).map((entry) => entry.id); - - if (isMini) { - const overflowMenuId = visibleActionIds.at(-1); - const otherIds = visibleActionIds.slice(0, -1); - if (otherIds.length > CONST.MINI_CONTEXT_MENU_MAX_ITEMS && overflowMenuId) { - visibleActionIds = [...otherIds.slice(0, CONST.MINI_CONTEXT_MENU_MAX_ITEMS - 1), overflowMenuId]; - } else { - visibleActionIds = otherIds; - } - } - - const contentActionIndexes = visibleActionIds - .map((id, index) => { - const entry = ORDERED_ACTION_SHOULD_SHOW.find((e) => e.id === id); - return entry?.isContentAction ? index : undefined; - }) - .filter((index): index is number => index !== undefined); - - const shouldEnableArrowNavigation = !isMini && (isVisible || shouldKeepOpen); - - const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({ - initialFocusedIndex: -1, - disabledIndexes: contentActionIndexes, - maxIndex: visibleActionIds.length - 1, - isActive: shouldEnableArrowNavigation, - }); const wrapperStyle = StyleUtils.getReportActionContextMenuStyles(isMini, shouldUseNarrowLayout); - const contextValue: ContextMenuVisibilityContextValue = { - visibleActionIds, - focusedIndex, - setFocusedIndex, - }; - return ( (isVisible || shouldKeepOpen || !isMini) && ( - - - - {children} - - - + + + {children} + + ) ); } export default ContextMenuLayout; -export {useContextMenuVisibility}; diff --git a/src/pages/inbox/report/ContextMenu/actions/CopyEmail.tsx b/src/pages/inbox/report/ContextMenu/actions/CopyEmail.tsx index ec6cba23f55e..29058df56a65 100644 --- a/src/pages/inbox/report/ContextMenu/actions/CopyEmail.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/CopyEmail.tsx @@ -4,23 +4,16 @@ import useLocalize from '@hooks/useLocalize'; import Clipboard from '@libs/Clipboard'; import EmailUtils from '@libs/EmailUtils'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; -import {ACTION_IDS} from './actionConfig'; -function CopyEmail() { +function CopyEmail({isFocused, onFocus, onBlur}: ContextMenuActionFocusProps) { const {selection, isMini, interceptAnonymousUser} = useContextMenuPayload(); - const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); const icons = useMemoizedLazyExpensifyIcons(['Copy', 'Checkmark'] as const); const {translate} = useLocalize(); - const actionIndex = visibleActionIds.indexOf(ACTION_IDS.COPY_EMAIL); - if (actionIndex === -1) { - return null; - } - const handlePress = () => { Clipboard.setString(EmailUtils.trimMailTo(selection)); hideContextMenu(true, ReportActionComposeFocusManager.focus); @@ -36,9 +29,9 @@ function CopyEmail() { isMini={isMini} isAnonymousAction onPress={() => interceptAnonymousUser(handlePress, true)} - isFocused={focusedIndex === actionIndex} - onFocus={() => setFocusedIndex(actionIndex)} - onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + isFocused={isFocused} + onFocus={onFocus} + onBlur={onBlur} sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_EMAIL} /> ); diff --git a/src/pages/inbox/report/ContextMenu/actions/CopyLink.tsx b/src/pages/inbox/report/ContextMenu/actions/CopyLink.tsx index 8eae26dc718d..ecc81c288e60 100644 --- a/src/pages/inbox/report/ContextMenu/actions/CopyLink.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/CopyLink.tsx @@ -4,23 +4,16 @@ import useLocalize from '@hooks/useLocalize'; import Clipboard from '@libs/Clipboard'; import {getEnvironmentURL} from '@libs/Environment/Environment'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; -import {ACTION_IDS} from './actionConfig'; -function CopyLink() { +function CopyLink({isFocused, onFocus, onBlur}: ContextMenuActionFocusProps) { const {reportAction, originalReportID, isMini, interceptAnonymousUser} = useContextMenuPayload(); - const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); const icons = useMemoizedLazyExpensifyIcons(['LinkCopy', 'Checkmark'] as const); const {translate} = useLocalize(); - const actionIndex = visibleActionIds.indexOf(ACTION_IDS.COPY_LINK); - if (actionIndex === -1) { - return null; - } - const handlePress = () => { getEnvironmentURL().then((environmentURL) => { const reportActionID = reportAction?.reportActionID; @@ -38,9 +31,9 @@ function CopyLink() { isMini={isMini} isAnonymousAction onPress={() => interceptAnonymousUser(handlePress, true)} - isFocused={focusedIndex === actionIndex} - onFocus={() => setFocusedIndex(actionIndex)} - onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + isFocused={isFocused} + onFocus={onFocus} + onBlur={onBlur} sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_LINK} /> ); diff --git a/src/pages/inbox/report/ContextMenu/actions/CopyMessage.tsx b/src/pages/inbox/report/ContextMenu/actions/CopyMessage.tsx index 732a51fabae5..492e3cdd21dc 100644 --- a/src/pages/inbox/report/ContextMenu/actions/CopyMessage.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/CopyMessage.tsx @@ -138,13 +138,13 @@ import { isExpenseReport, } from '@libs/ReportUtils'; import {getTaskCreatedMessage, getTaskReportActionMessage} from '@libs/TaskUtils'; -import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import type {ReportAction} from '@src/types/onyx'; -import {ACTION_IDS, getActionHtml} from './actionConfig'; +import {getActionHtml} from './actionConfig'; function setClipboardMessage(content: string | undefined) { if (!content) { @@ -500,17 +500,11 @@ function copyMessageToClipboard(payload: ContextMenuPayloadContextValue) { } } -function CopyMessage() { +function CopyMessage({isFocused, onFocus, onBlur}: ContextMenuActionFocusProps) { const payload = useContextMenuPayload(); - const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); const icons = useMemoizedLazyExpensifyIcons(['Copy', 'Checkmark'] as const); const {translate} = useLocalize(); - const actionIndex = visibleActionIds.indexOf(ACTION_IDS.COPY_MESSAGE); - if (actionIndex === -1) { - return null; - } - const closePopover = !payload.isMini; const handlePress = () => { @@ -529,9 +523,9 @@ function CopyMessage() { isMini={payload.isMini} isAnonymousAction onPress={() => payload.interceptAnonymousUser(handlePress, true)} - isFocused={focusedIndex === actionIndex} - onFocus={() => setFocusedIndex(actionIndex)} - onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + isFocused={isFocused} + onFocus={onFocus} + onBlur={onBlur} sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_MESSAGE} /> ); diff --git a/src/pages/inbox/report/ContextMenu/actions/CopyOnyxData.tsx b/src/pages/inbox/report/ContextMenu/actions/CopyOnyxData.tsx index 048bd585478e..a8b4c084e396 100644 --- a/src/pages/inbox/report/ContextMenu/actions/CopyOnyxData.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/CopyOnyxData.tsx @@ -3,23 +3,16 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import Clipboard from '@libs/Clipboard'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; -import {ACTION_IDS} from './actionConfig'; -function CopyOnyxData() { +function CopyOnyxData({isFocused, onFocus, onBlur}: ContextMenuActionFocusProps) { const {report, isMini, interceptAnonymousUser} = useContextMenuPayload(); - const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); const icons = useMemoizedLazyExpensifyIcons(['Copy', 'Checkmark'] as const); const {translate} = useLocalize(); - const actionIndex = visibleActionIds.indexOf(ACTION_IDS.COPY_ONYX_DATA); - if (actionIndex === -1) { - return null; - } - const handlePress = () => { Clipboard.setString(JSON.stringify(report, null, 4)); hideContextMenu(true, ReportActionComposeFocusManager.focus); @@ -34,9 +27,9 @@ function CopyOnyxData() { isMini={isMini} isAnonymousAction onPress={() => interceptAnonymousUser(handlePress, true)} - isFocused={focusedIndex === actionIndex} - onFocus={() => setFocusedIndex(actionIndex)} - onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + isFocused={isFocused} + onFocus={onFocus} + onBlur={onBlur} sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_ONYX_DATA} /> ); diff --git a/src/pages/inbox/report/ContextMenu/actions/CopyToClipboard.tsx b/src/pages/inbox/report/ContextMenu/actions/CopyToClipboard.tsx index 04238e53ca6a..fcfaead79f1f 100644 --- a/src/pages/inbox/report/ContextMenu/actions/CopyToClipboard.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/CopyToClipboard.tsx @@ -3,23 +3,16 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import Clipboard from '@libs/Clipboard'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; -import {ACTION_IDS} from './actionConfig'; -function CopyToClipboard() { +function CopyToClipboard({isFocused, onFocus, onBlur}: ContextMenuActionFocusProps) { const {selection, isMini, interceptAnonymousUser} = useContextMenuPayload(); - const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Copy', 'Checkmark'] as const); - const actionIndex = visibleActionIds.indexOf(ACTION_IDS.COPY_TO_CLIPBOARD); - if (actionIndex === -1) { - return null; - } - const handlePress = () => { Clipboard.setString(selection); hideContextMenu(true, ReportActionComposeFocusManager.focus); @@ -34,9 +27,9 @@ function CopyToClipboard() { isMini={isMini} isAnonymousAction onPress={() => interceptAnonymousUser(handlePress, true)} - isFocused={focusedIndex === actionIndex} - onFocus={() => setFocusedIndex(actionIndex)} - onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + isFocused={isFocused} + onFocus={onFocus} + onBlur={onBlur} sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_TO_CLIPBOARD} /> ); diff --git a/src/pages/inbox/report/ContextMenu/actions/CopyURL.tsx b/src/pages/inbox/report/ContextMenu/actions/CopyURL.tsx index 547e750c2f3c..2d24c245fb6a 100644 --- a/src/pages/inbox/report/ContextMenu/actions/CopyURL.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/CopyURL.tsx @@ -3,23 +3,16 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import Clipboard from '@libs/Clipboard'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; -import {ACTION_IDS} from './actionConfig'; -function CopyURL() { +function CopyURL({isFocused, onFocus, onBlur}: ContextMenuActionFocusProps) { const {selection, isMini, interceptAnonymousUser} = useContextMenuPayload(); - const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Copy', 'Checkmark'] as const); - const actionIndex = visibleActionIds.indexOf(ACTION_IDS.COPY_URL); - if (actionIndex === -1) { - return null; - } - const handlePress = () => { Clipboard.setString(selection); hideContextMenu(true, ReportActionComposeFocusManager.focus); @@ -35,9 +28,9 @@ function CopyURL() { isMini={isMini} isAnonymousAction onPress={() => interceptAnonymousUser(handlePress, true)} - isFocused={focusedIndex === actionIndex} - onFocus={() => setFocusedIndex(actionIndex)} - onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + isFocused={isFocused} + onFocus={onFocus} + onBlur={onBlur} sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_URL} /> ); diff --git a/src/pages/inbox/report/ContextMenu/actions/Debug.tsx b/src/pages/inbox/report/ContextMenu/actions/Debug.tsx index 0d28718f1950..010fd1fe74b8 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Debug.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/Debug.tsx @@ -3,24 +3,17 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; -import {ACTION_IDS} from './actionConfig'; -function Debug() { +function Debug({isFocused, onFocus, onBlur}: ContextMenuActionFocusProps) { const {reportID, reportAction, isMini, interceptAnonymousUser} = useContextMenuPayload(); - const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); const icons = useMemoizedLazyExpensifyIcons(['Bug'] as const); const {translate} = useLocalize(); - const actionIndex = visibleActionIds.indexOf(ACTION_IDS.DEBUG); - if (actionIndex === -1) { - return null; - } - const handlePress = () => { if (!reportID) { return; @@ -40,9 +33,9 @@ function Debug() { isMini={isMini} isAnonymousAction onPress={() => interceptAnonymousUser(handlePress, true)} - isFocused={focusedIndex === actionIndex} - onFocus={() => setFocusedIndex(actionIndex)} - onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + isFocused={isFocused} + onFocus={onFocus} + onBlur={onBlur} sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.DEBUG} /> ); diff --git a/src/pages/inbox/report/ContextMenu/actions/Delete.tsx b/src/pages/inbox/report/ContextMenu/actions/Delete.tsx index 73889bde03f1..8e7250c4a443 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Delete.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/Delete.tsx @@ -2,23 +2,16 @@ import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils'; -import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu, showDeleteModal} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; -import {ACTION_IDS} from './actionConfig'; -function Delete() { +function Delete({isFocused, onFocus, onBlur}: ContextMenuActionFocusProps) { const {reportID, reportAction, moneyRequestAction, isMini} = useContextMenuPayload(); - const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); const icons = useMemoizedLazyExpensifyIcons(['Trashcan'] as const); const {translate} = useLocalize(); - const actionIndex = visibleActionIds.indexOf(ACTION_IDS.DELETE); - if (actionIndex === -1) { - return null; - } - const closePopover = !isMini; const handlePress = () => { @@ -38,9 +31,9 @@ function Delete() { text={translate('common.delete')} isMini={isMini} onPress={handlePress} - isFocused={focusedIndex === actionIndex} - onFocus={() => setFocusedIndex(actionIndex)} - onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + isFocused={isFocused} + onFocus={onFocus} + onBlur={onBlur} sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.DELETE} /> ); diff --git a/src/pages/inbox/report/ContextMenu/actions/Download.tsx b/src/pages/inbox/report/ContextMenu/actions/Download.tsx index 5234b4656d88..bf4c52e3e4b7 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Download.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/Download.tsx @@ -6,24 +6,18 @@ import {isMobileSafari} from '@libs/Browser'; import fileDownload from '@libs/fileDownload'; import getAttachmentDetails from '@libs/fileDownload/getAttachmentDetails'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {setDownload} from '@userActions/Download'; import CONST from '@src/CONST'; -import {ACTION_IDS, getActionHtml} from './actionConfig'; +import {getActionHtml} from './actionConfig'; -function Download() { +function Download({isFocused, onFocus, onBlur}: ContextMenuActionFocusProps) { const {reportAction, encryptedAuthToken, isMini, interceptAnonymousUser, download} = useContextMenuPayload(); - const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); const icons = useMemoizedLazyExpensifyIcons(['Download'] as const); const {translate} = useLocalize(); - const actionIndex = visibleActionIds.indexOf(ACTION_IDS.DOWNLOAD); - if (actionIndex === -1) { - return null; - } - const closePopover = !isMini; const isDownloading = download?.isDownloading ?? false; @@ -52,9 +46,9 @@ function Download() { shouldShowLoadingSpinnerIcon={isDownloading} isAnonymousAction onPress={() => interceptAnonymousUser(handlePress, true)} - isFocused={focusedIndex === actionIndex} - onFocus={() => setFocusedIndex(actionIndex)} - onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + isFocused={isFocused} + onFocus={onFocus} + onBlur={onBlur} sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.DOWNLOAD} /> ); diff --git a/src/pages/inbox/report/ContextMenu/actions/Edit.tsx b/src/pages/inbox/report/ContextMenu/actions/Edit.tsx index 121852380aa9..47921ef0c60e 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Edit.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/Edit.tsx @@ -4,24 +4,19 @@ import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; import Parser from '@libs/Parser'; import {isMoneyRequestAction} from '@libs/ReportActionsUtils'; -import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {deleteReportActionDraft, openReport, saveReportActionDraft} from '@userActions/Report'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; -import {ACTION_IDS, getActionHtml} from './actionConfig'; +import {getActionHtml} from './actionConfig'; -function Edit() { +function Edit({isFocused, onFocus, onBlur}: ContextMenuActionFocusProps) { const {reportID, reportAction, moneyRequestAction, draftMessage, introSelected, isMini, interceptAnonymousUser} = useContextMenuPayload(); - const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Pencil'] as const); - const actionIndex = visibleActionIds.indexOf(ACTION_IDS.EDIT); - if (actionIndex === -1) { - return null; - } const closePopover = !isMini; const handlePress = () => { @@ -58,9 +53,9 @@ function Edit() { text={translate('reportActionContextMenu.editAction', {action: moneyRequestAction ?? reportAction})} isMini={isMini} onPress={() => interceptAnonymousUser(handlePress)} - isFocused={focusedIndex === actionIndex} - onFocus={() => setFocusedIndex(actionIndex)} - onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + isFocused={isFocused} + onFocus={onFocus} + onBlur={onBlur} sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.EDIT_COMMENT} /> ); diff --git a/src/pages/inbox/report/ContextMenu/actions/EmojiReaction.tsx b/src/pages/inbox/report/ContextMenu/actions/EmojiReaction.tsx index e0d8e60d136f..4834af4af456 100644 --- a/src/pages/inbox/report/ContextMenu/actions/EmojiReaction.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/EmojiReaction.tsx @@ -2,20 +2,13 @@ import type {OnyxEntry} from 'react-native-onyx'; import type {Emoji} from '@assets/emojis/types'; import MiniQuickEmojiReactions from '@components/Reactions/MiniQuickEmojiReactions'; import QuickEmojiReactions from '@components/Reactions/QuickEmojiReactions'; -import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {toggleEmojiReaction} from '@userActions/Report'; import type {ReportActionReactions} from '@src/types/onyx'; -import {ACTION_IDS} from './actionConfig'; function EmojiReaction() { const {reportID, reportAction, currentUserAccountID, close, openContextMenu, setIsEmojiPickerActive, isMini, interceptAnonymousUser} = useContextMenuPayload(); - const {visibleActionIds} = useContextMenuVisibility(); - - if (!visibleActionIds.includes(ACTION_IDS.EMOJI_REACTION)) { - return null; - } const closeContextMenu = (onHideCallback?: () => void) => { if (isMini) { diff --git a/src/pages/inbox/report/ContextMenu/actions/Explain.tsx b/src/pages/inbox/report/ContextMenu/actions/Explain.tsx index 33646053cbbe..11b073fae670 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Explain.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/Explain.tsx @@ -1,24 +1,18 @@ import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; -import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {explain} from '@userActions/Report'; import CONST from '@src/CONST'; import KeyboardUtils from '@src/utils/keyboard'; -import {ACTION_IDS} from './actionConfig'; -function Explain() { +function Explain({isFocused, onFocus, onBlur}: ContextMenuActionFocusProps) { const {childReport, originalReport, reportAction, currentUserPersonalDetails, isMini, interceptAnonymousUser} = useContextMenuPayload(); - const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Concierge'] as const); - const actionIndex = visibleActionIds.indexOf(ACTION_IDS.EXPLAIN); - if (actionIndex === -1) { - return null; - } const closePopover = !isMini; const handlePress = () => { @@ -42,9 +36,9 @@ function Explain() { text={translate('reportActionContextMenu.explain')} isMini={isMini} onPress={() => interceptAnonymousUser(handlePress)} - isFocused={focusedIndex === actionIndex} - onFocus={() => setFocusedIndex(actionIndex)} - onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + isFocused={isFocused} + onFocus={onFocus} + onBlur={onBlur} sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.EXPLAIN} /> ); diff --git a/src/pages/inbox/report/ContextMenu/actions/FlagAsOffensive.tsx b/src/pages/inbox/report/ContextMenu/actions/FlagAsOffensive.tsx index cffc24bb5f04..0009ab887741 100644 --- a/src/pages/inbox/report/ContextMenu/actions/FlagAsOffensive.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/FlagAsOffensive.tsx @@ -2,25 +2,18 @@ import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; -import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import KeyboardUtils from '@src/utils/keyboard'; -import {ACTION_IDS} from './actionConfig'; -function FlagAsOffensive() { +function FlagAsOffensive({isFocused, onFocus, onBlur}: ContextMenuActionFocusProps) { const {reportID, reportAction, isMini} = useContextMenuPayload(); - const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); const icons = useMemoizedLazyExpensifyIcons(['Flag'] as const); const {translate} = useLocalize(); - const actionIndex = visibleActionIds.indexOf(ACTION_IDS.FLAG_AS_OFFENSIVE); - if (actionIndex === -1) { - return null; - } - const closePopover = !isMini; const handlePress = () => { @@ -45,9 +38,9 @@ function FlagAsOffensive() { text={translate('reportActionContextMenu.flagAsOffensive')} isMini={isMini} onPress={handlePress} - isFocused={focusedIndex === actionIndex} - onFocus={() => setFocusedIndex(actionIndex)} - onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + isFocused={isFocused} + onFocus={onFocus} + onBlur={onBlur} sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.FLAG_AS_OFFENSIVE} /> ); diff --git a/src/pages/inbox/report/ContextMenu/actions/Hold.tsx b/src/pages/inbox/report/ContextMenu/actions/Hold.tsx index cac935f434c8..57121461da02 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Hold.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/Hold.tsx @@ -2,22 +2,16 @@ import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import {changeMoneyRequestHoldStatus} from '@libs/ReportUtils'; -import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; -import {ACTION_IDS} from './actionConfig'; -function Hold() { +function Hold({isFocused, onFocus, onBlur}: ContextMenuActionFocusProps) { const {moneyRequestAction, isDelegateAccessRestricted, showDelegateNoAccessModal, isMini, interceptAnonymousUser} = useContextMenuPayload(); - const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Stopwatch'] as const); - const actionIndex = visibleActionIds.indexOf(ACTION_IDS.HOLD); - if (actionIndex === -1) { - return null; - } const closePopover = !isMini; const handlePress = () => { @@ -38,9 +32,9 @@ function Hold() { text={translate('iou.hold')} isMini={isMini} onPress={() => interceptAnonymousUser(handlePress, false)} - isFocused={focusedIndex === actionIndex} - onFocus={() => setFocusedIndex(actionIndex)} - onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + isFocused={isFocused} + onFocus={onFocus} + onBlur={onBlur} sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.HOLD} /> ); diff --git a/src/pages/inbox/report/ContextMenu/actions/JoinThread.tsx b/src/pages/inbox/report/ContextMenu/actions/JoinThread.tsx index cbcd92a7790a..34f3086b1514 100644 --- a/src/pages/inbox/report/ContextMenu/actions/JoinThread.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/JoinThread.tsx @@ -3,23 +3,17 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import {getChildReportNotificationPreference} from '@libs/ReportUtils'; -import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {toggleSubscribeToChildReport} from '@userActions/Report'; import CONST from '@src/CONST'; -import {ACTION_IDS} from './actionConfig'; -function JoinThread() { +function JoinThread({isFocused, onFocus, onBlur}: ContextMenuActionFocusProps) { const {reportAction, originalReport, currentUserAccountID, isMini, interceptAnonymousUser} = useContextMenuPayload(); - const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Bell'] as const); - const actionIndex = visibleActionIds.indexOf(ACTION_IDS.JOIN_THREAD); - if (actionIndex === -1) { - return null; - } const closePopover = !isMini; const handlePress = () => { @@ -41,9 +35,9 @@ function JoinThread() { text={translate('reportActionContextMenu.joinThread')} isMini={isMini} onPress={() => interceptAnonymousUser(handlePress, false)} - isFocused={focusedIndex === actionIndex} - onFocus={() => setFocusedIndex(actionIndex)} - onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + isFocused={isFocused} + onFocus={onFocus} + onBlur={onBlur} sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.JOIN_THREAD} /> ); diff --git a/src/pages/inbox/report/ContextMenu/actions/LeaveThread.tsx b/src/pages/inbox/report/ContextMenu/actions/LeaveThread.tsx index 335f6180cc92..9ed7ded644c7 100644 --- a/src/pages/inbox/report/ContextMenu/actions/LeaveThread.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/LeaveThread.tsx @@ -3,23 +3,17 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import {getChildReportNotificationPreference} from '@libs/ReportUtils'; -import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {toggleSubscribeToChildReport} from '@userActions/Report'; import CONST from '@src/CONST'; -import {ACTION_IDS} from './actionConfig'; -function LeaveThread() { +function LeaveThread({isFocused, onFocus, onBlur}: ContextMenuActionFocusProps) { const {reportAction, originalReport, currentUserAccountID, isMini, interceptAnonymousUser} = useContextMenuPayload(); - const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Exit'] as const); - const actionIndex = visibleActionIds.indexOf(ACTION_IDS.LEAVE_THREAD); - if (actionIndex === -1) { - return null; - } const closePopover = !isMini; const handlePress = () => { @@ -41,9 +35,9 @@ function LeaveThread() { text={translate('reportActionContextMenu.leaveThread')} isMini={isMini} onPress={() => interceptAnonymousUser(handlePress, false)} - isFocused={focusedIndex === actionIndex} - onFocus={() => setFocusedIndex(actionIndex)} - onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + isFocused={isFocused} + onFocus={onFocus} + onBlur={onBlur} sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.LEAVE_THREAD} /> ); diff --git a/src/pages/inbox/report/ContextMenu/actions/MarkAsRead.tsx b/src/pages/inbox/report/ContextMenu/actions/MarkAsRead.tsx index b969ab4bb016..c583bb48ca3a 100644 --- a/src/pages/inbox/report/ContextMenu/actions/MarkAsRead.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/MarkAsRead.tsx @@ -2,23 +2,17 @@ import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {readNewestAction} from '@userActions/Report'; import CONST from '@src/CONST'; -import {ACTION_IDS} from './actionConfig'; -function MarkAsRead() { +function MarkAsRead({isFocused, onFocus, onBlur}: ContextMenuActionFocusProps) { const {reportID, isMini, interceptAnonymousUser} = useContextMenuPayload(); - const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Mail', 'Checkmark'] as const); - const actionIndex = visibleActionIds.indexOf(ACTION_IDS.MARK_AS_READ); - if (actionIndex === -1) { - return null; - } const closePopover = !isMini; const handlePress = () => { @@ -35,9 +29,9 @@ function MarkAsRead() { successIcon={icons.Checkmark} isMini={isMini} onPress={() => interceptAnonymousUser(handlePress)} - isFocused={focusedIndex === actionIndex} - onFocus={() => setFocusedIndex(actionIndex)} - onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + isFocused={isFocused} + onFocus={onFocus} + onBlur={onBlur} sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.MARK_AS_READ} /> ); diff --git a/src/pages/inbox/report/ContextMenu/actions/MarkAsUnread.tsx b/src/pages/inbox/report/ContextMenu/actions/MarkAsUnread.tsx index 5d1533c3acaa..ed7f944c118c 100644 --- a/src/pages/inbox/report/ContextMenu/actions/MarkAsUnread.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/MarkAsUnread.tsx @@ -2,23 +2,17 @@ import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {markCommentAsUnread} from '@userActions/Report'; import CONST from '@src/CONST'; -import {ACTION_IDS} from './actionConfig'; -function MarkAsUnread() { +function MarkAsUnread({isFocused, onFocus, onBlur}: ContextMenuActionFocusProps) { const {reportID, reportActions, reportAction, currentUserAccountID, isMini, interceptAnonymousUser} = useContextMenuPayload(); - const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['ChatBubbleUnread', 'Checkmark'] as const); - const actionIndex = visibleActionIds.indexOf(ACTION_IDS.MARK_AS_UNREAD); - if (actionIndex === -1) { - return null; - } const closePopover = !isMini; const handlePress = () => { @@ -35,9 +29,9 @@ function MarkAsUnread() { successIcon={icons.Checkmark} isMini={isMini} onPress={() => interceptAnonymousUser(handlePress)} - isFocused={focusedIndex === actionIndex} - onFocus={() => setFocusedIndex(actionIndex)} - onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + isFocused={isFocused} + onFocus={onFocus} + onBlur={onBlur} sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.MARK_AS_UNREAD} /> ); diff --git a/src/pages/inbox/report/ContextMenu/actions/OverflowMenu.tsx b/src/pages/inbox/report/ContextMenu/actions/OverflowMenu.tsx index 94089b5d169a..0604a8208956 100644 --- a/src/pages/inbox/report/ContextMenu/actions/OverflowMenu.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/OverflowMenu.tsx @@ -3,23 +3,21 @@ import type {GestureResponderEvent, View} from 'react-native'; import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; -import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import CONST from '@src/CONST'; -import {ACTION_IDS} from './actionConfig'; +import type {ActionId} from './actionConfig'; -function OverflowMenu() { +type OverflowMenuProps = ContextMenuActionFocusProps & { + visibleActionIds: ActionId[]; +}; + +function OverflowMenu({isFocused, onFocus, onBlur, visibleActionIds}: OverflowMenuProps) { const {openOverflowMenu, openContextMenu, isMini, interceptAnonymousUser} = useContextMenuPayload(); - const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); const icons = useMemoizedLazyExpensifyIcons(['ThreeDots'] as const); const {translate} = useLocalize(); const threeDotRef = useRef(null); - const actionIndex = visibleActionIds.indexOf(ACTION_IDS.OVERFLOW_MENU); - if (actionIndex === -1) { - return null; - } - const handlePress = (event?: GestureResponderEvent | MouseEvent | KeyboardEvent) => { openOverflowMenu(event as GestureResponderEvent | MouseEvent, threeDotRef, new Set(visibleActionIds)); openContextMenu(); @@ -33,9 +31,9 @@ function OverflowMenu() { isMini={isMini} onPress={(event) => interceptAnonymousUser(() => handlePress(event), true)} isAnonymousAction - isFocused={focusedIndex === actionIndex} - onFocus={() => setFocusedIndex(actionIndex)} - onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + isFocused={isFocused} + onFocus={onFocus} + onBlur={onBlur} shouldPreventDefaultFocusOnPress={false} sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.MENU} /> diff --git a/src/pages/inbox/report/ContextMenu/actions/Pin.tsx b/src/pages/inbox/report/ContextMenu/actions/Pin.tsx index 1ea43eef3de4..82e5fdccabc3 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Pin.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/Pin.tsx @@ -2,24 +2,17 @@ import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {togglePinnedState} from '@userActions/Report'; import CONST from '@src/CONST'; -import {ACTION_IDS} from './actionConfig'; -function Pin() { +function Pin({isFocused, onFocus, onBlur}: ContextMenuActionFocusProps) { const {reportID, isMini, interceptAnonymousUser} = useContextMenuPayload(); - const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); const icons = useMemoizedLazyExpensifyIcons(['Pin'] as const); const {translate} = useLocalize(); - const actionIndex = visibleActionIds.indexOf(ACTION_IDS.PIN); - if (actionIndex === -1) { - return null; - } - const closePopover = !isMini; const handlePress = () => { @@ -35,9 +28,9 @@ function Pin() { text={translate('common.pin')} isMini={isMini} onPress={() => interceptAnonymousUser(handlePress)} - isFocused={focusedIndex === actionIndex} - onFocus={() => setFocusedIndex(actionIndex)} - onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + isFocused={isFocused} + onFocus={onFocus} + onBlur={onBlur} sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.PIN} /> ); diff --git a/src/pages/inbox/report/ContextMenu/actions/ReplyInThread.tsx b/src/pages/inbox/report/ContextMenu/actions/ReplyInThread.tsx index f9686ccaae28..f4aa39b8a3c1 100644 --- a/src/pages/inbox/report/ContextMenu/actions/ReplyInThread.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/ReplyInThread.tsx @@ -1,24 +1,18 @@ import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; -import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {navigateToAndOpenChildReport} from '@userActions/Report'; import CONST from '@src/CONST'; import KeyboardUtils from '@src/utils/keyboard'; -import {ACTION_IDS} from './actionConfig'; -function ReplyInThread() { +function ReplyInThread({isFocused, onFocus, onBlur}: ContextMenuActionFocusProps) { const {childReport, reportAction, originalReport, currentUserAccountID, interceptAnonymousUser, isMini} = useContextMenuPayload(); - const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['ChatBubbleReply'] as const); - const actionIndex = visibleActionIds.indexOf(ACTION_IDS.REPLY_IN_THREAD); - if (actionIndex === -1) { - return null; - } const closePopover = !isMini; const handlePress = () => @@ -40,9 +34,9 @@ function ReplyInThread() { text={translate('reportActionContextMenu.replyInThread')} isMini={isMini} onPress={handlePress} - isFocused={focusedIndex === actionIndex} - onFocus={() => setFocusedIndex(actionIndex)} - onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + isFocused={isFocused} + onFocus={onFocus} + onBlur={onBlur} sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.REPLY_IN_THREAD} /> ); diff --git a/src/pages/inbox/report/ContextMenu/actions/Unhold.tsx b/src/pages/inbox/report/ContextMenu/actions/Unhold.tsx index b5e01fd03332..42de4a87316a 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Unhold.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/Unhold.tsx @@ -2,22 +2,16 @@ import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import {changeMoneyRequestHoldStatus} from '@libs/ReportUtils'; -import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; -import {ACTION_IDS} from './actionConfig'; -function Unhold() { +function Unhold({isFocused, onFocus, onBlur}: ContextMenuActionFocusProps) { const {moneyRequestAction, isDelegateAccessRestricted, showDelegateNoAccessModal, isMini, interceptAnonymousUser} = useContextMenuPayload(); - const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Stopwatch'] as const); - const actionIndex = visibleActionIds.indexOf(ACTION_IDS.UNHOLD); - if (actionIndex === -1) { - return null; - } const closePopover = !isMini; const handlePress = () => { @@ -38,9 +32,9 @@ function Unhold() { text={translate('iou.unhold')} isMini={isMini} onPress={() => interceptAnonymousUser(handlePress, false)} - isFocused={focusedIndex === actionIndex} - onFocus={() => setFocusedIndex(actionIndex)} - onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + isFocused={isFocused} + onFocus={onFocus} + onBlur={onBlur} sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.UNHOLD} /> ); diff --git a/src/pages/inbox/report/ContextMenu/actions/Unpin.tsx b/src/pages/inbox/report/ContextMenu/actions/Unpin.tsx index a54817b3ef7b..b10a5d3a57c1 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Unpin.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/Unpin.tsx @@ -2,24 +2,17 @@ import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import {useContextMenuVisibility} from '@pages/inbox/report/ContextMenu/ContextMenuLayout'; +import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {togglePinnedState} from '@userActions/Report'; import CONST from '@src/CONST'; -import {ACTION_IDS} from './actionConfig'; -function Unpin() { +function Unpin({isFocused, onFocus, onBlur}: ContextMenuActionFocusProps) { const {reportID, isMini, interceptAnonymousUser} = useContextMenuPayload(); - const {visibleActionIds, focusedIndex, setFocusedIndex} = useContextMenuVisibility(); const icons = useMemoizedLazyExpensifyIcons(['Pin'] as const); const {translate} = useLocalize(); - const actionIndex = visibleActionIds.indexOf(ACTION_IDS.UNPIN); - if (actionIndex === -1) { - return null; - } - const closePopover = !isMini; const handlePress = () => { @@ -35,9 +28,9 @@ function Unpin() { text={translate('common.unPin')} isMini={isMini} onPress={() => interceptAnonymousUser(handlePress)} - isFocused={focusedIndex === actionIndex} - onFocus={() => setFocusedIndex(actionIndex)} - onBlur={() => (actionIndex === visibleActionIds.length - 1 || actionIndex === 1) && setFocusedIndex(-1)} + isFocused={isFocused} + onFocus={onFocus} + onBlur={onBlur} sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.UNPIN} /> ); From 20c362a9af31e6d5b6dda45beb35237cb440351e Mon Sep 17 00:00:00 2001 From: rory Date: Sat, 28 Feb 2026 07:54:37 -0800 Subject: [PATCH 19/88] refactor(contextmenu): inline ContextMenuLayout into BaseReportActionContextMenu ContextMenuLayout was a thin wrapper after the previous refactor. Inline its FocusTrap, View, and styles directly into BaseReportActionContextMenu and delete the file. Made-with: Cursor --- .../BaseReportActionContextMenu.tsx | 77 +++++++++++-------- .../report/ContextMenu/ContextMenuLayout.tsx | 36 --------- 2 files changed, 43 insertions(+), 70 deletions(-) delete mode 100644 src/pages/inbox/report/ContextMenu/ContextMenuLayout.tsx diff --git a/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx index 8e40a7786598..fd8dc821610d 100755 --- a/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx +++ b/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx @@ -1,11 +1,12 @@ import type {RefObject} from 'react'; import React, {useState} from 'react'; -import {InteractionManager} from 'react-native'; +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 * as ActionSheetAwareScrollView from '@components/ActionSheetAwareScrollView'; import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; +import FocusTrapForModal from '@components/FocusTrap/FocusTrapForModal'; import {useSession} from '@components/OnyxListItemProvider'; import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; @@ -16,7 +17,9 @@ import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePaginatedReportActions from '@hooks/usePaginatedReportActions'; import useReportIsArchived from '@hooks/useReportIsArchived'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useRestoreInputFocus from '@hooks/useRestoreInputFocus'; +import useStyleUtils from '@hooks/useStyleUtils'; import useTransactionsAndViolationsForReport from '@hooks/useTransactionsAndViolationsForReport'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import {getMovedReportID} from '@libs/ModifiedExpenseMessage'; @@ -40,7 +43,6 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {ActionId} from './actions/actionConfig'; import {ORDERED_ACTION_SHOULD_SHOW} from './actions/actionConfig'; import ContextMenuAction from './actions/ContextMenuAction'; -import ContextMenuLayout from './ContextMenuLayout'; import {ContextMenuPayloadContext} from './ContextMenuPayloadProvider'; import type {ContextMenuPayloadContextValue} from './ContextMenuPayloadProvider'; import {useMiniContextMenuActions} from './MiniContextMenuProvider'; @@ -135,6 +137,9 @@ function BaseReportActionContextMenu({ disabledActionIds = new Set(), setIsEmojiPickerActive, }: BaseReportActionContextMenuProps) { + const StyleUtils = useStyleUtils(); + // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth + const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); const {transitionActionSheetState} = ActionSheetAwareScrollView.useActionSheetAwareScrollViewActions(); const {isDelegateAccessRestricted} = useDelegateNoAccessState(); const {showDelegateNoAccessModal} = useDelegateNoAccessActions(); @@ -437,39 +442,43 @@ function BaseReportActionContextMenu({ disabledActionIds, }; + const wrapperStyle = StyleUtils.getReportActionContextMenuStyles(isMini, shouldUseNarrowLayout); + return ( - - - {visibleSet.has('emojiReaction') && } - {visibleSet.has('replyInThread') && renderAction('replyInThread', ContextMenuAction.ReplyInThread)} - {visibleSet.has('markAsUnread') && renderAction('markAsUnread', ContextMenuAction.MarkAsUnread)} - {visibleSet.has('explain') && renderAction('explain', ContextMenuAction.Explain)} - {visibleSet.has('markAsRead') && renderAction('markAsRead', ContextMenuAction.MarkAsRead)} - {visibleSet.has('edit') && renderAction('edit', ContextMenuAction.Edit)} - {visibleSet.has('unhold') && renderAction('unhold', ContextMenuAction.Unhold)} - {visibleSet.has('hold') && renderAction('hold', ContextMenuAction.Hold)} - {visibleSet.has('joinThread') && renderAction('joinThread', ContextMenuAction.JoinThread)} - {visibleSet.has('leaveThread') && renderAction('leaveThread', ContextMenuAction.LeaveThread)} - {visibleSet.has('copyUrl') && renderAction('copyUrl', ContextMenuAction.CopyURL)} - {visibleSet.has('copyToClipboard') && renderAction('copyToClipboard', ContextMenuAction.CopyToClipboard)} - {visibleSet.has('copyEmail') && renderAction('copyEmail', ContextMenuAction.CopyEmail)} - {visibleSet.has('copyMessage') && renderAction('copyMessage', ContextMenuAction.CopyMessage)} - {visibleSet.has('copyLink') && renderAction('copyLink', ContextMenuAction.CopyLink)} - {visibleSet.has('pin') && renderAction('pin', ContextMenuAction.Pin)} - {visibleSet.has('unpin') && renderAction('unpin', ContextMenuAction.Unpin)} - {visibleSet.has('flagAsOffensive') && renderAction('flagAsOffensive', ContextMenuAction.FlagAsOffensive)} - {visibleSet.has('download') && renderAction('download', ContextMenuAction.Download)} - {visibleSet.has('copyOnyxData') && renderAction('copyOnyxData', ContextMenuAction.CopyOnyxData)} - {visibleSet.has('debug') && renderAction('debug', ContextMenuAction.Debug)} - {visibleSet.has('delete') && renderAction('delete', ContextMenuAction.Delete)} - {visibleSet.has('overflowMenu') && renderOverflowMenu()} - - + (isVisible || shouldKeepOpen || !isMini) && ( + + + + {visibleSet.has('emojiReaction') && } + {visibleSet.has('replyInThread') && renderAction('replyInThread', ContextMenuAction.ReplyInThread)} + {visibleSet.has('markAsUnread') && renderAction('markAsUnread', ContextMenuAction.MarkAsUnread)} + {visibleSet.has('explain') && renderAction('explain', ContextMenuAction.Explain)} + {visibleSet.has('markAsRead') && renderAction('markAsRead', ContextMenuAction.MarkAsRead)} + {visibleSet.has('edit') && renderAction('edit', ContextMenuAction.Edit)} + {visibleSet.has('unhold') && renderAction('unhold', ContextMenuAction.Unhold)} + {visibleSet.has('hold') && renderAction('hold', ContextMenuAction.Hold)} + {visibleSet.has('joinThread') && renderAction('joinThread', ContextMenuAction.JoinThread)} + {visibleSet.has('leaveThread') && renderAction('leaveThread', ContextMenuAction.LeaveThread)} + {visibleSet.has('copyUrl') && renderAction('copyUrl', ContextMenuAction.CopyURL)} + {visibleSet.has('copyToClipboard') && renderAction('copyToClipboard', ContextMenuAction.CopyToClipboard)} + {visibleSet.has('copyEmail') && renderAction('copyEmail', ContextMenuAction.CopyEmail)} + {visibleSet.has('copyMessage') && renderAction('copyMessage', ContextMenuAction.CopyMessage)} + {visibleSet.has('copyLink') && renderAction('copyLink', ContextMenuAction.CopyLink)} + {visibleSet.has('pin') && renderAction('pin', ContextMenuAction.Pin)} + {visibleSet.has('unpin') && renderAction('unpin', ContextMenuAction.Unpin)} + {visibleSet.has('flagAsOffensive') && renderAction('flagAsOffensive', ContextMenuAction.FlagAsOffensive)} + {visibleSet.has('download') && renderAction('download', ContextMenuAction.Download)} + {visibleSet.has('copyOnyxData') && renderAction('copyOnyxData', ContextMenuAction.CopyOnyxData)} + {visibleSet.has('debug') && renderAction('debug', ContextMenuAction.Debug)} + {visibleSet.has('delete') && renderAction('delete', ContextMenuAction.Delete)} + {visibleSet.has('overflowMenu') && renderOverflowMenu()} + + + + ) ); } diff --git a/src/pages/inbox/report/ContextMenu/ContextMenuLayout.tsx b/src/pages/inbox/report/ContextMenu/ContextMenuLayout.tsx deleted file mode 100644 index 77f3ca514a2b..000000000000 --- a/src/pages/inbox/report/ContextMenu/ContextMenuLayout.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import type {ReactNode, RefObject} from 'react'; -import {View} from 'react-native'; -import FocusTrapForModal from '@components/FocusTrap/FocusTrapForModal'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useStyleUtils from '@hooks/useStyleUtils'; - -type ContextMenuLayoutProps = { - isMini: boolean; - isVisible: boolean; - shouldKeepOpen: boolean; - contentRef?: RefObject; - children: ReactNode; -}; - -function ContextMenuLayout({isMini, isVisible, shouldKeepOpen, contentRef, children}: ContextMenuLayoutProps) { - const StyleUtils = useStyleUtils(); - // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth - const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); - - const wrapperStyle = StyleUtils.getReportActionContextMenuStyles(isMini, shouldUseNarrowLayout); - - return ( - (isVisible || shouldKeepOpen || !isMini) && ( - - - {children} - - - ) - ); -} - -export default ContextMenuLayout; From 03373f31451874eaa17639b260e9ec28dcd2fedc Mon Sep 17 00:00:00 2001 From: rory Date: Sat, 28 Feb 2026 08:08:18 -0800 Subject: [PATCH 20/88] refactor(contextmenu): derive disabledActionIDs internally Move the disabled action IDs computation from PureReportActionItem into BaseReportActionContextMenu, which already subscribes to the report via Onyx. This eliminates an unnecessary prop threaded through 6 intermediate components (MiniContextMenuProvider, MiniReportActionContextMenu, PopoverReportActionContextMenu, ReportActionContextMenu, OverflowMenu). Also renames disabledActionIds to disabledActionIDs per code style. Made-with: Cursor --- .../BaseReportActionContextMenu.tsx | 19 +++++++++---------- .../ContextMenuPayloadProvider.tsx | 4 ++-- .../ContextMenu/MiniContextMenuProvider.tsx | 1 - .../MiniReportActionContextMenu/index.tsx | 1 - .../PopoverReportActionContextMenu.tsx | 7 ------- .../ContextMenu/ReportActionContextMenu.ts | 1 - .../ContextMenu/actions/OverflowMenu.tsx | 9 +++------ .../inbox/report/PureReportActionItem.tsx | 9 --------- 8 files changed, 14 insertions(+), 37 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx index fd8dc821610d..553660096796 100755 --- a/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx +++ b/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx @@ -25,6 +25,7 @@ import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import {getMovedReportID} from '@libs/ModifiedExpenseMessage'; import {getLinkedTransactionID, getOneTransactionThreadReportID, getOriginalMessage, getReportAction, isDeletedAction, withDEWRoutedActionsObject} from '@libs/ReportActionsUtils'; import { + canWriteInReport, chatIncludesChronosWithID, getHarvestOriginalReportID, getSourceIDFromReportAction, @@ -41,7 +42,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {OriginalMessageIOU, ReportAction} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {ActionId} from './actions/actionConfig'; -import {ORDERED_ACTION_SHOULD_SHOW} from './actions/actionConfig'; +import {ORDERED_ACTION_SHOULD_SHOW, RESTRICTED_READONLY_ACTION_IDS} from './actions/actionConfig'; import ContextMenuAction from './actions/ContextMenuAction'; import {ContextMenuPayloadContext} from './ContextMenuPayloadProvider'; import type {ContextMenuPayloadContextValue} from './ContextMenuPayloadProvider'; @@ -49,6 +50,8 @@ import {useMiniContextMenuActions} from './MiniContextMenuProvider'; import type {ContextMenuAnchor, ContextMenuType} from './ReportActionContextMenu'; import {hideContextMenu, showContextMenu} from './ReportActionContextMenu'; +const EMPTY_SET = new Set(); + type ContextMenuActionFocusProps = { isFocused: boolean; onFocus: () => void; @@ -110,9 +113,6 @@ type BaseReportActionContextMenuProps = { /** Function to check if context menu is active */ checkIfContextMenuActive?: () => void; - /** List of disabled action IDs */ - disabledActionIds?: Set; - /** Function to update emoji picker state */ setIsEmojiPickerActive?: (state: boolean) => void; }; @@ -134,7 +134,6 @@ function BaseReportActionContextMenu({ reportID, originalReportID, checkIfContextMenuActive, - disabledActionIds = new Set(), setIsEmojiPickerActive, }: BaseReportActionContextMenuProps) { const StyleUtils = useStyleUtils(); @@ -162,6 +161,8 @@ function BaseReportActionContextMenu({ }); const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); const [originalReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${originalReportID}`); + + const disabledActionIDs = !canWriteInReport(report) ? RESTRICTED_READONLY_ACTION_IDS : EMPTY_SET; const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${getNonEmptyStringOnyxID(reportID)}`); const [isDebugModeEnabled] = useOnyx(ONYXKEYS.IS_DEBUG_MODE_ENABLED); const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT); @@ -257,7 +258,7 @@ function BaseReportActionContextMenu({ isHarvestReport, }; - let visibleActionIds = ORDERED_ACTION_SHOULD_SHOW.filter((entry) => !disabledActionIds.has(entry.id) && entry.shouldShow(shouldShowArgs)).map((entry) => entry.id); + let visibleActionIds = ORDERED_ACTION_SHOULD_SHOW.filter((entry) => !disabledActionIDs.has(entry.id) && entry.shouldShow(shouldShowArgs)).map((entry) => entry.id); if (isMini) { const overflowMenuId = visibleActionIds.at(-1); @@ -314,7 +315,6 @@ function BaseReportActionContextMenu({ isFocused={isFocused} onFocus={onFocus} onBlur={onBlur} - visibleActionIds={visibleActionIds} /> ); }; @@ -335,7 +335,7 @@ function BaseReportActionContextMenu({ } }; - const openOverflowMenu = (event: GestureResponderEvent | MouseEvent, anchorRef: RefObject, miniVisibleActionIds?: Set) => { + const openOverflowMenu = (event: GestureResponderEvent | MouseEvent, anchorRef: RefObject) => { showContextMenu({ type: CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, event, @@ -363,7 +363,6 @@ function BaseReportActionContextMenu({ } }, }, - disabledActionIds: miniVisibleActionIds, shouldCloseOnTarget: true, isOverflowMenu: true, }); @@ -439,7 +438,7 @@ function BaseReportActionContextMenu({ translate, getLocalDateFromDatetime, anchor, - disabledActionIds, + disabledActionIDs, }; const wrapperStyle = StyleUtils.getReportActionContextMenuStyles(isMini, shouldUseNarrowLayout); diff --git a/src/pages/inbox/report/ContextMenu/ContextMenuPayloadProvider.tsx b/src/pages/inbox/report/ContextMenu/ContextMenuPayloadProvider.tsx index 9bf6d48f9709..85e087d92e79 100644 --- a/src/pages/inbox/report/ContextMenu/ContextMenuPayloadProvider.tsx +++ b/src/pages/inbox/report/ContextMenu/ContextMenuPayloadProvider.tsx @@ -63,7 +63,7 @@ type ContextMenuPayloadContextValue = { transitionActionSheetState: (params: {type: string; payload?: Record}) => void; openContextMenu: () => void; interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; - openOverflowMenu: (event: GestureResponderEvent | MouseEvent, anchorRef: RefObject, miniVisibleActionIds?: Set) => void; + openOverflowMenu: (event: GestureResponderEvent | MouseEvent, anchorRef: RefObject) => void; setIsEmojiPickerActive: ((state: boolean) => void) | undefined; showDelegateNoAccessModal: (() => void) | undefined; @@ -72,7 +72,7 @@ type ContextMenuPayloadContextValue = { anchor: RefObject | undefined; - disabledActionIds: Set; + disabledActionIDs: Set; }; const ContextMenuPayloadContext = createContext(null); diff --git a/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx b/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx index 57eeb6055a4e..a5a07c4a2cf1 100644 --- a/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx @@ -20,7 +20,6 @@ type MiniContextMenuParams = { isThreadReportParentAction: boolean; draftMessage: string | undefined; isChronosReport: boolean; - disabledActionIds: Set; checkIfContextMenuActive: () => void; setIsEmojiPickerActive: (state: boolean) => void; rowMeasurements: RowMeasurements; diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index a7d9807d8e40..9b96b1314429 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -88,7 +88,6 @@ function MiniReportActionContextMenu() { isThreadReportParentAction={state.isThreadReportParentAction} draftMessage={state.draftMessage} isChronosReport={state.isChronosReport} - disabledActionIds={state.disabledActionIds} checkIfContextMenuActive={state.checkIfContextMenuActive} setIsEmojiPickerActive={state.setIsEmojiPickerActive} isVisible={state.isVisible} diff --git a/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx index 2e30a1e892c6..5f2399a122fb 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -15,8 +15,6 @@ import BaseReportActionContextMenu from './BaseReportActionContextMenu'; import ConfirmDeleteReportActionModal from './ConfirmDeleteReportActionModal'; import type {ContextMenuAnchor, ContextMenuType, ReportActionContextMenu} from './ReportActionContextMenu'; -const EMPTY_DISABLED_ACTION_IDS = new Set(); - function extractPointerEvent(event: GestureResponderEvent | MouseEvent): MouseEvent | NativeTouchEvent { if ('nativeEvent' in event) { return event.nativeEvent; @@ -43,7 +41,6 @@ type PopoverContextMenuState = { isPinnedChat: boolean; isUnreadChat: boolean; isThreadReportParentAction: boolean; - disabledActionIds: Set; isOverflowMenu: boolean; withoutOverlay: boolean; position: PopoverPosition; @@ -141,7 +138,6 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro report: currentReport = {}, reportAction: reportActionParam = {}, callbacks = {}, - disabledActionIds = new Set(), shouldCloseOnTarget = false, isOverflowMenu = false, withoutOverlay = true, @@ -209,7 +205,6 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro isPinnedChat, isUnreadChat, isThreadReportParentAction: isThreadReportParentActionParam, - disabledActionIds, isOverflowMenu, withoutOverlay, position, @@ -291,7 +286,6 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro isPinnedChat: false, isUnreadChat: false, isThreadReportParentAction: false, - disabledActionIds: EMPTY_DISABLED_ACTION_IDS, isOverflowMenu: false, withoutOverlay: true, position: {anchorHorizontal: 0, anchorVertical: 0, anchorWidth: 0, anchorHeight: 0}, @@ -375,7 +369,6 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro anchor={{current: menuState?.contextMenuTargetNode ?? null}} contentRef={contentRef} originalReportID={menuState?.originalReportID} - disabledActionIds={menuState?.disabledActionIds ?? EMPTY_DISABLED_ACTION_IDS} setIsEmojiPickerActive={menuState?.onEmojiPickerToggle} /> diff --git a/src/pages/inbox/report/ContextMenu/ReportActionContextMenu.ts b/src/pages/inbox/report/ContextMenu/ReportActionContextMenu.ts index efff9e46e480..936cd98c6de3 100644 --- a/src/pages/inbox/report/ContextMenu/ReportActionContextMenu.ts +++ b/src/pages/inbox/report/ContextMenu/ReportActionContextMenu.ts @@ -39,7 +39,6 @@ type ShowContextMenuParams = { onHide?: () => void; setIsEmojiPickerActive?: (state: boolean) => void; }; - disabledActionIds?: Set; shouldCloseOnTarget?: boolean; isOverflowMenu?: boolean; withoutOverlay?: boolean; diff --git a/src/pages/inbox/report/ContextMenu/actions/OverflowMenu.tsx b/src/pages/inbox/report/ContextMenu/actions/OverflowMenu.tsx index 0604a8208956..e6b9b31d4dac 100644 --- a/src/pages/inbox/report/ContextMenu/actions/OverflowMenu.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/OverflowMenu.tsx @@ -6,20 +6,17 @@ import useLocalize from '@hooks/useLocalize'; import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import CONST from '@src/CONST'; -import type {ActionId} from './actionConfig'; -type OverflowMenuProps = ContextMenuActionFocusProps & { - visibleActionIds: ActionId[]; -}; +type OverflowMenuProps = ContextMenuActionFocusProps; -function OverflowMenu({isFocused, onFocus, onBlur, visibleActionIds}: OverflowMenuProps) { +function OverflowMenu({isFocused, onFocus, onBlur}: OverflowMenuProps) { const {openOverflowMenu, openContextMenu, isMini, interceptAnonymousUser} = useContextMenuPayload(); const icons = useMemoizedLazyExpensifyIcons(['ThreeDots'] as const); const {translate} = useLocalize(); const threeDotRef = useRef(null); const handlePress = (event?: GestureResponderEvent | MouseEvent | KeyboardEvent) => { - openOverflowMenu(event as GestureResponderEvent | MouseEvent, threeDotRef, new Set(visibleActionIds)); + openOverflowMenu(event as GestureResponderEvent | MouseEvent, threeDotRef); openContextMenu(); }; diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx index 629d2f6a48e7..40495ae6f573 100644 --- a/src/pages/inbox/report/PureReportActionItem.tsx +++ b/src/pages/inbox/report/PureReportActionItem.tsx @@ -203,7 +203,6 @@ import { } from '@libs/ReportActionsUtils'; import type {MissingPaymentMethod} from '@libs/ReportUtils'; import { - canWriteInReport, chatIncludesConcierge, getChatListItemReportName, getDeletedTransactionMessage, @@ -247,7 +246,6 @@ import type * as OnyxTypes from '@src/types/onyx'; import type {Errors} from '@src/types/onyx/OnyxCommon'; import type {JoinWorkspaceResolution} from '@src/types/onyx/OriginalMessage'; import {isEmptyObject, isEmptyValueObject} from '@src/types/utils/EmptyObject'; -import {RESTRICTED_READONLY_ACTION_IDS} from './ContextMenu/actions/actionConfig'; import {useMiniContextMenuActions} from './ContextMenu/MiniContextMenuProvider'; import type {ContextMenuAnchor} from './ContextMenu/ReportActionContextMenu'; import {hideContextMenu, hideDeleteModal, isActiveReportAction, showContextMenu} from './ContextMenu/ReportActionContextMenu'; @@ -263,8 +261,6 @@ import ReportActionItemSingle from './ReportActionItemSingle'; import ReportActionItemThread from './ReportActionItemThread'; import TripSummary from './TripSummary'; -const EMPTY_SET = new Set(); - type PureReportActionItemProps = { /** All the data of the policy collection */ policies: OnyxCollection; @@ -773,8 +769,6 @@ function PureReportActionItem({ [transitionActionSheetState], ); - const disabledActionIds = !canWriteInReport(report) ? RESTRICTED_READONLY_ACTION_IDS : EMPTY_SET; - /** * Show the ReportActionContextMenu modal popover. * @@ -811,7 +805,6 @@ function PureReportActionItem({ onHide: toggleContextMenuFromActiveReportAction, setIsEmojiPickerActive: setIsEmojiPickerActive as () => void, }, - disabledActionIds, }); }); }, @@ -823,7 +816,6 @@ function PureReportActionItem({ toggleContextMenuFromActiveReportAction, originalReportID, shouldDisplayContextMenu, - disabledActionIds, isArchivedRoom, isChronosReport, handleShowContextMenu, @@ -2079,7 +2071,6 @@ function PureReportActionItem({ isThreadReportParentAction: !!isThreadReportParentAction, draftMessage, isChronosReport: !!isChronosReport, - disabledActionIds, checkIfContextMenuActive: toggleContextMenuFromActiveReportAction, setIsEmojiPickerActive, rowMeasurements: { From 3e2a6d6b6124582d9fac5e768907c4ee1f61e14d Mon Sep 17 00:00:00 2001 From: rory Date: Sat, 28 Feb 2026 08:29:52 -0800 Subject: [PATCH 21/88] refactor(contextmenu): derive 5 report props internally Move isChronosReport, isArchivedRoom, isPinnedChat, isUnreadChat, and isThreadReportParentAction computations from callers into BaseReportActionContextMenu, which already subscribes to the report via Onyx. This eliminates unnecessary prop threading through MiniContextMenuProvider, PopoverReportActionContextMenu, PureReportActionItem, ShowContextMenuContext, and their callers. Made-with: Cursor --- .../BaseAnchorForAttachmentsOnly.tsx | 3 +- .../HTMLRenderers/ImageRenderer.tsx | 5 +-- .../HTMLRenderers/MentionUserRenderer.tsx | 5 +-- .../HTMLRenderers/PreRenderer.tsx | 3 +- .../LHNOptionsList/OptionRowLHN.tsx | 2 -- src/components/ShowContextMenuContext.ts | 3 -- .../VideoPlayerThumbnail.tsx | 3 +- .../BaseReportActionContextMenu.tsx | 34 +++++-------------- .../ContextMenu/MiniContextMenuProvider.tsx | 3 -- .../MiniReportActionContextMenu/index.tsx | 3 -- .../PopoverReportActionContextMenu.tsx | 24 ++----------- .../ContextMenu/ReportActionContextMenu.ts | 5 --- .../inbox/report/PureReportActionItem.tsx | 21 ++---------- 13 files changed, 17 insertions(+), 97 deletions(-) diff --git a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx index 22ad207ba9e8..7e4825da9470 100644 --- a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx +++ b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx @@ -10,7 +10,6 @@ import useThemeStyles from '@hooks/useThemeStyles'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; import {isMobileSafari} from '@libs/Browser'; import fileDownload from '@libs/fileDownload'; -import {isArchivedNonExpenseReport} from '@libs/ReportUtils'; import {setDownload} from '@userActions/Download'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -57,7 +56,7 @@ function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', onP if (isDisabled || !shouldDisplayContextMenu) { return; } - showContextMenuForReport(event, anchor, report?.reportID, action, checkIfContextMenuActive, isArchivedNonExpenseReport(report, isReportArchived)); + showContextMenuForReport(event, anchor, report?.reportID, action, checkIfContextMenuActive); }} shouldUseHapticsOnLongPress accessibilityLabel={displayName} diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx index 56247d66db95..bece67758a57 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx @@ -13,7 +13,6 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {getFileName, getFileType, splitExtensionFromFileName} from '@libs/fileDownload/FileUtils'; import Navigation from '@libs/Navigation/Navigation'; -import {isArchivedNonExpenseReport} from '@libs/ReportUtils'; import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -121,9 +120,7 @@ function ImageRenderer({tnode}: CustomRendererProps) { if (isDisabled || !shouldDisplayContextMenu) { return; } - return onShowContextMenu(() => - showContextMenuForReport(event, anchor, report?.reportID, action, checkIfContextMenuActive, isArchivedNonExpenseReport(report, isReportArchived)), - ); + return onShowContextMenu(() => showContextMenuForReport(event, anchor, report?.reportID, action, checkIfContextMenuActive)); }} isNested shouldUseHapticsOnLongPress diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx index fac38983df18..70fe847b4500 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx @@ -17,7 +17,6 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import {getAccountIDsByLogins, getDisplayNameOrDefault, getShortMentionIfFound} from '@libs/PersonalDetailsUtils'; -import {isArchivedNonExpenseReport} from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {Route} from '@src/ROUTES'; @@ -82,9 +81,7 @@ function MentionUserRenderer({style, tnode, TDefaultRenderer, currentUserPersona if (isDisabled || !shouldDisplayContextMenu) { return; } - return onShowContextMenu(() => - showContextMenuForReport(event, anchor, report?.reportID, action, checkIfContextMenuActive, isArchivedNonExpenseReport(report, isReportArchived)), - ); + return onShowContextMenu(() => showContextMenuForReport(event, anchor, report?.reportID, action, checkIfContextMenuActive)); }} onPress={(event) => { event.preventDefault(); diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer.tsx index 8392bfefc4cf..780d7ef65984 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer.tsx @@ -9,7 +9,6 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; -import {isArchivedNonExpenseReport} from '@libs/ReportUtils'; import CONST from '@src/CONST'; type PreRendererProps = CustomRendererProps & { @@ -62,7 +61,7 @@ function PreRenderer({TDefaultRenderer, onPressIn, onPressOut, onLongPress, ...d if (isDisabled || !shouldDisplayContextMenu) { return; } - return showContextMenuForReport(event, anchor, report?.reportID, action, checkIfContextMenuActive, isArchivedNonExpenseReport(report, isReportArchived)); + return showContextMenuForReport(event, anchor, report?.reportID, action, checkIfContextMenuActive); }); }} shouldUseHapticsOnLongPress diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index bc4987eaea6a..ed5f78b9bdcc 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -147,8 +147,6 @@ function OptionRowLHN({ report: { reportID, originalReportID: reportID, - isPinnedChat: optionItem.isPinned, - isUnreadChat: !!optionItem.isUnread, }, reportAction: { reportActionID: '-1', diff --git a/src/components/ShowContextMenuContext.ts b/src/components/ShowContextMenuContext.ts index f56e6e62e8ec..8cac7fa04072 100644 --- a/src/components/ShowContextMenuContext.ts +++ b/src/components/ShowContextMenuContext.ts @@ -41,7 +41,6 @@ const ShowContextMenuContext = createContext({ * @param reportID - Active Report ID * @param action - ReportAction for ContextMenu * @param checkIfContextMenuActive Callback to update context menu active state - * @param isArchivedRoom - Is the report an archived room */ function showContextMenuForReport( event: GestureResponderEvent | MouseEvent, @@ -49,7 +48,6 @@ function showContextMenuForReport( reportID: string | undefined, action: OnyxEntry, checkIfContextMenuActive: () => void, - isArchivedRoom = false, ) { if (!canUseTouchScreen()) { return; @@ -63,7 +61,6 @@ function showContextMenuForReport( report: { reportID, originalReportID: reportID ? getOriginalReportID(reportID, action, undefined) : undefined, - isArchivedRoom, }, reportAction: { reportActionID: action?.reportActionID, diff --git a/src/components/VideoPlayerPreview/VideoPlayerThumbnail.tsx b/src/components/VideoPlayerPreview/VideoPlayerThumbnail.tsx index a7a031dd372f..6b2e91165a1a 100644 --- a/src/components/VideoPlayerPreview/VideoPlayerThumbnail.tsx +++ b/src/components/VideoPlayerPreview/VideoPlayerThumbnail.tsx @@ -10,7 +10,6 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useThemeStyles from '@hooks/useThemeStyles'; import ControlSelection from '@libs/ControlSelection'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; -import {isArchivedNonExpenseReport} from '@libs/ReportUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -59,7 +58,7 @@ function VideoPlayerThumbnail({thumbnailUrl, onPress, accessibilityLabel, isDele return; } onShowContextMenu(() => { - showContextMenuForReport(event, anchor, report?.reportID, action, checkIfContextMenuActive, isArchivedNonExpenseReport(report, isReportArchived)); + showContextMenuForReport(event, anchor, report?.reportID, action, checkIfContextMenuActive); }); }} shouldUseHapticsOnLongPress diff --git a/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx index 553660096796..ffd59a833fb0 100755 --- a/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx +++ b/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx @@ -30,7 +30,9 @@ import { getHarvestOriginalReportID, getSourceIDFromReportAction, isArchivedNonExpenseReport, + isChatThread, isHarvestCreatedExpenseReport, + isUnread, isInvoiceReport as ReportUtilsIsInvoiceReport, isMoneyRequest as ReportUtilsIsMoneyRequest, isMoneyRequestReport as ReportUtilsIsMoneyRequestReport, @@ -89,24 +91,6 @@ type BaseReportActionContextMenuProps = { /** Target node which is the target of ContentMenu */ anchor?: RefObject; - /** Flag to check if the chat participant is Chronos */ - isChronosReport?: boolean; - - /** Whether the provided report is an archived room */ - isArchivedRoom?: boolean; - - /** Flag to check if the chat is pinned in the LHN. Used for the Pin/Unpin action */ - isPinnedChat?: boolean; - - /** Flag to check if the chat is unread in the LHN. Used for the Mark as Read/Unread action */ - isUnreadChat?: boolean; - - /** - * Is the action a thread's parent reportAction viewed from within the thread report? - * It will be false if we're viewing the same parent report action from the report it belongs to rather than the thread. - */ - isThreadReportParentAction?: boolean; - /** Content Ref */ contentRef?: RefObject; @@ -121,13 +105,8 @@ function BaseReportActionContextMenu({ type = CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, anchor, contentRef, - isChronosReport = false, - isArchivedRoom = false, isMini = false, isVisible = false, - isPinnedChat = false, - isUnreadChat = false, - isThreadReportParentAction = false, selection = '', draftMessage = '', reportActionID, @@ -197,6 +176,12 @@ function BaseReportActionContextMenu({ const isChildReportArchived = useReportIsArchived(childReport?.reportID); const isParentReportArchived = useReportIsArchived(childReport?.parentReportID); + const isChronosReport = chatIncludesChronosWithID(originalReportID); + const isArchivedRoom = isArchivedNonExpenseReport(originalReport, isOriginalReportArchived); + const isPinnedChat = !!report?.isPinned; + const isUnreadChat = isUnread(report, undefined, isOriginalReportArchived); + const isThreadReportParentAction = isChatThread(report) && report?.parentReportActionID === reportAction?.reportActionID; + const isMoneyRequestReport = ReportUtilsIsMoneyRequestReport(childReport); const isInvoiceReport = ReportUtilsIsInvoiceReport(childReport); let requestParentReportAction; @@ -344,13 +329,10 @@ function BaseReportActionContextMenu({ report: { reportID, originalReportID, - isArchivedRoom: isArchivedNonExpenseReport(originalReport, isOriginalReportArchived), - isChronos: chatIncludesChronosWithID(originalReportID), }, reportAction: { reportActionID: reportAction?.reportActionID, draftMessage, - isThreadReportParentAction, }, callbacks: { onShow: checkIfContextMenuActive, diff --git a/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx b/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx index a5a07c4a2cf1..3502c14669bb 100644 --- a/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx @@ -16,10 +16,7 @@ type MiniContextMenuParams = { originalReportID: string | undefined; anchor: RefObject; displayAsGroup: boolean; - isArchivedRoom: boolean; - isThreadReportParentAction: boolean; draftMessage: string | undefined; - isChronosReport: boolean; checkIfContextMenuActive: () => void; setIsEmojiPickerActive: (state: boolean) => void; rowMeasurements: RowMeasurements; diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index 9b96b1314429..244249d3d777 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -84,10 +84,7 @@ function MiniReportActionContextMenu() { reportActionID={state.reportActionID} originalReportID={state.originalReportID} anchor={state.anchor} - isArchivedRoom={state.isArchivedRoom} - isThreadReportParentAction={state.isThreadReportParentAction} draftMessage={state.draftMessage} - isChronosReport={state.isChronosReport} checkIfContextMenuActive={state.checkIfContextMenuActive} setIsEmojiPickerActive={state.setIsEmojiPickerActive} isVisible={state.isVisible} diff --git a/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx index 5f2399a122fb..a9cc9667a930 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -36,11 +36,6 @@ type PopoverContextMenuState = { originalReportID: string | undefined; selection: string; draftMessage: string | undefined; - isArchivedRoom: boolean; - isChronos: boolean; - isPinnedChat: boolean; - isUnreadChat: boolean; - isThreadReportParentAction: boolean; isOverflowMenu: boolean; withoutOverlay: boolean; position: PopoverPosition; @@ -148,8 +143,8 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro setComposerToRefocusOnClose('edit'); } - const {reportID: showReportID, originalReportID: showOriginalReportID, isArchivedRoom = false, isChronos = false, isPinnedChat = false, isUnreadChat = false} = currentReport; - const {reportActionID: showReportActionID, draftMessage, isThreadReportParentAction: isThreadReportParentActionParam = false} = reportActionParam; + const {reportID: showReportID, originalReportID: showOriginalReportID} = currentReport; + const {reportActionID: showReportActionID, draftMessage} = reportActionParam; const {onShow = () => {}, onHide = () => {}, setIsEmojiPickerActive = () => {}} = callbacks; setIsContextMenuOpening(true); @@ -200,11 +195,6 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro originalReportID: showOriginalReportID || undefined, selection, draftMessage, - isArchivedRoom, - isChronos, - isPinnedChat, - isUnreadChat, - isThreadReportParentAction: isThreadReportParentActionParam, isOverflowMenu, withoutOverlay, position, @@ -281,11 +271,6 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro type: CONST.CONTEXT_MENU_TYPES.REPORT_ACTION as ContextMenuType, selection: '', draftMessage: undefined, - isArchivedRoom: false, - isChronos: false, - isPinnedChat: false, - isUnreadChat: false, - isThreadReportParentAction: false, isOverflowMenu: false, withoutOverlay: true, position: {anchorHorizontal: 0, anchorVertical: 0, anchorWidth: 0, anchorHeight: 0}, @@ -361,11 +346,6 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro reportActionID={menuState?.reportActionID} draftMessage={menuState?.draftMessage} selection={menuState?.selection ?? ''} - isArchivedRoom={menuState?.isArchivedRoom ?? false} - isChronosReport={menuState?.isChronos ?? false} - isPinnedChat={menuState?.isPinnedChat ?? false} - isUnreadChat={menuState?.isUnreadChat ?? false} - isThreadReportParentAction={menuState?.isThreadReportParentAction ?? false} anchor={{current: menuState?.contextMenuTargetNode ?? null}} contentRef={contentRef} originalReportID={menuState?.originalReportID} diff --git a/src/pages/inbox/report/ContextMenu/ReportActionContextMenu.ts b/src/pages/inbox/report/ContextMenu/ReportActionContextMenu.ts index 936cd98c6de3..aac9303b3fa2 100644 --- a/src/pages/inbox/report/ContextMenu/ReportActionContextMenu.ts +++ b/src/pages/inbox/report/ContextMenu/ReportActionContextMenu.ts @@ -24,15 +24,10 @@ type ShowContextMenuParams = { report?: { reportID?: string; originalReportID?: string; - isArchivedRoom?: boolean; - isChronos?: boolean; - isPinnedChat?: boolean; - isUnreadChat?: boolean; }; reportAction?: { reportActionID?: string; draftMessage?: string; - isThreadReportParentAction?: boolean; }; callbacks?: { onShow?: () => void; diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx index 40495ae6f573..e03a2110c6ef 100644 --- a/src/pages/inbox/report/PureReportActionItem.tsx +++ b/src/pages/inbox/report/PureReportActionItem.tsx @@ -528,6 +528,7 @@ function PureReportActionItem({ originalReport, deleteReportActionDraft = () => {}, isArchivedRoom, + // eslint-disable-next-line @typescript-eslint/no-unused-vars -- used in memo comparator isChronosReport, toggleEmojiReaction = () => {}, createDraftTransactionAndNavigateToParticipantSelector = () => {}, @@ -792,13 +793,10 @@ function PureReportActionItem({ report: { reportID, originalReportID, - isArchivedRoom, - isChronos: isChronosReport, }, reportAction: { reportActionID: action.reportActionID, draftMessage, - isThreadReportParentAction, }, callbacks: { onShow: toggleContextMenuFromActiveReportAction, @@ -808,19 +806,7 @@ function PureReportActionItem({ }); }); }, - [ - draftMessage, - action.errors, - action.reportActionID, - reportID, - toggleContextMenuFromActiveReportAction, - originalReportID, - shouldDisplayContextMenu, - isArchivedRoom, - isChronosReport, - handleShowContextMenu, - isThreadReportParentAction, - ], + [draftMessage, action.errors, action.reportActionID, reportID, toggleContextMenuFromActiveReportAction, originalReportID, shouldDisplayContextMenu, handleShowContextMenu], ); const toggleReaction = useCallback( @@ -2067,10 +2053,7 @@ function PureReportActionItem({ originalReportID, anchor: popoverAnchorRef, displayAsGroup: !!displayAsGroup, - isArchivedRoom: !!isArchivedRoom, - isThreadReportParentAction: !!isThreadReportParentAction, draftMessage, - isChronosReport: !!isChronosReport, checkIfContextMenuActive: toggleContextMenuFromActiveReportAction, setIsEmojiPickerActive, rowMeasurements: { From 907634cec24d8358e0cd6aa51f8841986cbe96a5 Mon Sep 17 00:00:00 2001 From: rory Date: Sat, 28 Feb 2026 20:34:02 -0800 Subject: [PATCH 22/88] refactor(contextmenu): eliminate isMini via composition Convert all 22 action components to headless hooks returning ActionDescriptor data, letting MiniReportActionContextMenu and PopoverReportActionContextMenu each render their own UI wrappers. - Extract shared Onyx subscriptions into useContextMenuData hook - Create useContextMenuActions aggregator for all action hooks - Add hideAndRun to ContextMenuPayloadContext for mode-agnostic close - Remove isMini from ContextMenuPayloadContextValue, actionConfig ShouldShowArgs, and ContextMenuItem - Delete BaseReportActionContextMenu (logic distributed to consumers) - Delete stale types.ts and ReportActionContextMenuContent.tsx Made-with: Cursor --- src/components/ContextMenuItem.tsx | 35 +- .../ContextMenuPayloadProvider.tsx | 2 +- .../index.native.tsx | 6 +- .../MiniReportActionContextMenu/index.tsx | 170 +++++++++- .../MiniReportActionContextMenu/types.ts | 8 - .../PopoverReportActionContextMenu.tsx | 212 ++++++++++-- .../ContextMenu/actions/ActionDescriptor.ts | 20 ++ .../ContextMenu/actions/ContextMenuAction.ts | 96 +++--- .../report/ContextMenu/actions/CopyEmail.tsx | 43 ++- .../report/ContextMenu/actions/CopyLink.tsx | 47 ++- .../ContextMenu/actions/CopyMessage.tsx | 43 +-- .../ContextMenu/actions/CopyOnyxData.tsx | 41 +-- .../ContextMenu/actions/CopyToClipboard.tsx | 41 +-- .../report/ContextMenu/actions/CopyURL.tsx | 43 ++- .../report/ContextMenu/actions/Debug.tsx | 51 ++- .../report/ContextMenu/actions/Delete.tsx | 46 +-- .../report/ContextMenu/actions/Download.tsx | 62 ++-- .../inbox/report/ContextMenu/actions/Edit.tsx | 74 ++--- .../ContextMenu/actions/EmojiReaction.tsx | 77 +++-- .../report/ContextMenu/actions/Explain.tsx | 54 ++-- .../ContextMenu/actions/FlagAsOffensive.tsx | 47 +-- .../inbox/report/ContextMenu/actions/Hold.tsx | 47 +-- .../report/ContextMenu/actions/JoinThread.tsx | 49 +-- .../ContextMenu/actions/LeaveThread.tsx | 49 +-- .../report/ContextMenu/actions/MarkAsRead.tsx | 42 +-- .../ContextMenu/actions/MarkAsUnread.tsx | 42 +-- .../ContextMenu/actions/OverflowMenu.tsx | 46 ++- .../inbox/report/ContextMenu/actions/Pin.tsx | 40 +-- .../ContextMenu/actions/ReplyInThread.tsx | 43 +-- .../report/ContextMenu/actions/Unhold.tsx | 47 +-- .../report/ContextMenu/actions/Unpin.tsx | 40 +-- .../ContextMenu/actions/actionConfig.ts | 3 +- .../ContextMenu/useContextMenuActions.ts | 81 +++++ ...nContextMenu.tsx => useContextMenuData.ts} | 301 +++--------------- 34 files changed, 965 insertions(+), 1083 deletions(-) delete mode 100644 src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/types.ts create mode 100644 src/pages/inbox/report/ContextMenu/actions/ActionDescriptor.ts create mode 100644 src/pages/inbox/report/ContextMenu/useContextMenuActions.ts rename src/pages/inbox/report/ContextMenu/{BaseReportActionContextMenu.tsx => useContextMenuData.ts} (50%) mode change 100755 => 100644 diff --git a/src/components/ContextMenuItem.tsx b/src/components/ContextMenuItem.tsx index d27f0aa8ae67..9db9b44827f2 100644 --- a/src/components/ContextMenuItem.tsx +++ b/src/components/ContextMenuItem.tsx @@ -1,15 +1,12 @@ import React from 'react'; -import type {GestureResponderEvent, StyleProp, View, ViewStyle} from 'react-native'; +import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useThrottledButtonState from '@hooks/useThrottledButtonState'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import getButtonState from '@libs/getButtonState'; import type IconAsset from '@src/types/utils/IconAsset'; import type WithSentryLabel from '@src/types/utils/SentryLabel'; -import BaseMiniContextMenuItem from './BaseMiniContextMenuItem'; import FocusableMenuItem from './FocusableMenuItem'; -import Icon from './Icon'; type ContextMenuItemProps = WithSentryLabel & { /** Icon Component */ @@ -24,9 +21,6 @@ type ContextMenuItemProps = WithSentryLabel & { /** Text to show when interaction was successful */ successText?: string; - /** Whether to show the mini menu */ - isMini?: boolean; - /** Callback to fire when the item is pressed */ onPress: (event?: GestureResponderEvent | MouseEvent | KeyboardEvent) => void; @@ -45,11 +39,6 @@ type ContextMenuItemProps = WithSentryLabel & { /** Styles to apply to MenuItem wrapper */ wrapperStyle?: StyleProp; - shouldPreventDefaultFocusOnPress?: boolean; - - /** The ref of mini context menu item */ - buttonRef?: React.RefObject; - /** Handles what to do when the item is focused */ onFocus?: () => void; @@ -69,14 +58,11 @@ function ContextMenuItem({ successText = '', icon, text, - isMini = false, description = '', isAnonymousAction = false, isFocused = false, shouldLimitWidth = true, wrapperStyle, - shouldPreventDefaultFocusOnPress = true, - buttonRef = {current: null}, onFocus = () => {}, onBlur = () => {}, disabled = false, @@ -104,24 +90,7 @@ function ContextMenuItem({ const itemIcon = !isThrottledButtonActive && successIcon ? successIcon : icon; const itemText = !isThrottledButtonActive && successText ? successText : text; - return isMini ? ( - - {({hovered, pressed}) => ( - - )} - - ) : ( + return ( ; close: () => void; + hideAndRun: (callback?: () => void) => void; transitionActionSheetState: (params: {type: string; payload?: Record}) => void; openContextMenu: () => void; interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.native.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.native.tsx index 7be6a850d51b..0617a41515ff 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.native.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.native.tsx @@ -1,4 +1,2 @@ -import type MiniReportActionContextMenuProps from './types'; - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export default (props: MiniReportActionContextMenuProps) => null; +// Mini context menu only renders on web via createPortal +export default () => null; diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index 244249d3d777..894b31decf2f 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -1,15 +1,104 @@ -import React, {useEffect, useRef} from 'react'; +import React, {useEffect, useMemo, useRef} from 'react'; +import type {RefObject} from 'react'; import {createPortal} from 'react-dom'; +import {View} from 'react-native'; +// eslint-disable-next-line no-restricted-imports +import type {GestureResponderEvent} from 'react-native'; import Animated, {Easing, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; -import BaseReportActionContextMenu from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; +import * as ActionSheetAwareScrollView from '@components/ActionSheetAwareScrollView'; +import BaseMiniContextMenuItem from '@components/BaseMiniContextMenuItem'; +import Icon from '@components/Icon'; +import MiniQuickEmojiReactions from '@components/Reactions/MiniQuickEmojiReactions'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useStyleUtils from '@hooks/useStyleUtils'; +import getButtonState from '@libs/getButtonState'; +import type {ActionDescriptor} from '@pages/inbox/report/ContextMenu/actions/ActionDescriptor'; +import {useEmojiReactionData, useOverflowMenuAction} from '@pages/inbox/report/ContextMenu/actions/ContextMenuAction'; +import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; +import {ContextMenuPayloadContext} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {useMiniContextMenuActions, useMiniContextMenuState} from '@pages/inbox/report/ContextMenu/MiniContextMenuProvider'; +import type {ContextMenuAnchor} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import {showContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import useContextMenuActions from '@pages/inbox/report/ContextMenu/useContextMenuActions'; +import useContextMenuData from '@pages/inbox/report/ContextMenu/useContextMenuData'; +import CONST from '@src/CONST'; const SLIDE_DURATION = 200; const OVERSHOOT_EASING = Easing.bezier(0.34, 1.56, 0.64, 1); +function MiniContextMenuContent({visibleActionIDs}: {visibleActionIDs: Set}) { + const actions = useContextMenuActions(visibleActionIDs); + const emojiData = useEmojiReactionData(); + const overflowMenu = useOverflowMenuAction(); + const StyleUtils = useStyleUtils(); + + const hasEmoji = visibleActionIDs.has('emojiReaction') && !!emojiData.reportAction && !!emojiData.reportActionID; + const needsOverflow = actions.length > CONST.MINI_CONTEXT_MENU_MAX_ITEMS; + const visibleActions = needsOverflow ? actions.slice(0, CONST.MINI_CONTEXT_MENU_MAX_ITEMS - 1) : actions; + + return ( + <> + {hasEmoji && emojiData.reportAction && emojiData.reportActionID && ( + + emojiData.interceptAnonymousUser(() => emojiData.toggleEmojiAndCloseMenu(emoji, existingReactions, preferredSkinTone)) + } + onPressOpenPicker={emojiData.onPressOpenPicker} + onEmojiPickerClosed={emojiData.onEmojiPickerClosed} + reportActionID={emojiData.reportActionID} + reportAction={emojiData.reportAction} + /> + )} + {visibleActions.map((action: ActionDescriptor) => ( + + {({hovered, pressed}) => ( + + )} + + ))} + {!!(needsOverflow && overflowMenu) && + (() => { + const {buttonRef, text, onPress, sentryLabel, icon} = overflowMenu; + return ( + + {({hovered, pressed}) => ( + + )} + + ); + })()} + + ); +} + function MiniReportActionContextMenu() { const state = useMiniContextMenuState(); - const {hideMiniContextMenu, cancelHide} = useMiniContextMenuActions(); + const miniActions = useMiniContextMenuActions(); + const {hideMiniContextMenu, cancelHide} = miniActions; + const {shouldUseNarrowLayout} = useResponsiveLayout(); + const StyleUtils = useStyleUtils(); + const {transitionActionSheetState} = ActionSheetAwareScrollView.useActionSheetAwareScrollViewActions(); const isVisible = state?.isVisible ?? false; const wasVisibleRef = useRef(false); @@ -55,10 +144,69 @@ function MiniReportActionContextMenu() { right: baseRight.get(), })); + const data = useContextMenuData({ + reportID: state?.reportID, + reportActionID: state?.reportActionID, + originalReportID: state?.originalReportID, + draftMessage: state?.draftMessage ?? '', + selection: '', + type: CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, + anchor: state?.anchor, + }); + + const visibleActionIDs = useMemo(() => new Set(data.getVisibleActionIDs()), [data]); + + const hideAndRun = (callback?: () => void) => { + miniActions.release(); + callback?.(); + }; + + const openOverflowMenu = (event: GestureResponderEvent | MouseEvent, anchorRef: RefObject) => { + showContextMenu({ + type: CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, + event, + selection: '', + contextMenuAnchor: anchorRef?.current ?? null, + report: { + reportID: state?.reportID, + originalReportID: state?.originalReportID, + }, + reportAction: { + reportActionID: data.reportAction?.reportActionID, + draftMessage: state?.draftMessage, + }, + callbacks: { + onShow: state?.checkIfContextMenuActive, + onHide: () => { + state?.checkIfContextMenuActive?.(); + miniActions.release(); + }, + }, + shouldCloseOnTarget: true, + isOverflowMenu: true, + }); + }; + + // eslint-disable-next-line react/jsx-no-constructed-context-values + const payloadValue: ContextMenuPayloadContextValue = { + ...data, + // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style + reportAction: (data.reportAction ?? null) as NonNullable, + currentUserAccountID: data.currentUserPersonalDetails?.accountID, + close: () => miniActions.release(), + hideAndRun, + transitionActionSheetState, + openContextMenu: () => miniActions.keepOpen(), + openOverflowMenu, + setIsEmojiPickerActive: state?.setIsEmojiPickerActive, + }; + if (!state) { return null; } + const wrapperStyle = StyleUtils.getReportActionContextMenuStyles(true, shouldUseNarrowLayout); + return createPortal( // eslint-disable-next-line jsx-a11y/mouse-events-have-key-events
- + + + + +
, document.body, diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/types.ts b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/types.ts deleted file mode 100644 index 59ec5195810c..000000000000 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type {BaseReportActionContextMenuProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; - -type MiniReportActionContextMenuProps = Omit & { - /** Should the reportAction this menu is attached to have the appearance of being grouped with the previous reportAction? */ - displayAsGroup?: boolean; -}; - -export default MiniReportActionContextMenuProps; diff --git a/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx index a9cc9667a930..e09f4f74926d 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -1,19 +1,35 @@ -import type {ForwardedRef} from 'react'; -import React, {useEffect, useImperativeHandle, useRef, useState} from 'react'; +import type {ForwardedRef, RefObject} from 'react'; +import React, {useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; /* eslint-disable no-restricted-imports */ -import type {EmitterSubscription, GestureResponderEvent, NativeTouchEvent, View} from 'react-native'; -import {Dimensions} from 'react-native'; +import type {EmitterSubscription, GestureResponderEvent, NativeTouchEvent, Text as RNText, View as ViewType} from 'react-native'; +import {Dimensions, View} from 'react-native'; import {Actions, useActionSheetAwareScrollViewActions} from '@components/ActionSheetAwareScrollView'; +import FocusableMenuItem from '@components/FocusableMenuItem'; +import FocusTrapForModal from '@components/FocusTrap/FocusTrapForModal'; import {ModalActions, useModal} from '@components/Modal/Global/ModalContext'; import PopoverWithMeasuredContent from '@components/PopoverWithMeasuredContent'; +import QuickEmojiReactions from '@components/Reactions/QuickEmojiReactions'; +import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useRestoreInputFocus from '@hooks/useRestoreInputFocus'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import calculateAnchorPosition from '@libs/calculateAnchorPosition'; import refocusComposerAfterPreventFirstResponder from '@libs/refocusComposerAfterPreventFirstResponder'; import type {ComposerType} from '@libs/ReportActionComposeFocusManager'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import CONST from '@src/CONST'; -import BaseReportActionContextMenu from './BaseReportActionContextMenu'; +import {ORDERED_ACTION_SHOULD_SHOW} from './actions/actionConfig'; +import type {ActionDescriptor} from './actions/ActionDescriptor'; +import {useEmojiReactionData} from './actions/ContextMenuAction'; import ConfirmDeleteReportActionModal from './ConfirmDeleteReportActionModal'; +import type {ContextMenuPayloadContextValue} from './ContextMenuPayloadProvider'; +import {ContextMenuPayloadContext} from './ContextMenuPayloadProvider'; import type {ContextMenuAnchor, ContextMenuType, ReportActionContextMenu} from './ReportActionContextMenu'; +import {showContextMenu} from './ReportActionContextMenu'; +import useContextMenuActions from './useContextMenuActions'; +import useContextMenuData from './useContextMenuData'; function extractPointerEvent(event: GestureResponderEvent | MouseEvent): MouseEvent | NativeTouchEvent { if ('nativeEvent' in event) { @@ -44,26 +60,110 @@ type PopoverContextMenuState = { }; type PopoverReportActionContextMenuProps = { - /** Reference to the outer element */ ref?: ForwardedRef; }; +function PopoverContextMenuContent({ + isPopoverVisible, + localShouldKeepOpen, + visibleActionIDs, + setLocalShouldKeepOpen, +}: { + isPopoverVisible: boolean; + localShouldKeepOpen: boolean; + visibleActionIDs: Set; + setLocalShouldKeepOpen: (val: boolean) => void; +}) { + const actions = useContextMenuActions(visibleActionIDs); + const emojiData = useEmojiReactionData(); + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const {windowWidth} = useWindowDimensions(); + // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth + const {isSmallScreenWidth} = useResponsiveLayout(); + + const shouldKeepOpen = localShouldKeepOpen; + const shouldEnableArrowNavigation = isPopoverVisible || shouldKeepOpen; + + const contentActionIndexes = actions + .map((action, index) => { + const entry = ORDERED_ACTION_SHOULD_SHOW.find((e) => e.id === action.id); + return entry?.isContentAction ? index : undefined; + }) + .filter((index): index is number => index !== undefined); + + const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({ + initialFocusedIndex: -1, + disabledIndexes: contentActionIndexes, + maxIndex: actions.length - 1, + isActive: shouldEnableArrowNavigation, + }); + + const hasEmoji = visibleActionIDs.has('emojiReaction'); + + return ( + + + {hasEmoji && ( + + emojiData.interceptAnonymousUser(() => emojiData.toggleEmojiAndCloseMenu(emoji, existingReactions, preferredSkinTone)) + } + reportActionID={emojiData.reportActionID ?? ''} + reportAction={emojiData.reportAction} + setIsEmojiPickerActive={(active) => { + if (!active) { + return; + } + setLocalShouldKeepOpen(true); + }} + /> + )} + {actions.map((action: ActionDescriptor, i: number) => ( + setFocusedIndex(i)} + onBlur={() => (i === actions.length - 1 || i === 1) && setFocusedIndex(-1)} + disabled={action.disabled} + shouldShowLoadingSpinnerIcon={action.shouldShowLoadingSpinnerIcon} + sentryLabel={action.sentryLabel} + /> + ))} + + + ); +} + function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuProps) { const {transitionActionSheetState} = useActionSheetAwareScrollViewActions(); const modalContext = useModal(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); + const StyleUtils = useStyleUtils(); const [menuState, setMenuState] = useState(null); const [isPopoverVisible, setIsPopoverVisible] = useState(false); const [isContextMenuOpening, setIsContextMenuOpening] = useState(false); const [composerToRefocusOnClose, setComposerToRefocusOnClose] = useState(); + const [localShouldKeepOpen, setLocalShouldKeepOpen] = useState(false); const reportActionID = menuState?.reportActionID; const cursorRelativePosition = useRef({horizontal: 0, vertical: 0}); const instanceIDRef = useRef(''); - const contentRef = useRef(null); - const anchorRef = useRef(null); + const contentRef = useRef(null); + const anchorRef = useRef(null); const dimensionsEventListener = useRef(null); const contextMenuAnchorRef = useRef(null); @@ -71,6 +171,8 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro const onPopoverHide = useRef(() => {}); const onPopoverHideActionCallback = useRef(() => {}); + useRestoreInputFocus(isPopoverVisible); + const getContextMenuMeasuredLocation = () => new Promise<{x: number; y: number}>((resolve) => { if (contextMenuAnchorRef.current && 'measureInWindow' in contextMenuAnchorRef.current && typeof contextMenuAnchorRef.current.measureInWindow === 'function') { @@ -124,7 +226,7 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro setMenuState(null); }; - const showContextMenu: ReportActionContextMenu['showContextMenu'] = (showContextMenuParams) => { + const showContextMenuHandler: ReportActionContextMenu['showContextMenu'] = (showContextMenuParams) => { const { type, event, @@ -227,7 +329,7 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro onPopoverHideActionCallback.current = runAndResetCallback(onPopoverHideActionCallback.current); }; - const hideContextMenu: ReportActionContextMenu['hideContextMenu'] = (hideContextMenuParams) => { + const hideContextMenuHandler: ReportActionContextMenu['hideContextMenu'] = (hideContextMenuParams) => { const {callbacks = {}} = hideContextMenuParams ?? {}; if (typeof callbacks.onHide === 'function') { @@ -304,8 +406,8 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro }; useImperativeHandle(ref, () => ({ - showContextMenu, - hideContextMenu, + showContextMenu: showContextMenuHandler, + hideContextMenu: hideContextMenuHandler, showDeleteModal, hideDeleteModal, isActiveReportAction, @@ -317,10 +419,69 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro composerToRefocusOnCloseEmojiPicker: composerToRefocusOnClose, })); + const data = useContextMenuData({ + reportID: menuState?.reportID, + reportActionID: menuState?.reportActionID, + originalReportID: menuState?.originalReportID, + draftMessage: menuState?.draftMessage ?? '', + selection: menuState?.selection ?? '', + type: menuState?.type ?? CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, + anchor: {current: menuState?.contextMenuTargetNode ?? null}, + }); + + const visibleActionIDs = useMemo(() => new Set(data.getVisibleActionIDs()), [data]); + + const hideAndRun = (callback?: () => void) => { + import('@pages/inbox/report/ContextMenu/ReportActionContextMenu').then(({hideContextMenu: hideCtx}) => { + hideCtx(false, callback); + }); + }; + + const openOverflowMenu = (event: GestureResponderEvent | MouseEvent, anchorRefParam: RefObject) => { + showContextMenu({ + type: CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, + event, + selection: menuState?.selection ?? '', + contextMenuAnchor: anchorRefParam?.current as ViewType | RNText | null, + report: { + reportID: menuState?.reportID, + originalReportID: menuState?.originalReportID, + }, + reportAction: { + reportActionID: data.reportAction?.reportActionID, + draftMessage: menuState?.draftMessage, + }, + callbacks: { + onShow: undefined, + onHide: () => { + setLocalShouldKeepOpen(false); + }, + }, + shouldCloseOnTarget: true, + isOverflowMenu: true, + }); + }; + + // eslint-disable-next-line react/jsx-no-constructed-context-values + const payloadValue: ContextMenuPayloadContextValue = { + ...data, + // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style + reportAction: (data.reportAction ?? null) as NonNullable, + currentUserAccountID: data.currentUserPersonalDetails?.accountID, + close: () => setLocalShouldKeepOpen(false), + hideAndRun, + transitionActionSheetState, + openContextMenu: () => setLocalShouldKeepOpen(true), + openOverflowMenu, + setIsEmojiPickerActive: menuState?.onEmojiPickerToggle, + }; + + const wrapperStyle = StyleUtils.getReportActionContextMenuStyles(false, shouldUseNarrowLayout); + return ( hideContextMenu()} + onClose={() => hideContextMenuHandler()} onModalShow={runAndResetOnPopoverShow} onModalHide={runAndResetOnPopoverHide} anchorPosition={{ @@ -339,18 +500,19 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro anchorRef={anchorRef} shouldSwitchPositionIfOverflow={menuState?.isOverflowMenu ?? false} > - + + + + + ); } diff --git a/src/pages/inbox/report/ContextMenu/actions/ActionDescriptor.ts b/src/pages/inbox/report/ContextMenu/actions/ActionDescriptor.ts new file mode 100644 index 000000000000..bbca316aeb29 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/ActionDescriptor.ts @@ -0,0 +1,20 @@ +/* eslint-disable import/prefer-default-export -- type-only module */ +import type {GestureResponderEvent} from 'react-native'; +import type IconAsset from '@src/types/utils/IconAsset'; + +type ActionDescriptor = { + id: string; + icon: IconAsset; + text: string; + onPress: (event?: GestureResponderEvent | MouseEvent | KeyboardEvent) => void; + successIcon?: IconAsset; + successText?: string; + description?: string; + isAnonymousAction?: boolean; + disabled?: boolean; + shouldShowLoadingSpinnerIcon?: boolean; + shouldPreventDefaultFocusOnPress?: boolean; + sentryLabel: string; +}; + +export type {ActionDescriptor}; diff --git a/src/pages/inbox/report/ContextMenu/actions/ContextMenuAction.ts b/src/pages/inbox/report/ContextMenu/actions/ContextMenuAction.ts index 71887c17cc66..0862a705ca61 100644 --- a/src/pages/inbox/report/ContextMenu/actions/ContextMenuAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/ContextMenuAction.ts @@ -1,51 +1,49 @@ -import CopyEmail from './CopyEmail'; -import CopyLink from './CopyLink'; -import CopyMessage from './CopyMessage'; -import CopyOnyxData from './CopyOnyxData'; -import CopyToClipboard from './CopyToClipboard'; -import CopyURL from './CopyURL'; -import Debug from './Debug'; -import Delete from './Delete'; -import Download from './Download'; -import Edit from './Edit'; -import EmojiReaction from './EmojiReaction'; -import Explain from './Explain'; -import FlagAsOffensive from './FlagAsOffensive'; -import Hold from './Hold'; -import JoinThread from './JoinThread'; -import LeaveThread from './LeaveThread'; -import MarkAsRead from './MarkAsRead'; -import MarkAsUnread from './MarkAsUnread'; -import OverflowMenu from './OverflowMenu'; -import Pin from './Pin'; -import ReplyInThread from './ReplyInThread'; -import Unhold from './Unhold'; -import Unpin from './Unpin'; +import useCopyEmailAction from './CopyEmail'; +import useCopyLinkAction from './CopyLink'; +import useCopyMessageAction from './CopyMessage'; +import useCopyOnyxDataAction from './CopyOnyxData'; +import useCopyToClipboardAction from './CopyToClipboard'; +import useCopyURLAction from './CopyURL'; +import useDebugAction from './Debug'; +import useDeleteAction from './Delete'; +import useDownloadAction from './Download'; +import useEditAction from './Edit'; +import useEmojiReactionData from './EmojiReaction'; +import useExplainAction from './Explain'; +import useFlagAsOffensiveAction from './FlagAsOffensive'; +import useHoldAction from './Hold'; +import useJoinThreadAction from './JoinThread'; +import useLeaveThreadAction from './LeaveThread'; +import useMarkAsReadAction from './MarkAsRead'; +import useMarkAsUnreadAction from './MarkAsUnread'; +import useOverflowMenuAction from './OverflowMenu'; +import usePinAction from './Pin'; +import useReplyInThreadAction from './ReplyInThread'; +import useUnholdAction from './Unhold'; +import useUnpinAction from './Unpin'; -const ContextMenuAction = { - EmojiReaction, - ReplyInThread, - MarkAsUnread, - Explain, - MarkAsRead, - Edit, - Unhold, - Hold, - JoinThread, - LeaveThread, - CopyURL, - CopyToClipboard, - CopyEmail, - CopyMessage, - CopyLink, - Pin, - Unpin, - FlagAsOffensive, - Download, - CopyOnyxData, - Debug, - Delete, - OverflowMenu, +export { + useEmojiReactionData, + useReplyInThreadAction, + useMarkAsUnreadAction, + useExplainAction, + useMarkAsReadAction, + useEditAction, + useUnholdAction, + useHoldAction, + useJoinThreadAction, + useLeaveThreadAction, + useCopyURLAction, + useCopyToClipboardAction, + useCopyEmailAction, + useCopyMessageAction, + useCopyLinkAction, + usePinAction, + useUnpinAction, + useFlagAsOffensiveAction, + useDownloadAction, + useCopyOnyxDataAction, + useDebugAction, + useDeleteAction, + useOverflowMenuAction, }; - -export default ContextMenuAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/CopyEmail.tsx b/src/pages/inbox/report/ContextMenu/actions/CopyEmail.tsx index 29058df56a65..355506bbc851 100644 --- a/src/pages/inbox/report/ContextMenu/actions/CopyEmail.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/CopyEmail.tsx @@ -1,40 +1,33 @@ -import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import Clipboard from '@libs/Clipboard'; import EmailUtils from '@libs/EmailUtils'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; +import type {ActionDescriptor} from './ActionDescriptor'; -function CopyEmail({isFocused, onFocus, onBlur}: ContextMenuActionFocusProps) { - const {selection, isMini, interceptAnonymousUser} = useContextMenuPayload(); +function useCopyEmailAction(): ActionDescriptor | null { + const {selection, interceptAnonymousUser} = useContextMenuPayload(); const icons = useMemoizedLazyExpensifyIcons(['Copy', 'Checkmark'] as const); const {translate} = useLocalize(); - const handlePress = () => { - Clipboard.setString(EmailUtils.trimMailTo(selection)); - hideContextMenu(true, ReportActionComposeFocusManager.focus); + return { + id: 'copyEmail', + icon: icons.Copy, + text: translate('reportActionContextMenu.copyEmailToClipboard'), + successText: translate('reportActionContextMenu.copied'), + successIcon: icons.Checkmark, + description: EmailUtils.prefixMailSeparatorsWithBreakOpportunities(EmailUtils.trimMailTo(selection ?? '')), + isAnonymousAction: true, + onPress: () => + interceptAnonymousUser(() => { + Clipboard.setString(EmailUtils.trimMailTo(selection)); + hideContextMenu(true, ReportActionComposeFocusManager.focus); + }, true), + sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_EMAIL, }; - - return ( - interceptAnonymousUser(handlePress, true)} - isFocused={isFocused} - onFocus={onFocus} - onBlur={onBlur} - sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_EMAIL} - /> - ); } -export default CopyEmail; +export default useCopyEmailAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/CopyLink.tsx b/src/pages/inbox/report/ContextMenu/actions/CopyLink.tsx index ecc81c288e60..6550c08196ff 100644 --- a/src/pages/inbox/report/ContextMenu/actions/CopyLink.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/CopyLink.tsx @@ -1,42 +1,35 @@ -import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import Clipboard from '@libs/Clipboard'; import {getEnvironmentURL} from '@libs/Environment/Environment'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; +import type {ActionDescriptor} from './ActionDescriptor'; -function CopyLink({isFocused, onFocus, onBlur}: ContextMenuActionFocusProps) { - const {reportAction, originalReportID, isMini, interceptAnonymousUser} = useContextMenuPayload(); +function useCopyLinkAction(): ActionDescriptor | null { + const {reportAction, originalReportID, interceptAnonymousUser} = useContextMenuPayload(); const icons = useMemoizedLazyExpensifyIcons(['LinkCopy', 'Checkmark'] as const); const {translate} = useLocalize(); - const handlePress = () => { - getEnvironmentURL().then((environmentURL) => { - const reportActionID = reportAction?.reportActionID; - Clipboard.setString(`${environmentURL}/r/${originalReportID}/${reportActionID}`); - }); - hideContextMenu(true, ReportActionComposeFocusManager.focus); + return { + id: 'copyLink', + icon: icons.LinkCopy, + text: translate('reportActionContextMenu.copyLink'), + successText: translate('reportActionContextMenu.copied'), + successIcon: icons.Checkmark, + isAnonymousAction: true, + onPress: () => + interceptAnonymousUser(() => { + getEnvironmentURL().then((environmentURL) => { + const reportActionID = reportAction?.reportActionID; + Clipboard.setString(`${environmentURL}/r/${originalReportID}/${reportActionID}`); + }); + hideContextMenu(true, ReportActionComposeFocusManager.focus); + }, true), + sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_LINK, }; - - return ( - interceptAnonymousUser(handlePress, true)} - isFocused={isFocused} - onFocus={onFocus} - onBlur={onBlur} - sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_LINK} - /> - ); } -export default CopyLink; +export default useCopyLinkAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/CopyMessage.tsx b/src/pages/inbox/report/ContextMenu/actions/CopyMessage.tsx index 492e3cdd21dc..d9a8c36310e8 100644 --- a/src/pages/inbox/report/ContextMenu/actions/CopyMessage.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/CopyMessage.tsx @@ -1,5 +1,4 @@ import {Str} from 'expensify-common'; -import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import Clipboard from '@libs/Clipboard'; @@ -138,13 +137,13 @@ import { isExpenseReport, } from '@libs/ReportUtils'; import {getTaskCreatedMessage, getTaskReportActionMessage} from '@libs/TaskUtils'; -import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import type {ReportAction} from '@src/types/onyx'; import {getActionHtml} from './actionConfig'; +import type {ActionDescriptor} from './ActionDescriptor'; function setClipboardMessage(content: string | undefined) { if (!content) { @@ -500,35 +499,25 @@ function copyMessageToClipboard(payload: ContextMenuPayloadContextValue) { } } -function CopyMessage({isFocused, onFocus, onBlur}: ContextMenuActionFocusProps) { +function useCopyMessageAction(): ActionDescriptor | null { const payload = useContextMenuPayload(); const icons = useMemoizedLazyExpensifyIcons(['Copy', 'Checkmark'] as const); const {translate} = useLocalize(); - const closePopover = !payload.isMini; - - const handlePress = () => { - copyMessageToClipboard(payload); - if (closePopover) { - hideContextMenu(true, ReportActionComposeFocusManager.focus); - } + return { + id: 'copyMessage', + icon: icons.Copy, + text: translate('reportActionContextMenu.copyMessage'), + successText: translate('reportActionContextMenu.copied'), + successIcon: icons.Checkmark, + isAnonymousAction: true, + onPress: () => + payload.interceptAnonymousUser(() => { + copyMessageToClipboard(payload); + hideContextMenu(true, ReportActionComposeFocusManager.focus); + }, true), + sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_MESSAGE, }; - - return ( - payload.interceptAnonymousUser(handlePress, true)} - isFocused={isFocused} - onFocus={onFocus} - onBlur={onBlur} - sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_MESSAGE} - /> - ); } -export default CopyMessage; +export default useCopyMessageAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/CopyOnyxData.tsx b/src/pages/inbox/report/ContextMenu/actions/CopyOnyxData.tsx index a8b4c084e396..b214ad24365f 100644 --- a/src/pages/inbox/report/ContextMenu/actions/CopyOnyxData.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/CopyOnyxData.tsx @@ -1,38 +1,31 @@ -import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import Clipboard from '@libs/Clipboard'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; +import type {ActionDescriptor} from './ActionDescriptor'; -function CopyOnyxData({isFocused, onFocus, onBlur}: ContextMenuActionFocusProps) { - const {report, isMini, interceptAnonymousUser} = useContextMenuPayload(); +function useCopyOnyxDataAction(): ActionDescriptor | null { + const {report, interceptAnonymousUser} = useContextMenuPayload(); const icons = useMemoizedLazyExpensifyIcons(['Copy', 'Checkmark'] as const); const {translate} = useLocalize(); - const handlePress = () => { - Clipboard.setString(JSON.stringify(report, null, 4)); - hideContextMenu(true, ReportActionComposeFocusManager.focus); + return { + id: 'copyOnyxData', + icon: icons.Copy, + text: translate('reportActionContextMenu.copyOnyxData'), + successText: translate('reportActionContextMenu.copied'), + successIcon: icons.Checkmark, + isAnonymousAction: true, + onPress: () => + interceptAnonymousUser(() => { + Clipboard.setString(JSON.stringify(report, null, 4)); + hideContextMenu(true, ReportActionComposeFocusManager.focus); + }, true), + sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_ONYX_DATA, }; - - return ( - interceptAnonymousUser(handlePress, true)} - isFocused={isFocused} - onFocus={onFocus} - onBlur={onBlur} - sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_ONYX_DATA} - /> - ); } -export default CopyOnyxData; +export default useCopyOnyxDataAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/CopyToClipboard.tsx b/src/pages/inbox/report/ContextMenu/actions/CopyToClipboard.tsx index fcfaead79f1f..779409020e04 100644 --- a/src/pages/inbox/report/ContextMenu/actions/CopyToClipboard.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/CopyToClipboard.tsx @@ -1,38 +1,31 @@ -import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import Clipboard from '@libs/Clipboard'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; +import type {ActionDescriptor} from './ActionDescriptor'; -function CopyToClipboard({isFocused, onFocus, onBlur}: ContextMenuActionFocusProps) { - const {selection, isMini, interceptAnonymousUser} = useContextMenuPayload(); +function useCopyToClipboardAction(): ActionDescriptor | null { + const {selection, interceptAnonymousUser} = useContextMenuPayload(); const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Copy', 'Checkmark'] as const); - const handlePress = () => { - Clipboard.setString(selection); - hideContextMenu(true, ReportActionComposeFocusManager.focus); + return { + id: 'copyToClipboard', + icon: icons.Copy, + text: translate('common.copyToClipboard'), + successText: translate('reportActionContextMenu.copied'), + successIcon: icons.Checkmark, + isAnonymousAction: true, + onPress: () => + interceptAnonymousUser(() => { + Clipboard.setString(selection); + hideContextMenu(true, ReportActionComposeFocusManager.focus); + }, true), + sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_TO_CLIPBOARD, }; - - return ( - interceptAnonymousUser(handlePress, true)} - isFocused={isFocused} - onFocus={onFocus} - onBlur={onBlur} - sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_TO_CLIPBOARD} - /> - ); } -export default CopyToClipboard; +export default useCopyToClipboardAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/CopyURL.tsx b/src/pages/inbox/report/ContextMenu/actions/CopyURL.tsx index 2d24c245fb6a..aeba6c0668b6 100644 --- a/src/pages/inbox/report/ContextMenu/actions/CopyURL.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/CopyURL.tsx @@ -1,39 +1,32 @@ -import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import Clipboard from '@libs/Clipboard'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; +import type {ActionDescriptor} from './ActionDescriptor'; -function CopyURL({isFocused, onFocus, onBlur}: ContextMenuActionFocusProps) { - const {selection, isMini, interceptAnonymousUser} = useContextMenuPayload(); +function useCopyURLAction(): ActionDescriptor | null { + const {selection, interceptAnonymousUser} = useContextMenuPayload(); const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Copy', 'Checkmark'] as const); - const handlePress = () => { - Clipboard.setString(selection); - hideContextMenu(true, ReportActionComposeFocusManager.focus); + return { + id: 'copyUrl', + icon: icons.Copy, + text: translate('reportActionContextMenu.copyURLToClipboard'), + successText: translate('reportActionContextMenu.copied'), + successIcon: icons.Checkmark, + description: selection, + isAnonymousAction: true, + onPress: () => + interceptAnonymousUser(() => { + Clipboard.setString(selection); + hideContextMenu(true, ReportActionComposeFocusManager.focus); + }, true), + sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_URL, }; - - return ( - interceptAnonymousUser(handlePress, true)} - isFocused={isFocused} - onFocus={onFocus} - onBlur={onBlur} - sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_URL} - /> - ); } -export default CopyURL; +export default useCopyURLAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/Debug.tsx b/src/pages/inbox/report/ContextMenu/actions/Debug.tsx index 010fd1fe74b8..d8e8a96c5cf4 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Debug.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/Debug.tsx @@ -1,44 +1,37 @@ -import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; +import type {ActionDescriptor} from './ActionDescriptor'; -function Debug({isFocused, onFocus, onBlur}: ContextMenuActionFocusProps) { - const {reportID, reportAction, isMini, interceptAnonymousUser} = useContextMenuPayload(); +function useDebugAction(): ActionDescriptor | null { + const {reportID, reportAction, interceptAnonymousUser} = useContextMenuPayload(); const icons = useMemoizedLazyExpensifyIcons(['Bug'] as const); const {translate} = useLocalize(); - const handlePress = () => { - if (!reportID) { - return; - } - if (reportAction) { - Navigation.navigate(ROUTES.DEBUG_REPORT_ACTION.getRoute(reportID, reportAction.reportActionID)); - } else { - Navigation.navigate(ROUTES.DEBUG_REPORT.getRoute(reportID)); - } - hideContextMenu(false, ReportActionComposeFocusManager.focus); + return { + id: 'debug', + icon: icons.Bug, + text: translate('debug.debug'), + isAnonymousAction: true, + onPress: () => + interceptAnonymousUser(() => { + if (!reportID) { + return; + } + if (reportAction) { + Navigation.navigate(ROUTES.DEBUG_REPORT_ACTION.getRoute(reportID, reportAction.reportActionID)); + } else { + Navigation.navigate(ROUTES.DEBUG_REPORT.getRoute(reportID)); + } + hideContextMenu(false, ReportActionComposeFocusManager.focus); + }, true), + sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.DEBUG, }; - - return ( - interceptAnonymousUser(handlePress, true)} - isFocused={isFocused} - onFocus={onFocus} - onBlur={onBlur} - sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.DEBUG} - /> - ); } -export default Debug; +export default useDebugAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/Delete.tsx b/src/pages/inbox/report/ContextMenu/actions/Delete.tsx index 8e7250c4a443..5b2ea478f9fa 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Delete.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/Delete.tsx @@ -1,42 +1,28 @@ -import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils'; -import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; -import {hideContextMenu, showDeleteModal} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import {showDeleteModal} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; +import type {ActionDescriptor} from './ActionDescriptor'; -function Delete({isFocused, onFocus, onBlur}: ContextMenuActionFocusProps) { - const {reportID, reportAction, moneyRequestAction, isMini} = useContextMenuPayload(); +function useDeleteAction(): ActionDescriptor | null { + const {reportID, reportAction, moneyRequestAction, hideAndRun} = useContextMenuPayload(); const icons = useMemoizedLazyExpensifyIcons(['Trashcan'] as const); const {translate} = useLocalize(); - const closePopover = !isMini; - - const handlePress = () => { - const iouReportID = isMoneyRequestAction(moneyRequestAction) ? getOriginalMessage(moneyRequestAction)?.IOUReportID : undefined; - const effectiveReportID = iouReportID && Number(iouReportID) !== 0 ? iouReportID : reportID; - const actionSourceID = effectiveReportID !== reportID ? reportID : undefined; - if (closePopover) { - hideContextMenu(false, () => showDeleteModal(effectiveReportID, moneyRequestAction ?? reportAction, undefined, undefined, undefined, actionSourceID)); - return; - } - showDeleteModal(effectiveReportID, moneyRequestAction ?? reportAction, undefined, undefined, undefined, actionSourceID); + return { + id: 'delete', + icon: icons.Trashcan, + text: translate('common.delete'), + onPress: () => { + const iouReportID = isMoneyRequestAction(moneyRequestAction) ? getOriginalMessage(moneyRequestAction)?.IOUReportID : undefined; + const effectiveReportID = iouReportID && Number(iouReportID) !== 0 ? iouReportID : reportID; + const actionSourceID = effectiveReportID !== reportID ? reportID : undefined; + hideAndRun(() => showDeleteModal(effectiveReportID, moneyRequestAction ?? reportAction, undefined, undefined, undefined, actionSourceID)); + }, + sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.DELETE, }; - - return ( - - ); } -export default Delete; +export default useDeleteAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/Download.tsx b/src/pages/inbox/report/ContextMenu/actions/Download.tsx index bf4c52e3e4b7..f818fd49e5c9 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Download.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/Download.tsx @@ -1,4 +1,3 @@ -import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; @@ -6,52 +5,43 @@ import {isMobileSafari} from '@libs/Browser'; import fileDownload from '@libs/fileDownload'; import getAttachmentDetails from '@libs/fileDownload/getAttachmentDetails'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {setDownload} from '@userActions/Download'; import CONST from '@src/CONST'; import {getActionHtml} from './actionConfig'; +import type {ActionDescriptor} from './ActionDescriptor'; -function Download({isFocused, onFocus, onBlur}: ContextMenuActionFocusProps) { - const {reportAction, encryptedAuthToken, isMini, interceptAnonymousUser, download} = useContextMenuPayload(); +function useDownloadAction(): ActionDescriptor | null { + const {reportAction, encryptedAuthToken, interceptAnonymousUser, download, translate: payloadTranslate} = useContextMenuPayload(); const icons = useMemoizedLazyExpensifyIcons(['Download'] as const); const {translate} = useLocalize(); - const closePopover = !isMini; const isDownloading = download?.isDownloading ?? false; - const handlePress = () => { - const html = getActionHtml(reportAction); - const {originalFileName, sourceURL} = getAttachmentDetails(html); - const sourceURLWithAuth = addEncryptedAuthTokenToURL(sourceURL ?? '', encryptedAuthToken); - const sourceID = (sourceURL?.match(CONST.REGEX.ATTACHMENT_ID) ?? [])[1]; - setDownload(sourceID, true); - const anchorRegex = CONST.REGEX_LINK_IN_ANCHOR; - const isAnchorTag = anchorRegex.test(html); - fileDownload(translate, sourceURLWithAuth, originalFileName ?? '', '', isAnchorTag && isMobileSafari()).then(() => setDownload(sourceID, false)); - if (closePopover) { - hideContextMenu(true, ReportActionComposeFocusManager.focus); - } + return { + id: 'download', + icon: icons.Download, + text: translate('common.download'), + successText: translate('common.download'), + successIcon: icons.Download, + isAnonymousAction: true, + disabled: isDownloading, + shouldShowLoadingSpinnerIcon: isDownloading, + onPress: () => + interceptAnonymousUser(() => { + const html = getActionHtml(reportAction); + const {originalFileName, sourceURL} = getAttachmentDetails(html); + const sourceURLWithAuth = addEncryptedAuthTokenToURL(sourceURL ?? '', encryptedAuthToken); + const sourceID = (sourceURL?.match(CONST.REGEX.ATTACHMENT_ID) ?? [])[1]; + setDownload(sourceID, true); + const anchorRegex = CONST.REGEX_LINK_IN_ANCHOR; + const isAnchorTag = anchorRegex.test(html); + fileDownload(payloadTranslate, sourceURLWithAuth, originalFileName ?? '', '', isAnchorTag && isMobileSafari()).then(() => setDownload(sourceID, false)); + hideContextMenu(true, ReportActionComposeFocusManager.focus); + }, true), + sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.DOWNLOAD, }; - - return ( - interceptAnonymousUser(handlePress, true)} - isFocused={isFocused} - onFocus={onFocus} - onBlur={onBlur} - sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.DOWNLOAD} - /> - ); } -export default Download; +export default useDownloadAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/Edit.tsx b/src/pages/inbox/report/ContextMenu/actions/Edit.tsx index 47921ef0c60e..45951dd028a1 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Edit.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/Edit.tsx @@ -1,64 +1,44 @@ -import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; import Parser from '@libs/Parser'; import {isMoneyRequestAction} from '@libs/ReportActionsUtils'; -import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; -import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {deleteReportActionDraft, openReport, saveReportActionDraft} from '@userActions/Report'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import {getActionHtml} from './actionConfig'; +import type {ActionDescriptor} from './ActionDescriptor'; -function Edit({isFocused, onFocus, onBlur}: ContextMenuActionFocusProps) { - const {reportID, reportAction, moneyRequestAction, draftMessage, introSelected, isMini, interceptAnonymousUser} = useContextMenuPayload(); +function useEditAction(): ActionDescriptor | null { + const {reportID, reportAction, moneyRequestAction, draftMessage, introSelected, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(); const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Pencil'] as const); - const closePopover = !isMini; - - const handlePress = () => { - if (isMoneyRequestAction(reportAction) || isMoneyRequestAction(moneyRequestAction)) { - const editExpense = () => { - const childReportID = reportAction?.childReportID; - openReport(childReportID, introSelected); - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID)); - }; - if (closePopover) { - hideContextMenu(false, editExpense); - return; - } - editExpense(); - return; - } - const editAction = () => { - if (!draftMessage) { - saveReportActionDraft(reportID, reportAction, Parser.htmlToMarkdown(getActionHtml(reportAction))); - } else { - deleteReportActionDraft(reportID, reportAction); - } - }; - if (closePopover) { - hideContextMenu(false, editAction); - return; - } - editAction(); + return { + id: 'edit', + icon: icons.Pencil, + text: translate('reportActionContextMenu.editAction', {action: moneyRequestAction ?? reportAction}), + onPress: () => + interceptAnonymousUser(() => { + if (isMoneyRequestAction(reportAction) || isMoneyRequestAction(moneyRequestAction)) { + hideAndRun(() => { + const childReportID = reportAction?.childReportID; + openReport(childReportID, introSelected); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID)); + }); + return; + } + hideAndRun(() => { + if (!draftMessage) { + saveReportActionDraft(reportID, reportAction, Parser.htmlToMarkdown(getActionHtml(reportAction))); + } else { + deleteReportActionDraft(reportID, reportAction); + } + }); + }), + sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.EDIT_COMMENT, }; - - return ( - interceptAnonymousUser(handlePress)} - isFocused={isFocused} - onFocus={onFocus} - onBlur={onBlur} - sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.EDIT_COMMENT} - /> - ); } -export default Edit; +export default useEditAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/EmojiReaction.tsx b/src/pages/inbox/report/ContextMenu/actions/EmojiReaction.tsx index 4834af4af456..1944ffb12be4 100644 --- a/src/pages/inbox/report/ContextMenu/actions/EmojiReaction.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/EmojiReaction.tsx @@ -1,24 +1,25 @@ import type {OnyxEntry} from 'react-native-onyx'; import type {Emoji} from '@assets/emojis/types'; -import MiniQuickEmojiReactions from '@components/Reactions/MiniQuickEmojiReactions'; -import QuickEmojiReactions from '@components/Reactions/QuickEmojiReactions'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; -import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {toggleEmojiReaction} from '@userActions/Report'; import type {ReportActionReactions} from '@src/types/onyx'; -function EmojiReaction() { - const {reportID, reportAction, currentUserAccountID, close, openContextMenu, setIsEmojiPickerActive, isMini, interceptAnonymousUser} = useContextMenuPayload(); +type EmojiReactionData = { + reportID: string | undefined; + reportAction: ReturnType['reportAction']; + reportActionID: string | undefined; + toggleEmojiAndCloseMenu: (emoji: Emoji, existingReactions: OnyxEntry, preferredSkinTone: number) => void; + closeContextMenu: (onHideCallback?: () => void) => void; + onPressOpenPicker: () => void; + onEmojiPickerClosed: () => void; + interceptAnonymousUser: ReturnType['interceptAnonymousUser']; +}; + +function useEmojiReactionData(): EmojiReactionData { + const {reportID, reportAction, currentUserAccountID, openContextMenu, setIsEmojiPickerActive, hideAndRun, interceptAnonymousUser} = useContextMenuPayload(); const closeContextMenu = (onHideCallback?: () => void) => { - if (isMini) { - close(); - if (onHideCallback) { - onHideCallback(); - } - } else { - hideContextMenu(false, onHideCallback); - } + hideAndRun(onHideCallback); }; const toggleEmojiAndCloseMenu = (emoji: Emoji, existingReactions: OnyxEntry, preferredSkinTone: number) => { @@ -27,33 +28,27 @@ function EmojiReaction() { setIsEmojiPickerActive?.(false); }; - if (isMini) { - return ( - interceptAnonymousUser(() => toggleEmojiAndCloseMenu(emoji, existingReactions, preferredSkinTone))} - onPressOpenPicker={() => { - openContextMenu(); - setIsEmojiPickerActive?.(true); - }} - onEmojiPickerClosed={() => { - closeContextMenu(); - setIsEmojiPickerActive?.(false); - }} - reportActionID={reportAction?.reportActionID} - reportAction={reportAction} - /> - ); - } - - return ( - interceptAnonymousUser(() => toggleEmojiAndCloseMenu(emoji, existingReactions, preferredSkinTone))} - reportActionID={reportAction?.reportActionID} - reportAction={reportAction} - setIsEmojiPickerActive={setIsEmojiPickerActive} - /> - ); + const onPressOpenPicker = () => { + openContextMenu(); + setIsEmojiPickerActive?.(true); + }; + + const onEmojiPickerClosed = () => { + closeContextMenu(); + setIsEmojiPickerActive?.(false); + }; + + return { + reportID, + reportAction, + reportActionID: reportAction?.reportActionID, + toggleEmojiAndCloseMenu, + closeContextMenu, + onPressOpenPicker, + onEmojiPickerClosed, + interceptAnonymousUser, + }; } -export default EmojiReaction; +export default useEmojiReactionData; +export type {EmojiReactionData}; diff --git a/src/pages/inbox/report/ContextMenu/actions/Explain.tsx b/src/pages/inbox/report/ContextMenu/actions/Explain.tsx index 11b073fae670..f78b60c90e28 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Explain.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/Explain.tsx @@ -1,47 +1,33 @@ -import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; -import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; -import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {explain} from '@userActions/Report'; import CONST from '@src/CONST'; import KeyboardUtils from '@src/utils/keyboard'; +import type {ActionDescriptor} from './ActionDescriptor'; -function Explain({isFocused, onFocus, onBlur}: ContextMenuActionFocusProps) { - const {childReport, originalReport, reportAction, currentUserPersonalDetails, isMini, interceptAnonymousUser} = useContextMenuPayload(); +function useExplainAction(): ActionDescriptor | null { + const {childReport, originalReport, reportAction, currentUserPersonalDetails, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(); const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Concierge'] as const); - const closePopover = !isMini; - - const handlePress = () => { - if (!originalReport?.reportID) { - return; - } - const doExplain = () => - explain(childReport, originalReport, reportAction, translate, currentUserPersonalDetails?.accountID ?? CONST.DEFAULT_NUMBER_ID, currentUserPersonalDetails?.timezone); - if (closePopover) { - hideContextMenu(false, () => { - KeyboardUtils.dismiss().then(doExplain); - }); - return; - } - doExplain(); + return { + id: 'explain', + icon: icons.Concierge, + text: translate('reportActionContextMenu.explain'), + onPress: () => + interceptAnonymousUser(() => { + if (!originalReport?.reportID) { + return; + } + hideAndRun(() => { + KeyboardUtils.dismiss().then(() => + explain(childReport, originalReport, reportAction, translate, currentUserPersonalDetails?.accountID ?? CONST.DEFAULT_NUMBER_ID, currentUserPersonalDetails?.timezone), + ); + }); + }), + sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.EXPLAIN, }; - - return ( - interceptAnonymousUser(handlePress)} - isFocused={isFocused} - onFocus={onFocus} - onBlur={onBlur} - sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.EXPLAIN} - /> - ); } -export default Explain; +export default useExplainAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/FlagAsOffensive.tsx b/src/pages/inbox/report/ContextMenu/actions/FlagAsOffensive.tsx index 0009ab887741..d113404e8e56 100644 --- a/src/pages/inbox/report/ContextMenu/actions/FlagAsOffensive.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/FlagAsOffensive.tsx @@ -1,49 +1,34 @@ -import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; -import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; -import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import KeyboardUtils from '@src/utils/keyboard'; +import type {ActionDescriptor} from './ActionDescriptor'; -function FlagAsOffensive({isFocused, onFocus, onBlur}: ContextMenuActionFocusProps) { - const {reportID, reportAction, isMini} = useContextMenuPayload(); +function useFlagAsOffensiveAction(): ActionDescriptor | null { + const {reportID, reportAction, hideAndRun} = useContextMenuPayload(); const icons = useMemoizedLazyExpensifyIcons(['Flag'] as const); const {translate} = useLocalize(); - const closePopover = !isMini; - - const handlePress = () => { - if (!reportID) { - return; - } - const activeRoute = Navigation.getActiveRoute(); - if (closePopover) { - hideContextMenu(false, () => { + return { + id: 'flagAsOffensive', + icon: icons.Flag, + text: translate('reportActionContextMenu.flagAsOffensive'), + onPress: () => { + if (!reportID) { + return; + } + const activeRoute = Navigation.getActiveRoute(); + hideAndRun(() => { KeyboardUtils.dismiss().then(() => { Navigation.navigate(ROUTES.FLAG_COMMENT.getRoute(reportID, reportAction?.reportActionID, activeRoute)); }); }); - return; - } - Navigation.navigate(ROUTES.FLAG_COMMENT.getRoute(reportID, reportAction?.reportActionID, activeRoute)); + }, + sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.FLAG_AS_OFFENSIVE, }; - - return ( - - ); } -export default FlagAsOffensive; +export default useFlagAsOffensiveAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/Hold.tsx b/src/pages/inbox/report/ContextMenu/actions/Hold.tsx index 57121461da02..0a2f036ecf7d 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Hold.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/Hold.tsx @@ -1,43 +1,30 @@ -import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import {changeMoneyRequestHoldStatus} from '@libs/ReportUtils'; -import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; +import type {ActionDescriptor} from './ActionDescriptor'; -function Hold({isFocused, onFocus, onBlur}: ContextMenuActionFocusProps) { - const {moneyRequestAction, isDelegateAccessRestricted, showDelegateNoAccessModal, isMini, interceptAnonymousUser} = useContextMenuPayload(); +function useHoldAction(): ActionDescriptor | null { + const {moneyRequestAction, isDelegateAccessRestricted, showDelegateNoAccessModal, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(); const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Stopwatch'] as const); - const closePopover = !isMini; - - const handlePress = () => { - if (isDelegateAccessRestricted) { - hideContextMenu(false, showDelegateNoAccessModal); - return; - } - if (closePopover) { - hideContextMenu(false, () => changeMoneyRequestHoldStatus(moneyRequestAction)); - return; - } - changeMoneyRequestHoldStatus(moneyRequestAction); + return { + id: 'hold', + icon: icons.Stopwatch, + text: translate('iou.hold'), + onPress: () => + interceptAnonymousUser(() => { + if (isDelegateAccessRestricted) { + hideContextMenu(false, showDelegateNoAccessModal); + return; + } + hideAndRun(() => changeMoneyRequestHoldStatus(moneyRequestAction)); + }, false), + sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.HOLD, }; - - return ( - interceptAnonymousUser(handlePress, false)} - isFocused={isFocused} - onFocus={onFocus} - onBlur={onBlur} - sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.HOLD} - /> - ); } -export default Hold; +export default useHoldAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/JoinThread.tsx b/src/pages/inbox/report/ContextMenu/actions/JoinThread.tsx index 34f3086b1514..426d05bc286a 100644 --- a/src/pages/inbox/report/ContextMenu/actions/JoinThread.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/JoinThread.tsx @@ -1,46 +1,31 @@ -import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import {getChildReportNotificationPreference} from '@libs/ReportUtils'; -import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; -import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {toggleSubscribeToChildReport} from '@userActions/Report'; import CONST from '@src/CONST'; +import type {ActionDescriptor} from './ActionDescriptor'; -function JoinThread({isFocused, onFocus, onBlur}: ContextMenuActionFocusProps) { - const {reportAction, originalReport, currentUserAccountID, isMini, interceptAnonymousUser} = useContextMenuPayload(); +function useJoinThreadAction(): ActionDescriptor | null { + const {reportAction, originalReport, currentUserAccountID, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(); const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Bell'] as const); - const closePopover = !isMini; - - const handlePress = () => { - const childReportNotificationPreference = getChildReportNotificationPreference(reportAction); - if (closePopover) { - hideContextMenu(false, () => { - ReportActionComposeFocusManager.focus(); - toggleSubscribeToChildReport(reportAction?.childReportID, currentUserAccountID, reportAction, originalReport, childReportNotificationPreference); - }); - return; - } - ReportActionComposeFocusManager.focus(); - toggleSubscribeToChildReport(reportAction?.childReportID, currentUserAccountID, reportAction, originalReport, childReportNotificationPreference); + return { + id: 'joinThread', + icon: icons.Bell, + text: translate('reportActionContextMenu.joinThread'), + onPress: () => + interceptAnonymousUser(() => { + const childReportNotificationPreference = getChildReportNotificationPreference(reportAction); + hideAndRun(() => { + ReportActionComposeFocusManager.focus(); + toggleSubscribeToChildReport(reportAction?.childReportID, currentUserAccountID, reportAction, originalReport, childReportNotificationPreference); + }); + }, false), + sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.JOIN_THREAD, }; - - return ( - interceptAnonymousUser(handlePress, false)} - isFocused={isFocused} - onFocus={onFocus} - onBlur={onBlur} - sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.JOIN_THREAD} - /> - ); } -export default JoinThread; +export default useJoinThreadAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/LeaveThread.tsx b/src/pages/inbox/report/ContextMenu/actions/LeaveThread.tsx index 9ed7ded644c7..a80f468f5ea9 100644 --- a/src/pages/inbox/report/ContextMenu/actions/LeaveThread.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/LeaveThread.tsx @@ -1,46 +1,31 @@ -import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import {getChildReportNotificationPreference} from '@libs/ReportUtils'; -import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; -import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {toggleSubscribeToChildReport} from '@userActions/Report'; import CONST from '@src/CONST'; +import type {ActionDescriptor} from './ActionDescriptor'; -function LeaveThread({isFocused, onFocus, onBlur}: ContextMenuActionFocusProps) { - const {reportAction, originalReport, currentUserAccountID, isMini, interceptAnonymousUser} = useContextMenuPayload(); +function useLeaveThreadAction(): ActionDescriptor | null { + const {reportAction, originalReport, currentUserAccountID, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(); const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Exit'] as const); - const closePopover = !isMini; - - const handlePress = () => { - const childReportNotificationPreference = getChildReportNotificationPreference(reportAction); - if (closePopover) { - hideContextMenu(false, () => { - ReportActionComposeFocusManager.focus(); - toggleSubscribeToChildReport(reportAction?.childReportID, currentUserAccountID, reportAction, originalReport, childReportNotificationPreference); - }); - return; - } - ReportActionComposeFocusManager.focus(); - toggleSubscribeToChildReport(reportAction?.childReportID, currentUserAccountID, reportAction, originalReport, childReportNotificationPreference); + return { + id: 'leaveThread', + icon: icons.Exit, + text: translate('reportActionContextMenu.leaveThread'), + onPress: () => + interceptAnonymousUser(() => { + const childReportNotificationPreference = getChildReportNotificationPreference(reportAction); + hideAndRun(() => { + ReportActionComposeFocusManager.focus(); + toggleSubscribeToChildReport(reportAction?.childReportID, currentUserAccountID, reportAction, originalReport, childReportNotificationPreference); + }); + }, false), + sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.LEAVE_THREAD, }; - - return ( - interceptAnonymousUser(handlePress, false)} - isFocused={isFocused} - onFocus={onFocus} - onBlur={onBlur} - sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.LEAVE_THREAD} - /> - ); } -export default LeaveThread; +export default useLeaveThreadAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/MarkAsRead.tsx b/src/pages/inbox/report/ContextMenu/actions/MarkAsRead.tsx index c583bb48ca3a..403b14a562cb 100644 --- a/src/pages/inbox/report/ContextMenu/actions/MarkAsRead.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/MarkAsRead.tsx @@ -1,40 +1,28 @@ -import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; -import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {readNewestAction} from '@userActions/Report'; import CONST from '@src/CONST'; +import type {ActionDescriptor} from './ActionDescriptor'; -function MarkAsRead({isFocused, onFocus, onBlur}: ContextMenuActionFocusProps) { - const {reportID, isMini, interceptAnonymousUser} = useContextMenuPayload(); +function useMarkAsReadAction(): ActionDescriptor | null { + const {reportID, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(); const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Mail', 'Checkmark'] as const); - const closePopover = !isMini; - - const handlePress = () => { - readNewestAction(reportID, true, true); - if (closePopover) { - hideContextMenu(true, ReportActionComposeFocusManager.focus); - } + return { + id: 'markAsRead', + icon: icons.Mail, + text: translate('reportActionContextMenu.markAsRead'), + successIcon: icons.Checkmark, + onPress: () => + interceptAnonymousUser(() => { + readNewestAction(reportID, true, true); + hideAndRun(ReportActionComposeFocusManager.focus); + }), + sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.MARK_AS_READ, }; - - return ( - interceptAnonymousUser(handlePress)} - isFocused={isFocused} - onFocus={onFocus} - onBlur={onBlur} - sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.MARK_AS_READ} - /> - ); } -export default MarkAsRead; +export default useMarkAsReadAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/MarkAsUnread.tsx b/src/pages/inbox/report/ContextMenu/actions/MarkAsUnread.tsx index ed7f944c118c..b5bb301a7b29 100644 --- a/src/pages/inbox/report/ContextMenu/actions/MarkAsUnread.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/MarkAsUnread.tsx @@ -1,40 +1,28 @@ -import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; -import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {markCommentAsUnread} from '@userActions/Report'; import CONST from '@src/CONST'; +import type {ActionDescriptor} from './ActionDescriptor'; -function MarkAsUnread({isFocused, onFocus, onBlur}: ContextMenuActionFocusProps) { - const {reportID, reportActions, reportAction, currentUserAccountID, isMini, interceptAnonymousUser} = useContextMenuPayload(); +function useMarkAsUnreadAction(): ActionDescriptor | null { + const {reportID, reportActions, reportAction, currentUserAccountID, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(); const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['ChatBubbleUnread', 'Checkmark'] as const); - const closePopover = !isMini; - - const handlePress = () => { - markCommentAsUnread(reportID, reportActions, reportAction, currentUserAccountID); - if (closePopover) { - hideContextMenu(true, ReportActionComposeFocusManager.focus); - } + return { + id: 'markAsUnread', + icon: icons.ChatBubbleUnread, + text: translate('reportActionContextMenu.markAsUnread'), + successIcon: icons.Checkmark, + onPress: () => + interceptAnonymousUser(() => { + markCommentAsUnread(reportID, reportActions, reportAction, currentUserAccountID); + hideAndRun(ReportActionComposeFocusManager.focus); + }), + sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.MARK_AS_UNREAD, }; - - return ( - interceptAnonymousUser(handlePress)} - isFocused={isFocused} - onFocus={onFocus} - onBlur={onBlur} - sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.MARK_AS_UNREAD} - /> - ); } -export default MarkAsUnread; +export default useMarkAsUnreadAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/OverflowMenu.tsx b/src/pages/inbox/report/ContextMenu/actions/OverflowMenu.tsx index e6b9b31d4dac..31a25baa2a0d 100644 --- a/src/pages/inbox/report/ContextMenu/actions/OverflowMenu.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/OverflowMenu.tsx @@ -1,40 +1,36 @@ import {useRef} from 'react'; import type {GestureResponderEvent, View} from 'react-native'; -import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; -import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import CONST from '@src/CONST'; +import type {ActionDescriptor} from './ActionDescriptor'; -type OverflowMenuProps = ContextMenuActionFocusProps; +type OverflowMenuDescriptor = ActionDescriptor & { + buttonRef: React.RefObject; +}; -function OverflowMenu({isFocused, onFocus, onBlur}: OverflowMenuProps) { - const {openOverflowMenu, openContextMenu, isMini, interceptAnonymousUser} = useContextMenuPayload(); +function useOverflowMenuAction(): OverflowMenuDescriptor | null { + const {openOverflowMenu, openContextMenu, interceptAnonymousUser} = useContextMenuPayload(); const icons = useMemoizedLazyExpensifyIcons(['ThreeDots'] as const); const {translate} = useLocalize(); const threeDotRef = useRef(null); - const handlePress = (event?: GestureResponderEvent | MouseEvent | KeyboardEvent) => { - openOverflowMenu(event as GestureResponderEvent | MouseEvent, threeDotRef); - openContextMenu(); + return { + id: 'overflowMenu', + icon: icons.ThreeDots, + text: translate('reportActionContextMenu.menu'), + isAnonymousAction: true, + shouldPreventDefaultFocusOnPress: false, + buttonRef: threeDotRef, + onPress: (event) => + interceptAnonymousUser(() => { + openOverflowMenu(event as GestureResponderEvent | MouseEvent, threeDotRef); + openContextMenu(); + }, true), + sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.MENU, }; - - return ( - interceptAnonymousUser(() => handlePress(event), true)} - isAnonymousAction - isFocused={isFocused} - onFocus={onFocus} - onBlur={onBlur} - shouldPreventDefaultFocusOnPress={false} - sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.MENU} - /> - ); } -export default OverflowMenu; +export default useOverflowMenuAction; +export type {OverflowMenuDescriptor}; diff --git a/src/pages/inbox/report/ContextMenu/actions/Pin.tsx b/src/pages/inbox/report/ContextMenu/actions/Pin.tsx index 82e5fdccabc3..29f44c85284a 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Pin.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/Pin.tsx @@ -1,39 +1,27 @@ -import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; -import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {togglePinnedState} from '@userActions/Report'; import CONST from '@src/CONST'; +import type {ActionDescriptor} from './ActionDescriptor'; -function Pin({isFocused, onFocus, onBlur}: ContextMenuActionFocusProps) { - const {reportID, isMini, interceptAnonymousUser} = useContextMenuPayload(); +function usePinAction(): ActionDescriptor | null { + const {reportID, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(); const icons = useMemoizedLazyExpensifyIcons(['Pin'] as const); const {translate} = useLocalize(); - const closePopover = !isMini; - - const handlePress = () => { - togglePinnedState(reportID, false); - if (closePopover) { - hideContextMenu(false, ReportActionComposeFocusManager.focus); - } + return { + id: 'pin', + icon: icons.Pin, + text: translate('common.pin'), + onPress: () => + interceptAnonymousUser(() => { + togglePinnedState(reportID, false); + hideAndRun(ReportActionComposeFocusManager.focus); + }), + sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.PIN, }; - - return ( - interceptAnonymousUser(handlePress)} - isFocused={isFocused} - onFocus={onFocus} - onBlur={onBlur} - sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.PIN} - /> - ); } -export default Pin; +export default usePinAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/ReplyInThread.tsx b/src/pages/inbox/report/ContextMenu/actions/ReplyInThread.tsx index f4aa39b8a3c1..c5899d23276d 100644 --- a/src/pages/inbox/report/ContextMenu/actions/ReplyInThread.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/ReplyInThread.tsx @@ -1,45 +1,30 @@ -import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; -import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; -import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {navigateToAndOpenChildReport} from '@userActions/Report'; import CONST from '@src/CONST'; import KeyboardUtils from '@src/utils/keyboard'; +import type {ActionDescriptor} from './ActionDescriptor'; -function ReplyInThread({isFocused, onFocus, onBlur}: ContextMenuActionFocusProps) { - const {childReport, reportAction, originalReport, currentUserAccountID, interceptAnonymousUser, isMini} = useContextMenuPayload(); +function useReplyInThreadAction(): ActionDescriptor | null { + const {childReport, reportAction, originalReport, currentUserAccountID, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(); const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['ChatBubbleReply'] as const); - const closePopover = !isMini; - - const handlePress = () => - interceptAnonymousUser(() => { - if (closePopover) { - hideContextMenu(false, () => { + return { + id: 'replyInThread', + icon: icons.ChatBubbleReply, + text: translate('reportActionContextMenu.replyInThread'), + onPress: () => + interceptAnonymousUser(() => { + hideAndRun(() => { KeyboardUtils.dismiss().then(() => { navigateToAndOpenChildReport(childReport, reportAction, originalReport, currentUserAccountID); }); }); - return; - } - navigateToAndOpenChildReport(childReport, reportAction, originalReport, currentUserAccountID); - }, false); - - return ( - - ); + }, false), + sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.REPLY_IN_THREAD, + }; } -export default ReplyInThread; +export default useReplyInThreadAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/Unhold.tsx b/src/pages/inbox/report/ContextMenu/actions/Unhold.tsx index 42de4a87316a..d2495191726d 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Unhold.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/Unhold.tsx @@ -1,43 +1,30 @@ -import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import {changeMoneyRequestHoldStatus} from '@libs/ReportUtils'; -import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; +import type {ActionDescriptor} from './ActionDescriptor'; -function Unhold({isFocused, onFocus, onBlur}: ContextMenuActionFocusProps) { - const {moneyRequestAction, isDelegateAccessRestricted, showDelegateNoAccessModal, isMini, interceptAnonymousUser} = useContextMenuPayload(); +function useUnholdAction(): ActionDescriptor | null { + const {moneyRequestAction, isDelegateAccessRestricted, showDelegateNoAccessModal, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(); const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Stopwatch'] as const); - const closePopover = !isMini; - - const handlePress = () => { - if (isDelegateAccessRestricted) { - hideContextMenu(false, showDelegateNoAccessModal); - return; - } - if (closePopover) { - hideContextMenu(false, () => changeMoneyRequestHoldStatus(moneyRequestAction)); - return; - } - changeMoneyRequestHoldStatus(moneyRequestAction); + return { + id: 'unhold', + icon: icons.Stopwatch, + text: translate('iou.unhold'), + onPress: () => + interceptAnonymousUser(() => { + if (isDelegateAccessRestricted) { + hideContextMenu(false, showDelegateNoAccessModal); + return; + } + hideAndRun(() => changeMoneyRequestHoldStatus(moneyRequestAction)); + }, false), + sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.UNHOLD, }; - - return ( - interceptAnonymousUser(handlePress, false)} - isFocused={isFocused} - onFocus={onFocus} - onBlur={onBlur} - sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.UNHOLD} - /> - ); } -export default Unhold; +export default useUnholdAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/Unpin.tsx b/src/pages/inbox/report/ContextMenu/actions/Unpin.tsx index b10a5d3a57c1..09a56443c78a 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Unpin.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/Unpin.tsx @@ -1,39 +1,27 @@ -import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import type {ContextMenuActionFocusProps} from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; -import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {togglePinnedState} from '@userActions/Report'; import CONST from '@src/CONST'; +import type {ActionDescriptor} from './ActionDescriptor'; -function Unpin({isFocused, onFocus, onBlur}: ContextMenuActionFocusProps) { - const {reportID, isMini, interceptAnonymousUser} = useContextMenuPayload(); +function useUnpinAction(): ActionDescriptor | null { + const {reportID, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(); const icons = useMemoizedLazyExpensifyIcons(['Pin'] as const); const {translate} = useLocalize(); - const closePopover = !isMini; - - const handlePress = () => { - togglePinnedState(reportID, true); - if (closePopover) { - hideContextMenu(false, ReportActionComposeFocusManager.focus); - } + return { + id: 'unpin', + icon: icons.Pin, + text: translate('common.unPin'), + onPress: () => + interceptAnonymousUser(() => { + togglePinnedState(reportID, true); + hideAndRun(ReportActionComposeFocusManager.focus); + }), + sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.UNPIN, }; - - return ( - interceptAnonymousUser(handlePress)} - isFocused={isFocused} - onFocus={onFocus} - onBlur={onBlur} - sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.UNPIN} - /> - ); } -export default Unpin; +export default useUnpinAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts b/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts index bc7316102599..4c34beca0c4b 100644 --- a/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts +++ b/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts @@ -70,7 +70,6 @@ type ShouldShowArgs = { isUnreadChat: boolean; isThreadReportParentAction: boolean; isOffline: boolean; - isMini: boolean; isProduction: boolean; moneyRequestAction: ReportAction | undefined; areHoldRequirementsMet: boolean; @@ -299,7 +298,7 @@ const ORDERED_ACTION_SHOULD_SHOW: Array<{id: ActionId; isContentAction: boolean; { id: ACTION_IDS.OVERFLOW_MENU, isContentAction: false, - shouldShow: ({isMini}) => isMini, + shouldShow: () => true, }, ]; diff --git a/src/pages/inbox/report/ContextMenu/useContextMenuActions.ts b/src/pages/inbox/report/ContextMenu/useContextMenuActions.ts new file mode 100644 index 000000000000..e4fc73668533 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/useContextMenuActions.ts @@ -0,0 +1,81 @@ +import type {ActionDescriptor} from './actions/ActionDescriptor'; +import { + useCopyEmailAction, + useCopyLinkAction, + useCopyMessageAction, + useCopyOnyxDataAction, + useCopyToClipboardAction, + useCopyURLAction, + useDebugAction, + useDeleteAction, + useDownloadAction, + useEditAction, + useExplainAction, + useFlagAsOffensiveAction, + useHoldAction, + useJoinThreadAction, + useLeaveThreadAction, + useMarkAsReadAction, + useMarkAsUnreadAction, + usePinAction, + useReplyInThreadAction, + useUnholdAction, + useUnpinAction, +} from './actions/ContextMenuAction'; + +/** + * Aggregates all individual context menu action hooks into a single ordered array. + * Each hook is always called (rules of hooks), and returns null when it shouldn't be shown. + * The returned array contains only the visible actions, in display order. + */ +function useContextMenuActions(visibleActionIDs: Set): ActionDescriptor[] { + const replyInThread = useReplyInThreadAction(); + const markAsUnread = useMarkAsUnreadAction(); + const explain = useExplainAction(); + const markAsRead = useMarkAsReadAction(); + const edit = useEditAction(); + const unhold = useUnholdAction(); + const hold = useHoldAction(); + const joinThread = useJoinThreadAction(); + const leaveThread = useLeaveThreadAction(); + const copyUrl = useCopyURLAction(); + const copyToClipboard = useCopyToClipboardAction(); + const copyEmail = useCopyEmailAction(); + const copyMessage = useCopyMessageAction(); + const copyLink = useCopyLinkAction(); + const pin = usePinAction(); + const unpin = useUnpinAction(); + const flagAsOffensive = useFlagAsOffensiveAction(); + const download = useDownloadAction(); + const copyOnyxData = useCopyOnyxDataAction(); + const debug = useDebugAction(); + const deleteAction = useDeleteAction(); + + const allActions = [ + replyInThread, + markAsUnread, + explain, + markAsRead, + edit, + unhold, + hold, + joinThread, + leaveThread, + copyUrl, + copyToClipboard, + copyEmail, + copyMessage, + copyLink, + pin, + unpin, + flagAsOffensive, + download, + copyOnyxData, + debug, + deleteAction, + ]; + + return allActions.filter((action): action is ActionDescriptor => action !== null && visibleActionIDs.has(action.id)); +} + +export default useContextMenuActions; diff --git a/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/inbox/report/ContextMenu/useContextMenuData.ts old mode 100755 new mode 100644 similarity index 50% rename from src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx rename to src/pages/inbox/report/ContextMenu/useContextMenuData.ts index ffd59a833fb0..fd5f994f561a --- a/src/pages/inbox/report/ContextMenu/BaseReportActionContextMenu.tsx +++ b/src/pages/inbox/report/ContextMenu/useContextMenuData.ts @@ -1,14 +1,8 @@ import type {RefObject} from 'react'; -import React, {useState} from 'react'; -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 {InteractionManager} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; -import * as ActionSheetAwareScrollView from '@components/ActionSheetAwareScrollView'; import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; -import FocusTrapForModal from '@components/FocusTrap/FocusTrapForModal'; import {useSession} from '@components/OnyxListItemProvider'; -import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useEnvironment from '@hooks/useEnvironment'; import useGetExpensifyCardFromReportAction from '@hooks/useGetExpensifyCardFromReportAction'; @@ -17,9 +11,6 @@ import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePaginatedReportActions from '@hooks/usePaginatedReportActions'; import useReportIsArchived from '@hooks/useReportIsArchived'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useRestoreInputFocus from '@hooks/useRestoreInputFocus'; -import useStyleUtils from '@hooks/useStyleUtils'; import useTransactionsAndViolationsForReport from '@hooks/useTransactionsAndViolationsForReport'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import {getMovedReportID} from '@libs/ModifiedExpenseMessage'; @@ -45,89 +36,29 @@ import type {OriginalMessageIOU, ReportAction} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {ActionId} from './actions/actionConfig'; import {ORDERED_ACTION_SHOULD_SHOW, RESTRICTED_READONLY_ACTION_IDS} from './actions/actionConfig'; -import ContextMenuAction from './actions/ContextMenuAction'; -import {ContextMenuPayloadContext} from './ContextMenuPayloadProvider'; -import type {ContextMenuPayloadContextValue} from './ContextMenuPayloadProvider'; -import {useMiniContextMenuActions} from './MiniContextMenuProvider'; import type {ContextMenuAnchor, ContextMenuType} from './ReportActionContextMenu'; -import {hideContextMenu, showContextMenu} from './ReportActionContextMenu'; +import {hideContextMenu} from './ReportActionContextMenu'; const EMPTY_SET = new Set(); -type ContextMenuActionFocusProps = { - isFocused: boolean; - onFocus: () => void; - onBlur: () => void; -}; - -type BaseReportActionContextMenuProps = { - /** The ID of the report this report action is attached to. */ +type UseContextMenuDataParams = { reportID: string | undefined; - - /** The ID of the report action this context menu is attached to. */ reportActionID: string | undefined; - - /** The ID of the original report from which the given reportAction is first created. */ originalReportID: string | undefined; - - /** - * If true, this component will be a small, row-oriented menu that displays icons but not text. - * If false, this component will be a larger, column-oriented menu that displays icons alongside text in each row. - */ - isMini?: boolean; - - /** Controls the visibility of this component. */ - isVisible?: boolean; - - /** The copy selection. */ - selection?: string; - - /** Draft message - if this is set the comment is in 'edit' mode */ - draftMessage?: string; - - /** String representing the context menu type [LINK, REPORT_ACTION] which controls context menu choices */ - type?: ContextMenuType; - - /** Target node which is the target of ContentMenu */ - anchor?: RefObject; - - /** Content Ref */ - contentRef?: RefObject; - - /** Function to check if context menu is active */ - checkIfContextMenuActive?: () => void; - - /** Function to update emoji picker state */ - setIsEmojiPickerActive?: (state: boolean) => void; + draftMessage: string; + selection: string; + type: ContextMenuType; + anchor: RefObject | undefined; }; -function BaseReportActionContextMenu({ - type = CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, - anchor, - contentRef, - isMini = false, - isVisible = false, - selection = '', - draftMessage = '', - reportActionID, - reportID, - originalReportID, - checkIfContextMenuActive, - setIsEmojiPickerActive, -}: BaseReportActionContextMenuProps) { - const StyleUtils = useStyleUtils(); - // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth - const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); - const {transitionActionSheetState} = ActionSheetAwareScrollView.useActionSheetAwareScrollViewActions(); - const {isDelegateAccessRestricted} = useDelegateNoAccessState(); - const {showDelegateNoAccessModal} = useDelegateNoAccessActions(); - const [localShouldKeepOpen, setLocalShouldKeepOpen] = useState(false); - const miniActions = useMiniContextMenuActions(); +function useContextMenuData({reportID, reportActionID, originalReportID, draftMessage, selection, type, anchor}: UseContextMenuDataParams) { const {translate, getLocalDateFromDatetime} = useLocalize(); const {isOffline} = useNetwork(); const {isProduction} = useEnvironment(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const encryptedAuthToken = useSession()?.encryptedAuthToken ?? ''; + const {isDelegateAccessRestricted} = useDelegateNoAccessState(); + const {showDelegateNoAccessModal} = useDelegateNoAccessActions(); const [betas] = useOnyx(ONYXKEYS.BETAS); const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, { @@ -213,11 +144,22 @@ function BaseReportActionContextMenu({ const isHarvestReport = isHarvestCreatedExpenseReport(reportNameValuePairs?.origin, reportNameValuePairs?.originalID); const isTryNewDotNVPDismissed = !!tryNewDot?.classicRedirect?.dismissed; - const shouldKeepOpen = isMini ? false : localShouldKeepOpen; - useRestoreInputFocus(isVisible); + // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style + const card = useGetExpensifyCardFromReportAction({reportAction: (reportAction ?? null) as ReportAction, policyID}); + + const interceptAnonymousUser = (callback: () => void, isAnonymousAction = false) => { + if (isAnonymousUser() && !isAnonymousAction) { + hideContextMenu(false); + // eslint-disable-next-line @typescript-eslint/no-deprecated + InteractionManager.runAfterInteractions(() => { + signOutAndRedirectToSignIn(); + }); + } else { + callback(); + } + }; - // Evaluate which actions are visible const shouldShowArgs = { type, reportAction, @@ -231,7 +173,6 @@ function BaseReportActionContextMenu({ isUnreadChat, isThreadReportParentAction, isOffline: !!isOffline, - isMini, isProduction, moneyRequestAction, areHoldRequirementsMet, @@ -243,126 +184,14 @@ function BaseReportActionContextMenu({ isHarvestReport, }; - let visibleActionIds = ORDERED_ACTION_SHOULD_SHOW.filter((entry) => !disabledActionIDs.has(entry.id) && entry.shouldShow(shouldShowArgs)).map((entry) => entry.id); + const getVisibleActionIDs = (): ActionId[] => + ORDERED_ACTION_SHOULD_SHOW.filter((entry) => entry.id !== 'overflowMenu' && !disabledActionIDs.has(entry.id) && entry.shouldShow(shouldShowArgs)).map((entry) => entry.id); - if (isMini) { - const overflowMenuId = visibleActionIds.at(-1); - const otherIds = visibleActionIds.slice(0, -1); - if (otherIds.length > CONST.MINI_CONTEXT_MENU_MAX_ITEMS && overflowMenuId) { - visibleActionIds = [...otherIds.slice(0, CONST.MINI_CONTEXT_MENU_MAX_ITEMS - 1), overflowMenuId]; - } else { - visibleActionIds = otherIds; - } - } - - const visibleSet = new Set(visibleActionIds); - - const contentActionIndexes = visibleActionIds - .map((id, index) => { - const entry = ORDERED_ACTION_SHOULD_SHOW.find((e) => e.id === id); - return entry?.isContentAction ? index : undefined; - }) - .filter((index): index is number => index !== undefined); - - const shouldEnableArrowNavigation = !isMini && (isVisible || shouldKeepOpen); - - const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({ - initialFocusedIndex: -1, - disabledIndexes: contentActionIndexes, - maxIndex: visibleActionIds.length - 1, - isActive: shouldEnableArrowNavigation, - }); - - const getFocusProps = (id: ActionId): ContextMenuActionFocusProps => { - const index = visibleActionIds.indexOf(id); - return { - isFocused: focusedIndex === index, - onFocus: () => setFocusedIndex(index), - onBlur: () => (index === visibleActionIds.length - 1 || index === 1) && setFocusedIndex(-1), - }; - }; - - const renderAction = (id: ActionId, Component: React.ComponentType) => { - const {isFocused, onFocus, onBlur} = getFocusProps(id); - return ( - - ); - }; - - const renderOverflowMenu = () => { - const {isFocused, onFocus, onBlur} = getFocusProps('overflowMenu'); - return ( - - ); - }; - - /** - * Checks if user is anonymous. If true and the action doesn't accept for anonymous user, hides the context menu and - * shows the sign in modal. Else, executes the callback. - */ - const interceptAnonymousUser = (callback: () => void, isAnonymousAction = false) => { - if (isAnonymousUser() && !isAnonymousAction) { - hideContextMenu(false); - // eslint-disable-next-line @typescript-eslint/no-deprecated - InteractionManager.runAfterInteractions(() => { - signOutAndRedirectToSignIn(); - }); - } else { - callback(); - } - }; - - const openOverflowMenu = (event: GestureResponderEvent | MouseEvent, anchorRef: RefObject) => { - showContextMenu({ - type: CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, - event, - selection, - contextMenuAnchor: anchorRef?.current as ViewType | RNText | null, - report: { - reportID, - originalReportID, - }, - reportAction: { - reportActionID: reportAction?.reportActionID, - draftMessage, - }, - callbacks: { - onShow: checkIfContextMenuActive, - onHide: () => { - checkIfContextMenuActive?.(); - if (isMini) { - miniActions.release(); - } else { - setLocalShouldKeepOpen(false); - } - }, - }, - shouldCloseOnTarget: true, - isOverflowMenu: true, - }); - }; - - // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style - const card = useGetExpensifyCardFromReportAction({reportAction: (reportAction ?? null) as ReportAction, policyID}); - - // eslint-disable-next-line react/jsx-no-constructed-context-values - const payloadValue: ContextMenuPayloadContextValue = { - type, - reportID, - originalReportID, - reportActions, - // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style - reportAction: (reportAction ?? null) as ReportAction, + return { report, originalReport, + reportActions, + reportAction, childReport, childReportActions, policy, @@ -373,7 +202,6 @@ function BaseReportActionContextMenu({ iouTransaction, transaction, card, - currentUserAccountID: currentUserPersonalDetails?.accountID, currentUserPersonalDetails, encryptedAuthToken, isArchivedRoom, @@ -382,7 +210,6 @@ function BaseReportActionContextMenu({ isUnreadChat, isThreadReportParentAction, isOffline: !!isOffline, - isMini, isProduction, isHarvestReport, isTryNewDotNVPDismissed, @@ -392,76 +219,24 @@ function BaseReportActionContextMenu({ betas, transactions, introSelected, - draftMessage, - selection, movedFromReport, movedToReport, harvestReport, download, - close: () => { - if (isMini) { - miniActions.release(); - } else { - setLocalShouldKeepOpen(false); - } - }, - transitionActionSheetState, - openContextMenu: () => { - if (isMini) { - miniActions.keepOpen(); - } else { - setLocalShouldKeepOpen(true); - } - }, + disabledActionIDs, interceptAnonymousUser, - openOverflowMenu, - setIsEmojiPickerActive, showDelegateNoAccessModal, translate, getLocalDateFromDatetime, + type, + reportID, + originalReportID, + draftMessage, + selection, anchor, - disabledActionIDs, + getVisibleActionIDs, }; - - const wrapperStyle = StyleUtils.getReportActionContextMenuStyles(isMini, shouldUseNarrowLayout); - - return ( - (isVisible || shouldKeepOpen || !isMini) && ( - - - - {visibleSet.has('emojiReaction') && } - {visibleSet.has('replyInThread') && renderAction('replyInThread', ContextMenuAction.ReplyInThread)} - {visibleSet.has('markAsUnread') && renderAction('markAsUnread', ContextMenuAction.MarkAsUnread)} - {visibleSet.has('explain') && renderAction('explain', ContextMenuAction.Explain)} - {visibleSet.has('markAsRead') && renderAction('markAsRead', ContextMenuAction.MarkAsRead)} - {visibleSet.has('edit') && renderAction('edit', ContextMenuAction.Edit)} - {visibleSet.has('unhold') && renderAction('unhold', ContextMenuAction.Unhold)} - {visibleSet.has('hold') && renderAction('hold', ContextMenuAction.Hold)} - {visibleSet.has('joinThread') && renderAction('joinThread', ContextMenuAction.JoinThread)} - {visibleSet.has('leaveThread') && renderAction('leaveThread', ContextMenuAction.LeaveThread)} - {visibleSet.has('copyUrl') && renderAction('copyUrl', ContextMenuAction.CopyURL)} - {visibleSet.has('copyToClipboard') && renderAction('copyToClipboard', ContextMenuAction.CopyToClipboard)} - {visibleSet.has('copyEmail') && renderAction('copyEmail', ContextMenuAction.CopyEmail)} - {visibleSet.has('copyMessage') && renderAction('copyMessage', ContextMenuAction.CopyMessage)} - {visibleSet.has('copyLink') && renderAction('copyLink', ContextMenuAction.CopyLink)} - {visibleSet.has('pin') && renderAction('pin', ContextMenuAction.Pin)} - {visibleSet.has('unpin') && renderAction('unpin', ContextMenuAction.Unpin)} - {visibleSet.has('flagAsOffensive') && renderAction('flagAsOffensive', ContextMenuAction.FlagAsOffensive)} - {visibleSet.has('download') && renderAction('download', ContextMenuAction.Download)} - {visibleSet.has('copyOnyxData') && renderAction('copyOnyxData', ContextMenuAction.CopyOnyxData)} - {visibleSet.has('debug') && renderAction('debug', ContextMenuAction.Debug)} - {visibleSet.has('delete') && renderAction('delete', ContextMenuAction.Delete)} - {visibleSet.has('overflowMenu') && renderOverflowMenu()} - - - - ) - ); } -export default BaseReportActionContextMenu; -export type {BaseReportActionContextMenuProps, ContextMenuActionFocusProps}; +export default useContextMenuData; +export type {UseContextMenuDataParams}; From 1a28b54575e51b608c5abaa048881516ab93baf3 Mon Sep 17 00:00:00 2001 From: rory Date: Sun, 1 Mar 2026 19:22:40 -0800 Subject: [PATCH 23/88] fix(lint): resolve ESLint and Prettier CI failures - Remove unused isReportArchived from ShowContextMenuContext consumers (MentionUserRenderer, PreRenderer, VideoPlayerThumbnail, ImageRenderer, BaseAnchorForAttachmentsOnly) - Fix boolean-conditional-rendering in MiniReportActionContextMenu - Fix no-default-id-values in PopoverReportActionContextMenu by using null check narrowing instead of defaulting reportActionID to '' - Fix Prettier formatting in MiniReportActionContextMenu/index.tsx Made-with: Cursor --- .../BaseAnchorForAttachmentsOnly.tsx | 2 +- .../HTMLRenderers/ImageRenderer.tsx | 2 +- .../HTMLRenderers/MentionUserRenderer.tsx | 2 +- .../HTMLRenderers/PreRenderer.tsx | 2 +- .../VideoPlayerThumbnail.tsx | 2 +- .../MiniReportActionContextMenu/index.tsx | 18 +++++++++--------- .../PopoverReportActionContextMenu.tsx | 4 ++-- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx index 7e4825da9470..e17669d7b7eb 100644 --- a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx +++ b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx @@ -40,7 +40,7 @@ function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', onP return ( - {({anchor, report, isReportArchived, action, checkIfContextMenuActive, isDisabled, shouldDisplayContextMenu}) => ( + {({anchor, report, action, checkIfContextMenuActive, isDisabled, shouldDisplayContextMenu}) => ( { diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx index bece67758a57..2d64f7e9310a 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx @@ -93,7 +93,7 @@ function ImageRenderer({tnode}: CustomRendererProps) { thumbnailImageComponent ) : ( - {({onShowContextMenu, anchor, report, isReportArchived, action, checkIfContextMenuActive, isDisabled, shouldDisplayContextMenu}) => ( + {({onShowContextMenu, anchor, report, action, checkIfContextMenuActive, isDisabled, shouldDisplayContextMenu}) => ( {({reportID, accountID, type}) => ( - {({onShowContextMenu, anchor, report, isReportArchived, action, checkIfContextMenuActive, isDisabled, shouldDisplayContextMenu}) => ( + {({onShowContextMenu, anchor, report, action, checkIfContextMenuActive, isDisabled, shouldDisplayContextMenu}) => ( { diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer.tsx index 780d7ef65984..561e95bfaf5c 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer.tsx @@ -51,7 +51,7 @@ function PreRenderer({TDefaultRenderer, onPressIn, onPressOut, onLongPress, ...d return ( - {({onShowContextMenu, anchor, report, isReportArchived, action, checkIfContextMenuActive, isDisabled, shouldDisplayContextMenu}) => ( + {({onShowContextMenu, anchor, report, action, checkIfContextMenuActive, isDisabled, shouldDisplayContextMenu}) => ( {})} onPressIn={onPressIn} diff --git a/src/components/VideoPlayerPreview/VideoPlayerThumbnail.tsx b/src/components/VideoPlayerPreview/VideoPlayerThumbnail.tsx index 6b2e91165a1a..78101ee5cedb 100644 --- a/src/components/VideoPlayerPreview/VideoPlayerThumbnail.tsx +++ b/src/components/VideoPlayerPreview/VideoPlayerThumbnail.tsx @@ -45,7 +45,7 @@ function VideoPlayerThumbnail({thumbnailUrl, onPress, accessibilityLabel, isDele )} {!isDeleted ? ( - {({anchor, report, isReportArchived, action, checkIfContextMenuActive, isDisabled, onShowContextMenu, shouldDisplayContextMenu}) => ( + {({anchor, report, action, checkIfContextMenuActive, isDisabled, onShowContextMenu, shouldDisplayContextMenu}) => ( - {hasEmoji && emojiData.reportAction && emojiData.reportActionID && ( + {hasEmoji && !!emojiData.reportAction && !!emojiData.reportActionID && ( emojiData.interceptAnonymousUser(() => emojiData.toggleEmojiAndCloseMenu(emoji, existingReactions, preferredSkinTone)) @@ -70,14 +70,14 @@ function MiniContextMenuContent({visibleActionIDs}: {visibleActionIDs: Set { const {buttonRef, text, onPress, sentryLabel, icon} = overflowMenu; return ( - + {({hovered, pressed}) => ( - {hasEmoji && ( + {hasEmoji && emojiData.reportActionID != null && ( emojiData.interceptAnonymousUser(() => emojiData.toggleEmojiAndCloseMenu(emoji, existingReactions, preferredSkinTone)) } - reportActionID={emojiData.reportActionID ?? ''} + reportActionID={emojiData.reportActionID} reportAction={emojiData.reportAction} setIsEmojiPickerActive={(active) => { if (!active) { From edb3720e8a01a5c2f184b6908c3a5211f525abc1 Mon Sep 17 00:00:00 2001 From: rory Date: Sun, 1 Mar 2026 19:34:01 -0800 Subject: [PATCH 24/88] refactor(contextmenu): inline content components, one component per file Add optional payloadOverride to useContextMenuPayload so action hooks can receive data directly instead of reading from context. This removes the need for separate child components (MiniContextMenuContent and PopoverContextMenuContent) that existed solely to bridge the context boundary between provider and consumer. Made-with: Cursor --- .../ContextMenuPayloadProvider.tsx | 5 +- .../MiniReportActionContextMenu/index.tsx | 125 +++++++------- .../PopoverReportActionContextMenu.tsx | 154 ++++++++---------- .../report/ContextMenu/actions/CopyEmail.tsx | 5 +- .../report/ContextMenu/actions/CopyLink.tsx | 5 +- .../ContextMenu/actions/CopyMessage.tsx | 4 +- .../ContextMenu/actions/CopyOnyxData.tsx | 5 +- .../ContextMenu/actions/CopyToClipboard.tsx | 5 +- .../report/ContextMenu/actions/CopyURL.tsx | 5 +- .../report/ContextMenu/actions/Debug.tsx | 5 +- .../report/ContextMenu/actions/Delete.tsx | 5 +- .../report/ContextMenu/actions/Download.tsx | 5 +- .../inbox/report/ContextMenu/actions/Edit.tsx | 5 +- .../ContextMenu/actions/EmojiReaction.tsx | 5 +- .../report/ContextMenu/actions/Explain.tsx | 5 +- .../ContextMenu/actions/FlagAsOffensive.tsx | 5 +- .../inbox/report/ContextMenu/actions/Hold.tsx | 5 +- .../report/ContextMenu/actions/JoinThread.tsx | 5 +- .../ContextMenu/actions/LeaveThread.tsx | 5 +- .../report/ContextMenu/actions/MarkAsRead.tsx | 5 +- .../ContextMenu/actions/MarkAsUnread.tsx | 5 +- .../ContextMenu/actions/OverflowMenu.tsx | 5 +- .../inbox/report/ContextMenu/actions/Pin.tsx | 5 +- .../ContextMenu/actions/ReplyInThread.tsx | 5 +- .../report/ContextMenu/actions/Unhold.tsx | 5 +- .../report/ContextMenu/actions/Unpin.tsx | 5 +- .../ContextMenu/useContextMenuActions.ts | 45 ++--- 27 files changed, 218 insertions(+), 225 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/ContextMenuPayloadProvider.tsx b/src/pages/inbox/report/ContextMenu/ContextMenuPayloadProvider.tsx index 417e065a3ba3..70fcda7dbfd1 100644 --- a/src/pages/inbox/report/ContextMenu/ContextMenuPayloadProvider.tsx +++ b/src/pages/inbox/report/ContextMenu/ContextMenuPayloadProvider.tsx @@ -77,8 +77,11 @@ type ContextMenuPayloadContextValue = { const ContextMenuPayloadContext = createContext(null); -function useContextMenuPayload(): ContextMenuPayloadContextValue { +function useContextMenuPayload(override?: ContextMenuPayloadContextValue): ContextMenuPayloadContextValue { const ctx = useContext(ContextMenuPayloadContext); + if (override) { + return override; + } if (ctx === null) { throw new Error('useContextMenuPayload must be used within a ContextMenuPayloadProvider'); } diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index 0b385e15bb74..cdd0d26a13cb 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -26,72 +26,6 @@ import CONST from '@src/CONST'; const SLIDE_DURATION = 200; const OVERSHOOT_EASING = Easing.bezier(0.34, 1.56, 0.64, 1); -function MiniContextMenuContent({visibleActionIDs}: {visibleActionIDs: Set}) { - const actions = useContextMenuActions(visibleActionIDs); - const emojiData = useEmojiReactionData(); - const overflowMenu = useOverflowMenuAction(); - const StyleUtils = useStyleUtils(); - - const hasEmoji = visibleActionIDs.has('emojiReaction') && !!emojiData.reportAction && !!emojiData.reportActionID; - const needsOverflow = actions.length > CONST.MINI_CONTEXT_MENU_MAX_ITEMS; - const visibleActions = needsOverflow ? actions.slice(0, CONST.MINI_CONTEXT_MENU_MAX_ITEMS - 1) : actions; - - return ( - <> - {hasEmoji && !!emojiData.reportAction && !!emojiData.reportActionID && ( - - emojiData.interceptAnonymousUser(() => emojiData.toggleEmojiAndCloseMenu(emoji, existingReactions, preferredSkinTone)) - } - onPressOpenPicker={emojiData.onPressOpenPicker} - onEmojiPickerClosed={emojiData.onEmojiPickerClosed} - reportActionID={emojiData.reportActionID} - reportAction={emojiData.reportAction} - /> - )} - {visibleActions.map((action: ActionDescriptor) => ( - - {({hovered, pressed}) => ( - - )} - - ))} - {!!(needsOverflow && overflowMenu) && - (() => { - const {buttonRef, text, onPress, sentryLabel, icon} = overflowMenu; - return ( - - {({hovered, pressed}) => ( - - )} - - ); - })()} - - ); -} - function MiniReportActionContextMenu() { const state = useMiniContextMenuState(); const miniActions = useMiniContextMenuActions(); @@ -201,6 +135,14 @@ function MiniReportActionContextMenu() { setIsEmojiPickerActive: state?.setIsEmojiPickerActive, }; + const actions = useContextMenuActions(visibleActionIDs, payloadValue); + const emojiData = useEmojiReactionData(payloadValue); + const overflowMenu = useOverflowMenuAction(payloadValue); + + const hasEmoji = visibleActionIDs.has('emojiReaction') && !!emojiData.reportAction && !!emojiData.reportActionID; + const needsOverflow = actions.length > CONST.MINI_CONTEXT_MENU_MAX_ITEMS; + const visibleActions = needsOverflow ? actions.slice(0, CONST.MINI_CONTEXT_MENU_MAX_ITEMS - 1) : actions; + if (!state) { return null; } @@ -228,7 +170,56 @@ function MiniReportActionContextMenu() { - + {hasEmoji && !!emojiData.reportAction && !!emojiData.reportActionID && ( + + emojiData.interceptAnonymousUser(() => emojiData.toggleEmojiAndCloseMenu(emoji, existingReactions, preferredSkinTone)) + } + onPressOpenPicker={emojiData.onPressOpenPicker} + onEmojiPickerClosed={emojiData.onEmojiPickerClosed} + reportActionID={emojiData.reportActionID} + reportAction={emojiData.reportAction} + /> + )} + {visibleActions.map((action: ActionDescriptor) => ( + + {({hovered, pressed}) => ( + + )} + + ))} + {!!(needsOverflow && overflowMenu) && + (() => { + const {buttonRef, text, onPress, sentryLabel, icon} = overflowMenu; + return ( + + {({hovered, pressed}) => ( + + )} + + ); + })()} diff --git a/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx index ef92df06a101..fabbda3b5edc 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -63,93 +63,14 @@ type PopoverReportActionContextMenuProps = { ref?: ForwardedRef; }; -function PopoverContextMenuContent({ - isPopoverVisible, - localShouldKeepOpen, - visibleActionIDs, - setLocalShouldKeepOpen, -}: { - isPopoverVisible: boolean; - localShouldKeepOpen: boolean; - visibleActionIDs: Set; - setLocalShouldKeepOpen: (val: boolean) => void; -}) { - const actions = useContextMenuActions(visibleActionIDs); - const emojiData = useEmojiReactionData(); - const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); - const {windowWidth} = useWindowDimensions(); - // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth - const {isSmallScreenWidth} = useResponsiveLayout(); - - const shouldKeepOpen = localShouldKeepOpen; - const shouldEnableArrowNavigation = isPopoverVisible || shouldKeepOpen; - - const contentActionIndexes = actions - .map((action, index) => { - const entry = ORDERED_ACTION_SHOULD_SHOW.find((e) => e.id === action.id); - return entry?.isContentAction ? index : undefined; - }) - .filter((index): index is number => index !== undefined); - - const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({ - initialFocusedIndex: -1, - disabledIndexes: contentActionIndexes, - maxIndex: actions.length - 1, - isActive: shouldEnableArrowNavigation, - }); - - const hasEmoji = visibleActionIDs.has('emojiReaction'); - - return ( - - - {hasEmoji && emojiData.reportActionID != null && ( - - emojiData.interceptAnonymousUser(() => emojiData.toggleEmojiAndCloseMenu(emoji, existingReactions, preferredSkinTone)) - } - reportActionID={emojiData.reportActionID} - reportAction={emojiData.reportAction} - setIsEmojiPickerActive={(active) => { - if (!active) { - return; - } - setLocalShouldKeepOpen(true); - }} - /> - )} - {actions.map((action: ActionDescriptor, i: number) => ( - setFocusedIndex(i)} - onBlur={() => (i === actions.length - 1 || i === 1) && setFocusedIndex(-1)} - disabled={action.disabled} - shouldShowLoadingSpinnerIcon={action.shouldShowLoadingSpinnerIcon} - sentryLabel={action.sentryLabel} - /> - ))} - - - ); -} - function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuProps) { const {transitionActionSheetState} = useActionSheetAwareScrollViewActions(); const modalContext = useModal(); - const {shouldUseNarrowLayout} = useResponsiveLayout(); + // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth + const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); + const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); + const {windowWidth} = useWindowDimensions(); const [menuState, setMenuState] = useState(null); const [isPopoverVisible, setIsPopoverVisible] = useState(false); @@ -476,6 +397,27 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro setIsEmojiPickerActive: menuState?.onEmojiPickerToggle, }; + const actions = useContextMenuActions(visibleActionIDs, payloadValue); + const emojiData = useEmojiReactionData(payloadValue); + + const shouldKeepOpen = localShouldKeepOpen; + const shouldEnableArrowNavigation = isPopoverVisible || shouldKeepOpen; + + const contentActionIndexes = actions + .map((action, index) => { + const entry = ORDERED_ACTION_SHOULD_SHOW.find((e) => e.id === action.id); + return entry?.isContentAction ? index : undefined; + }) + .filter((index): index is number => index !== undefined); + + const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({ + initialFocusedIndex: -1, + disabledIndexes: contentActionIndexes, + maxIndex: actions.length - 1, + isActive: shouldEnableArrowNavigation, + }); + + const hasEmoji = visibleActionIDs.has('emojiReaction'); const wrapperStyle = StyleUtils.getReportActionContextMenuStyles(false, shouldUseNarrowLayout); return ( @@ -505,12 +447,46 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro ref={contentRef} style={wrapperStyle} > - + + + {hasEmoji && emojiData.reportActionID != null && ( + + emojiData.interceptAnonymousUser(() => emojiData.toggleEmojiAndCloseMenu(emoji, existingReactions, preferredSkinTone)) + } + reportActionID={emojiData.reportActionID} + reportAction={emojiData.reportAction} + setIsEmojiPickerActive={(active) => { + if (!active) { + return; + } + setLocalShouldKeepOpen(true); + }} + /> + )} + {actions.map((action: ActionDescriptor, i: number) => ( + setFocusedIndex(i)} + onBlur={() => (i === actions.length - 1 || i === 1) && setFocusedIndex(-1)} + disabled={action.disabled} + shouldShowLoadingSpinnerIcon={action.shouldShowLoadingSpinnerIcon} + sentryLabel={action.sentryLabel} + /> + ))} + + diff --git a/src/pages/inbox/report/ContextMenu/actions/CopyEmail.tsx b/src/pages/inbox/report/ContextMenu/actions/CopyEmail.tsx index 355506bbc851..02ccdeae92e1 100644 --- a/src/pages/inbox/report/ContextMenu/actions/CopyEmail.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/CopyEmail.tsx @@ -3,13 +3,14 @@ import useLocalize from '@hooks/useLocalize'; import Clipboard from '@libs/Clipboard'; import EmailUtils from '@libs/EmailUtils'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import type {ActionDescriptor} from './ActionDescriptor'; -function useCopyEmailAction(): ActionDescriptor | null { - const {selection, interceptAnonymousUser} = useContextMenuPayload(); +function useCopyEmailAction(payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor | null { + const {selection, interceptAnonymousUser} = useContextMenuPayload(payloadOverride); const icons = useMemoizedLazyExpensifyIcons(['Copy', 'Checkmark'] as const); const {translate} = useLocalize(); diff --git a/src/pages/inbox/report/ContextMenu/actions/CopyLink.tsx b/src/pages/inbox/report/ContextMenu/actions/CopyLink.tsx index 6550c08196ff..0a8ea6f3a57b 100644 --- a/src/pages/inbox/report/ContextMenu/actions/CopyLink.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/CopyLink.tsx @@ -3,13 +3,14 @@ import useLocalize from '@hooks/useLocalize'; import Clipboard from '@libs/Clipboard'; import {getEnvironmentURL} from '@libs/Environment/Environment'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import type {ActionDescriptor} from './ActionDescriptor'; -function useCopyLinkAction(): ActionDescriptor | null { - const {reportAction, originalReportID, interceptAnonymousUser} = useContextMenuPayload(); +function useCopyLinkAction(payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor | null { + const {reportAction, originalReportID, interceptAnonymousUser} = useContextMenuPayload(payloadOverride); const icons = useMemoizedLazyExpensifyIcons(['LinkCopy', 'Checkmark'] as const); const {translate} = useLocalize(); diff --git a/src/pages/inbox/report/ContextMenu/actions/CopyMessage.tsx b/src/pages/inbox/report/ContextMenu/actions/CopyMessage.tsx index d9a8c36310e8..33c587c12269 100644 --- a/src/pages/inbox/report/ContextMenu/actions/CopyMessage.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/CopyMessage.tsx @@ -499,8 +499,8 @@ function copyMessageToClipboard(payload: ContextMenuPayloadContextValue) { } } -function useCopyMessageAction(): ActionDescriptor | null { - const payload = useContextMenuPayload(); +function useCopyMessageAction(payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor | null { + const payload = useContextMenuPayload(payloadOverride); const icons = useMemoizedLazyExpensifyIcons(['Copy', 'Checkmark'] as const); const {translate} = useLocalize(); diff --git a/src/pages/inbox/report/ContextMenu/actions/CopyOnyxData.tsx b/src/pages/inbox/report/ContextMenu/actions/CopyOnyxData.tsx index b214ad24365f..35578d8e3ad3 100644 --- a/src/pages/inbox/report/ContextMenu/actions/CopyOnyxData.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/CopyOnyxData.tsx @@ -2,13 +2,14 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import Clipboard from '@libs/Clipboard'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import type {ActionDescriptor} from './ActionDescriptor'; -function useCopyOnyxDataAction(): ActionDescriptor | null { - const {report, interceptAnonymousUser} = useContextMenuPayload(); +function useCopyOnyxDataAction(payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor | null { + const {report, interceptAnonymousUser} = useContextMenuPayload(payloadOverride); const icons = useMemoizedLazyExpensifyIcons(['Copy', 'Checkmark'] as const); const {translate} = useLocalize(); diff --git a/src/pages/inbox/report/ContextMenu/actions/CopyToClipboard.tsx b/src/pages/inbox/report/ContextMenu/actions/CopyToClipboard.tsx index 779409020e04..c39a46844cec 100644 --- a/src/pages/inbox/report/ContextMenu/actions/CopyToClipboard.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/CopyToClipboard.tsx @@ -2,13 +2,14 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import Clipboard from '@libs/Clipboard'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import type {ActionDescriptor} from './ActionDescriptor'; -function useCopyToClipboardAction(): ActionDescriptor | null { - const {selection, interceptAnonymousUser} = useContextMenuPayload(); +function useCopyToClipboardAction(payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor | null { + const {selection, interceptAnonymousUser} = useContextMenuPayload(payloadOverride); const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Copy', 'Checkmark'] as const); diff --git a/src/pages/inbox/report/ContextMenu/actions/CopyURL.tsx b/src/pages/inbox/report/ContextMenu/actions/CopyURL.tsx index aeba6c0668b6..87fe227d0adb 100644 --- a/src/pages/inbox/report/ContextMenu/actions/CopyURL.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/CopyURL.tsx @@ -2,13 +2,14 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import Clipboard from '@libs/Clipboard'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import type {ActionDescriptor} from './ActionDescriptor'; -function useCopyURLAction(): ActionDescriptor | null { - const {selection, interceptAnonymousUser} = useContextMenuPayload(); +function useCopyURLAction(payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor | null { + const {selection, interceptAnonymousUser} = useContextMenuPayload(payloadOverride); const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Copy', 'Checkmark'] as const); diff --git a/src/pages/inbox/report/ContextMenu/actions/Debug.tsx b/src/pages/inbox/report/ContextMenu/actions/Debug.tsx index d8e8a96c5cf4..4f27d4448446 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Debug.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/Debug.tsx @@ -2,14 +2,15 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {ActionDescriptor} from './ActionDescriptor'; -function useDebugAction(): ActionDescriptor | null { - const {reportID, reportAction, interceptAnonymousUser} = useContextMenuPayload(); +function useDebugAction(payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor | null { + const {reportID, reportAction, interceptAnonymousUser} = useContextMenuPayload(payloadOverride); const icons = useMemoizedLazyExpensifyIcons(['Bug'] as const); const {translate} = useLocalize(); diff --git a/src/pages/inbox/report/ContextMenu/actions/Delete.tsx b/src/pages/inbox/report/ContextMenu/actions/Delete.tsx index 5b2ea478f9fa..d34f6e95abe2 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Delete.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/Delete.tsx @@ -1,13 +1,14 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils'; +import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {showDeleteModal} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import type {ActionDescriptor} from './ActionDescriptor'; -function useDeleteAction(): ActionDescriptor | null { - const {reportID, reportAction, moneyRequestAction, hideAndRun} = useContextMenuPayload(); +function useDeleteAction(payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor | null { + const {reportID, reportAction, moneyRequestAction, hideAndRun} = useContextMenuPayload(payloadOverride); const icons = useMemoizedLazyExpensifyIcons(['Trashcan'] as const); const {translate} = useLocalize(); diff --git a/src/pages/inbox/report/ContextMenu/actions/Download.tsx b/src/pages/inbox/report/ContextMenu/actions/Download.tsx index f818fd49e5c9..a05359cbcf27 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Download.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/Download.tsx @@ -5,6 +5,7 @@ import {isMobileSafari} from '@libs/Browser'; import fileDownload from '@libs/fileDownload'; import getAttachmentDetails from '@libs/fileDownload/getAttachmentDetails'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {setDownload} from '@userActions/Download'; @@ -12,8 +13,8 @@ import CONST from '@src/CONST'; import {getActionHtml} from './actionConfig'; import type {ActionDescriptor} from './ActionDescriptor'; -function useDownloadAction(): ActionDescriptor | null { - const {reportAction, encryptedAuthToken, interceptAnonymousUser, download, translate: payloadTranslate} = useContextMenuPayload(); +function useDownloadAction(payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor | null { + const {reportAction, encryptedAuthToken, interceptAnonymousUser, download, translate: payloadTranslate} = useContextMenuPayload(payloadOverride); const icons = useMemoizedLazyExpensifyIcons(['Download'] as const); const {translate} = useLocalize(); diff --git a/src/pages/inbox/report/ContextMenu/actions/Edit.tsx b/src/pages/inbox/report/ContextMenu/actions/Edit.tsx index 45951dd028a1..5586b9931af0 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Edit.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/Edit.tsx @@ -3,6 +3,7 @@ import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; import Parser from '@libs/Parser'; import {isMoneyRequestAction} from '@libs/ReportActionsUtils'; +import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {deleteReportActionDraft, openReport, saveReportActionDraft} from '@userActions/Report'; import CONST from '@src/CONST'; @@ -10,8 +11,8 @@ import ROUTES from '@src/ROUTES'; import {getActionHtml} from './actionConfig'; import type {ActionDescriptor} from './ActionDescriptor'; -function useEditAction(): ActionDescriptor | null { - const {reportID, reportAction, moneyRequestAction, draftMessage, introSelected, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(); +function useEditAction(payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor | null { + const {reportID, reportAction, moneyRequestAction, draftMessage, introSelected, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(payloadOverride); const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Pencil'] as const); diff --git a/src/pages/inbox/report/ContextMenu/actions/EmojiReaction.tsx b/src/pages/inbox/report/ContextMenu/actions/EmojiReaction.tsx index 1944ffb12be4..8cce8403b583 100644 --- a/src/pages/inbox/report/ContextMenu/actions/EmojiReaction.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/EmojiReaction.tsx @@ -1,5 +1,6 @@ import type {OnyxEntry} from 'react-native-onyx'; import type {Emoji} from '@assets/emojis/types'; +import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {toggleEmojiReaction} from '@userActions/Report'; import type {ReportActionReactions} from '@src/types/onyx'; @@ -15,8 +16,8 @@ type EmojiReactionData = { interceptAnonymousUser: ReturnType['interceptAnonymousUser']; }; -function useEmojiReactionData(): EmojiReactionData { - const {reportID, reportAction, currentUserAccountID, openContextMenu, setIsEmojiPickerActive, hideAndRun, interceptAnonymousUser} = useContextMenuPayload(); +function useEmojiReactionData(payloadOverride?: ContextMenuPayloadContextValue): EmojiReactionData { + const {reportID, reportAction, currentUserAccountID, openContextMenu, setIsEmojiPickerActive, hideAndRun, interceptAnonymousUser} = useContextMenuPayload(payloadOverride); const closeContextMenu = (onHideCallback?: () => void) => { hideAndRun(onHideCallback); diff --git a/src/pages/inbox/report/ContextMenu/actions/Explain.tsx b/src/pages/inbox/report/ContextMenu/actions/Explain.tsx index f78b60c90e28..7348db535762 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Explain.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/Explain.tsx @@ -1,13 +1,14 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; +import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {explain} from '@userActions/Report'; import CONST from '@src/CONST'; import KeyboardUtils from '@src/utils/keyboard'; import type {ActionDescriptor} from './ActionDescriptor'; -function useExplainAction(): ActionDescriptor | null { - const {childReport, originalReport, reportAction, currentUserPersonalDetails, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(); +function useExplainAction(payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor | null { + const {childReport, originalReport, reportAction, currentUserPersonalDetails, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(payloadOverride); const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Concierge'] as const); diff --git a/src/pages/inbox/report/ContextMenu/actions/FlagAsOffensive.tsx b/src/pages/inbox/report/ContextMenu/actions/FlagAsOffensive.tsx index d113404e8e56..11c6bda8785c 100644 --- a/src/pages/inbox/report/ContextMenu/actions/FlagAsOffensive.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/FlagAsOffensive.tsx @@ -1,14 +1,15 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; +import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import KeyboardUtils from '@src/utils/keyboard'; import type {ActionDescriptor} from './ActionDescriptor'; -function useFlagAsOffensiveAction(): ActionDescriptor | null { - const {reportID, reportAction, hideAndRun} = useContextMenuPayload(); +function useFlagAsOffensiveAction(payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor | null { + const {reportID, reportAction, hideAndRun} = useContextMenuPayload(payloadOverride); const icons = useMemoizedLazyExpensifyIcons(['Flag'] as const); const {translate} = useLocalize(); diff --git a/src/pages/inbox/report/ContextMenu/actions/Hold.tsx b/src/pages/inbox/report/ContextMenu/actions/Hold.tsx index 0a2f036ecf7d..565a8687427b 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Hold.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/Hold.tsx @@ -1,13 +1,14 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import {changeMoneyRequestHoldStatus} from '@libs/ReportUtils'; +import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import type {ActionDescriptor} from './ActionDescriptor'; -function useHoldAction(): ActionDescriptor | null { - const {moneyRequestAction, isDelegateAccessRestricted, showDelegateNoAccessModal, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(); +function useHoldAction(payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor | null { + const {moneyRequestAction, isDelegateAccessRestricted, showDelegateNoAccessModal, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(payloadOverride); const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Stopwatch'] as const); diff --git a/src/pages/inbox/report/ContextMenu/actions/JoinThread.tsx b/src/pages/inbox/report/ContextMenu/actions/JoinThread.tsx index 426d05bc286a..7cad03c6b245 100644 --- a/src/pages/inbox/report/ContextMenu/actions/JoinThread.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/JoinThread.tsx @@ -2,13 +2,14 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import {getChildReportNotificationPreference} from '@libs/ReportUtils'; +import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {toggleSubscribeToChildReport} from '@userActions/Report'; import CONST from '@src/CONST'; import type {ActionDescriptor} from './ActionDescriptor'; -function useJoinThreadAction(): ActionDescriptor | null { - const {reportAction, originalReport, currentUserAccountID, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(); +function useJoinThreadAction(payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor | null { + const {reportAction, originalReport, currentUserAccountID, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(payloadOverride); const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Bell'] as const); diff --git a/src/pages/inbox/report/ContextMenu/actions/LeaveThread.tsx b/src/pages/inbox/report/ContextMenu/actions/LeaveThread.tsx index a80f468f5ea9..cd1aae0ed73f 100644 --- a/src/pages/inbox/report/ContextMenu/actions/LeaveThread.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/LeaveThread.tsx @@ -2,13 +2,14 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import {getChildReportNotificationPreference} from '@libs/ReportUtils'; +import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {toggleSubscribeToChildReport} from '@userActions/Report'; import CONST from '@src/CONST'; import type {ActionDescriptor} from './ActionDescriptor'; -function useLeaveThreadAction(): ActionDescriptor | null { - const {reportAction, originalReport, currentUserAccountID, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(); +function useLeaveThreadAction(payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor | null { + const {reportAction, originalReport, currentUserAccountID, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(payloadOverride); const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Exit'] as const); diff --git a/src/pages/inbox/report/ContextMenu/actions/MarkAsRead.tsx b/src/pages/inbox/report/ContextMenu/actions/MarkAsRead.tsx index 403b14a562cb..99d30a4adf17 100644 --- a/src/pages/inbox/report/ContextMenu/actions/MarkAsRead.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/MarkAsRead.tsx @@ -1,13 +1,14 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {readNewestAction} from '@userActions/Report'; import CONST from '@src/CONST'; import type {ActionDescriptor} from './ActionDescriptor'; -function useMarkAsReadAction(): ActionDescriptor | null { - const {reportID, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(); +function useMarkAsReadAction(payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor | null { + const {reportID, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(payloadOverride); const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Mail', 'Checkmark'] as const); diff --git a/src/pages/inbox/report/ContextMenu/actions/MarkAsUnread.tsx b/src/pages/inbox/report/ContextMenu/actions/MarkAsUnread.tsx index b5bb301a7b29..0ee9be924689 100644 --- a/src/pages/inbox/report/ContextMenu/actions/MarkAsUnread.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/MarkAsUnread.tsx @@ -1,13 +1,14 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {markCommentAsUnread} from '@userActions/Report'; import CONST from '@src/CONST'; import type {ActionDescriptor} from './ActionDescriptor'; -function useMarkAsUnreadAction(): ActionDescriptor | null { - const {reportID, reportActions, reportAction, currentUserAccountID, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(); +function useMarkAsUnreadAction(payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor | null { + const {reportID, reportActions, reportAction, currentUserAccountID, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(payloadOverride); const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['ChatBubbleUnread', 'Checkmark'] as const); diff --git a/src/pages/inbox/report/ContextMenu/actions/OverflowMenu.tsx b/src/pages/inbox/report/ContextMenu/actions/OverflowMenu.tsx index 31a25baa2a0d..682e40f15314 100644 --- a/src/pages/inbox/report/ContextMenu/actions/OverflowMenu.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/OverflowMenu.tsx @@ -2,6 +2,7 @@ import {useRef} from 'react'; import type {GestureResponderEvent, View} from 'react-native'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; +import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import CONST from '@src/CONST'; import type {ActionDescriptor} from './ActionDescriptor'; @@ -10,8 +11,8 @@ type OverflowMenuDescriptor = ActionDescriptor & { buttonRef: React.RefObject; }; -function useOverflowMenuAction(): OverflowMenuDescriptor | null { - const {openOverflowMenu, openContextMenu, interceptAnonymousUser} = useContextMenuPayload(); +function useOverflowMenuAction(payloadOverride?: ContextMenuPayloadContextValue): OverflowMenuDescriptor | null { + const {openOverflowMenu, openContextMenu, interceptAnonymousUser} = useContextMenuPayload(payloadOverride); const icons = useMemoizedLazyExpensifyIcons(['ThreeDots'] as const); const {translate} = useLocalize(); const threeDotRef = useRef(null); diff --git a/src/pages/inbox/report/ContextMenu/actions/Pin.tsx b/src/pages/inbox/report/ContextMenu/actions/Pin.tsx index 29f44c85284a..e996abb3bc46 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Pin.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/Pin.tsx @@ -1,13 +1,14 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {togglePinnedState} from '@userActions/Report'; import CONST from '@src/CONST'; import type {ActionDescriptor} from './ActionDescriptor'; -function usePinAction(): ActionDescriptor | null { - const {reportID, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(); +function usePinAction(payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor | null { + const {reportID, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(payloadOverride); const icons = useMemoizedLazyExpensifyIcons(['Pin'] as const); const {translate} = useLocalize(); diff --git a/src/pages/inbox/report/ContextMenu/actions/ReplyInThread.tsx b/src/pages/inbox/report/ContextMenu/actions/ReplyInThread.tsx index c5899d23276d..771277289bb6 100644 --- a/src/pages/inbox/report/ContextMenu/actions/ReplyInThread.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/ReplyInThread.tsx @@ -1,13 +1,14 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; +import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {navigateToAndOpenChildReport} from '@userActions/Report'; import CONST from '@src/CONST'; import KeyboardUtils from '@src/utils/keyboard'; import type {ActionDescriptor} from './ActionDescriptor'; -function useReplyInThreadAction(): ActionDescriptor | null { - const {childReport, reportAction, originalReport, currentUserAccountID, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(); +function useReplyInThreadAction(payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor | null { + const {childReport, reportAction, originalReport, currentUserAccountID, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(payloadOverride); const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['ChatBubbleReply'] as const); diff --git a/src/pages/inbox/report/ContextMenu/actions/Unhold.tsx b/src/pages/inbox/report/ContextMenu/actions/Unhold.tsx index d2495191726d..6433cb2fa76e 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Unhold.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/Unhold.tsx @@ -1,13 +1,14 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import {changeMoneyRequestHoldStatus} from '@libs/ReportUtils'; +import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import type {ActionDescriptor} from './ActionDescriptor'; -function useUnholdAction(): ActionDescriptor | null { - const {moneyRequestAction, isDelegateAccessRestricted, showDelegateNoAccessModal, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(); +function useUnholdAction(payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor | null { + const {moneyRequestAction, isDelegateAccessRestricted, showDelegateNoAccessModal, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(payloadOverride); const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Stopwatch'] as const); diff --git a/src/pages/inbox/report/ContextMenu/actions/Unpin.tsx b/src/pages/inbox/report/ContextMenu/actions/Unpin.tsx index 09a56443c78a..f793f90630a1 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Unpin.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/Unpin.tsx @@ -1,13 +1,14 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {togglePinnedState} from '@userActions/Report'; import CONST from '@src/CONST'; import type {ActionDescriptor} from './ActionDescriptor'; -function useUnpinAction(): ActionDescriptor | null { - const {reportID, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(); +function useUnpinAction(payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor | null { + const {reportID, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(payloadOverride); const icons = useMemoizedLazyExpensifyIcons(['Pin'] as const); const {translate} = useLocalize(); diff --git a/src/pages/inbox/report/ContextMenu/useContextMenuActions.ts b/src/pages/inbox/report/ContextMenu/useContextMenuActions.ts index e4fc73668533..d9425de0ce8e 100644 --- a/src/pages/inbox/report/ContextMenu/useContextMenuActions.ts +++ b/src/pages/inbox/report/ContextMenu/useContextMenuActions.ts @@ -22,34 +22,35 @@ import { useUnholdAction, useUnpinAction, } from './actions/ContextMenuAction'; +import type {ContextMenuPayloadContextValue} from './ContextMenuPayloadProvider'; /** * Aggregates all individual context menu action hooks into a single ordered array. * Each hook is always called (rules of hooks), and returns null when it shouldn't be shown. * The returned array contains only the visible actions, in display order. */ -function useContextMenuActions(visibleActionIDs: Set): ActionDescriptor[] { - const replyInThread = useReplyInThreadAction(); - const markAsUnread = useMarkAsUnreadAction(); - const explain = useExplainAction(); - const markAsRead = useMarkAsReadAction(); - const edit = useEditAction(); - const unhold = useUnholdAction(); - const hold = useHoldAction(); - const joinThread = useJoinThreadAction(); - const leaveThread = useLeaveThreadAction(); - const copyUrl = useCopyURLAction(); - const copyToClipboard = useCopyToClipboardAction(); - const copyEmail = useCopyEmailAction(); - const copyMessage = useCopyMessageAction(); - const copyLink = useCopyLinkAction(); - const pin = usePinAction(); - const unpin = useUnpinAction(); - const flagAsOffensive = useFlagAsOffensiveAction(); - const download = useDownloadAction(); - const copyOnyxData = useCopyOnyxDataAction(); - const debug = useDebugAction(); - const deleteAction = useDeleteAction(); +function useContextMenuActions(visibleActionIDs: Set, payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor[] { + const replyInThread = useReplyInThreadAction(payloadOverride); + const markAsUnread = useMarkAsUnreadAction(payloadOverride); + const explain = useExplainAction(payloadOverride); + const markAsRead = useMarkAsReadAction(payloadOverride); + const edit = useEditAction(payloadOverride); + const unhold = useUnholdAction(payloadOverride); + const hold = useHoldAction(payloadOverride); + const joinThread = useJoinThreadAction(payloadOverride); + const leaveThread = useLeaveThreadAction(payloadOverride); + const copyUrl = useCopyURLAction(payloadOverride); + const copyToClipboard = useCopyToClipboardAction(payloadOverride); + const copyEmail = useCopyEmailAction(payloadOverride); + const copyMessage = useCopyMessageAction(payloadOverride); + const copyLink = useCopyLinkAction(payloadOverride); + const pin = usePinAction(payloadOverride); + const unpin = useUnpinAction(payloadOverride); + const flagAsOffensive = useFlagAsOffensiveAction(payloadOverride); + const download = useDownloadAction(payloadOverride); + const copyOnyxData = useCopyOnyxDataAction(payloadOverride); + const debug = useDebugAction(payloadOverride); + const deleteAction = useDeleteAction(payloadOverride); const allActions = [ replyInThread, From c49a546d8ca76024e094b64be228717eed8eda62 Mon Sep 17 00:00:00 2001 From: rory Date: Sun, 1 Mar 2026 19:34:40 -0800 Subject: [PATCH 25/88] rename BaseMiniContextMenuItem to MiniContextMenuItem Made-with: Cursor --- ...MiniContextMenuItem.tsx => MiniContextMenuItem.tsx} | 8 ++++---- src/components/Reactions/MiniQuickEmojiReactions.tsx | 10 +++++----- .../ContextMenu/MiniReportActionContextMenu/index.tsx | 10 +++++----- 3 files changed, 14 insertions(+), 14 deletions(-) rename src/components/{BaseMiniContextMenuItem.tsx => MiniContextMenuItem.tsx} (95%) diff --git a/src/components/BaseMiniContextMenuItem.tsx b/src/components/MiniContextMenuItem.tsx similarity index 95% rename from src/components/BaseMiniContextMenuItem.tsx rename to src/components/MiniContextMenuItem.tsx index 23a4520eae8a..51be32c00429 100644 --- a/src/components/BaseMiniContextMenuItem.tsx +++ b/src/components/MiniContextMenuItem.tsx @@ -13,7 +13,7 @@ import type {PressableRef} from './Pressable/GenericPressable/types'; import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback'; import Tooltip from './Tooltip/PopoverAnchorTooltip'; -type BaseMiniContextMenuItemProps = WithSentryLabel & { +type MiniContextMenuItemProps = WithSentryLabel & { /** * Text to display when hovering the menu item */ @@ -48,7 +48,7 @@ type BaseMiniContextMenuItemProps = WithSentryLabel & { * Component that renders a mini context menu item with a * pressable. Also renders a tooltip when hovering the item. */ -function BaseMiniContextMenuItem({ +function MiniContextMenuItem({ tooltipText, onPress, children, @@ -56,7 +56,7 @@ function BaseMiniContextMenuItem({ shouldPreventDefaultFocusOnPress = true, ref, sentryLabel, -}: BaseMiniContextMenuItemProps) { +}: MiniContextMenuItemProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); return ( @@ -105,4 +105,4 @@ function BaseMiniContextMenuItem({ ); } -export default BaseMiniContextMenuItem; +export default MiniContextMenuItem; diff --git a/src/components/Reactions/MiniQuickEmojiReactions.tsx b/src/components/Reactions/MiniQuickEmojiReactions.tsx index 0f562dc77116..9c5679906ed2 100644 --- a/src/components/Reactions/MiniQuickEmojiReactions.tsx +++ b/src/components/Reactions/MiniQuickEmojiReactions.tsx @@ -1,7 +1,7 @@ import React, {useCallback, useRef} from 'react'; import {View} from 'react-native'; import type {Emoji} from '@assets/emojis/types'; -import BaseMiniContextMenuItem from '@components/BaseMiniContextMenuItem'; +import MiniContextMenuItem from '@components/MiniContextMenuItem'; import Icon from '@components/Icon'; import Text from '@components/Text'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; @@ -65,7 +65,7 @@ function MiniQuickEmojiReactions({reportAction, reportActionID, onEmojiSelected, return ( {CONST.QUICK_REACTIONS.slice(0, 3).map((emoji: Emoji) => ( - {getPreferredEmojiCode(emoji, preferredSkinTone)} - + ))} - { if (!emojiPickerRef.current?.isEmojiPickerVisible) { @@ -101,7 +101,7 @@ function MiniQuickEmojiReactions({reportAction, reportActionID, onEmojiSelected, fill={StyleUtils.getIconFillColor(getButtonState(hovered, pressed, false))} /> )} - + ); } diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index cdd0d26a13cb..4b66c0f80aa3 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -6,7 +6,7 @@ import {View} from 'react-native'; import type {GestureResponderEvent} from 'react-native'; import Animated, {Easing, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import * as ActionSheetAwareScrollView from '@components/ActionSheetAwareScrollView'; -import BaseMiniContextMenuItem from '@components/BaseMiniContextMenuItem'; +import MiniContextMenuItem from '@components/MiniContextMenuItem'; import Icon from '@components/Icon'; import MiniQuickEmojiReactions from '@components/Reactions/MiniQuickEmojiReactions'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -182,7 +182,7 @@ function MiniReportActionContextMenu() { /> )} {visibleActions.map((action: ActionDescriptor) => ( - )} - + ))} {!!(needsOverflow && overflowMenu) && (() => { const {buttonRef, text, onPress, sentryLabel, icon} = overflowMenu; return ( - )} - + ); })()} From 16975a013026d4c8ad1aacc777176dfb42f6a5d4 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 2 Mar 2026 16:14:06 -0800 Subject: [PATCH 26/88] refactor(contextmenu): decompose into factory functions and type-specific components Convert action hooks to pure factory functions, decompose the monolithic PopoverReportActionContextMenu into a shell + 5 type-specific content components, and replace useContextMenuData with type-specific hooks to eliminate unnecessary Onyx subscriptions for simpler menu types. Made-with: Cursor --- src/Expensify.tsx | 4 +- .../MiniReportActionContextMenu/index.tsx | 171 +++++++----- ...ContextMenu.tsx => PopoverContextMenu.tsx} | 248 +++++++----------- .../ContextMenu/PopoverEmailContent.tsx | 66 +++++ .../report/ContextMenu/PopoverLinkContent.tsx | 64 +++++ .../PopoverReportActionContent.tsx | 209 +++++++++++++++ .../ContextMenu/PopoverReportContent.tsx | 127 +++++++++ .../report/ContextMenu/PopoverTextContent.tsx | 63 +++++ .../ContextMenu/actions/ContextMenuAction.ts | 92 +++---- .../report/ContextMenu/actions/MarkAsRead.tsx | 29 -- .../ContextMenu/actions/MarkAsUnread.tsx | 29 -- .../inbox/report/ContextMenu/actions/Pin.tsx | 28 -- .../report/ContextMenu/actions/Unpin.tsx | 28 -- .../ContextMenu/actions/actionConfig.ts | 17 +- .../actionTypes.ts} | 60 +++-- .../{CopyEmail.tsx => copyEmailAction.ts} | 18 +- .../{CopyLink.tsx => copyLinkAction.ts} | 18 +- .../{CopyMessage.tsx => copyMessageAction.ts} | 19 +- ...CopyOnyxData.tsx => copyOnyxDataAction.ts} | 18 +- ...Clipboard.tsx => copyToClipboardAction.ts} | 18 +- .../actions/{CopyURL.tsx => copyURLAction.ts} | 18 +- .../actions/{Debug.tsx => debugAction.ts} | 14 +- .../actions/{Delete.tsx => deleteAction.ts} | 14 +- .../{Download.tsx => downloadAction.ts} | 18 +- .../actions/{Edit.tsx => editAction.ts} | 14 +- ...ojiReaction.tsx => emojiReactionAction.ts} | 13 +- .../actions/{Explain.tsx => explainAction.ts} | 14 +- ...Offensive.tsx => flagAsOffensiveAction.ts} | 14 +- .../actions/{Hold.tsx => holdAction.ts} | 14 +- .../{JoinThread.tsx => joinThreadAction.ts} | 14 +- .../{LeaveThread.tsx => leaveThreadAction.ts} | 14 +- .../ContextMenu/actions/markAsReadAction.ts | 25 ++ .../ContextMenu/actions/markAsUnreadAction.ts | 25 ++ ...OverflowMenu.tsx => overflowMenuAction.ts} | 19 +- .../report/ContextMenu/actions/pinAction.ts | 24 ++ ...plyInThread.tsx => replyInThreadAction.ts} | 16 +- .../actions/{Unhold.tsx => unholdAction.ts} | 14 +- .../report/ContextMenu/actions/unpinAction.ts | 24 ++ .../ContextMenu/useContextMenuActions.ts | 82 ------ ...a.ts => useReportActionContextMenuData.ts} | 23 +- .../ContextMenu/useReportContextMenuData.ts | 142 ++++++++++ 41 files changed, 1199 insertions(+), 682 deletions(-) rename src/pages/inbox/report/ContextMenu/{PopoverReportActionContextMenu.tsx => PopoverContextMenu.tsx} (62%) create mode 100644 src/pages/inbox/report/ContextMenu/PopoverEmailContent.tsx create mode 100644 src/pages/inbox/report/ContextMenu/PopoverLinkContent.tsx create mode 100644 src/pages/inbox/report/ContextMenu/PopoverReportActionContent.tsx create mode 100644 src/pages/inbox/report/ContextMenu/PopoverReportContent.tsx create mode 100644 src/pages/inbox/report/ContextMenu/PopoverTextContent.tsx delete mode 100644 src/pages/inbox/report/ContextMenu/actions/MarkAsRead.tsx delete mode 100644 src/pages/inbox/report/ContextMenu/actions/MarkAsUnread.tsx delete mode 100644 src/pages/inbox/report/ContextMenu/actions/Pin.tsx delete mode 100644 src/pages/inbox/report/ContextMenu/actions/Unpin.tsx rename src/pages/inbox/report/ContextMenu/{ContextMenuPayloadProvider.tsx => actions/actionTypes.ts} (75%) rename src/pages/inbox/report/ContextMenu/actions/{CopyEmail.tsx => copyEmailAction.ts} (57%) rename src/pages/inbox/report/ContextMenu/actions/{CopyLink.tsx => copyLinkAction.ts} (61%) rename src/pages/inbox/report/ContextMenu/actions/{CopyMessage.tsx => copyMessageAction.ts} (97%) rename src/pages/inbox/report/ContextMenu/actions/{CopyOnyxData.tsx => copyOnyxDataAction.ts} (52%) rename src/pages/inbox/report/ContextMenu/actions/{CopyToClipboard.tsx => copyToClipboardAction.ts} (51%) rename src/pages/inbox/report/ContextMenu/actions/{CopyURL.tsx => copyURLAction.ts} (53%) rename src/pages/inbox/report/ContextMenu/actions/{Debug.tsx => debugAction.ts} (61%) rename src/pages/inbox/report/ContextMenu/actions/{Delete.tsx => deleteAction.ts} (62%) rename src/pages/inbox/report/ContextMenu/actions/{Download.tsx => downloadAction.ts} (71%) rename src/pages/inbox/report/ContextMenu/actions/{Edit.tsx => editAction.ts} (70%) rename src/pages/inbox/report/ContextMenu/actions/{EmojiReaction.tsx => emojiReactionAction.ts} (74%) rename src/pages/inbox/report/ContextMenu/actions/{Explain.tsx => explainAction.ts} (58%) rename src/pages/inbox/report/ContextMenu/actions/{FlagAsOffensive.tsx => flagAsOffensiveAction.ts} (56%) rename src/pages/inbox/report/ContextMenu/actions/{Hold.tsx => holdAction.ts} (55%) rename src/pages/inbox/report/ContextMenu/actions/{JoinThread.tsx => joinThreadAction.ts} (62%) rename src/pages/inbox/report/ContextMenu/actions/{LeaveThread.tsx => leaveThreadAction.ts} (62%) create mode 100644 src/pages/inbox/report/ContextMenu/actions/markAsReadAction.ts create mode 100644 src/pages/inbox/report/ContextMenu/actions/markAsUnreadAction.ts rename src/pages/inbox/report/ContextMenu/actions/{OverflowMenu.tsx => overflowMenuAction.ts} (52%) create mode 100644 src/pages/inbox/report/ContextMenu/actions/pinAction.ts rename src/pages/inbox/report/ContextMenu/actions/{ReplyInThread.tsx => replyInThreadAction.ts} (52%) rename src/pages/inbox/report/ContextMenu/actions/{Unhold.tsx => unholdAction.ts} (55%) create mode 100644 src/pages/inbox/report/ContextMenu/actions/unpinAction.ts delete mode 100644 src/pages/inbox/report/ContextMenu/useContextMenuActions.ts rename src/pages/inbox/report/ContextMenu/{useContextMenuData.ts => useReportActionContextMenuData.ts} (91%) create mode 100644 src/pages/inbox/report/ContextMenu/useReportContextMenuData.ts diff --git a/src/Expensify.tsx b/src/Expensify.tsx index 4a397b68801f..6f865f3f5a65 100644 --- a/src/Expensify.tsx +++ b/src/Expensify.tsx @@ -44,7 +44,7 @@ import {cleanupMemoryTrackingTelemetry, initializeMemoryTrackingTelemetry} from import './libs/UnreadIndicatorUpdater'; import Visibility from './libs/Visibility'; import ONYXKEYS from './ONYXKEYS'; -import PopoverReportActionContextMenu from './pages/inbox/report/ContextMenu/PopoverReportActionContextMenu'; +import PopoverContextMenu from './pages/inbox/report/ContextMenu/PopoverContextMenu'; import * as ReportActionContextMenu from './pages/inbox/report/ContextMenu/ReportActionContextMenu'; import PriorityModeHandler from './PriorityModeHandler'; import type {Route} from './ROUTES'; @@ -292,7 +292,7 @@ function Expensify() { <> - + {/* We include the modal for showing a new update at the top level so the option is always present. */} diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index 4b66c0f80aa3..57c7b983b57f 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -6,21 +6,39 @@ import {View} from 'react-native'; import type {GestureResponderEvent} from 'react-native'; import Animated, {Easing, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import * as ActionSheetAwareScrollView from '@components/ActionSheetAwareScrollView'; -import MiniContextMenuItem from '@components/MiniContextMenuItem'; import Icon from '@components/Icon'; +import MiniContextMenuItem from '@components/MiniContextMenuItem'; import MiniQuickEmojiReactions from '@components/Reactions/MiniQuickEmojiReactions'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import getButtonState from '@libs/getButtonState'; +import type {ActionID} from '@pages/inbox/report/ContextMenu/actions/actionConfig'; import type {ActionDescriptor} from '@pages/inbox/report/ContextMenu/actions/ActionDescriptor'; -import {useEmojiReactionData, useOverflowMenuAction} from '@pages/inbox/report/ContextMenu/actions/ContextMenuAction'; -import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; -import {ContextMenuPayloadContext} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; +import {CONTEXT_MENU_ICON_NAMES} from '@pages/inbox/report/ContextMenu/actions/actionTypes'; +import type {ContextMenuPayload} from '@pages/inbox/report/ContextMenu/actions/actionTypes'; +import { + createCopyLinkAction, + createCopyMessageAction, + createDeleteAction, + createDownloadAction, + createEditAction, + createEmojiReactionData, + createExplainAction, + createFlagAsOffensiveAction, + createHoldAction, + createJoinThreadAction, + createLeaveThreadAction, + createMarkAsReadAction, + createMarkAsUnreadAction, + createOverflowMenuAction, + createReplyInThreadAction, + createUnholdAction, +} from '@pages/inbox/report/ContextMenu/actions/ContextMenuAction'; import {useMiniContextMenuActions, useMiniContextMenuState} from '@pages/inbox/report/ContextMenu/MiniContextMenuProvider'; import type {ContextMenuAnchor} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {showContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; -import useContextMenuActions from '@pages/inbox/report/ContextMenu/useContextMenuActions'; -import useContextMenuData from '@pages/inbox/report/ContextMenu/useContextMenuData'; +import useReportActionContextMenuData from '@pages/inbox/report/ContextMenu/useReportActionContextMenuData'; import CONST from '@src/CONST'; const SLIDE_DURATION = 200; @@ -34,6 +52,9 @@ function MiniReportActionContextMenu() { const StyleUtils = useStyleUtils(); const {transitionActionSheetState} = ActionSheetAwareScrollView.useActionSheetAwareScrollViewActions(); + const icons = useMemoizedLazyExpensifyIcons(CONTEXT_MENU_ICON_NAMES); + const threeDotRef = useRef(null); + const isVisible = state?.isVisible ?? false; const wasVisibleRef = useRef(false); @@ -78,7 +99,7 @@ function MiniReportActionContextMenu() { right: baseRight.get(), })); - const data = useContextMenuData({ + const data = useReportActionContextMenuData({ reportID: state?.reportID, reportActionID: state?.reportActionID, originalReportID: state?.originalReportID, @@ -121,8 +142,7 @@ function MiniReportActionContextMenu() { }); }; - // eslint-disable-next-line react/jsx-no-constructed-context-values - const payloadValue: ContextMenuPayloadContextValue = { + const payload: ContextMenuPayload = { ...data, // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style reportAction: (data.reportAction ?? null) as NonNullable, @@ -135,9 +155,30 @@ function MiniReportActionContextMenu() { setIsEmojiPickerActive: state?.setIsEmojiPickerActive, }; - const actions = useContextMenuActions(visibleActionIDs, payloadValue); - const emojiData = useEmojiReactionData(payloadValue); - const overflowMenu = useOverflowMenuAction(payloadValue); + const params = {payload, icons}; + + /* eslint-disable react-hooks/refs -- factory functions store refs for later use, they don't read .current during render */ + const allActions: ActionDescriptor[] = [ + createReplyInThreadAction(params), + createMarkAsUnreadAction(params), + createExplainAction(params), + createMarkAsReadAction(params), + createEditAction(params), + createUnholdAction(params), + createHoldAction(params), + createJoinThreadAction(params), + createLeaveThreadAction(params), + createCopyMessageAction(params), + createCopyLinkAction(params), + createFlagAsOffensiveAction(params), + createDownloadAction(params), + createDeleteAction(params), + ]; + + const actions = allActions.filter((action) => visibleActionIDs.has(action.id as ActionID)); + const emojiData = createEmojiReactionData(payload); + const overflowMenu = createOverflowMenuAction(params, threeDotRef); + /* eslint-enable react-hooks/refs */ const hasEmoji = visibleActionIDs.has('emojiReaction') && !!emojiData.reportAction && !!emojiData.reportActionID; const needsOverflow = actions.length > CONST.MINI_CONTEXT_MENU_MAX_ITEMS; @@ -168,60 +209,58 @@ function MiniReportActionContextMenu() { }} > - - - {hasEmoji && !!emojiData.reportAction && !!emojiData.reportActionID && ( - - emojiData.interceptAnonymousUser(() => emojiData.toggleEmojiAndCloseMenu(emoji, existingReactions, preferredSkinTone)) - } - onPressOpenPicker={emojiData.onPressOpenPicker} - onEmojiPickerClosed={emojiData.onEmojiPickerClosed} - reportActionID={emojiData.reportActionID} - reportAction={emojiData.reportAction} - /> - )} - {visibleActions.map((action: ActionDescriptor) => ( - - {({hovered, pressed}) => ( - - )} - - ))} - {!!(needsOverflow && overflowMenu) && - (() => { - const {buttonRef, text, onPress, sentryLabel, icon} = overflowMenu; - return ( - - {({hovered, pressed}) => ( - - )} - - ); - })()} - - + + {hasEmoji && !!emojiData.reportAction && !!emojiData.reportActionID && ( + + emojiData.interceptAnonymousUser(() => emojiData.toggleEmojiAndCloseMenu(emoji, existingReactions, preferredSkinTone)) + } + onPressOpenPicker={emojiData.onPressOpenPicker} + onEmojiPickerClosed={emojiData.onEmojiPickerClosed} + reportActionID={emojiData.reportActionID} + reportAction={emojiData.reportAction} + /> + )} + {visibleActions.map((action: ActionDescriptor) => ( + + {({hovered, pressed}) => ( + + )} + + ))} + {!!(needsOverflow && overflowMenu) && + (() => { + const {buttonRef, text, onPress, sentryLabel, icon} = overflowMenu; + return ( + + {({hovered, pressed}) => ( + + )} + + ); + })()} +
, document.body, diff --git a/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu.tsx similarity index 62% rename from src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx rename to src/pages/inbox/report/ContextMenu/PopoverContextMenu.tsx index fabbda3b5edc..633411d1567a 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu.tsx @@ -1,35 +1,24 @@ -import type {ForwardedRef, RefObject} from 'react'; -import React, {useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; +import type {RefObject} from 'react'; +import React, {useEffect, useImperativeHandle, useRef, useState} from 'react'; /* eslint-disable no-restricted-imports */ -import type {EmitterSubscription, GestureResponderEvent, NativeTouchEvent, Text as RNText, View as ViewType} from 'react-native'; -import {Dimensions, View} from 'react-native'; +import type {EmitterSubscription, GestureResponderEvent, NativeTouchEvent, View as ViewType} from 'react-native'; +import {Dimensions} from 'react-native'; import {Actions, useActionSheetAwareScrollViewActions} from '@components/ActionSheetAwareScrollView'; -import FocusableMenuItem from '@components/FocusableMenuItem'; -import FocusTrapForModal from '@components/FocusTrap/FocusTrapForModal'; import {ModalActions, useModal} from '@components/Modal/Global/ModalContext'; import PopoverWithMeasuredContent from '@components/PopoverWithMeasuredContent'; -import QuickEmojiReactions from '@components/Reactions/QuickEmojiReactions'; -import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useRestoreInputFocus from '@hooks/useRestoreInputFocus'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; import calculateAnchorPosition from '@libs/calculateAnchorPosition'; import refocusComposerAfterPreventFirstResponder from '@libs/refocusComposerAfterPreventFirstResponder'; import type {ComposerType} from '@libs/ReportActionComposeFocusManager'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import CONST from '@src/CONST'; -import {ORDERED_ACTION_SHOULD_SHOW} from './actions/actionConfig'; -import type {ActionDescriptor} from './actions/ActionDescriptor'; -import {useEmojiReactionData} from './actions/ContextMenuAction'; import ConfirmDeleteReportActionModal from './ConfirmDeleteReportActionModal'; -import type {ContextMenuPayloadContextValue} from './ContextMenuPayloadProvider'; -import {ContextMenuPayloadContext} from './ContextMenuPayloadProvider'; +import PopoverEmailContent from './PopoverEmailContent'; +import PopoverLinkContent from './PopoverLinkContent'; +import PopoverReportActionContent from './PopoverReportActionContent'; +import PopoverReportContent from './PopoverReportContent'; +import PopoverTextContent from './PopoverTextContent'; import type {ContextMenuAnchor, ContextMenuType, ReportActionContextMenu} from './ReportActionContextMenu'; -import {showContextMenu} from './ReportActionContextMenu'; -import useContextMenuActions from './useContextMenuActions'; -import useContextMenuData from './useContextMenuData'; function extractPointerEvent(event: GestureResponderEvent | MouseEvent): MouseEvent | NativeTouchEvent { if ('nativeEvent' in event) { @@ -59,18 +48,22 @@ type PopoverContextMenuState = { onEmojiPickerToggle: ((state: boolean) => void) | undefined; }; -type PopoverReportActionContextMenuProps = { - ref?: ForwardedRef; +type PopoverContentProps = { + menuState: PopoverContextMenuState; + hideAndRun: (callback?: () => void) => void; + setLocalShouldKeepOpen: (value: boolean) => void; + transitionActionSheetState: (params: {type: string; payload?: Record}) => void; + contentRef: RefObject; + shouldEnableArrowNavigation: boolean; }; -function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuProps) { +type PopoverContextMenuProps = { + ref?: React.Ref; +}; + +function PopoverContextMenu({ref: forwardedRef}: PopoverContextMenuProps) { const {transitionActionSheetState} = useActionSheetAwareScrollViewActions(); const modalContext = useModal(); - // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth - const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); - const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); - const {windowWidth} = useWindowDimensions(); const [menuState, setMenuState] = useState(null); const [isPopoverVisible, setIsPopoverVisible] = useState(false); @@ -326,7 +319,7 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro }); }; - useImperativeHandle(ref, () => ({ + useImperativeHandle(forwardedRef, () => ({ showContextMenu: showContextMenuHandler, hideContextMenu: hideContextMenuHandler, showDeleteModal, @@ -340,85 +333,89 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro composerToRefocusOnCloseEmojiPicker: composerToRefocusOnClose, })); - const data = useContextMenuData({ - reportID: menuState?.reportID, - reportActionID: menuState?.reportActionID, - originalReportID: menuState?.originalReportID, - draftMessage: menuState?.draftMessage ?? '', - selection: menuState?.selection ?? '', - type: menuState?.type ?? CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, - anchor: {current: menuState?.contextMenuTargetNode ?? null}, - }); - - const visibleActionIDs = useMemo(() => new Set(data.getVisibleActionIDs()), [data]); - const hideAndRun = (callback?: () => void) => { import('@pages/inbox/report/ContextMenu/ReportActionContextMenu').then(({hideContextMenu: hideCtx}) => { hideCtx(false, callback); }); }; - const openOverflowMenu = (event: GestureResponderEvent | MouseEvent, anchorRefParam: RefObject) => { - showContextMenu({ - type: CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, - event, - selection: menuState?.selection ?? '', - contextMenuAnchor: anchorRefParam?.current as ViewType | RNText | null, - report: { - reportID: menuState?.reportID, - originalReportID: menuState?.originalReportID, - }, - reportAction: { - reportActionID: data.reportAction?.reportActionID, - draftMessage: menuState?.draftMessage, - }, - callbacks: { - onShow: undefined, - onHide: () => { - setLocalShouldKeepOpen(false); - }, - }, - shouldCloseOnTarget: true, - isOverflowMenu: true, - }); - }; - - // eslint-disable-next-line react/jsx-no-constructed-context-values - const payloadValue: ContextMenuPayloadContextValue = { - ...data, - // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style - reportAction: (data.reportAction ?? null) as NonNullable, - currentUserAccountID: data.currentUserPersonalDetails?.accountID, - close: () => setLocalShouldKeepOpen(false), - hideAndRun, - transitionActionSheetState, - openContextMenu: () => setLocalShouldKeepOpen(true), - openOverflowMenu, - setIsEmojiPickerActive: menuState?.onEmojiPickerToggle, - }; - - const actions = useContextMenuActions(visibleActionIDs, payloadValue); - const emojiData = useEmojiReactionData(payloadValue); - const shouldKeepOpen = localShouldKeepOpen; const shouldEnableArrowNavigation = isPopoverVisible || shouldKeepOpen; - const contentActionIndexes = actions - .map((action, index) => { - const entry = ORDERED_ACTION_SHOULD_SHOW.find((e) => e.id === action.id); - return entry?.isContentAction ? index : undefined; - }) - .filter((index): index is number => index !== undefined); - - const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({ - initialFocusedIndex: -1, - disabledIndexes: contentActionIndexes, - maxIndex: actions.length - 1, - isActive: shouldEnableArrowNavigation, - }); - - const hasEmoji = visibleActionIDs.has('emojiReaction'); - const wrapperStyle = StyleUtils.getReportActionContextMenuStyles(false, shouldUseNarrowLayout); + const renderContent = () => { + if (!menuState) { + return null; + } + const contentProps: PopoverContentProps = { + menuState, + hideAndRun, + setLocalShouldKeepOpen, + transitionActionSheetState, + contentRef, + shouldEnableArrowNavigation, + }; + if (menuState.type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION) { + return ( + + ); + } + if (menuState.type === CONST.CONTEXT_MENU_TYPES.REPORT) { + return ( + + ); + } + if (menuState.type === CONST.CONTEXT_MENU_TYPES.LINK) { + return ( + + ); + } + if (menuState.type === CONST.CONTEXT_MENU_TYPES.EMAIL) { + return ( + + ); + } + if (menuState.type === CONST.CONTEXT_MENU_TYPES.TEXT) { + return ( + + ); + } + return null; + }; return ( - - - - - {hasEmoji && emojiData.reportActionID != null && ( - - emojiData.interceptAnonymousUser(() => emojiData.toggleEmojiAndCloseMenu(emoji, existingReactions, preferredSkinTone)) - } - reportActionID={emojiData.reportActionID} - reportAction={emojiData.reportAction} - setIsEmojiPickerActive={(active) => { - if (!active) { - return; - } - setLocalShouldKeepOpen(true); - }} - /> - )} - {actions.map((action: ActionDescriptor, i: number) => ( - setFocusedIndex(i)} - onBlur={() => (i === actions.length - 1 || i === 1) && setFocusedIndex(-1)} - disabled={action.disabled} - shouldShowLoadingSpinnerIcon={action.shouldShowLoadingSpinnerIcon} - sentryLabel={action.sentryLabel} - /> - ))} - - - - + {renderContent()} ); } -export default PopoverReportActionContextMenu; +PopoverContextMenu.displayName = 'PopoverContextMenu'; + +export default PopoverContextMenu; +export type {PopoverPosition, PopoverContextMenuState, PopoverContentProps}; diff --git a/src/pages/inbox/report/ContextMenu/PopoverEmailContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverEmailContent.tsx new file mode 100644 index 000000000000..4789a144c0a6 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/PopoverEmailContent.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import {InteractionManager, View} from 'react-native'; +import ContextMenuItem from '@components/ContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Clipboard from '@libs/Clipboard'; +import EmailUtils from '@libs/EmailUtils'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {isAnonymousUser, signOutAndRedirectToSignIn} from '@userActions/Session'; +import CONST from '@src/CONST'; +import type {PopoverContentProps} from './PopoverContextMenu'; +import {hideContextMenu} from './ReportActionContextMenu'; + +function PopoverEmailContent({menuState, contentRef}: PopoverContentProps) { + const {translate} = useLocalize(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const icons = useMemoizedLazyExpensifyIcons(['Copy', 'Checkmark'] as const); + + const interceptAnonymousUser = (callback: () => void, isAnonymousAction = false) => { + if (isAnonymousUser() && !isAnonymousAction) { + hideContextMenu(false); + // eslint-disable-next-line @typescript-eslint/no-deprecated + InteractionManager.runAfterInteractions(() => { + signOutAndRedirectToSignIn(); + }); + } else { + callback(); + } + }; + + const handlePress = () => { + interceptAnonymousUser(() => { + Clipboard.setString(EmailUtils.trimMailTo(menuState.selection)); + hideContextMenu(true, ReportActionComposeFocusManager.focus); + }, true); + }; + + const wrapperStyle = StyleUtils.getReportActionContextMenuStyles(false, shouldUseNarrowLayout); + const description = EmailUtils.prefixMailSeparatorsWithBreakOpportunities(EmailUtils.trimMailTo(menuState.selection ?? '')); + + return ( + + + + ); +} + +export default PopoverEmailContent; diff --git a/src/pages/inbox/report/ContextMenu/PopoverLinkContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverLinkContent.tsx new file mode 100644 index 000000000000..ae6f5b9bc3a2 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/PopoverLinkContent.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import {InteractionManager, View} from 'react-native'; +import ContextMenuItem from '@components/ContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Clipboard from '@libs/Clipboard'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {isAnonymousUser, signOutAndRedirectToSignIn} from '@userActions/Session'; +import CONST from '@src/CONST'; +import type {PopoverContentProps} from './PopoverContextMenu'; +import {hideContextMenu} from './ReportActionContextMenu'; + +function PopoverLinkContent({menuState, contentRef}: PopoverContentProps) { + const {translate} = useLocalize(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const icons = useMemoizedLazyExpensifyIcons(['Copy', 'Checkmark'] as const); + + const interceptAnonymousUser = (callback: () => void, isAnonymousAction = false) => { + if (isAnonymousUser() && !isAnonymousAction) { + hideContextMenu(false); + // eslint-disable-next-line @typescript-eslint/no-deprecated + InteractionManager.runAfterInteractions(() => { + signOutAndRedirectToSignIn(); + }); + } else { + callback(); + } + }; + + const handlePress = () => { + interceptAnonymousUser(() => { + Clipboard.setString(menuState.selection); + hideContextMenu(true, ReportActionComposeFocusManager.focus); + }, true); + }; + + const wrapperStyle = StyleUtils.getReportActionContextMenuStyles(false, shouldUseNarrowLayout); + + return ( + + + + ); +} + +export default PopoverLinkContent; diff --git a/src/pages/inbox/report/ContextMenu/PopoverReportActionContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverReportActionContent.tsx new file mode 100644 index 000000000000..205df712cb7c --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/PopoverReportActionContent.tsx @@ -0,0 +1,209 @@ +import type {RefObject} from 'react'; +import React, {useMemo, useRef} from 'react'; +import {View} from 'react-native'; +// eslint-disable-next-line no-restricted-imports +import type {GestureResponderEvent, Text as RNText, View as ViewType} from 'react-native'; +import FocusableMenuItem from '@components/FocusableMenuItem'; +import FocusTrapForModal from '@components/FocusTrap/FocusTrapForModal'; +import QuickEmojiReactions from '@components/Reactions/QuickEmojiReactions'; +import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import CONST from '@src/CONST'; +import type {ActionID} from './actions/actionConfig'; +import {ORDERED_ACTION_SHOULD_SHOW} from './actions/actionConfig'; +import type {ActionDescriptor} from './actions/ActionDescriptor'; +import {CONTEXT_MENU_ICON_NAMES} from './actions/actionTypes'; +import type {ContextMenuPayload} from './actions/actionTypes'; +import { + createCopyEmailAction, + createCopyLinkAction, + createCopyMessageAction, + createCopyOnyxDataAction, + createCopyToClipboardAction, + createCopyURLAction, + createDebugAction, + createDeleteAction, + createDownloadAction, + createEditAction, + createEmojiReactionData, + createExplainAction, + createFlagAsOffensiveAction, + createHoldAction, + createJoinThreadAction, + createLeaveThreadAction, + createMarkAsReadAction, + createMarkAsUnreadAction, + createOverflowMenuAction, + createPinAction, + createReplyInThreadAction, + createUnholdAction, + createUnpinAction, +} from './actions/ContextMenuAction'; +import type {PopoverContentProps} from './PopoverContextMenu'; +import {showContextMenu} from './ReportActionContextMenu'; +import useReportActionContextMenuData from './useReportActionContextMenuData'; + +function PopoverReportActionContent({menuState, hideAndRun, setLocalShouldKeepOpen, transitionActionSheetState, contentRef, shouldEnableArrowNavigation}: PopoverContentProps) { + const {shouldUseNarrowLayout} = useResponsiveLayout(); + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const {windowWidth} = useWindowDimensions(); + const overflowMenuRef = useRef(null); + + const icons = useMemoizedLazyExpensifyIcons(CONTEXT_MENU_ICON_NAMES); + + const data = useReportActionContextMenuData({ + reportID: menuState.reportID, + reportActionID: menuState.reportActionID, + originalReportID: menuState.originalReportID, + draftMessage: menuState.draftMessage ?? '', + selection: menuState.selection ?? '', + type: CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, + anchor: {current: menuState.contextMenuTargetNode ?? null}, + }); + + const visibleActionIDs = useMemo(() => new Set(data.getVisibleActionIDs()), [data]); + + const openOverflowMenu = (event: GestureResponderEvent | MouseEvent, anchorRefParam: RefObject) => { + showContextMenu({ + type: CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, + event, + selection: menuState.selection ?? '', + contextMenuAnchor: anchorRefParam?.current as ViewType | RNText | null, + report: { + reportID: menuState.reportID, + originalReportID: menuState.originalReportID, + }, + reportAction: { + reportActionID: data.reportAction?.reportActionID, + draftMessage: menuState.draftMessage, + }, + callbacks: { + onShow: undefined, + onHide: () => { + setLocalShouldKeepOpen(false); + }, + }, + shouldCloseOnTarget: true, + isOverflowMenu: true, + }); + }; + + const payload: ContextMenuPayload = { + ...data, + // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style + reportAction: (data.reportAction ?? null) as NonNullable, + currentUserAccountID: data.currentUserPersonalDetails?.accountID ?? 0, + close: () => setLocalShouldKeepOpen(false), + hideAndRun, + transitionActionSheetState, + openContextMenu: () => setLocalShouldKeepOpen(true), + openOverflowMenu, + setIsEmojiPickerActive: menuState.onEmojiPickerToggle, + }; + + const params = {payload, icons}; + + /* eslint-disable react-hooks/refs -- factory functions store refs for later use, they don't read .current during render */ + const allActions: ActionDescriptor[] = [ + createReplyInThreadAction(params), + createMarkAsUnreadAction(params), + createExplainAction(params), + createMarkAsReadAction(params), + createEditAction(params), + createUnholdAction(params), + createHoldAction(params), + createJoinThreadAction(params), + createLeaveThreadAction(params), + createCopyURLAction(params), + createCopyToClipboardAction(params), + createCopyEmailAction(params), + createCopyMessageAction(params), + createCopyLinkAction(params), + createPinAction(params), + createUnpinAction(params), + createFlagAsOffensiveAction(params), + createDownloadAction(params), + createCopyOnyxDataAction(params), + createDebugAction(params), + createDeleteAction(params), + ]; + + const overflowMenu = createOverflowMenuAction(params, overflowMenuRef); + const actionsWithOverflow = [...allActions, overflowMenu]; + const actions = actionsWithOverflow.filter((action) => visibleActionIDs.has(action.id as ActionID)); + + const emojiData = createEmojiReactionData(payload); + /* eslint-enable react-hooks/refs */ + + const contentActionIndexes = actions + .map((action, index) => { + const entry = ORDERED_ACTION_SHOULD_SHOW.find((e) => e.id === action.id); + return entry?.isContentAction ? index : undefined; + }) + .filter((index): index is number => index !== undefined); + + const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({ + initialFocusedIndex: -1, + disabledIndexes: contentActionIndexes, + maxIndex: actions.length - 1, + isActive: shouldEnableArrowNavigation, + }); + + const hasEmoji = visibleActionIDs.has('emojiReaction'); + const wrapperStyle = StyleUtils.getReportActionContextMenuStyles(false, shouldUseNarrowLayout); + + return ( + + + + {hasEmoji && emojiData.reportActionID != null && ( + + emojiData.interceptAnonymousUser(() => emojiData.toggleEmojiAndCloseMenu(emoji, existingReactions, preferredSkinTone)) + } + reportActionID={emojiData.reportActionID} + reportAction={emojiData.reportAction} + setIsEmojiPickerActive={(active) => { + if (!active) { + return; + } + setLocalShouldKeepOpen(true); + }} + /> + )} + {actions.map((action: ActionDescriptor, i: number) => ( + setFocusedIndex(i)} + onBlur={() => (i === actions.length - 1 || i === 1) && setFocusedIndex(-1)} + disabled={action.disabled} + shouldShowLoadingSpinnerIcon={action.shouldShowLoadingSpinnerIcon} + sentryLabel={action.sentryLabel} + /> + ))} + + + + ); +} + +export default PopoverReportActionContent; diff --git a/src/pages/inbox/report/ContextMenu/PopoverReportContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverReportContent.tsx new file mode 100644 index 000000000000..500418e126b2 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/PopoverReportContent.tsx @@ -0,0 +1,127 @@ +import React, {useMemo} from 'react'; +import {View} from 'react-native'; +import FocusableMenuItem from '@components/FocusableMenuItem'; +import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import CONST from '@src/CONST'; +import type {ActionID} from './actions/actionConfig'; +import type {ActionDescriptor} from './actions/ActionDescriptor'; +import {CONTEXT_MENU_ICON_NAMES} from './actions/actionTypes'; +import type {ContextMenuPayload} from './actions/actionTypes'; +import {createCopyOnyxDataAction, createDebugAction, createMarkAsReadAction, createMarkAsUnreadAction, createPinAction, createUnpinAction} from './actions/ContextMenuAction'; +import type {PopoverContentProps} from './PopoverContextMenu'; +import useReportContextMenuData from './useReportContextMenuData'; + +function PopoverReportContent({menuState, hideAndRun, setLocalShouldKeepOpen, transitionActionSheetState, contentRef, shouldEnableArrowNavigation}: PopoverContentProps) { + const {shouldUseNarrowLayout} = useResponsiveLayout(); + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const {windowWidth} = useWindowDimensions(); + + const icons = useMemoizedLazyExpensifyIcons(CONTEXT_MENU_ICON_NAMES); + + const data = useReportContextMenuData({ + reportID: menuState.reportID, + reportActionID: menuState.reportActionID, + originalReportID: menuState.originalReportID, + draftMessage: menuState.draftMessage ?? '', + selection: menuState.selection ?? '', + type: CONST.CONTEXT_MENU_TYPES.REPORT, + anchor: {current: menuState.contextMenuTargetNode ?? null}, + }); + + const visibleActionIDs = useMemo(() => new Set(data.getVisibleActionIDs()), [data]); + + const payload = { + ...data, + reportAction: data.reportAction as NonNullable, + currentUserAccountID: 0, + currentUserPersonalDetails: undefined as unknown as ContextMenuPayload['currentUserPersonalDetails'], + encryptedAuthToken: '', + childReport: undefined, + childReportActions: undefined, + policy: undefined, + policyTags: undefined, + moneyRequestAction: undefined, + moneyRequestReport: undefined, + moneyRequestPolicy: undefined, + iouTransaction: undefined, + transaction: undefined, + card: undefined, + isThreadReportParentAction: false, + isHarvestReport: false, + isTryNewDotNVPDismissed: false, + isDelegateAccessRestricted: false, + areHoldRequirementsMet: false, + betas: undefined, + transactions: undefined, + introSelected: undefined, + movedFromReport: undefined, + movedToReport: undefined, + harvestReport: undefined, + download: undefined, + close: () => setLocalShouldKeepOpen(false), + hideAndRun, + transitionActionSheetState, + openContextMenu: () => setLocalShouldKeepOpen(true), + openOverflowMenu: () => {}, + setIsEmojiPickerActive: undefined, + showDelegateNoAccessModal: undefined, + } satisfies ContextMenuPayload; + + const params = {payload, icons}; + + const allActions: ActionDescriptor[] = [ + createMarkAsReadAction(params), + createMarkAsUnreadAction(params), + createPinAction(params), + createUnpinAction(params), + createCopyOnyxDataAction(params), + createDebugAction(params), + ]; + + const actions = allActions.filter((action) => visibleActionIDs.has(action.id as ActionID)); + + const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({ + initialFocusedIndex: -1, + disabledIndexes: [], + maxIndex: actions.length - 1, + isActive: shouldEnableArrowNavigation, + }); + + const wrapperStyle = StyleUtils.getReportActionContextMenuStyles(false, shouldUseNarrowLayout); + + return ( + + {actions.map((action: ActionDescriptor, i: number) => ( + setFocusedIndex(i)} + onBlur={() => (i === actions.length - 1 || i === 1) && setFocusedIndex(-1)} + disabled={action.disabled} + shouldShowLoadingSpinnerIcon={action.shouldShowLoadingSpinnerIcon} + sentryLabel={action.sentryLabel} + /> + ))} + + ); +} + +export default PopoverReportContent; diff --git a/src/pages/inbox/report/ContextMenu/PopoverTextContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverTextContent.tsx new file mode 100644 index 000000000000..d1e6caed2b1b --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/PopoverTextContent.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import {InteractionManager, View} from 'react-native'; +import ContextMenuItem from '@components/ContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Clipboard from '@libs/Clipboard'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {isAnonymousUser, signOutAndRedirectToSignIn} from '@userActions/Session'; +import CONST from '@src/CONST'; +import type {PopoverContentProps} from './PopoverContextMenu'; +import {hideContextMenu} from './ReportActionContextMenu'; + +function PopoverTextContent({menuState, contentRef}: PopoverContentProps) { + const {translate} = useLocalize(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const icons = useMemoizedLazyExpensifyIcons(['Copy', 'Checkmark'] as const); + + const interceptAnonymousUser = (callback: () => void, isAnonymousAction = false) => { + if (isAnonymousUser() && !isAnonymousAction) { + hideContextMenu(false); + // eslint-disable-next-line @typescript-eslint/no-deprecated + InteractionManager.runAfterInteractions(() => { + signOutAndRedirectToSignIn(); + }); + } else { + callback(); + } + }; + + const handlePress = () => { + interceptAnonymousUser(() => { + Clipboard.setString(menuState.selection); + hideContextMenu(true, ReportActionComposeFocusManager.focus); + }, true); + }; + + const wrapperStyle = StyleUtils.getReportActionContextMenuStyles(false, shouldUseNarrowLayout); + + return ( + + + + ); +} + +export default PopoverTextContent; diff --git a/src/pages/inbox/report/ContextMenu/actions/ContextMenuAction.ts b/src/pages/inbox/report/ContextMenu/actions/ContextMenuAction.ts index 0862a705ca61..0a13da635c72 100644 --- a/src/pages/inbox/report/ContextMenu/actions/ContextMenuAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/ContextMenuAction.ts @@ -1,49 +1,49 @@ -import useCopyEmailAction from './CopyEmail'; -import useCopyLinkAction from './CopyLink'; -import useCopyMessageAction from './CopyMessage'; -import useCopyOnyxDataAction from './CopyOnyxData'; -import useCopyToClipboardAction from './CopyToClipboard'; -import useCopyURLAction from './CopyURL'; -import useDebugAction from './Debug'; -import useDeleteAction from './Delete'; -import useDownloadAction from './Download'; -import useEditAction from './Edit'; -import useEmojiReactionData from './EmojiReaction'; -import useExplainAction from './Explain'; -import useFlagAsOffensiveAction from './FlagAsOffensive'; -import useHoldAction from './Hold'; -import useJoinThreadAction from './JoinThread'; -import useLeaveThreadAction from './LeaveThread'; -import useMarkAsReadAction from './MarkAsRead'; -import useMarkAsUnreadAction from './MarkAsUnread'; -import useOverflowMenuAction from './OverflowMenu'; -import usePinAction from './Pin'; -import useReplyInThreadAction from './ReplyInThread'; -import useUnholdAction from './Unhold'; -import useUnpinAction from './Unpin'; +import createCopyEmailAction from './copyEmailAction'; +import createCopyLinkAction from './copyLinkAction'; +import createCopyMessageAction from './copyMessageAction'; +import createCopyOnyxDataAction from './copyOnyxDataAction'; +import createCopyToClipboardAction from './copyToClipboardAction'; +import createCopyURLAction from './copyURLAction'; +import createDebugAction from './debugAction'; +import createDeleteAction from './deleteAction'; +import createDownloadAction from './downloadAction'; +import createEditAction from './editAction'; +import createEmojiReactionData from './emojiReactionAction'; +import createExplainAction from './explainAction'; +import createFlagAsOffensiveAction from './flagAsOffensiveAction'; +import createHoldAction from './holdAction'; +import createJoinThreadAction from './joinThreadAction'; +import createLeaveThreadAction from './leaveThreadAction'; +import createMarkAsReadAction from './markAsReadAction'; +import createMarkAsUnreadAction from './markAsUnreadAction'; +import createOverflowMenuAction from './overflowMenuAction'; +import createPinAction from './pinAction'; +import createReplyInThreadAction from './replyInThreadAction'; +import createUnholdAction from './unholdAction'; +import createUnpinAction from './unpinAction'; export { - useEmojiReactionData, - useReplyInThreadAction, - useMarkAsUnreadAction, - useExplainAction, - useMarkAsReadAction, - useEditAction, - useUnholdAction, - useHoldAction, - useJoinThreadAction, - useLeaveThreadAction, - useCopyURLAction, - useCopyToClipboardAction, - useCopyEmailAction, - useCopyMessageAction, - useCopyLinkAction, - usePinAction, - useUnpinAction, - useFlagAsOffensiveAction, - useDownloadAction, - useCopyOnyxDataAction, - useDebugAction, - useDeleteAction, - useOverflowMenuAction, + createEmojiReactionData, + createReplyInThreadAction, + createMarkAsUnreadAction, + createExplainAction, + createMarkAsReadAction, + createEditAction, + createUnholdAction, + createHoldAction, + createJoinThreadAction, + createLeaveThreadAction, + createCopyURLAction, + createCopyToClipboardAction, + createCopyEmailAction, + createCopyMessageAction, + createCopyLinkAction, + createPinAction, + createUnpinAction, + createFlagAsOffensiveAction, + createDownloadAction, + createCopyOnyxDataAction, + createDebugAction, + createDeleteAction, + createOverflowMenuAction, }; diff --git a/src/pages/inbox/report/ContextMenu/actions/MarkAsRead.tsx b/src/pages/inbox/report/ContextMenu/actions/MarkAsRead.tsx deleted file mode 100644 index 99d30a4adf17..000000000000 --- a/src/pages/inbox/report/ContextMenu/actions/MarkAsRead.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; -import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; -import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; -import {readNewestAction} from '@userActions/Report'; -import CONST from '@src/CONST'; -import type {ActionDescriptor} from './ActionDescriptor'; - -function useMarkAsReadAction(payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor | null { - const {reportID, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(payloadOverride); - const {translate} = useLocalize(); - const icons = useMemoizedLazyExpensifyIcons(['Mail', 'Checkmark'] as const); - - return { - id: 'markAsRead', - icon: icons.Mail, - text: translate('reportActionContextMenu.markAsRead'), - successIcon: icons.Checkmark, - onPress: () => - interceptAnonymousUser(() => { - readNewestAction(reportID, true, true); - hideAndRun(ReportActionComposeFocusManager.focus); - }), - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.MARK_AS_READ, - }; -} - -export default useMarkAsReadAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/MarkAsUnread.tsx b/src/pages/inbox/report/ContextMenu/actions/MarkAsUnread.tsx deleted file mode 100644 index 0ee9be924689..000000000000 --- a/src/pages/inbox/report/ContextMenu/actions/MarkAsUnread.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; -import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; -import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; -import {markCommentAsUnread} from '@userActions/Report'; -import CONST from '@src/CONST'; -import type {ActionDescriptor} from './ActionDescriptor'; - -function useMarkAsUnreadAction(payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor | null { - const {reportID, reportActions, reportAction, currentUserAccountID, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(payloadOverride); - const {translate} = useLocalize(); - const icons = useMemoizedLazyExpensifyIcons(['ChatBubbleUnread', 'Checkmark'] as const); - - return { - id: 'markAsUnread', - icon: icons.ChatBubbleUnread, - text: translate('reportActionContextMenu.markAsUnread'), - successIcon: icons.Checkmark, - onPress: () => - interceptAnonymousUser(() => { - markCommentAsUnread(reportID, reportActions, reportAction, currentUserAccountID); - hideAndRun(ReportActionComposeFocusManager.focus); - }), - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.MARK_AS_UNREAD, - }; -} - -export default useMarkAsUnreadAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/Pin.tsx b/src/pages/inbox/report/ContextMenu/actions/Pin.tsx deleted file mode 100644 index e996abb3bc46..000000000000 --- a/src/pages/inbox/report/ContextMenu/actions/Pin.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; -import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; -import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; -import {togglePinnedState} from '@userActions/Report'; -import CONST from '@src/CONST'; -import type {ActionDescriptor} from './ActionDescriptor'; - -function usePinAction(payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor | null { - const {reportID, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(payloadOverride); - const icons = useMemoizedLazyExpensifyIcons(['Pin'] as const); - const {translate} = useLocalize(); - - return { - id: 'pin', - icon: icons.Pin, - text: translate('common.pin'), - onPress: () => - interceptAnonymousUser(() => { - togglePinnedState(reportID, false); - hideAndRun(ReportActionComposeFocusManager.focus); - }), - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.PIN, - }; -} - -export default usePinAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/Unpin.tsx b/src/pages/inbox/report/ContextMenu/actions/Unpin.tsx deleted file mode 100644 index f793f90630a1..000000000000 --- a/src/pages/inbox/report/ContextMenu/actions/Unpin.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; -import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; -import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; -import {togglePinnedState} from '@userActions/Report'; -import CONST from '@src/CONST'; -import type {ActionDescriptor} from './ActionDescriptor'; - -function useUnpinAction(payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor | null { - const {reportID, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(payloadOverride); - const icons = useMemoizedLazyExpensifyIcons(['Pin'] as const); - const {translate} = useLocalize(); - - return { - id: 'unpin', - icon: icons.Pin, - text: translate('common.unPin'), - onPress: () => - interceptAnonymousUser(() => { - togglePinnedState(reportID, true); - hideAndRun(ReportActionComposeFocusManager.focus); - }), - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.UNPIN, - }; -} - -export default useUnpinAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts b/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts index 4c34beca0c4b..385568915b63 100644 --- a/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts +++ b/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts @@ -27,7 +27,7 @@ import { } from '@libs/ReportUtils'; import type {ContextMenuAnchor} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; -import type {Beta, Policy, ReportAction, Report as ReportType, Transaction} from '@src/types/onyx'; +import type {Policy, ReportAction, Report as ReportType, Transaction} from '@src/types/onyx'; const ACTION_IDS = { EMOJI_REACTION: 'emojiReaction', @@ -55,14 +55,13 @@ const ACTION_IDS = { OVERFLOW_MENU: 'overflowMenu', } as const; -type ActionId = (typeof ACTION_IDS)[keyof typeof ACTION_IDS]; +type ActionID = (typeof ACTION_IDS)[keyof typeof ACTION_IDS]; type ShouldShowArgs = { type: string; reportAction: OnyxEntry; childReportActions: OnyxCollection; isArchivedRoom: boolean; - betas: OnyxEntry; menuTarget: RefObject | undefined; isChronosReport: boolean; reportID?: string; @@ -86,7 +85,7 @@ function getActionHtml(reportAction: OnyxEntry): string { return message?.html ?? ''; } -const ORDERED_ACTION_SHOULD_SHOW: Array<{id: ActionId; isContentAction: boolean; shouldShow: (args: ShouldShowArgs) => boolean}> = [ +const ORDERED_ACTION_SHOULD_SHOW: Array<{id: ActionID; isContentAction: boolean; shouldShow: (args: ShouldShowArgs) => boolean}> = [ { id: ACTION_IDS.EMOJI_REACTION, isContentAction: true, @@ -302,7 +301,11 @@ const ORDERED_ACTION_SHOULD_SHOW: Array<{id: ActionId; isContentAction: boolean; }, ]; -const RESTRICTED_READONLY_ACTION_IDS = new Set([ACTION_IDS.REPLY_IN_THREAD, ACTION_IDS.EDIT, ACTION_IDS.JOIN_THREAD, ACTION_IDS.DELETE]); +const RESTRICTED_READONLY_ACTION_IDS = new Set([ACTION_IDS.REPLY_IN_THREAD, ACTION_IDS.EDIT, ACTION_IDS.JOIN_THREAD, ACTION_IDS.DELETE]); -export {ACTION_IDS, ORDERED_ACTION_SHOULD_SHOW, RESTRICTED_READONLY_ACTION_IDS, getActionHtml}; -export type {ActionId, ShouldShowArgs}; +function getVisibleActionIDs(shouldShowArgs: ShouldShowArgs, disabledActionIDs: Set): ActionID[] { + return ORDERED_ACTION_SHOULD_SHOW.filter((entry) => entry.id !== 'overflowMenu' && !disabledActionIDs.has(entry.id) && entry.shouldShow(shouldShowArgs)).map((entry) => entry.id); +} + +export {ACTION_IDS, ORDERED_ACTION_SHOULD_SHOW, RESTRICTED_READONLY_ACTION_IDS, getActionHtml, getVisibleActionIDs}; +export type {ActionID, ShouldShowArgs}; diff --git a/src/pages/inbox/report/ContextMenu/ContextMenuPayloadProvider.tsx b/src/pages/inbox/report/ContextMenu/actions/actionTypes.ts similarity index 75% rename from src/pages/inbox/report/ContextMenu/ContextMenuPayloadProvider.tsx rename to src/pages/inbox/report/ContextMenu/actions/actionTypes.ts index 70fcda7dbfd1..ca6ad75658a9 100644 --- a/src/pages/inbox/report/ContextMenu/ContextMenuPayloadProvider.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/actionTypes.ts @@ -1,13 +1,38 @@ import type {RefObject} from 'react'; -import {createContext, useContext} from 'react'; import type {GestureResponderEvent, View} from 'react-native'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {ExpensifyIconName} from '@components/Icon/ExpensifyIconLoader'; import type {LocaleContextProps, LocalizedTranslate} from '@components/LocaleContextProvider'; import type useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import type {Beta, Card, Download as DownloadOnyx, IntroSelected, Policy, PolicyTagLists, ReportAction, ReportActions, Report as ReportType, Transaction} from '@src/types/onyx'; -import type {ContextMenuAnchor, ContextMenuType} from './ReportActionContextMenu'; - -type ContextMenuPayloadContextValue = { +import type IconAsset from '@src/types/utils/IconAsset'; +import type {ContextMenuAnchor, ContextMenuType} from '../ReportActionContextMenu'; + +const CONTEXT_MENU_ICON_NAMES = [ + 'Bell', + 'Bug', + 'ChatBubbleReply', + 'ChatBubbleUnread', + 'Checkmark', + 'Concierge', + 'Copy', + 'Download', + 'Exit', + 'Flag', + 'LinkCopy', + 'Mail', + 'Pencil', + 'Pin', + 'Stopwatch', + 'ThreeDots', + 'Trashcan', +] as const; + +type ContextMenuIconName = (typeof CONTEXT_MENU_ICON_NAMES)[number]; + +type ContextMenuIcons = Record; + +type ContextMenuPayload = { type: ContextMenuType; reportID: string | undefined; originalReportID: string | undefined; @@ -19,9 +44,6 @@ type ContextMenuPayloadContextValue = { childReport: OnyxEntry; childReportActions: OnyxCollection; - policy: OnyxEntry; - policyTags: OnyxEntry; - moneyRequestAction: ReportAction | undefined; moneyRequestReport: OnyxEntry; moneyRequestPolicy: OnyxEntry; @@ -69,24 +91,18 @@ type ContextMenuPayloadContextValue = { translate: LocalizedTranslate; getLocalDateFromDatetime: LocaleContextProps['getLocalDateFromDatetime']; + policy: OnyxEntry; + policyTags: OnyxEntry; anchor: RefObject | undefined; disabledActionIDs: Set; }; -const ContextMenuPayloadContext = createContext(null); - -function useContextMenuPayload(override?: ContextMenuPayloadContextValue): ContextMenuPayloadContextValue { - const ctx = useContext(ContextMenuPayloadContext); - if (override) { - return override; - } - if (ctx === null) { - throw new Error('useContextMenuPayload must be used within a ContextMenuPayloadProvider'); - } - return ctx; -} - -export {ContextMenuPayloadContext, useContextMenuPayload}; -export type {ContextMenuPayloadContextValue}; +type ContextMenuActionParams = { + payload: ContextMenuPayload; + icons: ContextMenuIcons; +}; + +export {CONTEXT_MENU_ICON_NAMES}; +export type {ContextMenuActionParams, ContextMenuPayload, ContextMenuIcons, ContextMenuIconName}; diff --git a/src/pages/inbox/report/ContextMenu/actions/CopyEmail.tsx b/src/pages/inbox/report/ContextMenu/actions/copyEmailAction.ts similarity index 57% rename from src/pages/inbox/report/ContextMenu/actions/CopyEmail.tsx rename to src/pages/inbox/report/ContextMenu/actions/copyEmailAction.ts index 02ccdeae92e1..28f8e9f384a7 100644 --- a/src/pages/inbox/report/ContextMenu/actions/CopyEmail.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/copyEmailAction.ts @@ -1,25 +1,21 @@ -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; import Clipboard from '@libs/Clipboard'; import EmailUtils from '@libs/EmailUtils'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; -import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import type {ActionDescriptor} from './ActionDescriptor'; +import type {ContextMenuActionParams} from './actionTypes'; -function useCopyEmailAction(payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor | null { - const {selection, interceptAnonymousUser} = useContextMenuPayload(payloadOverride); - const icons = useMemoizedLazyExpensifyIcons(['Copy', 'Checkmark'] as const); - const {translate} = useLocalize(); +function createCopyEmailAction(params: ContextMenuActionParams): ActionDescriptor { + const {selection, interceptAnonymousUser, translate} = params.payload; + const {Copy, Checkmark} = params.icons; return { id: 'copyEmail', - icon: icons.Copy, + icon: Copy, text: translate('reportActionContextMenu.copyEmailToClipboard'), successText: translate('reportActionContextMenu.copied'), - successIcon: icons.Checkmark, + successIcon: Checkmark, description: EmailUtils.prefixMailSeparatorsWithBreakOpportunities(EmailUtils.trimMailTo(selection ?? '')), isAnonymousAction: true, onPress: () => @@ -31,4 +27,4 @@ function useCopyEmailAction(payloadOverride?: ContextMenuPayloadContextValue): A }; } -export default useCopyEmailAction; +export default createCopyEmailAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/CopyLink.tsx b/src/pages/inbox/report/ContextMenu/actions/copyLinkAction.ts similarity index 61% rename from src/pages/inbox/report/ContextMenu/actions/CopyLink.tsx rename to src/pages/inbox/report/ContextMenu/actions/copyLinkAction.ts index 0a8ea6f3a57b..bb9f59acf9a4 100644 --- a/src/pages/inbox/report/ContextMenu/actions/CopyLink.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/copyLinkAction.ts @@ -1,25 +1,21 @@ -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; import Clipboard from '@libs/Clipboard'; import {getEnvironmentURL} from '@libs/Environment/Environment'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; -import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import type {ActionDescriptor} from './ActionDescriptor'; +import type {ContextMenuActionParams} from './actionTypes'; -function useCopyLinkAction(payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor | null { - const {reportAction, originalReportID, interceptAnonymousUser} = useContextMenuPayload(payloadOverride); - const icons = useMemoizedLazyExpensifyIcons(['LinkCopy', 'Checkmark'] as const); - const {translate} = useLocalize(); +function createCopyLinkAction(params: ContextMenuActionParams): ActionDescriptor { + const {reportAction, originalReportID, interceptAnonymousUser, translate} = params.payload; + const {LinkCopy, Checkmark} = params.icons; return { id: 'copyLink', - icon: icons.LinkCopy, + icon: LinkCopy, text: translate('reportActionContextMenu.copyLink'), successText: translate('reportActionContextMenu.copied'), - successIcon: icons.Checkmark, + successIcon: Checkmark, isAnonymousAction: true, onPress: () => interceptAnonymousUser(() => { @@ -33,4 +29,4 @@ function useCopyLinkAction(payloadOverride?: ContextMenuPayloadContextValue): Ac }; } -export default useCopyLinkAction; +export default createCopyLinkAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/CopyMessage.tsx b/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.ts similarity index 97% rename from src/pages/inbox/report/ContextMenu/actions/CopyMessage.tsx rename to src/pages/inbox/report/ContextMenu/actions/copyMessageAction.ts index 33c587c12269..6a6f8ea43634 100644 --- a/src/pages/inbox/report/ContextMenu/actions/CopyMessage.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.ts @@ -1,6 +1,4 @@ import {Str} from 'expensify-common'; -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; import Clipboard from '@libs/Clipboard'; import getClipboardText from '@libs/Clipboard/getClipboardText'; import {formatPhoneNumber as formatPhoneNumberPhoneUtils} from '@libs/LocalePhoneNumber'; @@ -137,13 +135,12 @@ import { isExpenseReport, } from '@libs/ReportUtils'; import {getTaskCreatedMessage, getTaskReportActionMessage} from '@libs/TaskUtils'; -import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; -import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import type {ReportAction} from '@src/types/onyx'; import {getActionHtml} from './actionConfig'; import type {ActionDescriptor} from './ActionDescriptor'; +import type {ContextMenuActionParams, ContextMenuPayload} from './actionTypes'; function setClipboardMessage(content: string | undefined) { if (!content) { @@ -157,7 +154,7 @@ function setClipboardMessage(content: string | undefined) { } } -function copyMessageToClipboard(payload: ContextMenuPayloadContextValue) { +export function copyMessageToClipboard(payload: ContextMenuPayload) { const { reportAction, transaction, @@ -499,16 +496,14 @@ function copyMessageToClipboard(payload: ContextMenuPayloadContextValue) { } } -function useCopyMessageAction(payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor | null { - const payload = useContextMenuPayload(payloadOverride); - const icons = useMemoizedLazyExpensifyIcons(['Copy', 'Checkmark'] as const); - const {translate} = useLocalize(); +function createCopyMessageAction(params: ContextMenuActionParams): ActionDescriptor { + const {payload, icons} = params; return { id: 'copyMessage', icon: icons.Copy, - text: translate('reportActionContextMenu.copyMessage'), - successText: translate('reportActionContextMenu.copied'), + text: payload.translate('reportActionContextMenu.copyMessage'), + successText: payload.translate('reportActionContextMenu.copied'), successIcon: icons.Checkmark, isAnonymousAction: true, onPress: () => @@ -520,4 +515,4 @@ function useCopyMessageAction(payloadOverride?: ContextMenuPayloadContextValue): }; } -export default useCopyMessageAction; +export default createCopyMessageAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/CopyOnyxData.tsx b/src/pages/inbox/report/ContextMenu/actions/copyOnyxDataAction.ts similarity index 52% rename from src/pages/inbox/report/ContextMenu/actions/CopyOnyxData.tsx rename to src/pages/inbox/report/ContextMenu/actions/copyOnyxDataAction.ts index 35578d8e3ad3..632d4bba32bf 100644 --- a/src/pages/inbox/report/ContextMenu/actions/CopyOnyxData.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/copyOnyxDataAction.ts @@ -1,24 +1,20 @@ -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; import Clipboard from '@libs/Clipboard'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; -import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import type {ActionDescriptor} from './ActionDescriptor'; +import type {ContextMenuActionParams} from './actionTypes'; -function useCopyOnyxDataAction(payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor | null { - const {report, interceptAnonymousUser} = useContextMenuPayload(payloadOverride); - const icons = useMemoizedLazyExpensifyIcons(['Copy', 'Checkmark'] as const); - const {translate} = useLocalize(); +function createCopyOnyxDataAction(params: ContextMenuActionParams): ActionDescriptor { + const {report, interceptAnonymousUser, translate} = params.payload; + const {Copy, Checkmark} = params.icons; return { id: 'copyOnyxData', - icon: icons.Copy, + icon: Copy, text: translate('reportActionContextMenu.copyOnyxData'), successText: translate('reportActionContextMenu.copied'), - successIcon: icons.Checkmark, + successIcon: Checkmark, isAnonymousAction: true, onPress: () => interceptAnonymousUser(() => { @@ -29,4 +25,4 @@ function useCopyOnyxDataAction(payloadOverride?: ContextMenuPayloadContextValue) }; } -export default useCopyOnyxDataAction; +export default createCopyOnyxDataAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/CopyToClipboard.tsx b/src/pages/inbox/report/ContextMenu/actions/copyToClipboardAction.ts similarity index 51% rename from src/pages/inbox/report/ContextMenu/actions/CopyToClipboard.tsx rename to src/pages/inbox/report/ContextMenu/actions/copyToClipboardAction.ts index c39a46844cec..63a11e9d5758 100644 --- a/src/pages/inbox/report/ContextMenu/actions/CopyToClipboard.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/copyToClipboardAction.ts @@ -1,24 +1,20 @@ -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; import Clipboard from '@libs/Clipboard'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; -import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import type {ActionDescriptor} from './ActionDescriptor'; +import type {ContextMenuActionParams} from './actionTypes'; -function useCopyToClipboardAction(payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor | null { - const {selection, interceptAnonymousUser} = useContextMenuPayload(payloadOverride); - const {translate} = useLocalize(); - const icons = useMemoizedLazyExpensifyIcons(['Copy', 'Checkmark'] as const); +function createCopyToClipboardAction(params: ContextMenuActionParams): ActionDescriptor { + const {selection, interceptAnonymousUser, translate} = params.payload; + const {Copy, Checkmark} = params.icons; return { id: 'copyToClipboard', - icon: icons.Copy, + icon: Copy, text: translate('common.copyToClipboard'), successText: translate('reportActionContextMenu.copied'), - successIcon: icons.Checkmark, + successIcon: Checkmark, isAnonymousAction: true, onPress: () => interceptAnonymousUser(() => { @@ -29,4 +25,4 @@ function useCopyToClipboardAction(payloadOverride?: ContextMenuPayloadContextVal }; } -export default useCopyToClipboardAction; +export default createCopyToClipboardAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/CopyURL.tsx b/src/pages/inbox/report/ContextMenu/actions/copyURLAction.ts similarity index 53% rename from src/pages/inbox/report/ContextMenu/actions/CopyURL.tsx rename to src/pages/inbox/report/ContextMenu/actions/copyURLAction.ts index 87fe227d0adb..229fdd15e6af 100644 --- a/src/pages/inbox/report/ContextMenu/actions/CopyURL.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/copyURLAction.ts @@ -1,24 +1,20 @@ -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; import Clipboard from '@libs/Clipboard'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; -import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import type {ActionDescriptor} from './ActionDescriptor'; +import type {ContextMenuActionParams} from './actionTypes'; -function useCopyURLAction(payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor | null { - const {selection, interceptAnonymousUser} = useContextMenuPayload(payloadOverride); - const {translate} = useLocalize(); - const icons = useMemoizedLazyExpensifyIcons(['Copy', 'Checkmark'] as const); +function createCopyURLAction(params: ContextMenuActionParams): ActionDescriptor { + const {selection, interceptAnonymousUser, translate} = params.payload; + const {Copy, Checkmark} = params.icons; return { id: 'copyUrl', - icon: icons.Copy, + icon: Copy, text: translate('reportActionContextMenu.copyURLToClipboard'), successText: translate('reportActionContextMenu.copied'), - successIcon: icons.Checkmark, + successIcon: Checkmark, description: selection, isAnonymousAction: true, onPress: () => @@ -30,4 +26,4 @@ function useCopyURLAction(payloadOverride?: ContextMenuPayloadContextValue): Act }; } -export default useCopyURLAction; +export default createCopyURLAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/Debug.tsx b/src/pages/inbox/report/ContextMenu/actions/debugAction.ts similarity index 61% rename from src/pages/inbox/report/ContextMenu/actions/Debug.tsx rename to src/pages/inbox/report/ContextMenu/actions/debugAction.ts index 4f27d4448446..dba10bd97e83 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Debug.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/debugAction.ts @@ -1,18 +1,14 @@ -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; -import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {ActionDescriptor} from './ActionDescriptor'; +import type {ContextMenuActionParams} from './actionTypes'; -function useDebugAction(payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor | null { - const {reportID, reportAction, interceptAnonymousUser} = useContextMenuPayload(payloadOverride); - const icons = useMemoizedLazyExpensifyIcons(['Bug'] as const); - const {translate} = useLocalize(); +function createDebugAction(params: ContextMenuActionParams): ActionDescriptor { + const {payload, icons} = params; + const {reportID, reportAction, interceptAnonymousUser, translate} = payload; return { id: 'debug', @@ -35,4 +31,4 @@ function useDebugAction(payloadOverride?: ContextMenuPayloadContextValue): Actio }; } -export default useDebugAction; +export default createDebugAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/Delete.tsx b/src/pages/inbox/report/ContextMenu/actions/deleteAction.ts similarity index 62% rename from src/pages/inbox/report/ContextMenu/actions/Delete.tsx rename to src/pages/inbox/report/ContextMenu/actions/deleteAction.ts index d34f6e95abe2..892f89e83596 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Delete.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/deleteAction.ts @@ -1,16 +1,12 @@ -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils'; -import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; -import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {showDeleteModal} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import type {ActionDescriptor} from './ActionDescriptor'; +import type {ContextMenuActionParams} from './actionTypes'; -function useDeleteAction(payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor | null { - const {reportID, reportAction, moneyRequestAction, hideAndRun} = useContextMenuPayload(payloadOverride); - const icons = useMemoizedLazyExpensifyIcons(['Trashcan'] as const); - const {translate} = useLocalize(); +function createDeleteAction(params: ContextMenuActionParams): ActionDescriptor { + const {payload, icons} = params; + const {reportID, reportAction, moneyRequestAction, hideAndRun, translate} = payload; return { id: 'delete', @@ -26,4 +22,4 @@ function useDeleteAction(payloadOverride?: ContextMenuPayloadContextValue): Acti }; } -export default useDeleteAction; +export default createDeleteAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/Download.tsx b/src/pages/inbox/report/ContextMenu/actions/downloadAction.ts similarity index 71% rename from src/pages/inbox/report/ContextMenu/actions/Download.tsx rename to src/pages/inbox/report/ContextMenu/actions/downloadAction.ts index a05359cbcf27..aeae75552d35 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Download.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/downloadAction.ts @@ -1,30 +1,26 @@ -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; import {isMobileSafari} from '@libs/Browser'; import fileDownload from '@libs/fileDownload'; import getAttachmentDetails from '@libs/fileDownload/getAttachmentDetails'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; -import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {setDownload} from '@userActions/Download'; import CONST from '@src/CONST'; import {getActionHtml} from './actionConfig'; import type {ActionDescriptor} from './ActionDescriptor'; +import type {ContextMenuActionParams} from './actionTypes'; -function useDownloadAction(payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor | null { - const {reportAction, encryptedAuthToken, interceptAnonymousUser, download, translate: payloadTranslate} = useContextMenuPayload(payloadOverride); - const icons = useMemoizedLazyExpensifyIcons(['Download'] as const); - const {translate} = useLocalize(); +function createDownloadAction(params: ContextMenuActionParams): ActionDescriptor { + const {payload, icons} = params; + const {reportAction, encryptedAuthToken, interceptAnonymousUser, download, translate: payloadTranslate} = payload; const isDownloading = download?.isDownloading ?? false; return { id: 'download', icon: icons.Download, - text: translate('common.download'), - successText: translate('common.download'), + text: payloadTranslate('common.download'), + successText: payloadTranslate('common.download'), successIcon: icons.Download, isAnonymousAction: true, disabled: isDownloading, @@ -45,4 +41,4 @@ function useDownloadAction(payloadOverride?: ContextMenuPayloadContextValue): Ac }; } -export default useDownloadAction; +export default createDownloadAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/Edit.tsx b/src/pages/inbox/report/ContextMenu/actions/editAction.ts similarity index 70% rename from src/pages/inbox/report/ContextMenu/actions/Edit.tsx rename to src/pages/inbox/report/ContextMenu/actions/editAction.ts index 5586b9931af0..536a54100530 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Edit.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/editAction.ts @@ -1,20 +1,16 @@ -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; import Parser from '@libs/Parser'; import {isMoneyRequestAction} from '@libs/ReportActionsUtils'; -import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; -import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {deleteReportActionDraft, openReport, saveReportActionDraft} from '@userActions/Report'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import {getActionHtml} from './actionConfig'; import type {ActionDescriptor} from './ActionDescriptor'; +import type {ContextMenuActionParams} from './actionTypes'; -function useEditAction(payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor | null { - const {reportID, reportAction, moneyRequestAction, draftMessage, introSelected, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(payloadOverride); - const {translate} = useLocalize(); - const icons = useMemoizedLazyExpensifyIcons(['Pencil'] as const); +function createEditAction(params: ContextMenuActionParams): ActionDescriptor { + const {payload, icons} = params; + const {reportID, reportAction, moneyRequestAction, draftMessage, introSelected, interceptAnonymousUser, hideAndRun, translate} = payload; return { id: 'edit', @@ -42,4 +38,4 @@ function useEditAction(payloadOverride?: ContextMenuPayloadContextValue): Action }; } -export default useEditAction; +export default createEditAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/EmojiReaction.tsx b/src/pages/inbox/report/ContextMenu/actions/emojiReactionAction.ts similarity index 74% rename from src/pages/inbox/report/ContextMenu/actions/EmojiReaction.tsx rename to src/pages/inbox/report/ContextMenu/actions/emojiReactionAction.ts index 8cce8403b583..497adccc5c6b 100644 --- a/src/pages/inbox/report/ContextMenu/actions/EmojiReaction.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/emojiReactionAction.ts @@ -1,23 +1,22 @@ import type {OnyxEntry} from 'react-native-onyx'; import type {Emoji} from '@assets/emojis/types'; -import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; -import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {toggleEmojiReaction} from '@userActions/Report'; import type {ReportActionReactions} from '@src/types/onyx'; +import type {ContextMenuPayload} from './actionTypes'; type EmojiReactionData = { reportID: string | undefined; - reportAction: ReturnType['reportAction']; + reportAction: ContextMenuPayload['reportAction']; reportActionID: string | undefined; toggleEmojiAndCloseMenu: (emoji: Emoji, existingReactions: OnyxEntry, preferredSkinTone: number) => void; closeContextMenu: (onHideCallback?: () => void) => void; onPressOpenPicker: () => void; onEmojiPickerClosed: () => void; - interceptAnonymousUser: ReturnType['interceptAnonymousUser']; + interceptAnonymousUser: ContextMenuPayload['interceptAnonymousUser']; }; -function useEmojiReactionData(payloadOverride?: ContextMenuPayloadContextValue): EmojiReactionData { - const {reportID, reportAction, currentUserAccountID, openContextMenu, setIsEmojiPickerActive, hideAndRun, interceptAnonymousUser} = useContextMenuPayload(payloadOverride); +function createEmojiReactionData(payload: ContextMenuPayload): EmojiReactionData { + const {reportID, reportAction, currentUserAccountID, openContextMenu, setIsEmojiPickerActive, hideAndRun, interceptAnonymousUser} = payload; const closeContextMenu = (onHideCallback?: () => void) => { hideAndRun(onHideCallback); @@ -51,5 +50,5 @@ function useEmojiReactionData(payloadOverride?: ContextMenuPayloadContextValue): }; } -export default useEmojiReactionData; +export default createEmojiReactionData; export type {EmojiReactionData}; diff --git a/src/pages/inbox/report/ContextMenu/actions/Explain.tsx b/src/pages/inbox/report/ContextMenu/actions/explainAction.ts similarity index 58% rename from src/pages/inbox/report/ContextMenu/actions/Explain.tsx rename to src/pages/inbox/report/ContextMenu/actions/explainAction.ts index 7348db535762..82de3b9ae771 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Explain.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/explainAction.ts @@ -1,16 +1,12 @@ -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; -import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; -import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {explain} from '@userActions/Report'; import CONST from '@src/CONST'; import KeyboardUtils from '@src/utils/keyboard'; import type {ActionDescriptor} from './ActionDescriptor'; +import type {ContextMenuActionParams} from './actionTypes'; -function useExplainAction(payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor | null { - const {childReport, originalReport, reportAction, currentUserPersonalDetails, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(payloadOverride); - const {translate} = useLocalize(); - const icons = useMemoizedLazyExpensifyIcons(['Concierge'] as const); +function createExplainAction(params: ContextMenuActionParams): ActionDescriptor { + const {payload, icons} = params; + const {childReport, originalReport, reportAction, currentUserPersonalDetails, interceptAnonymousUser, hideAndRun, translate} = payload; return { id: 'explain', @@ -31,4 +27,4 @@ function useExplainAction(payloadOverride?: ContextMenuPayloadContextValue): Act }; } -export default useExplainAction; +export default createExplainAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/FlagAsOffensive.tsx b/src/pages/inbox/report/ContextMenu/actions/flagAsOffensiveAction.ts similarity index 56% rename from src/pages/inbox/report/ContextMenu/actions/FlagAsOffensive.tsx rename to src/pages/inbox/report/ContextMenu/actions/flagAsOffensiveAction.ts index 11c6bda8785c..375b0ef62102 100644 --- a/src/pages/inbox/report/ContextMenu/actions/FlagAsOffensive.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/flagAsOffensiveAction.ts @@ -1,17 +1,13 @@ -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; -import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; -import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import KeyboardUtils from '@src/utils/keyboard'; import type {ActionDescriptor} from './ActionDescriptor'; +import type {ContextMenuActionParams} from './actionTypes'; -function useFlagAsOffensiveAction(payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor | null { - const {reportID, reportAction, hideAndRun} = useContextMenuPayload(payloadOverride); - const icons = useMemoizedLazyExpensifyIcons(['Flag'] as const); - const {translate} = useLocalize(); +function createFlagAsOffensiveAction(params: ContextMenuActionParams): ActionDescriptor { + const {payload, icons} = params; + const {reportID, reportAction, hideAndRun, translate} = payload; return { id: 'flagAsOffensive', @@ -32,4 +28,4 @@ function useFlagAsOffensiveAction(payloadOverride?: ContextMenuPayloadContextVal }; } -export default useFlagAsOffensiveAction; +export default createFlagAsOffensiveAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/Hold.tsx b/src/pages/inbox/report/ContextMenu/actions/holdAction.ts similarity index 55% rename from src/pages/inbox/report/ContextMenu/actions/Hold.tsx rename to src/pages/inbox/report/ContextMenu/actions/holdAction.ts index 565a8687427b..3636f981343d 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Hold.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/holdAction.ts @@ -1,16 +1,12 @@ -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; import {changeMoneyRequestHoldStatus} from '@libs/ReportUtils'; -import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; -import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import type {ActionDescriptor} from './ActionDescriptor'; +import type {ContextMenuActionParams} from './actionTypes'; -function useHoldAction(payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor | null { - const {moneyRequestAction, isDelegateAccessRestricted, showDelegateNoAccessModal, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(payloadOverride); - const {translate} = useLocalize(); - const icons = useMemoizedLazyExpensifyIcons(['Stopwatch'] as const); +function createHoldAction(params: ContextMenuActionParams): ActionDescriptor { + const {payload, icons} = params; + const {moneyRequestAction, isDelegateAccessRestricted, showDelegateNoAccessModal, interceptAnonymousUser, hideAndRun, translate} = payload; return { id: 'hold', @@ -28,4 +24,4 @@ function useHoldAction(payloadOverride?: ContextMenuPayloadContextValue): Action }; } -export default useHoldAction; +export default createHoldAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/JoinThread.tsx b/src/pages/inbox/report/ContextMenu/actions/joinThreadAction.ts similarity index 62% rename from src/pages/inbox/report/ContextMenu/actions/JoinThread.tsx rename to src/pages/inbox/report/ContextMenu/actions/joinThreadAction.ts index 7cad03c6b245..19655d6997b1 100644 --- a/src/pages/inbox/report/ContextMenu/actions/JoinThread.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/joinThreadAction.ts @@ -1,17 +1,13 @@ -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import {getChildReportNotificationPreference} from '@libs/ReportUtils'; -import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; -import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {toggleSubscribeToChildReport} from '@userActions/Report'; import CONST from '@src/CONST'; import type {ActionDescriptor} from './ActionDescriptor'; +import type {ContextMenuActionParams} from './actionTypes'; -function useJoinThreadAction(payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor | null { - const {reportAction, originalReport, currentUserAccountID, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(payloadOverride); - const {translate} = useLocalize(); - const icons = useMemoizedLazyExpensifyIcons(['Bell'] as const); +function createJoinThreadAction(params: ContextMenuActionParams): ActionDescriptor { + const {payload, icons} = params; + const {reportAction, originalReport, currentUserAccountID, interceptAnonymousUser, hideAndRun, translate} = payload; return { id: 'joinThread', @@ -29,4 +25,4 @@ function useJoinThreadAction(payloadOverride?: ContextMenuPayloadContextValue): }; } -export default useJoinThreadAction; +export default createJoinThreadAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/LeaveThread.tsx b/src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.ts similarity index 62% rename from src/pages/inbox/report/ContextMenu/actions/LeaveThread.tsx rename to src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.ts index cd1aae0ed73f..e315e5e23d83 100644 --- a/src/pages/inbox/report/ContextMenu/actions/LeaveThread.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.ts @@ -1,17 +1,13 @@ -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import {getChildReportNotificationPreference} from '@libs/ReportUtils'; -import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; -import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {toggleSubscribeToChildReport} from '@userActions/Report'; import CONST from '@src/CONST'; import type {ActionDescriptor} from './ActionDescriptor'; +import type {ContextMenuActionParams} from './actionTypes'; -function useLeaveThreadAction(payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor | null { - const {reportAction, originalReport, currentUserAccountID, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(payloadOverride); - const {translate} = useLocalize(); - const icons = useMemoizedLazyExpensifyIcons(['Exit'] as const); +function createLeaveThreadAction(params: ContextMenuActionParams): ActionDescriptor { + const {payload, icons} = params; + const {reportAction, originalReport, currentUserAccountID, interceptAnonymousUser, hideAndRun, translate} = payload; return { id: 'leaveThread', @@ -29,4 +25,4 @@ function useLeaveThreadAction(payloadOverride?: ContextMenuPayloadContextValue): }; } -export default useLeaveThreadAction; +export default createLeaveThreadAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/markAsReadAction.ts b/src/pages/inbox/report/ContextMenu/actions/markAsReadAction.ts new file mode 100644 index 000000000000..8b83634669c5 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/markAsReadAction.ts @@ -0,0 +1,25 @@ +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {readNewestAction} from '@userActions/Report'; +import CONST from '@src/CONST'; +import type {ActionDescriptor} from './ActionDescriptor'; +import type {ContextMenuActionParams} from './actionTypes'; + +function createMarkAsReadAction(params: ContextMenuActionParams): ActionDescriptor { + const {reportID, interceptAnonymousUser, hideAndRun, translate} = params.payload; + const {Mail, Checkmark} = params.icons; + + return { + id: 'markAsRead', + icon: Mail, + text: translate('reportActionContextMenu.markAsRead'), + successIcon: Checkmark, + onPress: () => + interceptAnonymousUser(() => { + readNewestAction(reportID, true, true); + hideAndRun(ReportActionComposeFocusManager.focus); + }), + sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.MARK_AS_READ, + }; +} + +export default createMarkAsReadAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/markAsUnreadAction.ts b/src/pages/inbox/report/ContextMenu/actions/markAsUnreadAction.ts new file mode 100644 index 000000000000..c927c27ec160 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/markAsUnreadAction.ts @@ -0,0 +1,25 @@ +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {markCommentAsUnread} from '@userActions/Report'; +import CONST from '@src/CONST'; +import type {ActionDescriptor} from './ActionDescriptor'; +import type {ContextMenuActionParams} from './actionTypes'; + +function createMarkAsUnreadAction(params: ContextMenuActionParams): ActionDescriptor { + const {reportID, reportActions, reportAction, currentUserAccountID, interceptAnonymousUser, hideAndRun, translate} = params.payload; + const {ChatBubbleUnread, Checkmark} = params.icons; + + return { + id: 'markAsUnread', + icon: ChatBubbleUnread, + text: translate('reportActionContextMenu.markAsUnread'), + successIcon: Checkmark, + onPress: () => + interceptAnonymousUser(() => { + markCommentAsUnread(reportID, reportActions, reportAction, currentUserAccountID); + hideAndRun(ReportActionComposeFocusManager.focus); + }), + sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.MARK_AS_UNREAD, + }; +} + +export default createMarkAsUnreadAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/OverflowMenu.tsx b/src/pages/inbox/report/ContextMenu/actions/overflowMenuAction.ts similarity index 52% rename from src/pages/inbox/report/ContextMenu/actions/OverflowMenu.tsx rename to src/pages/inbox/report/ContextMenu/actions/overflowMenuAction.ts index 682e40f15314..8acdbdff7a7f 100644 --- a/src/pages/inbox/report/ContextMenu/actions/OverflowMenu.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/overflowMenuAction.ts @@ -1,21 +1,16 @@ -import {useRef} from 'react'; +import type {RefObject} from 'react'; import type {GestureResponderEvent, View} from 'react-native'; -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; -import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; -import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import CONST from '@src/CONST'; import type {ActionDescriptor} from './ActionDescriptor'; +import type {ContextMenuActionParams} from './actionTypes'; type OverflowMenuDescriptor = ActionDescriptor & { - buttonRef: React.RefObject; + buttonRef: RefObject; }; -function useOverflowMenuAction(payloadOverride?: ContextMenuPayloadContextValue): OverflowMenuDescriptor | null { - const {openOverflowMenu, openContextMenu, interceptAnonymousUser} = useContextMenuPayload(payloadOverride); - const icons = useMemoizedLazyExpensifyIcons(['ThreeDots'] as const); - const {translate} = useLocalize(); - const threeDotRef = useRef(null); +function createOverflowMenuAction(params: ContextMenuActionParams, threeDotRef: RefObject): OverflowMenuDescriptor { + const {payload, icons} = params; + const {openOverflowMenu, openContextMenu, interceptAnonymousUser, translate} = payload; return { id: 'overflowMenu', @@ -33,5 +28,5 @@ function useOverflowMenuAction(payloadOverride?: ContextMenuPayloadContextValue) }; } -export default useOverflowMenuAction; +export default createOverflowMenuAction; export type {OverflowMenuDescriptor}; diff --git a/src/pages/inbox/report/ContextMenu/actions/pinAction.ts b/src/pages/inbox/report/ContextMenu/actions/pinAction.ts new file mode 100644 index 000000000000..0b31cf1f1ca4 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/pinAction.ts @@ -0,0 +1,24 @@ +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {togglePinnedState} from '@userActions/Report'; +import CONST from '@src/CONST'; +import type {ActionDescriptor} from './ActionDescriptor'; +import type {ContextMenuActionParams} from './actionTypes'; + +function createPinAction(params: ContextMenuActionParams): ActionDescriptor { + const {reportID, interceptAnonymousUser, hideAndRun, translate} = params.payload; + const {Pin} = params.icons; + + return { + id: 'pin', + icon: Pin, + text: translate('common.pin'), + onPress: () => + interceptAnonymousUser(() => { + togglePinnedState(reportID, false); + hideAndRun(ReportActionComposeFocusManager.focus); + }), + sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.PIN, + }; +} + +export default createPinAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/ReplyInThread.tsx b/src/pages/inbox/report/ContextMenu/actions/replyInThreadAction.ts similarity index 52% rename from src/pages/inbox/report/ContextMenu/actions/ReplyInThread.tsx rename to src/pages/inbox/report/ContextMenu/actions/replyInThreadAction.ts index 771277289bb6..0b9655246503 100644 --- a/src/pages/inbox/report/ContextMenu/actions/ReplyInThread.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/replyInThreadAction.ts @@ -1,20 +1,16 @@ -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; -import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; -import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {navigateToAndOpenChildReport} from '@userActions/Report'; import CONST from '@src/CONST'; import KeyboardUtils from '@src/utils/keyboard'; import type {ActionDescriptor} from './ActionDescriptor'; +import type {ContextMenuActionParams} from './actionTypes'; -function useReplyInThreadAction(payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor | null { - const {childReport, reportAction, originalReport, currentUserAccountID, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(payloadOverride); - const {translate} = useLocalize(); - const icons = useMemoizedLazyExpensifyIcons(['ChatBubbleReply'] as const); +function createReplyInThreadAction(params: ContextMenuActionParams): ActionDescriptor { + const {childReport, reportAction, originalReport, currentUserAccountID, interceptAnonymousUser, hideAndRun, translate} = params.payload; + const {ChatBubbleReply} = params.icons; return { id: 'replyInThread', - icon: icons.ChatBubbleReply, + icon: ChatBubbleReply, text: translate('reportActionContextMenu.replyInThread'), onPress: () => interceptAnonymousUser(() => { @@ -28,4 +24,4 @@ function useReplyInThreadAction(payloadOverride?: ContextMenuPayloadContextValue }; } -export default useReplyInThreadAction; +export default createReplyInThreadAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/Unhold.tsx b/src/pages/inbox/report/ContextMenu/actions/unholdAction.ts similarity index 55% rename from src/pages/inbox/report/ContextMenu/actions/Unhold.tsx rename to src/pages/inbox/report/ContextMenu/actions/unholdAction.ts index 6433cb2fa76e..33986f732974 100644 --- a/src/pages/inbox/report/ContextMenu/actions/Unhold.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/unholdAction.ts @@ -1,16 +1,12 @@ -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; import {changeMoneyRequestHoldStatus} from '@libs/ReportUtils'; -import type {ContextMenuPayloadContextValue} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; -import {useContextMenuPayload} from '@pages/inbox/report/ContextMenu/ContextMenuPayloadProvider'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import type {ActionDescriptor} from './ActionDescriptor'; +import type {ContextMenuActionParams} from './actionTypes'; -function useUnholdAction(payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor | null { - const {moneyRequestAction, isDelegateAccessRestricted, showDelegateNoAccessModal, interceptAnonymousUser, hideAndRun} = useContextMenuPayload(payloadOverride); - const {translate} = useLocalize(); - const icons = useMemoizedLazyExpensifyIcons(['Stopwatch'] as const); +function createUnholdAction(params: ContextMenuActionParams): ActionDescriptor { + const {payload, icons} = params; + const {moneyRequestAction, isDelegateAccessRestricted, showDelegateNoAccessModal, interceptAnonymousUser, hideAndRun, translate} = payload; return { id: 'unhold', @@ -28,4 +24,4 @@ function useUnholdAction(payloadOverride?: ContextMenuPayloadContextValue): Acti }; } -export default useUnholdAction; +export default createUnholdAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/unpinAction.ts b/src/pages/inbox/report/ContextMenu/actions/unpinAction.ts new file mode 100644 index 000000000000..6d86a18029cf --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/unpinAction.ts @@ -0,0 +1,24 @@ +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {togglePinnedState} from '@userActions/Report'; +import CONST from '@src/CONST'; +import type {ActionDescriptor} from './ActionDescriptor'; +import type {ContextMenuActionParams} from './actionTypes'; + +function createUnpinAction(params: ContextMenuActionParams): ActionDescriptor { + const {reportID, interceptAnonymousUser, hideAndRun, translate} = params.payload; + const {Pin} = params.icons; + + return { + id: 'unpin', + icon: Pin, + text: translate('common.unPin'), + onPress: () => + interceptAnonymousUser(() => { + togglePinnedState(reportID, true); + hideAndRun(ReportActionComposeFocusManager.focus); + }), + sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.UNPIN, + }; +} + +export default createUnpinAction; diff --git a/src/pages/inbox/report/ContextMenu/useContextMenuActions.ts b/src/pages/inbox/report/ContextMenu/useContextMenuActions.ts deleted file mode 100644 index d9425de0ce8e..000000000000 --- a/src/pages/inbox/report/ContextMenu/useContextMenuActions.ts +++ /dev/null @@ -1,82 +0,0 @@ -import type {ActionDescriptor} from './actions/ActionDescriptor'; -import { - useCopyEmailAction, - useCopyLinkAction, - useCopyMessageAction, - useCopyOnyxDataAction, - useCopyToClipboardAction, - useCopyURLAction, - useDebugAction, - useDeleteAction, - useDownloadAction, - useEditAction, - useExplainAction, - useFlagAsOffensiveAction, - useHoldAction, - useJoinThreadAction, - useLeaveThreadAction, - useMarkAsReadAction, - useMarkAsUnreadAction, - usePinAction, - useReplyInThreadAction, - useUnholdAction, - useUnpinAction, -} from './actions/ContextMenuAction'; -import type {ContextMenuPayloadContextValue} from './ContextMenuPayloadProvider'; - -/** - * Aggregates all individual context menu action hooks into a single ordered array. - * Each hook is always called (rules of hooks), and returns null when it shouldn't be shown. - * The returned array contains only the visible actions, in display order. - */ -function useContextMenuActions(visibleActionIDs: Set, payloadOverride?: ContextMenuPayloadContextValue): ActionDescriptor[] { - const replyInThread = useReplyInThreadAction(payloadOverride); - const markAsUnread = useMarkAsUnreadAction(payloadOverride); - const explain = useExplainAction(payloadOverride); - const markAsRead = useMarkAsReadAction(payloadOverride); - const edit = useEditAction(payloadOverride); - const unhold = useUnholdAction(payloadOverride); - const hold = useHoldAction(payloadOverride); - const joinThread = useJoinThreadAction(payloadOverride); - const leaveThread = useLeaveThreadAction(payloadOverride); - const copyUrl = useCopyURLAction(payloadOverride); - const copyToClipboard = useCopyToClipboardAction(payloadOverride); - const copyEmail = useCopyEmailAction(payloadOverride); - const copyMessage = useCopyMessageAction(payloadOverride); - const copyLink = useCopyLinkAction(payloadOverride); - const pin = usePinAction(payloadOverride); - const unpin = useUnpinAction(payloadOverride); - const flagAsOffensive = useFlagAsOffensiveAction(payloadOverride); - const download = useDownloadAction(payloadOverride); - const copyOnyxData = useCopyOnyxDataAction(payloadOverride); - const debug = useDebugAction(payloadOverride); - const deleteAction = useDeleteAction(payloadOverride); - - const allActions = [ - replyInThread, - markAsUnread, - explain, - markAsRead, - edit, - unhold, - hold, - joinThread, - leaveThread, - copyUrl, - copyToClipboard, - copyEmail, - copyMessage, - copyLink, - pin, - unpin, - flagAsOffensive, - download, - copyOnyxData, - debug, - deleteAction, - ]; - - return allActions.filter((action): action is ActionDescriptor => action !== null && visibleActionIDs.has(action.id)); -} - -export default useContextMenuActions; diff --git a/src/pages/inbox/report/ContextMenu/useContextMenuData.ts b/src/pages/inbox/report/ContextMenu/useReportActionContextMenuData.ts similarity index 91% rename from src/pages/inbox/report/ContextMenu/useContextMenuData.ts rename to src/pages/inbox/report/ContextMenu/useReportActionContextMenuData.ts index fd5f994f561a..239408433cdd 100644 --- a/src/pages/inbox/report/ContextMenu/useContextMenuData.ts +++ b/src/pages/inbox/report/ContextMenu/useReportActionContextMenuData.ts @@ -34,8 +34,9 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {OriginalMessageIOU, ReportAction} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import type {ActionId} from './actions/actionConfig'; -import {ORDERED_ACTION_SHOULD_SHOW, RESTRICTED_READONLY_ACTION_IDS} from './actions/actionConfig'; +import type {ActionID} from './actions/actionConfig'; +import {getVisibleActionIDs as getVisibleActionIDsFromConfig, RESTRICTED_READONLY_ACTION_IDS} from './actions/actionConfig'; +import type {ContextMenuPayload} from './actions/actionTypes'; import type {ContextMenuAnchor, ContextMenuType} from './ReportActionContextMenu'; import {hideContextMenu} from './ReportActionContextMenu'; @@ -51,7 +52,15 @@ type UseContextMenuDataParams = { anchor: RefObject | undefined; }; -function useContextMenuData({reportID, reportActionID, originalReportID, draftMessage, selection, type, anchor}: UseContextMenuDataParams) { +type UseReportActionContextMenuDataReturn = Omit< + ContextMenuPayload, + 'close' | 'hideAndRun' | 'transitionActionSheetState' | 'openContextMenu' | 'openOverflowMenu' | 'setIsEmojiPickerActive' | 'reportAction' | 'currentUserAccountID' +> & { + reportAction: OnyxEntry; + getVisibleActionIDs: () => ActionID[]; +}; + +function useReportActionContextMenuData({reportID, reportActionID, originalReportID, draftMessage, selection, type, anchor}: UseContextMenuDataParams): UseReportActionContextMenuDataReturn { const {translate, getLocalDateFromDatetime} = useLocalize(); const {isOffline} = useNetwork(); const {isProduction} = useEnvironment(); @@ -165,7 +174,6 @@ function useContextMenuData({reportID, reportActionID, originalReportID, draftMe reportAction, childReportActions, isArchivedRoom, - betas, menuTarget: anchor, isChronosReport, reportID, @@ -184,8 +192,7 @@ function useContextMenuData({reportID, reportActionID, originalReportID, draftMe isHarvestReport, }; - const getVisibleActionIDs = (): ActionId[] => - ORDERED_ACTION_SHOULD_SHOW.filter((entry) => entry.id !== 'overflowMenu' && !disabledActionIDs.has(entry.id) && entry.shouldShow(shouldShowArgs)).map((entry) => entry.id); + const getVisibleActionIDs = (): ActionID[] => getVisibleActionIDsFromConfig(shouldShowArgs, disabledActionIDs); return { report, @@ -238,5 +245,5 @@ function useContextMenuData({reportID, reportActionID, originalReportID, draftMe }; } -export default useContextMenuData; -export type {UseContextMenuDataParams}; +export default useReportActionContextMenuData; +export type {UseContextMenuDataParams, UseReportActionContextMenuDataReturn}; diff --git a/src/pages/inbox/report/ContextMenu/useReportContextMenuData.ts b/src/pages/inbox/report/ContextMenu/useReportContextMenuData.ts new file mode 100644 index 000000000000..43303cad6d2e --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/useReportContextMenuData.ts @@ -0,0 +1,142 @@ +import type {RefObject} from 'react'; +import {InteractionManager} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import useEnvironment from '@hooks/useEnvironment'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import useOnyx from '@hooks/useOnyx'; +import useReportIsArchived from '@hooks/useReportIsArchived'; +import {canWriteInReport, chatIncludesChronosWithID, isArchivedNonExpenseReport, isUnread} from '@libs/ReportUtils'; +import {isAnonymousUser, signOutAndRedirectToSignIn} from '@userActions/Session'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {ReportAction, ReportActions, Report as ReportType} from '@src/types/onyx'; +import type {ActionID} from './actions/actionConfig'; +import {getVisibleActionIDs as getVisibleActionIDsFromConfig, RESTRICTED_READONLY_ACTION_IDS} from './actions/actionConfig'; +import type {ContextMenuAnchor, ContextMenuType} from './ReportActionContextMenu'; +import {hideContextMenu} from './ReportActionContextMenu'; + +const EMPTY_SET = new Set(); + +type UseContextMenuDataParams = { + reportID: string | undefined; + reportActionID: string | undefined; + originalReportID: string | undefined; + draftMessage: string; + selection: string; + type: ContextMenuType; + anchor: RefObject | undefined; +}; + +type UseReportContextMenuDataReturn = { + report: OnyxEntry; + originalReport: OnyxEntry; + reportActions: OnyxEntry; + reportAction: OnyxEntry; + isArchivedRoom: boolean; + isChronosReport: boolean; + isPinnedChat: boolean; + isUnreadChat: boolean; + isProduction: boolean; + isDebugModeEnabled: OnyxEntry; + isOffline: boolean; + disabledActionIDs: Set; + translate: ReturnType['translate']; + getLocalDateFromDatetime: ReturnType['getLocalDateFromDatetime']; + interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; + getVisibleActionIDs: () => ActionID[]; + type: ContextMenuType; + reportID: string | undefined; + originalReportID: string | undefined; + draftMessage: string; + selection: string; + anchor: RefObject | undefined; +}; + +function useReportContextMenuData({reportID, reportActionID, originalReportID, draftMessage, selection, type, anchor}: UseContextMenuDataParams): UseReportContextMenuDataReturn { + const {translate, getLocalDateFromDatetime} = useLocalize(); + const {isOffline} = useNetwork(); + const {isProduction} = useEnvironment(); + + const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, {canEvict: false}); + const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); + const [originalReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${originalReportID}`); + const [isDebugModeEnabled] = useOnyx(ONYXKEYS.IS_DEBUG_MODE_ENABLED); + + const isOriginalReportArchived = useReportIsArchived(originalReportID); + + const disabledActionIDs = !canWriteInReport(report) ? RESTRICTED_READONLY_ACTION_IDS : EMPTY_SET; + + const hasValidReportAction = reportActions && reportActionID && reportActionID !== '0' && reportActionID !== '-1'; + const reportAction: OnyxEntry = hasValidReportAction ? reportActions[reportActionID] : undefined; + + const isChronosReport = chatIncludesChronosWithID(originalReportID); + const isArchivedRoom = isArchivedNonExpenseReport(originalReport, isOriginalReportArchived); + const isPinnedChat = !!report?.isPinned; + const isUnreadChat = isUnread(report, undefined, isOriginalReportArchived); + + const interceptAnonymousUser = (callback: () => void, isAnonymousAction = false) => { + if (isAnonymousUser() && !isAnonymousAction) { + hideContextMenu(false); + // eslint-disable-next-line @typescript-eslint/no-deprecated + InteractionManager.runAfterInteractions(() => { + signOutAndRedirectToSignIn(); + }); + } else { + callback(); + } + }; + + const shouldShowArgs = { + type: CONST.CONTEXT_MENU_TYPES.REPORT, + reportAction, + childReportActions: undefined, + isArchivedRoom, + menuTarget: anchor, + isChronosReport, + reportID, + isPinnedChat, + isUnreadChat, + isThreadReportParentAction: false, + isOffline: !!isOffline, + isProduction, + moneyRequestAction: undefined as ReportAction | undefined, + areHoldRequirementsMet: false, + isDebugModeEnabled, + iouTransaction: undefined, + transactions: undefined, + moneyRequestReport: undefined, + moneyRequestPolicy: undefined, + isHarvestReport: false, + }; + + const getVisibleActionIDs = (): ActionID[] => getVisibleActionIDsFromConfig(shouldShowArgs, disabledActionIDs); + + return { + report, + originalReport, + reportActions, + reportAction, + isArchivedRoom, + isChronosReport, + isPinnedChat, + isUnreadChat, + isProduction, + isDebugModeEnabled, + isOffline: !!isOffline, + disabledActionIDs, + translate, + getLocalDateFromDatetime, + interceptAnonymousUser, + getVisibleActionIDs, + type, + reportID, + originalReportID, + draftMessage, + selection, + anchor, + }; +} + +export default useReportContextMenuData; +export type {UseContextMenuDataParams, UseReportContextMenuDataReturn}; From 4881f5c7d4ac33721645a8a51a82ef63f07e0ad2 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 2 Mar 2026 17:14:10 -0800 Subject: [PATCH 27/88] refactor(contextmenu): eliminate god object params from action factories Replace monolithic ContextMenuActionParams/ContextMenuPayload types with per-factory param types so each action factory takes only the specific parameters it needs. Update all three consumer components to pass explicit fields instead of assembling a shared payload object. Made-with: Cursor --- .../MiniReportActionContextMenu/index.tsx | 199 +++++++++++--- .../PopoverReportActionContent.tsx | 257 +++++++++++++++--- .../ContextMenu/PopoverReportContent.tsx | 101 +++---- .../ContextMenu/actions/ActionDescriptor.ts | 20 -- .../ContextMenu/actions/actionConfig.ts | 3 +- .../report/ContextMenu/actions/actionTypes.ts | 97 ++----- .../ContextMenu/actions/copyEmailAction.ts | 19 +- .../ContextMenu/actions/copyLinkAction.ts | 21 +- .../ContextMenu/actions/copyMessageAction.ts | 59 +++- .../ContextMenu/actions/copyOnyxDataAction.ts | 21 +- .../actions/copyToClipboardAction.ts | 19 +- .../ContextMenu/actions/copyURLAction.ts | 19 +- .../report/ContextMenu/actions/debugAction.ts | 18 +- .../ContextMenu/actions/deleteAction.ts | 19 +- .../ContextMenu/actions/downloadAction.ts | 28 +- .../report/ContextMenu/actions/editAction.ts | 23 +- .../actions/emojiReactionAction.ts | 25 +- .../ContextMenu/actions/explainAction.ts | 23 +- .../actions/flagAsOffensiveAction.ts | 18 +- .../report/ContextMenu/actions/holdAction.ts | 20 +- .../ContextMenu/actions/joinThreadAction.ts | 21 +- .../ContextMenu/actions/leaveThreadAction.ts | 21 +- .../ContextMenu/actions/markAsReadAction.ts | 20 +- .../ContextMenu/actions/markAsUnreadAction.ts | 35 ++- .../ContextMenu/actions/overflowMenuAction.ts | 20 +- .../report/ContextMenu/actions/pinAction.ts | 17 +- .../actions/replyInThreadAction.ts | 31 ++- .../ContextMenu/actions/unholdAction.ts | 20 +- .../report/ContextMenu/actions/unpinAction.ts | 17 +- .../useReportActionContextMenuData.ts | 13 +- 30 files changed, 826 insertions(+), 398 deletions(-) delete mode 100644 src/pages/inbox/report/ContextMenu/actions/ActionDescriptor.ts diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index 57c7b983b57f..347194a27618 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -14,9 +14,8 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import getButtonState from '@libs/getButtonState'; import type {ActionID} from '@pages/inbox/report/ContextMenu/actions/actionConfig'; -import type {ActionDescriptor} from '@pages/inbox/report/ContextMenu/actions/ActionDescriptor'; +import type {ContextMenuAction} from '@pages/inbox/report/ContextMenu/actions/actionTypes'; import {CONTEXT_MENU_ICON_NAMES} from '@pages/inbox/report/ContextMenu/actions/actionTypes'; -import type {ContextMenuPayload} from '@pages/inbox/report/ContextMenu/actions/actionTypes'; import { createCopyLinkAction, createCopyMessageAction, @@ -142,42 +141,174 @@ function MiniReportActionContextMenu() { }); }; - const payload: ContextMenuPayload = { - ...data, - // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style - reportAction: (data.reportAction ?? null) as NonNullable, - currentUserAccountID: data.currentUserPersonalDetails?.accountID, - close: () => miniActions.release(), - hideAndRun, - transitionActionSheetState, - openContextMenu: () => miniActions.keepOpen(), - openOverflowMenu, - setIsEmojiPickerActive: state?.setIsEmojiPickerActive, - }; - - const params = {payload, icons}; + // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style + const reportAction = (data.reportAction ?? null) as NonNullable; + const currentUserAccountID = data.currentUserPersonalDetails?.accountID ?? 0; + const {interceptAnonymousUser, translate} = data; /* eslint-disable react-hooks/refs -- factory functions store refs for later use, they don't read .current during render */ - const allActions: ActionDescriptor[] = [ - createReplyInThreadAction(params), - createMarkAsUnreadAction(params), - createExplainAction(params), - createMarkAsReadAction(params), - createEditAction(params), - createUnholdAction(params), - createHoldAction(params), - createJoinThreadAction(params), - createLeaveThreadAction(params), - createCopyMessageAction(params), - createCopyLinkAction(params), - createFlagAsOffensiveAction(params), - createDownloadAction(params), - createDeleteAction(params), + const allActions: ContextMenuAction[] = [ + createReplyInThreadAction({ + childReport: data.childReport, + reportAction, + originalReport: data.originalReport, + currentUserAccountID, + interceptAnonymousUser, + hideAndRun, + translate, + chatBubbleReplyIcon: icons.ChatBubbleReply, + }), + createMarkAsUnreadAction({ + reportID: data.reportID, + reportActions: data.reportActions, + reportAction, + currentUserAccountID, + interceptAnonymousUser, + hideAndRun, + translate, + chatBubbleUnreadIcon: icons.ChatBubbleUnread, + checkmarkIcon: icons.Checkmark, + }), + createExplainAction({ + childReport: data.childReport, + originalReport: data.originalReport, + reportAction, + currentUserPersonalDetails: data.currentUserPersonalDetails, + interceptAnonymousUser, + hideAndRun, + translate, + conciergeIcon: icons.Concierge, + }), + createMarkAsReadAction({ + reportID: data.reportID, + interceptAnonymousUser, + hideAndRun, + translate, + mailIcon: icons.Mail, + checkmarkIcon: icons.Checkmark, + }), + createEditAction({ + reportID: data.reportID, + reportAction, + moneyRequestAction: data.moneyRequestAction, + draftMessage: data.draftMessage, + introSelected: data.introSelected, + interceptAnonymousUser, + hideAndRun, + translate, + pencilIcon: icons.Pencil, + }), + createUnholdAction({ + moneyRequestAction: data.moneyRequestAction, + isDelegateAccessRestricted: data.isDelegateAccessRestricted, + showDelegateNoAccessModal: data.showDelegateNoAccessModal, + interceptAnonymousUser, + hideAndRun, + translate, + stopwatchIcon: icons.Stopwatch, + }), + createHoldAction({ + moneyRequestAction: data.moneyRequestAction, + isDelegateAccessRestricted: data.isDelegateAccessRestricted, + showDelegateNoAccessModal: data.showDelegateNoAccessModal, + interceptAnonymousUser, + hideAndRun, + translate, + stopwatchIcon: icons.Stopwatch, + }), + createJoinThreadAction({ + reportAction, + originalReport: data.originalReport, + currentUserAccountID, + interceptAnonymousUser, + hideAndRun, + translate, + bellIcon: icons.Bell, + }), + createLeaveThreadAction({ + reportAction, + originalReport: data.originalReport, + currentUserAccountID, + interceptAnonymousUser, + hideAndRun, + translate, + exitIcon: icons.Exit, + }), + createCopyMessageAction({ + reportAction, + transaction: data.transaction, + selection: data.selection, + report: data.report, + card: data.card, + originalReport: data.originalReport, + isHarvestReport: data.isHarvestReport, + isTryNewDotNVPDismissed: data.isTryNewDotNVPDismissed, + movedFromReport: data.movedFromReport, + movedToReport: data.movedToReport, + childReport: data.childReport, + policy: data.policy, + getLocalDateFromDatetime: data.getLocalDateFromDatetime, + policyTags: data.policyTags, + translate, + harvestReport: data.harvestReport, + currentUserPersonalDetails: data.currentUserPersonalDetails, + interceptAnonymousUser, + copyIcon: icons.Copy, + checkmarkIcon: icons.Checkmark, + }), + createCopyLinkAction({ + reportAction, + originalReportID: data.originalReportID, + interceptAnonymousUser, + translate, + linkCopyIcon: icons.LinkCopy, + checkmarkIcon: icons.Checkmark, + }), + createFlagAsOffensiveAction({ + reportID: data.reportID, + reportAction, + hideAndRun, + translate, + flagIcon: icons.Flag, + }), + createDownloadAction({ + reportAction, + encryptedAuthToken: data.encryptedAuthToken, + interceptAnonymousUser, + download: data.download, + translate, + downloadIcon: icons.Download, + }), + createDeleteAction({ + reportID: data.reportID, + reportAction, + moneyRequestAction: data.moneyRequestAction, + hideAndRun, + translate, + trashcanIcon: icons.Trashcan, + }), ]; const actions = allActions.filter((action) => visibleActionIDs.has(action.id as ActionID)); - const emojiData = createEmojiReactionData(payload); - const overflowMenu = createOverflowMenuAction(params, threeDotRef); + const emojiData = createEmojiReactionData({ + reportID: data.reportID, + reportAction: data.reportAction, + currentUserAccountID, + openContextMenu: () => miniActions.keepOpen(), + setIsEmojiPickerActive: state?.setIsEmojiPickerActive, + hideAndRun, + interceptAnonymousUser, + }); + const overflowMenu = createOverflowMenuAction( + { + openOverflowMenu, + openContextMenu: () => miniActions.keepOpen(), + interceptAnonymousUser, + translate, + threeDotsIcon: icons.ThreeDots, + }, + threeDotRef, + ); /* eslint-enable react-hooks/refs */ const hasEmoji = visibleActionIDs.has('emojiReaction') && !!emojiData.reportAction && !!emojiData.reportActionID; @@ -221,7 +352,7 @@ function MiniReportActionContextMenu() { reportAction={emojiData.reportAction} /> )} - {visibleActions.map((action: ActionDescriptor) => ( + {visibleActions.map((action: ContextMenuAction) => ( , - currentUserAccountID: data.currentUserPersonalDetails?.accountID ?? 0, - close: () => setLocalShouldKeepOpen(false), - hideAndRun, - transitionActionSheetState, - openContextMenu: () => setLocalShouldKeepOpen(true), - openOverflowMenu, - setIsEmojiPickerActive: menuState.onEmojiPickerToggle, - }; - - const params = {payload, icons}; + // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style + const reportAction = (data.reportAction ?? null) as NonNullable; + const currentUserAccountID = data.currentUserPersonalDetails?.accountID ?? 0; + const {interceptAnonymousUser, translate} = data; /* eslint-disable react-hooks/refs -- factory functions store refs for later use, they don't read .current during render */ - const allActions: ActionDescriptor[] = [ - createReplyInThreadAction(params), - createMarkAsUnreadAction(params), - createExplainAction(params), - createMarkAsReadAction(params), - createEditAction(params), - createUnholdAction(params), - createHoldAction(params), - createJoinThreadAction(params), - createLeaveThreadAction(params), - createCopyURLAction(params), - createCopyToClipboardAction(params), - createCopyEmailAction(params), - createCopyMessageAction(params), - createCopyLinkAction(params), - createPinAction(params), - createUnpinAction(params), - createFlagAsOffensiveAction(params), - createDownloadAction(params), - createCopyOnyxDataAction(params), - createDebugAction(params), - createDeleteAction(params), + const allActions: ContextMenuAction[] = [ + createReplyInThreadAction({ + childReport: data.childReport, + reportAction, + originalReport: data.originalReport, + currentUserAccountID, + interceptAnonymousUser, + hideAndRun, + translate, + chatBubbleReplyIcon: icons.ChatBubbleReply, + }), + createMarkAsUnreadAction({ + reportID: data.reportID, + reportActions: data.reportActions, + reportAction, + currentUserAccountID, + interceptAnonymousUser, + hideAndRun, + translate, + chatBubbleUnreadIcon: icons.ChatBubbleUnread, + checkmarkIcon: icons.Checkmark, + }), + createExplainAction({ + childReport: data.childReport, + originalReport: data.originalReport, + reportAction, + currentUserPersonalDetails: data.currentUserPersonalDetails, + interceptAnonymousUser, + hideAndRun, + translate, + conciergeIcon: icons.Concierge, + }), + createMarkAsReadAction({ + reportID: data.reportID, + interceptAnonymousUser, + hideAndRun, + translate, + mailIcon: icons.Mail, + checkmarkIcon: icons.Checkmark, + }), + createEditAction({ + reportID: data.reportID, + reportAction, + moneyRequestAction: data.moneyRequestAction, + draftMessage: data.draftMessage, + introSelected: data.introSelected, + interceptAnonymousUser, + hideAndRun, + translate, + pencilIcon: icons.Pencil, + }), + createUnholdAction({ + moneyRequestAction: data.moneyRequestAction, + isDelegateAccessRestricted: data.isDelegateAccessRestricted, + showDelegateNoAccessModal: data.showDelegateNoAccessModal, + interceptAnonymousUser, + hideAndRun, + translate, + stopwatchIcon: icons.Stopwatch, + }), + createHoldAction({ + moneyRequestAction: data.moneyRequestAction, + isDelegateAccessRestricted: data.isDelegateAccessRestricted, + showDelegateNoAccessModal: data.showDelegateNoAccessModal, + interceptAnonymousUser, + hideAndRun, + translate, + stopwatchIcon: icons.Stopwatch, + }), + createJoinThreadAction({ + reportAction, + originalReport: data.originalReport, + currentUserAccountID, + interceptAnonymousUser, + hideAndRun, + translate, + bellIcon: icons.Bell, + }), + createLeaveThreadAction({ + reportAction, + originalReport: data.originalReport, + currentUserAccountID, + interceptAnonymousUser, + hideAndRun, + translate, + exitIcon: icons.Exit, + }), + createCopyURLAction({ + selection: data.selection, + interceptAnonymousUser, + translate, + copyIcon: icons.Copy, + checkmarkIcon: icons.Checkmark, + }), + createCopyToClipboardAction({ + selection: data.selection, + interceptAnonymousUser, + translate, + copyIcon: icons.Copy, + checkmarkIcon: icons.Checkmark, + }), + createCopyEmailAction({ + selection: data.selection, + interceptAnonymousUser, + translate, + copyIcon: icons.Copy, + checkmarkIcon: icons.Checkmark, + }), + createCopyMessageAction({ + reportAction, + transaction: data.transaction, + selection: data.selection, + report: data.report, + card: data.card, + originalReport: data.originalReport, + isHarvestReport: data.isHarvestReport, + isTryNewDotNVPDismissed: data.isTryNewDotNVPDismissed, + movedFromReport: data.movedFromReport, + movedToReport: data.movedToReport, + childReport: data.childReport, + policy: data.policy, + getLocalDateFromDatetime: data.getLocalDateFromDatetime, + policyTags: data.policyTags, + translate, + harvestReport: data.harvestReport, + currentUserPersonalDetails: data.currentUserPersonalDetails, + interceptAnonymousUser, + copyIcon: icons.Copy, + checkmarkIcon: icons.Checkmark, + }), + createCopyLinkAction({ + reportAction, + originalReportID: data.originalReportID, + interceptAnonymousUser, + translate, + linkCopyIcon: icons.LinkCopy, + checkmarkIcon: icons.Checkmark, + }), + createPinAction({ + reportID: data.reportID, + interceptAnonymousUser, + hideAndRun, + translate, + pinIcon: icons.Pin, + }), + createUnpinAction({ + reportID: data.reportID, + interceptAnonymousUser, + hideAndRun, + translate, + pinIcon: icons.Pin, + }), + createFlagAsOffensiveAction({ + reportID: data.reportID, + reportAction, + hideAndRun, + translate, + flagIcon: icons.Flag, + }), + createDownloadAction({ + reportAction, + encryptedAuthToken: data.encryptedAuthToken, + interceptAnonymousUser, + download: data.download, + translate, + downloadIcon: icons.Download, + }), + createCopyOnyxDataAction({ + report: data.report, + interceptAnonymousUser, + translate, + copyIcon: icons.Copy, + checkmarkIcon: icons.Checkmark, + }), + createDebugAction({ + reportID: data.reportID, + reportAction, + interceptAnonymousUser, + translate, + bugIcon: icons.Bug, + }), + createDeleteAction({ + reportID: data.reportID, + reportAction, + moneyRequestAction: data.moneyRequestAction, + hideAndRun, + translate, + trashcanIcon: icons.Trashcan, + }), ]; - const overflowMenu = createOverflowMenuAction(params, overflowMenuRef); + const overflowMenu = createOverflowMenuAction( + { + openOverflowMenu, + openContextMenu: () => setLocalShouldKeepOpen(true), + interceptAnonymousUser, + translate, + threeDotsIcon: icons.ThreeDots, + }, + overflowMenuRef, + ); const actionsWithOverflow = [...allActions, overflowMenu]; const actions = actionsWithOverflow.filter((action) => visibleActionIDs.has(action.id as ActionID)); - const emojiData = createEmojiReactionData(payload); + const emojiData = createEmojiReactionData({ + reportID: data.reportID, + reportAction: data.reportAction, + currentUserAccountID, + openContextMenu: () => setLocalShouldKeepOpen(true), + setIsEmojiPickerActive: menuState.onEmojiPickerToggle, + hideAndRun, + interceptAnonymousUser, + }); /* eslint-enable react-hooks/refs */ const contentActionIndexes = actions @@ -164,7 +337,7 @@ function PopoverReportActionContent({menuState, hideAndRun, setLocalShouldKeepOp > - {hasEmoji && emojiData.reportActionID != null && ( + {hasEmoji && emojiData.reportActionID != null && emojiData.reportAction != null && ( @@ -180,7 +353,7 @@ function PopoverReportActionContent({menuState, hideAndRun, setLocalShouldKeepOp }} /> )} - {actions.map((action: ActionDescriptor, i: number) => ( + {actions.map((action: ContextMenuAction, i: number) => ( new Set(data.getVisibleActionIDs()), [data]); - const payload = { - ...data, - reportAction: data.reportAction as NonNullable, - currentUserAccountID: 0, - currentUserPersonalDetails: undefined as unknown as ContextMenuPayload['currentUserPersonalDetails'], - encryptedAuthToken: '', - childReport: undefined, - childReportActions: undefined, - policy: undefined, - policyTags: undefined, - moneyRequestAction: undefined, - moneyRequestReport: undefined, - moneyRequestPolicy: undefined, - iouTransaction: undefined, - transaction: undefined, - card: undefined, - isThreadReportParentAction: false, - isHarvestReport: false, - isTryNewDotNVPDismissed: false, - isDelegateAccessRestricted: false, - areHoldRequirementsMet: false, - betas: undefined, - transactions: undefined, - introSelected: undefined, - movedFromReport: undefined, - movedToReport: undefined, - harvestReport: undefined, - download: undefined, - close: () => setLocalShouldKeepOpen(false), - hideAndRun, - transitionActionSheetState, - openContextMenu: () => setLocalShouldKeepOpen(true), - openOverflowMenu: () => {}, - setIsEmojiPickerActive: undefined, - showDelegateNoAccessModal: undefined, - } satisfies ContextMenuPayload; + // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style + const reportAction = (data.reportAction ?? null) as NonNullable; + const {interceptAnonymousUser, translate} = data; - const params = {payload, icons}; - - const allActions: ActionDescriptor[] = [ - createMarkAsReadAction(params), - createMarkAsUnreadAction(params), - createPinAction(params), - createUnpinAction(params), - createCopyOnyxDataAction(params), - createDebugAction(params), + const allActions: ContextMenuAction[] = [ + createMarkAsReadAction({ + reportID: data.reportID, + interceptAnonymousUser, + hideAndRun, + translate, + mailIcon: icons.Mail, + checkmarkIcon: icons.Checkmark, + }), + createMarkAsUnreadAction({ + reportID: data.reportID, + reportActions: data.reportActions, + reportAction, + currentUserAccountID: 0, + interceptAnonymousUser, + hideAndRun, + translate, + chatBubbleUnreadIcon: icons.ChatBubbleUnread, + checkmarkIcon: icons.Checkmark, + }), + createPinAction({ + reportID: data.reportID, + interceptAnonymousUser, + hideAndRun, + translate, + pinIcon: icons.Pin, + }), + createUnpinAction({ + reportID: data.reportID, + interceptAnonymousUser, + hideAndRun, + translate, + pinIcon: icons.Pin, + }), + createCopyOnyxDataAction({ + report: data.report, + interceptAnonymousUser, + translate, + copyIcon: icons.Copy, + checkmarkIcon: icons.Checkmark, + }), + createDebugAction({ + reportID: data.reportID, + reportAction, + interceptAnonymousUser, + translate, + bugIcon: icons.Bug, + }), ]; const actions = allActions.filter((action) => visibleActionIDs.has(action.id as ActionID)); @@ -100,7 +105,7 @@ function PopoverReportContent({menuState, hideAndRun, setLocalShouldKeepOpen, tr ref={contentRef} style={wrapperStyle} > - {actions.map((action: ActionDescriptor, i: number) => ( + {actions.map((action: ContextMenuAction, i: number) => ( void; - successIcon?: IconAsset; - successText?: string; - description?: string; - isAnonymousAction?: boolean; - disabled?: boolean; - shouldShowLoadingSpinnerIcon?: boolean; - shouldPreventDefaultFocusOnPress?: boolean; - sentryLabel: string; -}; - -export type {ActionDescriptor}; diff --git a/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts b/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts index 385568915b63..b423bc5f5095 100644 --- a/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts +++ b/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts @@ -1,5 +1,6 @@ import type {RefObject} from 'react'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; import { getOriginalMessage, getReportAction, @@ -55,7 +56,7 @@ const ACTION_IDS = { OVERFLOW_MENU: 'overflowMenu', } as const; -type ActionID = (typeof ACTION_IDS)[keyof typeof ACTION_IDS]; +type ActionID = ValueOf; type ShouldShowArgs = { type: string; diff --git a/src/pages/inbox/report/ContextMenu/actions/actionTypes.ts b/src/pages/inbox/report/ContextMenu/actions/actionTypes.ts index ca6ad75658a9..13a1049b6a20 100644 --- a/src/pages/inbox/report/ContextMenu/actions/actionTypes.ts +++ b/src/pages/inbox/report/ContextMenu/actions/actionTypes.ts @@ -1,12 +1,6 @@ -import type {RefObject} from 'react'; -import type {GestureResponderEvent, View} from 'react-native'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import type {ExpensifyIconName} from '@components/Icon/ExpensifyIconLoader'; -import type {LocaleContextProps, LocalizedTranslate} from '@components/LocaleContextProvider'; -import type useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; -import type {Beta, Card, Download as DownloadOnyx, IntroSelected, Policy, PolicyTagLists, ReportAction, ReportActions, Report as ReportType, Transaction} from '@src/types/onyx'; +import type {GestureResponderEvent} from 'react-native'; +import type {LocalizedTranslate} from '@components/LocaleContextProvider'; import type IconAsset from '@src/types/utils/IconAsset'; -import type {ContextMenuAnchor, ContextMenuType} from '../ReportActionContextMenu'; const CONTEXT_MENU_ICON_NAMES = [ 'Bell', @@ -28,81 +22,24 @@ const CONTEXT_MENU_ICON_NAMES = [ 'Trashcan', ] as const; -type ContextMenuIconName = (typeof CONTEXT_MENU_ICON_NAMES)[number]; - -type ContextMenuIcons = Record; - -type ContextMenuPayload = { - type: ContextMenuType; - reportID: string | undefined; - originalReportID: string | undefined; - - reportActions: OnyxEntry; - reportAction: ReportAction; - report: OnyxEntry; - originalReport: OnyxEntry; - childReport: OnyxEntry; - childReportActions: OnyxCollection; - - moneyRequestAction: ReportAction | undefined; - moneyRequestReport: OnyxEntry; - moneyRequestPolicy: OnyxEntry; - iouTransaction: OnyxEntry; - transaction: OnyxEntry; - card: Card | undefined; - - currentUserAccountID: number; - currentUserPersonalDetails: ReturnType; - encryptedAuthToken: string; - - isArchivedRoom: boolean; - isChronosReport: boolean; - isPinnedChat: boolean; - isUnreadChat: boolean; - isThreadReportParentAction: boolean; - isOffline: boolean; - isProduction: boolean; - isHarvestReport: boolean; - isTryNewDotNVPDismissed: boolean; - isDelegateAccessRestricted: boolean; - areHoldRequirementsMet: boolean; - isDebugModeEnabled: OnyxEntry; - - betas: OnyxEntry; - transactions: OnyxCollection; - introSelected: OnyxEntry; - draftMessage: string; - selection: string; - - movedFromReport: OnyxEntry; - movedToReport: OnyxEntry; - harvestReport: OnyxEntry; - - download: OnyxEntry; - - close: () => void; - hideAndRun: (callback?: () => void) => void; - transitionActionSheetState: (params: {type: string; payload?: Record}) => void; - openContextMenu: () => void; - interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; - openOverflowMenu: (event: GestureResponderEvent | MouseEvent, anchorRef: RefObject) => void; - setIsEmojiPickerActive: ((state: boolean) => void) | undefined; - showDelegateNoAccessModal: (() => void) | undefined; - +type BaseContextMenuActionParams = { translate: LocalizedTranslate; - getLocalDateFromDatetime: LocaleContextProps['getLocalDateFromDatetime']; - policy: OnyxEntry; - policyTags: OnyxEntry; - - anchor: RefObject | undefined; - - disabledActionIDs: Set; }; -type ContextMenuActionParams = { - payload: ContextMenuPayload; - icons: ContextMenuIcons; +type ContextMenuAction = { + id: string; + icon: IconAsset; + text: string; + onPress: (event?: GestureResponderEvent | MouseEvent | KeyboardEvent) => void; + successIcon?: IconAsset; + successText?: string; + description?: string; + isAnonymousAction?: boolean; + disabled?: boolean; + shouldShowLoadingSpinnerIcon?: boolean; + shouldPreventDefaultFocusOnPress?: boolean; + sentryLabel: string; }; export {CONTEXT_MENU_ICON_NAMES}; -export type {ContextMenuActionParams, ContextMenuPayload, ContextMenuIcons, ContextMenuIconName}; +export type {BaseContextMenuActionParams, ContextMenuAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/copyEmailAction.ts b/src/pages/inbox/report/ContextMenu/actions/copyEmailAction.ts index 28f8e9f384a7..3e431dfde6fb 100644 --- a/src/pages/inbox/report/ContextMenu/actions/copyEmailAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/copyEmailAction.ts @@ -3,19 +3,23 @@ import EmailUtils from '@libs/EmailUtils'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; -import type {ActionDescriptor} from './ActionDescriptor'; -import type {ContextMenuActionParams} from './actionTypes'; +import type IconAsset from '@src/types/utils/IconAsset'; +import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; -function createCopyEmailAction(params: ContextMenuActionParams): ActionDescriptor { - const {selection, interceptAnonymousUser, translate} = params.payload; - const {Copy, Checkmark} = params.icons; +type CopyEmailActionParams = BaseContextMenuActionParams & { + selection: string; + interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; + copyIcon: IconAsset; + checkmarkIcon: IconAsset; +}; +function createCopyEmailAction({selection, interceptAnonymousUser, translate, copyIcon, checkmarkIcon}: CopyEmailActionParams): ContextMenuAction { return { id: 'copyEmail', - icon: Copy, + icon: copyIcon, text: translate('reportActionContextMenu.copyEmailToClipboard'), successText: translate('reportActionContextMenu.copied'), - successIcon: Checkmark, + successIcon: checkmarkIcon, description: EmailUtils.prefixMailSeparatorsWithBreakOpportunities(EmailUtils.trimMailTo(selection ?? '')), isAnonymousAction: true, onPress: () => @@ -28,3 +32,4 @@ function createCopyEmailAction(params: ContextMenuActionParams): ActionDescripto } export default createCopyEmailAction; +export type {CopyEmailActionParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/copyLinkAction.ts b/src/pages/inbox/report/ContextMenu/actions/copyLinkAction.ts index bb9f59acf9a4..0a341a273cb4 100644 --- a/src/pages/inbox/report/ContextMenu/actions/copyLinkAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/copyLinkAction.ts @@ -3,19 +3,25 @@ import {getEnvironmentURL} from '@libs/Environment/Environment'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; -import type {ActionDescriptor} from './ActionDescriptor'; -import type {ContextMenuActionParams} from './actionTypes'; +import type {ReportAction} from '@src/types/onyx'; +import type IconAsset from '@src/types/utils/IconAsset'; +import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; -function createCopyLinkAction(params: ContextMenuActionParams): ActionDescriptor { - const {reportAction, originalReportID, interceptAnonymousUser, translate} = params.payload; - const {LinkCopy, Checkmark} = params.icons; +type CopyLinkActionParams = BaseContextMenuActionParams & { + reportAction: ReportAction; + originalReportID: string | undefined; + interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; + linkCopyIcon: IconAsset; + checkmarkIcon: IconAsset; +}; +function createCopyLinkAction({reportAction, originalReportID, interceptAnonymousUser, translate, linkCopyIcon, checkmarkIcon}: CopyLinkActionParams): ContextMenuAction { return { id: 'copyLink', - icon: LinkCopy, + icon: linkCopyIcon, text: translate('reportActionContextMenu.copyLink'), successText: translate('reportActionContextMenu.copied'), - successIcon: Checkmark, + successIcon: checkmarkIcon, isAnonymousAction: true, onPress: () => interceptAnonymousUser(() => { @@ -30,3 +36,4 @@ function createCopyLinkAction(params: ContextMenuActionParams): ActionDescriptor } export default createCopyLinkAction; +export type {CopyLinkActionParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.ts b/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.ts index 6a6f8ea43634..a68cc61a8245 100644 --- a/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.ts @@ -1,4 +1,7 @@ import {Str} from 'expensify-common'; +import type {OnyxEntry} from 'react-native-onyx'; +import type {LocaleContextProps, LocalizedTranslate} from '@components/LocaleContextProvider'; +import type useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import Clipboard from '@libs/Clipboard'; import getClipboardText from '@libs/Clipboard/getClipboardText'; import {formatPhoneNumber as formatPhoneNumberPhoneUtils} from '@libs/LocalePhoneNumber'; @@ -62,12 +65,12 @@ import { getTagListUpdatedRequiredMessage, getTravelUpdateMessage, getUpdateACHAccountMessage, - getUpdatedApprovalRuleMessage, getUpdatedAuditRateMessage, getUpdatedAutoHarvestingMessage, getUpdatedBudgetMessage, getUpdatedDefaultTitleMessage, getUpdatedIndividualBudgetNotificationMessage, + getUpdatedApprovalRuleMessage, getUpdatedManualApprovalThresholdMessage, getUpdatedOwnershipMessage, getUpdatedProhibitedExpensesMessage, @@ -137,10 +140,37 @@ import { import {getTaskCreatedMessage, getTaskReportActionMessage} from '@libs/TaskUtils'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; -import type {ReportAction} from '@src/types/onyx'; +import type {Card, Policy, PolicyTagLists, ReportAction, Transaction, Report as ReportType} from '@src/types/onyx'; +import type IconAsset from '@src/types/utils/IconAsset'; import {getActionHtml} from './actionConfig'; -import type {ActionDescriptor} from './ActionDescriptor'; -import type {ContextMenuActionParams, ContextMenuPayload} from './actionTypes'; +import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; + +type CopyMessageClipboardParams = { + reportAction: ReportAction; + transaction: OnyxEntry; + selection: string; + report: OnyxEntry; + card: Card | undefined; + originalReport: OnyxEntry; + isHarvestReport: boolean; + isTryNewDotNVPDismissed: boolean; + movedFromReport: OnyxEntry; + movedToReport: OnyxEntry; + childReport: OnyxEntry; + policy: OnyxEntry; + getLocalDateFromDatetime: LocaleContextProps['getLocalDateFromDatetime']; + policyTags: OnyxEntry; + translate: LocalizedTranslate; + harvestReport: OnyxEntry; + currentUserPersonalDetails: ReturnType; +}; + +type CopyMessageActionParams = BaseContextMenuActionParams & + CopyMessageClipboardParams & { + interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; + copyIcon: IconAsset; + checkmarkIcon: IconAsset; + }; function setClipboardMessage(content: string | undefined) { if (!content) { @@ -154,7 +184,7 @@ function setClipboardMessage(content: string | undefined) { } } -export function copyMessageToClipboard(payload: ContextMenuPayload) { +export function copyMessageToClipboard(params: CopyMessageClipboardParams) { const { reportAction, transaction, @@ -173,7 +203,7 @@ export function copyMessageToClipboard(payload: ContextMenuPayload) { translate, harvestReport, currentUserPersonalDetails, - } = payload; + } = params; const isReportPreviewAction = isReportPreviewActionReportActionsUtils(reportAction); const messageHtml = getActionHtml(reportAction); @@ -496,19 +526,17 @@ export function copyMessageToClipboard(payload: ContextMenuPayload) { } } -function createCopyMessageAction(params: ContextMenuActionParams): ActionDescriptor { - const {payload, icons} = params; - +function createCopyMessageAction({interceptAnonymousUser, translate, copyIcon, checkmarkIcon, ...clipboardParams}: CopyMessageActionParams): ContextMenuAction { return { id: 'copyMessage', - icon: icons.Copy, - text: payload.translate('reportActionContextMenu.copyMessage'), - successText: payload.translate('reportActionContextMenu.copied'), - successIcon: icons.Checkmark, + icon: copyIcon, + text: translate('reportActionContextMenu.copyMessage'), + successText: translate('reportActionContextMenu.copied'), + successIcon: checkmarkIcon, isAnonymousAction: true, onPress: () => - payload.interceptAnonymousUser(() => { - copyMessageToClipboard(payload); + interceptAnonymousUser(() => { + copyMessageToClipboard({...clipboardParams, translate}); hideContextMenu(true, ReportActionComposeFocusManager.focus); }, true), sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_MESSAGE, @@ -516,3 +544,4 @@ function createCopyMessageAction(params: ContextMenuActionParams): ActionDescrip } export default createCopyMessageAction; +export type {CopyMessageActionParams, CopyMessageClipboardParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/copyOnyxDataAction.ts b/src/pages/inbox/report/ContextMenu/actions/copyOnyxDataAction.ts index 632d4bba32bf..518adf74b2a6 100644 --- a/src/pages/inbox/report/ContextMenu/actions/copyOnyxDataAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/copyOnyxDataAction.ts @@ -1,20 +1,26 @@ +import type {OnyxEntry} from 'react-native-onyx'; import Clipboard from '@libs/Clipboard'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; -import type {ActionDescriptor} from './ActionDescriptor'; -import type {ContextMenuActionParams} from './actionTypes'; +import type {Report} from '@src/types/onyx'; +import type IconAsset from '@src/types/utils/IconAsset'; +import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; -function createCopyOnyxDataAction(params: ContextMenuActionParams): ActionDescriptor { - const {report, interceptAnonymousUser, translate} = params.payload; - const {Copy, Checkmark} = params.icons; +type CopyOnyxDataActionParams = BaseContextMenuActionParams & { + report: OnyxEntry; + interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; + copyIcon: IconAsset; + checkmarkIcon: IconAsset; +}; +function createCopyOnyxDataAction({report, interceptAnonymousUser, translate, copyIcon, checkmarkIcon}: CopyOnyxDataActionParams): ContextMenuAction { return { id: 'copyOnyxData', - icon: Copy, + icon: copyIcon, text: translate('reportActionContextMenu.copyOnyxData'), successText: translate('reportActionContextMenu.copied'), - successIcon: Checkmark, + successIcon: checkmarkIcon, isAnonymousAction: true, onPress: () => interceptAnonymousUser(() => { @@ -26,3 +32,4 @@ function createCopyOnyxDataAction(params: ContextMenuActionParams): ActionDescri } export default createCopyOnyxDataAction; +export type {CopyOnyxDataActionParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/copyToClipboardAction.ts b/src/pages/inbox/report/ContextMenu/actions/copyToClipboardAction.ts index 63a11e9d5758..c54df5ae6622 100644 --- a/src/pages/inbox/report/ContextMenu/actions/copyToClipboardAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/copyToClipboardAction.ts @@ -2,19 +2,23 @@ import Clipboard from '@libs/Clipboard'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; -import type {ActionDescriptor} from './ActionDescriptor'; -import type {ContextMenuActionParams} from './actionTypes'; +import type IconAsset from '@src/types/utils/IconAsset'; +import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; -function createCopyToClipboardAction(params: ContextMenuActionParams): ActionDescriptor { - const {selection, interceptAnonymousUser, translate} = params.payload; - const {Copy, Checkmark} = params.icons; +type CopyToClipboardActionParams = BaseContextMenuActionParams & { + selection: string; + interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; + copyIcon: IconAsset; + checkmarkIcon: IconAsset; +}; +function createCopyToClipboardAction({selection, interceptAnonymousUser, translate, copyIcon, checkmarkIcon}: CopyToClipboardActionParams): ContextMenuAction { return { id: 'copyToClipboard', - icon: Copy, + icon: copyIcon, text: translate('common.copyToClipboard'), successText: translate('reportActionContextMenu.copied'), - successIcon: Checkmark, + successIcon: checkmarkIcon, isAnonymousAction: true, onPress: () => interceptAnonymousUser(() => { @@ -26,3 +30,4 @@ function createCopyToClipboardAction(params: ContextMenuActionParams): ActionDes } export default createCopyToClipboardAction; +export type {CopyToClipboardActionParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/copyURLAction.ts b/src/pages/inbox/report/ContextMenu/actions/copyURLAction.ts index 229fdd15e6af..4f58f5108f28 100644 --- a/src/pages/inbox/report/ContextMenu/actions/copyURLAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/copyURLAction.ts @@ -2,19 +2,23 @@ import Clipboard from '@libs/Clipboard'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; -import type {ActionDescriptor} from './ActionDescriptor'; -import type {ContextMenuActionParams} from './actionTypes'; +import type IconAsset from '@src/types/utils/IconAsset'; +import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; -function createCopyURLAction(params: ContextMenuActionParams): ActionDescriptor { - const {selection, interceptAnonymousUser, translate} = params.payload; - const {Copy, Checkmark} = params.icons; +type CopyURLActionParams = BaseContextMenuActionParams & { + selection: string; + interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; + copyIcon: IconAsset; + checkmarkIcon: IconAsset; +}; +function createCopyURLAction({selection, interceptAnonymousUser, translate, copyIcon, checkmarkIcon}: CopyURLActionParams): ContextMenuAction { return { id: 'copyUrl', - icon: Copy, + icon: copyIcon, text: translate('reportActionContextMenu.copyURLToClipboard'), successText: translate('reportActionContextMenu.copied'), - successIcon: Checkmark, + successIcon: checkmarkIcon, description: selection, isAnonymousAction: true, onPress: () => @@ -27,3 +31,4 @@ function createCopyURLAction(params: ContextMenuActionParams): ActionDescriptor } export default createCopyURLAction; +export type {CopyURLActionParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/debugAction.ts b/src/pages/inbox/report/ContextMenu/actions/debugAction.ts index dba10bd97e83..ce4baa1385c5 100644 --- a/src/pages/inbox/report/ContextMenu/actions/debugAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/debugAction.ts @@ -3,16 +3,21 @@ import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManag import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; -import type {ActionDescriptor} from './ActionDescriptor'; -import type {ContextMenuActionParams} from './actionTypes'; +import type {ReportAction} from '@src/types/onyx'; +import type IconAsset from '@src/types/utils/IconAsset'; +import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; -function createDebugAction(params: ContextMenuActionParams): ActionDescriptor { - const {payload, icons} = params; - const {reportID, reportAction, interceptAnonymousUser, translate} = payload; +type DebugActionParams = BaseContextMenuActionParams & { + reportID: string | undefined; + reportAction: ReportAction; + interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; + bugIcon: IconAsset; +}; +function createDebugAction({reportID, reportAction, interceptAnonymousUser, translate, bugIcon}: DebugActionParams): ContextMenuAction { return { id: 'debug', - icon: icons.Bug, + icon: bugIcon, text: translate('debug.debug'), isAnonymousAction: true, onPress: () => @@ -32,3 +37,4 @@ function createDebugAction(params: ContextMenuActionParams): ActionDescriptor { } export default createDebugAction; +export type {DebugActionParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/deleteAction.ts b/src/pages/inbox/report/ContextMenu/actions/deleteAction.ts index 892f89e83596..67960d5acbe9 100644 --- a/src/pages/inbox/report/ContextMenu/actions/deleteAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/deleteAction.ts @@ -1,16 +1,22 @@ import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils'; import {showDeleteModal} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; -import type {ActionDescriptor} from './ActionDescriptor'; -import type {ContextMenuActionParams} from './actionTypes'; +import type {ReportAction} from '@src/types/onyx'; +import type IconAsset from '@src/types/utils/IconAsset'; +import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; -function createDeleteAction(params: ContextMenuActionParams): ActionDescriptor { - const {payload, icons} = params; - const {reportID, reportAction, moneyRequestAction, hideAndRun, translate} = payload; +type DeleteActionParams = BaseContextMenuActionParams & { + reportID: string | undefined; + reportAction: ReportAction; + moneyRequestAction: ReportAction | undefined; + hideAndRun: (callback?: () => void) => void; + trashcanIcon: IconAsset; +}; +function createDeleteAction({reportID, reportAction, moneyRequestAction, hideAndRun, translate, trashcanIcon}: DeleteActionParams): ContextMenuAction { return { id: 'delete', - icon: icons.Trashcan, + icon: trashcanIcon, text: translate('common.delete'), onPress: () => { const iouReportID = isMoneyRequestAction(moneyRequestAction) ? getOriginalMessage(moneyRequestAction)?.IOUReportID : undefined; @@ -23,3 +29,4 @@ function createDeleteAction(params: ContextMenuActionParams): ActionDescriptor { } export default createDeleteAction; +export type {DeleteActionParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/downloadAction.ts b/src/pages/inbox/report/ContextMenu/actions/downloadAction.ts index aeae75552d35..9462ed6473da 100644 --- a/src/pages/inbox/report/ContextMenu/actions/downloadAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/downloadAction.ts @@ -1,3 +1,4 @@ +import type {OnyxEntry} from 'react-native-onyx'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; import {isMobileSafari} from '@libs/Browser'; import fileDownload from '@libs/fileDownload'; @@ -6,22 +7,28 @@ import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManag import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {setDownload} from '@userActions/Download'; import CONST from '@src/CONST'; +import type {Download as DownloadOnyx, ReportAction} from '@src/types/onyx'; +import type IconAsset from '@src/types/utils/IconAsset'; import {getActionHtml} from './actionConfig'; -import type {ActionDescriptor} from './ActionDescriptor'; -import type {ContextMenuActionParams} from './actionTypes'; +import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; -function createDownloadAction(params: ContextMenuActionParams): ActionDescriptor { - const {payload, icons} = params; - const {reportAction, encryptedAuthToken, interceptAnonymousUser, download, translate: payloadTranslate} = payload; +type DownloadActionParams = BaseContextMenuActionParams & { + reportAction: ReportAction; + encryptedAuthToken: string; + interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; + download: OnyxEntry; + downloadIcon: IconAsset; +}; +function createDownloadAction({reportAction, encryptedAuthToken, interceptAnonymousUser, download, translate, downloadIcon}: DownloadActionParams): ContextMenuAction { const isDownloading = download?.isDownloading ?? false; return { id: 'download', - icon: icons.Download, - text: payloadTranslate('common.download'), - successText: payloadTranslate('common.download'), - successIcon: icons.Download, + icon: downloadIcon, + text: translate('common.download'), + successText: translate('common.download'), + successIcon: downloadIcon, isAnonymousAction: true, disabled: isDownloading, shouldShowLoadingSpinnerIcon: isDownloading, @@ -34,7 +41,7 @@ function createDownloadAction(params: ContextMenuActionParams): ActionDescriptor setDownload(sourceID, true); const anchorRegex = CONST.REGEX_LINK_IN_ANCHOR; const isAnchorTag = anchorRegex.test(html); - fileDownload(payloadTranslate, sourceURLWithAuth, originalFileName ?? '', '', isAnchorTag && isMobileSafari()).then(() => setDownload(sourceID, false)); + fileDownload(translate, sourceURLWithAuth, originalFileName ?? '', '', isAnchorTag && isMobileSafari()).then(() => setDownload(sourceID, false)); hideContextMenu(true, ReportActionComposeFocusManager.focus); }, true), sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.DOWNLOAD, @@ -42,3 +49,4 @@ function createDownloadAction(params: ContextMenuActionParams): ActionDescriptor } export default createDownloadAction; +export type {DownloadActionParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/editAction.ts b/src/pages/inbox/report/ContextMenu/actions/editAction.ts index 536a54100530..dad7ae213ab1 100644 --- a/src/pages/inbox/report/ContextMenu/actions/editAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/editAction.ts @@ -1,20 +1,30 @@ +import type {OnyxEntry} from 'react-native-onyx'; import Navigation from '@libs/Navigation/Navigation'; import Parser from '@libs/Parser'; import {isMoneyRequestAction} from '@libs/ReportActionsUtils'; import {deleteReportActionDraft, openReport, saveReportActionDraft} from '@userActions/Report'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; +import type {IntroSelected, ReportAction} from '@src/types/onyx'; +import type IconAsset from '@src/types/utils/IconAsset'; import {getActionHtml} from './actionConfig'; -import type {ActionDescriptor} from './ActionDescriptor'; -import type {ContextMenuActionParams} from './actionTypes'; +import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; -function createEditAction(params: ContextMenuActionParams): ActionDescriptor { - const {payload, icons} = params; - const {reportID, reportAction, moneyRequestAction, draftMessage, introSelected, interceptAnonymousUser, hideAndRun, translate} = payload; +type EditActionParams = BaseContextMenuActionParams & { + reportID: string | undefined; + reportAction: ReportAction; + moneyRequestAction: ReportAction | undefined; + draftMessage: string; + introSelected: OnyxEntry; + interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; + hideAndRun: (callback?: () => void) => void; + pencilIcon: IconAsset; +}; +function createEditAction({reportID, reportAction, moneyRequestAction, draftMessage, introSelected, interceptAnonymousUser, hideAndRun, translate, pencilIcon}: EditActionParams): ContextMenuAction { return { id: 'edit', - icon: icons.Pencil, + icon: pencilIcon, text: translate('reportActionContextMenu.editAction', {action: moneyRequestAction ?? reportAction}), onPress: () => interceptAnonymousUser(() => { @@ -39,3 +49,4 @@ function createEditAction(params: ContextMenuActionParams): ActionDescriptor { } export default createEditAction; +export type {EditActionParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/emojiReactionAction.ts b/src/pages/inbox/report/ContextMenu/actions/emojiReactionAction.ts index 497adccc5c6b..e18a0b2eee98 100644 --- a/src/pages/inbox/report/ContextMenu/actions/emojiReactionAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/emojiReactionAction.ts @@ -1,29 +1,38 @@ import type {OnyxEntry} from 'react-native-onyx'; import type {Emoji} from '@assets/emojis/types'; import {toggleEmojiReaction} from '@userActions/Report'; -import type {ReportActionReactions} from '@src/types/onyx'; -import type {ContextMenuPayload} from './actionTypes'; +import type {ReportAction, ReportActionReactions} from '@src/types/onyx'; type EmojiReactionData = { reportID: string | undefined; - reportAction: ContextMenuPayload['reportAction']; + reportAction: ReportAction | undefined; reportActionID: string | undefined; toggleEmojiAndCloseMenu: (emoji: Emoji, existingReactions: OnyxEntry, preferredSkinTone: number) => void; closeContextMenu: (onHideCallback?: () => void) => void; onPressOpenPicker: () => void; onEmojiPickerClosed: () => void; - interceptAnonymousUser: ContextMenuPayload['interceptAnonymousUser']; + interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; }; -function createEmojiReactionData(payload: ContextMenuPayload): EmojiReactionData { - const {reportID, reportAction, currentUserAccountID, openContextMenu, setIsEmojiPickerActive, hideAndRun, interceptAnonymousUser} = payload; +type EmojiReactionParams = { + reportID: string | undefined; + reportAction: ReportAction | undefined; + currentUserAccountID: number; + openContextMenu: () => void; + setIsEmojiPickerActive: ((state: boolean) => void) | undefined; + hideAndRun: (callback?: () => void) => void; + interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; +}; +function createEmojiReactionData({reportID, reportAction, currentUserAccountID, openContextMenu, setIsEmojiPickerActive, hideAndRun, interceptAnonymousUser}: EmojiReactionParams): EmojiReactionData { const closeContextMenu = (onHideCallback?: () => void) => { hideAndRun(onHideCallback); }; const toggleEmojiAndCloseMenu = (emoji: Emoji, existingReactions: OnyxEntry, preferredSkinTone: number) => { - toggleEmojiReaction(reportID, reportAction, emoji, existingReactions, preferredSkinTone, currentUserAccountID); + if (reportAction) { + toggleEmojiReaction(reportID, reportAction, emoji, existingReactions, preferredSkinTone, currentUserAccountID); + } closeContextMenu(); setIsEmojiPickerActive?.(false); }; @@ -51,4 +60,4 @@ function createEmojiReactionData(payload: ContextMenuPayload): EmojiReactionData } export default createEmojiReactionData; -export type {EmojiReactionData}; +export type {EmojiReactionData, EmojiReactionParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/explainAction.ts b/src/pages/inbox/report/ContextMenu/actions/explainAction.ts index 82de3b9ae771..ffe31b8c18f6 100644 --- a/src/pages/inbox/report/ContextMenu/actions/explainAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/explainAction.ts @@ -1,16 +1,26 @@ +import type {OnyxEntry} from 'react-native-onyx'; +import type useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import {explain} from '@userActions/Report'; import CONST from '@src/CONST'; +import type {ReportAction, Report as ReportType} from '@src/types/onyx'; +import type IconAsset from '@src/types/utils/IconAsset'; import KeyboardUtils from '@src/utils/keyboard'; -import type {ActionDescriptor} from './ActionDescriptor'; -import type {ContextMenuActionParams} from './actionTypes'; +import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; -function createExplainAction(params: ContextMenuActionParams): ActionDescriptor { - const {payload, icons} = params; - const {childReport, originalReport, reportAction, currentUserPersonalDetails, interceptAnonymousUser, hideAndRun, translate} = payload; +type ExplainActionParams = BaseContextMenuActionParams & { + childReport: OnyxEntry; + originalReport: OnyxEntry; + reportAction: ReportAction; + currentUserPersonalDetails: ReturnType; + interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; + hideAndRun: (callback?: () => void) => void; + conciergeIcon: IconAsset; +}; +function createExplainAction({childReport, originalReport, reportAction, currentUserPersonalDetails, interceptAnonymousUser, hideAndRun, translate, conciergeIcon}: ExplainActionParams): ContextMenuAction { return { id: 'explain', - icon: icons.Concierge, + icon: conciergeIcon, text: translate('reportActionContextMenu.explain'), onPress: () => interceptAnonymousUser(() => { @@ -28,3 +38,4 @@ function createExplainAction(params: ContextMenuActionParams): ActionDescriptor } export default createExplainAction; +export type {ExplainActionParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/flagAsOffensiveAction.ts b/src/pages/inbox/report/ContextMenu/actions/flagAsOffensiveAction.ts index 375b0ef62102..b91ff93e9dec 100644 --- a/src/pages/inbox/report/ContextMenu/actions/flagAsOffensiveAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/flagAsOffensiveAction.ts @@ -1,17 +1,22 @@ import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; +import type {ReportAction} from '@src/types/onyx'; +import type IconAsset from '@src/types/utils/IconAsset'; import KeyboardUtils from '@src/utils/keyboard'; -import type {ActionDescriptor} from './ActionDescriptor'; -import type {ContextMenuActionParams} from './actionTypes'; +import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; -function createFlagAsOffensiveAction(params: ContextMenuActionParams): ActionDescriptor { - const {payload, icons} = params; - const {reportID, reportAction, hideAndRun, translate} = payload; +type FlagAsOffensiveActionParams = BaseContextMenuActionParams & { + reportID: string | undefined; + reportAction: ReportAction; + hideAndRun: (callback?: () => void) => void; + flagIcon: IconAsset; +}; +function createFlagAsOffensiveAction({reportID, reportAction, hideAndRun, translate, flagIcon}: FlagAsOffensiveActionParams): ContextMenuAction { return { id: 'flagAsOffensive', - icon: icons.Flag, + icon: flagIcon, text: translate('reportActionContextMenu.flagAsOffensive'), onPress: () => { if (!reportID) { @@ -29,3 +34,4 @@ function createFlagAsOffensiveAction(params: ContextMenuActionParams): ActionDes } export default createFlagAsOffensiveAction; +export type {FlagAsOffensiveActionParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/holdAction.ts b/src/pages/inbox/report/ContextMenu/actions/holdAction.ts index 3636f981343d..f33147417445 100644 --- a/src/pages/inbox/report/ContextMenu/actions/holdAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/holdAction.ts @@ -1,16 +1,23 @@ import {changeMoneyRequestHoldStatus} from '@libs/ReportUtils'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; -import type {ActionDescriptor} from './ActionDescriptor'; -import type {ContextMenuActionParams} from './actionTypes'; +import type {ReportAction} from '@src/types/onyx'; +import type IconAsset from '@src/types/utils/IconAsset'; +import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; -function createHoldAction(params: ContextMenuActionParams): ActionDescriptor { - const {payload, icons} = params; - const {moneyRequestAction, isDelegateAccessRestricted, showDelegateNoAccessModal, interceptAnonymousUser, hideAndRun, translate} = payload; +type HoldActionParams = BaseContextMenuActionParams & { + moneyRequestAction: ReportAction | undefined; + isDelegateAccessRestricted: boolean; + showDelegateNoAccessModal: (() => void) | undefined; + interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; + hideAndRun: (callback?: () => void) => void; + stopwatchIcon: IconAsset; +}; +function createHoldAction({moneyRequestAction, isDelegateAccessRestricted, showDelegateNoAccessModal, interceptAnonymousUser, hideAndRun, translate, stopwatchIcon}: HoldActionParams): ContextMenuAction { return { id: 'hold', - icon: icons.Stopwatch, + icon: stopwatchIcon, text: translate('iou.hold'), onPress: () => interceptAnonymousUser(() => { @@ -25,3 +32,4 @@ function createHoldAction(params: ContextMenuActionParams): ActionDescriptor { } export default createHoldAction; +export type {HoldActionParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/joinThreadAction.ts b/src/pages/inbox/report/ContextMenu/actions/joinThreadAction.ts index 19655d6997b1..0d4328a190fb 100644 --- a/src/pages/inbox/report/ContextMenu/actions/joinThreadAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/joinThreadAction.ts @@ -1,17 +1,25 @@ +import type {OnyxEntry} from 'react-native-onyx'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import {getChildReportNotificationPreference} from '@libs/ReportUtils'; import {toggleSubscribeToChildReport} from '@userActions/Report'; import CONST from '@src/CONST'; -import type {ActionDescriptor} from './ActionDescriptor'; -import type {ContextMenuActionParams} from './actionTypes'; +import type {ReportAction, Report as ReportType} from '@src/types/onyx'; +import type IconAsset from '@src/types/utils/IconAsset'; +import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; -function createJoinThreadAction(params: ContextMenuActionParams): ActionDescriptor { - const {payload, icons} = params; - const {reportAction, originalReport, currentUserAccountID, interceptAnonymousUser, hideAndRun, translate} = payload; +type JoinThreadActionParams = BaseContextMenuActionParams & { + reportAction: ReportAction; + originalReport: OnyxEntry; + currentUserAccountID: number; + interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; + hideAndRun: (callback?: () => void) => void; + bellIcon: IconAsset; +}; +function createJoinThreadAction({reportAction, originalReport, currentUserAccountID, interceptAnonymousUser, hideAndRun, translate, bellIcon}: JoinThreadActionParams): ContextMenuAction { return { id: 'joinThread', - icon: icons.Bell, + icon: bellIcon, text: translate('reportActionContextMenu.joinThread'), onPress: () => interceptAnonymousUser(() => { @@ -26,3 +34,4 @@ function createJoinThreadAction(params: ContextMenuActionParams): ActionDescript } export default createJoinThreadAction; +export type {JoinThreadActionParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.ts b/src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.ts index e315e5e23d83..621a8b927b9a 100644 --- a/src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.ts @@ -1,17 +1,25 @@ +import type {OnyxEntry} from 'react-native-onyx'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import {getChildReportNotificationPreference} from '@libs/ReportUtils'; import {toggleSubscribeToChildReport} from '@userActions/Report'; import CONST from '@src/CONST'; -import type {ActionDescriptor} from './ActionDescriptor'; -import type {ContextMenuActionParams} from './actionTypes'; +import type {ReportAction, Report as ReportType} from '@src/types/onyx'; +import type IconAsset from '@src/types/utils/IconAsset'; +import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; -function createLeaveThreadAction(params: ContextMenuActionParams): ActionDescriptor { - const {payload, icons} = params; - const {reportAction, originalReport, currentUserAccountID, interceptAnonymousUser, hideAndRun, translate} = payload; +type LeaveThreadActionParams = BaseContextMenuActionParams & { + reportAction: ReportAction; + originalReport: OnyxEntry; + currentUserAccountID: number; + interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; + hideAndRun: (callback?: () => void) => void; + exitIcon: IconAsset; +}; +function createLeaveThreadAction({reportAction, originalReport, currentUserAccountID, interceptAnonymousUser, hideAndRun, translate, exitIcon}: LeaveThreadActionParams): ContextMenuAction { return { id: 'leaveThread', - icon: icons.Exit, + icon: exitIcon, text: translate('reportActionContextMenu.leaveThread'), onPress: () => interceptAnonymousUser(() => { @@ -26,3 +34,4 @@ function createLeaveThreadAction(params: ContextMenuActionParams): ActionDescrip } export default createLeaveThreadAction; +export type {LeaveThreadActionParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/markAsReadAction.ts b/src/pages/inbox/report/ContextMenu/actions/markAsReadAction.ts index 8b83634669c5..326be689fbd6 100644 --- a/src/pages/inbox/report/ContextMenu/actions/markAsReadAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/markAsReadAction.ts @@ -1,18 +1,23 @@ import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import {readNewestAction} from '@userActions/Report'; import CONST from '@src/CONST'; -import type {ActionDescriptor} from './ActionDescriptor'; -import type {ContextMenuActionParams} from './actionTypes'; +import type IconAsset from '@src/types/utils/IconAsset'; +import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; -function createMarkAsReadAction(params: ContextMenuActionParams): ActionDescriptor { - const {reportID, interceptAnonymousUser, hideAndRun, translate} = params.payload; - const {Mail, Checkmark} = params.icons; +type MarkAsReadActionParams = BaseContextMenuActionParams & { + reportID: string | undefined; + interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; + hideAndRun: (callback?: () => void) => void; + mailIcon: IconAsset; + checkmarkIcon: IconAsset; +}; +function createMarkAsReadAction({reportID, interceptAnonymousUser, hideAndRun, translate, mailIcon, checkmarkIcon}: MarkAsReadActionParams): ContextMenuAction { return { id: 'markAsRead', - icon: Mail, + icon: mailIcon, text: translate('reportActionContextMenu.markAsRead'), - successIcon: Checkmark, + successIcon: checkmarkIcon, onPress: () => interceptAnonymousUser(() => { readNewestAction(reportID, true, true); @@ -23,3 +28,4 @@ function createMarkAsReadAction(params: ContextMenuActionParams): ActionDescript } export default createMarkAsReadAction; +export type {MarkAsReadActionParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/markAsUnreadAction.ts b/src/pages/inbox/report/ContextMenu/actions/markAsUnreadAction.ts index c927c27ec160..1ef741174916 100644 --- a/src/pages/inbox/report/ContextMenu/actions/markAsUnreadAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/markAsUnreadAction.ts @@ -1,18 +1,38 @@ +import type {OnyxEntry} from 'react-native-onyx'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import {markCommentAsUnread} from '@userActions/Report'; import CONST from '@src/CONST'; -import type {ActionDescriptor} from './ActionDescriptor'; -import type {ContextMenuActionParams} from './actionTypes'; +import type {ReportAction, ReportActions} from '@src/types/onyx'; +import type IconAsset from '@src/types/utils/IconAsset'; +import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; -function createMarkAsUnreadAction(params: ContextMenuActionParams): ActionDescriptor { - const {reportID, reportActions, reportAction, currentUserAccountID, interceptAnonymousUser, hideAndRun, translate} = params.payload; - const {ChatBubbleUnread, Checkmark} = params.icons; +type MarkAsUnreadActionParams = BaseContextMenuActionParams & { + reportID: string | undefined; + reportActions: OnyxEntry; + reportAction: ReportAction; + currentUserAccountID: number; + interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; + hideAndRun: (callback?: () => void) => void; + chatBubbleUnreadIcon: IconAsset; + checkmarkIcon: IconAsset; +}; +function createMarkAsUnreadAction({ + reportID, + reportActions, + reportAction, + currentUserAccountID, + interceptAnonymousUser, + hideAndRun, + translate, + chatBubbleUnreadIcon, + checkmarkIcon, +}: MarkAsUnreadActionParams): ContextMenuAction { return { id: 'markAsUnread', - icon: ChatBubbleUnread, + icon: chatBubbleUnreadIcon, text: translate('reportActionContextMenu.markAsUnread'), - successIcon: Checkmark, + successIcon: checkmarkIcon, onPress: () => interceptAnonymousUser(() => { markCommentAsUnread(reportID, reportActions, reportAction, currentUserAccountID); @@ -23,3 +43,4 @@ function createMarkAsUnreadAction(params: ContextMenuActionParams): ActionDescri } export default createMarkAsUnreadAction; +export type {MarkAsUnreadActionParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/overflowMenuAction.ts b/src/pages/inbox/report/ContextMenu/actions/overflowMenuAction.ts index 8acdbdff7a7f..56fc9366ceba 100644 --- a/src/pages/inbox/report/ContextMenu/actions/overflowMenuAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/overflowMenuAction.ts @@ -1,20 +1,24 @@ import type {RefObject} from 'react'; import type {GestureResponderEvent, View} from 'react-native'; import CONST from '@src/CONST'; -import type {ActionDescriptor} from './ActionDescriptor'; -import type {ContextMenuActionParams} from './actionTypes'; +import type IconAsset from '@src/types/utils/IconAsset'; +import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; -type OverflowMenuDescriptor = ActionDescriptor & { +type OverflowMenuDescriptor = ContextMenuAction & { buttonRef: RefObject; }; -function createOverflowMenuAction(params: ContextMenuActionParams, threeDotRef: RefObject): OverflowMenuDescriptor { - const {payload, icons} = params; - const {openOverflowMenu, openContextMenu, interceptAnonymousUser, translate} = payload; +type OverflowMenuActionParams = BaseContextMenuActionParams & { + openOverflowMenu: (event: GestureResponderEvent | MouseEvent, anchorRef: RefObject) => void; + openContextMenu: () => void; + interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; + threeDotsIcon: IconAsset; +}; +function createOverflowMenuAction({openOverflowMenu, openContextMenu, interceptAnonymousUser, translate, threeDotsIcon}: OverflowMenuActionParams, threeDotRef: RefObject): OverflowMenuDescriptor { return { id: 'overflowMenu', - icon: icons.ThreeDots, + icon: threeDotsIcon, text: translate('reportActionContextMenu.menu'), isAnonymousAction: true, shouldPreventDefaultFocusOnPress: false, @@ -29,4 +33,4 @@ function createOverflowMenuAction(params: ContextMenuActionParams, threeDotRef: } export default createOverflowMenuAction; -export type {OverflowMenuDescriptor}; +export type {OverflowMenuDescriptor, OverflowMenuActionParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/pinAction.ts b/src/pages/inbox/report/ContextMenu/actions/pinAction.ts index 0b31cf1f1ca4..4b4ed8b6cfd1 100644 --- a/src/pages/inbox/report/ContextMenu/actions/pinAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/pinAction.ts @@ -1,16 +1,20 @@ import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import {togglePinnedState} from '@userActions/Report'; import CONST from '@src/CONST'; -import type {ActionDescriptor} from './ActionDescriptor'; -import type {ContextMenuActionParams} from './actionTypes'; +import type IconAsset from '@src/types/utils/IconAsset'; +import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; -function createPinAction(params: ContextMenuActionParams): ActionDescriptor { - const {reportID, interceptAnonymousUser, hideAndRun, translate} = params.payload; - const {Pin} = params.icons; +type PinActionParams = BaseContextMenuActionParams & { + reportID: string | undefined; + interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; + hideAndRun: (callback?: () => void) => void; + pinIcon: IconAsset; +}; +function createPinAction({reportID, interceptAnonymousUser, hideAndRun, translate, pinIcon}: PinActionParams): ContextMenuAction { return { id: 'pin', - icon: Pin, + icon: pinIcon, text: translate('common.pin'), onPress: () => interceptAnonymousUser(() => { @@ -22,3 +26,4 @@ function createPinAction(params: ContextMenuActionParams): ActionDescriptor { } export default createPinAction; +export type {PinActionParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/replyInThreadAction.ts b/src/pages/inbox/report/ContextMenu/actions/replyInThreadAction.ts index 0b9655246503..3c53df94932a 100644 --- a/src/pages/inbox/report/ContextMenu/actions/replyInThreadAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/replyInThreadAction.ts @@ -1,16 +1,34 @@ +import type {OnyxEntry} from 'react-native-onyx'; import {navigateToAndOpenChildReport} from '@userActions/Report'; import CONST from '@src/CONST'; +import type {ReportAction, Report as ReportType} from '@src/types/onyx'; +import type IconAsset from '@src/types/utils/IconAsset'; import KeyboardUtils from '@src/utils/keyboard'; -import type {ActionDescriptor} from './ActionDescriptor'; -import type {ContextMenuActionParams} from './actionTypes'; +import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; -function createReplyInThreadAction(params: ContextMenuActionParams): ActionDescriptor { - const {childReport, reportAction, originalReport, currentUserAccountID, interceptAnonymousUser, hideAndRun, translate} = params.payload; - const {ChatBubbleReply} = params.icons; +type ReplyInThreadActionParams = BaseContextMenuActionParams & { + childReport: OnyxEntry; + reportAction: ReportAction; + originalReport: OnyxEntry; + currentUserAccountID: number; + interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; + hideAndRun: (callback?: () => void) => void; + chatBubbleReplyIcon: IconAsset; +}; +function createReplyInThreadAction({ + childReport, + reportAction, + originalReport, + currentUserAccountID, + interceptAnonymousUser, + hideAndRun, + translate, + chatBubbleReplyIcon, +}: ReplyInThreadActionParams): ContextMenuAction { return { id: 'replyInThread', - icon: ChatBubbleReply, + icon: chatBubbleReplyIcon, text: translate('reportActionContextMenu.replyInThread'), onPress: () => interceptAnonymousUser(() => { @@ -25,3 +43,4 @@ function createReplyInThreadAction(params: ContextMenuActionParams): ActionDescr } export default createReplyInThreadAction; +export type {ReplyInThreadActionParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/unholdAction.ts b/src/pages/inbox/report/ContextMenu/actions/unholdAction.ts index 33986f732974..3c36869f301e 100644 --- a/src/pages/inbox/report/ContextMenu/actions/unholdAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/unholdAction.ts @@ -1,16 +1,23 @@ import {changeMoneyRequestHoldStatus} from '@libs/ReportUtils'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; -import type {ActionDescriptor} from './ActionDescriptor'; -import type {ContextMenuActionParams} from './actionTypes'; +import type {ReportAction} from '@src/types/onyx'; +import type IconAsset from '@src/types/utils/IconAsset'; +import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; -function createUnholdAction(params: ContextMenuActionParams): ActionDescriptor { - const {payload, icons} = params; - const {moneyRequestAction, isDelegateAccessRestricted, showDelegateNoAccessModal, interceptAnonymousUser, hideAndRun, translate} = payload; +type UnholdActionParams = BaseContextMenuActionParams & { + moneyRequestAction: ReportAction | undefined; + isDelegateAccessRestricted: boolean; + showDelegateNoAccessModal: (() => void) | undefined; + interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; + hideAndRun: (callback?: () => void) => void; + stopwatchIcon: IconAsset; +}; +function createUnholdAction({moneyRequestAction, isDelegateAccessRestricted, showDelegateNoAccessModal, interceptAnonymousUser, hideAndRun, translate, stopwatchIcon}: UnholdActionParams): ContextMenuAction { return { id: 'unhold', - icon: icons.Stopwatch, + icon: stopwatchIcon, text: translate('iou.unhold'), onPress: () => interceptAnonymousUser(() => { @@ -25,3 +32,4 @@ function createUnholdAction(params: ContextMenuActionParams): ActionDescriptor { } export default createUnholdAction; +export type {UnholdActionParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/unpinAction.ts b/src/pages/inbox/report/ContextMenu/actions/unpinAction.ts index 6d86a18029cf..33e9fadc8c98 100644 --- a/src/pages/inbox/report/ContextMenu/actions/unpinAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/unpinAction.ts @@ -1,16 +1,20 @@ import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import {togglePinnedState} from '@userActions/Report'; import CONST from '@src/CONST'; -import type {ActionDescriptor} from './ActionDescriptor'; -import type {ContextMenuActionParams} from './actionTypes'; +import type IconAsset from '@src/types/utils/IconAsset'; +import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; -function createUnpinAction(params: ContextMenuActionParams): ActionDescriptor { - const {reportID, interceptAnonymousUser, hideAndRun, translate} = params.payload; - const {Pin} = params.icons; +type UnpinActionParams = BaseContextMenuActionParams & { + reportID: string | undefined; + interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; + hideAndRun: (callback?: () => void) => void; + pinIcon: IconAsset; +}; +function createUnpinAction({reportID, interceptAnonymousUser, hideAndRun, translate, pinIcon}: UnpinActionParams): ContextMenuAction { return { id: 'unpin', - icon: Pin, + icon: pinIcon, text: translate('common.unPin'), onPress: () => interceptAnonymousUser(() => { @@ -22,3 +26,4 @@ function createUnpinAction(params: ContextMenuActionParams): ActionDescriptor { } export default createUnpinAction; +export type {UnpinActionParams}; diff --git a/src/pages/inbox/report/ContextMenu/useReportActionContextMenuData.ts b/src/pages/inbox/report/ContextMenu/useReportActionContextMenuData.ts index 239408433cdd..2280d016892b 100644 --- a/src/pages/inbox/report/ContextMenu/useReportActionContextMenuData.ts +++ b/src/pages/inbox/report/ContextMenu/useReportActionContextMenuData.ts @@ -36,7 +36,6 @@ import type {OriginalMessageIOU, ReportAction} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {ActionID} from './actions/actionConfig'; import {getVisibleActionIDs as getVisibleActionIDsFromConfig, RESTRICTED_READONLY_ACTION_IDS} from './actions/actionConfig'; -import type {ContextMenuPayload} from './actions/actionTypes'; import type {ContextMenuAnchor, ContextMenuType} from './ReportActionContextMenu'; import {hideContextMenu} from './ReportActionContextMenu'; @@ -52,15 +51,7 @@ type UseContextMenuDataParams = { anchor: RefObject | undefined; }; -type UseReportActionContextMenuDataReturn = Omit< - ContextMenuPayload, - 'close' | 'hideAndRun' | 'transitionActionSheetState' | 'openContextMenu' | 'openOverflowMenu' | 'setIsEmojiPickerActive' | 'reportAction' | 'currentUserAccountID' -> & { - reportAction: OnyxEntry; - getVisibleActionIDs: () => ActionID[]; -}; - -function useReportActionContextMenuData({reportID, reportActionID, originalReportID, draftMessage, selection, type, anchor}: UseContextMenuDataParams): UseReportActionContextMenuDataReturn { +function useReportActionContextMenuData({reportID, reportActionID, originalReportID, draftMessage, selection, type, anchor}: UseContextMenuDataParams) { const {translate, getLocalDateFromDatetime} = useLocalize(); const {isOffline} = useNetwork(); const {isProduction} = useEnvironment(); @@ -246,4 +237,4 @@ function useReportActionContextMenuData({reportID, reportActionID, originalRepor } export default useReportActionContextMenuData; -export type {UseContextMenuDataParams, UseReportActionContextMenuDataReturn}; +export type {UseContextMenuDataParams}; From 9560020202b0f95d6105c7270d11b08a06ab2442 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 2 Mar 2026 17:21:23 -0800 Subject: [PATCH 28/88] chore: remove dead code from context menu refactor - Remove unused `transitionActionSheetState` destructures from PopoverReportActionContent, PopoverReportContent, and MiniReportActionContextMenu - Un-export `ACTION_IDS` from actionConfig (only used internally) - Un-export `copyMessageToClipboard` from copyMessageAction (only called internally) - Remove unused type exports (`XxxActionParams`, `OverflowMenuDescriptor`, `CopyMessageClipboardParams`, `EmojiReactionData`, `EmojiReactionParams`) from all 23 factory files - Remove unused `MiniContextMenuActionsContext` and `MiniContextMenuStateContext` exports from MiniContextMenuProvider Made-with: Cursor --- src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx | 2 +- .../report/ContextMenu/MiniReportActionContextMenu/index.tsx | 2 +- .../inbox/report/ContextMenu/PopoverReportActionContent.tsx | 2 +- src/pages/inbox/report/ContextMenu/PopoverReportContent.tsx | 2 +- src/pages/inbox/report/ContextMenu/actions/actionConfig.ts | 2 +- src/pages/inbox/report/ContextMenu/actions/copyEmailAction.ts | 1 - src/pages/inbox/report/ContextMenu/actions/copyLinkAction.ts | 1 - .../inbox/report/ContextMenu/actions/copyMessageAction.ts | 3 +-- .../inbox/report/ContextMenu/actions/copyOnyxDataAction.ts | 1 - .../inbox/report/ContextMenu/actions/copyToClipboardAction.ts | 1 - src/pages/inbox/report/ContextMenu/actions/copyURLAction.ts | 1 - src/pages/inbox/report/ContextMenu/actions/debugAction.ts | 1 - src/pages/inbox/report/ContextMenu/actions/deleteAction.ts | 1 - src/pages/inbox/report/ContextMenu/actions/downloadAction.ts | 1 - src/pages/inbox/report/ContextMenu/actions/editAction.ts | 1 - .../inbox/report/ContextMenu/actions/emojiReactionAction.ts | 1 - src/pages/inbox/report/ContextMenu/actions/explainAction.ts | 1 - .../inbox/report/ContextMenu/actions/flagAsOffensiveAction.ts | 1 - src/pages/inbox/report/ContextMenu/actions/holdAction.ts | 1 - src/pages/inbox/report/ContextMenu/actions/joinThreadAction.ts | 1 - .../inbox/report/ContextMenu/actions/leaveThreadAction.ts | 1 - src/pages/inbox/report/ContextMenu/actions/markAsReadAction.ts | 1 - .../inbox/report/ContextMenu/actions/markAsUnreadAction.ts | 1 - .../inbox/report/ContextMenu/actions/overflowMenuAction.ts | 1 - src/pages/inbox/report/ContextMenu/actions/pinAction.ts | 1 - .../inbox/report/ContextMenu/actions/replyInThreadAction.ts | 1 - src/pages/inbox/report/ContextMenu/actions/unholdAction.ts | 1 - src/pages/inbox/report/ContextMenu/actions/unpinAction.ts | 1 - 28 files changed, 6 insertions(+), 29 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx b/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx index 3502c14669bb..277ec4fdb73e 100644 --- a/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx @@ -120,5 +120,5 @@ function useMiniContextMenuState(): MiniContextMenuState | null { return useContext(MiniContextMenuStateContext); } -export {MiniContextMenuProvider, useMiniContextMenuActions, useMiniContextMenuState, MiniContextMenuActionsContext, MiniContextMenuStateContext}; +export {MiniContextMenuProvider, useMiniContextMenuActions, useMiniContextMenuState}; export type {MiniContextMenuParams, MiniContextMenuState, RowMeasurements, MiniContextMenuActions}; diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index 347194a27618..c3beac4cf2c4 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -49,7 +49,7 @@ function MiniReportActionContextMenu() { const {hideMiniContextMenu, cancelHide} = miniActions; const {shouldUseNarrowLayout} = useResponsiveLayout(); const StyleUtils = useStyleUtils(); - const {transitionActionSheetState} = ActionSheetAwareScrollView.useActionSheetAwareScrollViewActions(); + ActionSheetAwareScrollView.useActionSheetAwareScrollViewActions(); const icons = useMemoizedLazyExpensifyIcons(CONTEXT_MENU_ICON_NAMES); const threeDotRef = useRef(null); diff --git a/src/pages/inbox/report/ContextMenu/PopoverReportActionContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverReportActionContent.tsx index 2daf00bd93bc..9e7072762f7f 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverReportActionContent.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverReportActionContent.tsx @@ -46,7 +46,7 @@ import type {PopoverContentProps} from './PopoverContextMenu'; import {showContextMenu} from './ReportActionContextMenu'; import useReportActionContextMenuData from './useReportActionContextMenuData'; -function PopoverReportActionContent({menuState, hideAndRun, setLocalShouldKeepOpen, transitionActionSheetState, contentRef, shouldEnableArrowNavigation}: PopoverContentProps) { +function PopoverReportActionContent({menuState, hideAndRun, setLocalShouldKeepOpen, contentRef, shouldEnableArrowNavigation}: PopoverContentProps) { const {shouldUseNarrowLayout} = useResponsiveLayout(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); diff --git a/src/pages/inbox/report/ContextMenu/PopoverReportContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverReportContent.tsx index 6b10027d1006..3d4065b27f19 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverReportContent.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverReportContent.tsx @@ -15,7 +15,7 @@ import {createCopyOnyxDataAction, createDebugAction, createMarkAsReadAction, cre import type {PopoverContentProps} from './PopoverContextMenu'; import useReportContextMenuData from './useReportContextMenuData'; -function PopoverReportContent({menuState, hideAndRun, setLocalShouldKeepOpen, transitionActionSheetState, contentRef, shouldEnableArrowNavigation}: PopoverContentProps) { +function PopoverReportContent({menuState, hideAndRun, setLocalShouldKeepOpen, contentRef, shouldEnableArrowNavigation}: PopoverContentProps) { const {shouldUseNarrowLayout} = useResponsiveLayout(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); diff --git a/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts b/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts index b423bc5f5095..822fa5707908 100644 --- a/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts +++ b/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts @@ -308,5 +308,5 @@ function getVisibleActionIDs(shouldShowArgs: ShouldShowArgs, disabledActionIDs: return ORDERED_ACTION_SHOULD_SHOW.filter((entry) => entry.id !== 'overflowMenu' && !disabledActionIDs.has(entry.id) && entry.shouldShow(shouldShowArgs)).map((entry) => entry.id); } -export {ACTION_IDS, ORDERED_ACTION_SHOULD_SHOW, RESTRICTED_READONLY_ACTION_IDS, getActionHtml, getVisibleActionIDs}; +export {ORDERED_ACTION_SHOULD_SHOW, RESTRICTED_READONLY_ACTION_IDS, getActionHtml, getVisibleActionIDs}; export type {ActionID, ShouldShowArgs}; diff --git a/src/pages/inbox/report/ContextMenu/actions/copyEmailAction.ts b/src/pages/inbox/report/ContextMenu/actions/copyEmailAction.ts index 3e431dfde6fb..667ac6054a22 100644 --- a/src/pages/inbox/report/ContextMenu/actions/copyEmailAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/copyEmailAction.ts @@ -32,4 +32,3 @@ function createCopyEmailAction({selection, interceptAnonymousUser, translate, co } export default createCopyEmailAction; -export type {CopyEmailActionParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/copyLinkAction.ts b/src/pages/inbox/report/ContextMenu/actions/copyLinkAction.ts index 0a341a273cb4..10ef73c05839 100644 --- a/src/pages/inbox/report/ContextMenu/actions/copyLinkAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/copyLinkAction.ts @@ -36,4 +36,3 @@ function createCopyLinkAction({reportAction, originalReportID, interceptAnonymou } export default createCopyLinkAction; -export type {CopyLinkActionParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.ts b/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.ts index a68cc61a8245..ffd5903b9f03 100644 --- a/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.ts @@ -184,7 +184,7 @@ function setClipboardMessage(content: string | undefined) { } } -export function copyMessageToClipboard(params: CopyMessageClipboardParams) { +function copyMessageToClipboard(params: CopyMessageClipboardParams) { const { reportAction, transaction, @@ -544,4 +544,3 @@ function createCopyMessageAction({interceptAnonymousUser, translate, copyIcon, c } export default createCopyMessageAction; -export type {CopyMessageActionParams, CopyMessageClipboardParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/copyOnyxDataAction.ts b/src/pages/inbox/report/ContextMenu/actions/copyOnyxDataAction.ts index 518adf74b2a6..7214e48e9a7b 100644 --- a/src/pages/inbox/report/ContextMenu/actions/copyOnyxDataAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/copyOnyxDataAction.ts @@ -32,4 +32,3 @@ function createCopyOnyxDataAction({report, interceptAnonymousUser, translate, co } export default createCopyOnyxDataAction; -export type {CopyOnyxDataActionParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/copyToClipboardAction.ts b/src/pages/inbox/report/ContextMenu/actions/copyToClipboardAction.ts index c54df5ae6622..6cf784b20989 100644 --- a/src/pages/inbox/report/ContextMenu/actions/copyToClipboardAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/copyToClipboardAction.ts @@ -30,4 +30,3 @@ function createCopyToClipboardAction({selection, interceptAnonymousUser, transla } export default createCopyToClipboardAction; -export type {CopyToClipboardActionParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/copyURLAction.ts b/src/pages/inbox/report/ContextMenu/actions/copyURLAction.ts index 4f58f5108f28..ab0241d85c61 100644 --- a/src/pages/inbox/report/ContextMenu/actions/copyURLAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/copyURLAction.ts @@ -31,4 +31,3 @@ function createCopyURLAction({selection, interceptAnonymousUser, translate, copy } export default createCopyURLAction; -export type {CopyURLActionParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/debugAction.ts b/src/pages/inbox/report/ContextMenu/actions/debugAction.ts index ce4baa1385c5..7916e0aea85d 100644 --- a/src/pages/inbox/report/ContextMenu/actions/debugAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/debugAction.ts @@ -37,4 +37,3 @@ function createDebugAction({reportID, reportAction, interceptAnonymousUser, tran } export default createDebugAction; -export type {DebugActionParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/deleteAction.ts b/src/pages/inbox/report/ContextMenu/actions/deleteAction.ts index 67960d5acbe9..e5f2bc9fe49e 100644 --- a/src/pages/inbox/report/ContextMenu/actions/deleteAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/deleteAction.ts @@ -29,4 +29,3 @@ function createDeleteAction({reportID, reportAction, moneyRequestAction, hideAnd } export default createDeleteAction; -export type {DeleteActionParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/downloadAction.ts b/src/pages/inbox/report/ContextMenu/actions/downloadAction.ts index 9462ed6473da..2b4a3b890034 100644 --- a/src/pages/inbox/report/ContextMenu/actions/downloadAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/downloadAction.ts @@ -49,4 +49,3 @@ function createDownloadAction({reportAction, encryptedAuthToken, interceptAnonym } export default createDownloadAction; -export type {DownloadActionParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/editAction.ts b/src/pages/inbox/report/ContextMenu/actions/editAction.ts index dad7ae213ab1..bbc375302f57 100644 --- a/src/pages/inbox/report/ContextMenu/actions/editAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/editAction.ts @@ -49,4 +49,3 @@ function createEditAction({reportID, reportAction, moneyRequestAction, draftMess } export default createEditAction; -export type {EditActionParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/emojiReactionAction.ts b/src/pages/inbox/report/ContextMenu/actions/emojiReactionAction.ts index e18a0b2eee98..92e8377215fa 100644 --- a/src/pages/inbox/report/ContextMenu/actions/emojiReactionAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/emojiReactionAction.ts @@ -60,4 +60,3 @@ function createEmojiReactionData({reportID, reportAction, currentUserAccountID, } export default createEmojiReactionData; -export type {EmojiReactionData, EmojiReactionParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/explainAction.ts b/src/pages/inbox/report/ContextMenu/actions/explainAction.ts index ffe31b8c18f6..e7d05e01051c 100644 --- a/src/pages/inbox/report/ContextMenu/actions/explainAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/explainAction.ts @@ -38,4 +38,3 @@ function createExplainAction({childReport, originalReport, reportAction, current } export default createExplainAction; -export type {ExplainActionParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/flagAsOffensiveAction.ts b/src/pages/inbox/report/ContextMenu/actions/flagAsOffensiveAction.ts index b91ff93e9dec..080c1239d9cc 100644 --- a/src/pages/inbox/report/ContextMenu/actions/flagAsOffensiveAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/flagAsOffensiveAction.ts @@ -34,4 +34,3 @@ function createFlagAsOffensiveAction({reportID, reportAction, hideAndRun, transl } export default createFlagAsOffensiveAction; -export type {FlagAsOffensiveActionParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/holdAction.ts b/src/pages/inbox/report/ContextMenu/actions/holdAction.ts index f33147417445..65986e5635e5 100644 --- a/src/pages/inbox/report/ContextMenu/actions/holdAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/holdAction.ts @@ -32,4 +32,3 @@ function createHoldAction({moneyRequestAction, isDelegateAccessRestricted, showD } export default createHoldAction; -export type {HoldActionParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/joinThreadAction.ts b/src/pages/inbox/report/ContextMenu/actions/joinThreadAction.ts index 0d4328a190fb..166feb78bf8d 100644 --- a/src/pages/inbox/report/ContextMenu/actions/joinThreadAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/joinThreadAction.ts @@ -34,4 +34,3 @@ function createJoinThreadAction({reportAction, originalReport, currentUserAccoun } export default createJoinThreadAction; -export type {JoinThreadActionParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.ts b/src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.ts index 621a8b927b9a..76db6a8a9c4a 100644 --- a/src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.ts @@ -34,4 +34,3 @@ function createLeaveThreadAction({reportAction, originalReport, currentUserAccou } export default createLeaveThreadAction; -export type {LeaveThreadActionParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/markAsReadAction.ts b/src/pages/inbox/report/ContextMenu/actions/markAsReadAction.ts index 326be689fbd6..43e72ed458e2 100644 --- a/src/pages/inbox/report/ContextMenu/actions/markAsReadAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/markAsReadAction.ts @@ -28,4 +28,3 @@ function createMarkAsReadAction({reportID, interceptAnonymousUser, hideAndRun, t } export default createMarkAsReadAction; -export type {MarkAsReadActionParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/markAsUnreadAction.ts b/src/pages/inbox/report/ContextMenu/actions/markAsUnreadAction.ts index 1ef741174916..035a6d8bc341 100644 --- a/src/pages/inbox/report/ContextMenu/actions/markAsUnreadAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/markAsUnreadAction.ts @@ -43,4 +43,3 @@ function createMarkAsUnreadAction({ } export default createMarkAsUnreadAction; -export type {MarkAsUnreadActionParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/overflowMenuAction.ts b/src/pages/inbox/report/ContextMenu/actions/overflowMenuAction.ts index 56fc9366ceba..3482efefaddc 100644 --- a/src/pages/inbox/report/ContextMenu/actions/overflowMenuAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/overflowMenuAction.ts @@ -33,4 +33,3 @@ function createOverflowMenuAction({openOverflowMenu, openContextMenu, interceptA } export default createOverflowMenuAction; -export type {OverflowMenuDescriptor, OverflowMenuActionParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/pinAction.ts b/src/pages/inbox/report/ContextMenu/actions/pinAction.ts index 4b4ed8b6cfd1..3508c83848fe 100644 --- a/src/pages/inbox/report/ContextMenu/actions/pinAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/pinAction.ts @@ -26,4 +26,3 @@ function createPinAction({reportID, interceptAnonymousUser, hideAndRun, translat } export default createPinAction; -export type {PinActionParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/replyInThreadAction.ts b/src/pages/inbox/report/ContextMenu/actions/replyInThreadAction.ts index 3c53df94932a..4658a7574157 100644 --- a/src/pages/inbox/report/ContextMenu/actions/replyInThreadAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/replyInThreadAction.ts @@ -43,4 +43,3 @@ function createReplyInThreadAction({ } export default createReplyInThreadAction; -export type {ReplyInThreadActionParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/unholdAction.ts b/src/pages/inbox/report/ContextMenu/actions/unholdAction.ts index 3c36869f301e..6f24073b8ee2 100644 --- a/src/pages/inbox/report/ContextMenu/actions/unholdAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/unholdAction.ts @@ -32,4 +32,3 @@ function createUnholdAction({moneyRequestAction, isDelegateAccessRestricted, sho } export default createUnholdAction; -export type {UnholdActionParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/unpinAction.ts b/src/pages/inbox/report/ContextMenu/actions/unpinAction.ts index 33e9fadc8c98..e579011c8ab4 100644 --- a/src/pages/inbox/report/ContextMenu/actions/unpinAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/unpinAction.ts @@ -26,4 +26,3 @@ function createUnpinAction({reportID, interceptAnonymousUser, hideAndRun, transl } export default createUnpinAction; -export type {UnpinActionParams}; From c953db4c8dc68cce0cf45b4c0ee416f489763315 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 2 Mar 2026 17:28:12 -0800 Subject: [PATCH 29/88] refactor: remove ContextMenuAction barrel file Replace the aggregator with direct imports from individual factory files. The barrel was a leftover from the namespaced component pattern and adds no value for plain factory functions. Made-with: Cursor --- .../MiniReportActionContextMenu/index.tsx | 34 ++++++------- .../PopoverReportActionContent.tsx | 48 +++++++++--------- .../ContextMenu/PopoverReportContent.tsx | 7 ++- .../ContextMenu/actions/ContextMenuAction.ts | 49 ------------------- 4 files changed, 45 insertions(+), 93 deletions(-) delete mode 100644 src/pages/inbox/report/ContextMenu/actions/ContextMenuAction.ts diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index c3beac4cf2c4..e14c0900f13f 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -16,24 +16,22 @@ import getButtonState from '@libs/getButtonState'; import type {ActionID} from '@pages/inbox/report/ContextMenu/actions/actionConfig'; import type {ContextMenuAction} from '@pages/inbox/report/ContextMenu/actions/actionTypes'; import {CONTEXT_MENU_ICON_NAMES} from '@pages/inbox/report/ContextMenu/actions/actionTypes'; -import { - createCopyLinkAction, - createCopyMessageAction, - createDeleteAction, - createDownloadAction, - createEditAction, - createEmojiReactionData, - createExplainAction, - createFlagAsOffensiveAction, - createHoldAction, - createJoinThreadAction, - createLeaveThreadAction, - createMarkAsReadAction, - createMarkAsUnreadAction, - createOverflowMenuAction, - createReplyInThreadAction, - createUnholdAction, -} from '@pages/inbox/report/ContextMenu/actions/ContextMenuAction'; +import createCopyLinkAction from '@pages/inbox/report/ContextMenu/actions/copyLinkAction'; +import createCopyMessageAction from '@pages/inbox/report/ContextMenu/actions/copyMessageAction'; +import createDeleteAction from '@pages/inbox/report/ContextMenu/actions/deleteAction'; +import createDownloadAction from '@pages/inbox/report/ContextMenu/actions/downloadAction'; +import createEditAction from '@pages/inbox/report/ContextMenu/actions/editAction'; +import createEmojiReactionData from '@pages/inbox/report/ContextMenu/actions/emojiReactionAction'; +import createExplainAction from '@pages/inbox/report/ContextMenu/actions/explainAction'; +import createFlagAsOffensiveAction from '@pages/inbox/report/ContextMenu/actions/flagAsOffensiveAction'; +import createHoldAction from '@pages/inbox/report/ContextMenu/actions/holdAction'; +import createJoinThreadAction from '@pages/inbox/report/ContextMenu/actions/joinThreadAction'; +import createLeaveThreadAction from '@pages/inbox/report/ContextMenu/actions/leaveThreadAction'; +import createMarkAsReadAction from '@pages/inbox/report/ContextMenu/actions/markAsReadAction'; +import createMarkAsUnreadAction from '@pages/inbox/report/ContextMenu/actions/markAsUnreadAction'; +import createOverflowMenuAction from '@pages/inbox/report/ContextMenu/actions/overflowMenuAction'; +import createReplyInThreadAction from '@pages/inbox/report/ContextMenu/actions/replyInThreadAction'; +import createUnholdAction from '@pages/inbox/report/ContextMenu/actions/unholdAction'; import {useMiniContextMenuActions, useMiniContextMenuState} from '@pages/inbox/report/ContextMenu/MiniContextMenuProvider'; import type {ContextMenuAnchor} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {showContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; diff --git a/src/pages/inbox/report/ContextMenu/PopoverReportActionContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverReportActionContent.tsx index 9e7072762f7f..f5141d23ed24 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverReportActionContent.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverReportActionContent.tsx @@ -17,31 +17,29 @@ import type {ActionID} from './actions/actionConfig'; import {ORDERED_ACTION_SHOULD_SHOW} from './actions/actionConfig'; import type {ContextMenuAction} from './actions/actionTypes'; import {CONTEXT_MENU_ICON_NAMES} from './actions/actionTypes'; -import { - createCopyEmailAction, - createCopyLinkAction, - createCopyMessageAction, - createCopyOnyxDataAction, - createCopyToClipboardAction, - createCopyURLAction, - createDebugAction, - createDeleteAction, - createDownloadAction, - createEditAction, - createEmojiReactionData, - createExplainAction, - createFlagAsOffensiveAction, - createHoldAction, - createJoinThreadAction, - createLeaveThreadAction, - createMarkAsReadAction, - createMarkAsUnreadAction, - createOverflowMenuAction, - createPinAction, - createReplyInThreadAction, - createUnholdAction, - createUnpinAction, -} from './actions/ContextMenuAction'; +import createCopyEmailAction from './actions/copyEmailAction'; +import createCopyLinkAction from './actions/copyLinkAction'; +import createCopyMessageAction from './actions/copyMessageAction'; +import createCopyOnyxDataAction from './actions/copyOnyxDataAction'; +import createCopyToClipboardAction from './actions/copyToClipboardAction'; +import createCopyURLAction from './actions/copyURLAction'; +import createDebugAction from './actions/debugAction'; +import createDeleteAction from './actions/deleteAction'; +import createDownloadAction from './actions/downloadAction'; +import createEditAction from './actions/editAction'; +import createEmojiReactionData from './actions/emojiReactionAction'; +import createExplainAction from './actions/explainAction'; +import createFlagAsOffensiveAction from './actions/flagAsOffensiveAction'; +import createHoldAction from './actions/holdAction'; +import createJoinThreadAction from './actions/joinThreadAction'; +import createLeaveThreadAction from './actions/leaveThreadAction'; +import createMarkAsReadAction from './actions/markAsReadAction'; +import createMarkAsUnreadAction from './actions/markAsUnreadAction'; +import createOverflowMenuAction from './actions/overflowMenuAction'; +import createPinAction from './actions/pinAction'; +import createReplyInThreadAction from './actions/replyInThreadAction'; +import createUnholdAction from './actions/unholdAction'; +import createUnpinAction from './actions/unpinAction'; import type {PopoverContentProps} from './PopoverContextMenu'; import {showContextMenu} from './ReportActionContextMenu'; import useReportActionContextMenuData from './useReportActionContextMenuData'; diff --git a/src/pages/inbox/report/ContextMenu/PopoverReportContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverReportContent.tsx index 3d4065b27f19..6fee2c581ed1 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverReportContent.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverReportContent.tsx @@ -11,7 +11,12 @@ import CONST from '@src/CONST'; import type {ActionID} from './actions/actionConfig'; import type {ContextMenuAction} from './actions/actionTypes'; import {CONTEXT_MENU_ICON_NAMES} from './actions/actionTypes'; -import {createCopyOnyxDataAction, createDebugAction, createMarkAsReadAction, createMarkAsUnreadAction, createPinAction, createUnpinAction} from './actions/ContextMenuAction'; +import createCopyOnyxDataAction from './actions/copyOnyxDataAction'; +import createDebugAction from './actions/debugAction'; +import createMarkAsReadAction from './actions/markAsReadAction'; +import createMarkAsUnreadAction from './actions/markAsUnreadAction'; +import createPinAction from './actions/pinAction'; +import createUnpinAction from './actions/unpinAction'; import type {PopoverContentProps} from './PopoverContextMenu'; import useReportContextMenuData from './useReportContextMenuData'; diff --git a/src/pages/inbox/report/ContextMenu/actions/ContextMenuAction.ts b/src/pages/inbox/report/ContextMenu/actions/ContextMenuAction.ts deleted file mode 100644 index 0a13da635c72..000000000000 --- a/src/pages/inbox/report/ContextMenu/actions/ContextMenuAction.ts +++ /dev/null @@ -1,49 +0,0 @@ -import createCopyEmailAction from './copyEmailAction'; -import createCopyLinkAction from './copyLinkAction'; -import createCopyMessageAction from './copyMessageAction'; -import createCopyOnyxDataAction from './copyOnyxDataAction'; -import createCopyToClipboardAction from './copyToClipboardAction'; -import createCopyURLAction from './copyURLAction'; -import createDebugAction from './debugAction'; -import createDeleteAction from './deleteAction'; -import createDownloadAction from './downloadAction'; -import createEditAction from './editAction'; -import createEmojiReactionData from './emojiReactionAction'; -import createExplainAction from './explainAction'; -import createFlagAsOffensiveAction from './flagAsOffensiveAction'; -import createHoldAction from './holdAction'; -import createJoinThreadAction from './joinThreadAction'; -import createLeaveThreadAction from './leaveThreadAction'; -import createMarkAsReadAction from './markAsReadAction'; -import createMarkAsUnreadAction from './markAsUnreadAction'; -import createOverflowMenuAction from './overflowMenuAction'; -import createPinAction from './pinAction'; -import createReplyInThreadAction from './replyInThreadAction'; -import createUnholdAction from './unholdAction'; -import createUnpinAction from './unpinAction'; - -export { - createEmojiReactionData, - createReplyInThreadAction, - createMarkAsUnreadAction, - createExplainAction, - createMarkAsReadAction, - createEditAction, - createUnholdAction, - createHoldAction, - createJoinThreadAction, - createLeaveThreadAction, - createCopyURLAction, - createCopyToClipboardAction, - createCopyEmailAction, - createCopyMessageAction, - createCopyLinkAction, - createPinAction, - createUnpinAction, - createFlagAsOffensiveAction, - createDownloadAction, - createCopyOnyxDataAction, - createDebugAction, - createDeleteAction, - createOverflowMenuAction, -}; From 56e85eeaf0de3b17614f091ea36486fc879de81d Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 2 Mar 2026 18:14:00 -0800 Subject: [PATCH 30/88] refactor(contextmenu): eliminate centralized shouldShow config Co-locate shouldShow predicates into factory files with narrow param types, removing the centralized ORDERED_ACTION_SHOULD_SHOW array, ShouldShowArgs type, and getVisibleActionIDs function. Each consumer now determines visibility inline using the co-located shouldShow functions, and dead cross-type actions are removed from each consumer. Made-with: Cursor --- src/components/ShowContextMenuContext.ts | 1 - .../index.native.tsx | 2 +- .../MiniReportActionContextMenu/index.tsx | 407 +++++++++------ .../PopoverReportActionContent.tsx | 487 ++++++++++-------- .../ContextMenu/PopoverReportContent.tsx | 134 ++--- .../ContextMenu/actions/actionConfig.ts | 279 +--------- .../ContextMenu/actions/copyLinkAction.ts | 12 + .../ContextMenu/actions/copyMessageAction.ts | 11 +- .../ContextMenu/actions/copyOnyxDataAction.ts | 5 + .../report/ContextMenu/actions/debugAction.ts | 6 + .../ContextMenu/actions/deleteAction.ts | 41 +- .../ContextMenu/actions/downloadAction.ts | 9 + .../report/ContextMenu/actions/editAction.ts | 28 +- .../actions/emojiReactionAction.ts | 18 +- .../ContextMenu/actions/explainAction.ts | 20 +- .../actions/flagAsOffensiveAction.ts | 17 + .../report/ContextMenu/actions/holdAction.ts | 37 +- .../ContextMenu/actions/joinThreadAction.ts | 36 +- .../ContextMenu/actions/leaveThreadAction.ts | 34 +- .../ContextMenu/actions/markAsReadAction.ts | 5 + .../ContextMenu/actions/markAsUnreadAction.ts | 10 + .../report/ContextMenu/actions/pinAction.ts | 5 + .../actions/replyInThreadAction.ts | 19 + .../ContextMenu/actions/unholdAction.ts | 37 +- .../report/ContextMenu/actions/unpinAction.ts | 5 + .../useReportActionContextMenuData.ts | 29 +- .../ContextMenu/useReportContextMenuData.ts | 31 +- 27 files changed, 919 insertions(+), 806 deletions(-) diff --git a/src/components/ShowContextMenuContext.ts b/src/components/ShowContextMenuContext.ts index 8cac7fa04072..1fccfda97d01 100644 --- a/src/components/ShowContextMenuContext.ts +++ b/src/components/ShowContextMenuContext.ts @@ -1,5 +1,4 @@ import {createContext} from 'react'; -// eslint-disable-next-line no-restricted-imports import type {GestureResponderEvent} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.native.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.native.tsx index 0617a41515ff..9304b28c1140 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.native.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.native.tsx @@ -1,2 +1,2 @@ -// Mini context menu only renders on web via createPortal +// Mini context menu only renders on web export default () => null; diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index e14c0900f13f..7d6fe61d3b26 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -13,25 +13,24 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import getButtonState from '@libs/getButtonState'; -import type {ActionID} from '@pages/inbox/report/ContextMenu/actions/actionConfig'; -import type {ContextMenuAction} from '@pages/inbox/report/ContextMenu/actions/actionTypes'; +import {ACTION_IDS} from '@pages/inbox/report/ContextMenu/actions/actionConfig'; import {CONTEXT_MENU_ICON_NAMES} from '@pages/inbox/report/ContextMenu/actions/actionTypes'; -import createCopyLinkAction from '@pages/inbox/report/ContextMenu/actions/copyLinkAction'; -import createCopyMessageAction from '@pages/inbox/report/ContextMenu/actions/copyMessageAction'; -import createDeleteAction from '@pages/inbox/report/ContextMenu/actions/deleteAction'; -import createDownloadAction from '@pages/inbox/report/ContextMenu/actions/downloadAction'; -import createEditAction from '@pages/inbox/report/ContextMenu/actions/editAction'; -import createEmojiReactionData from '@pages/inbox/report/ContextMenu/actions/emojiReactionAction'; -import createExplainAction from '@pages/inbox/report/ContextMenu/actions/explainAction'; -import createFlagAsOffensiveAction from '@pages/inbox/report/ContextMenu/actions/flagAsOffensiveAction'; -import createHoldAction from '@pages/inbox/report/ContextMenu/actions/holdAction'; -import createJoinThreadAction from '@pages/inbox/report/ContextMenu/actions/joinThreadAction'; -import createLeaveThreadAction from '@pages/inbox/report/ContextMenu/actions/leaveThreadAction'; -import createMarkAsReadAction from '@pages/inbox/report/ContextMenu/actions/markAsReadAction'; -import createMarkAsUnreadAction from '@pages/inbox/report/ContextMenu/actions/markAsUnreadAction'; +import type {ContextMenuAction} from '@pages/inbox/report/ContextMenu/actions/actionTypes'; +import createCopyLinkAction, {shouldShowCopyLinkAction} from '@pages/inbox/report/ContextMenu/actions/copyLinkAction'; +import createCopyMessageAction, {shouldShowCopyMessageAction} from '@pages/inbox/report/ContextMenu/actions/copyMessageAction'; +import createDeleteAction, {shouldShowDeleteAction} from '@pages/inbox/report/ContextMenu/actions/deleteAction'; +import createDownloadAction, {shouldShowDownloadAction} from '@pages/inbox/report/ContextMenu/actions/downloadAction'; +import createEditAction, {shouldShowEditAction} from '@pages/inbox/report/ContextMenu/actions/editAction'; +import createEmojiReactionData, {shouldShowEmojiReaction} from '@pages/inbox/report/ContextMenu/actions/emojiReactionAction'; +import createExplainAction, {shouldShowExplainAction} from '@pages/inbox/report/ContextMenu/actions/explainAction'; +import createFlagAsOffensiveAction, {shouldShowFlagAsOffensiveAction} from '@pages/inbox/report/ContextMenu/actions/flagAsOffensiveAction'; +import createHoldAction, {shouldShowHoldAction} from '@pages/inbox/report/ContextMenu/actions/holdAction'; +import createJoinThreadAction, {shouldShowJoinThreadAction} from '@pages/inbox/report/ContextMenu/actions/joinThreadAction'; +import createLeaveThreadAction, {shouldShowLeaveThreadAction} from '@pages/inbox/report/ContextMenu/actions/leaveThreadAction'; +import createMarkAsUnreadAction, {shouldShowMarkAsUnreadForReportAction} from '@pages/inbox/report/ContextMenu/actions/markAsUnreadAction'; import createOverflowMenuAction from '@pages/inbox/report/ContextMenu/actions/overflowMenuAction'; -import createReplyInThreadAction from '@pages/inbox/report/ContextMenu/actions/replyInThreadAction'; -import createUnholdAction from '@pages/inbox/report/ContextMenu/actions/unholdAction'; +import createReplyInThreadAction, {shouldShowReplyInThreadAction} from '@pages/inbox/report/ContextMenu/actions/replyInThreadAction'; +import createUnholdAction, {shouldShowUnholdAction} from '@pages/inbox/report/ContextMenu/actions/unholdAction'; import {useMiniContextMenuActions, useMiniContextMenuState} from '@pages/inbox/report/ContextMenu/MiniContextMenuProvider'; import type {ContextMenuAnchor} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {showContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; @@ -106,8 +105,6 @@ function MiniReportActionContextMenu() { anchor: state?.anchor, }); - const visibleActionIDs = useMemo(() => new Set(data.getVisibleActionIDs()), [data]); - const hideAndRun = (callback?: () => void) => { miniActions.release(); callback?.(); @@ -142,152 +139,240 @@ function MiniReportActionContextMenu() { // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style const reportAction = (data.reportAction ?? null) as NonNullable; const currentUserAccountID = data.currentUserPersonalDetails?.accountID ?? 0; - const {interceptAnonymousUser, translate} = data; + const {interceptAnonymousUser, translate, disabledActionIDs} = data; - /* eslint-disable react-hooks/refs -- factory functions store refs for later use, they don't read .current during render */ - const allActions: ContextMenuAction[] = [ - createReplyInThreadAction({ - childReport: data.childReport, - reportAction, - originalReport: data.originalReport, - currentUserAccountID, - interceptAnonymousUser, - hideAndRun, - translate, - chatBubbleReplyIcon: icons.ChatBubbleReply, - }), - createMarkAsUnreadAction({ - reportID: data.reportID, - reportActions: data.reportActions, - reportAction, - currentUserAccountID, - interceptAnonymousUser, - hideAndRun, - translate, - chatBubbleUnreadIcon: icons.ChatBubbleUnread, - checkmarkIcon: icons.Checkmark, - }), - createExplainAction({ - childReport: data.childReport, - originalReport: data.originalReport, - reportAction, - currentUserPersonalDetails: data.currentUserPersonalDetails, - interceptAnonymousUser, - hideAndRun, - translate, - conciergeIcon: icons.Concierge, - }), - createMarkAsReadAction({ - reportID: data.reportID, - interceptAnonymousUser, - hideAndRun, - translate, - mailIcon: icons.Mail, - checkmarkIcon: icons.Checkmark, - }), - createEditAction({ + const isDisabled = (id: string) => disabledActionIDs.has(id); + + const showReplyInThread = + shouldShowReplyInThreadAction({ + reportAction: data.reportAction, reportID: data.reportID, - reportAction, + isThreadReportParentAction: data.isThreadReportParentAction, + isArchivedRoom: data.isArchivedRoom, + }) && !isDisabled(ACTION_IDS.REPLY_IN_THREAD); + const showMarkAsUnread = shouldShowMarkAsUnreadForReportAction({reportAction: data.reportAction}) && !isDisabled(ACTION_IDS.MARK_AS_UNREAD); + const showExplain = shouldShowExplainAction({reportAction: data.reportAction, isArchivedRoom: data.isArchivedRoom}) && !isDisabled(ACTION_IDS.EXPLAIN); + const showEdit = + shouldShowEditAction({reportAction: data.reportAction, isArchivedRoom: data.isArchivedRoom, isChronosReport: data.isChronosReport, moneyRequestAction: data.moneyRequestAction}) && + !isDisabled(ACTION_IDS.EDIT); + const showUnhold = + shouldShowUnholdAction({ + moneyRequestReport: data.moneyRequestReport, moneyRequestAction: data.moneyRequestAction, - draftMessage: data.draftMessage, - introSelected: data.introSelected, - interceptAnonymousUser, - hideAndRun, - translate, - pencilIcon: icons.Pencil, - }), - createUnholdAction({ + moneyRequestPolicy: data.moneyRequestPolicy, + areHoldRequirementsMet: data.areHoldRequirementsMet, + iouTransaction: data.iouTransaction, + }) && !isDisabled(ACTION_IDS.UNHOLD); + const showHold = + shouldShowHoldAction({ + moneyRequestReport: data.moneyRequestReport, moneyRequestAction: data.moneyRequestAction, - isDelegateAccessRestricted: data.isDelegateAccessRestricted, - showDelegateNoAccessModal: data.showDelegateNoAccessModal, - interceptAnonymousUser, - hideAndRun, - translate, - stopwatchIcon: icons.Stopwatch, - }), - createHoldAction({ - moneyRequestAction: data.moneyRequestAction, - isDelegateAccessRestricted: data.isDelegateAccessRestricted, - showDelegateNoAccessModal: data.showDelegateNoAccessModal, - interceptAnonymousUser, - hideAndRun, - translate, - stopwatchIcon: icons.Stopwatch, - }), - createJoinThreadAction({ - reportAction, - originalReport: data.originalReport, - currentUserAccountID, - interceptAnonymousUser, - hideAndRun, - translate, - bellIcon: icons.Bell, - }), - createLeaveThreadAction({ - reportAction, - originalReport: data.originalReport, - currentUserAccountID, - interceptAnonymousUser, - hideAndRun, - translate, - exitIcon: icons.Exit, - }), - createCopyMessageAction({ - reportAction, - transaction: data.transaction, - selection: data.selection, - report: data.report, - card: data.card, - originalReport: data.originalReport, + moneyRequestPolicy: data.moneyRequestPolicy, + areHoldRequirementsMet: data.areHoldRequirementsMet, + iouTransaction: data.iouTransaction, + }) && !isDisabled(ACTION_IDS.HOLD); + const showJoinThread = + shouldShowJoinThreadAction({ + reportAction: data.reportAction, + isArchivedRoom: data.isArchivedRoom, + isThreadReportParentAction: data.isThreadReportParentAction, isHarvestReport: data.isHarvestReport, - isTryNewDotNVPDismissed: data.isTryNewDotNVPDismissed, - movedFromReport: data.movedFromReport, - movedToReport: data.movedToReport, - childReport: data.childReport, - policy: data.policy, - getLocalDateFromDatetime: data.getLocalDateFromDatetime, - policyTags: data.policyTags, - translate, - harvestReport: data.harvestReport, - currentUserPersonalDetails: data.currentUserPersonalDetails, - interceptAnonymousUser, - copyIcon: icons.Copy, - checkmarkIcon: icons.Checkmark, - }), - createCopyLinkAction({ - reportAction, - originalReportID: data.originalReportID, - interceptAnonymousUser, - translate, - linkCopyIcon: icons.LinkCopy, - checkmarkIcon: icons.Checkmark, - }), - createFlagAsOffensiveAction({ - reportID: data.reportID, - reportAction, - hideAndRun, - translate, - flagIcon: icons.Flag, - }), - createDownloadAction({ - reportAction, - encryptedAuthToken: data.encryptedAuthToken, - interceptAnonymousUser, - download: data.download, - translate, - downloadIcon: icons.Download, - }), - createDeleteAction({ + }) && !isDisabled(ACTION_IDS.JOIN_THREAD); + const showLeaveThread = + shouldShowLeaveThreadAction({ + reportAction: data.reportAction, + isArchivedRoom: data.isArchivedRoom, + isThreadReportParentAction: data.isThreadReportParentAction, + isHarvestReport: data.isHarvestReport, + }) && !isDisabled(ACTION_IDS.LEAVE_THREAD); + const showCopyMessage = shouldShowCopyMessageAction({reportAction: data.reportAction}) && !isDisabled(ACTION_IDS.COPY_MESSAGE); + const showCopyLink = shouldShowCopyLinkAction({reportAction: data.reportAction, menuTarget: data.anchor}) && !isDisabled(ACTION_IDS.COPY_LINK); + const showFlagAsOffensive = + shouldShowFlagAsOffensiveAction({reportAction: data.reportAction, isArchivedRoom: data.isArchivedRoom, isChronosReport: data.isChronosReport, reportID: data.reportID}) && + !isDisabled(ACTION_IDS.FLAG_AS_OFFENSIVE); + const showDownload = shouldShowDownloadAction({reportAction: data.reportAction, isOffline: data.isOffline}) && !isDisabled(ACTION_IDS.DOWNLOAD); + const showDelete = + shouldShowDeleteAction({ + reportAction: data.reportAction, + isArchivedRoom: data.isArchivedRoom, + isChronosReport: data.isChronosReport, reportID: data.reportID, - reportAction, moneyRequestAction: data.moneyRequestAction, - hideAndRun, - translate, - trashcanIcon: icons.Trashcan, - }), - ]; + iouTransaction: data.iouTransaction, + transactions: data.transactions, + childReportActions: data.childReportActions, + }) && !isDisabled(ACTION_IDS.DELETE); + + /* eslint-disable react-hooks/refs -- factory functions store refs for later use, they don't read .current during render */ + const visibleActions = useMemo(() => { + const items: ContextMenuAction[] = []; + if (showReplyInThread) { + items.push( + createReplyInThreadAction({ + childReport: data.childReport, + reportAction, + originalReport: data.originalReport, + currentUserAccountID, + interceptAnonymousUser, + hideAndRun, + translate, + chatBubbleReplyIcon: icons.ChatBubbleReply, + }), + ); + } + if (showMarkAsUnread) { + items.push( + createMarkAsUnreadAction({ + reportID: data.reportID, + reportActions: data.reportActions, + reportAction, + currentUserAccountID, + interceptAnonymousUser, + hideAndRun, + translate, + chatBubbleUnreadIcon: icons.ChatBubbleUnread, + checkmarkIcon: icons.Checkmark, + }), + ); + } + if (showExplain) { + items.push( + createExplainAction({ + childReport: data.childReport, + originalReport: data.originalReport, + reportAction, + currentUserPersonalDetails: data.currentUserPersonalDetails, + interceptAnonymousUser, + hideAndRun, + translate, + conciergeIcon: icons.Concierge, + }), + ); + } + if (showEdit) { + items.push( + createEditAction({ + reportID: data.reportID, + reportAction, + moneyRequestAction: data.moneyRequestAction, + draftMessage: data.draftMessage, + introSelected: data.introSelected, + interceptAnonymousUser, + hideAndRun, + translate, + pencilIcon: icons.Pencil, + }), + ); + } + if (showUnhold) { + items.push( + createUnholdAction({ + moneyRequestAction: data.moneyRequestAction, + isDelegateAccessRestricted: data.isDelegateAccessRestricted, + showDelegateNoAccessModal: data.showDelegateNoAccessModal, + interceptAnonymousUser, + hideAndRun, + translate, + stopwatchIcon: icons.Stopwatch, + }), + ); + } + if (showHold) { + items.push( + createHoldAction({ + moneyRequestAction: data.moneyRequestAction, + isDelegateAccessRestricted: data.isDelegateAccessRestricted, + showDelegateNoAccessModal: data.showDelegateNoAccessModal, + interceptAnonymousUser, + hideAndRun, + translate, + stopwatchIcon: icons.Stopwatch, + }), + ); + } + if (showJoinThread) { + items.push( + createJoinThreadAction({reportAction, originalReport: data.originalReport, currentUserAccountID, interceptAnonymousUser, hideAndRun, translate, bellIcon: icons.Bell}), + ); + } + if (showLeaveThread) { + items.push( + createLeaveThreadAction({reportAction, originalReport: data.originalReport, currentUserAccountID, interceptAnonymousUser, hideAndRun, translate, exitIcon: icons.Exit}), + ); + } + if (showCopyMessage) { + items.push( + createCopyMessageAction({ + reportAction, + transaction: data.transaction, + selection: data.selection, + report: data.report, + card: data.card, + originalReport: data.originalReport, + isHarvestReport: data.isHarvestReport, + isTryNewDotNVPDismissed: data.isTryNewDotNVPDismissed, + movedFromReport: data.movedFromReport, + movedToReport: data.movedToReport, + childReport: data.childReport, + policy: data.policy, + getLocalDateFromDatetime: data.getLocalDateFromDatetime, + policyTags: data.policyTags, + translate, + harvestReport: data.harvestReport, + currentUserPersonalDetails: data.currentUserPersonalDetails, + interceptAnonymousUser, + copyIcon: icons.Copy, + checkmarkIcon: icons.Checkmark, + }), + ); + } + if (showCopyLink) { + items.push( + createCopyLinkAction({ + reportAction, + originalReportID: data.originalReportID, + interceptAnonymousUser, + translate, + linkCopyIcon: icons.LinkCopy, + checkmarkIcon: icons.Checkmark, + }), + ); + } + if (showFlagAsOffensive) { + items.push(createFlagAsOffensiveAction({reportID: data.reportID, reportAction, hideAndRun, translate, flagIcon: icons.Flag})); + } + if (showDownload) { + items.push( + createDownloadAction({reportAction, encryptedAuthToken: data.encryptedAuthToken, interceptAnonymousUser, download: data.download, translate, downloadIcon: icons.Download}), + ); + } + if (showDelete) { + items.push(createDeleteAction({reportID: data.reportID, reportAction, moneyRequestAction: data.moneyRequestAction, hideAndRun, translate, trashcanIcon: icons.Trashcan})); + } + return items; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + showReplyInThread, + showMarkAsUnread, + showExplain, + showEdit, + showUnhold, + showHold, + showJoinThread, + showLeaveThread, + showCopyMessage, + showCopyLink, + showFlagAsOffensive, + showDownload, + showDelete, + data, + reportAction, + currentUserAccountID, + interceptAnonymousUser, + translate, + icons, + ]); - const actions = allActions.filter((action) => visibleActionIDs.has(action.id as ActionID)); const emojiData = createEmojiReactionData({ reportID: data.reportID, reportAction: data.reportAction, @@ -309,9 +394,9 @@ function MiniReportActionContextMenu() { ); /* eslint-enable react-hooks/refs */ - const hasEmoji = visibleActionIDs.has('emojiReaction') && !!emojiData.reportAction && !!emojiData.reportActionID; - const needsOverflow = actions.length > CONST.MINI_CONTEXT_MENU_MAX_ITEMS; - const visibleActions = needsOverflow ? actions.slice(0, CONST.MINI_CONTEXT_MENU_MAX_ITEMS - 1) : actions; + const hasEmoji = shouldShowEmojiReaction({reportAction: data.reportAction}) && !!emojiData.reportAction && !!emojiData.reportActionID; + const needsOverflow = visibleActions.length > CONST.MINI_CONTEXT_MENU_MAX_ITEMS; + const displayedActions = needsOverflow ? visibleActions.slice(0, CONST.MINI_CONTEXT_MENU_MAX_ITEMS - 1) : visibleActions; if (!state) { return null; @@ -350,7 +435,7 @@ function MiniReportActionContextMenu() { reportAction={emojiData.reportAction} /> )} - {visibleActions.map((action: ContextMenuAction) => ( + {displayedActions.map((action: ContextMenuAction) => ( new Set(data.getVisibleActionIDs()), [data]); - const openOverflowMenu = (event: GestureResponderEvent | MouseEvent, anchorRefParam: RefObject) => { showContextMenu({ type: CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, @@ -93,199 +83,185 @@ function PopoverReportActionContent({menuState, hideAndRun, setLocalShouldKeepOp // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style const reportAction = (data.reportAction ?? null) as NonNullable; const currentUserAccountID = data.currentUserPersonalDetails?.accountID ?? 0; - const {interceptAnonymousUser, translate} = data; + const {interceptAnonymousUser, translate, disabledActionIDs} = data; - /* eslint-disable react-hooks/refs -- factory functions store refs for later use, they don't read .current during render */ - const allActions: ContextMenuAction[] = [ - createReplyInThreadAction({ - childReport: data.childReport, - reportAction, - originalReport: data.originalReport, - currentUserAccountID, - interceptAnonymousUser, - hideAndRun, - translate, - chatBubbleReplyIcon: icons.ChatBubbleReply, - }), - createMarkAsUnreadAction({ - reportID: data.reportID, - reportActions: data.reportActions, - reportAction, - currentUserAccountID, - interceptAnonymousUser, - hideAndRun, - translate, - chatBubbleUnreadIcon: icons.ChatBubbleUnread, - checkmarkIcon: icons.Checkmark, - }), - createExplainAction({ - childReport: data.childReport, - originalReport: data.originalReport, - reportAction, - currentUserPersonalDetails: data.currentUserPersonalDetails, - interceptAnonymousUser, - hideAndRun, - translate, - conciergeIcon: icons.Concierge, - }), - createMarkAsReadAction({ - reportID: data.reportID, - interceptAnonymousUser, - hideAndRun, - translate, - mailIcon: icons.Mail, - checkmarkIcon: icons.Checkmark, - }), - createEditAction({ + const isDisabled = (id: string) => disabledActionIDs.has(id); + + const showReplyInThread = + shouldShowReplyInThreadAction({ + reportAction: data.reportAction, reportID: data.reportID, - reportAction, + isThreadReportParentAction: data.isThreadReportParentAction, + isArchivedRoom: data.isArchivedRoom, + }) && !isDisabled(ACTION_IDS.REPLY_IN_THREAD); + const showMarkAsUnread = shouldShowMarkAsUnreadForReportAction({reportAction: data.reportAction}) && !isDisabled(ACTION_IDS.MARK_AS_UNREAD); + const showExplain = shouldShowExplainAction({reportAction: data.reportAction, isArchivedRoom: data.isArchivedRoom}) && !isDisabled(ACTION_IDS.EXPLAIN); + const showEdit = + shouldShowEditAction({reportAction: data.reportAction, isArchivedRoom: data.isArchivedRoom, isChronosReport: data.isChronosReport, moneyRequestAction: data.moneyRequestAction}) && + !isDisabled(ACTION_IDS.EDIT); + const showUnhold = + shouldShowUnholdAction({ + moneyRequestReport: data.moneyRequestReport, moneyRequestAction: data.moneyRequestAction, - draftMessage: data.draftMessage, - introSelected: data.introSelected, - interceptAnonymousUser, - hideAndRun, - translate, - pencilIcon: icons.Pencil, - }), - createUnholdAction({ - moneyRequestAction: data.moneyRequestAction, - isDelegateAccessRestricted: data.isDelegateAccessRestricted, - showDelegateNoAccessModal: data.showDelegateNoAccessModal, - interceptAnonymousUser, - hideAndRun, - translate, - stopwatchIcon: icons.Stopwatch, - }), - createHoldAction({ + moneyRequestPolicy: data.moneyRequestPolicy, + areHoldRequirementsMet: data.areHoldRequirementsMet, + iouTransaction: data.iouTransaction, + }) && !isDisabled(ACTION_IDS.UNHOLD); + const showHold = + shouldShowHoldAction({ + moneyRequestReport: data.moneyRequestReport, moneyRequestAction: data.moneyRequestAction, - isDelegateAccessRestricted: data.isDelegateAccessRestricted, - showDelegateNoAccessModal: data.showDelegateNoAccessModal, - interceptAnonymousUser, - hideAndRun, - translate, - stopwatchIcon: icons.Stopwatch, - }), - createJoinThreadAction({ - reportAction, - originalReport: data.originalReport, - currentUserAccountID, - interceptAnonymousUser, - hideAndRun, - translate, - bellIcon: icons.Bell, - }), - createLeaveThreadAction({ - reportAction, - originalReport: data.originalReport, - currentUserAccountID, - interceptAnonymousUser, - hideAndRun, - translate, - exitIcon: icons.Exit, - }), - createCopyURLAction({ - selection: data.selection, - interceptAnonymousUser, - translate, - copyIcon: icons.Copy, - checkmarkIcon: icons.Checkmark, - }), - createCopyToClipboardAction({ - selection: data.selection, - interceptAnonymousUser, - translate, - copyIcon: icons.Copy, - checkmarkIcon: icons.Checkmark, - }), - createCopyEmailAction({ - selection: data.selection, - interceptAnonymousUser, - translate, - copyIcon: icons.Copy, - checkmarkIcon: icons.Checkmark, - }), - createCopyMessageAction({ - reportAction, - transaction: data.transaction, - selection: data.selection, - report: data.report, - card: data.card, - originalReport: data.originalReport, + moneyRequestPolicy: data.moneyRequestPolicy, + areHoldRequirementsMet: data.areHoldRequirementsMet, + iouTransaction: data.iouTransaction, + }) && !isDisabled(ACTION_IDS.HOLD); + const showJoinThread = + shouldShowJoinThreadAction({ + reportAction: data.reportAction, + isArchivedRoom: data.isArchivedRoom, + isThreadReportParentAction: data.isThreadReportParentAction, isHarvestReport: data.isHarvestReport, - isTryNewDotNVPDismissed: data.isTryNewDotNVPDismissed, - movedFromReport: data.movedFromReport, - movedToReport: data.movedToReport, - childReport: data.childReport, - policy: data.policy, - getLocalDateFromDatetime: data.getLocalDateFromDatetime, - policyTags: data.policyTags, - translate, - harvestReport: data.harvestReport, - currentUserPersonalDetails: data.currentUserPersonalDetails, - interceptAnonymousUser, - copyIcon: icons.Copy, - checkmarkIcon: icons.Checkmark, - }), - createCopyLinkAction({ - reportAction, - originalReportID: data.originalReportID, - interceptAnonymousUser, - translate, - linkCopyIcon: icons.LinkCopy, - checkmarkIcon: icons.Checkmark, - }), - createPinAction({ - reportID: data.reportID, - interceptAnonymousUser, - hideAndRun, - translate, - pinIcon: icons.Pin, - }), - createUnpinAction({ - reportID: data.reportID, - interceptAnonymousUser, - hideAndRun, - translate, - pinIcon: icons.Pin, - }), - createFlagAsOffensiveAction({ - reportID: data.reportID, - reportAction, - hideAndRun, - translate, - flagIcon: icons.Flag, - }), - createDownloadAction({ - reportAction, - encryptedAuthToken: data.encryptedAuthToken, - interceptAnonymousUser, - download: data.download, - translate, - downloadIcon: icons.Download, - }), - createCopyOnyxDataAction({ - report: data.report, - interceptAnonymousUser, - translate, - copyIcon: icons.Copy, - checkmarkIcon: icons.Checkmark, - }), - createDebugAction({ - reportID: data.reportID, - reportAction, - interceptAnonymousUser, - translate, - bugIcon: icons.Bug, - }), - createDeleteAction({ + }) && !isDisabled(ACTION_IDS.JOIN_THREAD); + const showLeaveThread = + shouldShowLeaveThreadAction({ + reportAction: data.reportAction, + isArchivedRoom: data.isArchivedRoom, + isThreadReportParentAction: data.isThreadReportParentAction, + isHarvestReport: data.isHarvestReport, + }) && !isDisabled(ACTION_IDS.LEAVE_THREAD); + const showCopyMessage = shouldShowCopyMessageAction({reportAction: data.reportAction}) && !isDisabled(ACTION_IDS.COPY_MESSAGE); + const showCopyLink = shouldShowCopyLinkAction({reportAction: data.reportAction, menuTarget: data.anchor}) && !isDisabled(ACTION_IDS.COPY_LINK); + const showFlagAsOffensive = + shouldShowFlagAsOffensiveAction({reportAction: data.reportAction, isArchivedRoom: data.isArchivedRoom, isChronosReport: data.isChronosReport, reportID: data.reportID}) && + !isDisabled(ACTION_IDS.FLAG_AS_OFFENSIVE); + const showDownload = shouldShowDownloadAction({reportAction: data.reportAction, isOffline: data.isOffline}) && !isDisabled(ACTION_IDS.DOWNLOAD); + const showDebug = shouldShowDebugAction({isDebugModeEnabled: data.isDebugModeEnabled}) && !isDisabled(ACTION_IDS.DEBUG); + const showDelete = + shouldShowDeleteAction({ + reportAction: data.reportAction, + isArchivedRoom: data.isArchivedRoom, + isChronosReport: data.isChronosReport, reportID: data.reportID, - reportAction, moneyRequestAction: data.moneyRequestAction, - hideAndRun, - translate, - trashcanIcon: icons.Trashcan, - }), - ]; + iouTransaction: data.iouTransaction, + transactions: data.transactions, + childReportActions: data.childReportActions, + }) && !isDisabled(ACTION_IDS.DELETE); + + /* eslint-disable react-hooks/refs -- factory functions store refs for later use, they don't read .current during render */ + const replyInThreadAction = showReplyInThread + ? createReplyInThreadAction({ + childReport: data.childReport, + reportAction, + originalReport: data.originalReport, + currentUserAccountID, + interceptAnonymousUser, + hideAndRun, + translate, + chatBubbleReplyIcon: icons.ChatBubbleReply, + }) + : undefined; + const markAsUnreadAction = showMarkAsUnread + ? createMarkAsUnreadAction({ + reportID: data.reportID, + reportActions: data.reportActions, + reportAction, + currentUserAccountID, + interceptAnonymousUser, + hideAndRun, + translate, + chatBubbleUnreadIcon: icons.ChatBubbleUnread, + checkmarkIcon: icons.Checkmark, + }) + : undefined; + const explainActionItem = showExplain + ? createExplainAction({ + childReport: data.childReport, + originalReport: data.originalReport, + reportAction, + currentUserPersonalDetails: data.currentUserPersonalDetails, + interceptAnonymousUser, + hideAndRun, + translate, + conciergeIcon: icons.Concierge, + }) + : undefined; + const editActionItem = showEdit + ? createEditAction({ + reportID: data.reportID, + reportAction, + moneyRequestAction: data.moneyRequestAction, + draftMessage: data.draftMessage, + introSelected: data.introSelected, + interceptAnonymousUser, + hideAndRun, + translate, + pencilIcon: icons.Pencil, + }) + : undefined; + const unholdActionItem = showUnhold + ? createUnholdAction({ + moneyRequestAction: data.moneyRequestAction, + isDelegateAccessRestricted: data.isDelegateAccessRestricted, + showDelegateNoAccessModal: data.showDelegateNoAccessModal, + interceptAnonymousUser, + hideAndRun, + translate, + stopwatchIcon: icons.Stopwatch, + }) + : undefined; + const holdActionItem = showHold + ? createHoldAction({ + moneyRequestAction: data.moneyRequestAction, + isDelegateAccessRestricted: data.isDelegateAccessRestricted, + showDelegateNoAccessModal: data.showDelegateNoAccessModal, + interceptAnonymousUser, + hideAndRun, + translate, + stopwatchIcon: icons.Stopwatch, + }) + : undefined; + const joinThreadActionItem = showJoinThread + ? createJoinThreadAction({reportAction, originalReport: data.originalReport, currentUserAccountID, interceptAnonymousUser, hideAndRun, translate, bellIcon: icons.Bell}) + : undefined; + const leaveThreadActionItem = showLeaveThread + ? createLeaveThreadAction({reportAction, originalReport: data.originalReport, currentUserAccountID, interceptAnonymousUser, hideAndRun, translate, exitIcon: icons.Exit}) + : undefined; + const copyMessageActionItem = showCopyMessage + ? createCopyMessageAction({ + reportAction, + transaction: data.transaction, + selection: data.selection, + report: data.report, + card: data.card, + originalReport: data.originalReport, + isHarvestReport: data.isHarvestReport, + isTryNewDotNVPDismissed: data.isTryNewDotNVPDismissed, + movedFromReport: data.movedFromReport, + movedToReport: data.movedToReport, + childReport: data.childReport, + policy: data.policy, + getLocalDateFromDatetime: data.getLocalDateFromDatetime, + policyTags: data.policyTags, + translate, + harvestReport: data.harvestReport, + currentUserPersonalDetails: data.currentUserPersonalDetails, + interceptAnonymousUser, + copyIcon: icons.Copy, + checkmarkIcon: icons.Checkmark, + }) + : undefined; + const copyLinkActionItem = showCopyLink + ? createCopyLinkAction({reportAction, originalReportID: data.originalReportID, interceptAnonymousUser, translate, linkCopyIcon: icons.LinkCopy, checkmarkIcon: icons.Checkmark}) + : undefined; + const flagAsOffensiveActionItem = showFlagAsOffensive ? createFlagAsOffensiveAction({reportID: data.reportID, reportAction, hideAndRun, translate, flagIcon: icons.Flag}) : undefined; + const downloadActionItem = showDownload + ? createDownloadAction({reportAction, encryptedAuthToken: data.encryptedAuthToken, interceptAnonymousUser, download: data.download, translate, downloadIcon: icons.Download}) + : undefined; + const debugActionItem = showDebug ? createDebugAction({reportID: data.reportID, reportAction, interceptAnonymousUser, translate, bugIcon: icons.Bug}) : undefined; + const deleteActionItem = showDelete + ? createDeleteAction({reportID: data.reportID, reportAction, moneyRequestAction: data.moneyRequestAction, hideAndRun, translate, trashcanIcon: icons.Trashcan}) + : undefined; const overflowMenu = createOverflowMenuAction( { @@ -297,8 +273,71 @@ function PopoverReportActionContent({menuState, hideAndRun, setLocalShouldKeepOp }, overflowMenuRef, ); - const actionsWithOverflow = [...allActions, overflowMenu]; - const actions = actionsWithOverflow.filter((action) => visibleActionIDs.has(action.id as ActionID)); + /* eslint-enable react-hooks/refs */ + + const visibleActions = useMemo(() => { + const items: ContextMenuAction[] = []; + if (replyInThreadAction) { + items.push(replyInThreadAction); + } + if (markAsUnreadAction) { + items.push(markAsUnreadAction); + } + if (explainActionItem) { + items.push(explainActionItem); + } + if (editActionItem) { + items.push(editActionItem); + } + if (unholdActionItem) { + items.push(unholdActionItem); + } + if (holdActionItem) { + items.push(holdActionItem); + } + if (joinThreadActionItem) { + items.push(joinThreadActionItem); + } + if (leaveThreadActionItem) { + items.push(leaveThreadActionItem); + } + if (copyMessageActionItem) { + items.push(copyMessageActionItem); + } + if (copyLinkActionItem) { + items.push(copyLinkActionItem); + } + if (flagAsOffensiveActionItem) { + items.push(flagAsOffensiveActionItem); + } + if (downloadActionItem) { + items.push(downloadActionItem); + } + if (debugActionItem) { + items.push(debugActionItem); + } + if (deleteActionItem) { + items.push(deleteActionItem); + } + items.push(overflowMenu); + return items; + }, [ + replyInThreadAction, + markAsUnreadAction, + explainActionItem, + editActionItem, + unholdActionItem, + holdActionItem, + joinThreadActionItem, + leaveThreadActionItem, + copyMessageActionItem, + copyLinkActionItem, + flagAsOffensiveActionItem, + downloadActionItem, + debugActionItem, + deleteActionItem, + overflowMenu, + ]); const emojiData = createEmojiReactionData({ reportID: data.reportID, @@ -309,23 +348,15 @@ function PopoverReportActionContent({menuState, hideAndRun, setLocalShouldKeepOp hideAndRun, interceptAnonymousUser, }); - /* eslint-enable react-hooks/refs */ - - const contentActionIndexes = actions - .map((action, index) => { - const entry = ORDERED_ACTION_SHOULD_SHOW.find((e) => e.id === action.id); - return entry?.isContentAction ? index : undefined; - }) - .filter((index): index is number => index !== undefined); const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({ initialFocusedIndex: -1, - disabledIndexes: contentActionIndexes, - maxIndex: actions.length - 1, + disabledIndexes: [], + maxIndex: visibleActions.length - 1, isActive: shouldEnableArrowNavigation, }); - const hasEmoji = visibleActionIDs.has('emojiReaction'); + const hasEmoji = shouldShowEmojiReaction({reportAction: data.reportAction}); const wrapperStyle = StyleUtils.getReportActionContextMenuStyles(false, shouldUseNarrowLayout); return ( @@ -351,7 +382,7 @@ function PopoverReportActionContent({menuState, hideAndRun, setLocalShouldKeepOp }} /> )} - {actions.map((action: ContextMenuAction, i: number) => ( + {visibleActions.map((action: ContextMenuAction, i: number) => ( setFocusedIndex(i)} - onBlur={() => (i === actions.length - 1 || i === 1) && setFocusedIndex(-1)} + onBlur={() => (i === visibleActions.length - 1 || i === 1) && setFocusedIndex(-1)} disabled={action.disabled} shouldShowLoadingSpinnerIcon={action.shouldShowLoadingSpinnerIcon} sentryLabel={action.sentryLabel} diff --git a/src/pages/inbox/report/ContextMenu/PopoverReportContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverReportContent.tsx index 6fee2c581ed1..0e4a06a23960 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverReportContent.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverReportContent.tsx @@ -7,20 +7,19 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import CONST from '@src/CONST'; -import type {ActionID} from './actions/actionConfig'; -import type {ContextMenuAction} from './actions/actionTypes'; +import {ACTION_IDS} from './actions/actionConfig'; import {CONTEXT_MENU_ICON_NAMES} from './actions/actionTypes'; -import createCopyOnyxDataAction from './actions/copyOnyxDataAction'; -import createDebugAction from './actions/debugAction'; -import createMarkAsReadAction from './actions/markAsReadAction'; -import createMarkAsUnreadAction from './actions/markAsUnreadAction'; -import createPinAction from './actions/pinAction'; -import createUnpinAction from './actions/unpinAction'; +import type {ContextMenuAction} from './actions/actionTypes'; +import createCopyOnyxDataAction, {shouldShowCopyOnyxDataAction} from './actions/copyOnyxDataAction'; +import createDebugAction, {shouldShowDebugAction} from './actions/debugAction'; +import createMarkAsReadAction, {shouldShowMarkAsReadAction} from './actions/markAsReadAction'; +import createMarkAsUnreadAction, {shouldShowMarkAsUnreadForReport} from './actions/markAsUnreadAction'; +import createPinAction, {shouldShowPinAction} from './actions/pinAction'; +import createUnpinAction, {shouldShowUnpinAction} from './actions/unpinAction'; import type {PopoverContentProps} from './PopoverContextMenu'; import useReportContextMenuData from './useReportContextMenuData'; -function PopoverReportContent({menuState, hideAndRun, setLocalShouldKeepOpen, contentRef, shouldEnableArrowNavigation}: PopoverContentProps) { +function PopoverReportContent({menuState, hideAndRun, contentRef, shouldEnableArrowNavigation}: PopoverContentProps) { const {shouldUseNarrowLayout} = useResponsiveLayout(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -34,72 +33,73 @@ function PopoverReportContent({menuState, hideAndRun, setLocalShouldKeepOpen, co originalReportID: menuState.originalReportID, draftMessage: menuState.draftMessage ?? '', selection: menuState.selection ?? '', - type: CONST.CONTEXT_MENU_TYPES.REPORT, + type: 'REPORT', anchor: {current: menuState.contextMenuTargetNode ?? null}, }); - const visibleActionIDs = useMemo(() => new Set(data.getVisibleActionIDs()), [data]); - // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style const reportAction = (data.reportAction ?? null) as NonNullable; - const {interceptAnonymousUser, translate} = data; + const {interceptAnonymousUser, translate, disabledActionIDs} = data; + + const isDisabled = (id: string) => disabledActionIDs.has(id); + + const showMarkAsRead = shouldShowMarkAsReadAction({isUnreadChat: data.isUnreadChat}) && !isDisabled(ACTION_IDS.MARK_AS_READ); + const showMarkAsUnread = shouldShowMarkAsUnreadForReport({isUnreadChat: data.isUnreadChat}) && !isDisabled(ACTION_IDS.MARK_AS_UNREAD); + const showPin = shouldShowPinAction({isPinnedChat: data.isPinnedChat}) && !isDisabled(ACTION_IDS.PIN); + const showUnpin = shouldShowUnpinAction({isPinnedChat: data.isPinnedChat}) && !isDisabled(ACTION_IDS.UNPIN); + const showCopyOnyxData = shouldShowCopyOnyxDataAction({isProduction: data.isProduction}) && !isDisabled(ACTION_IDS.COPY_ONYX_DATA); + const showDebug = shouldShowDebugAction({isDebugModeEnabled: data.isDebugModeEnabled}) && !isDisabled(ACTION_IDS.DEBUG); - const allActions: ContextMenuAction[] = [ - createMarkAsReadAction({ - reportID: data.reportID, - interceptAnonymousUser, - hideAndRun, - translate, - mailIcon: icons.Mail, - checkmarkIcon: icons.Checkmark, - }), - createMarkAsUnreadAction({ - reportID: data.reportID, - reportActions: data.reportActions, - reportAction, - currentUserAccountID: 0, - interceptAnonymousUser, - hideAndRun, - translate, - chatBubbleUnreadIcon: icons.ChatBubbleUnread, - checkmarkIcon: icons.Checkmark, - }), - createPinAction({ - reportID: data.reportID, - interceptAnonymousUser, - hideAndRun, - translate, - pinIcon: icons.Pin, - }), - createUnpinAction({ - reportID: data.reportID, - interceptAnonymousUser, - hideAndRun, - translate, - pinIcon: icons.Pin, - }), - createCopyOnyxDataAction({ - report: data.report, - interceptAnonymousUser, - translate, - copyIcon: icons.Copy, - checkmarkIcon: icons.Checkmark, - }), - createDebugAction({ - reportID: data.reportID, - reportAction, - interceptAnonymousUser, - translate, - bugIcon: icons.Bug, - }), - ]; + const markAsReadActionItem = showMarkAsRead + ? createMarkAsReadAction({reportID: data.reportID, interceptAnonymousUser, hideAndRun, translate, mailIcon: icons.Mail, checkmarkIcon: icons.Checkmark}) + : undefined; + const markAsUnreadActionItem = showMarkAsUnread + ? createMarkAsUnreadAction({ + reportID: data.reportID, + reportActions: data.reportActions, + reportAction, + currentUserAccountID: 0, + interceptAnonymousUser, + hideAndRun, + translate, + chatBubbleUnreadIcon: icons.ChatBubbleUnread, + checkmarkIcon: icons.Checkmark, + }) + : undefined; + const pinActionItem = showPin ? createPinAction({reportID: data.reportID, interceptAnonymousUser, hideAndRun, translate, pinIcon: icons.Pin}) : undefined; + const unpinActionItem = showUnpin ? createUnpinAction({reportID: data.reportID, interceptAnonymousUser, hideAndRun, translate, pinIcon: icons.Pin}) : undefined; + const copyOnyxDataActionItem = showCopyOnyxData + ? createCopyOnyxDataAction({report: data.report, interceptAnonymousUser, translate, copyIcon: icons.Copy, checkmarkIcon: icons.Checkmark}) + : undefined; + const debugActionItem = showDebug ? createDebugAction({reportID: data.reportID, reportAction, interceptAnonymousUser, translate, bugIcon: icons.Bug}) : undefined; - const actions = allActions.filter((action) => visibleActionIDs.has(action.id as ActionID)); + const visibleActions = useMemo(() => { + const items: ContextMenuAction[] = []; + if (markAsReadActionItem) { + items.push(markAsReadActionItem); + } + if (markAsUnreadActionItem) { + items.push(markAsUnreadActionItem); + } + if (pinActionItem) { + items.push(pinActionItem); + } + if (unpinActionItem) { + items.push(unpinActionItem); + } + if (copyOnyxDataActionItem) { + items.push(copyOnyxDataActionItem); + } + if (debugActionItem) { + items.push(debugActionItem); + } + return items; + }, [markAsReadActionItem, markAsUnreadActionItem, pinActionItem, unpinActionItem, copyOnyxDataActionItem, debugActionItem]); const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({ initialFocusedIndex: -1, disabledIndexes: [], - maxIndex: actions.length - 1, + maxIndex: visibleActions.length - 1, isActive: shouldEnableArrowNavigation, }); @@ -110,7 +110,7 @@ function PopoverReportContent({menuState, hideAndRun, setLocalShouldKeepOpen, co ref={contentRef} style={wrapperStyle} > - {actions.map((action: ContextMenuAction, i: number) => ( + {visibleActions.map((action: ContextMenuAction, i: number) => ( setFocusedIndex(i)} - onBlur={() => (i === actions.length - 1 || i === 1) && setFocusedIndex(-1)} + onBlur={() => (i === visibleActions.length - 1 || i === 1) && setFocusedIndex(-1)} disabled={action.disabled} shouldShowLoadingSpinnerIcon={action.shouldShowLoadingSpinnerIcon} sentryLabel={action.sentryLabel} diff --git a/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts b/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts index 822fa5707908..33cd2827a3a0 100644 --- a/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts +++ b/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts @@ -1,34 +1,6 @@ -import type {RefObject} from 'react'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; -import { - getOriginalMessage, - getReportAction, - hasReasoning, - isActionableTrackExpense, - isActionOfType, - isCreatedAction, - isCreatedTaskReportAction, - isDeletedAction, - isMessageDeleted, - isMoneyRequestAction, - isReportActionAttachment, - isReportPreviewAction, - isTripPreview, - isWhisperAction, -} from '@libs/ReportActionsUtils'; -import { - canDeleteReportAction, - canEditReportAction, - canFlagReportAction, - canHoldUnholdReportAction, - getChildReportNotificationPreference, - shouldDisableThread, - shouldDisplayThreadReplies, -} from '@libs/ReportUtils'; -import type {ContextMenuAnchor} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; -import CONST from '@src/CONST'; -import type {Policy, ReportAction, Report as ReportType, Transaction} from '@src/types/onyx'; +import type {ReportAction} from '@src/types/onyx'; const ACTION_IDS = { EMOJI_REACTION: 'emojiReaction', @@ -58,255 +30,12 @@ const ACTION_IDS = { type ActionID = ValueOf; -type ShouldShowArgs = { - type: string; - reportAction: OnyxEntry; - childReportActions: OnyxCollection; - isArchivedRoom: boolean; - menuTarget: RefObject | undefined; - isChronosReport: boolean; - reportID?: string; - isPinnedChat: boolean; - isUnreadChat: boolean; - isThreadReportParentAction: boolean; - isOffline: boolean; - isProduction: boolean; - moneyRequestAction: ReportAction | undefined; - areHoldRequirementsMet: boolean; - isDebugModeEnabled: OnyxEntry; - iouTransaction: OnyxEntry; - transactions?: OnyxCollection; - moneyRequestReport?: OnyxEntry; - moneyRequestPolicy?: OnyxEntry; - isHarvestReport?: boolean; -}; - function getActionHtml(reportAction: OnyxEntry): string { const message = Array.isArray(reportAction?.message) ? (reportAction?.message?.at(-1) ?? null) : (reportAction?.message ?? null); return message?.html ?? ''; } -const ORDERED_ACTION_SHOULD_SHOW: Array<{id: ActionID; isContentAction: boolean; shouldShow: (args: ShouldShowArgs) => boolean}> = [ - { - id: ACTION_IDS.EMOJI_REACTION, - isContentAction: true, - shouldShow: ({type, reportAction}) => { - const isDynamicWorkflowRoutedAction = isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.DYNAMIC_EXTERNAL_WORKFLOW_ROUTED); - return type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && !!reportAction && 'message' in reportAction && !isMessageDeleted(reportAction) && !isDynamicWorkflowRoutedAction; - }, - }, - { - id: ACTION_IDS.REPLY_IN_THREAD, - isContentAction: false, - shouldShow: ({type, reportAction, reportID, isThreadReportParentAction, isArchivedRoom}) => { - if (type !== CONST.CONTEXT_MENU_TYPES.REPORT_ACTION || !reportID) { - return false; - } - return !shouldDisableThread(reportAction, isThreadReportParentAction, isArchivedRoom); - }, - }, - { - id: ACTION_IDS.MARK_AS_UNREAD, - isContentAction: false, - shouldShow: ({type, reportAction, isUnreadChat}) => { - const isDynamicWorkflowRoutedAction = isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.DYNAMIC_EXTERNAL_WORKFLOW_ROUTED); - return (type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && !isDynamicWorkflowRoutedAction) || (type === CONST.CONTEXT_MENU_TYPES.REPORT && !isUnreadChat); - }, - }, - { - id: ACTION_IDS.EXPLAIN, - isContentAction: false, - shouldShow: ({type, reportAction, isArchivedRoom}) => { - if (type !== CONST.CONTEXT_MENU_TYPES.REPORT_ACTION || isArchivedRoom || !reportAction) { - return false; - } - return hasReasoning(reportAction); - }, - }, - { - id: ACTION_IDS.MARK_AS_READ, - isContentAction: false, - shouldShow: ({type, isUnreadChat}) => type === CONST.CONTEXT_MENU_TYPES.REPORT && isUnreadChat, - }, - { - id: ACTION_IDS.EDIT, - isContentAction: false, - shouldShow: ({type, reportAction, isArchivedRoom, isChronosReport, moneyRequestAction}) => - type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && (canEditReportAction(reportAction) || canEditReportAction(moneyRequestAction)) && !isArchivedRoom && !isChronosReport, - }, - { - id: ACTION_IDS.UNHOLD, - isContentAction: false, - shouldShow: ({type, moneyRequestReport, moneyRequestAction, moneyRequestPolicy, areHoldRequirementsMet, iouTransaction}) => { - if (type !== CONST.CONTEXT_MENU_TYPES.REPORT_ACTION || !areHoldRequirementsMet) { - return false; - } - const holdReportAction = getReportAction(moneyRequestAction?.childReportID, `${iouTransaction?.comment?.hold ?? ''}`); - return canHoldUnholdReportAction(moneyRequestReport, moneyRequestAction, holdReportAction, iouTransaction, moneyRequestPolicy).canUnholdRequest; - }, - }, - { - id: ACTION_IDS.HOLD, - isContentAction: false, - shouldShow: ({type, moneyRequestReport, moneyRequestAction, moneyRequestPolicy, areHoldRequirementsMet, iouTransaction}) => { - if (type !== CONST.CONTEXT_MENU_TYPES.REPORT_ACTION || !areHoldRequirementsMet) { - return false; - } - const holdReportAction = getReportAction(moneyRequestAction?.childReportID, `${iouTransaction?.comment?.hold ?? ''}`); - return canHoldUnholdReportAction(moneyRequestReport, moneyRequestAction, holdReportAction, iouTransaction, moneyRequestPolicy).canHoldRequest; - }, - }, - { - id: ACTION_IDS.JOIN_THREAD, - isContentAction: false, - shouldShow: ({reportAction, isArchivedRoom, isThreadReportParentAction, isHarvestReport}) => { - const childReportNotificationPreference = getChildReportNotificationPreference(reportAction); - const isDeletedActionResult = isDeletedAction(reportAction); - const shouldDisplayReplies = shouldDisplayThreadReplies(reportAction, isThreadReportParentAction); - const subscribed = childReportNotificationPreference !== 'hidden'; - const isWhisper = isWhisperAction(reportAction) || isActionableTrackExpense(reportAction); - const isExpenseReportAction = isMoneyRequestAction(reportAction) || isReportPreviewAction(reportAction); - const isTaskAction = isCreatedTaskReportAction(reportAction); - const isHarvestCreatedExpenseReportAction = !!isHarvestReport && isCreatedAction(reportAction); - const shouldDisableJoin = shouldDisableThread(reportAction, isThreadReportParentAction, isArchivedRoom); - return ( - !subscribed && - !isWhisper && - !isTaskAction && - !isExpenseReportAction && - !isThreadReportParentAction && - !isHarvestCreatedExpenseReportAction && - !shouldDisableJoin && - (shouldDisplayReplies || (!isDeletedActionResult && !isArchivedRoom)) - ); - }, - }, - { - id: ACTION_IDS.LEAVE_THREAD, - isContentAction: false, - shouldShow: ({reportAction, isArchivedRoom, isThreadReportParentAction, isHarvestReport}) => { - const childReportNotificationPreference = getChildReportNotificationPreference(reportAction); - const isDeletedActionResult = isDeletedAction(reportAction); - const shouldDisplayReplies = shouldDisplayThreadReplies(reportAction, isThreadReportParentAction); - const subscribed = childReportNotificationPreference !== 'hidden'; - const isWhisper = isWhisperAction(reportAction) || isActionableTrackExpense(reportAction); - const isExpenseReportAction = isMoneyRequestAction(reportAction) || isReportPreviewAction(reportAction); - const isTaskAction = isCreatedTaskReportAction(reportAction); - const isHarvestCreatedExpenseReportAction = !!isHarvestReport && isCreatedAction(reportAction); - return ( - subscribed && - !isWhisper && - !isTaskAction && - !isExpenseReportAction && - !isThreadReportParentAction && - !isHarvestCreatedExpenseReportAction && - (shouldDisplayReplies || (!isDeletedActionResult && !isArchivedRoom)) - ); - }, - }, - { - id: ACTION_IDS.COPY_URL, - isContentAction: false, - shouldShow: ({type}) => type === CONST.CONTEXT_MENU_TYPES.LINK, - }, - { - id: ACTION_IDS.COPY_TO_CLIPBOARD, - isContentAction: false, - shouldShow: ({type}) => type === CONST.CONTEXT_MENU_TYPES.TEXT, - }, - { - id: ACTION_IDS.COPY_EMAIL, - isContentAction: false, - shouldShow: ({type}) => type === CONST.CONTEXT_MENU_TYPES.EMAIL, - }, - { - id: ACTION_IDS.COPY_MESSAGE, - isContentAction: false, - shouldShow: ({type, reportAction}) => - type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && !isReportActionAttachment(reportAction) && !isMessageDeleted(reportAction) && !isTripPreview(reportAction), - }, - { - id: ACTION_IDS.COPY_LINK, - isContentAction: false, - shouldShow: ({type, reportAction, menuTarget}) => { - const isAttachment = isReportActionAttachment(reportAction); - const isAttachmentTarget = menuTarget?.current && 'tagName' in menuTarget.current && menuTarget?.current.tagName === 'IMG' && isAttachment; - const isDynamicWorkflowRoutedAction = isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.DYNAMIC_EXTERNAL_WORKFLOW_ROUTED); - return type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && !isAttachmentTarget && !isMessageDeleted(reportAction) && !isDynamicWorkflowRoutedAction; - }, - }, - { - id: ACTION_IDS.PIN, - isContentAction: false, - shouldShow: ({type, isPinnedChat}) => type === CONST.CONTEXT_MENU_TYPES.REPORT && !isPinnedChat, - }, - { - id: ACTION_IDS.UNPIN, - isContentAction: false, - shouldShow: ({type, isPinnedChat}) => type === CONST.CONTEXT_MENU_TYPES.REPORT && isPinnedChat, - }, - { - id: ACTION_IDS.FLAG_AS_OFFENSIVE, - isContentAction: false, - shouldShow: ({type, reportAction, isArchivedRoom, isChronosReport, reportID}) => - type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && - canFlagReportAction(reportAction, reportID) && - !isArchivedRoom && - !isChronosReport && - reportAction?.actorAccountID !== CONST.ACCOUNT_ID.CONCIERGE, - }, - { - id: ACTION_IDS.DOWNLOAD, - isContentAction: false, - shouldShow: ({reportAction, isOffline}) => { - const isAttachment = isReportActionAttachment(reportAction); - const html = getActionHtml(reportAction); - const isUploading = html.includes(CONST.ATTACHMENT_OPTIMISTIC_SOURCE_ATTRIBUTE); - return isAttachment && !isUploading && !!reportAction?.reportActionID && !isMessageDeleted(reportAction) && !isOffline; - }, - }, - { - id: ACTION_IDS.COPY_ONYX_DATA, - isContentAction: false, - shouldShow: ({type, isProduction}) => type === CONST.CONTEXT_MENU_TYPES.REPORT && !isProduction, - }, - { - id: ACTION_IDS.DEBUG, - isContentAction: false, - shouldShow: ({type, isDebugModeEnabled}) => [CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, CONST.CONTEXT_MENU_TYPES.REPORT].some((value) => value === type) && !!isDebugModeEnabled, - }, - { - id: ACTION_IDS.DELETE, - isContentAction: false, - shouldShow: ({type, reportAction, isArchivedRoom, isChronosReport, reportID: reportIDParam, moneyRequestAction, iouTransaction, transactions, childReportActions}) => { - let reportID = reportIDParam; - if (isMoneyRequestAction(moneyRequestAction)) { - reportID = getOriginalMessage(moneyRequestAction)?.IOUReportID; - } else if (isReportPreviewAction(reportAction)) { - reportID = reportAction?.childReportID; - } - return ( - !!reportIDParam && - type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && - canDeleteReportAction(moneyRequestAction ?? reportAction, reportID, iouTransaction, transactions, childReportActions) && - !isArchivedRoom && - !isChronosReport && - !isMessageDeleted(reportAction) - ); - }, - }, - { - id: ACTION_IDS.OVERFLOW_MENU, - isContentAction: false, - shouldShow: () => true, - }, -]; - const RESTRICTED_READONLY_ACTION_IDS = new Set([ACTION_IDS.REPLY_IN_THREAD, ACTION_IDS.EDIT, ACTION_IDS.JOIN_THREAD, ACTION_IDS.DELETE]); -function getVisibleActionIDs(shouldShowArgs: ShouldShowArgs, disabledActionIDs: Set): ActionID[] { - return ORDERED_ACTION_SHOULD_SHOW.filter((entry) => entry.id !== 'overflowMenu' && !disabledActionIDs.has(entry.id) && entry.shouldShow(shouldShowArgs)).map((entry) => entry.id); -} - -export {ORDERED_ACTION_SHOULD_SHOW, RESTRICTED_READONLY_ACTION_IDS, getActionHtml, getVisibleActionIDs}; -export type {ActionID, ShouldShowArgs}; +export {ACTION_IDS, RESTRICTED_READONLY_ACTION_IDS, getActionHtml}; +export type {ActionID}; diff --git a/src/pages/inbox/report/ContextMenu/actions/copyLinkAction.ts b/src/pages/inbox/report/ContextMenu/actions/copyLinkAction.ts index 10ef73c05839..4b6f83eecda4 100644 --- a/src/pages/inbox/report/ContextMenu/actions/copyLinkAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/copyLinkAction.ts @@ -1,7 +1,11 @@ +import type {RefObject} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; import Clipboard from '@libs/Clipboard'; import {getEnvironmentURL} from '@libs/Environment/Environment'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {isActionOfType, isMessageDeleted, isReportActionAttachment} from '@libs/ReportActionsUtils'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import type {ContextMenuAnchor} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import type {ReportAction} from '@src/types/onyx'; import type IconAsset from '@src/types/utils/IconAsset'; @@ -15,6 +19,13 @@ type CopyLinkActionParams = BaseContextMenuActionParams & { checkmarkIcon: IconAsset; }; +function shouldShowCopyLinkAction({reportAction, menuTarget}: {reportAction: OnyxEntry; menuTarget: RefObject | undefined}): boolean { + const isAttachment = isReportActionAttachment(reportAction); + const isAttachmentTarget = menuTarget?.current && 'tagName' in menuTarget.current && menuTarget?.current.tagName === 'IMG' && isAttachment; + const isDEWRouted = isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.DYNAMIC_EXTERNAL_WORKFLOW_ROUTED); + return !isAttachmentTarget && !isMessageDeleted(reportAction) && !isDEWRouted; +} + function createCopyLinkAction({reportAction, originalReportID, interceptAnonymousUser, translate, linkCopyIcon, checkmarkIcon}: CopyLinkActionParams): ContextMenuAction { return { id: 'copyLink', @@ -36,3 +47,4 @@ function createCopyLinkAction({reportAction, originalReportID, interceptAnonymou } export default createCopyLinkAction; +export {shouldShowCopyLinkAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.ts b/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.ts index ffd5903b9f03..0e9ba6ba2916 100644 --- a/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.ts @@ -65,12 +65,12 @@ import { getTagListUpdatedRequiredMessage, getTravelUpdateMessage, getUpdateACHAccountMessage, + getUpdatedApprovalRuleMessage, getUpdatedAuditRateMessage, getUpdatedAutoHarvestingMessage, getUpdatedBudgetMessage, getUpdatedDefaultTitleMessage, getUpdatedIndividualBudgetNotificationMessage, - getUpdatedApprovalRuleMessage, getUpdatedManualApprovalThresholdMessage, getUpdatedOwnershipMessage, getUpdatedProhibitedExpensesMessage, @@ -108,6 +108,7 @@ import { isCreatedTaskReportAction, isMarkAsClosedAction, isMemberChangeAction, + isMessageDeleted, isModifiedExpenseAction, isMoneyRequestAction, isMovedAction, @@ -119,6 +120,7 @@ import { isReportPreviewAction as isReportPreviewActionReportActionsUtils, isTagModificationAction, isTaskAction as isTaskActionReportActionsUtils, + isTripPreview, isUnapprovedAction, } from '@libs/ReportActionsUtils'; import {getReportName} from '@libs/ReportNameUtils'; @@ -140,7 +142,7 @@ import { import {getTaskCreatedMessage, getTaskReportActionMessage} from '@libs/TaskUtils'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; -import type {Card, Policy, PolicyTagLists, ReportAction, Transaction, Report as ReportType} from '@src/types/onyx'; +import type {Card, Policy, PolicyTagLists, ReportAction, Report as ReportType, Transaction} from '@src/types/onyx'; import type IconAsset from '@src/types/utils/IconAsset'; import {getActionHtml} from './actionConfig'; import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; @@ -172,6 +174,10 @@ type CopyMessageActionParams = BaseContextMenuActionParams & checkmarkIcon: IconAsset; }; +function shouldShowCopyMessageAction({reportAction}: {reportAction: OnyxEntry}): boolean { + return !isReportActionAttachment(reportAction) && !isMessageDeleted(reportAction) && !isTripPreview(reportAction); +} + function setClipboardMessage(content: string | undefined) { if (!content) { return; @@ -544,3 +550,4 @@ function createCopyMessageAction({interceptAnonymousUser, translate, copyIcon, c } export default createCopyMessageAction; +export {shouldShowCopyMessageAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/copyOnyxDataAction.ts b/src/pages/inbox/report/ContextMenu/actions/copyOnyxDataAction.ts index 7214e48e9a7b..af589360d917 100644 --- a/src/pages/inbox/report/ContextMenu/actions/copyOnyxDataAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/copyOnyxDataAction.ts @@ -14,6 +14,10 @@ type CopyOnyxDataActionParams = BaseContextMenuActionParams & { checkmarkIcon: IconAsset; }; +function shouldShowCopyOnyxDataAction({isProduction}: {isProduction: boolean}): boolean { + return !isProduction; +} + function createCopyOnyxDataAction({report, interceptAnonymousUser, translate, copyIcon, checkmarkIcon}: CopyOnyxDataActionParams): ContextMenuAction { return { id: 'copyOnyxData', @@ -32,3 +36,4 @@ function createCopyOnyxDataAction({report, interceptAnonymousUser, translate, co } export default createCopyOnyxDataAction; +export {shouldShowCopyOnyxDataAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/debugAction.ts b/src/pages/inbox/report/ContextMenu/actions/debugAction.ts index 7916e0aea85d..27380dfd7624 100644 --- a/src/pages/inbox/report/ContextMenu/actions/debugAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/debugAction.ts @@ -1,3 +1,4 @@ +import type {OnyxEntry} from 'react-native-onyx'; import Navigation from '@libs/Navigation/Navigation'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; @@ -14,6 +15,10 @@ type DebugActionParams = BaseContextMenuActionParams & { bugIcon: IconAsset; }; +function shouldShowDebugAction({isDebugModeEnabled}: {isDebugModeEnabled: OnyxEntry}): boolean { + return !!isDebugModeEnabled; +} + function createDebugAction({reportID, reportAction, interceptAnonymousUser, translate, bugIcon}: DebugActionParams): ContextMenuAction { return { id: 'debug', @@ -37,3 +42,4 @@ function createDebugAction({reportID, reportAction, interceptAnonymousUser, tran } export default createDebugAction; +export {shouldShowDebugAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/deleteAction.ts b/src/pages/inbox/report/ContextMenu/actions/deleteAction.ts index e5f2bc9fe49e..66ee8885382d 100644 --- a/src/pages/inbox/report/ContextMenu/actions/deleteAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/deleteAction.ts @@ -1,7 +1,9 @@ -import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import {getOriginalMessage, isMessageDeleted, isMoneyRequestAction, isReportPreviewAction} from '@libs/ReportActionsUtils'; +import {canDeleteReportAction} from '@libs/ReportUtils'; import {showDeleteModal} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; -import type {ReportAction} from '@src/types/onyx'; +import type {ReportAction, Transaction} from '@src/types/onyx'; import type IconAsset from '@src/types/utils/IconAsset'; import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; @@ -13,6 +15,40 @@ type DeleteActionParams = BaseContextMenuActionParams & { trashcanIcon: IconAsset; }; +function shouldShowDeleteAction({ + reportAction, + isArchivedRoom, + isChronosReport, + reportID, + moneyRequestAction, + iouTransaction, + transactions, + childReportActions, +}: { + reportAction: OnyxEntry; + isArchivedRoom: boolean; + isChronosReport: boolean; + reportID: string | undefined; + moneyRequestAction: ReportAction | undefined; + iouTransaction: OnyxEntry; + transactions: OnyxCollection | undefined; + childReportActions: OnyxCollection; +}): boolean { + let effectiveReportID: string | undefined = reportID; + if (isMoneyRequestAction(moneyRequestAction)) { + effectiveReportID = getOriginalMessage(moneyRequestAction)?.IOUReportID; + } else if (isReportPreviewAction(reportAction)) { + effectiveReportID = reportAction?.childReportID; + } + return ( + !!reportID && + canDeleteReportAction(moneyRequestAction ?? reportAction, effectiveReportID, iouTransaction, transactions, childReportActions) && + !isArchivedRoom && + !isChronosReport && + !isMessageDeleted(reportAction) + ); +} + function createDeleteAction({reportID, reportAction, moneyRequestAction, hideAndRun, translate, trashcanIcon}: DeleteActionParams): ContextMenuAction { return { id: 'delete', @@ -29,3 +65,4 @@ function createDeleteAction({reportID, reportAction, moneyRequestAction, hideAnd } export default createDeleteAction; +export {shouldShowDeleteAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/downloadAction.ts b/src/pages/inbox/report/ContextMenu/actions/downloadAction.ts index 2b4a3b890034..a37b55589210 100644 --- a/src/pages/inbox/report/ContextMenu/actions/downloadAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/downloadAction.ts @@ -4,6 +4,7 @@ import {isMobileSafari} from '@libs/Browser'; import fileDownload from '@libs/fileDownload'; import getAttachmentDetails from '@libs/fileDownload/getAttachmentDetails'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {isMessageDeleted, isReportActionAttachment} from '@libs/ReportActionsUtils'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {setDownload} from '@userActions/Download'; import CONST from '@src/CONST'; @@ -20,6 +21,13 @@ type DownloadActionParams = BaseContextMenuActionParams & { downloadIcon: IconAsset; }; +function shouldShowDownloadAction({reportAction, isOffline}: {reportAction: OnyxEntry; isOffline: boolean}): boolean { + const isAttachment = isReportActionAttachment(reportAction); + const html = getActionHtml(reportAction); + const isUploading = html.includes(CONST.ATTACHMENT_OPTIMISTIC_SOURCE_ATTRIBUTE); + return isAttachment && !isUploading && !!reportAction?.reportActionID && !isMessageDeleted(reportAction) && !isOffline; +} + function createDownloadAction({reportAction, encryptedAuthToken, interceptAnonymousUser, download, translate, downloadIcon}: DownloadActionParams): ContextMenuAction { const isDownloading = download?.isDownloading ?? false; @@ -49,3 +57,4 @@ function createDownloadAction({reportAction, encryptedAuthToken, interceptAnonym } export default createDownloadAction; +export {shouldShowDownloadAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/editAction.ts b/src/pages/inbox/report/ContextMenu/actions/editAction.ts index bbc375302f57..d41b1b649473 100644 --- a/src/pages/inbox/report/ContextMenu/actions/editAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/editAction.ts @@ -2,6 +2,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import Navigation from '@libs/Navigation/Navigation'; import Parser from '@libs/Parser'; import {isMoneyRequestAction} from '@libs/ReportActionsUtils'; +import {canEditReportAction} from '@libs/ReportUtils'; import {deleteReportActionDraft, openReport, saveReportActionDraft} from '@userActions/Report'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; @@ -21,7 +22,31 @@ type EditActionParams = BaseContextMenuActionParams & { pencilIcon: IconAsset; }; -function createEditAction({reportID, reportAction, moneyRequestAction, draftMessage, introSelected, interceptAnonymousUser, hideAndRun, translate, pencilIcon}: EditActionParams): ContextMenuAction { +function shouldShowEditAction({ + reportAction, + isArchivedRoom, + isChronosReport, + moneyRequestAction, +}: { + reportAction: OnyxEntry; + isArchivedRoom: boolean; + isChronosReport: boolean; + moneyRequestAction: ReportAction | undefined; +}): boolean { + return (canEditReportAction(reportAction) || canEditReportAction(moneyRequestAction)) && !isArchivedRoom && !isChronosReport; +} + +function createEditAction({ + reportID, + reportAction, + moneyRequestAction, + draftMessage, + introSelected, + interceptAnonymousUser, + hideAndRun, + translate, + pencilIcon, +}: EditActionParams): ContextMenuAction { return { id: 'edit', icon: pencilIcon, @@ -49,3 +74,4 @@ function createEditAction({reportID, reportAction, moneyRequestAction, draftMess } export default createEditAction; +export {shouldShowEditAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/emojiReactionAction.ts b/src/pages/inbox/report/ContextMenu/actions/emojiReactionAction.ts index 92e8377215fa..0fad428dbf19 100644 --- a/src/pages/inbox/report/ContextMenu/actions/emojiReactionAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/emojiReactionAction.ts @@ -1,6 +1,8 @@ import type {OnyxEntry} from 'react-native-onyx'; import type {Emoji} from '@assets/emojis/types'; +import {isActionOfType, isMessageDeleted} from '@libs/ReportActionsUtils'; import {toggleEmojiReaction} from '@userActions/Report'; +import CONST from '@src/CONST'; import type {ReportAction, ReportActionReactions} from '@src/types/onyx'; type EmojiReactionData = { @@ -24,7 +26,20 @@ type EmojiReactionParams = { interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; }; -function createEmojiReactionData({reportID, reportAction, currentUserAccountID, openContextMenu, setIsEmojiPickerActive, hideAndRun, interceptAnonymousUser}: EmojiReactionParams): EmojiReactionData { +function shouldShowEmojiReaction({reportAction}: {reportAction: OnyxEntry}): boolean { + const isDEWRouted = isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.DYNAMIC_EXTERNAL_WORKFLOW_ROUTED); + return !!reportAction && 'message' in reportAction && !isMessageDeleted(reportAction) && !isDEWRouted; +} + +function createEmojiReactionData({ + reportID, + reportAction, + currentUserAccountID, + openContextMenu, + setIsEmojiPickerActive, + hideAndRun, + interceptAnonymousUser, +}: EmojiReactionParams): EmojiReactionData { const closeContextMenu = (onHideCallback?: () => void) => { hideAndRun(onHideCallback); }; @@ -60,3 +75,4 @@ function createEmojiReactionData({reportID, reportAction, currentUserAccountID, } export default createEmojiReactionData; +export {shouldShowEmojiReaction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/explainAction.ts b/src/pages/inbox/report/ContextMenu/actions/explainAction.ts index e7d05e01051c..5ad25fcd2df6 100644 --- a/src/pages/inbox/report/ContextMenu/actions/explainAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/explainAction.ts @@ -1,5 +1,6 @@ import type {OnyxEntry} from 'react-native-onyx'; import type useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import {hasReasoning} from '@libs/ReportActionsUtils'; import {explain} from '@userActions/Report'; import CONST from '@src/CONST'; import type {ReportAction, Report as ReportType} from '@src/types/onyx'; @@ -17,7 +18,23 @@ type ExplainActionParams = BaseContextMenuActionParams & { conciergeIcon: IconAsset; }; -function createExplainAction({childReport, originalReport, reportAction, currentUserPersonalDetails, interceptAnonymousUser, hideAndRun, translate, conciergeIcon}: ExplainActionParams): ContextMenuAction { +function shouldShowExplainAction({reportAction, isArchivedRoom}: {reportAction: OnyxEntry; isArchivedRoom: boolean}): boolean { + if (isArchivedRoom || !reportAction) { + return false; + } + return hasReasoning(reportAction); +} + +function createExplainAction({ + childReport, + originalReport, + reportAction, + currentUserPersonalDetails, + interceptAnonymousUser, + hideAndRun, + translate, + conciergeIcon, +}: ExplainActionParams): ContextMenuAction { return { id: 'explain', icon: conciergeIcon, @@ -38,3 +55,4 @@ function createExplainAction({childReport, originalReport, reportAction, current } export default createExplainAction; +export {shouldShowExplainAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/flagAsOffensiveAction.ts b/src/pages/inbox/report/ContextMenu/actions/flagAsOffensiveAction.ts index 080c1239d9cc..e9fe10eca0c0 100644 --- a/src/pages/inbox/report/ContextMenu/actions/flagAsOffensiveAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/flagAsOffensiveAction.ts @@ -1,4 +1,6 @@ +import type {OnyxEntry} from 'react-native-onyx'; import Navigation from '@libs/Navigation/Navigation'; +import {canFlagReportAction} from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {ReportAction} from '@src/types/onyx'; @@ -13,6 +15,20 @@ type FlagAsOffensiveActionParams = BaseContextMenuActionParams & { flagIcon: IconAsset; }; +function shouldShowFlagAsOffensiveAction({ + reportAction, + isArchivedRoom, + isChronosReport, + reportID, +}: { + reportAction: OnyxEntry; + isArchivedRoom: boolean; + isChronosReport: boolean; + reportID: string | undefined; +}): boolean { + return canFlagReportAction(reportAction, reportID) && !isArchivedRoom && !isChronosReport && reportAction?.actorAccountID !== CONST.ACCOUNT_ID.CONCIERGE; +} + function createFlagAsOffensiveAction({reportID, reportAction, hideAndRun, translate, flagIcon}: FlagAsOffensiveActionParams): ContextMenuAction { return { id: 'flagAsOffensive', @@ -34,3 +50,4 @@ function createFlagAsOffensiveAction({reportID, reportAction, hideAndRun, transl } export default createFlagAsOffensiveAction; +export {shouldShowFlagAsOffensiveAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/holdAction.ts b/src/pages/inbox/report/ContextMenu/actions/holdAction.ts index 65986e5635e5..297862423690 100644 --- a/src/pages/inbox/report/ContextMenu/actions/holdAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/holdAction.ts @@ -1,7 +1,9 @@ -import {changeMoneyRequestHoldStatus} from '@libs/ReportUtils'; +import type {OnyxEntry} from 'react-native-onyx'; +import {getReportAction} from '@libs/ReportActionsUtils'; +import {canHoldUnholdReportAction, changeMoneyRequestHoldStatus} from '@libs/ReportUtils'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; -import type {ReportAction} from '@src/types/onyx'; +import type {Policy, ReportAction, Report as ReportType, Transaction} from '@src/types/onyx'; import type IconAsset from '@src/types/utils/IconAsset'; import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; @@ -14,7 +16,35 @@ type HoldActionParams = BaseContextMenuActionParams & { stopwatchIcon: IconAsset; }; -function createHoldAction({moneyRequestAction, isDelegateAccessRestricted, showDelegateNoAccessModal, interceptAnonymousUser, hideAndRun, translate, stopwatchIcon}: HoldActionParams): ContextMenuAction { +function shouldShowHoldAction({ + moneyRequestReport, + moneyRequestAction, + moneyRequestPolicy, + areHoldRequirementsMet, + iouTransaction, +}: { + moneyRequestReport: OnyxEntry; + moneyRequestAction: ReportAction | undefined; + moneyRequestPolicy: OnyxEntry; + areHoldRequirementsMet: boolean; + iouTransaction: OnyxEntry; +}): boolean { + if (!areHoldRequirementsMet) { + return false; + } + const holdReportAction = getReportAction(moneyRequestAction?.childReportID, `${iouTransaction?.comment?.hold ?? ''}`); + return canHoldUnholdReportAction(moneyRequestReport, moneyRequestAction, holdReportAction, iouTransaction, moneyRequestPolicy).canHoldRequest; +} + +function createHoldAction({ + moneyRequestAction, + isDelegateAccessRestricted, + showDelegateNoAccessModal, + interceptAnonymousUser, + hideAndRun, + translate, + stopwatchIcon, +}: HoldActionParams): ContextMenuAction { return { id: 'hold', icon: stopwatchIcon, @@ -32,3 +62,4 @@ function createHoldAction({moneyRequestAction, isDelegateAccessRestricted, showD } export default createHoldAction; +export {shouldShowHoldAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/joinThreadAction.ts b/src/pages/inbox/report/ContextMenu/actions/joinThreadAction.ts index 166feb78bf8d..7655957e4eed 100644 --- a/src/pages/inbox/report/ContextMenu/actions/joinThreadAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/joinThreadAction.ts @@ -1,6 +1,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import {getChildReportNotificationPreference} from '@libs/ReportUtils'; +import {isActionableTrackExpense, isCreatedAction, isCreatedTaskReportAction, isDeletedAction, isMoneyRequestAction, isReportPreviewAction, isWhisperAction} from '@libs/ReportActionsUtils'; +import {getChildReportNotificationPreference, shouldDisableThread, shouldDisplayThreadReplies} from '@libs/ReportUtils'; import {toggleSubscribeToChildReport} from '@userActions/Report'; import CONST from '@src/CONST'; import type {ReportAction, Report as ReportType} from '@src/types/onyx'; @@ -16,6 +17,38 @@ type JoinThreadActionParams = BaseContextMenuActionParams & { bellIcon: IconAsset; }; +function shouldShowJoinThreadAction({ + reportAction, + isArchivedRoom, + isThreadReportParentAction, + isHarvestReport, +}: { + reportAction: OnyxEntry; + isArchivedRoom: boolean; + isThreadReportParentAction: boolean; + isHarvestReport: boolean; +}): boolean { + const childReportNotificationPreference = getChildReportNotificationPreference(reportAction); + const isDeletedActionResult = isDeletedAction(reportAction); + const shouldDisplayReplies = shouldDisplayThreadReplies(reportAction, isThreadReportParentAction); + const subscribed = childReportNotificationPreference !== 'hidden'; + const isWhisper = isWhisperAction(reportAction) || isActionableTrackExpense(reportAction); + const isExpenseReportAction = isMoneyRequestAction(reportAction) || isReportPreviewAction(reportAction); + const isTaskAction = isCreatedTaskReportAction(reportAction); + const isHarvestCreatedExpenseReportAction = !!isHarvestReport && isCreatedAction(reportAction); + const shouldDisableJoin = shouldDisableThread(reportAction, isThreadReportParentAction, isArchivedRoom); + return ( + !subscribed && + !isWhisper && + !isTaskAction && + !isExpenseReportAction && + !isThreadReportParentAction && + !isHarvestCreatedExpenseReportAction && + !shouldDisableJoin && + (shouldDisplayReplies || (!isDeletedActionResult && !isArchivedRoom)) + ); +} + function createJoinThreadAction({reportAction, originalReport, currentUserAccountID, interceptAnonymousUser, hideAndRun, translate, bellIcon}: JoinThreadActionParams): ContextMenuAction { return { id: 'joinThread', @@ -34,3 +67,4 @@ function createJoinThreadAction({reportAction, originalReport, currentUserAccoun } export default createJoinThreadAction; +export {shouldShowJoinThreadAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.ts b/src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.ts index 76db6a8a9c4a..d562af76f3a2 100644 --- a/src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.ts @@ -1,6 +1,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import {getChildReportNotificationPreference} from '@libs/ReportUtils'; +import {isActionableTrackExpense, isCreatedAction, isCreatedTaskReportAction, isDeletedAction, isMoneyRequestAction, isReportPreviewAction, isWhisperAction} from '@libs/ReportActionsUtils'; +import {getChildReportNotificationPreference, shouldDisplayThreadReplies} from '@libs/ReportUtils'; import {toggleSubscribeToChildReport} from '@userActions/Report'; import CONST from '@src/CONST'; import type {ReportAction, Report as ReportType} from '@src/types/onyx'; @@ -16,6 +17,36 @@ type LeaveThreadActionParams = BaseContextMenuActionParams & { exitIcon: IconAsset; }; +function shouldShowLeaveThreadAction({ + reportAction, + isArchivedRoom, + isThreadReportParentAction, + isHarvestReport, +}: { + reportAction: OnyxEntry; + isArchivedRoom: boolean; + isThreadReportParentAction: boolean; + isHarvestReport: boolean; +}): boolean { + const childReportNotificationPreference = getChildReportNotificationPreference(reportAction); + const isDeletedActionResult = isDeletedAction(reportAction); + const shouldDisplayReplies = shouldDisplayThreadReplies(reportAction, isThreadReportParentAction); + const subscribed = childReportNotificationPreference !== 'hidden'; + const isWhisper = isWhisperAction(reportAction) || isActionableTrackExpense(reportAction); + const isExpenseReportAction = isMoneyRequestAction(reportAction) || isReportPreviewAction(reportAction); + const isTaskAction = isCreatedTaskReportAction(reportAction); + const isHarvestCreatedExpenseReportAction = !!isHarvestReport && isCreatedAction(reportAction); + return ( + subscribed && + !isWhisper && + !isTaskAction && + !isExpenseReportAction && + !isThreadReportParentAction && + !isHarvestCreatedExpenseReportAction && + (shouldDisplayReplies || (!isDeletedActionResult && !isArchivedRoom)) + ); +} + function createLeaveThreadAction({reportAction, originalReport, currentUserAccountID, interceptAnonymousUser, hideAndRun, translate, exitIcon}: LeaveThreadActionParams): ContextMenuAction { return { id: 'leaveThread', @@ -34,3 +65,4 @@ function createLeaveThreadAction({reportAction, originalReport, currentUserAccou } export default createLeaveThreadAction; +export {shouldShowLeaveThreadAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/markAsReadAction.ts b/src/pages/inbox/report/ContextMenu/actions/markAsReadAction.ts index 43e72ed458e2..17161b0e41a4 100644 --- a/src/pages/inbox/report/ContextMenu/actions/markAsReadAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/markAsReadAction.ts @@ -12,6 +12,10 @@ type MarkAsReadActionParams = BaseContextMenuActionParams & { checkmarkIcon: IconAsset; }; +function shouldShowMarkAsReadAction({isUnreadChat}: {isUnreadChat: boolean}): boolean { + return isUnreadChat; +} + function createMarkAsReadAction({reportID, interceptAnonymousUser, hideAndRun, translate, mailIcon, checkmarkIcon}: MarkAsReadActionParams): ContextMenuAction { return { id: 'markAsRead', @@ -28,3 +32,4 @@ function createMarkAsReadAction({reportID, interceptAnonymousUser, hideAndRun, t } export default createMarkAsReadAction; +export {shouldShowMarkAsReadAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/markAsUnreadAction.ts b/src/pages/inbox/report/ContextMenu/actions/markAsUnreadAction.ts index 035a6d8bc341..95937a9577b5 100644 --- a/src/pages/inbox/report/ContextMenu/actions/markAsUnreadAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/markAsUnreadAction.ts @@ -1,5 +1,6 @@ import type {OnyxEntry} from 'react-native-onyx'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {isActionOfType} from '@libs/ReportActionsUtils'; import {markCommentAsUnread} from '@userActions/Report'; import CONST from '@src/CONST'; import type {ReportAction, ReportActions} from '@src/types/onyx'; @@ -17,6 +18,14 @@ type MarkAsUnreadActionParams = BaseContextMenuActionParams & { checkmarkIcon: IconAsset; }; +function shouldShowMarkAsUnreadForReportAction({reportAction}: {reportAction: OnyxEntry}): boolean { + return !isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.DYNAMIC_EXTERNAL_WORKFLOW_ROUTED); +} + +function shouldShowMarkAsUnreadForReport({isUnreadChat}: {isUnreadChat: boolean}): boolean { + return !isUnreadChat; +} + function createMarkAsUnreadAction({ reportID, reportActions, @@ -43,3 +52,4 @@ function createMarkAsUnreadAction({ } export default createMarkAsUnreadAction; +export {shouldShowMarkAsUnreadForReportAction, shouldShowMarkAsUnreadForReport}; diff --git a/src/pages/inbox/report/ContextMenu/actions/pinAction.ts b/src/pages/inbox/report/ContextMenu/actions/pinAction.ts index 3508c83848fe..3598bc4a5f1b 100644 --- a/src/pages/inbox/report/ContextMenu/actions/pinAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/pinAction.ts @@ -11,6 +11,10 @@ type PinActionParams = BaseContextMenuActionParams & { pinIcon: IconAsset; }; +function shouldShowPinAction({isPinnedChat}: {isPinnedChat: boolean}): boolean { + return !isPinnedChat; +} + function createPinAction({reportID, interceptAnonymousUser, hideAndRun, translate, pinIcon}: PinActionParams): ContextMenuAction { return { id: 'pin', @@ -26,3 +30,4 @@ function createPinAction({reportID, interceptAnonymousUser, hideAndRun, translat } export default createPinAction; +export {shouldShowPinAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/replyInThreadAction.ts b/src/pages/inbox/report/ContextMenu/actions/replyInThreadAction.ts index 4658a7574157..0cb781fb3fc7 100644 --- a/src/pages/inbox/report/ContextMenu/actions/replyInThreadAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/replyInThreadAction.ts @@ -1,4 +1,5 @@ import type {OnyxEntry} from 'react-native-onyx'; +import {shouldDisableThread} from '@libs/ReportUtils'; import {navigateToAndOpenChildReport} from '@userActions/Report'; import CONST from '@src/CONST'; import type {ReportAction, Report as ReportType} from '@src/types/onyx'; @@ -16,6 +17,23 @@ type ReplyInThreadActionParams = BaseContextMenuActionParams & { chatBubbleReplyIcon: IconAsset; }; +function shouldShowReplyInThreadAction({ + reportAction, + reportID, + isThreadReportParentAction, + isArchivedRoom, +}: { + reportAction: OnyxEntry; + reportID: string | undefined; + isThreadReportParentAction: boolean; + isArchivedRoom: boolean; +}): boolean { + if (!reportID) { + return false; + } + return !shouldDisableThread(reportAction, isThreadReportParentAction, isArchivedRoom); +} + function createReplyInThreadAction({ childReport, reportAction, @@ -43,3 +61,4 @@ function createReplyInThreadAction({ } export default createReplyInThreadAction; +export {shouldShowReplyInThreadAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/unholdAction.ts b/src/pages/inbox/report/ContextMenu/actions/unholdAction.ts index 6f24073b8ee2..d963de7210fc 100644 --- a/src/pages/inbox/report/ContextMenu/actions/unholdAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/unholdAction.ts @@ -1,7 +1,9 @@ -import {changeMoneyRequestHoldStatus} from '@libs/ReportUtils'; +import type {OnyxEntry} from 'react-native-onyx'; +import {getReportAction} from '@libs/ReportActionsUtils'; +import {canHoldUnholdReportAction, changeMoneyRequestHoldStatus} from '@libs/ReportUtils'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; -import type {ReportAction} from '@src/types/onyx'; +import type {Policy, ReportAction, Report as ReportType, Transaction} from '@src/types/onyx'; import type IconAsset from '@src/types/utils/IconAsset'; import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; @@ -14,7 +16,35 @@ type UnholdActionParams = BaseContextMenuActionParams & { stopwatchIcon: IconAsset; }; -function createUnholdAction({moneyRequestAction, isDelegateAccessRestricted, showDelegateNoAccessModal, interceptAnonymousUser, hideAndRun, translate, stopwatchIcon}: UnholdActionParams): ContextMenuAction { +function shouldShowUnholdAction({ + moneyRequestReport, + moneyRequestAction, + moneyRequestPolicy, + areHoldRequirementsMet, + iouTransaction, +}: { + moneyRequestReport: OnyxEntry; + moneyRequestAction: ReportAction | undefined; + moneyRequestPolicy: OnyxEntry; + areHoldRequirementsMet: boolean; + iouTransaction: OnyxEntry; +}): boolean { + if (!areHoldRequirementsMet) { + return false; + } + const holdReportAction = getReportAction(moneyRequestAction?.childReportID, `${iouTransaction?.comment?.hold ?? ''}`); + return canHoldUnholdReportAction(moneyRequestReport, moneyRequestAction, holdReportAction, iouTransaction, moneyRequestPolicy).canUnholdRequest; +} + +function createUnholdAction({ + moneyRequestAction, + isDelegateAccessRestricted, + showDelegateNoAccessModal, + interceptAnonymousUser, + hideAndRun, + translate, + stopwatchIcon, +}: UnholdActionParams): ContextMenuAction { return { id: 'unhold', icon: stopwatchIcon, @@ -32,3 +62,4 @@ function createUnholdAction({moneyRequestAction, isDelegateAccessRestricted, sho } export default createUnholdAction; +export {shouldShowUnholdAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/unpinAction.ts b/src/pages/inbox/report/ContextMenu/actions/unpinAction.ts index e579011c8ab4..badf710e47b5 100644 --- a/src/pages/inbox/report/ContextMenu/actions/unpinAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/unpinAction.ts @@ -11,6 +11,10 @@ type UnpinActionParams = BaseContextMenuActionParams & { pinIcon: IconAsset; }; +function shouldShowUnpinAction({isPinnedChat}: {isPinnedChat: boolean}): boolean { + return isPinnedChat; +} + function createUnpinAction({reportID, interceptAnonymousUser, hideAndRun, translate, pinIcon}: UnpinActionParams): ContextMenuAction { return { id: 'unpin', @@ -26,3 +30,4 @@ function createUnpinAction({reportID, interceptAnonymousUser, hideAndRun, transl } export default createUnpinAction; +export {shouldShowUnpinAction}; diff --git a/src/pages/inbox/report/ContextMenu/useReportActionContextMenuData.ts b/src/pages/inbox/report/ContextMenu/useReportActionContextMenuData.ts index 2280d016892b..edca2852f062 100644 --- a/src/pages/inbox/report/ContextMenu/useReportActionContextMenuData.ts +++ b/src/pages/inbox/report/ContextMenu/useReportActionContextMenuData.ts @@ -34,8 +34,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {OriginalMessageIOU, ReportAction} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import type {ActionID} from './actions/actionConfig'; -import {getVisibleActionIDs as getVisibleActionIDsFromConfig, RESTRICTED_READONLY_ACTION_IDS} from './actions/actionConfig'; +import {RESTRICTED_READONLY_ACTION_IDS} from './actions/actionConfig'; import type {ContextMenuAnchor, ContextMenuType} from './ReportActionContextMenu'; import {hideContextMenu} from './ReportActionContextMenu'; @@ -160,31 +159,6 @@ function useReportActionContextMenuData({reportID, reportActionID, originalRepor } }; - const shouldShowArgs = { - type, - reportAction, - childReportActions, - isArchivedRoom, - menuTarget: anchor, - isChronosReport, - reportID, - isPinnedChat, - isUnreadChat, - isThreadReportParentAction, - isOffline: !!isOffline, - isProduction, - moneyRequestAction, - areHoldRequirementsMet, - isDebugModeEnabled, - iouTransaction, - transactions, - moneyRequestReport, - moneyRequestPolicy, - isHarvestReport, - }; - - const getVisibleActionIDs = (): ActionID[] => getVisibleActionIDsFromConfig(shouldShowArgs, disabledActionIDs); - return { report, originalReport, @@ -232,7 +206,6 @@ function useReportActionContextMenuData({reportID, reportActionID, originalRepor draftMessage, selection, anchor, - getVisibleActionIDs, }; } diff --git a/src/pages/inbox/report/ContextMenu/useReportContextMenuData.ts b/src/pages/inbox/report/ContextMenu/useReportContextMenuData.ts index 43303cad6d2e..fba8d2133c3a 100644 --- a/src/pages/inbox/report/ContextMenu/useReportContextMenuData.ts +++ b/src/pages/inbox/report/ContextMenu/useReportContextMenuData.ts @@ -8,11 +8,9 @@ import useOnyx from '@hooks/useOnyx'; import useReportIsArchived from '@hooks/useReportIsArchived'; import {canWriteInReport, chatIncludesChronosWithID, isArchivedNonExpenseReport, isUnread} from '@libs/ReportUtils'; import {isAnonymousUser, signOutAndRedirectToSignIn} from '@userActions/Session'; -import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {ReportAction, ReportActions, Report as ReportType} from '@src/types/onyx'; -import type {ActionID} from './actions/actionConfig'; -import {getVisibleActionIDs as getVisibleActionIDsFromConfig, RESTRICTED_READONLY_ACTION_IDS} from './actions/actionConfig'; +import {RESTRICTED_READONLY_ACTION_IDS} from './actions/actionConfig'; import type {ContextMenuAnchor, ContextMenuType} from './ReportActionContextMenu'; import {hideContextMenu} from './ReportActionContextMenu'; @@ -44,7 +42,6 @@ type UseReportContextMenuDataReturn = { translate: ReturnType['translate']; getLocalDateFromDatetime: ReturnType['getLocalDateFromDatetime']; interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; - getVisibleActionIDs: () => ActionID[]; type: ContextMenuType; reportID: string | undefined; originalReportID: string | undefined; @@ -87,31 +84,6 @@ function useReportContextMenuData({reportID, reportActionID, originalReportID, d } }; - const shouldShowArgs = { - type: CONST.CONTEXT_MENU_TYPES.REPORT, - reportAction, - childReportActions: undefined, - isArchivedRoom, - menuTarget: anchor, - isChronosReport, - reportID, - isPinnedChat, - isUnreadChat, - isThreadReportParentAction: false, - isOffline: !!isOffline, - isProduction, - moneyRequestAction: undefined as ReportAction | undefined, - areHoldRequirementsMet: false, - isDebugModeEnabled, - iouTransaction: undefined, - transactions: undefined, - moneyRequestReport: undefined, - moneyRequestPolicy: undefined, - isHarvestReport: false, - }; - - const getVisibleActionIDs = (): ActionID[] => getVisibleActionIDsFromConfig(shouldShowArgs, disabledActionIDs); - return { report, originalReport, @@ -128,7 +100,6 @@ function useReportContextMenuData({reportID, reportActionID, originalReportID, d translate, getLocalDateFromDatetime, interceptAnonymousUser, - getVisibleActionIDs, type, reportID, originalReportID, From 77a730268d040a9deda0e9436d2abc68f0d81129 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 2 Mar 2026 18:29:17 -0800 Subject: [PATCH 31/88] refactor: remove unnecessary eslint-disable suppressions Replace non-nullable type assertions with proper TypeScript narrowing: - useReportActionContextMenuData: pass reportAction directly (hook accepts optional) - MiniReportActionContextMenu/PopoverReportActionContent: null guard inside useMemo - PopoverReportContent: inline narrowing at call sites - PopoverContextMenu: narrow file-level disable to per-line disables Made-with: Cursor --- .../MiniReportActionContextMenu/index.tsx | 7 +- .../report/ContextMenu/PopoverContextMenu.tsx | 3 +- .../PopoverReportActionContent.tsx | 340 +++++++++--------- .../ContextMenu/PopoverReportContent.tsx | 32 +- .../useReportActionContextMenuData.ts | 3 +- 5 files changed, 191 insertions(+), 194 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index 7d6fe61d3b26..2d3a11bc16c3 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -136,8 +136,6 @@ function MiniReportActionContextMenu() { }); }; - // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style - const reportAction = (data.reportAction ?? null) as NonNullable; const currentUserAccountID = data.currentUserPersonalDetails?.accountID ?? 0; const {interceptAnonymousUser, translate, disabledActionIDs} = data; @@ -205,6 +203,10 @@ function MiniReportActionContextMenu() { /* eslint-disable react-hooks/refs -- factory functions store refs for later use, they don't read .current during render */ const visibleActions = useMemo(() => { + if (!data.reportAction) { + return []; + } + const reportAction = data.reportAction; const items: ContextMenuAction[] = []; if (showReplyInThread) { items.push( @@ -366,7 +368,6 @@ function MiniReportActionContextMenu() { showDownload, showDelete, data, - reportAction, currentUserAccountID, interceptAnonymousUser, translate, diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu.tsx index 633411d1567a..2ed25433ce8c 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu.tsx @@ -1,7 +1,8 @@ import type {RefObject} from 'react'; import React, {useEffect, useImperativeHandle, useRef, useState} from 'react'; -/* eslint-disable no-restricted-imports */ +// eslint-disable-next-line no-restricted-imports import type {EmitterSubscription, GestureResponderEvent, NativeTouchEvent, View as ViewType} from 'react-native'; +// eslint-disable-next-line no-restricted-imports import {Dimensions} from 'react-native'; import {Actions, useActionSheetAwareScrollViewActions} from '@components/ActionSheetAwareScrollView'; import {ModalActions, useModal} from '@components/Modal/Global/ModalContext'; diff --git a/src/pages/inbox/report/ContextMenu/PopoverReportActionContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverReportActionContent.tsx index cc2f736e584b..76cede546246 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverReportActionContent.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverReportActionContent.tsx @@ -80,8 +80,6 @@ function PopoverReportActionContent({menuState, hideAndRun, setLocalShouldKeepOp }); }; - // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style - const reportAction = (data.reportAction ?? null) as NonNullable; const currentUserAccountID = data.currentUserPersonalDetails?.accountID ?? 0; const {interceptAnonymousUser, translate, disabledActionIDs} = data; @@ -149,195 +147,193 @@ function PopoverReportActionContent({menuState, hideAndRun, setLocalShouldKeepOp }) && !isDisabled(ACTION_IDS.DELETE); /* eslint-disable react-hooks/refs -- factory functions store refs for later use, they don't read .current during render */ - const replyInThreadAction = showReplyInThread - ? createReplyInThreadAction({ - childReport: data.childReport, - reportAction, - originalReport: data.originalReport, - currentUserAccountID, - interceptAnonymousUser, - hideAndRun, - translate, - chatBubbleReplyIcon: icons.ChatBubbleReply, - }) - : undefined; - const markAsUnreadAction = showMarkAsUnread - ? createMarkAsUnreadAction({ - reportID: data.reportID, - reportActions: data.reportActions, - reportAction, - currentUserAccountID, - interceptAnonymousUser, - hideAndRun, - translate, - chatBubbleUnreadIcon: icons.ChatBubbleUnread, - checkmarkIcon: icons.Checkmark, - }) - : undefined; - const explainActionItem = showExplain - ? createExplainAction({ - childReport: data.childReport, - originalReport: data.originalReport, - reportAction, - currentUserPersonalDetails: data.currentUserPersonalDetails, - interceptAnonymousUser, - hideAndRun, - translate, - conciergeIcon: icons.Concierge, - }) - : undefined; - const editActionItem = showEdit - ? createEditAction({ - reportID: data.reportID, - reportAction, - moneyRequestAction: data.moneyRequestAction, - draftMessage: data.draftMessage, - introSelected: data.introSelected, - interceptAnonymousUser, - hideAndRun, - translate, - pencilIcon: icons.Pencil, - }) - : undefined; - const unholdActionItem = showUnhold - ? createUnholdAction({ - moneyRequestAction: data.moneyRequestAction, - isDelegateAccessRestricted: data.isDelegateAccessRestricted, - showDelegateNoAccessModal: data.showDelegateNoAccessModal, - interceptAnonymousUser, - hideAndRun, - translate, - stopwatchIcon: icons.Stopwatch, - }) - : undefined; - const holdActionItem = showHold - ? createHoldAction({ - moneyRequestAction: data.moneyRequestAction, - isDelegateAccessRestricted: data.isDelegateAccessRestricted, - showDelegateNoAccessModal: data.showDelegateNoAccessModal, - interceptAnonymousUser, - hideAndRun, - translate, - stopwatchIcon: icons.Stopwatch, - }) - : undefined; - const joinThreadActionItem = showJoinThread - ? createJoinThreadAction({reportAction, originalReport: data.originalReport, currentUserAccountID, interceptAnonymousUser, hideAndRun, translate, bellIcon: icons.Bell}) - : undefined; - const leaveThreadActionItem = showLeaveThread - ? createLeaveThreadAction({reportAction, originalReport: data.originalReport, currentUserAccountID, interceptAnonymousUser, hideAndRun, translate, exitIcon: icons.Exit}) - : undefined; - const copyMessageActionItem = showCopyMessage - ? createCopyMessageAction({ - reportAction, - transaction: data.transaction, - selection: data.selection, - report: data.report, - card: data.card, - originalReport: data.originalReport, - isHarvestReport: data.isHarvestReport, - isTryNewDotNVPDismissed: data.isTryNewDotNVPDismissed, - movedFromReport: data.movedFromReport, - movedToReport: data.movedToReport, - childReport: data.childReport, - policy: data.policy, - getLocalDateFromDatetime: data.getLocalDateFromDatetime, - policyTags: data.policyTags, - translate, - harvestReport: data.harvestReport, - currentUserPersonalDetails: data.currentUserPersonalDetails, - interceptAnonymousUser, - copyIcon: icons.Copy, - checkmarkIcon: icons.Checkmark, - }) - : undefined; - const copyLinkActionItem = showCopyLink - ? createCopyLinkAction({reportAction, originalReportID: data.originalReportID, interceptAnonymousUser, translate, linkCopyIcon: icons.LinkCopy, checkmarkIcon: icons.Checkmark}) - : undefined; - const flagAsOffensiveActionItem = showFlagAsOffensive ? createFlagAsOffensiveAction({reportID: data.reportID, reportAction, hideAndRun, translate, flagIcon: icons.Flag}) : undefined; - const downloadActionItem = showDownload - ? createDownloadAction({reportAction, encryptedAuthToken: data.encryptedAuthToken, interceptAnonymousUser, download: data.download, translate, downloadIcon: icons.Download}) - : undefined; - const debugActionItem = showDebug ? createDebugAction({reportID: data.reportID, reportAction, interceptAnonymousUser, translate, bugIcon: icons.Bug}) : undefined; - const deleteActionItem = showDelete - ? createDeleteAction({reportID: data.reportID, reportAction, moneyRequestAction: data.moneyRequestAction, hideAndRun, translate, trashcanIcon: icons.Trashcan}) - : undefined; - - const overflowMenu = createOverflowMenuAction( - { - openOverflowMenu, - openContextMenu: () => setLocalShouldKeepOpen(true), - interceptAnonymousUser, - translate, - threeDotsIcon: icons.ThreeDots, - }, - overflowMenuRef, - ); - /* eslint-enable react-hooks/refs */ - const visibleActions = useMemo(() => { + if (!data.reportAction) { + return [ + createOverflowMenuAction( + {openOverflowMenu, openContextMenu: () => setLocalShouldKeepOpen(true), interceptAnonymousUser, translate, threeDotsIcon: icons.ThreeDots}, + overflowMenuRef, + ), + ]; + } + const reportAction = data.reportAction; const items: ContextMenuAction[] = []; - if (replyInThreadAction) { - items.push(replyInThreadAction); + if (showReplyInThread) { + items.push( + createReplyInThreadAction({ + childReport: data.childReport, + reportAction, + originalReport: data.originalReport, + currentUserAccountID, + interceptAnonymousUser, + hideAndRun, + translate, + chatBubbleReplyIcon: icons.ChatBubbleReply, + }), + ); } - if (markAsUnreadAction) { - items.push(markAsUnreadAction); + if (showMarkAsUnread) { + items.push( + createMarkAsUnreadAction({ + reportID: data.reportID, + reportActions: data.reportActions, + reportAction, + currentUserAccountID, + interceptAnonymousUser, + hideAndRun, + translate, + chatBubbleUnreadIcon: icons.ChatBubbleUnread, + checkmarkIcon: icons.Checkmark, + }), + ); } - if (explainActionItem) { - items.push(explainActionItem); + if (showExplain) { + items.push( + createExplainAction({ + childReport: data.childReport, + originalReport: data.originalReport, + reportAction, + currentUserPersonalDetails: data.currentUserPersonalDetails, + interceptAnonymousUser, + hideAndRun, + translate, + conciergeIcon: icons.Concierge, + }), + ); } - if (editActionItem) { - items.push(editActionItem); + if (showEdit) { + items.push( + createEditAction({ + reportID: data.reportID, + reportAction, + moneyRequestAction: data.moneyRequestAction, + draftMessage: data.draftMessage, + introSelected: data.introSelected, + interceptAnonymousUser, + hideAndRun, + translate, + pencilIcon: icons.Pencil, + }), + ); } - if (unholdActionItem) { - items.push(unholdActionItem); + if (showUnhold) { + items.push( + createUnholdAction({ + moneyRequestAction: data.moneyRequestAction, + isDelegateAccessRestricted: data.isDelegateAccessRestricted, + showDelegateNoAccessModal: data.showDelegateNoAccessModal, + interceptAnonymousUser, + hideAndRun, + translate, + stopwatchIcon: icons.Stopwatch, + }), + ); } - if (holdActionItem) { - items.push(holdActionItem); + if (showHold) { + items.push( + createHoldAction({ + moneyRequestAction: data.moneyRequestAction, + isDelegateAccessRestricted: data.isDelegateAccessRestricted, + showDelegateNoAccessModal: data.showDelegateNoAccessModal, + interceptAnonymousUser, + hideAndRun, + translate, + stopwatchIcon: icons.Stopwatch, + }), + ); } - if (joinThreadActionItem) { - items.push(joinThreadActionItem); + if (showJoinThread) { + items.push( + createJoinThreadAction({reportAction, originalReport: data.originalReport, currentUserAccountID, interceptAnonymousUser, hideAndRun, translate, bellIcon: icons.Bell}), + ); } - if (leaveThreadActionItem) { - items.push(leaveThreadActionItem); + if (showLeaveThread) { + items.push( + createLeaveThreadAction({reportAction, originalReport: data.originalReport, currentUserAccountID, interceptAnonymousUser, hideAndRun, translate, exitIcon: icons.Exit}), + ); } - if (copyMessageActionItem) { - items.push(copyMessageActionItem); + if (showCopyMessage) { + items.push( + createCopyMessageAction({ + reportAction, + transaction: data.transaction, + selection: data.selection, + report: data.report, + card: data.card, + originalReport: data.originalReport, + isHarvestReport: data.isHarvestReport, + isTryNewDotNVPDismissed: data.isTryNewDotNVPDismissed, + movedFromReport: data.movedFromReport, + movedToReport: data.movedToReport, + childReport: data.childReport, + policy: data.policy, + getLocalDateFromDatetime: data.getLocalDateFromDatetime, + policyTags: data.policyTags, + translate, + harvestReport: data.harvestReport, + currentUserPersonalDetails: data.currentUserPersonalDetails, + interceptAnonymousUser, + copyIcon: icons.Copy, + checkmarkIcon: icons.Checkmark, + }), + ); } - if (copyLinkActionItem) { - items.push(copyLinkActionItem); + if (showCopyLink) { + items.push( + createCopyLinkAction({ + reportAction, + originalReportID: data.originalReportID, + interceptAnonymousUser, + translate, + linkCopyIcon: icons.LinkCopy, + checkmarkIcon: icons.Checkmark, + }), + ); } - if (flagAsOffensiveActionItem) { - items.push(flagAsOffensiveActionItem); + if (showFlagAsOffensive) { + items.push(createFlagAsOffensiveAction({reportID: data.reportID, reportAction, hideAndRun, translate, flagIcon: icons.Flag})); } - if (downloadActionItem) { - items.push(downloadActionItem); + if (showDownload) { + items.push( + createDownloadAction({reportAction, encryptedAuthToken: data.encryptedAuthToken, interceptAnonymousUser, download: data.download, translate, downloadIcon: icons.Download}), + ); } - if (debugActionItem) { - items.push(debugActionItem); + if (showDebug) { + items.push(createDebugAction({reportID: data.reportID, reportAction, interceptAnonymousUser, translate, bugIcon: icons.Bug})); } - if (deleteActionItem) { - items.push(deleteActionItem); + if (showDelete) { + items.push(createDeleteAction({reportID: data.reportID, reportAction, moneyRequestAction: data.moneyRequestAction, hideAndRun, translate, trashcanIcon: icons.Trashcan})); } - items.push(overflowMenu); + items.push( + createOverflowMenuAction( + {openOverflowMenu, openContextMenu: () => setLocalShouldKeepOpen(true), interceptAnonymousUser, translate, threeDotsIcon: icons.ThreeDots}, + overflowMenuRef, + ), + ); return items; + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ - replyInThreadAction, - markAsUnreadAction, - explainActionItem, - editActionItem, - unholdActionItem, - holdActionItem, - joinThreadActionItem, - leaveThreadActionItem, - copyMessageActionItem, - copyLinkActionItem, - flagAsOffensiveActionItem, - downloadActionItem, - debugActionItem, - deleteActionItem, - overflowMenu, + showReplyInThread, + showMarkAsUnread, + showExplain, + showEdit, + showUnhold, + showHold, + showJoinThread, + showLeaveThread, + showCopyMessage, + showCopyLink, + showFlagAsOffensive, + showDownload, + showDebug, + showDelete, + data, + currentUserAccountID, + interceptAnonymousUser, + translate, + icons, ]); + /* eslint-enable react-hooks/refs */ const emojiData = createEmojiReactionData({ reportID: data.reportID, diff --git a/src/pages/inbox/report/ContextMenu/PopoverReportContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverReportContent.tsx index 0e4a06a23960..b37228208d89 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverReportContent.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverReportContent.tsx @@ -37,8 +37,6 @@ function PopoverReportContent({menuState, hideAndRun, contentRef, shouldEnableAr anchor: {current: menuState.contextMenuTargetNode ?? null}, }); - // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style - const reportAction = (data.reportAction ?? null) as NonNullable; const {interceptAnonymousUser, translate, disabledActionIDs} = data; const isDisabled = (id: string) => disabledActionIDs.has(id); @@ -53,25 +51,27 @@ function PopoverReportContent({menuState, hideAndRun, contentRef, shouldEnableAr const markAsReadActionItem = showMarkAsRead ? createMarkAsReadAction({reportID: data.reportID, interceptAnonymousUser, hideAndRun, translate, mailIcon: icons.Mail, checkmarkIcon: icons.Checkmark}) : undefined; - const markAsUnreadActionItem = showMarkAsUnread - ? createMarkAsUnreadAction({ - reportID: data.reportID, - reportActions: data.reportActions, - reportAction, - currentUserAccountID: 0, - interceptAnonymousUser, - hideAndRun, - translate, - chatBubbleUnreadIcon: icons.ChatBubbleUnread, - checkmarkIcon: icons.Checkmark, - }) - : undefined; + const markAsUnreadActionItem = + showMarkAsUnread && data.reportAction + ? createMarkAsUnreadAction({ + reportID: data.reportID, + reportActions: data.reportActions, + reportAction: data.reportAction, + currentUserAccountID: 0, + interceptAnonymousUser, + hideAndRun, + translate, + chatBubbleUnreadIcon: icons.ChatBubbleUnread, + checkmarkIcon: icons.Checkmark, + }) + : undefined; const pinActionItem = showPin ? createPinAction({reportID: data.reportID, interceptAnonymousUser, hideAndRun, translate, pinIcon: icons.Pin}) : undefined; const unpinActionItem = showUnpin ? createUnpinAction({reportID: data.reportID, interceptAnonymousUser, hideAndRun, translate, pinIcon: icons.Pin}) : undefined; const copyOnyxDataActionItem = showCopyOnyxData ? createCopyOnyxDataAction({report: data.report, interceptAnonymousUser, translate, copyIcon: icons.Copy, checkmarkIcon: icons.Checkmark}) : undefined; - const debugActionItem = showDebug ? createDebugAction({reportID: data.reportID, reportAction, interceptAnonymousUser, translate, bugIcon: icons.Bug}) : undefined; + const debugActionItem = + showDebug && data.reportAction ? createDebugAction({reportID: data.reportID, reportAction: data.reportAction, interceptAnonymousUser, translate, bugIcon: icons.Bug}) : undefined; const visibleActions = useMemo(() => { const items: ContextMenuAction[] = []; diff --git a/src/pages/inbox/report/ContextMenu/useReportActionContextMenuData.ts b/src/pages/inbox/report/ContextMenu/useReportActionContextMenuData.ts index edca2852f062..2ed5e73c803c 100644 --- a/src/pages/inbox/report/ContextMenu/useReportActionContextMenuData.ts +++ b/src/pages/inbox/report/ContextMenu/useReportActionContextMenuData.ts @@ -144,8 +144,7 @@ function useReportActionContextMenuData({reportID, reportActionID, originalRepor const isHarvestReport = isHarvestCreatedExpenseReport(reportNameValuePairs?.origin, reportNameValuePairs?.originalID); const isTryNewDotNVPDismissed = !!tryNewDot?.classicRedirect?.dismissed; - // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style - const card = useGetExpensifyCardFromReportAction({reportAction: (reportAction ?? null) as ReportAction, policyID}); + const card = useGetExpensifyCardFromReportAction({reportAction, policyID}); const interceptAnonymousUser = (callback: () => void, isAnonymousAction = false) => { if (isAnonymousUser() && !isAnonymousAction) { From 62643ac85c19901d8f6fa747474efb5a9607d75f Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 2 Mar 2026 21:31:57 -0800 Subject: [PATCH 32/88] refactor: make MiniReportActionContextMenu React Compiler compatible Remove useMemo and eslint-disable suppressions that prevented React Compiler from optimizing this component. Inline all action JSX instead of mapping over an array, and inline the overflow menu button directly to avoid passing refs through factory functions. Made-with: Cursor --- .../MiniReportActionContextMenu/index.tsx | 570 +++++++++++------- 1 file changed, 366 insertions(+), 204 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index 2d3a11bc16c3..f43e9bf54fd5 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useMemo, useRef} from 'react'; +import React, {useEffect, useRef} from 'react'; import type {RefObject} from 'react'; import {createPortal} from 'react-dom'; import {View} from 'react-native'; @@ -15,7 +15,6 @@ import useStyleUtils from '@hooks/useStyleUtils'; import getButtonState from '@libs/getButtonState'; import {ACTION_IDS} from '@pages/inbox/report/ContextMenu/actions/actionConfig'; import {CONTEXT_MENU_ICON_NAMES} from '@pages/inbox/report/ContextMenu/actions/actionTypes'; -import type {ContextMenuAction} from '@pages/inbox/report/ContextMenu/actions/actionTypes'; import createCopyLinkAction, {shouldShowCopyLinkAction} from '@pages/inbox/report/ContextMenu/actions/copyLinkAction'; import createCopyMessageAction, {shouldShowCopyMessageAction} from '@pages/inbox/report/ContextMenu/actions/copyMessageAction'; import createDeleteAction, {shouldShowDeleteAction} from '@pages/inbox/report/ContextMenu/actions/deleteAction'; @@ -28,7 +27,6 @@ import createHoldAction, {shouldShowHoldAction} from '@pages/inbox/report/Contex import createJoinThreadAction, {shouldShowJoinThreadAction} from '@pages/inbox/report/ContextMenu/actions/joinThreadAction'; import createLeaveThreadAction, {shouldShowLeaveThreadAction} from '@pages/inbox/report/ContextMenu/actions/leaveThreadAction'; import createMarkAsUnreadAction, {shouldShowMarkAsUnreadForReportAction} from '@pages/inbox/report/ContextMenu/actions/markAsUnreadAction'; -import createOverflowMenuAction from '@pages/inbox/report/ContextMenu/actions/overflowMenuAction'; import createReplyInThreadAction, {shouldShowReplyInThreadAction} from '@pages/inbox/report/ContextMenu/actions/replyInThreadAction'; import createUnholdAction, {shouldShowUnholdAction} from '@pages/inbox/report/ContextMenu/actions/unholdAction'; import {useMiniContextMenuActions, useMiniContextMenuState} from '@pages/inbox/report/ContextMenu/MiniContextMenuProvider'; @@ -136,6 +134,7 @@ function MiniReportActionContextMenu() { }); }; + const reportAction = data.reportAction; const currentUserAccountID = data.currentUserPersonalDetails?.accountID ?? 0; const {interceptAnonymousUser, translate, disabledActionIDs} = data; @@ -201,159 +200,7 @@ function MiniReportActionContextMenu() { childReportActions: data.childReportActions, }) && !isDisabled(ACTION_IDS.DELETE); - /* eslint-disable react-hooks/refs -- factory functions store refs for later use, they don't read .current during render */ - const visibleActions = useMemo(() => { - if (!data.reportAction) { - return []; - } - const reportAction = data.reportAction; - const items: ContextMenuAction[] = []; - if (showReplyInThread) { - items.push( - createReplyInThreadAction({ - childReport: data.childReport, - reportAction, - originalReport: data.originalReport, - currentUserAccountID, - interceptAnonymousUser, - hideAndRun, - translate, - chatBubbleReplyIcon: icons.ChatBubbleReply, - }), - ); - } - if (showMarkAsUnread) { - items.push( - createMarkAsUnreadAction({ - reportID: data.reportID, - reportActions: data.reportActions, - reportAction, - currentUserAccountID, - interceptAnonymousUser, - hideAndRun, - translate, - chatBubbleUnreadIcon: icons.ChatBubbleUnread, - checkmarkIcon: icons.Checkmark, - }), - ); - } - if (showExplain) { - items.push( - createExplainAction({ - childReport: data.childReport, - originalReport: data.originalReport, - reportAction, - currentUserPersonalDetails: data.currentUserPersonalDetails, - interceptAnonymousUser, - hideAndRun, - translate, - conciergeIcon: icons.Concierge, - }), - ); - } - if (showEdit) { - items.push( - createEditAction({ - reportID: data.reportID, - reportAction, - moneyRequestAction: data.moneyRequestAction, - draftMessage: data.draftMessage, - introSelected: data.introSelected, - interceptAnonymousUser, - hideAndRun, - translate, - pencilIcon: icons.Pencil, - }), - ); - } - if (showUnhold) { - items.push( - createUnholdAction({ - moneyRequestAction: data.moneyRequestAction, - isDelegateAccessRestricted: data.isDelegateAccessRestricted, - showDelegateNoAccessModal: data.showDelegateNoAccessModal, - interceptAnonymousUser, - hideAndRun, - translate, - stopwatchIcon: icons.Stopwatch, - }), - ); - } - if (showHold) { - items.push( - createHoldAction({ - moneyRequestAction: data.moneyRequestAction, - isDelegateAccessRestricted: data.isDelegateAccessRestricted, - showDelegateNoAccessModal: data.showDelegateNoAccessModal, - interceptAnonymousUser, - hideAndRun, - translate, - stopwatchIcon: icons.Stopwatch, - }), - ); - } - if (showJoinThread) { - items.push( - createJoinThreadAction({reportAction, originalReport: data.originalReport, currentUserAccountID, interceptAnonymousUser, hideAndRun, translate, bellIcon: icons.Bell}), - ); - } - if (showLeaveThread) { - items.push( - createLeaveThreadAction({reportAction, originalReport: data.originalReport, currentUserAccountID, interceptAnonymousUser, hideAndRun, translate, exitIcon: icons.Exit}), - ); - } - if (showCopyMessage) { - items.push( - createCopyMessageAction({ - reportAction, - transaction: data.transaction, - selection: data.selection, - report: data.report, - card: data.card, - originalReport: data.originalReport, - isHarvestReport: data.isHarvestReport, - isTryNewDotNVPDismissed: data.isTryNewDotNVPDismissed, - movedFromReport: data.movedFromReport, - movedToReport: data.movedToReport, - childReport: data.childReport, - policy: data.policy, - getLocalDateFromDatetime: data.getLocalDateFromDatetime, - policyTags: data.policyTags, - translate, - harvestReport: data.harvestReport, - currentUserPersonalDetails: data.currentUserPersonalDetails, - interceptAnonymousUser, - copyIcon: icons.Copy, - checkmarkIcon: icons.Checkmark, - }), - ); - } - if (showCopyLink) { - items.push( - createCopyLinkAction({ - reportAction, - originalReportID: data.originalReportID, - interceptAnonymousUser, - translate, - linkCopyIcon: icons.LinkCopy, - checkmarkIcon: icons.Checkmark, - }), - ); - } - if (showFlagAsOffensive) { - items.push(createFlagAsOffensiveAction({reportID: data.reportID, reportAction, hideAndRun, translate, flagIcon: icons.Flag})); - } - if (showDownload) { - items.push( - createDownloadAction({reportAction, encryptedAuthToken: data.encryptedAuthToken, interceptAnonymousUser, download: data.download, translate, downloadIcon: icons.Download}), - ); - } - if (showDelete) { - items.push(createDeleteAction({reportID: data.reportID, reportAction, moneyRequestAction: data.moneyRequestAction, hideAndRun, translate, trashcanIcon: icons.Trashcan})); - } - return items; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ + const visibleCount = [ showReplyInThread, showMarkAsUnread, showExplain, @@ -367,12 +214,148 @@ function MiniReportActionContextMenu() { showFlagAsOffensive, showDownload, showDelete, - data, - currentUserAccountID, - interceptAnonymousUser, - translate, - icons, - ]); + ].filter(Boolean).length; + const needsOverflow = visibleCount > CONST.MINI_CONTEXT_MENU_MAX_ITEMS; + const displayLimit = needsOverflow ? CONST.MINI_CONTEXT_MENU_MAX_ITEMS - 1 : visibleCount; + + let displayedCount = 0; + const displayReplyInThread = showReplyInThread && ++displayedCount <= displayLimit; + const displayMarkAsUnread = showMarkAsUnread && ++displayedCount <= displayLimit; + const displayExplain = showExplain && ++displayedCount <= displayLimit; + const displayEdit = showEdit && ++displayedCount <= displayLimit; + const displayUnhold = showUnhold && ++displayedCount <= displayLimit; + const displayHold = showHold && ++displayedCount <= displayLimit; + const displayJoinThread = showJoinThread && ++displayedCount <= displayLimit; + const displayLeaveThread = showLeaveThread && ++displayedCount <= displayLimit; + const displayCopyMessage = showCopyMessage && ++displayedCount <= displayLimit; + const displayCopyLink = showCopyLink && ++displayedCount <= displayLimit; + const displayFlagAsOffensive = showFlagAsOffensive && ++displayedCount <= displayLimit; + const displayDownload = showDownload && ++displayedCount <= displayLimit; + const displayDelete = showDelete && ++displayedCount <= displayLimit; + + const replyInThreadAction = + displayReplyInThread && reportAction + ? createReplyInThreadAction({ + childReport: data.childReport, + reportAction, + originalReport: data.originalReport, + currentUserAccountID, + interceptAnonymousUser, + hideAndRun, + translate, + chatBubbleReplyIcon: icons.ChatBubbleReply, + }) + : null; + const markAsUnreadAction = + displayMarkAsUnread && reportAction + ? createMarkAsUnreadAction({ + reportID: data.reportID, + reportActions: data.reportActions, + reportAction, + currentUserAccountID, + interceptAnonymousUser, + hideAndRun, + translate, + chatBubbleUnreadIcon: icons.ChatBubbleUnread, + checkmarkIcon: icons.Checkmark, + }) + : null; + const explainAction = + displayExplain && reportAction + ? createExplainAction({ + childReport: data.childReport, + originalReport: data.originalReport, + reportAction, + currentUserPersonalDetails: data.currentUserPersonalDetails, + interceptAnonymousUser, + hideAndRun, + translate, + conciergeIcon: icons.Concierge, + }) + : null; + const editAction = + displayEdit && reportAction + ? createEditAction({ + reportID: data.reportID, + reportAction, + moneyRequestAction: data.moneyRequestAction, + draftMessage: data.draftMessage, + introSelected: data.introSelected, + interceptAnonymousUser, + hideAndRun, + translate, + pencilIcon: icons.Pencil, + }) + : null; + const unholdAction = displayUnhold + ? createUnholdAction({ + moneyRequestAction: data.moneyRequestAction, + isDelegateAccessRestricted: data.isDelegateAccessRestricted, + showDelegateNoAccessModal: data.showDelegateNoAccessModal, + interceptAnonymousUser, + hideAndRun, + translate, + stopwatchIcon: icons.Stopwatch, + }) + : null; + const holdAction = displayHold + ? createHoldAction({ + moneyRequestAction: data.moneyRequestAction, + isDelegateAccessRestricted: data.isDelegateAccessRestricted, + showDelegateNoAccessModal: data.showDelegateNoAccessModal, + interceptAnonymousUser, + hideAndRun, + translate, + stopwatchIcon: icons.Stopwatch, + }) + : null; + const joinThreadAction = + displayJoinThread && reportAction + ? createJoinThreadAction({reportAction, originalReport: data.originalReport, currentUserAccountID, interceptAnonymousUser, hideAndRun, translate, bellIcon: icons.Bell}) + : null; + const leaveThreadAction = + displayLeaveThread && reportAction + ? createLeaveThreadAction({reportAction, originalReport: data.originalReport, currentUserAccountID, interceptAnonymousUser, hideAndRun, translate, exitIcon: icons.Exit}) + : null; + const copyMessageAction = + displayCopyMessage && reportAction + ? createCopyMessageAction({ + reportAction, + transaction: data.transaction, + selection: data.selection, + report: data.report, + card: data.card, + originalReport: data.originalReport, + isHarvestReport: data.isHarvestReport, + isTryNewDotNVPDismissed: data.isTryNewDotNVPDismissed, + movedFromReport: data.movedFromReport, + movedToReport: data.movedToReport, + childReport: data.childReport, + policy: data.policy, + getLocalDateFromDatetime: data.getLocalDateFromDatetime, + policyTags: data.policyTags, + translate, + harvestReport: data.harvestReport, + currentUserPersonalDetails: data.currentUserPersonalDetails, + interceptAnonymousUser, + copyIcon: icons.Copy, + checkmarkIcon: icons.Checkmark, + }) + : null; + const copyLinkAction = + displayCopyLink && reportAction + ? createCopyLinkAction({reportAction, originalReportID: data.originalReportID, interceptAnonymousUser, translate, linkCopyIcon: icons.LinkCopy, checkmarkIcon: icons.Checkmark}) + : null; + const flagAsOffensiveAction = + displayFlagAsOffensive && reportAction ? createFlagAsOffensiveAction({reportID: data.reportID, reportAction, hideAndRun, translate, flagIcon: icons.Flag}) : null; + const downloadAction = + displayDownload && reportAction + ? createDownloadAction({reportAction, encryptedAuthToken: data.encryptedAuthToken, interceptAnonymousUser, download: data.download, translate, downloadIcon: icons.Download}) + : null; + const deleteAction = + displayDelete && reportAction + ? createDeleteAction({reportID: data.reportID, reportAction, moneyRequestAction: data.moneyRequestAction, hideAndRun, translate, trashcanIcon: icons.Trashcan}) + : null; const emojiData = createEmojiReactionData({ reportID: data.reportID, @@ -383,21 +366,8 @@ function MiniReportActionContextMenu() { hideAndRun, interceptAnonymousUser, }); - const overflowMenu = createOverflowMenuAction( - { - openOverflowMenu, - openContextMenu: () => miniActions.keepOpen(), - interceptAnonymousUser, - translate, - threeDotsIcon: icons.ThreeDots, - }, - threeDotRef, - ); - /* eslint-enable react-hooks/refs */ const hasEmoji = shouldShowEmojiReaction({reportAction: data.reportAction}) && !!emojiData.reportAction && !!emojiData.reportActionID; - const needsOverflow = visibleActions.length > CONST.MINI_CONTEXT_MENU_MAX_ITEMS; - const displayedActions = needsOverflow ? visibleActions.slice(0, CONST.MINI_CONTEXT_MENU_MAX_ITEMS - 1) : visibleActions; if (!state) { return null; @@ -436,45 +406,237 @@ function MiniReportActionContextMenu() { reportAction={emojiData.reportAction} /> )} - {displayedActions.map((action: ContextMenuAction) => ( + {!!replyInThreadAction && ( + + {({hovered, pressed}) => ( + + )} + + )} + {!!markAsUnreadAction && ( + + {({hovered, pressed}) => ( + + )} + + )} + {!!explainAction && ( + + {({hovered, pressed}) => ( + + )} + + )} + {!!editAction && ( + + {({hovered, pressed}) => ( + + )} + + )} + {!!unholdAction && ( + + {({hovered, pressed}) => ( + + )} + + )} + {!!holdAction && ( + + {({hovered, pressed}) => ( + + )} + + )} + {!!joinThreadAction && ( + + {({hovered, pressed}) => ( + + )} + + )} + {!!leaveThreadAction && ( + + {({hovered, pressed}) => ( + + )} + + )} + {!!copyMessageAction && ( + + {({hovered, pressed}) => ( + + )} + + )} + {!!copyLinkAction && ( + + {({hovered, pressed}) => ( + + )} + + )} + {!!flagAsOffensiveAction && ( {({hovered, pressed}) => ( )} - ))} - {!!(needsOverflow && overflowMenu) && - (() => { - const {buttonRef, text, onPress, sentryLabel, icon} = overflowMenu; - return ( - - {({hovered, pressed}) => ( - - )} - - ); - })()} + )} + {!!downloadAction && ( + + {({hovered, pressed}) => ( + + )} + + )} + {!!deleteAction && ( + + {({hovered, pressed}) => ( + + )} + + )} + {needsOverflow && ( + + interceptAnonymousUser(() => { + openOverflowMenu(new MouseEvent('click'), threeDotRef); + miniActions.keepOpen(); + }, true) + } + shouldPreventDefaultFocusOnPress={false} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.MENU} + > + {({hovered, pressed}) => ( + + )} + + )} , From e3a45dcb509f3acffb78b57dc63e64e2661391ff Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 3 Mar 2026 10:54:06 -0800 Subject: [PATCH 33/88] refactor: fix PopoverContextMenu React Compiler and composition issues - Inline measureContextMenuAnchorPosition into useEffect, remove eslint suppression - Replace renderContent switch with inline conditional JSX for each menu type - Remove dimensionsEventListener ref and EmitterSubscription import Made-with: Cursor --- .../report/ContextMenu/PopoverContextMenu.tsx | 181 ++++++++---------- 1 file changed, 77 insertions(+), 104 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu.tsx index 2ed25433ce8c..1283c042e8e6 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu.tsx @@ -1,7 +1,7 @@ import type {RefObject} from 'react'; import React, {useEffect, useImperativeHandle, useRef, useState} from 'react'; // eslint-disable-next-line no-restricted-imports -import type {EmitterSubscription, GestureResponderEvent, NativeTouchEvent, View as ViewType} from 'react-native'; +import type {GestureResponderEvent, NativeTouchEvent, View as ViewType} from 'react-native'; // eslint-disable-next-line no-restricted-imports import {Dimensions} from 'react-native'; import {Actions, useActionSheetAwareScrollViewActions} from '@components/ActionSheetAwareScrollView'; @@ -79,7 +79,6 @@ function PopoverContextMenu({ref: forwardedRef}: PopoverContextMenuProps) { const contentRef = useRef(null); const anchorRef = useRef(null); - const dimensionsEventListener = useRef(null); const contextMenuAnchorRef = useRef(null); const onPopoverShow = useRef(() => {}); @@ -97,42 +96,42 @@ function PopoverContextMenu({ref: forwardedRef}: PopoverContextMenuProps) { } }); - const measureContextMenuAnchorPosition = () => { + useEffect(() => { if (!isPopoverVisible) { return; } - getContextMenuMeasuredLocation().then(({x, y}) => { - if (!x || !y) { - return; - } - - setMenuState((prev) => { - if (!prev) { - return prev; + const listener = Dimensions.addEventListener('change', () => { + new Promise<{x: number; y: number}>((resolve) => { + if (contextMenuAnchorRef.current && 'measureInWindow' in contextMenuAnchorRef.current && typeof contextMenuAnchorRef.current.measureInWindow === 'function') { + contextMenuAnchorRef.current.measureInWindow((x, y) => resolve({x, y})); + } else { + resolve({x: 0, y: 0}); + } + }).then(({x, y}) => { + if (!x || !y) { + return; } - return { - ...prev, - position: { - ...prev.position, - anchorHorizontal: cursorRelativePosition.current.horizontal + x, - anchorVertical: cursorRelativePosition.current.vertical + y, - }, - }; + + setMenuState((prev) => { + if (!prev) { + return prev; + } + return { + ...prev, + position: { + ...prev.position, + anchorHorizontal: cursorRelativePosition.current.horizontal + x, + anchorVertical: cursorRelativePosition.current.vertical + y, + }, + }; + }); }); }); - }; - - useEffect(() => { - dimensionsEventListener.current = Dimensions.addEventListener('change', measureContextMenuAnchorPosition); return () => { - if (!dimensionsEventListener.current) { - return; - } - dimensionsEventListener.current.remove(); + listener.remove(); }; - // eslint-disable-next-line react-hooks/exhaustive-deps }, [isPopoverVisible]); const isActiveReportAction: ReportActionContextMenu['isActiveReportAction'] = (actionID) => !!actionID && reportActionID === String(actionID); @@ -343,81 +342,6 @@ function PopoverContextMenu({ref: forwardedRef}: PopoverContextMenuProps) { const shouldKeepOpen = localShouldKeepOpen; const shouldEnableArrowNavigation = isPopoverVisible || shouldKeepOpen; - const renderContent = () => { - if (!menuState) { - return null; - } - const contentProps: PopoverContentProps = { - menuState, - hideAndRun, - setLocalShouldKeepOpen, - transitionActionSheetState, - contentRef, - shouldEnableArrowNavigation, - }; - if (menuState.type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION) { - return ( - - ); - } - if (menuState.type === CONST.CONTEXT_MENU_TYPES.REPORT) { - return ( - - ); - } - if (menuState.type === CONST.CONTEXT_MENU_TYPES.LINK) { - return ( - - ); - } - if (menuState.type === CONST.CONTEXT_MENU_TYPES.EMAIL) { - return ( - - ); - } - if (menuState.type === CONST.CONTEXT_MENU_TYPES.TEXT) { - return ( - - ); - } - return null; - }; - return ( - {renderContent()} + {menuState?.type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && ( + + )} + {menuState?.type === CONST.CONTEXT_MENU_TYPES.REPORT && ( + + )} + {menuState?.type === CONST.CONTEXT_MENU_TYPES.LINK && ( + + )} + {menuState?.type === CONST.CONTEXT_MENU_TYPES.EMAIL && ( + + )} + {menuState?.type === CONST.CONTEXT_MENU_TYPES.TEXT && ( + + )} ); } From d8613ec32bca377fc1b25357dfcbb94e6383b148 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 3 Mar 2026 11:16:34 -0800 Subject: [PATCH 34/88] fix: replace dynamic import with static import in PopoverContextMenu React Compiler does not support import() expressions. Use static import of hideContextMenu since there is no circular dependency. Made-with: Cursor --- src/pages/inbox/report/ContextMenu/PopoverContextMenu.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu.tsx index 1283c042e8e6..113e3d070a48 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu.tsx @@ -19,6 +19,7 @@ import PopoverLinkContent from './PopoverLinkContent'; import PopoverReportActionContent from './PopoverReportActionContent'; import PopoverReportContent from './PopoverReportContent'; import PopoverTextContent from './PopoverTextContent'; +import {hideContextMenu} from './ReportActionContextMenu'; import type {ContextMenuAnchor, ContextMenuType, ReportActionContextMenu} from './ReportActionContextMenu'; function extractPointerEvent(event: GestureResponderEvent | MouseEvent): MouseEvent | NativeTouchEvent { @@ -334,9 +335,7 @@ function PopoverContextMenu({ref: forwardedRef}: PopoverContextMenuProps) { })); const hideAndRun = (callback?: () => void) => { - import('@pages/inbox/report/ContextMenu/ReportActionContextMenu').then(({hideContextMenu: hideCtx}) => { - hideCtx(false, callback); - }); + hideContextMenu(false, callback); }; const shouldKeepOpen = localShouldKeepOpen; From 03a155f7038fb02e79fb96e2d7130219ad0fb2cd Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 3 Mar 2026 12:33:56 -0800 Subject: [PATCH 35/88] Reorganize ContextMenu directory structure - Delete dead code: ContextMenuActions.tsx (zero importers) - Move PopoverContextMenu into PopoverContextMenu/ subdirectory with index.tsx - Create ReportAction/, Report/, Link/, Email/, Text/ subdirectories for each popover subtype - Move content components and data hooks into appropriate subdirectories - Update all import paths for moved files Made-with: Cursor --- .../report/ContextMenu/ContextMenuActions.tsx | 1295 ----------------- .../MiniReportActionContextMenu/index.tsx | 2 +- .../ConfirmDeleteReportActionModal.tsx | 0 .../Email}/PopoverEmailContent.tsx | 4 +- .../Link}/PopoverLinkContent.tsx | 4 +- .../Report}/PopoverReportContent.tsx | 20 +- .../Report}/useReportContextMenuData.ts | 6 +- .../PopoverReportActionContent.tsx | 42 +- .../useReportActionContextMenuData.ts | 6 +- .../Text}/PopoverTextContent.tsx | 4 +- .../index.tsx} | 14 +- 11 files changed, 51 insertions(+), 1346 deletions(-) delete mode 100644 src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx rename src/pages/inbox/report/ContextMenu/{ => PopoverContextMenu}/ConfirmDeleteReportActionModal.tsx (100%) rename src/pages/inbox/report/ContextMenu/{ => PopoverContextMenu/Email}/PopoverEmailContent.tsx (95%) rename src/pages/inbox/report/ContextMenu/{ => PopoverContextMenu/Link}/PopoverLinkContent.tsx (94%) rename src/pages/inbox/report/ContextMenu/{ => PopoverContextMenu/Report}/PopoverReportContent.tsx (88%) rename src/pages/inbox/report/ContextMenu/{ => PopoverContextMenu/Report}/useReportContextMenuData.ts (93%) rename src/pages/inbox/report/ContextMenu/{ => PopoverContextMenu/ReportAction}/PopoverReportActionContent.tsx (90%) rename src/pages/inbox/report/ContextMenu/{ => PopoverContextMenu/ReportAction}/useReportActionContextMenuData.ts (97%) rename src/pages/inbox/report/ContextMenu/{ => PopoverContextMenu/Text}/PopoverTextContent.tsx (94%) rename src/pages/inbox/report/ContextMenu/{PopoverContextMenu.tsx => PopoverContextMenu/index.tsx} (97%) diff --git a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx deleted file mode 100644 index 92b9cdc3770f..000000000000 --- a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx +++ /dev/null @@ -1,1295 +0,0 @@ -import {Str} from 'expensify-common'; -import type {RefObject} from 'react'; -import React from 'react'; -import type {GestureResponderEvent, View} from 'react-native'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import type {Emoji} from '@assets/emojis/types'; -import type {ExpensifyIconName} from '@components/Icon/ExpensifyIconLoader'; -import type {LocaleContextProps, LocalizedTranslate} from '@components/LocaleContextProvider'; -import MiniQuickEmojiReactions from '@components/Reactions/MiniQuickEmojiReactions'; -import QuickEmojiReactions from '@components/Reactions/QuickEmojiReactions'; -import type useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; -import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; -import {isMobileSafari} from '@libs/Browser'; -import Clipboard from '@libs/Clipboard'; -import getClipboardText from '@libs/Clipboard/getClipboardText'; -import EmailUtils from '@libs/EmailUtils'; -import {getEnvironmentURL} from '@libs/Environment/Environment'; -import fileDownload from '@libs/fileDownload'; -import getAttachmentDetails from '@libs/fileDownload/getAttachmentDetails'; -import {formatPhoneNumber as formatPhoneNumberPhoneUtils} from '@libs/LocalePhoneNumber'; -import {getForReportActionTemp} from '@libs/ModifiedExpenseMessage'; -import Navigation from '@libs/Navigation/Navigation'; -import Parser from '@libs/Parser'; -import {getCleanedTagName, isPolicyAdmin} from '@libs/PolicyUtils'; -import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import { - getActionableCardFraudAlertMessage, - getActionableMentionWhisperMessage, - getAddedApprovalRuleMessage, - getAddedBudgetMessage, - getAddedConnectionMessage, - getAutoPayApprovedReportsEnabledMessage, - getAutoReimbursementMessage, - getCardIssuedMessage, - getChangedApproverActionMessage, - getCompanyAddressUpdateMessage, - getCompanyCardConnectionBrokenMessage, - getCreatedReportForUnapprovedTransactionsMessage, - getCurrencyDefaultTaxUpdateMessage, - getCustomTaxNameUpdateMessage, - getDefaultApproverUpdateMessage, - getDeletedApprovalRuleMessage, - getDeletedBudgetMessage, - getDismissedViolationMessageText, - getDynamicExternalWorkflowRoutedMessage, - getExportIntegrationMessageHTML, - getForeignCurrencyDefaultTaxUpdateMessage, - getForwardsToUpdateMessage, - getHarvestCreatedExpenseReportMessage, - getIntegrationSyncFailedMessage, - getInvoiceCompanyNameUpdateMessage, - getInvoiceCompanyWebsiteUpdateMessage, - getIOUReportIDFromReportActionPreview, - getJoinRequestMessage, - getMarkedReimbursedMessage, - getMemberChangeMessageFragment, - getMessageOfOldDotReportAction, - getOriginalMessage, - getPlaidBalanceFailureMessage, - getPolicyChangeLogAddEmployeeMessage, - getPolicyChangeLogDefaultBillableMessage, - getPolicyChangeLogDefaultReimbursableMessage, - getPolicyChangeLogDefaultTitleEnforcedMessage, - getPolicyChangeLogDeleteMemberMessage, - getPolicyChangeLogMaxExpenseAgeMessage, - getPolicyChangeLogMaxExpenseAmountMessage, - getPolicyChangeLogMaxExpenseAmountNoReceiptMessage, - getPolicyChangeLogUpdateEmployee, - getReimburserUpdateMessage, - getRemovedConnectionMessage, - getRenamedAction, - getReportAction, - getReportActionMessageText, - getRoomAvatarUpdatedMessage, - getSetAutoJoinMessage, - getSettlementAccountLockedMessage, - getSubmitsToUpdateMessage, - getTagListNameUpdatedMessage, - getTagListUpdatedMessage, - getTagListUpdatedRequiredMessage, - getTravelUpdateMessage, - getUpdateACHAccountMessage, - getUpdatedApprovalRuleMessage, - getUpdatedAuditRateMessage, - getUpdatedAutoHarvestingMessage, - getUpdatedBudgetMessage, - getUpdatedDefaultTitleMessage, - getUpdatedIndividualBudgetNotificationMessage, - getUpdatedManualApprovalThresholdMessage, - getUpdatedOwnershipMessage, - getUpdatedProhibitedExpensesMessage, - getUpdatedReimbursementChoiceMessage, - getUpdatedSharedBudgetNotificationMessage, - getUpdatedTimeEnabledMessage, - getUpdatedTimeRateMessage, - getUpdateRoomDescriptionMessage, - getWorkspaceAttendeeTrackingUpdateMessage, - getWorkspaceCategoriesUpdatedMessage, - getWorkspaceCategoryUpdateMessage, - getWorkspaceCurrencyUpdateMessage, - getWorkspaceCustomUnitRateAddedMessage, - getWorkspaceCustomUnitRateDeletedMessage, - getWorkspaceCustomUnitRateImportedMessage, - getWorkspaceCustomUnitRateUpdatedMessage, - getWorkspaceCustomUnitSubRateDeletedMessage, - getWorkspaceCustomUnitSubRateUpdatedMessage, - getWorkspaceCustomUnitUpdatedMessage, - getWorkspaceDescriptionUpdatedMessage, - getWorkspaceFeatureEnabledMessage, - getWorkspaceFrequencyUpdateMessage, - getWorkspaceReimbursementUpdateMessage, - getWorkspaceReportFieldAddMessage, - getWorkspaceReportFieldDeleteMessage, - getWorkspaceReportFieldUpdateMessage, - getWorkspaceTagUpdateMessage, - getWorkspaceTaxUpdateMessage, - getWorkspaceUpdateFieldMessage, - hasReasoning, - isActionableJoinRequest, - isActionableMentionWhisper, - isActionableTrackExpense, - isActionOfType, - isCardIssuedAction, - isCreatedAction, - isCreatedTaskReportAction, - isDeletedAction as isDeletedActionReportActionsUtils, - isMarkAsClosedAction, - isMemberChangeAction, - isMessageDeleted, - isModifiedExpenseAction, - isMoneyRequestAction, - isMovedAction, - isOldDotReportAction, - isReimbursementDeQueuedOrCanceledAction, - isReimbursementQueuedAction, - isRenamedAction, - isReportActionAttachment, - isReportPreviewAction as isReportPreviewActionReportActionsUtils, - isTagModificationAction, - isTaskAction as isTaskActionReportActionsUtils, - isTripPreview, - isUnapprovedAction, - isWhisperAction as isWhisperActionReportActionsUtils, -} from '@libs/ReportActionsUtils'; -import {getReportName} from '@libs/ReportNameUtils'; -import { - canDeleteReportAction, - canEditReportAction, - canFlagReportAction, - canHoldUnholdReportAction, - changeMoneyRequestHoldStatus, - getChildReportNotificationPreference as getChildReportNotificationPreferenceReportUtils, - getDeletedTransactionMessage, - getIOUReportActionDisplayMessage, - getMovedActionMessage, - getMovedTransactionMessage, - getPolicyChangeMessage, - getReimbursementDeQueuedOrCanceledActionMessage, - getReimbursementQueuedActionMessage, - getReportName as getReportNameDeprecated, - getReportOrDraftReport, - getReportPreviewMessage, - getUnreportedTransactionMessage, - getWorkspaceNameUpdatedMessage, - isExpenseReport, - shouldDisableThread, - shouldDisplayThreadReplies as shouldDisplayThreadRepliesReportUtils, -} from '@libs/ReportUtils'; -import {getTaskCreatedMessage, getTaskReportActionMessage} from '@libs/TaskUtils'; -import {setDownload} from '@userActions/Download'; -import { - deleteReportActionDraft, - explain, - markCommentAsUnread, - navigateToAndOpenChildReport, - openReport, - readNewestAction, - saveReportActionDraft, - toggleEmojiReaction, - togglePinnedState, - toggleSubscribeToChildReport, -} from '@userActions/Report'; -import CONST from '@src/CONST'; -import type {TranslationPaths} from '@src/languages/types'; -import ROUTES from '@src/ROUTES'; -import type { - Beta, - Card, - Download as DownloadOnyx, - IntroSelected, - OnyxInputOrEntry, - Policy, - PolicyTagLists, - ReportAction, - ReportActionReactions, - ReportActions, - Report as ReportType, - Transaction, -} from '@src/types/onyx'; -import type IconAsset from '@src/types/utils/IconAsset'; -import type WithSentryLabel from '@src/types/utils/SentryLabel'; -import KeyboardUtils from '@src/utils/keyboard'; -import type {ContextMenuAnchor} from './ReportActionContextMenu'; -import {hideContextMenu, showDeleteModal} from './ReportActionContextMenu'; - -/** Gets the HTML version of the message in an action */ -function getActionHtml(reportAction: OnyxInputOrEntry): string { - const message = Array.isArray(reportAction?.message) ? (reportAction?.message?.at(-1) ?? null) : (reportAction?.message ?? null); - return message?.html ?? ''; -} - -/** Sets the HTML string to Clipboard */ -function setClipboardMessage(content: string | undefined) { - if (!content) { - return; - } - const clipboardText = getClipboardText(content); - if (!Clipboard.canSetHtml()) { - Clipboard.setString(clipboardText); - } else { - Clipboard.setHtml(content, clipboardText); - } -} - -type ShouldShow = (args: { - type: string; - reportAction: OnyxEntry; - childReportActions: OnyxCollection; - isArchivedRoom: boolean; - betas: OnyxEntry; - menuTarget: RefObject | undefined; - isChronosReport: boolean; - reportID?: string; - isPinnedChat: boolean; - isUnreadChat: boolean; - isThreadReportParentAction: boolean; - isOffline: boolean; - isMini: boolean; - isProduction: boolean; - moneyRequestAction: ReportAction | undefined; - areHoldRequirementsMet: boolean; - isDebugModeEnabled: OnyxEntry; - iouTransaction: OnyxEntry; - transactions?: OnyxCollection; - moneyRequestReport?: OnyxEntry; - moneyRequestPolicy?: OnyxEntry; - isHarvestReport?: boolean; -}) => boolean; - -type ContextMenuActionPayload = { - reportActions: OnyxEntry; - reportAction: ReportAction; - transaction?: OnyxEntry; - reportID: string | undefined; - originalReportID: string | undefined; - currentUserAccountID: number; - report: OnyxEntry; - policy?: OnyxEntry; - draftMessage: string; - selection: string; - close: () => void; - transitionActionSheetState: (params: {type: string; payload?: Record}) => void; - openContextMenu: () => void; - interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; - anchor?: RefObject; - checkIfContextMenuActive?: () => void; - openOverflowMenu: (event: GestureResponderEvent | MouseEvent, anchorRef: RefObject) => void; - event?: GestureResponderEvent | MouseEvent | KeyboardEvent; - setIsEmojiPickerActive?: (state: boolean) => void; - anchorRef?: RefObject; - moneyRequestAction: ReportAction | undefined; - card?: Card; - originalReport: OnyxEntry; - isHarvestReport?: boolean; - isTryNewDotNVPDismissed?: boolean; - childReport?: OnyxEntry; - movedFromReport?: OnyxEntry; - movedToReport?: OnyxEntry; - getLocalDateFromDatetime: LocaleContextProps['getLocalDateFromDatetime']; - policyTags: OnyxEntry; - translate: LocalizedTranslate; - harvestReport?: OnyxEntry; - introSelected?: OnyxEntry; - isDelegateAccessRestricted?: boolean; - showDelegateNoAccessModal?: () => void; - currentUserPersonalDetails: ReturnType; - encryptedAuthToken: string; -}; - -type OnPress = (closePopover: boolean, payload: ContextMenuActionPayload, selection?: string, reportID?: string, draftMessage?: string) => void; - -type RenderContent = (closePopover: boolean, payload: ContextMenuActionPayload) => React.ReactElement; - -type GetDescription = (selection?: string) => string | void; - -type ContextMenuActionWithContent = { - renderContent: RenderContent; -}; - -type ContextMenuActionWithIcon = WithSentryLabel & { - textTranslateKey: TranslationPaths; - icon: - | IconAsset - | Extract< - ExpensifyIconName, - | 'Download' - | 'ThreeDots' - | 'ChatBubbleReply' - | 'ChatBubbleUnread' - | 'Mail' - | 'Pencil' - | 'Stopwatch' - | 'Bell' - | 'Copy' - | 'LinkCopy' - | 'Pin' - | 'Flag' - | 'Bug' - | 'Trashcan' - | 'Exit' - | 'Concierge' - >; - successTextTranslateKey?: TranslationPaths; - successIcon?: - | IconAsset - | Extract< - ExpensifyIconName, - | 'Download' - | 'ChatBubbleReply' - | 'ChatBubbleUnread' - | 'Checkmark' - | 'Mail' - | 'Pencil' - | 'Stopwatch' - | 'Bell' - | 'Copy' - | 'LinkCopy' - | 'Pin' - | 'Flag' - | 'Bug' - | 'Trashcan' - | 'ThreeDots' - | 'Concierge' - >; - onPress: OnPress; - getDescription: GetDescription; -}; - -type ContextMenuAction = (ContextMenuActionWithContent | ContextMenuActionWithIcon) & { - isAnonymousAction: boolean; - shouldShow: ShouldShow; - shouldPreventDefaultFocusOnPress?: boolean; - shouldDisable?: (download: OnyxEntry) => boolean; -}; - -// A list of all the context actions in this menu. -const ContextMenuActions: ContextMenuAction[] = [ - { - isAnonymousAction: false, - shouldShow: ({type, reportAction}) => { - const isDynamicWorkflowRoutedAction = isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.DYNAMIC_EXTERNAL_WORKFLOW_ROUTED); - return type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && !!reportAction && 'message' in reportAction && !isMessageDeleted(reportAction) && !isDynamicWorkflowRoutedAction; - }, - renderContent: (closePopover, {reportID, reportAction, currentUserAccountID, close: closeManually, openContextMenu, setIsEmojiPickerActive}) => { - const isMini = !closePopover; - - const closeContextMenu = (onHideCallback?: () => void) => { - if (isMini) { - closeManually(); - if (onHideCallback) { - onHideCallback(); - } - } else { - hideContextMenu(false, onHideCallback); - } - }; - - const toggleEmojiAndCloseMenu = (emoji: Emoji, existingReactions: OnyxEntry, preferredSkinTone: number) => { - toggleEmojiReaction(reportID, reportAction, emoji, existingReactions, preferredSkinTone, currentUserAccountID); - closeContextMenu(); - setIsEmojiPickerActive?.(false); - }; - - if (isMini) { - return ( - { - openContextMenu(); - setIsEmojiPickerActive?.(true); - }} - onEmojiPickerClosed={() => { - closeContextMenu(); - setIsEmojiPickerActive?.(false); - }} - reportActionID={reportAction?.reportActionID} - reportAction={reportAction} - /> - ); - } - - return ( - - ); - }, - }, - { - isAnonymousAction: false, - textTranslateKey: 'reportActionContextMenu.replyInThread', - icon: 'ChatBubbleReply', - shouldShow: ({type, reportAction, reportID, isThreadReportParentAction, isArchivedRoom}) => { - if (type !== CONST.CONTEXT_MENU_TYPES.REPORT_ACTION || !reportID) { - return false; - } - return !shouldDisableThread(reportAction, isThreadReportParentAction, isArchivedRoom); - }, - onPress: (closePopover, {reportAction, childReport, originalReport, currentUserAccountID}) => { - if (closePopover) { - hideContextMenu(false, () => { - KeyboardUtils.dismiss().then(() => { - navigateToAndOpenChildReport(childReport, reportAction, originalReport, currentUserAccountID); - }); - }); - return; - } - navigateToAndOpenChildReport(childReport, reportAction, originalReport, currentUserAccountID); - }, - getDescription: () => {}, - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.REPLY_IN_THREAD, - }, - { - isAnonymousAction: false, - textTranslateKey: 'reportActionContextMenu.markAsUnread', - icon: 'ChatBubbleUnread', - successIcon: 'Checkmark', - shouldShow: ({type, reportAction, isUnreadChat}) => { - const isDynamicWorkflowRoutedAction = isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.DYNAMIC_EXTERNAL_WORKFLOW_ROUTED); - return (type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && !isDynamicWorkflowRoutedAction) || (type === CONST.CONTEXT_MENU_TYPES.REPORT && !isUnreadChat); - }, - onPress: (closePopover, {reportActions, reportAction, reportID, currentUserAccountID}) => { - markCommentAsUnread(reportID, reportActions, reportAction, currentUserAccountID); - if (closePopover) { - hideContextMenu(true, ReportActionComposeFocusManager.focus); - } - }, - getDescription: () => {}, - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.MARK_AS_UNREAD, - }, - { - isAnonymousAction: false, - textTranslateKey: 'reportActionContextMenu.explain', - icon: 'Concierge', - shouldShow: ({type, reportAction, isArchivedRoom}): boolean => { - if (type !== CONST.CONTEXT_MENU_TYPES.REPORT_ACTION || isArchivedRoom || !reportAction) { - return false; - } - - return hasReasoning(reportAction); - }, - onPress: (closePopover, {reportAction, childReport, originalReport, translate, currentUserPersonalDetails}) => { - if (!originalReport?.reportID) { - return; - } - - if (closePopover) { - hideContextMenu(false, () => { - KeyboardUtils.dismiss().then(() => { - explain(childReport, originalReport, reportAction, translate, currentUserPersonalDetails.accountID, currentUserPersonalDetails?.timezone); - }); - }); - return; - } - - explain(childReport, originalReport, reportAction, translate, currentUserPersonalDetails.accountID, currentUserPersonalDetails?.timezone); - }, - getDescription: () => {}, - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.EXPLAIN, - }, - { - isAnonymousAction: false, - textTranslateKey: 'reportActionContextMenu.markAsRead', - icon: 'Mail', - successIcon: 'Checkmark', - shouldShow: ({type, isUnreadChat}) => type === CONST.CONTEXT_MENU_TYPES.REPORT && isUnreadChat, - onPress: (closePopover, {reportID}) => { - readNewestAction(reportID, true, true); - if (closePopover) { - hideContextMenu(true, ReportActionComposeFocusManager.focus); - } - }, - getDescription: () => {}, - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.MARK_AS_READ, - }, - { - isAnonymousAction: false, - textTranslateKey: 'reportActionContextMenu.editAction', - icon: 'Pencil', - shouldShow: ({type, reportAction, isArchivedRoom, isChronosReport, moneyRequestAction}) => - type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && (canEditReportAction(reportAction) || canEditReportAction(moneyRequestAction)) && !isArchivedRoom && !isChronosReport, - onPress: (closePopover, {reportID, reportAction, draftMessage, moneyRequestAction, introSelected}) => { - if (isMoneyRequestAction(reportAction) || isMoneyRequestAction(moneyRequestAction)) { - const editExpense = () => { - const childReportID = reportAction?.childReportID; - openReport(childReportID, introSelected); - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID)); - }; - if (closePopover) { - hideContextMenu(false, editExpense); - return; - } - editExpense(); - return; - } - const editAction = () => { - if (!draftMessage) { - saveReportActionDraft(reportID, reportAction, Parser.htmlToMarkdown(getActionHtml(reportAction))); - } else { - deleteReportActionDraft(reportID, reportAction); - } - }; - - if (closePopover) { - // Hide popover, then call editAction - hideContextMenu(false, editAction); - return; - } - - // No popover to hide, call editAction immediately - editAction(); - }, - getDescription: () => {}, - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.EDIT_COMMENT, - }, - { - isAnonymousAction: false, - textTranslateKey: 'iou.unhold', - icon: 'Stopwatch', - shouldShow: ({type, moneyRequestReport, moneyRequestAction, moneyRequestPolicy, areHoldRequirementsMet, iouTransaction}) => { - if (type !== CONST.CONTEXT_MENU_TYPES.REPORT_ACTION || !areHoldRequirementsMet) { - return false; - } - const holdReportAction = getReportAction(moneyRequestAction?.childReportID, `${iouTransaction?.comment?.hold ?? ''}`); - return canHoldUnholdReportAction(moneyRequestReport, moneyRequestAction, holdReportAction, iouTransaction, moneyRequestPolicy).canUnholdRequest; - }, - onPress: (closePopover, {moneyRequestAction, isDelegateAccessRestricted, showDelegateNoAccessModal}) => { - if (isDelegateAccessRestricted) { - hideContextMenu(false, showDelegateNoAccessModal); - return; - } - - if (closePopover) { - hideContextMenu(false, () => changeMoneyRequestHoldStatus(moneyRequestAction)); - return; - } - - // No popover to hide, call changeMoneyRequestHoldStatus immediately - changeMoneyRequestHoldStatus(moneyRequestAction); - }, - getDescription: () => {}, - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.UNHOLD, - }, - { - isAnonymousAction: false, - textTranslateKey: 'iou.hold', - icon: 'Stopwatch', - shouldShow: ({type, moneyRequestReport, moneyRequestAction, moneyRequestPolicy, areHoldRequirementsMet, iouTransaction}) => { - if (type !== CONST.CONTEXT_MENU_TYPES.REPORT_ACTION || !areHoldRequirementsMet) { - return false; - } - const holdReportAction = getReportAction(moneyRequestAction?.childReportID, `${iouTransaction?.comment?.hold ?? ''}`); - return canHoldUnholdReportAction(moneyRequestReport, moneyRequestAction, holdReportAction, iouTransaction, moneyRequestPolicy).canHoldRequest; - }, - onPress: (closePopover, {moneyRequestAction, isDelegateAccessRestricted, showDelegateNoAccessModal}) => { - if (isDelegateAccessRestricted) { - hideContextMenu(false, showDelegateNoAccessModal); - return; - } - - if (closePopover) { - hideContextMenu(false, () => changeMoneyRequestHoldStatus(moneyRequestAction)); - return; - } - - // No popover to hide, call changeMoneyRequestHoldStatus immediately - changeMoneyRequestHoldStatus(moneyRequestAction); - }, - getDescription: () => {}, - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.HOLD, - }, - { - isAnonymousAction: false, - textTranslateKey: 'reportActionContextMenu.joinThread', - icon: 'Bell', - shouldShow: ({reportAction, isArchivedRoom, isThreadReportParentAction, isHarvestReport}) => { - const childReportNotificationPreference = getChildReportNotificationPreferenceReportUtils(reportAction); - const isDeletedAction = isDeletedActionReportActionsUtils(reportAction); - const shouldDisplayThreadReplies = shouldDisplayThreadRepliesReportUtils(reportAction, isThreadReportParentAction); - const subscribed = childReportNotificationPreference !== 'hidden'; - const isWhisperAction = isWhisperActionReportActionsUtils(reportAction) || isActionableTrackExpense(reportAction); - const isExpenseReportAction = isMoneyRequestAction(reportAction) || isReportPreviewActionReportActionsUtils(reportAction); - const isTaskAction = isCreatedTaskReportAction(reportAction); - const isHarvestCreatedExpenseReportAction = isHarvestReport && isCreatedAction(reportAction); - const shouldDisableJoinThread = shouldDisableThread(reportAction, isThreadReportParentAction, isArchivedRoom); - return ( - !subscribed && - !isWhisperAction && - !isTaskAction && - !isExpenseReportAction && - !isThreadReportParentAction && - !isHarvestCreatedExpenseReportAction && - !shouldDisableJoinThread && - (shouldDisplayThreadReplies || (!isDeletedAction && !isArchivedRoom)) - ); - }, - onPress: (closePopover, {reportAction, currentUserAccountID, originalReport}) => { - const childReportNotificationPreference = getChildReportNotificationPreferenceReportUtils(reportAction); - if (closePopover) { - hideContextMenu(false, () => { - ReportActionComposeFocusManager.focus(); - toggleSubscribeToChildReport(reportAction?.childReportID, currentUserAccountID, reportAction, originalReport, childReportNotificationPreference); - }); - return; - } - - ReportActionComposeFocusManager.focus(); - toggleSubscribeToChildReport(reportAction?.childReportID, currentUserAccountID, reportAction, originalReport, childReportNotificationPreference); - }, - getDescription: () => {}, - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.JOIN_THREAD, - }, - { - isAnonymousAction: false, - textTranslateKey: 'reportActionContextMenu.leaveThread', - icon: 'Exit', - shouldShow: ({reportAction, isArchivedRoom, isThreadReportParentAction, isHarvestReport}) => { - const childReportNotificationPreference = getChildReportNotificationPreferenceReportUtils(reportAction); - const isDeletedAction = isDeletedActionReportActionsUtils(reportAction); - const shouldDisplayThreadReplies = shouldDisplayThreadRepliesReportUtils(reportAction, isThreadReportParentAction); - const subscribed = childReportNotificationPreference !== 'hidden'; - const isWhisperAction = isWhisperActionReportActionsUtils(reportAction) || isActionableTrackExpense(reportAction); - const isExpenseReportAction = isMoneyRequestAction(reportAction) || isReportPreviewActionReportActionsUtils(reportAction); - const isTaskAction = isCreatedTaskReportAction(reportAction); - const isHarvestCreatedExpenseReportAction = isHarvestReport && isCreatedAction(reportAction); - return ( - subscribed && - !isWhisperAction && - !isTaskAction && - !isExpenseReportAction && - !isThreadReportParentAction && - !isHarvestCreatedExpenseReportAction && - (shouldDisplayThreadReplies || (!isDeletedAction && !isArchivedRoom)) - ); - }, - onPress: (closePopover, {reportAction, currentUserAccountID, originalReport}) => { - const childReportNotificationPreference = getChildReportNotificationPreferenceReportUtils(reportAction); - if (closePopover) { - hideContextMenu(false, () => { - ReportActionComposeFocusManager.focus(); - toggleSubscribeToChildReport(reportAction?.childReportID, currentUserAccountID, reportAction, originalReport, childReportNotificationPreference); - }); - return; - } - - ReportActionComposeFocusManager.focus(); - toggleSubscribeToChildReport(reportAction?.childReportID, currentUserAccountID, reportAction, originalReport, childReportNotificationPreference); - }, - getDescription: () => {}, - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.LEAVE_THREAD, - }, - { - isAnonymousAction: true, - textTranslateKey: 'reportActionContextMenu.copyURLToClipboard', - icon: 'Copy', - successTextTranslateKey: 'reportActionContextMenu.copied', - successIcon: 'Checkmark', - shouldShow: ({type}) => type === CONST.CONTEXT_MENU_TYPES.LINK, - onPress: (closePopover, {selection}) => { - Clipboard.setString(selection); - hideContextMenu(true, ReportActionComposeFocusManager.focus); - }, - getDescription: (selection) => selection, - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_URL, - }, - { - isAnonymousAction: true, - textTranslateKey: 'common.copyToClipboard', - icon: 'Copy', - successTextTranslateKey: 'reportActionContextMenu.copied', - successIcon: 'Checkmark', - shouldShow: ({type}) => type === CONST.CONTEXT_MENU_TYPES.TEXT, - onPress: (closePopover, {selection}) => { - Clipboard.setString(selection); - hideContextMenu(true, ReportActionComposeFocusManager.focus); - }, - getDescription: () => undefined, - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_TO_CLIPBOARD, - }, - { - isAnonymousAction: true, - textTranslateKey: 'reportActionContextMenu.copyEmailToClipboard', - icon: 'Copy', - successTextTranslateKey: 'reportActionContextMenu.copied', - successIcon: 'Checkmark', - shouldShow: ({type}) => type === CONST.CONTEXT_MENU_TYPES.EMAIL, - onPress: (closePopover, {selection}) => { - Clipboard.setString(EmailUtils.trimMailTo(selection)); - hideContextMenu(true, ReportActionComposeFocusManager.focus); - }, - getDescription: (selection) => EmailUtils.prefixMailSeparatorsWithBreakOpportunities(EmailUtils.trimMailTo(selection ?? '')), - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_EMAIL, - }, - { - isAnonymousAction: true, - textTranslateKey: 'reportActionContextMenu.copyMessage', - icon: 'Copy', - successTextTranslateKey: 'reportActionContextMenu.copied', - successIcon: 'Checkmark', - shouldShow: ({type, reportAction}) => - type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && !isReportActionAttachment(reportAction) && !isMessageDeleted(reportAction) && !isTripPreview(reportAction), - - // If return value is true, we switch the `text` and `icon` on - // `ContextMenuItem` with `successText` and `successIcon` which will fall back to - // the `text` and `icon` - onPress: ( - closePopover, - { - reportAction, - transaction, - selection, - report, - card, - originalReport, - isHarvestReport, - isTryNewDotNVPDismissed, - movedFromReport, - movedToReport, - childReport, - policy, - getLocalDateFromDatetime, - policyTags, - translate, - harvestReport, - currentUserPersonalDetails, - }, - ) => { - const isReportPreviewAction = isReportPreviewActionReportActionsUtils(reportAction); - const messageHtml = getActionHtml(reportAction); - const messageText = getReportActionMessageText(reportAction); - - const isAttachment = isReportActionAttachment(reportAction); - if (!isAttachment) { - const content = selection || messageHtml; - if (isReportPreviewAction) { - const iouReportID = getIOUReportIDFromReportActionPreview(reportAction); - const displayMessage = getReportPreviewMessage(iouReportID, reportAction, undefined, undefined, undefined, undefined, undefined, true); - Clipboard.setString(displayMessage); - } else if (isTaskActionReportActionsUtils(reportAction)) { - const {text, html} = getTaskReportActionMessage(translate, reportAction); - const displayMessage = html ?? text; - setClipboardMessage(displayMessage); - } else if (isModifiedExpenseAction(reportAction)) { - const modifyExpenseMessage = getForReportActionTemp({ - translate, - reportAction, - policy, - movedFromReport, - movedToReport, - policyTags, - currentUserLogin: currentUserPersonalDetails?.email ?? '', - }); - Clipboard.setString(modifyExpenseMessage); - } else if (isReimbursementDeQueuedOrCanceledAction(reportAction)) { - const displayMessage = getReimbursementDeQueuedOrCanceledActionMessage(translate, reportAction, report); - Clipboard.setString(displayMessage); - } else if (isMoneyRequestAction(reportAction)) { - const displayMessage = getIOUReportActionDisplayMessage(translate, reportAction, transaction, report); - if (displayMessage === Parser.htmlToText(displayMessage)) { - Clipboard.setString(displayMessage); - } else { - setClipboardMessage(displayMessage); - } - } else if (isCreatedTaskReportAction(reportAction)) { - const taskPreviewMessage = getTaskCreatedMessage(translate, reportAction, childReport, true); - Clipboard.setString(taskPreviewMessage); - } else if (isMemberChangeAction(reportAction)) { - // eslint-disable-next-line @typescript-eslint/no-deprecated - const logMessage = getMemberChangeMessageFragment(translate, reportAction, getReportNameDeprecated).html ?? ''; - setClipboardMessage(logMessage); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_NAME) { - Clipboard.setString(Str.htmlDecode(getWorkspaceNameUpdatedMessage(translate, reportAction))); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_DESCRIPTION) { - Clipboard.setString(getWorkspaceDescriptionUpdatedMessage(translate, reportAction)); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_CURRENCY) { - Clipboard.setString(getWorkspaceCurrencyUpdateMessage(translate, reportAction)); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_AUTO_REPORTING_FREQUENCY) { - Clipboard.setString(getWorkspaceFrequencyUpdateMessage(translate, reportAction)); - } else if ( - reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_CATEGORY || - reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_CATEGORY || - reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_CATEGORY || - reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.SET_CATEGORY_NAME - ) { - Clipboard.setString(getWorkspaceCategoryUpdateMessage(translate, reportAction)); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_CATEGORIES) { - Clipboard.setString(getWorkspaceCategoriesUpdatedMessage(translate, reportAction)); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.IMPORT_TAGS) { - Clipboard.setString(translate('workspaceActions.importTags')); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_ALL_TAGS) { - Clipboard.setString(translate('workspaceActions.deletedAllTags')); - } else if ( - reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_TAX || - reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_TAX || - reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_TAX - ) { - Clipboard.setString(getWorkspaceTaxUpdateMessage(translate, reportAction)); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_CUSTOM_TAX_NAME) { - Clipboard.setString(getCustomTaxNameUpdateMessage(translate, reportAction)); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_CURRENCY_DEFAULT_TAX) { - Clipboard.setString(getCurrencyDefaultTaxUpdateMessage(translate, reportAction)); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_FOREIGN_CURRENCY_DEFAULT_TAX) { - Clipboard.setString(getForeignCurrencyDefaultTaxUpdateMessage(translate, reportAction)); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_TAG_LIST_NAME) { - Clipboard.setString(getCleanedTagName(getTagListNameUpdatedMessage(translate, reportAction))); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_TAG_LIST) { - Clipboard.setString(getCleanedTagName(getTagListUpdatedMessage(translate, reportAction))); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_TAG_LIST_REQUIRED) { - Clipboard.setString(getCleanedTagName(getTagListUpdatedRequiredMessage(translate, reportAction))); - } else if (isTagModificationAction(reportAction.actionName)) { - Clipboard.setString(getCleanedTagName(getWorkspaceTagUpdateMessage(translate, reportAction))); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_CUSTOM_UNIT) { - Clipboard.setString(getWorkspaceCustomUnitUpdatedMessage(translate, reportAction)); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.IMPORT_CUSTOM_UNIT_RATES) { - Clipboard.setString(getWorkspaceCustomUnitRateImportedMessage(translate, reportAction)); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_CUSTOM_UNIT_RATE) { - Clipboard.setString(getWorkspaceCustomUnitRateAddedMessage(translate, reportAction)); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_CUSTOM_UNIT_RATE) { - Clipboard.setString(getWorkspaceCustomUnitRateUpdatedMessage(translate, reportAction)); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_CUSTOM_UNIT_RATE) { - Clipboard.setString(getWorkspaceCustomUnitRateDeletedMessage(translate, reportAction)); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_CUSTOM_UNIT_SUB_RATE) { - Clipboard.setString(getWorkspaceCustomUnitSubRateUpdatedMessage(translate, reportAction)); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_CUSTOM_UNIT_SUB_RATE) { - Clipboard.setString(getWorkspaceCustomUnitSubRateDeletedMessage(translate, reportAction)); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_REPORT_FIELD) { - Clipboard.setString(getWorkspaceReportFieldAddMessage(translate, reportAction)); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_REPORT_FIELD) { - Clipboard.setString(getWorkspaceReportFieldUpdateMessage(translate, reportAction)); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_REPORT_FIELD) { - Clipboard.setString(getWorkspaceReportFieldDeleteMessage(translate, reportAction)); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_FIELD) { - setClipboardMessage(getWorkspaceUpdateFieldMessage(translate, reportAction)); - } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_FEATURE_ENABLED) { - Clipboard.setString(getWorkspaceFeatureEnabledMessage(translate, reportAction)); - } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_IS_ATTENDEE_TRACKING_ENABLED) { - Clipboard.setString(getWorkspaceAttendeeTrackingUpdateMessage(translate, reportAction)); - } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_DEFAULT_APPROVER) { - Clipboard.setString(getDefaultApproverUpdateMessage(translate, reportAction)); - } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_SUBMITS_TO) { - Clipboard.setString(getSubmitsToUpdateMessage(translate, reportAction)); - } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_FORWARDS_TO) { - Clipboard.setString(getForwardsToUpdateMessage(translate, reportAction)); - } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_AUTO_PAY_APPROVED_REPORTS_ENABLED) { - Clipboard.setString(getAutoPayApprovedReportsEnabledMessage(translate, reportAction)); - } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_AUTO_REIMBURSEMENT) { - Clipboard.setString(getAutoReimbursementMessage(translate, reportAction)); - } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_INVOICE_COMPANY_NAME) { - Clipboard.setString(getInvoiceCompanyNameUpdateMessage(translate, reportAction)); - } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_INVOICE_COMPANY_WEBSITE) { - Clipboard.setString(getInvoiceCompanyWebsiteUpdateMessage(translate, reportAction)); - } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_REIMBURSER) { - Clipboard.setString(getReimburserUpdateMessage(translate, reportAction)); - } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_REIMBURSEMENT_ENABLED) { - Clipboard.setString(getWorkspaceReimbursementUpdateMessage(translate, reportAction)); - } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_ACH_ACCOUNT) { - Clipboard.setString(getUpdateACHAccountMessage(translate, reportAction)); - } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_ADDRESS) { - Clipboard.setString(getCompanyAddressUpdateMessage(translate, reportAction)); - } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_MAX_EXPENSE_AMOUNT_NO_RECEIPT) { - Clipboard.setString(getPolicyChangeLogMaxExpenseAmountNoReceiptMessage(translate, reportAction)); - } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_MAX_EXPENSE_AMOUNT) { - Clipboard.setString(getPolicyChangeLogMaxExpenseAmountMessage(translate, reportAction)); - } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_MAX_EXPENSE_AGE) { - Clipboard.setString(getPolicyChangeLogMaxExpenseAgeMessage(translate, reportAction)); - } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_DEFAULT_BILLABLE) { - Clipboard.setString(getPolicyChangeLogDefaultBillableMessage(translate, reportAction)); - } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_DEFAULT_REIMBURSABLE) { - Clipboard.setString(getPolicyChangeLogDefaultReimbursableMessage(translate, reportAction)); - } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_DEFAULT_TITLE_ENFORCED) { - Clipboard.setString(getPolicyChangeLogDefaultTitleEnforcedMessage(translate, reportAction)); - } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_OWNERSHIP) { - setClipboardMessage(Parser.htmlToText(getUpdatedOwnershipMessage(translate, reportAction, policy) ?? '')); - } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.UNREPORTED_TRANSACTION)) { - setClipboardMessage(getUnreportedTransactionMessage(translate, reportAction)); - } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.MARKED_REIMBURSED)) { - Clipboard.setString(getMarkedReimbursedMessage(translate, reportAction)); - } else if (isReimbursementQueuedAction(reportAction)) { - Clipboard.setString( - getReimbursementQueuedActionMessage({reportAction, translate, formatPhoneNumber: formatPhoneNumberPhoneUtils, report, shouldUseShortDisplayName: false}), - ); - } else if (isActionableMentionWhisper(reportAction)) { - const mentionWhisperMessage = getActionableMentionWhisperMessage(translate, reportAction); - setClipboardMessage(mentionWhisperMessage); - } else if (isActionableTrackExpense(reportAction)) { - setClipboardMessage(CONST.ACTIONABLE_TRACK_EXPENSE_WHISPER_MESSAGE); - } else if (isRenamedAction(reportAction)) { - setClipboardMessage(getRenamedAction(translate, reportAction, isExpenseReport(report))); - } else if ( - isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.SUBMITTED) || - isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.SUBMITTED_AND_CLOSED) || - isMarkAsClosedAction(reportAction) - ) { - const harvesting = !isMarkAsClosedAction(reportAction) ? (getOriginalMessage(reportAction)?.harvesting ?? false) : false; - if (harvesting) { - setClipboardMessage(translate('iou.automaticallySubmitted')); - } else { - Clipboard.setString(translate('iou.submitted', getOriginalMessage(reportAction)?.message)); - } - } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.APPROVED)) { - const {automaticAction} = getOriginalMessage(reportAction) ?? {}; - if (automaticAction) { - setClipboardMessage(translate('iou.automaticallyApproved')); - } else { - Clipboard.setString(translate('iou.approvedMessage')); - } - } else if (isUnapprovedAction(reportAction)) { - Clipboard.setString(translate('iou.unapproved')); - } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.FORWARDED)) { - const {automaticAction} = getOriginalMessage(reportAction) ?? {}; - if (automaticAction) { - setClipboardMessage(translate('iou.automaticallyForwarded')); - } else { - Clipboard.setString(translate('iou.forwarded')); - } - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REJECTED) { - Clipboard.setString(translate('iou.rejectedThisReport')); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.CORPORATE_UPGRADE) { - const displayMessage = translate('workspaceActions.upgradedWorkspace'); - Clipboard.setString(displayMessage); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.CORPORATE_FORCE_UPGRADE) { - const displayMessage = Parser.htmlToText(translate('workspaceActions.forcedCorporateUpgrade')); - Clipboard.setString(displayMessage); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.TEAM_DOWNGRADE) { - Clipboard.setString(translate('workspaceActions.downgradedWorkspace')); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.HOLD) { - Clipboard.setString(translate('iou.heldExpense')); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.UNHOLD) { - Clipboard.setString(translate('iou.unheldExpense')); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REJECTEDTRANSACTION_THREAD) { - Clipboard.setString(translate('iou.reject.reportActions.rejectedExpense')); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REJECTED_TRANSACTION_MARKASRESOLVED) { - Clipboard.setString(translate('iou.reject.reportActions.markedAsResolved')); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.RETRACTED) { - Clipboard.setString(translate('iou.retracted')); - } else if (isOldDotReportAction(reportAction)) { - const oldDotActionMessage = getMessageOfOldDotReportAction(translate, reportAction); - Clipboard.setString(oldDotActionMessage); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.DISMISSED_VIOLATION) { - const originalMessage = getOriginalMessage(reportAction) as ReportAction['originalMessage']; - Clipboard.setString(getDismissedViolationMessageText(translate, originalMessage)); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.RESOLVED_DUPLICATES) { - Clipboard.setString(translate('violations.resolvedDuplicates')); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.EXPORTED_TO_INTEGRATION) { - setClipboardMessage(getExportIntegrationMessageHTML(translate, reportAction)); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOM_CHANGE_LOG.UPDATE_ROOM_DESCRIPTION) { - setClipboardMessage(getUpdateRoomDescriptionMessage(translate, reportAction)); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOM_CHANGE_LOG.UPDATE_ROOM_AVATAR) { - setClipboardMessage(getRoomAvatarUpdatedMessage(translate, reportAction)); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EMPLOYEE) { - setClipboardMessage(getPolicyChangeLogAddEmployeeMessage(translate, reportAction)); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EMPLOYEE) { - setClipboardMessage(getPolicyChangeLogUpdateEmployee(translate, reportAction)); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_EMPLOYEE) { - setClipboardMessage(getPolicyChangeLogDeleteMemberMessage(translate, reportAction)); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.DELETED_TRANSACTION) { - setClipboardMessage(getDeletedTransactionMessage(translate, reportAction)); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REOPENED) { - setClipboardMessage(translate('iou.reopened')); - } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.INTEGRATION_SYNC_FAILED)) { - setClipboardMessage(getIntegrationSyncFailedMessage(translate, reportAction, report?.policyID, isTryNewDotNVPDismissed)); - } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.COMPANY_CARD_CONNECTION_BROKEN)) { - setClipboardMessage(getCompanyCardConnectionBrokenMessage(translate, reportAction)); - } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.PLAID_BALANCE_FAILURE)) { - setClipboardMessage(getPlaidBalanceFailureMessage(translate, reportAction)); - } else if (isCardIssuedAction(reportAction)) { - const shouldNavigateToCardDetails = isPolicyAdmin(policy, currentUserPersonalDetails.login); - setClipboardMessage( - getCardIssuedMessage({reportAction, shouldRenderHTML: true, shouldNavigateToCardDetails, policyID: report?.policyID, expensifyCard: card, translate}), - ); - } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_INTEGRATION)) { - setClipboardMessage(getAddedConnectionMessage(translate, reportAction)); - } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_INTEGRATION)) { - setClipboardMessage(getRemovedConnectionMessage(translate, reportAction)); - } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.TRAVEL_UPDATE)) { - setClipboardMessage(getTravelUpdateMessage(translate, reportAction)); - } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_AUDIT_RATE)) { - setClipboardMessage(getUpdatedAuditRateMessage(translate, reportAction)); - } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_APPROVER_RULE)) { - setClipboardMessage(getAddedApprovalRuleMessage(translate, reportAction)); - } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_APPROVER_RULE)) { - setClipboardMessage(getDeletedApprovalRuleMessage(translate, reportAction)); - } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_APPROVER_RULE)) { - setClipboardMessage(getUpdatedApprovalRuleMessage(translate, reportAction)); - } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_MANUAL_APPROVAL_THRESHOLD)) { - setClipboardMessage(getUpdatedManualApprovalThresholdMessage(translate, reportAction)); - } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_BUDGET)) { - setClipboardMessage(getAddedBudgetMessage(translate, reportAction, policy)); - } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_BUDGET)) { - setClipboardMessage(getUpdatedBudgetMessage(translate, reportAction, policy)); - } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_BUDGET)) { - setClipboardMessage(getDeletedBudgetMessage(translate, reportAction, policy)); - } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_TIME_ENABLED)) { - setClipboardMessage(getUpdatedTimeEnabledMessage(translate, reportAction)); - } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_TIME_RATE)) { - setClipboardMessage(getUpdatedTimeRateMessage(translate, reportAction)); - } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_PROHIBITED_EXPENSES)) { - setClipboardMessage(getUpdatedProhibitedExpensesMessage(translate, reportAction)); - } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_REIMBURSEMENT_CHOICE)) { - setClipboardMessage(getUpdatedReimbursementChoiceMessage(translate, reportAction)); - } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.SET_AUTO_JOIN)) { - setClipboardMessage(getSetAutoJoinMessage(translate, reportAction)); - } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_DEFAULT_TITLE)) { - setClipboardMessage(getUpdatedDefaultTitleMessage(translate, reportAction)); - } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_AUTO_HARVESTING)) { - setClipboardMessage(getUpdatedAutoHarvestingMessage(translate, reportAction)); - } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.INDIVIDUAL_BUDGET_NOTIFICATION)) { - setClipboardMessage(getUpdatedIndividualBudgetNotificationMessage(translate, reportAction)); - } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.SHARED_BUDGET_NOTIFICATION)) { - setClipboardMessage(getUpdatedSharedBudgetNotificationMessage(translate, reportAction)); - } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.TAKE_CONTROL) || isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.REROUTE)) { - setClipboardMessage(getChangedApproverActionMessage(translate, reportAction)); - } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.MOVED_TRANSACTION)) { - setClipboardMessage(getMovedTransactionMessage(translate, reportAction)); - } else if (isMovedAction(reportAction)) { - setClipboardMessage(getMovedActionMessage(translate, reportAction, originalReport)); - } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_CARD_FRAUD_ALERT)) { - setClipboardMessage(getActionableCardFraudAlertMessage(translate, reportAction, getLocalDateFromDatetime)); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CHANGE_POLICY) { - const displayMessage = getPolicyChangeMessage(translate, reportAction); - Clipboard.setString(displayMessage); - } else if (isActionableJoinRequest(reportAction)) { - const displayMessage = getJoinRequestMessage(translate, policy, reportAction); - Clipboard.setString(displayMessage); - } else if ( - reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOM_CHANGE_LOG.LEAVE_ROOM || - reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.LEAVE_ROOM - ) { - Clipboard.setString(translate('report.actions.type.leftTheChat')); - } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.DYNAMIC_EXTERNAL_WORKFLOW_ROUTED)) { - setClipboardMessage(getDynamicExternalWorkflowRoutedMessage(reportAction, translate)); - } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.CREATED) && isHarvestReport) { - const harvestReportName = getReportName(harvestReport); - const displayMessage = getHarvestCreatedExpenseReportMessage(harvestReport?.reportID, harvestReportName, translate); - setClipboardMessage(displayMessage); - } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.CREATED_REPORT_FOR_UNAPPROVED_TRANSACTIONS)) { - const {originalID} = getOriginalMessage(reportAction) ?? {}; - const reportName = getReportName(getReportOrDraftReport(originalID)); - const displayMessage = getCreatedReportForUnapprovedTransactionsMessage(originalID, reportName, translate); - setClipboardMessage(displayMessage); - } else if (content) { - setClipboardMessage( - content.replaceAll(/()(.*?)(<\/mention-user>)/gi, (match, openTag: string, innerContent: string, closeTag: string): string => { - const modifiedContent = Str.removeSMSDomain(innerContent) || ''; - return openTag + modifiedContent + closeTag || ''; - }), - ); - } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.SETTLEMENT_ACCOUNT_LOCKED)) { - setClipboardMessage(getSettlementAccountLockedMessage(translate, reportAction)); - } else if (messageText) { - Clipboard.setString(messageText); - } - } - - if (closePopover) { - hideContextMenu(true, ReportActionComposeFocusManager.focus); - } - }, - getDescription: () => {}, - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_MESSAGE, - }, - { - isAnonymousAction: true, - textTranslateKey: 'reportActionContextMenu.copyLink', - icon: 'LinkCopy', - successIcon: 'Checkmark', - successTextTranslateKey: 'reportActionContextMenu.copied', - shouldShow: ({type, reportAction, menuTarget}) => { - const isAttachment = isReportActionAttachment(reportAction); - - // Only hide the copy link menu item when context menu is opened over img element. - const isAttachmentTarget = menuTarget?.current && 'tagName' in menuTarget.current && menuTarget?.current.tagName === 'IMG' && isAttachment; - const isDynamicWorkflowRoutedAction = isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.DYNAMIC_EXTERNAL_WORKFLOW_ROUTED); - return type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && !isAttachmentTarget && !isMessageDeleted(reportAction) && !isDynamicWorkflowRoutedAction; - }, - onPress: (closePopover, {reportAction, originalReportID}) => { - getEnvironmentURL().then((environmentURL) => { - const reportActionID = reportAction?.reportActionID; - Clipboard.setString(`${environmentURL}/r/${originalReportID}/${reportActionID}`); - }); - hideContextMenu(true, ReportActionComposeFocusManager.focus); - }, - getDescription: () => {}, - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_LINK, - }, - { - isAnonymousAction: false, - textTranslateKey: 'common.pin', - icon: 'Pin', - shouldShow: ({type, isPinnedChat}) => type === CONST.CONTEXT_MENU_TYPES.REPORT && !isPinnedChat, - onPress: (closePopover, {reportID}) => { - togglePinnedState(reportID, false); - if (closePopover) { - hideContextMenu(false, ReportActionComposeFocusManager.focus); - } - }, - getDescription: () => {}, - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.PIN, - }, - { - isAnonymousAction: false, - textTranslateKey: 'common.unPin', - icon: 'Pin', - shouldShow: ({type, isPinnedChat}) => type === CONST.CONTEXT_MENU_TYPES.REPORT && isPinnedChat, - onPress: (closePopover, {reportID}) => { - togglePinnedState(reportID, true); - if (closePopover) { - hideContextMenu(false, ReportActionComposeFocusManager.focus); - } - }, - getDescription: () => {}, - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.UNPIN, - }, - { - isAnonymousAction: false, - textTranslateKey: 'reportActionContextMenu.flagAsOffensive', - icon: 'Flag', - shouldShow: ({type, reportAction, isArchivedRoom, isChronosReport, reportID}) => - type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && - canFlagReportAction(reportAction, reportID) && - !isArchivedRoom && - !isChronosReport && - reportAction?.actorAccountID !== CONST.ACCOUNT_ID.CONCIERGE, - onPress: (closePopover, {reportID, reportAction}) => { - if (!reportID) { - return; - } - - const activeRoute = Navigation.getActiveRoute(); - if (closePopover) { - hideContextMenu(false, () => { - KeyboardUtils.dismiss().then(() => { - Navigation.navigate(ROUTES.FLAG_COMMENT.getRoute(reportID, reportAction?.reportActionID, activeRoute)); - }); - }); - return; - } - - Navigation.navigate(ROUTES.FLAG_COMMENT.getRoute(reportID, reportAction?.reportActionID, activeRoute)); - }, - getDescription: () => {}, - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.FLAG_AS_OFFENSIVE, - }, - { - isAnonymousAction: true, - textTranslateKey: 'common.download', - icon: 'Download', - successTextTranslateKey: 'common.download', - successIcon: 'Download', - shouldShow: ({reportAction, isOffline}) => { - const isAttachment = isReportActionAttachment(reportAction); - const html = getActionHtml(reportAction); - const isUploading = html.includes(CONST.ATTACHMENT_OPTIMISTIC_SOURCE_ATTRIBUTE); - return isAttachment && !isUploading && !!reportAction?.reportActionID && !isMessageDeleted(reportAction) && !isOffline; - }, - onPress: (closePopover, {reportAction, translate, encryptedAuthToken}) => { - const html = getActionHtml(reportAction); - const {originalFileName, sourceURL} = getAttachmentDetails(html); - const sourceURLWithAuth = addEncryptedAuthTokenToURL(sourceURL ?? '', encryptedAuthToken); - const sourceID = (sourceURL?.match(CONST.REGEX.ATTACHMENT_ID) ?? [])[1]; - setDownload(sourceID, true); - const anchorRegex = CONST.REGEX_LINK_IN_ANCHOR; - const isAnchorTag = anchorRegex.test(html); - fileDownload(translate, sourceURLWithAuth, originalFileName ?? '', '', isAnchorTag && isMobileSafari()).then(() => setDownload(sourceID, false)); - if (closePopover) { - hideContextMenu(true, ReportActionComposeFocusManager.focus); - } - }, - getDescription: () => {}, - shouldDisable: (download) => download?.isDownloading ?? false, - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.DOWNLOAD, - }, - { - isAnonymousAction: true, - textTranslateKey: 'reportActionContextMenu.copyOnyxData', - icon: 'Copy', - successTextTranslateKey: 'reportActionContextMenu.copied', - successIcon: 'Checkmark', - shouldShow: ({type, isProduction}) => type === CONST.CONTEXT_MENU_TYPES.REPORT && !isProduction, - onPress: (closePopover, {report}) => { - Clipboard.setString(JSON.stringify(report, null, 4)); - hideContextMenu(true, ReportActionComposeFocusManager.focus); - }, - getDescription: () => {}, - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_ONYX_DATA, - }, - { - isAnonymousAction: true, - textTranslateKey: 'debug.debug', - icon: 'Bug', - shouldShow: ({type, isDebugModeEnabled}) => [CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, CONST.CONTEXT_MENU_TYPES.REPORT].some((value) => value === type) && !!isDebugModeEnabled, - onPress: (closePopover, {reportID, reportAction}) => { - if (reportAction) { - Navigation.navigate(ROUTES.DEBUG_REPORT_ACTION.getRoute(reportID, reportAction.reportActionID)); - } else { - Navigation.navigate(ROUTES.DEBUG_REPORT.getRoute(reportID)); - } - hideContextMenu(false, ReportActionComposeFocusManager.focus); - }, - getDescription: () => {}, - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.DEBUG, - }, - { - isAnonymousAction: false, - textTranslateKey: 'common.delete', - icon: 'Trashcan', - shouldShow: ({type, reportAction, isArchivedRoom, isChronosReport, reportID: reportIDParam, moneyRequestAction, iouTransaction, transactions, childReportActions}) => { - // Until deleting parent threads is supported in FE, we will prevent the user from deleting a thread parent - let reportID = reportIDParam; - - if (isMoneyRequestAction(moneyRequestAction)) { - reportID = getOriginalMessage(moneyRequestAction)?.IOUReportID; - } else if (isReportPreviewActionReportActionsUtils(reportAction)) { - reportID = reportAction?.childReportID; - } - return ( - !!reportIDParam && - type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && - canDeleteReportAction(moneyRequestAction ?? reportAction, reportID, iouTransaction, transactions, childReportActions) && - !isArchivedRoom && - !isChronosReport && - !isMessageDeleted(reportAction) - ); - }, - onPress: (closePopover, {reportID: reportIDParam, reportAction, moneyRequestAction}) => { - const iouReportID = isMoneyRequestAction(moneyRequestAction) ? getOriginalMessage(moneyRequestAction)?.IOUReportID : undefined; - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const reportID = iouReportID && Number(iouReportID) !== 0 ? iouReportID : reportIDParam; - if (closePopover) { - // Hide popover, then call showDeleteConfirmModal - hideContextMenu(false, () => showDeleteModal(reportID, moneyRequestAction ?? reportAction)); - return; - } - - // No popover to hide, call showDeleteConfirmModal immediately - showDeleteModal(reportID, moneyRequestAction ?? reportAction); - }, - getDescription: () => {}, - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.DELETE, - }, - { - isAnonymousAction: true, - textTranslateKey: 'reportActionContextMenu.menu', - icon: 'ThreeDots', - shouldShow: ({isMini}) => isMini, - onPress: (closePopover, {openOverflowMenu, event, openContextMenu, anchorRef}) => { - openOverflowMenu(event as GestureResponderEvent | MouseEvent, anchorRef ?? {current: null}); - openContextMenu(); - }, - getDescription: () => {}, - shouldPreventDefaultFocusOnPress: false, - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.MENU, - }, -]; - -const restrictedReadOnlyActions = new Set([ - 'reportActionContextMenu.replyInThread', - 'reportActionContextMenu.editAction', - 'reportActionContextMenu.joinThread', - 'common.delete', -]); - -const RestrictedReadOnlyContextMenuActions: ContextMenuAction[] = ContextMenuActions.filter( - (action) => 'textTranslateKey' in action && restrictedReadOnlyActions.has(action.textTranslateKey), -); - -export {RestrictedReadOnlyContextMenuActions}; -export default ContextMenuActions; -export type {ContextMenuActionPayload, ContextMenuAction}; diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index f43e9bf54fd5..99ae7c4bae26 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -30,9 +30,9 @@ import createMarkAsUnreadAction, {shouldShowMarkAsUnreadForReportAction} from '@ import createReplyInThreadAction, {shouldShowReplyInThreadAction} from '@pages/inbox/report/ContextMenu/actions/replyInThreadAction'; import createUnholdAction, {shouldShowUnholdAction} from '@pages/inbox/report/ContextMenu/actions/unholdAction'; import {useMiniContextMenuActions, useMiniContextMenuState} from '@pages/inbox/report/ContextMenu/MiniContextMenuProvider'; +import useReportActionContextMenuData from '@pages/inbox/report/ContextMenu/PopoverContextMenu/ReportAction/useReportActionContextMenuData'; import type {ContextMenuAnchor} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {showContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; -import useReportActionContextMenuData from '@pages/inbox/report/ContextMenu/useReportActionContextMenuData'; import CONST from '@src/CONST'; const SLIDE_DURATION = 200; diff --git a/src/pages/inbox/report/ContextMenu/ConfirmDeleteReportActionModal.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/ConfirmDeleteReportActionModal.tsx similarity index 100% rename from src/pages/inbox/report/ContextMenu/ConfirmDeleteReportActionModal.tsx rename to src/pages/inbox/report/ContextMenu/PopoverContextMenu/ConfirmDeleteReportActionModal.tsx diff --git a/src/pages/inbox/report/ContextMenu/PopoverEmailContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/Email/PopoverEmailContent.tsx similarity index 95% rename from src/pages/inbox/report/ContextMenu/PopoverEmailContent.tsx rename to src/pages/inbox/report/ContextMenu/PopoverContextMenu/Email/PopoverEmailContent.tsx index 4789a144c0a6..fa6bc285afe8 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverEmailContent.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/Email/PopoverEmailContent.tsx @@ -9,10 +9,10 @@ import useThemeStyles from '@hooks/useThemeStyles'; import Clipboard from '@libs/Clipboard'; import EmailUtils from '@libs/EmailUtils'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {isAnonymousUser, signOutAndRedirectToSignIn} from '@userActions/Session'; import CONST from '@src/CONST'; -import type {PopoverContentProps} from './PopoverContextMenu'; -import {hideContextMenu} from './ReportActionContextMenu'; +import type {PopoverContentProps} from '..'; function PopoverEmailContent({menuState, contentRef}: PopoverContentProps) { const {translate} = useLocalize(); diff --git a/src/pages/inbox/report/ContextMenu/PopoverLinkContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/Link/PopoverLinkContent.tsx similarity index 94% rename from src/pages/inbox/report/ContextMenu/PopoverLinkContent.tsx rename to src/pages/inbox/report/ContextMenu/PopoverContextMenu/Link/PopoverLinkContent.tsx index ae6f5b9bc3a2..019a99bdc43b 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverLinkContent.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/Link/PopoverLinkContent.tsx @@ -8,10 +8,10 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import Clipboard from '@libs/Clipboard'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {isAnonymousUser, signOutAndRedirectToSignIn} from '@userActions/Session'; import CONST from '@src/CONST'; -import type {PopoverContentProps} from './PopoverContextMenu'; -import {hideContextMenu} from './ReportActionContextMenu'; +import type {PopoverContentProps} from '..'; function PopoverLinkContent({menuState, contentRef}: PopoverContentProps) { const {translate} = useLocalize(); diff --git a/src/pages/inbox/report/ContextMenu/PopoverReportContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/Report/PopoverReportContent.tsx similarity index 88% rename from src/pages/inbox/report/ContextMenu/PopoverReportContent.tsx rename to src/pages/inbox/report/ContextMenu/PopoverContextMenu/Report/PopoverReportContent.tsx index b37228208d89..aa8e8580b441 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverReportContent.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/Report/PopoverReportContent.tsx @@ -7,16 +7,16 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import {ACTION_IDS} from './actions/actionConfig'; -import {CONTEXT_MENU_ICON_NAMES} from './actions/actionTypes'; -import type {ContextMenuAction} from './actions/actionTypes'; -import createCopyOnyxDataAction, {shouldShowCopyOnyxDataAction} from './actions/copyOnyxDataAction'; -import createDebugAction, {shouldShowDebugAction} from './actions/debugAction'; -import createMarkAsReadAction, {shouldShowMarkAsReadAction} from './actions/markAsReadAction'; -import createMarkAsUnreadAction, {shouldShowMarkAsUnreadForReport} from './actions/markAsUnreadAction'; -import createPinAction, {shouldShowPinAction} from './actions/pinAction'; -import createUnpinAction, {shouldShowUnpinAction} from './actions/unpinAction'; -import type {PopoverContentProps} from './PopoverContextMenu'; +import {ACTION_IDS} from '@pages/inbox/report/ContextMenu/actions/actionConfig'; +import {CONTEXT_MENU_ICON_NAMES} from '@pages/inbox/report/ContextMenu/actions/actionTypes'; +import type {ContextMenuAction} from '@pages/inbox/report/ContextMenu/actions/actionTypes'; +import createCopyOnyxDataAction, {shouldShowCopyOnyxDataAction} from '@pages/inbox/report/ContextMenu/actions/copyOnyxDataAction'; +import createDebugAction, {shouldShowDebugAction} from '@pages/inbox/report/ContextMenu/actions/debugAction'; +import createMarkAsReadAction, {shouldShowMarkAsReadAction} from '@pages/inbox/report/ContextMenu/actions/markAsReadAction'; +import createMarkAsUnreadAction, {shouldShowMarkAsUnreadForReport} from '@pages/inbox/report/ContextMenu/actions/markAsUnreadAction'; +import createPinAction, {shouldShowPinAction} from '@pages/inbox/report/ContextMenu/actions/pinAction'; +import createUnpinAction, {shouldShowUnpinAction} from '@pages/inbox/report/ContextMenu/actions/unpinAction'; +import type {PopoverContentProps} from '..'; import useReportContextMenuData from './useReportContextMenuData'; function PopoverReportContent({menuState, hideAndRun, contentRef, shouldEnableArrowNavigation}: PopoverContentProps) { diff --git a/src/pages/inbox/report/ContextMenu/useReportContextMenuData.ts b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/Report/useReportContextMenuData.ts similarity index 93% rename from src/pages/inbox/report/ContextMenu/useReportContextMenuData.ts rename to src/pages/inbox/report/ContextMenu/PopoverContextMenu/Report/useReportContextMenuData.ts index fba8d2133c3a..1b597bfd7d38 100644 --- a/src/pages/inbox/report/ContextMenu/useReportContextMenuData.ts +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/Report/useReportContextMenuData.ts @@ -7,12 +7,12 @@ import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import useReportIsArchived from '@hooks/useReportIsArchived'; import {canWriteInReport, chatIncludesChronosWithID, isArchivedNonExpenseReport, isUnread} from '@libs/ReportUtils'; +import {RESTRICTED_READONLY_ACTION_IDS} from '@pages/inbox/report/ContextMenu/actions/actionConfig'; +import type {ContextMenuAnchor, ContextMenuType} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {isAnonymousUser, signOutAndRedirectToSignIn} from '@userActions/Session'; import ONYXKEYS from '@src/ONYXKEYS'; import type {ReportAction, ReportActions, Report as ReportType} from '@src/types/onyx'; -import {RESTRICTED_READONLY_ACTION_IDS} from './actions/actionConfig'; -import type {ContextMenuAnchor, ContextMenuType} from './ReportActionContextMenu'; -import {hideContextMenu} from './ReportActionContextMenu'; const EMPTY_SET = new Set(); diff --git a/src/pages/inbox/report/ContextMenu/PopoverReportActionContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/ReportAction/PopoverReportActionContent.tsx similarity index 90% rename from src/pages/inbox/report/ContextMenu/PopoverReportActionContent.tsx rename to src/pages/inbox/report/ContextMenu/PopoverContextMenu/ReportAction/PopoverReportActionContent.tsx index 76cede546246..0c96bd86cca7 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverReportActionContent.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/ReportAction/PopoverReportActionContent.tsx @@ -12,28 +12,28 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import {ACTION_IDS} from '@pages/inbox/report/ContextMenu/actions/actionConfig'; +import {CONTEXT_MENU_ICON_NAMES} from '@pages/inbox/report/ContextMenu/actions/actionTypes'; +import type {ContextMenuAction} from '@pages/inbox/report/ContextMenu/actions/actionTypes'; +import createCopyLinkAction, {shouldShowCopyLinkAction} from '@pages/inbox/report/ContextMenu/actions/copyLinkAction'; +import createCopyMessageAction, {shouldShowCopyMessageAction} from '@pages/inbox/report/ContextMenu/actions/copyMessageAction'; +import createDebugAction, {shouldShowDebugAction} from '@pages/inbox/report/ContextMenu/actions/debugAction'; +import createDeleteAction, {shouldShowDeleteAction} from '@pages/inbox/report/ContextMenu/actions/deleteAction'; +import createDownloadAction, {shouldShowDownloadAction} from '@pages/inbox/report/ContextMenu/actions/downloadAction'; +import createEditAction, {shouldShowEditAction} from '@pages/inbox/report/ContextMenu/actions/editAction'; +import createEmojiReactionData, {shouldShowEmojiReaction} from '@pages/inbox/report/ContextMenu/actions/emojiReactionAction'; +import createExplainAction, {shouldShowExplainAction} from '@pages/inbox/report/ContextMenu/actions/explainAction'; +import createFlagAsOffensiveAction, {shouldShowFlagAsOffensiveAction} from '@pages/inbox/report/ContextMenu/actions/flagAsOffensiveAction'; +import createHoldAction, {shouldShowHoldAction} from '@pages/inbox/report/ContextMenu/actions/holdAction'; +import createJoinThreadAction, {shouldShowJoinThreadAction} from '@pages/inbox/report/ContextMenu/actions/joinThreadAction'; +import createLeaveThreadAction, {shouldShowLeaveThreadAction} from '@pages/inbox/report/ContextMenu/actions/leaveThreadAction'; +import createMarkAsUnreadAction, {shouldShowMarkAsUnreadForReportAction} from '@pages/inbox/report/ContextMenu/actions/markAsUnreadAction'; +import createOverflowMenuAction from '@pages/inbox/report/ContextMenu/actions/overflowMenuAction'; +import createReplyInThreadAction, {shouldShowReplyInThreadAction} from '@pages/inbox/report/ContextMenu/actions/replyInThreadAction'; +import createUnholdAction, {shouldShowUnholdAction} from '@pages/inbox/report/ContextMenu/actions/unholdAction'; +import {showContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; -import {ACTION_IDS} from './actions/actionConfig'; -import {CONTEXT_MENU_ICON_NAMES} from './actions/actionTypes'; -import type {ContextMenuAction} from './actions/actionTypes'; -import createCopyLinkAction, {shouldShowCopyLinkAction} from './actions/copyLinkAction'; -import createCopyMessageAction, {shouldShowCopyMessageAction} from './actions/copyMessageAction'; -import createDebugAction, {shouldShowDebugAction} from './actions/debugAction'; -import createDeleteAction, {shouldShowDeleteAction} from './actions/deleteAction'; -import createDownloadAction, {shouldShowDownloadAction} from './actions/downloadAction'; -import createEditAction, {shouldShowEditAction} from './actions/editAction'; -import createEmojiReactionData, {shouldShowEmojiReaction} from './actions/emojiReactionAction'; -import createExplainAction, {shouldShowExplainAction} from './actions/explainAction'; -import createFlagAsOffensiveAction, {shouldShowFlagAsOffensiveAction} from './actions/flagAsOffensiveAction'; -import createHoldAction, {shouldShowHoldAction} from './actions/holdAction'; -import createJoinThreadAction, {shouldShowJoinThreadAction} from './actions/joinThreadAction'; -import createLeaveThreadAction, {shouldShowLeaveThreadAction} from './actions/leaveThreadAction'; -import createMarkAsUnreadAction, {shouldShowMarkAsUnreadForReportAction} from './actions/markAsUnreadAction'; -import createOverflowMenuAction from './actions/overflowMenuAction'; -import createReplyInThreadAction, {shouldShowReplyInThreadAction} from './actions/replyInThreadAction'; -import createUnholdAction, {shouldShowUnholdAction} from './actions/unholdAction'; -import type {PopoverContentProps} from './PopoverContextMenu'; -import {showContextMenu} from './ReportActionContextMenu'; +import type {PopoverContentProps} from '..'; import useReportActionContextMenuData from './useReportActionContextMenuData'; function PopoverReportActionContent({menuState, hideAndRun, setLocalShouldKeepOpen, contentRef, shouldEnableArrowNavigation}: PopoverContentProps) { diff --git a/src/pages/inbox/report/ContextMenu/useReportActionContextMenuData.ts b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/ReportAction/useReportActionContextMenuData.ts similarity index 97% rename from src/pages/inbox/report/ContextMenu/useReportActionContextMenuData.ts rename to src/pages/inbox/report/ContextMenu/PopoverContextMenu/ReportAction/useReportActionContextMenuData.ts index 2ed5e73c803c..cd085401b356 100644 --- a/src/pages/inbox/report/ContextMenu/useReportActionContextMenuData.ts +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/ReportAction/useReportActionContextMenuData.ts @@ -29,14 +29,14 @@ import { isMoneyRequestReport as ReportUtilsIsMoneyRequestReport, isTrackExpenseReport as ReportUtilsIsTrackExpenseReport, } from '@libs/ReportUtils'; +import {RESTRICTED_READONLY_ACTION_IDS} from '@pages/inbox/report/ContextMenu/actions/actionConfig'; +import type {ContextMenuAnchor, ContextMenuType} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {isAnonymousUser, signOutAndRedirectToSignIn} from '@userActions/Session'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {OriginalMessageIOU, ReportAction} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import {RESTRICTED_READONLY_ACTION_IDS} from './actions/actionConfig'; -import type {ContextMenuAnchor, ContextMenuType} from './ReportActionContextMenu'; -import {hideContextMenu} from './ReportActionContextMenu'; const EMPTY_SET = new Set(); diff --git a/src/pages/inbox/report/ContextMenu/PopoverTextContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/Text/PopoverTextContent.tsx similarity index 94% rename from src/pages/inbox/report/ContextMenu/PopoverTextContent.tsx rename to src/pages/inbox/report/ContextMenu/PopoverContextMenu/Text/PopoverTextContent.tsx index d1e6caed2b1b..0dcc677ad853 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverTextContent.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/Text/PopoverTextContent.tsx @@ -8,10 +8,10 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import Clipboard from '@libs/Clipboard'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {isAnonymousUser, signOutAndRedirectToSignIn} from '@userActions/Session'; import CONST from '@src/CONST'; -import type {PopoverContentProps} from './PopoverContextMenu'; -import {hideContextMenu} from './ReportActionContextMenu'; +import type {PopoverContentProps} from '..'; function PopoverTextContent({menuState, contentRef}: PopoverContentProps) { const {translate} = useLocalize(); diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/index.tsx similarity index 97% rename from src/pages/inbox/report/ContextMenu/PopoverContextMenu.tsx rename to src/pages/inbox/report/ContextMenu/PopoverContextMenu/index.tsx index 113e3d070a48..01783764b1b6 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/index.tsx @@ -12,15 +12,15 @@ import calculateAnchorPosition from '@libs/calculateAnchorPosition'; import refocusComposerAfterPreventFirstResponder from '@libs/refocusComposerAfterPreventFirstResponder'; import type {ComposerType} from '@libs/ReportActionComposeFocusManager'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import type {ContextMenuAnchor, ContextMenuType, ReportActionContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import ConfirmDeleteReportActionModal from './ConfirmDeleteReportActionModal'; -import PopoverEmailContent from './PopoverEmailContent'; -import PopoverLinkContent from './PopoverLinkContent'; -import PopoverReportActionContent from './PopoverReportActionContent'; -import PopoverReportContent from './PopoverReportContent'; -import PopoverTextContent from './PopoverTextContent'; -import {hideContextMenu} from './ReportActionContextMenu'; -import type {ContextMenuAnchor, ContextMenuType, ReportActionContextMenu} from './ReportActionContextMenu'; +import PopoverEmailContent from './Email/PopoverEmailContent'; +import PopoverLinkContent from './Link/PopoverLinkContent'; +import PopoverReportContent from './Report/PopoverReportContent'; +import PopoverReportActionContent from './ReportAction/PopoverReportActionContent'; +import PopoverTextContent from './Text/PopoverTextContent'; function extractPointerEvent(event: GestureResponderEvent | MouseEvent): MouseEvent | NativeTouchEvent { if ('nativeEvent' in event) { From b8fda9e5cf60a9d75105e3d9d61294a92da1b9c8 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 3 Mar 2026 12:38:33 -0800 Subject: [PATCH 36/88] Destructure useMiniContextMenuState into individual fields in MiniReportActionContextMenu Made-with: Cursor --- .../MiniReportActionContextMenu/index.tsx | 51 +++++++++++-------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index 99ae7c4bae26..6415224d0904 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -39,7 +39,18 @@ const SLIDE_DURATION = 200; const OVERSHOOT_EASING = Easing.bezier(0.34, 1.56, 0.64, 1); function MiniReportActionContextMenu() { - const state = useMiniContextMenuState(); + const { + isVisible = false, + rowMeasurements, + displayAsGroup = false, + reportID, + reportActionID, + originalReportID, + draftMessage = '', + anchor, + checkIfContextMenuActive, + setIsEmojiPickerActive, + } = useMiniContextMenuState() ?? {}; const miniActions = useMiniContextMenuActions(); const {hideMiniContextMenu, cancelHide} = miniActions; const {shouldUseNarrowLayout} = useResponsiveLayout(); @@ -48,21 +59,19 @@ function MiniReportActionContextMenu() { const icons = useMemoizedLazyExpensifyIcons(CONTEXT_MENU_ICON_NAMES); const threeDotRef = useRef(null); - - const isVisible = state?.isVisible ?? false; const wasVisibleRef = useRef(false); const baseTop = useSharedValue(0); const baseRight = useSharedValue(0); useEffect(() => { - if (!state) { + if (!rowMeasurements) { return; } - if (state.isVisible) { - const targetY = state.rowMeasurements.top + (state.displayAsGroup ? -8 : -4); - const targetRight = window.innerWidth - state.rowMeasurements.right + 4; + if (isVisible) { + const targetY = rowMeasurements.top + (displayAsGroup ? -8 : -4); + const targetRight = window.innerWidth - rowMeasurements.right + 4; if (wasVisibleRef.current) { baseTop.set(withTiming(targetY, {duration: SLIDE_DURATION, easing: OVERSHOOT_EASING})); @@ -72,8 +81,8 @@ function MiniReportActionContextMenu() { baseRight.set(targetRight); } } - wasVisibleRef.current = state.isVisible; - }, [state, baseTop, baseRight]); + wasVisibleRef.current = isVisible; + }, [isVisible, rowMeasurements, displayAsGroup, baseTop, baseRight]); useEffect(() => { if (!isVisible) { @@ -94,13 +103,13 @@ function MiniReportActionContextMenu() { })); const data = useReportActionContextMenuData({ - reportID: state?.reportID, - reportActionID: state?.reportActionID, - originalReportID: state?.originalReportID, - draftMessage: state?.draftMessage ?? '', + reportID, + reportActionID, + originalReportID, + draftMessage, selection: '', type: CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, - anchor: state?.anchor, + anchor, }); const hideAndRun = (callback?: () => void) => { @@ -115,17 +124,17 @@ function MiniReportActionContextMenu() { selection: '', contextMenuAnchor: anchorRef?.current ?? null, report: { - reportID: state?.reportID, - originalReportID: state?.originalReportID, + reportID, + originalReportID, }, reportAction: { reportActionID: data.reportAction?.reportActionID, - draftMessage: state?.draftMessage, + draftMessage, }, callbacks: { - onShow: state?.checkIfContextMenuActive, + onShow: checkIfContextMenuActive, onHide: () => { - state?.checkIfContextMenuActive?.(); + checkIfContextMenuActive?.(); miniActions.release(); }, }, @@ -362,14 +371,14 @@ function MiniReportActionContextMenu() { reportAction: data.reportAction, currentUserAccountID, openContextMenu: () => miniActions.keepOpen(), - setIsEmojiPickerActive: state?.setIsEmojiPickerActive, + setIsEmojiPickerActive, hideAndRun, interceptAnonymousUser, }); const hasEmoji = shouldShowEmojiReaction({reportAction: data.reportAction}) && !!emojiData.reportAction && !!emojiData.reportActionID; - if (!state) { + if (!rowMeasurements) { return null; } From 57dc808a4dcc2df9219ea59eacd2292ea9cb308a Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 3 Mar 2026 12:50:11 -0800 Subject: [PATCH 37/88] Remove redundant isDelayButtonStateComplete props (default is already true) Made-with: Cursor --- src/components/MiniContextMenuItem.tsx | 2 +- .../MiniReportActionContextMenu/index.tsx | 14 -------------- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/src/components/MiniContextMenuItem.tsx b/src/components/MiniContextMenuItem.tsx index 51be32c00429..268ed3d37c1b 100644 --- a/src/components/MiniContextMenuItem.tsx +++ b/src/components/MiniContextMenuItem.tsx @@ -32,7 +32,7 @@ type MiniContextMenuItemProps = WithSentryLabel & { /** * Whether the button should be in the active state */ - isDelayButtonStateComplete: boolean; + isDelayButtonStateComplete?: boolean; /** * Can be used to control the click event, and for example whether or not to lose focus from the composer when pressing the item */ diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index 6415224d0904..05f554d961ad 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -417,7 +417,6 @@ function MiniReportActionContextMenu() { )} {!!replyInThreadAction && ( interceptAnonymousUser(() => { From d3e8e46cdc2d6c7db49fe819d8b4a06604bfba63 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 3 Mar 2026 13:21:19 -0800 Subject: [PATCH 38/88] Consolidate interceptAnonymousUser: use @libs version, remove param from action factories - Update src/libs/interceptAnonymousUser.ts with hideContextMenu and InteractionManager - Remove interceptAnonymousUser param from all context menu action factories; they import it directly - Remove interceptAnonymousUser from useReportActionContextMenuData return value - Update joinThreadAction, leaveThreadAction, unholdAction to import from @libs - Remove interceptAnonymousUser from consumer call sites (MiniReportActionContextMenu, PopoverReportActionContent) - Add direct import in MiniReportActionContextMenu for overflow menu button Made-with: Cursor --- src/libs/interceptAnonymousUser.ts | 21 +++++----- .../MiniReportActionContextMenu/index.tsx | 26 ++++++------ .../Email/PopoverEmailContent.tsx | 16 +------- .../Link/PopoverLinkContent.tsx | 16 +------- .../Report/PopoverReportContent.tsx | 12 ++---- .../PopoverReportActionContent.tsx | 40 ++++--------------- .../useReportActionContextMenuData.ts | 16 -------- .../Text/PopoverTextContent.tsx | 16 +------- .../ContextMenu/actions/copyLinkAction.ts | 4 +- .../ContextMenu/actions/copyMessageAction.ts | 4 +- .../ContextMenu/actions/copyOnyxDataAction.ts | 4 +- .../report/ContextMenu/actions/debugAction.ts | 4 +- .../ContextMenu/actions/downloadAction.ts | 4 +- .../report/ContextMenu/actions/editAction.ts | 14 +------ .../actions/emojiReactionAction.ts | 12 +----- .../ContextMenu/actions/explainAction.ts | 13 +----- .../report/ContextMenu/actions/holdAction.ts | 12 +----- .../ContextMenu/actions/joinThreadAction.ts | 4 +- .../ContextMenu/actions/leaveThreadAction.ts | 4 +- .../ContextMenu/actions/markAsUnreadAction.ts | 3 +- .../ContextMenu/actions/overflowMenuAction.ts | 4 +- .../report/ContextMenu/actions/pinAction.ts | 4 +- .../actions/replyInThreadAction.ts | 3 +- .../ContextMenu/actions/unholdAction.ts | 12 +----- .../report/ContextMenu/actions/unpinAction.ts | 4 +- 25 files changed, 73 insertions(+), 199 deletions(-) diff --git a/src/libs/interceptAnonymousUser.ts b/src/libs/interceptAnonymousUser.ts index d4e40cf44779..679d1fd86ad6 100644 --- a/src/libs/interceptAnonymousUser.ts +++ b/src/libs/interceptAnonymousUser.ts @@ -1,16 +1,17 @@ -import * as Session from './actions/Session'; +import {InteractionManager} from 'react-native'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import {isAnonymousUser, signOutAndRedirectToSignIn} from './actions/Session'; -/** - * Checks if user is anonymous. If true, shows the sign in modal, else, - * executes the callback. - */ -const interceptAnonymousUser = (callback: () => void) => { - const isAnonymousUser = Session.isAnonymousUser(); - if (isAnonymousUser) { - Session.signOutAndRedirectToSignIn(); +function interceptAnonymousUser(callback: () => void, isAnonymousAction = false) { + if (isAnonymousUser() && !isAnonymousAction) { + hideContextMenu(false); + // eslint-disable-next-line @typescript-eslint/no-deprecated + InteractionManager.runAfterInteractions(() => { + signOutAndRedirectToSignIn(); + }); } else { callback(); } -}; +} export default interceptAnonymousUser; diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index 05f554d961ad..5bf8adf712f9 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -13,6 +13,7 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import getButtonState from '@libs/getButtonState'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {ACTION_IDS} from '@pages/inbox/report/ContextMenu/actions/actionConfig'; import {CONTEXT_MENU_ICON_NAMES} from '@pages/inbox/report/ContextMenu/actions/actionTypes'; import createCopyLinkAction, {shouldShowCopyLinkAction} from '@pages/inbox/report/ContextMenu/actions/copyLinkAction'; @@ -145,7 +146,7 @@ function MiniReportActionContextMenu() { const reportAction = data.reportAction; const currentUserAccountID = data.currentUserPersonalDetails?.accountID ?? 0; - const {interceptAnonymousUser, translate, disabledActionIDs} = data; + const {translate, disabledActionIDs} = data; const isDisabled = (id: string) => disabledActionIDs.has(id); @@ -249,7 +250,7 @@ function MiniReportActionContextMenu() { reportAction, originalReport: data.originalReport, currentUserAccountID, - interceptAnonymousUser, + hideAndRun, translate, chatBubbleReplyIcon: icons.ChatBubbleReply, @@ -262,7 +263,7 @@ function MiniReportActionContextMenu() { reportActions: data.reportActions, reportAction, currentUserAccountID, - interceptAnonymousUser, + hideAndRun, translate, chatBubbleUnreadIcon: icons.ChatBubbleUnread, @@ -276,7 +277,7 @@ function MiniReportActionContextMenu() { originalReport: data.originalReport, reportAction, currentUserPersonalDetails: data.currentUserPersonalDetails, - interceptAnonymousUser, + hideAndRun, translate, conciergeIcon: icons.Concierge, @@ -290,7 +291,7 @@ function MiniReportActionContextMenu() { moneyRequestAction: data.moneyRequestAction, draftMessage: data.draftMessage, introSelected: data.introSelected, - interceptAnonymousUser, + hideAndRun, translate, pencilIcon: icons.Pencil, @@ -301,7 +302,7 @@ function MiniReportActionContextMenu() { moneyRequestAction: data.moneyRequestAction, isDelegateAccessRestricted: data.isDelegateAccessRestricted, showDelegateNoAccessModal: data.showDelegateNoAccessModal, - interceptAnonymousUser, + hideAndRun, translate, stopwatchIcon: icons.Stopwatch, @@ -312,7 +313,7 @@ function MiniReportActionContextMenu() { moneyRequestAction: data.moneyRequestAction, isDelegateAccessRestricted: data.isDelegateAccessRestricted, showDelegateNoAccessModal: data.showDelegateNoAccessModal, - interceptAnonymousUser, + hideAndRun, translate, stopwatchIcon: icons.Stopwatch, @@ -320,11 +321,11 @@ function MiniReportActionContextMenu() { : null; const joinThreadAction = displayJoinThread && reportAction - ? createJoinThreadAction({reportAction, originalReport: data.originalReport, currentUserAccountID, interceptAnonymousUser, hideAndRun, translate, bellIcon: icons.Bell}) + ? createJoinThreadAction({reportAction, originalReport: data.originalReport, currentUserAccountID, hideAndRun, translate, bellIcon: icons.Bell}) : null; const leaveThreadAction = displayLeaveThread && reportAction - ? createLeaveThreadAction({reportAction, originalReport: data.originalReport, currentUserAccountID, interceptAnonymousUser, hideAndRun, translate, exitIcon: icons.Exit}) + ? createLeaveThreadAction({reportAction, originalReport: data.originalReport, currentUserAccountID, hideAndRun, translate, exitIcon: icons.Exit}) : null; const copyMessageAction = displayCopyMessage && reportAction @@ -346,20 +347,20 @@ function MiniReportActionContextMenu() { translate, harvestReport: data.harvestReport, currentUserPersonalDetails: data.currentUserPersonalDetails, - interceptAnonymousUser, + copyIcon: icons.Copy, checkmarkIcon: icons.Checkmark, }) : null; const copyLinkAction = displayCopyLink && reportAction - ? createCopyLinkAction({reportAction, originalReportID: data.originalReportID, interceptAnonymousUser, translate, linkCopyIcon: icons.LinkCopy, checkmarkIcon: icons.Checkmark}) + ? createCopyLinkAction({reportAction, originalReportID: data.originalReportID, translate, linkCopyIcon: icons.LinkCopy, checkmarkIcon: icons.Checkmark}) : null; const flagAsOffensiveAction = displayFlagAsOffensive && reportAction ? createFlagAsOffensiveAction({reportID: data.reportID, reportAction, hideAndRun, translate, flagIcon: icons.Flag}) : null; const downloadAction = displayDownload && reportAction - ? createDownloadAction({reportAction, encryptedAuthToken: data.encryptedAuthToken, interceptAnonymousUser, download: data.download, translate, downloadIcon: icons.Download}) + ? createDownloadAction({reportAction, encryptedAuthToken: data.encryptedAuthToken, download: data.download, translate, downloadIcon: icons.Download}) : null; const deleteAction = displayDelete && reportAction @@ -373,7 +374,6 @@ function MiniReportActionContextMenu() { openContextMenu: () => miniActions.keepOpen(), setIsEmojiPickerActive, hideAndRun, - interceptAnonymousUser, }); const hasEmoji = shouldShowEmojiReaction({reportAction: data.reportAction}) && !!emojiData.reportAction && !!emojiData.reportActionID; diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/Email/PopoverEmailContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/Email/PopoverEmailContent.tsx index fa6bc285afe8..9d4e0a9b653d 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/Email/PopoverEmailContent.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/Email/PopoverEmailContent.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import {InteractionManager, View} from 'react-native'; +import {View} from 'react-native'; import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; @@ -8,9 +8,9 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import Clipboard from '@libs/Clipboard'; import EmailUtils from '@libs/EmailUtils'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; -import {isAnonymousUser, signOutAndRedirectToSignIn} from '@userActions/Session'; import CONST from '@src/CONST'; import type {PopoverContentProps} from '..'; @@ -21,18 +21,6 @@ function PopoverEmailContent({menuState, contentRef}: PopoverContentProps) { const StyleUtils = useStyleUtils(); const icons = useMemoizedLazyExpensifyIcons(['Copy', 'Checkmark'] as const); - const interceptAnonymousUser = (callback: () => void, isAnonymousAction = false) => { - if (isAnonymousUser() && !isAnonymousAction) { - hideContextMenu(false); - // eslint-disable-next-line @typescript-eslint/no-deprecated - InteractionManager.runAfterInteractions(() => { - signOutAndRedirectToSignIn(); - }); - } else { - callback(); - } - }; - const handlePress = () => { interceptAnonymousUser(() => { Clipboard.setString(EmailUtils.trimMailTo(menuState.selection)); diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/Link/PopoverLinkContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/Link/PopoverLinkContent.tsx index 019a99bdc43b..a604f5940cec 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/Link/PopoverLinkContent.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/Link/PopoverLinkContent.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import {InteractionManager, View} from 'react-native'; +import {View} from 'react-native'; import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; @@ -7,9 +7,9 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import Clipboard from '@libs/Clipboard'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; -import {isAnonymousUser, signOutAndRedirectToSignIn} from '@userActions/Session'; import CONST from '@src/CONST'; import type {PopoverContentProps} from '..'; @@ -20,18 +20,6 @@ function PopoverLinkContent({menuState, contentRef}: PopoverContentProps) { const StyleUtils = useStyleUtils(); const icons = useMemoizedLazyExpensifyIcons(['Copy', 'Checkmark'] as const); - const interceptAnonymousUser = (callback: () => void, isAnonymousAction = false) => { - if (isAnonymousUser() && !isAnonymousAction) { - hideContextMenu(false); - // eslint-disable-next-line @typescript-eslint/no-deprecated - InteractionManager.runAfterInteractions(() => { - signOutAndRedirectToSignIn(); - }); - } else { - callback(); - } - }; - const handlePress = () => { interceptAnonymousUser(() => { Clipboard.setString(menuState.selection); diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/Report/PopoverReportContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/Report/PopoverReportContent.tsx index aa8e8580b441..e9f12fa4d901 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/Report/PopoverReportContent.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/Report/PopoverReportContent.tsx @@ -58,20 +58,16 @@ function PopoverReportContent({menuState, hideAndRun, contentRef, shouldEnableAr reportActions: data.reportActions, reportAction: data.reportAction, currentUserAccountID: 0, - interceptAnonymousUser, hideAndRun, translate, chatBubbleUnreadIcon: icons.ChatBubbleUnread, checkmarkIcon: icons.Checkmark, }) : undefined; - const pinActionItem = showPin ? createPinAction({reportID: data.reportID, interceptAnonymousUser, hideAndRun, translate, pinIcon: icons.Pin}) : undefined; - const unpinActionItem = showUnpin ? createUnpinAction({reportID: data.reportID, interceptAnonymousUser, hideAndRun, translate, pinIcon: icons.Pin}) : undefined; - const copyOnyxDataActionItem = showCopyOnyxData - ? createCopyOnyxDataAction({report: data.report, interceptAnonymousUser, translate, copyIcon: icons.Copy, checkmarkIcon: icons.Checkmark}) - : undefined; - const debugActionItem = - showDebug && data.reportAction ? createDebugAction({reportID: data.reportID, reportAction: data.reportAction, interceptAnonymousUser, translate, bugIcon: icons.Bug}) : undefined; + const pinActionItem = showPin ? createPinAction({reportID: data.reportID, hideAndRun, translate, pinIcon: icons.Pin}) : undefined; + const unpinActionItem = showUnpin ? createUnpinAction({reportID: data.reportID, hideAndRun, translate, pinIcon: icons.Pin}) : undefined; + const copyOnyxDataActionItem = showCopyOnyxData ? createCopyOnyxDataAction({report: data.report, translate, copyIcon: icons.Copy, checkmarkIcon: icons.Checkmark}) : undefined; + const debugActionItem = showDebug && data.reportAction ? createDebugAction({reportID: data.reportID, reportAction: data.reportAction, translate, bugIcon: icons.Bug}) : undefined; const visibleActions = useMemo(() => { const items: ContextMenuAction[] = []; diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/ReportAction/PopoverReportActionContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/ReportAction/PopoverReportActionContent.tsx index 0c96bd86cca7..c9205336a8a1 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/ReportAction/PopoverReportActionContent.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/ReportAction/PopoverReportActionContent.tsx @@ -81,7 +81,7 @@ function PopoverReportActionContent({menuState, hideAndRun, setLocalShouldKeepOp }; const currentUserAccountID = data.currentUserPersonalDetails?.accountID ?? 0; - const {interceptAnonymousUser, translate, disabledActionIDs} = data; + const {translate, disabledActionIDs} = data; const isDisabled = (id: string) => disabledActionIDs.has(id); @@ -149,12 +149,7 @@ function PopoverReportActionContent({menuState, hideAndRun, setLocalShouldKeepOp /* eslint-disable react-hooks/refs -- factory functions store refs for later use, they don't read .current during render */ const visibleActions = useMemo(() => { if (!data.reportAction) { - return [ - createOverflowMenuAction( - {openOverflowMenu, openContextMenu: () => setLocalShouldKeepOpen(true), interceptAnonymousUser, translate, threeDotsIcon: icons.ThreeDots}, - overflowMenuRef, - ), - ]; + return [createOverflowMenuAction({openOverflowMenu, openContextMenu: () => setLocalShouldKeepOpen(true), translate, threeDotsIcon: icons.ThreeDots}, overflowMenuRef)]; } const reportAction = data.reportAction; const items: ContextMenuAction[] = []; @@ -165,7 +160,6 @@ function PopoverReportActionContent({menuState, hideAndRun, setLocalShouldKeepOp reportAction, originalReport: data.originalReport, currentUserAccountID, - interceptAnonymousUser, hideAndRun, translate, chatBubbleReplyIcon: icons.ChatBubbleReply, @@ -179,7 +173,6 @@ function PopoverReportActionContent({menuState, hideAndRun, setLocalShouldKeepOp reportActions: data.reportActions, reportAction, currentUserAccountID, - interceptAnonymousUser, hideAndRun, translate, chatBubbleUnreadIcon: icons.ChatBubbleUnread, @@ -194,7 +187,6 @@ function PopoverReportActionContent({menuState, hideAndRun, setLocalShouldKeepOp originalReport: data.originalReport, reportAction, currentUserPersonalDetails: data.currentUserPersonalDetails, - interceptAnonymousUser, hideAndRun, translate, conciergeIcon: icons.Concierge, @@ -209,7 +201,6 @@ function PopoverReportActionContent({menuState, hideAndRun, setLocalShouldKeepOp moneyRequestAction: data.moneyRequestAction, draftMessage: data.draftMessage, introSelected: data.introSelected, - interceptAnonymousUser, hideAndRun, translate, pencilIcon: icons.Pencil, @@ -222,7 +213,6 @@ function PopoverReportActionContent({menuState, hideAndRun, setLocalShouldKeepOp moneyRequestAction: data.moneyRequestAction, isDelegateAccessRestricted: data.isDelegateAccessRestricted, showDelegateNoAccessModal: data.showDelegateNoAccessModal, - interceptAnonymousUser, hideAndRun, translate, stopwatchIcon: icons.Stopwatch, @@ -235,7 +225,6 @@ function PopoverReportActionContent({menuState, hideAndRun, setLocalShouldKeepOp moneyRequestAction: data.moneyRequestAction, isDelegateAccessRestricted: data.isDelegateAccessRestricted, showDelegateNoAccessModal: data.showDelegateNoAccessModal, - interceptAnonymousUser, hideAndRun, translate, stopwatchIcon: icons.Stopwatch, @@ -243,14 +232,10 @@ function PopoverReportActionContent({menuState, hideAndRun, setLocalShouldKeepOp ); } if (showJoinThread) { - items.push( - createJoinThreadAction({reportAction, originalReport: data.originalReport, currentUserAccountID, interceptAnonymousUser, hideAndRun, translate, bellIcon: icons.Bell}), - ); + items.push(createJoinThreadAction({reportAction, originalReport: data.originalReport, currentUserAccountID, hideAndRun, translate, bellIcon: icons.Bell})); } if (showLeaveThread) { - items.push( - createLeaveThreadAction({reportAction, originalReport: data.originalReport, currentUserAccountID, interceptAnonymousUser, hideAndRun, translate, exitIcon: icons.Exit}), - ); + items.push(createLeaveThreadAction({reportAction, originalReport: data.originalReport, currentUserAccountID, hideAndRun, translate, exitIcon: icons.Exit})); } if (showCopyMessage) { items.push( @@ -272,7 +257,6 @@ function PopoverReportActionContent({menuState, hideAndRun, setLocalShouldKeepOp translate, harvestReport: data.harvestReport, currentUserPersonalDetails: data.currentUserPersonalDetails, - interceptAnonymousUser, copyIcon: icons.Copy, checkmarkIcon: icons.Checkmark, }), @@ -283,7 +267,6 @@ function PopoverReportActionContent({menuState, hideAndRun, setLocalShouldKeepOp createCopyLinkAction({ reportAction, originalReportID: data.originalReportID, - interceptAnonymousUser, translate, linkCopyIcon: icons.LinkCopy, checkmarkIcon: icons.Checkmark, @@ -294,22 +277,15 @@ function PopoverReportActionContent({menuState, hideAndRun, setLocalShouldKeepOp items.push(createFlagAsOffensiveAction({reportID: data.reportID, reportAction, hideAndRun, translate, flagIcon: icons.Flag})); } if (showDownload) { - items.push( - createDownloadAction({reportAction, encryptedAuthToken: data.encryptedAuthToken, interceptAnonymousUser, download: data.download, translate, downloadIcon: icons.Download}), - ); + items.push(createDownloadAction({reportAction, encryptedAuthToken: data.encryptedAuthToken, download: data.download, translate, downloadIcon: icons.Download})); } if (showDebug) { - items.push(createDebugAction({reportID: data.reportID, reportAction, interceptAnonymousUser, translate, bugIcon: icons.Bug})); + items.push(createDebugAction({reportID: data.reportID, reportAction, translate, bugIcon: icons.Bug})); } if (showDelete) { items.push(createDeleteAction({reportID: data.reportID, reportAction, moneyRequestAction: data.moneyRequestAction, hideAndRun, translate, trashcanIcon: icons.Trashcan})); } - items.push( - createOverflowMenuAction( - {openOverflowMenu, openContextMenu: () => setLocalShouldKeepOpen(true), interceptAnonymousUser, translate, threeDotsIcon: icons.ThreeDots}, - overflowMenuRef, - ), - ); + items.push(createOverflowMenuAction({openOverflowMenu, openContextMenu: () => setLocalShouldKeepOpen(true), translate, threeDotsIcon: icons.ThreeDots}, overflowMenuRef)); return items; // eslint-disable-next-line react-hooks/exhaustive-deps }, [ @@ -329,7 +305,6 @@ function PopoverReportActionContent({menuState, hideAndRun, setLocalShouldKeepOp showDelete, data, currentUserAccountID, - interceptAnonymousUser, translate, icons, ]); @@ -342,7 +317,6 @@ function PopoverReportActionContent({menuState, hideAndRun, setLocalShouldKeepOp openContextMenu: () => setLocalShouldKeepOpen(true), setIsEmojiPickerActive: menuState.onEmojiPickerToggle, hideAndRun, - interceptAnonymousUser, }); const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({ diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/ReportAction/useReportActionContextMenuData.ts b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/ReportAction/useReportActionContextMenuData.ts index cd085401b356..64fb1802e6b3 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/ReportAction/useReportActionContextMenuData.ts +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/ReportAction/useReportActionContextMenuData.ts @@ -1,5 +1,4 @@ import type {RefObject} from 'react'; -import {InteractionManager} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; import {useSession} from '@components/OnyxListItemProvider'; @@ -31,8 +30,6 @@ import { } from '@libs/ReportUtils'; import {RESTRICTED_READONLY_ACTION_IDS} from '@pages/inbox/report/ContextMenu/actions/actionConfig'; import type {ContextMenuAnchor, ContextMenuType} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; -import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; -import {isAnonymousUser, signOutAndRedirectToSignIn} from '@userActions/Session'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {OriginalMessageIOU, ReportAction} from '@src/types/onyx'; @@ -146,18 +143,6 @@ function useReportActionContextMenuData({reportID, reportActionID, originalRepor const card = useGetExpensifyCardFromReportAction({reportAction, policyID}); - const interceptAnonymousUser = (callback: () => void, isAnonymousAction = false) => { - if (isAnonymousUser() && !isAnonymousAction) { - hideContextMenu(false); - // eslint-disable-next-line @typescript-eslint/no-deprecated - InteractionManager.runAfterInteractions(() => { - signOutAndRedirectToSignIn(); - }); - } else { - callback(); - } - }; - return { report, originalReport, @@ -195,7 +180,6 @@ function useReportActionContextMenuData({reportID, reportActionID, originalRepor harvestReport, download, disabledActionIDs, - interceptAnonymousUser, showDelegateNoAccessModal, translate, getLocalDateFromDatetime, diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/Text/PopoverTextContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/Text/PopoverTextContent.tsx index 0dcc677ad853..6d77b8a44159 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/Text/PopoverTextContent.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/Text/PopoverTextContent.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import {InteractionManager, View} from 'react-native'; +import {View} from 'react-native'; import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; @@ -7,9 +7,9 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import Clipboard from '@libs/Clipboard'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; -import {isAnonymousUser, signOutAndRedirectToSignIn} from '@userActions/Session'; import CONST from '@src/CONST'; import type {PopoverContentProps} from '..'; @@ -20,18 +20,6 @@ function PopoverTextContent({menuState, contentRef}: PopoverContentProps) { const StyleUtils = useStyleUtils(); const icons = useMemoizedLazyExpensifyIcons(['Copy', 'Checkmark'] as const); - const interceptAnonymousUser = (callback: () => void, isAnonymousAction = false) => { - if (isAnonymousUser() && !isAnonymousAction) { - hideContextMenu(false); - // eslint-disable-next-line @typescript-eslint/no-deprecated - InteractionManager.runAfterInteractions(() => { - signOutAndRedirectToSignIn(); - }); - } else { - callback(); - } - }; - const handlePress = () => { interceptAnonymousUser(() => { Clipboard.setString(menuState.selection); diff --git a/src/pages/inbox/report/ContextMenu/actions/copyLinkAction.ts b/src/pages/inbox/report/ContextMenu/actions/copyLinkAction.ts index 4b6f83eecda4..6d0c71c6b495 100644 --- a/src/pages/inbox/report/ContextMenu/actions/copyLinkAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/copyLinkAction.ts @@ -2,6 +2,7 @@ import type {RefObject} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import Clipboard from '@libs/Clipboard'; import {getEnvironmentURL} from '@libs/Environment/Environment'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import {isActionOfType, isMessageDeleted, isReportActionAttachment} from '@libs/ReportActionsUtils'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; @@ -14,7 +15,6 @@ import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes type CopyLinkActionParams = BaseContextMenuActionParams & { reportAction: ReportAction; originalReportID: string | undefined; - interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; linkCopyIcon: IconAsset; checkmarkIcon: IconAsset; }; @@ -26,7 +26,7 @@ function shouldShowCopyLinkAction({reportAction, menuTarget}: {reportAction: Ony return !isAttachmentTarget && !isMessageDeleted(reportAction) && !isDEWRouted; } -function createCopyLinkAction({reportAction, originalReportID, interceptAnonymousUser, translate, linkCopyIcon, checkmarkIcon}: CopyLinkActionParams): ContextMenuAction { +function createCopyLinkAction({reportAction, originalReportID, translate, linkCopyIcon, checkmarkIcon}: CopyLinkActionParams): ContextMenuAction { return { id: 'copyLink', icon: linkCopyIcon, diff --git a/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.ts b/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.ts index 0e9ba6ba2916..599b633bfe7e 100644 --- a/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.ts @@ -4,6 +4,7 @@ import type {LocaleContextProps, LocalizedTranslate} from '@components/LocaleCon import type useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import Clipboard from '@libs/Clipboard'; import getClipboardText from '@libs/Clipboard/getClipboardText'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {formatPhoneNumber as formatPhoneNumberPhoneUtils} from '@libs/LocalePhoneNumber'; import {getForReportActionTemp} from '@libs/ModifiedExpenseMessage'; import Parser from '@libs/Parser'; @@ -169,7 +170,6 @@ type CopyMessageClipboardParams = { type CopyMessageActionParams = BaseContextMenuActionParams & CopyMessageClipboardParams & { - interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; copyIcon: IconAsset; checkmarkIcon: IconAsset; }; @@ -532,7 +532,7 @@ function copyMessageToClipboard(params: CopyMessageClipboardParams) { } } -function createCopyMessageAction({interceptAnonymousUser, translate, copyIcon, checkmarkIcon, ...clipboardParams}: CopyMessageActionParams): ContextMenuAction { +function createCopyMessageAction({translate, copyIcon, checkmarkIcon, ...clipboardParams}: CopyMessageActionParams): ContextMenuAction { return { id: 'copyMessage', icon: copyIcon, diff --git a/src/pages/inbox/report/ContextMenu/actions/copyOnyxDataAction.ts b/src/pages/inbox/report/ContextMenu/actions/copyOnyxDataAction.ts index af589360d917..f16c5a21c020 100644 --- a/src/pages/inbox/report/ContextMenu/actions/copyOnyxDataAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/copyOnyxDataAction.ts @@ -1,6 +1,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import Clipboard from '@libs/Clipboard'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import type {Report} from '@src/types/onyx'; @@ -9,7 +10,6 @@ import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes type CopyOnyxDataActionParams = BaseContextMenuActionParams & { report: OnyxEntry; - interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; copyIcon: IconAsset; checkmarkIcon: IconAsset; }; @@ -18,7 +18,7 @@ function shouldShowCopyOnyxDataAction({isProduction}: {isProduction: boolean}): return !isProduction; } -function createCopyOnyxDataAction({report, interceptAnonymousUser, translate, copyIcon, checkmarkIcon}: CopyOnyxDataActionParams): ContextMenuAction { +function createCopyOnyxDataAction({report, translate, copyIcon, checkmarkIcon}: CopyOnyxDataActionParams): ContextMenuAction { return { id: 'copyOnyxData', icon: copyIcon, diff --git a/src/pages/inbox/report/ContextMenu/actions/debugAction.ts b/src/pages/inbox/report/ContextMenu/actions/debugAction.ts index 27380dfd7624..6f6117173c40 100644 --- a/src/pages/inbox/report/ContextMenu/actions/debugAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/debugAction.ts @@ -1,6 +1,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import Navigation from '@libs/Navigation/Navigation'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; @@ -11,7 +12,6 @@ import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes type DebugActionParams = BaseContextMenuActionParams & { reportID: string | undefined; reportAction: ReportAction; - interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; bugIcon: IconAsset; }; @@ -19,7 +19,7 @@ function shouldShowDebugAction({isDebugModeEnabled}: {isDebugModeEnabled: OnyxEn return !!isDebugModeEnabled; } -function createDebugAction({reportID, reportAction, interceptAnonymousUser, translate, bugIcon}: DebugActionParams): ContextMenuAction { +function createDebugAction({reportID, reportAction, translate, bugIcon}: DebugActionParams): ContextMenuAction { return { id: 'debug', icon: bugIcon, diff --git a/src/pages/inbox/report/ContextMenu/actions/downloadAction.ts b/src/pages/inbox/report/ContextMenu/actions/downloadAction.ts index a37b55589210..1f41c2d32c02 100644 --- a/src/pages/inbox/report/ContextMenu/actions/downloadAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/downloadAction.ts @@ -3,6 +3,7 @@ import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; import {isMobileSafari} from '@libs/Browser'; import fileDownload from '@libs/fileDownload'; import getAttachmentDetails from '@libs/fileDownload/getAttachmentDetails'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import {isMessageDeleted, isReportActionAttachment} from '@libs/ReportActionsUtils'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; @@ -16,7 +17,6 @@ import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes type DownloadActionParams = BaseContextMenuActionParams & { reportAction: ReportAction; encryptedAuthToken: string; - interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; download: OnyxEntry; downloadIcon: IconAsset; }; @@ -28,7 +28,7 @@ function shouldShowDownloadAction({reportAction, isOffline}: {reportAction: Onyx return isAttachment && !isUploading && !!reportAction?.reportActionID && !isMessageDeleted(reportAction) && !isOffline; } -function createDownloadAction({reportAction, encryptedAuthToken, interceptAnonymousUser, download, translate, downloadIcon}: DownloadActionParams): ContextMenuAction { +function createDownloadAction({reportAction, encryptedAuthToken, download, translate, downloadIcon}: DownloadActionParams): ContextMenuAction { const isDownloading = download?.isDownloading ?? false; return { diff --git a/src/pages/inbox/report/ContextMenu/actions/editAction.ts b/src/pages/inbox/report/ContextMenu/actions/editAction.ts index d41b1b649473..4e7a870f977b 100644 --- a/src/pages/inbox/report/ContextMenu/actions/editAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/editAction.ts @@ -1,4 +1,5 @@ import type {OnyxEntry} from 'react-native-onyx'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import Navigation from '@libs/Navigation/Navigation'; import Parser from '@libs/Parser'; import {isMoneyRequestAction} from '@libs/ReportActionsUtils'; @@ -17,7 +18,6 @@ type EditActionParams = BaseContextMenuActionParams & { moneyRequestAction: ReportAction | undefined; draftMessage: string; introSelected: OnyxEntry; - interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; hideAndRun: (callback?: () => void) => void; pencilIcon: IconAsset; }; @@ -36,17 +36,7 @@ function shouldShowEditAction({ return (canEditReportAction(reportAction) || canEditReportAction(moneyRequestAction)) && !isArchivedRoom && !isChronosReport; } -function createEditAction({ - reportID, - reportAction, - moneyRequestAction, - draftMessage, - introSelected, - interceptAnonymousUser, - hideAndRun, - translate, - pencilIcon, -}: EditActionParams): ContextMenuAction { +function createEditAction({reportID, reportAction, moneyRequestAction, draftMessage, introSelected, hideAndRun, translate, pencilIcon}: EditActionParams): ContextMenuAction { return { id: 'edit', icon: pencilIcon, diff --git a/src/pages/inbox/report/ContextMenu/actions/emojiReactionAction.ts b/src/pages/inbox/report/ContextMenu/actions/emojiReactionAction.ts index 0fad428dbf19..a354cf423914 100644 --- a/src/pages/inbox/report/ContextMenu/actions/emojiReactionAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/emojiReactionAction.ts @@ -1,5 +1,6 @@ import type {OnyxEntry} from 'react-native-onyx'; import type {Emoji} from '@assets/emojis/types'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {isActionOfType, isMessageDeleted} from '@libs/ReportActionsUtils'; import {toggleEmojiReaction} from '@userActions/Report'; import CONST from '@src/CONST'; @@ -23,7 +24,6 @@ type EmojiReactionParams = { openContextMenu: () => void; setIsEmojiPickerActive: ((state: boolean) => void) | undefined; hideAndRun: (callback?: () => void) => void; - interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; }; function shouldShowEmojiReaction({reportAction}: {reportAction: OnyxEntry}): boolean { @@ -31,15 +31,7 @@ function shouldShowEmojiReaction({reportAction}: {reportAction: OnyxEntry void) => { hideAndRun(onHideCallback); }; diff --git a/src/pages/inbox/report/ContextMenu/actions/explainAction.ts b/src/pages/inbox/report/ContextMenu/actions/explainAction.ts index 5ad25fcd2df6..db797eb55f9f 100644 --- a/src/pages/inbox/report/ContextMenu/actions/explainAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/explainAction.ts @@ -1,5 +1,6 @@ import type {OnyxEntry} from 'react-native-onyx'; import type useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {hasReasoning} from '@libs/ReportActionsUtils'; import {explain} from '@userActions/Report'; import CONST from '@src/CONST'; @@ -13,7 +14,6 @@ type ExplainActionParams = BaseContextMenuActionParams & { originalReport: OnyxEntry; reportAction: ReportAction; currentUserPersonalDetails: ReturnType; - interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; hideAndRun: (callback?: () => void) => void; conciergeIcon: IconAsset; }; @@ -25,16 +25,7 @@ function shouldShowExplainAction({reportAction, isArchivedRoom}: {reportAction: return hasReasoning(reportAction); } -function createExplainAction({ - childReport, - originalReport, - reportAction, - currentUserPersonalDetails, - interceptAnonymousUser, - hideAndRun, - translate, - conciergeIcon, -}: ExplainActionParams): ContextMenuAction { +function createExplainAction({childReport, originalReport, reportAction, currentUserPersonalDetails, hideAndRun, translate, conciergeIcon}: ExplainActionParams): ContextMenuAction { return { id: 'explain', icon: conciergeIcon, diff --git a/src/pages/inbox/report/ContextMenu/actions/holdAction.ts b/src/pages/inbox/report/ContextMenu/actions/holdAction.ts index 297862423690..8491d91e4505 100644 --- a/src/pages/inbox/report/ContextMenu/actions/holdAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/holdAction.ts @@ -1,4 +1,5 @@ import type {OnyxEntry} from 'react-native-onyx'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {getReportAction} from '@libs/ReportActionsUtils'; import {canHoldUnholdReportAction, changeMoneyRequestHoldStatus} from '@libs/ReportUtils'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; @@ -11,7 +12,6 @@ type HoldActionParams = BaseContextMenuActionParams & { moneyRequestAction: ReportAction | undefined; isDelegateAccessRestricted: boolean; showDelegateNoAccessModal: (() => void) | undefined; - interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; hideAndRun: (callback?: () => void) => void; stopwatchIcon: IconAsset; }; @@ -36,15 +36,7 @@ function shouldShowHoldAction({ return canHoldUnholdReportAction(moneyRequestReport, moneyRequestAction, holdReportAction, iouTransaction, moneyRequestPolicy).canHoldRequest; } -function createHoldAction({ - moneyRequestAction, - isDelegateAccessRestricted, - showDelegateNoAccessModal, - interceptAnonymousUser, - hideAndRun, - translate, - stopwatchIcon, -}: HoldActionParams): ContextMenuAction { +function createHoldAction({moneyRequestAction, isDelegateAccessRestricted, showDelegateNoAccessModal, hideAndRun, translate, stopwatchIcon}: HoldActionParams): ContextMenuAction { return { id: 'hold', icon: stopwatchIcon, diff --git a/src/pages/inbox/report/ContextMenu/actions/joinThreadAction.ts b/src/pages/inbox/report/ContextMenu/actions/joinThreadAction.ts index 7655957e4eed..26f77e4cf29c 100644 --- a/src/pages/inbox/report/ContextMenu/actions/joinThreadAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/joinThreadAction.ts @@ -1,4 +1,5 @@ import type {OnyxEntry} from 'react-native-onyx'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import {isActionableTrackExpense, isCreatedAction, isCreatedTaskReportAction, isDeletedAction, isMoneyRequestAction, isReportPreviewAction, isWhisperAction} from '@libs/ReportActionsUtils'; import {getChildReportNotificationPreference, shouldDisableThread, shouldDisplayThreadReplies} from '@libs/ReportUtils'; @@ -12,7 +13,6 @@ type JoinThreadActionParams = BaseContextMenuActionParams & { reportAction: ReportAction; originalReport: OnyxEntry; currentUserAccountID: number; - interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; hideAndRun: (callback?: () => void) => void; bellIcon: IconAsset; }; @@ -49,7 +49,7 @@ function shouldShowJoinThreadAction({ ); } -function createJoinThreadAction({reportAction, originalReport, currentUserAccountID, interceptAnonymousUser, hideAndRun, translate, bellIcon}: JoinThreadActionParams): ContextMenuAction { +function createJoinThreadAction({reportAction, originalReport, currentUserAccountID, hideAndRun, translate, bellIcon}: JoinThreadActionParams): ContextMenuAction { return { id: 'joinThread', icon: bellIcon, diff --git a/src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.ts b/src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.ts index d562af76f3a2..028405d4ddf1 100644 --- a/src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.ts @@ -1,4 +1,5 @@ import type {OnyxEntry} from 'react-native-onyx'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import {isActionableTrackExpense, isCreatedAction, isCreatedTaskReportAction, isDeletedAction, isMoneyRequestAction, isReportPreviewAction, isWhisperAction} from '@libs/ReportActionsUtils'; import {getChildReportNotificationPreference, shouldDisplayThreadReplies} from '@libs/ReportUtils'; @@ -12,7 +13,6 @@ type LeaveThreadActionParams = BaseContextMenuActionParams & { reportAction: ReportAction; originalReport: OnyxEntry; currentUserAccountID: number; - interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; hideAndRun: (callback?: () => void) => void; exitIcon: IconAsset; }; @@ -47,7 +47,7 @@ function shouldShowLeaveThreadAction({ ); } -function createLeaveThreadAction({reportAction, originalReport, currentUserAccountID, interceptAnonymousUser, hideAndRun, translate, exitIcon}: LeaveThreadActionParams): ContextMenuAction { +function createLeaveThreadAction({reportAction, originalReport, currentUserAccountID, hideAndRun, translate, exitIcon}: LeaveThreadActionParams): ContextMenuAction { return { id: 'leaveThread', icon: exitIcon, diff --git a/src/pages/inbox/report/ContextMenu/actions/markAsUnreadAction.ts b/src/pages/inbox/report/ContextMenu/actions/markAsUnreadAction.ts index 95937a9577b5..f866136f88e5 100644 --- a/src/pages/inbox/report/ContextMenu/actions/markAsUnreadAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/markAsUnreadAction.ts @@ -1,4 +1,5 @@ import type {OnyxEntry} from 'react-native-onyx'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import {isActionOfType} from '@libs/ReportActionsUtils'; import {markCommentAsUnread} from '@userActions/Report'; @@ -12,7 +13,6 @@ type MarkAsUnreadActionParams = BaseContextMenuActionParams & { reportActions: OnyxEntry; reportAction: ReportAction; currentUserAccountID: number; - interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; hideAndRun: (callback?: () => void) => void; chatBubbleUnreadIcon: IconAsset; checkmarkIcon: IconAsset; @@ -31,7 +31,6 @@ function createMarkAsUnreadAction({ reportActions, reportAction, currentUserAccountID, - interceptAnonymousUser, hideAndRun, translate, chatBubbleUnreadIcon, diff --git a/src/pages/inbox/report/ContextMenu/actions/overflowMenuAction.ts b/src/pages/inbox/report/ContextMenu/actions/overflowMenuAction.ts index 3482efefaddc..24677b961384 100644 --- a/src/pages/inbox/report/ContextMenu/actions/overflowMenuAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/overflowMenuAction.ts @@ -1,5 +1,6 @@ import type {RefObject} from 'react'; import type {GestureResponderEvent, View} from 'react-native'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import CONST from '@src/CONST'; import type IconAsset from '@src/types/utils/IconAsset'; import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; @@ -11,11 +12,10 @@ type OverflowMenuDescriptor = ContextMenuAction & { type OverflowMenuActionParams = BaseContextMenuActionParams & { openOverflowMenu: (event: GestureResponderEvent | MouseEvent, anchorRef: RefObject) => void; openContextMenu: () => void; - interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; threeDotsIcon: IconAsset; }; -function createOverflowMenuAction({openOverflowMenu, openContextMenu, interceptAnonymousUser, translate, threeDotsIcon}: OverflowMenuActionParams, threeDotRef: RefObject): OverflowMenuDescriptor { +function createOverflowMenuAction({openOverflowMenu, openContextMenu, translate, threeDotsIcon}: OverflowMenuActionParams, threeDotRef: RefObject): OverflowMenuDescriptor { return { id: 'overflowMenu', icon: threeDotsIcon, diff --git a/src/pages/inbox/report/ContextMenu/actions/pinAction.ts b/src/pages/inbox/report/ContextMenu/actions/pinAction.ts index 3598bc4a5f1b..618afbf6893f 100644 --- a/src/pages/inbox/report/ContextMenu/actions/pinAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/pinAction.ts @@ -1,4 +1,5 @@ import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {togglePinnedState} from '@userActions/Report'; import CONST from '@src/CONST'; import type IconAsset from '@src/types/utils/IconAsset'; @@ -6,7 +7,6 @@ import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes type PinActionParams = BaseContextMenuActionParams & { reportID: string | undefined; - interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; hideAndRun: (callback?: () => void) => void; pinIcon: IconAsset; }; @@ -15,7 +15,7 @@ function shouldShowPinAction({isPinnedChat}: {isPinnedChat: boolean}): boolean { return !isPinnedChat; } -function createPinAction({reportID, interceptAnonymousUser, hideAndRun, translate, pinIcon}: PinActionParams): ContextMenuAction { +function createPinAction({reportID, hideAndRun, translate, pinIcon}: PinActionParams): ContextMenuAction { return { id: 'pin', icon: pinIcon, diff --git a/src/pages/inbox/report/ContextMenu/actions/replyInThreadAction.ts b/src/pages/inbox/report/ContextMenu/actions/replyInThreadAction.ts index 0cb781fb3fc7..429aec64cf12 100644 --- a/src/pages/inbox/report/ContextMenu/actions/replyInThreadAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/replyInThreadAction.ts @@ -1,4 +1,5 @@ import type {OnyxEntry} from 'react-native-onyx'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {shouldDisableThread} from '@libs/ReportUtils'; import {navigateToAndOpenChildReport} from '@userActions/Report'; import CONST from '@src/CONST'; @@ -12,7 +13,6 @@ type ReplyInThreadActionParams = BaseContextMenuActionParams & { reportAction: ReportAction; originalReport: OnyxEntry; currentUserAccountID: number; - interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; hideAndRun: (callback?: () => void) => void; chatBubbleReplyIcon: IconAsset; }; @@ -39,7 +39,6 @@ function createReplyInThreadAction({ reportAction, originalReport, currentUserAccountID, - interceptAnonymousUser, hideAndRun, translate, chatBubbleReplyIcon, diff --git a/src/pages/inbox/report/ContextMenu/actions/unholdAction.ts b/src/pages/inbox/report/ContextMenu/actions/unholdAction.ts index d963de7210fc..87b383187cfe 100644 --- a/src/pages/inbox/report/ContextMenu/actions/unholdAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/unholdAction.ts @@ -1,4 +1,5 @@ import type {OnyxEntry} from 'react-native-onyx'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {getReportAction} from '@libs/ReportActionsUtils'; import {canHoldUnholdReportAction, changeMoneyRequestHoldStatus} from '@libs/ReportUtils'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; @@ -11,7 +12,6 @@ type UnholdActionParams = BaseContextMenuActionParams & { moneyRequestAction: ReportAction | undefined; isDelegateAccessRestricted: boolean; showDelegateNoAccessModal: (() => void) | undefined; - interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; hideAndRun: (callback?: () => void) => void; stopwatchIcon: IconAsset; }; @@ -36,15 +36,7 @@ function shouldShowUnholdAction({ return canHoldUnholdReportAction(moneyRequestReport, moneyRequestAction, holdReportAction, iouTransaction, moneyRequestPolicy).canUnholdRequest; } -function createUnholdAction({ - moneyRequestAction, - isDelegateAccessRestricted, - showDelegateNoAccessModal, - interceptAnonymousUser, - hideAndRun, - translate, - stopwatchIcon, -}: UnholdActionParams): ContextMenuAction { +function createUnholdAction({moneyRequestAction, isDelegateAccessRestricted, showDelegateNoAccessModal, hideAndRun, translate, stopwatchIcon}: UnholdActionParams): ContextMenuAction { return { id: 'unhold', icon: stopwatchIcon, diff --git a/src/pages/inbox/report/ContextMenu/actions/unpinAction.ts b/src/pages/inbox/report/ContextMenu/actions/unpinAction.ts index badf710e47b5..b14ce19b0fd0 100644 --- a/src/pages/inbox/report/ContextMenu/actions/unpinAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/unpinAction.ts @@ -1,4 +1,5 @@ import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {togglePinnedState} from '@userActions/Report'; import CONST from '@src/CONST'; import type IconAsset from '@src/types/utils/IconAsset'; @@ -6,7 +7,6 @@ import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes type UnpinActionParams = BaseContextMenuActionParams & { reportID: string | undefined; - interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; hideAndRun: (callback?: () => void) => void; pinIcon: IconAsset; }; @@ -15,7 +15,7 @@ function shouldShowUnpinAction({isPinnedChat}: {isPinnedChat: boolean}): boolean return isPinnedChat; } -function createUnpinAction({reportID, interceptAnonymousUser, hideAndRun, translate, pinIcon}: UnpinActionParams): ContextMenuAction { +function createUnpinAction({reportID, hideAndRun, translate, pinIcon}: UnpinActionParams): ContextMenuAction { return { id: 'unpin', icon: pinIcon, From 0b0b86e4a8b22bea10c27f4af908b2aba226679f Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 3 Mar 2026 14:08:52 -0800 Subject: [PATCH 39/88] Inline useReportContextMenuData into PopoverReportContent The hook was only used in one place, so inlining simplifies the data flow. Also removes interceptAnonymousUser param from markAsReadAction (it now imports directly like the other action factories). Made-with: Cursor --- .../Report/PopoverReportContent.tsx | 72 ++++++----- .../Report/useReportContextMenuData.ts | 113 ------------------ .../ContextMenu/actions/markAsReadAction.ts | 4 +- 3 files changed, 45 insertions(+), 144 deletions(-) delete mode 100644 src/pages/inbox/report/ContextMenu/PopoverContextMenu/Report/useReportContextMenuData.ts diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/Report/PopoverReportContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/Report/PopoverReportContent.tsx index e9f12fa4d901..5129cf94cbf4 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/Report/PopoverReportContent.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/Report/PopoverReportContent.tsx @@ -1,13 +1,19 @@ import React, {useMemo} from 'react'; import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import FocusableMenuItem from '@components/FocusableMenuItem'; import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; +import useEnvironment from '@hooks/useEnvironment'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import useReportIsArchived from '@hooks/useReportIsArchived'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import {ACTION_IDS} from '@pages/inbox/report/ContextMenu/actions/actionConfig'; +import {canWriteInReport, isUnread} from '@libs/ReportUtils'; +import {ACTION_IDS, RESTRICTED_READONLY_ACTION_IDS} from '@pages/inbox/report/ContextMenu/actions/actionConfig'; import {CONTEXT_MENU_ICON_NAMES} from '@pages/inbox/report/ContextMenu/actions/actionTypes'; import type {ContextMenuAction} from '@pages/inbox/report/ContextMenu/actions/actionTypes'; import createCopyOnyxDataAction, {shouldShowCopyOnyxDataAction} from '@pages/inbox/report/ContextMenu/actions/copyOnyxDataAction'; @@ -16,47 +22,55 @@ import createMarkAsReadAction, {shouldShowMarkAsReadAction} from '@pages/inbox/r import createMarkAsUnreadAction, {shouldShowMarkAsUnreadForReport} from '@pages/inbox/report/ContextMenu/actions/markAsUnreadAction'; import createPinAction, {shouldShowPinAction} from '@pages/inbox/report/ContextMenu/actions/pinAction'; import createUnpinAction, {shouldShowUnpinAction} from '@pages/inbox/report/ContextMenu/actions/unpinAction'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {ReportAction} from '@src/types/onyx'; import type {PopoverContentProps} from '..'; -import useReportContextMenuData from './useReportContextMenuData'; + +const EMPTY_SET = new Set(); function PopoverReportContent({menuState, hideAndRun, contentRef, shouldEnableArrowNavigation}: PopoverContentProps) { const {shouldUseNarrowLayout} = useResponsiveLayout(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {windowWidth} = useWindowDimensions(); + const {translate} = useLocalize(); + const {isProduction} = useEnvironment(); const icons = useMemoizedLazyExpensifyIcons(CONTEXT_MENU_ICON_NAMES); - const data = useReportContextMenuData({ - reportID: menuState.reportID, - reportActionID: menuState.reportActionID, - originalReportID: menuState.originalReportID, - draftMessage: menuState.draftMessage ?? '', - selection: menuState.selection ?? '', - type: 'REPORT', - anchor: {current: menuState.contextMenuTargetNode ?? null}, - }); + const reportID = menuState.reportID; + const reportActionID = menuState.reportActionID; + const originalReportID = menuState.originalReportID; + + const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, {canEvict: false}); + const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); + const [isDebugModeEnabled] = useOnyx(ONYXKEYS.IS_DEBUG_MODE_ENABLED); - const {interceptAnonymousUser, translate, disabledActionIDs} = data; + const isOriginalReportArchived = useReportIsArchived(originalReportID); + const disabledActionIDs = !canWriteInReport(report) ? RESTRICTED_READONLY_ACTION_IDS : EMPTY_SET; const isDisabled = (id: string) => disabledActionIDs.has(id); - const showMarkAsRead = shouldShowMarkAsReadAction({isUnreadChat: data.isUnreadChat}) && !isDisabled(ACTION_IDS.MARK_AS_READ); - const showMarkAsUnread = shouldShowMarkAsUnreadForReport({isUnreadChat: data.isUnreadChat}) && !isDisabled(ACTION_IDS.MARK_AS_UNREAD); - const showPin = shouldShowPinAction({isPinnedChat: data.isPinnedChat}) && !isDisabled(ACTION_IDS.PIN); - const showUnpin = shouldShowUnpinAction({isPinnedChat: data.isPinnedChat}) && !isDisabled(ACTION_IDS.UNPIN); - const showCopyOnyxData = shouldShowCopyOnyxDataAction({isProduction: data.isProduction}) && !isDisabled(ACTION_IDS.COPY_ONYX_DATA); - const showDebug = shouldShowDebugAction({isDebugModeEnabled: data.isDebugModeEnabled}) && !isDisabled(ACTION_IDS.DEBUG); + const hasValidReportAction = reportActions && reportActionID && reportActionID !== '0' && reportActionID !== '-1'; + const reportAction: OnyxEntry = hasValidReportAction ? reportActions[reportActionID] : undefined; + + const isPinnedChat = !!report?.isPinned; + const isUnreadChat = isUnread(report, undefined, isOriginalReportArchived); + + const showMarkAsRead = shouldShowMarkAsReadAction({isUnreadChat}) && !isDisabled(ACTION_IDS.MARK_AS_READ); + const showMarkAsUnread = shouldShowMarkAsUnreadForReport({isUnreadChat}) && !isDisabled(ACTION_IDS.MARK_AS_UNREAD); + const showPin = shouldShowPinAction({isPinnedChat}) && !isDisabled(ACTION_IDS.PIN); + const showUnpin = shouldShowUnpinAction({isPinnedChat}) && !isDisabled(ACTION_IDS.UNPIN); + const showCopyOnyxData = shouldShowCopyOnyxDataAction({isProduction}) && !isDisabled(ACTION_IDS.COPY_ONYX_DATA); + const showDebug = shouldShowDebugAction({isDebugModeEnabled}) && !isDisabled(ACTION_IDS.DEBUG); - const markAsReadActionItem = showMarkAsRead - ? createMarkAsReadAction({reportID: data.reportID, interceptAnonymousUser, hideAndRun, translate, mailIcon: icons.Mail, checkmarkIcon: icons.Checkmark}) - : undefined; + const markAsReadActionItem = showMarkAsRead ? createMarkAsReadAction({reportID, hideAndRun, translate, mailIcon: icons.Mail, checkmarkIcon: icons.Checkmark}) : undefined; const markAsUnreadActionItem = - showMarkAsUnread && data.reportAction + showMarkAsUnread && reportAction ? createMarkAsUnreadAction({ - reportID: data.reportID, - reportActions: data.reportActions, - reportAction: data.reportAction, + reportID, + reportActions, + reportAction, currentUserAccountID: 0, hideAndRun, translate, @@ -64,10 +78,10 @@ function PopoverReportContent({menuState, hideAndRun, contentRef, shouldEnableAr checkmarkIcon: icons.Checkmark, }) : undefined; - const pinActionItem = showPin ? createPinAction({reportID: data.reportID, hideAndRun, translate, pinIcon: icons.Pin}) : undefined; - const unpinActionItem = showUnpin ? createUnpinAction({reportID: data.reportID, hideAndRun, translate, pinIcon: icons.Pin}) : undefined; - const copyOnyxDataActionItem = showCopyOnyxData ? createCopyOnyxDataAction({report: data.report, translate, copyIcon: icons.Copy, checkmarkIcon: icons.Checkmark}) : undefined; - const debugActionItem = showDebug && data.reportAction ? createDebugAction({reportID: data.reportID, reportAction: data.reportAction, translate, bugIcon: icons.Bug}) : undefined; + const pinActionItem = showPin ? createPinAction({reportID, hideAndRun, translate, pinIcon: icons.Pin}) : undefined; + const unpinActionItem = showUnpin ? createUnpinAction({reportID, hideAndRun, translate, pinIcon: icons.Pin}) : undefined; + const copyOnyxDataActionItem = showCopyOnyxData ? createCopyOnyxDataAction({report, translate, copyIcon: icons.Copy, checkmarkIcon: icons.Checkmark}) : undefined; + const debugActionItem = showDebug && reportAction ? createDebugAction({reportID, reportAction, translate, bugIcon: icons.Bug}) : undefined; const visibleActions = useMemo(() => { const items: ContextMenuAction[] = []; diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/Report/useReportContextMenuData.ts b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/Report/useReportContextMenuData.ts deleted file mode 100644 index 1b597bfd7d38..000000000000 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/Report/useReportContextMenuData.ts +++ /dev/null @@ -1,113 +0,0 @@ -import type {RefObject} from 'react'; -import {InteractionManager} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; -import useEnvironment from '@hooks/useEnvironment'; -import useLocalize from '@hooks/useLocalize'; -import useNetwork from '@hooks/useNetwork'; -import useOnyx from '@hooks/useOnyx'; -import useReportIsArchived from '@hooks/useReportIsArchived'; -import {canWriteInReport, chatIncludesChronosWithID, isArchivedNonExpenseReport, isUnread} from '@libs/ReportUtils'; -import {RESTRICTED_READONLY_ACTION_IDS} from '@pages/inbox/report/ContextMenu/actions/actionConfig'; -import type {ContextMenuAnchor, ContextMenuType} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; -import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; -import {isAnonymousUser, signOutAndRedirectToSignIn} from '@userActions/Session'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type {ReportAction, ReportActions, Report as ReportType} from '@src/types/onyx'; - -const EMPTY_SET = new Set(); - -type UseContextMenuDataParams = { - reportID: string | undefined; - reportActionID: string | undefined; - originalReportID: string | undefined; - draftMessage: string; - selection: string; - type: ContextMenuType; - anchor: RefObject | undefined; -}; - -type UseReportContextMenuDataReturn = { - report: OnyxEntry; - originalReport: OnyxEntry; - reportActions: OnyxEntry; - reportAction: OnyxEntry; - isArchivedRoom: boolean; - isChronosReport: boolean; - isPinnedChat: boolean; - isUnreadChat: boolean; - isProduction: boolean; - isDebugModeEnabled: OnyxEntry; - isOffline: boolean; - disabledActionIDs: Set; - translate: ReturnType['translate']; - getLocalDateFromDatetime: ReturnType['getLocalDateFromDatetime']; - interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; - type: ContextMenuType; - reportID: string | undefined; - originalReportID: string | undefined; - draftMessage: string; - selection: string; - anchor: RefObject | undefined; -}; - -function useReportContextMenuData({reportID, reportActionID, originalReportID, draftMessage, selection, type, anchor}: UseContextMenuDataParams): UseReportContextMenuDataReturn { - const {translate, getLocalDateFromDatetime} = useLocalize(); - const {isOffline} = useNetwork(); - const {isProduction} = useEnvironment(); - - const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, {canEvict: false}); - const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); - const [originalReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${originalReportID}`); - const [isDebugModeEnabled] = useOnyx(ONYXKEYS.IS_DEBUG_MODE_ENABLED); - - const isOriginalReportArchived = useReportIsArchived(originalReportID); - - const disabledActionIDs = !canWriteInReport(report) ? RESTRICTED_READONLY_ACTION_IDS : EMPTY_SET; - - const hasValidReportAction = reportActions && reportActionID && reportActionID !== '0' && reportActionID !== '-1'; - const reportAction: OnyxEntry = hasValidReportAction ? reportActions[reportActionID] : undefined; - - const isChronosReport = chatIncludesChronosWithID(originalReportID); - const isArchivedRoom = isArchivedNonExpenseReport(originalReport, isOriginalReportArchived); - const isPinnedChat = !!report?.isPinned; - const isUnreadChat = isUnread(report, undefined, isOriginalReportArchived); - - const interceptAnonymousUser = (callback: () => void, isAnonymousAction = false) => { - if (isAnonymousUser() && !isAnonymousAction) { - hideContextMenu(false); - // eslint-disable-next-line @typescript-eslint/no-deprecated - InteractionManager.runAfterInteractions(() => { - signOutAndRedirectToSignIn(); - }); - } else { - callback(); - } - }; - - return { - report, - originalReport, - reportActions, - reportAction, - isArchivedRoom, - isChronosReport, - isPinnedChat, - isUnreadChat, - isProduction, - isDebugModeEnabled, - isOffline: !!isOffline, - disabledActionIDs, - translate, - getLocalDateFromDatetime, - interceptAnonymousUser, - type, - reportID, - originalReportID, - draftMessage, - selection, - anchor, - }; -} - -export default useReportContextMenuData; -export type {UseContextMenuDataParams, UseReportContextMenuDataReturn}; diff --git a/src/pages/inbox/report/ContextMenu/actions/markAsReadAction.ts b/src/pages/inbox/report/ContextMenu/actions/markAsReadAction.ts index 17161b0e41a4..ffc8a6439dd1 100644 --- a/src/pages/inbox/report/ContextMenu/actions/markAsReadAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/markAsReadAction.ts @@ -1,3 +1,4 @@ +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import {readNewestAction} from '@userActions/Report'; import CONST from '@src/CONST'; @@ -6,7 +7,6 @@ import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes type MarkAsReadActionParams = BaseContextMenuActionParams & { reportID: string | undefined; - interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; hideAndRun: (callback?: () => void) => void; mailIcon: IconAsset; checkmarkIcon: IconAsset; @@ -16,7 +16,7 @@ function shouldShowMarkAsReadAction({isUnreadChat}: {isUnreadChat: boolean}): bo return isUnreadChat; } -function createMarkAsReadAction({reportID, interceptAnonymousUser, hideAndRun, translate, mailIcon, checkmarkIcon}: MarkAsReadActionParams): ContextMenuAction { +function createMarkAsReadAction({reportID, hideAndRun, translate, mailIcon, checkmarkIcon}: MarkAsReadActionParams): ContextMenuAction { return { id: 'markAsRead', icon: mailIcon, From 6671d6459fa5322090186a9012bb6e875dfdcab2 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 3 Mar 2026 14:15:51 -0800 Subject: [PATCH 40/88] Flatten PopoverContextMenu subdirectories and move shared hook Move useReportActionContextMenuData to ContextMenu/ root since it's shared across PopoverContextMenu and MiniReportActionContextMenu. Flatten single-file subdirectories (Email/, Link/, Report/, ReportAction/, Text/) into PopoverContextMenu/ directly. Made-with: Cursor --- .../ContextMenu/MiniReportActionContextMenu/index.tsx | 2 +- .../{Email => }/PopoverEmailContent.tsx | 2 +- .../{Link => }/PopoverLinkContent.tsx | 2 +- .../{ReportAction => }/PopoverReportActionContent.tsx | 4 ++-- .../{Report => }/PopoverReportContent.tsx | 2 +- .../{Text => }/PopoverTextContent.tsx | 2 +- .../report/ContextMenu/PopoverContextMenu/index.tsx | 10 +++++----- .../useReportActionContextMenuData.ts | 0 8 files changed, 12 insertions(+), 12 deletions(-) rename src/pages/inbox/report/ContextMenu/PopoverContextMenu/{Email => }/PopoverEmailContent.tsx (98%) rename src/pages/inbox/report/ContextMenu/PopoverContextMenu/{Link => }/PopoverLinkContent.tsx (97%) rename src/pages/inbox/report/ContextMenu/PopoverContextMenu/{ReportAction => }/PopoverReportActionContent.tsx (99%) rename src/pages/inbox/report/ContextMenu/PopoverContextMenu/{Report => }/PopoverReportContent.tsx (99%) rename src/pages/inbox/report/ContextMenu/PopoverContextMenu/{Text => }/PopoverTextContent.tsx (97%) rename src/pages/inbox/report/ContextMenu/{PopoverContextMenu/ReportAction => }/useReportActionContextMenuData.ts (100%) diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index 5bf8adf712f9..2b77e7828437 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -31,7 +31,7 @@ import createMarkAsUnreadAction, {shouldShowMarkAsUnreadForReportAction} from '@ import createReplyInThreadAction, {shouldShowReplyInThreadAction} from '@pages/inbox/report/ContextMenu/actions/replyInThreadAction'; import createUnholdAction, {shouldShowUnholdAction} from '@pages/inbox/report/ContextMenu/actions/unholdAction'; import {useMiniContextMenuActions, useMiniContextMenuState} from '@pages/inbox/report/ContextMenu/MiniContextMenuProvider'; -import useReportActionContextMenuData from '@pages/inbox/report/ContextMenu/PopoverContextMenu/ReportAction/useReportActionContextMenuData'; +import useReportActionContextMenuData from '@pages/inbox/report/ContextMenu/useReportActionContextMenuData'; import type {ContextMenuAnchor} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {showContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/Email/PopoverEmailContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverEmailContent.tsx similarity index 98% rename from src/pages/inbox/report/ContextMenu/PopoverContextMenu/Email/PopoverEmailContent.tsx rename to src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverEmailContent.tsx index 9d4e0a9b653d..d0e624893e9f 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/Email/PopoverEmailContent.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverEmailContent.tsx @@ -12,7 +12,7 @@ import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; -import type {PopoverContentProps} from '..'; +import type {PopoverContentProps} from '.'; function PopoverEmailContent({menuState, contentRef}: PopoverContentProps) { const {translate} = useLocalize(); diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/Link/PopoverLinkContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverLinkContent.tsx similarity index 97% rename from src/pages/inbox/report/ContextMenu/PopoverContextMenu/Link/PopoverLinkContent.tsx rename to src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverLinkContent.tsx index a604f5940cec..210032e688d4 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/Link/PopoverLinkContent.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverLinkContent.tsx @@ -11,7 +11,7 @@ import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; -import type {PopoverContentProps} from '..'; +import type {PopoverContentProps} from '.'; function PopoverLinkContent({menuState, contentRef}: PopoverContentProps) { const {translate} = useLocalize(); diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/ReportAction/PopoverReportActionContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx similarity index 99% rename from src/pages/inbox/report/ContextMenu/PopoverContextMenu/ReportAction/PopoverReportActionContent.tsx rename to src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx index c9205336a8a1..2198005478ee 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/ReportAction/PopoverReportActionContent.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx @@ -33,8 +33,8 @@ import createReplyInThreadAction, {shouldShowReplyInThreadAction} from '@pages/i import createUnholdAction, {shouldShowUnholdAction} from '@pages/inbox/report/ContextMenu/actions/unholdAction'; import {showContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; -import type {PopoverContentProps} from '..'; -import useReportActionContextMenuData from './useReportActionContextMenuData'; +import type {PopoverContentProps} from '.'; +import useReportActionContextMenuData from '@pages/inbox/report/ContextMenu/useReportActionContextMenuData'; function PopoverReportActionContent({menuState, hideAndRun, setLocalShouldKeepOpen, contentRef, shouldEnableArrowNavigation}: PopoverContentProps) { const {shouldUseNarrowLayout} = useResponsiveLayout(); diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/Report/PopoverReportContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportContent.tsx similarity index 99% rename from src/pages/inbox/report/ContextMenu/PopoverContextMenu/Report/PopoverReportContent.tsx rename to src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportContent.tsx index 5129cf94cbf4..afbdc6c87d98 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/Report/PopoverReportContent.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportContent.tsx @@ -24,7 +24,7 @@ import createPinAction, {shouldShowPinAction} from '@pages/inbox/report/ContextM import createUnpinAction, {shouldShowUnpinAction} from '@pages/inbox/report/ContextMenu/actions/unpinAction'; import ONYXKEYS from '@src/ONYXKEYS'; import type {ReportAction} from '@src/types/onyx'; -import type {PopoverContentProps} from '..'; +import type {PopoverContentProps} from '.'; const EMPTY_SET = new Set(); diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/Text/PopoverTextContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverTextContent.tsx similarity index 97% rename from src/pages/inbox/report/ContextMenu/PopoverContextMenu/Text/PopoverTextContent.tsx rename to src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverTextContent.tsx index 6d77b8a44159..6717a20a1c9c 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/Text/PopoverTextContent.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverTextContent.tsx @@ -11,7 +11,7 @@ import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; -import type {PopoverContentProps} from '..'; +import type {PopoverContentProps} from '.'; function PopoverTextContent({menuState, contentRef}: PopoverContentProps) { const {translate} = useLocalize(); diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/index.tsx index 01783764b1b6..5a3879e0a521 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/index.tsx @@ -16,11 +16,11 @@ import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionConte import type {ContextMenuAnchor, ContextMenuType, ReportActionContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import ConfirmDeleteReportActionModal from './ConfirmDeleteReportActionModal'; -import PopoverEmailContent from './Email/PopoverEmailContent'; -import PopoverLinkContent from './Link/PopoverLinkContent'; -import PopoverReportContent from './Report/PopoverReportContent'; -import PopoverReportActionContent from './ReportAction/PopoverReportActionContent'; -import PopoverTextContent from './Text/PopoverTextContent'; +import PopoverEmailContent from './PopoverEmailContent'; +import PopoverLinkContent from './PopoverLinkContent'; +import PopoverReportContent from './PopoverReportContent'; +import PopoverReportActionContent from './PopoverReportActionContent'; +import PopoverTextContent from './PopoverTextContent'; function extractPointerEvent(event: GestureResponderEvent | MouseEvent): MouseEvent | NativeTouchEvent { if ('nativeEvent' in event) { diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/ReportAction/useReportActionContextMenuData.ts b/src/pages/inbox/report/ContextMenu/useReportActionContextMenuData.ts similarity index 100% rename from src/pages/inbox/report/ContextMenu/PopoverContextMenu/ReportAction/useReportActionContextMenuData.ts rename to src/pages/inbox/report/ContextMenu/useReportActionContextMenuData.ts From a88626f16a116fab46706c01ecf7ce6c3e3c5f26 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 3 Mar 2026 14:27:14 -0800 Subject: [PATCH 41/88] Remove interceptAnonymousUser param from remaining action factories copyEmailAction, copyURLAction, copyToClipboardAction, and emojiReactionAction all now import interceptAnonymousUser directly from @libs/interceptAnonymousUser instead of accepting it as a parameter. This completes the elimination of passing static functions as params throughout the context menu actions. Made-with: Cursor --- .../report/ContextMenu/MiniReportActionContextMenu/index.tsx | 4 ++-- .../PopoverContextMenu/PopoverReportActionContent.tsx | 5 +++-- .../inbox/report/ContextMenu/actions/copyEmailAction.ts | 4 ++-- .../report/ContextMenu/actions/copyToClipboardAction.ts | 4 ++-- src/pages/inbox/report/ContextMenu/actions/copyURLAction.ts | 4 ++-- .../inbox/report/ContextMenu/actions/emojiReactionAction.ts | 3 --- 6 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index 2b77e7828437..b03dccf054d5 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -31,9 +31,9 @@ import createMarkAsUnreadAction, {shouldShowMarkAsUnreadForReportAction} from '@ import createReplyInThreadAction, {shouldShowReplyInThreadAction} from '@pages/inbox/report/ContextMenu/actions/replyInThreadAction'; import createUnholdAction, {shouldShowUnholdAction} from '@pages/inbox/report/ContextMenu/actions/unholdAction'; import {useMiniContextMenuActions, useMiniContextMenuState} from '@pages/inbox/report/ContextMenu/MiniContextMenuProvider'; -import useReportActionContextMenuData from '@pages/inbox/report/ContextMenu/useReportActionContextMenuData'; import type {ContextMenuAnchor} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {showContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import useReportActionContextMenuData from '@pages/inbox/report/ContextMenu/useReportActionContextMenuData'; import CONST from '@src/CONST'; const SLIDE_DURATION = 200; @@ -407,7 +407,7 @@ function MiniReportActionContextMenu() { {hasEmoji && !!emojiData.reportAction && !!emojiData.reportActionID && ( - emojiData.interceptAnonymousUser(() => emojiData.toggleEmojiAndCloseMenu(emoji, existingReactions, preferredSkinTone)) + interceptAnonymousUser(() => emojiData.toggleEmojiAndCloseMenu(emoji, existingReactions, preferredSkinTone)) } onPressOpenPicker={emojiData.onPressOpenPicker} onEmojiPickerClosed={emojiData.onEmojiPickerClosed} diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx index 2198005478ee..a3b1d085211a 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx @@ -12,6 +12,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {ACTION_IDS} from '@pages/inbox/report/ContextMenu/actions/actionConfig'; import {CONTEXT_MENU_ICON_NAMES} from '@pages/inbox/report/ContextMenu/actions/actionTypes'; import type {ContextMenuAction} from '@pages/inbox/report/ContextMenu/actions/actionTypes'; @@ -32,9 +33,9 @@ import createOverflowMenuAction from '@pages/inbox/report/ContextMenu/actions/ov import createReplyInThreadAction, {shouldShowReplyInThreadAction} from '@pages/inbox/report/ContextMenu/actions/replyInThreadAction'; import createUnholdAction, {shouldShowUnholdAction} from '@pages/inbox/report/ContextMenu/actions/unholdAction'; import {showContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import useReportActionContextMenuData from '@pages/inbox/report/ContextMenu/useReportActionContextMenuData'; import CONST from '@src/CONST'; import type {PopoverContentProps} from '.'; -import useReportActionContextMenuData from '@pages/inbox/report/ContextMenu/useReportActionContextMenuData'; function PopoverReportActionContent({menuState, hideAndRun, setLocalShouldKeepOpen, contentRef, shouldEnableArrowNavigation}: PopoverContentProps) { const {shouldUseNarrowLayout} = useResponsiveLayout(); @@ -340,7 +341,7 @@ function PopoverReportActionContent({menuState, hideAndRun, setLocalShouldKeepOp - emojiData.interceptAnonymousUser(() => emojiData.toggleEmojiAndCloseMenu(emoji, existingReactions, preferredSkinTone)) + interceptAnonymousUser(() => emojiData.toggleEmojiAndCloseMenu(emoji, existingReactions, preferredSkinTone)) } reportActionID={emojiData.reportActionID} reportAction={emojiData.reportAction} diff --git a/src/pages/inbox/report/ContextMenu/actions/copyEmailAction.ts b/src/pages/inbox/report/ContextMenu/actions/copyEmailAction.ts index 667ac6054a22..4dce93531b50 100644 --- a/src/pages/inbox/report/ContextMenu/actions/copyEmailAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/copyEmailAction.ts @@ -1,5 +1,6 @@ import Clipboard from '@libs/Clipboard'; import EmailUtils from '@libs/EmailUtils'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; @@ -8,12 +9,11 @@ import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes type CopyEmailActionParams = BaseContextMenuActionParams & { selection: string; - interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; copyIcon: IconAsset; checkmarkIcon: IconAsset; }; -function createCopyEmailAction({selection, interceptAnonymousUser, translate, copyIcon, checkmarkIcon}: CopyEmailActionParams): ContextMenuAction { +function createCopyEmailAction({selection, translate, copyIcon, checkmarkIcon}: CopyEmailActionParams): ContextMenuAction { return { id: 'copyEmail', icon: copyIcon, diff --git a/src/pages/inbox/report/ContextMenu/actions/copyToClipboardAction.ts b/src/pages/inbox/report/ContextMenu/actions/copyToClipboardAction.ts index 6cf784b20989..531185ea6803 100644 --- a/src/pages/inbox/report/ContextMenu/actions/copyToClipboardAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/copyToClipboardAction.ts @@ -1,4 +1,5 @@ import Clipboard from '@libs/Clipboard'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; @@ -7,12 +8,11 @@ import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes type CopyToClipboardActionParams = BaseContextMenuActionParams & { selection: string; - interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; copyIcon: IconAsset; checkmarkIcon: IconAsset; }; -function createCopyToClipboardAction({selection, interceptAnonymousUser, translate, copyIcon, checkmarkIcon}: CopyToClipboardActionParams): ContextMenuAction { +function createCopyToClipboardAction({selection, translate, copyIcon, checkmarkIcon}: CopyToClipboardActionParams): ContextMenuAction { return { id: 'copyToClipboard', icon: copyIcon, diff --git a/src/pages/inbox/report/ContextMenu/actions/copyURLAction.ts b/src/pages/inbox/report/ContextMenu/actions/copyURLAction.ts index ab0241d85c61..fc55eb6f0822 100644 --- a/src/pages/inbox/report/ContextMenu/actions/copyURLAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/copyURLAction.ts @@ -1,4 +1,5 @@ import Clipboard from '@libs/Clipboard'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; @@ -7,12 +8,11 @@ import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes type CopyURLActionParams = BaseContextMenuActionParams & { selection: string; - interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; copyIcon: IconAsset; checkmarkIcon: IconAsset; }; -function createCopyURLAction({selection, interceptAnonymousUser, translate, copyIcon, checkmarkIcon}: CopyURLActionParams): ContextMenuAction { +function createCopyURLAction({selection, translate, copyIcon, checkmarkIcon}: CopyURLActionParams): ContextMenuAction { return { id: 'copyUrl', icon: copyIcon, diff --git a/src/pages/inbox/report/ContextMenu/actions/emojiReactionAction.ts b/src/pages/inbox/report/ContextMenu/actions/emojiReactionAction.ts index a354cf423914..b2ed2f7b220b 100644 --- a/src/pages/inbox/report/ContextMenu/actions/emojiReactionAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/emojiReactionAction.ts @@ -1,6 +1,5 @@ import type {OnyxEntry} from 'react-native-onyx'; import type {Emoji} from '@assets/emojis/types'; -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {isActionOfType, isMessageDeleted} from '@libs/ReportActionsUtils'; import {toggleEmojiReaction} from '@userActions/Report'; import CONST from '@src/CONST'; @@ -14,7 +13,6 @@ type EmojiReactionData = { closeContextMenu: (onHideCallback?: () => void) => void; onPressOpenPicker: () => void; onEmojiPickerClosed: () => void; - interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; }; type EmojiReactionParams = { @@ -62,7 +60,6 @@ function createEmojiReactionData({reportID, reportAction, currentUserAccountID, closeContextMenu, onPressOpenPicker, onEmojiPickerClosed, - interceptAnonymousUser, }; } From 858f61559e4d2daa083dbfbc2d893df02a138f8f Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 3 Mar 2026 14:35:12 -0800 Subject: [PATCH 42/88] Delete unused copy action factory files copyEmailAction, copyURLAction, and copyToClipboardAction were created during the factory decomposition but never wired up. The copy functionality already exists inline in PopoverEmailContent, PopoverLinkContent, and PopoverTextContent respectively. Made-with: Cursor --- .../ContextMenu/actions/copyEmailAction.ts | 34 ------------------- .../actions/copyToClipboardAction.ts | 32 ----------------- .../ContextMenu/actions/copyURLAction.ts | 33 ------------------ 3 files changed, 99 deletions(-) delete mode 100644 src/pages/inbox/report/ContextMenu/actions/copyEmailAction.ts delete mode 100644 src/pages/inbox/report/ContextMenu/actions/copyToClipboardAction.ts delete mode 100644 src/pages/inbox/report/ContextMenu/actions/copyURLAction.ts diff --git a/src/pages/inbox/report/ContextMenu/actions/copyEmailAction.ts b/src/pages/inbox/report/ContextMenu/actions/copyEmailAction.ts deleted file mode 100644 index 4dce93531b50..000000000000 --- a/src/pages/inbox/report/ContextMenu/actions/copyEmailAction.ts +++ /dev/null @@ -1,34 +0,0 @@ -import Clipboard from '@libs/Clipboard'; -import EmailUtils from '@libs/EmailUtils'; -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; -import CONST from '@src/CONST'; -import type IconAsset from '@src/types/utils/IconAsset'; -import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; - -type CopyEmailActionParams = BaseContextMenuActionParams & { - selection: string; - copyIcon: IconAsset; - checkmarkIcon: IconAsset; -}; - -function createCopyEmailAction({selection, translate, copyIcon, checkmarkIcon}: CopyEmailActionParams): ContextMenuAction { - return { - id: 'copyEmail', - icon: copyIcon, - text: translate('reportActionContextMenu.copyEmailToClipboard'), - successText: translate('reportActionContextMenu.copied'), - successIcon: checkmarkIcon, - description: EmailUtils.prefixMailSeparatorsWithBreakOpportunities(EmailUtils.trimMailTo(selection ?? '')), - isAnonymousAction: true, - onPress: () => - interceptAnonymousUser(() => { - Clipboard.setString(EmailUtils.trimMailTo(selection)); - hideContextMenu(true, ReportActionComposeFocusManager.focus); - }, true), - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_EMAIL, - }; -} - -export default createCopyEmailAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/copyToClipboardAction.ts b/src/pages/inbox/report/ContextMenu/actions/copyToClipboardAction.ts deleted file mode 100644 index 531185ea6803..000000000000 --- a/src/pages/inbox/report/ContextMenu/actions/copyToClipboardAction.ts +++ /dev/null @@ -1,32 +0,0 @@ -import Clipboard from '@libs/Clipboard'; -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; -import CONST from '@src/CONST'; -import type IconAsset from '@src/types/utils/IconAsset'; -import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; - -type CopyToClipboardActionParams = BaseContextMenuActionParams & { - selection: string; - copyIcon: IconAsset; - checkmarkIcon: IconAsset; -}; - -function createCopyToClipboardAction({selection, translate, copyIcon, checkmarkIcon}: CopyToClipboardActionParams): ContextMenuAction { - return { - id: 'copyToClipboard', - icon: copyIcon, - text: translate('common.copyToClipboard'), - successText: translate('reportActionContextMenu.copied'), - successIcon: checkmarkIcon, - isAnonymousAction: true, - onPress: () => - interceptAnonymousUser(() => { - Clipboard.setString(selection); - hideContextMenu(true, ReportActionComposeFocusManager.focus); - }, true), - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_TO_CLIPBOARD, - }; -} - -export default createCopyToClipboardAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/copyURLAction.ts b/src/pages/inbox/report/ContextMenu/actions/copyURLAction.ts deleted file mode 100644 index fc55eb6f0822..000000000000 --- a/src/pages/inbox/report/ContextMenu/actions/copyURLAction.ts +++ /dev/null @@ -1,33 +0,0 @@ -import Clipboard from '@libs/Clipboard'; -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; -import CONST from '@src/CONST'; -import type IconAsset from '@src/types/utils/IconAsset'; -import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; - -type CopyURLActionParams = BaseContextMenuActionParams & { - selection: string; - copyIcon: IconAsset; - checkmarkIcon: IconAsset; -}; - -function createCopyURLAction({selection, translate, copyIcon, checkmarkIcon}: CopyURLActionParams): ContextMenuAction { - return { - id: 'copyUrl', - icon: copyIcon, - text: translate('reportActionContextMenu.copyURLToClipboard'), - successText: translate('reportActionContextMenu.copied'), - successIcon: checkmarkIcon, - description: selection, - isAnonymousAction: true, - onPress: () => - interceptAnonymousUser(() => { - Clipboard.setString(selection); - hideContextMenu(true, ReportActionComposeFocusManager.focus); - }, true), - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_URL, - }; -} - -export default createCopyURLAction; From 597c37c015a1741fec9c2f2088c97fa7c7d11fbe Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 3 Mar 2026 16:08:13 -0800 Subject: [PATCH 43/88] Remove redundant ancestorsRef in ConfirmDeleteReportActionModal Pass ancestors directly to deleteReportComment; the ref and effect were unnecessary since handleConfirm already closes over the latest ancestors. Made-with: Cursor --- .../ConfirmDeleteReportActionModal.tsx | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/ConfirmDeleteReportActionModal.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/ConfirmDeleteReportActionModal.tsx index a704bcf1ad70..21b522beb769 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/ConfirmDeleteReportActionModal.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/ConfirmDeleteReportActionModal.tsx @@ -1,4 +1,4 @@ -import {useEffect, useRef, useState} from 'react'; +import {useState} from 'react'; import {DeviceEventEmitter, InteractionManager} from 'react-native'; import ConfirmModal from '@components/ConfirmModal'; import type {ModalProps} from '@components/Modal/Global/ModalContext'; @@ -64,17 +64,9 @@ function ConfirmDeleteReportActionModal({closeModal, reportID, reportActionID, a policy, }); - const ancestorsRef = useRef>([]); const ancestors = useAncestors(originalReport); const {transactions: reportTransactions} = useTransactionsAndViolationsForReport(originalReport?.iouReportID); - useEffect(() => { - if (!originalReport) { - return; - } - ancestorsRef.current = ancestors; - }, [originalReport, ancestors]); - const [isVisible, setIsVisible] = useState(true); const [closeAction, setCloseAction] = useState(ModalActions.CLOSE); @@ -105,7 +97,7 @@ function ConfirmDeleteReportActionModal({closeModal, reportID, reportActionID, a } else if (reportAction) { // eslint-disable-next-line @typescript-eslint/no-deprecated InteractionManager.runAfterInteractions(() => { - deleteReportComment(report, reportAction, ancestorsRef.current, isReportArchived, isOriginalReportArchived, email ?? '', visibleReportActionsData ?? undefined); + deleteReportComment(report, reportAction, ancestors, isReportArchived, isOriginalReportArchived, email ?? '', visibleReportActionsData ?? undefined); }); } From 6bc97c575a471c01bd9510829c5ff01566687d64 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 3 Mar 2026 16:14:49 -0800 Subject: [PATCH 44/88] Extract originalReportID to avoid duplicate getOriginalReportID calls Made-with: Cursor --- .../PopoverContextMenu/ConfirmDeleteReportActionModal.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/ConfirmDeleteReportActionModal.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/ConfirmDeleteReportActionModal.tsx index 21b522beb769..d31f794f4482 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/ConfirmDeleteReportActionModal.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/ConfirmDeleteReportActionModal.tsx @@ -45,8 +45,9 @@ function ConfirmDeleteReportActionModal({closeModal, reportID, reportActionID, a const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS); const isReportArchived = useReportIsArchived(reportID); - const [originalReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getOriginalReportID(reportID, reportAction, actionReportActions)}`); - const isOriginalReportArchived = useReportIsArchived(getOriginalReportID(reportID, reportAction, actionReportActions)); + const originalReportID = getOriginalReportID(reportID, reportAction, actionReportActions); + const [originalReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${originalReportID}`); + const isOriginalReportArchived = useReportIsArchived(originalReportID); const {iouReport, chatReport, isChatIOUReportArchived} = useGetIOUReportFromReportAction(reportAction); const transactionIDs: string[] = []; From 2d5edf5563110b6eb798052de854e41910128366 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 3 Mar 2026 16:40:08 -0800 Subject: [PATCH 45/88] Move actionTypes exports into actionConfig and delete actionTypes.ts Consolidate CONTEXT_MENU_ICON_NAMES, ContextMenuAction, and BaseContextMenuActionParams into actionConfig.ts. Update all consumers to import from actionConfig instead of the now-deleted actionTypes. Also remove useMemo from PopoverReportContent (React Compiler handles it). Made-with: Cursor --- .../MiniReportActionContextMenu/index.tsx | 3 +- .../PopoverReportActionContent.tsx | 5 +- .../PopoverReportContent.tsx | 48 +++++++++---------- .../ContextMenu/actions/actionConfig.ts | 46 +++++++++++++++++- .../report/ContextMenu/actions/actionTypes.ts | 45 ----------------- .../ContextMenu/actions/copyLinkAction.ts | 2 +- .../ContextMenu/actions/copyMessageAction.ts | 2 +- .../ContextMenu/actions/copyOnyxDataAction.ts | 4 +- .../report/ContextMenu/actions/debugAction.ts | 4 +- .../ContextMenu/actions/deleteAction.ts | 2 +- .../ContextMenu/actions/downloadAction.ts | 2 +- .../report/ContextMenu/actions/editAction.ts | 2 +- .../ContextMenu/actions/explainAction.ts | 2 +- .../actions/flagAsOffensiveAction.ts | 2 +- .../report/ContextMenu/actions/holdAction.ts | 2 +- .../ContextMenu/actions/joinThreadAction.ts | 2 +- .../ContextMenu/actions/leaveThreadAction.ts | 2 +- .../ContextMenu/actions/markAsReadAction.ts | 2 +- .../ContextMenu/actions/markAsUnreadAction.ts | 2 +- .../ContextMenu/actions/overflowMenuAction.ts | 2 +- .../report/ContextMenu/actions/pinAction.ts | 4 +- .../actions/replyInThreadAction.ts | 2 +- .../ContextMenu/actions/unholdAction.ts | 2 +- .../report/ContextMenu/actions/unpinAction.ts | 4 +- 24 files changed, 92 insertions(+), 101 deletions(-) delete mode 100644 src/pages/inbox/report/ContextMenu/actions/actionTypes.ts diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index b03dccf054d5..7ae9b181f589 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -14,8 +14,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import getButtonState from '@libs/getButtonState'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import {ACTION_IDS} from '@pages/inbox/report/ContextMenu/actions/actionConfig'; -import {CONTEXT_MENU_ICON_NAMES} from '@pages/inbox/report/ContextMenu/actions/actionTypes'; +import {ACTION_IDS, CONTEXT_MENU_ICON_NAMES} from '@pages/inbox/report/ContextMenu/actions/actionConfig'; import createCopyLinkAction, {shouldShowCopyLinkAction} from '@pages/inbox/report/ContextMenu/actions/copyLinkAction'; import createCopyMessageAction, {shouldShowCopyMessageAction} from '@pages/inbox/report/ContextMenu/actions/copyMessageAction'; import createDeleteAction, {shouldShowDeleteAction} from '@pages/inbox/report/ContextMenu/actions/deleteAction'; diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx index a3b1d085211a..3f82a578d601 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx @@ -13,9 +13,8 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import {ACTION_IDS} from '@pages/inbox/report/ContextMenu/actions/actionConfig'; -import {CONTEXT_MENU_ICON_NAMES} from '@pages/inbox/report/ContextMenu/actions/actionTypes'; -import type {ContextMenuAction} from '@pages/inbox/report/ContextMenu/actions/actionTypes'; +import {ACTION_IDS, CONTEXT_MENU_ICON_NAMES} from '@pages/inbox/report/ContextMenu/actions/actionConfig'; +import type {ContextMenuAction} from '@pages/inbox/report/ContextMenu/actions/actionConfig'; import createCopyLinkAction, {shouldShowCopyLinkAction} from '@pages/inbox/report/ContextMenu/actions/copyLinkAction'; import createCopyMessageAction, {shouldShowCopyMessageAction} from '@pages/inbox/report/ContextMenu/actions/copyMessageAction'; import createDebugAction, {shouldShowDebugAction} from '@pages/inbox/report/ContextMenu/actions/debugAction'; diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportContent.tsx index afbdc6c87d98..c1c4b6dc9cd3 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportContent.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportContent.tsx @@ -1,4 +1,4 @@ -import React, {useMemo} from 'react'; +import React from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import FocusableMenuItem from '@components/FocusableMenuItem'; @@ -13,9 +13,8 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import {canWriteInReport, isUnread} from '@libs/ReportUtils'; -import {ACTION_IDS, RESTRICTED_READONLY_ACTION_IDS} from '@pages/inbox/report/ContextMenu/actions/actionConfig'; -import {CONTEXT_MENU_ICON_NAMES} from '@pages/inbox/report/ContextMenu/actions/actionTypes'; -import type {ContextMenuAction} from '@pages/inbox/report/ContextMenu/actions/actionTypes'; +import {ACTION_IDS, CONTEXT_MENU_ICON_NAMES, RESTRICTED_READONLY_ACTION_IDS} from '@pages/inbox/report/ContextMenu/actions/actionConfig'; +import type {ContextMenuAction} from '@pages/inbox/report/ContextMenu/actions/actionConfig'; import createCopyOnyxDataAction, {shouldShowCopyOnyxDataAction} from '@pages/inbox/report/ContextMenu/actions/copyOnyxDataAction'; import createDebugAction, {shouldShowDebugAction} from '@pages/inbox/report/ContextMenu/actions/debugAction'; import createMarkAsReadAction, {shouldShowMarkAsReadAction} from '@pages/inbox/report/ContextMenu/actions/markAsReadAction'; @@ -83,28 +82,25 @@ function PopoverReportContent({menuState, hideAndRun, contentRef, shouldEnableAr const copyOnyxDataActionItem = showCopyOnyxData ? createCopyOnyxDataAction({report, translate, copyIcon: icons.Copy, checkmarkIcon: icons.Checkmark}) : undefined; const debugActionItem = showDebug && reportAction ? createDebugAction({reportID, reportAction, translate, bugIcon: icons.Bug}) : undefined; - const visibleActions = useMemo(() => { - const items: ContextMenuAction[] = []; - if (markAsReadActionItem) { - items.push(markAsReadActionItem); - } - if (markAsUnreadActionItem) { - items.push(markAsUnreadActionItem); - } - if (pinActionItem) { - items.push(pinActionItem); - } - if (unpinActionItem) { - items.push(unpinActionItem); - } - if (copyOnyxDataActionItem) { - items.push(copyOnyxDataActionItem); - } - if (debugActionItem) { - items.push(debugActionItem); - } - return items; - }, [markAsReadActionItem, markAsUnreadActionItem, pinActionItem, unpinActionItem, copyOnyxDataActionItem, debugActionItem]); + const visibleActions: ContextMenuAction[] = []; + if (markAsReadActionItem) { + visibleActions.push(markAsReadActionItem); + } + if (markAsUnreadActionItem) { + visibleActions.push(markAsUnreadActionItem); + } + if (pinActionItem) { + visibleActions.push(pinActionItem); + } + if (unpinActionItem) { + visibleActions.push(unpinActionItem); + } + if (copyOnyxDataActionItem) { + visibleActions.push(copyOnyxDataActionItem); + } + if (debugActionItem) { + visibleActions.push(debugActionItem); + } const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({ initialFocusedIndex: -1, diff --git a/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts b/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts index 33cd2827a3a0..f62e68b1289c 100644 --- a/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts +++ b/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts @@ -1,7 +1,49 @@ +import type {GestureResponderEvent} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; +import type {LocalizedTranslate} from '@components/LocaleContextProvider'; +import type IconAsset from '@src/types/utils/IconAsset'; import type {ReportAction} from '@src/types/onyx'; +const CONTEXT_MENU_ICON_NAMES = [ + 'Bell', + 'Bug', + 'ChatBubbleReply', + 'ChatBubbleUnread', + 'Checkmark', + 'Concierge', + 'Copy', + 'Download', + 'Exit', + 'Flag', + 'LinkCopy', + 'Mail', + 'Pencil', + 'Pin', + 'Stopwatch', + 'ThreeDots', + 'Trashcan', +] as const; + +type BaseContextMenuActionParams = { + translate: LocalizedTranslate; +}; + +type ContextMenuAction = { + id: string; + icon: IconAsset; + text: string; + onPress: (event?: GestureResponderEvent | MouseEvent | KeyboardEvent) => void; + successIcon?: IconAsset; + successText?: string; + description?: string; + isAnonymousAction?: boolean; + disabled?: boolean; + shouldShowLoadingSpinnerIcon?: boolean; + shouldPreventDefaultFocusOnPress?: boolean; + sentryLabel: string; +}; + const ACTION_IDS = { EMOJI_REACTION: 'emojiReaction', REPLY_IN_THREAD: 'replyInThread', @@ -37,5 +79,5 @@ function getActionHtml(reportAction: OnyxEntry): string { const RESTRICTED_READONLY_ACTION_IDS = new Set([ACTION_IDS.REPLY_IN_THREAD, ACTION_IDS.EDIT, ACTION_IDS.JOIN_THREAD, ACTION_IDS.DELETE]); -export {ACTION_IDS, RESTRICTED_READONLY_ACTION_IDS, getActionHtml}; -export type {ActionID}; +export {ACTION_IDS, CONTEXT_MENU_ICON_NAMES, RESTRICTED_READONLY_ACTION_IDS, getActionHtml}; +export type {ActionID, BaseContextMenuActionParams, ContextMenuAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/actionTypes.ts b/src/pages/inbox/report/ContextMenu/actions/actionTypes.ts deleted file mode 100644 index 13a1049b6a20..000000000000 --- a/src/pages/inbox/report/ContextMenu/actions/actionTypes.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type {GestureResponderEvent} from 'react-native'; -import type {LocalizedTranslate} from '@components/LocaleContextProvider'; -import type IconAsset from '@src/types/utils/IconAsset'; - -const CONTEXT_MENU_ICON_NAMES = [ - 'Bell', - 'Bug', - 'ChatBubbleReply', - 'ChatBubbleUnread', - 'Checkmark', - 'Concierge', - 'Copy', - 'Download', - 'Exit', - 'Flag', - 'LinkCopy', - 'Mail', - 'Pencil', - 'Pin', - 'Stopwatch', - 'ThreeDots', - 'Trashcan', -] as const; - -type BaseContextMenuActionParams = { - translate: LocalizedTranslate; -}; - -type ContextMenuAction = { - id: string; - icon: IconAsset; - text: string; - onPress: (event?: GestureResponderEvent | MouseEvent | KeyboardEvent) => void; - successIcon?: IconAsset; - successText?: string; - description?: string; - isAnonymousAction?: boolean; - disabled?: boolean; - shouldShowLoadingSpinnerIcon?: boolean; - shouldPreventDefaultFocusOnPress?: boolean; - sentryLabel: string; -}; - -export {CONTEXT_MENU_ICON_NAMES}; -export type {BaseContextMenuActionParams, ContextMenuAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/copyLinkAction.ts b/src/pages/inbox/report/ContextMenu/actions/copyLinkAction.ts index 6d0c71c6b495..1d661c04cc72 100644 --- a/src/pages/inbox/report/ContextMenu/actions/copyLinkAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/copyLinkAction.ts @@ -10,7 +10,7 @@ import type {ContextMenuAnchor} from '@pages/inbox/report/ContextMenu/ReportActi import CONST from '@src/CONST'; import type {ReportAction} from '@src/types/onyx'; import type IconAsset from '@src/types/utils/IconAsset'; -import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; +import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; type CopyLinkActionParams = BaseContextMenuActionParams & { reportAction: ReportAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.ts b/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.ts index 599b633bfe7e..60ad03b5e6b7 100644 --- a/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.ts @@ -146,7 +146,7 @@ import CONST from '@src/CONST'; import type {Card, Policy, PolicyTagLists, ReportAction, Report as ReportType, Transaction} from '@src/types/onyx'; import type IconAsset from '@src/types/utils/IconAsset'; import {getActionHtml} from './actionConfig'; -import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; +import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; type CopyMessageClipboardParams = { reportAction: ReportAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/copyOnyxDataAction.ts b/src/pages/inbox/report/ContextMenu/actions/copyOnyxDataAction.ts index f16c5a21c020..5c0bb132698e 100644 --- a/src/pages/inbox/report/ContextMenu/actions/copyOnyxDataAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/copyOnyxDataAction.ts @@ -1,12 +1,12 @@ import type {OnyxEntry} from 'react-native-onyx'; import Clipboard from '@libs/Clipboard'; -import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import type {Report} from '@src/types/onyx'; import type IconAsset from '@src/types/utils/IconAsset'; -import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; +import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; type CopyOnyxDataActionParams = BaseContextMenuActionParams & { report: OnyxEntry; diff --git a/src/pages/inbox/report/ContextMenu/actions/debugAction.ts b/src/pages/inbox/report/ContextMenu/actions/debugAction.ts index 6f6117173c40..6b28562aec82 100644 --- a/src/pages/inbox/report/ContextMenu/actions/debugAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/debugAction.ts @@ -1,13 +1,13 @@ import type {OnyxEntry} from 'react-native-onyx'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import Navigation from '@libs/Navigation/Navigation'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {ReportAction} from '@src/types/onyx'; import type IconAsset from '@src/types/utils/IconAsset'; -import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; +import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; type DebugActionParams = BaseContextMenuActionParams & { reportID: string | undefined; diff --git a/src/pages/inbox/report/ContextMenu/actions/deleteAction.ts b/src/pages/inbox/report/ContextMenu/actions/deleteAction.ts index 66ee8885382d..c383d3a6ff40 100644 --- a/src/pages/inbox/report/ContextMenu/actions/deleteAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/deleteAction.ts @@ -5,7 +5,7 @@ import {showDeleteModal} from '@pages/inbox/report/ContextMenu/ReportActionConte import CONST from '@src/CONST'; import type {ReportAction, Transaction} from '@src/types/onyx'; import type IconAsset from '@src/types/utils/IconAsset'; -import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; +import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; type DeleteActionParams = BaseContextMenuActionParams & { reportID: string | undefined; diff --git a/src/pages/inbox/report/ContextMenu/actions/downloadAction.ts b/src/pages/inbox/report/ContextMenu/actions/downloadAction.ts index 1f41c2d32c02..f3a9c58c5f29 100644 --- a/src/pages/inbox/report/ContextMenu/actions/downloadAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/downloadAction.ts @@ -12,7 +12,7 @@ import CONST from '@src/CONST'; import type {Download as DownloadOnyx, ReportAction} from '@src/types/onyx'; import type IconAsset from '@src/types/utils/IconAsset'; import {getActionHtml} from './actionConfig'; -import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; +import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; type DownloadActionParams = BaseContextMenuActionParams & { reportAction: ReportAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/editAction.ts b/src/pages/inbox/report/ContextMenu/actions/editAction.ts index 4e7a870f977b..03c141d316b0 100644 --- a/src/pages/inbox/report/ContextMenu/actions/editAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/editAction.ts @@ -10,7 +10,7 @@ import ROUTES from '@src/ROUTES'; import type {IntroSelected, ReportAction} from '@src/types/onyx'; import type IconAsset from '@src/types/utils/IconAsset'; import {getActionHtml} from './actionConfig'; -import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; +import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; type EditActionParams = BaseContextMenuActionParams & { reportID: string | undefined; diff --git a/src/pages/inbox/report/ContextMenu/actions/explainAction.ts b/src/pages/inbox/report/ContextMenu/actions/explainAction.ts index db797eb55f9f..a80a11a127cd 100644 --- a/src/pages/inbox/report/ContextMenu/actions/explainAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/explainAction.ts @@ -7,7 +7,7 @@ import CONST from '@src/CONST'; import type {ReportAction, Report as ReportType} from '@src/types/onyx'; import type IconAsset from '@src/types/utils/IconAsset'; import KeyboardUtils from '@src/utils/keyboard'; -import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; +import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; type ExplainActionParams = BaseContextMenuActionParams & { childReport: OnyxEntry; diff --git a/src/pages/inbox/report/ContextMenu/actions/flagAsOffensiveAction.ts b/src/pages/inbox/report/ContextMenu/actions/flagAsOffensiveAction.ts index e9fe10eca0c0..9ecd84f7da09 100644 --- a/src/pages/inbox/report/ContextMenu/actions/flagAsOffensiveAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/flagAsOffensiveAction.ts @@ -6,7 +6,7 @@ import ROUTES from '@src/ROUTES'; import type {ReportAction} from '@src/types/onyx'; import type IconAsset from '@src/types/utils/IconAsset'; import KeyboardUtils from '@src/utils/keyboard'; -import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; +import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; type FlagAsOffensiveActionParams = BaseContextMenuActionParams & { reportID: string | undefined; diff --git a/src/pages/inbox/report/ContextMenu/actions/holdAction.ts b/src/pages/inbox/report/ContextMenu/actions/holdAction.ts index 8491d91e4505..39774c14e63d 100644 --- a/src/pages/inbox/report/ContextMenu/actions/holdAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/holdAction.ts @@ -6,7 +6,7 @@ import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionConte import CONST from '@src/CONST'; import type {Policy, ReportAction, Report as ReportType, Transaction} from '@src/types/onyx'; import type IconAsset from '@src/types/utils/IconAsset'; -import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; +import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; type HoldActionParams = BaseContextMenuActionParams & { moneyRequestAction: ReportAction | undefined; diff --git a/src/pages/inbox/report/ContextMenu/actions/joinThreadAction.ts b/src/pages/inbox/report/ContextMenu/actions/joinThreadAction.ts index 26f77e4cf29c..35b5becdc480 100644 --- a/src/pages/inbox/report/ContextMenu/actions/joinThreadAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/joinThreadAction.ts @@ -7,7 +7,7 @@ import {toggleSubscribeToChildReport} from '@userActions/Report'; import CONST from '@src/CONST'; import type {ReportAction, Report as ReportType} from '@src/types/onyx'; import type IconAsset from '@src/types/utils/IconAsset'; -import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; +import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; type JoinThreadActionParams = BaseContextMenuActionParams & { reportAction: ReportAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.ts b/src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.ts index 028405d4ddf1..13911318d3ac 100644 --- a/src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.ts @@ -7,7 +7,7 @@ import {toggleSubscribeToChildReport} from '@userActions/Report'; import CONST from '@src/CONST'; import type {ReportAction, Report as ReportType} from '@src/types/onyx'; import type IconAsset from '@src/types/utils/IconAsset'; -import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; +import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; type LeaveThreadActionParams = BaseContextMenuActionParams & { reportAction: ReportAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/markAsReadAction.ts b/src/pages/inbox/report/ContextMenu/actions/markAsReadAction.ts index ffc8a6439dd1..cafe876a5f20 100644 --- a/src/pages/inbox/report/ContextMenu/actions/markAsReadAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/markAsReadAction.ts @@ -3,7 +3,7 @@ import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManag import {readNewestAction} from '@userActions/Report'; import CONST from '@src/CONST'; import type IconAsset from '@src/types/utils/IconAsset'; -import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; +import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; type MarkAsReadActionParams = BaseContextMenuActionParams & { reportID: string | undefined; diff --git a/src/pages/inbox/report/ContextMenu/actions/markAsUnreadAction.ts b/src/pages/inbox/report/ContextMenu/actions/markAsUnreadAction.ts index f866136f88e5..8b73a80a70e5 100644 --- a/src/pages/inbox/report/ContextMenu/actions/markAsUnreadAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/markAsUnreadAction.ts @@ -6,7 +6,7 @@ import {markCommentAsUnread} from '@userActions/Report'; import CONST from '@src/CONST'; import type {ReportAction, ReportActions} from '@src/types/onyx'; import type IconAsset from '@src/types/utils/IconAsset'; -import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; +import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; type MarkAsUnreadActionParams = BaseContextMenuActionParams & { reportID: string | undefined; diff --git a/src/pages/inbox/report/ContextMenu/actions/overflowMenuAction.ts b/src/pages/inbox/report/ContextMenu/actions/overflowMenuAction.ts index 24677b961384..55bb768e818e 100644 --- a/src/pages/inbox/report/ContextMenu/actions/overflowMenuAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/overflowMenuAction.ts @@ -3,7 +3,7 @@ import type {GestureResponderEvent, View} from 'react-native'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import CONST from '@src/CONST'; import type IconAsset from '@src/types/utils/IconAsset'; -import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; +import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; type OverflowMenuDescriptor = ContextMenuAction & { buttonRef: RefObject; diff --git a/src/pages/inbox/report/ContextMenu/actions/pinAction.ts b/src/pages/inbox/report/ContextMenu/actions/pinAction.ts index 618afbf6893f..acb3bd949a80 100644 --- a/src/pages/inbox/report/ContextMenu/actions/pinAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/pinAction.ts @@ -1,9 +1,9 @@ -import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import {togglePinnedState} from '@userActions/Report'; import CONST from '@src/CONST'; import type IconAsset from '@src/types/utils/IconAsset'; -import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; +import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; type PinActionParams = BaseContextMenuActionParams & { reportID: string | undefined; diff --git a/src/pages/inbox/report/ContextMenu/actions/replyInThreadAction.ts b/src/pages/inbox/report/ContextMenu/actions/replyInThreadAction.ts index 429aec64cf12..e02d5212b542 100644 --- a/src/pages/inbox/report/ContextMenu/actions/replyInThreadAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/replyInThreadAction.ts @@ -6,7 +6,7 @@ import CONST from '@src/CONST'; import type {ReportAction, Report as ReportType} from '@src/types/onyx'; import type IconAsset from '@src/types/utils/IconAsset'; import KeyboardUtils from '@src/utils/keyboard'; -import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; +import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; type ReplyInThreadActionParams = BaseContextMenuActionParams & { childReport: OnyxEntry; diff --git a/src/pages/inbox/report/ContextMenu/actions/unholdAction.ts b/src/pages/inbox/report/ContextMenu/actions/unholdAction.ts index 87b383187cfe..3aaaddc93b71 100644 --- a/src/pages/inbox/report/ContextMenu/actions/unholdAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/unholdAction.ts @@ -6,7 +6,7 @@ import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionConte import CONST from '@src/CONST'; import type {Policy, ReportAction, Report as ReportType, Transaction} from '@src/types/onyx'; import type IconAsset from '@src/types/utils/IconAsset'; -import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; +import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; type UnholdActionParams = BaseContextMenuActionParams & { moneyRequestAction: ReportAction | undefined; diff --git a/src/pages/inbox/report/ContextMenu/actions/unpinAction.ts b/src/pages/inbox/report/ContextMenu/actions/unpinAction.ts index b14ce19b0fd0..5b73bd893e94 100644 --- a/src/pages/inbox/report/ContextMenu/actions/unpinAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/unpinAction.ts @@ -1,9 +1,9 @@ -import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import {togglePinnedState} from '@userActions/Report'; import CONST from '@src/CONST'; import type IconAsset from '@src/types/utils/IconAsset'; -import type {BaseContextMenuActionParams, ContextMenuAction} from './actionTypes'; +import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; type UnpinActionParams = BaseContextMenuActionParams & { reportID: string | undefined; From ba5745bc0e94220b79bacccc67fafe70b9735951 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 3 Mar 2026 16:44:18 -0800 Subject: [PATCH 46/88] Replace menuState prop with specific props in context menu content components Each content component now receives only the props it actually depends on: - PopoverEmailContent, PopoverLinkContent, PopoverTextContent: selection + contentRef - PopoverReportContent: reportID, reportActionID, originalReportID + hideAndRun, contentRef, shouldEnableArrowNavigation - PopoverReportActionContent: 7 menu state fields + hideAndRun, setLocalShouldKeepOpen, contentRef, shouldEnableArrowNavigation This prevents unnecessary re-renders when unrelated menuState fields change, and removes unused props (transitionActionSheetState, shouldEnableArrowNavigation for simple content components). The shared PopoverContentProps type is removed. Made-with: Cursor --- .../PopoverEmailContent.tsx | 14 +++-- .../PopoverContextMenu/PopoverLinkContent.tsx | 14 +++-- .../PopoverReportActionContent.tsx | 51 ++++++++++++++----- .../PopoverReportContent.tsx | 19 ++++--- .../PopoverContextMenu/PopoverTextContent.tsx | 12 +++-- .../ContextMenu/PopoverContextMenu/index.tsx | 47 ++++++----------- 6 files changed, 95 insertions(+), 62 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverEmailContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverEmailContent.tsx index d0e624893e9f..0e7bf4ec06dc 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverEmailContent.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverEmailContent.tsx @@ -1,4 +1,6 @@ import React from 'react'; +// eslint-disable-next-line no-restricted-imports +import type {View as ViewType} from 'react-native'; import {View} from 'react-native'; import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; @@ -12,9 +14,13 @@ import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; -import type {PopoverContentProps} from '.'; -function PopoverEmailContent({menuState, contentRef}: PopoverContentProps) { +type PopoverEmailContentProps = { + selection: string; + contentRef: React.RefObject; +}; + +function PopoverEmailContent({selection, contentRef}: PopoverEmailContentProps) { const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const styles = useThemeStyles(); @@ -23,13 +29,13 @@ function PopoverEmailContent({menuState, contentRef}: PopoverContentProps) { const handlePress = () => { interceptAnonymousUser(() => { - Clipboard.setString(EmailUtils.trimMailTo(menuState.selection)); + Clipboard.setString(EmailUtils.trimMailTo(selection)); hideContextMenu(true, ReportActionComposeFocusManager.focus); }, true); }; const wrapperStyle = StyleUtils.getReportActionContextMenuStyles(false, shouldUseNarrowLayout); - const description = EmailUtils.prefixMailSeparatorsWithBreakOpportunities(EmailUtils.trimMailTo(menuState.selection ?? '')); + const description = EmailUtils.prefixMailSeparatorsWithBreakOpportunities(EmailUtils.trimMailTo(selection ?? '')); return ( ; +}; + +function PopoverLinkContent({selection, contentRef}: PopoverLinkContentProps) { const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const styles = useThemeStyles(); @@ -22,7 +28,7 @@ function PopoverLinkContent({menuState, contentRef}: PopoverContentProps) { const handlePress = () => { interceptAnonymousUser(() => { - Clipboard.setString(menuState.selection); + Clipboard.setString(selection); hideContextMenu(true, ReportActionComposeFocusManager.focus); }, true); }; @@ -39,7 +45,7 @@ function PopoverLinkContent({menuState, contentRef}: PopoverContentProps) { icon={icons.Copy} onPress={handlePress} wrapperStyle={[styles.pr8]} - description={menuState.selection} + description={selection} isAnonymousAction successText={translate('reportActionContextMenu.copied')} successIcon={icons.Checkmark} diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx index 3f82a578d601..28103760748d 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx @@ -34,9 +34,34 @@ import createUnholdAction, {shouldShowUnholdAction} from '@pages/inbox/report/Co import {showContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import useReportActionContextMenuData from '@pages/inbox/report/ContextMenu/useReportActionContextMenuData'; import CONST from '@src/CONST'; -import type {PopoverContentProps} from '.'; -function PopoverReportActionContent({menuState, hideAndRun, setLocalShouldKeepOpen, contentRef, shouldEnableArrowNavigation}: PopoverContentProps) { +type PopoverReportActionContentProps = { + reportID: string | undefined; + reportActionID: string | undefined; + originalReportID: string | undefined; + draftMessage: string | undefined; + selection: string; + contextMenuTargetNode: HTMLDivElement | null; + onEmojiPickerToggle: ((state: boolean) => void) | undefined; + hideAndRun: (callback?: () => void) => void; + setLocalShouldKeepOpen: (value: boolean) => void; + contentRef: RefObject; + shouldEnableArrowNavigation: boolean; +}; + +function PopoverReportActionContent({ + reportID, + reportActionID, + originalReportID, + draftMessage, + selection, + contextMenuTargetNode, + onEmojiPickerToggle, + hideAndRun, + setLocalShouldKeepOpen, + contentRef, + shouldEnableArrowNavigation, +}: PopoverReportActionContentProps) { const {shouldUseNarrowLayout} = useResponsiveLayout(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -46,28 +71,28 @@ function PopoverReportActionContent({menuState, hideAndRun, setLocalShouldKeepOp const icons = useMemoizedLazyExpensifyIcons(CONTEXT_MENU_ICON_NAMES); const data = useReportActionContextMenuData({ - reportID: menuState.reportID, - reportActionID: menuState.reportActionID, - originalReportID: menuState.originalReportID, - draftMessage: menuState.draftMessage ?? '', - selection: menuState.selection ?? '', + reportID, + reportActionID, + originalReportID, + draftMessage: draftMessage ?? '', + selection: selection ?? '', type: CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, - anchor: {current: menuState.contextMenuTargetNode ?? null}, + anchor: {current: contextMenuTargetNode ?? null}, }); const openOverflowMenu = (event: GestureResponderEvent | MouseEvent, anchorRefParam: RefObject) => { showContextMenu({ type: CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, event, - selection: menuState.selection ?? '', + selection: selection ?? '', contextMenuAnchor: anchorRefParam?.current as ViewType | RNText | null, report: { - reportID: menuState.reportID, - originalReportID: menuState.originalReportID, + reportID, + originalReportID, }, reportAction: { reportActionID: data.reportAction?.reportActionID, - draftMessage: menuState.draftMessage, + draftMessage, }, callbacks: { onShow: undefined, @@ -315,7 +340,7 @@ function PopoverReportActionContent({menuState, hideAndRun, setLocalShouldKeepOp reportAction: data.reportAction, currentUserAccountID, openContextMenu: () => setLocalShouldKeepOpen(true), - setIsEmojiPickerActive: menuState.onEmojiPickerToggle, + setIsEmojiPickerActive: onEmojiPickerToggle, hideAndRun, }); diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportContent.tsx index c1c4b6dc9cd3..efce7bca2c93 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportContent.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportContent.tsx @@ -1,4 +1,7 @@ +import type {RefObject} from 'react'; import React from 'react'; +// eslint-disable-next-line no-restricted-imports +import type {View as ViewType} from 'react-native'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import FocusableMenuItem from '@components/FocusableMenuItem'; @@ -23,11 +26,19 @@ import createPinAction, {shouldShowPinAction} from '@pages/inbox/report/ContextM import createUnpinAction, {shouldShowUnpinAction} from '@pages/inbox/report/ContextMenu/actions/unpinAction'; import ONYXKEYS from '@src/ONYXKEYS'; import type {ReportAction} from '@src/types/onyx'; -import type {PopoverContentProps} from '.'; + +type PopoverReportContentProps = { + reportID: string | undefined; + reportActionID: string | undefined; + originalReportID: string | undefined; + hideAndRun: (callback?: () => void) => void; + contentRef: RefObject; + shouldEnableArrowNavigation: boolean; +}; const EMPTY_SET = new Set(); -function PopoverReportContent({menuState, hideAndRun, contentRef, shouldEnableArrowNavigation}: PopoverContentProps) { +function PopoverReportContent({reportID, reportActionID, originalReportID, hideAndRun, contentRef, shouldEnableArrowNavigation}: PopoverReportContentProps) { const {shouldUseNarrowLayout} = useResponsiveLayout(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -37,10 +48,6 @@ function PopoverReportContent({menuState, hideAndRun, contentRef, shouldEnableAr const icons = useMemoizedLazyExpensifyIcons(CONTEXT_MENU_ICON_NAMES); - const reportID = menuState.reportID; - const reportActionID = menuState.reportActionID; - const originalReportID = menuState.originalReportID; - const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, {canEvict: false}); const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); const [isDebugModeEnabled] = useOnyx(ONYXKEYS.IS_DEBUG_MODE_ENABLED); diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverTextContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverTextContent.tsx index 6717a20a1c9c..f45bbbd2b5e2 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverTextContent.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverTextContent.tsx @@ -1,4 +1,6 @@ import React from 'react'; +// eslint-disable-next-line no-restricted-imports +import type {View as ViewType} from 'react-native'; import {View} from 'react-native'; import ContextMenuItem from '@components/ContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; @@ -11,9 +13,13 @@ import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; -import type {PopoverContentProps} from '.'; -function PopoverTextContent({menuState, contentRef}: PopoverContentProps) { +type PopoverTextContentProps = { + selection: string; + contentRef: React.RefObject; +}; + +function PopoverTextContent({selection, contentRef}: PopoverTextContentProps) { const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const styles = useThemeStyles(); @@ -22,7 +28,7 @@ function PopoverTextContent({menuState, contentRef}: PopoverContentProps) { const handlePress = () => { interceptAnonymousUser(() => { - Clipboard.setString(menuState.selection); + Clipboard.setString(selection); hideContextMenu(true, ReportActionComposeFocusManager.focus); }, true); }; diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/index.tsx index 5a3879e0a521..5f120466293e 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/index.tsx @@ -1,4 +1,3 @@ -import type {RefObject} from 'react'; import React, {useEffect, useImperativeHandle, useRef, useState} from 'react'; // eslint-disable-next-line no-restricted-imports import type {GestureResponderEvent, NativeTouchEvent, View as ViewType} from 'react-native'; @@ -18,8 +17,8 @@ import CONST from '@src/CONST'; import ConfirmDeleteReportActionModal from './ConfirmDeleteReportActionModal'; import PopoverEmailContent from './PopoverEmailContent'; import PopoverLinkContent from './PopoverLinkContent'; -import PopoverReportContent from './PopoverReportContent'; import PopoverReportActionContent from './PopoverReportActionContent'; +import PopoverReportContent from './PopoverReportContent'; import PopoverTextContent from './PopoverTextContent'; function extractPointerEvent(event: GestureResponderEvent | MouseEvent): MouseEvent | NativeTouchEvent { @@ -50,15 +49,6 @@ type PopoverContextMenuState = { onEmojiPickerToggle: ((state: boolean) => void) | undefined; }; -type PopoverContentProps = { - menuState: PopoverContextMenuState; - hideAndRun: (callback?: () => void) => void; - setLocalShouldKeepOpen: (value: boolean) => void; - transitionActionSheetState: (params: {type: string; payload?: Record}) => void; - contentRef: RefObject; - shouldEnableArrowNavigation: boolean; -}; - type PopoverContextMenuProps = { ref?: React.Ref; }; @@ -365,52 +355,45 @@ function PopoverContextMenu({ref: forwardedRef}: PopoverContextMenuProps) { > {menuState?.type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && ( )} {menuState?.type === CONST.CONTEXT_MENU_TYPES.REPORT && ( )} {menuState?.type === CONST.CONTEXT_MENU_TYPES.LINK && ( )} {menuState?.type === CONST.CONTEXT_MENU_TYPES.EMAIL && ( )} {menuState?.type === CONST.CONTEXT_MENU_TYPES.TEXT && ( )} @@ -420,4 +403,4 @@ function PopoverContextMenu({ref: forwardedRef}: PopoverContextMenuProps) { PopoverContextMenu.displayName = 'PopoverContextMenu'; export default PopoverContextMenu; -export type {PopoverPosition, PopoverContextMenuState, PopoverContentProps}; +export type {PopoverPosition, PopoverContextMenuState}; From 604fe38fd50d0b76240fae351408fa86da62c83d Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 3 Mar 2026 17:08:50 -0800 Subject: [PATCH 47/88] Remove useMemo from PopoverReportActionContent for React Compiler Made-with: Cursor --- .../PopoverReportActionContent.tsx | 70 +++++++------------ 1 file changed, 25 insertions(+), 45 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx index 28103760748d..6ae52259950e 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx @@ -1,5 +1,5 @@ import type {RefObject} from 'react'; -import React, {useMemo, useRef} from 'react'; +import React, {useRef} from 'react'; import {View} from 'react-native'; // eslint-disable-next-line no-restricted-imports import type {GestureResponderEvent, Text as RNText, View as ViewType} from 'react-native'; @@ -171,15 +171,14 @@ function PopoverReportActionContent({ childReportActions: data.childReportActions, }) && !isDisabled(ACTION_IDS.DELETE); - /* eslint-disable react-hooks/refs -- factory functions store refs for later use, they don't read .current during render */ - const visibleActions = useMemo(() => { - if (!data.reportAction) { - return [createOverflowMenuAction({openOverflowMenu, openContextMenu: () => setLocalShouldKeepOpen(true), translate, threeDotsIcon: icons.ThreeDots}, overflowMenuRef)]; - } + const visibleActions: ContextMenuAction[] = []; + if (!data.reportAction) { + // eslint-disable-next-line react-hooks/refs -- factory stores ref for later use, doesn't read .current during render + visibleActions.push(createOverflowMenuAction({openOverflowMenu, openContextMenu: () => setLocalShouldKeepOpen(true), translate, threeDotsIcon: icons.ThreeDots}, overflowMenuRef)); + } else { const reportAction = data.reportAction; - const items: ContextMenuAction[] = []; if (showReplyInThread) { - items.push( + visibleActions.push( createReplyInThreadAction({ childReport: data.childReport, reportAction, @@ -192,7 +191,7 @@ function PopoverReportActionContent({ ); } if (showMarkAsUnread) { - items.push( + visibleActions.push( createMarkAsUnreadAction({ reportID: data.reportID, reportActions: data.reportActions, @@ -206,7 +205,7 @@ function PopoverReportActionContent({ ); } if (showExplain) { - items.push( + visibleActions.push( createExplainAction({ childReport: data.childReport, originalReport: data.originalReport, @@ -219,7 +218,7 @@ function PopoverReportActionContent({ ); } if (showEdit) { - items.push( + visibleActions.push( createEditAction({ reportID: data.reportID, reportAction, @@ -233,7 +232,7 @@ function PopoverReportActionContent({ ); } if (showUnhold) { - items.push( + visibleActions.push( createUnholdAction({ moneyRequestAction: data.moneyRequestAction, isDelegateAccessRestricted: data.isDelegateAccessRestricted, @@ -245,7 +244,7 @@ function PopoverReportActionContent({ ); } if (showHold) { - items.push( + visibleActions.push( createHoldAction({ moneyRequestAction: data.moneyRequestAction, isDelegateAccessRestricted: data.isDelegateAccessRestricted, @@ -257,13 +256,13 @@ function PopoverReportActionContent({ ); } if (showJoinThread) { - items.push(createJoinThreadAction({reportAction, originalReport: data.originalReport, currentUserAccountID, hideAndRun, translate, bellIcon: icons.Bell})); + visibleActions.push(createJoinThreadAction({reportAction, originalReport: data.originalReport, currentUserAccountID, hideAndRun, translate, bellIcon: icons.Bell})); } if (showLeaveThread) { - items.push(createLeaveThreadAction({reportAction, originalReport: data.originalReport, currentUserAccountID, hideAndRun, translate, exitIcon: icons.Exit})); + visibleActions.push(createLeaveThreadAction({reportAction, originalReport: data.originalReport, currentUserAccountID, hideAndRun, translate, exitIcon: icons.Exit})); } if (showCopyMessage) { - items.push( + visibleActions.push( createCopyMessageAction({ reportAction, transaction: data.transaction, @@ -288,7 +287,7 @@ function PopoverReportActionContent({ ); } if (showCopyLink) { - items.push( + visibleActions.push( createCopyLinkAction({ reportAction, originalReportID: data.originalReportID, @@ -299,41 +298,22 @@ function PopoverReportActionContent({ ); } if (showFlagAsOffensive) { - items.push(createFlagAsOffensiveAction({reportID: data.reportID, reportAction, hideAndRun, translate, flagIcon: icons.Flag})); + visibleActions.push(createFlagAsOffensiveAction({reportID: data.reportID, reportAction, hideAndRun, translate, flagIcon: icons.Flag})); } if (showDownload) { - items.push(createDownloadAction({reportAction, encryptedAuthToken: data.encryptedAuthToken, download: data.download, translate, downloadIcon: icons.Download})); + visibleActions.push(createDownloadAction({reportAction, encryptedAuthToken: data.encryptedAuthToken, download: data.download, translate, downloadIcon: icons.Download})); } if (showDebug) { - items.push(createDebugAction({reportID: data.reportID, reportAction, translate, bugIcon: icons.Bug})); + visibleActions.push(createDebugAction({reportID: data.reportID, reportAction, translate, bugIcon: icons.Bug})); } if (showDelete) { - items.push(createDeleteAction({reportID: data.reportID, reportAction, moneyRequestAction: data.moneyRequestAction, hideAndRun, translate, trashcanIcon: icons.Trashcan})); + visibleActions.push( + createDeleteAction({reportID: data.reportID, reportAction, moneyRequestAction: data.moneyRequestAction, hideAndRun, translate, trashcanIcon: icons.Trashcan}), + ); } - items.push(createOverflowMenuAction({openOverflowMenu, openContextMenu: () => setLocalShouldKeepOpen(true), translate, threeDotsIcon: icons.ThreeDots}, overflowMenuRef)); - return items; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - showReplyInThread, - showMarkAsUnread, - showExplain, - showEdit, - showUnhold, - showHold, - showJoinThread, - showLeaveThread, - showCopyMessage, - showCopyLink, - showFlagAsOffensive, - showDownload, - showDebug, - showDelete, - data, - currentUserAccountID, - translate, - icons, - ]); - /* eslint-enable react-hooks/refs */ + // eslint-disable-next-line react-hooks/refs -- factory stores ref for later use, doesn't read .current during render + visibleActions.push(createOverflowMenuAction({openOverflowMenu, openContextMenu: () => setLocalShouldKeepOpen(true), translate, threeDotsIcon: icons.ThreeDots}, overflowMenuRef)); + } const emojiData = createEmojiReactionData({ reportID: data.reportID, From 18e7b3a4e9eccc735532f810419f205a95344db1 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 3 Mar 2026 18:03:31 -0800 Subject: [PATCH 48/88] Refactor context menus to use visibleActions array pattern Made-with: Cursor --- .../MiniReportActionContextMenu/index.tsx | 477 +++++------------- .../PopoverReportActionContent.tsx | 41 +- 2 files changed, 157 insertions(+), 361 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index 7ae9b181f589..5d294533cbfc 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -15,6 +15,7 @@ import useStyleUtils from '@hooks/useStyleUtils'; import getButtonState from '@libs/getButtonState'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {ACTION_IDS, CONTEXT_MENU_ICON_NAMES} from '@pages/inbox/report/ContextMenu/actions/actionConfig'; +import type {ContextMenuAction} from '@pages/inbox/report/ContextMenu/actions/actionConfig'; import createCopyLinkAction, {shouldShowCopyLinkAction} from '@pages/inbox/report/ContextMenu/actions/copyLinkAction'; import createCopyMessageAction, {shouldShowCopyMessageAction} from '@pages/inbox/report/ContextMenu/actions/copyMessageAction'; import createDeleteAction, {shouldShowDeleteAction} from '@pages/inbox/report/ContextMenu/actions/deleteAction'; @@ -209,162 +210,135 @@ function MiniReportActionContextMenu() { childReportActions: data.childReportActions, }) && !isDisabled(ACTION_IDS.DELETE); - const visibleCount = [ - showReplyInThread, - showMarkAsUnread, - showExplain, - showEdit, - showUnhold, - showHold, - showJoinThread, - showLeaveThread, - showCopyMessage, - showCopyLink, - showFlagAsOffensive, - showDownload, - showDelete, - ].filter(Boolean).length; - const needsOverflow = visibleCount > CONST.MINI_CONTEXT_MENU_MAX_ITEMS; - const displayLimit = needsOverflow ? CONST.MINI_CONTEXT_MENU_MAX_ITEMS - 1 : visibleCount; - - let displayedCount = 0; - const displayReplyInThread = showReplyInThread && ++displayedCount <= displayLimit; - const displayMarkAsUnread = showMarkAsUnread && ++displayedCount <= displayLimit; - const displayExplain = showExplain && ++displayedCount <= displayLimit; - const displayEdit = showEdit && ++displayedCount <= displayLimit; - const displayUnhold = showUnhold && ++displayedCount <= displayLimit; - const displayHold = showHold && ++displayedCount <= displayLimit; - const displayJoinThread = showJoinThread && ++displayedCount <= displayLimit; - const displayLeaveThread = showLeaveThread && ++displayedCount <= displayLimit; - const displayCopyMessage = showCopyMessage && ++displayedCount <= displayLimit; - const displayCopyLink = showCopyLink && ++displayedCount <= displayLimit; - const displayFlagAsOffensive = showFlagAsOffensive && ++displayedCount <= displayLimit; - const displayDownload = showDownload && ++displayedCount <= displayLimit; - const displayDelete = showDelete && ++displayedCount <= displayLimit; - - const replyInThreadAction = - displayReplyInThread && reportAction - ? createReplyInThreadAction({ - childReport: data.childReport, - reportAction, - originalReport: data.originalReport, - currentUserAccountID, - - hideAndRun, - translate, - chatBubbleReplyIcon: icons.ChatBubbleReply, - }) - : null; - const markAsUnreadAction = - displayMarkAsUnread && reportAction - ? createMarkAsUnreadAction({ - reportID: data.reportID, - reportActions: data.reportActions, - reportAction, - currentUserAccountID, - - hideAndRun, - translate, - chatBubbleUnreadIcon: icons.ChatBubbleUnread, - checkmarkIcon: icons.Checkmark, - }) - : null; - const explainAction = - displayExplain && reportAction - ? createExplainAction({ - childReport: data.childReport, - originalReport: data.originalReport, - reportAction, - currentUserPersonalDetails: data.currentUserPersonalDetails, - - hideAndRun, - translate, - conciergeIcon: icons.Concierge, - }) - : null; - const editAction = - displayEdit && reportAction - ? createEditAction({ - reportID: data.reportID, - reportAction, - moneyRequestAction: data.moneyRequestAction, - draftMessage: data.draftMessage, - introSelected: data.introSelected, - - hideAndRun, - translate, - pencilIcon: icons.Pencil, - }) - : null; - const unholdAction = displayUnhold - ? createUnholdAction({ - moneyRequestAction: data.moneyRequestAction, - isDelegateAccessRestricted: data.isDelegateAccessRestricted, - showDelegateNoAccessModal: data.showDelegateNoAccessModal, - - hideAndRun, - translate, - stopwatchIcon: icons.Stopwatch, - }) - : null; - const holdAction = displayHold - ? createHoldAction({ - moneyRequestAction: data.moneyRequestAction, - isDelegateAccessRestricted: data.isDelegateAccessRestricted, - showDelegateNoAccessModal: data.showDelegateNoAccessModal, - - hideAndRun, - translate, - stopwatchIcon: icons.Stopwatch, - }) - : null; - const joinThreadAction = - displayJoinThread && reportAction - ? createJoinThreadAction({reportAction, originalReport: data.originalReport, currentUserAccountID, hideAndRun, translate, bellIcon: icons.Bell}) - : null; - const leaveThreadAction = - displayLeaveThread && reportAction - ? createLeaveThreadAction({reportAction, originalReport: data.originalReport, currentUserAccountID, hideAndRun, translate, exitIcon: icons.Exit}) - : null; - const copyMessageAction = - displayCopyMessage && reportAction - ? createCopyMessageAction({ - reportAction, - transaction: data.transaction, - selection: data.selection, - report: data.report, - card: data.card, - originalReport: data.originalReport, - isHarvestReport: data.isHarvestReport, - isTryNewDotNVPDismissed: data.isTryNewDotNVPDismissed, - movedFromReport: data.movedFromReport, - movedToReport: data.movedToReport, - childReport: data.childReport, - policy: data.policy, - getLocalDateFromDatetime: data.getLocalDateFromDatetime, - policyTags: data.policyTags, - translate, - harvestReport: data.harvestReport, - currentUserPersonalDetails: data.currentUserPersonalDetails, + const allVisibleActions: ContextMenuAction[] = []; + if (reportAction) { + if (showReplyInThread) { + allVisibleActions.push( + createReplyInThreadAction({ + childReport: data.childReport, + reportAction, + originalReport: data.originalReport, + currentUserAccountID, + hideAndRun, + translate, + chatBubbleReplyIcon: icons.ChatBubbleReply, + }), + ); + } + if (showMarkAsUnread) { + allVisibleActions.push( + createMarkAsUnreadAction({ + reportID: data.reportID, + reportActions: data.reportActions, + reportAction, + currentUserAccountID, + hideAndRun, + translate, + chatBubbleUnreadIcon: icons.ChatBubbleUnread, + checkmarkIcon: icons.Checkmark, + }), + ); + } + if (showExplain) { + allVisibleActions.push( + createExplainAction({ + childReport: data.childReport, + originalReport: data.originalReport, + reportAction, + currentUserPersonalDetails: data.currentUserPersonalDetails, + hideAndRun, + translate, + conciergeIcon: icons.Concierge, + }), + ); + } + if (showEdit) { + allVisibleActions.push( + createEditAction({ + reportID: data.reportID, + reportAction, + moneyRequestAction: data.moneyRequestAction, + draftMessage: data.draftMessage, + introSelected: data.introSelected, + hideAndRun, + translate, + pencilIcon: icons.Pencil, + }), + ); + } + if (showUnhold) { + allVisibleActions.push( + createUnholdAction({ + moneyRequestAction: data.moneyRequestAction, + isDelegateAccessRestricted: data.isDelegateAccessRestricted, + showDelegateNoAccessModal: data.showDelegateNoAccessModal, + hideAndRun, + translate, + stopwatchIcon: icons.Stopwatch, + }), + ); + } + if (showHold) { + allVisibleActions.push( + createHoldAction({ + moneyRequestAction: data.moneyRequestAction, + isDelegateAccessRestricted: data.isDelegateAccessRestricted, + showDelegateNoAccessModal: data.showDelegateNoAccessModal, + hideAndRun, + translate, + stopwatchIcon: icons.Stopwatch, + }), + ); + } + if (showJoinThread) { + allVisibleActions.push(createJoinThreadAction({reportAction, originalReport: data.originalReport, currentUserAccountID, hideAndRun, translate, bellIcon: icons.Bell})); + } + if (showLeaveThread) { + allVisibleActions.push(createLeaveThreadAction({reportAction, originalReport: data.originalReport, currentUserAccountID, hideAndRun, translate, exitIcon: icons.Exit})); + } + if (showCopyMessage) { + allVisibleActions.push( + createCopyMessageAction({ + reportAction, + transaction: data.transaction, + selection: data.selection, + report: data.report, + card: data.card, + originalReport: data.originalReport, + isHarvestReport: data.isHarvestReport, + isTryNewDotNVPDismissed: data.isTryNewDotNVPDismissed, + movedFromReport: data.movedFromReport, + movedToReport: data.movedToReport, + childReport: data.childReport, + policy: data.policy, + getLocalDateFromDatetime: data.getLocalDateFromDatetime, + policyTags: data.policyTags, + translate, + harvestReport: data.harvestReport, + currentUserPersonalDetails: data.currentUserPersonalDetails, + copyIcon: icons.Copy, + checkmarkIcon: icons.Checkmark, + }), + ); + } + if (showCopyLink) { + allVisibleActions.push(createCopyLinkAction({reportAction, originalReportID: data.originalReportID, translate, linkCopyIcon: icons.LinkCopy, checkmarkIcon: icons.Checkmark})); + } + if (showFlagAsOffensive) { + allVisibleActions.push(createFlagAsOffensiveAction({reportID: data.reportID, reportAction, hideAndRun, translate, flagIcon: icons.Flag})); + } + if (showDownload) { + allVisibleActions.push(createDownloadAction({reportAction, encryptedAuthToken: data.encryptedAuthToken, download: data.download, translate, downloadIcon: icons.Download})); + } + if (showDelete) { + allVisibleActions.push( + createDeleteAction({reportID: data.reportID, reportAction, moneyRequestAction: data.moneyRequestAction, hideAndRun, translate, trashcanIcon: icons.Trashcan}), + ); + } + } - copyIcon: icons.Copy, - checkmarkIcon: icons.Checkmark, - }) - : null; - const copyLinkAction = - displayCopyLink && reportAction - ? createCopyLinkAction({reportAction, originalReportID: data.originalReportID, translate, linkCopyIcon: icons.LinkCopy, checkmarkIcon: icons.Checkmark}) - : null; - const flagAsOffensiveAction = - displayFlagAsOffensive && reportAction ? createFlagAsOffensiveAction({reportID: data.reportID, reportAction, hideAndRun, translate, flagIcon: icons.Flag}) : null; - const downloadAction = - displayDownload && reportAction - ? createDownloadAction({reportAction, encryptedAuthToken: data.encryptedAuthToken, download: data.download, translate, downloadIcon: icons.Download}) - : null; - const deleteAction = - displayDelete && reportAction - ? createDeleteAction({reportID: data.reportID, reportAction, moneyRequestAction: data.moneyRequestAction, hideAndRun, translate, trashcanIcon: icons.Trashcan}) - : null; + const needsOverflow = allVisibleActions.length > CONST.MINI_CONTEXT_MENU_MAX_ITEMS; + const displayedActions = needsOverflow ? allVisibleActions.slice(0, CONST.MINI_CONTEXT_MENU_MAX_ITEMS - 1) : allVisibleActions; const emojiData = createEmojiReactionData({ reportID: data.reportID, @@ -414,201 +388,22 @@ function MiniReportActionContextMenu() { reportAction={emojiData.reportAction} /> )} - {!!replyInThreadAction && ( - - {({hovered, pressed}) => ( - - )} - - )} - {!!markAsUnreadAction && ( - - {({hovered, pressed}) => ( - - )} - - )} - {!!explainAction && ( - - {({hovered, pressed}) => ( - - )} - - )} - {!!editAction && ( - - {({hovered, pressed}) => ( - - )} - - )} - {!!unholdAction && ( - - {({hovered, pressed}) => ( - - )} - - )} - {!!holdAction && ( + {displayedActions.map((action) => ( {({hovered, pressed}) => ( )} - )} - {!!joinThreadAction && ( - - {({hovered, pressed}) => ( - - )} - - )} - {!!leaveThreadAction && ( - - {({hovered, pressed}) => ( - - )} - - )} - {!!copyMessageAction && ( - - {({hovered, pressed}) => ( - - )} - - )} - {!!copyLinkAction && ( - - {({hovered, pressed}) => ( - - )} - - )} - {!!flagAsOffensiveAction && ( - - {({hovered, pressed}) => ( - - )} - - )} - {!!downloadAction && ( - - {({hovered, pressed}) => ( - - )} - - )} - {!!deleteAction && ( - - {({hovered, pressed}) => ( - - )} - - )} + ))} {needsOverflow && ( (null); - const icons = useMemoizedLazyExpensifyIcons(CONTEXT_MENU_ICON_NAMES); const data = useReportActionContextMenuData({ @@ -80,12 +77,12 @@ function PopoverReportActionContent({ anchor: {current: contextMenuTargetNode ?? null}, }); - const openOverflowMenu = (event: GestureResponderEvent | MouseEvent, anchorRefParam: RefObject) => { + const openOverflowMenu = (event: GestureResponderEvent | MouseEvent) => { showContextMenu({ type: CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, event, selection: selection ?? '', - contextMenuAnchor: anchorRefParam?.current as ViewType | RNText | null, + contextMenuAnchor: null, report: { reportID, originalReportID, @@ -171,10 +168,23 @@ function PopoverReportActionContent({ childReportActions: data.childReportActions, }) && !isDisabled(ACTION_IDS.DELETE); + const overflowAction: ContextMenuAction = { + id: 'overflowMenu', + icon: icons.ThreeDots, + text: translate('reportActionContextMenu.menu'), + isAnonymousAction: true, + shouldPreventDefaultFocusOnPress: false, + onPress: (event) => + interceptAnonymousUser(() => { + openOverflowMenu(event as GestureResponderEvent | MouseEvent); + setLocalShouldKeepOpen(true); + }, true), + sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.MENU, + }; + const visibleActions: ContextMenuAction[] = []; if (!data.reportAction) { - // eslint-disable-next-line react-hooks/refs -- factory stores ref for later use, doesn't read .current during render - visibleActions.push(createOverflowMenuAction({openOverflowMenu, openContextMenu: () => setLocalShouldKeepOpen(true), translate, threeDotsIcon: icons.ThreeDots}, overflowMenuRef)); + visibleActions.push(overflowAction); } else { const reportAction = data.reportAction; if (showReplyInThread) { @@ -287,15 +297,7 @@ function PopoverReportActionContent({ ); } if (showCopyLink) { - visibleActions.push( - createCopyLinkAction({ - reportAction, - originalReportID: data.originalReportID, - translate, - linkCopyIcon: icons.LinkCopy, - checkmarkIcon: icons.Checkmark, - }), - ); + visibleActions.push(createCopyLinkAction({reportAction, originalReportID: data.originalReportID, translate, linkCopyIcon: icons.LinkCopy, checkmarkIcon: icons.Checkmark})); } if (showFlagAsOffensive) { visibleActions.push(createFlagAsOffensiveAction({reportID: data.reportID, reportAction, hideAndRun, translate, flagIcon: icons.Flag})); @@ -311,8 +313,7 @@ function PopoverReportActionContent({ createDeleteAction({reportID: data.reportID, reportAction, moneyRequestAction: data.moneyRequestAction, hideAndRun, translate, trashcanIcon: icons.Trashcan}), ); } - // eslint-disable-next-line react-hooks/refs -- factory stores ref for later use, doesn't read .current during render - visibleActions.push(createOverflowMenuAction({openOverflowMenu, openContextMenu: () => setLocalShouldKeepOpen(true), translate, threeDotsIcon: icons.ThreeDots}, overflowMenuRef)); + visibleActions.push(overflowAction); } const emojiData = createEmojiReactionData({ From 7244203510ebc8bf468972bcc8d80e2337efe543 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 3 Mar 2026 18:12:28 -0800 Subject: [PATCH 49/88] Remove unused isChronosReport prop from PureReportActionItem Made-with: Cursor --- src/pages/inbox/report/PureReportActionItem.tsx | 6 ------ src/pages/inbox/report/ReportActionItem.tsx | 2 -- 2 files changed, 8 deletions(-) diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx index e03a2110c6ef..c68545ea5415 100644 --- a/src/pages/inbox/report/PureReportActionItem.tsx +++ b/src/pages/inbox/report/PureReportActionItem.tsx @@ -373,9 +373,6 @@ type PureReportActionItemProps = { /** Whether the room is archived */ isArchivedRoom?: boolean; - /** Whether the room is a chronos report */ - isChronosReport?: boolean; - /** All cards */ cardList?: OnyxTypes.CardList; @@ -528,8 +525,6 @@ function PureReportActionItem({ originalReport, deleteReportActionDraft = () => {}, isArchivedRoom, - // eslint-disable-next-line @typescript-eslint/no-unused-vars -- used in memo comparator - isChronosReport, toggleEmojiReaction = () => {}, createDraftTransactionAndNavigateToParticipantSelector = () => {}, resolveActionableReportMentionWhisper = () => {}, @@ -2177,7 +2172,6 @@ export default memo(PureReportActionItem, (prevProps, nextProps) => { prevProps.originalReportID === nextProps.originalReportID && deepEqual(prevProps.originalReport?.participants, nextProps.originalReport?.participants) && prevProps.isArchivedRoom === nextProps.isArchivedRoom && - prevProps.isChronosReport === nextProps.isChronosReport && prevProps.isClosedExpenseReportWithNoExpenses === nextProps.isClosedExpenseReportWithNoExpenses && deepEqual(prevProps.missingPaymentMethod, nextProps.missingPaymentMethod) && prevProps.reimbursementDeQueuedOrCanceledActionMessage === nextProps.reimbursementDeQueuedOrCanceledActionMessage && diff --git a/src/pages/inbox/report/ReportActionItem.tsx b/src/pages/inbox/report/ReportActionItem.tsx index 19833f34a465..5cfcecabcd40 100644 --- a/src/pages/inbox/report/ReportActionItem.tsx +++ b/src/pages/inbox/report/ReportActionItem.tsx @@ -12,7 +12,6 @@ import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import {getForReportActionTemp, getMovedReportID} from '@libs/ModifiedExpenseMessage'; import {getIOUReportIDFromReportActionPreview, getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils'; import { - chatIncludesChronosWithID, createDraftTransactionAndNavigateToParticipantSelector, getIndicatedMissingPaymentMethod, getReimbursementDeQueuedOrCanceledActionMessage, @@ -161,7 +160,6 @@ function ReportActionItem({ originalReport={originalReport} deleteReportActionDraft={deleteReportActionDraft} isArchivedRoom={isArchivedNonExpenseReport(originalReport, isOriginalReportArchived)} - isChronosReport={chatIncludesChronosWithID(originalReportID)} toggleEmojiReaction={toggleEmojiReaction} createDraftTransactionAndNavigateToParticipantSelector={createDraftTransactionAndNavigateToParticipantSelector} resolveActionableReportMentionWhisper={resolveActionableReportMentionWhisper} From 8de81577380f78016d86360e06cc92dcca4bc401 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 3 Mar 2026 19:17:24 -0800 Subject: [PATCH 50/88] Add JSDoc comments to MiniContextMenuProvider, actionConfig, and useReportActionContextMenuData Made-with: Cursor --- .../inbox/report/ContextMenu/MiniContextMenuProvider.tsx | 9 +++++++++ .../inbox/report/ContextMenu/actions/actionConfig.ts | 2 ++ .../report/ContextMenu/useReportActionContextMenuData.ts | 4 ++++ 3 files changed, 15 insertions(+) diff --git a/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx b/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx index 277ec4fdb73e..fa3e8e1054f2 100644 --- a/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx @@ -27,10 +27,19 @@ type MiniContextMenuState = MiniContextMenuParams & { }; type MiniContextMenuActions = { + /** Display the mini context menu with the given parameters. Cancels any pending hide. */ showMiniContextMenu: (params: MiniContextMenuParams) => void; + + /** Hide the mini context menu after a short delay (or immediately if `options.immediate` is set). No-op while `keepOpen` is active; the hide intent is deferred until `release`. */ hideMiniContextMenu: (options?: {immediate?: boolean}) => void; + + /** Cancel a pending delayed hide without locking the menu open. Future `hideMiniContextMenu` calls still take effect normally. */ cancelHide: () => void; + + /** Lock the menu open so that `hideMiniContextMenu` calls are deferred until `release` is called. Use when a sub-interaction (overflow menu, emoji picker) needs the menu to stay visible. */ keepOpen: () => void; + + /** Unlock the menu after `keepOpen`. If a hide was deferred while locked, it executes immediately. */ release: () => void; }; diff --git a/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts b/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts index f62e68b1289c..923ef8d33548 100644 --- a/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts +++ b/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts @@ -29,6 +29,7 @@ type BaseContextMenuActionParams = { translate: LocalizedTranslate; }; +/** A fully-resolved context menu action ready to be rendered. Created by the `create*Action` factory in each action module. */ type ContextMenuAction = { id: string; icon: IconAsset; @@ -77,6 +78,7 @@ function getActionHtml(reportAction: OnyxEntry): string { return message?.html ?? ''; } +/** Actions that are disabled when the user cannot write in the report. */ const RESTRICTED_READONLY_ACTION_IDS = new Set([ACTION_IDS.REPLY_IN_THREAD, ACTION_IDS.EDIT, ACTION_IDS.JOIN_THREAD, ACTION_IDS.DELETE]); export {ACTION_IDS, CONTEXT_MENU_ICON_NAMES, RESTRICTED_READONLY_ACTION_IDS, getActionHtml}; diff --git a/src/pages/inbox/report/ContextMenu/useReportActionContextMenuData.ts b/src/pages/inbox/report/ContextMenu/useReportActionContextMenuData.ts index 64fb1802e6b3..90abf7a01a38 100644 --- a/src/pages/inbox/report/ContextMenu/useReportActionContextMenuData.ts +++ b/src/pages/inbox/report/ContextMenu/useReportActionContextMenuData.ts @@ -47,6 +47,10 @@ type UseContextMenuDataParams = { anchor: RefObject | undefined; }; +/** + * Aggregates all Onyx data and derived state needed for context menus. + * Consumed by both PopoverReportActionContent (long-press menu) and MiniReportActionContextMenu (hover menu). + */ function useReportActionContextMenuData({reportID, reportActionID, originalReportID, draftMessage, selection, type, anchor}: UseContextMenuDataParams) { const {translate, getLocalDateFromDatetime} = useLocalize(); const {isOffline} = useNetwork(); From 12119fbcc9e96366fe1bd23bbf7ca2de1250b221 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 4 Mar 2026 09:23:41 -0800 Subject: [PATCH 51/88] Context menu: destructure hook data, remove unused fields, optimize show checks - Destructure useReportActionContextMenuData return in both consumers - Remove unused fields: isPinnedChat, isUnreadChat, isProduction, betas, type - Call isDisabled first (fast Set.has) before shouldShow* (potential slow path) - Remove type param from hook; drop useEnvironment, betas Onyx, isUnread import Made-with: Cursor --- .../MiniReportActionContextMenu/index.tsx | 229 ++++++++++------- .../PopoverReportActionContent.tsx | 236 ++++++++++-------- .../ContextMenu/actions/overflowMenuAction.ts | 14 +- .../useReportActionContextMenuData.ts | 16 +- 4 files changed, 276 insertions(+), 219 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index 5d294533cbfc..fde71c0ee625 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -103,13 +103,52 @@ function MiniReportActionContextMenu() { right: baseRight.get(), })); - const data = useReportActionContextMenuData({ + const { + report, + reportAction, + reportActions: reportActionsMap, + originalReport, + childReport, + childReportActions, + policy, + policyTags, + moneyRequestAction, + moneyRequestReport, + moneyRequestPolicy, + iouTransaction, + transaction, + card, + currentUserPersonalDetails, + encryptedAuthToken, + isArchivedRoom, + isChronosReport, + isThreadReportParentAction, + isOffline, + isHarvestReport, + isTryNewDotNVPDismissed, + isDelegateAccessRestricted, + areHoldRequirementsMet, + transactions, + introSelected, + movedFromReport, + movedToReport, + harvestReport, + download, + disabledActionIDs, + showDelegateNoAccessModal, + translate, + getLocalDateFromDatetime, + reportID: resolvedReportID, + originalReportID: resolvedOriginalReportID, + draftMessage: resolvedDraftMessage, + selection: resolvedSelection, + anchor: resolvedAnchor, + } = useReportActionContextMenuData({ reportID, reportActionID, originalReportID, draftMessage, selection: '', - type: CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, anchor, }); @@ -129,7 +168,7 @@ function MiniReportActionContextMenu() { originalReportID, }, reportAction: { - reportActionID: data.reportAction?.reportActionID, + reportActionID: reportAction?.reportActionID, draftMessage, }, callbacks: { @@ -144,80 +183,80 @@ function MiniReportActionContextMenu() { }); }; - const reportAction = data.reportAction; - const currentUserAccountID = data.currentUserPersonalDetails?.accountID ?? 0; - const {translate, disabledActionIDs} = data; + const currentUserAccountID = currentUserPersonalDetails?.accountID ?? 0; const isDisabled = (id: string) => disabledActionIDs.has(id); const showReplyInThread = + !isDisabled(ACTION_IDS.REPLY_IN_THREAD) && shouldShowReplyInThreadAction({ - reportAction: data.reportAction, - reportID: data.reportID, - isThreadReportParentAction: data.isThreadReportParentAction, - isArchivedRoom: data.isArchivedRoom, - }) && !isDisabled(ACTION_IDS.REPLY_IN_THREAD); - const showMarkAsUnread = shouldShowMarkAsUnreadForReportAction({reportAction: data.reportAction}) && !isDisabled(ACTION_IDS.MARK_AS_UNREAD); - const showExplain = shouldShowExplainAction({reportAction: data.reportAction, isArchivedRoom: data.isArchivedRoom}) && !isDisabled(ACTION_IDS.EXPLAIN); - const showEdit = - shouldShowEditAction({reportAction: data.reportAction, isArchivedRoom: data.isArchivedRoom, isChronosReport: data.isChronosReport, moneyRequestAction: data.moneyRequestAction}) && - !isDisabled(ACTION_IDS.EDIT); + reportAction, + reportID: resolvedReportID, + isThreadReportParentAction, + isArchivedRoom, + }); + const showMarkAsUnread = !isDisabled(ACTION_IDS.MARK_AS_UNREAD) && shouldShowMarkAsUnreadForReportAction({reportAction}); + const showExplain = !isDisabled(ACTION_IDS.EXPLAIN) && shouldShowExplainAction({reportAction, isArchivedRoom}); + const showEdit = !isDisabled(ACTION_IDS.EDIT) && shouldShowEditAction({reportAction, isArchivedRoom, isChronosReport, moneyRequestAction}); const showUnhold = + !isDisabled(ACTION_IDS.UNHOLD) && shouldShowUnholdAction({ - moneyRequestReport: data.moneyRequestReport, - moneyRequestAction: data.moneyRequestAction, - moneyRequestPolicy: data.moneyRequestPolicy, - areHoldRequirementsMet: data.areHoldRequirementsMet, - iouTransaction: data.iouTransaction, - }) && !isDisabled(ACTION_IDS.UNHOLD); + moneyRequestReport, + moneyRequestAction, + moneyRequestPolicy, + areHoldRequirementsMet, + iouTransaction, + }); const showHold = + !isDisabled(ACTION_IDS.HOLD) && shouldShowHoldAction({ - moneyRequestReport: data.moneyRequestReport, - moneyRequestAction: data.moneyRequestAction, - moneyRequestPolicy: data.moneyRequestPolicy, - areHoldRequirementsMet: data.areHoldRequirementsMet, - iouTransaction: data.iouTransaction, - }) && !isDisabled(ACTION_IDS.HOLD); + moneyRequestReport, + moneyRequestAction, + moneyRequestPolicy, + areHoldRequirementsMet, + iouTransaction, + }); const showJoinThread = + !isDisabled(ACTION_IDS.JOIN_THREAD) && shouldShowJoinThreadAction({ - reportAction: data.reportAction, - isArchivedRoom: data.isArchivedRoom, - isThreadReportParentAction: data.isThreadReportParentAction, - isHarvestReport: data.isHarvestReport, - }) && !isDisabled(ACTION_IDS.JOIN_THREAD); + reportAction, + isArchivedRoom, + isThreadReportParentAction, + isHarvestReport, + }); const showLeaveThread = + !isDisabled(ACTION_IDS.LEAVE_THREAD) && shouldShowLeaveThreadAction({ - reportAction: data.reportAction, - isArchivedRoom: data.isArchivedRoom, - isThreadReportParentAction: data.isThreadReportParentAction, - isHarvestReport: data.isHarvestReport, - }) && !isDisabled(ACTION_IDS.LEAVE_THREAD); - const showCopyMessage = shouldShowCopyMessageAction({reportAction: data.reportAction}) && !isDisabled(ACTION_IDS.COPY_MESSAGE); - const showCopyLink = shouldShowCopyLinkAction({reportAction: data.reportAction, menuTarget: data.anchor}) && !isDisabled(ACTION_IDS.COPY_LINK); - const showFlagAsOffensive = - shouldShowFlagAsOffensiveAction({reportAction: data.reportAction, isArchivedRoom: data.isArchivedRoom, isChronosReport: data.isChronosReport, reportID: data.reportID}) && - !isDisabled(ACTION_IDS.FLAG_AS_OFFENSIVE); - const showDownload = shouldShowDownloadAction({reportAction: data.reportAction, isOffline: data.isOffline}) && !isDisabled(ACTION_IDS.DOWNLOAD); + reportAction, + isArchivedRoom, + isThreadReportParentAction, + isHarvestReport, + }); + const showCopyMessage = !isDisabled(ACTION_IDS.COPY_MESSAGE) && shouldShowCopyMessageAction({reportAction}); + const showCopyLink = !isDisabled(ACTION_IDS.COPY_LINK) && shouldShowCopyLinkAction({reportAction, menuTarget: resolvedAnchor}); + const showFlagAsOffensive = !isDisabled(ACTION_IDS.FLAG_AS_OFFENSIVE) && shouldShowFlagAsOffensiveAction({reportAction, isArchivedRoom, isChronosReport, reportID: resolvedReportID}); + const showDownload = !isDisabled(ACTION_IDS.DOWNLOAD) && shouldShowDownloadAction({reportAction, isOffline}); const showDelete = + !isDisabled(ACTION_IDS.DELETE) && shouldShowDeleteAction({ - reportAction: data.reportAction, - isArchivedRoom: data.isArchivedRoom, - isChronosReport: data.isChronosReport, - reportID: data.reportID, - moneyRequestAction: data.moneyRequestAction, - iouTransaction: data.iouTransaction, - transactions: data.transactions, - childReportActions: data.childReportActions, - }) && !isDisabled(ACTION_IDS.DELETE); + reportAction, + isArchivedRoom, + isChronosReport, + reportID: resolvedReportID, + moneyRequestAction, + iouTransaction, + transactions, + childReportActions, + }); const allVisibleActions: ContextMenuAction[] = []; if (reportAction) { if (showReplyInThread) { allVisibleActions.push( createReplyInThreadAction({ - childReport: data.childReport, + childReport, reportAction, - originalReport: data.originalReport, + originalReport, currentUserAccountID, hideAndRun, translate, @@ -228,8 +267,8 @@ function MiniReportActionContextMenu() { if (showMarkAsUnread) { allVisibleActions.push( createMarkAsUnreadAction({ - reportID: data.reportID, - reportActions: data.reportActions, + reportID: resolvedReportID, + reportActions: reportActionsMap, reportAction, currentUserAccountID, hideAndRun, @@ -242,10 +281,10 @@ function MiniReportActionContextMenu() { if (showExplain) { allVisibleActions.push( createExplainAction({ - childReport: data.childReport, - originalReport: data.originalReport, + childReport, + originalReport, reportAction, - currentUserPersonalDetails: data.currentUserPersonalDetails, + currentUserPersonalDetails, hideAndRun, translate, conciergeIcon: icons.Concierge, @@ -255,11 +294,11 @@ function MiniReportActionContextMenu() { if (showEdit) { allVisibleActions.push( createEditAction({ - reportID: data.reportID, + reportID: resolvedReportID, reportAction, - moneyRequestAction: data.moneyRequestAction, - draftMessage: data.draftMessage, - introSelected: data.introSelected, + moneyRequestAction, + draftMessage: resolvedDraftMessage, + introSelected, hideAndRun, translate, pencilIcon: icons.Pencil, @@ -269,9 +308,9 @@ function MiniReportActionContextMenu() { if (showUnhold) { allVisibleActions.push( createUnholdAction({ - moneyRequestAction: data.moneyRequestAction, - isDelegateAccessRestricted: data.isDelegateAccessRestricted, - showDelegateNoAccessModal: data.showDelegateNoAccessModal, + moneyRequestAction, + isDelegateAccessRestricted, + showDelegateNoAccessModal, hideAndRun, translate, stopwatchIcon: icons.Stopwatch, @@ -281,9 +320,9 @@ function MiniReportActionContextMenu() { if (showHold) { allVisibleActions.push( createHoldAction({ - moneyRequestAction: data.moneyRequestAction, - isDelegateAccessRestricted: data.isDelegateAccessRestricted, - showDelegateNoAccessModal: data.showDelegateNoAccessModal, + moneyRequestAction, + isDelegateAccessRestricted, + showDelegateNoAccessModal, hideAndRun, translate, stopwatchIcon: icons.Stopwatch, @@ -291,49 +330,47 @@ function MiniReportActionContextMenu() { ); } if (showJoinThread) { - allVisibleActions.push(createJoinThreadAction({reportAction, originalReport: data.originalReport, currentUserAccountID, hideAndRun, translate, bellIcon: icons.Bell})); + allVisibleActions.push(createJoinThreadAction({reportAction, originalReport, currentUserAccountID, hideAndRun, translate, bellIcon: icons.Bell})); } if (showLeaveThread) { - allVisibleActions.push(createLeaveThreadAction({reportAction, originalReport: data.originalReport, currentUserAccountID, hideAndRun, translate, exitIcon: icons.Exit})); + allVisibleActions.push(createLeaveThreadAction({reportAction, originalReport, currentUserAccountID, hideAndRun, translate, exitIcon: icons.Exit})); } if (showCopyMessage) { allVisibleActions.push( createCopyMessageAction({ reportAction, - transaction: data.transaction, - selection: data.selection, - report: data.report, - card: data.card, - originalReport: data.originalReport, - isHarvestReport: data.isHarvestReport, - isTryNewDotNVPDismissed: data.isTryNewDotNVPDismissed, - movedFromReport: data.movedFromReport, - movedToReport: data.movedToReport, - childReport: data.childReport, - policy: data.policy, - getLocalDateFromDatetime: data.getLocalDateFromDatetime, - policyTags: data.policyTags, + transaction, + selection: resolvedSelection, + report, + card, + originalReport, + isHarvestReport, + isTryNewDotNVPDismissed, + movedFromReport, + movedToReport, + childReport, + policy, + getLocalDateFromDatetime, + policyTags, translate, - harvestReport: data.harvestReport, - currentUserPersonalDetails: data.currentUserPersonalDetails, + harvestReport, + currentUserPersonalDetails, copyIcon: icons.Copy, checkmarkIcon: icons.Checkmark, }), ); } if (showCopyLink) { - allVisibleActions.push(createCopyLinkAction({reportAction, originalReportID: data.originalReportID, translate, linkCopyIcon: icons.LinkCopy, checkmarkIcon: icons.Checkmark})); + allVisibleActions.push(createCopyLinkAction({reportAction, originalReportID: resolvedOriginalReportID, translate, linkCopyIcon: icons.LinkCopy, checkmarkIcon: icons.Checkmark})); } if (showFlagAsOffensive) { - allVisibleActions.push(createFlagAsOffensiveAction({reportID: data.reportID, reportAction, hideAndRun, translate, flagIcon: icons.Flag})); + allVisibleActions.push(createFlagAsOffensiveAction({reportID: resolvedReportID, reportAction, hideAndRun, translate, flagIcon: icons.Flag})); } if (showDownload) { - allVisibleActions.push(createDownloadAction({reportAction, encryptedAuthToken: data.encryptedAuthToken, download: data.download, translate, downloadIcon: icons.Download})); + allVisibleActions.push(createDownloadAction({reportAction, encryptedAuthToken, download, translate, downloadIcon: icons.Download})); } if (showDelete) { - allVisibleActions.push( - createDeleteAction({reportID: data.reportID, reportAction, moneyRequestAction: data.moneyRequestAction, hideAndRun, translate, trashcanIcon: icons.Trashcan}), - ); + allVisibleActions.push(createDeleteAction({reportID: resolvedReportID, reportAction, moneyRequestAction, hideAndRun, translate, trashcanIcon: icons.Trashcan})); } } @@ -341,15 +378,15 @@ function MiniReportActionContextMenu() { const displayedActions = needsOverflow ? allVisibleActions.slice(0, CONST.MINI_CONTEXT_MENU_MAX_ITEMS - 1) : allVisibleActions; const emojiData = createEmojiReactionData({ - reportID: data.reportID, - reportAction: data.reportAction, + reportID: resolvedReportID, + reportAction, currentUserAccountID, openContextMenu: () => miniActions.keepOpen(), setIsEmojiPickerActive, hideAndRun, }); - const hasEmoji = shouldShowEmojiReaction({reportAction: data.reportAction}) && !!emojiData.reportAction && !!emojiData.reportActionID; + const hasEmoji = shouldShowEmojiReaction({reportAction}) && !!emojiData.reportAction && !!emojiData.reportActionID; if (!rowMeasurements) { return null; diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx index 4697d7f7481c..3b69c2a82527 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx @@ -67,13 +67,53 @@ function PopoverReportActionContent({ const {windowWidth} = useWindowDimensions(); const icons = useMemoizedLazyExpensifyIcons(CONTEXT_MENU_ICON_NAMES); - const data = useReportActionContextMenuData({ + const { + report, + reportAction, + reportActions: reportActionsMap, + originalReport, + childReport, + childReportActions, + policy, + policyTags, + moneyRequestAction, + moneyRequestReport, + moneyRequestPolicy, + iouTransaction, + transaction, + card, + currentUserPersonalDetails, + encryptedAuthToken, + isArchivedRoom, + isChronosReport, + isThreadReportParentAction, + isOffline, + isHarvestReport, + isTryNewDotNVPDismissed, + isDelegateAccessRestricted, + areHoldRequirementsMet, + isDebugModeEnabled, + transactions, + introSelected, + movedFromReport, + movedToReport, + harvestReport, + download, + disabledActionIDs, + showDelegateNoAccessModal, + translate, + getLocalDateFromDatetime, + reportID: resolvedReportID, + originalReportID: resolvedOriginalReportID, + draftMessage: resolvedDraftMessage, + selection: resolvedSelection, + anchor, + } = useReportActionContextMenuData({ reportID, reportActionID, originalReportID, draftMessage: draftMessage ?? '', selection: selection ?? '', - type: CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, anchor: {current: contextMenuTargetNode ?? null}, }); @@ -88,7 +128,7 @@ function PopoverReportActionContent({ originalReportID, }, reportAction: { - reportActionID: data.reportAction?.reportActionID, + reportActionID: reportAction?.reportActionID, draftMessage, }, callbacks: { @@ -102,71 +142,72 @@ function PopoverReportActionContent({ }); }; - const currentUserAccountID = data.currentUserPersonalDetails?.accountID ?? 0; - const {translate, disabledActionIDs} = data; + const currentUserAccountID = currentUserPersonalDetails?.accountID ?? 0; const isDisabled = (id: string) => disabledActionIDs.has(id); const showReplyInThread = + !isDisabled(ACTION_IDS.REPLY_IN_THREAD) && shouldShowReplyInThreadAction({ - reportAction: data.reportAction, - reportID: data.reportID, - isThreadReportParentAction: data.isThreadReportParentAction, - isArchivedRoom: data.isArchivedRoom, - }) && !isDisabled(ACTION_IDS.REPLY_IN_THREAD); - const showMarkAsUnread = shouldShowMarkAsUnreadForReportAction({reportAction: data.reportAction}) && !isDisabled(ACTION_IDS.MARK_AS_UNREAD); - const showExplain = shouldShowExplainAction({reportAction: data.reportAction, isArchivedRoom: data.isArchivedRoom}) && !isDisabled(ACTION_IDS.EXPLAIN); - const showEdit = - shouldShowEditAction({reportAction: data.reportAction, isArchivedRoom: data.isArchivedRoom, isChronosReport: data.isChronosReport, moneyRequestAction: data.moneyRequestAction}) && - !isDisabled(ACTION_IDS.EDIT); + reportAction, + reportID: resolvedReportID, + isThreadReportParentAction, + isArchivedRoom, + }); + const showMarkAsUnread = !isDisabled(ACTION_IDS.MARK_AS_UNREAD) && shouldShowMarkAsUnreadForReportAction({reportAction}); + const showExplain = !isDisabled(ACTION_IDS.EXPLAIN) && shouldShowExplainAction({reportAction, isArchivedRoom}); + const showEdit = !isDisabled(ACTION_IDS.EDIT) && shouldShowEditAction({reportAction, isArchivedRoom, isChronosReport, moneyRequestAction}); const showUnhold = + !isDisabled(ACTION_IDS.UNHOLD) && shouldShowUnholdAction({ - moneyRequestReport: data.moneyRequestReport, - moneyRequestAction: data.moneyRequestAction, - moneyRequestPolicy: data.moneyRequestPolicy, - areHoldRequirementsMet: data.areHoldRequirementsMet, - iouTransaction: data.iouTransaction, - }) && !isDisabled(ACTION_IDS.UNHOLD); + moneyRequestReport, + moneyRequestAction, + moneyRequestPolicy, + areHoldRequirementsMet, + iouTransaction, + }); const showHold = + !isDisabled(ACTION_IDS.HOLD) && shouldShowHoldAction({ - moneyRequestReport: data.moneyRequestReport, - moneyRequestAction: data.moneyRequestAction, - moneyRequestPolicy: data.moneyRequestPolicy, - areHoldRequirementsMet: data.areHoldRequirementsMet, - iouTransaction: data.iouTransaction, - }) && !isDisabled(ACTION_IDS.HOLD); + moneyRequestReport, + moneyRequestAction, + moneyRequestPolicy, + areHoldRequirementsMet, + iouTransaction, + }); const showJoinThread = + !isDisabled(ACTION_IDS.JOIN_THREAD) && shouldShowJoinThreadAction({ - reportAction: data.reportAction, - isArchivedRoom: data.isArchivedRoom, - isThreadReportParentAction: data.isThreadReportParentAction, - isHarvestReport: data.isHarvestReport, - }) && !isDisabled(ACTION_IDS.JOIN_THREAD); + reportAction, + isArchivedRoom, + isThreadReportParentAction, + isHarvestReport, + }); const showLeaveThread = + !isDisabled(ACTION_IDS.LEAVE_THREAD) && shouldShowLeaveThreadAction({ - reportAction: data.reportAction, - isArchivedRoom: data.isArchivedRoom, - isThreadReportParentAction: data.isThreadReportParentAction, - isHarvestReport: data.isHarvestReport, - }) && !isDisabled(ACTION_IDS.LEAVE_THREAD); - const showCopyMessage = shouldShowCopyMessageAction({reportAction: data.reportAction}) && !isDisabled(ACTION_IDS.COPY_MESSAGE); - const showCopyLink = shouldShowCopyLinkAction({reportAction: data.reportAction, menuTarget: data.anchor}) && !isDisabled(ACTION_IDS.COPY_LINK); - const showFlagAsOffensive = - shouldShowFlagAsOffensiveAction({reportAction: data.reportAction, isArchivedRoom: data.isArchivedRoom, isChronosReport: data.isChronosReport, reportID: data.reportID}) && - !isDisabled(ACTION_IDS.FLAG_AS_OFFENSIVE); - const showDownload = shouldShowDownloadAction({reportAction: data.reportAction, isOffline: data.isOffline}) && !isDisabled(ACTION_IDS.DOWNLOAD); - const showDebug = shouldShowDebugAction({isDebugModeEnabled: data.isDebugModeEnabled}) && !isDisabled(ACTION_IDS.DEBUG); + reportAction, + isArchivedRoom, + isThreadReportParentAction, + isHarvestReport, + }); + const showCopyMessage = !isDisabled(ACTION_IDS.COPY_MESSAGE) && shouldShowCopyMessageAction({reportAction}); + const showCopyLink = !isDisabled(ACTION_IDS.COPY_LINK) && shouldShowCopyLinkAction({reportAction, menuTarget: anchor}); + const showFlagAsOffensive = !isDisabled(ACTION_IDS.FLAG_AS_OFFENSIVE) && shouldShowFlagAsOffensiveAction({reportAction, isArchivedRoom, isChronosReport, reportID: resolvedReportID}); + const showDownload = !isDisabled(ACTION_IDS.DOWNLOAD) && shouldShowDownloadAction({reportAction, isOffline}); + const showDebug = !isDisabled(ACTION_IDS.DEBUG) && shouldShowDebugAction({isDebugModeEnabled}); const showDelete = + !isDisabled(ACTION_IDS.DELETE) && shouldShowDeleteAction({ - reportAction: data.reportAction, - isArchivedRoom: data.isArchivedRoom, - isChronosReport: data.isChronosReport, - reportID: data.reportID, - moneyRequestAction: data.moneyRequestAction, - iouTransaction: data.iouTransaction, - transactions: data.transactions, - childReportActions: data.childReportActions, - }) && !isDisabled(ACTION_IDS.DELETE); + reportAction, + isArchivedRoom, + isChronosReport, + reportID: resolvedReportID, + moneyRequestAction, + iouTransaction, + transactions, + childReportActions, + }); const overflowAction: ContextMenuAction = { id: 'overflowMenu', @@ -183,16 +224,15 @@ function PopoverReportActionContent({ }; const visibleActions: ContextMenuAction[] = []; - if (!data.reportAction) { + if (!reportAction) { visibleActions.push(overflowAction); } else { - const reportAction = data.reportAction; if (showReplyInThread) { visibleActions.push( createReplyInThreadAction({ - childReport: data.childReport, + childReport, reportAction, - originalReport: data.originalReport, + originalReport, currentUserAccountID, hideAndRun, translate, @@ -203,8 +243,8 @@ function PopoverReportActionContent({ if (showMarkAsUnread) { visibleActions.push( createMarkAsUnreadAction({ - reportID: data.reportID, - reportActions: data.reportActions, + reportID: resolvedReportID, + reportActions: reportActionsMap, reportAction, currentUserAccountID, hideAndRun, @@ -217,10 +257,10 @@ function PopoverReportActionContent({ if (showExplain) { visibleActions.push( createExplainAction({ - childReport: data.childReport, - originalReport: data.originalReport, + childReport, + originalReport, reportAction, - currentUserPersonalDetails: data.currentUserPersonalDetails, + currentUserPersonalDetails, hideAndRun, translate, conciergeIcon: icons.Concierge, @@ -230,11 +270,11 @@ function PopoverReportActionContent({ if (showEdit) { visibleActions.push( createEditAction({ - reportID: data.reportID, + reportID: resolvedReportID, reportAction, - moneyRequestAction: data.moneyRequestAction, - draftMessage: data.draftMessage, - introSelected: data.introSelected, + moneyRequestAction, + draftMessage: resolvedDraftMessage, + introSelected, hideAndRun, translate, pencilIcon: icons.Pencil, @@ -244,9 +284,9 @@ function PopoverReportActionContent({ if (showUnhold) { visibleActions.push( createUnholdAction({ - moneyRequestAction: data.moneyRequestAction, - isDelegateAccessRestricted: data.isDelegateAccessRestricted, - showDelegateNoAccessModal: data.showDelegateNoAccessModal, + moneyRequestAction, + isDelegateAccessRestricted, + showDelegateNoAccessModal, hideAndRun, translate, stopwatchIcon: icons.Stopwatch, @@ -256,9 +296,9 @@ function PopoverReportActionContent({ if (showHold) { visibleActions.push( createHoldAction({ - moneyRequestAction: data.moneyRequestAction, - isDelegateAccessRestricted: data.isDelegateAccessRestricted, - showDelegateNoAccessModal: data.showDelegateNoAccessModal, + moneyRequestAction, + isDelegateAccessRestricted, + showDelegateNoAccessModal, hideAndRun, translate, stopwatchIcon: icons.Stopwatch, @@ -266,59 +306,57 @@ function PopoverReportActionContent({ ); } if (showJoinThread) { - visibleActions.push(createJoinThreadAction({reportAction, originalReport: data.originalReport, currentUserAccountID, hideAndRun, translate, bellIcon: icons.Bell})); + visibleActions.push(createJoinThreadAction({reportAction, originalReport, currentUserAccountID, hideAndRun, translate, bellIcon: icons.Bell})); } if (showLeaveThread) { - visibleActions.push(createLeaveThreadAction({reportAction, originalReport: data.originalReport, currentUserAccountID, hideAndRun, translate, exitIcon: icons.Exit})); + visibleActions.push(createLeaveThreadAction({reportAction, originalReport, currentUserAccountID, hideAndRun, translate, exitIcon: icons.Exit})); } if (showCopyMessage) { visibleActions.push( createCopyMessageAction({ reportAction, - transaction: data.transaction, - selection: data.selection, - report: data.report, - card: data.card, - originalReport: data.originalReport, - isHarvestReport: data.isHarvestReport, - isTryNewDotNVPDismissed: data.isTryNewDotNVPDismissed, - movedFromReport: data.movedFromReport, - movedToReport: data.movedToReport, - childReport: data.childReport, - policy: data.policy, - getLocalDateFromDatetime: data.getLocalDateFromDatetime, - policyTags: data.policyTags, + transaction, + selection: resolvedSelection, + report, + card, + originalReport, + isHarvestReport, + isTryNewDotNVPDismissed, + movedFromReport, + movedToReport, + childReport, + policy, + getLocalDateFromDatetime, + policyTags, translate, - harvestReport: data.harvestReport, - currentUserPersonalDetails: data.currentUserPersonalDetails, + harvestReport, + currentUserPersonalDetails, copyIcon: icons.Copy, checkmarkIcon: icons.Checkmark, }), ); } if (showCopyLink) { - visibleActions.push(createCopyLinkAction({reportAction, originalReportID: data.originalReportID, translate, linkCopyIcon: icons.LinkCopy, checkmarkIcon: icons.Checkmark})); + visibleActions.push(createCopyLinkAction({reportAction, originalReportID: resolvedOriginalReportID, translate, linkCopyIcon: icons.LinkCopy, checkmarkIcon: icons.Checkmark})); } if (showFlagAsOffensive) { - visibleActions.push(createFlagAsOffensiveAction({reportID: data.reportID, reportAction, hideAndRun, translate, flagIcon: icons.Flag})); + visibleActions.push(createFlagAsOffensiveAction({reportID: resolvedReportID, reportAction, hideAndRun, translate, flagIcon: icons.Flag})); } if (showDownload) { - visibleActions.push(createDownloadAction({reportAction, encryptedAuthToken: data.encryptedAuthToken, download: data.download, translate, downloadIcon: icons.Download})); + visibleActions.push(createDownloadAction({reportAction, encryptedAuthToken, download, translate, downloadIcon: icons.Download})); } if (showDebug) { - visibleActions.push(createDebugAction({reportID: data.reportID, reportAction, translate, bugIcon: icons.Bug})); + visibleActions.push(createDebugAction({reportID: resolvedReportID, reportAction, translate, bugIcon: icons.Bug})); } if (showDelete) { - visibleActions.push( - createDeleteAction({reportID: data.reportID, reportAction, moneyRequestAction: data.moneyRequestAction, hideAndRun, translate, trashcanIcon: icons.Trashcan}), - ); + visibleActions.push(createDeleteAction({reportID: resolvedReportID, reportAction, moneyRequestAction, hideAndRun, translate, trashcanIcon: icons.Trashcan})); } visibleActions.push(overflowAction); } const emojiData = createEmojiReactionData({ - reportID: data.reportID, - reportAction: data.reportAction, + reportID: resolvedReportID, + reportAction, currentUserAccountID, openContextMenu: () => setLocalShouldKeepOpen(true), setIsEmojiPickerActive: onEmojiPickerToggle, @@ -332,7 +370,7 @@ function PopoverReportActionContent({ isActive: shouldEnableArrowNavigation, }); - const hasEmoji = shouldShowEmojiReaction({reportAction: data.reportAction}); + const hasEmoji = shouldShowEmojiReaction({reportAction}); const wrapperStyle = StyleUtils.getReportActionContextMenuStyles(false, shouldUseNarrowLayout); return ( diff --git a/src/pages/inbox/report/ContextMenu/actions/overflowMenuAction.ts b/src/pages/inbox/report/ContextMenu/actions/overflowMenuAction.ts index 55bb768e818e..01edbe18215d 100644 --- a/src/pages/inbox/report/ContextMenu/actions/overflowMenuAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/overflowMenuAction.ts @@ -1,31 +1,25 @@ -import type {RefObject} from 'react'; -import type {GestureResponderEvent, View} from 'react-native'; +import type {GestureResponderEvent} from 'react-native'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import CONST from '@src/CONST'; import type IconAsset from '@src/types/utils/IconAsset'; import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; -type OverflowMenuDescriptor = ContextMenuAction & { - buttonRef: RefObject; -}; - type OverflowMenuActionParams = BaseContextMenuActionParams & { - openOverflowMenu: (event: GestureResponderEvent | MouseEvent, anchorRef: RefObject) => void; + openOverflowMenu: (event: GestureResponderEvent | MouseEvent) => void; openContextMenu: () => void; threeDotsIcon: IconAsset; }; -function createOverflowMenuAction({openOverflowMenu, openContextMenu, translate, threeDotsIcon}: OverflowMenuActionParams, threeDotRef: RefObject): OverflowMenuDescriptor { +function createOverflowMenuAction({openOverflowMenu, openContextMenu, translate, threeDotsIcon}: OverflowMenuActionParams): ContextMenuAction { return { id: 'overflowMenu', icon: threeDotsIcon, text: translate('reportActionContextMenu.menu'), isAnonymousAction: true, shouldPreventDefaultFocusOnPress: false, - buttonRef: threeDotRef, onPress: (event) => interceptAnonymousUser(() => { - openOverflowMenu(event as GestureResponderEvent | MouseEvent, threeDotRef); + openOverflowMenu(event as GestureResponderEvent | MouseEvent); openContextMenu(); }, true), sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.MENU, diff --git a/src/pages/inbox/report/ContextMenu/useReportActionContextMenuData.ts b/src/pages/inbox/report/ContextMenu/useReportActionContextMenuData.ts index 90abf7a01a38..60577bc1aa0c 100644 --- a/src/pages/inbox/report/ContextMenu/useReportActionContextMenuData.ts +++ b/src/pages/inbox/report/ContextMenu/useReportActionContextMenuData.ts @@ -3,7 +3,6 @@ import type {OnyxEntry} from 'react-native-onyx'; import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; import {useSession} from '@components/OnyxListItemProvider'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; -import useEnvironment from '@hooks/useEnvironment'; import useGetExpensifyCardFromReportAction from '@hooks/useGetExpensifyCardFromReportAction'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -22,14 +21,13 @@ import { isArchivedNonExpenseReport, isChatThread, isHarvestCreatedExpenseReport, - isUnread, isInvoiceReport as ReportUtilsIsInvoiceReport, isMoneyRequest as ReportUtilsIsMoneyRequest, isMoneyRequestReport as ReportUtilsIsMoneyRequestReport, isTrackExpenseReport as ReportUtilsIsTrackExpenseReport, } from '@libs/ReportUtils'; import {RESTRICTED_READONLY_ACTION_IDS} from '@pages/inbox/report/ContextMenu/actions/actionConfig'; -import type {ContextMenuAnchor, ContextMenuType} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import type {ContextMenuAnchor} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {OriginalMessageIOU, ReportAction} from '@src/types/onyx'; @@ -43,7 +41,6 @@ type UseContextMenuDataParams = { originalReportID: string | undefined; draftMessage: string; selection: string; - type: ContextMenuType; anchor: RefObject | undefined; }; @@ -51,16 +48,14 @@ type UseContextMenuDataParams = { * Aggregates all Onyx data and derived state needed for context menus. * Consumed by both PopoverReportActionContent (long-press menu) and MiniReportActionContextMenu (hover menu). */ -function useReportActionContextMenuData({reportID, reportActionID, originalReportID, draftMessage, selection, type, anchor}: UseContextMenuDataParams) { +function useReportActionContextMenuData({reportID, reportActionID, originalReportID, draftMessage, selection, anchor}: UseContextMenuDataParams) { const {translate, getLocalDateFromDatetime} = useLocalize(); const {isOffline} = useNetwork(); - const {isProduction} = useEnvironment(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const encryptedAuthToken = useSession()?.encryptedAuthToken ?? ''; const {isDelegateAccessRestricted} = useDelegateNoAccessState(); const {showDelegateNoAccessModal} = useDelegateNoAccessActions(); - const [betas] = useOnyx(ONYXKEYS.BETAS); const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, { canEvict: false, selector: withDEWRoutedActionsObject, @@ -109,8 +104,6 @@ function useReportActionContextMenuData({reportID, reportActionID, originalRepor const isChronosReport = chatIncludesChronosWithID(originalReportID); const isArchivedRoom = isArchivedNonExpenseReport(originalReport, isOriginalReportArchived); - const isPinnedChat = !!report?.isPinned; - const isUnreadChat = isUnread(report, undefined, isOriginalReportArchived); const isThreadReportParentAction = isChatThread(report) && report?.parentReportActionID === reportAction?.reportActionID; const isMoneyRequestReport = ReportUtilsIsMoneyRequestReport(childReport); @@ -166,17 +159,13 @@ function useReportActionContextMenuData({reportID, reportActionID, originalRepor encryptedAuthToken, isArchivedRoom, isChronosReport, - isPinnedChat, - isUnreadChat, isThreadReportParentAction, isOffline: !!isOffline, - isProduction, isHarvestReport, isTryNewDotNVPDismissed, isDelegateAccessRestricted: !!isDelegateAccessRestricted, areHoldRequirementsMet, isDebugModeEnabled, - betas, transactions, introSelected, movedFromReport, @@ -187,7 +176,6 @@ function useReportActionContextMenuData({reportID, reportActionID, originalRepor showDelegateNoAccessModal, translate, getLocalDateFromDatetime, - type, reportID, originalReportID, draftMessage, From d046575cacf92a161ea145b40799bab78d8e79e1 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 4 Mar 2026 09:25:46 -0800 Subject: [PATCH 52/88] Fix jsx-a11y mouse-events-have-key-events; use relative imports - Add onFocusCapture/onBlurCapture to MiniReportActionContextMenu div (paired with onMouseEnter/onMouseLeave for keyboard accessibility) - Use relative imports in useReportActionContextMenuData Made-with: Cursor --- .../ContextMenu/MiniReportActionContextMenu/index.tsx | 10 +++++++++- .../ContextMenu/useReportActionContextMenuData.ts | 4 ++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index fde71c0ee625..c54d7d27462f 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -394,11 +394,19 @@ function MiniReportActionContextMenu() { const wrapperStyle = StyleUtils.getReportActionContextMenuStyles(true, shouldUseNarrowLayout); + const handleBlur = (e: React.FocusEvent) => { + if (e.relatedTarget && e.currentTarget.contains(e.relatedTarget)) { + return; + } + hideMiniContextMenu(); + }; + return createPortal( - // eslint-disable-next-line jsx-a11y/mouse-events-have-key-events
hideMiniContextMenu()} + onFocusCapture={cancelHide} + onBlurCapture={handleBlur} data-selection-scraper-hidden-element={isVisible} style={{ position: 'fixed', diff --git a/src/pages/inbox/report/ContextMenu/useReportActionContextMenuData.ts b/src/pages/inbox/report/ContextMenu/useReportActionContextMenuData.ts index 60577bc1aa0c..bdfee6f9bf88 100644 --- a/src/pages/inbox/report/ContextMenu/useReportActionContextMenuData.ts +++ b/src/pages/inbox/report/ContextMenu/useReportActionContextMenuData.ts @@ -26,12 +26,12 @@ import { isMoneyRequestReport as ReportUtilsIsMoneyRequestReport, isTrackExpenseReport as ReportUtilsIsTrackExpenseReport, } from '@libs/ReportUtils'; -import {RESTRICTED_READONLY_ACTION_IDS} from '@pages/inbox/report/ContextMenu/actions/actionConfig'; -import type {ContextMenuAnchor} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {OriginalMessageIOU, ReportAction} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import {RESTRICTED_READONLY_ACTION_IDS} from './actions/actionConfig'; +import type {ContextMenuAnchor} from './ReportActionContextMenu'; const EMPTY_SET = new Set(); From 87e6a1a95c542c53e3c977dc41f9acfef772aac3 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 4 Mar 2026 09:55:35 -0800 Subject: [PATCH 53/88] chore: reduce lint warning budget from 353 to 334 After merging main, the combined warning count dropped to 334. Made-with: Cursor --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0ce4106f493d..4fc201764786 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "perf-test": "NODE_OPTIONS=--experimental-vm-modules npx reassure", "typecheck": "NODE_OPTIONS=--max_old_space_size=8192 tsc", "typecheck-tsgo": "tsgo --project tsconfig.tsgo.json", - "lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=353 --cache --cache-location=node_modules/.cache/eslint --cache-strategy content --concurrency=auto", + "lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=334 --cache --cache-location=node_modules/.cache/eslint --cache-strategy content --concurrency=auto", "lint-changed": "NODE_OPTIONS=--max_old_space_size=8192 ./scripts/lintChanged.sh", "check-lazy-loading": "ts-node scripts/checkLazyLoading.ts", "lint-watch": "npx eslint-watch --watch --changed", From 24fa4b5dd86d7a8f5c0ee42727bff7d6f25a8807 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 4 Mar 2026 10:44:50 -0800 Subject: [PATCH 54/88] fix: resolve CI failures (lint, prettier, test) - Use CONST.DEFAULT_NUMBER_ID for currentUserAccountID default - Run prettier on MiniContextMenuItem, MiniQuickEmojiReactions, actionConfig - Fix indentation in MiniReportActionContextMenu - Export copyMessageToClipboard and update test to use new API (ContextMenuActions module was removed in this branch) Made-with: Cursor --- src/components/MiniContextMenuItem.tsx | 10 +--- .../Reactions/MiniQuickEmojiReactions.tsx | 2 +- .../MiniReportActionContextMenu/index.tsx | 2 +- .../PopoverReportActionContent.tsx | 2 +- .../ContextMenu/actions/actionConfig.ts | 2 +- .../ContextMenu/actions/copyMessageAction.ts | 2 +- .../unit/ContextMenuActionsCopyMessageTest.ts | 47 ++++++++----------- 7 files changed, 26 insertions(+), 41 deletions(-) diff --git a/src/components/MiniContextMenuItem.tsx b/src/components/MiniContextMenuItem.tsx index 268ed3d37c1b..da13c219fb30 100644 --- a/src/components/MiniContextMenuItem.tsx +++ b/src/components/MiniContextMenuItem.tsx @@ -48,15 +48,7 @@ type MiniContextMenuItemProps = WithSentryLabel & { * Component that renders a mini context menu item with a * pressable. Also renders a tooltip when hovering the item. */ -function MiniContextMenuItem({ - tooltipText, - onPress, - children, - isDelayButtonStateComplete = true, - shouldPreventDefaultFocusOnPress = true, - ref, - sentryLabel, -}: MiniContextMenuItemProps) { +function MiniContextMenuItem({tooltipText, onPress, children, isDelayButtonStateComplete = true, shouldPreventDefaultFocusOnPress = true, ref, sentryLabel}: MiniContextMenuItemProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); return ( diff --git a/src/components/Reactions/MiniQuickEmojiReactions.tsx b/src/components/Reactions/MiniQuickEmojiReactions.tsx index 9c5679906ed2..abf12511e163 100644 --- a/src/components/Reactions/MiniQuickEmojiReactions.tsx +++ b/src/components/Reactions/MiniQuickEmojiReactions.tsx @@ -1,8 +1,8 @@ import React, {useCallback, useRef} from 'react'; import {View} from 'react-native'; import type {Emoji} from '@assets/emojis/types'; -import MiniContextMenuItem from '@components/MiniContextMenuItem'; import Icon from '@components/Icon'; +import MiniContextMenuItem from '@components/MiniContextMenuItem'; import Text from '@components/Text'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index c54d7d27462f..c7fd576130e2 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -183,7 +183,7 @@ function MiniReportActionContextMenu() { }); }; - const currentUserAccountID = currentUserPersonalDetails?.accountID ?? 0; + const currentUserAccountID = currentUserPersonalDetails?.accountID ?? CONST.DEFAULT_NUMBER_ID; const isDisabled = (id: string) => disabledActionIDs.has(id); diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx index 3b69c2a82527..afd51961633b 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx @@ -142,7 +142,7 @@ function PopoverReportActionContent({ }); }; - const currentUserAccountID = currentUserPersonalDetails?.accountID ?? 0; + const currentUserAccountID = currentUserPersonalDetails?.accountID ?? CONST.DEFAULT_NUMBER_ID; const isDisabled = (id: string) => disabledActionIDs.has(id); diff --git a/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts b/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts index 923ef8d33548..862975a6c046 100644 --- a/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts +++ b/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts @@ -2,8 +2,8 @@ import type {GestureResponderEvent} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type {LocalizedTranslate} from '@components/LocaleContextProvider'; -import type IconAsset from '@src/types/utils/IconAsset'; import type {ReportAction} from '@src/types/onyx'; +import type IconAsset from '@src/types/utils/IconAsset'; const CONTEXT_MENU_ICON_NAMES = [ 'Bell', diff --git a/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.ts b/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.ts index 60ad03b5e6b7..d7dbc4a7ffde 100644 --- a/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.ts @@ -550,4 +550,4 @@ function createCopyMessageAction({translate, copyIcon, checkmarkIcon, ...clipboa } export default createCopyMessageAction; -export {shouldShowCopyMessageAction}; +export {shouldShowCopyMessageAction, copyMessageToClipboard}; diff --git a/tests/unit/ContextMenuActionsCopyMessageTest.ts b/tests/unit/ContextMenuActionsCopyMessageTest.ts index 70ef7ee7b5c5..f7c0f5824dde 100644 --- a/tests/unit/ContextMenuActionsCopyMessageTest.ts +++ b/tests/unit/ContextMenuActionsCopyMessageTest.ts @@ -1,6 +1,8 @@ import Clipboard from '@libs/Clipboard'; import getClipboardText from '@libs/Clipboard/getClipboardText'; +import {copyMessageToClipboard} from '@pages/inbox/report/ContextMenu/actions/copyMessageAction'; import CONST from '@src/CONST'; +import type {ReportAction} from '@src/types/onyx'; jest.mock( 'expo-web-browser', @@ -36,32 +38,31 @@ const mockClipboard = Clipboard as { }; const mockGetClipboardText = getClipboardText as jest.Mock; -type ContextMenuAction = { - sentryLabel?: string; - onPress?: (closePopover: boolean, payload: Record) => void; -}; - -// eslint-disable-next-line @typescript-eslint/no-var-requires -const {default: ContextMenuActions} = require('@pages/inbox/report/ContextMenu/ContextMenuActions') as {default: ContextMenuAction[]}; - -const copyMessageAction = ContextMenuActions.find((action) => action.sentryLabel === CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_MESSAGE); - -const createPayload = (selection: string): Record => ({ +const createParams = (selection: string) => ({ reportAction: { actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, message: [{html: selection}], - }, + } as unknown as ReportAction, + transaction: undefined, selection, - report: {}, - originalReport: {}, + report: undefined, + card: undefined, + originalReport: undefined, + isHarvestReport: false, + isTryNewDotNVPDismissed: false, + movedFromReport: undefined, + movedToReport: undefined, + childReport: undefined, + policy: undefined, getLocalDateFromDatetime: jest.fn(), - policyTags: {}, - translate: (translateKey: string) => translateKey, + policyTags: undefined, + translate: ((translateKey: string) => translateKey) as unknown as Parameters[0]['translate'], + harvestReport: undefined, currentUserPersonalDetails: { accountID: 1, login: 'user@expensify.com', email: 'user@expensify.com', - }, + } as unknown as Parameters[0]['currentUserPersonalDetails'], }); describe('ContextMenuActions copy message', () => { @@ -74,11 +75,7 @@ describe('ContextMenuActions copy message', () => { mockClipboard.canSetHtml.mockReturnValue(false); mockGetClipboardText.mockReturnValue('Expensify'); - if (!copyMessageAction?.onPress) { - throw new Error('Copy message context menu action was not found'); - } - - copyMessageAction.onPress(false, createPayload(selection)); + copyMessageToClipboard(createParams(selection)); expect(mockGetClipboardText).toHaveBeenCalledWith(selection); expect(mockClipboard.setString).toHaveBeenCalledWith('Expensify'); @@ -90,11 +87,7 @@ describe('ContextMenuActions copy message', () => { mockClipboard.canSetHtml.mockReturnValue(true); mockGetClipboardText.mockReturnValue('Expensify'); - if (!copyMessageAction?.onPress) { - throw new Error('Copy message context menu action was not found'); - } - - copyMessageAction.onPress(false, createPayload(selection)); + copyMessageToClipboard(createParams(selection)); expect(mockGetClipboardText).toHaveBeenCalledWith(selection); expect(mockClipboard.setHtml).toHaveBeenCalledWith(selection, 'Expensify'); From 7cf6141f3a67a0cb4eb5f4329610f3a60d231306 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 4 Mar 2026 10:51:40 -0800 Subject: [PATCH 55/88] fix: add sentryLabel to PreRenderer PressableWithoutFeedback Made-with: Cursor --- package-lock.json | 370 ++---------------- src/CONST/index.ts | 1 + .../HTMLRenderers/PreRenderer.tsx | 1 + 3 files changed, 43 insertions(+), 329 deletions(-) diff --git a/package-lock.json b/package-lock.json index 95e48561fd12..c3506138ddec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -470,27 +470,6 @@ "node": ">=6.0.0" } }, - "node_modules/@asamuzakjp/css-color": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", - "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@csstools/css-calc": "^2.1.3", - "@csstools/css-color-parser": "^3.0.9", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "lru-cache": "^10.4.3" - } - }, - "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, "node_modules/@aws-crypto/crc32": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", @@ -4783,121 +4762,6 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "node_modules/@csstools/color-helpers": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", - "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - } - }, - "node_modules/@csstools/css-calc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", - "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/@csstools/css-color-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", - "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/color-helpers": "^5.1.0", - "@csstools/css-calc": "^2.1.4" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/@csstools/css-parser-algorithms": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", - "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/@csstools/css-tokenizer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", - "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "dev": true, @@ -21325,20 +21189,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cssstyle": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", - "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/css-color": "^3.2.0", - "rrweb-cssom": "^0.8.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/csstype": { "version": "3.1.1", "license": "MIT" @@ -21547,47 +21397,6 @@ "dev": true, "license": "BSD-2-Clause" }, - "node_modules/data-urls": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", - "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/data-urls/node_modules/tr46": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", - "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/data-urls/node_modules/whatwg-url": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", - "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tr46": "^5.1.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -26564,30 +26373,6 @@ "node": ">=8.0.0" } }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/http-proxy-agent/node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, "node_modules/http-proxy-middleware": { "version": "2.0.7", "dev": true, @@ -28424,6 +28209,21 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/jest-environment-jsdom/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/jest-environment-jsdom/node_modules/jsdom": { "version": "20.0.3", "dev": true, @@ -28479,6 +28279,32 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/jest-environment-jsdom/node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jest-environment-jsdom/node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/jest-environment-jsdom/node_modules/tr46": { "version": "3.0.0", "dev": true, @@ -28529,47 +28355,6 @@ "node": ">=12" } }, - "node_modules/jest-environment-jsdom/node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/jest-environment-jsdom/node_modules/tough-cookie": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", - "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jest-environment-jsdom/node_modules/tough-cookie/node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/jest-environment-node": { "version": "29.7.0", "license": "MIT", @@ -35944,13 +35729,6 @@ "rock": "dist/src/bin.js" } }, - "node_modules/rrweb-cssom": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", - "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", - "dev": true, - "license": "MIT" - }, "node_modules/run-applescript": { "version": "7.0.0", "dev": true, @@ -38094,26 +37872,6 @@ "node": ">=14.0.0" } }, - "node_modules/tldts": { - "version": "6.1.86", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", - "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tldts-core": "^6.1.86" - }, - "bin": { - "tldts": "bin/cli.js" - } - }, - "node_modules/tldts-core": { - "version": "6.1.86", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", - "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", - "dev": true, - "license": "MIT" - }, "node_modules/tmpl": { "version": "1.0.5", "license": "BSD-3-Clause" @@ -38143,19 +37901,6 @@ "node": ">=6" } }, - "node_modules/tough-cookie": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", - "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tldts": "^6.1.32" - }, - "engines": { - "node": ">=16" - } - }, "node_modules/tr46": { "version": "0.0.3", "license": "MIT" @@ -38957,19 +38702,6 @@ "pbf": "^3.2.1" } }, - "node_modules/w3c-xmlserializer": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", - "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/wait-port": { "version": "0.2.14", "dev": true, @@ -39527,16 +39259,6 @@ "version": "3.6.2", "license": "MIT" }, - "node_modules/whatwg-mimetype": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/whatwg-url": { "version": "5.0.0", "license": "MIT", @@ -39918,16 +39640,6 @@ "node": ">=0.8" } }, - "node_modules/xml-name-validator": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", - "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18" - } - }, "node_modules/xml2js": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.0.tgz", diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 60a350410ce3..84bdc69573ea 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -8350,6 +8350,7 @@ const CONST = { }, HTML_RENDERER: { IMAGE: 'HTMLRenderer-Image', + PRE: 'HTMLRenderer-Pre', }, RECEIPT: { IMAGE: 'Receipt-Image', diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer.tsx index 561e95bfaf5c..8a373b31a518 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer.tsx @@ -67,6 +67,7 @@ function PreRenderer({TDefaultRenderer, onPressIn, onPressOut, onLongPress, ...d shouldUseHapticsOnLongPress role={CONST.ROLE.PRESENTATION} accessibilityLabel={translate('accessibilityHints.preStyledText')} + sentryLabel={CONST.SENTRY_LABEL.HTML_RENDERER.PRE} > From ec1227f6beb7c5d50a5c6a108b1cbda9690913b6 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 4 Mar 2026 12:58:37 -0800 Subject: [PATCH 56/88] refactor: use @gorhom/portal for MiniReportActionContextMenu Replace createPortal with @gorhom/portal, use Animated.View with Reanimated CSS transitions for position animation, Hoverable for mouse events, and constrain the portal host to the report content area so the menu can't appear over the report header. Narrow the MiniContextMenuProvider scope to only wrap components that need it. Made-with: Cursor --- src/CONST/index.ts | 4 + src/pages/inbox/ReportScreen.tsx | 46 ++-- .../MiniReportActionContextMenu/index.tsx | 242 ++++++++++-------- 3 files changed, 171 insertions(+), 121 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 84bdc69573ea..f2f166fa7ce8 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -8287,6 +8287,10 @@ const CONST = { }, }, + PORTAL_HOST_NAMES: { + CONTEXT_MENU: 'contextMenu', + }, + SENTRY_LABEL: { NAVIGATION_TAB_BAR: { EXPENSIFY_LOGO: 'NavigationTabBar-ExpensifyLogo', diff --git a/src/pages/inbox/ReportScreen.tsx b/src/pages/inbox/ReportScreen.tsx index 200c0d0da444..b0f7523d26ca 100644 --- a/src/pages/inbox/ReportScreen.tsx +++ b/src/pages/inbox/ReportScreen.tsx @@ -5,7 +5,7 @@ import type {ViewStyle} from 'react-native'; // We use Animated for all functionality related to wide RHP to make it easier // to interact with react-navigation components (e.g., CardContainer, interpolator), which also use Animated. // eslint-disable-next-line no-restricted-imports -import {Animated, DeviceEventEmitter, InteractionManager, View} from 'react-native'; +import {Animated, DeviceEventEmitter, InteractionManager, StyleSheet, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import DragAndDropProvider from '@components/DragAndDrop/Provider'; @@ -1027,13 +1027,12 @@ function ReportScreen({route, navigation, isInSidePanel = false}: ReportScreenPr )} - - - - {(!report || shouldWaitForTransactions) && } + + {(!report || shouldWaitForTransactions) && } + {!!report && !shouldDisplayMoneyRequestActionsList && !shouldWaitForTransactions ? ( ) : null} - {isCurrentReportLoadedFromOnyx ? ( - - ) : null} - - + + + + + + {isCurrentReportLoadedFromOnyx ? ( + + ) : null} + diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index c7fd576130e2..19989ad4b99f 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -1,11 +1,12 @@ -import React, {useEffect, useRef} from 'react'; +import {Portal} from '@gorhom/portal'; +import React, {useEffect, useRef, useState} from 'react'; import type {RefObject} from 'react'; -import {createPortal} from 'react-dom'; -import {View} from 'react-native'; +import {StyleSheet, View} from 'react-native'; // eslint-disable-next-line no-restricted-imports import type {GestureResponderEvent} from 'react-native'; -import Animated, {Easing, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; +import Animated from 'react-native-reanimated'; import * as ActionSheetAwareScrollView from '@components/ActionSheetAwareScrollView'; +import Hoverable from '@components/Hoverable'; import Icon from '@components/Icon'; import MiniContextMenuItem from '@components/MiniContextMenuItem'; import MiniQuickEmojiReactions from '@components/Reactions/MiniQuickEmojiReactions'; @@ -37,7 +38,6 @@ import useReportActionContextMenuData from '@pages/inbox/report/ContextMenu/useR import CONST from '@src/CONST'; const SLIDE_DURATION = 200; -const OVERSHOOT_EASING = Easing.bezier(0.34, 1.56, 0.64, 1); function MiniReportActionContextMenu() { const { @@ -61,29 +61,41 @@ function MiniReportActionContextMenu() { const icons = useMemoizedLazyExpensifyIcons(CONTEXT_MENU_ICON_NAMES); const threeDotRef = useRef(null); const wasVisibleRef = useRef(false); - - const baseTop = useSharedValue(0); - const baseRight = useSharedValue(0); + const overlayRef = useRef(null); + const menuContainerRef = useRef(null); + const [position, setPosition] = useState<{top: number; right: number} | null>(null); + const [shouldAnimateSlide, setShouldAnimateSlide] = useState(false); useEffect(() => { - if (!rowMeasurements) { + if (!isVisible || !rowMeasurements) { return; } - if (isVisible) { - const targetY = rowMeasurements.top + (displayAsGroup ? -8 : -4); - const targetRight = window.innerWidth - rowMeasurements.right + 4; + const el = overlayRef.current as unknown as HTMLElement | null; + if (!el) { + return; + } - if (wasVisibleRef.current) { - baseTop.set(withTiming(targetY, {duration: SLIDE_DURATION, easing: OVERSHOOT_EASING})); - baseRight.set(withTiming(targetRight, {duration: SLIDE_DURATION})); - } else { - baseTop.set(targetY); - baseRight.set(targetRight); - } + const containerRect = el.getBoundingClientRect(); + const newTop = rowMeasurements.top - containerRect.top + (displayAsGroup ? -8 : -4); + const newRight = containerRect.right - rowMeasurements.right + 4; + + setShouldAnimateSlide(wasVisibleRef.current); + setPosition({top: newTop, right: newRight}); + }, [isVisible, rowMeasurements, displayAsGroup]); + + useEffect(() => { + if (isVisible) { + wasVisibleRef.current = true; + return; } - wasVisibleRef.current = isVisible; - }, [isVisible, rowMeasurements, displayAsGroup, baseTop, baseRight]); + wasVisibleRef.current = false; + const timer = setTimeout(() => { + setPosition(null); + setShouldAnimateSlide(false); + }, 0); + return () => clearTimeout(timer); + }, [isVisible]); useEffect(() => { if (!isVisible) { @@ -98,10 +110,35 @@ function MiniReportActionContextMenu() { }; }, [isVisible, hideMiniContextMenu]); - const positionStyle = useAnimatedStyle(() => ({ - top: baseTop.get(), - right: baseRight.get(), - })); + useEffect(() => { + const el = menuContainerRef.current as unknown as HTMLElement | null; + if (!el) { + return; + } + + const onFocusCapture = () => cancelHide(); + const onBlurCapture = (e: FocusEvent) => { + if (e.relatedTarget && el.contains(e.relatedTarget as Node)) { + return; + } + hideMiniContextMenu(); + }; + + el.addEventListener('focus', onFocusCapture, true); + el.addEventListener('blur', onBlurCapture, true); + return () => { + el.removeEventListener('focus', onFocusCapture, true); + el.removeEventListener('blur', onBlurCapture, true); + }; + }, [cancelHide, hideMiniContextMenu]); + + useEffect(() => { + const el = menuContainerRef.current as unknown as HTMLElement | null; + if (!el) { + return; + } + el.dataset.selectionScraperHiddenElement = String(isVisible); + }, [isVisible]); const { report, @@ -394,87 +431,90 @@ function MiniReportActionContextMenu() { const wrapperStyle = StyleUtils.getReportActionContextMenuStyles(true, shouldUseNarrowLayout); - const handleBlur = (e: React.FocusEvent) => { - if (e.relatedTarget && e.currentTarget.contains(e.relatedTarget)) { - return; - } - hideMiniContextMenu(); - }; + const shouldTransitionPosition = shouldAnimateSlide && isVisible; - return createPortal( -
hideMiniContextMenu()} - onFocusCapture={cancelHide} - onBlurCapture={handleBlur} - data-selection-scraper-hidden-element={isVisible} - style={{ - position: 'fixed', - zIndex: 8, - opacity: isVisible ? 1 : 0, - pointerEvents: isVisible ? 'auto' : 'none', - cursor: 'default', - userSelect: 'none', - transitionProperty: 'opacity', - transitionDuration: '150ms', - transitionTimingFunction: 'ease-in-out', - }} - > - - - {hasEmoji && !!emojiData.reportAction && !!emojiData.reportActionID && ( - - interceptAnonymousUser(() => emojiData.toggleEmojiAndCloseMenu(emoji, existingReactions, preferredSkinTone)) - } - onPressOpenPicker={emojiData.onPressOpenPicker} - onEmojiPickerClosed={emojiData.onEmojiPickerClosed} - reportActionID={emojiData.reportActionID} - reportAction={emojiData.reportAction} - /> - )} - {displayedActions.map((action) => ( - + + + hideMiniContextMenu()} + > + - {({hovered, pressed}) => ( - + interceptAnonymousUser(() => emojiData.toggleEmojiAndCloseMenu(emoji, existingReactions, preferredSkinTone)) + } + onPressOpenPicker={emojiData.onPressOpenPicker} + onEmojiPickerClosed={emojiData.onEmojiPickerClosed} + reportActionID={emojiData.reportActionID} + reportAction={emojiData.reportAction} /> )} - - ))} - {needsOverflow && ( - - interceptAnonymousUser(() => { - openOverflowMenu(new MouseEvent('click'), threeDotRef); - miniActions.keepOpen(); - }, true) - } - shouldPreventDefaultFocusOnPress={false} - sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.MENU} - > - {({hovered, pressed}) => ( - + {displayedActions.map((action) => ( + + {({hovered, pressed}) => ( + + )} + + ))} + {needsOverflow && ( + + interceptAnonymousUser(() => { + openOverflowMenu(new MouseEvent('click'), threeDotRef); + miniActions.keepOpen(); + }, true) + } + shouldPreventDefaultFocusOnPress={false} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.MENU} + > + {({hovered, pressed}) => ( + + )} + )} - - )} - - -
, - document.body, +
+ + +
+ ); } From 726a2a7d6630140852e0708ebe5587ef6f17fbd7 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 4 Mar 2026 13:37:30 -0800 Subject: [PATCH 57/88] fix: keep MiniReportActionContextMenu visible during scroll Remove shouldHandleScroll from Hoverable in PureReportActionItem to prevent the scroll-triggered hide/show cycle that caused flicker. Add scroll position tracking via window scroll listener with requestAnimationFrame throttling so the menu follows its row. Replace React state (position, shouldAnimateSlide) with Reanimated shared values (topValue, rightValue) and useAnimatedStyle so scroll updates bypass React's render cycle entirely. Hover-to-hover slides use withTiming; scroll and show/hide updates are instant. Remove the immediate hide option from hideMiniContextMenu since scroll-to-hide is no longer needed. Made-with: Cursor --- .../ContextMenu/MiniContextMenuProvider.tsx | 12 +-- .../MiniReportActionContextMenu/index.tsx | 77 +++++++++++-------- .../inbox/report/PureReportActionItem.tsx | 1 - 3 files changed, 48 insertions(+), 42 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx b/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx index fa3e8e1054f2..9d0387eb5355 100644 --- a/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx @@ -30,8 +30,8 @@ type MiniContextMenuActions = { /** Display the mini context menu with the given parameters. Cancels any pending hide. */ showMiniContextMenu: (params: MiniContextMenuParams) => void; - /** Hide the mini context menu after a short delay (or immediately if `options.immediate` is set). No-op while `keepOpen` is active; the hide intent is deferred until `release`. */ - hideMiniContextMenu: (options?: {immediate?: boolean}) => void; + /** Hide the mini context menu after a short delay. No-op while `keepOpen` is active; the hide intent is deferred until `release`. */ + hideMiniContextMenu: () => void; /** Cancel a pending delayed hide without locking the menu open. Future `hideMiniContextMenu` calls still take effect normally. */ cancelHide: () => void; @@ -83,17 +83,13 @@ function MiniContextMenuProvider({children}: MiniContextMenuProviderProps) { pendingHideRef.current = false; setState({...params, isVisible: true}); }, - hideMiniContextMenu: (options) => { + hideMiniContextMenu: () => { if (shouldKeepOpenRef.current) { pendingHideRef.current = true; return; } clearHideTimer(); - if (options?.immediate) { - performHide(); - } else { - hideTimerRef.current = setTimeout(performHide, HIDE_DELAY_MS); - } + hideTimerRef.current = setTimeout(performHide, HIDE_DELAY_MS); }, cancelHide: () => { clearHideTimer(); diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index 19989ad4b99f..d7bb07afbc0a 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -1,10 +1,10 @@ import {Portal} from '@gorhom/portal'; -import React, {useEffect, useRef, useState} from 'react'; +import React, {useEffect, useRef} from 'react'; import type {RefObject} from 'react'; import {StyleSheet, View} from 'react-native'; // eslint-disable-next-line no-restricted-imports import type {GestureResponderEvent} from 'react-native'; -import Animated from 'react-native-reanimated'; +import Animated, {Easing, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import * as ActionSheetAwareScrollView from '@components/ActionSheetAwareScrollView'; import Hoverable from '@components/Hoverable'; import Icon from '@components/Icon'; @@ -52,8 +52,7 @@ function MiniReportActionContextMenu() { checkIfContextMenuActive, setIsEmojiPickerActive, } = useMiniContextMenuState() ?? {}; - const miniActions = useMiniContextMenuActions(); - const {hideMiniContextMenu, cancelHide} = miniActions; + const {hideMiniContextMenu, cancelHide, keepOpen, release} = useMiniContextMenuActions(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const StyleUtils = useStyleUtils(); ActionSheetAwareScrollView.useActionSheetAwareScrollViewActions(); @@ -63,8 +62,8 @@ function MiniReportActionContextMenu() { const wasVisibleRef = useRef(false); const overlayRef = useRef(null); const menuContainerRef = useRef(null); - const [position, setPosition] = useState<{top: number; right: number} | null>(null); - const [shouldAnimateSlide, setShouldAnimateSlide] = useState(false); + const topValue = useSharedValue(0); + const rightValue = useSharedValue(0); useEffect(() => { if (!isVisible || !rowMeasurements) { @@ -80,9 +79,15 @@ function MiniReportActionContextMenu() { const newTop = rowMeasurements.top - containerRect.top + (displayAsGroup ? -8 : -4); const newRight = containerRect.right - rowMeasurements.right + 4; - setShouldAnimateSlide(wasVisibleRef.current); - setPosition({top: newTop, right: newRight}); - }, [isVisible, rowMeasurements, displayAsGroup]); + const timingConfig = {duration: SLIDE_DURATION, easing: Easing.inOut(Easing.ease)}; + if (wasVisibleRef.current) { + topValue.set(withTiming(newTop, timingConfig)); + rightValue.set(withTiming(newRight, timingConfig)); + } else { + topValue.set(newTop); + rightValue.set(newRight); + } + }, [isVisible, rowMeasurements, displayAsGroup, topValue, rightValue]); useEffect(() => { if (isVisible) { @@ -90,25 +95,37 @@ function MiniReportActionContextMenu() { return; } wasVisibleRef.current = false; - const timer = setTimeout(() => { - setPosition(null); - setShouldAnimateSlide(false); - }, 0); - return () => clearTimeout(timer); }, [isVisible]); useEffect(() => { - if (!isVisible) { + if (!isVisible || !anchor?.current) { return; } + + let rafId: number; const handleScroll = () => { - hideMiniContextMenu({immediate: true}); + cancelAnimationFrame(rafId); + rafId = requestAnimationFrame(() => { + const node = anchor.current as unknown as HTMLElement | null; + const el = overlayRef.current as unknown as HTMLElement | null; + if (!node || !el) { + return; + } + const rect = node.getBoundingClientRect(); + const containerRect = el.getBoundingClientRect(); + const newTop = rect.top - containerRect.top + (displayAsGroup ? -8 : -4); + const newRight = containerRect.right - rect.right + 4; + topValue.set(newTop); + rightValue.set(newRight); + }); }; + window.addEventListener('scroll', handleScroll, true); return () => { window.removeEventListener('scroll', handleScroll, true); + cancelAnimationFrame(rafId); }; - }, [isVisible, hideMiniContextMenu]); + }, [isVisible, anchor, displayAsGroup, topValue, rightValue]); useEffect(() => { const el = menuContainerRef.current as unknown as HTMLElement | null; @@ -190,7 +207,7 @@ function MiniReportActionContextMenu() { }); const hideAndRun = (callback?: () => void) => { - miniActions.release(); + release(); callback?.(); }; @@ -212,7 +229,7 @@ function MiniReportActionContextMenu() { onShow: checkIfContextMenuActive, onHide: () => { checkIfContextMenuActive?.(); - miniActions.release(); + release(); }, }, shouldCloseOnTarget: true, @@ -418,21 +435,24 @@ function MiniReportActionContextMenu() { reportID: resolvedReportID, reportAction, currentUserAccountID, - openContextMenu: () => miniActions.keepOpen(), + openContextMenu: () => keepOpen(), setIsEmojiPickerActive, hideAndRun, }); const hasEmoji = shouldShowEmojiReaction({reportAction}) && !!emojiData.reportAction && !!emojiData.reportActionID; + const animatedPositionStyle = useAnimatedStyle(() => ({ + top: topValue.get(), + right: rightValue.get(), + })); + if (!rowMeasurements) { return null; } const wrapperStyle = StyleUtils.getReportActionContextMenuStyles(true, shouldUseNarrowLayout); - const shouldTransitionPosition = shouldAnimateSlide && isVisible; - return ( interceptAnonymousUser(() => { openOverflowMenu(new MouseEvent('click'), threeDotRef); - miniActions.keepOpen(); + keepOpen(); }, true) } shouldPreventDefaultFocusOnPress={false} diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx index 184b37d25378..d3620a1a8495 100644 --- a/src/pages/inbox/report/PureReportActionItem.tsx +++ b/src/pages/inbox/report/PureReportActionItem.tsx @@ -2027,7 +2027,6 @@ function PureReportActionItem({ sentryLabel={CONST.SENTRY_LABEL.REPORT.PURE_REPORT_ACTION_ITEM} > { From 0b212ee11dcb9c668cb518ca2eab09b65a09a2e4 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 4 Mar 2026 14:23:38 -0800 Subject: [PATCH 58/88] Restore main-branch MiniReportActionContextMenu scroll behavior Strip all Reanimated animation and scroll-tracking code from MiniReportActionContextMenu. Position is now derived during render from a container rect measurement (useLayoutEffect) plus rowMeasurements/displayAsGroup props. Re-add shouldHandleScroll to Hoverable in PureReportActionItem to hide on scroll. Move positioning style to StyleUtils.getMiniReportActionContextMenuWrapperStyle. Made-with: Cursor --- .../MiniReportActionContextMenu/index.tsx | 86 ++++--------------- .../inbox/report/PureReportActionItem.tsx | 1 + src/styles/utils/index.ts | 10 +-- 3 files changed, 20 insertions(+), 77 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index d7bb07afbc0a..598e11a47871 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -1,10 +1,9 @@ import {Portal} from '@gorhom/portal'; -import React, {useEffect, useRef} from 'react'; +import React, {useEffect, useLayoutEffect, useRef, useState} from 'react'; import type {RefObject} from 'react'; import {StyleSheet, View} from 'react-native'; // eslint-disable-next-line no-restricted-imports import type {GestureResponderEvent} from 'react-native'; -import Animated, {Easing, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import * as ActionSheetAwareScrollView from '@components/ActionSheetAwareScrollView'; import Hoverable from '@components/Hoverable'; import Icon from '@components/Icon'; @@ -37,8 +36,6 @@ import {showContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionConte import useReportActionContextMenuData from '@pages/inbox/report/ContextMenu/useReportActionContextMenuData'; import CONST from '@src/CONST'; -const SLIDE_DURATION = 200; - function MiniReportActionContextMenu() { const { isVisible = false, @@ -59,73 +56,25 @@ function MiniReportActionContextMenu() { const icons = useMemoizedLazyExpensifyIcons(CONTEXT_MENU_ICON_NAMES); const threeDotRef = useRef(null); - const wasVisibleRef = useRef(false); const overlayRef = useRef(null); const menuContainerRef = useRef(null); - const topValue = useSharedValue(0); - const rightValue = useSharedValue(0); - - useEffect(() => { - if (!isVisible || !rowMeasurements) { - return; - } + const [containerRect, setContainerRect] = useState(null); + useLayoutEffect(() => { const el = overlayRef.current as unknown as HTMLElement | null; if (!el) { return; } + setContainerRect(el.getBoundingClientRect()); + }, [isVisible, rowMeasurements]); - const containerRect = el.getBoundingClientRect(); - const newTop = rowMeasurements.top - containerRect.top + (displayAsGroup ? -8 : -4); - const newRight = containerRect.right - rowMeasurements.right + 4; - - const timingConfig = {duration: SLIDE_DURATION, easing: Easing.inOut(Easing.ease)}; - if (wasVisibleRef.current) { - topValue.set(withTiming(newTop, timingConfig)); - rightValue.set(withTiming(newRight, timingConfig)); - } else { - topValue.set(newTop); - rightValue.set(newRight); - } - }, [isVisible, rowMeasurements, displayAsGroup, topValue, rightValue]); - - useEffect(() => { - if (isVisible) { - wasVisibleRef.current = true; - return; - } - wasVisibleRef.current = false; - }, [isVisible]); - - useEffect(() => { - if (!isVisible || !anchor?.current) { - return; - } - - let rafId: number; - const handleScroll = () => { - cancelAnimationFrame(rafId); - rafId = requestAnimationFrame(() => { - const node = anchor.current as unknown as HTMLElement | null; - const el = overlayRef.current as unknown as HTMLElement | null; - if (!node || !el) { - return; - } - const rect = node.getBoundingClientRect(); - const containerRect = el.getBoundingClientRect(); - const newTop = rect.top - containerRect.top + (displayAsGroup ? -8 : -4); - const newRight = containerRect.right - rect.right + 4; - topValue.set(newTop); - rightValue.set(newRight); - }); - }; - - window.addEventListener('scroll', handleScroll, true); - return () => { - window.removeEventListener('scroll', handleScroll, true); - cancelAnimationFrame(rafId); - }; - }, [isVisible, anchor, displayAsGroup, topValue, rightValue]); + const position = + isVisible && rowMeasurements && containerRect + ? { + top: rowMeasurements.top - containerRect.top + (displayAsGroup ? -8 : -4), + right: containerRect.right - rowMeasurements.right + 4, + } + : null; useEffect(() => { const el = menuContainerRef.current as unknown as HTMLElement | null; @@ -442,11 +391,6 @@ function MiniReportActionContextMenu() { const hasEmoji = shouldShowEmojiReaction({reportAction}) && !!emojiData.reportAction && !!emojiData.reportActionID; - const animatedPositionStyle = useAnimatedStyle(() => ({ - top: topValue.get(), - right: rightValue.get(), - })); - if (!rowMeasurements) { return null; } @@ -460,8 +404,8 @@ function MiniReportActionContextMenu() { style={StyleSheet.absoluteFill} pointerEvents="box-none" > - - + ); diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx index d3620a1a8495..184b37d25378 100644 --- a/src/pages/inbox/report/PureReportActionItem.tsx +++ b/src/pages/inbox/report/PureReportActionItem.tsx @@ -2027,6 +2027,7 @@ function PureReportActionItem({ sentryLabel={CONST.SENTRY_LABEL.REPORT.PURE_REPORT_ACTION_ITEM} > { diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 5403d53c47df..1dc61dde36e6 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -1695,14 +1695,12 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ /** * Generate the wrapper styles for the mini ReportActionContextMenu. */ - getMiniReportActionContextMenuWrapperStyle: (isReportActionItemGrouped: boolean): ViewStyle => ({ - ...(isReportActionItemGrouped ? positioning.tn8 : positioning.tn4), - ...positioning.r4, - ...styles.cursorDefault, - ...styles.userSelectNone, - overflowAnchor: 'none', + getMiniReportActionContextMenuWrapperStyle: (pos: {top: number; right: number} | null, isVisible: boolean): ViewStyle => ({ position: 'absolute', zIndex: 8, + top: pos?.top ?? 0, + right: pos?.right ?? 0, + opacity: isVisible && pos ? 1 : 0, }), /** From d8da67480bb8e027cf0ca98f600523465338339f Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 4 Mar 2026 14:31:50 -0800 Subject: [PATCH 59/88] Remove hide timer from MiniContextMenuProvider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 120ms delayed hide was unnecessary — hiding immediately causes no flicker. This removes the timer, cancelHide action, and all related plumbing from both the provider and consumer. Made-with: Cursor --- .../ContextMenu/MiniContextMenuProvider.tsx | 29 ++----------------- .../MiniReportActionContextMenu/index.tsx | 12 ++------ 2 files changed, 6 insertions(+), 35 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx b/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx index 9d0387eb5355..ff467b1bd623 100644 --- a/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx @@ -2,8 +2,6 @@ import type {ReactNode, RefObject} from 'react'; import React, {createContext, useContext, useRef, useState} from 'react'; import type {ContextMenuAnchor} from './ReportActionContextMenu'; -const HIDE_DELAY_MS = 120; - type RowMeasurements = { top: number; height: number; @@ -27,15 +25,12 @@ type MiniContextMenuState = MiniContextMenuParams & { }; type MiniContextMenuActions = { - /** Display the mini context menu with the given parameters. Cancels any pending hide. */ + /** Display the mini context menu with the given parameters. */ showMiniContextMenu: (params: MiniContextMenuParams) => void; - /** Hide the mini context menu after a short delay. No-op while `keepOpen` is active; the hide intent is deferred until `release`. */ + /** Hide the mini context menu immediately. No-op while `keepOpen` is active; the hide intent is deferred until `release`. */ hideMiniContextMenu: () => void; - /** Cancel a pending delayed hide without locking the menu open. Future `hideMiniContextMenu` calls still take effect normally. */ - cancelHide: () => void; - /** Lock the menu open so that `hideMiniContextMenu` calls are deferred until `release` is called. Use when a sub-interaction (overflow menu, emoji picker) needs the menu to stay visible. */ keepOpen: () => void; @@ -46,7 +41,6 @@ type MiniContextMenuActions = { const MiniContextMenuActionsContext = createContext({ showMiniContextMenu: () => {}, hideMiniContextMenu: () => {}, - cancelHide: () => {}, keepOpen: () => {}, release: () => {}, }); @@ -59,27 +53,16 @@ type MiniContextMenuProviderProps = { function MiniContextMenuProvider({children}: MiniContextMenuProviderProps) { const [state, setState] = useState(null); - const hideTimerRef = useRef | null>(null); const shouldKeepOpenRef = useRef(false); const pendingHideRef = useRef(false); const [actions] = useState(() => { - const clearHideTimer = () => { - if (hideTimerRef.current == null) { - return; - } - clearTimeout(hideTimerRef.current); - hideTimerRef.current = null; - }; - const performHide = () => { - clearHideTimer(); setState((prev) => (prev ? {...prev, isVisible: false} : null)); }; return { showMiniContextMenu: (params: MiniContextMenuParams) => { - clearHideTimer(); pendingHideRef.current = false; setState({...params, isVisible: true}); }, @@ -88,16 +71,10 @@ function MiniContextMenuProvider({children}: MiniContextMenuProviderProps) { pendingHideRef.current = true; return; } - clearHideTimer(); - hideTimerRef.current = setTimeout(performHide, HIDE_DELAY_MS); - }, - cancelHide: () => { - clearHideTimer(); - pendingHideRef.current = false; + performHide(); }, keepOpen: () => { shouldKeepOpenRef.current = true; - clearHideTimer(); pendingHideRef.current = false; }, release: () => { diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index 598e11a47871..b4241c10c073 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -49,7 +49,7 @@ function MiniReportActionContextMenu() { checkIfContextMenuActive, setIsEmojiPickerActive, } = useMiniContextMenuState() ?? {}; - const {hideMiniContextMenu, cancelHide, keepOpen, release} = useMiniContextMenuActions(); + const {hideMiniContextMenu, keepOpen, release} = useMiniContextMenuActions(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const StyleUtils = useStyleUtils(); ActionSheetAwareScrollView.useActionSheetAwareScrollViewActions(); @@ -82,7 +82,6 @@ function MiniReportActionContextMenu() { return; } - const onFocusCapture = () => cancelHide(); const onBlurCapture = (e: FocusEvent) => { if (e.relatedTarget && el.contains(e.relatedTarget as Node)) { return; @@ -90,13 +89,11 @@ function MiniReportActionContextMenu() { hideMiniContextMenu(); }; - el.addEventListener('focus', onFocusCapture, true); el.addEventListener('blur', onBlurCapture, true); return () => { - el.removeEventListener('focus', onFocusCapture, true); el.removeEventListener('blur', onBlurCapture, true); }; - }, [cancelHide, hideMiniContextMenu]); + }, [hideMiniContextMenu]); useEffect(() => { const el = menuContainerRef.current as unknown as HTMLElement | null; @@ -408,10 +405,7 @@ function MiniReportActionContextMenu() { style={StyleUtils.getMiniReportActionContextMenuWrapperStyle(position, isVisible)} pointerEvents={isVisible ? 'auto' : 'none'} > - hideMiniContextMenu()} - > + hideMiniContextMenu()}> Date: Wed, 4 Mar 2026 18:07:53 -0800 Subject: [PATCH 60/88] Fix six MiniReportActionContextMenu UI regressions - Defer shouldKeepOpenRef check in performHide's setState updater and add keepOpen/release on the menu's Hoverable so the menu stays visible during row-to-menu hover transitions (flash loop fix) - Pass onMenuHide callback through showMiniContextMenu so the row can clear isContextMenuActive when the menu actually hides (row hover fix) - Fix positioning offsets from -8/-4/4 to -32/-16/16 to match the positioning constants (tn8/tn4/r4) used on main - Add isDelayButtonStateComplete={false} to action MiniContextMenuItems so hover backgrounds appear on non-emoji items - Change onHide in showPopover to explicitly setIsContextMenuActive(false) instead of toggleContextMenuFromActiveReportAction (stale closure fix) - Remove unconditional overflowAction push from PopoverReportActionContent Made-with: Cursor --- .../ContextMenu/MiniContextMenuProvider.tsx | 36 +++++++++++++++---- .../MiniReportActionContextMenu/index.tsx | 14 ++++++-- .../PopoverReportActionContent.tsx | 1 - .../inbox/report/PureReportActionItem.tsx | 4 ++- src/styles/utils/index.ts | 3 ++ 5 files changed, 46 insertions(+), 12 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx b/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx index ff467b1bd623..6742a021e8b2 100644 --- a/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx @@ -1,5 +1,5 @@ import type {ReactNode, RefObject} from 'react'; -import React, {createContext, useContext, useRef, useState} from 'react'; +import React, {createContext, useContext, useEffect, useRef, useState} from 'react'; import type {ContextMenuAnchor} from './ReportActionContextMenu'; type RowMeasurements = { @@ -20,18 +20,22 @@ type MiniContextMenuParams = { rowMeasurements: RowMeasurements; }; +type ShowMiniContextMenuParams = MiniContextMenuParams & { + onMenuHide?: () => void; +}; + type MiniContextMenuState = MiniContextMenuParams & { isVisible: boolean; }; type MiniContextMenuActions = { /** Display the mini context menu with the given parameters. */ - showMiniContextMenu: (params: MiniContextMenuParams) => void; + showMiniContextMenu: (params: ShowMiniContextMenuParams) => void; /** Hide the mini context menu immediately. No-op while `keepOpen` is active; the hide intent is deferred until `release`. */ hideMiniContextMenu: () => void; - /** Lock the menu open so that `hideMiniContextMenu` calls are deferred until `release` is called. Use when a sub-interaction (overflow menu, emoji picker) needs the menu to stay visible. */ + /** Lock the menu open so that `hideMiniContextMenu` calls are deferred until `release` is called. Use when a sub-interaction (overflow menu, emoji picker) needs the menu to stay visible. Also used by the menu's own Hoverable to prevent hide during row-to-menu hover transitions. */ keepOpen: () => void; /** Unlock the menu after `keepOpen`. If a hide was deferred while locked, it executes immediately. */ @@ -55,16 +59,34 @@ function MiniContextMenuProvider({children}: MiniContextMenuProviderProps) { const [state, setState] = useState(null); const shouldKeepOpenRef = useRef(false); const pendingHideRef = useRef(false); + const onMenuHideRef = useRef<(() => void) | null>(null); + + useEffect(() => { + if (state?.isVisible ?? false) { + return; + } + onMenuHideRef.current?.(); + onMenuHideRef.current = null; + }, [state?.isVisible]); const [actions] = useState(() => { const performHide = () => { - setState((prev) => (prev ? {...prev, isVisible: false} : null)); + setState((prev) => { + if (shouldKeepOpenRef.current) { + pendingHideRef.current = true; + return prev; + } + return prev ? {...prev, isVisible: false} : null; + }); }; return { - showMiniContextMenu: (params: MiniContextMenuParams) => { + showMiniContextMenu: (params: ShowMiniContextMenuParams) => { + onMenuHideRef.current?.(); + const {onMenuHide, ...stateParams} = params; + onMenuHideRef.current = onMenuHide ?? null; pendingHideRef.current = false; - setState({...params, isVisible: true}); + setState({...stateParams, isVisible: true}); }, hideMiniContextMenu: () => { if (shouldKeepOpenRef.current) { @@ -103,4 +125,4 @@ function useMiniContextMenuState(): MiniContextMenuState | null { } export {MiniContextMenuProvider, useMiniContextMenuActions, useMiniContextMenuState}; -export type {MiniContextMenuParams, MiniContextMenuState, RowMeasurements, MiniContextMenuActions}; +export type {MiniContextMenuParams, ShowMiniContextMenuParams, MiniContextMenuState, RowMeasurements, MiniContextMenuActions}; diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index b4241c10c073..40784f7ad0ad 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -71,8 +71,8 @@ function MiniReportActionContextMenu() { const position = isVisible && rowMeasurements && containerRect ? { - top: rowMeasurements.top - containerRect.top + (displayAsGroup ? -8 : -4), - right: containerRect.right - rowMeasurements.right + 4, + top: rowMeasurements.top - containerRect.top + (displayAsGroup ? -32 : -16), + right: containerRect.right - rowMeasurements.right + 16, } : null; @@ -405,7 +405,13 @@ function MiniReportActionContextMenu() { style={StyleUtils.getMiniReportActionContextMenuWrapperStyle(position, isVisible)} pointerEvents={isVisible ? 'auto' : 'none'} > - hideMiniContextMenu()}> + keepOpen()} + onHoverOut={() => { + release(); + hideMiniContextMenu(); + }} + > {({hovered, pressed}) => ( @@ -447,6 +454,7 @@ function MiniReportActionContextMenu() { keepOpen(); }, true) } + isDelayButtonStateComplete={false} shouldPreventDefaultFocusOnPress={false} sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.MENU} > diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx index afd51961633b..6c8bea919693 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx @@ -351,7 +351,6 @@ function PopoverReportActionContent({ if (showDelete) { visibleActions.push(createDeleteAction({reportID: resolvedReportID, reportAction, moneyRequestAction, hideAndRun, translate, trashcanIcon: icons.Trashcan})); } - visibleActions.push(overflowAction); } const emojiData = createEmojiReactionData({ diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx index 184b37d25378..d1b96297b729 100644 --- a/src/pages/inbox/report/PureReportActionItem.tsx +++ b/src/pages/inbox/report/PureReportActionItem.tsx @@ -793,7 +793,7 @@ function PureReportActionItem({ }, callbacks: { onShow: toggleContextMenuFromActiveReportAction, - onHide: toggleContextMenuFromActiveReportAction, + onHide: () => setIsContextMenuActive(false), setIsEmojiPickerActive: setIsEmojiPickerActive as () => void, }, }); @@ -2040,6 +2040,7 @@ function PureReportActionItem({ return; } const rect = node.getBoundingClientRect(); + setIsContextMenuActive(true); showMiniContextMenu({ reportID, reportActionID: action.reportActionID, @@ -2054,6 +2055,7 @@ function PureReportActionItem({ height: rect.height, right: rect.right, }, + onMenuHide: () => setIsContextMenuActive(false), }); }} onHoverOut={() => { diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 1dc61dde36e6..01d33f4cae81 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -1701,6 +1701,9 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ top: pos?.top ?? 0, right: pos?.right ?? 0, opacity: isVisible && pos ? 1 : 0, + ...styles.cursorDefault, + ...styles.userSelectNone, + overflowAnchor: 'none', }), /** From 11579554b657ac992075a2b6d236f1b5c54bca26 Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 6 Mar 2026 12:47:29 -0800 Subject: [PATCH 61/88] Port main's ContextMenuActions changes to decomposed action files Main modified ContextMenuActions.tsx (which our branch deleted and decomposed). Port the three functional changes to their new locations: - editAction.ts: openReport now takes object param {reportID, introSelected} - copyMessageAction.ts: use isRejectedAction() helper instead of raw actionName comparison - copyMessageAction.ts: pass isOriginalReportDeleted to getCreatedReportForUnapprovedTransactionsMessage (new 3rd param) Made-with: Cursor --- .../ContextMenu/actions/copyMessageAction.ts | 14 +++++++++++--- .../inbox/report/ContextMenu/actions/editAction.ts | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.ts b/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.ts index d7dbc4a7ffde..b7492669793a 100644 --- a/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.ts @@ -114,8 +114,10 @@ import { isMoneyRequestAction, isMovedAction, isOldDotReportAction, + isOriginalReportDeleted, isReimbursementDeQueuedOrCanceledAction, isReimbursementQueuedAction, + isRejectedAction, isRenamedAction, isReportActionAttachment, isReportPreviewAction as isReportPreviewActionReportActionsUtils, @@ -397,7 +399,7 @@ function copyMessageToClipboard(params: CopyMessageClipboardParams) { } else { Clipboard.setString(translate('iou.forwarded')); } - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REJECTED) { + } else if (isRejectedAction(reportAction)) { Clipboard.setString(translate('iou.rejectedThisReport')); } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.CORPORATE_UPGRADE) { const displayMessage = translate('workspaceActions.upgradedWorkspace'); @@ -514,8 +516,14 @@ function copyMessageToClipboard(params: CopyMessageClipboardParams) { setClipboardMessage(displayMessage); } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.CREATED_REPORT_FOR_UNAPPROVED_TRANSACTIONS)) { const {originalID} = getOriginalMessage(reportAction) ?? {}; - const reportName = getReportName(getReportOrDraftReport(originalID)); - const displayMessage = getCreatedReportForUnapprovedTransactionsMessage(originalID, reportName, translate); + const originalReportOfUnapprovedTransaction = getReportOrDraftReport(originalID); + const reportName = getReportName(originalReportOfUnapprovedTransaction); + const displayMessage = getCreatedReportForUnapprovedTransactionsMessage( + originalID, + reportName, + isOriginalReportDeleted(reportAction, originalReportOfUnapprovedTransaction), + translate, + ); setClipboardMessage(displayMessage); } else if (content) { setClipboardMessage( diff --git a/src/pages/inbox/report/ContextMenu/actions/editAction.ts b/src/pages/inbox/report/ContextMenu/actions/editAction.ts index 03c141d316b0..1061c4eb8a34 100644 --- a/src/pages/inbox/report/ContextMenu/actions/editAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/editAction.ts @@ -46,7 +46,7 @@ function createEditAction({reportID, reportAction, moneyRequestAction, draftMess if (isMoneyRequestAction(reportAction) || isMoneyRequestAction(moneyRequestAction)) { hideAndRun(() => { const childReportID = reportAction?.childReportID; - openReport(childReportID, introSelected); + openReport({reportID: childReportID, introSelected}); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID)); }); return; From ad9e7e2d5ff33b69e2d4c9465fc4d5415d83db65 Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 6 Mar 2026 12:59:38 -0800 Subject: [PATCH 62/88] Fix merge: remove isArchivedNonExpenseReport from context menu consumers Our architecture computes isArchivedRoom internally in useReportActionContextMenuData, so callers of showContextMenuForReport don't need to pass it. The merge incorrectly kept main's call-site usage while our branch had removed the import, causing a runtime crash. Made-with: Cursor --- .../BaseAnchorForAttachmentsOnly.tsx | 4 ++-- .../HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx | 6 ++---- .../HTMLRenderers/MentionUserRenderer.tsx | 6 ++---- .../HTMLEngineProvider/HTMLRenderers/PreRenderer.tsx | 4 ++-- src/components/ShowContextMenuContext/index.tsx | 2 -- src/components/VideoPlayerPreview/VideoPlayerThumbnail.tsx | 4 ++-- 6 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx index 908f8c6bfa9a..66427e3742c7 100644 --- a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx +++ b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.tsx @@ -38,7 +38,7 @@ function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', onP const encryptedAuthToken = session?.encryptedAuthToken ?? ''; const sourceURLWithAuth = addEncryptedAuthTokenToURL(source, encryptedAuthToken); - const {anchor, report, isReportArchived, action, isDisabled, shouldDisplayContextMenu} = useShowContextMenuState(); + const {anchor, report, action, isDisabled, shouldDisplayContextMenu} = useShowContextMenuState(); const {checkIfContextMenuActive} = useShowContextMenuActions(); return ( @@ -57,7 +57,7 @@ function BaseAnchorForAttachmentsOnly({style, source = '', displayName = '', onP if (isDisabled || !shouldDisplayContextMenu) { return; } - showContextMenuForReport(event, anchor, report?.reportID, action, checkIfContextMenuActive, isArchivedNonExpenseReport(report, isReportArchived)); + showContextMenuForReport(event, anchor, report?.reportID, action, checkIfContextMenuActive); }} shouldUseHapticsOnLongPress accessibilityLabel={displayName} diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx index ff7b8d3b1a7a..a7bcbcc7af7f 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx @@ -89,7 +89,7 @@ function ImageRenderer({tnode}: CustomRendererProps) { /> ); - const {anchor, report, isReportArchived, action, isDisabled, shouldDisplayContextMenu} = useShowContextMenuState(); + const {anchor, report, action, isDisabled, shouldDisplayContextMenu} = useShowContextMenuState(); const {onShowContextMenu, checkIfContextMenuActive} = useShowContextMenuActions(); return imagePreviewModalDisabled ? ( @@ -121,9 +121,7 @@ function ImageRenderer({tnode}: CustomRendererProps) { if (isDisabled || !shouldDisplayContextMenu) { return; } - return onShowContextMenu(() => - showContextMenuForReport(event, anchor, report?.reportID, action, checkIfContextMenuActive, isArchivedNonExpenseReport(report, isReportArchived)), - ); + return onShowContextMenu(() => showContextMenuForReport(event, anchor, report?.reportID, action, checkIfContextMenuActive)); }} isNested shouldUseHapticsOnLongPress diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx index 0db23fab6e26..8d37c2fc27f7 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx @@ -32,7 +32,7 @@ function MentionUserRenderer({style, tnode, TDefaultRenderer, currentUserPersona const htmlAttribAccountID = tnode.attributes.accountid; const personalDetails = usePersonalDetails(); const htmlAttributeAccountID = tnode.attributes.accountid; - const {anchor, report, isReportArchived, action, isDisabled, shouldDisplayContextMenu} = useShowContextMenuState(); + const {anchor, report, action, isDisabled, shouldDisplayContextMenu} = useShowContextMenuState(); const {onShowContextMenu, checkIfContextMenuActive} = useShowContextMenuActions(); let accountID: number; @@ -81,9 +81,7 @@ function MentionUserRenderer({style, tnode, TDefaultRenderer, currentUserPersona if (isDisabled || !shouldDisplayContextMenu) { return; } - return onShowContextMenu(() => - showContextMenuForReport(event, anchor, report?.reportID, action, checkIfContextMenuActive, isArchivedNonExpenseReport(report, isReportArchived)), - ); + return onShowContextMenu(() => showContextMenuForReport(event, anchor, report?.reportID, action, checkIfContextMenuActive)); }} onPress={(event) => { event.preventDefault(); diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer.tsx index e2c09d276400..249a41784351 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/PreRenderer.tsx @@ -32,7 +32,7 @@ function PreRenderer({TDefaultRenderer, onPressIn, onPressOut, onLongPress, ...d const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); - const {anchor, report, isReportArchived, action, isDisabled, shouldDisplayContextMenu} = useShowContextMenuState(); + const {anchor, report, action, isDisabled, shouldDisplayContextMenu} = useShowContextMenuState(); const {onShowContextMenu, checkIfContextMenuActive} = useShowContextMenuActions(); const isLast = defaultRendererProps.renderIndex === defaultRendererProps.renderLength - 1; @@ -62,7 +62,7 @@ function PreRenderer({TDefaultRenderer, onPressIn, onPressOut, onLongPress, ...d if (isDisabled || !shouldDisplayContextMenu) { return; } - return showContextMenuForReport(event, anchor, report?.reportID, action, checkIfContextMenuActive, isArchivedNonExpenseReport(report, isReportArchived)); + return showContextMenuForReport(event, anchor, report?.reportID, action, checkIfContextMenuActive); }); }} shouldUseHapticsOnLongPress diff --git a/src/components/ShowContextMenuContext/index.tsx b/src/components/ShowContextMenuContext/index.tsx index 8449fb9ad552..f6d0bbb8e304 100644 --- a/src/components/ShowContextMenuContext/index.tsx +++ b/src/components/ShowContextMenuContext/index.tsx @@ -36,7 +36,6 @@ function showContextMenuForReport( reportID: string | undefined, action: OnyxEntry, checkIfContextMenuActive: () => void, - isArchivedRoom = false, ) { if (!canUseTouchScreen()) { return; @@ -50,7 +49,6 @@ function showContextMenuForReport( report: { reportID, originalReportID: reportID ? getOriginalReportID(reportID, action, undefined) : undefined, - isArchivedRoom, }, reportAction: { reportActionID: action?.reportActionID, diff --git a/src/components/VideoPlayerPreview/VideoPlayerThumbnail.tsx b/src/components/VideoPlayerPreview/VideoPlayerThumbnail.tsx index d93d3b5328eb..b61e004dbf22 100644 --- a/src/components/VideoPlayerPreview/VideoPlayerThumbnail.tsx +++ b/src/components/VideoPlayerPreview/VideoPlayerThumbnail.tsx @@ -30,7 +30,7 @@ type VideoPlayerThumbnailProps = { function VideoPlayerThumbnail({thumbnailUrl, onPress, accessibilityLabel, isDeleted}: VideoPlayerThumbnailProps) { const styles = useThemeStyles(); const icons = useMemoizedLazyExpensifyIcons(['Play'] as const); - const {anchor, report, isReportArchived, action, isDisabled, shouldDisplayContextMenu} = useShowContextMenuState(); + const {anchor, report, action, isDisabled, shouldDisplayContextMenu} = useShowContextMenuState(); const {onShowContextMenu, checkIfContextMenuActive} = useShowContextMenuActions(); return ( @@ -58,7 +58,7 @@ function VideoPlayerThumbnail({thumbnailUrl, onPress, accessibilityLabel, isDele return; } onShowContextMenu(() => { - showContextMenuForReport(event, anchor, report?.reportID, action, checkIfContextMenuActive, isArchivedNonExpenseReport(report, isReportArchived)); + showContextMenuForReport(event, anchor, report?.reportID, action, checkIfContextMenuActive); }); }} shouldUseHapticsOnLongPress From 19f5451617e66d2af60b16e3c7c7edf466d37853 Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 6 Mar 2026 17:44:15 -0800 Subject: [PATCH 63/88] Fix hover flash regressions in MiniReportActionContextMenu - Defer hide decisions to a microtask so all synchronous event handlers complete before we decide whether to actually hide the menu - Track activeReportActionID to avoid resetting isContextMenuActive when re-hovering the same row - Move Hoverable to wrap the rectangular positioning wrapper instead of the rounded inner View, eliminating border-radius hit-testing dead zones - Reorder setIsContextMenuActive(true) after showMiniContextMenu() so the final batched state is correct Made-with: Cursor --- .../ContextMenu/MiniContextMenuProvider.tsx | 37 ++++++++++++++----- .../MiniReportActionContextMenu/index.tsx | 22 +++++------ .../inbox/report/PureReportActionItem.tsx | 2 +- 3 files changed, 39 insertions(+), 22 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx b/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx index 6742a021e8b2..8ff80425a2b1 100644 --- a/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx @@ -60,6 +60,7 @@ function MiniContextMenuProvider({children}: MiniContextMenuProviderProps) { const shouldKeepOpenRef = useRef(false); const pendingHideRef = useRef(false); const onMenuHideRef = useRef<(() => void) | null>(null); + const activeReportActionIDRef = useRef(undefined); useEffect(() => { if (state?.isVisible ?? false) { @@ -67,29 +68,48 @@ function MiniContextMenuProvider({children}: MiniContextMenuProviderProps) { } onMenuHideRef.current?.(); onMenuHideRef.current = null; + activeReportActionIDRef.current = undefined; }, [state?.isVisible]); const [actions] = useState(() => { + const isGuarded = () => shouldKeepOpenRef.current; + + // Deferred to a microtask so that all event handlers in the current + // task (e.g. both mouseleave on the row AND mouseenter on the menu) + // finish and update refs before we decide whether to actually hide. const performHide = () => { - setState((prev) => { - if (shouldKeepOpenRef.current) { + queueMicrotask(() => { + if (isGuarded()) { pendingHideRef.current = true; - return prev; + return; } - return prev ? {...prev, isVisible: false} : null; + setState((prev) => (prev ? {...prev, isVisible: false} : null)); }); }; + const drainPendingHide = () => { + if (!pendingHideRef.current || isGuarded()) { + return; + } + pendingHideRef.current = false; + performHide(); + }; + return { showMiniContextMenu: (params: ShowMiniContextMenuParams) => { - onMenuHideRef.current?.(); + const isSameRow = params.reportActionID === activeReportActionIDRef.current; + if (!isSameRow) { + onMenuHideRef.current?.(); + } + activeReportActionIDRef.current = params.reportActionID; const {onMenuHide, ...stateParams} = params; onMenuHideRef.current = onMenuHide ?? null; pendingHideRef.current = false; + shouldKeepOpenRef.current = true; setState({...stateParams, isVisible: true}); }, hideMiniContextMenu: () => { - if (shouldKeepOpenRef.current) { + if (isGuarded()) { pendingHideRef.current = true; return; } @@ -101,10 +121,7 @@ function MiniContextMenuProvider({children}: MiniContextMenuProviderProps) { }, release: () => { shouldKeepOpenRef.current = false; - if (pendingHideRef.current) { - pendingHideRef.current = false; - performHide(); - } + drainPendingHide(); }, }; }); diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index 40784f7ad0ad..7fcceb0d72c8 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -401,16 +401,16 @@ function MiniReportActionContextMenu() { style={StyleSheet.absoluteFill} pointerEvents="box-none" > - keepOpen()} + onHoverOut={() => { + release(); + hideMiniContextMenu(); + }} > - keepOpen()} - onHoverOut={() => { - release(); - hideMiniContextMenu(); - }} + )} - - + + ); diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx index 28db0c03096c..ee285b5e10a0 100644 --- a/src/pages/inbox/report/PureReportActionItem.tsx +++ b/src/pages/inbox/report/PureReportActionItem.tsx @@ -2048,7 +2048,6 @@ function PureReportActionItem({ return; } const rect = node.getBoundingClientRect(); - setIsContextMenuActive(true); showMiniContextMenu({ reportID, reportActionID: action.reportActionID, @@ -2065,6 +2064,7 @@ function PureReportActionItem({ }, onMenuHide: () => setIsContextMenuActive(false), }); + setIsContextMenuActive(true); }} onHoverOut={() => { setIsReportActionActive(!!isReportActionLinked); From 531a5a881a8be1495256b92549690be127d5ec52 Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 6 Mar 2026 17:58:26 -0800 Subject: [PATCH 64/88] Move ref cleanup from useEffect into performHide handler The useEffect reacting to isVisible changes is unnecessary since performHide is the only code path that sets isVisible to false. Made-with: Cursor --- .../report/ContextMenu/MiniContextMenuProvider.tsx | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx b/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx index 8ff80425a2b1..fad7102d0a11 100644 --- a/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx @@ -1,5 +1,5 @@ import type {ReactNode, RefObject} from 'react'; -import React, {createContext, useContext, useEffect, useRef, useState} from 'react'; +import React, {createContext, useContext, useRef, useState} from 'react'; import type {ContextMenuAnchor} from './ReportActionContextMenu'; type RowMeasurements = { @@ -62,15 +62,6 @@ function MiniContextMenuProvider({children}: MiniContextMenuProviderProps) { const onMenuHideRef = useRef<(() => void) | null>(null); const activeReportActionIDRef = useRef(undefined); - useEffect(() => { - if (state?.isVisible ?? false) { - return; - } - onMenuHideRef.current?.(); - onMenuHideRef.current = null; - activeReportActionIDRef.current = undefined; - }, [state?.isVisible]); - const [actions] = useState(() => { const isGuarded = () => shouldKeepOpenRef.current; @@ -84,6 +75,9 @@ function MiniContextMenuProvider({children}: MiniContextMenuProviderProps) { return; } setState((prev) => (prev ? {...prev, isVisible: false} : null)); + onMenuHideRef.current?.(); + onMenuHideRef.current = null; + activeReportActionIDRef.current = undefined; }); }; From c0eabbf5d16105813f33316c7505e124d1f59f1c Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 6 Mar 2026 18:07:23 -0800 Subject: [PATCH 65/88] Remove unnecessary double type assertions in test - reportAction: single `as ReportAction` instead of `as unknown as` - translate: single `as LocalizedTranslate` with named import - currentUserPersonalDetails: no cast needed, object satisfies the type Made-with: Cursor --- tests/unit/ContextMenuActionsCopyMessageTest.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/unit/ContextMenuActionsCopyMessageTest.ts b/tests/unit/ContextMenuActionsCopyMessageTest.ts index f7c0f5824dde..bc973274828c 100644 --- a/tests/unit/ContextMenuActionsCopyMessageTest.ts +++ b/tests/unit/ContextMenuActionsCopyMessageTest.ts @@ -1,5 +1,6 @@ import Clipboard from '@libs/Clipboard'; import getClipboardText from '@libs/Clipboard/getClipboardText'; +import type {LocalizedTranslate} from '@components/LocaleContextProvider'; import {copyMessageToClipboard} from '@pages/inbox/report/ContextMenu/actions/copyMessageAction'; import CONST from '@src/CONST'; import type {ReportAction} from '@src/types/onyx'; @@ -42,7 +43,7 @@ const createParams = (selection: string) => ({ reportAction: { actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, message: [{html: selection}], - } as unknown as ReportAction, + } as ReportAction, transaction: undefined, selection, report: undefined, @@ -56,13 +57,13 @@ const createParams = (selection: string) => ({ policy: undefined, getLocalDateFromDatetime: jest.fn(), policyTags: undefined, - translate: ((translateKey: string) => translateKey) as unknown as Parameters[0]['translate'], + translate: ((translateKey: string) => translateKey) as LocalizedTranslate, harvestReport: undefined, currentUserPersonalDetails: { accountID: 1, login: 'user@expensify.com', email: 'user@expensify.com', - } as unknown as Parameters[0]['currentUserPersonalDetails'], + }, }); describe('ContextMenuActions copy message', () => { From 9111027b090758867d5a9c2b2be7adb0b4342e00 Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 6 Mar 2026 18:07:45 -0800 Subject: [PATCH 66/88] Restore accidentally removed comment in ReportScreen Made-with: Cursor --- src/pages/inbox/ReportScreen.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/inbox/ReportScreen.tsx b/src/pages/inbox/ReportScreen.tsx index 75fccede8a2d..b8a17c8c3ca4 100644 --- a/src/pages/inbox/ReportScreen.tsx +++ b/src/pages/inbox/ReportScreen.tsx @@ -1075,6 +1075,7 @@ function ReportScreen({route, navigation, isInSidePanel = false}: ReportScreenPr report={report} lastReportAction={lastReportAction} reportTransactions={reportTransactions} + // If the report is from the 'Send Money' flow, we add the comment to the `iou` report because for these we don't combine reportActions even if there is a single transaction (they always have a single transaction) transactionThreadReportID={isSentMoneyReport ? undefined : transactionThreadReportID} isInSidePanel={isInSidePanel} kickoffWaitingIndicator={kickoffWaitingIndicator} From 32477cf294ebf938fcb74c6aed00248b12192d8e Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 9 Mar 2026 14:34:07 -0700 Subject: [PATCH 67/88] Explode PopoverContextMenuState into individual useState variables Individual state variables are independently trackable by React Compiler, make unused state obvious, and let the dimension listener update only popoverPosition without creating a new object for all consumers. Made-with: Cursor --- .../ContextMenu/PopoverContextMenu/index.tsx | 165 ++++++++---------- 1 file changed, 73 insertions(+), 92 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/index.tsx index 5f120466293e..d25353e8fae3 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/index.tsx @@ -35,20 +35,6 @@ type PopoverPosition = { anchorHeight: number; }; -type PopoverContextMenuState = { - type: ContextMenuType; - reportID: string | undefined; - reportActionID: string | undefined; - originalReportID: string | undefined; - selection: string; - draftMessage: string | undefined; - isOverflowMenu: boolean; - withoutOverlay: boolean; - position: PopoverPosition; - contextMenuTargetNode: HTMLDivElement | null; - onEmojiPickerToggle: ((state: boolean) => void) | undefined; -}; - type PopoverContextMenuProps = { ref?: React.Ref; }; @@ -57,14 +43,22 @@ function PopoverContextMenu({ref: forwardedRef}: PopoverContextMenuProps) { const {transitionActionSheetState} = useActionSheetAwareScrollViewActions(); const modalContext = useModal(); - const [menuState, setMenuState] = useState(null); + const [type, setType] = useState(null); + const [reportID, setReportID] = useState(); + const [reportActionID, setReportActionID] = useState(); + const [originalReportID, setOriginalReportID] = useState(); + const [selection, setSelection] = useState(''); + const [draftMessage, setDraftMessage] = useState(); + const [isOverflowMenu, setIsOverflowMenu] = useState(false); + const [withoutOverlay, setWithoutOverlay] = useState(true); + const [popoverPosition, setPopoverPosition] = useState({anchorHorizontal: 0, anchorVertical: 0, anchorWidth: 0, anchorHeight: 0}); + const [contextMenuTargetNode, setContextMenuTargetNode] = useState(null); + const [onEmojiPickerToggle, setOnEmojiPickerToggle] = useState<((state: boolean) => void) | undefined>(); const [isPopoverVisible, setIsPopoverVisible] = useState(false); const [isContextMenuOpening, setIsContextMenuOpening] = useState(false); const [composerToRefocusOnClose, setComposerToRefocusOnClose] = useState(); const [localShouldKeepOpen, setLocalShouldKeepOpen] = useState(false); - const reportActionID = menuState?.reportActionID; - const cursorRelativePosition = useRef({horizontal: 0, vertical: 0}); const instanceIDRef = useRef(''); @@ -104,19 +98,11 @@ function PopoverContextMenu({ref: forwardedRef}: PopoverContextMenuProps) { return; } - setMenuState((prev) => { - if (!prev) { - return prev; - } - return { - ...prev, - position: { - ...prev.position, - anchorHorizontal: cursorRelativePosition.current.horizontal + x, - anchorVertical: cursorRelativePosition.current.vertical + y, - }, - }; - }); + setPopoverPosition((prev) => ({ + ...prev, + anchorHorizontal: cursorRelativePosition.current.horizontal + x, + anchorVertical: cursorRelativePosition.current.vertical + y, + })); }); }); @@ -128,21 +114,31 @@ function PopoverContextMenu({ref: forwardedRef}: PopoverContextMenuProps) { const isActiveReportAction: ReportActionContextMenu['isActiveReportAction'] = (actionID) => !!actionID && reportActionID === String(actionID); const clearActiveReportAction = () => { - setMenuState(null); + setType(null); + setReportID(undefined); + setReportActionID(undefined); + setOriginalReportID(undefined); + setSelection(''); + setDraftMessage(undefined); + setIsOverflowMenu(false); + setWithoutOverlay(true); + setPopoverPosition({anchorHorizontal: 0, anchorVertical: 0, anchorWidth: 0, anchorHeight: 0}); + setContextMenuTargetNode(null); + setOnEmojiPickerToggle(undefined); }; const showContextMenuHandler: ReportActionContextMenu['showContextMenu'] = (showContextMenuParams) => { const { - type, + type: showType, event, - selection, + selection: showSelection, contextMenuAnchor, report: currentReport = {}, reportAction: reportActionParam = {}, callbacks = {}, shouldCloseOnTarget = false, - isOverflowMenu = false, - withoutOverlay = true, + isOverflowMenu: showIsOverflowMenu = false, + withoutOverlay: showWithoutOverlay = true, } = showContextMenuParams; if (ReportActionComposeFocusManager.isFocused()) { setComposerToRefocusOnClose('main'); @@ -151,7 +147,7 @@ function PopoverContextMenu({ref: forwardedRef}: PopoverContextMenuProps) { } const {reportID: showReportID, originalReportID: showOriginalReportID} = currentReport; - const {reportActionID: showReportActionID, draftMessage} = reportActionParam; + const {reportActionID: showReportActionID, draftMessage: showDraftMessage} = reportActionParam; const {onShow = () => {}, onHide = () => {}, setIsEmojiPickerActive = () => {}} = callbacks; setIsContextMenuOpening(true); @@ -169,7 +165,7 @@ function PopoverContextMenu({ref: forwardedRef}: PopoverContextMenuProps) { new Promise((resolve) => { const anchor = contextMenuAnchorRef.current; - const useAnchorPosition = isOverflowMenu || (anchor != null && !pageX && !pageY); + const useAnchorPosition = showIsOverflowMenu || (anchor != null && !pageX && !pageY); if (useAnchorPosition && anchor) { calculateAnchorPosition(anchor).then((position) => { resolve({ @@ -194,20 +190,18 @@ function PopoverContextMenu({ref: forwardedRef}: PopoverContextMenuProps) { }); } }).then((position) => { - setMenuState({ - type, - reportID: showReportID, - reportActionID: showReportActionID, - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - originalReportID: showOriginalReportID || undefined, - selection, - draftMessage, - isOverflowMenu, - withoutOverlay, - position, - contextMenuTargetNode: targetNode, - onEmojiPickerToggle: setIsEmojiPickerActive, - }); + setType(showType); + setReportID(showReportID); + setReportActionID(showReportActionID); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + setOriginalReportID(showOriginalReportID || undefined); + setSelection(showSelection); + setDraftMessage(showDraftMessage); + setIsOverflowMenu(showIsOverflowMenu); + setWithoutOverlay(showWithoutOverlay); + setPopoverPosition(position); + setContextMenuTargetNode(targetNode); + setOnEmojiPickerToggle(() => setIsEmojiPickerActive); setIsPopoverVisible(true); }); }; @@ -227,7 +221,7 @@ function PopoverContextMenu({ref: forwardedRef}: PopoverContextMenuProps) { }; const runAndResetOnPopoverHide = () => { - setMenuState(null); + clearActiveReportAction(); instanceIDRef.current = ''; onPopoverHide.current = runAndResetCallback(onPopoverHide.current); @@ -273,21 +267,8 @@ function PopoverContextMenu({ref: forwardedRef}: PopoverContextMenuProps) { return; } - setMenuState((prev) => ({ - ...(prev ?? { - type: CONST.CONTEXT_MENU_TYPES.REPORT_ACTION as ContextMenuType, - selection: '', - draftMessage: undefined, - isOverflowMenu: false, - withoutOverlay: true, - position: {anchorHorizontal: 0, anchorVertical: 0, anchorWidth: 0, anchorHeight: 0}, - contextMenuTargetNode: null, - onEmojiPickerToggle: undefined, - }), - reportID: showReportID, - reportActionID: showReportAction.reportActionID, - originalReportID: prev?.originalReportID, - })); + setReportID(showReportID); + setReportActionID(showReportAction.reportActionID); isDeleteModalActiveRef.current = true; modalContext @@ -338,61 +319,61 @@ function PopoverContextMenu({ref: forwardedRef}: PopoverContextMenuProps) { onModalShow={runAndResetOnPopoverShow} onModalHide={runAndResetOnPopoverHide} anchorPosition={{ - horizontal: menuState?.position.anchorHorizontal ?? 0, - vertical: menuState?.position.anchorVertical ?? 0, + horizontal: popoverPosition.anchorHorizontal, + vertical: popoverPosition.anchorVertical, }} animationIn="fadeIn" disableAnimation={false} shouldSetModalVisibility={false} fullscreen - withoutOverlay={menuState?.withoutOverlay ?? true} + withoutOverlay={withoutOverlay} anchorDimensions={{ - width: menuState?.position.anchorWidth ?? 0, - height: menuState?.position.anchorHeight ?? 0, + width: popoverPosition.anchorWidth, + height: popoverPosition.anchorHeight, }} anchorRef={anchorRef} - shouldSwitchPositionIfOverflow={menuState?.isOverflowMenu ?? false} + shouldSwitchPositionIfOverflow={isOverflowMenu} > - {menuState?.type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && ( + {type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && ( )} - {menuState?.type === CONST.CONTEXT_MENU_TYPES.REPORT && ( + {type === CONST.CONTEXT_MENU_TYPES.REPORT && ( )} - {menuState?.type === CONST.CONTEXT_MENU_TYPES.LINK && ( + {type === CONST.CONTEXT_MENU_TYPES.LINK && ( )} - {menuState?.type === CONST.CONTEXT_MENU_TYPES.EMAIL && ( + {type === CONST.CONTEXT_MENU_TYPES.EMAIL && ( )} - {menuState?.type === CONST.CONTEXT_MENU_TYPES.TEXT && ( + {type === CONST.CONTEXT_MENU_TYPES.TEXT && ( )} @@ -403,4 +384,4 @@ function PopoverContextMenu({ref: forwardedRef}: PopoverContextMenuProps) { PopoverContextMenu.displayName = 'PopoverContextMenu'; export default PopoverContextMenu; -export type {PopoverPosition, PopoverContextMenuState}; +export type {PopoverPosition}; From b0817319f09cd47b7c2cb7e05359995d6ed0a293 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 9 Mar 2026 23:44:57 -0700 Subject: [PATCH 68/88] Fix pretter --- tests/unit/ContextMenuActionsCopyMessageTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/ContextMenuActionsCopyMessageTest.ts b/tests/unit/ContextMenuActionsCopyMessageTest.ts index bc973274828c..b1c53d9a70e4 100644 --- a/tests/unit/ContextMenuActionsCopyMessageTest.ts +++ b/tests/unit/ContextMenuActionsCopyMessageTest.ts @@ -1,6 +1,6 @@ +import type {LocalizedTranslate} from '@components/LocaleContextProvider'; import Clipboard from '@libs/Clipboard'; import getClipboardText from '@libs/Clipboard/getClipboardText'; -import type {LocalizedTranslate} from '@components/LocaleContextProvider'; import {copyMessageToClipboard} from '@pages/inbox/report/ContextMenu/actions/copyMessageAction'; import CONST from '@src/CONST'; import type {ReportAction} from '@src/types/onyx'; From e06611c86a694a45988af78a9eb2704df79cab6b Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 11 Mar 2026 23:10:02 -0300 Subject: [PATCH 69/88] Port main's functional changes to decomposed action files - replyInThreadAction: pass introSelected to navigateToAndOpenChildReport - explainAction: pass introSelected to explain - joinThreadAction/leaveThreadAction: pass introSelected to toggleSubscribeToChildReport - copyMessageAction: strip followup list from HTML before clipboard copy - ConfirmDeleteReportActionModal: convert deleteAppReport to object params, add personalPolicy/translate/toLocaleDigit - PopoverReportActionContent/MiniReportActionContextMenu: pass introSelected through to action creators Made-with: Cursor --- .../MiniReportActionContextMenu/index.tsx | 26 +++++++++++++++++-- .../ConfirmDeleteReportActionModal.tsx | 18 +++++++++++-- .../PopoverReportActionContent.tsx | 26 +++++++++++++++++-- .../ContextMenu/actions/copyMessageAction.ts | 4 ++- .../ContextMenu/actions/explainAction.ts | 24 ++++++++++++++--- .../ContextMenu/actions/joinThreadAction.ts | 7 ++--- .../ContextMenu/actions/leaveThreadAction.ts | 7 ++--- .../actions/replyInThreadAction.ts | 6 +++-- 8 files changed, 100 insertions(+), 18 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index 7fcceb0d72c8..0135ff3443de 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -258,6 +258,7 @@ function MiniReportActionContextMenu() { reportAction, originalReport, currentUserAccountID, + introSelected, hideAndRun, translate, chatBubbleReplyIcon: icons.ChatBubbleReply, @@ -285,6 +286,7 @@ function MiniReportActionContextMenu() { originalReport, reportAction, currentUserPersonalDetails, + introSelected, hideAndRun, translate, conciergeIcon: icons.Concierge, @@ -330,10 +332,30 @@ function MiniReportActionContextMenu() { ); } if (showJoinThread) { - allVisibleActions.push(createJoinThreadAction({reportAction, originalReport, currentUserAccountID, hideAndRun, translate, bellIcon: icons.Bell})); + allVisibleActions.push( + createJoinThreadAction({ + reportAction, + originalReport, + currentUserAccountID, + introSelected, + hideAndRun, + translate, + bellIcon: icons.Bell, + }), + ); } if (showLeaveThread) { - allVisibleActions.push(createLeaveThreadAction({reportAction, originalReport, currentUserAccountID, hideAndRun, translate, exitIcon: icons.Exit})); + allVisibleActions.push( + createLeaveThreadAction({ + reportAction, + originalReport, + currentUserAccountID, + introSelected, + hideAndRun, + translate, + exitIcon: icons.Exit, + }), + ); } if (showCopyMessage) { allVisibleActions.push( diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/ConfirmDeleteReportActionModal.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/ConfirmDeleteReportActionModal.tsx index d31f794f4482..8bf79a867c3d 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/ConfirmDeleteReportActionModal.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/ConfirmDeleteReportActionModal.tsx @@ -11,6 +11,7 @@ import useDuplicateTransactionsAndViolations from '@hooks/useDuplicateTransactio import useGetIOUReportFromReportAction from '@hooks/useGetIOUReportFromReportAction'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; +import usePersonalPolicy from '@hooks/usePersonalPolicy'; import useReportIsArchived from '@hooks/useReportIsArchived'; import useTransactionsAndViolationsForReport from '@hooks/useTransactionsAndViolationsForReport'; import {deleteTrackExpense} from '@libs/actions/IOU'; @@ -26,7 +27,8 @@ type ConfirmDeleteReportActionModalProps = ModalProps & { }; function ConfirmDeleteReportActionModal({closeModal, reportID, reportActionID, actionSourceReportID}: ConfirmDeleteReportActionModalProps) { - const {translate} = useLocalize(); + const {translate, toLocaleDigit} = useLocalize(); + const personalPolicy = usePersonalPolicy(); const {email, accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); const {currentSearchHash} = useSearchStateContext(); @@ -94,7 +96,19 @@ function ConfirmDeleteReportActionModal({closeModal, reportID, reportActionID, a deleteTransactions([originalMessage.IOUTransactionID], duplicateTransactions, duplicateTransactionViolations, currentSearchHash); } } else if (isReportPreviewAction(reportAction)) { - deleteAppReport(childReport, selfDMReport, email ?? '', currentUserAccountID, reportTransactions, allTransactionViolations, bankAccountList, currentSearchHash); + deleteAppReport({ + report: childReport, + selfDMReport, + currentUserEmailParam: email ?? '', + currentUserAccountIDParam: currentUserAccountID, + reportTransactions, + allTransactionViolations, + bankAccountList, + personalPolicy, + translate, + toLocaleDigit, + hash: currentSearchHash, + }); } else if (reportAction) { // eslint-disable-next-line @typescript-eslint/no-deprecated InteractionManager.runAfterInteractions(() => { diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx index 6c8bea919693..a85525bd74d9 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx @@ -234,6 +234,7 @@ function PopoverReportActionContent({ reportAction, originalReport, currentUserAccountID, + introSelected, hideAndRun, translate, chatBubbleReplyIcon: icons.ChatBubbleReply, @@ -261,6 +262,7 @@ function PopoverReportActionContent({ originalReport, reportAction, currentUserPersonalDetails, + introSelected, hideAndRun, translate, conciergeIcon: icons.Concierge, @@ -306,10 +308,30 @@ function PopoverReportActionContent({ ); } if (showJoinThread) { - visibleActions.push(createJoinThreadAction({reportAction, originalReport, currentUserAccountID, hideAndRun, translate, bellIcon: icons.Bell})); + visibleActions.push( + createJoinThreadAction({ + reportAction, + originalReport, + currentUserAccountID, + introSelected, + hideAndRun, + translate, + bellIcon: icons.Bell, + }), + ); } if (showLeaveThread) { - visibleActions.push(createLeaveThreadAction({reportAction, originalReport, currentUserAccountID, hideAndRun, translate, exitIcon: icons.Exit})); + visibleActions.push( + createLeaveThreadAction({ + reportAction, + originalReport, + currentUserAccountID, + introSelected, + hideAndRun, + translate, + exitIcon: icons.Exit, + }), + ); } if (showCopyMessage) { visibleActions.push( diff --git a/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.ts b/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.ts index b7492669793a..3bd2bd6a20bd 100644 --- a/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.ts @@ -10,6 +10,7 @@ import {getForReportActionTemp} from '@libs/ModifiedExpenseMessage'; import Parser from '@libs/Parser'; import {getCleanedTagName, isPolicyAdmin} from '@libs/PolicyUtils'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import stripFollowupListFromHtml from '@libs/ReportActionFollowupUtils/stripFollowupListFromHtml'; import { getActionableCardFraudAlertMessage, getActionableMentionWhisperMessage, @@ -526,8 +527,9 @@ function copyMessageToClipboard(params: CopyMessageClipboardParams) { ); setClipboardMessage(displayMessage); } else if (content) { + const contentWithoutFollowups = stripFollowupListFromHtml(content) ?? content; setClipboardMessage( - content.replaceAll(/()(.*?)(<\/mention-user>)/gi, (match, openTag: string, innerContent: string, closeTag: string): string => { + contentWithoutFollowups.replaceAll(/()(.*?)(<\/mention-user>)/gi, (match, openTag: string, innerContent: string, closeTag: string): string => { const modifiedContent = Str.removeSMSDomain(innerContent) || ''; return openTag + modifiedContent + closeTag || ''; }), diff --git a/src/pages/inbox/report/ContextMenu/actions/explainAction.ts b/src/pages/inbox/report/ContextMenu/actions/explainAction.ts index a80a11a127cd..03d2fe225175 100644 --- a/src/pages/inbox/report/ContextMenu/actions/explainAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/explainAction.ts @@ -4,7 +4,7 @@ import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {hasReasoning} from '@libs/ReportActionsUtils'; import {explain} from '@userActions/Report'; import CONST from '@src/CONST'; -import type {ReportAction, Report as ReportType} from '@src/types/onyx'; +import type {IntroSelected, ReportAction, Report as ReportType} from '@src/types/onyx'; import type IconAsset from '@src/types/utils/IconAsset'; import KeyboardUtils from '@src/utils/keyboard'; import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; @@ -14,6 +14,7 @@ type ExplainActionParams = BaseContextMenuActionParams & { originalReport: OnyxEntry; reportAction: ReportAction; currentUserPersonalDetails: ReturnType; + introSelected: OnyxEntry; hideAndRun: (callback?: () => void) => void; conciergeIcon: IconAsset; }; @@ -25,7 +26,16 @@ function shouldShowExplainAction({reportAction, isArchivedRoom}: {reportAction: return hasReasoning(reportAction); } -function createExplainAction({childReport, originalReport, reportAction, currentUserPersonalDetails, hideAndRun, translate, conciergeIcon}: ExplainActionParams): ContextMenuAction { +function createExplainAction({ + childReport, + originalReport, + reportAction, + currentUserPersonalDetails, + introSelected, + hideAndRun, + translate, + conciergeIcon, +}: ExplainActionParams): ContextMenuAction { return { id: 'explain', icon: conciergeIcon, @@ -37,7 +47,15 @@ function createExplainAction({childReport, originalReport, reportAction, current } hideAndRun(() => { KeyboardUtils.dismiss().then(() => - explain(childReport, originalReport, reportAction, translate, currentUserPersonalDetails?.accountID ?? CONST.DEFAULT_NUMBER_ID, currentUserPersonalDetails?.timezone), + explain( + childReport, + originalReport, + reportAction, + translate, + currentUserPersonalDetails?.accountID ?? CONST.DEFAULT_NUMBER_ID, + introSelected, + currentUserPersonalDetails?.timezone, + ), ); }); }), diff --git a/src/pages/inbox/report/ContextMenu/actions/joinThreadAction.ts b/src/pages/inbox/report/ContextMenu/actions/joinThreadAction.ts index 35b5becdc480..15c3c46622c5 100644 --- a/src/pages/inbox/report/ContextMenu/actions/joinThreadAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/joinThreadAction.ts @@ -5,7 +5,7 @@ import {isActionableTrackExpense, isCreatedAction, isCreatedTaskReportAction, is import {getChildReportNotificationPreference, shouldDisableThread, shouldDisplayThreadReplies} from '@libs/ReportUtils'; import {toggleSubscribeToChildReport} from '@userActions/Report'; import CONST from '@src/CONST'; -import type {ReportAction, Report as ReportType} from '@src/types/onyx'; +import type {IntroSelected, ReportAction, Report as ReportType} from '@src/types/onyx'; import type IconAsset from '@src/types/utils/IconAsset'; import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; @@ -13,6 +13,7 @@ type JoinThreadActionParams = BaseContextMenuActionParams & { reportAction: ReportAction; originalReport: OnyxEntry; currentUserAccountID: number; + introSelected: OnyxEntry; hideAndRun: (callback?: () => void) => void; bellIcon: IconAsset; }; @@ -49,7 +50,7 @@ function shouldShowJoinThreadAction({ ); } -function createJoinThreadAction({reportAction, originalReport, currentUserAccountID, hideAndRun, translate, bellIcon}: JoinThreadActionParams): ContextMenuAction { +function createJoinThreadAction({reportAction, originalReport, currentUserAccountID, introSelected, hideAndRun, translate, bellIcon}: JoinThreadActionParams): ContextMenuAction { return { id: 'joinThread', icon: bellIcon, @@ -59,7 +60,7 @@ function createJoinThreadAction({reportAction, originalReport, currentUserAccoun const childReportNotificationPreference = getChildReportNotificationPreference(reportAction); hideAndRun(() => { ReportActionComposeFocusManager.focus(); - toggleSubscribeToChildReport(reportAction?.childReportID, currentUserAccountID, reportAction, originalReport, childReportNotificationPreference); + toggleSubscribeToChildReport(reportAction?.childReportID, currentUserAccountID, reportAction, originalReport, introSelected, childReportNotificationPreference); }); }, false), sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.JOIN_THREAD, diff --git a/src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.ts b/src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.ts index 13911318d3ac..11f459483231 100644 --- a/src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.ts @@ -5,7 +5,7 @@ import {isActionableTrackExpense, isCreatedAction, isCreatedTaskReportAction, is import {getChildReportNotificationPreference, shouldDisplayThreadReplies} from '@libs/ReportUtils'; import {toggleSubscribeToChildReport} from '@userActions/Report'; import CONST from '@src/CONST'; -import type {ReportAction, Report as ReportType} from '@src/types/onyx'; +import type {IntroSelected, ReportAction, Report as ReportType} from '@src/types/onyx'; import type IconAsset from '@src/types/utils/IconAsset'; import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; @@ -13,6 +13,7 @@ type LeaveThreadActionParams = BaseContextMenuActionParams & { reportAction: ReportAction; originalReport: OnyxEntry; currentUserAccountID: number; + introSelected: OnyxEntry; hideAndRun: (callback?: () => void) => void; exitIcon: IconAsset; }; @@ -47,7 +48,7 @@ function shouldShowLeaveThreadAction({ ); } -function createLeaveThreadAction({reportAction, originalReport, currentUserAccountID, hideAndRun, translate, exitIcon}: LeaveThreadActionParams): ContextMenuAction { +function createLeaveThreadAction({reportAction, originalReport, currentUserAccountID, introSelected, hideAndRun, translate, exitIcon}: LeaveThreadActionParams): ContextMenuAction { return { id: 'leaveThread', icon: exitIcon, @@ -57,7 +58,7 @@ function createLeaveThreadAction({reportAction, originalReport, currentUserAccou const childReportNotificationPreference = getChildReportNotificationPreference(reportAction); hideAndRun(() => { ReportActionComposeFocusManager.focus(); - toggleSubscribeToChildReport(reportAction?.childReportID, currentUserAccountID, reportAction, originalReport, childReportNotificationPreference); + toggleSubscribeToChildReport(reportAction?.childReportID, currentUserAccountID, reportAction, originalReport, introSelected, childReportNotificationPreference); }); }, false), sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.LEAVE_THREAD, diff --git a/src/pages/inbox/report/ContextMenu/actions/replyInThreadAction.ts b/src/pages/inbox/report/ContextMenu/actions/replyInThreadAction.ts index e02d5212b542..78a8a59cd59e 100644 --- a/src/pages/inbox/report/ContextMenu/actions/replyInThreadAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/replyInThreadAction.ts @@ -3,7 +3,7 @@ import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {shouldDisableThread} from '@libs/ReportUtils'; import {navigateToAndOpenChildReport} from '@userActions/Report'; import CONST from '@src/CONST'; -import type {ReportAction, Report as ReportType} from '@src/types/onyx'; +import type {IntroSelected, ReportAction, Report as ReportType} from '@src/types/onyx'; import type IconAsset from '@src/types/utils/IconAsset'; import KeyboardUtils from '@src/utils/keyboard'; import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; @@ -13,6 +13,7 @@ type ReplyInThreadActionParams = BaseContextMenuActionParams & { reportAction: ReportAction; originalReport: OnyxEntry; currentUserAccountID: number; + introSelected: OnyxEntry; hideAndRun: (callback?: () => void) => void; chatBubbleReplyIcon: IconAsset; }; @@ -39,6 +40,7 @@ function createReplyInThreadAction({ reportAction, originalReport, currentUserAccountID, + introSelected, hideAndRun, translate, chatBubbleReplyIcon, @@ -51,7 +53,7 @@ function createReplyInThreadAction({ interceptAnonymousUser(() => { hideAndRun(() => { KeyboardUtils.dismiss().then(() => { - navigateToAndOpenChildReport(childReport, reportAction, originalReport, currentUserAccountID); + navigateToAndOpenChildReport(childReport, reportAction, originalReport, currentUserAccountID, introSelected); }); }); }, false), From 78b7b62b14d7c025995eaefb6bc73dbe4015532f Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 6 Apr 2026 13:31:46 -0700 Subject: [PATCH 70/88] refactor(contextmenu): convert action files to composition components Replace factory function pattern (.ts) with self-contained React components (.tsx) for each context menu action. Each action exports Popover*Item and Mini*Item components that manage their own icons, translations, and success states. - MiniContextMenuItem: add optional icon/successIcon/successTooltipText props with useThrottledButtonState for success feedback - Popover actions use ContextMenuItem (success states) or FocusableMenuItem - Mini actions use enhanced MiniContextMenuItem with icon swap - Consumer components (PopoverReportActionContent, PopoverReportContent, MiniReportActionContextMenu) render explicit JSX instead of .map() - Remove dead code: ContextMenuAction type, BaseContextMenuActionParams, CONTEXT_MENU_ICON_NAMES array, all create* factories - Delete overflowMenuAction.ts (logic inlined into consumers) - Pass introSelected to actions requiring it (main merge port) - Add stripFollowupListFromHtml to copy message clipboard logic Made-with: Cursor --- src/components/MiniContextMenuItem.tsx | 75 +++- .../MiniReportActionContextMenu/index.tsx | 333 +++++++++-------- .../PopoverReportActionContent.tsx | 342 +++++++++--------- .../PopoverReportContent.tsx | 138 ++++--- .../ContextMenu/actions/actionConfig.ts | 47 +-- .../ContextMenu/actions/copyLinkAction.ts | 50 --- .../ContextMenu/actions/copyLinkAction.tsx | 90 +++++ ...MessageAction.ts => copyMessageAction.tsx} | 157 ++++++-- .../ContextMenu/actions/copyOnyxDataAction.ts | 39 -- .../actions/copyOnyxDataAction.tsx | 49 +++ .../report/ContextMenu/actions/debugAction.ts | 45 --- .../ContextMenu/actions/debugAction.tsx | 56 +++ .../ContextMenu/actions/deleteAction.ts | 68 ---- .../ContextMenu/actions/deleteAction.tsx | 114 ++++++ .../ContextMenu/actions/downloadAction.ts | 60 --- .../ContextMenu/actions/downloadAction.tsx | 104 ++++++ .../report/ContextMenu/actions/editAction.ts | 67 ---- .../report/ContextMenu/actions/editAction.tsx | 129 +++++++ .../ContextMenu/actions/explainAction.ts | 67 ---- .../ContextMenu/actions/explainAction.tsx | 121 +++++++ .../actions/flagAsOffensiveAction.ts | 53 --- .../actions/flagAsOffensiveAction.tsx | 103 ++++++ .../report/ContextMenu/actions/holdAction.ts | 57 --- .../report/ContextMenu/actions/holdAction.tsx | 107 ++++++ .../ContextMenu/actions/joinThreadAction.ts | 71 ---- .../ContextMenu/actions/joinThreadAction.tsx | 122 +++++++ .../ContextMenu/actions/leaveThreadAction.ts | 69 ---- .../ContextMenu/actions/leaveThreadAction.tsx | 120 ++++++ .../ContextMenu/actions/markAsReadAction.ts | 35 -- .../ContextMenu/actions/markAsReadAction.tsx | 45 +++ .../ContextMenu/actions/markAsUnreadAction.ts | 54 --- .../actions/markAsUnreadAction.tsx | 84 +++++ .../ContextMenu/actions/overflowMenuAction.ts | 29 -- .../report/ContextMenu/actions/pinAction.ts | 33 -- .../report/ContextMenu/actions/pinAction.tsx | 44 +++ .../actions/replyInThreadAction.ts | 65 ---- .../actions/replyInThreadAction.tsx | 104 ++++++ .../ContextMenu/actions/unholdAction.ts | 57 --- .../ContextMenu/actions/unholdAction.tsx | 107 ++++++ .../report/ContextMenu/actions/unpinAction.ts | 33 -- .../ContextMenu/actions/unpinAction.tsx | 44 +++ 41 files changed, 2150 insertions(+), 1437 deletions(-) delete mode 100644 src/pages/inbox/report/ContextMenu/actions/copyLinkAction.ts create mode 100644 src/pages/inbox/report/ContextMenu/actions/copyLinkAction.tsx rename src/pages/inbox/report/ContextMenu/actions/{copyMessageAction.ts => copyMessageAction.tsx} (89%) delete mode 100644 src/pages/inbox/report/ContextMenu/actions/copyOnyxDataAction.ts create mode 100644 src/pages/inbox/report/ContextMenu/actions/copyOnyxDataAction.tsx delete mode 100644 src/pages/inbox/report/ContextMenu/actions/debugAction.ts create mode 100644 src/pages/inbox/report/ContextMenu/actions/debugAction.tsx delete mode 100644 src/pages/inbox/report/ContextMenu/actions/deleteAction.ts create mode 100644 src/pages/inbox/report/ContextMenu/actions/deleteAction.tsx delete mode 100644 src/pages/inbox/report/ContextMenu/actions/downloadAction.ts create mode 100644 src/pages/inbox/report/ContextMenu/actions/downloadAction.tsx delete mode 100644 src/pages/inbox/report/ContextMenu/actions/editAction.ts create mode 100644 src/pages/inbox/report/ContextMenu/actions/editAction.tsx delete mode 100644 src/pages/inbox/report/ContextMenu/actions/explainAction.ts create mode 100644 src/pages/inbox/report/ContextMenu/actions/explainAction.tsx delete mode 100644 src/pages/inbox/report/ContextMenu/actions/flagAsOffensiveAction.ts create mode 100644 src/pages/inbox/report/ContextMenu/actions/flagAsOffensiveAction.tsx delete mode 100644 src/pages/inbox/report/ContextMenu/actions/holdAction.ts create mode 100644 src/pages/inbox/report/ContextMenu/actions/holdAction.tsx delete mode 100644 src/pages/inbox/report/ContextMenu/actions/joinThreadAction.ts create mode 100644 src/pages/inbox/report/ContextMenu/actions/joinThreadAction.tsx delete mode 100644 src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.ts create mode 100644 src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.tsx delete mode 100644 src/pages/inbox/report/ContextMenu/actions/markAsReadAction.ts create mode 100644 src/pages/inbox/report/ContextMenu/actions/markAsReadAction.tsx delete mode 100644 src/pages/inbox/report/ContextMenu/actions/markAsUnreadAction.ts create mode 100644 src/pages/inbox/report/ContextMenu/actions/markAsUnreadAction.tsx delete mode 100644 src/pages/inbox/report/ContextMenu/actions/overflowMenuAction.ts delete mode 100644 src/pages/inbox/report/ContextMenu/actions/pinAction.ts create mode 100644 src/pages/inbox/report/ContextMenu/actions/pinAction.tsx delete mode 100644 src/pages/inbox/report/ContextMenu/actions/replyInThreadAction.ts create mode 100644 src/pages/inbox/report/ContextMenu/actions/replyInThreadAction.tsx delete mode 100644 src/pages/inbox/report/ContextMenu/actions/unholdAction.ts create mode 100644 src/pages/inbox/report/ContextMenu/actions/unholdAction.tsx delete mode 100644 src/pages/inbox/report/ContextMenu/actions/unpinAction.ts create mode 100644 src/pages/inbox/report/ContextMenu/actions/unpinAction.tsx diff --git a/src/components/MiniContextMenuItem.tsx b/src/components/MiniContextMenuItem.tsx index da13c219fb30..6bc9cb781206 100644 --- a/src/components/MiniContextMenuItem.tsx +++ b/src/components/MiniContextMenuItem.tsx @@ -3,12 +3,15 @@ import type {PressableStateCallbackType} from 'react-native'; import {View} from 'react-native'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; +import useThrottledButtonState from '@hooks/useThrottledButtonState'; import DomUtils from '@libs/DomUtils'; import getButtonState from '@libs/getButtonState'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import variables from '@styles/variables'; import CONST from '@src/CONST'; +import type IconAsset from '@src/types/utils/IconAsset'; import type WithSentryLabel from '@src/types/utils/SentryLabel'; +import Icon from './Icon'; import type {PressableRef} from './Pressable/GenericPressable/types'; import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback'; import Tooltip from './Tooltip/PopoverAnchorTooltip'; @@ -25,9 +28,28 @@ type MiniContextMenuItemProps = WithSentryLabel & { onPress: () => void; /** - * The children to display within the menu item + * The children to display within the menu item. + * Used when custom rendering is needed (e.g. overflow button). + * Mutually exclusive with `icon`. */ - children: React.ReactNode | ((state: PressableStateCallbackType) => React.ReactNode); + children?: React.ReactNode | ((state: PressableStateCallbackType) => React.ReactNode); + + /** + * Icon to display. When provided, the component renders an Icon internally + * instead of using children. + */ + icon?: IconAsset; + + /** + * Icon to show after a successful press. Requires `icon` to be set. + * When provided, the component manages a throttled success state internally. + */ + successIcon?: IconAsset; + + /** + * Tooltip text to show during the success state. + */ + successTooltipText?: string; /** * Whether the button should be in the active state @@ -48,17 +70,45 @@ type MiniContextMenuItemProps = WithSentryLabel & { * Component that renders a mini context menu item with a * pressable. Also renders a tooltip when hovering the item. */ -function MiniContextMenuItem({tooltipText, onPress, children, isDelayButtonStateComplete = true, shouldPreventDefaultFocusOnPress = true, ref, sentryLabel}: MiniContextMenuItemProps) { +function MiniContextMenuItem({ + tooltipText, + onPress, + children, + icon, + successIcon, + successTooltipText, + isDelayButtonStateComplete = true, + shouldPreventDefaultFocusOnPress = true, + ref, + sentryLabel, +}: MiniContextMenuItemProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); + const [isThrottledButtonActive, setThrottledButtonInactive] = useThrottledButtonState(); + + const showSuccessState = !!successIcon && !isThrottledButtonActive; + const displayIcon = showSuccessState ? successIcon : icon; + const displayTooltip = showSuccessState && successTooltipText ? successTooltipText : tooltipText; + const isComplete = showSuccessState || isDelayButtonStateComplete; + + const handlePress = () => { + if (successIcon && !isThrottledButtonActive) { + return; + } + onPress(); + if (successIcon) { + setThrottledButtonInactive(); + } + }; + return ( { if (!ReportActionComposeFocusManager.isFocused() && !ReportActionComposeFocusManager.isEditFocused()) { const activeElement = DomUtils.getActiveElement(); @@ -78,18 +128,25 @@ function MiniContextMenuItem({tooltipText, onPress, children, isDelayButtonState event.preventDefault(); } }} - accessibilityLabel={tooltipText} + accessibilityLabel={displayTooltip} role={CONST.ROLE.BUTTON} sentryLabel={sentryLabel} style={({hovered, pressed}) => [ styles.reportActionContextMenuMiniButton, - StyleUtils.getButtonBackgroundColorStyle(getButtonState(hovered, pressed, isDelayButtonStateComplete), true), - isDelayButtonStateComplete && styles.cursorDefault, + StyleUtils.getButtonBackgroundColorStyle(getButtonState(hovered, pressed, isComplete), true), + isComplete && styles.cursorDefault, ]} > {(pressableState) => ( - {typeof children === 'function' ? children(pressableState) : children} + {!!displayIcon && ( + + )} + {!displayIcon && (typeof children === 'function' ? children(pressableState) : children)} )} diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index 0135ff3443de..5988239d3056 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -6,30 +6,28 @@ import {StyleSheet, View} from 'react-native'; import type {GestureResponderEvent} from 'react-native'; import * as ActionSheetAwareScrollView from '@components/ActionSheetAwareScrollView'; import Hoverable from '@components/Hoverable'; -import Icon from '@components/Icon'; import MiniContextMenuItem from '@components/MiniContextMenuItem'; import MiniQuickEmojiReactions from '@components/Reactions/MiniQuickEmojiReactions'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; -import getButtonState from '@libs/getButtonState'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import {ACTION_IDS, CONTEXT_MENU_ICON_NAMES} from '@pages/inbox/report/ContextMenu/actions/actionConfig'; -import type {ContextMenuAction} from '@pages/inbox/report/ContextMenu/actions/actionConfig'; -import createCopyLinkAction, {shouldShowCopyLinkAction} from '@pages/inbox/report/ContextMenu/actions/copyLinkAction'; -import createCopyMessageAction, {shouldShowCopyMessageAction} from '@pages/inbox/report/ContextMenu/actions/copyMessageAction'; -import createDeleteAction, {shouldShowDeleteAction} from '@pages/inbox/report/ContextMenu/actions/deleteAction'; -import createDownloadAction, {shouldShowDownloadAction} from '@pages/inbox/report/ContextMenu/actions/downloadAction'; -import createEditAction, {shouldShowEditAction} from '@pages/inbox/report/ContextMenu/actions/editAction'; +import {ACTION_IDS} from '@pages/inbox/report/ContextMenu/actions/actionConfig'; +import {MiniCopyLinkItem, shouldShowCopyLinkAction} from '@pages/inbox/report/ContextMenu/actions/copyLinkAction'; +import {MiniCopyMessageItem, shouldShowCopyMessageAction} from '@pages/inbox/report/ContextMenu/actions/copyMessageAction'; +import {MiniDeleteItem, shouldShowDeleteAction} from '@pages/inbox/report/ContextMenu/actions/deleteAction'; +import {MiniDownloadItem, shouldShowDownloadAction} from '@pages/inbox/report/ContextMenu/actions/downloadAction'; +import {MiniEditItem, shouldShowEditAction} from '@pages/inbox/report/ContextMenu/actions/editAction'; import createEmojiReactionData, {shouldShowEmojiReaction} from '@pages/inbox/report/ContextMenu/actions/emojiReactionAction'; -import createExplainAction, {shouldShowExplainAction} from '@pages/inbox/report/ContextMenu/actions/explainAction'; -import createFlagAsOffensiveAction, {shouldShowFlagAsOffensiveAction} from '@pages/inbox/report/ContextMenu/actions/flagAsOffensiveAction'; -import createHoldAction, {shouldShowHoldAction} from '@pages/inbox/report/ContextMenu/actions/holdAction'; -import createJoinThreadAction, {shouldShowJoinThreadAction} from '@pages/inbox/report/ContextMenu/actions/joinThreadAction'; -import createLeaveThreadAction, {shouldShowLeaveThreadAction} from '@pages/inbox/report/ContextMenu/actions/leaveThreadAction'; -import createMarkAsUnreadAction, {shouldShowMarkAsUnreadForReportAction} from '@pages/inbox/report/ContextMenu/actions/markAsUnreadAction'; -import createReplyInThreadAction, {shouldShowReplyInThreadAction} from '@pages/inbox/report/ContextMenu/actions/replyInThreadAction'; -import createUnholdAction, {shouldShowUnholdAction} from '@pages/inbox/report/ContextMenu/actions/unholdAction'; +import {MiniExplainItem, shouldShowExplainAction} from '@pages/inbox/report/ContextMenu/actions/explainAction'; +import {MiniFlagAsOffensiveItem, shouldShowFlagAsOffensiveAction} from '@pages/inbox/report/ContextMenu/actions/flagAsOffensiveAction'; +import {MiniHoldItem, shouldShowHoldAction} from '@pages/inbox/report/ContextMenu/actions/holdAction'; +import {MiniJoinThreadItem, shouldShowJoinThreadAction} from '@pages/inbox/report/ContextMenu/actions/joinThreadAction'; +import {MiniLeaveThreadItem, shouldShowLeaveThreadAction} from '@pages/inbox/report/ContextMenu/actions/leaveThreadAction'; +import {MiniMarkAsUnreadItem, shouldShowMarkAsUnreadForReportAction} from '@pages/inbox/report/ContextMenu/actions/markAsUnreadAction'; +import {MiniReplyInThreadItem, shouldShowReplyInThreadAction} from '@pages/inbox/report/ContextMenu/actions/replyInThreadAction'; +import {MiniUnholdItem, shouldShowUnholdAction} from '@pages/inbox/report/ContextMenu/actions/unholdAction'; import {useMiniContextMenuActions, useMiniContextMenuState} from '@pages/inbox/report/ContextMenu/MiniContextMenuProvider'; import type {ContextMenuAnchor} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {showContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; @@ -52,9 +50,10 @@ function MiniReportActionContextMenu() { const {hideMiniContextMenu, keepOpen, release} = useMiniContextMenuActions(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const StyleUtils = useStyleUtils(); + const {translate} = useLocalize(); ActionSheetAwareScrollView.useActionSheetAwareScrollViewActions(); - const icons = useMemoizedLazyExpensifyIcons(CONTEXT_MENU_ICON_NAMES); + const overflowIcons = useMemoizedLazyExpensifyIcons(['ThreeDots'] as const); const threeDotRef = useRef(null); const overlayRef = useRef(null); const menuContainerRef = useRef(null); @@ -133,10 +132,8 @@ function MiniReportActionContextMenu() { movedFromReport, movedToReport, harvestReport, - download, disabledActionIDs, showDelegateNoAccessModal, - translate, getLocalDateFromDatetime, reportID: resolvedReportID, originalReportID: resolvedOriginalReportID, @@ -185,21 +182,21 @@ function MiniReportActionContextMenu() { const currentUserAccountID = currentUserPersonalDetails?.accountID ?? CONST.DEFAULT_NUMBER_ID; - const isDisabled = (id: string) => disabledActionIDs.has(id); + const isDisabledAction = (id: string) => disabledActionIDs.has(id); const showReplyInThread = - !isDisabled(ACTION_IDS.REPLY_IN_THREAD) && + !isDisabledAction(ACTION_IDS.REPLY_IN_THREAD) && shouldShowReplyInThreadAction({ reportAction, reportID: resolvedReportID, isThreadReportParentAction, isArchivedRoom, }); - const showMarkAsUnread = !isDisabled(ACTION_IDS.MARK_AS_UNREAD) && shouldShowMarkAsUnreadForReportAction({reportAction}); - const showExplain = !isDisabled(ACTION_IDS.EXPLAIN) && shouldShowExplainAction({reportAction, isArchivedRoom}); - const showEdit = !isDisabled(ACTION_IDS.EDIT) && shouldShowEditAction({reportAction, isArchivedRoom, isChronosReport, moneyRequestAction}); + const showMarkAsUnread = !isDisabledAction(ACTION_IDS.MARK_AS_UNREAD) && shouldShowMarkAsUnreadForReportAction({reportAction}); + const showExplain = !isDisabledAction(ACTION_IDS.EXPLAIN) && shouldShowExplainAction({reportAction, isArchivedRoom}); + const showEdit = !isDisabledAction(ACTION_IDS.EDIT) && shouldShowEditAction({reportAction, isArchivedRoom, isChronosReport, moneyRequestAction}); const showUnhold = - !isDisabled(ACTION_IDS.UNHOLD) && + !isDisabledAction(ACTION_IDS.UNHOLD) && shouldShowUnholdAction({ moneyRequestReport, moneyRequestAction, @@ -208,7 +205,7 @@ function MiniReportActionContextMenu() { iouTransaction, }); const showHold = - !isDisabled(ACTION_IDS.HOLD) && + !isDisabledAction(ACTION_IDS.HOLD) && shouldShowHoldAction({ moneyRequestReport, moneyRequestAction, @@ -217,7 +214,7 @@ function MiniReportActionContextMenu() { iouTransaction, }); const showJoinThread = - !isDisabled(ACTION_IDS.JOIN_THREAD) && + !isDisabledAction(ACTION_IDS.JOIN_THREAD) && shouldShowJoinThreadAction({ reportAction, isArchivedRoom, @@ -225,19 +222,20 @@ function MiniReportActionContextMenu() { isHarvestReport, }); const showLeaveThread = - !isDisabled(ACTION_IDS.LEAVE_THREAD) && + !isDisabledAction(ACTION_IDS.LEAVE_THREAD) && shouldShowLeaveThreadAction({ reportAction, isArchivedRoom, isThreadReportParentAction, isHarvestReport, }); - const showCopyMessage = !isDisabled(ACTION_IDS.COPY_MESSAGE) && shouldShowCopyMessageAction({reportAction}); - const showCopyLink = !isDisabled(ACTION_IDS.COPY_LINK) && shouldShowCopyLinkAction({reportAction, menuTarget: resolvedAnchor}); - const showFlagAsOffensive = !isDisabled(ACTION_IDS.FLAG_AS_OFFENSIVE) && shouldShowFlagAsOffensiveAction({reportAction, isArchivedRoom, isChronosReport, reportID: resolvedReportID}); - const showDownload = !isDisabled(ACTION_IDS.DOWNLOAD) && shouldShowDownloadAction({reportAction, isOffline}); + const showCopyMessage = !isDisabledAction(ACTION_IDS.COPY_MESSAGE) && shouldShowCopyMessageAction({reportAction}); + const showCopyLink = !isDisabledAction(ACTION_IDS.COPY_LINK) && shouldShowCopyLinkAction({reportAction, menuTarget: resolvedAnchor}); + const showFlagAsOffensive = + !isDisabledAction(ACTION_IDS.FLAG_AS_OFFENSIVE) && shouldShowFlagAsOffensiveAction({reportAction, isArchivedRoom, isChronosReport, reportID: resolvedReportID}); + const showDownload = !isDisabledAction(ACTION_IDS.DOWNLOAD) && shouldShowDownloadAction({reportAction, isOffline}); const showDelete = - !isDisabled(ACTION_IDS.DELETE) && + !isDisabledAction(ACTION_IDS.DELETE) && shouldShowDeleteAction({ reportAction, isArchivedRoom, @@ -249,155 +247,171 @@ function MiniReportActionContextMenu() { childReportActions, }); - const allVisibleActions: ContextMenuAction[] = []; + const allVisibleItems: React.ReactElement[] = []; if (reportAction) { if (showReplyInThread) { - allVisibleActions.push( - createReplyInThreadAction({ - childReport, - reportAction, - originalReport, - currentUserAccountID, - introSelected, - hideAndRun, - translate, - chatBubbleReplyIcon: icons.ChatBubbleReply, - }), + allVisibleItems.push( + , ); } if (showMarkAsUnread) { - allVisibleActions.push( - createMarkAsUnreadAction({ - reportID: resolvedReportID, - reportActions: reportActionsMap, - reportAction, - currentUserAccountID, - hideAndRun, - translate, - chatBubbleUnreadIcon: icons.ChatBubbleUnread, - checkmarkIcon: icons.Checkmark, - }), + allVisibleItems.push( + , ); } if (showExplain) { - allVisibleActions.push( - createExplainAction({ - childReport, - originalReport, - reportAction, - currentUserPersonalDetails, - introSelected, - hideAndRun, - translate, - conciergeIcon: icons.Concierge, - }), + allVisibleItems.push( + , ); } if (showEdit) { - allVisibleActions.push( - createEditAction({ - reportID: resolvedReportID, - reportAction, - moneyRequestAction, - draftMessage: resolvedDraftMessage, - introSelected, - hideAndRun, - translate, - pencilIcon: icons.Pencil, - }), + allVisibleItems.push( + , ); } if (showUnhold) { - allVisibleActions.push( - createUnholdAction({ - moneyRequestAction, - isDelegateAccessRestricted, - showDelegateNoAccessModal, - hideAndRun, - translate, - stopwatchIcon: icons.Stopwatch, - }), + allVisibleItems.push( + , ); } if (showHold) { - allVisibleActions.push( - createHoldAction({ - moneyRequestAction, - isDelegateAccessRestricted, - showDelegateNoAccessModal, - hideAndRun, - translate, - stopwatchIcon: icons.Stopwatch, - }), + allVisibleItems.push( + , ); } if (showJoinThread) { - allVisibleActions.push( - createJoinThreadAction({ - reportAction, - originalReport, - currentUserAccountID, - introSelected, - hideAndRun, - translate, - bellIcon: icons.Bell, - }), + allVisibleItems.push( + , ); } if (showLeaveThread) { - allVisibleActions.push( - createLeaveThreadAction({ - reportAction, - originalReport, - currentUserAccountID, - introSelected, - hideAndRun, - translate, - exitIcon: icons.Exit, - }), + allVisibleItems.push( + , ); } if (showCopyMessage) { - allVisibleActions.push( - createCopyMessageAction({ - reportAction, - transaction, - selection: resolvedSelection, - report, - card, - originalReport, - isHarvestReport, - isTryNewDotNVPDismissed, - movedFromReport, - movedToReport, - childReport, - policy, - getLocalDateFromDatetime, - policyTags, - translate, - harvestReport, - currentUserPersonalDetails, - copyIcon: icons.Copy, - checkmarkIcon: icons.Checkmark, - }), + allVisibleItems.push( + , ); } if (showCopyLink) { - allVisibleActions.push(createCopyLinkAction({reportAction, originalReportID: resolvedOriginalReportID, translate, linkCopyIcon: icons.LinkCopy, checkmarkIcon: icons.Checkmark})); + allVisibleItems.push( + , + ); } if (showFlagAsOffensive) { - allVisibleActions.push(createFlagAsOffensiveAction({reportID: resolvedReportID, reportAction, hideAndRun, translate, flagIcon: icons.Flag})); + allVisibleItems.push( + , + ); } if (showDownload) { - allVisibleActions.push(createDownloadAction({reportAction, encryptedAuthToken, download, translate, downloadIcon: icons.Download})); + allVisibleItems.push( + , + ); } if (showDelete) { - allVisibleActions.push(createDeleteAction({reportID: resolvedReportID, reportAction, moneyRequestAction, hideAndRun, translate, trashcanIcon: icons.Trashcan})); + allVisibleItems.push( + , + ); } } - const needsOverflow = allVisibleActions.length > CONST.MINI_CONTEXT_MENU_MAX_ITEMS; - const displayedActions = needsOverflow ? allVisibleActions.slice(0, CONST.MINI_CONTEXT_MENU_MAX_ITEMS - 1) : allVisibleActions; + const needsOverflow = allVisibleItems.length > CONST.MINI_CONTEXT_MENU_MAX_ITEMS; + const displayedItems = needsOverflow ? allVisibleItems.slice(0, CONST.MINI_CONTEXT_MENU_MAX_ITEMS - 1) : allVisibleItems; const emojiData = createEmojiReactionData({ reportID: resolvedReportID, @@ -449,27 +463,12 @@ function MiniReportActionContextMenu() { reportAction={emojiData.reportAction} /> )} - {displayedActions.map((action) => ( - - {({hovered, pressed}) => ( - - )} - - ))} + {displayedItems} {needsOverflow && ( interceptAnonymousUser(() => { openOverflowMenu(new MouseEvent('click'), threeDotRef); @@ -479,15 +478,7 @@ function MiniReportActionContextMenu() { isDelayButtonStateComplete={false} shouldPreventDefaultFocusOnPress={false} sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.MENU} - > - {({hovered, pressed}) => ( - - )} - + /> )} diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx index a85525bd74d9..402a7943a36f 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx @@ -8,28 +8,28 @@ import FocusTrapForModal from '@components/FocusTrap/FocusTrapForModal'; import QuickEmojiReactions from '@components/Reactions/QuickEmojiReactions'; import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import {ACTION_IDS, CONTEXT_MENU_ICON_NAMES} from '@pages/inbox/report/ContextMenu/actions/actionConfig'; -import type {ContextMenuAction} from '@pages/inbox/report/ContextMenu/actions/actionConfig'; -import createCopyLinkAction, {shouldShowCopyLinkAction} from '@pages/inbox/report/ContextMenu/actions/copyLinkAction'; -import createCopyMessageAction, {shouldShowCopyMessageAction} from '@pages/inbox/report/ContextMenu/actions/copyMessageAction'; -import createDebugAction, {shouldShowDebugAction} from '@pages/inbox/report/ContextMenu/actions/debugAction'; -import createDeleteAction, {shouldShowDeleteAction} from '@pages/inbox/report/ContextMenu/actions/deleteAction'; -import createDownloadAction, {shouldShowDownloadAction} from '@pages/inbox/report/ContextMenu/actions/downloadAction'; -import createEditAction, {shouldShowEditAction} from '@pages/inbox/report/ContextMenu/actions/editAction'; +import {ACTION_IDS} from '@pages/inbox/report/ContextMenu/actions/actionConfig'; +import {PopoverCopyLinkItem, shouldShowCopyLinkAction} from '@pages/inbox/report/ContextMenu/actions/copyLinkAction'; +import {PopoverCopyMessageItem, shouldShowCopyMessageAction} from '@pages/inbox/report/ContextMenu/actions/copyMessageAction'; +import {PopoverDebugItem, shouldShowDebugAction} from '@pages/inbox/report/ContextMenu/actions/debugAction'; +import {PopoverDeleteItem, shouldShowDeleteAction} from '@pages/inbox/report/ContextMenu/actions/deleteAction'; +import {PopoverDownloadItem, shouldShowDownloadAction} from '@pages/inbox/report/ContextMenu/actions/downloadAction'; +import {PopoverEditItem, shouldShowEditAction} from '@pages/inbox/report/ContextMenu/actions/editAction'; import createEmojiReactionData, {shouldShowEmojiReaction} from '@pages/inbox/report/ContextMenu/actions/emojiReactionAction'; -import createExplainAction, {shouldShowExplainAction} from '@pages/inbox/report/ContextMenu/actions/explainAction'; -import createFlagAsOffensiveAction, {shouldShowFlagAsOffensiveAction} from '@pages/inbox/report/ContextMenu/actions/flagAsOffensiveAction'; -import createHoldAction, {shouldShowHoldAction} from '@pages/inbox/report/ContextMenu/actions/holdAction'; -import createJoinThreadAction, {shouldShowJoinThreadAction} from '@pages/inbox/report/ContextMenu/actions/joinThreadAction'; -import createLeaveThreadAction, {shouldShowLeaveThreadAction} from '@pages/inbox/report/ContextMenu/actions/leaveThreadAction'; -import createMarkAsUnreadAction, {shouldShowMarkAsUnreadForReportAction} from '@pages/inbox/report/ContextMenu/actions/markAsUnreadAction'; -import createReplyInThreadAction, {shouldShowReplyInThreadAction} from '@pages/inbox/report/ContextMenu/actions/replyInThreadAction'; -import createUnholdAction, {shouldShowUnholdAction} from '@pages/inbox/report/ContextMenu/actions/unholdAction'; +import {PopoverExplainItem, shouldShowExplainAction} from '@pages/inbox/report/ContextMenu/actions/explainAction'; +import {PopoverFlagAsOffensiveItem, shouldShowFlagAsOffensiveAction} from '@pages/inbox/report/ContextMenu/actions/flagAsOffensiveAction'; +import {PopoverHoldItem, shouldShowHoldAction} from '@pages/inbox/report/ContextMenu/actions/holdAction'; +import {PopoverJoinThreadItem, shouldShowJoinThreadAction} from '@pages/inbox/report/ContextMenu/actions/joinThreadAction'; +import {PopoverLeaveThreadItem, shouldShowLeaveThreadAction} from '@pages/inbox/report/ContextMenu/actions/leaveThreadAction'; +import {PopoverMarkAsUnreadItem, shouldShowMarkAsUnreadForReportAction} from '@pages/inbox/report/ContextMenu/actions/markAsUnreadAction'; +import {PopoverReplyInThreadItem, shouldShowReplyInThreadAction} from '@pages/inbox/report/ContextMenu/actions/replyInThreadAction'; +import {PopoverUnholdItem, shouldShowUnholdAction} from '@pages/inbox/report/ContextMenu/actions/unholdAction'; import {showContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import useReportActionContextMenuData from '@pages/inbox/report/ContextMenu/useReportActionContextMenuData'; import CONST from '@src/CONST'; @@ -65,7 +65,8 @@ function PopoverReportActionContent({ const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {windowWidth} = useWindowDimensions(); - const icons = useMemoizedLazyExpensifyIcons(CONTEXT_MENU_ICON_NAMES); + const {translate} = useLocalize(); + const overflowIcons = useMemoizedLazyExpensifyIcons(['ThreeDots'] as const); const { report, @@ -101,7 +102,6 @@ function PopoverReportActionContent({ download, disabledActionIDs, showDelegateNoAccessModal, - translate, getLocalDateFromDatetime, reportID: resolvedReportID, originalReportID: resolvedOriginalReportID, @@ -209,169 +209,195 @@ function PopoverReportActionContent({ childReportActions, }); - const overflowAction: ContextMenuAction = { - id: 'overflowMenu', - icon: icons.ThreeDots, - text: translate('reportActionContextMenu.menu'), - isAnonymousAction: true, - shouldPreventDefaultFocusOnPress: false, - onPress: (event) => - interceptAnonymousUser(() => { - openOverflowMenu(event as GestureResponderEvent | MouseEvent); - setLocalShouldKeepOpen(true); - }, true), - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.MENU, - }; - - const visibleActions: ContextMenuAction[] = []; + const visibleItems: React.ReactElement[] = []; if (!reportAction) { - visibleActions.push(overflowAction); + visibleItems.push( + + interceptAnonymousUser(() => { + openOverflowMenu(event as GestureResponderEvent | MouseEvent); + setLocalShouldKeepOpen(true); + }, true) + } + isAnonymousAction + wrapperStyle={[styles.pr8]} + style={StyleUtils.getContextMenuItemStyles(windowWidth)} + interactive + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.MENU} + />, + ); } else { if (showReplyInThread) { - visibleActions.push( - createReplyInThreadAction({ - childReport, - reportAction, - originalReport, - currentUserAccountID, - introSelected, - hideAndRun, - translate, - chatBubbleReplyIcon: icons.ChatBubbleReply, - }), + visibleItems.push( + , ); } if (showMarkAsUnread) { - visibleActions.push( - createMarkAsUnreadAction({ - reportID: resolvedReportID, - reportActions: reportActionsMap, - reportAction, - currentUserAccountID, - hideAndRun, - translate, - chatBubbleUnreadIcon: icons.ChatBubbleUnread, - checkmarkIcon: icons.Checkmark, - }), + visibleItems.push( + , ); } if (showExplain) { - visibleActions.push( - createExplainAction({ - childReport, - originalReport, - reportAction, - currentUserPersonalDetails, - introSelected, - hideAndRun, - translate, - conciergeIcon: icons.Concierge, - }), + visibleItems.push( + , ); } if (showEdit) { - visibleActions.push( - createEditAction({ - reportID: resolvedReportID, - reportAction, - moneyRequestAction, - draftMessage: resolvedDraftMessage, - introSelected, - hideAndRun, - translate, - pencilIcon: icons.Pencil, - }), + visibleItems.push( + , ); } if (showUnhold) { - visibleActions.push( - createUnholdAction({ - moneyRequestAction, - isDelegateAccessRestricted, - showDelegateNoAccessModal, - hideAndRun, - translate, - stopwatchIcon: icons.Stopwatch, - }), + visibleItems.push( + , ); } if (showHold) { - visibleActions.push( - createHoldAction({ - moneyRequestAction, - isDelegateAccessRestricted, - showDelegateNoAccessModal, - hideAndRun, - translate, - stopwatchIcon: icons.Stopwatch, - }), + visibleItems.push( + , ); } if (showJoinThread) { - visibleActions.push( - createJoinThreadAction({ - reportAction, - originalReport, - currentUserAccountID, - introSelected, - hideAndRun, - translate, - bellIcon: icons.Bell, - }), + visibleItems.push( + , ); } if (showLeaveThread) { - visibleActions.push( - createLeaveThreadAction({ - reportAction, - originalReport, - currentUserAccountID, - introSelected, - hideAndRun, - translate, - exitIcon: icons.Exit, - }), + visibleItems.push( + , ); } if (showCopyMessage) { - visibleActions.push( - createCopyMessageAction({ - reportAction, - transaction, - selection: resolvedSelection, - report, - card, - originalReport, - isHarvestReport, - isTryNewDotNVPDismissed, - movedFromReport, - movedToReport, - childReport, - policy, - getLocalDateFromDatetime, - policyTags, - translate, - harvestReport, - currentUserPersonalDetails, - copyIcon: icons.Copy, - checkmarkIcon: icons.Checkmark, - }), + visibleItems.push( + , ); } if (showCopyLink) { - visibleActions.push(createCopyLinkAction({reportAction, originalReportID: resolvedOriginalReportID, translate, linkCopyIcon: icons.LinkCopy, checkmarkIcon: icons.Checkmark})); + visibleItems.push( + , + ); } if (showFlagAsOffensive) { - visibleActions.push(createFlagAsOffensiveAction({reportID: resolvedReportID, reportAction, hideAndRun, translate, flagIcon: icons.Flag})); + visibleItems.push( + , + ); } if (showDownload) { - visibleActions.push(createDownloadAction({reportAction, encryptedAuthToken, download, translate, downloadIcon: icons.Download})); + visibleItems.push( + , + ); } if (showDebug) { - visibleActions.push(createDebugAction({reportID: resolvedReportID, reportAction, translate, bugIcon: icons.Bug})); + visibleItems.push( + , + ); } if (showDelete) { - visibleActions.push(createDeleteAction({reportID: resolvedReportID, reportAction, moneyRequestAction, hideAndRun, translate, trashcanIcon: icons.Trashcan})); + visibleItems.push( + , + ); } } @@ -387,7 +413,7 @@ function PopoverReportActionContent({ const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({ initialFocusedIndex: -1, disabledIndexes: [], - maxIndex: visibleActions.length - 1, + maxIndex: visibleItems.length - 1, isActive: shouldEnableArrowNavigation, }); @@ -417,26 +443,14 @@ function PopoverReportActionContent({ }} /> )} - {visibleActions.map((action: ContextMenuAction, i: number) => ( - setFocusedIndex(i)} - onBlur={() => (i === visibleActions.length - 1 || i === 1) && setFocusedIndex(-1)} - disabled={action.disabled} - shouldShowLoadingSpinnerIcon={action.shouldShowLoadingSpinnerIcon} - sentryLabel={action.sentryLabel} - /> - ))} + {visibleItems.map((item, i) => + // eslint-disable-next-line @typescript-eslint/no-explicit-any + React.cloneElement(item as React.ReactElement, { + isFocused: focusedIndex === i, + onFocus: () => setFocusedIndex(i), + onBlur: () => (i === visibleItems.length - 1 || i === 1) && setFocusedIndex(-1), + }), + )} diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportContent.tsx index efce7bca2c93..0ad9f79835ad 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportContent.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportContent.tsx @@ -4,26 +4,20 @@ import React from 'react'; import type {View as ViewType} from 'react-native'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; -import FocusableMenuItem from '@components/FocusableMenuItem'; import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; import useEnvironment from '@hooks/useEnvironment'; -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useReportIsArchived from '@hooks/useReportIsArchived'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; -import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; import {canWriteInReport, isUnread} from '@libs/ReportUtils'; -import {ACTION_IDS, CONTEXT_MENU_ICON_NAMES, RESTRICTED_READONLY_ACTION_IDS} from '@pages/inbox/report/ContextMenu/actions/actionConfig'; -import type {ContextMenuAction} from '@pages/inbox/report/ContextMenu/actions/actionConfig'; -import createCopyOnyxDataAction, {shouldShowCopyOnyxDataAction} from '@pages/inbox/report/ContextMenu/actions/copyOnyxDataAction'; -import createDebugAction, {shouldShowDebugAction} from '@pages/inbox/report/ContextMenu/actions/debugAction'; -import createMarkAsReadAction, {shouldShowMarkAsReadAction} from '@pages/inbox/report/ContextMenu/actions/markAsReadAction'; -import createMarkAsUnreadAction, {shouldShowMarkAsUnreadForReport} from '@pages/inbox/report/ContextMenu/actions/markAsUnreadAction'; -import createPinAction, {shouldShowPinAction} from '@pages/inbox/report/ContextMenu/actions/pinAction'; -import createUnpinAction, {shouldShowUnpinAction} from '@pages/inbox/report/ContextMenu/actions/unpinAction'; +import {ACTION_IDS, RESTRICTED_READONLY_ACTION_IDS} from '@pages/inbox/report/ContextMenu/actions/actionConfig'; +import {PopoverCopyOnyxDataItem, shouldShowCopyOnyxDataAction} from '@pages/inbox/report/ContextMenu/actions/copyOnyxDataAction'; +import {PopoverDebugItem, shouldShowDebugAction} from '@pages/inbox/report/ContextMenu/actions/debugAction'; +import {PopoverMarkAsReadItem, shouldShowMarkAsReadAction} from '@pages/inbox/report/ContextMenu/actions/markAsReadAction'; +import {PopoverMarkAsUnreadItem, shouldShowMarkAsUnreadForReport} from '@pages/inbox/report/ContextMenu/actions/markAsUnreadAction'; +import {PopoverPinItem, shouldShowPinAction} from '@pages/inbox/report/ContextMenu/actions/pinAction'; +import {PopoverUnpinItem, shouldShowUnpinAction} from '@pages/inbox/report/ContextMenu/actions/unpinAction'; import ONYXKEYS from '@src/ONYXKEYS'; import type {ReportAction} from '@src/types/onyx'; @@ -40,14 +34,9 @@ const EMPTY_SET = new Set(); function PopoverReportContent({reportID, reportActionID, originalReportID, hideAndRun, contentRef, shouldEnableArrowNavigation}: PopoverReportContentProps) { const {shouldUseNarrowLayout} = useResponsiveLayout(); - const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const {windowWidth} = useWindowDimensions(); - const {translate} = useLocalize(); const {isProduction} = useEnvironment(); - const icons = useMemoizedLazyExpensifyIcons(CONTEXT_MENU_ICON_NAMES); - const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, {canEvict: false}); const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); const [isDebugModeEnabled] = useOnyx(ONYXKEYS.IS_DEBUG_MODE_ENABLED); @@ -70,49 +59,68 @@ function PopoverReportContent({reportID, reportActionID, originalReportID, hideA const showCopyOnyxData = shouldShowCopyOnyxDataAction({isProduction}) && !isDisabled(ACTION_IDS.COPY_ONYX_DATA); const showDebug = shouldShowDebugAction({isDebugModeEnabled}) && !isDisabled(ACTION_IDS.DEBUG); - const markAsReadActionItem = showMarkAsRead ? createMarkAsReadAction({reportID, hideAndRun, translate, mailIcon: icons.Mail, checkmarkIcon: icons.Checkmark}) : undefined; - const markAsUnreadActionItem = - showMarkAsUnread && reportAction - ? createMarkAsUnreadAction({ - reportID, - reportActions, - reportAction, - currentUserAccountID: 0, - hideAndRun, - translate, - chatBubbleUnreadIcon: icons.ChatBubbleUnread, - checkmarkIcon: icons.Checkmark, - }) - : undefined; - const pinActionItem = showPin ? createPinAction({reportID, hideAndRun, translate, pinIcon: icons.Pin}) : undefined; - const unpinActionItem = showUnpin ? createUnpinAction({reportID, hideAndRun, translate, pinIcon: icons.Pin}) : undefined; - const copyOnyxDataActionItem = showCopyOnyxData ? createCopyOnyxDataAction({report, translate, copyIcon: icons.Copy, checkmarkIcon: icons.Checkmark}) : undefined; - const debugActionItem = showDebug && reportAction ? createDebugAction({reportID, reportAction, translate, bugIcon: icons.Bug}) : undefined; - - const visibleActions: ContextMenuAction[] = []; - if (markAsReadActionItem) { - visibleActions.push(markAsReadActionItem); + const visibleItems: React.ReactElement[] = []; + if (showMarkAsRead) { + visibleItems.push( + , + ); } - if (markAsUnreadActionItem) { - visibleActions.push(markAsUnreadActionItem); + if (showMarkAsUnread && reportAction) { + visibleItems.push( + , + ); } - if (pinActionItem) { - visibleActions.push(pinActionItem); + if (showPin) { + visibleItems.push( + , + ); } - if (unpinActionItem) { - visibleActions.push(unpinActionItem); + if (showUnpin) { + visibleItems.push( + , + ); } - if (copyOnyxDataActionItem) { - visibleActions.push(copyOnyxDataActionItem); + if (showCopyOnyxData) { + visibleItems.push( + , + ); } - if (debugActionItem) { - visibleActions.push(debugActionItem); + if (showDebug && reportAction) { + visibleItems.push( + , + ); } const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({ initialFocusedIndex: -1, disabledIndexes: [], - maxIndex: visibleActions.length - 1, + maxIndex: visibleItems.length - 1, isActive: shouldEnableArrowNavigation, }); @@ -123,26 +131,14 @@ function PopoverReportContent({reportID, reportActionID, originalReportID, hideA ref={contentRef} style={wrapperStyle} > - {visibleActions.map((action: ContextMenuAction, i: number) => ( - setFocusedIndex(i)} - onBlur={() => (i === visibleActions.length - 1 || i === 1) && setFocusedIndex(-1)} - disabled={action.disabled} - shouldShowLoadingSpinnerIcon={action.shouldShowLoadingSpinnerIcon} - sentryLabel={action.sentryLabel} - /> - ))} + {visibleItems.map((item, i) => + // eslint-disable-next-line @typescript-eslint/no-explicit-any + React.cloneElement(item as React.ReactElement, { + isFocused: focusedIndex === i, + onFocus: () => setFocusedIndex(i), + onBlur: () => (i === visibleItems.length - 1 || i === 0) && setFocusedIndex(-1), + }), + )} ); } diff --git a/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts b/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts index 862975a6c046..53fb76297b0e 100644 --- a/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts +++ b/src/pages/inbox/report/ContextMenu/actions/actionConfig.ts @@ -1,49 +1,6 @@ -import type {GestureResponderEvent} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; -import type {LocalizedTranslate} from '@components/LocaleContextProvider'; import type {ReportAction} from '@src/types/onyx'; -import type IconAsset from '@src/types/utils/IconAsset'; - -const CONTEXT_MENU_ICON_NAMES = [ - 'Bell', - 'Bug', - 'ChatBubbleReply', - 'ChatBubbleUnread', - 'Checkmark', - 'Concierge', - 'Copy', - 'Download', - 'Exit', - 'Flag', - 'LinkCopy', - 'Mail', - 'Pencil', - 'Pin', - 'Stopwatch', - 'ThreeDots', - 'Trashcan', -] as const; - -type BaseContextMenuActionParams = { - translate: LocalizedTranslate; -}; - -/** A fully-resolved context menu action ready to be rendered. Created by the `create*Action` factory in each action module. */ -type ContextMenuAction = { - id: string; - icon: IconAsset; - text: string; - onPress: (event?: GestureResponderEvent | MouseEvent | KeyboardEvent) => void; - successIcon?: IconAsset; - successText?: string; - description?: string; - isAnonymousAction?: boolean; - disabled?: boolean; - shouldShowLoadingSpinnerIcon?: boolean; - shouldPreventDefaultFocusOnPress?: boolean; - sentryLabel: string; -}; const ACTION_IDS = { EMOJI_REACTION: 'emojiReaction', @@ -81,5 +38,5 @@ function getActionHtml(reportAction: OnyxEntry): string { /** Actions that are disabled when the user cannot write in the report. */ const RESTRICTED_READONLY_ACTION_IDS = new Set([ACTION_IDS.REPLY_IN_THREAD, ACTION_IDS.EDIT, ACTION_IDS.JOIN_THREAD, ACTION_IDS.DELETE]); -export {ACTION_IDS, CONTEXT_MENU_ICON_NAMES, RESTRICTED_READONLY_ACTION_IDS, getActionHtml}; -export type {ActionID, BaseContextMenuActionParams, ContextMenuAction}; +export {ACTION_IDS, RESTRICTED_READONLY_ACTION_IDS, getActionHtml}; +export type {ActionID}; diff --git a/src/pages/inbox/report/ContextMenu/actions/copyLinkAction.ts b/src/pages/inbox/report/ContextMenu/actions/copyLinkAction.ts deleted file mode 100644 index 1d661c04cc72..000000000000 --- a/src/pages/inbox/report/ContextMenu/actions/copyLinkAction.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type {RefObject} from 'react'; -import type {OnyxEntry} from 'react-native-onyx'; -import Clipboard from '@libs/Clipboard'; -import {getEnvironmentURL} from '@libs/Environment/Environment'; -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import {isActionOfType, isMessageDeleted, isReportActionAttachment} from '@libs/ReportActionsUtils'; -import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; -import type {ContextMenuAnchor} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; -import CONST from '@src/CONST'; -import type {ReportAction} from '@src/types/onyx'; -import type IconAsset from '@src/types/utils/IconAsset'; -import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; - -type CopyLinkActionParams = BaseContextMenuActionParams & { - reportAction: ReportAction; - originalReportID: string | undefined; - linkCopyIcon: IconAsset; - checkmarkIcon: IconAsset; -}; - -function shouldShowCopyLinkAction({reportAction, menuTarget}: {reportAction: OnyxEntry; menuTarget: RefObject | undefined}): boolean { - const isAttachment = isReportActionAttachment(reportAction); - const isAttachmentTarget = menuTarget?.current && 'tagName' in menuTarget.current && menuTarget?.current.tagName === 'IMG' && isAttachment; - const isDEWRouted = isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.DYNAMIC_EXTERNAL_WORKFLOW_ROUTED); - return !isAttachmentTarget && !isMessageDeleted(reportAction) && !isDEWRouted; -} - -function createCopyLinkAction({reportAction, originalReportID, translate, linkCopyIcon, checkmarkIcon}: CopyLinkActionParams): ContextMenuAction { - return { - id: 'copyLink', - icon: linkCopyIcon, - text: translate('reportActionContextMenu.copyLink'), - successText: translate('reportActionContextMenu.copied'), - successIcon: checkmarkIcon, - isAnonymousAction: true, - onPress: () => - interceptAnonymousUser(() => { - getEnvironmentURL().then((environmentURL) => { - const reportActionID = reportAction?.reportActionID; - Clipboard.setString(`${environmentURL}/r/${originalReportID}/${reportActionID}`); - }); - hideContextMenu(true, ReportActionComposeFocusManager.focus); - }, true), - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_LINK, - }; -} - -export default createCopyLinkAction; -export {shouldShowCopyLinkAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/copyLinkAction.tsx b/src/pages/inbox/report/ContextMenu/actions/copyLinkAction.tsx new file mode 100644 index 000000000000..1c986c46070b --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/copyLinkAction.tsx @@ -0,0 +1,90 @@ +import type {RefObject} from 'react'; +import React from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import ContextMenuItem from '@components/ContextMenuItem'; +import MiniContextMenuItem from '@components/MiniContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import Clipboard from '@libs/Clipboard'; +import {getEnvironmentURL} from '@libs/Environment/Environment'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {isActionOfType, isMessageDeleted, isReportActionAttachment} from '@libs/ReportActionsUtils'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import type {ContextMenuAnchor} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import CONST from '@src/CONST'; +import type {ReportAction} from '@src/types/onyx'; + +type PopoverCopyLinkItemProps = { + reportAction: ReportAction; + originalReportID: string | undefined; + isFocused?: boolean; + onFocus?: () => void; + onBlur?: () => void; +}; + +function PopoverCopyLinkItem({reportAction, originalReportID, isFocused, onFocus, onBlur}: PopoverCopyLinkItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['LinkCopy', 'Checkmark'] as const); + + return ( + + interceptAnonymousUser(() => { + getEnvironmentURL().then((environmentURL) => { + const reportActionID = reportAction?.reportActionID; + Clipboard.setString(`${environmentURL}/r/${originalReportID}/${reportActionID}`); + }); + hideContextMenu(true, ReportActionComposeFocusManager.focus); + }, true) + } + isAnonymousAction + isFocused={isFocused} + onFocus={onFocus} + onBlur={onBlur} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_LINK} + /> + ); +} + +type MiniCopyLinkItemProps = { + reportAction: ReportAction; + originalReportID: string | undefined; +}; + +function MiniCopyLinkItem({reportAction, originalReportID}: MiniCopyLinkItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['LinkCopy', 'Checkmark'] as const); + + return ( + + interceptAnonymousUser(() => { + getEnvironmentURL().then((environmentURL) => { + const reportActionID = reportAction?.reportActionID; + Clipboard.setString(`${environmentURL}/r/${originalReportID}/${reportActionID}`); + }); + hideContextMenu(true, ReportActionComposeFocusManager.focus); + }, true) + } + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_LINK} + /> + ); +} + +function shouldShowCopyLinkAction({reportAction, menuTarget}: {reportAction: OnyxEntry; menuTarget: RefObject | undefined}): boolean { + const isAttachment = isReportActionAttachment(reportAction); + const isAttachmentTarget = menuTarget?.current && 'tagName' in menuTarget.current && menuTarget?.current.tagName === 'IMG' && isAttachment; + const isDEWRouted = isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.DYNAMIC_EXTERNAL_WORKFLOW_ROUTED); + return !isAttachmentTarget && !isMessageDeleted(reportAction) && !isDEWRouted; +} + +export {shouldShowCopyLinkAction, PopoverCopyLinkItem, MiniCopyLinkItem}; diff --git a/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.ts b/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.tsx similarity index 89% rename from src/pages/inbox/report/ContextMenu/actions/copyMessageAction.ts rename to src/pages/inbox/report/ContextMenu/actions/copyMessageAction.tsx index 3bd2bd6a20bd..d53dc862a32a 100644 --- a/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.ts +++ b/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.tsx @@ -1,7 +1,12 @@ import {Str} from 'expensify-common'; +import React from 'react'; import type {OnyxEntry} from 'react-native-onyx'; +import ContextMenuItem from '@components/ContextMenuItem'; import type {LocaleContextProps, LocalizedTranslate} from '@components/LocaleContextProvider'; +import MiniContextMenuItem from '@components/MiniContextMenuItem'; import type useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; import Clipboard from '@libs/Clipboard'; import getClipboardText from '@libs/Clipboard/getClipboardText'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; @@ -147,9 +152,7 @@ import {getTaskCreatedMessage, getTaskReportActionMessage} from '@libs/TaskUtils import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import type {Card, Policy, PolicyTagLists, ReportAction, Report as ReportType, Transaction} from '@src/types/onyx'; -import type IconAsset from '@src/types/utils/IconAsset'; import {getActionHtml} from './actionConfig'; -import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; type CopyMessageClipboardParams = { reportAction: ReportAction; @@ -171,12 +174,6 @@ type CopyMessageClipboardParams = { currentUserPersonalDetails: ReturnType; }; -type CopyMessageActionParams = BaseContextMenuActionParams & - CopyMessageClipboardParams & { - copyIcon: IconAsset; - checkmarkIcon: IconAsset; - }; - function shouldShowCopyMessageAction({reportAction}: {reportAction: OnyxEntry}): boolean { return !isReportActionAttachment(reportAction) && !isMessageDeleted(reportAction) && !isTripPreview(reportAction); } @@ -542,22 +539,132 @@ function copyMessageToClipboard(params: CopyMessageClipboardParams) { } } -function createCopyMessageAction({translate, copyIcon, checkmarkIcon, ...clipboardParams}: CopyMessageActionParams): ContextMenuAction { - return { - id: 'copyMessage', - icon: copyIcon, - text: translate('reportActionContextMenu.copyMessage'), - successText: translate('reportActionContextMenu.copied'), - successIcon: checkmarkIcon, - isAnonymousAction: true, - onPress: () => - interceptAnonymousUser(() => { - copyMessageToClipboard({...clipboardParams, translate}); - hideContextMenu(true, ReportActionComposeFocusManager.focus); - }, true), - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_MESSAGE, - }; +type PopoverCopyMessageItemProps = Omit & { + isFocused?: boolean; + onFocus?: () => void; + onBlur?: () => void; +}; + +function PopoverCopyMessageItem({ + reportAction, + transaction, + selection, + report, + card, + originalReport, + isHarvestReport, + isTryNewDotNVPDismissed, + movedFromReport, + movedToReport, + childReport, + policy, + getLocalDateFromDatetime, + policyTags, + harvestReport, + currentUserPersonalDetails, + isFocused, + onFocus, + onBlur, +}: PopoverCopyMessageItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Copy', 'Checkmark'] as const); + + return ( + + interceptAnonymousUser(() => { + copyMessageToClipboard({ + reportAction, + transaction, + selection, + report, + card, + originalReport, + isHarvestReport, + isTryNewDotNVPDismissed, + movedFromReport, + movedToReport, + childReport, + policy, + getLocalDateFromDatetime, + policyTags, + translate, + harvestReport, + currentUserPersonalDetails, + }); + hideContextMenu(true, ReportActionComposeFocusManager.focus); + }, true) + } + isAnonymousAction + isFocused={isFocused} + onFocus={onFocus} + onBlur={onBlur} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_MESSAGE} + /> + ); +} + +type MiniCopyMessageItemProps = Omit; + +function MiniCopyMessageItem({ + reportAction, + transaction, + selection, + report, + card, + originalReport, + isHarvestReport, + isTryNewDotNVPDismissed, + movedFromReport, + movedToReport, + childReport, + policy, + getLocalDateFromDatetime, + policyTags, + harvestReport, + currentUserPersonalDetails, +}: MiniCopyMessageItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Copy', 'Checkmark'] as const); + + return ( + + interceptAnonymousUser(() => { + copyMessageToClipboard({ + reportAction, + transaction, + selection, + report, + card, + originalReport, + isHarvestReport, + isTryNewDotNVPDismissed, + movedFromReport, + movedToReport, + childReport, + policy, + getLocalDateFromDatetime, + policyTags, + translate, + harvestReport, + currentUserPersonalDetails, + }); + hideContextMenu(true, ReportActionComposeFocusManager.focus); + }, true) + } + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_MESSAGE} + /> + ); } -export default createCopyMessageAction; -export {shouldShowCopyMessageAction, copyMessageToClipboard}; +export {shouldShowCopyMessageAction, copyMessageToClipboard, PopoverCopyMessageItem, MiniCopyMessageItem}; +export type {CopyMessageClipboardParams}; diff --git a/src/pages/inbox/report/ContextMenu/actions/copyOnyxDataAction.ts b/src/pages/inbox/report/ContextMenu/actions/copyOnyxDataAction.ts deleted file mode 100644 index 5c0bb132698e..000000000000 --- a/src/pages/inbox/report/ContextMenu/actions/copyOnyxDataAction.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type {OnyxEntry} from 'react-native-onyx'; -import Clipboard from '@libs/Clipboard'; -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; -import CONST from '@src/CONST'; -import type {Report} from '@src/types/onyx'; -import type IconAsset from '@src/types/utils/IconAsset'; -import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; - -type CopyOnyxDataActionParams = BaseContextMenuActionParams & { - report: OnyxEntry; - copyIcon: IconAsset; - checkmarkIcon: IconAsset; -}; - -function shouldShowCopyOnyxDataAction({isProduction}: {isProduction: boolean}): boolean { - return !isProduction; -} - -function createCopyOnyxDataAction({report, translate, copyIcon, checkmarkIcon}: CopyOnyxDataActionParams): ContextMenuAction { - return { - id: 'copyOnyxData', - icon: copyIcon, - text: translate('reportActionContextMenu.copyOnyxData'), - successText: translate('reportActionContextMenu.copied'), - successIcon: checkmarkIcon, - isAnonymousAction: true, - onPress: () => - interceptAnonymousUser(() => { - Clipboard.setString(JSON.stringify(report, null, 4)); - hideContextMenu(true, ReportActionComposeFocusManager.focus); - }, true), - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_ONYX_DATA, - }; -} - -export default createCopyOnyxDataAction; -export {shouldShowCopyOnyxDataAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/copyOnyxDataAction.tsx b/src/pages/inbox/report/ContextMenu/actions/copyOnyxDataAction.tsx new file mode 100644 index 000000000000..8458ee6c3bca --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/copyOnyxDataAction.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import ContextMenuItem from '@components/ContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import Clipboard from '@libs/Clipboard'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import CONST from '@src/CONST'; +import type {Report} from '@src/types/onyx'; + +type PopoverCopyOnyxDataItemProps = { + report: OnyxEntry; + isFocused?: boolean; + onFocus?: () => void; + onBlur?: () => void; +}; + +function PopoverCopyOnyxDataItem({report, isFocused, onFocus, onBlur}: PopoverCopyOnyxDataItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Copy', 'Checkmark'] as const); + + return ( + + interceptAnonymousUser(() => { + Clipboard.setString(JSON.stringify(report, null, 4)); + hideContextMenu(true, ReportActionComposeFocusManager.focus); + }, true) + } + isAnonymousAction + isFocused={isFocused} + onFocus={onFocus} + onBlur={onBlur} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_ONYX_DATA} + /> + ); +} + +function shouldShowCopyOnyxDataAction({isProduction}: {isProduction: boolean}): boolean { + return !isProduction; +} + +export {shouldShowCopyOnyxDataAction, PopoverCopyOnyxDataItem}; diff --git a/src/pages/inbox/report/ContextMenu/actions/debugAction.ts b/src/pages/inbox/report/ContextMenu/actions/debugAction.ts deleted file mode 100644 index 6b28562aec82..000000000000 --- a/src/pages/inbox/report/ContextMenu/actions/debugAction.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type {OnyxEntry} from 'react-native-onyx'; -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import Navigation from '@libs/Navigation/Navigation'; -import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; -import CONST from '@src/CONST'; -import ROUTES from '@src/ROUTES'; -import type {ReportAction} from '@src/types/onyx'; -import type IconAsset from '@src/types/utils/IconAsset'; -import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; - -type DebugActionParams = BaseContextMenuActionParams & { - reportID: string | undefined; - reportAction: ReportAction; - bugIcon: IconAsset; -}; - -function shouldShowDebugAction({isDebugModeEnabled}: {isDebugModeEnabled: OnyxEntry}): boolean { - return !!isDebugModeEnabled; -} - -function createDebugAction({reportID, reportAction, translate, bugIcon}: DebugActionParams): ContextMenuAction { - return { - id: 'debug', - icon: bugIcon, - text: translate('debug.debug'), - isAnonymousAction: true, - onPress: () => - interceptAnonymousUser(() => { - if (!reportID) { - return; - } - if (reportAction) { - Navigation.navigate(ROUTES.DEBUG_REPORT_ACTION.getRoute(reportID, reportAction.reportActionID)); - } else { - Navigation.navigate(ROUTES.DEBUG_REPORT.getRoute(reportID)); - } - hideContextMenu(false, ReportActionComposeFocusManager.focus); - }, true), - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.DEBUG, - }; -} - -export default createDebugAction; -export {shouldShowDebugAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/debugAction.tsx b/src/pages/inbox/report/ContextMenu/actions/debugAction.tsx new file mode 100644 index 000000000000..cce251823750 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/debugAction.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import ContextMenuItem from '@components/ContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import Navigation from '@libs/Navigation/Navigation'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; +import type {ReportAction} from '@src/types/onyx'; + +type PopoverDebugItemProps = { + reportID: string | undefined; + reportAction: ReportAction; + isFocused?: boolean; + onFocus?: () => void; + onBlur?: () => void; +}; + +function PopoverDebugItem({reportID, reportAction, isFocused, onFocus, onBlur}: PopoverDebugItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Bug'] as const); + + return ( + + interceptAnonymousUser(() => { + if (!reportID) { + return; + } + if (reportAction) { + Navigation.navigate(ROUTES.DEBUG_REPORT_ACTION.getRoute(reportID, reportAction.reportActionID)); + } else { + Navigation.navigate(ROUTES.DEBUG_REPORT.getRoute(reportID)); + } + hideContextMenu(false, ReportActionComposeFocusManager.focus); + }, true) + } + isAnonymousAction + isFocused={isFocused} + onFocus={onFocus} + onBlur={onBlur} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.DEBUG} + /> + ); +} + +function shouldShowDebugAction({isDebugModeEnabled}: {isDebugModeEnabled: OnyxEntry}): boolean { + return !!isDebugModeEnabled; +} + +export {shouldShowDebugAction, PopoverDebugItem}; diff --git a/src/pages/inbox/report/ContextMenu/actions/deleteAction.ts b/src/pages/inbox/report/ContextMenu/actions/deleteAction.ts deleted file mode 100644 index c383d3a6ff40..000000000000 --- a/src/pages/inbox/report/ContextMenu/actions/deleteAction.ts +++ /dev/null @@ -1,68 +0,0 @@ -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import {getOriginalMessage, isMessageDeleted, isMoneyRequestAction, isReportPreviewAction} from '@libs/ReportActionsUtils'; -import {canDeleteReportAction} from '@libs/ReportUtils'; -import {showDeleteModal} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; -import CONST from '@src/CONST'; -import type {ReportAction, Transaction} from '@src/types/onyx'; -import type IconAsset from '@src/types/utils/IconAsset'; -import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; - -type DeleteActionParams = BaseContextMenuActionParams & { - reportID: string | undefined; - reportAction: ReportAction; - moneyRequestAction: ReportAction | undefined; - hideAndRun: (callback?: () => void) => void; - trashcanIcon: IconAsset; -}; - -function shouldShowDeleteAction({ - reportAction, - isArchivedRoom, - isChronosReport, - reportID, - moneyRequestAction, - iouTransaction, - transactions, - childReportActions, -}: { - reportAction: OnyxEntry; - isArchivedRoom: boolean; - isChronosReport: boolean; - reportID: string | undefined; - moneyRequestAction: ReportAction | undefined; - iouTransaction: OnyxEntry; - transactions: OnyxCollection | undefined; - childReportActions: OnyxCollection; -}): boolean { - let effectiveReportID: string | undefined = reportID; - if (isMoneyRequestAction(moneyRequestAction)) { - effectiveReportID = getOriginalMessage(moneyRequestAction)?.IOUReportID; - } else if (isReportPreviewAction(reportAction)) { - effectiveReportID = reportAction?.childReportID; - } - return ( - !!reportID && - canDeleteReportAction(moneyRequestAction ?? reportAction, effectiveReportID, iouTransaction, transactions, childReportActions) && - !isArchivedRoom && - !isChronosReport && - !isMessageDeleted(reportAction) - ); -} - -function createDeleteAction({reportID, reportAction, moneyRequestAction, hideAndRun, translate, trashcanIcon}: DeleteActionParams): ContextMenuAction { - return { - id: 'delete', - icon: trashcanIcon, - text: translate('common.delete'), - onPress: () => { - const iouReportID = isMoneyRequestAction(moneyRequestAction) ? getOriginalMessage(moneyRequestAction)?.IOUReportID : undefined; - const effectiveReportID = iouReportID && Number(iouReportID) !== 0 ? iouReportID : reportID; - const actionSourceID = effectiveReportID !== reportID ? reportID : undefined; - hideAndRun(() => showDeleteModal(effectiveReportID, moneyRequestAction ?? reportAction, undefined, undefined, undefined, actionSourceID)); - }, - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.DELETE, - }; -} - -export default createDeleteAction; -export {shouldShowDeleteAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/deleteAction.tsx b/src/pages/inbox/report/ContextMenu/actions/deleteAction.tsx new file mode 100644 index 000000000000..e1c2f42cdcdf --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/deleteAction.tsx @@ -0,0 +1,114 @@ +import React from 'react'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import FocusableMenuItem from '@components/FocusableMenuItem'; +import MiniContextMenuItem from '@components/MiniContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import {getOriginalMessage, isMessageDeleted, isMoneyRequestAction, isReportPreviewAction} from '@libs/ReportActionsUtils'; +import {canDeleteReportAction} from '@libs/ReportUtils'; +import {showDeleteModal} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import CONST from '@src/CONST'; +import type {ReportAction, Transaction} from '@src/types/onyx'; + +type PopoverDeleteItemProps = { + reportID: string | undefined; + reportAction: ReportAction; + moneyRequestAction: ReportAction | undefined; + hideAndRun: (callback?: () => void) => void; + isFocused?: boolean; + onFocus?: () => void; + onBlur?: () => void; +}; + +function PopoverDeleteItem({reportID, reportAction, moneyRequestAction, hideAndRun, isFocused, onFocus, onBlur}: PopoverDeleteItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Trashcan'] as const); + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const {windowWidth} = useWindowDimensions(); + + return ( + { + const iouReportID = isMoneyRequestAction(moneyRequestAction) ? getOriginalMessage(moneyRequestAction)?.IOUReportID : undefined; + const effectiveReportID = iouReportID && Number(iouReportID) !== 0 ? iouReportID : reportID; + const actionSourceID = effectiveReportID !== reportID ? reportID : undefined; + hideAndRun(() => showDeleteModal(effectiveReportID, moneyRequestAction ?? reportAction, undefined, undefined, undefined, actionSourceID)); + }} + wrapperStyle={[styles.pr8]} + style={StyleUtils.getContextMenuItemStyles(windowWidth)} + focused={isFocused} + interactive + onFocus={onFocus} + onBlur={onBlur} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.DELETE} + /> + ); +} + +type MiniDeleteItemProps = { + reportID: string | undefined; + reportAction: ReportAction; + moneyRequestAction: ReportAction | undefined; + hideAndRun: (callback?: () => void) => void; +}; + +function MiniDeleteItem({reportID, reportAction, moneyRequestAction, hideAndRun}: MiniDeleteItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Trashcan'] as const); + + return ( + { + const iouReportID = isMoneyRequestAction(moneyRequestAction) ? getOriginalMessage(moneyRequestAction)?.IOUReportID : undefined; + const effectiveReportID = iouReportID && Number(iouReportID) !== 0 ? iouReportID : reportID; + const actionSourceID = effectiveReportID !== reportID ? reportID : undefined; + hideAndRun(() => showDeleteModal(effectiveReportID, moneyRequestAction ?? reportAction, undefined, undefined, undefined, actionSourceID)); + }} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.DELETE} + /> + ); +} + +function shouldShowDeleteAction({ + reportAction, + isArchivedRoom, + isChronosReport, + reportID, + moneyRequestAction, + iouTransaction, + transactions, + childReportActions, +}: { + reportAction: OnyxEntry; + isArchivedRoom: boolean; + isChronosReport: boolean; + reportID: string | undefined; + moneyRequestAction: ReportAction | undefined; + iouTransaction: OnyxEntry; + transactions: OnyxCollection | undefined; + childReportActions: OnyxCollection; +}): boolean { + let effectiveReportID: string | undefined = reportID; + if (isMoneyRequestAction(moneyRequestAction)) { + effectiveReportID = getOriginalMessage(moneyRequestAction)?.IOUReportID; + } else if (isReportPreviewAction(reportAction)) { + effectiveReportID = reportAction?.childReportID; + } + return ( + !!reportID && + canDeleteReportAction(moneyRequestAction ?? reportAction, effectiveReportID, iouTransaction, transactions, childReportActions) && + !isArchivedRoom && + !isChronosReport && + !isMessageDeleted(reportAction) + ); +} + +export {shouldShowDeleteAction, PopoverDeleteItem, MiniDeleteItem}; diff --git a/src/pages/inbox/report/ContextMenu/actions/downloadAction.ts b/src/pages/inbox/report/ContextMenu/actions/downloadAction.ts deleted file mode 100644 index f3a9c58c5f29..000000000000 --- a/src/pages/inbox/report/ContextMenu/actions/downloadAction.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type {OnyxEntry} from 'react-native-onyx'; -import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; -import {isMobileSafari} from '@libs/Browser'; -import fileDownload from '@libs/fileDownload'; -import getAttachmentDetails from '@libs/fileDownload/getAttachmentDetails'; -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import {isMessageDeleted, isReportActionAttachment} from '@libs/ReportActionsUtils'; -import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; -import {setDownload} from '@userActions/Download'; -import CONST from '@src/CONST'; -import type {Download as DownloadOnyx, ReportAction} from '@src/types/onyx'; -import type IconAsset from '@src/types/utils/IconAsset'; -import {getActionHtml} from './actionConfig'; -import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; - -type DownloadActionParams = BaseContextMenuActionParams & { - reportAction: ReportAction; - encryptedAuthToken: string; - download: OnyxEntry; - downloadIcon: IconAsset; -}; - -function shouldShowDownloadAction({reportAction, isOffline}: {reportAction: OnyxEntry; isOffline: boolean}): boolean { - const isAttachment = isReportActionAttachment(reportAction); - const html = getActionHtml(reportAction); - const isUploading = html.includes(CONST.ATTACHMENT_OPTIMISTIC_SOURCE_ATTRIBUTE); - return isAttachment && !isUploading && !!reportAction?.reportActionID && !isMessageDeleted(reportAction) && !isOffline; -} - -function createDownloadAction({reportAction, encryptedAuthToken, download, translate, downloadIcon}: DownloadActionParams): ContextMenuAction { - const isDownloading = download?.isDownloading ?? false; - - return { - id: 'download', - icon: downloadIcon, - text: translate('common.download'), - successText: translate('common.download'), - successIcon: downloadIcon, - isAnonymousAction: true, - disabled: isDownloading, - shouldShowLoadingSpinnerIcon: isDownloading, - onPress: () => - interceptAnonymousUser(() => { - const html = getActionHtml(reportAction); - const {originalFileName, sourceURL} = getAttachmentDetails(html); - const sourceURLWithAuth = addEncryptedAuthTokenToURL(sourceURL ?? '', encryptedAuthToken); - const sourceID = (sourceURL?.match(CONST.REGEX.ATTACHMENT_ID) ?? [])[1]; - setDownload(sourceID, true); - const anchorRegex = CONST.REGEX_LINK_IN_ANCHOR; - const isAnchorTag = anchorRegex.test(html); - fileDownload(translate, sourceURLWithAuth, originalFileName ?? '', '', isAnchorTag && isMobileSafari()).then(() => setDownload(sourceID, false)); - hideContextMenu(true, ReportActionComposeFocusManager.focus); - }, true), - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.DOWNLOAD, - }; -} - -export default createDownloadAction; -export {shouldShowDownloadAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/downloadAction.tsx b/src/pages/inbox/report/ContextMenu/actions/downloadAction.tsx new file mode 100644 index 000000000000..fee1133b8cee --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/downloadAction.tsx @@ -0,0 +1,104 @@ +import React from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import ContextMenuItem from '@components/ContextMenuItem'; +import MiniContextMenuItem from '@components/MiniContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; +import {isMobileSafari} from '@libs/Browser'; +import fileDownload from '@libs/fileDownload'; +import getAttachmentDetails from '@libs/fileDownload/getAttachmentDetails'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {isMessageDeleted, isReportActionAttachment} from '@libs/ReportActionsUtils'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import {setDownload} from '@userActions/Download'; +import CONST from '@src/CONST'; +import type {Download as DownloadOnyx, ReportAction} from '@src/types/onyx'; +import {getActionHtml} from './actionConfig'; + +type PopoverDownloadItemProps = { + reportAction: ReportAction; + encryptedAuthToken: string; + download: OnyxEntry; + isFocused?: boolean; + onFocus?: () => void; + onBlur?: () => void; +}; + +function PopoverDownloadItem({reportAction, encryptedAuthToken, download, isFocused, onFocus, onBlur}: PopoverDownloadItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Download'] as const); + const isDownloading = download?.isDownloading ?? false; + + return ( + + interceptAnonymousUser(() => { + const html = getActionHtml(reportAction); + const {originalFileName, sourceURL} = getAttachmentDetails(html); + const sourceURLWithAuth = addEncryptedAuthTokenToURL(sourceURL ?? '', encryptedAuthToken); + const sourceID = (sourceURL?.match(CONST.REGEX.ATTACHMENT_ID) ?? [])[1]; + setDownload(sourceID, true); + const anchorRegex = CONST.REGEX_LINK_IN_ANCHOR; + const isAnchorTag = anchorRegex.test(html); + fileDownload(translate, sourceURLWithAuth, originalFileName ?? '', '', isAnchorTag && isMobileSafari()).then(() => setDownload(sourceID, false)); + hideContextMenu(true, ReportActionComposeFocusManager.focus); + }, true) + } + isAnonymousAction + disabled={isDownloading} + shouldShowLoadingSpinnerIcon={isDownloading} + isFocused={isFocused} + onFocus={onFocus} + onBlur={onBlur} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.DOWNLOAD} + /> + ); +} + +type MiniDownloadItemProps = { + reportAction: ReportAction; + encryptedAuthToken: string; +}; + +function MiniDownloadItem({reportAction, encryptedAuthToken}: MiniDownloadItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Download'] as const); + + return ( + + interceptAnonymousUser(() => { + const html = getActionHtml(reportAction); + const {originalFileName, sourceURL} = getAttachmentDetails(html); + const sourceURLWithAuth = addEncryptedAuthTokenToURL(sourceURL ?? '', encryptedAuthToken); + const sourceID = (sourceURL?.match(CONST.REGEX.ATTACHMENT_ID) ?? [])[1]; + setDownload(sourceID, true); + const anchorRegex = CONST.REGEX_LINK_IN_ANCHOR; + const isAnchorTag = anchorRegex.test(html); + fileDownload(translate, sourceURLWithAuth, originalFileName ?? '', '', isAnchorTag && isMobileSafari()).then(() => setDownload(sourceID, false)); + hideContextMenu(true, ReportActionComposeFocusManager.focus); + }, true) + } + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.DOWNLOAD} + /> + ); +} + +function shouldShowDownloadAction({reportAction, isOffline}: {reportAction: OnyxEntry; isOffline: boolean}): boolean { + const isAttachment = isReportActionAttachment(reportAction); + const html = getActionHtml(reportAction); + const isUploading = html.includes(CONST.ATTACHMENT_OPTIMISTIC_SOURCE_ATTRIBUTE); + return isAttachment && !isUploading && !!reportAction?.reportActionID && !isMessageDeleted(reportAction) && !isOffline; +} + +export {shouldShowDownloadAction, PopoverDownloadItem, MiniDownloadItem}; diff --git a/src/pages/inbox/report/ContextMenu/actions/editAction.ts b/src/pages/inbox/report/ContextMenu/actions/editAction.ts deleted file mode 100644 index 1061c4eb8a34..000000000000 --- a/src/pages/inbox/report/ContextMenu/actions/editAction.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type {OnyxEntry} from 'react-native-onyx'; -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import Navigation from '@libs/Navigation/Navigation'; -import Parser from '@libs/Parser'; -import {isMoneyRequestAction} from '@libs/ReportActionsUtils'; -import {canEditReportAction} from '@libs/ReportUtils'; -import {deleteReportActionDraft, openReport, saveReportActionDraft} from '@userActions/Report'; -import CONST from '@src/CONST'; -import ROUTES from '@src/ROUTES'; -import type {IntroSelected, ReportAction} from '@src/types/onyx'; -import type IconAsset from '@src/types/utils/IconAsset'; -import {getActionHtml} from './actionConfig'; -import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; - -type EditActionParams = BaseContextMenuActionParams & { - reportID: string | undefined; - reportAction: ReportAction; - moneyRequestAction: ReportAction | undefined; - draftMessage: string; - introSelected: OnyxEntry; - hideAndRun: (callback?: () => void) => void; - pencilIcon: IconAsset; -}; - -function shouldShowEditAction({ - reportAction, - isArchivedRoom, - isChronosReport, - moneyRequestAction, -}: { - reportAction: OnyxEntry; - isArchivedRoom: boolean; - isChronosReport: boolean; - moneyRequestAction: ReportAction | undefined; -}): boolean { - return (canEditReportAction(reportAction) || canEditReportAction(moneyRequestAction)) && !isArchivedRoom && !isChronosReport; -} - -function createEditAction({reportID, reportAction, moneyRequestAction, draftMessage, introSelected, hideAndRun, translate, pencilIcon}: EditActionParams): ContextMenuAction { - return { - id: 'edit', - icon: pencilIcon, - text: translate('reportActionContextMenu.editAction', {action: moneyRequestAction ?? reportAction}), - onPress: () => - interceptAnonymousUser(() => { - if (isMoneyRequestAction(reportAction) || isMoneyRequestAction(moneyRequestAction)) { - hideAndRun(() => { - const childReportID = reportAction?.childReportID; - openReport({reportID: childReportID, introSelected}); - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID)); - }); - return; - } - hideAndRun(() => { - if (!draftMessage) { - saveReportActionDraft(reportID, reportAction, Parser.htmlToMarkdown(getActionHtml(reportAction))); - } else { - deleteReportActionDraft(reportID, reportAction); - } - }); - }), - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.EDIT_COMMENT, - }; -} - -export default createEditAction; -export {shouldShowEditAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/editAction.tsx b/src/pages/inbox/report/ContextMenu/actions/editAction.tsx new file mode 100644 index 000000000000..2960d3f9449f --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/editAction.tsx @@ -0,0 +1,129 @@ +import React from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import FocusableMenuItem from '@components/FocusableMenuItem'; +import MiniContextMenuItem from '@components/MiniContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import Navigation from '@libs/Navigation/Navigation'; +import Parser from '@libs/Parser'; +import {isMoneyRequestAction} from '@libs/ReportActionsUtils'; +import {canEditReportAction} from '@libs/ReportUtils'; +import {deleteReportActionDraft, openReport, saveReportActionDraft} from '@userActions/Report'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; +import type {IntroSelected, ReportAction} from '@src/types/onyx'; +import {getActionHtml} from './actionConfig'; + +type PopoverEditItemProps = { + reportID: string | undefined; + reportAction: ReportAction; + moneyRequestAction: ReportAction | undefined; + draftMessage: string; + introSelected: OnyxEntry; + hideAndRun: (callback?: () => void) => void; + isFocused?: boolean; + onFocus?: () => void; + onBlur?: () => void; +}; + +function PopoverEditItem({reportID, reportAction, moneyRequestAction, draftMessage, introSelected, hideAndRun, isFocused, onFocus, onBlur}: PopoverEditItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Pencil'] as const); + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const {windowWidth} = useWindowDimensions(); + + return ( + + interceptAnonymousUser(() => { + if (isMoneyRequestAction(reportAction) || isMoneyRequestAction(moneyRequestAction)) { + hideAndRun(() => { + const childReportID = reportAction?.childReportID; + openReport({reportID: childReportID, introSelected}); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID)); + }); + return; + } + hideAndRun(() => { + if (!draftMessage) { + saveReportActionDraft(reportID, reportAction, Parser.htmlToMarkdown(getActionHtml(reportAction))); + } else { + deleteReportActionDraft(reportID, reportAction); + } + }); + }) + } + wrapperStyle={[styles.pr8]} + style={StyleUtils.getContextMenuItemStyles(windowWidth)} + focused={isFocused} + interactive + onFocus={onFocus} + onBlur={onBlur} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.EDIT_COMMENT} + /> + ); +} + +type MiniEditItemProps = { + reportID: string | undefined; + reportAction: ReportAction; + moneyRequestAction: ReportAction | undefined; + draftMessage: string; + introSelected: OnyxEntry; + hideAndRun: (callback?: () => void) => void; +}; + +function MiniEditItem({reportID, reportAction, moneyRequestAction, draftMessage, introSelected, hideAndRun}: MiniEditItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Pencil'] as const); + + return ( + + interceptAnonymousUser(() => { + if (isMoneyRequestAction(reportAction) || isMoneyRequestAction(moneyRequestAction)) { + hideAndRun(() => { + const childReportID = reportAction?.childReportID; + openReport({reportID: childReportID, introSelected}); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID)); + }); + return; + } + hideAndRun(() => { + if (!draftMessage) { + saveReportActionDraft(reportID, reportAction, Parser.htmlToMarkdown(getActionHtml(reportAction))); + } else { + deleteReportActionDraft(reportID, reportAction); + } + }); + }) + } + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.EDIT_COMMENT} + /> + ); +} + +function shouldShowEditAction({ + reportAction, + isArchivedRoom, + isChronosReport, + moneyRequestAction, +}: { + reportAction: OnyxEntry; + isArchivedRoom: boolean; + isChronosReport: boolean; + moneyRequestAction: ReportAction | undefined; +}): boolean { + return (canEditReportAction(reportAction) || canEditReportAction(moneyRequestAction)) && !isArchivedRoom && !isChronosReport; +} + +export {shouldShowEditAction, PopoverEditItem, MiniEditItem}; diff --git a/src/pages/inbox/report/ContextMenu/actions/explainAction.ts b/src/pages/inbox/report/ContextMenu/actions/explainAction.ts deleted file mode 100644 index 03d2fe225175..000000000000 --- a/src/pages/inbox/report/ContextMenu/actions/explainAction.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type {OnyxEntry} from 'react-native-onyx'; -import type useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import {hasReasoning} from '@libs/ReportActionsUtils'; -import {explain} from '@userActions/Report'; -import CONST from '@src/CONST'; -import type {IntroSelected, ReportAction, Report as ReportType} from '@src/types/onyx'; -import type IconAsset from '@src/types/utils/IconAsset'; -import KeyboardUtils from '@src/utils/keyboard'; -import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; - -type ExplainActionParams = BaseContextMenuActionParams & { - childReport: OnyxEntry; - originalReport: OnyxEntry; - reportAction: ReportAction; - currentUserPersonalDetails: ReturnType; - introSelected: OnyxEntry; - hideAndRun: (callback?: () => void) => void; - conciergeIcon: IconAsset; -}; - -function shouldShowExplainAction({reportAction, isArchivedRoom}: {reportAction: OnyxEntry; isArchivedRoom: boolean}): boolean { - if (isArchivedRoom || !reportAction) { - return false; - } - return hasReasoning(reportAction); -} - -function createExplainAction({ - childReport, - originalReport, - reportAction, - currentUserPersonalDetails, - introSelected, - hideAndRun, - translate, - conciergeIcon, -}: ExplainActionParams): ContextMenuAction { - return { - id: 'explain', - icon: conciergeIcon, - text: translate('reportActionContextMenu.explain'), - onPress: () => - interceptAnonymousUser(() => { - if (!originalReport?.reportID) { - return; - } - hideAndRun(() => { - KeyboardUtils.dismiss().then(() => - explain( - childReport, - originalReport, - reportAction, - translate, - currentUserPersonalDetails?.accountID ?? CONST.DEFAULT_NUMBER_ID, - introSelected, - currentUserPersonalDetails?.timezone, - ), - ); - }); - }), - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.EXPLAIN, - }; -} - -export default createExplainAction; -export {shouldShowExplainAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/explainAction.tsx b/src/pages/inbox/report/ContextMenu/actions/explainAction.tsx new file mode 100644 index 000000000000..3a8b89bce302 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/explainAction.tsx @@ -0,0 +1,121 @@ +import React from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import FocusableMenuItem from '@components/FocusableMenuItem'; +import MiniContextMenuItem from '@components/MiniContextMenuItem'; +import type useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import {hasReasoning} from '@libs/ReportActionsUtils'; +import {explain} from '@userActions/Report'; +import CONST from '@src/CONST'; +import type {IntroSelected, ReportAction, Report as ReportType} from '@src/types/onyx'; +import KeyboardUtils from '@src/utils/keyboard'; + +type PopoverExplainItemProps = { + childReport: OnyxEntry; + originalReport: OnyxEntry; + reportAction: ReportAction; + currentUserPersonalDetails: ReturnType; + introSelected: OnyxEntry; + hideAndRun: (callback?: () => void) => void; + isFocused?: boolean; + onFocus?: () => void; + onBlur?: () => void; +}; + +function PopoverExplainItem({childReport, originalReport, reportAction, currentUserPersonalDetails, introSelected, hideAndRun, isFocused, onFocus, onBlur}: PopoverExplainItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Concierge'] as const); + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const {windowWidth} = useWindowDimensions(); + + return ( + + interceptAnonymousUser(() => { + if (!originalReport?.reportID) { + return; + } + hideAndRun(() => { + KeyboardUtils.dismiss().then(() => + explain( + childReport, + originalReport, + reportAction, + translate, + currentUserPersonalDetails?.accountID ?? CONST.DEFAULT_NUMBER_ID, + introSelected, + currentUserPersonalDetails?.timezone, + ), + ); + }); + }) + } + wrapperStyle={[styles.pr8]} + style={StyleUtils.getContextMenuItemStyles(windowWidth)} + focused={isFocused} + interactive + onFocus={onFocus} + onBlur={onBlur} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.EXPLAIN} + /> + ); +} + +type MiniExplainItemProps = { + childReport: OnyxEntry; + originalReport: OnyxEntry; + reportAction: ReportAction; + currentUserPersonalDetails: ReturnType; + introSelected: OnyxEntry; + hideAndRun: (callback?: () => void) => void; +}; + +function MiniExplainItem({childReport, originalReport, reportAction, currentUserPersonalDetails, introSelected, hideAndRun}: MiniExplainItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Concierge'] as const); + + return ( + + interceptAnonymousUser(() => { + if (!originalReport?.reportID) { + return; + } + hideAndRun(() => { + KeyboardUtils.dismiss().then(() => + explain( + childReport, + originalReport, + reportAction, + translate, + currentUserPersonalDetails?.accountID ?? CONST.DEFAULT_NUMBER_ID, + introSelected, + currentUserPersonalDetails?.timezone, + ), + ); + }); + }) + } + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.EXPLAIN} + /> + ); +} + +function shouldShowExplainAction({reportAction, isArchivedRoom}: {reportAction: OnyxEntry; isArchivedRoom: boolean}): boolean { + if (isArchivedRoom || !reportAction) { + return false; + } + return hasReasoning(reportAction); +} + +export {shouldShowExplainAction, PopoverExplainItem, MiniExplainItem}; diff --git a/src/pages/inbox/report/ContextMenu/actions/flagAsOffensiveAction.ts b/src/pages/inbox/report/ContextMenu/actions/flagAsOffensiveAction.ts deleted file mode 100644 index 9ecd84f7da09..000000000000 --- a/src/pages/inbox/report/ContextMenu/actions/flagAsOffensiveAction.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type {OnyxEntry} from 'react-native-onyx'; -import Navigation from '@libs/Navigation/Navigation'; -import {canFlagReportAction} from '@libs/ReportUtils'; -import CONST from '@src/CONST'; -import ROUTES from '@src/ROUTES'; -import type {ReportAction} from '@src/types/onyx'; -import type IconAsset from '@src/types/utils/IconAsset'; -import KeyboardUtils from '@src/utils/keyboard'; -import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; - -type FlagAsOffensiveActionParams = BaseContextMenuActionParams & { - reportID: string | undefined; - reportAction: ReportAction; - hideAndRun: (callback?: () => void) => void; - flagIcon: IconAsset; -}; - -function shouldShowFlagAsOffensiveAction({ - reportAction, - isArchivedRoom, - isChronosReport, - reportID, -}: { - reportAction: OnyxEntry; - isArchivedRoom: boolean; - isChronosReport: boolean; - reportID: string | undefined; -}): boolean { - return canFlagReportAction(reportAction, reportID) && !isArchivedRoom && !isChronosReport && reportAction?.actorAccountID !== CONST.ACCOUNT_ID.CONCIERGE; -} - -function createFlagAsOffensiveAction({reportID, reportAction, hideAndRun, translate, flagIcon}: FlagAsOffensiveActionParams): ContextMenuAction { - return { - id: 'flagAsOffensive', - icon: flagIcon, - text: translate('reportActionContextMenu.flagAsOffensive'), - onPress: () => { - if (!reportID) { - return; - } - const activeRoute = Navigation.getActiveRoute(); - hideAndRun(() => { - KeyboardUtils.dismiss().then(() => { - Navigation.navigate(ROUTES.FLAG_COMMENT.getRoute(reportID, reportAction?.reportActionID, activeRoute)); - }); - }); - }, - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.FLAG_AS_OFFENSIVE, - }; -} - -export default createFlagAsOffensiveAction; -export {shouldShowFlagAsOffensiveAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/flagAsOffensiveAction.tsx b/src/pages/inbox/report/ContextMenu/actions/flagAsOffensiveAction.tsx new file mode 100644 index 000000000000..4119be262327 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/flagAsOffensiveAction.tsx @@ -0,0 +1,103 @@ +import React from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import FocusableMenuItem from '@components/FocusableMenuItem'; +import MiniContextMenuItem from '@components/MiniContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import Navigation from '@libs/Navigation/Navigation'; +import {canFlagReportAction} from '@libs/ReportUtils'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; +import type {ReportAction} from '@src/types/onyx'; +import KeyboardUtils from '@src/utils/keyboard'; + +type PopoverFlagAsOffensiveItemProps = { + reportID: string | undefined; + reportAction: ReportAction; + hideAndRun: (callback?: () => void) => void; + isFocused?: boolean; + onFocus?: () => void; + onBlur?: () => void; +}; + +function PopoverFlagAsOffensiveItem({reportID, reportAction, hideAndRun, isFocused, onFocus, onBlur}: PopoverFlagAsOffensiveItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Flag'] as const); + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const {windowWidth} = useWindowDimensions(); + + return ( + { + if (!reportID) { + return; + } + const activeRoute = Navigation.getActiveRoute(); + hideAndRun(() => { + KeyboardUtils.dismiss().then(() => { + Navigation.navigate(ROUTES.FLAG_COMMENT.getRoute(reportID, reportAction?.reportActionID, activeRoute)); + }); + }); + }} + wrapperStyle={[styles.pr8]} + style={StyleUtils.getContextMenuItemStyles(windowWidth)} + focused={isFocused} + interactive + onFocus={onFocus} + onBlur={onBlur} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.FLAG_AS_OFFENSIVE} + /> + ); +} + +type MiniFlagAsOffensiveItemProps = { + reportID: string | undefined; + reportAction: ReportAction; + hideAndRun: (callback?: () => void) => void; +}; + +function MiniFlagAsOffensiveItem({reportID, reportAction, hideAndRun}: MiniFlagAsOffensiveItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Flag'] as const); + + return ( + { + if (!reportID) { + return; + } + const activeRoute = Navigation.getActiveRoute(); + hideAndRun(() => { + KeyboardUtils.dismiss().then(() => { + Navigation.navigate(ROUTES.FLAG_COMMENT.getRoute(reportID, reportAction?.reportActionID, activeRoute)); + }); + }); + }} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.FLAG_AS_OFFENSIVE} + /> + ); +} + +function shouldShowFlagAsOffensiveAction({ + reportAction, + isArchivedRoom, + isChronosReport, + reportID, +}: { + reportAction: OnyxEntry; + isArchivedRoom: boolean; + isChronosReport: boolean; + reportID: string | undefined; +}): boolean { + return canFlagReportAction(reportAction, reportID) && !isArchivedRoom && !isChronosReport && reportAction?.actorAccountID !== CONST.ACCOUNT_ID.CONCIERGE; +} + +export {shouldShowFlagAsOffensiveAction, PopoverFlagAsOffensiveItem, MiniFlagAsOffensiveItem}; diff --git a/src/pages/inbox/report/ContextMenu/actions/holdAction.ts b/src/pages/inbox/report/ContextMenu/actions/holdAction.ts deleted file mode 100644 index 39774c14e63d..000000000000 --- a/src/pages/inbox/report/ContextMenu/actions/holdAction.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type {OnyxEntry} from 'react-native-onyx'; -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import {getReportAction} from '@libs/ReportActionsUtils'; -import {canHoldUnholdReportAction, changeMoneyRequestHoldStatus} from '@libs/ReportUtils'; -import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; -import CONST from '@src/CONST'; -import type {Policy, ReportAction, Report as ReportType, Transaction} from '@src/types/onyx'; -import type IconAsset from '@src/types/utils/IconAsset'; -import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; - -type HoldActionParams = BaseContextMenuActionParams & { - moneyRequestAction: ReportAction | undefined; - isDelegateAccessRestricted: boolean; - showDelegateNoAccessModal: (() => void) | undefined; - hideAndRun: (callback?: () => void) => void; - stopwatchIcon: IconAsset; -}; - -function shouldShowHoldAction({ - moneyRequestReport, - moneyRequestAction, - moneyRequestPolicy, - areHoldRequirementsMet, - iouTransaction, -}: { - moneyRequestReport: OnyxEntry; - moneyRequestAction: ReportAction | undefined; - moneyRequestPolicy: OnyxEntry; - areHoldRequirementsMet: boolean; - iouTransaction: OnyxEntry; -}): boolean { - if (!areHoldRequirementsMet) { - return false; - } - const holdReportAction = getReportAction(moneyRequestAction?.childReportID, `${iouTransaction?.comment?.hold ?? ''}`); - return canHoldUnholdReportAction(moneyRequestReport, moneyRequestAction, holdReportAction, iouTransaction, moneyRequestPolicy).canHoldRequest; -} - -function createHoldAction({moneyRequestAction, isDelegateAccessRestricted, showDelegateNoAccessModal, hideAndRun, translate, stopwatchIcon}: HoldActionParams): ContextMenuAction { - return { - id: 'hold', - icon: stopwatchIcon, - text: translate('iou.hold'), - onPress: () => - interceptAnonymousUser(() => { - if (isDelegateAccessRestricted) { - hideContextMenu(false, showDelegateNoAccessModal); - return; - } - hideAndRun(() => changeMoneyRequestHoldStatus(moneyRequestAction)); - }, false), - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.HOLD, - }; -} - -export default createHoldAction; -export {shouldShowHoldAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/holdAction.tsx b/src/pages/inbox/report/ContextMenu/actions/holdAction.tsx new file mode 100644 index 000000000000..b24fc6618ce0 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/holdAction.tsx @@ -0,0 +1,107 @@ +import React from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import FocusableMenuItem from '@components/FocusableMenuItem'; +import MiniContextMenuItem from '@components/MiniContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import {getReportAction} from '@libs/ReportActionsUtils'; +import {canHoldUnholdReportAction, changeMoneyRequestHoldStatus} from '@libs/ReportUtils'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import CONST from '@src/CONST'; +import type {Policy, ReportAction, Report as ReportType, Transaction} from '@src/types/onyx'; + +type PopoverHoldItemProps = { + moneyRequestAction: ReportAction | undefined; + isDelegateAccessRestricted: boolean; + showDelegateNoAccessModal: (() => void) | undefined; + hideAndRun: (callback?: () => void) => void; + isFocused?: boolean; + onFocus?: () => void; + onBlur?: () => void; +}; + +function PopoverHoldItem({moneyRequestAction, isDelegateAccessRestricted, showDelegateNoAccessModal, hideAndRun, isFocused, onFocus, onBlur}: PopoverHoldItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Stopwatch'] as const); + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const {windowWidth} = useWindowDimensions(); + + return ( + + interceptAnonymousUser(() => { + if (isDelegateAccessRestricted) { + hideContextMenu(false, showDelegateNoAccessModal); + return; + } + hideAndRun(() => changeMoneyRequestHoldStatus(moneyRequestAction)); + }, false) + } + wrapperStyle={[styles.pr8]} + style={StyleUtils.getContextMenuItemStyles(windowWidth)} + focused={isFocused} + interactive + onFocus={onFocus} + onBlur={onBlur} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.HOLD} + /> + ); +} + +type MiniHoldItemProps = { + moneyRequestAction: ReportAction | undefined; + isDelegateAccessRestricted: boolean; + showDelegateNoAccessModal: (() => void) | undefined; + hideAndRun: (callback?: () => void) => void; +}; + +function MiniHoldItem({moneyRequestAction, isDelegateAccessRestricted, showDelegateNoAccessModal, hideAndRun}: MiniHoldItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Stopwatch'] as const); + + return ( + + interceptAnonymousUser(() => { + if (isDelegateAccessRestricted) { + hideContextMenu(false, showDelegateNoAccessModal); + return; + } + hideAndRun(() => changeMoneyRequestHoldStatus(moneyRequestAction)); + }, false) + } + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.HOLD} + /> + ); +} + +function shouldShowHoldAction({ + moneyRequestReport, + moneyRequestAction, + moneyRequestPolicy, + areHoldRequirementsMet, + iouTransaction, +}: { + moneyRequestReport: OnyxEntry; + moneyRequestAction: ReportAction | undefined; + moneyRequestPolicy: OnyxEntry; + areHoldRequirementsMet: boolean; + iouTransaction: OnyxEntry; +}): boolean { + if (!areHoldRequirementsMet) { + return false; + } + const holdReportAction = getReportAction(moneyRequestAction?.childReportID, `${iouTransaction?.comment?.hold ?? ''}`); + return canHoldUnholdReportAction(moneyRequestReport, moneyRequestAction, holdReportAction, iouTransaction, moneyRequestPolicy).canHoldRequest; +} + +export {shouldShowHoldAction, PopoverHoldItem, MiniHoldItem}; diff --git a/src/pages/inbox/report/ContextMenu/actions/joinThreadAction.ts b/src/pages/inbox/report/ContextMenu/actions/joinThreadAction.ts deleted file mode 100644 index 15c3c46622c5..000000000000 --- a/src/pages/inbox/report/ContextMenu/actions/joinThreadAction.ts +++ /dev/null @@ -1,71 +0,0 @@ -import type {OnyxEntry} from 'react-native-onyx'; -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import {isActionableTrackExpense, isCreatedAction, isCreatedTaskReportAction, isDeletedAction, isMoneyRequestAction, isReportPreviewAction, isWhisperAction} from '@libs/ReportActionsUtils'; -import {getChildReportNotificationPreference, shouldDisableThread, shouldDisplayThreadReplies} from '@libs/ReportUtils'; -import {toggleSubscribeToChildReport} from '@userActions/Report'; -import CONST from '@src/CONST'; -import type {IntroSelected, ReportAction, Report as ReportType} from '@src/types/onyx'; -import type IconAsset from '@src/types/utils/IconAsset'; -import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; - -type JoinThreadActionParams = BaseContextMenuActionParams & { - reportAction: ReportAction; - originalReport: OnyxEntry; - currentUserAccountID: number; - introSelected: OnyxEntry; - hideAndRun: (callback?: () => void) => void; - bellIcon: IconAsset; -}; - -function shouldShowJoinThreadAction({ - reportAction, - isArchivedRoom, - isThreadReportParentAction, - isHarvestReport, -}: { - reportAction: OnyxEntry; - isArchivedRoom: boolean; - isThreadReportParentAction: boolean; - isHarvestReport: boolean; -}): boolean { - const childReportNotificationPreference = getChildReportNotificationPreference(reportAction); - const isDeletedActionResult = isDeletedAction(reportAction); - const shouldDisplayReplies = shouldDisplayThreadReplies(reportAction, isThreadReportParentAction); - const subscribed = childReportNotificationPreference !== 'hidden'; - const isWhisper = isWhisperAction(reportAction) || isActionableTrackExpense(reportAction); - const isExpenseReportAction = isMoneyRequestAction(reportAction) || isReportPreviewAction(reportAction); - const isTaskAction = isCreatedTaskReportAction(reportAction); - const isHarvestCreatedExpenseReportAction = !!isHarvestReport && isCreatedAction(reportAction); - const shouldDisableJoin = shouldDisableThread(reportAction, isThreadReportParentAction, isArchivedRoom); - return ( - !subscribed && - !isWhisper && - !isTaskAction && - !isExpenseReportAction && - !isThreadReportParentAction && - !isHarvestCreatedExpenseReportAction && - !shouldDisableJoin && - (shouldDisplayReplies || (!isDeletedActionResult && !isArchivedRoom)) - ); -} - -function createJoinThreadAction({reportAction, originalReport, currentUserAccountID, introSelected, hideAndRun, translate, bellIcon}: JoinThreadActionParams): ContextMenuAction { - return { - id: 'joinThread', - icon: bellIcon, - text: translate('reportActionContextMenu.joinThread'), - onPress: () => - interceptAnonymousUser(() => { - const childReportNotificationPreference = getChildReportNotificationPreference(reportAction); - hideAndRun(() => { - ReportActionComposeFocusManager.focus(); - toggleSubscribeToChildReport(reportAction?.childReportID, currentUserAccountID, reportAction, originalReport, introSelected, childReportNotificationPreference); - }); - }, false), - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.JOIN_THREAD, - }; -} - -export default createJoinThreadAction; -export {shouldShowJoinThreadAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/joinThreadAction.tsx b/src/pages/inbox/report/ContextMenu/actions/joinThreadAction.tsx new file mode 100644 index 000000000000..eb12ca8dbfe2 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/joinThreadAction.tsx @@ -0,0 +1,122 @@ +import React from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import FocusableMenuItem from '@components/FocusableMenuItem'; +import MiniContextMenuItem from '@components/MiniContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {isActionableTrackExpense, isCreatedAction, isCreatedTaskReportAction, isDeletedAction, isMoneyRequestAction, isReportPreviewAction, isWhisperAction} from '@libs/ReportActionsUtils'; +import {getChildReportNotificationPreference, shouldDisableThread, shouldDisplayThreadReplies} from '@libs/ReportUtils'; +import {toggleSubscribeToChildReport} from '@userActions/Report'; +import CONST from '@src/CONST'; +import type {IntroSelected, ReportAction, Report as ReportType} from '@src/types/onyx'; + +type PopoverJoinThreadItemProps = { + reportAction: ReportAction; + originalReport: OnyxEntry; + currentUserAccountID: number; + introSelected: OnyxEntry; + hideAndRun: (callback?: () => void) => void; + isFocused?: boolean; + onFocus?: () => void; + onBlur?: () => void; +}; + +function PopoverJoinThreadItem({reportAction, originalReport, currentUserAccountID, introSelected, hideAndRun, isFocused, onFocus, onBlur}: PopoverJoinThreadItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Bell'] as const); + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const {windowWidth} = useWindowDimensions(); + + return ( + + interceptAnonymousUser(() => { + const childReportNotificationPreference = getChildReportNotificationPreference(reportAction); + hideAndRun(() => { + ReportActionComposeFocusManager.focus(); + toggleSubscribeToChildReport(reportAction?.childReportID, currentUserAccountID, reportAction, originalReport, introSelected, childReportNotificationPreference); + }); + }, false) + } + wrapperStyle={[styles.pr8]} + style={StyleUtils.getContextMenuItemStyles(windowWidth)} + focused={isFocused} + interactive + onFocus={onFocus} + onBlur={onBlur} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.JOIN_THREAD} + /> + ); +} + +type MiniJoinThreadItemProps = { + reportAction: ReportAction; + originalReport: OnyxEntry; + currentUserAccountID: number; + introSelected: OnyxEntry; + hideAndRun: (callback?: () => void) => void; +}; + +function MiniJoinThreadItem({reportAction, originalReport, currentUserAccountID, introSelected, hideAndRun}: MiniJoinThreadItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Bell'] as const); + + return ( + + interceptAnonymousUser(() => { + const childReportNotificationPreference = getChildReportNotificationPreference(reportAction); + hideAndRun(() => { + ReportActionComposeFocusManager.focus(); + toggleSubscribeToChildReport(reportAction?.childReportID, currentUserAccountID, reportAction, originalReport, introSelected, childReportNotificationPreference); + }); + }, false) + } + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.JOIN_THREAD} + /> + ); +} + +function shouldShowJoinThreadAction({ + reportAction, + isArchivedRoom, + isThreadReportParentAction, + isHarvestReport, +}: { + reportAction: OnyxEntry; + isArchivedRoom: boolean; + isThreadReportParentAction: boolean; + isHarvestReport: boolean; +}): boolean { + const childReportNotificationPreference = getChildReportNotificationPreference(reportAction); + const isDeletedActionResult = isDeletedAction(reportAction); + const shouldDisplayReplies = shouldDisplayThreadReplies(reportAction, isThreadReportParentAction); + const subscribed = childReportNotificationPreference !== 'hidden'; + const isWhisper = isWhisperAction(reportAction) || isActionableTrackExpense(reportAction); + const isExpenseReportAction = isMoneyRequestAction(reportAction) || isReportPreviewAction(reportAction); + const isTaskAction = isCreatedTaskReportAction(reportAction); + const isHarvestCreatedExpenseReportAction = !!isHarvestReport && isCreatedAction(reportAction); + const shouldDisableJoin = shouldDisableThread(reportAction, isThreadReportParentAction, isArchivedRoom); + return ( + !subscribed && + !isWhisper && + !isTaskAction && + !isExpenseReportAction && + !isThreadReportParentAction && + !isHarvestCreatedExpenseReportAction && + !shouldDisableJoin && + (shouldDisplayReplies || (!isDeletedActionResult && !isArchivedRoom)) + ); +} + +export {shouldShowJoinThreadAction, PopoverJoinThreadItem, MiniJoinThreadItem}; diff --git a/src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.ts b/src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.ts deleted file mode 100644 index 11f459483231..000000000000 --- a/src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.ts +++ /dev/null @@ -1,69 +0,0 @@ -import type {OnyxEntry} from 'react-native-onyx'; -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import {isActionableTrackExpense, isCreatedAction, isCreatedTaskReportAction, isDeletedAction, isMoneyRequestAction, isReportPreviewAction, isWhisperAction} from '@libs/ReportActionsUtils'; -import {getChildReportNotificationPreference, shouldDisplayThreadReplies} from '@libs/ReportUtils'; -import {toggleSubscribeToChildReport} from '@userActions/Report'; -import CONST from '@src/CONST'; -import type {IntroSelected, ReportAction, Report as ReportType} from '@src/types/onyx'; -import type IconAsset from '@src/types/utils/IconAsset'; -import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; - -type LeaveThreadActionParams = BaseContextMenuActionParams & { - reportAction: ReportAction; - originalReport: OnyxEntry; - currentUserAccountID: number; - introSelected: OnyxEntry; - hideAndRun: (callback?: () => void) => void; - exitIcon: IconAsset; -}; - -function shouldShowLeaveThreadAction({ - reportAction, - isArchivedRoom, - isThreadReportParentAction, - isHarvestReport, -}: { - reportAction: OnyxEntry; - isArchivedRoom: boolean; - isThreadReportParentAction: boolean; - isHarvestReport: boolean; -}): boolean { - const childReportNotificationPreference = getChildReportNotificationPreference(reportAction); - const isDeletedActionResult = isDeletedAction(reportAction); - const shouldDisplayReplies = shouldDisplayThreadReplies(reportAction, isThreadReportParentAction); - const subscribed = childReportNotificationPreference !== 'hidden'; - const isWhisper = isWhisperAction(reportAction) || isActionableTrackExpense(reportAction); - const isExpenseReportAction = isMoneyRequestAction(reportAction) || isReportPreviewAction(reportAction); - const isTaskAction = isCreatedTaskReportAction(reportAction); - const isHarvestCreatedExpenseReportAction = !!isHarvestReport && isCreatedAction(reportAction); - return ( - subscribed && - !isWhisper && - !isTaskAction && - !isExpenseReportAction && - !isThreadReportParentAction && - !isHarvestCreatedExpenseReportAction && - (shouldDisplayReplies || (!isDeletedActionResult && !isArchivedRoom)) - ); -} - -function createLeaveThreadAction({reportAction, originalReport, currentUserAccountID, introSelected, hideAndRun, translate, exitIcon}: LeaveThreadActionParams): ContextMenuAction { - return { - id: 'leaveThread', - icon: exitIcon, - text: translate('reportActionContextMenu.leaveThread'), - onPress: () => - interceptAnonymousUser(() => { - const childReportNotificationPreference = getChildReportNotificationPreference(reportAction); - hideAndRun(() => { - ReportActionComposeFocusManager.focus(); - toggleSubscribeToChildReport(reportAction?.childReportID, currentUserAccountID, reportAction, originalReport, introSelected, childReportNotificationPreference); - }); - }, false), - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.LEAVE_THREAD, - }; -} - -export default createLeaveThreadAction; -export {shouldShowLeaveThreadAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.tsx b/src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.tsx new file mode 100644 index 000000000000..caea8fd955ff --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.tsx @@ -0,0 +1,120 @@ +import React from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import FocusableMenuItem from '@components/FocusableMenuItem'; +import MiniContextMenuItem from '@components/MiniContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {isActionableTrackExpense, isCreatedAction, isCreatedTaskReportAction, isDeletedAction, isMoneyRequestAction, isReportPreviewAction, isWhisperAction} from '@libs/ReportActionsUtils'; +import {getChildReportNotificationPreference, shouldDisplayThreadReplies} from '@libs/ReportUtils'; +import {toggleSubscribeToChildReport} from '@userActions/Report'; +import CONST from '@src/CONST'; +import type {IntroSelected, ReportAction, Report as ReportType} from '@src/types/onyx'; + +type PopoverLeaveThreadItemProps = { + reportAction: ReportAction; + originalReport: OnyxEntry; + currentUserAccountID: number; + introSelected: OnyxEntry; + hideAndRun: (callback?: () => void) => void; + isFocused?: boolean; + onFocus?: () => void; + onBlur?: () => void; +}; + +function PopoverLeaveThreadItem({reportAction, originalReport, currentUserAccountID, introSelected, hideAndRun, isFocused, onFocus, onBlur}: PopoverLeaveThreadItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Exit'] as const); + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const {windowWidth} = useWindowDimensions(); + + return ( + + interceptAnonymousUser(() => { + const childReportNotificationPreference = getChildReportNotificationPreference(reportAction); + hideAndRun(() => { + ReportActionComposeFocusManager.focus(); + toggleSubscribeToChildReport(reportAction?.childReportID, currentUserAccountID, reportAction, originalReport, introSelected, childReportNotificationPreference); + }); + }, false) + } + wrapperStyle={[styles.pr8]} + style={StyleUtils.getContextMenuItemStyles(windowWidth)} + focused={isFocused} + interactive + onFocus={onFocus} + onBlur={onBlur} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.LEAVE_THREAD} + /> + ); +} + +type MiniLeaveThreadItemProps = { + reportAction: ReportAction; + originalReport: OnyxEntry; + currentUserAccountID: number; + introSelected: OnyxEntry; + hideAndRun: (callback?: () => void) => void; +}; + +function MiniLeaveThreadItem({reportAction, originalReport, currentUserAccountID, introSelected, hideAndRun}: MiniLeaveThreadItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Exit'] as const); + + return ( + + interceptAnonymousUser(() => { + const childReportNotificationPreference = getChildReportNotificationPreference(reportAction); + hideAndRun(() => { + ReportActionComposeFocusManager.focus(); + toggleSubscribeToChildReport(reportAction?.childReportID, currentUserAccountID, reportAction, originalReport, introSelected, childReportNotificationPreference); + }); + }, false) + } + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.LEAVE_THREAD} + /> + ); +} + +function shouldShowLeaveThreadAction({ + reportAction, + isArchivedRoom, + isThreadReportParentAction, + isHarvestReport, +}: { + reportAction: OnyxEntry; + isArchivedRoom: boolean; + isThreadReportParentAction: boolean; + isHarvestReport: boolean; +}): boolean { + const childReportNotificationPreference = getChildReportNotificationPreference(reportAction); + const isDeletedActionResult = isDeletedAction(reportAction); + const shouldDisplayReplies = shouldDisplayThreadReplies(reportAction, isThreadReportParentAction); + const subscribed = childReportNotificationPreference !== 'hidden'; + const isWhisper = isWhisperAction(reportAction) || isActionableTrackExpense(reportAction); + const isExpenseReportAction = isMoneyRequestAction(reportAction) || isReportPreviewAction(reportAction); + const isTaskAction = isCreatedTaskReportAction(reportAction); + const isHarvestCreatedExpenseReportAction = !!isHarvestReport && isCreatedAction(reportAction); + return ( + subscribed && + !isWhisper && + !isTaskAction && + !isExpenseReportAction && + !isThreadReportParentAction && + !isHarvestCreatedExpenseReportAction && + (shouldDisplayReplies || (!isDeletedActionResult && !isArchivedRoom)) + ); +} + +export {shouldShowLeaveThreadAction, PopoverLeaveThreadItem, MiniLeaveThreadItem}; diff --git a/src/pages/inbox/report/ContextMenu/actions/markAsReadAction.ts b/src/pages/inbox/report/ContextMenu/actions/markAsReadAction.ts deleted file mode 100644 index cafe876a5f20..000000000000 --- a/src/pages/inbox/report/ContextMenu/actions/markAsReadAction.ts +++ /dev/null @@ -1,35 +0,0 @@ -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import {readNewestAction} from '@userActions/Report'; -import CONST from '@src/CONST'; -import type IconAsset from '@src/types/utils/IconAsset'; -import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; - -type MarkAsReadActionParams = BaseContextMenuActionParams & { - reportID: string | undefined; - hideAndRun: (callback?: () => void) => void; - mailIcon: IconAsset; - checkmarkIcon: IconAsset; -}; - -function shouldShowMarkAsReadAction({isUnreadChat}: {isUnreadChat: boolean}): boolean { - return isUnreadChat; -} - -function createMarkAsReadAction({reportID, hideAndRun, translate, mailIcon, checkmarkIcon}: MarkAsReadActionParams): ContextMenuAction { - return { - id: 'markAsRead', - icon: mailIcon, - text: translate('reportActionContextMenu.markAsRead'), - successIcon: checkmarkIcon, - onPress: () => - interceptAnonymousUser(() => { - readNewestAction(reportID, true, true); - hideAndRun(ReportActionComposeFocusManager.focus); - }), - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.MARK_AS_READ, - }; -} - -export default createMarkAsReadAction; -export {shouldShowMarkAsReadAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/markAsReadAction.tsx b/src/pages/inbox/report/ContextMenu/actions/markAsReadAction.tsx new file mode 100644 index 000000000000..102c22919b86 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/markAsReadAction.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import ContextMenuItem from '@components/ContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {readNewestAction} from '@userActions/Report'; +import CONST from '@src/CONST'; + +type PopoverMarkAsReadItemProps = { + reportID: string | undefined; + hideAndRun: (callback?: () => void) => void; + isFocused?: boolean; + onFocus?: () => void; + onBlur?: () => void; +}; + +function PopoverMarkAsReadItem({reportID, hideAndRun, isFocused, onFocus, onBlur}: PopoverMarkAsReadItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Mail', 'Checkmark'] as const); + + return ( + + interceptAnonymousUser(() => { + readNewestAction(reportID, true, true); + hideAndRun(ReportActionComposeFocusManager.focus); + }) + } + isFocused={isFocused} + onFocus={onFocus} + onBlur={onBlur} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.MARK_AS_READ} + /> + ); +} + +function shouldShowMarkAsReadAction({isUnreadChat}: {isUnreadChat: boolean}): boolean { + return isUnreadChat; +} + +export {shouldShowMarkAsReadAction, PopoverMarkAsReadItem}; diff --git a/src/pages/inbox/report/ContextMenu/actions/markAsUnreadAction.ts b/src/pages/inbox/report/ContextMenu/actions/markAsUnreadAction.ts deleted file mode 100644 index 8b73a80a70e5..000000000000 --- a/src/pages/inbox/report/ContextMenu/actions/markAsUnreadAction.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type {OnyxEntry} from 'react-native-onyx'; -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import {isActionOfType} from '@libs/ReportActionsUtils'; -import {markCommentAsUnread} from '@userActions/Report'; -import CONST from '@src/CONST'; -import type {ReportAction, ReportActions} from '@src/types/onyx'; -import type IconAsset from '@src/types/utils/IconAsset'; -import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; - -type MarkAsUnreadActionParams = BaseContextMenuActionParams & { - reportID: string | undefined; - reportActions: OnyxEntry; - reportAction: ReportAction; - currentUserAccountID: number; - hideAndRun: (callback?: () => void) => void; - chatBubbleUnreadIcon: IconAsset; - checkmarkIcon: IconAsset; -}; - -function shouldShowMarkAsUnreadForReportAction({reportAction}: {reportAction: OnyxEntry}): boolean { - return !isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.DYNAMIC_EXTERNAL_WORKFLOW_ROUTED); -} - -function shouldShowMarkAsUnreadForReport({isUnreadChat}: {isUnreadChat: boolean}): boolean { - return !isUnreadChat; -} - -function createMarkAsUnreadAction({ - reportID, - reportActions, - reportAction, - currentUserAccountID, - hideAndRun, - translate, - chatBubbleUnreadIcon, - checkmarkIcon, -}: MarkAsUnreadActionParams): ContextMenuAction { - return { - id: 'markAsUnread', - icon: chatBubbleUnreadIcon, - text: translate('reportActionContextMenu.markAsUnread'), - successIcon: checkmarkIcon, - onPress: () => - interceptAnonymousUser(() => { - markCommentAsUnread(reportID, reportActions, reportAction, currentUserAccountID); - hideAndRun(ReportActionComposeFocusManager.focus); - }), - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.MARK_AS_UNREAD, - }; -} - -export default createMarkAsUnreadAction; -export {shouldShowMarkAsUnreadForReportAction, shouldShowMarkAsUnreadForReport}; diff --git a/src/pages/inbox/report/ContextMenu/actions/markAsUnreadAction.tsx b/src/pages/inbox/report/ContextMenu/actions/markAsUnreadAction.tsx new file mode 100644 index 000000000000..e0734bdfde7b --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/markAsUnreadAction.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import ContextMenuItem from '@components/ContextMenuItem'; +import MiniContextMenuItem from '@components/MiniContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {isActionOfType} from '@libs/ReportActionsUtils'; +import {markCommentAsUnread} from '@userActions/Report'; +import CONST from '@src/CONST'; +import type {ReportAction, ReportActions} from '@src/types/onyx'; + +type PopoverMarkAsUnreadItemProps = { + reportID: string | undefined; + reportActions: OnyxEntry; + reportAction: ReportAction; + currentUserAccountID: number; + hideAndRun: (callback?: () => void) => void; + isFocused?: boolean; + onFocus?: () => void; + onBlur?: () => void; +}; + +function PopoverMarkAsUnreadItem({reportID, reportActions, reportAction, currentUserAccountID, hideAndRun, isFocused, onFocus, onBlur}: PopoverMarkAsUnreadItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['ChatBubbleUnread', 'Checkmark'] as const); + + return ( + + interceptAnonymousUser(() => { + markCommentAsUnread(reportID, reportActions, reportAction, currentUserAccountID); + hideAndRun(ReportActionComposeFocusManager.focus); + }) + } + isFocused={isFocused} + onFocus={onFocus} + onBlur={onBlur} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.MARK_AS_UNREAD} + /> + ); +} + +type MiniMarkAsUnreadItemProps = { + reportID: string | undefined; + reportActions: OnyxEntry; + reportAction: ReportAction; + currentUserAccountID: number; + hideAndRun: (callback?: () => void) => void; +}; + +function MiniMarkAsUnreadItem({reportID, reportActions, reportAction, currentUserAccountID, hideAndRun}: MiniMarkAsUnreadItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['ChatBubbleUnread', 'Checkmark'] as const); + + return ( + + interceptAnonymousUser(() => { + markCommentAsUnread(reportID, reportActions, reportAction, currentUserAccountID); + hideAndRun(ReportActionComposeFocusManager.focus); + }) + } + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.MARK_AS_UNREAD} + /> + ); +} + +function shouldShowMarkAsUnreadForReportAction({reportAction}: {reportAction: OnyxEntry}): boolean { + return !isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.DYNAMIC_EXTERNAL_WORKFLOW_ROUTED); +} + +function shouldShowMarkAsUnreadForReport({isUnreadChat}: {isUnreadChat: boolean}): boolean { + return !isUnreadChat; +} + +export {shouldShowMarkAsUnreadForReportAction, shouldShowMarkAsUnreadForReport, PopoverMarkAsUnreadItem, MiniMarkAsUnreadItem}; diff --git a/src/pages/inbox/report/ContextMenu/actions/overflowMenuAction.ts b/src/pages/inbox/report/ContextMenu/actions/overflowMenuAction.ts deleted file mode 100644 index 01edbe18215d..000000000000 --- a/src/pages/inbox/report/ContextMenu/actions/overflowMenuAction.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type {GestureResponderEvent} from 'react-native'; -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import CONST from '@src/CONST'; -import type IconAsset from '@src/types/utils/IconAsset'; -import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; - -type OverflowMenuActionParams = BaseContextMenuActionParams & { - openOverflowMenu: (event: GestureResponderEvent | MouseEvent) => void; - openContextMenu: () => void; - threeDotsIcon: IconAsset; -}; - -function createOverflowMenuAction({openOverflowMenu, openContextMenu, translate, threeDotsIcon}: OverflowMenuActionParams): ContextMenuAction { - return { - id: 'overflowMenu', - icon: threeDotsIcon, - text: translate('reportActionContextMenu.menu'), - isAnonymousAction: true, - shouldPreventDefaultFocusOnPress: false, - onPress: (event) => - interceptAnonymousUser(() => { - openOverflowMenu(event as GestureResponderEvent | MouseEvent); - openContextMenu(); - }, true), - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.MENU, - }; -} - -export default createOverflowMenuAction; diff --git a/src/pages/inbox/report/ContextMenu/actions/pinAction.ts b/src/pages/inbox/report/ContextMenu/actions/pinAction.ts deleted file mode 100644 index acb3bd949a80..000000000000 --- a/src/pages/inbox/report/ContextMenu/actions/pinAction.ts +++ /dev/null @@ -1,33 +0,0 @@ -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import {togglePinnedState} from '@userActions/Report'; -import CONST from '@src/CONST'; -import type IconAsset from '@src/types/utils/IconAsset'; -import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; - -type PinActionParams = BaseContextMenuActionParams & { - reportID: string | undefined; - hideAndRun: (callback?: () => void) => void; - pinIcon: IconAsset; -}; - -function shouldShowPinAction({isPinnedChat}: {isPinnedChat: boolean}): boolean { - return !isPinnedChat; -} - -function createPinAction({reportID, hideAndRun, translate, pinIcon}: PinActionParams): ContextMenuAction { - return { - id: 'pin', - icon: pinIcon, - text: translate('common.pin'), - onPress: () => - interceptAnonymousUser(() => { - togglePinnedState(reportID, false); - hideAndRun(ReportActionComposeFocusManager.focus); - }), - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.PIN, - }; -} - -export default createPinAction; -export {shouldShowPinAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/pinAction.tsx b/src/pages/inbox/report/ContextMenu/actions/pinAction.tsx new file mode 100644 index 000000000000..42a3eb75cce8 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/pinAction.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import ContextMenuItem from '@components/ContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {togglePinnedState} from '@userActions/Report'; +import CONST from '@src/CONST'; + +type PopoverPinItemProps = { + reportID: string | undefined; + hideAndRun: (callback?: () => void) => void; + isFocused?: boolean; + onFocus?: () => void; + onBlur?: () => void; +}; + +function PopoverPinItem({reportID, hideAndRun, isFocused, onFocus, onBlur}: PopoverPinItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Pin'] as const); + + return ( + + interceptAnonymousUser(() => { + togglePinnedState(reportID, false); + hideAndRun(ReportActionComposeFocusManager.focus); + }) + } + isFocused={isFocused} + onFocus={onFocus} + onBlur={onBlur} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.PIN} + /> + ); +} + +function shouldShowPinAction({isPinnedChat}: {isPinnedChat: boolean}): boolean { + return !isPinnedChat; +} + +export {shouldShowPinAction, PopoverPinItem}; diff --git a/src/pages/inbox/report/ContextMenu/actions/replyInThreadAction.ts b/src/pages/inbox/report/ContextMenu/actions/replyInThreadAction.ts deleted file mode 100644 index 78a8a59cd59e..000000000000 --- a/src/pages/inbox/report/ContextMenu/actions/replyInThreadAction.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type {OnyxEntry} from 'react-native-onyx'; -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import {shouldDisableThread} from '@libs/ReportUtils'; -import {navigateToAndOpenChildReport} from '@userActions/Report'; -import CONST from '@src/CONST'; -import type {IntroSelected, ReportAction, Report as ReportType} from '@src/types/onyx'; -import type IconAsset from '@src/types/utils/IconAsset'; -import KeyboardUtils from '@src/utils/keyboard'; -import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; - -type ReplyInThreadActionParams = BaseContextMenuActionParams & { - childReport: OnyxEntry; - reportAction: ReportAction; - originalReport: OnyxEntry; - currentUserAccountID: number; - introSelected: OnyxEntry; - hideAndRun: (callback?: () => void) => void; - chatBubbleReplyIcon: IconAsset; -}; - -function shouldShowReplyInThreadAction({ - reportAction, - reportID, - isThreadReportParentAction, - isArchivedRoom, -}: { - reportAction: OnyxEntry; - reportID: string | undefined; - isThreadReportParentAction: boolean; - isArchivedRoom: boolean; -}): boolean { - if (!reportID) { - return false; - } - return !shouldDisableThread(reportAction, isThreadReportParentAction, isArchivedRoom); -} - -function createReplyInThreadAction({ - childReport, - reportAction, - originalReport, - currentUserAccountID, - introSelected, - hideAndRun, - translate, - chatBubbleReplyIcon, -}: ReplyInThreadActionParams): ContextMenuAction { - return { - id: 'replyInThread', - icon: chatBubbleReplyIcon, - text: translate('reportActionContextMenu.replyInThread'), - onPress: () => - interceptAnonymousUser(() => { - hideAndRun(() => { - KeyboardUtils.dismiss().then(() => { - navigateToAndOpenChildReport(childReport, reportAction, originalReport, currentUserAccountID, introSelected); - }); - }); - }, false), - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.REPLY_IN_THREAD, - }; -} - -export default createReplyInThreadAction; -export {shouldShowReplyInThreadAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/replyInThreadAction.tsx b/src/pages/inbox/report/ContextMenu/actions/replyInThreadAction.tsx new file mode 100644 index 000000000000..5e1686b7af78 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/replyInThreadAction.tsx @@ -0,0 +1,104 @@ +import React from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import FocusableMenuItem from '@components/FocusableMenuItem'; +import MiniContextMenuItem from '@components/MiniContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import {shouldDisableThread} from '@libs/ReportUtils'; +import {navigateToAndOpenChildReport} from '@userActions/Report'; +import CONST from '@src/CONST'; +import type {IntroSelected, ReportAction, Report as ReportType} from '@src/types/onyx'; +import KeyboardUtils from '@src/utils/keyboard'; + +type PopoverReplyInThreadItemProps = { + childReport: OnyxEntry; + reportAction: ReportAction; + originalReport: OnyxEntry; + currentUserAccountID: number; + introSelected: OnyxEntry; + hideAndRun: (callback?: () => void) => void; + isFocused?: boolean; + onFocus?: () => void; + onBlur?: () => void; +}; + +function PopoverReplyInThreadItem({childReport, reportAction, originalReport, currentUserAccountID, introSelected, hideAndRun, isFocused, onFocus, onBlur}: PopoverReplyInThreadItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['ChatBubbleReply'] as const); + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const {windowWidth} = useWindowDimensions(); + + return ( + + interceptAnonymousUser(() => { + hideAndRun(() => { + KeyboardUtils.dismiss().then(() => navigateToAndOpenChildReport(childReport, reportAction, originalReport, currentUserAccountID, introSelected)); + }); + }, false) + } + wrapperStyle={[styles.pr8]} + style={StyleUtils.getContextMenuItemStyles(windowWidth)} + focused={isFocused} + interactive + onFocus={onFocus} + onBlur={onBlur} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.REPLY_IN_THREAD} + /> + ); +} + +type MiniReplyInThreadItemProps = { + childReport: OnyxEntry; + reportAction: ReportAction; + originalReport: OnyxEntry; + currentUserAccountID: number; + introSelected: OnyxEntry; + hideAndRun: (callback?: () => void) => void; +}; + +function MiniReplyInThreadItem({childReport, reportAction, originalReport, currentUserAccountID, introSelected, hideAndRun}: MiniReplyInThreadItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['ChatBubbleReply'] as const); + + return ( + + interceptAnonymousUser(() => { + hideAndRun(() => { + KeyboardUtils.dismiss().then(() => navigateToAndOpenChildReport(childReport, reportAction, originalReport, currentUserAccountID, introSelected)); + }); + }, false) + } + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.REPLY_IN_THREAD} + /> + ); +} + +function shouldShowReplyInThreadAction({ + reportAction, + reportID, + isThreadReportParentAction, + isArchivedRoom, +}: { + reportAction: OnyxEntry; + reportID: string | undefined; + isThreadReportParentAction: boolean; + isArchivedRoom: boolean; +}): boolean { + if (!reportID) { + return false; + } + return !shouldDisableThread(reportAction, isThreadReportParentAction, isArchivedRoom); +} + +export {shouldShowReplyInThreadAction, PopoverReplyInThreadItem, MiniReplyInThreadItem}; diff --git a/src/pages/inbox/report/ContextMenu/actions/unholdAction.ts b/src/pages/inbox/report/ContextMenu/actions/unholdAction.ts deleted file mode 100644 index 3aaaddc93b71..000000000000 --- a/src/pages/inbox/report/ContextMenu/actions/unholdAction.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type {OnyxEntry} from 'react-native-onyx'; -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import {getReportAction} from '@libs/ReportActionsUtils'; -import {canHoldUnholdReportAction, changeMoneyRequestHoldStatus} from '@libs/ReportUtils'; -import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; -import CONST from '@src/CONST'; -import type {Policy, ReportAction, Report as ReportType, Transaction} from '@src/types/onyx'; -import type IconAsset from '@src/types/utils/IconAsset'; -import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; - -type UnholdActionParams = BaseContextMenuActionParams & { - moneyRequestAction: ReportAction | undefined; - isDelegateAccessRestricted: boolean; - showDelegateNoAccessModal: (() => void) | undefined; - hideAndRun: (callback?: () => void) => void; - stopwatchIcon: IconAsset; -}; - -function shouldShowUnholdAction({ - moneyRequestReport, - moneyRequestAction, - moneyRequestPolicy, - areHoldRequirementsMet, - iouTransaction, -}: { - moneyRequestReport: OnyxEntry; - moneyRequestAction: ReportAction | undefined; - moneyRequestPolicy: OnyxEntry; - areHoldRequirementsMet: boolean; - iouTransaction: OnyxEntry; -}): boolean { - if (!areHoldRequirementsMet) { - return false; - } - const holdReportAction = getReportAction(moneyRequestAction?.childReportID, `${iouTransaction?.comment?.hold ?? ''}`); - return canHoldUnholdReportAction(moneyRequestReport, moneyRequestAction, holdReportAction, iouTransaction, moneyRequestPolicy).canUnholdRequest; -} - -function createUnholdAction({moneyRequestAction, isDelegateAccessRestricted, showDelegateNoAccessModal, hideAndRun, translate, stopwatchIcon}: UnholdActionParams): ContextMenuAction { - return { - id: 'unhold', - icon: stopwatchIcon, - text: translate('iou.unhold'), - onPress: () => - interceptAnonymousUser(() => { - if (isDelegateAccessRestricted) { - hideContextMenu(false, showDelegateNoAccessModal); - return; - } - hideAndRun(() => changeMoneyRequestHoldStatus(moneyRequestAction)); - }, false), - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.UNHOLD, - }; -} - -export default createUnholdAction; -export {shouldShowUnholdAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/unholdAction.tsx b/src/pages/inbox/report/ContextMenu/actions/unholdAction.tsx new file mode 100644 index 000000000000..4a76f6ed22ee --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/unholdAction.tsx @@ -0,0 +1,107 @@ +import React from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import FocusableMenuItem from '@components/FocusableMenuItem'; +import MiniContextMenuItem from '@components/MiniContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import {getReportAction} from '@libs/ReportActionsUtils'; +import {canHoldUnholdReportAction, changeMoneyRequestHoldStatus} from '@libs/ReportUtils'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import CONST from '@src/CONST'; +import type {Policy, ReportAction, Report as ReportType, Transaction} from '@src/types/onyx'; + +type PopoverUnholdItemProps = { + moneyRequestAction: ReportAction | undefined; + isDelegateAccessRestricted: boolean; + showDelegateNoAccessModal: (() => void) | undefined; + hideAndRun: (callback?: () => void) => void; + isFocused?: boolean; + onFocus?: () => void; + onBlur?: () => void; +}; + +function PopoverUnholdItem({moneyRequestAction, isDelegateAccessRestricted, showDelegateNoAccessModal, hideAndRun, isFocused, onFocus, onBlur}: PopoverUnholdItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Stopwatch'] as const); + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const {windowWidth} = useWindowDimensions(); + + return ( + + interceptAnonymousUser(() => { + if (isDelegateAccessRestricted) { + hideContextMenu(false, showDelegateNoAccessModal); + return; + } + hideAndRun(() => changeMoneyRequestHoldStatus(moneyRequestAction)); + }, false) + } + wrapperStyle={[styles.pr8]} + style={StyleUtils.getContextMenuItemStyles(windowWidth)} + focused={isFocused} + interactive + onFocus={onFocus} + onBlur={onBlur} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.UNHOLD} + /> + ); +} + +type MiniUnholdItemProps = { + moneyRequestAction: ReportAction | undefined; + isDelegateAccessRestricted: boolean; + showDelegateNoAccessModal: (() => void) | undefined; + hideAndRun: (callback?: () => void) => void; +}; + +function MiniUnholdItem({moneyRequestAction, isDelegateAccessRestricted, showDelegateNoAccessModal, hideAndRun}: MiniUnholdItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Stopwatch'] as const); + + return ( + + interceptAnonymousUser(() => { + if (isDelegateAccessRestricted) { + hideContextMenu(false, showDelegateNoAccessModal); + return; + } + hideAndRun(() => changeMoneyRequestHoldStatus(moneyRequestAction)); + }, false) + } + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.UNHOLD} + /> + ); +} + +function shouldShowUnholdAction({ + moneyRequestReport, + moneyRequestAction, + moneyRequestPolicy, + areHoldRequirementsMet, + iouTransaction, +}: { + moneyRequestReport: OnyxEntry; + moneyRequestAction: ReportAction | undefined; + moneyRequestPolicy: OnyxEntry; + areHoldRequirementsMet: boolean; + iouTransaction: OnyxEntry; +}): boolean { + if (!areHoldRequirementsMet) { + return false; + } + const holdReportAction = getReportAction(moneyRequestAction?.childReportID, `${iouTransaction?.comment?.hold ?? ''}`); + return canHoldUnholdReportAction(moneyRequestReport, moneyRequestAction, holdReportAction, iouTransaction, moneyRequestPolicy).canUnholdRequest; +} + +export {shouldShowUnholdAction, PopoverUnholdItem, MiniUnholdItem}; diff --git a/src/pages/inbox/report/ContextMenu/actions/unpinAction.ts b/src/pages/inbox/report/ContextMenu/actions/unpinAction.ts deleted file mode 100644 index 5b73bd893e94..000000000000 --- a/src/pages/inbox/report/ContextMenu/actions/unpinAction.ts +++ /dev/null @@ -1,33 +0,0 @@ -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import {togglePinnedState} from '@userActions/Report'; -import CONST from '@src/CONST'; -import type IconAsset from '@src/types/utils/IconAsset'; -import type {BaseContextMenuActionParams, ContextMenuAction} from './actionConfig'; - -type UnpinActionParams = BaseContextMenuActionParams & { - reportID: string | undefined; - hideAndRun: (callback?: () => void) => void; - pinIcon: IconAsset; -}; - -function shouldShowUnpinAction({isPinnedChat}: {isPinnedChat: boolean}): boolean { - return isPinnedChat; -} - -function createUnpinAction({reportID, hideAndRun, translate, pinIcon}: UnpinActionParams): ContextMenuAction { - return { - id: 'unpin', - icon: pinIcon, - text: translate('common.unPin'), - onPress: () => - interceptAnonymousUser(() => { - togglePinnedState(reportID, true); - hideAndRun(ReportActionComposeFocusManager.focus); - }), - sentryLabel: CONST.SENTRY_LABEL.CONTEXT_MENU.UNPIN, - }; -} - -export default createUnpinAction; -export {shouldShowUnpinAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/unpinAction.tsx b/src/pages/inbox/report/ContextMenu/actions/unpinAction.tsx new file mode 100644 index 000000000000..356c497d3d34 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/unpinAction.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import ContextMenuItem from '@components/ContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {togglePinnedState} from '@userActions/Report'; +import CONST from '@src/CONST'; + +type PopoverUnpinItemProps = { + reportID: string | undefined; + hideAndRun: (callback?: () => void) => void; + isFocused?: boolean; + onFocus?: () => void; + onBlur?: () => void; +}; + +function PopoverUnpinItem({reportID, hideAndRun, isFocused, onFocus, onBlur}: PopoverUnpinItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Pin'] as const); + + return ( + + interceptAnonymousUser(() => { + togglePinnedState(reportID, true); + hideAndRun(ReportActionComposeFocusManager.focus); + }) + } + isFocused={isFocused} + onFocus={onFocus} + onBlur={onBlur} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.UNPIN} + /> + ); +} + +function shouldShowUnpinAction({isPinnedChat}: {isPinnedChat: boolean}): boolean { + return isPinnedChat; +} + +export {shouldShowUnpinAction, PopoverUnpinItem}; From 324c0a98599adad966be2b77154bc791a45cf28a Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 6 Apr 2026 14:00:48 -0700 Subject: [PATCH 71/88] Port main's functional changes to decomposed action files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Main modified ContextMenuActions.tsx, PopoverReportActionContextMenu.tsx, and BaseReportActionContextMenu.tsx (all deleted in our branch). Port the functional changes to their new locations: copyMessageAction.tsx: - getForReportActionTemp → getForReportAction (renamed) - Modified expense: convert HTML to markdown via Parser.htmlToMarkdown - getReportPreviewMessage: add conciergeReportID (2nd param) - getIOUReportActionDisplayMessage: add bankAccountList (5th param) - Move stripFollowupListFromHtml into setClipboardMessage - Add card feed clipboard handlers (7 new action types) - Add actionableCard3DSTransactionApproval handler - Add dynamicExternalWorkflow submit/approve failed handlers useReportActionContextMenuData.ts: - Add isSelfTourViewed, bankAccountList, conciergeReportID, betas Action file parameter updates: - replyInThread/explain: add betas to navigateToAndOpenChildReport/explain - edit: add iouTransaction to canEditReportAction, betas to openReport - hold/unhold: add iouTransaction, isOffline to changeMoneyRequestHoldStatus - joinThread/leaveThread: add isSelfTourViewed, betas to toggleSubscribeToChildReport - download: ATTACHMENT_ID → ATTACHMENT.ATTACHMENT_SOURCE_ID ConfirmDeleteReportActionModal.tsx: - deleteTrackExpense import: IOU → IOU/TrackExpense - Remove usePersonalPolicy, toLocaleDigit - deleteAppReport: remove personalPolicy/translate/toLocaleDigit - deleteReportComment: add reportActions as 8th param - deleteTransactions: replace currentSearchHash with undefined Other: - Add isArchivedRoom to ShowContextMenuParams report type - Remove BaseReportActionContextMenuTest (imports deleted module) Made-with: Cursor --- .../MiniReportActionContextMenu/index.tsx | 21 +- .../ConfirmDeleteReportActionModal.tsx | 27 +- .../PopoverReportActionContent.tsx | 21 +- .../ContextMenu/ReportActionContextMenu.ts | 1 + .../ContextMenu/actions/copyMessageAction.tsx | 65 ++- .../ContextMenu/actions/downloadAction.tsx | 4 +- .../report/ContextMenu/actions/editAction.tsx | 16 +- .../ContextMenu/actions/explainAction.tsx | 10 +- .../report/ContextMenu/actions/holdAction.tsx | 22 +- .../ContextMenu/actions/joinThreadAction.tsx | 43 +- .../ContextMenu/actions/leaveThreadAction.tsx | 43 +- .../actions/replyInThreadAction.tsx | 23 +- .../ContextMenu/actions/unholdAction.tsx | 22 +- .../useReportActionContextMenuData.ts | 9 + .../BaseReportActionContextMenuTest.tsx | 397 ------------------ .../unit/ContextMenuActionsCopyMessageTest.ts | 2 + 16 files changed, 273 insertions(+), 453 deletions(-) delete mode 100644 tests/ui/components/BaseReportActionContextMenuTest.tsx diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index 5988239d3056..5f8e250014ac 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -4,6 +4,7 @@ import type {RefObject} from 'react'; import {StyleSheet, View} from 'react-native'; // eslint-disable-next-line no-restricted-imports import type {GestureResponderEvent} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import * as ActionSheetAwareScrollView from '@components/ActionSheetAwareScrollView'; import Hoverable from '@components/Hoverable'; import MiniContextMenuItem from '@components/MiniContextMenuItem'; @@ -33,6 +34,7 @@ import type {ContextMenuAnchor} from '@pages/inbox/report/ContextMenu/ReportActi import {showContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import useReportActionContextMenuData from '@pages/inbox/report/ContextMenu/useReportActionContextMenuData'; import CONST from '@src/CONST'; +import type {BankAccountList} from '@src/types/onyx'; function MiniReportActionContextMenu() { const { @@ -116,7 +118,9 @@ function MiniReportActionContextMenu() { moneyRequestPolicy, iouTransaction, transaction, + bankAccountList, card, + conciergeReportID, currentUserPersonalDetails, encryptedAuthToken, isArchivedRoom, @@ -129,6 +133,8 @@ function MiniReportActionContextMenu() { areHoldRequirementsMet, transactions, introSelected, + isSelfTourViewed, + betas, movedFromReport, movedToReport, harvestReport, @@ -194,7 +200,7 @@ function MiniReportActionContextMenu() { }); const showMarkAsUnread = !isDisabledAction(ACTION_IDS.MARK_AS_UNREAD) && shouldShowMarkAsUnreadForReportAction({reportAction}); const showExplain = !isDisabledAction(ACTION_IDS.EXPLAIN) && shouldShowExplainAction({reportAction, isArchivedRoom}); - const showEdit = !isDisabledAction(ACTION_IDS.EDIT) && shouldShowEditAction({reportAction, isArchivedRoom, isChronosReport, moneyRequestAction}); + const showEdit = !isDisabledAction(ACTION_IDS.EDIT) && shouldShowEditAction({reportAction, isArchivedRoom, isChronosReport, moneyRequestAction, iouTransaction}); const showUnhold = !isDisabledAction(ACTION_IDS.UNHOLD) && shouldShowUnholdAction({ @@ -258,6 +264,7 @@ function MiniReportActionContextMenu() { originalReport={originalReport} currentUserAccountID={currentUserAccountID} introSelected={introSelected} + betas={betas} hideAndRun={hideAndRun} />, ); @@ -283,6 +290,7 @@ function MiniReportActionContextMenu() { reportAction={reportAction} currentUserPersonalDetails={currentUserPersonalDetails} introSelected={introSelected} + betas={betas} hideAndRun={hideAndRun} />, ); @@ -296,6 +304,7 @@ function MiniReportActionContextMenu() { moneyRequestAction={moneyRequestAction} draftMessage={resolvedDraftMessage} introSelected={introSelected} + betas={betas} hideAndRun={hideAndRun} />, ); @@ -305,6 +314,8 @@ function MiniReportActionContextMenu() { , ); @@ -342,6 +357,8 @@ function MiniReportActionContextMenu() { originalReport={originalReport} currentUserAccountID={currentUserAccountID} introSelected={introSelected} + isSelfTourViewed={isSelfTourViewed} + betas={betas} hideAndRun={hideAndRun} />, ); @@ -354,6 +371,8 @@ function MiniReportActionContextMenu() { transaction={transaction} selection={resolvedSelection} report={report} + conciergeReportID={conciergeReportID} + bankAccountList={bankAccountList as OnyxEntry} card={card} originalReport={originalReport} isHarvestReport={isHarvestReport} diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/ConfirmDeleteReportActionModal.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/ConfirmDeleteReportActionModal.tsx index 8bf79a867c3d..a63970bcbc16 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/ConfirmDeleteReportActionModal.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/ConfirmDeleteReportActionModal.tsx @@ -1,4 +1,4 @@ -import {useState} from 'react'; +import {useRef, useState} from 'react'; import {DeviceEventEmitter, InteractionManager} from 'react-native'; import ConfirmModal from '@components/ConfirmModal'; import type {ModalProps} from '@components/Modal/Global/ModalContext'; @@ -11,10 +11,9 @@ import useDuplicateTransactionsAndViolations from '@hooks/useDuplicateTransactio import useGetIOUReportFromReportAction from '@hooks/useGetIOUReportFromReportAction'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; -import usePersonalPolicy from '@hooks/usePersonalPolicy'; import useReportIsArchived from '@hooks/useReportIsArchived'; import useTransactionsAndViolationsForReport from '@hooks/useTransactionsAndViolationsForReport'; -import {deleteTrackExpense} from '@libs/actions/IOU'; +import {deleteTrackExpense} from '@libs/actions/IOU/TrackExpense'; import {deleteAppReport, deleteReportComment} from '@libs/actions/Report'; import {getOriginalMessage, isMoneyRequestAction, isReportPreviewAction, isTrackExpenseAction} from '@libs/ReportActionsUtils'; import {getOriginalReportID} from '@libs/ReportUtils'; @@ -27,12 +26,14 @@ type ConfirmDeleteReportActionModalProps = ModalProps & { }; function ConfirmDeleteReportActionModal({closeModal, reportID, reportActionID, actionSourceReportID}: ConfirmDeleteReportActionModalProps) { - const {translate, toLocaleDigit} = useLocalize(); - const personalPolicy = usePersonalPolicy(); + const {translate} = useLocalize(); const {email, accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); const {currentSearchHash} = useSearchStateContext(); const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`); + const reportActionsRef = useRef(reportActions); + // eslint-disable-next-line react-hooks/refs + reportActionsRef.current = reportActions; const [sourceReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${actionSourceReportID}`); const actionReportActions = reportActions?.[reportActionID] ? reportActions : sourceReportActions; const reportAction = actionReportActions?.[reportActionID]; @@ -93,7 +94,7 @@ function ConfirmDeleteReportActionModal({closeModal, reportID, reportActionID, a currentUserAccountID, }); } else if (originalMessage?.IOUTransactionID) { - deleteTransactions([originalMessage.IOUTransactionID], duplicateTransactions, duplicateTransactionViolations, currentSearchHash); + deleteTransactions([originalMessage.IOUTransactionID], duplicateTransactions, duplicateTransactionViolations, undefined); } } else if (isReportPreviewAction(reportAction)) { deleteAppReport({ @@ -104,15 +105,21 @@ function ConfirmDeleteReportActionModal({closeModal, reportID, reportActionID, a reportTransactions, allTransactionViolations, bankAccountList, - personalPolicy, - translate, - toLocaleDigit, hash: currentSearchHash, }); } else if (reportAction) { // eslint-disable-next-line @typescript-eslint/no-deprecated InteractionManager.runAfterInteractions(() => { - deleteReportComment(report, reportAction, ancestors, isReportArchived, isOriginalReportArchived, email ?? '', visibleReportActionsData ?? undefined); + deleteReportComment( + report, + reportAction, + ancestors, + isReportArchived, + isOriginalReportArchived, + email ?? '', + visibleReportActionsData ?? undefined, + reportActionsRef.current ?? undefined, + ); }); } diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx index 402a7943a36f..8447788af7fe 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx @@ -3,6 +3,7 @@ import React from 'react'; import {View} from 'react-native'; // eslint-disable-next-line no-restricted-imports import type {GestureResponderEvent, View as ViewType} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import FocusableMenuItem from '@components/FocusableMenuItem'; import FocusTrapForModal from '@components/FocusTrap/FocusTrapForModal'; import QuickEmojiReactions from '@components/Reactions/QuickEmojiReactions'; @@ -33,6 +34,7 @@ import {PopoverUnholdItem, shouldShowUnholdAction} from '@pages/inbox/report/Con import {showContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import useReportActionContextMenuData from '@pages/inbox/report/ContextMenu/useReportActionContextMenuData'; import CONST from '@src/CONST'; +import type {BankAccountList} from '@src/types/onyx'; type PopoverReportActionContentProps = { reportID: string | undefined; @@ -82,7 +84,9 @@ function PopoverReportActionContent({ moneyRequestPolicy, iouTransaction, transaction, + bankAccountList, card, + conciergeReportID, currentUserPersonalDetails, encryptedAuthToken, isArchivedRoom, @@ -96,6 +100,8 @@ function PopoverReportActionContent({ isDebugModeEnabled, transactions, introSelected, + isSelfTourViewed, + betas, movedFromReport, movedToReport, harvestReport, @@ -156,7 +162,7 @@ function PopoverReportActionContent({ }); const showMarkAsUnread = !isDisabled(ACTION_IDS.MARK_AS_UNREAD) && shouldShowMarkAsUnreadForReportAction({reportAction}); const showExplain = !isDisabled(ACTION_IDS.EXPLAIN) && shouldShowExplainAction({reportAction, isArchivedRoom}); - const showEdit = !isDisabled(ACTION_IDS.EDIT) && shouldShowEditAction({reportAction, isArchivedRoom, isChronosReport, moneyRequestAction}); + const showEdit = !isDisabled(ACTION_IDS.EDIT) && shouldShowEditAction({reportAction, isArchivedRoom, isChronosReport, moneyRequestAction, iouTransaction}); const showUnhold = !isDisabled(ACTION_IDS.UNHOLD) && shouldShowUnholdAction({ @@ -239,6 +245,7 @@ function PopoverReportActionContent({ originalReport={originalReport} currentUserAccountID={currentUserAccountID} introSelected={introSelected} + betas={betas} hideAndRun={hideAndRun} />, ); @@ -264,6 +271,7 @@ function PopoverReportActionContent({ reportAction={reportAction} currentUserPersonalDetails={currentUserPersonalDetails} introSelected={introSelected} + betas={betas} hideAndRun={hideAndRun} />, ); @@ -277,6 +285,7 @@ function PopoverReportActionContent({ moneyRequestAction={moneyRequestAction} draftMessage={resolvedDraftMessage} introSelected={introSelected} + betas={betas} hideAndRun={hideAndRun} />, ); @@ -286,6 +295,8 @@ function PopoverReportActionContent({ , ); @@ -323,6 +338,8 @@ function PopoverReportActionContent({ originalReport={originalReport} currentUserAccountID={currentUserAccountID} introSelected={introSelected} + isSelfTourViewed={isSelfTourViewed} + betas={betas} hideAndRun={hideAndRun} />, ); @@ -335,6 +352,8 @@ function PopoverReportActionContent({ transaction={transaction} selection={resolvedSelection} report={report} + conciergeReportID={conciergeReportID} + bankAccountList={bankAccountList as OnyxEntry} card={card} originalReport={originalReport} isHarvestReport={isHarvestReport} diff --git a/src/pages/inbox/report/ContextMenu/ReportActionContextMenu.ts b/src/pages/inbox/report/ContextMenu/ReportActionContextMenu.ts index aac9303b3fa2..c268d097ce3d 100644 --- a/src/pages/inbox/report/ContextMenu/ReportActionContextMenu.ts +++ b/src/pages/inbox/report/ContextMenu/ReportActionContextMenu.ts @@ -24,6 +24,7 @@ type ShowContextMenuParams = { report?: { reportID?: string; originalReportID?: string; + isArchivedRoom?: boolean; }; reportAction?: { reportActionID?: string; diff --git a/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.tsx b/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.tsx index d53dc862a32a..36914d4f2fe1 100644 --- a/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.tsx @@ -11,17 +11,20 @@ import Clipboard from '@libs/Clipboard'; import getClipboardText from '@libs/Clipboard/getClipboardText'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {formatPhoneNumber as formatPhoneNumberPhoneUtils} from '@libs/LocalePhoneNumber'; -import {getForReportActionTemp} from '@libs/ModifiedExpenseMessage'; +import {getForReportAction} from '@libs/ModifiedExpenseMessage'; import Parser from '@libs/Parser'; import {getCleanedTagName, isPolicyAdmin} from '@libs/PolicyUtils'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import stripFollowupListFromHtml from '@libs/ReportActionFollowupUtils/stripFollowupListFromHtml'; import { + getActionableCard3DSTransactionApprovalMessage, getActionableCardFraudAlertMessage, getActionableMentionWhisperMessage, getAddedApprovalRuleMessage, getAddedBudgetMessage, + getAddedCardFeedMessage, getAddedConnectionMessage, + getAssignedCompanyCardMessage, getAutoPayApprovedReportsEnabledMessage, getAutoReimbursementMessage, getCardIssuedMessage, @@ -35,7 +38,9 @@ import { getDeletedApprovalRuleMessage, getDeletedBudgetMessage, getDismissedViolationMessageText, + getDynamicExternalWorkflowApproveFailedActionMessage, getDynamicExternalWorkflowRoutedMessage, + getDynamicExternalWorkflowSubmitFailedActionMessage, getExportIntegrationMessageHTML, getForeignCurrencyDefaultTaxUpdateMessage, getForwardsToUpdateMessage, @@ -60,8 +65,10 @@ import { getPolicyChangeLogMaxExpenseAmountNoReceiptMessage, getPolicyChangeLogUpdateEmployee, getReimburserUpdateMessage, + getRemovedCardFeedMessage, getRemovedConnectionMessage, getRenamedAction, + getRenamedCardFeedMessage, getReportActionMessageText, getRoomAvatarUpdatedMessage, getSetAutoJoinMessage, @@ -71,11 +78,14 @@ import { getTagListUpdatedMessage, getTagListUpdatedRequiredMessage, getTravelUpdateMessage, + getUnassignedCompanyCardMessage, getUpdateACHAccountMessage, getUpdatedApprovalRuleMessage, getUpdatedAuditRateMessage, getUpdatedAutoHarvestingMessage, getUpdatedBudgetMessage, + getUpdatedCardFeedLiabilityMessage, + getUpdatedCardFeedStatementPeriodMessage, getUpdatedDefaultTitleMessage, getUpdatedIndividualBudgetNotificationMessage, getUpdatedManualApprovalThresholdMessage, @@ -113,6 +123,8 @@ import { isActionOfType, isCardIssuedAction, isCreatedTaskReportAction, + isDynamicExternalWorkflowApproveFailedAction, + isDynamicExternalWorkflowSubmitFailedAction, isMarkAsClosedAction, isMemberChangeAction, isMessageDeleted, @@ -151,7 +163,7 @@ import { import {getTaskCreatedMessage, getTaskReportActionMessage} from '@libs/TaskUtils'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; -import type {Card, Policy, PolicyTagLists, ReportAction, Report as ReportType, Transaction} from '@src/types/onyx'; +import type {BankAccountList, Card, Policy, PolicyTagLists, ReportAction, Report as ReportType, Transaction} from '@src/types/onyx'; import {getActionHtml} from './actionConfig'; type CopyMessageClipboardParams = { @@ -159,6 +171,8 @@ type CopyMessageClipboardParams = { transaction: OnyxEntry; selection: string; report: OnyxEntry; + conciergeReportID: string | undefined; + bankAccountList: OnyxEntry; card: Card | undefined; originalReport: OnyxEntry; isHarvestReport: boolean; @@ -179,14 +193,15 @@ function shouldShowCopyMessageAction({reportAction}: {reportAction: OnyxEntry)(.*?)(<\/mention-user>)/gi, (match, openTag: string, innerContent: string, closeTag: string): string => { + content.replaceAll(/()(.*?)(<\/mention-user>)/gi, (match, openTag: string, innerContent: string, closeTag: string): string => { const modifiedContent = Str.removeSMSDomain(innerContent) || ''; return openTag + modifiedContent + closeTag || ''; }), @@ -550,6 +587,8 @@ function PopoverCopyMessageItem({ transaction, selection, report, + conciergeReportID, + bankAccountList, card, originalReport, isHarvestReport, @@ -582,6 +621,8 @@ function PopoverCopyMessageItem({ transaction, selection, report, + conciergeReportID, + bankAccountList, card, originalReport, isHarvestReport, @@ -615,6 +656,8 @@ function MiniCopyMessageItem({ transaction, selection, report, + conciergeReportID, + bankAccountList, card, originalReport, isHarvestReport, @@ -644,6 +687,8 @@ function MiniCopyMessageItem({ transaction, selection, report, + conciergeReportID, + bankAccountList, card, originalReport, isHarvestReport, diff --git a/src/pages/inbox/report/ContextMenu/actions/downloadAction.tsx b/src/pages/inbox/report/ContextMenu/actions/downloadAction.tsx index fee1133b8cee..c363797a07b5 100644 --- a/src/pages/inbox/report/ContextMenu/actions/downloadAction.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/downloadAction.tsx @@ -42,7 +42,7 @@ function PopoverDownloadItem({reportAction, encryptedAuthToken, download, isFocu const html = getActionHtml(reportAction); const {originalFileName, sourceURL} = getAttachmentDetails(html); const sourceURLWithAuth = addEncryptedAuthTokenToURL(sourceURL ?? '', encryptedAuthToken); - const sourceID = (sourceURL?.match(CONST.REGEX.ATTACHMENT_ID) ?? [])[1]; + const sourceID = (sourceURL?.match(CONST.REGEX.ATTACHMENT.ATTACHMENT_SOURCE_ID) ?? [])[1]; setDownload(sourceID, true); const anchorRegex = CONST.REGEX_LINK_IN_ANCHOR; const isAnchorTag = anchorRegex.test(html); @@ -81,7 +81,7 @@ function MiniDownloadItem({reportAction, encryptedAuthToken}: MiniDownloadItemPr const html = getActionHtml(reportAction); const {originalFileName, sourceURL} = getAttachmentDetails(html); const sourceURLWithAuth = addEncryptedAuthTokenToURL(sourceURL ?? '', encryptedAuthToken); - const sourceID = (sourceURL?.match(CONST.REGEX.ATTACHMENT_ID) ?? [])[1]; + const sourceID = (sourceURL?.match(CONST.REGEX.ATTACHMENT.ATTACHMENT_SOURCE_ID) ?? [])[1]; setDownload(sourceID, true); const anchorRegex = CONST.REGEX_LINK_IN_ANCHOR; const isAnchorTag = anchorRegex.test(html); diff --git a/src/pages/inbox/report/ContextMenu/actions/editAction.tsx b/src/pages/inbox/report/ContextMenu/actions/editAction.tsx index 2960d3f9449f..7a5de09004a4 100644 --- a/src/pages/inbox/report/ContextMenu/actions/editAction.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/editAction.tsx @@ -15,7 +15,7 @@ import {canEditReportAction} from '@libs/ReportUtils'; import {deleteReportActionDraft, openReport, saveReportActionDraft} from '@userActions/Report'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; -import type {IntroSelected, ReportAction} from '@src/types/onyx'; +import type {Beta, IntroSelected, ReportAction, Transaction} from '@src/types/onyx'; import {getActionHtml} from './actionConfig'; type PopoverEditItemProps = { @@ -24,13 +24,14 @@ type PopoverEditItemProps = { moneyRequestAction: ReportAction | undefined; draftMessage: string; introSelected: OnyxEntry; + betas: OnyxEntry; hideAndRun: (callback?: () => void) => void; isFocused?: boolean; onFocus?: () => void; onBlur?: () => void; }; -function PopoverEditItem({reportID, reportAction, moneyRequestAction, draftMessage, introSelected, hideAndRun, isFocused, onFocus, onBlur}: PopoverEditItemProps) { +function PopoverEditItem({reportID, reportAction, moneyRequestAction, draftMessage, introSelected, betas, hideAndRun, isFocused, onFocus, onBlur}: PopoverEditItemProps) { const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Pencil'] as const); const styles = useThemeStyles(); @@ -46,7 +47,7 @@ function PopoverEditItem({reportID, reportAction, moneyRequestAction, draftMessa if (isMoneyRequestAction(reportAction) || isMoneyRequestAction(moneyRequestAction)) { hideAndRun(() => { const childReportID = reportAction?.childReportID; - openReport({reportID: childReportID, introSelected}); + openReport({reportID: childReportID, introSelected, betas}); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID)); }); return; @@ -77,10 +78,11 @@ type MiniEditItemProps = { moneyRequestAction: ReportAction | undefined; draftMessage: string; introSelected: OnyxEntry; + betas: OnyxEntry; hideAndRun: (callback?: () => void) => void; }; -function MiniEditItem({reportID, reportAction, moneyRequestAction, draftMessage, introSelected, hideAndRun}: MiniEditItemProps) { +function MiniEditItem({reportID, reportAction, moneyRequestAction, draftMessage, introSelected, betas, hideAndRun}: MiniEditItemProps) { const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Pencil'] as const); @@ -93,7 +95,7 @@ function MiniEditItem({reportID, reportAction, moneyRequestAction, draftMessage, if (isMoneyRequestAction(reportAction) || isMoneyRequestAction(moneyRequestAction)) { hideAndRun(() => { const childReportID = reportAction?.childReportID; - openReport({reportID: childReportID, introSelected}); + openReport({reportID: childReportID, introSelected, betas}); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID)); }); return; @@ -117,13 +119,15 @@ function shouldShowEditAction({ isArchivedRoom, isChronosReport, moneyRequestAction, + iouTransaction, }: { reportAction: OnyxEntry; isArchivedRoom: boolean; isChronosReport: boolean; moneyRequestAction: ReportAction | undefined; + iouTransaction: OnyxEntry; }): boolean { - return (canEditReportAction(reportAction) || canEditReportAction(moneyRequestAction)) && !isArchivedRoom && !isChronosReport; + return (canEditReportAction(reportAction, iouTransaction) || canEditReportAction(moneyRequestAction, iouTransaction)) && !isArchivedRoom && !isChronosReport; } export {shouldShowEditAction, PopoverEditItem, MiniEditItem}; diff --git a/src/pages/inbox/report/ContextMenu/actions/explainAction.tsx b/src/pages/inbox/report/ContextMenu/actions/explainAction.tsx index 3a8b89bce302..17a8c6cdd88e 100644 --- a/src/pages/inbox/report/ContextMenu/actions/explainAction.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/explainAction.tsx @@ -12,7 +12,7 @@ import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {hasReasoning} from '@libs/ReportActionsUtils'; import {explain} from '@userActions/Report'; import CONST from '@src/CONST'; -import type {IntroSelected, ReportAction, Report as ReportType} from '@src/types/onyx'; +import type {Beta, IntroSelected, ReportAction, Report as ReportType} from '@src/types/onyx'; import KeyboardUtils from '@src/utils/keyboard'; type PopoverExplainItemProps = { @@ -21,13 +21,14 @@ type PopoverExplainItemProps = { reportAction: ReportAction; currentUserPersonalDetails: ReturnType; introSelected: OnyxEntry; + betas: OnyxEntry; hideAndRun: (callback?: () => void) => void; isFocused?: boolean; onFocus?: () => void; onBlur?: () => void; }; -function PopoverExplainItem({childReport, originalReport, reportAction, currentUserPersonalDetails, introSelected, hideAndRun, isFocused, onFocus, onBlur}: PopoverExplainItemProps) { +function PopoverExplainItem({childReport, originalReport, reportAction, currentUserPersonalDetails, introSelected, betas, hideAndRun, isFocused, onFocus, onBlur}: PopoverExplainItemProps) { const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Concierge'] as const); const styles = useThemeStyles(); @@ -52,6 +53,7 @@ function PopoverExplainItem({childReport, originalReport, reportAction, currentU translate, currentUserPersonalDetails?.accountID ?? CONST.DEFAULT_NUMBER_ID, introSelected, + betas, currentUserPersonalDetails?.timezone, ), ); @@ -75,10 +77,11 @@ type MiniExplainItemProps = { reportAction: ReportAction; currentUserPersonalDetails: ReturnType; introSelected: OnyxEntry; + betas: OnyxEntry; hideAndRun: (callback?: () => void) => void; }; -function MiniExplainItem({childReport, originalReport, reportAction, currentUserPersonalDetails, introSelected, hideAndRun}: MiniExplainItemProps) { +function MiniExplainItem({childReport, originalReport, reportAction, currentUserPersonalDetails, introSelected, betas, hideAndRun}: MiniExplainItemProps) { const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Concierge'] as const); @@ -100,6 +103,7 @@ function MiniExplainItem({childReport, originalReport, reportAction, currentUser translate, currentUserPersonalDetails?.accountID ?? CONST.DEFAULT_NUMBER_ID, introSelected, + betas, currentUserPersonalDetails?.timezone, ), ); diff --git a/src/pages/inbox/report/ContextMenu/actions/holdAction.tsx b/src/pages/inbox/report/ContextMenu/actions/holdAction.tsx index b24fc6618ce0..e1fb17697ed9 100644 --- a/src/pages/inbox/report/ContextMenu/actions/holdAction.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/holdAction.tsx @@ -16,6 +16,8 @@ import type {Policy, ReportAction, Report as ReportType, Transaction} from '@src type PopoverHoldItemProps = { moneyRequestAction: ReportAction | undefined; + iouTransaction: OnyxEntry; + isOffline: boolean; isDelegateAccessRestricted: boolean; showDelegateNoAccessModal: (() => void) | undefined; hideAndRun: (callback?: () => void) => void; @@ -24,7 +26,17 @@ type PopoverHoldItemProps = { onBlur?: () => void; }; -function PopoverHoldItem({moneyRequestAction, isDelegateAccessRestricted, showDelegateNoAccessModal, hideAndRun, isFocused, onFocus, onBlur}: PopoverHoldItemProps) { +function PopoverHoldItem({ + moneyRequestAction, + iouTransaction, + isOffline, + isDelegateAccessRestricted, + showDelegateNoAccessModal, + hideAndRun, + isFocused, + onFocus, + onBlur, +}: PopoverHoldItemProps) { const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Stopwatch'] as const); const styles = useThemeStyles(); @@ -41,7 +53,7 @@ function PopoverHoldItem({moneyRequestAction, isDelegateAccessRestricted, showDe hideContextMenu(false, showDelegateNoAccessModal); return; } - hideAndRun(() => changeMoneyRequestHoldStatus(moneyRequestAction)); + hideAndRun(() => changeMoneyRequestHoldStatus(moneyRequestAction, iouTransaction, isOffline)); }, false) } wrapperStyle={[styles.pr8]} @@ -57,12 +69,14 @@ function PopoverHoldItem({moneyRequestAction, isDelegateAccessRestricted, showDe type MiniHoldItemProps = { moneyRequestAction: ReportAction | undefined; + iouTransaction: OnyxEntry; + isOffline: boolean; isDelegateAccessRestricted: boolean; showDelegateNoAccessModal: (() => void) | undefined; hideAndRun: (callback?: () => void) => void; }; -function MiniHoldItem({moneyRequestAction, isDelegateAccessRestricted, showDelegateNoAccessModal, hideAndRun}: MiniHoldItemProps) { +function MiniHoldItem({moneyRequestAction, iouTransaction, isOffline, isDelegateAccessRestricted, showDelegateNoAccessModal, hideAndRun}: MiniHoldItemProps) { const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Stopwatch'] as const); @@ -76,7 +90,7 @@ function MiniHoldItem({moneyRequestAction, isDelegateAccessRestricted, showDeleg hideContextMenu(false, showDelegateNoAccessModal); return; } - hideAndRun(() => changeMoneyRequestHoldStatus(moneyRequestAction)); + hideAndRun(() => changeMoneyRequestHoldStatus(moneyRequestAction, iouTransaction, isOffline)); }, false) } sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.HOLD} diff --git a/src/pages/inbox/report/ContextMenu/actions/joinThreadAction.tsx b/src/pages/inbox/report/ContextMenu/actions/joinThreadAction.tsx index eb12ca8dbfe2..4589766d2cea 100644 --- a/src/pages/inbox/report/ContextMenu/actions/joinThreadAction.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/joinThreadAction.tsx @@ -13,20 +13,33 @@ import {isActionableTrackExpense, isCreatedAction, isCreatedTaskReportAction, is import {getChildReportNotificationPreference, shouldDisableThread, shouldDisplayThreadReplies} from '@libs/ReportUtils'; import {toggleSubscribeToChildReport} from '@userActions/Report'; import CONST from '@src/CONST'; -import type {IntroSelected, ReportAction, Report as ReportType} from '@src/types/onyx'; +import type {Beta, IntroSelected, ReportAction, Report as ReportType} from '@src/types/onyx'; type PopoverJoinThreadItemProps = { reportAction: ReportAction; originalReport: OnyxEntry; currentUserAccountID: number; introSelected: OnyxEntry; + isSelfTourViewed: boolean | undefined; + betas: OnyxEntry; hideAndRun: (callback?: () => void) => void; isFocused?: boolean; onFocus?: () => void; onBlur?: () => void; }; -function PopoverJoinThreadItem({reportAction, originalReport, currentUserAccountID, introSelected, hideAndRun, isFocused, onFocus, onBlur}: PopoverJoinThreadItemProps) { +function PopoverJoinThreadItem({ + reportAction, + originalReport, + currentUserAccountID, + introSelected, + isSelfTourViewed, + betas, + hideAndRun, + isFocused, + onFocus, + onBlur, +}: PopoverJoinThreadItemProps) { const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Bell'] as const); const styles = useThemeStyles(); @@ -42,7 +55,16 @@ function PopoverJoinThreadItem({reportAction, originalReport, currentUserAccount const childReportNotificationPreference = getChildReportNotificationPreference(reportAction); hideAndRun(() => { ReportActionComposeFocusManager.focus(); - toggleSubscribeToChildReport(reportAction?.childReportID, currentUserAccountID, reportAction, originalReport, introSelected, childReportNotificationPreference); + toggleSubscribeToChildReport( + reportAction?.childReportID, + currentUserAccountID, + reportAction, + originalReport, + introSelected, + isSelfTourViewed, + betas, + childReportNotificationPreference, + ); }); }, false) } @@ -62,10 +84,12 @@ type MiniJoinThreadItemProps = { originalReport: OnyxEntry; currentUserAccountID: number; introSelected: OnyxEntry; + isSelfTourViewed: boolean | undefined; + betas: OnyxEntry; hideAndRun: (callback?: () => void) => void; }; -function MiniJoinThreadItem({reportAction, originalReport, currentUserAccountID, introSelected, hideAndRun}: MiniJoinThreadItemProps) { +function MiniJoinThreadItem({reportAction, originalReport, currentUserAccountID, introSelected, isSelfTourViewed, betas, hideAndRun}: MiniJoinThreadItemProps) { const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Bell'] as const); @@ -78,7 +102,16 @@ function MiniJoinThreadItem({reportAction, originalReport, currentUserAccountID, const childReportNotificationPreference = getChildReportNotificationPreference(reportAction); hideAndRun(() => { ReportActionComposeFocusManager.focus(); - toggleSubscribeToChildReport(reportAction?.childReportID, currentUserAccountID, reportAction, originalReport, introSelected, childReportNotificationPreference); + toggleSubscribeToChildReport( + reportAction?.childReportID, + currentUserAccountID, + reportAction, + originalReport, + introSelected, + isSelfTourViewed, + betas, + childReportNotificationPreference, + ); }); }, false) } diff --git a/src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.tsx b/src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.tsx index caea8fd955ff..b4f732623d56 100644 --- a/src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.tsx @@ -13,20 +13,33 @@ import {isActionableTrackExpense, isCreatedAction, isCreatedTaskReportAction, is import {getChildReportNotificationPreference, shouldDisplayThreadReplies} from '@libs/ReportUtils'; import {toggleSubscribeToChildReport} from '@userActions/Report'; import CONST from '@src/CONST'; -import type {IntroSelected, ReportAction, Report as ReportType} from '@src/types/onyx'; +import type {Beta, IntroSelected, ReportAction, Report as ReportType} from '@src/types/onyx'; type PopoverLeaveThreadItemProps = { reportAction: ReportAction; originalReport: OnyxEntry; currentUserAccountID: number; introSelected: OnyxEntry; + isSelfTourViewed: boolean | undefined; + betas: OnyxEntry; hideAndRun: (callback?: () => void) => void; isFocused?: boolean; onFocus?: () => void; onBlur?: () => void; }; -function PopoverLeaveThreadItem({reportAction, originalReport, currentUserAccountID, introSelected, hideAndRun, isFocused, onFocus, onBlur}: PopoverLeaveThreadItemProps) { +function PopoverLeaveThreadItem({ + reportAction, + originalReport, + currentUserAccountID, + introSelected, + isSelfTourViewed, + betas, + hideAndRun, + isFocused, + onFocus, + onBlur, +}: PopoverLeaveThreadItemProps) { const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Exit'] as const); const styles = useThemeStyles(); @@ -42,7 +55,16 @@ function PopoverLeaveThreadItem({reportAction, originalReport, currentUserAccoun const childReportNotificationPreference = getChildReportNotificationPreference(reportAction); hideAndRun(() => { ReportActionComposeFocusManager.focus(); - toggleSubscribeToChildReport(reportAction?.childReportID, currentUserAccountID, reportAction, originalReport, introSelected, childReportNotificationPreference); + toggleSubscribeToChildReport( + reportAction?.childReportID, + currentUserAccountID, + reportAction, + originalReport, + introSelected, + isSelfTourViewed, + betas, + childReportNotificationPreference, + ); }); }, false) } @@ -62,10 +84,12 @@ type MiniLeaveThreadItemProps = { originalReport: OnyxEntry; currentUserAccountID: number; introSelected: OnyxEntry; + isSelfTourViewed: boolean | undefined; + betas: OnyxEntry; hideAndRun: (callback?: () => void) => void; }; -function MiniLeaveThreadItem({reportAction, originalReport, currentUserAccountID, introSelected, hideAndRun}: MiniLeaveThreadItemProps) { +function MiniLeaveThreadItem({reportAction, originalReport, currentUserAccountID, introSelected, isSelfTourViewed, betas, hideAndRun}: MiniLeaveThreadItemProps) { const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Exit'] as const); @@ -78,7 +102,16 @@ function MiniLeaveThreadItem({reportAction, originalReport, currentUserAccountID const childReportNotificationPreference = getChildReportNotificationPreference(reportAction); hideAndRun(() => { ReportActionComposeFocusManager.focus(); - toggleSubscribeToChildReport(reportAction?.childReportID, currentUserAccountID, reportAction, originalReport, introSelected, childReportNotificationPreference); + toggleSubscribeToChildReport( + reportAction?.childReportID, + currentUserAccountID, + reportAction, + originalReport, + introSelected, + isSelfTourViewed, + betas, + childReportNotificationPreference, + ); }); }, false) } diff --git a/src/pages/inbox/report/ContextMenu/actions/replyInThreadAction.tsx b/src/pages/inbox/report/ContextMenu/actions/replyInThreadAction.tsx index 5e1686b7af78..8c3fbb64d63a 100644 --- a/src/pages/inbox/report/ContextMenu/actions/replyInThreadAction.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/replyInThreadAction.tsx @@ -11,7 +11,7 @@ import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {shouldDisableThread} from '@libs/ReportUtils'; import {navigateToAndOpenChildReport} from '@userActions/Report'; import CONST from '@src/CONST'; -import type {IntroSelected, ReportAction, Report as ReportType} from '@src/types/onyx'; +import type {Beta, IntroSelected, ReportAction, Report as ReportType} from '@src/types/onyx'; import KeyboardUtils from '@src/utils/keyboard'; type PopoverReplyInThreadItemProps = { @@ -20,13 +20,25 @@ type PopoverReplyInThreadItemProps = { originalReport: OnyxEntry; currentUserAccountID: number; introSelected: OnyxEntry; + betas: OnyxEntry; hideAndRun: (callback?: () => void) => void; isFocused?: boolean; onFocus?: () => void; onBlur?: () => void; }; -function PopoverReplyInThreadItem({childReport, reportAction, originalReport, currentUserAccountID, introSelected, hideAndRun, isFocused, onFocus, onBlur}: PopoverReplyInThreadItemProps) { +function PopoverReplyInThreadItem({ + childReport, + reportAction, + originalReport, + currentUserAccountID, + introSelected, + betas, + hideAndRun, + isFocused, + onFocus, + onBlur, +}: PopoverReplyInThreadItemProps) { const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['ChatBubbleReply'] as const); const styles = useThemeStyles(); @@ -40,7 +52,7 @@ function PopoverReplyInThreadItem({childReport, reportAction, originalReport, cu onPress={() => interceptAnonymousUser(() => { hideAndRun(() => { - KeyboardUtils.dismiss().then(() => navigateToAndOpenChildReport(childReport, reportAction, originalReport, currentUserAccountID, introSelected)); + KeyboardUtils.dismiss().then(() => navigateToAndOpenChildReport(childReport, reportAction, originalReport, currentUserAccountID, introSelected, betas)); }); }, false) } @@ -61,10 +73,11 @@ type MiniReplyInThreadItemProps = { originalReport: OnyxEntry; currentUserAccountID: number; introSelected: OnyxEntry; + betas: OnyxEntry; hideAndRun: (callback?: () => void) => void; }; -function MiniReplyInThreadItem({childReport, reportAction, originalReport, currentUserAccountID, introSelected, hideAndRun}: MiniReplyInThreadItemProps) { +function MiniReplyInThreadItem({childReport, reportAction, originalReport, currentUserAccountID, introSelected, betas, hideAndRun}: MiniReplyInThreadItemProps) { const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['ChatBubbleReply'] as const); @@ -75,7 +88,7 @@ function MiniReplyInThreadItem({childReport, reportAction, originalReport, curre onPress={() => interceptAnonymousUser(() => { hideAndRun(() => { - KeyboardUtils.dismiss().then(() => navigateToAndOpenChildReport(childReport, reportAction, originalReport, currentUserAccountID, introSelected)); + KeyboardUtils.dismiss().then(() => navigateToAndOpenChildReport(childReport, reportAction, originalReport, currentUserAccountID, introSelected, betas)); }); }, false) } diff --git a/src/pages/inbox/report/ContextMenu/actions/unholdAction.tsx b/src/pages/inbox/report/ContextMenu/actions/unholdAction.tsx index 4a76f6ed22ee..6e35962572c6 100644 --- a/src/pages/inbox/report/ContextMenu/actions/unholdAction.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/unholdAction.tsx @@ -16,6 +16,8 @@ import type {Policy, ReportAction, Report as ReportType, Transaction} from '@src type PopoverUnholdItemProps = { moneyRequestAction: ReportAction | undefined; + iouTransaction: OnyxEntry; + isOffline: boolean; isDelegateAccessRestricted: boolean; showDelegateNoAccessModal: (() => void) | undefined; hideAndRun: (callback?: () => void) => void; @@ -24,7 +26,17 @@ type PopoverUnholdItemProps = { onBlur?: () => void; }; -function PopoverUnholdItem({moneyRequestAction, isDelegateAccessRestricted, showDelegateNoAccessModal, hideAndRun, isFocused, onFocus, onBlur}: PopoverUnholdItemProps) { +function PopoverUnholdItem({ + moneyRequestAction, + iouTransaction, + isOffline, + isDelegateAccessRestricted, + showDelegateNoAccessModal, + hideAndRun, + isFocused, + onFocus, + onBlur, +}: PopoverUnholdItemProps) { const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Stopwatch'] as const); const styles = useThemeStyles(); @@ -41,7 +53,7 @@ function PopoverUnholdItem({moneyRequestAction, isDelegateAccessRestricted, show hideContextMenu(false, showDelegateNoAccessModal); return; } - hideAndRun(() => changeMoneyRequestHoldStatus(moneyRequestAction)); + hideAndRun(() => changeMoneyRequestHoldStatus(moneyRequestAction, iouTransaction, isOffline)); }, false) } wrapperStyle={[styles.pr8]} @@ -57,12 +69,14 @@ function PopoverUnholdItem({moneyRequestAction, isDelegateAccessRestricted, show type MiniUnholdItemProps = { moneyRequestAction: ReportAction | undefined; + iouTransaction: OnyxEntry; + isOffline: boolean; isDelegateAccessRestricted: boolean; showDelegateNoAccessModal: (() => void) | undefined; hideAndRun: (callback?: () => void) => void; }; -function MiniUnholdItem({moneyRequestAction, isDelegateAccessRestricted, showDelegateNoAccessModal, hideAndRun}: MiniUnholdItemProps) { +function MiniUnholdItem({moneyRequestAction, iouTransaction, isOffline, isDelegateAccessRestricted, showDelegateNoAccessModal, hideAndRun}: MiniUnholdItemProps) { const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Stopwatch'] as const); @@ -76,7 +90,7 @@ function MiniUnholdItem({moneyRequestAction, isDelegateAccessRestricted, showDel hideContextMenu(false, showDelegateNoAccessModal); return; } - hideAndRun(() => changeMoneyRequestHoldStatus(moneyRequestAction)); + hideAndRun(() => changeMoneyRequestHoldStatus(moneyRequestAction, iouTransaction, isOffline)); }, false) } sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.UNHOLD} diff --git a/src/pages/inbox/report/ContextMenu/useReportActionContextMenuData.ts b/src/pages/inbox/report/ContextMenu/useReportActionContextMenuData.ts index bdfee6f9bf88..b84044cbfb33 100644 --- a/src/pages/inbox/report/ContextMenu/useReportActionContextMenuData.ts +++ b/src/pages/inbox/report/ContextMenu/useReportActionContextMenuData.ts @@ -1,3 +1,4 @@ +import {hasSeenTourSelector} from '@selectors/Onboarding'; import type {RefObject} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; @@ -72,6 +73,10 @@ function useReportActionContextMenuData({reportID, reportActionID, originalRepor const [isDebugModeEnabled] = useOnyx(ONYXKEYS.IS_DEBUG_MODE_ENABLED); const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT); const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); + const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector}); + const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); + const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); + const [betas] = useOnyx(ONYXKEYS.BETAS); const hasValidReportAction = !isEmptyObject(originalReportActions) && reportActionID && reportActionID !== '0' && reportActionID !== '-1'; const reportAction: OnyxEntry = hasValidReportAction ? originalReportActions[reportActionID] : undefined; @@ -168,6 +173,10 @@ function useReportActionContextMenuData({reportID, reportActionID, originalRepor isDebugModeEnabled, transactions, introSelected, + isSelfTourViewed, + betas, + bankAccountList, + conciergeReportID, movedFromReport, movedToReport, harvestReport, diff --git a/tests/ui/components/BaseReportActionContextMenuTest.tsx b/tests/ui/components/BaseReportActionContextMenuTest.tsx deleted file mode 100644 index 8c685c76d309..000000000000 --- a/tests/ui/components/BaseReportActionContextMenuTest.tsx +++ /dev/null @@ -1,397 +0,0 @@ -import {act, render, waitFor} from '@testing-library/react-native'; -import React from 'react'; -import Onyx from 'react-native-onyx'; -import BaseReportActionContextMenu from '@pages/inbox/report/ContextMenu/BaseReportActionContextMenu'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; -import type {PersonalDetailsList} from '@src/types/onyx'; -import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; - -jest.mock('@components/ActionSheetAwareScrollView', () => ({ - useActionSheetAwareScrollViewActions: () => ({ - transitionActionSheetState: jest.fn(), - }), -})); - -type MockContextMenuItemProps = { - sentryLabel?: string; - onPress?: (event: unknown) => void; - [key: string]: unknown; -}; - -const mockContextMenuItemProps: MockContextMenuItemProps[] = []; - -jest.mock('@components/ContextMenuItem', () => { - function MockContextMenuItem(props: MockContextMenuItemProps) { - const {sentryLabel, onPress} = props; - mockContextMenuItemProps.push({...props, sentryLabel, onPress}); - return null; - } - - return MockContextMenuItem; -}); - -jest.mock('@components/DelegateNoAccessModalProvider', () => ({ - useDelegateNoAccessState: () => ({isDelegateAccessRestricted: false}), - useDelegateNoAccessActions: () => ({showDelegateNoAccessModal: jest.fn()}), -})); - -jest.mock('@components/FocusTrap/FocusTrapForModal', () => { - function MockFocusTrapForModal({children}: {children: React.ReactNode}) { - return children; - } - - return MockFocusTrapForModal; -}); - -jest.mock('@components/OnyxListItemProvider', () => ({ - useSession: () => ({encryptedAuthToken: 'token'}), -})); - -jest.mock('@hooks/useArrowKeyFocusManager', () => () => [-1, jest.fn()] as const); -jest.mock('@hooks/useCurrentUserPersonalDetails', () => () => ({accountID: 1, login: 'user@test.com', email: 'user@test.com'})); -jest.mock('@hooks/useEnvironment', () => () => ({isProduction: false})); -jest.mock('@hooks/useGetExpensifyCardFromReportAction', () => () => undefined); -jest.mock('@hooks/useLazyAsset', () => ({ - useMemoizedLazyExpensifyIcons: () => ({ - Bell: undefined, - Bug: undefined, - ChatBubbleReply: undefined, - ChatBubbleUnread: undefined, - Checkmark: undefined, - Concierge: undefined, - Copy: undefined, - Download: undefined, - Exit: undefined, - Flag: undefined, - LinkCopy: undefined, - Mail: undefined, - Pencil: undefined, - Pin: undefined, - Stopwatch: undefined, - ThreeDots: undefined, - Trashcan: undefined, - }), -})); - -jest.mock('@hooks/useLocalize', () => () => ({ - translate: (key: string) => key, - getLocalDateFromDatetime: jest.fn(), -})); -jest.mock('@hooks/useNetwork', () => () => ({isOffline: false})); -jest.mock('@hooks/usePaginatedReportActions', () => () => ({reportActions: []})); -jest.mock('@hooks/useReportIsArchived', () => () => false); -jest.mock('@hooks/useResponsiveLayout', () => () => ({shouldUseNarrowLayout: true, isSmallScreenWidth: false})); -jest.mock('@hooks/useRestoreInputFocus', () => () => {}); -jest.mock( - '@hooks/useStyleUtils', - () => () => - new Proxy( - {}, - { - get: () => () => ({}), - }, - ), -); -jest.mock('@hooks/useTransactionsAndViolationsForReport', () => () => ({transactions: {}})); - -jest.mock('@userActions/Session', () => ({ - isAnonymousUser: () => false, - signOutAndRedirectToSignIn: jest.fn(), - callFunctionIfActionIsAllowed: (fn: () => void) => fn, -})); - -jest.mock('@pages/inbox/report/ContextMenu/ReportActionContextMenu', () => ({ - hideContextMenu: jest.fn((_: boolean, onHideCallback?: () => void) => onHideCallback?.()), - showContextMenu: jest.fn(), -})); - -const mockUnholdRequest = jest.fn(); -jest.mock('@libs/actions/IOU/Hold', () => { - // eslint-disable-next-line @typescript-eslint/consistent-type-imports -- Ignoring type errors for testing purposes - const actual = jest.requireActual('@libs/actions/IOU/Hold'); - return { - ...actual, - // eslint-disable-next-line @typescript-eslint/no-unsafe-return -- Ignoring type errors for testing purposes - unholdRequest: (...args: Parameters) => mockUnholdRequest(...args), - }; -}); - -const mockNavigate = jest.fn(); -const mockSetParams = jest.fn(); -const mockIsReady = jest.fn(() => false); -const mockGetActiveRoute = jest.fn(() => ''); -const mockGetCurrentRoute = jest.fn(() => undefined as {name: string; params: Record} | undefined); - -jest.mock('@libs/Navigation/Navigation', () => ({ - navigate: (...args: unknown[]) => mockNavigate(...args) as void, - setParams: (...args: unknown[]) => mockSetParams(...args) as void, - getActiveRoute: () => mockGetActiveRoute(), - navigationRef: { - isReady: () => mockIsReady(), - getCurrentRoute: () => mockGetCurrentRoute(), - }, -})); - -const currentUserAccountID = 1; -const originalReportID = '100'; -const reportActionID = '200'; -const childReportID = '300'; -const iouReportID = '400'; -const transactionID = '500'; -const policyID = 'policy1'; -const holdActionID = 'holdAction1'; -const testPersonalDetails: PersonalDetailsList = { - [currentUserAccountID]: { - accountID: currentUserAccountID, - login: 'user@test.com', - displayName: 'Test User', - }, -}; -async function seedOnyxData({isOnHold}: {isOnHold: boolean}) { - await Onyx.clear(); - - // Session and personal details for current user (needed by canHoldUnholdReportAction / canModifyHoldStatus) - await Onyx.merge(ONYXKEYS.SESSION, { - accountID: currentUserAccountID, - email: 'user@test.com', - }); - - await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, testPersonalDetails); - - // Policy (needed by canModifyHoldStatus for isPolicyAdmin, and by changeMoneyRequestHoldStatus) - await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { - id: policyID, - type: CONST.POLICY.TYPE.TEAM, - role: CONST.POLICY.ROLE.ADMIN, - }); - - // Original report (chat report containing the report action) - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${originalReportID}`, { - reportID: originalReportID, - type: CONST.REPORT.TYPE.CHAT, - }); - - // Report actions on the original report - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${originalReportID}`, { - [reportActionID]: { - reportActionID, - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - childReportID, - }, - parentIOUAction: { - reportActionID: 'parentIOUAction', - actionName: CONST.REPORT.ACTIONS.TYPE.IOU, - actorAccountID: currentUserAccountID, - childReportID, - originalMessage: { - IOUReportID: iouReportID, - IOUTransactionID: transactionID, - type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, - }, - }, - }); - - // Child report (expense report) linked from the report action - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${childReportID}`, { - reportID: childReportID, - type: CONST.REPORT.TYPE.EXPENSE, - parentReportID: originalReportID, - parentReportActionID: 'parentIOUAction', - policyID, - managerID: currentUserAccountID, - stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, - statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, - }); - - // Report actions on child report - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${childReportID}`, { - iouAction: { - reportActionID: 'iouAction', - actionName: CONST.REPORT.ACTIONS.TYPE.IOU, - actorAccountID: currentUserAccountID, - childReportID, - originalMessage: { - IOUReportID: iouReportID, - IOUTransactionID: transactionID, - type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, - }, - }, - ...(isOnHold - ? { - [holdActionID]: { - reportActionID: holdActionID, - actionName: CONST.REPORT.ACTIONS.TYPE.HOLD, - actorAccountID: currentUserAccountID, - }, - } - : {}), - }); - - // IOU report referenced by the money request action - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, { - reportID: iouReportID, - type: CONST.REPORT.TYPE.EXPENSE, - policyID, - managerID: currentUserAccountID, - stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, - statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, - }); - - // Transaction (on hold or not) - await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, { - transactionID, - reportID: iouReportID, - comment: isOnHold ? {hold: holdActionID} : {}, - }); - - await waitForBatchedUpdates(); -} - -async function getContextMenuItemOnPress(sentryLabel: string): Promise<(event: unknown) => void> { - let contextMenuItem = mockContextMenuItemProps.find((item) => item.sentryLabel === sentryLabel); - - await waitFor(() => { - contextMenuItem = mockContextMenuItemProps.find((item) => item.sentryLabel === sentryLabel); - expect(contextMenuItem).toBeDefined(); - expect(contextMenuItem?.onPress).toBeDefined(); - }); - - return contextMenuItem?.onPress ?? (() => undefined); -} - -describe('BaseReportActionContextMenu edit action', () => { - beforeEach(async () => { - jest.clearAllMocks(); - mockContextMenuItemProps.length = 0; - }); - - beforeAll(() => { - Onyx.init({keys: ONYXKEYS, evictableKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS]}); - }); - - it('shows the edit action for an editable comment by current user', async () => { - await seedOnyxData({isOnHold: false}); - - // Override the report action to be a plain ADD_COMMENT (editable) - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${originalReportID}`, { - [reportActionID]: { - reportActionID, - actorAccountID: currentUserAccountID, - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - message: [ - { - type: 'COMMENT', - html: 'Hello world', - text: 'Hello world', - }, - ], - created: '2025-03-05 16:34:27', - }, - }); - await waitForBatchedUpdates(); - - render( - , - ); - - await waitFor(() => { - const editItem = mockContextMenuItemProps.find((item) => item.sentryLabel === CONST.SENTRY_LABEL.CONTEXT_MENU.EDIT_COMMENT); - expect(editItem).toBeDefined(); - }); - }); - - it('does not show the edit action for a comment by another user', async () => { - await seedOnyxData({isOnHold: false}); - - const otherUserAccountID = 999; - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${originalReportID}`, { - [reportActionID]: { - reportActionID, - actorAccountID: otherUserAccountID, - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - message: [ - { - type: 'COMMENT', - html: 'Hello from another user', - text: 'Hello from another user', - }, - ], - created: '2025-03-05 16:34:27', - }, - }); - await waitForBatchedUpdates(); - - render( - , - ); - - await waitForBatchedUpdates(); - const editItem = mockContextMenuItemProps.find((item) => item.sentryLabel === CONST.SENTRY_LABEL.CONTEXT_MENU.EDIT_COMMENT); - expect(editItem).toBeUndefined(); - }); -}); - -describe('BaseReportActionContextMenu hold/unhold action', () => { - beforeEach(async () => { - jest.clearAllMocks(); - mockContextMenuItemProps.length = 0; - }); - - beforeAll(() => { - Onyx.init({keys: ONYXKEYS, evictableKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS]}); - }); - - it('navigates to hold reason page when pressing the hold action', async () => { - await seedOnyxData({isOnHold: false}); - - render( - , - ); - - const onPress = await getContextMenuItemOnPress(CONST.SENTRY_LABEL.CONTEXT_MENU.HOLD); - await act(async () => { - onPress({}); - }); - - expect(mockNavigate).toHaveBeenCalledTimes(1); - expect(mockNavigate).toHaveBeenCalledWith(ROUTES.MONEY_REQUEST_HOLD_REASON.getRoute(CONST.POLICY.TYPE.TEAM, transactionID, childReportID, encodeURIComponent(mockGetActiveRoute()))); - }); - - it('calls unholdRequest when pressing the unhold action', async () => { - await seedOnyxData({isOnHold: true}); - - render( - , - ); - - const onPress = await getContextMenuItemOnPress(CONST.SENTRY_LABEL.CONTEXT_MENU.UNHOLD); - await act(async () => { - onPress({}); - }); - - expect(mockUnholdRequest).toHaveBeenCalledTimes(1); - expect(mockUnholdRequest).toHaveBeenCalledWith(transactionID, childReportID, expect.objectContaining({id: policyID}), false); - }); -}); diff --git a/tests/unit/ContextMenuActionsCopyMessageTest.ts b/tests/unit/ContextMenuActionsCopyMessageTest.ts index b1c53d9a70e4..18e89c0d6ab7 100644 --- a/tests/unit/ContextMenuActionsCopyMessageTest.ts +++ b/tests/unit/ContextMenuActionsCopyMessageTest.ts @@ -47,6 +47,8 @@ const createParams = (selection: string) => ({ transaction: undefined, selection, report: undefined, + conciergeReportID: undefined, + bankAccountList: undefined, card: undefined, originalReport: undefined, isHarvestReport: false, From 2aedc8fbed8a28bac844e6fd2cc2dc15de300b27 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 6 Apr 2026 15:15:41 -0700 Subject: [PATCH 72/88] refactor(contextmenu): rename action files to PascalCase and split multi-component files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split 13 multi-component action files into dedicated directories with separate files per component (Popover/Mini) and shared utilities. Renamed 5 single-component files from camelCase to PascalCase to match component naming conventions. No barrel files — consumers import directly from individual paths. Made-with: Cursor --- .../MiniReportActionContextMenu/index.tsx | 39 +++-- .../PopoverReportActionContent.tsx | 41 +++-- .../PopoverReportContent.tsx | 13 +- .../CopyLinkAction/MiniCopyLinkItem.tsx | 40 +++++ .../CopyLinkAction/PopoverCopyLinkItem.tsx | 47 ++++++ .../actions/CopyLinkAction/copyLinkAction.ts | 16 ++ .../CopyMessageAction/MiniCopyMessageItem.tsx | 72 ++++++++ .../PopoverCopyMessageItem.tsx | 83 ++++++++++ .../copyMessageAction.ts} | 148 +---------------- ...xDataAction.tsx => CopyOnyxDataAction.tsx} | 0 .../{debugAction.tsx => DebugAction.tsx} | 0 .../actions/DeleteAction/MiniDeleteItem.tsx | 34 ++++ .../DeleteAction/PopoverDeleteItem.tsx | 49 ++++++ .../actions/DeleteAction/deleteAction.ts | 41 +++++ .../DownloadAction/MiniDownloadItem.tsx | 49 ++++++ .../PopoverDownloadItem.tsx} | 49 +----- .../actions/DownloadAction/downloadAction.ts | 16 ++ .../actions/EditAction/MiniEditItem.tsx | 56 +++++++ .../PopoverEditItem.tsx} | 68 +------- .../actions/EditAction/editAction.ts | 22 +++ .../actions/ExplainAction/MiniExplainItem.tsx | 55 +++++++ .../PopoverExplainItem.tsx} | 68 ++------ .../actions/ExplainAction/explainAction.ts | 13 ++ .../MiniFlagAsOffensiveItem.tsx | 39 +++++ .../PopoverFlagAsOffensiveItem.tsx} | 51 +----- .../flagAsOffensiveAction.ts | 21 +++ .../actions/HoldAction/MiniHoldItem.tsx | 41 +++++ .../actions/HoldAction/PopoverHoldItem.tsx | 66 ++++++++ .../actions/HoldAction/holdAction.ts | 27 +++ .../JoinThreadAction/MiniJoinThreadItem.tsx | 52 ++++++ .../PopoverJoinThreadItem.tsx | 78 +++++++++ .../JoinThreadAction/joinThreadAction.ts | 39 +++++ .../LeaveThreadAction/MiniLeaveThreadItem.tsx | 52 ++++++ .../PopoverLeaveThreadItem.tsx | 78 +++++++++ .../LeaveThreadAction/leaveThreadAction.ts | 37 +++++ ...kAsReadAction.tsx => MarkAsReadAction.tsx} | 0 .../MiniMarkAsUnreadItem.tsx | 38 +++++ .../PopoverMarkAsUnreadItem.tsx | 44 +++++ .../MarkAsUnreadAction/markAsUnreadAction.ts | 14 ++ .../actions/{pinAction.tsx => PinAction.tsx} | 0 .../MiniReplyInThreadItem.tsx | 40 +++++ .../PopoverReplyInThreadItem.tsx} | 53 +----- .../replyInThreadAction.ts | 23 +++ .../actions/UnholdAction/MiniUnholdItem.tsx | 41 +++++ .../UnholdAction/PopoverUnholdItem.tsx | 66 ++++++++ .../actions/UnholdAction/unholdAction.ts | 27 +++ .../{unpinAction.tsx => UnpinAction.tsx} | 0 .../ContextMenu/actions/copyLinkAction.tsx | 90 ---------- .../ContextMenu/actions/deleteAction.tsx | 114 ------------- .../report/ContextMenu/actions/holdAction.tsx | 121 -------------- .../ContextMenu/actions/joinThreadAction.tsx | 155 ------------------ .../ContextMenu/actions/leaveThreadAction.tsx | 153 ----------------- .../actions/markAsUnreadAction.tsx | 84 ---------- .../ContextMenu/actions/unholdAction.tsx | 121 -------------- .../unit/ContextMenuActionsCopyMessageTest.ts | 2 +- 55 files changed, 1500 insertions(+), 1286 deletions(-) create mode 100644 src/pages/inbox/report/ContextMenu/actions/CopyLinkAction/MiniCopyLinkItem.tsx create mode 100644 src/pages/inbox/report/ContextMenu/actions/CopyLinkAction/PopoverCopyLinkItem.tsx create mode 100644 src/pages/inbox/report/ContextMenu/actions/CopyLinkAction/copyLinkAction.ts create mode 100644 src/pages/inbox/report/ContextMenu/actions/CopyMessageAction/MiniCopyMessageItem.tsx create mode 100644 src/pages/inbox/report/ContextMenu/actions/CopyMessageAction/PopoverCopyMessageItem.tsx rename src/pages/inbox/report/ContextMenu/actions/{copyMessageAction.tsx => CopyMessageAction/copyMessageAction.ts} (88%) rename src/pages/inbox/report/ContextMenu/actions/{copyOnyxDataAction.tsx => CopyOnyxDataAction.tsx} (100%) rename src/pages/inbox/report/ContextMenu/actions/{debugAction.tsx => DebugAction.tsx} (100%) create mode 100644 src/pages/inbox/report/ContextMenu/actions/DeleteAction/MiniDeleteItem.tsx create mode 100644 src/pages/inbox/report/ContextMenu/actions/DeleteAction/PopoverDeleteItem.tsx create mode 100644 src/pages/inbox/report/ContextMenu/actions/DeleteAction/deleteAction.ts create mode 100644 src/pages/inbox/report/ContextMenu/actions/DownloadAction/MiniDownloadItem.tsx rename src/pages/inbox/report/ContextMenu/actions/{downloadAction.tsx => DownloadAction/PopoverDownloadItem.tsx} (53%) create mode 100644 src/pages/inbox/report/ContextMenu/actions/DownloadAction/downloadAction.ts create mode 100644 src/pages/inbox/report/ContextMenu/actions/EditAction/MiniEditItem.tsx rename src/pages/inbox/report/ContextMenu/actions/{editAction.tsx => EditAction/PopoverEditItem.tsx} (50%) create mode 100644 src/pages/inbox/report/ContextMenu/actions/EditAction/editAction.ts create mode 100644 src/pages/inbox/report/ContextMenu/actions/ExplainAction/MiniExplainItem.tsx rename src/pages/inbox/report/ContextMenu/actions/{explainAction.tsx => ExplainAction/PopoverExplainItem.tsx} (54%) create mode 100644 src/pages/inbox/report/ContextMenu/actions/ExplainAction/explainAction.ts create mode 100644 src/pages/inbox/report/ContextMenu/actions/FlagAsOffensiveAction/MiniFlagAsOffensiveItem.tsx rename src/pages/inbox/report/ContextMenu/actions/{flagAsOffensiveAction.tsx => FlagAsOffensiveAction/PopoverFlagAsOffensiveItem.tsx} (50%) create mode 100644 src/pages/inbox/report/ContextMenu/actions/FlagAsOffensiveAction/flagAsOffensiveAction.ts create mode 100644 src/pages/inbox/report/ContextMenu/actions/HoldAction/MiniHoldItem.tsx create mode 100644 src/pages/inbox/report/ContextMenu/actions/HoldAction/PopoverHoldItem.tsx create mode 100644 src/pages/inbox/report/ContextMenu/actions/HoldAction/holdAction.ts create mode 100644 src/pages/inbox/report/ContextMenu/actions/JoinThreadAction/MiniJoinThreadItem.tsx create mode 100644 src/pages/inbox/report/ContextMenu/actions/JoinThreadAction/PopoverJoinThreadItem.tsx create mode 100644 src/pages/inbox/report/ContextMenu/actions/JoinThreadAction/joinThreadAction.ts create mode 100644 src/pages/inbox/report/ContextMenu/actions/LeaveThreadAction/MiniLeaveThreadItem.tsx create mode 100644 src/pages/inbox/report/ContextMenu/actions/LeaveThreadAction/PopoverLeaveThreadItem.tsx create mode 100644 src/pages/inbox/report/ContextMenu/actions/LeaveThreadAction/leaveThreadAction.ts rename src/pages/inbox/report/ContextMenu/actions/{markAsReadAction.tsx => MarkAsReadAction.tsx} (100%) create mode 100644 src/pages/inbox/report/ContextMenu/actions/MarkAsUnreadAction/MiniMarkAsUnreadItem.tsx create mode 100644 src/pages/inbox/report/ContextMenu/actions/MarkAsUnreadAction/PopoverMarkAsUnreadItem.tsx create mode 100644 src/pages/inbox/report/ContextMenu/actions/MarkAsUnreadAction/markAsUnreadAction.ts rename src/pages/inbox/report/ContextMenu/actions/{pinAction.tsx => PinAction.tsx} (100%) create mode 100644 src/pages/inbox/report/ContextMenu/actions/ReplyInThreadAction/MiniReplyInThreadItem.tsx rename src/pages/inbox/report/ContextMenu/actions/{replyInThreadAction.tsx => ReplyInThreadAction/PopoverReplyInThreadItem.tsx} (55%) create mode 100644 src/pages/inbox/report/ContextMenu/actions/ReplyInThreadAction/replyInThreadAction.ts create mode 100644 src/pages/inbox/report/ContextMenu/actions/UnholdAction/MiniUnholdItem.tsx create mode 100644 src/pages/inbox/report/ContextMenu/actions/UnholdAction/PopoverUnholdItem.tsx create mode 100644 src/pages/inbox/report/ContextMenu/actions/UnholdAction/unholdAction.ts rename src/pages/inbox/report/ContextMenu/actions/{unpinAction.tsx => UnpinAction.tsx} (100%) delete mode 100644 src/pages/inbox/report/ContextMenu/actions/copyLinkAction.tsx delete mode 100644 src/pages/inbox/report/ContextMenu/actions/deleteAction.tsx delete mode 100644 src/pages/inbox/report/ContextMenu/actions/holdAction.tsx delete mode 100644 src/pages/inbox/report/ContextMenu/actions/joinThreadAction.tsx delete mode 100644 src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.tsx delete mode 100644 src/pages/inbox/report/ContextMenu/actions/markAsUnreadAction.tsx delete mode 100644 src/pages/inbox/report/ContextMenu/actions/unholdAction.tsx diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index 5f8e250014ac..9415f628a6c9 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -15,20 +15,33 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {ACTION_IDS} from '@pages/inbox/report/ContextMenu/actions/actionConfig'; -import {MiniCopyLinkItem, shouldShowCopyLinkAction} from '@pages/inbox/report/ContextMenu/actions/copyLinkAction'; -import {MiniCopyMessageItem, shouldShowCopyMessageAction} from '@pages/inbox/report/ContextMenu/actions/copyMessageAction'; -import {MiniDeleteItem, shouldShowDeleteAction} from '@pages/inbox/report/ContextMenu/actions/deleteAction'; -import {MiniDownloadItem, shouldShowDownloadAction} from '@pages/inbox/report/ContextMenu/actions/downloadAction'; -import {MiniEditItem, shouldShowEditAction} from '@pages/inbox/report/ContextMenu/actions/editAction'; +import {shouldShowCopyLinkAction} from '@pages/inbox/report/ContextMenu/actions/CopyLinkAction/copyLinkAction'; +import MiniCopyLinkItem from '@pages/inbox/report/ContextMenu/actions/CopyLinkAction/MiniCopyLinkItem'; +import {shouldShowCopyMessageAction} from '@pages/inbox/report/ContextMenu/actions/CopyMessageAction/copyMessageAction'; +import MiniCopyMessageItem from '@pages/inbox/report/ContextMenu/actions/CopyMessageAction/MiniCopyMessageItem'; +import {shouldShowDeleteAction} from '@pages/inbox/report/ContextMenu/actions/DeleteAction/deleteAction'; +import MiniDeleteItem from '@pages/inbox/report/ContextMenu/actions/DeleteAction/MiniDeleteItem'; +import {shouldShowDownloadAction} from '@pages/inbox/report/ContextMenu/actions/DownloadAction/downloadAction'; +import MiniDownloadItem from '@pages/inbox/report/ContextMenu/actions/DownloadAction/MiniDownloadItem'; +import {shouldShowEditAction} from '@pages/inbox/report/ContextMenu/actions/EditAction/editAction'; +import MiniEditItem from '@pages/inbox/report/ContextMenu/actions/EditAction/MiniEditItem'; import createEmojiReactionData, {shouldShowEmojiReaction} from '@pages/inbox/report/ContextMenu/actions/emojiReactionAction'; -import {MiniExplainItem, shouldShowExplainAction} from '@pages/inbox/report/ContextMenu/actions/explainAction'; -import {MiniFlagAsOffensiveItem, shouldShowFlagAsOffensiveAction} from '@pages/inbox/report/ContextMenu/actions/flagAsOffensiveAction'; -import {MiniHoldItem, shouldShowHoldAction} from '@pages/inbox/report/ContextMenu/actions/holdAction'; -import {MiniJoinThreadItem, shouldShowJoinThreadAction} from '@pages/inbox/report/ContextMenu/actions/joinThreadAction'; -import {MiniLeaveThreadItem, shouldShowLeaveThreadAction} from '@pages/inbox/report/ContextMenu/actions/leaveThreadAction'; -import {MiniMarkAsUnreadItem, shouldShowMarkAsUnreadForReportAction} from '@pages/inbox/report/ContextMenu/actions/markAsUnreadAction'; -import {MiniReplyInThreadItem, shouldShowReplyInThreadAction} from '@pages/inbox/report/ContextMenu/actions/replyInThreadAction'; -import {MiniUnholdItem, shouldShowUnholdAction} from '@pages/inbox/report/ContextMenu/actions/unholdAction'; +import {shouldShowExplainAction} from '@pages/inbox/report/ContextMenu/actions/ExplainAction/explainAction'; +import MiniExplainItem from '@pages/inbox/report/ContextMenu/actions/ExplainAction/MiniExplainItem'; +import {shouldShowFlagAsOffensiveAction} from '@pages/inbox/report/ContextMenu/actions/FlagAsOffensiveAction/flagAsOffensiveAction'; +import MiniFlagAsOffensiveItem from '@pages/inbox/report/ContextMenu/actions/FlagAsOffensiveAction/MiniFlagAsOffensiveItem'; +import {shouldShowHoldAction} from '@pages/inbox/report/ContextMenu/actions/HoldAction/holdAction'; +import MiniHoldItem from '@pages/inbox/report/ContextMenu/actions/HoldAction/MiniHoldItem'; +import {shouldShowJoinThreadAction} from '@pages/inbox/report/ContextMenu/actions/JoinThreadAction/joinThreadAction'; +import MiniJoinThreadItem from '@pages/inbox/report/ContextMenu/actions/JoinThreadAction/MiniJoinThreadItem'; +import {shouldShowLeaveThreadAction} from '@pages/inbox/report/ContextMenu/actions/LeaveThreadAction/leaveThreadAction'; +import MiniLeaveThreadItem from '@pages/inbox/report/ContextMenu/actions/LeaveThreadAction/MiniLeaveThreadItem'; +import {shouldShowMarkAsUnreadForReportAction} from '@pages/inbox/report/ContextMenu/actions/MarkAsUnreadAction/markAsUnreadAction'; +import MiniMarkAsUnreadItem from '@pages/inbox/report/ContextMenu/actions/MarkAsUnreadAction/MiniMarkAsUnreadItem'; +import MiniReplyInThreadItem from '@pages/inbox/report/ContextMenu/actions/ReplyInThreadAction/MiniReplyInThreadItem'; +import {shouldShowReplyInThreadAction} from '@pages/inbox/report/ContextMenu/actions/ReplyInThreadAction/replyInThreadAction'; +import MiniUnholdItem from '@pages/inbox/report/ContextMenu/actions/UnholdAction/MiniUnholdItem'; +import {shouldShowUnholdAction} from '@pages/inbox/report/ContextMenu/actions/UnholdAction/unholdAction'; import {useMiniContextMenuActions, useMiniContextMenuState} from '@pages/inbox/report/ContextMenu/MiniContextMenuProvider'; import type {ContextMenuAnchor} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {showContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx index 8447788af7fe..2853ed8af409 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent.tsx @@ -16,21 +16,34 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {ACTION_IDS} from '@pages/inbox/report/ContextMenu/actions/actionConfig'; -import {PopoverCopyLinkItem, shouldShowCopyLinkAction} from '@pages/inbox/report/ContextMenu/actions/copyLinkAction'; -import {PopoverCopyMessageItem, shouldShowCopyMessageAction} from '@pages/inbox/report/ContextMenu/actions/copyMessageAction'; -import {PopoverDebugItem, shouldShowDebugAction} from '@pages/inbox/report/ContextMenu/actions/debugAction'; -import {PopoverDeleteItem, shouldShowDeleteAction} from '@pages/inbox/report/ContextMenu/actions/deleteAction'; -import {PopoverDownloadItem, shouldShowDownloadAction} from '@pages/inbox/report/ContextMenu/actions/downloadAction'; -import {PopoverEditItem, shouldShowEditAction} from '@pages/inbox/report/ContextMenu/actions/editAction'; +import {shouldShowCopyLinkAction} from '@pages/inbox/report/ContextMenu/actions/CopyLinkAction/copyLinkAction'; +import PopoverCopyLinkItem from '@pages/inbox/report/ContextMenu/actions/CopyLinkAction/PopoverCopyLinkItem'; +import {shouldShowCopyMessageAction} from '@pages/inbox/report/ContextMenu/actions/CopyMessageAction/copyMessageAction'; +import PopoverCopyMessageItem from '@pages/inbox/report/ContextMenu/actions/CopyMessageAction/PopoverCopyMessageItem'; +import {PopoverDebugItem, shouldShowDebugAction} from '@pages/inbox/report/ContextMenu/actions/DebugAction'; +import {shouldShowDeleteAction} from '@pages/inbox/report/ContextMenu/actions/DeleteAction/deleteAction'; +import PopoverDeleteItem from '@pages/inbox/report/ContextMenu/actions/DeleteAction/PopoverDeleteItem'; +import {shouldShowDownloadAction} from '@pages/inbox/report/ContextMenu/actions/DownloadAction/downloadAction'; +import PopoverDownloadItem from '@pages/inbox/report/ContextMenu/actions/DownloadAction/PopoverDownloadItem'; +import {shouldShowEditAction} from '@pages/inbox/report/ContextMenu/actions/EditAction/editAction'; +import PopoverEditItem from '@pages/inbox/report/ContextMenu/actions/EditAction/PopoverEditItem'; import createEmojiReactionData, {shouldShowEmojiReaction} from '@pages/inbox/report/ContextMenu/actions/emojiReactionAction'; -import {PopoverExplainItem, shouldShowExplainAction} from '@pages/inbox/report/ContextMenu/actions/explainAction'; -import {PopoverFlagAsOffensiveItem, shouldShowFlagAsOffensiveAction} from '@pages/inbox/report/ContextMenu/actions/flagAsOffensiveAction'; -import {PopoverHoldItem, shouldShowHoldAction} from '@pages/inbox/report/ContextMenu/actions/holdAction'; -import {PopoverJoinThreadItem, shouldShowJoinThreadAction} from '@pages/inbox/report/ContextMenu/actions/joinThreadAction'; -import {PopoverLeaveThreadItem, shouldShowLeaveThreadAction} from '@pages/inbox/report/ContextMenu/actions/leaveThreadAction'; -import {PopoverMarkAsUnreadItem, shouldShowMarkAsUnreadForReportAction} from '@pages/inbox/report/ContextMenu/actions/markAsUnreadAction'; -import {PopoverReplyInThreadItem, shouldShowReplyInThreadAction} from '@pages/inbox/report/ContextMenu/actions/replyInThreadAction'; -import {PopoverUnholdItem, shouldShowUnholdAction} from '@pages/inbox/report/ContextMenu/actions/unholdAction'; +import {shouldShowExplainAction} from '@pages/inbox/report/ContextMenu/actions/ExplainAction/explainAction'; +import PopoverExplainItem from '@pages/inbox/report/ContextMenu/actions/ExplainAction/PopoverExplainItem'; +import {shouldShowFlagAsOffensiveAction} from '@pages/inbox/report/ContextMenu/actions/FlagAsOffensiveAction/flagAsOffensiveAction'; +import PopoverFlagAsOffensiveItem from '@pages/inbox/report/ContextMenu/actions/FlagAsOffensiveAction/PopoverFlagAsOffensiveItem'; +import {shouldShowHoldAction} from '@pages/inbox/report/ContextMenu/actions/HoldAction/holdAction'; +import PopoverHoldItem from '@pages/inbox/report/ContextMenu/actions/HoldAction/PopoverHoldItem'; +import {shouldShowJoinThreadAction} from '@pages/inbox/report/ContextMenu/actions/JoinThreadAction/joinThreadAction'; +import PopoverJoinThreadItem from '@pages/inbox/report/ContextMenu/actions/JoinThreadAction/PopoverJoinThreadItem'; +import {shouldShowLeaveThreadAction} from '@pages/inbox/report/ContextMenu/actions/LeaveThreadAction/leaveThreadAction'; +import PopoverLeaveThreadItem from '@pages/inbox/report/ContextMenu/actions/LeaveThreadAction/PopoverLeaveThreadItem'; +import {shouldShowMarkAsUnreadForReportAction} from '@pages/inbox/report/ContextMenu/actions/MarkAsUnreadAction/markAsUnreadAction'; +import PopoverMarkAsUnreadItem from '@pages/inbox/report/ContextMenu/actions/MarkAsUnreadAction/PopoverMarkAsUnreadItem'; +import PopoverReplyInThreadItem from '@pages/inbox/report/ContextMenu/actions/ReplyInThreadAction/PopoverReplyInThreadItem'; +import {shouldShowReplyInThreadAction} from '@pages/inbox/report/ContextMenu/actions/ReplyInThreadAction/replyInThreadAction'; +import PopoverUnholdItem from '@pages/inbox/report/ContextMenu/actions/UnholdAction/PopoverUnholdItem'; +import {shouldShowUnholdAction} from '@pages/inbox/report/ContextMenu/actions/UnholdAction/unholdAction'; import {showContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import useReportActionContextMenuData from '@pages/inbox/report/ContextMenu/useReportActionContextMenuData'; import CONST from '@src/CONST'; diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportContent.tsx index 0ad9f79835ad..1c63800b06f7 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportContent.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportContent.tsx @@ -12,12 +12,13 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import {canWriteInReport, isUnread} from '@libs/ReportUtils'; import {ACTION_IDS, RESTRICTED_READONLY_ACTION_IDS} from '@pages/inbox/report/ContextMenu/actions/actionConfig'; -import {PopoverCopyOnyxDataItem, shouldShowCopyOnyxDataAction} from '@pages/inbox/report/ContextMenu/actions/copyOnyxDataAction'; -import {PopoverDebugItem, shouldShowDebugAction} from '@pages/inbox/report/ContextMenu/actions/debugAction'; -import {PopoverMarkAsReadItem, shouldShowMarkAsReadAction} from '@pages/inbox/report/ContextMenu/actions/markAsReadAction'; -import {PopoverMarkAsUnreadItem, shouldShowMarkAsUnreadForReport} from '@pages/inbox/report/ContextMenu/actions/markAsUnreadAction'; -import {PopoverPinItem, shouldShowPinAction} from '@pages/inbox/report/ContextMenu/actions/pinAction'; -import {PopoverUnpinItem, shouldShowUnpinAction} from '@pages/inbox/report/ContextMenu/actions/unpinAction'; +import {PopoverCopyOnyxDataItem, shouldShowCopyOnyxDataAction} from '@pages/inbox/report/ContextMenu/actions/CopyOnyxDataAction'; +import {PopoverDebugItem, shouldShowDebugAction} from '@pages/inbox/report/ContextMenu/actions/DebugAction'; +import {PopoverMarkAsReadItem, shouldShowMarkAsReadAction} from '@pages/inbox/report/ContextMenu/actions/MarkAsReadAction'; +import {shouldShowMarkAsUnreadForReport} from '@pages/inbox/report/ContextMenu/actions/MarkAsUnreadAction/markAsUnreadAction'; +import PopoverMarkAsUnreadItem from '@pages/inbox/report/ContextMenu/actions/MarkAsUnreadAction/PopoverMarkAsUnreadItem'; +import {PopoverPinItem, shouldShowPinAction} from '@pages/inbox/report/ContextMenu/actions/PinAction'; +import {PopoverUnpinItem, shouldShowUnpinAction} from '@pages/inbox/report/ContextMenu/actions/UnpinAction'; import ONYXKEYS from '@src/ONYXKEYS'; import type {ReportAction} from '@src/types/onyx'; diff --git a/src/pages/inbox/report/ContextMenu/actions/CopyLinkAction/MiniCopyLinkItem.tsx b/src/pages/inbox/report/ContextMenu/actions/CopyLinkAction/MiniCopyLinkItem.tsx new file mode 100644 index 000000000000..5525bae6f118 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/CopyLinkAction/MiniCopyLinkItem.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import MiniContextMenuItem from '@components/MiniContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import Clipboard from '@libs/Clipboard'; +import {getEnvironmentURL} from '@libs/Environment/Environment'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import CONST from '@src/CONST'; +import type {ReportAction} from '@src/types/onyx'; + +type MiniCopyLinkItemProps = { + reportAction: ReportAction; + originalReportID: string | undefined; +}; + +export default function MiniCopyLinkItem({reportAction, originalReportID}: MiniCopyLinkItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['LinkCopy', 'Checkmark'] as const); + + return ( + + interceptAnonymousUser(() => { + getEnvironmentURL().then((environmentURL) => { + const reportActionID = reportAction?.reportActionID; + Clipboard.setString(`${environmentURL}/r/${originalReportID}/${reportActionID}`); + }); + hideContextMenu(true, ReportActionComposeFocusManager.focus); + }, true) + } + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_LINK} + /> + ); +} diff --git a/src/pages/inbox/report/ContextMenu/actions/CopyLinkAction/PopoverCopyLinkItem.tsx b/src/pages/inbox/report/ContextMenu/actions/CopyLinkAction/PopoverCopyLinkItem.tsx new file mode 100644 index 000000000000..35908afba8cb --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/CopyLinkAction/PopoverCopyLinkItem.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import ContextMenuItem from '@components/ContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import Clipboard from '@libs/Clipboard'; +import {getEnvironmentURL} from '@libs/Environment/Environment'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import CONST from '@src/CONST'; +import type {ReportAction} from '@src/types/onyx'; + +type PopoverCopyLinkItemProps = { + reportAction: ReportAction; + originalReportID: string | undefined; + isFocused?: boolean; + onFocus?: () => void; + onBlur?: () => void; +}; + +export default function PopoverCopyLinkItem({reportAction, originalReportID, isFocused, onFocus, onBlur}: PopoverCopyLinkItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['LinkCopy', 'Checkmark'] as const); + + return ( + + interceptAnonymousUser(() => { + getEnvironmentURL().then((environmentURL) => { + const reportActionID = reportAction?.reportActionID; + Clipboard.setString(`${environmentURL}/r/${originalReportID}/${reportActionID}`); + }); + hideContextMenu(true, ReportActionComposeFocusManager.focus); + }, true) + } + isAnonymousAction + isFocused={isFocused} + onFocus={onFocus} + onBlur={onBlur} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_LINK} + /> + ); +} diff --git a/src/pages/inbox/report/ContextMenu/actions/CopyLinkAction/copyLinkAction.ts b/src/pages/inbox/report/ContextMenu/actions/CopyLinkAction/copyLinkAction.ts new file mode 100644 index 000000000000..576120fdd3f2 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/CopyLinkAction/copyLinkAction.ts @@ -0,0 +1,16 @@ +import type {RefObject} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import {isActionOfType, isMessageDeleted, isReportActionAttachment} from '@libs/ReportActionsUtils'; +import type {ContextMenuAnchor} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import CONST from '@src/CONST'; +import type {ReportAction} from '@src/types/onyx'; + +function shouldShowCopyLinkAction({reportAction, menuTarget}: {reportAction: OnyxEntry; menuTarget: RefObject | undefined}): boolean { + const isAttachment = isReportActionAttachment(reportAction); + const isAttachmentTarget = menuTarget?.current && 'tagName' in menuTarget.current && menuTarget?.current.tagName === 'IMG' && isAttachment; + const isDEWRouted = isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.DYNAMIC_EXTERNAL_WORKFLOW_ROUTED); + return !isAttachmentTarget && !isMessageDeleted(reportAction) && !isDEWRouted; +} + +// eslint-disable-next-line import/prefer-default-export -- named utility export per module convention +export {shouldShowCopyLinkAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/CopyMessageAction/MiniCopyMessageItem.tsx b/src/pages/inbox/report/ContextMenu/actions/CopyMessageAction/MiniCopyMessageItem.tsx new file mode 100644 index 000000000000..6d13ecbe63e4 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/CopyMessageAction/MiniCopyMessageItem.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import MiniContextMenuItem from '@components/MiniContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import CONST from '@src/CONST'; +import type {CopyMessageClipboardParams} from './copyMessageAction'; +import {copyMessageToClipboard} from './copyMessageAction'; + +type MiniCopyMessageItemProps = Omit; + +export default function MiniCopyMessageItem({ + reportAction, + transaction, + selection, + report, + conciergeReportID, + bankAccountList, + card, + originalReport, + isHarvestReport, + isTryNewDotNVPDismissed, + movedFromReport, + movedToReport, + childReport, + policy, + getLocalDateFromDatetime, + policyTags, + harvestReport, + currentUserPersonalDetails, +}: MiniCopyMessageItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Copy', 'Checkmark'] as const); + + return ( + + interceptAnonymousUser(() => { + copyMessageToClipboard({ + reportAction, + transaction, + selection, + report, + conciergeReportID, + bankAccountList, + card, + originalReport, + isHarvestReport, + isTryNewDotNVPDismissed, + movedFromReport, + movedToReport, + childReport, + policy, + getLocalDateFromDatetime, + policyTags, + translate, + harvestReport, + currentUserPersonalDetails, + }); + hideContextMenu(true, ReportActionComposeFocusManager.focus); + }, true) + } + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_MESSAGE} + /> + ); +} diff --git a/src/pages/inbox/report/ContextMenu/actions/CopyMessageAction/PopoverCopyMessageItem.tsx b/src/pages/inbox/report/ContextMenu/actions/CopyMessageAction/PopoverCopyMessageItem.tsx new file mode 100644 index 000000000000..3dc53d5cbe37 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/CopyMessageAction/PopoverCopyMessageItem.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import ContextMenuItem from '@components/ContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import CONST from '@src/CONST'; +import type {CopyMessageClipboardParams} from './copyMessageAction'; +import {copyMessageToClipboard} from './copyMessageAction'; + +type PopoverCopyMessageItemProps = Omit & { + isFocused?: boolean; + onFocus?: () => void; + onBlur?: () => void; +}; + +export default function PopoverCopyMessageItem({ + reportAction, + transaction, + selection, + report, + conciergeReportID, + bankAccountList, + card, + originalReport, + isHarvestReport, + isTryNewDotNVPDismissed, + movedFromReport, + movedToReport, + childReport, + policy, + getLocalDateFromDatetime, + policyTags, + harvestReport, + currentUserPersonalDetails, + isFocused, + onFocus, + onBlur, +}: PopoverCopyMessageItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Copy', 'Checkmark'] as const); + + return ( + + interceptAnonymousUser(() => { + copyMessageToClipboard({ + reportAction, + transaction, + selection, + report, + conciergeReportID, + bankAccountList, + card, + originalReport, + isHarvestReport, + isTryNewDotNVPDismissed, + movedFromReport, + movedToReport, + childReport, + policy, + getLocalDateFromDatetime, + policyTags, + translate, + harvestReport, + currentUserPersonalDetails, + }); + hideContextMenu(true, ReportActionComposeFocusManager.focus); + }, true) + } + isAnonymousAction + isFocused={isFocused} + onFocus={onFocus} + onBlur={onBlur} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_MESSAGE} + /> + ); +} diff --git a/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.tsx b/src/pages/inbox/report/ContextMenu/actions/CopyMessageAction/copyMessageAction.ts similarity index 88% rename from src/pages/inbox/report/ContextMenu/actions/copyMessageAction.tsx rename to src/pages/inbox/report/ContextMenu/actions/CopyMessageAction/copyMessageAction.ts index 36914d4f2fe1..da7ab9c89618 100644 --- a/src/pages/inbox/report/ContextMenu/actions/copyMessageAction.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/CopyMessageAction/copyMessageAction.ts @@ -1,20 +1,13 @@ import {Str} from 'expensify-common'; -import React from 'react'; import type {OnyxEntry} from 'react-native-onyx'; -import ContextMenuItem from '@components/ContextMenuItem'; import type {LocaleContextProps, LocalizedTranslate} from '@components/LocaleContextProvider'; -import MiniContextMenuItem from '@components/MiniContextMenuItem'; import type useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; import Clipboard from '@libs/Clipboard'; import getClipboardText from '@libs/Clipboard/getClipboardText'; -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {formatPhoneNumber as formatPhoneNumberPhoneUtils} from '@libs/LocalePhoneNumber'; import {getForReportAction} from '@libs/ModifiedExpenseMessage'; import Parser from '@libs/Parser'; import {getCleanedTagName, isPolicyAdmin} from '@libs/PolicyUtils'; -import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import stripFollowupListFromHtml from '@libs/ReportActionFollowupUtils/stripFollowupListFromHtml'; import { getActionableCard3DSTransactionApprovalMessage, @@ -161,10 +154,10 @@ import { isExpenseReport, } from '@libs/ReportUtils'; import {getTaskCreatedMessage, getTaskReportActionMessage} from '@libs/TaskUtils'; -import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; import type {BankAccountList, Card, Policy, PolicyTagLists, ReportAction, Report as ReportType, Transaction} from '@src/types/onyx'; -import {getActionHtml} from './actionConfig'; +// eslint-disable-next-line @dword-design/import-alias/prefer-alias -- sibling of actions/ from CopyMessageAction subfolder +import {getActionHtml} from '../actionConfig'; type CopyMessageClipboardParams = { reportAction: ReportAction; @@ -576,140 +569,5 @@ function copyMessageToClipboard(params: CopyMessageClipboardParams) { } } -type PopoverCopyMessageItemProps = Omit & { - isFocused?: boolean; - onFocus?: () => void; - onBlur?: () => void; -}; - -function PopoverCopyMessageItem({ - reportAction, - transaction, - selection, - report, - conciergeReportID, - bankAccountList, - card, - originalReport, - isHarvestReport, - isTryNewDotNVPDismissed, - movedFromReport, - movedToReport, - childReport, - policy, - getLocalDateFromDatetime, - policyTags, - harvestReport, - currentUserPersonalDetails, - isFocused, - onFocus, - onBlur, -}: PopoverCopyMessageItemProps) { - const {translate} = useLocalize(); - const icons = useMemoizedLazyExpensifyIcons(['Copy', 'Checkmark'] as const); - - return ( - - interceptAnonymousUser(() => { - copyMessageToClipboard({ - reportAction, - transaction, - selection, - report, - conciergeReportID, - bankAccountList, - card, - originalReport, - isHarvestReport, - isTryNewDotNVPDismissed, - movedFromReport, - movedToReport, - childReport, - policy, - getLocalDateFromDatetime, - policyTags, - translate, - harvestReport, - currentUserPersonalDetails, - }); - hideContextMenu(true, ReportActionComposeFocusManager.focus); - }, true) - } - isAnonymousAction - isFocused={isFocused} - onFocus={onFocus} - onBlur={onBlur} - sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_MESSAGE} - /> - ); -} - -type MiniCopyMessageItemProps = Omit; - -function MiniCopyMessageItem({ - reportAction, - transaction, - selection, - report, - conciergeReportID, - bankAccountList, - card, - originalReport, - isHarvestReport, - isTryNewDotNVPDismissed, - movedFromReport, - movedToReport, - childReport, - policy, - getLocalDateFromDatetime, - policyTags, - harvestReport, - currentUserPersonalDetails, -}: MiniCopyMessageItemProps) { - const {translate} = useLocalize(); - const icons = useMemoizedLazyExpensifyIcons(['Copy', 'Checkmark'] as const); - - return ( - - interceptAnonymousUser(() => { - copyMessageToClipboard({ - reportAction, - transaction, - selection, - report, - conciergeReportID, - bankAccountList, - card, - originalReport, - isHarvestReport, - isTryNewDotNVPDismissed, - movedFromReport, - movedToReport, - childReport, - policy, - getLocalDateFromDatetime, - policyTags, - translate, - harvestReport, - currentUserPersonalDetails, - }); - hideContextMenu(true, ReportActionComposeFocusManager.focus); - }, true) - } - sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_MESSAGE} - /> - ); -} - -export {shouldShowCopyMessageAction, copyMessageToClipboard, PopoverCopyMessageItem, MiniCopyMessageItem}; export type {CopyMessageClipboardParams}; +export {shouldShowCopyMessageAction, copyMessageToClipboard}; diff --git a/src/pages/inbox/report/ContextMenu/actions/copyOnyxDataAction.tsx b/src/pages/inbox/report/ContextMenu/actions/CopyOnyxDataAction.tsx similarity index 100% rename from src/pages/inbox/report/ContextMenu/actions/copyOnyxDataAction.tsx rename to src/pages/inbox/report/ContextMenu/actions/CopyOnyxDataAction.tsx diff --git a/src/pages/inbox/report/ContextMenu/actions/debugAction.tsx b/src/pages/inbox/report/ContextMenu/actions/DebugAction.tsx similarity index 100% rename from src/pages/inbox/report/ContextMenu/actions/debugAction.tsx rename to src/pages/inbox/report/ContextMenu/actions/DebugAction.tsx diff --git a/src/pages/inbox/report/ContextMenu/actions/DeleteAction/MiniDeleteItem.tsx b/src/pages/inbox/report/ContextMenu/actions/DeleteAction/MiniDeleteItem.tsx new file mode 100644 index 000000000000..435e9b4c5f68 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/DeleteAction/MiniDeleteItem.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import MiniContextMenuItem from '@components/MiniContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils'; +import {showDeleteModal} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import CONST from '@src/CONST'; +import type {ReportAction} from '@src/types/onyx'; + +type MiniDeleteItemProps = { + reportID: string | undefined; + reportAction: ReportAction; + moneyRequestAction: ReportAction | undefined; + hideAndRun: (callback?: () => void) => void; +}; + +export default function MiniDeleteItem({reportID, reportAction, moneyRequestAction, hideAndRun}: MiniDeleteItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Trashcan'] as const); + + return ( + { + const iouReportID = isMoneyRequestAction(moneyRequestAction) ? getOriginalMessage(moneyRequestAction)?.IOUReportID : undefined; + const effectiveReportID = iouReportID && Number(iouReportID) !== 0 ? iouReportID : reportID; + const actionSourceID = effectiveReportID !== reportID ? reportID : undefined; + hideAndRun(() => showDeleteModal(effectiveReportID, moneyRequestAction ?? reportAction, undefined, undefined, undefined, actionSourceID)); + }} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.DELETE} + /> + ); +} diff --git a/src/pages/inbox/report/ContextMenu/actions/DeleteAction/PopoverDeleteItem.tsx b/src/pages/inbox/report/ContextMenu/actions/DeleteAction/PopoverDeleteItem.tsx new file mode 100644 index 000000000000..74dd2e164301 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/DeleteAction/PopoverDeleteItem.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import FocusableMenuItem from '@components/FocusableMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils'; +import {showDeleteModal} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import CONST from '@src/CONST'; +import type {ReportAction} from '@src/types/onyx'; + +type PopoverDeleteItemProps = { + reportID: string | undefined; + reportAction: ReportAction; + moneyRequestAction: ReportAction | undefined; + hideAndRun: (callback?: () => void) => void; + isFocused?: boolean; + onFocus?: () => void; + onBlur?: () => void; +}; + +export default function PopoverDeleteItem({reportID, reportAction, moneyRequestAction, hideAndRun, isFocused, onFocus, onBlur}: PopoverDeleteItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Trashcan'] as const); + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const {windowWidth} = useWindowDimensions(); + + return ( + { + const iouReportID = isMoneyRequestAction(moneyRequestAction) ? getOriginalMessage(moneyRequestAction)?.IOUReportID : undefined; + const effectiveReportID = iouReportID && Number(iouReportID) !== 0 ? iouReportID : reportID; + const actionSourceID = effectiveReportID !== reportID ? reportID : undefined; + hideAndRun(() => showDeleteModal(effectiveReportID, moneyRequestAction ?? reportAction, undefined, undefined, undefined, actionSourceID)); + }} + wrapperStyle={[styles.pr8]} + style={StyleUtils.getContextMenuItemStyles(windowWidth)} + focused={isFocused} + interactive + onFocus={onFocus} + onBlur={onBlur} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.DELETE} + /> + ); +} diff --git a/src/pages/inbox/report/ContextMenu/actions/DeleteAction/deleteAction.ts b/src/pages/inbox/report/ContextMenu/actions/DeleteAction/deleteAction.ts new file mode 100644 index 000000000000..f7dc179da2c0 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/DeleteAction/deleteAction.ts @@ -0,0 +1,41 @@ +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import {getOriginalMessage, isMessageDeleted, isMoneyRequestAction, isReportPreviewAction} from '@libs/ReportActionsUtils'; +import {canDeleteReportAction} from '@libs/ReportUtils'; +import type {ReportAction, Transaction} from '@src/types/onyx'; + +function shouldShowDeleteAction({ + reportAction, + isArchivedRoom, + isChronosReport, + reportID, + moneyRequestAction, + iouTransaction, + transactions, + childReportActions, +}: { + reportAction: OnyxEntry; + isArchivedRoom: boolean; + isChronosReport: boolean; + reportID: string | undefined; + moneyRequestAction: ReportAction | undefined; + iouTransaction: OnyxEntry; + transactions: OnyxCollection | undefined; + childReportActions: OnyxCollection; +}): boolean { + let effectiveReportID: string | undefined = reportID; + if (isMoneyRequestAction(moneyRequestAction)) { + effectiveReportID = getOriginalMessage(moneyRequestAction)?.IOUReportID; + } else if (isReportPreviewAction(reportAction)) { + effectiveReportID = reportAction?.childReportID; + } + return ( + !!reportID && + canDeleteReportAction(moneyRequestAction ?? reportAction, effectiveReportID, iouTransaction, transactions, childReportActions) && + !isArchivedRoom && + !isChronosReport && + !isMessageDeleted(reportAction) + ); +} + +// eslint-disable-next-line import/prefer-default-export -- named utility export per module convention +export {shouldShowDeleteAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/DownloadAction/MiniDownloadItem.tsx b/src/pages/inbox/report/ContextMenu/actions/DownloadAction/MiniDownloadItem.tsx new file mode 100644 index 000000000000..cfc701bed0c6 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/DownloadAction/MiniDownloadItem.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import MiniContextMenuItem from '@components/MiniContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; +import {isMobileSafari} from '@libs/Browser'; +import fileDownload from '@libs/fileDownload'; +import getAttachmentDetails from '@libs/fileDownload/getAttachmentDetails'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import {setDownload} from '@userActions/Download'; +import CONST from '@src/CONST'; +import type {ReportAction} from '@src/types/onyx'; +// eslint-disable-next-line @dword-design/import-alias/prefer-alias -- subdirectory relative to actions/actionConfig +import {getActionHtml} from '../actionConfig'; + +type MiniDownloadItemProps = { + reportAction: ReportAction; + encryptedAuthToken: string; +}; + +export default function MiniDownloadItem({reportAction, encryptedAuthToken}: MiniDownloadItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Download'] as const); + + return ( + + interceptAnonymousUser(() => { + const html = getActionHtml(reportAction); + const {originalFileName, sourceURL} = getAttachmentDetails(html); + const sourceURLWithAuth = addEncryptedAuthTokenToURL(sourceURL ?? '', encryptedAuthToken); + const sourceID = (sourceURL?.match(CONST.REGEX.ATTACHMENT.ATTACHMENT_SOURCE_ID) ?? [])[1]; + setDownload(sourceID, true); + const anchorRegex = CONST.REGEX_LINK_IN_ANCHOR; + const isAnchorTag = anchorRegex.test(html); + fileDownload(translate, sourceURLWithAuth, originalFileName ?? '', '', isAnchorTag && isMobileSafari()).then(() => setDownload(sourceID, false)); + hideContextMenu(true, ReportActionComposeFocusManager.focus); + }, true) + } + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.DOWNLOAD} + /> + ); +} diff --git a/src/pages/inbox/report/ContextMenu/actions/downloadAction.tsx b/src/pages/inbox/report/ContextMenu/actions/DownloadAction/PopoverDownloadItem.tsx similarity index 53% rename from src/pages/inbox/report/ContextMenu/actions/downloadAction.tsx rename to src/pages/inbox/report/ContextMenu/actions/DownloadAction/PopoverDownloadItem.tsx index c363797a07b5..2e19ee65c12d 100644 --- a/src/pages/inbox/report/ContextMenu/actions/downloadAction.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/DownloadAction/PopoverDownloadItem.tsx @@ -1,7 +1,6 @@ import React from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import ContextMenuItem from '@components/ContextMenuItem'; -import MiniContextMenuItem from '@components/MiniContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; @@ -10,12 +9,12 @@ import fileDownload from '@libs/fileDownload'; import getAttachmentDetails from '@libs/fileDownload/getAttachmentDetails'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import {isMessageDeleted, isReportActionAttachment} from '@libs/ReportActionsUtils'; import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import {setDownload} from '@userActions/Download'; import CONST from '@src/CONST'; import type {Download as DownloadOnyx, ReportAction} from '@src/types/onyx'; -import {getActionHtml} from './actionConfig'; +// eslint-disable-next-line @dword-design/import-alias/prefer-alias -- subdirectory relative to actions/actionConfig +import {getActionHtml} from '../actionConfig'; type PopoverDownloadItemProps = { reportAction: ReportAction; @@ -26,7 +25,7 @@ type PopoverDownloadItemProps = { onBlur?: () => void; }; -function PopoverDownloadItem({reportAction, encryptedAuthToken, download, isFocused, onFocus, onBlur}: PopoverDownloadItemProps) { +export default function PopoverDownloadItem({reportAction, encryptedAuthToken, download, isFocused, onFocus, onBlur}: PopoverDownloadItemProps) { const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Download'] as const); const isDownloading = download?.isDownloading ?? false; @@ -60,45 +59,3 @@ function PopoverDownloadItem({reportAction, encryptedAuthToken, download, isFocu /> ); } - -type MiniDownloadItemProps = { - reportAction: ReportAction; - encryptedAuthToken: string; -}; - -function MiniDownloadItem({reportAction, encryptedAuthToken}: MiniDownloadItemProps) { - const {translate} = useLocalize(); - const icons = useMemoizedLazyExpensifyIcons(['Download'] as const); - - return ( - - interceptAnonymousUser(() => { - const html = getActionHtml(reportAction); - const {originalFileName, sourceURL} = getAttachmentDetails(html); - const sourceURLWithAuth = addEncryptedAuthTokenToURL(sourceURL ?? '', encryptedAuthToken); - const sourceID = (sourceURL?.match(CONST.REGEX.ATTACHMENT.ATTACHMENT_SOURCE_ID) ?? [])[1]; - setDownload(sourceID, true); - const anchorRegex = CONST.REGEX_LINK_IN_ANCHOR; - const isAnchorTag = anchorRegex.test(html); - fileDownload(translate, sourceURLWithAuth, originalFileName ?? '', '', isAnchorTag && isMobileSafari()).then(() => setDownload(sourceID, false)); - hideContextMenu(true, ReportActionComposeFocusManager.focus); - }, true) - } - sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.DOWNLOAD} - /> - ); -} - -function shouldShowDownloadAction({reportAction, isOffline}: {reportAction: OnyxEntry; isOffline: boolean}): boolean { - const isAttachment = isReportActionAttachment(reportAction); - const html = getActionHtml(reportAction); - const isUploading = html.includes(CONST.ATTACHMENT_OPTIMISTIC_SOURCE_ATTRIBUTE); - return isAttachment && !isUploading && !!reportAction?.reportActionID && !isMessageDeleted(reportAction) && !isOffline; -} - -export {shouldShowDownloadAction, PopoverDownloadItem, MiniDownloadItem}; diff --git a/src/pages/inbox/report/ContextMenu/actions/DownloadAction/downloadAction.ts b/src/pages/inbox/report/ContextMenu/actions/DownloadAction/downloadAction.ts new file mode 100644 index 000000000000..271df88c07c9 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/DownloadAction/downloadAction.ts @@ -0,0 +1,16 @@ +import type {OnyxEntry} from 'react-native-onyx'; +import {isMessageDeleted, isReportActionAttachment} from '@libs/ReportActionsUtils'; +import CONST from '@src/CONST'; +import type {ReportAction} from '@src/types/onyx'; +// eslint-disable-next-line @dword-design/import-alias/prefer-alias -- subdirectory relative to actions/actionConfig +import {getActionHtml} from '../actionConfig'; + +function shouldShowDownloadAction({reportAction, isOffline}: {reportAction: OnyxEntry; isOffline: boolean}): boolean { + const isAttachment = isReportActionAttachment(reportAction); + const html = getActionHtml(reportAction); + const isUploading = html.includes(CONST.ATTACHMENT_OPTIMISTIC_SOURCE_ATTRIBUTE); + return isAttachment && !isUploading && !!reportAction?.reportActionID && !isMessageDeleted(reportAction) && !isOffline; +} + +// eslint-disable-next-line import/prefer-default-export -- named utility export per module convention +export {shouldShowDownloadAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/EditAction/MiniEditItem.tsx b/src/pages/inbox/report/ContextMenu/actions/EditAction/MiniEditItem.tsx new file mode 100644 index 000000000000..556c9485f3ce --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/EditAction/MiniEditItem.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import MiniContextMenuItem from '@components/MiniContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import Navigation from '@libs/Navigation/Navigation'; +import Parser from '@libs/Parser'; +import {isMoneyRequestAction} from '@libs/ReportActionsUtils'; +import {getActionHtml} from '@pages/inbox/report/ContextMenu/actions/actionConfig'; +import {deleteReportActionDraft, openReport, saveReportActionDraft} from '@userActions/Report'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; +import type {Beta, IntroSelected, ReportAction} from '@src/types/onyx'; + +type MiniEditItemProps = { + reportID: string | undefined; + reportAction: ReportAction; + moneyRequestAction: ReportAction | undefined; + draftMessage: string; + introSelected: OnyxEntry; + betas: OnyxEntry; + hideAndRun: (callback?: () => void) => void; +}; + +export default function MiniEditItem({reportID, reportAction, moneyRequestAction, draftMessage, introSelected, betas, hideAndRun}: MiniEditItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Pencil'] as const); + + return ( + + interceptAnonymousUser(() => { + if (isMoneyRequestAction(reportAction) || isMoneyRequestAction(moneyRequestAction)) { + hideAndRun(() => { + const childReportID = reportAction?.childReportID; + openReport({reportID: childReportID, introSelected, betas}); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID)); + }); + return; + } + hideAndRun(() => { + if (!draftMessage) { + saveReportActionDraft(reportID, reportAction, Parser.htmlToMarkdown(getActionHtml(reportAction))); + } else { + deleteReportActionDraft(reportID, reportAction); + } + }); + }) + } + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.EDIT_COMMENT} + /> + ); +} diff --git a/src/pages/inbox/report/ContextMenu/actions/editAction.tsx b/src/pages/inbox/report/ContextMenu/actions/EditAction/PopoverEditItem.tsx similarity index 50% rename from src/pages/inbox/report/ContextMenu/actions/editAction.tsx rename to src/pages/inbox/report/ContextMenu/actions/EditAction/PopoverEditItem.tsx index 7a5de09004a4..a08c27f9a11a 100644 --- a/src/pages/inbox/report/ContextMenu/actions/editAction.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/EditAction/PopoverEditItem.tsx @@ -1,7 +1,6 @@ import React from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import FocusableMenuItem from '@components/FocusableMenuItem'; -import MiniContextMenuItem from '@components/MiniContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -11,12 +10,11 @@ import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import Navigation from '@libs/Navigation/Navigation'; import Parser from '@libs/Parser'; import {isMoneyRequestAction} from '@libs/ReportActionsUtils'; -import {canEditReportAction} from '@libs/ReportUtils'; +import {getActionHtml} from '@pages/inbox/report/ContextMenu/actions/actionConfig'; import {deleteReportActionDraft, openReport, saveReportActionDraft} from '@userActions/Report'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; -import type {Beta, IntroSelected, ReportAction, Transaction} from '@src/types/onyx'; -import {getActionHtml} from './actionConfig'; +import type {Beta, IntroSelected, ReportAction} from '@src/types/onyx'; type PopoverEditItemProps = { reportID: string | undefined; @@ -31,7 +29,7 @@ type PopoverEditItemProps = { onBlur?: () => void; }; -function PopoverEditItem({reportID, reportAction, moneyRequestAction, draftMessage, introSelected, betas, hideAndRun, isFocused, onFocus, onBlur}: PopoverEditItemProps) { +export default function PopoverEditItem({reportID, reportAction, moneyRequestAction, draftMessage, introSelected, betas, hideAndRun, isFocused, onFocus, onBlur}: PopoverEditItemProps) { const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Pencil'] as const); const styles = useThemeStyles(); @@ -71,63 +69,3 @@ function PopoverEditItem({reportID, reportAction, moneyRequestAction, draftMessa /> ); } - -type MiniEditItemProps = { - reportID: string | undefined; - reportAction: ReportAction; - moneyRequestAction: ReportAction | undefined; - draftMessage: string; - introSelected: OnyxEntry; - betas: OnyxEntry; - hideAndRun: (callback?: () => void) => void; -}; - -function MiniEditItem({reportID, reportAction, moneyRequestAction, draftMessage, introSelected, betas, hideAndRun}: MiniEditItemProps) { - const {translate} = useLocalize(); - const icons = useMemoizedLazyExpensifyIcons(['Pencil'] as const); - - return ( - - interceptAnonymousUser(() => { - if (isMoneyRequestAction(reportAction) || isMoneyRequestAction(moneyRequestAction)) { - hideAndRun(() => { - const childReportID = reportAction?.childReportID; - openReport({reportID: childReportID, introSelected, betas}); - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID)); - }); - return; - } - hideAndRun(() => { - if (!draftMessage) { - saveReportActionDraft(reportID, reportAction, Parser.htmlToMarkdown(getActionHtml(reportAction))); - } else { - deleteReportActionDraft(reportID, reportAction); - } - }); - }) - } - sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.EDIT_COMMENT} - /> - ); -} - -function shouldShowEditAction({ - reportAction, - isArchivedRoom, - isChronosReport, - moneyRequestAction, - iouTransaction, -}: { - reportAction: OnyxEntry; - isArchivedRoom: boolean; - isChronosReport: boolean; - moneyRequestAction: ReportAction | undefined; - iouTransaction: OnyxEntry; -}): boolean { - return (canEditReportAction(reportAction, iouTransaction) || canEditReportAction(moneyRequestAction, iouTransaction)) && !isArchivedRoom && !isChronosReport; -} - -export {shouldShowEditAction, PopoverEditItem, MiniEditItem}; diff --git a/src/pages/inbox/report/ContextMenu/actions/EditAction/editAction.ts b/src/pages/inbox/report/ContextMenu/actions/EditAction/editAction.ts new file mode 100644 index 000000000000..993d84fcb98e --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/EditAction/editAction.ts @@ -0,0 +1,22 @@ +import type {OnyxEntry} from 'react-native-onyx'; +import {canEditReportAction} from '@libs/ReportUtils'; +import type {ReportAction, Transaction} from '@src/types/onyx'; + +function shouldShowEditAction({ + reportAction, + isArchivedRoom, + isChronosReport, + moneyRequestAction, + iouTransaction, +}: { + reportAction: OnyxEntry; + isArchivedRoom: boolean; + isChronosReport: boolean; + moneyRequestAction: ReportAction | undefined; + iouTransaction: OnyxEntry; +}): boolean { + return (canEditReportAction(reportAction, iouTransaction) || canEditReportAction(moneyRequestAction, iouTransaction)) && !isArchivedRoom && !isChronosReport; +} + +// eslint-disable-next-line import/prefer-default-export -- named utility export per module convention +export {shouldShowEditAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/ExplainAction/MiniExplainItem.tsx b/src/pages/inbox/report/ContextMenu/actions/ExplainAction/MiniExplainItem.tsx new file mode 100644 index 000000000000..20d9560d6798 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/ExplainAction/MiniExplainItem.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import MiniContextMenuItem from '@components/MiniContextMenuItem'; +import type useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import {explain} from '@userActions/Report'; +import CONST from '@src/CONST'; +import type {Beta, IntroSelected, ReportAction, Report as ReportType} from '@src/types/onyx'; +import KeyboardUtils from '@src/utils/keyboard'; + +type MiniExplainItemProps = { + childReport: OnyxEntry; + originalReport: OnyxEntry; + reportAction: ReportAction; + currentUserPersonalDetails: ReturnType; + introSelected: OnyxEntry; + betas: OnyxEntry; + hideAndRun: (callback?: () => void) => void; +}; + +export default function MiniExplainItem({childReport, originalReport, reportAction, currentUserPersonalDetails, introSelected, betas, hideAndRun}: MiniExplainItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Concierge'] as const); + + return ( + + interceptAnonymousUser(() => { + if (!originalReport?.reportID) { + return; + } + hideAndRun(() => { + KeyboardUtils.dismiss().then(() => + explain( + childReport, + originalReport, + reportAction, + translate, + currentUserPersonalDetails?.accountID ?? CONST.DEFAULT_NUMBER_ID, + introSelected, + betas, + currentUserPersonalDetails?.timezone, + ), + ); + }); + }) + } + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.EXPLAIN} + /> + ); +} diff --git a/src/pages/inbox/report/ContextMenu/actions/explainAction.tsx b/src/pages/inbox/report/ContextMenu/actions/ExplainAction/PopoverExplainItem.tsx similarity index 54% rename from src/pages/inbox/report/ContextMenu/actions/explainAction.tsx rename to src/pages/inbox/report/ContextMenu/actions/ExplainAction/PopoverExplainItem.tsx index 17a8c6cdd88e..270318b4f88d 100644 --- a/src/pages/inbox/report/ContextMenu/actions/explainAction.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/ExplainAction/PopoverExplainItem.tsx @@ -1,7 +1,6 @@ import React from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import FocusableMenuItem from '@components/FocusableMenuItem'; -import MiniContextMenuItem from '@components/MiniContextMenuItem'; import type useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; @@ -9,7 +8,6 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import {hasReasoning} from '@libs/ReportActionsUtils'; import {explain} from '@userActions/Report'; import CONST from '@src/CONST'; import type {Beta, IntroSelected, ReportAction, Report as ReportType} from '@src/types/onyx'; @@ -28,7 +26,18 @@ type PopoverExplainItemProps = { onBlur?: () => void; }; -function PopoverExplainItem({childReport, originalReport, reportAction, currentUserPersonalDetails, introSelected, betas, hideAndRun, isFocused, onFocus, onBlur}: PopoverExplainItemProps) { +export default function PopoverExplainItem({ + childReport, + originalReport, + reportAction, + currentUserPersonalDetails, + introSelected, + betas, + hideAndRun, + isFocused, + onFocus, + onBlur, +}: PopoverExplainItemProps) { const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Concierge'] as const); const styles = useThemeStyles(); @@ -70,56 +79,3 @@ function PopoverExplainItem({childReport, originalReport, reportAction, currentU /> ); } - -type MiniExplainItemProps = { - childReport: OnyxEntry; - originalReport: OnyxEntry; - reportAction: ReportAction; - currentUserPersonalDetails: ReturnType; - introSelected: OnyxEntry; - betas: OnyxEntry; - hideAndRun: (callback?: () => void) => void; -}; - -function MiniExplainItem({childReport, originalReport, reportAction, currentUserPersonalDetails, introSelected, betas, hideAndRun}: MiniExplainItemProps) { - const {translate} = useLocalize(); - const icons = useMemoizedLazyExpensifyIcons(['Concierge'] as const); - - return ( - - interceptAnonymousUser(() => { - if (!originalReport?.reportID) { - return; - } - hideAndRun(() => { - KeyboardUtils.dismiss().then(() => - explain( - childReport, - originalReport, - reportAction, - translate, - currentUserPersonalDetails?.accountID ?? CONST.DEFAULT_NUMBER_ID, - introSelected, - betas, - currentUserPersonalDetails?.timezone, - ), - ); - }); - }) - } - sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.EXPLAIN} - /> - ); -} - -function shouldShowExplainAction({reportAction, isArchivedRoom}: {reportAction: OnyxEntry; isArchivedRoom: boolean}): boolean { - if (isArchivedRoom || !reportAction) { - return false; - } - return hasReasoning(reportAction); -} - -export {shouldShowExplainAction, PopoverExplainItem, MiniExplainItem}; diff --git a/src/pages/inbox/report/ContextMenu/actions/ExplainAction/explainAction.ts b/src/pages/inbox/report/ContextMenu/actions/ExplainAction/explainAction.ts new file mode 100644 index 000000000000..1aaaf1ffa53e --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/ExplainAction/explainAction.ts @@ -0,0 +1,13 @@ +import type {OnyxEntry} from 'react-native-onyx'; +import {hasReasoning} from '@libs/ReportActionsUtils'; +import type {ReportAction} from '@src/types/onyx'; + +function shouldShowExplainAction({reportAction, isArchivedRoom}: {reportAction: OnyxEntry; isArchivedRoom: boolean}): boolean { + if (isArchivedRoom || !reportAction) { + return false; + } + return hasReasoning(reportAction); +} + +// eslint-disable-next-line import/prefer-default-export -- named utility export per module convention +export {shouldShowExplainAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/FlagAsOffensiveAction/MiniFlagAsOffensiveItem.tsx b/src/pages/inbox/report/ContextMenu/actions/FlagAsOffensiveAction/MiniFlagAsOffensiveItem.tsx new file mode 100644 index 000000000000..a126b455950e --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/FlagAsOffensiveAction/MiniFlagAsOffensiveItem.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import MiniContextMenuItem from '@components/MiniContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import Navigation from '@libs/Navigation/Navigation'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; +import type {ReportAction} from '@src/types/onyx'; +import KeyboardUtils from '@src/utils/keyboard'; + +type MiniFlagAsOffensiveItemProps = { + reportID: string | undefined; + reportAction: ReportAction; + hideAndRun: (callback?: () => void) => void; +}; + +export default function MiniFlagAsOffensiveItem({reportID, reportAction, hideAndRun}: MiniFlagAsOffensiveItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Flag'] as const); + + return ( + { + if (!reportID) { + return; + } + const activeRoute = Navigation.getActiveRoute(); + hideAndRun(() => { + KeyboardUtils.dismiss().then(() => { + Navigation.navigate(ROUTES.FLAG_COMMENT.getRoute(reportID, reportAction?.reportActionID, activeRoute)); + }); + }); + }} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.FLAG_AS_OFFENSIVE} + /> + ); +} diff --git a/src/pages/inbox/report/ContextMenu/actions/flagAsOffensiveAction.tsx b/src/pages/inbox/report/ContextMenu/actions/FlagAsOffensiveAction/PopoverFlagAsOffensiveItem.tsx similarity index 50% rename from src/pages/inbox/report/ContextMenu/actions/flagAsOffensiveAction.tsx rename to src/pages/inbox/report/ContextMenu/actions/FlagAsOffensiveAction/PopoverFlagAsOffensiveItem.tsx index 4119be262327..345457a388f7 100644 --- a/src/pages/inbox/report/ContextMenu/actions/flagAsOffensiveAction.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/FlagAsOffensiveAction/PopoverFlagAsOffensiveItem.tsx @@ -1,14 +1,11 @@ import React from 'react'; -import type {OnyxEntry} from 'react-native-onyx'; import FocusableMenuItem from '@components/FocusableMenuItem'; -import MiniContextMenuItem from '@components/MiniContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import Navigation from '@libs/Navigation/Navigation'; -import {canFlagReportAction} from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {ReportAction} from '@src/types/onyx'; @@ -23,7 +20,7 @@ type PopoverFlagAsOffensiveItemProps = { onBlur?: () => void; }; -function PopoverFlagAsOffensiveItem({reportID, reportAction, hideAndRun, isFocused, onFocus, onBlur}: PopoverFlagAsOffensiveItemProps) { +export default function PopoverFlagAsOffensiveItem({reportID, reportAction, hideAndRun, isFocused, onFocus, onBlur}: PopoverFlagAsOffensiveItemProps) { const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Flag'] as const); const styles = useThemeStyles(); @@ -55,49 +52,3 @@ function PopoverFlagAsOffensiveItem({reportID, reportAction, hideAndRun, isFocus /> ); } - -type MiniFlagAsOffensiveItemProps = { - reportID: string | undefined; - reportAction: ReportAction; - hideAndRun: (callback?: () => void) => void; -}; - -function MiniFlagAsOffensiveItem({reportID, reportAction, hideAndRun}: MiniFlagAsOffensiveItemProps) { - const {translate} = useLocalize(); - const icons = useMemoizedLazyExpensifyIcons(['Flag'] as const); - - return ( - { - if (!reportID) { - return; - } - const activeRoute = Navigation.getActiveRoute(); - hideAndRun(() => { - KeyboardUtils.dismiss().then(() => { - Navigation.navigate(ROUTES.FLAG_COMMENT.getRoute(reportID, reportAction?.reportActionID, activeRoute)); - }); - }); - }} - sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.FLAG_AS_OFFENSIVE} - /> - ); -} - -function shouldShowFlagAsOffensiveAction({ - reportAction, - isArchivedRoom, - isChronosReport, - reportID, -}: { - reportAction: OnyxEntry; - isArchivedRoom: boolean; - isChronosReport: boolean; - reportID: string | undefined; -}): boolean { - return canFlagReportAction(reportAction, reportID) && !isArchivedRoom && !isChronosReport && reportAction?.actorAccountID !== CONST.ACCOUNT_ID.CONCIERGE; -} - -export {shouldShowFlagAsOffensiveAction, PopoverFlagAsOffensiveItem, MiniFlagAsOffensiveItem}; diff --git a/src/pages/inbox/report/ContextMenu/actions/FlagAsOffensiveAction/flagAsOffensiveAction.ts b/src/pages/inbox/report/ContextMenu/actions/FlagAsOffensiveAction/flagAsOffensiveAction.ts new file mode 100644 index 000000000000..d778ea078d99 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/FlagAsOffensiveAction/flagAsOffensiveAction.ts @@ -0,0 +1,21 @@ +import type {OnyxEntry} from 'react-native-onyx'; +import {canFlagReportAction} from '@libs/ReportUtils'; +import CONST from '@src/CONST'; +import type {ReportAction} from '@src/types/onyx'; + +function shouldShowFlagAsOffensiveAction({ + reportAction, + isArchivedRoom, + isChronosReport, + reportID, +}: { + reportAction: OnyxEntry; + isArchivedRoom: boolean; + isChronosReport: boolean; + reportID: string | undefined; +}): boolean { + return canFlagReportAction(reportAction, reportID) && !isArchivedRoom && !isChronosReport && reportAction?.actorAccountID !== CONST.ACCOUNT_ID.CONCIERGE; +} + +// eslint-disable-next-line import/prefer-default-export -- named utility export per module convention +export {shouldShowFlagAsOffensiveAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/HoldAction/MiniHoldItem.tsx b/src/pages/inbox/report/ContextMenu/actions/HoldAction/MiniHoldItem.tsx new file mode 100644 index 000000000000..281cf2932668 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/HoldAction/MiniHoldItem.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import MiniContextMenuItem from '@components/MiniContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import {changeMoneyRequestHoldStatus} from '@libs/ReportUtils'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import CONST from '@src/CONST'; +import type {ReportAction, Transaction} from '@src/types/onyx'; + +type MiniHoldItemProps = { + moneyRequestAction: ReportAction | undefined; + iouTransaction: OnyxEntry; + isOffline: boolean; + isDelegateAccessRestricted: boolean; + showDelegateNoAccessModal: (() => void) | undefined; + hideAndRun: (callback?: () => void) => void; +}; + +export default function MiniHoldItem({moneyRequestAction, iouTransaction, isOffline, isDelegateAccessRestricted, showDelegateNoAccessModal, hideAndRun}: MiniHoldItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Stopwatch'] as const); + + return ( + + interceptAnonymousUser(() => { + if (isDelegateAccessRestricted) { + hideContextMenu(false, showDelegateNoAccessModal); + return; + } + hideAndRun(() => changeMoneyRequestHoldStatus(moneyRequestAction, iouTransaction, isOffline)); + }, false) + } + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.HOLD} + /> + ); +} diff --git a/src/pages/inbox/report/ContextMenu/actions/HoldAction/PopoverHoldItem.tsx b/src/pages/inbox/report/ContextMenu/actions/HoldAction/PopoverHoldItem.tsx new file mode 100644 index 000000000000..e51f4ae18f85 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/HoldAction/PopoverHoldItem.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import FocusableMenuItem from '@components/FocusableMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import {changeMoneyRequestHoldStatus} from '@libs/ReportUtils'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import CONST from '@src/CONST'; +import type {ReportAction, Transaction} from '@src/types/onyx'; + +type PopoverHoldItemProps = { + moneyRequestAction: ReportAction | undefined; + iouTransaction: OnyxEntry; + isOffline: boolean; + isDelegateAccessRestricted: boolean; + showDelegateNoAccessModal: (() => void) | undefined; + hideAndRun: (callback?: () => void) => void; + isFocused?: boolean; + onFocus?: () => void; + onBlur?: () => void; +}; + +export default function PopoverHoldItem({ + moneyRequestAction, + iouTransaction, + isOffline, + isDelegateAccessRestricted, + showDelegateNoAccessModal, + hideAndRun, + isFocused, + onFocus, + onBlur, +}: PopoverHoldItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Stopwatch'] as const); + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const {windowWidth} = useWindowDimensions(); + + return ( + + interceptAnonymousUser(() => { + if (isDelegateAccessRestricted) { + hideContextMenu(false, showDelegateNoAccessModal); + return; + } + hideAndRun(() => changeMoneyRequestHoldStatus(moneyRequestAction, iouTransaction, isOffline)); + }, false) + } + wrapperStyle={[styles.pr8]} + style={StyleUtils.getContextMenuItemStyles(windowWidth)} + focused={isFocused} + interactive + onFocus={onFocus} + onBlur={onBlur} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.HOLD} + /> + ); +} diff --git a/src/pages/inbox/report/ContextMenu/actions/HoldAction/holdAction.ts b/src/pages/inbox/report/ContextMenu/actions/HoldAction/holdAction.ts new file mode 100644 index 000000000000..4450ee9b4758 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/HoldAction/holdAction.ts @@ -0,0 +1,27 @@ +import type {OnyxEntry} from 'react-native-onyx'; +import {getReportAction} from '@libs/ReportActionsUtils'; +import {canHoldUnholdReportAction} from '@libs/ReportUtils'; +import type {Policy, ReportAction, Report as ReportType, Transaction} from '@src/types/onyx'; + +function shouldShowHoldAction({ + moneyRequestReport, + moneyRequestAction, + moneyRequestPolicy, + areHoldRequirementsMet, + iouTransaction, +}: { + moneyRequestReport: OnyxEntry; + moneyRequestAction: ReportAction | undefined; + moneyRequestPolicy: OnyxEntry; + areHoldRequirementsMet: boolean; + iouTransaction: OnyxEntry; +}): boolean { + if (!areHoldRequirementsMet) { + return false; + } + const holdReportAction = getReportAction(moneyRequestAction?.childReportID, `${iouTransaction?.comment?.hold ?? ''}`); + return canHoldUnholdReportAction(moneyRequestReport, moneyRequestAction, holdReportAction, iouTransaction, moneyRequestPolicy).canHoldRequest; +} + +// eslint-disable-next-line import/prefer-default-export -- named utility export per module convention +export {shouldShowHoldAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/JoinThreadAction/MiniJoinThreadItem.tsx b/src/pages/inbox/report/ContextMenu/actions/JoinThreadAction/MiniJoinThreadItem.tsx new file mode 100644 index 000000000000..3158499ccad0 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/JoinThreadAction/MiniJoinThreadItem.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import MiniContextMenuItem from '@components/MiniContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {getChildReportNotificationPreference} from '@libs/ReportUtils'; +import {toggleSubscribeToChildReport} from '@userActions/Report'; +import CONST from '@src/CONST'; +import type {Beta, IntroSelected, ReportAction, Report as ReportType} from '@src/types/onyx'; + +type MiniJoinThreadItemProps = { + reportAction: ReportAction; + originalReport: OnyxEntry; + currentUserAccountID: number; + introSelected: OnyxEntry; + isSelfTourViewed: boolean | undefined; + betas: OnyxEntry; + hideAndRun: (callback?: () => void) => void; +}; + +export default function MiniJoinThreadItem({reportAction, originalReport, currentUserAccountID, introSelected, isSelfTourViewed, betas, hideAndRun}: MiniJoinThreadItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Bell'] as const); + + return ( + + interceptAnonymousUser(() => { + const childReportNotificationPreference = getChildReportNotificationPreference(reportAction); + hideAndRun(() => { + ReportActionComposeFocusManager.focus(); + toggleSubscribeToChildReport( + reportAction?.childReportID, + currentUserAccountID, + reportAction, + originalReport, + introSelected, + isSelfTourViewed, + betas, + childReportNotificationPreference, + ); + }); + }, false) + } + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.JOIN_THREAD} + /> + ); +} diff --git a/src/pages/inbox/report/ContextMenu/actions/JoinThreadAction/PopoverJoinThreadItem.tsx b/src/pages/inbox/report/ContextMenu/actions/JoinThreadAction/PopoverJoinThreadItem.tsx new file mode 100644 index 000000000000..9b478aa533f6 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/JoinThreadAction/PopoverJoinThreadItem.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import FocusableMenuItem from '@components/FocusableMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {getChildReportNotificationPreference} from '@libs/ReportUtils'; +import {toggleSubscribeToChildReport} from '@userActions/Report'; +import CONST from '@src/CONST'; +import type {Beta, IntroSelected, ReportAction, Report as ReportType} from '@src/types/onyx'; + +type PopoverJoinThreadItemProps = { + reportAction: ReportAction; + originalReport: OnyxEntry; + currentUserAccountID: number; + introSelected: OnyxEntry; + isSelfTourViewed: boolean | undefined; + betas: OnyxEntry; + hideAndRun: (callback?: () => void) => void; + isFocused?: boolean; + onFocus?: () => void; + onBlur?: () => void; +}; + +export default function PopoverJoinThreadItem({ + reportAction, + originalReport, + currentUserAccountID, + introSelected, + isSelfTourViewed, + betas, + hideAndRun, + isFocused, + onFocus, + onBlur, +}: PopoverJoinThreadItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Bell'] as const); + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const {windowWidth} = useWindowDimensions(); + + return ( + + interceptAnonymousUser(() => { + const childReportNotificationPreference = getChildReportNotificationPreference(reportAction); + hideAndRun(() => { + ReportActionComposeFocusManager.focus(); + toggleSubscribeToChildReport( + reportAction?.childReportID, + currentUserAccountID, + reportAction, + originalReport, + introSelected, + isSelfTourViewed, + betas, + childReportNotificationPreference, + ); + }); + }, false) + } + wrapperStyle={[styles.pr8]} + style={StyleUtils.getContextMenuItemStyles(windowWidth)} + focused={isFocused} + interactive + onFocus={onFocus} + onBlur={onBlur} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.JOIN_THREAD} + /> + ); +} diff --git a/src/pages/inbox/report/ContextMenu/actions/JoinThreadAction/joinThreadAction.ts b/src/pages/inbox/report/ContextMenu/actions/JoinThreadAction/joinThreadAction.ts new file mode 100644 index 000000000000..3dead385cf32 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/JoinThreadAction/joinThreadAction.ts @@ -0,0 +1,39 @@ +import type {OnyxEntry} from 'react-native-onyx'; +import {isActionableTrackExpense, isCreatedAction, isCreatedTaskReportAction, isDeletedAction, isMoneyRequestAction, isReportPreviewAction, isWhisperAction} from '@libs/ReportActionsUtils'; +import {getChildReportNotificationPreference, shouldDisableThread, shouldDisplayThreadReplies} from '@libs/ReportUtils'; +import type {ReportAction} from '@src/types/onyx'; + +function shouldShowJoinThreadAction({ + reportAction, + isArchivedRoom, + isThreadReportParentAction, + isHarvestReport, +}: { + reportAction: OnyxEntry; + isArchivedRoom: boolean; + isThreadReportParentAction: boolean; + isHarvestReport: boolean; +}): boolean { + const childReportNotificationPreference = getChildReportNotificationPreference(reportAction); + const isDeletedActionResult = isDeletedAction(reportAction); + const shouldDisplayReplies = shouldDisplayThreadReplies(reportAction, isThreadReportParentAction); + const subscribed = childReportNotificationPreference !== 'hidden'; + const isWhisper = isWhisperAction(reportAction) || isActionableTrackExpense(reportAction); + const isExpenseReportAction = isMoneyRequestAction(reportAction) || isReportPreviewAction(reportAction); + const isTaskAction = isCreatedTaskReportAction(reportAction); + const isHarvestCreatedExpenseReportAction = !!isHarvestReport && isCreatedAction(reportAction); + const shouldDisableJoin = shouldDisableThread(reportAction, isThreadReportParentAction, isArchivedRoom); + return ( + !subscribed && + !isWhisper && + !isTaskAction && + !isExpenseReportAction && + !isThreadReportParentAction && + !isHarvestCreatedExpenseReportAction && + !shouldDisableJoin && + (shouldDisplayReplies || (!isDeletedActionResult && !isArchivedRoom)) + ); +} + +// eslint-disable-next-line import/prefer-default-export -- named utility export per module convention +export {shouldShowJoinThreadAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/LeaveThreadAction/MiniLeaveThreadItem.tsx b/src/pages/inbox/report/ContextMenu/actions/LeaveThreadAction/MiniLeaveThreadItem.tsx new file mode 100644 index 000000000000..5738dcc37b39 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/LeaveThreadAction/MiniLeaveThreadItem.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import MiniContextMenuItem from '@components/MiniContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {getChildReportNotificationPreference} from '@libs/ReportUtils'; +import {toggleSubscribeToChildReport} from '@userActions/Report'; +import CONST from '@src/CONST'; +import type {Beta, IntroSelected, ReportAction, Report as ReportType} from '@src/types/onyx'; + +type MiniLeaveThreadItemProps = { + reportAction: ReportAction; + originalReport: OnyxEntry; + currentUserAccountID: number; + introSelected: OnyxEntry; + isSelfTourViewed: boolean | undefined; + betas: OnyxEntry; + hideAndRun: (callback?: () => void) => void; +}; + +export default function MiniLeaveThreadItem({reportAction, originalReport, currentUserAccountID, introSelected, isSelfTourViewed, betas, hideAndRun}: MiniLeaveThreadItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Exit'] as const); + + return ( + + interceptAnonymousUser(() => { + const childReportNotificationPreference = getChildReportNotificationPreference(reportAction); + hideAndRun(() => { + ReportActionComposeFocusManager.focus(); + toggleSubscribeToChildReport( + reportAction?.childReportID, + currentUserAccountID, + reportAction, + originalReport, + introSelected, + isSelfTourViewed, + betas, + childReportNotificationPreference, + ); + }); + }, false) + } + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.LEAVE_THREAD} + /> + ); +} diff --git a/src/pages/inbox/report/ContextMenu/actions/LeaveThreadAction/PopoverLeaveThreadItem.tsx b/src/pages/inbox/report/ContextMenu/actions/LeaveThreadAction/PopoverLeaveThreadItem.tsx new file mode 100644 index 000000000000..1ba097b63484 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/LeaveThreadAction/PopoverLeaveThreadItem.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import FocusableMenuItem from '@components/FocusableMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {getChildReportNotificationPreference} from '@libs/ReportUtils'; +import {toggleSubscribeToChildReport} from '@userActions/Report'; +import CONST from '@src/CONST'; +import type {Beta, IntroSelected, ReportAction, Report as ReportType} from '@src/types/onyx'; + +type PopoverLeaveThreadItemProps = { + reportAction: ReportAction; + originalReport: OnyxEntry; + currentUserAccountID: number; + introSelected: OnyxEntry; + isSelfTourViewed: boolean | undefined; + betas: OnyxEntry; + hideAndRun: (callback?: () => void) => void; + isFocused?: boolean; + onFocus?: () => void; + onBlur?: () => void; +}; + +export default function PopoverLeaveThreadItem({ + reportAction, + originalReport, + currentUserAccountID, + introSelected, + isSelfTourViewed, + betas, + hideAndRun, + isFocused, + onFocus, + onBlur, +}: PopoverLeaveThreadItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Exit'] as const); + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const {windowWidth} = useWindowDimensions(); + + return ( + + interceptAnonymousUser(() => { + const childReportNotificationPreference = getChildReportNotificationPreference(reportAction); + hideAndRun(() => { + ReportActionComposeFocusManager.focus(); + toggleSubscribeToChildReport( + reportAction?.childReportID, + currentUserAccountID, + reportAction, + originalReport, + introSelected, + isSelfTourViewed, + betas, + childReportNotificationPreference, + ); + }); + }, false) + } + wrapperStyle={[styles.pr8]} + style={StyleUtils.getContextMenuItemStyles(windowWidth)} + focused={isFocused} + interactive + onFocus={onFocus} + onBlur={onBlur} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.LEAVE_THREAD} + /> + ); +} diff --git a/src/pages/inbox/report/ContextMenu/actions/LeaveThreadAction/leaveThreadAction.ts b/src/pages/inbox/report/ContextMenu/actions/LeaveThreadAction/leaveThreadAction.ts new file mode 100644 index 000000000000..eb313ccb7a07 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/LeaveThreadAction/leaveThreadAction.ts @@ -0,0 +1,37 @@ +import type {OnyxEntry} from 'react-native-onyx'; +import {isActionableTrackExpense, isCreatedAction, isCreatedTaskReportAction, isDeletedAction, isMoneyRequestAction, isReportPreviewAction, isWhisperAction} from '@libs/ReportActionsUtils'; +import {getChildReportNotificationPreference, shouldDisplayThreadReplies} from '@libs/ReportUtils'; +import type {ReportAction} from '@src/types/onyx'; + +function shouldShowLeaveThreadAction({ + reportAction, + isArchivedRoom, + isThreadReportParentAction, + isHarvestReport, +}: { + reportAction: OnyxEntry; + isArchivedRoom: boolean; + isThreadReportParentAction: boolean; + isHarvestReport: boolean; +}): boolean { + const childReportNotificationPreference = getChildReportNotificationPreference(reportAction); + const isDeletedActionResult = isDeletedAction(reportAction); + const shouldDisplayReplies = shouldDisplayThreadReplies(reportAction, isThreadReportParentAction); + const subscribed = childReportNotificationPreference !== 'hidden'; + const isWhisper = isWhisperAction(reportAction) || isActionableTrackExpense(reportAction); + const isExpenseReportAction = isMoneyRequestAction(reportAction) || isReportPreviewAction(reportAction); + const isTaskAction = isCreatedTaskReportAction(reportAction); + const isHarvestCreatedExpenseReportAction = !!isHarvestReport && isCreatedAction(reportAction); + return ( + subscribed && + !isWhisper && + !isTaskAction && + !isExpenseReportAction && + !isThreadReportParentAction && + !isHarvestCreatedExpenseReportAction && + (shouldDisplayReplies || (!isDeletedActionResult && !isArchivedRoom)) + ); +} + +// eslint-disable-next-line import/prefer-default-export -- named utility export per module convention +export {shouldShowLeaveThreadAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/markAsReadAction.tsx b/src/pages/inbox/report/ContextMenu/actions/MarkAsReadAction.tsx similarity index 100% rename from src/pages/inbox/report/ContextMenu/actions/markAsReadAction.tsx rename to src/pages/inbox/report/ContextMenu/actions/MarkAsReadAction.tsx diff --git a/src/pages/inbox/report/ContextMenu/actions/MarkAsUnreadAction/MiniMarkAsUnreadItem.tsx b/src/pages/inbox/report/ContextMenu/actions/MarkAsUnreadAction/MiniMarkAsUnreadItem.tsx new file mode 100644 index 000000000000..bfae9bd282c6 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/MarkAsUnreadAction/MiniMarkAsUnreadItem.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import MiniContextMenuItem from '@components/MiniContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {markCommentAsUnread} from '@userActions/Report'; +import CONST from '@src/CONST'; +import type {ReportAction, ReportActions} from '@src/types/onyx'; + +type MiniMarkAsUnreadItemProps = { + reportID: string | undefined; + reportActions: OnyxEntry; + reportAction: ReportAction; + currentUserAccountID: number; + hideAndRun: (callback?: () => void) => void; +}; + +export default function MiniMarkAsUnreadItem({reportID, reportActions, reportAction, currentUserAccountID, hideAndRun}: MiniMarkAsUnreadItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['ChatBubbleUnread', 'Checkmark'] as const); + + return ( + + interceptAnonymousUser(() => { + markCommentAsUnread(reportID, reportActions, reportAction, currentUserAccountID); + hideAndRun(ReportActionComposeFocusManager.focus); + }) + } + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.MARK_AS_UNREAD} + /> + ); +} diff --git a/src/pages/inbox/report/ContextMenu/actions/MarkAsUnreadAction/PopoverMarkAsUnreadItem.tsx b/src/pages/inbox/report/ContextMenu/actions/MarkAsUnreadAction/PopoverMarkAsUnreadItem.tsx new file mode 100644 index 000000000000..097286c2a001 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/MarkAsUnreadAction/PopoverMarkAsUnreadItem.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import ContextMenuItem from '@components/ContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import {markCommentAsUnread} from '@userActions/Report'; +import CONST from '@src/CONST'; +import type {ReportAction, ReportActions} from '@src/types/onyx'; + +type PopoverMarkAsUnreadItemProps = { + reportID: string | undefined; + reportActions: OnyxEntry; + reportAction: ReportAction; + currentUserAccountID: number; + hideAndRun: (callback?: () => void) => void; + isFocused?: boolean; + onFocus?: () => void; + onBlur?: () => void; +}; + +export default function PopoverMarkAsUnreadItem({reportID, reportActions, reportAction, currentUserAccountID, hideAndRun, isFocused, onFocus, onBlur}: PopoverMarkAsUnreadItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['ChatBubbleUnread', 'Checkmark'] as const); + + return ( + + interceptAnonymousUser(() => { + markCommentAsUnread(reportID, reportActions, reportAction, currentUserAccountID); + hideAndRun(ReportActionComposeFocusManager.focus); + }) + } + isFocused={isFocused} + onFocus={onFocus} + onBlur={onBlur} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.MARK_AS_UNREAD} + /> + ); +} diff --git a/src/pages/inbox/report/ContextMenu/actions/MarkAsUnreadAction/markAsUnreadAction.ts b/src/pages/inbox/report/ContextMenu/actions/MarkAsUnreadAction/markAsUnreadAction.ts new file mode 100644 index 000000000000..b83f7ea53b12 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/MarkAsUnreadAction/markAsUnreadAction.ts @@ -0,0 +1,14 @@ +import type {OnyxEntry} from 'react-native-onyx'; +import {isActionOfType} from '@libs/ReportActionsUtils'; +import CONST from '@src/CONST'; +import type {ReportAction} from '@src/types/onyx'; + +function shouldShowMarkAsUnreadForReportAction({reportAction}: {reportAction: OnyxEntry}): boolean { + return !isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.DYNAMIC_EXTERNAL_WORKFLOW_ROUTED); +} + +function shouldShowMarkAsUnreadForReport({isUnreadChat}: {isUnreadChat: boolean}): boolean { + return !isUnreadChat; +} + +export {shouldShowMarkAsUnreadForReportAction, shouldShowMarkAsUnreadForReport}; diff --git a/src/pages/inbox/report/ContextMenu/actions/pinAction.tsx b/src/pages/inbox/report/ContextMenu/actions/PinAction.tsx similarity index 100% rename from src/pages/inbox/report/ContextMenu/actions/pinAction.tsx rename to src/pages/inbox/report/ContextMenu/actions/PinAction.tsx diff --git a/src/pages/inbox/report/ContextMenu/actions/ReplyInThreadAction/MiniReplyInThreadItem.tsx b/src/pages/inbox/report/ContextMenu/actions/ReplyInThreadAction/MiniReplyInThreadItem.tsx new file mode 100644 index 000000000000..a9e8b4b20738 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/ReplyInThreadAction/MiniReplyInThreadItem.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import MiniContextMenuItem from '@components/MiniContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import {navigateToAndOpenChildReport} from '@userActions/Report'; +import CONST from '@src/CONST'; +import type {Beta, IntroSelected, ReportAction, Report as ReportType} from '@src/types/onyx'; +import KeyboardUtils from '@src/utils/keyboard'; + +type MiniReplyInThreadItemProps = { + childReport: OnyxEntry; + reportAction: ReportAction; + originalReport: OnyxEntry; + currentUserAccountID: number; + introSelected: OnyxEntry; + betas: OnyxEntry; + hideAndRun: (callback?: () => void) => void; +}; + +export default function MiniReplyInThreadItem({childReport, reportAction, originalReport, currentUserAccountID, introSelected, betas, hideAndRun}: MiniReplyInThreadItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['ChatBubbleReply'] as const); + + return ( + + interceptAnonymousUser(() => { + hideAndRun(() => { + KeyboardUtils.dismiss().then(() => navigateToAndOpenChildReport(childReport, reportAction, originalReport, currentUserAccountID, introSelected, betas)); + }); + }, false) + } + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.REPLY_IN_THREAD} + /> + ); +} diff --git a/src/pages/inbox/report/ContextMenu/actions/replyInThreadAction.tsx b/src/pages/inbox/report/ContextMenu/actions/ReplyInThreadAction/PopoverReplyInThreadItem.tsx similarity index 55% rename from src/pages/inbox/report/ContextMenu/actions/replyInThreadAction.tsx rename to src/pages/inbox/report/ContextMenu/actions/ReplyInThreadAction/PopoverReplyInThreadItem.tsx index 8c3fbb64d63a..817373b6736f 100644 --- a/src/pages/inbox/report/ContextMenu/actions/replyInThreadAction.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/ReplyInThreadAction/PopoverReplyInThreadItem.tsx @@ -1,14 +1,12 @@ import React from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import FocusableMenuItem from '@components/FocusableMenuItem'; -import MiniContextMenuItem from '@components/MiniContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import {shouldDisableThread} from '@libs/ReportUtils'; import {navigateToAndOpenChildReport} from '@userActions/Report'; import CONST from '@src/CONST'; import type {Beta, IntroSelected, ReportAction, Report as ReportType} from '@src/types/onyx'; @@ -27,7 +25,7 @@ type PopoverReplyInThreadItemProps = { onBlur?: () => void; }; -function PopoverReplyInThreadItem({ +export default function PopoverReplyInThreadItem({ childReport, reportAction, originalReport, @@ -66,52 +64,3 @@ function PopoverReplyInThreadItem({ /> ); } - -type MiniReplyInThreadItemProps = { - childReport: OnyxEntry; - reportAction: ReportAction; - originalReport: OnyxEntry; - currentUserAccountID: number; - introSelected: OnyxEntry; - betas: OnyxEntry; - hideAndRun: (callback?: () => void) => void; -}; - -function MiniReplyInThreadItem({childReport, reportAction, originalReport, currentUserAccountID, introSelected, betas, hideAndRun}: MiniReplyInThreadItemProps) { - const {translate} = useLocalize(); - const icons = useMemoizedLazyExpensifyIcons(['ChatBubbleReply'] as const); - - return ( - - interceptAnonymousUser(() => { - hideAndRun(() => { - KeyboardUtils.dismiss().then(() => navigateToAndOpenChildReport(childReport, reportAction, originalReport, currentUserAccountID, introSelected, betas)); - }); - }, false) - } - sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.REPLY_IN_THREAD} - /> - ); -} - -function shouldShowReplyInThreadAction({ - reportAction, - reportID, - isThreadReportParentAction, - isArchivedRoom, -}: { - reportAction: OnyxEntry; - reportID: string | undefined; - isThreadReportParentAction: boolean; - isArchivedRoom: boolean; -}): boolean { - if (!reportID) { - return false; - } - return !shouldDisableThread(reportAction, isThreadReportParentAction, isArchivedRoom); -} - -export {shouldShowReplyInThreadAction, PopoverReplyInThreadItem, MiniReplyInThreadItem}; diff --git a/src/pages/inbox/report/ContextMenu/actions/ReplyInThreadAction/replyInThreadAction.ts b/src/pages/inbox/report/ContextMenu/actions/ReplyInThreadAction/replyInThreadAction.ts new file mode 100644 index 000000000000..0e0558de7912 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/ReplyInThreadAction/replyInThreadAction.ts @@ -0,0 +1,23 @@ +import type {OnyxEntry} from 'react-native-onyx'; +import {shouldDisableThread} from '@libs/ReportUtils'; +import type {ReportAction} from '@src/types/onyx'; + +function shouldShowReplyInThreadAction({ + reportAction, + reportID, + isThreadReportParentAction, + isArchivedRoom, +}: { + reportAction: OnyxEntry; + reportID: string | undefined; + isThreadReportParentAction: boolean; + isArchivedRoom: boolean; +}): boolean { + if (!reportID) { + return false; + } + return !shouldDisableThread(reportAction, isThreadReportParentAction, isArchivedRoom); +} + +// eslint-disable-next-line import/prefer-default-export -- named utility export per module convention +export {shouldShowReplyInThreadAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/UnholdAction/MiniUnholdItem.tsx b/src/pages/inbox/report/ContextMenu/actions/UnholdAction/MiniUnholdItem.tsx new file mode 100644 index 000000000000..ee5cc3f55770 --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/UnholdAction/MiniUnholdItem.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import MiniContextMenuItem from '@components/MiniContextMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import {changeMoneyRequestHoldStatus} from '@libs/ReportUtils'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import CONST from '@src/CONST'; +import type {ReportAction, Transaction} from '@src/types/onyx'; + +type MiniUnholdItemProps = { + moneyRequestAction: ReportAction | undefined; + iouTransaction: OnyxEntry; + isOffline: boolean; + isDelegateAccessRestricted: boolean; + showDelegateNoAccessModal: (() => void) | undefined; + hideAndRun: (callback?: () => void) => void; +}; + +export default function MiniUnholdItem({moneyRequestAction, iouTransaction, isOffline, isDelegateAccessRestricted, showDelegateNoAccessModal, hideAndRun}: MiniUnholdItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Stopwatch'] as const); + + return ( + + interceptAnonymousUser(() => { + if (isDelegateAccessRestricted) { + hideContextMenu(false, showDelegateNoAccessModal); + return; + } + hideAndRun(() => changeMoneyRequestHoldStatus(moneyRequestAction, iouTransaction, isOffline)); + }, false) + } + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.UNHOLD} + /> + ); +} diff --git a/src/pages/inbox/report/ContextMenu/actions/UnholdAction/PopoverUnholdItem.tsx b/src/pages/inbox/report/ContextMenu/actions/UnholdAction/PopoverUnholdItem.tsx new file mode 100644 index 000000000000..7b44caf112bc --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/UnholdAction/PopoverUnholdItem.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import FocusableMenuItem from '@components/FocusableMenuItem'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import {changeMoneyRequestHoldStatus} from '@libs/ReportUtils'; +import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; +import CONST from '@src/CONST'; +import type {ReportAction, Transaction} from '@src/types/onyx'; + +type PopoverUnholdItemProps = { + moneyRequestAction: ReportAction | undefined; + iouTransaction: OnyxEntry; + isOffline: boolean; + isDelegateAccessRestricted: boolean; + showDelegateNoAccessModal: (() => void) | undefined; + hideAndRun: (callback?: () => void) => void; + isFocused?: boolean; + onFocus?: () => void; + onBlur?: () => void; +}; + +export default function PopoverUnholdItem({ + moneyRequestAction, + iouTransaction, + isOffline, + isDelegateAccessRestricted, + showDelegateNoAccessModal, + hideAndRun, + isFocused, + onFocus, + onBlur, +}: PopoverUnholdItemProps) { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Stopwatch'] as const); + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const {windowWidth} = useWindowDimensions(); + + return ( + + interceptAnonymousUser(() => { + if (isDelegateAccessRestricted) { + hideContextMenu(false, showDelegateNoAccessModal); + return; + } + hideAndRun(() => changeMoneyRequestHoldStatus(moneyRequestAction, iouTransaction, isOffline)); + }, false) + } + wrapperStyle={[styles.pr8]} + style={StyleUtils.getContextMenuItemStyles(windowWidth)} + focused={isFocused} + interactive + onFocus={onFocus} + onBlur={onBlur} + sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.UNHOLD} + /> + ); +} diff --git a/src/pages/inbox/report/ContextMenu/actions/UnholdAction/unholdAction.ts b/src/pages/inbox/report/ContextMenu/actions/UnholdAction/unholdAction.ts new file mode 100644 index 000000000000..3a4171c5e2ce --- /dev/null +++ b/src/pages/inbox/report/ContextMenu/actions/UnholdAction/unholdAction.ts @@ -0,0 +1,27 @@ +import type {OnyxEntry} from 'react-native-onyx'; +import {getReportAction} from '@libs/ReportActionsUtils'; +import {canHoldUnholdReportAction} from '@libs/ReportUtils'; +import type {Policy, ReportAction, Report as ReportType, Transaction} from '@src/types/onyx'; + +function shouldShowUnholdAction({ + moneyRequestReport, + moneyRequestAction, + moneyRequestPolicy, + areHoldRequirementsMet, + iouTransaction, +}: { + moneyRequestReport: OnyxEntry; + moneyRequestAction: ReportAction | undefined; + moneyRequestPolicy: OnyxEntry; + areHoldRequirementsMet: boolean; + iouTransaction: OnyxEntry; +}): boolean { + if (!areHoldRequirementsMet) { + return false; + } + const holdReportAction = getReportAction(moneyRequestAction?.childReportID, `${iouTransaction?.comment?.hold ?? ''}`); + return canHoldUnholdReportAction(moneyRequestReport, moneyRequestAction, holdReportAction, iouTransaction, moneyRequestPolicy).canUnholdRequest; +} + +// eslint-disable-next-line import/prefer-default-export -- named utility export per module convention +export {shouldShowUnholdAction}; diff --git a/src/pages/inbox/report/ContextMenu/actions/unpinAction.tsx b/src/pages/inbox/report/ContextMenu/actions/UnpinAction.tsx similarity index 100% rename from src/pages/inbox/report/ContextMenu/actions/unpinAction.tsx rename to src/pages/inbox/report/ContextMenu/actions/UnpinAction.tsx diff --git a/src/pages/inbox/report/ContextMenu/actions/copyLinkAction.tsx b/src/pages/inbox/report/ContextMenu/actions/copyLinkAction.tsx deleted file mode 100644 index 1c986c46070b..000000000000 --- a/src/pages/inbox/report/ContextMenu/actions/copyLinkAction.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import type {RefObject} from 'react'; -import React from 'react'; -import type {OnyxEntry} from 'react-native-onyx'; -import ContextMenuItem from '@components/ContextMenuItem'; -import MiniContextMenuItem from '@components/MiniContextMenuItem'; -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; -import Clipboard from '@libs/Clipboard'; -import {getEnvironmentURL} from '@libs/Environment/Environment'; -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import {isActionOfType, isMessageDeleted, isReportActionAttachment} from '@libs/ReportActionsUtils'; -import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; -import type {ContextMenuAnchor} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; -import CONST from '@src/CONST'; -import type {ReportAction} from '@src/types/onyx'; - -type PopoverCopyLinkItemProps = { - reportAction: ReportAction; - originalReportID: string | undefined; - isFocused?: boolean; - onFocus?: () => void; - onBlur?: () => void; -}; - -function PopoverCopyLinkItem({reportAction, originalReportID, isFocused, onFocus, onBlur}: PopoverCopyLinkItemProps) { - const {translate} = useLocalize(); - const icons = useMemoizedLazyExpensifyIcons(['LinkCopy', 'Checkmark'] as const); - - return ( - - interceptAnonymousUser(() => { - getEnvironmentURL().then((environmentURL) => { - const reportActionID = reportAction?.reportActionID; - Clipboard.setString(`${environmentURL}/r/${originalReportID}/${reportActionID}`); - }); - hideContextMenu(true, ReportActionComposeFocusManager.focus); - }, true) - } - isAnonymousAction - isFocused={isFocused} - onFocus={onFocus} - onBlur={onBlur} - sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_LINK} - /> - ); -} - -type MiniCopyLinkItemProps = { - reportAction: ReportAction; - originalReportID: string | undefined; -}; - -function MiniCopyLinkItem({reportAction, originalReportID}: MiniCopyLinkItemProps) { - const {translate} = useLocalize(); - const icons = useMemoizedLazyExpensifyIcons(['LinkCopy', 'Checkmark'] as const); - - return ( - - interceptAnonymousUser(() => { - getEnvironmentURL().then((environmentURL) => { - const reportActionID = reportAction?.reportActionID; - Clipboard.setString(`${environmentURL}/r/${originalReportID}/${reportActionID}`); - }); - hideContextMenu(true, ReportActionComposeFocusManager.focus); - }, true) - } - sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_LINK} - /> - ); -} - -function shouldShowCopyLinkAction({reportAction, menuTarget}: {reportAction: OnyxEntry; menuTarget: RefObject | undefined}): boolean { - const isAttachment = isReportActionAttachment(reportAction); - const isAttachmentTarget = menuTarget?.current && 'tagName' in menuTarget.current && menuTarget?.current.tagName === 'IMG' && isAttachment; - const isDEWRouted = isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.DYNAMIC_EXTERNAL_WORKFLOW_ROUTED); - return !isAttachmentTarget && !isMessageDeleted(reportAction) && !isDEWRouted; -} - -export {shouldShowCopyLinkAction, PopoverCopyLinkItem, MiniCopyLinkItem}; diff --git a/src/pages/inbox/report/ContextMenu/actions/deleteAction.tsx b/src/pages/inbox/report/ContextMenu/actions/deleteAction.tsx deleted file mode 100644 index e1c2f42cdcdf..000000000000 --- a/src/pages/inbox/report/ContextMenu/actions/deleteAction.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import React from 'react'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import FocusableMenuItem from '@components/FocusableMenuItem'; -import MiniContextMenuItem from '@components/MiniContextMenuItem'; -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; -import {getOriginalMessage, isMessageDeleted, isMoneyRequestAction, isReportPreviewAction} from '@libs/ReportActionsUtils'; -import {canDeleteReportAction} from '@libs/ReportUtils'; -import {showDeleteModal} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; -import CONST from '@src/CONST'; -import type {ReportAction, Transaction} from '@src/types/onyx'; - -type PopoverDeleteItemProps = { - reportID: string | undefined; - reportAction: ReportAction; - moneyRequestAction: ReportAction | undefined; - hideAndRun: (callback?: () => void) => void; - isFocused?: boolean; - onFocus?: () => void; - onBlur?: () => void; -}; - -function PopoverDeleteItem({reportID, reportAction, moneyRequestAction, hideAndRun, isFocused, onFocus, onBlur}: PopoverDeleteItemProps) { - const {translate} = useLocalize(); - const icons = useMemoizedLazyExpensifyIcons(['Trashcan'] as const); - const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); - const {windowWidth} = useWindowDimensions(); - - return ( - { - const iouReportID = isMoneyRequestAction(moneyRequestAction) ? getOriginalMessage(moneyRequestAction)?.IOUReportID : undefined; - const effectiveReportID = iouReportID && Number(iouReportID) !== 0 ? iouReportID : reportID; - const actionSourceID = effectiveReportID !== reportID ? reportID : undefined; - hideAndRun(() => showDeleteModal(effectiveReportID, moneyRequestAction ?? reportAction, undefined, undefined, undefined, actionSourceID)); - }} - wrapperStyle={[styles.pr8]} - style={StyleUtils.getContextMenuItemStyles(windowWidth)} - focused={isFocused} - interactive - onFocus={onFocus} - onBlur={onBlur} - sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.DELETE} - /> - ); -} - -type MiniDeleteItemProps = { - reportID: string | undefined; - reportAction: ReportAction; - moneyRequestAction: ReportAction | undefined; - hideAndRun: (callback?: () => void) => void; -}; - -function MiniDeleteItem({reportID, reportAction, moneyRequestAction, hideAndRun}: MiniDeleteItemProps) { - const {translate} = useLocalize(); - const icons = useMemoizedLazyExpensifyIcons(['Trashcan'] as const); - - return ( - { - const iouReportID = isMoneyRequestAction(moneyRequestAction) ? getOriginalMessage(moneyRequestAction)?.IOUReportID : undefined; - const effectiveReportID = iouReportID && Number(iouReportID) !== 0 ? iouReportID : reportID; - const actionSourceID = effectiveReportID !== reportID ? reportID : undefined; - hideAndRun(() => showDeleteModal(effectiveReportID, moneyRequestAction ?? reportAction, undefined, undefined, undefined, actionSourceID)); - }} - sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.DELETE} - /> - ); -} - -function shouldShowDeleteAction({ - reportAction, - isArchivedRoom, - isChronosReport, - reportID, - moneyRequestAction, - iouTransaction, - transactions, - childReportActions, -}: { - reportAction: OnyxEntry; - isArchivedRoom: boolean; - isChronosReport: boolean; - reportID: string | undefined; - moneyRequestAction: ReportAction | undefined; - iouTransaction: OnyxEntry; - transactions: OnyxCollection | undefined; - childReportActions: OnyxCollection; -}): boolean { - let effectiveReportID: string | undefined = reportID; - if (isMoneyRequestAction(moneyRequestAction)) { - effectiveReportID = getOriginalMessage(moneyRequestAction)?.IOUReportID; - } else if (isReportPreviewAction(reportAction)) { - effectiveReportID = reportAction?.childReportID; - } - return ( - !!reportID && - canDeleteReportAction(moneyRequestAction ?? reportAction, effectiveReportID, iouTransaction, transactions, childReportActions) && - !isArchivedRoom && - !isChronosReport && - !isMessageDeleted(reportAction) - ); -} - -export {shouldShowDeleteAction, PopoverDeleteItem, MiniDeleteItem}; diff --git a/src/pages/inbox/report/ContextMenu/actions/holdAction.tsx b/src/pages/inbox/report/ContextMenu/actions/holdAction.tsx deleted file mode 100644 index e1fb17697ed9..000000000000 --- a/src/pages/inbox/report/ContextMenu/actions/holdAction.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import React from 'react'; -import type {OnyxEntry} from 'react-native-onyx'; -import FocusableMenuItem from '@components/FocusableMenuItem'; -import MiniContextMenuItem from '@components/MiniContextMenuItem'; -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import {getReportAction} from '@libs/ReportActionsUtils'; -import {canHoldUnholdReportAction, changeMoneyRequestHoldStatus} from '@libs/ReportUtils'; -import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; -import CONST from '@src/CONST'; -import type {Policy, ReportAction, Report as ReportType, Transaction} from '@src/types/onyx'; - -type PopoverHoldItemProps = { - moneyRequestAction: ReportAction | undefined; - iouTransaction: OnyxEntry; - isOffline: boolean; - isDelegateAccessRestricted: boolean; - showDelegateNoAccessModal: (() => void) | undefined; - hideAndRun: (callback?: () => void) => void; - isFocused?: boolean; - onFocus?: () => void; - onBlur?: () => void; -}; - -function PopoverHoldItem({ - moneyRequestAction, - iouTransaction, - isOffline, - isDelegateAccessRestricted, - showDelegateNoAccessModal, - hideAndRun, - isFocused, - onFocus, - onBlur, -}: PopoverHoldItemProps) { - const {translate} = useLocalize(); - const icons = useMemoizedLazyExpensifyIcons(['Stopwatch'] as const); - const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); - const {windowWidth} = useWindowDimensions(); - - return ( - - interceptAnonymousUser(() => { - if (isDelegateAccessRestricted) { - hideContextMenu(false, showDelegateNoAccessModal); - return; - } - hideAndRun(() => changeMoneyRequestHoldStatus(moneyRequestAction, iouTransaction, isOffline)); - }, false) - } - wrapperStyle={[styles.pr8]} - style={StyleUtils.getContextMenuItemStyles(windowWidth)} - focused={isFocused} - interactive - onFocus={onFocus} - onBlur={onBlur} - sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.HOLD} - /> - ); -} - -type MiniHoldItemProps = { - moneyRequestAction: ReportAction | undefined; - iouTransaction: OnyxEntry; - isOffline: boolean; - isDelegateAccessRestricted: boolean; - showDelegateNoAccessModal: (() => void) | undefined; - hideAndRun: (callback?: () => void) => void; -}; - -function MiniHoldItem({moneyRequestAction, iouTransaction, isOffline, isDelegateAccessRestricted, showDelegateNoAccessModal, hideAndRun}: MiniHoldItemProps) { - const {translate} = useLocalize(); - const icons = useMemoizedLazyExpensifyIcons(['Stopwatch'] as const); - - return ( - - interceptAnonymousUser(() => { - if (isDelegateAccessRestricted) { - hideContextMenu(false, showDelegateNoAccessModal); - return; - } - hideAndRun(() => changeMoneyRequestHoldStatus(moneyRequestAction, iouTransaction, isOffline)); - }, false) - } - sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.HOLD} - /> - ); -} - -function shouldShowHoldAction({ - moneyRequestReport, - moneyRequestAction, - moneyRequestPolicy, - areHoldRequirementsMet, - iouTransaction, -}: { - moneyRequestReport: OnyxEntry; - moneyRequestAction: ReportAction | undefined; - moneyRequestPolicy: OnyxEntry; - areHoldRequirementsMet: boolean; - iouTransaction: OnyxEntry; -}): boolean { - if (!areHoldRequirementsMet) { - return false; - } - const holdReportAction = getReportAction(moneyRequestAction?.childReportID, `${iouTransaction?.comment?.hold ?? ''}`); - return canHoldUnholdReportAction(moneyRequestReport, moneyRequestAction, holdReportAction, iouTransaction, moneyRequestPolicy).canHoldRequest; -} - -export {shouldShowHoldAction, PopoverHoldItem, MiniHoldItem}; diff --git a/src/pages/inbox/report/ContextMenu/actions/joinThreadAction.tsx b/src/pages/inbox/report/ContextMenu/actions/joinThreadAction.tsx deleted file mode 100644 index 4589766d2cea..000000000000 --- a/src/pages/inbox/report/ContextMenu/actions/joinThreadAction.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import React from 'react'; -import type {OnyxEntry} from 'react-native-onyx'; -import FocusableMenuItem from '@components/FocusableMenuItem'; -import MiniContextMenuItem from '@components/MiniContextMenuItem'; -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import {isActionableTrackExpense, isCreatedAction, isCreatedTaskReportAction, isDeletedAction, isMoneyRequestAction, isReportPreviewAction, isWhisperAction} from '@libs/ReportActionsUtils'; -import {getChildReportNotificationPreference, shouldDisableThread, shouldDisplayThreadReplies} from '@libs/ReportUtils'; -import {toggleSubscribeToChildReport} from '@userActions/Report'; -import CONST from '@src/CONST'; -import type {Beta, IntroSelected, ReportAction, Report as ReportType} from '@src/types/onyx'; - -type PopoverJoinThreadItemProps = { - reportAction: ReportAction; - originalReport: OnyxEntry; - currentUserAccountID: number; - introSelected: OnyxEntry; - isSelfTourViewed: boolean | undefined; - betas: OnyxEntry; - hideAndRun: (callback?: () => void) => void; - isFocused?: boolean; - onFocus?: () => void; - onBlur?: () => void; -}; - -function PopoverJoinThreadItem({ - reportAction, - originalReport, - currentUserAccountID, - introSelected, - isSelfTourViewed, - betas, - hideAndRun, - isFocused, - onFocus, - onBlur, -}: PopoverJoinThreadItemProps) { - const {translate} = useLocalize(); - const icons = useMemoizedLazyExpensifyIcons(['Bell'] as const); - const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); - const {windowWidth} = useWindowDimensions(); - - return ( - - interceptAnonymousUser(() => { - const childReportNotificationPreference = getChildReportNotificationPreference(reportAction); - hideAndRun(() => { - ReportActionComposeFocusManager.focus(); - toggleSubscribeToChildReport( - reportAction?.childReportID, - currentUserAccountID, - reportAction, - originalReport, - introSelected, - isSelfTourViewed, - betas, - childReportNotificationPreference, - ); - }); - }, false) - } - wrapperStyle={[styles.pr8]} - style={StyleUtils.getContextMenuItemStyles(windowWidth)} - focused={isFocused} - interactive - onFocus={onFocus} - onBlur={onBlur} - sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.JOIN_THREAD} - /> - ); -} - -type MiniJoinThreadItemProps = { - reportAction: ReportAction; - originalReport: OnyxEntry; - currentUserAccountID: number; - introSelected: OnyxEntry; - isSelfTourViewed: boolean | undefined; - betas: OnyxEntry; - hideAndRun: (callback?: () => void) => void; -}; - -function MiniJoinThreadItem({reportAction, originalReport, currentUserAccountID, introSelected, isSelfTourViewed, betas, hideAndRun}: MiniJoinThreadItemProps) { - const {translate} = useLocalize(); - const icons = useMemoizedLazyExpensifyIcons(['Bell'] as const); - - return ( - - interceptAnonymousUser(() => { - const childReportNotificationPreference = getChildReportNotificationPreference(reportAction); - hideAndRun(() => { - ReportActionComposeFocusManager.focus(); - toggleSubscribeToChildReport( - reportAction?.childReportID, - currentUserAccountID, - reportAction, - originalReport, - introSelected, - isSelfTourViewed, - betas, - childReportNotificationPreference, - ); - }); - }, false) - } - sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.JOIN_THREAD} - /> - ); -} - -function shouldShowJoinThreadAction({ - reportAction, - isArchivedRoom, - isThreadReportParentAction, - isHarvestReport, -}: { - reportAction: OnyxEntry; - isArchivedRoom: boolean; - isThreadReportParentAction: boolean; - isHarvestReport: boolean; -}): boolean { - const childReportNotificationPreference = getChildReportNotificationPreference(reportAction); - const isDeletedActionResult = isDeletedAction(reportAction); - const shouldDisplayReplies = shouldDisplayThreadReplies(reportAction, isThreadReportParentAction); - const subscribed = childReportNotificationPreference !== 'hidden'; - const isWhisper = isWhisperAction(reportAction) || isActionableTrackExpense(reportAction); - const isExpenseReportAction = isMoneyRequestAction(reportAction) || isReportPreviewAction(reportAction); - const isTaskAction = isCreatedTaskReportAction(reportAction); - const isHarvestCreatedExpenseReportAction = !!isHarvestReport && isCreatedAction(reportAction); - const shouldDisableJoin = shouldDisableThread(reportAction, isThreadReportParentAction, isArchivedRoom); - return ( - !subscribed && - !isWhisper && - !isTaskAction && - !isExpenseReportAction && - !isThreadReportParentAction && - !isHarvestCreatedExpenseReportAction && - !shouldDisableJoin && - (shouldDisplayReplies || (!isDeletedActionResult && !isArchivedRoom)) - ); -} - -export {shouldShowJoinThreadAction, PopoverJoinThreadItem, MiniJoinThreadItem}; diff --git a/src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.tsx b/src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.tsx deleted file mode 100644 index b4f732623d56..000000000000 --- a/src/pages/inbox/report/ContextMenu/actions/leaveThreadAction.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import React from 'react'; -import type {OnyxEntry} from 'react-native-onyx'; -import FocusableMenuItem from '@components/FocusableMenuItem'; -import MiniContextMenuItem from '@components/MiniContextMenuItem'; -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import {isActionableTrackExpense, isCreatedAction, isCreatedTaskReportAction, isDeletedAction, isMoneyRequestAction, isReportPreviewAction, isWhisperAction} from '@libs/ReportActionsUtils'; -import {getChildReportNotificationPreference, shouldDisplayThreadReplies} from '@libs/ReportUtils'; -import {toggleSubscribeToChildReport} from '@userActions/Report'; -import CONST from '@src/CONST'; -import type {Beta, IntroSelected, ReportAction, Report as ReportType} from '@src/types/onyx'; - -type PopoverLeaveThreadItemProps = { - reportAction: ReportAction; - originalReport: OnyxEntry; - currentUserAccountID: number; - introSelected: OnyxEntry; - isSelfTourViewed: boolean | undefined; - betas: OnyxEntry; - hideAndRun: (callback?: () => void) => void; - isFocused?: boolean; - onFocus?: () => void; - onBlur?: () => void; -}; - -function PopoverLeaveThreadItem({ - reportAction, - originalReport, - currentUserAccountID, - introSelected, - isSelfTourViewed, - betas, - hideAndRun, - isFocused, - onFocus, - onBlur, -}: PopoverLeaveThreadItemProps) { - const {translate} = useLocalize(); - const icons = useMemoizedLazyExpensifyIcons(['Exit'] as const); - const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); - const {windowWidth} = useWindowDimensions(); - - return ( - - interceptAnonymousUser(() => { - const childReportNotificationPreference = getChildReportNotificationPreference(reportAction); - hideAndRun(() => { - ReportActionComposeFocusManager.focus(); - toggleSubscribeToChildReport( - reportAction?.childReportID, - currentUserAccountID, - reportAction, - originalReport, - introSelected, - isSelfTourViewed, - betas, - childReportNotificationPreference, - ); - }); - }, false) - } - wrapperStyle={[styles.pr8]} - style={StyleUtils.getContextMenuItemStyles(windowWidth)} - focused={isFocused} - interactive - onFocus={onFocus} - onBlur={onBlur} - sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.LEAVE_THREAD} - /> - ); -} - -type MiniLeaveThreadItemProps = { - reportAction: ReportAction; - originalReport: OnyxEntry; - currentUserAccountID: number; - introSelected: OnyxEntry; - isSelfTourViewed: boolean | undefined; - betas: OnyxEntry; - hideAndRun: (callback?: () => void) => void; -}; - -function MiniLeaveThreadItem({reportAction, originalReport, currentUserAccountID, introSelected, isSelfTourViewed, betas, hideAndRun}: MiniLeaveThreadItemProps) { - const {translate} = useLocalize(); - const icons = useMemoizedLazyExpensifyIcons(['Exit'] as const); - - return ( - - interceptAnonymousUser(() => { - const childReportNotificationPreference = getChildReportNotificationPreference(reportAction); - hideAndRun(() => { - ReportActionComposeFocusManager.focus(); - toggleSubscribeToChildReport( - reportAction?.childReportID, - currentUserAccountID, - reportAction, - originalReport, - introSelected, - isSelfTourViewed, - betas, - childReportNotificationPreference, - ); - }); - }, false) - } - sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.LEAVE_THREAD} - /> - ); -} - -function shouldShowLeaveThreadAction({ - reportAction, - isArchivedRoom, - isThreadReportParentAction, - isHarvestReport, -}: { - reportAction: OnyxEntry; - isArchivedRoom: boolean; - isThreadReportParentAction: boolean; - isHarvestReport: boolean; -}): boolean { - const childReportNotificationPreference = getChildReportNotificationPreference(reportAction); - const isDeletedActionResult = isDeletedAction(reportAction); - const shouldDisplayReplies = shouldDisplayThreadReplies(reportAction, isThreadReportParentAction); - const subscribed = childReportNotificationPreference !== 'hidden'; - const isWhisper = isWhisperAction(reportAction) || isActionableTrackExpense(reportAction); - const isExpenseReportAction = isMoneyRequestAction(reportAction) || isReportPreviewAction(reportAction); - const isTaskAction = isCreatedTaskReportAction(reportAction); - const isHarvestCreatedExpenseReportAction = !!isHarvestReport && isCreatedAction(reportAction); - return ( - subscribed && - !isWhisper && - !isTaskAction && - !isExpenseReportAction && - !isThreadReportParentAction && - !isHarvestCreatedExpenseReportAction && - (shouldDisplayReplies || (!isDeletedActionResult && !isArchivedRoom)) - ); -} - -export {shouldShowLeaveThreadAction, PopoverLeaveThreadItem, MiniLeaveThreadItem}; diff --git a/src/pages/inbox/report/ContextMenu/actions/markAsUnreadAction.tsx b/src/pages/inbox/report/ContextMenu/actions/markAsUnreadAction.tsx deleted file mode 100644 index e0734bdfde7b..000000000000 --- a/src/pages/inbox/report/ContextMenu/actions/markAsUnreadAction.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import React from 'react'; -import type {OnyxEntry} from 'react-native-onyx'; -import ContextMenuItem from '@components/ContextMenuItem'; -import MiniContextMenuItem from '@components/MiniContextMenuItem'; -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; -import {isActionOfType} from '@libs/ReportActionsUtils'; -import {markCommentAsUnread} from '@userActions/Report'; -import CONST from '@src/CONST'; -import type {ReportAction, ReportActions} from '@src/types/onyx'; - -type PopoverMarkAsUnreadItemProps = { - reportID: string | undefined; - reportActions: OnyxEntry; - reportAction: ReportAction; - currentUserAccountID: number; - hideAndRun: (callback?: () => void) => void; - isFocused?: boolean; - onFocus?: () => void; - onBlur?: () => void; -}; - -function PopoverMarkAsUnreadItem({reportID, reportActions, reportAction, currentUserAccountID, hideAndRun, isFocused, onFocus, onBlur}: PopoverMarkAsUnreadItemProps) { - const {translate} = useLocalize(); - const icons = useMemoizedLazyExpensifyIcons(['ChatBubbleUnread', 'Checkmark'] as const); - - return ( - - interceptAnonymousUser(() => { - markCommentAsUnread(reportID, reportActions, reportAction, currentUserAccountID); - hideAndRun(ReportActionComposeFocusManager.focus); - }) - } - isFocused={isFocused} - onFocus={onFocus} - onBlur={onBlur} - sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.MARK_AS_UNREAD} - /> - ); -} - -type MiniMarkAsUnreadItemProps = { - reportID: string | undefined; - reportActions: OnyxEntry; - reportAction: ReportAction; - currentUserAccountID: number; - hideAndRun: (callback?: () => void) => void; -}; - -function MiniMarkAsUnreadItem({reportID, reportActions, reportAction, currentUserAccountID, hideAndRun}: MiniMarkAsUnreadItemProps) { - const {translate} = useLocalize(); - const icons = useMemoizedLazyExpensifyIcons(['ChatBubbleUnread', 'Checkmark'] as const); - - return ( - - interceptAnonymousUser(() => { - markCommentAsUnread(reportID, reportActions, reportAction, currentUserAccountID); - hideAndRun(ReportActionComposeFocusManager.focus); - }) - } - sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.MARK_AS_UNREAD} - /> - ); -} - -function shouldShowMarkAsUnreadForReportAction({reportAction}: {reportAction: OnyxEntry}): boolean { - return !isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.DYNAMIC_EXTERNAL_WORKFLOW_ROUTED); -} - -function shouldShowMarkAsUnreadForReport({isUnreadChat}: {isUnreadChat: boolean}): boolean { - return !isUnreadChat; -} - -export {shouldShowMarkAsUnreadForReportAction, shouldShowMarkAsUnreadForReport, PopoverMarkAsUnreadItem, MiniMarkAsUnreadItem}; diff --git a/src/pages/inbox/report/ContextMenu/actions/unholdAction.tsx b/src/pages/inbox/report/ContextMenu/actions/unholdAction.tsx deleted file mode 100644 index 6e35962572c6..000000000000 --- a/src/pages/inbox/report/ContextMenu/actions/unholdAction.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import React from 'react'; -import type {OnyxEntry} from 'react-native-onyx'; -import FocusableMenuItem from '@components/FocusableMenuItem'; -import MiniContextMenuItem from '@components/MiniContextMenuItem'; -import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; -import interceptAnonymousUser from '@libs/interceptAnonymousUser'; -import {getReportAction} from '@libs/ReportActionsUtils'; -import {canHoldUnholdReportAction, changeMoneyRequestHoldStatus} from '@libs/ReportUtils'; -import {hideContextMenu} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; -import CONST from '@src/CONST'; -import type {Policy, ReportAction, Report as ReportType, Transaction} from '@src/types/onyx'; - -type PopoverUnholdItemProps = { - moneyRequestAction: ReportAction | undefined; - iouTransaction: OnyxEntry; - isOffline: boolean; - isDelegateAccessRestricted: boolean; - showDelegateNoAccessModal: (() => void) | undefined; - hideAndRun: (callback?: () => void) => void; - isFocused?: boolean; - onFocus?: () => void; - onBlur?: () => void; -}; - -function PopoverUnholdItem({ - moneyRequestAction, - iouTransaction, - isOffline, - isDelegateAccessRestricted, - showDelegateNoAccessModal, - hideAndRun, - isFocused, - onFocus, - onBlur, -}: PopoverUnholdItemProps) { - const {translate} = useLocalize(); - const icons = useMemoizedLazyExpensifyIcons(['Stopwatch'] as const); - const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); - const {windowWidth} = useWindowDimensions(); - - return ( - - interceptAnonymousUser(() => { - if (isDelegateAccessRestricted) { - hideContextMenu(false, showDelegateNoAccessModal); - return; - } - hideAndRun(() => changeMoneyRequestHoldStatus(moneyRequestAction, iouTransaction, isOffline)); - }, false) - } - wrapperStyle={[styles.pr8]} - style={StyleUtils.getContextMenuItemStyles(windowWidth)} - focused={isFocused} - interactive - onFocus={onFocus} - onBlur={onBlur} - sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.UNHOLD} - /> - ); -} - -type MiniUnholdItemProps = { - moneyRequestAction: ReportAction | undefined; - iouTransaction: OnyxEntry; - isOffline: boolean; - isDelegateAccessRestricted: boolean; - showDelegateNoAccessModal: (() => void) | undefined; - hideAndRun: (callback?: () => void) => void; -}; - -function MiniUnholdItem({moneyRequestAction, iouTransaction, isOffline, isDelegateAccessRestricted, showDelegateNoAccessModal, hideAndRun}: MiniUnholdItemProps) { - const {translate} = useLocalize(); - const icons = useMemoizedLazyExpensifyIcons(['Stopwatch'] as const); - - return ( - - interceptAnonymousUser(() => { - if (isDelegateAccessRestricted) { - hideContextMenu(false, showDelegateNoAccessModal); - return; - } - hideAndRun(() => changeMoneyRequestHoldStatus(moneyRequestAction, iouTransaction, isOffline)); - }, false) - } - sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.UNHOLD} - /> - ); -} - -function shouldShowUnholdAction({ - moneyRequestReport, - moneyRequestAction, - moneyRequestPolicy, - areHoldRequirementsMet, - iouTransaction, -}: { - moneyRequestReport: OnyxEntry; - moneyRequestAction: ReportAction | undefined; - moneyRequestPolicy: OnyxEntry; - areHoldRequirementsMet: boolean; - iouTransaction: OnyxEntry; -}): boolean { - if (!areHoldRequirementsMet) { - return false; - } - const holdReportAction = getReportAction(moneyRequestAction?.childReportID, `${iouTransaction?.comment?.hold ?? ''}`); - return canHoldUnholdReportAction(moneyRequestReport, moneyRequestAction, holdReportAction, iouTransaction, moneyRequestPolicy).canUnholdRequest; -} - -export {shouldShowUnholdAction, PopoverUnholdItem, MiniUnholdItem}; diff --git a/tests/unit/ContextMenuActionsCopyMessageTest.ts b/tests/unit/ContextMenuActionsCopyMessageTest.ts index 18e89c0d6ab7..546151f4edd2 100644 --- a/tests/unit/ContextMenuActionsCopyMessageTest.ts +++ b/tests/unit/ContextMenuActionsCopyMessageTest.ts @@ -1,7 +1,7 @@ import type {LocalizedTranslate} from '@components/LocaleContextProvider'; import Clipboard from '@libs/Clipboard'; import getClipboardText from '@libs/Clipboard/getClipboardText'; -import {copyMessageToClipboard} from '@pages/inbox/report/ContextMenu/actions/copyMessageAction'; +import {copyMessageToClipboard} from '@pages/inbox/report/ContextMenu/actions/CopyMessageAction/copyMessageAction'; import CONST from '@src/CONST'; import type {ReportAction} from '@src/types/onyx'; From e94e948e05d0fb663a8c93ce0012ab4a6e03bc07 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 6 Apr 2026 15:57:27 -0700 Subject: [PATCH 73/88] fix(contextmenu): hide mini context menu on scroll Portal-rendered menu stayed visible with stale position because keepOpen blocked hide during row hover-out. Listen for scroll in capture phase and release + hide to match production behavior. Made-with: Cursor --- .../MiniReportActionContextMenu/index.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index 9415f628a6c9..3187e9488c13 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -109,6 +109,20 @@ function MiniReportActionContextMenu() { }; }, [hideMiniContextMenu]); + useEffect(() => { + if (!isVisible) { + return; + } + const onScroll = () => { + release(); + hideMiniContextMenu(); + }; + window.addEventListener('scroll', onScroll, true); + return () => { + window.removeEventListener('scroll', onScroll, true); + }; + }, [isVisible, release, hideMiniContextMenu]); + useEffect(() => { const el = menuContainerRef.current as unknown as HTMLElement | null; if (!el) { From ec93c03c7e920b94520a7bb09532a7f1f68a1976 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 6 Apr 2026 16:02:01 -0700 Subject: [PATCH 74/88] fix(contextmenu): hide mini on popover open and restore when popover closes Add hideMiniContextMenuWithoutNotification to bypass keepOpen and skip onMenuHide so the row highlight stays correct. Track pointer-over-row and re-show the mini menu with fresh measurements when the popover dismisses if the pointer is still over the message. Made-with: Cursor --- .../ContextMenu/MiniContextMenuProvider.tsx | 16 +++++++ .../inbox/report/PureReportActionItem.tsx | 48 +++++++++++++++++-- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx b/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx index fad7102d0a11..9ab42183cfaf 100644 --- a/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx @@ -35,6 +35,12 @@ type MiniContextMenuActions = { /** Hide the mini context menu immediately. No-op while `keepOpen` is active; the hide intent is deferred until `release`. */ hideMiniContextMenu: () => void; + /** + * Hide the mini menu without invoking `onMenuHide` (e.g. when opening the full popover context menu while the pointer stays over the row). + * Clears the keep-open guard so the menu actually hides. + */ + hideMiniContextMenuWithoutNotification: () => void; + /** Lock the menu open so that `hideMiniContextMenu` calls are deferred until `release` is called. Use when a sub-interaction (overflow menu, emoji picker) needs the menu to stay visible. Also used by the menu's own Hoverable to prevent hide during row-to-menu hover transitions. */ keepOpen: () => void; @@ -45,6 +51,7 @@ type MiniContextMenuActions = { const MiniContextMenuActionsContext = createContext({ showMiniContextMenu: () => {}, hideMiniContextMenu: () => {}, + hideMiniContextMenuWithoutNotification: () => {}, keepOpen: () => {}, release: () => {}, }); @@ -109,6 +116,15 @@ function MiniContextMenuProvider({children}: MiniContextMenuProviderProps) { } performHide(); }, + hideMiniContextMenuWithoutNotification: () => { + shouldKeepOpenRef.current = false; + pendingHideRef.current = false; + queueMicrotask(() => { + setState((prev) => (prev ? {...prev, isVisible: false} : null)); + onMenuHideRef.current = null; + activeReportActionIDRef.current = undefined; + }); + }, keepOpen: () => { shouldKeepOpenRef.current = true; pendingHideRef.current = false; diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx index 17cab9b160b2..22b5f48be558 100644 --- a/src/pages/inbox/report/PureReportActionItem.tsx +++ b/src/pages/inbox/report/PureReportActionItem.tsx @@ -558,7 +558,7 @@ function PureReportActionItem({ const {transitionActionSheetState} = ActionSheetAwareScrollView.useActionSheetAwareScrollViewActions(); const {translate, formatPhoneNumber, localeCompare, formatTravelDate, getLocalDateFromDatetime, datetimeToCalendarTime} = useLocalize(); const {showConfirmModal} = useConfirmModal(); - const {showMiniContextMenu, hideMiniContextMenu} = useMiniContextMenuActions(); + const {showMiniContextMenu, hideMiniContextMenu, hideMiniContextMenuWithoutNotification} = useMiniContextMenuActions(); const personalDetail = useCurrentUserPersonalDetails(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const reportID = report?.reportID ?? action?.reportID; @@ -578,6 +578,7 @@ function PureReportActionItem({ const kycWallRef = useContext(KYCWallContext); const composerTextInputRef = useRef(null); const popoverAnchorRef = useRef>(null); + const isPointerOverReportActionRowRef = useRef(false); const downloadedPreviews = useRef([]); const prevDraftMessage = usePrevious(draftMessage); const isReportActionLinked = linkedReportActionID && action.reportActionID && linkedReportActionID === action.reportActionID; @@ -786,6 +787,7 @@ function PureReportActionItem({ } handleShowContextMenu(() => { + hideMiniContextMenuWithoutNotification(); setIsContextMenuActive(true); const selection = SelectionScraper.getCurrentSelection(); showContextMenu({ @@ -803,13 +805,51 @@ function PureReportActionItem({ }, callbacks: { onShow: toggleContextMenuFromActiveReportAction, - onHide: () => setIsContextMenuActive(false), + onHide: () => { + setIsContextMenuActive(false); + if (isPointerOverReportActionRowRef.current && shouldDisplayContextMenuValue && draftMessage === undefined && isEmptyValueObject(action.errors)) { + const node = popoverAnchorRef.current; + if (!node || !('getBoundingClientRect' in node)) { + return; + } + const rect = node.getBoundingClientRect(); + showMiniContextMenu({ + reportID, + reportActionID: action.reportActionID, + originalReportID, + anchor: popoverAnchorRef, + displayAsGroup: !!displayAsGroup, + draftMessage, + checkIfContextMenuActive: toggleContextMenuFromActiveReportAction, + setIsEmojiPickerActive, + rowMeasurements: { + top: rect.top, + height: rect.height, + right: rect.right, + }, + onMenuHide: () => setIsContextMenuActive(false), + }); + setIsContextMenuActive(true); + } + }, setIsEmojiPickerActive: setIsEmojiPickerActive as () => void, }, }); }); }, - [draftMessage, action.errors, action.reportActionID, reportID, toggleContextMenuFromActiveReportAction, originalReportID, shouldDisplayContextMenuValue, handleShowContextMenu], + [ + draftMessage, + action.errors, + action.reportActionID, + reportID, + toggleContextMenuFromActiveReportAction, + originalReportID, + shouldDisplayContextMenuValue, + handleShowContextMenu, + hideMiniContextMenuWithoutNotification, + showMiniContextMenu, + displayAsGroup, + ], ); const toggleReaction = useCallback( @@ -2104,6 +2144,7 @@ function PureReportActionItem({ if (!shouldDisplayContextMenu || draftMessage !== undefined || hasErrors) { return; } + isPointerOverReportActionRowRef.current = true; const node = popoverAnchorRef.current; if (!node || !('getBoundingClientRect' in node)) { return; @@ -2128,6 +2169,7 @@ function PureReportActionItem({ setIsContextMenuActive(true); }} onHoverOut={() => { + isPointerOverReportActionRowRef.current = false; setIsReportActionActive(!!isReportActionLinked); hideMiniContextMenu(); }} From f50d465d3725051b2f9f664d0194f0977a995f9f Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 8 Apr 2026 16:28:22 -0700 Subject: [PATCH 75/88] fix(contextmenu): show Mark as Unread in report-level menus without reportAction OptionRowLHN passes reportActionID: '-1' for report-level context menus, resolving reportAction to undefined. The guard `&& reportAction` prevented the Mark as Unread item from rendering in report menus. markCommentAsUnread already handles undefined reportAction via optional chaining, so widen the type to match. Made-with: Cursor --- src/libs/actions/Report/index.ts | 2 +- .../ContextMenu/PopoverContextMenu/PopoverReportContent.tsx | 2 +- .../actions/MarkAsUnreadAction/PopoverMarkAsUnreadItem.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index a9e5d5f5b452..5e637d797c87 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -2350,7 +2350,7 @@ function readNewestAction(reportID: string | undefined, hasOnceLoadedReportActio /** * Sets the last read time on a report */ -function markCommentAsUnread(reportID: string | undefined, reportActions: OnyxEntry, reportAction: ReportAction, currentUserAccountID: number) { +function markCommentAsUnread(reportID: string | undefined, reportActions: OnyxEntry, reportAction: ReportAction | undefined, currentUserAccountID: number) { if (!reportID) { Log.warn('7339cd6c-3263-4f89-98e5-730f0be15784 Invalid report passed to MarkCommentAsUnread. Not calling the API because it wil fail.'); return; diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportContent.tsx index 1c63800b06f7..c1d2cccd34af 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportContent.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportContent.tsx @@ -70,7 +70,7 @@ function PopoverReportContent({reportID, reportActionID, originalReportID, hideA />, ); } - if (showMarkAsUnread && reportAction) { + if (showMarkAsUnread) { visibleItems.push( ; - reportAction: ReportAction; + reportAction?: ReportAction; currentUserAccountID: number; hideAndRun: (callback?: () => void) => void; isFocused?: boolean; From 6867fe7212f46c186b642d489827c0a14209a266 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 8 Apr 2026 16:47:12 -0700 Subject: [PATCH 76/88] fix(contextmenu): address Codex review comments 1. Wrap delete and flag-as-offensive actions with interceptAnonymousUser so anonymous users are redirected to sign-in instead of executing destructive/reporting actions (P1). 2. Use shouldDisplayContextMenuValue (which excludes Concierge greeting actions) instead of shouldDisplayContextMenu in the hover-in guard of PureReportActionItem, matching the right-click/long-press path (P2). 3. Pass the real currentUserAccountID to PopoverMarkAsUnreadItem in PopoverReportContent instead of hardcoded 0, so markCommentAsUnread correctly excludes the caller's own actions when anchoring (P2). Made-with: Cursor --- .../PopoverReportContent.tsx | 6 ++++- .../actions/DeleteAction/MiniDeleteItem.tsx | 15 +++++++----- .../DeleteAction/PopoverDeleteItem.tsx | 15 +++++++----- .../MiniFlagAsOffensiveItem.tsx | 23 +++++++++++-------- .../PopoverFlagAsOffensiveItem.tsx | 23 +++++++++++-------- .../inbox/report/PureReportActionItem.tsx | 2 +- 6 files changed, 50 insertions(+), 34 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportContent.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportContent.tsx index c1d2cccd34af..0ec354e81996 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportContent.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportContent.tsx @@ -5,6 +5,7 @@ import type {View as ViewType} from 'react-native'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useEnvironment from '@hooks/useEnvironment'; import useOnyx from '@hooks/useOnyx'; import useReportIsArchived from '@hooks/useReportIsArchived'; @@ -19,6 +20,7 @@ import {shouldShowMarkAsUnreadForReport} from '@pages/inbox/report/ContextMenu/a import PopoverMarkAsUnreadItem from '@pages/inbox/report/ContextMenu/actions/MarkAsUnreadAction/PopoverMarkAsUnreadItem'; import {PopoverPinItem, shouldShowPinAction} from '@pages/inbox/report/ContextMenu/actions/PinAction'; import {PopoverUnpinItem, shouldShowUnpinAction} from '@pages/inbox/report/ContextMenu/actions/UnpinAction'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {ReportAction} from '@src/types/onyx'; @@ -37,6 +39,8 @@ function PopoverReportContent({reportID, reportActionID, originalReportID, hideA const {shouldUseNarrowLayout} = useResponsiveLayout(); const StyleUtils = useStyleUtils(); const {isProduction} = useEnvironment(); + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); + const currentUserAccountID = currentUserPersonalDetails?.accountID ?? CONST.DEFAULT_NUMBER_ID; const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, {canEvict: false}); const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); @@ -77,7 +81,7 @@ function PopoverReportContent({reportID, reportActionID, originalReportID, hideA reportID={reportID} reportActions={reportActions} reportAction={reportAction} - currentUserAccountID={0} + currentUserAccountID={currentUserAccountID} hideAndRun={hideAndRun} />, ); diff --git a/src/pages/inbox/report/ContextMenu/actions/DeleteAction/MiniDeleteItem.tsx b/src/pages/inbox/report/ContextMenu/actions/DeleteAction/MiniDeleteItem.tsx index 435e9b4c5f68..53bdeb8ab679 100644 --- a/src/pages/inbox/report/ContextMenu/actions/DeleteAction/MiniDeleteItem.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/DeleteAction/MiniDeleteItem.tsx @@ -2,6 +2,7 @@ import React from 'react'; import MiniContextMenuItem from '@components/MiniContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils'; import {showDeleteModal} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; @@ -22,12 +23,14 @@ export default function MiniDeleteItem({reportID, reportAction, moneyRequestActi { - const iouReportID = isMoneyRequestAction(moneyRequestAction) ? getOriginalMessage(moneyRequestAction)?.IOUReportID : undefined; - const effectiveReportID = iouReportID && Number(iouReportID) !== 0 ? iouReportID : reportID; - const actionSourceID = effectiveReportID !== reportID ? reportID : undefined; - hideAndRun(() => showDeleteModal(effectiveReportID, moneyRequestAction ?? reportAction, undefined, undefined, undefined, actionSourceID)); - }} + onPress={() => + interceptAnonymousUser(() => { + const iouReportID = isMoneyRequestAction(moneyRequestAction) ? getOriginalMessage(moneyRequestAction)?.IOUReportID : undefined; + const effectiveReportID = iouReportID && Number(iouReportID) !== 0 ? iouReportID : reportID; + const actionSourceID = effectiveReportID !== reportID ? reportID : undefined; + hideAndRun(() => showDeleteModal(effectiveReportID, moneyRequestAction ?? reportAction, undefined, undefined, undefined, actionSourceID)); + }) + } sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.DELETE} /> ); diff --git a/src/pages/inbox/report/ContextMenu/actions/DeleteAction/PopoverDeleteItem.tsx b/src/pages/inbox/report/ContextMenu/actions/DeleteAction/PopoverDeleteItem.tsx index 74dd2e164301..b6f1da560dd7 100644 --- a/src/pages/inbox/report/ContextMenu/actions/DeleteAction/PopoverDeleteItem.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/DeleteAction/PopoverDeleteItem.tsx @@ -5,6 +5,7 @@ import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils'; import {showDeleteModal} from '@pages/inbox/report/ContextMenu/ReportActionContextMenu'; import CONST from '@src/CONST'; @@ -31,12 +32,14 @@ export default function PopoverDeleteItem({reportID, reportAction, moneyRequestA { - const iouReportID = isMoneyRequestAction(moneyRequestAction) ? getOriginalMessage(moneyRequestAction)?.IOUReportID : undefined; - const effectiveReportID = iouReportID && Number(iouReportID) !== 0 ? iouReportID : reportID; - const actionSourceID = effectiveReportID !== reportID ? reportID : undefined; - hideAndRun(() => showDeleteModal(effectiveReportID, moneyRequestAction ?? reportAction, undefined, undefined, undefined, actionSourceID)); - }} + onPress={() => + interceptAnonymousUser(() => { + const iouReportID = isMoneyRequestAction(moneyRequestAction) ? getOriginalMessage(moneyRequestAction)?.IOUReportID : undefined; + const effectiveReportID = iouReportID && Number(iouReportID) !== 0 ? iouReportID : reportID; + const actionSourceID = effectiveReportID !== reportID ? reportID : undefined; + hideAndRun(() => showDeleteModal(effectiveReportID, moneyRequestAction ?? reportAction, undefined, undefined, undefined, actionSourceID)); + }) + } wrapperStyle={[styles.pr8]} style={StyleUtils.getContextMenuItemStyles(windowWidth)} focused={isFocused} diff --git a/src/pages/inbox/report/ContextMenu/actions/FlagAsOffensiveAction/MiniFlagAsOffensiveItem.tsx b/src/pages/inbox/report/ContextMenu/actions/FlagAsOffensiveAction/MiniFlagAsOffensiveItem.tsx index a126b455950e..b239b1acebe0 100644 --- a/src/pages/inbox/report/ContextMenu/actions/FlagAsOffensiveAction/MiniFlagAsOffensiveItem.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/FlagAsOffensiveAction/MiniFlagAsOffensiveItem.tsx @@ -2,6 +2,7 @@ import React from 'react'; import MiniContextMenuItem from '@components/MiniContextMenuItem'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; @@ -22,17 +23,19 @@ export default function MiniFlagAsOffensiveItem({reportID, reportAction, hideAnd { - if (!reportID) { - return; - } - const activeRoute = Navigation.getActiveRoute(); - hideAndRun(() => { - KeyboardUtils.dismiss().then(() => { - Navigation.navigate(ROUTES.FLAG_COMMENT.getRoute(reportID, reportAction?.reportActionID, activeRoute)); + onPress={() => + interceptAnonymousUser(() => { + if (!reportID) { + return; + } + const activeRoute = Navigation.getActiveRoute(); + hideAndRun(() => { + KeyboardUtils.dismiss().then(() => { + Navigation.navigate(ROUTES.FLAG_COMMENT.getRoute(reportID, reportAction?.reportActionID, activeRoute)); + }); }); - }); - }} + }) + } sentryLabel={CONST.SENTRY_LABEL.CONTEXT_MENU.FLAG_AS_OFFENSIVE} /> ); diff --git a/src/pages/inbox/report/ContextMenu/actions/FlagAsOffensiveAction/PopoverFlagAsOffensiveItem.tsx b/src/pages/inbox/report/ContextMenu/actions/FlagAsOffensiveAction/PopoverFlagAsOffensiveItem.tsx index 345457a388f7..5aa9fe9525a7 100644 --- a/src/pages/inbox/report/ContextMenu/actions/FlagAsOffensiveAction/PopoverFlagAsOffensiveItem.tsx +++ b/src/pages/inbox/report/ContextMenu/actions/FlagAsOffensiveAction/PopoverFlagAsOffensiveItem.tsx @@ -5,6 +5,7 @@ import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; @@ -31,17 +32,19 @@ export default function PopoverFlagAsOffensiveItem({reportID, reportAction, hide { - if (!reportID) { - return; - } - const activeRoute = Navigation.getActiveRoute(); - hideAndRun(() => { - KeyboardUtils.dismiss().then(() => { - Navigation.navigate(ROUTES.FLAG_COMMENT.getRoute(reportID, reportAction?.reportActionID, activeRoute)); + onPress={() => + interceptAnonymousUser(() => { + if (!reportID) { + return; + } + const activeRoute = Navigation.getActiveRoute(); + hideAndRun(() => { + KeyboardUtils.dismiss().then(() => { + Navigation.navigate(ROUTES.FLAG_COMMENT.getRoute(reportID, reportAction?.reportActionID, activeRoute)); + }); }); - }); - }} + }) + } wrapperStyle={[styles.pr8]} style={StyleUtils.getContextMenuItemStyles(windowWidth)} focused={isFocused} diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx index 9e8d684ac31d..8d609eb69c15 100644 --- a/src/pages/inbox/report/PureReportActionItem.tsx +++ b/src/pages/inbox/report/PureReportActionItem.tsx @@ -2144,7 +2144,7 @@ function PureReportActionItem({ shouldFreezeCapture={isPaymentMethodPopoverActive} onHoverIn={() => { setIsReportActionActive(false); - if (!shouldDisplayContextMenu || draftMessage !== undefined || hasErrors) { + if (!shouldDisplayContextMenuValue || draftMessage !== undefined || hasErrors) { return; } isPointerOverReportActionRowRef.current = true; From b7ee8df244163325fe7111f98250e9886e424d3e Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 8 Apr 2026 17:13:06 -0700 Subject: [PATCH 77/88] fix(contextmenu): fix lint and typecheck for optional reportAction in markCommentAsUnread Use ?? instead of || for reportAction?.created (prefer-nullish-coalescing). Make reportActionID optional in MarkAsUnreadParams since report-level mark-as-unread doesn't target a specific action. Made-with: Cursor --- src/libs/API/parameters/MarkAsUnreadParams.ts | 2 +- src/libs/actions/Report/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/API/parameters/MarkAsUnreadParams.ts b/src/libs/API/parameters/MarkAsUnreadParams.ts index 56ab9bf563ea..36615527f497 100644 --- a/src/libs/API/parameters/MarkAsUnreadParams.ts +++ b/src/libs/API/parameters/MarkAsUnreadParams.ts @@ -1,7 +1,7 @@ type MarkAsUnreadParams = { reportID: string; lastReadTime: string; - reportActionID: string; + reportActionID?: string; }; export default MarkAsUnreadParams; diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index 5e637d797c87..18beba007ff4 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -2377,7 +2377,7 @@ function markCommentAsUnread(reportID: string | undefined, reportActions: OnyxEn // If no action created date is provided, use the last action's from other user const actionCreationTime = - reportAction?.created || (latestReportActionFromOtherUsers?.created ?? getReportLastVisibleActionCreated(report, transactionThreadReport) ?? DateUtils.getDBTime(0)); + reportAction?.created ?? (latestReportActionFromOtherUsers?.created ?? getReportLastVisibleActionCreated(report, transactionThreadReport) ?? DateUtils.getDBTime(0)); // We subtract 1 millisecond so that the lastReadTime is updated to just before a given reportAction's created date // For example, if we want to mark a report action with ID 100 and created date '2014-04-01 16:07:02.999' unread, we set the lastReadTime to '2014-04-01 16:07:02.998' From 897fa05e8407c72ae639866122db07fe6b33cd67 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 8 Apr 2026 17:15:23 -0700 Subject: [PATCH 78/88] Prettier --- src/libs/actions/Report/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index 18beba007ff4..a2200cf5b16a 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -2377,7 +2377,7 @@ function markCommentAsUnread(reportID: string | undefined, reportActions: OnyxEn // If no action created date is provided, use the last action's from other user const actionCreationTime = - reportAction?.created ?? (latestReportActionFromOtherUsers?.created ?? getReportLastVisibleActionCreated(report, transactionThreadReport) ?? DateUtils.getDBTime(0)); + reportAction?.created ?? latestReportActionFromOtherUsers?.created ?? getReportLastVisibleActionCreated(report, transactionThreadReport) ?? DateUtils.getDBTime(0); // We subtract 1 millisecond so that the lastReadTime is updated to just before a given reportAction's created date // For example, if we want to mark a report action with ID 100 and created date '2014-04-01 16:07:02.999' unread, we set the lastReadTime to '2014-04-01 16:07:02.998' From 54f3dc80b531eecfd0f022edfe19d7dfcb5c4f11 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 15 Apr 2026 11:27:28 -0700 Subject: [PATCH 79/88] fix: add missing React import to ConfirmDeleteReportActionModal The file uses JSX but only imported named exports from 'react', causing "React is not defined" crash when opening the delete confirmation modal. Made-with: Cursor --- .../PopoverContextMenu/ConfirmDeleteReportActionModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/ConfirmDeleteReportActionModal.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/ConfirmDeleteReportActionModal.tsx index a63970bcbc16..aaefaa469552 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/ConfirmDeleteReportActionModal.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/ConfirmDeleteReportActionModal.tsx @@ -1,4 +1,4 @@ -import {useRef, useState} from 'react'; +import React, {useRef, useState} from 'react'; import {DeviceEventEmitter, InteractionManager} from 'react-native'; import ConfirmModal from '@components/ConfirmModal'; import type {ModalProps} from '@components/Modal/Global/ModalContext'; From 6d8d3654eff058c66cfd24108c69b982e7847bfe Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 15 Apr 2026 12:50:44 -0700 Subject: [PATCH 80/88] fix(contextmenu): hide mini menu when navigating via Reply in Thread hideAndRun only called release() but never initiated a hide, so the menu stayed visible. When navigating to a thread the old ReportScreen stays mounted and its Portal content leaked into the new screen's PortalHost. Two fixes: - hideAndRun now calls hideMiniContextMenu() after release() - Return null when !isVisible so no Portal content exists to leak Made-with: Cursor --- .../report/ContextMenu/MiniReportActionContextMenu/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index 3187e9488c13..182bddfe1a35 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -184,6 +184,7 @@ function MiniReportActionContextMenu() { const hideAndRun = (callback?: () => void) => { release(); + hideMiniContextMenu(); callback?.(); }; @@ -470,7 +471,7 @@ function MiniReportActionContextMenu() { const hasEmoji = shouldShowEmojiReaction({reportAction}) && !!emojiData.reportAction && !!emojiData.reportActionID; - if (!rowMeasurements) { + if (!isVisible || !rowMeasurements) { return null; } From 166a79d482817e825eeabe1aa06b66c786ec14c2 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 15 Apr 2026 16:24:29 -0700 Subject: [PATCH 81/88] fix(contextmenu): use callback ref for reliable overlay measurement after Portal mount MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When isVisible transitions from false to true, the @gorhom/portal library inserts content asynchronously via a state update in the PortalHost. The existing useLayoutEffect fires before the Portal content is in the DOM, so overlayRef.current is null and containerRect is never set — leaving the menu at opacity: 0. A callback ref fires exactly when the DOM element is attached, regardless of Portal timing. The useLayoutEffect is kept for subsequent rowMeasurements changes when the element is already mounted. Made-with: Cursor --- .../MiniReportActionContextMenu/index.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index 5f42f8f28e09..27db09fb8d0b 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -1,5 +1,5 @@ import {Portal} from '@gorhom/portal'; -import React, {useEffect, useLayoutEffect, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useLayoutEffect, useRef, useState} from 'react'; import type {RefObject} from 'react'; import {StyleSheet, View} from 'react-native'; // eslint-disable-next-line no-restricted-imports @@ -74,6 +74,14 @@ function MiniReportActionContextMenu() { const menuContainerRef = useRef(null); const [containerRect, setContainerRect] = useState(null); + const overlayCallbackRef = useCallback((node: View | null) => { + overlayRef.current = node; + const el = node as unknown as HTMLElement | null; + if (el) { + setContainerRect(el.getBoundingClientRect()); + } + }, []); + useLayoutEffect(() => { const el = overlayRef.current as unknown as HTMLElement | null; if (!el) { @@ -482,7 +490,7 @@ function MiniReportActionContextMenu() { return ( From def4d76766dbbbda869b44c948fa20ba45151c1b Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 15 Apr 2026 16:24:35 -0700 Subject: [PATCH 82/88] fix(hoverable): detect cursor already over element on mount When a component mounts under a stationary cursor (e.g. after navigation), mouseenter doesn't fire because the cursor didn't physically enter the element. Add a useEffect that checks element.matches(':hover') on mount to handle this case. The same :hover technique is already used in the scroll handler within the same file. Made-with: Cursor --- src/components/Hoverable/ActiveHoverable.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/Hoverable/ActiveHoverable.tsx b/src/components/Hoverable/ActiveHoverable.tsx index f6c32f821088..9dc252e90044 100644 --- a/src/components/Hoverable/ActiveHoverable.tsx +++ b/src/components/Hoverable/ActiveHoverable.tsx @@ -84,6 +84,12 @@ function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, isFocused = setIsHovered(false); }, [isFocused]); + useEffect(() => { + if (elementRef.current?.matches(':hover') && !isHoveredRef.current && !isVisibilityHidden.current) { + updateIsHovered(true); + } + }, [updateIsHovered]); + const handleMouseEvents = useCallback( (type: 'enter' | 'leave') => () => { if (shouldFreezeCapture) { From da6062dcd34cbffaa41f7218e94480949d8bac94 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 15 Apr 2026 16:55:12 -0700 Subject: [PATCH 83/88] fix(a11y): restore Tab focus from report action row to mini context menu The mini context menu is now rendered via a Portal, placing it at the end of the DOM order. This broke keyboard navigation since Tab from a hovered report action would skip the menu entirely. Bridge focus by intercepting Tab/Shift+Tab between the row and the Portal-rendered menu container via keydown listeners. Made-with: Cursor --- .../ContextMenu/MiniContextMenuProvider.tsx | 6 ++++ .../MiniReportActionContextMenu/index.tsx | 35 +++++++++++++++---- .../inbox/report/PureReportActionItem.tsx | 27 +++++++++++++- 3 files changed, 61 insertions(+), 7 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx b/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx index 9ab42183cfaf..2850cdfb72a6 100644 --- a/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx @@ -46,6 +46,9 @@ type MiniContextMenuActions = { /** Unlock the menu after `keepOpen`. If a hide was deferred while locked, it executes immediately. */ release: () => void; + + /** Ref to the mini menu's container element, used by PureReportActionItem to bridge Tab focus from the row to the Portal-rendered menu. */ + menuContainerRef: RefObject; }; const MiniContextMenuActionsContext = createContext({ @@ -54,6 +57,7 @@ const MiniContextMenuActionsContext = createContext({ hideMiniContextMenuWithoutNotification: () => {}, keepOpen: () => {}, release: () => {}, + menuContainerRef: {current: null}, }); const MiniContextMenuStateContext = createContext(null); @@ -68,6 +72,7 @@ function MiniContextMenuProvider({children}: MiniContextMenuProviderProps) { const pendingHideRef = useRef(false); const onMenuHideRef = useRef<(() => void) | null>(null); const activeReportActionIDRef = useRef(undefined); + const menuContainerRef = useRef(null); const [actions] = useState(() => { const isGuarded = () => shouldKeepOpenRef.current; @@ -133,6 +138,7 @@ function MiniContextMenuProvider({children}: MiniContextMenuProviderProps) { shouldKeepOpenRef.current = false; drainPendingHide(); }, + menuContainerRef, }; }); diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index 27db09fb8d0b..c3dc678e8dfe 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -62,7 +62,7 @@ function MiniReportActionContextMenu() { checkIfContextMenuActive, setIsEmojiPickerActive, } = useMiniContextMenuState() ?? {}; - const {hideMiniContextMenu, keepOpen, release} = useMiniContextMenuActions(); + const {hideMiniContextMenu, keepOpen, release, menuContainerRef} = useMiniContextMenuActions(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); @@ -71,7 +71,15 @@ function MiniReportActionContextMenu() { const overflowIcons = useMemoizedLazyExpensifyIcons(['ThreeDots'] as const); const threeDotRef = useRef(null); const overlayRef = useRef(null); - const menuContainerRef = useRef(null); + const localMenuContainerRef = useRef(null); + const menuContainerCallbackRef = useCallback( + (node: View | null) => { + localMenuContainerRef.current = node; + // eslint-disable-next-line no-param-reassign + menuContainerRef.current = node as unknown as HTMLElement | null; + }, + [menuContainerRef], + ); const [containerRect, setContainerRect] = useState(null); const overlayCallbackRef = useCallback((node: View | null) => { @@ -99,7 +107,7 @@ function MiniReportActionContextMenu() { : null; useEffect(() => { - const el = menuContainerRef.current as unknown as HTMLElement | null; + const el = localMenuContainerRef.current as unknown as HTMLElement | null; if (!el) { return; } @@ -111,11 +119,26 @@ function MiniReportActionContextMenu() { hideMiniContextMenu(); }; + const onKeyDown = (e: KeyboardEvent) => { + if (e.key !== 'Tab' || !e.shiftKey) { + return; + } + const anchorEl = anchor?.current as unknown as HTMLElement | null; + if (!anchorEl) { + return; + } + e.preventDefault(); + anchorEl.focus(); + }; + el.addEventListener('blur', onBlurCapture, true); + el.addEventListener('keydown', onKeyDown); return () => { el.removeEventListener('blur', onBlurCapture, true); + el.removeEventListener('keydown', onKeyDown); }; - }, [hideMiniContextMenu]); + // eslint-disable-next-line react-hooks/exhaustive-deps -- anchor is a ref object, only its .current matters + }, [hideMiniContextMenu, anchor?.current]); useEffect(() => { if (!isVisible) { @@ -132,7 +155,7 @@ function MiniReportActionContextMenu() { }, [isVisible, release, hideMiniContextMenu]); useEffect(() => { - const el = menuContainerRef.current as unknown as HTMLElement | null; + const el = localMenuContainerRef.current as unknown as HTMLElement | null; if (!el) { return; } @@ -506,7 +529,7 @@ function MiniReportActionContextMenu() { pointerEvents={isVisible ? 'auto' : 'none'} > {hasEmoji && !!emojiData.reportAction && !!emojiData.reportActionID && ( diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx index f9762afe8470..a68b09a581c7 100644 --- a/src/pages/inbox/report/PureReportActionItem.tsx +++ b/src/pages/inbox/report/PureReportActionItem.tsx @@ -467,7 +467,7 @@ function PureReportActionItem({ const {transitionActionSheetState} = ActionSheetAwareScrollView.useActionSheetAwareScrollViewActions(); const {translate, formatPhoneNumber, localeCompare, formatTravelDate, datetimeToCalendarTime} = useLocalize(); const {showConfirmModal} = useConfirmModal(); - const {showMiniContextMenu, hideMiniContextMenu, hideMiniContextMenuWithoutNotification} = useMiniContextMenuActions(); + const {showMiniContextMenu, hideMiniContextMenu, hideMiniContextMenuWithoutNotification, menuContainerRef} = useMiniContextMenuActions(); const personalDetail = useCurrentUserPersonalDetails(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const reportID = report?.reportID ?? action?.reportID; @@ -658,6 +658,31 @@ function PureReportActionItem({ setIsHidden(false); }, [latestDecision, action]); + useEffect(() => { + if (!isContextMenuActive) { + return; + } + const el = popoverAnchorRef.current as unknown as HTMLElement | null; + if (!el) { + return; + } + + const onKeyDown = (e: KeyboardEvent) => { + if (e.key !== 'Tab' || e.shiftKey) { + return; + } + const firstButton = menuContainerRef.current?.querySelector('[role="button"]') as HTMLElement | null; + if (!firstButton) { + return; + } + e.preventDefault(); + firstButton.focus(); + }; + + el.addEventListener('keydown', onKeyDown); + return () => el.removeEventListener('keydown', onKeyDown); + }, [isContextMenuActive, menuContainerRef]); + const toggleContextMenuFromActiveReportAction = useCallback(() => { setIsContextMenuActive(isActiveReportAction(action.reportActionID)); }, [action.reportActionID]); From 21f4ed9afa23887d7969d0f466f315d9aa067cba Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 16 Apr 2026 15:16:45 -0700 Subject: [PATCH 84/88] test(contextmenu): pin the ordering of popover context menu items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a jest UI test that renders PopoverReportActionContent and PopoverReportContent against seeded Onyx data and asserts the resulting items appear in the expected sequence (keyed off sentryLabel, already present on every item). The ordering is intentionally hardcoded in the test file so any accidental reshuffling — from the composition refactor or future edits — trips the assertion. Scenarios covered: - Plain comment by another user (Join thread appears because the current user is not subscribed to the thread). - Current user's own comment (Edit and Delete appear; Leave thread appears because the creator is auto-subscribed). - No reportAction supplied (only the overflow Menu is rendered). - Report-level menu on a read, unpinned chat (Mark as unread + Pin). - Report-level menu on an unread, pinned chat (Mark as read + Unpin). Made-with: Cursor --- tests/ui/ContextMenuOrderingTest.tsx | 257 +++++++++++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 tests/ui/ContextMenuOrderingTest.tsx diff --git a/tests/ui/ContextMenuOrderingTest.tsx b/tests/ui/ContextMenuOrderingTest.tsx new file mode 100644 index 000000000000..9e0c50db4e31 --- /dev/null +++ b/tests/ui/ContextMenuOrderingTest.tsx @@ -0,0 +1,257 @@ +import {PortalProvider} from '@gorhom/portal'; +import * as NativeNavigation from '@react-navigation/native'; +import {act, render} from '@testing-library/react-native'; +import React, {useRef} from 'react'; +import Onyx from 'react-native-onyx'; +import type {ReactTestInstance} from 'react-test-renderer'; +import ComposeProviders from '@components/ComposeProviders'; +import DelegateNoAccessModalProvider from '@components/DelegateNoAccessModalProvider'; +import HTMLEngineProvider from '@components/HTMLEngineProvider'; +import {LocaleContextProvider} from '@components/LocaleContextProvider'; +import OnyxListItemProvider from '@components/OnyxListItemProvider'; +import OptionsListContextProvider from '@components/OptionListContextProvider'; +import ScreenWrapper from '@components/ScreenWrapper'; +import PopoverReportActionContent from '@pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportActionContent'; +import PopoverReportContent from '@pages/inbox/report/ContextMenu/PopoverContextMenu/PopoverReportContent'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Report, ReportAction} from '@src/types/onyx'; +import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; +import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatchedUpdates'; + +jest.mock('@react-navigation/native'); + +const CURRENT_USER_ACCOUNT_ID = 11111111; +const CURRENT_USER_EMAIL = 'me@test.com'; +const OTHER_USER_ACCOUNT_ID = 22222222; +const OTHER_USER_EMAIL = 'other@test.com'; +const REPORT_ID = 'testReport'; + +/** + * Collect rendered context-menu items in tree order, de-duplicated by + * `sentryLabel` (since the prop propagates through several nested wrappers + * — MenuItem → PressableWithSecondaryInteraction → … — all of which expose + * the same `sentryLabel`). + */ +function collectSentryLabels(root: ReactTestInstance): string[] { + const matches = root.findAll((el) => { + const label: unknown = el.props?.sentryLabel; + return typeof label === 'string' && label.startsWith('ContextMenu-'); + }); + const seen = new Set(); + const ordered: string[] = []; + for (const match of matches) { + const label = match.props.sentryLabel as string; + if (seen.has(label)) { + continue; + } + seen.add(label); + ordered.push(label); + } + return ordered; +} + +function buildTextCommentAction(overrides: Partial = {}): ReportAction { + return { + reportActionID: '100', + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + actorAccountID: OTHER_USER_ACCOUNT_ID, + created: '2026-04-15 09:00:00.000', + automatic: false, + shouldShow: true, + avatar: '', + person: [{type: 'TEXT', style: 'strong', text: OTHER_USER_EMAIL}], + message: [{type: 'COMMENT', html: '

hello world

', text: 'hello world'}], + originalMessage: {html: '

hello world

'}, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ...(overrides as any), + } as ReportAction; +} + +function PopoverReportActionContentHarness({reportActionID = '100'}: {reportActionID?: string | undefined}) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const contentRef = useRef(null); + return ( + {}} + setLocalShouldKeepOpen={() => {}} + contentRef={contentRef} + shouldEnableArrowNavigation={false} + /> + ); +} + +function PopoverReportContentHarness() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const contentRef = useRef(null); + return ( + {}} + contentRef={contentRef} + shouldEnableArrowNavigation={false} + /> + ); +} + +function renderWithProviders(element: React.ReactElement) { + return render( + + + + + {element} + + + + , + ); +} + +describe('Context menu item ordering', () => { + beforeAll(() => { + Onyx.init({ + keys: ONYXKEYS, + evictableKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS], + }); + jest.spyOn(NativeNavigation, 'useRoute').mockReturnValue({key: '', name: ''}); + }); + + beforeEach(async () => { + wrapOnyxWithWaitForBatchedUpdates(Onyx); + await act(async () => { + await Onyx.merge(ONYXKEYS.SESSION, { + accountID: CURRENT_USER_ACCOUNT_ID, + email: CURRENT_USER_EMAIL, + encryptedAuthToken: 'fake-token', + }); + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, { + [CURRENT_USER_ACCOUNT_ID]: { + accountID: CURRENT_USER_ACCOUNT_ID, + login: CURRENT_USER_EMAIL, + displayName: CURRENT_USER_EMAIL, + }, + [OTHER_USER_ACCOUNT_ID]: { + accountID: OTHER_USER_ACCOUNT_ID, + login: OTHER_USER_EMAIL, + displayName: OTHER_USER_EMAIL, + }, + }); + }); + await waitForBatchedUpdatesWithAct(); + }); + + afterEach(async () => { + await act(async () => { + await Onyx.clear(); + }); + await waitForBatchedUpdatesWithAct(); + }); + + async function seedReport(report: Partial, reportActions: Record) { + await act(async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, { + reportID: REPORT_ID, + chatType: undefined, + ...report, + } as Report); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, reportActions); + }); + await waitForBatchedUpdatesWithAct(); + } + + describe('PopoverReportActionContent (right-click on a message)', () => { + it('renders items in the canonical order for a plain comment by another user', async () => { + const action = buildTextCommentAction({actorAccountID: OTHER_USER_ACCOUNT_ID}); + await seedReport({}, {[action.reportActionID]: action}); + + const {root} = renderWithProviders(); + await waitForBatchedUpdatesWithAct(); + + // Another user's action defaults to HIDDEN notification preference, so + // Join thread is offered between Mark as unread and Copy message. + expect(collectSentryLabels(root)).toEqual([ + CONST.SENTRY_LABEL.CONTEXT_MENU.REPLY_IN_THREAD, + CONST.SENTRY_LABEL.CONTEXT_MENU.MARK_AS_UNREAD, + CONST.SENTRY_LABEL.CONTEXT_MENU.JOIN_THREAD, + CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_MESSAGE, + CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_LINK, + CONST.SENTRY_LABEL.CONTEXT_MENU.FLAG_AS_OFFENSIVE, + ]); + }); + + it('renders items in the canonical order for the current user’s own comment', async () => { + const action = buildTextCommentAction({actorAccountID: CURRENT_USER_ACCOUNT_ID, reportActionID: '101'}); + await seedReport({}, {[action.reportActionID]: action}); + + const {root} = renderWithProviders(); + await waitForBatchedUpdatesWithAct(); + + // The current user is treated as the creator of their own action, so + // Leave thread surfaces between Edit and Copy message. + expect(collectSentryLabels(root)).toEqual([ + CONST.SENTRY_LABEL.CONTEXT_MENU.REPLY_IN_THREAD, + CONST.SENTRY_LABEL.CONTEXT_MENU.MARK_AS_UNREAD, + CONST.SENTRY_LABEL.CONTEXT_MENU.EDIT_COMMENT, + CONST.SENTRY_LABEL.CONTEXT_MENU.LEAVE_THREAD, + CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_MESSAGE, + CONST.SENTRY_LABEL.CONTEXT_MENU.COPY_LINK, + CONST.SENTRY_LABEL.CONTEXT_MENU.DELETE, + ]); + }); + + it('renders only the overflow "Menu" item when no reportAction is supplied', async () => { + await seedReport({}, {}); + + const {root} = renderWithProviders(); + await waitForBatchedUpdatesWithAct(); + + expect(collectSentryLabels(root)).toEqual([CONST.SENTRY_LABEL.CONTEXT_MENU.MENU]); + }); + }); + + describe('PopoverReportContent (right-click on a report row)', () => { + it('renders items in the canonical order for a read, unpinned chat', async () => { + await seedReport( + { + isPinned: false, + lastMessageText: 'hello', + lastReadTime: '2100-01-01 00:00:00.000', + lastVisibleActionCreated: '2020-01-01 00:00:00.000', + }, + {}, + ); + + const {root} = renderWithProviders(); + await waitForBatchedUpdatesWithAct(); + + expect(collectSentryLabels(root)).toEqual([CONST.SENTRY_LABEL.CONTEXT_MENU.MARK_AS_UNREAD, CONST.SENTRY_LABEL.CONTEXT_MENU.PIN]); + }); + + it('renders Mark as read and Unpin for an unread, pinned chat', async () => { + await seedReport( + { + isPinned: true, + lastMessageText: 'hello', + lastReadTime: '2020-01-01 00:00:00.000', + lastVisibleActionCreated: '2100-01-01 00:00:00.000', + }, + {}, + ); + + const {root} = renderWithProviders(); + await waitForBatchedUpdatesWithAct(); + + expect(collectSentryLabels(root)).toEqual([CONST.SENTRY_LABEL.CONTEXT_MENU.MARK_AS_READ, CONST.SENTRY_LABEL.CONTEXT_MENU.UNPIN]); + }); + }); +}); From 624133382ebf7c925a6b733121be23ef83ce7938 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 16 Apr 2026 15:44:30 -0700 Subject: [PATCH 85/88] fix(contextmenu): resolve lint and React Compiler compliance issues - ActiveHoverable: use early return in the :hover-on-mount effect to satisfy rulesdir/prefer-early-return. - MiniReportActionContextMenu: depend on the anchor RefObject itself instead of anchor?.current, avoiding ref access during render. Swap the react-hooks/exhaustive-deps disable (which made React Compiler skip the component) for prefer-narrow-hook-dependencies. - ConfirmDeleteReportActionModal: update reportActionsRef inside a useEffect instead of during render. - ContextMenuOrderingTest: stop importing the deprecated ReactTestInstance type from react-test-renderer; infer from @testing-library/react-native's RenderResult instead. Made-with: Cursor --- src/components/Hoverable/ActiveHoverable.tsx | 5 +++-- .../ContextMenu/MiniReportActionContextMenu/index.tsx | 6 ++++-- .../ConfirmDeleteReportActionModal.tsx | 10 +++++++--- tests/ui/ContextMenuOrderingTest.tsx | 4 ++-- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/components/Hoverable/ActiveHoverable.tsx b/src/components/Hoverable/ActiveHoverable.tsx index 9dc252e90044..f9b722cc3771 100644 --- a/src/components/Hoverable/ActiveHoverable.tsx +++ b/src/components/Hoverable/ActiveHoverable.tsx @@ -85,9 +85,10 @@ function ActiveHoverable({onHoverIn, onHoverOut, shouldHandleScroll, isFocused = }, [isFocused]); useEffect(() => { - if (elementRef.current?.matches(':hover') && !isHoveredRef.current && !isVisibilityHidden.current) { - updateIsHovered(true); + if (!elementRef.current?.matches(':hover') || isHoveredRef.current || isVisibilityHidden.current) { + return; } + updateIsHovered(true); }, [updateIsHovered]); const handleMouseEvents = useCallback( diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index c3dc678e8dfe..5029836558d5 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -137,8 +137,10 @@ function MiniReportActionContextMenu() { el.removeEventListener('blur', onBlurCapture, true); el.removeEventListener('keydown', onKeyDown); }; - // eslint-disable-next-line react-hooks/exhaustive-deps -- anchor is a ref object, only its .current matters - }, [hideMiniContextMenu, anchor?.current]); + // Depending on the ref object rather than anchor?.current avoids accessing + // refs during render (required for React Compiler compliance); the ref identity is stable. + // eslint-disable-next-line rulesdir/prefer-narrow-hook-dependencies + }, [hideMiniContextMenu, anchor]); useEffect(() => { if (!isVisible) { diff --git a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/ConfirmDeleteReportActionModal.tsx b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/ConfirmDeleteReportActionModal.tsx index b55bdc477de4..484f2bf70f45 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverContextMenu/ConfirmDeleteReportActionModal.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverContextMenu/ConfirmDeleteReportActionModal.tsx @@ -1,4 +1,4 @@ -import React, {useRef, useState} from 'react'; +import React, {useEffect, useRef, useState} from 'react'; import {DeviceEventEmitter, InteractionManager} from 'react-native'; import ConfirmModal from '@components/ConfirmModal'; import type {ModalProps} from '@components/Modal/Global/ModalContext'; @@ -31,9 +31,13 @@ function ConfirmDeleteReportActionModal({closeModal, reportID, reportActionID, a const {currentSearchHash} = useSearchStateContext(); const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`); + // Track the latest reportActions so runAfterInteractions sees post-click updates + // (e.g. another action deleted in the meantime). Updated inside an effect so the + // ref isn't touched during render. const reportActionsRef = useRef(reportActions); - // eslint-disable-next-line react-hooks/refs - reportActionsRef.current = reportActions; + useEffect(() => { + reportActionsRef.current = reportActions; + }, [reportActions]); const [sourceReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${actionSourceReportID}`); const actionReportActions = reportActions?.[reportActionID] ? reportActions : sourceReportActions; const reportAction = actionReportActions?.[reportActionID]; diff --git a/tests/ui/ContextMenuOrderingTest.tsx b/tests/ui/ContextMenuOrderingTest.tsx index 9e0c50db4e31..013cebc5a4cc 100644 --- a/tests/ui/ContextMenuOrderingTest.tsx +++ b/tests/ui/ContextMenuOrderingTest.tsx @@ -1,9 +1,9 @@ import {PortalProvider} from '@gorhom/portal'; import * as NativeNavigation from '@react-navigation/native'; import {act, render} from '@testing-library/react-native'; +import type {RenderResult} from '@testing-library/react-native'; import React, {useRef} from 'react'; import Onyx from 'react-native-onyx'; -import type {ReactTestInstance} from 'react-test-renderer'; import ComposeProviders from '@components/ComposeProviders'; import DelegateNoAccessModalProvider from '@components/DelegateNoAccessModalProvider'; import HTMLEngineProvider from '@components/HTMLEngineProvider'; @@ -33,7 +33,7 @@ const REPORT_ID = 'testReport'; * — MenuItem → PressableWithSecondaryInteraction → … — all of which expose * the same `sentryLabel`). */ -function collectSentryLabels(root: ReactTestInstance): string[] { +function collectSentryLabels(root: RenderResult['root']): string[] { const matches = root.findAll((el) => { const label: unknown = el.props?.sentryLabel; return typeof label === 'string' && label.startsWith('ContextMenu-'); From 13c0844177482c33a0ed9afeb5b5dd6a8a2f9151 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 16 Apr 2026 16:30:18 -0700 Subject: [PATCH 86/88] fix(contextmenu): hide mini menu when cursor leaves without entering it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit showMiniContextMenu() set shouldKeepOpenRef=true to guard against the row→menu hover transition, but the only release path was the menu's own onHoverOut. When the cursor moved from the row straight to outside (the common case), hideMiniContextMenu() always deferred, release() never ran, and the menu stayed visible indefinitely. The stuck isVisible=true also blocked onMenuHide from firing, so PureReportActionItem's isContextMenuActive state couldn't clear, keeping the row highlighted. Replace the keep-open-on-show guard with a cancellable setTimeout: - hideMiniContextMenu() schedules the actual hide after an 80ms grace period so the menu's own onHoverIn has a chance to cancel it via keepOpen() during the row→menu transition. - showMiniContextMenu() cancels any pending hide, so rapid row-to-row hovers keep the menu visible. - keepOpen/release remain as an explicit lock for sub-interactions (overflow popover, emoji picker) that need the menu pinned regardless of hover state. Fixes three linked regressions reported on the PR: - reaction bar stays visible after the cursor leaves the row, - the row's highlighted state never clears (both instances). Made-with: Cursor --- .../ContextMenu/MiniContextMenuProvider.tsx | 94 ++++++++++++------- 1 file changed, 61 insertions(+), 33 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx b/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx index 2850cdfb72a6..ee045391c17d 100644 --- a/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniContextMenuProvider.tsx @@ -1,7 +1,14 @@ import type {ReactNode, RefObject} from 'react'; -import React, {createContext, useContext, useRef, useState} from 'react'; +import React, {createContext, useContext, useEffect, useRef, useState} from 'react'; import type {ContextMenuAnchor} from './ReportActionContextMenu'; +/** + * Grace period between a hide being requested (e.g. row mouseleave) and the menu actually hiding. + * Gives the menu's own Hoverable a window to cancel the hide when the cursor lands on it, enabling + * seamless row → menu hover transitions without the menu flickering off. + */ +const HIDE_GRACE_PERIOD_MS = 80; + type RowMeasurements = { top: number; height: number; @@ -32,7 +39,12 @@ type MiniContextMenuActions = { /** Display the mini context menu with the given parameters. */ showMiniContextMenu: (params: ShowMiniContextMenuParams) => void; - /** Hide the mini context menu immediately. No-op while `keepOpen` is active; the hide intent is deferred until `release`. */ + /** + * Schedule hiding the mini context menu after a short grace period. The hide is cancellable + * by a subsequent `showMiniContextMenu` or `keepOpen` call — this is what allows the cursor + * to transition from the hovered row onto the menu itself without the menu flickering away. + * While `keepOpen` is active the hide intent is deferred until `release` is called. + */ hideMiniContextMenu: () => void; /** @@ -68,41 +80,48 @@ type MiniContextMenuProviderProps = { function MiniContextMenuProvider({children}: MiniContextMenuProviderProps) { const [state, setState] = useState(null); + // Explicit lock for sub-interactions that must keep the menu pinned (overflow popover, + // emoji picker, right-click popover). Unrelated to the grace-period hide timer below. const shouldKeepOpenRef = useRef(false); + // Set when a hide was requested while locked; drained when `release()` is called. const pendingHideRef = useRef(false); + const hideTimeoutRef = useRef | null>(null); const onMenuHideRef = useRef<(() => void) | null>(null); const activeReportActionIDRef = useRef(undefined); const menuContainerRef = useRef(null); const [actions] = useState(() => { - const isGuarded = () => shouldKeepOpenRef.current; + const cancelScheduledHide = () => { + if (!hideTimeoutRef.current) { + return; + } + clearTimeout(hideTimeoutRef.current); + hideTimeoutRef.current = null; + }; - // Deferred to a microtask so that all event handlers in the current - // task (e.g. both mouseleave on the row AND mouseenter on the menu) - // finish and update refs before we decide whether to actually hide. const performHide = () => { - queueMicrotask(() => { - if (isGuarded()) { - pendingHideRef.current = true; - return; - } - setState((prev) => (prev ? {...prev, isVisible: false} : null)); - onMenuHideRef.current?.(); - onMenuHideRef.current = null; - activeReportActionIDRef.current = undefined; - }); + hideTimeoutRef.current = null; + setState((prev) => (prev ? {...prev, isVisible: false} : null)); + onMenuHideRef.current?.(); + onMenuHideRef.current = null; + activeReportActionIDRef.current = undefined; }; - const drainPendingHide = () => { - if (!pendingHideRef.current || isGuarded()) { + const scheduleHide = () => { + if (shouldKeepOpenRef.current) { + pendingHideRef.current = true; return; } - pendingHideRef.current = false; - performHide(); + if (hideTimeoutRef.current) { + return; + } + hideTimeoutRef.current = setTimeout(performHide, HIDE_GRACE_PERIOD_MS); }; return { showMiniContextMenu: (params: ShowMiniContextMenuParams) => { + cancelScheduledHide(); + pendingHideRef.current = false; const isSameRow = params.reportActionID === activeReportActionIDRef.current; if (!isSameRow) { onMenuHideRef.current?.(); @@ -110,38 +129,47 @@ function MiniContextMenuProvider({children}: MiniContextMenuProviderProps) { activeReportActionIDRef.current = params.reportActionID; const {onMenuHide, ...stateParams} = params; onMenuHideRef.current = onMenuHide ?? null; - pendingHideRef.current = false; - shouldKeepOpenRef.current = true; setState({...stateParams, isVisible: true}); }, hideMiniContextMenu: () => { - if (isGuarded()) { - pendingHideRef.current = true; - return; - } - performHide(); + scheduleHide(); }, hideMiniContextMenuWithoutNotification: () => { + cancelScheduledHide(); shouldKeepOpenRef.current = false; pendingHideRef.current = false; - queueMicrotask(() => { - setState((prev) => (prev ? {...prev, isVisible: false} : null)); - onMenuHideRef.current = null; - activeReportActionIDRef.current = undefined; - }); + setState((prev) => (prev ? {...prev, isVisible: false} : null)); + onMenuHideRef.current = null; + activeReportActionIDRef.current = undefined; }, keepOpen: () => { shouldKeepOpenRef.current = true; + cancelScheduledHide(); pendingHideRef.current = false; }, release: () => { shouldKeepOpenRef.current = false; - drainPendingHide(); + if (!pendingHideRef.current) { + return; + } + pendingHideRef.current = false; + performHide(); }, menuContainerRef, }; }); + useEffect( + () => () => { + if (!hideTimeoutRef.current) { + return; + } + clearTimeout(hideTimeoutRef.current); + hideTimeoutRef.current = null; + }, + [], + ); + return ( {children} From db186da7749e683279ac9ed011ed00c62f96736d Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 16 Apr 2026 16:31:47 -0700 Subject: [PATCH 87/88] fix(contextmenu): ignore scrolls originating inside the anchored row MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The global scroll listener used capture-phase window subscription and hid the menu for every scroll event. Horizontal-scrolling descendants of the row — notably the expense carousel inside a report preview — would bubble scroll events up and kick off an unwanted hide mid-swipe. On main this wasn't an issue because each row rendered its own menu inside its subtree. Skip scrolls whose target is contained by the anchor element. Only ancestor scrolls (the chat list itself) move the row, so only those need to dismiss the menu. Made-with: Cursor --- .../MiniReportActionContextMenu/index.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index 5029836558d5..cf929b812751 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -146,7 +146,15 @@ function MiniReportActionContextMenu() { if (!isVisible) { return; } - const onScroll = () => { + const onScroll = (event: Event) => { + // Ignore scrolls that originate from inside the anchored row itself, e.g. the + // horizontal carousel on a report preview. Those don't move the row, so the + // menu is still correctly positioned and shouldn't flicker away. + const anchorEl = anchor?.current as unknown as HTMLElement | null; + const target = event.target as Node | null; + if (anchorEl && target && anchorEl.contains(target)) { + return; + } release(); hideMiniContextMenu(); }; @@ -154,7 +162,10 @@ function MiniReportActionContextMenu() { return () => { window.removeEventListener('scroll', onScroll, true); }; - }, [isVisible, release, hideMiniContextMenu]); + // Depending on the ref object rather than anchor?.current avoids accessing + // refs during render (required for React Compiler compliance); the ref identity is stable. + // eslint-disable-next-line rulesdir/prefer-narrow-hook-dependencies + }, [isVisible, release, hideMiniContextMenu, anchor]); useEffect(() => { const el = localMenuContainerRef.current as unknown as HTMLElement | null; From 1cad108b294948f50ea51d695a9c73674712ceee Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 16 Apr 2026 17:18:28 -0700 Subject: [PATCH 88/88] fix(a11y): restore Tab/Shift+Tab focus bridge with Portal-mounted menu The listener-attach useEffect depended on a ref populated by a callback ref on the menu's container View. @gorhom/portal mounts that View asynchronously via a state update in the PortalHost, so by the time our effect ran, localMenuContainerRef.current was still null and the blur/ Shift+Tab listeners never bound. Tab still worked because the handler lives on the (non-Portal) report action row, but Shift+Tab fell back to the browser default and focused the previous element in DOM order instead of returning to the row. Track the container as state (in addition to a ref for cases where a stale closure would be fine). Setting state in the callback ref triggers a re-render after the Portal commit, which re-runs the effect with the now-attached element so the listeners bind correctly. Made-with: Cursor --- .../MiniReportActionContextMenu/index.tsx | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx index cf929b812751..31b6dfc0bd62 100644 --- a/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/inbox/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -71,12 +71,17 @@ function MiniReportActionContextMenu() { const overflowIcons = useMemoizedLazyExpensifyIcons(['ThreeDots'] as const); const threeDotRef = useRef(null); const overlayRef = useRef(null); - const localMenuContainerRef = useRef(null); + const localMenuContainerRef = useRef(null); + // Tracked as state (not only a ref) because the Portal attaches this View asynchronously; + // state causes the dependent effect to re-run once React commits the Portal content. + const [menuContainerEl, setMenuContainerEl] = useState(null); const menuContainerCallbackRef = useCallback( (node: View | null) => { - localMenuContainerRef.current = node; + const el = node as unknown as HTMLElement | null; + localMenuContainerRef.current = el; + setMenuContainerEl(el); // eslint-disable-next-line no-param-reassign - menuContainerRef.current = node as unknown as HTMLElement | null; + menuContainerRef.current = el; }, [menuContainerRef], ); @@ -107,13 +112,12 @@ function MiniReportActionContextMenu() { : null; useEffect(() => { - const el = localMenuContainerRef.current as unknown as HTMLElement | null; - if (!el) { + if (!menuContainerEl) { return; } const onBlurCapture = (e: FocusEvent) => { - if (e.relatedTarget && el.contains(e.relatedTarget as Node)) { + if (e.relatedTarget && menuContainerEl.contains(e.relatedTarget as Node)) { return; } hideMiniContextMenu(); @@ -131,16 +135,16 @@ function MiniReportActionContextMenu() { anchorEl.focus(); }; - el.addEventListener('blur', onBlurCapture, true); - el.addEventListener('keydown', onKeyDown); + menuContainerEl.addEventListener('blur', onBlurCapture, true); + menuContainerEl.addEventListener('keydown', onKeyDown); return () => { - el.removeEventListener('blur', onBlurCapture, true); - el.removeEventListener('keydown', onKeyDown); + menuContainerEl.removeEventListener('blur', onBlurCapture, true); + menuContainerEl.removeEventListener('keydown', onKeyDown); }; // Depending on the ref object rather than anchor?.current avoids accessing // refs during render (required for React Compiler compliance); the ref identity is stable. // eslint-disable-next-line rulesdir/prefer-narrow-hook-dependencies - }, [hideMiniContextMenu, anchor]); + }, [menuContainerEl, hideMiniContextMenu, anchor]); useEffect(() => { if (!isVisible) { @@ -168,12 +172,12 @@ function MiniReportActionContextMenu() { }, [isVisible, release, hideMiniContextMenu, anchor]); useEffect(() => { - const el = localMenuContainerRef.current as unknown as HTMLElement | null; + const el = localMenuContainerRef.current; if (!el) { return; } el.dataset.selectionScraperHiddenElement = String(isVisible); - }, [isVisible]); + }, [menuContainerEl, isVisible]); const { report,