From 10dd583f74e84527bb59d7e844f8568fd9617a11 Mon Sep 17 00:00:00 2001 From: someone-here Date: Fri, 19 Jan 2024 00:24:00 +0530 Subject: [PATCH 1/4] Fix Context menu from keyboard --- src/libs/calculateAnchorPosition.ts | 6 +-- .../PopoverReportActionContextMenu.tsx | 41 +++++++++++-------- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/src/libs/calculateAnchorPosition.ts b/src/libs/calculateAnchorPosition.ts index 66966b7b504c..2ff0c178e6e0 100644 --- a/src/libs/calculateAnchorPosition.ts +++ b/src/libs/calculateAnchorPosition.ts @@ -1,5 +1,5 @@ -/* eslint-disable no-console */ -import type {View} from 'react-native'; +/* eslint-disable no-restricted-imports */ +import type {Text as RNText, View} from 'react-native'; import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import type {AnchorPosition} from '@src/styles'; @@ -13,7 +13,7 @@ type AnchorOrigin = { /** * Gets the x,y position of the passed in component for the purpose of anchoring another component to it. */ -export default function calculateAnchorPosition(anchorComponent: View, anchorOrigin?: AnchorOrigin): Promise { +export default function calculateAnchorPosition(anchorComponent: View | RNText, anchorOrigin?: AnchorOrigin): Promise { return new Promise((resolve) => { if (!anchorComponent) { return resolve({horizontal: 0, vertical: 0}); diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx index 46b783bca3f9..476efa591177 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -1,11 +1,14 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; -import type {EmitterSubscription, GestureResponderEvent, NativeTouchEvent, View} from 'react-native'; + +/* eslint-disable no-restricted-imports */ +import type {EmitterSubscription, GestureResponderEvent, NativeTouchEvent, Text as RNText, View} from 'react-native'; import {Dimensions} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import ConfirmModal from '@components/ConfirmModal'; import PopoverWithMeasuredContent from '@components/PopoverWithMeasuredContent'; import useLocalize from '@hooks/useLocalize'; +import calculateAnchorPosition from '@libs/calculateAnchorPosition'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as IOU from '@userActions/IOU'; import * as Report from '@userActions/Report'; @@ -14,10 +17,6 @@ import type {ReportAction} from '@src/types/onyx'; import BaseReportActionContextMenu from './BaseReportActionContextMenu'; import type {ContextMenuType, ReportActionContextMenu} from './ReportActionContextMenu'; -type ContextMenuAnchorCallback = (x: number, y: number) => void; - -type ContextMenuAnchor = {measureInWindow: (callback: ContextMenuAnchorCallback) => void}; - type Location = { x: number; y: number; @@ -66,7 +65,7 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef(null); const anchorRef = useRef(null); const dimensionsEventListener = useRef(null); - const contextMenuAnchorRef = useRef(null); + const contextMenuAnchorRef = useRef(null); const contextMenuTargetNode = useRef(null); const onPopoverShow = useRef(() => {}); @@ -171,16 +170,26 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef { - popoverAnchorPosition.current = { - horizontal: pageX - x, - vertical: pageY - y, - }; - - popoverAnchorPosition.current = { - horizontal: pageX, - vertical: pageY, - }; + new Promise((resolve) => { + if (!pageX && !pageY && contextMenuAnchorRef.current) { + calculateAnchorPosition(contextMenuAnchorRef.current).then((position) => { + popoverAnchorPosition.current = position; + resolve(); + }); + } else { + getContextMenuMeasuredLocation().then(({x, y}) => { + cursorRelativePosition.current = { + horizontal: pageX - x, + vertical: pageY - y, + }; + popoverAnchorPosition.current = { + horizontal: pageX, + vertical: pageY, + }; + resolve(); + }); + } + }).then(() => { typeRef.current = type; reportIDRef.current = reportID ?? '0'; reportActionIDRef.current = reportActionID ?? '0'; From 5dfacd83687526f6000f5b0f095d48e83595791f Mon Sep 17 00:00:00 2001 From: someone-here Date: Sat, 20 Jan 2024 21:08:22 +0530 Subject: [PATCH 2/4] Show only left-out options in overflow menu --- .../BaseReportActionContextMenu.tsx | 58 +++++++++++++++---- .../report/ContextMenu/ContextMenuActions.tsx | 24 ++------ .../PopoverReportActionContextMenu.tsx | 5 ++ .../ContextMenu/ReportActionContextMenu.ts | 4 ++ 4 files changed, 61 insertions(+), 30 deletions(-) diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx index 3eecb74a048a..213d94f51f81 100755 --- a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx @@ -2,6 +2,8 @@ import lodashIsEqual from 'lodash/isEqual'; import type {MutableRefObject, RefObject} from 'react'; import React, {memo, useMemo, 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'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import type {ContextMenuItemHandle} from '@components/ContextMenuItem'; @@ -12,15 +14,16 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import * as ReportUtils from '@libs/ReportUtils'; import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Beta, ReportAction, ReportActions} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import type {ContextMenuActionPayload} from './ContextMenuActions'; +import type {ContextMenuAction, ContextMenuActionPayload} from './ContextMenuActions'; import ContextMenuActions from './ContextMenuActions'; import type {ContextMenuType} from './ReportActionContextMenu'; -import {hideContextMenu} from './ReportActionContextMenu'; +import {hideContextMenu, showContextMenu} from './ReportActionContextMenu'; type BaseReportActionContextMenuOnyxProps = { /** Beta features list */ @@ -78,7 +81,11 @@ type BaseReportActionContextMenuProps = BaseReportActionContextMenuOnyxProps & { /** Content Ref */ contentRef?: RefObject; + /** Function to check if context menu is active */ checkIfContextMenuActive?: () => void; + + /** List of disabled actions */ + disabledActions?: ContextMenuAction[]; }; type MenuItemRefs = Record; @@ -100,6 +107,7 @@ function BaseReportActionContextMenu({ betas, reportActions, checkIfContextMenuActive, + disabledActions = [], }: BaseReportActionContextMenuProps) { const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); @@ -117,13 +125,22 @@ function BaseReportActionContextMenu({ }, [reportActions, reportActionID]); const shouldEnableArrowNavigation = !isMini && (isVisible || shouldKeepOpen); - let filteredContextMenuActions = ContextMenuActions.filter((contextAction) => - contextAction.shouldShow(type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat, !!isOffline, isMini), + let filteredContextMenuActions = ContextMenuActions.filter( + (contextAction) => + !disabledActions.includes(contextAction) && + contextAction.shouldShow(type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat, !!isOffline, isMini), ); - filteredContextMenuActions = - isMini && filteredContextMenuActions.length > CONST.MINI_CONTEXT_MENU_MAX_ITEMS - ? ([...filteredContextMenuActions.slice(0, CONST.MINI_CONTEXT_MENU_MAX_ITEMS - 1), filteredContextMenuActions.at(-1)] as typeof filteredContextMenuActions) - : filteredContextMenuActions; + + 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) => @@ -172,6 +189,28 @@ function BaseReportActionContextMenu({ {isActive: shouldEnableArrowNavigation}, ); + const openOverflowMenu = (event: GestureResponderEvent | MouseEvent) => { + const originalReportID = ReportUtils.getOriginalReportID(reportID, reportAction); + const originalReport = ReportUtils.getReport(originalReportID); + showContextMenu( + CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, + event, + selection, + anchor?.current as ViewType | RNText | null, + reportID, + reportAction?.reportActionID, + originalReportID, + draftMessage, + checkIfContextMenuActive, + checkIfContextMenuActive, + ReportUtils.isArchivedRoom(originalReport), + ReportUtils.chatIncludesChronos(originalReport), + undefined, + undefined, + filteredContextMenuActions, + ); + }; + return ( (isVisible || shouldKeepOpen) && ( setShouldKeepOpen(false), openContextMenu: () => setShouldKeepOpen(true), interceptAnonymousUser, - anchor, - checkIfContextMenuActive, + openOverflowMenu, }; if ('renderContent' in contextAction) { diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index ea25a00ee1d3..1e8f9dde64d6 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -70,8 +70,7 @@ type ContextMenuActionPayload = { close: () => void; openContextMenu: () => void; interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; - anchor?: MutableRefObject; - checkIfContextMenuActive?: () => void; + openOverflowMenu: (event: GestureResponderEvent | MouseEvent) => void; event?: GestureResponderEvent | MouseEvent | KeyboardEvent; }; @@ -502,27 +501,12 @@ const ContextMenuActions: ContextMenuAction[] = [ textTranslateKey: 'reportActionContextMenu.menu', icon: Expensicons.ThreeDots, shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat, isOffline, isMini) => isMini, - onPress: (closePopover, {reportAction, reportID, event, anchor, selection, draftMessage, checkIfContextMenuActive}) => { - const originalReportID = ReportUtils.getOriginalReportID(reportID, reportAction); - const originalReport = ReportUtils.getReport(originalReportID); - showContextMenu( - CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, - event as GestureResponderEvent | MouseEvent, - selection, - anchor?.current as View | RNText | null, - reportID, - reportAction.reportActionID, - originalReportID, - draftMessage, - checkIfContextMenuActive, - checkIfContextMenuActive, - ReportUtils.isArchivedRoom(originalReport), - ReportUtils.chatIncludesChronos(originalReport), - ); + onPress: (closePopover, {openOverflowMenu, event}) => { + openOverflowMenu(event as GestureResponderEvent | MouseEvent); }, getDescription: () => {}, }, ]; export default ContextMenuActions; -export type {ContextMenuActionPayload}; +export type {ContextMenuActionPayload, ContextMenuAction}; diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx index 476efa591177..42f27ecdfd59 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -15,6 +15,7 @@ import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import type {ReportAction} from '@src/types/onyx'; import BaseReportActionContextMenu from './BaseReportActionContextMenu'; +import type {ContextMenuAction} from './ContextMenuActions'; import type {ContextMenuType, ReportActionContextMenu} from './ReportActionContextMenu'; type Location = { @@ -61,6 +62,7 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef([]); const contentRef = useRef(null); const anchorRef = useRef(null); @@ -160,6 +162,7 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef { const {pageX = 0, pageY = 0} = extractPointerEvent(event); contextMenuAnchorRef.current = contextMenuAnchor; @@ -190,6 +193,7 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef { + setDisabledActions(disabledActions); typeRef.current = type; reportIDRef.current = reportID ?? '0'; reportActionIDRef.current = reportActionID ?? '0'; @@ -319,6 +323,7 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef void; @@ -30,6 +31,7 @@ type ShowContextMenu = ( isChronosReport?: boolean, isPinnedChat?: boolean, isUnreadChat?: boolean, + disabledOptions?: ContextMenuAction[], ) => void; type ReportActionContextMenu = { @@ -108,6 +110,7 @@ function showContextMenu( isChronosReport = false, isPinnedChat = false, isUnreadChat = false, + disabledActions: ContextMenuAction[] = [], ) { if (!contextMenuRef.current) { return; @@ -134,6 +137,7 @@ function showContextMenu( isChronosReport, isPinnedChat, isUnreadChat, + disabledActions, ); } From cbc19a1ab83129ce5dc351f8d5dfb3dddcd9050e Mon Sep 17 00:00:00 2001 From: someone-here Date: Sat, 20 Jan 2024 22:01:56 +0530 Subject: [PATCH 3/4] Fix lint --- src/pages/home/report/ContextMenu/ContextMenuActions.tsx | 5 ++--- .../report/ContextMenu/PopoverReportActionContextMenu.tsx | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index 1e8f9dde64d6..dde99f746d9f 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -1,8 +1,7 @@ import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import type {MutableRefObject} from 'react'; import React from 'react'; -// eslint-disable-next-line no-restricted-imports -import type {GestureResponderEvent, Text as RNText, View} from 'react-native'; +import type {GestureResponderEvent} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import type {Emoji} from '@assets/emojis/types'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -29,7 +28,7 @@ import type {TranslationPaths} from '@src/languages/types'; import ROUTES from '@src/ROUTES'; import type {Beta, ReportAction, ReportActionReactions} from '@src/types/onyx'; import type IconAsset from '@src/types/utils/IconAsset'; -import {hideContextMenu, showContextMenu, showDeleteModal} from './ReportActionContextMenu'; +import {hideContextMenu, showDeleteModal} from './ReportActionContextMenu'; /** Gets the HTML version of the message in an action */ function getActionText(reportAction: OnyxEntry): string { diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx index 42f27ecdfd59..b28374fae04a 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -162,7 +162,7 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef { const {pageX = 0, pageY = 0} = extractPointerEvent(event); contextMenuAnchorRef.current = contextMenuAnchor; @@ -193,7 +193,7 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef { - setDisabledActions(disabledActions); + setDisabledActions(disabledOptions); typeRef.current = type; reportIDRef.current = reportID ?? '0'; reportActionIDRef.current = reportActionID ?? '0'; From 51a4c6479a1ba2c6d20eaf9e652e4d2159b092ba Mon Sep 17 00:00:00 2001 From: someone-here Date: Mon, 22 Jan 2024 23:15:21 +0530 Subject: [PATCH 4/4] Change icon for unread --- src/pages/home/report/ContextMenu/ContextMenuActions.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index dde99f746d9f..95300d4ed3f4 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -238,7 +238,7 @@ const ContextMenuActions: ContextMenuAction[] = [ { isAnonymousAction: false, textTranslateKey: 'reportActionContextMenu.markAsUnread', - icon: Expensicons.Mail, + icon: Expensicons.ChatBubbleUnread, successIcon: Expensicons.Checkmark, shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport, reportID, isPinnedChat, isUnreadChat) => type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION || (type === CONST.CONTEXT_MENU_TYPES.REPORT && !isUnreadChat),