From 4081242e7db0f95d4cc367a7524194fce937fae3 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Mon, 11 Dec 2023 18:19:39 +0100 Subject: [PATCH 01/62] [TS migration] Migrate 'ShowContextMenuContext.js' component to TypeScript --- ...MenuContext.js => ShowContextMenuContext.tsx} | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) rename src/components/{ShowContextMenuContext.js => ShowContextMenuContext.tsx} (67%) diff --git a/src/components/ShowContextMenuContext.js b/src/components/ShowContextMenuContext.tsx similarity index 67% rename from src/components/ShowContextMenuContext.js rename to src/components/ShowContextMenuContext.tsx index 6248478e5fea..b2c1835d59e7 100644 --- a/src/components/ShowContextMenuContext.js +++ b/src/components/ShowContextMenuContext.tsx @@ -3,6 +3,7 @@ import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as ReportUtils from '@libs/ReportUtils'; import * as ContextMenuActions from '@pages/home/report/ContextMenu/ContextMenuActions'; import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu'; +import ReportAction from '@src/types/onyx/ReportAction'; const ShowContextMenuContext = React.createContext({ anchor: null, @@ -16,17 +17,18 @@ ShowContextMenuContext.displayName = 'ShowContextMenuContext'; /** * Show the report action context menu. * - * @param {Object} event - Press event object - * @param {Element} anchor - Context menu anchor - * @param {String} reportID - Active Report ID - * @param {Object} action - ReportAction for ContextMenu - * @param {Function} checkIfContextMenuActive Callback to update context menu active state - * @param {Boolean} [isArchivedRoom=false] - Is the report an archived room + * @param event - Press event object + * @param anchor - Context menu anchor + * @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, anchor, reportID, action, checkIfContextMenuActive, isArchivedRoom = false) { +function showContextMenuForReport(event: Event, anchor: HTMLElement, reportID: string, action: ReportAction, checkIfContextMenuActive: () => void, isArchivedRoom = false) { if (!DeviceCapabilities.canUseTouchScreen()) { return; } + ReportActionContextMenu.showContextMenu( ContextMenuActions.CONTEXT_MENU_TYPES.REPORT_ACTION, event, From 7b67f02ec496bce8075bb50f5bf703fc5daa12ac Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 28 Dec 2023 17:23:47 +0100 Subject: [PATCH 02/62] Change ContextMenu extenstions --- .../BaseReportActionContextMenu.js | 204 ----------------- .../BaseReportActionContextMenu.tsx | 208 ++++++++++++++++++ ...tMenuActions.js => ContextMenuActions.tsx} | 9 - .../{index.native.js => index.native.tsx} | 0 .../{index.js => index.tsx} | 0 ....js => PopoverReportActionContextMenu.tsx} | 0 ...extMenu.js => ReportActionContextMenu.tsx} | 0 ...enericReportActionContextMenuPropTypes.ts} | 0 src/pages/home/report/ContextMenu/types.ts | 33 +++ 9 files changed, 241 insertions(+), 213 deletions(-) delete mode 100755 src/pages/home/report/ContextMenu/BaseReportActionContextMenu.js create mode 100755 src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx rename src/pages/home/report/ContextMenu/{ContextMenuActions.js => ContextMenuActions.tsx} (99%) rename src/pages/home/report/ContextMenu/MiniReportActionContextMenu/{index.native.js => index.native.tsx} (100%) rename src/pages/home/report/ContextMenu/MiniReportActionContextMenu/{index.js => index.tsx} (100%) rename src/pages/home/report/ContextMenu/{PopoverReportActionContextMenu.js => PopoverReportActionContextMenu.tsx} (100%) rename src/pages/home/report/ContextMenu/{ReportActionContextMenu.js => ReportActionContextMenu.tsx} (100%) rename src/pages/home/report/ContextMenu/{genericReportActionContextMenuPropTypes.js => genericReportActionContextMenuPropTypes.ts} (100%) create mode 100644 src/pages/home/report/ContextMenu/types.ts diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.js b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.js deleted file mode 100755 index 33adfa4b35f9..000000000000 --- a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.js +++ /dev/null @@ -1,204 +0,0 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; -import React, {memo, useMemo, useRef, useState} from 'react'; -import {InteractionManager, View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import ContextMenuItem from '@components/ContextMenuItem'; -import {withBetas} from '@components/OnyxProvider'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; -import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; -import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; -import useNetwork from '@hooks/useNetwork'; -import compose from '@libs/compose'; -import useStyleUtils from '@styles/useStyleUtils'; -import * as Session from '@userActions/Session'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ContextMenuActions, {CONTEXT_MENU_TYPES} from './ContextMenuActions'; -import {defaultProps as GenericReportActionContextMenuDefaultProps, propTypes as genericReportActionContextMenuPropTypes} from './genericReportActionContextMenuPropTypes'; -import {hideContextMenu} from './ReportActionContextMenu'; - -const propTypes = { - /** String representing the context menu type [LINK, REPORT_ACTION] which controls context menu choices */ - type: PropTypes.string, - - /** Target node which is the target of ContentMenu */ - anchor: PropTypes.oneOfType([PropTypes.node, PropTypes.object]), - - /** Flag to check if the chat participant is Chronos */ - isChronosReport: PropTypes.bool, - - /** Whether the provided report is an archived room */ - isArchivedRoom: PropTypes.bool, - - contentRef: PropTypes.oneOfType([PropTypes.node, PropTypes.object, PropTypes.func]), - - ...genericReportActionContextMenuPropTypes, - ...withLocalizePropTypes, - ...windowDimensionsPropTypes, -}; - -const defaultProps = { - type: CONTEXT_MENU_TYPES.REPORT_ACTION, - anchor: null, - contentRef: null, - isChronosReport: false, - isArchivedRoom: false, - ...GenericReportActionContextMenuDefaultProps, -}; -function BaseReportActionContextMenu(props) { - const StyleUtils = useStyleUtils(); - const menuItemRefs = useRef({}); - const [shouldKeepOpen, setShouldKeepOpen] = useState(false); - const wrapperStyle = StyleUtils.getReportActionContextMenuStyles(props.isMini, props.isSmallScreenWidth); - const {isOffline} = useNetwork(); - - const reportAction = useMemo(() => { - if (_.isEmpty(props.reportActions) || props.reportActionID === '0') { - return {}; - } - return props.reportActions[props.reportActionID] || {}; - }, [props.reportActions, props.reportActionID]); - - const shouldShowFilter = (contextAction) => - contextAction.shouldShow( - props.type, - reportAction, - props.isArchivedRoom, - props.betas, - props.anchor, - props.isChronosReport, - props.reportID, - props.isPinnedChat, - props.isUnreadChat, - isOffline, - ); - - const shouldEnableArrowNavigation = !props.isMini && (props.isVisible || shouldKeepOpen); - const filteredContextMenuActions = _.filter(ContextMenuActions, shouldShowFilter); - - // Context menu actions that are not rendered as menu items are excluded from arrow navigation - const nonMenuItemActionIndexes = _.map(filteredContextMenuActions, (contextAction, index) => (_.isFunction(contextAction.renderContent) ? index : undefined)); - const disabledIndexes = _.filter(nonMenuItemActionIndexes, (index) => !_.isUndefined(index)); - - 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. - * - * @param {Function} callback - * @param {Boolean} isAnonymousAction - */ - const interceptAnonymousUser = (callback, isAnonymousAction = false) => { - if (Session.isAnonymousUser() && !isAnonymousAction) { - hideContextMenu(false); - - InteractionManager.runAfterInteractions(() => { - Session.signOutAndRedirectToSignIn(); - }); - } else { - callback(); - } - }; - - useKeyboardShortcut( - CONST.KEYBOARD_SHORTCUTS.ENTER, - (event) => { - if (!menuItemRefs.current[focusedIndex]) { - return; - } - - // Ensures the event does not cause side-effects beyond the context menu, e.g. when an outside element is focused - if (event) { - event.stopPropagation(); - } - - menuItemRefs.current[focusedIndex].triggerPressAndUpdateSuccess(); - setFocusedIndex(-1); - }, - {isActive: shouldEnableArrowNavigation}, - ); - - return ( - (props.isVisible || shouldKeepOpen) && ( - - {_.map(filteredContextMenuActions, (contextAction, index) => { - const closePopup = !props.isMini; - const payload = { - reportAction, - reportID: props.reportID, - draftMessage: props.draftMessage, - selection: props.selection, - close: () => setShouldKeepOpen(false), - openContextMenu: () => setShouldKeepOpen(true), - interceptAnonymousUser, - }; - - if (contextAction.renderContent) { - // make sure that renderContent isn't mixed with unsupported props - if (__DEV__ && (contextAction.text != null || contextAction.icon != null)) { - throw new Error('Dev error: renderContent() and text/icon cannot be used together.'); - } - - return contextAction.renderContent(closePopup, payload); - } - - return ( - { - menuItemRefs.current[index] = ref; - }} - icon={contextAction.icon} - text={props.translate(contextAction.textTranslateKey, {action: reportAction})} - successIcon={contextAction.successIcon} - successText={contextAction.successTextTranslateKey ? props.translate(contextAction.successTextTranslateKey) : undefined} - isMini={props.isMini} - key={contextAction.textTranslateKey} - onPress={() => interceptAnonymousUser(() => contextAction.onPress(closePopup, payload), contextAction.isAnonymousAction)} - description={contextAction.getDescription(props.selection, props.isSmallScreenWidth)} - isAnonymousAction={contextAction.isAnonymousAction} - isFocused={focusedIndex === index} - /> - ); - })} - - ) - ); -} - -BaseReportActionContextMenu.propTypes = propTypes; -BaseReportActionContextMenu.defaultProps = defaultProps; - -export default compose( - withLocalize, - withBetas(), - withWindowDimensions, - withOnyx({ - reportActions: { - key: ({originalReportID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${originalReportID}`, - canEvict: false, - }, - }), -)( - memo(BaseReportActionContextMenu, (prevProps, nextProps) => { - const prevReportAction = lodashGet(prevProps.reportActions, prevProps.reportActionID, ''); - const nextReportAction = lodashGet(nextProps.reportActions, nextProps.reportActionID, ''); - - // We only want to re-render when the report action that is attached to is changed - if (prevReportAction !== nextReportAction) { - return false; - } - return _.isEqual(_.omit(prevProps, 'reportActions'), _.omit(nextProps, 'reportActions')); - }), -); diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx new file mode 100755 index 000000000000..3639c9349549 --- /dev/null +++ b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx @@ -0,0 +1,208 @@ +import lodashIsEqual from 'lodash/isEqual'; +import React, {memo, useMemo, useRef, useState} from 'react'; +import {InteractionManager, View} from 'react-native'; +import {OnyxEntry, withOnyx} from 'react-native-onyx'; +import {ValueOf} from 'type-fest'; +import ContextMenuItem from '@components/ContextMenuItem'; +import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; +import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import useStyleUtils from '@styles/useStyleUtils'; +import * as Session from '@userActions/Session'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import {Beta, ReportActions} from '@src/types/onyx'; +import ContextMenuActions from './ContextMenuActions'; +import {hideContextMenu} from './ReportActionContextMenu'; +import {CONTEXT_MENU_TYPES, GenericReportActionContextMenuProps} from './types'; + +type BaseReportActionContextMenuOnyxProps = { + /** Beta features list */ + betas: OnyxEntry; + + /** All of the actions of the report */ + reportActions: OnyxEntry; +}; + +type BaseReportActionContextMenuProps = GenericReportActionContextMenuProps & + BaseReportActionContextMenuOnyxProps & { + /** String representing the context menu type [LINK, REPORT_ACTION] which controls context menu choices */ + type?: ValueOf; + + /** Target node which is the target of ContentMenu */ + anchor: any; + + /** 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; + + /** Content Ref */ + contentRef: any; + }; + +function BaseReportActionContextMenu({ + type = CONTEXT_MENU_TYPES.REPORT_ACTION, + anchor = null, + contentRef = null, + isChronosReport = false, + isArchivedRoom = false, + isMini = false, + isVisible = false, + isPinnedChat = false, + isUnreadChat = false, + selection = '', + draftMessage = '', + reportActionID, + reportID, + betas, + reportActions, +}: BaseReportActionContextMenuProps) { + const StyleUtils = useStyleUtils(); + const {translate} = useLocalize(); + const {isSmallScreenWidth} = useWindowDimensions(); + const menuItemRefs = useRef void}>>({}); + const [shouldKeepOpen, setShouldKeepOpen] = useState(false); + const wrapperStyle = StyleUtils.getReportActionContextMenuStyles(isMini, isSmallScreenWidth); + const {isOffline} = useNetwork(); + + const reportAction = useMemo(() => { + if (_.isEmpty(reportActions) || reportActionID === '0') { + return {}; + } + return reportActions[reportActionID] || {}; + }, [reportActions, reportActionID]); + + const shouldEnableArrowNavigation = !isMini && (isVisible || shouldKeepOpen); + const filteredContextMenuActions = ContextMenuActions.filter((contextAction) => + contextAction.shouldShow(type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat, isOffline), + ); + + // Context menu actions that are not rendered as menu items are excluded from arrow navigation + const nonMenuItemActionIndexes = filteredContextMenuActions.map((contextAction, index) => (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 (Session.isAnonymousUser() && !isAnonymousAction) { + hideContextMenu(false); + + InteractionManager.runAfterInteractions(() => { + Session.signOutAndRedirectToSignIn(); + }); + } else { + callback(); + } + }; + + useKeyboardShortcut( + CONST.KEYBOARD_SHORTCUTS.ENTER, + (event) => { + if (!menuItemRefs.current[focusedIndex]) { + return; + } + + // Ensures the event does not cause side-effects beyond the context menu, e.g. when an outside element is focused + if (event) { + event.stopPropagation(); + } + + menuItemRefs.current[focusedIndex].triggerPressAndUpdateSuccess(); + setFocusedIndex(-1); + }, + {isActive: shouldEnableArrowNavigation}, + ); + + return ( + (isVisible || shouldKeepOpen) && ( + + {filteredContextMenuActions.map((contextAction, index) => { + const closePopup = !isMini; + const payload = { + reportAction, + reportID, + draftMessage, + selection, + close: () => setShouldKeepOpen(false), + openContextMenu: () => setShouldKeepOpen(true), + interceptAnonymousUser, + }; + + if (contextAction.renderContent) { + // make sure that renderContent isn't mixed with unsupported props + if (__DEV__ && (contextAction.text != null || contextAction.icon != null)) { + throw new Error('Dev error: renderContent() and text/icon cannot be used together.'); + } + + return contextAction.renderContent(closePopup, payload); + } + + return ( + { + menuItemRefs.current[index] = ref; + }} + icon={contextAction.icon} + text={translate(contextAction.textTranslateKey, {action: reportAction})} + successIcon={contextAction.successIcon} + successText={contextAction.successTextTranslateKey ? translate(contextAction.successTextTranslateKey) : undefined} + isMini={isMini} + key={contextAction.textTranslateKey} + onPress={() => interceptAnonymousUser(() => contextAction.onPress(closePopup, payload), contextAction.isAnonymousAction)} + description={contextAction.getDescription(selection, isSmallScreenWidth)} + isAnonymousAction={contextAction.isAnonymousAction} + isFocused={focusedIndex === index} + /> + ); + })} + + ) + ); +} + +export default withOnyx({ + betas: { + key: ONYXKEYS.BETAS, + }, + reportActions: { + key: ({originalReportID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${originalReportID}`, + canEvict: false, + }, +})( + memo(BaseReportActionContextMenu, (prevProps, nextProps) => { + const {reportActions: prevReportActions, ...prevPropsWithoutReportActions} = prevProps; + const {reportActions: nextReportActions, ...nextPropsWithoutReportActions} = nextProps; + + const prevReportAction = prevReportActions?.[prevProps.reportActionID] ?? ''; + const nextReportAction = nextReportActions?.[nextProps.reportActionID] ?? ''; + + // We only want to re-render when the report action that is attached to is changed + if (prevReportAction !== nextReportAction) { + return false; + } + + return lodashIsEqual(prevPropsWithoutReportActions, nextPropsWithoutReportActions); + }), +); diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx similarity index 99% rename from src/pages/home/report/ContextMenu/ContextMenuActions.js rename to src/pages/home/report/ContextMenu/ContextMenuActions.tsx index 2d9faa574ebb..dbd63516c525 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.js +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -34,13 +34,6 @@ function getActionText(reportAction) { return lodashGet(message, 'html', ''); } -const CONTEXT_MENU_TYPES = { - LINK: 'LINK', - REPORT_ACTION: 'REPORT_ACTION', - EMAIL: 'EMAIL', - REPORT: 'REPORT', -}; - // A list of all the context actions in this menu. export default [ { @@ -468,5 +461,3 @@ export default [ getDescription: () => {}, }, ]; - -export {CONTEXT_MENU_TYPES}; diff --git a/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.native.js b/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.native.tsx similarity index 100% rename from src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.native.js rename to src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.native.tsx diff --git a/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.js b/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.tsx similarity index 100% rename from src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.js rename to src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.tsx diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx similarity index 100% rename from src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js rename to src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx diff --git a/src/pages/home/report/ContextMenu/ReportActionContextMenu.js b/src/pages/home/report/ContextMenu/ReportActionContextMenu.tsx similarity index 100% rename from src/pages/home/report/ContextMenu/ReportActionContextMenu.js rename to src/pages/home/report/ContextMenu/ReportActionContextMenu.tsx diff --git a/src/pages/home/report/ContextMenu/genericReportActionContextMenuPropTypes.js b/src/pages/home/report/ContextMenu/genericReportActionContextMenuPropTypes.ts similarity index 100% rename from src/pages/home/report/ContextMenu/genericReportActionContextMenuPropTypes.js rename to src/pages/home/report/ContextMenu/genericReportActionContextMenuPropTypes.ts diff --git a/src/pages/home/report/ContextMenu/types.ts b/src/pages/home/report/ContextMenu/types.ts new file mode 100644 index 000000000000..051e7635bc80 --- /dev/null +++ b/src/pages/home/report/ContextMenu/types.ts @@ -0,0 +1,33 @@ +const CONTEXT_MENU_TYPES = { + LINK: 'LINK', + REPORT_ACTION: 'REPORT_ACTION', + EMAIL: 'EMAIL', + REPORT: 'REPORT', +} as const; + +type GenericReportActionContextMenuProps = { + /** The ID of the report this report action is attached to. */ + reportID: string; + + /** The ID of the report action this context menu is attached to. */ + reportActionID: string; + + /** The ID of the original report from which the given reportAction is first created. */ + originalReportID: string; + + /** 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; +}; + +export {CONTEXT_MENU_TYPES}; +export type {GenericReportActionContextMenuProps}; From 44abb622fb39576c25a3bf12fd3ef0beb5c18777 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 28 Dec 2023 17:43:43 +0100 Subject: [PATCH 03/62] Migrate MiniReportActionContextMenu --- .../BaseReportActionContextMenu.tsx | 59 +++++--- .../index.native.tsx | 5 +- .../MiniReportActionContextMenu/index.tsx | 30 +--- .../MiniReportActionContextMenu/types.ts | 8 + .../ContextMenu/ReportActionContextMenu.tsx | 138 ------------------ ...genericReportActionContextMenuPropTypes.ts | 34 ----- src/pages/home/report/ContextMenu/types.ts | 8 - 7 files changed, 58 insertions(+), 224 deletions(-) create mode 100644 src/pages/home/report/ContextMenu/MiniReportActionContextMenu/types.ts delete mode 100644 src/pages/home/report/ContextMenu/ReportActionContextMenu.tsx delete mode 100644 src/pages/home/report/ContextMenu/genericReportActionContextMenuPropTypes.ts diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx index 20012c15ef30..08d1e7a0c28a 100755 --- a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx @@ -16,7 +16,6 @@ import ONYXKEYS from '@src/ONYXKEYS'; import {Beta, ReportActions} from '@src/types/onyx'; import ContextMenuActions from './ContextMenuActions'; import {hideContextMenu} from './ReportActionContextMenu'; -import {GenericReportActionContextMenuProps} from './types'; type BaseReportActionContextMenuOnyxProps = { /** Beta features list */ @@ -26,29 +25,51 @@ type BaseReportActionContextMenuOnyxProps = { reportActions: OnyxEntry; }; -type BaseReportActionContextMenuProps = GenericReportActionContextMenuProps & - BaseReportActionContextMenuOnyxProps & { - /** String representing the context menu type [LINK, REPORT_ACTION] which controls context menu choices */ - type?: ValueOf; +type BaseReportActionContextMenuProps = BaseReportActionContextMenuOnyxProps & { + /** The ID of the report this report action is attached to. */ + reportID: string; - /** Target node which is the target of ContentMenu */ - anchor: any; + /** The ID of the report action this context menu is attached to. */ + reportActionID: string; - /** Flag to check if the chat participant is Chronos */ - isChronosReport: boolean; + /** The ID of the original report from which the given reportAction is first created. */ + // eslint-disable-next-line react/no-unused-prop-types + originalReportID: string; - /** Whether the provided report is an archived room */ - isArchivedRoom: boolean; + /** 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; - /** Flag to check if the chat is pinned in the LHN. Used for the Pin/Unpin action */ - isPinnedChat?: boolean; + /** Controls the visibility of this component. */ + isVisible?: boolean; - /** Flag to check if the chat is unread in the LHN. Used for the Mark as Read/Unread action */ - isUnreadChat?: boolean; + /** The copy selection. */ + selection?: string; - /** Content Ref */ - contentRef: any; - }; + /** 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?: ValueOf; + + /** Target node which is the target of ContentMenu */ + anchor: any; + + /** 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; + + /** Content Ref */ + contentRef: any; +}; function BaseReportActionContextMenu({ type = CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, @@ -206,3 +227,5 @@ export default withOnyx null; +import MiniReportActionContextMenuProps from './types'; + +// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-unused-vars +export default (props: MiniReportActionContextMenuProps) => null; diff --git a/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.tsx b/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.tsx index d858206cdfc3..fbc6b90a3424 100644 --- a/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.tsx +++ b/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/index.tsx @@ -1,47 +1,27 @@ -import PropTypes from 'prop-types'; import React from 'react'; import {View} from 'react-native'; -import _ from 'underscore'; import useStyleUtils from '@hooks/useStyleUtils'; import BaseReportActionContextMenu from '@pages/home/report/ContextMenu/BaseReportActionContextMenu'; -import { - defaultProps as GenericReportActionContextMenuDefaultProps, - propTypes as genericReportActionContextMenuPropTypes, -} from '@pages/home/report/ContextMenu/genericReportActionContextMenuPropTypes'; import CONST from '@src/CONST'; +import MiniReportActionContextMenuProps from './types'; -const propTypes = { - ..._.omit(genericReportActionContextMenuPropTypes, ['isMini']), - - /** Should the reportAction this menu is attached to have the appearance of being - * grouped with the previous reportAction? */ - displayAsGroup: PropTypes.bool, -}; - -const defaultProps = { - ..._.omit(GenericReportActionContextMenuDefaultProps, ['isMini']), - displayAsGroup: false, -}; - -function MiniReportActionContextMenu(props) { +function MiniReportActionContextMenu({displayAsGroup = false, ...rest}: MiniReportActionContextMenuProps) { const StyleUtils = useStyleUtils(); return ( ); } -MiniReportActionContextMenu.propTypes = propTypes; -MiniReportActionContextMenu.defaultProps = defaultProps; MiniReportActionContextMenu.displayName = 'MiniReportActionContextMenu'; export default MiniReportActionContextMenu; diff --git a/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/types.ts b/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/types.ts new file mode 100644 index 000000000000..d28d70180819 --- /dev/null +++ b/src/pages/home/report/ContextMenu/MiniReportActionContextMenu/types.ts @@ -0,0 +1,8 @@ +import {BaseReportActionContextMenuProps} from '@pages/home/report/ContextMenu/BaseReportActionContextMenu'; + +type MiniReportActionContextMenuProps = BaseReportActionContextMenuProps & { + /** 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/home/report/ContextMenu/ReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/ReportActionContextMenu.tsx deleted file mode 100644 index 9467ff19b2f5..000000000000 --- a/src/pages/home/report/ContextMenu/ReportActionContextMenu.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import React from 'react'; - -const contextMenuRef = React.createRef(); - -/** - * Hide the ReportActionContextMenu modal popover. - * Hides the popover menu with an optional delay - * @param {Boolean} shouldDelay - whether the menu should close after a delay - * @param {Function} [onHideCallback=() => {}] - Callback to be called after Context Menu is completely hidden - */ -function hideContextMenu(shouldDelay, onHideCallback = () => {}) { - if (!contextMenuRef.current) { - return; - } - if (!shouldDelay) { - contextMenuRef.current.hideContextMenu(onHideCallback); - - return; - } - - // Save the active instanceID for which hide action was called. - // If menu is being closed with a delay, check that whether the same instance exists or a new was created. - // If instance is not same, cancel the hide action - const instanceID = contextMenuRef.current.instanceID; - setTimeout(() => { - if (contextMenuRef.current.instanceID !== instanceID) { - return; - } - - contextMenuRef.current.hideContextMenu(onHideCallback); - }, 800); -} - -/** - * Show the ReportActionContextMenu modal popover. - * - * @param {string} type - the context menu type to display [EMAIL, LINK, REPORT_ACTION, REPORT] - * @param {Object} [event] - A press event. - * @param {String} [selection] - Copied content. - * @param {Element} contextMenuAnchor - popoverAnchor - * @param {String} reportID - Active Report Id - * @param {String} reportActionID - ReportActionID for ContextMenu - * @param {String} originalReportID - The currrent Report Id of the reportAction - * @param {String} draftMessage - ReportAction Draftmessage - * @param {Function} [onShow=() => {}] - Run a callback when Menu is shown - * @param {Function} [onHide=() => {}] - Run a callback when Menu is hidden - * @param {Boolean} isArchivedRoom - Whether the provided report is an archived room - * @param {Boolean} isChronosReport - Flag to check if the chat participant is Chronos - * @param {Boolean} isPinnedChat - Flag to check if the chat is pinned in the LHN. Used for the Pin/Unpin action - * @param {Boolean} isUnreadChat - Flag to check if the chat has unread messages in the LHN. Used for the Mark as Read/Unread action - */ -function showContextMenu( - type, - event, - selection, - contextMenuAnchor, - reportID = '0', - reportActionID = '0', - originalReportID = '0', - draftMessage = '', - onShow = () => {}, - onHide = () => {}, - isArchivedRoom = false, - isChronosReport = false, - isPinnedChat = false, - isUnreadChat = false, -) { - if (!contextMenuRef.current) { - return; - } - // If there is an already open context menu, close it first before opening - // a new one. - if (contextMenuRef.current.instanceID) { - hideContextMenu(); - contextMenuRef.current.runAndResetOnPopoverHide(); - } - - contextMenuRef.current.showContextMenu( - type, - event, - selection, - contextMenuAnchor, - reportID, - reportActionID, - originalReportID, - draftMessage, - onShow, - onHide, - isArchivedRoom, - isChronosReport, - isPinnedChat, - isUnreadChat, - ); -} - -function hideDeleteModal() { - if (!contextMenuRef.current) { - return; - } - contextMenuRef.current.hideDeleteModal(); -} - -/** - * Opens the Confirm delete action modal - * @param {String} reportID - * @param {Object} reportAction - * @param {Boolean} [shouldSetModalVisibility] - * @param {Function} [onConfirm] - * @param {Function} [onCancel] - */ -function showDeleteModal(reportID, reportAction, shouldSetModalVisibility, onConfirm, onCancel) { - if (!contextMenuRef.current) { - return; - } - contextMenuRef.current.showDeleteModal(reportID, reportAction, shouldSetModalVisibility, onConfirm, onCancel); -} - -/** - * Whether Context Menu is active for the Report Action. - * - * @param {Number|String} actionID - * @return {Boolean} - */ -function isActiveReportAction(actionID) { - if (!contextMenuRef.current) { - return; - } - return contextMenuRef.current.isActiveReportAction(actionID); -} - -function clearActiveReportAction() { - if (!contextMenuRef.current) { - return; - } - return contextMenuRef.current.clearActiveReportAction(); -} - -export {contextMenuRef, showContextMenu, hideContextMenu, isActiveReportAction, clearActiveReportAction, showDeleteModal, hideDeleteModal}; diff --git a/src/pages/home/report/ContextMenu/genericReportActionContextMenuPropTypes.ts b/src/pages/home/report/ContextMenu/genericReportActionContextMenuPropTypes.ts deleted file mode 100644 index 3d8667e44e62..000000000000 --- a/src/pages/home/report/ContextMenu/genericReportActionContextMenuPropTypes.ts +++ /dev/null @@ -1,34 +0,0 @@ -import PropTypes from 'prop-types'; - -const propTypes = { - /** The ID of the report this report action is attached to. */ - reportID: PropTypes.string.isRequired, - - /** The ID of the report action this context menu is attached to. */ - reportActionID: PropTypes.string.isRequired, - - /** The ID of the original report from which the given reportAction is first created. */ - originalReportID: PropTypes.string.isRequired, - - /** 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: PropTypes.bool, - - /** Controls the visibility of this component. */ - isVisible: PropTypes.bool, - - /** The copy selection. */ - selection: PropTypes.string, - - /** Draft message - if this is set the comment is in 'edit' mode */ - draftMessage: PropTypes.string, -}; - -const defaultProps = { - isMini: false, - isVisible: false, - selection: '', - draftMessage: '', -}; - -export {propTypes, defaultProps}; diff --git a/src/pages/home/report/ContextMenu/types.ts b/src/pages/home/report/ContextMenu/types.ts index 051e7635bc80..76af0d1e3f13 100644 --- a/src/pages/home/report/ContextMenu/types.ts +++ b/src/pages/home/report/ContextMenu/types.ts @@ -1,10 +1,3 @@ -const CONTEXT_MENU_TYPES = { - LINK: 'LINK', - REPORT_ACTION: 'REPORT_ACTION', - EMAIL: 'EMAIL', - REPORT: 'REPORT', -} as const; - type GenericReportActionContextMenuProps = { /** The ID of the report this report action is attached to. */ reportID: string; @@ -29,5 +22,4 @@ type GenericReportActionContextMenuProps = { draftMessage?: string; }; -export {CONTEXT_MENU_TYPES}; export type {GenericReportActionContextMenuProps}; From 84e77cd4ffb0b18ecc4ddf17c9d4798a3b8f14b1 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Fri, 29 Dec 2023 14:31:52 +0100 Subject: [PATCH 04/62] Migrate ContextMenuActions --- .../report/ContextMenu/ContextMenuActions.tsx | 132 +++++++++++------- 1 file changed, 83 insertions(+), 49 deletions(-) diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index 2f00f81e0fa5..ab28e741e6bc 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -1,7 +1,7 @@ import ExpensiMark from 'expensify-common/lib/ExpensiMark'; -import lodashGet from 'lodash/get'; import React from 'react'; -import _ from 'underscore'; +import {OnyxEntry} from 'react-native-onyx'; +import {Emoji} from '@assets/emojis/types'; import * as Expensicons from '@components/Icon/Expensicons'; import MiniQuickEmojiReactions from '@components/Reactions/MiniQuickEmojiReactions'; import QuickEmojiReactions from '@components/Reactions/QuickEmojiReactions'; @@ -22,24 +22,20 @@ import * as TaskUtils from '@libs/TaskUtils'; import * as Download from '@userActions/Download'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; +import {TranslationPaths} from '@src/languages/types'; import ROUTES from '@src/ROUTES'; +import {Beta, ReportAction, ReportActionReactions} from '@src/types/onyx'; +import IconAsset from '@src/types/utils/IconAsset'; import {clearActiveReportAction, hideContextMenu, showDeleteModal} from './ReportActionContextMenu'; -/** - * Gets the HTML version of the message in an action. - * @param reportAction - * @return - */ -function getActionText(reportAction) { - const message = _.last(lodashGet(reportAction, 'message', null)); - return lodashGet(message, 'html', ''); +/** Gets the HTML version of the message in an action */ +function getActionText(reportAction: OnyxEntry) { + const message = reportAction?.message?.at(-1) ?? null; + return message?.html ?? ''; } -/** - * Sets the HTML string to Clipboard. - * @param content - */ -function setClipboardMessage(content) { +/** Sets the HTML string to Clipboard */ +function setClipboardMessage(content: string) { const parser = new ExpensiMark(); if (!Clipboard.canSetHtml()) { Clipboard.setString(parser.htmlToMarkdown(content)); @@ -49,16 +45,56 @@ function setClipboardMessage(content) { } } +type ShouldShow = ( + type: string, + reportAction: ReportAction, + isArchivedRoom: boolean, + betas: Beta[], + menuTarget: HTMLElement, + isChronosReport: boolean, + reportID: string, + isPinnedChat: boolean, + isUnreadChat: boolean, + anchor: string, +) => boolean; + +type Payload = { + reportAction: ReportAction; + reportID: string; + draftMessage: string; + selection: string; + close: () => void; + openContextMenu: () => void; + interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; +}; + +type OnPress = (closePopover: boolean, payload: Payload, selection: string | undefined, reportID: string, draftMessage: string) => void; + +type RenderContent = (closePopover: boolean, payload: Payload) => React.ReactElement; + +type GetDescription = (selection?: string) => string | void; + +type ContextMenuAction = { + isAnonymousAction: boolean; + shouldShow: ShouldShow; + textTranslateKey?: TranslationPaths; + successTextTranslateKey?: TranslationPaths; + icon?: IconAsset; + successIcon?: IconAsset; + renderContent?: RenderContent; + onPress?: OnPress; + getDescription?: GetDescription; +}; + // A list of all the context actions in this menu. -export default [ +const ContextMenuActions: ContextMenuAction[] = [ { isAnonymousAction: false, - shouldKeepOpen: true, - shouldShow: (type, reportAction) => type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && _.has(reportAction, 'message') && !ReportActionsUtils.isMessageDeleted(reportAction), + shouldShow: (type, reportAction) => type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && 'message' in reportAction && !ReportActionsUtils.isMessageDeleted(reportAction), renderContent: (closePopover, {reportID, reportAction, close: closeManually, openContextMenu}) => { const isMini = !closePopover; - const closeContextMenu = (onHideCallback) => { + const closeContextMenu = (onHideCallback?: () => void) => { if (isMini) { closeManually(); if (onHideCallback) { @@ -69,7 +105,7 @@ export default [ } }; - const toggleEmojiAndCloseMenu = (emoji, existingReactions) => { + const toggleEmojiAndCloseMenu = (emoji: Emoji, existingReactions: ReportActionReactions | undefined) => { Report.toggleEmojiReaction(reportID, reportAction, emoji, existingReactions); closeContextMenu(); }; @@ -78,6 +114,7 @@ export default [ return ( @@ -106,12 +144,13 @@ export default [ successIcon: Expensicons.Download, shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat, isOffline) => { const isAttachment = ReportActionsUtils.isReportActionAttachment(reportAction); - const messageHtml = lodashGet(reportAction, ['message', 0, 'html']); - return isAttachment && messageHtml !== CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML && reportAction.reportActionID && !ReportActionsUtils.isMessageDeleted(reportAction) && !isOffline; + const messageHtml = getActionText; + return ( + isAttachment && messageHtml !== CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML && !!reportAction.reportActionID && !ReportActionsUtils.isMessageDeleted(reportAction) && !isOffline + ); }, onPress: (closePopover, {reportAction}) => { - const message = _.last(lodashGet(reportAction, 'message', [{}])); - const html = lodashGet(message, 'html', ''); + const html = getActionText(reportAction); const attachmentDetails = getAttachmentDetails(html); const {originalFileName, sourceURL} = attachmentDetails; const sourceURLWithAuth = addEncryptedAuthTokenToURL(sourceURL); @@ -128,8 +167,6 @@ export default [ isAnonymousAction: false, textTranslateKey: 'reportActionContextMenu.replyInThread', icon: Expensicons.ChatBubble, - successTextTranslateKey: '', - successIcon: null, shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID) => { if (type !== CONST.CONTEXT_MENU_TYPES.REPORT_ACTION) { return false; @@ -150,12 +187,12 @@ export default [ if (closePopover) { hideContextMenu(false, () => { ReportActionComposeFocusManager.focus(); - Report.navigateToAndOpenChildReport(lodashGet(reportAction, 'childReportID', '0'), reportAction, reportID); + Report.navigateToAndOpenChildReport(reportAction?.childReportID ?? '0', reportAction, reportID); }); return; } - Report.navigateToAndOpenChildReport(lodashGet(reportAction, 'childReportID', '0'), reportAction, reportID); + Report.navigateToAndOpenChildReport(reportAction?.childReportID ?? '0', reportAction, reportID); }, getDescription: () => {}, }, @@ -163,10 +200,8 @@ export default [ isAnonymousAction: false, textTranslateKey: 'reportActionContextMenu.subscribeToThread', icon: Expensicons.Bell, - successTextTranslateKey: '', - successIcon: null, shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID) => { - let childReportNotificationPreference = lodashGet(reportAction, 'childReportNotificationPreference', ''); + let childReportNotificationPreference = reportAction?.childReportNotificationPreference ?? ''; if (!childReportNotificationPreference) { const isActionCreator = ReportUtils.isActionCreator(reportAction); childReportNotificationPreference = isActionCreator ? CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS : CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; @@ -179,7 +214,7 @@ export default [ return !subscribed && !isWhisperAction && (isCommentAction || isReportPreviewAction || isIOUAction); }, onPress: (closePopover, {reportAction, reportID}) => { - let childReportNotificationPreference = lodashGet(reportAction, 'childReportNotificationPreference', ''); + let childReportNotificationPreference = reportAction?.childReportNotificationPreference ?? ''; if (!childReportNotificationPreference) { const isActionCreator = ReportUtils.isActionCreator(reportAction); childReportNotificationPreference = isActionCreator ? CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS : CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; @@ -187,13 +222,13 @@ export default [ if (closePopover) { hideContextMenu(false, () => { ReportActionComposeFocusManager.focus(); - Report.toggleSubscribeToChildReport(lodashGet(reportAction, 'childReportID', '0'), reportAction, reportID, childReportNotificationPreference); + Report.toggleSubscribeToChildReport(reportAction?.childReportID ?? '0', reportAction, reportID, childReportNotificationPreference); }); return; } ReportActionComposeFocusManager.focus(); - Report.toggleSubscribeToChildReport(lodashGet(reportAction, 'childReportID', '0'), reportAction, reportID, childReportNotificationPreference); + Report.toggleSubscribeToChildReport(reportAction?.childReportID ?? '0', reportAction, reportID, childReportNotificationPreference); }, getDescription: () => {}, }, @@ -201,10 +236,8 @@ export default [ isAnonymousAction: false, textTranslateKey: 'reportActionContextMenu.unsubscribeFromThread', icon: Expensicons.BellSlash, - successTextTranslateKey: '', - successIcon: null, shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID) => { - let childReportNotificationPreference = lodashGet(reportAction, 'childReportNotificationPreference', ''); + let childReportNotificationPreference = reportAction?.childReportNotificationPreference ?? ''; if (!childReportNotificationPreference) { const isActionCreator = ReportUtils.isActionCreator(reportAction); childReportNotificationPreference = isActionCreator ? CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS : CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; @@ -219,7 +252,7 @@ export default [ return subscribed && (isCommentAction || isReportPreviewAction || isIOUAction); }, onPress: (closePopover, {reportAction, reportID}) => { - let childReportNotificationPreference = lodashGet(reportAction, 'childReportNotificationPreference', ''); + let childReportNotificationPreference = reportAction?.childReportNotificationPreference ?? ''; if (!childReportNotificationPreference) { const isActionCreator = ReportUtils.isActionCreator(reportAction); childReportNotificationPreference = isActionCreator ? CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS : CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; @@ -227,13 +260,13 @@ export default [ if (closePopover) { hideContextMenu(false, () => { ReportActionComposeFocusManager.focus(); - Report.toggleSubscribeToChildReport(lodashGet(reportAction, 'childReportID', '0'), reportAction, reportID, childReportNotificationPreference); + Report.toggleSubscribeToChildReport(reportAction?.childReportID ?? '0', reportAction, reportID, childReportNotificationPreference); }); return; } ReportActionComposeFocusManager.focus(); - Report.toggleSubscribeToChildReport(lodashGet(reportAction, 'childReportID', '0'), reportAction, reportID, childReportNotificationPreference); + Report.toggleSubscribeToChildReport(reportAction?.childReportID ?? '0', reportAction, reportID, childReportNotificationPreference); }, getDescription: () => {}, }, @@ -278,8 +311,7 @@ export default [ onPress: (closePopover, {reportAction, selection}) => { const isTaskAction = ReportActionsUtils.isTaskAction(reportAction); const isReportPreviewAction = ReportActionsUtils.isReportPreviewAction(reportAction); - const message = _.last(lodashGet(reportAction, 'message', [{}])); - const messageHtml = isTaskAction ? TaskUtils.getTaskReportActionMessage(reportAction.actionName) : lodashGet(message, 'html', ''); + const messageHtml = isTaskAction ? TaskUtils.getTaskReportActionMessage(reportAction.actionName) : getActionText(reportAction); const isAttachment = ReportActionsUtils.isReportActionAttachment(reportAction); if (!isAttachment) { @@ -298,11 +330,11 @@ export default [ const taskPreviewMessage = TaskUtils.getTaskCreatedMessage(reportAction); Clipboard.setString(taskPreviewMessage); } else if (ReportActionsUtils.isMemberChangeAction(reportAction)) { - const logMessage = ReportActionsUtils.getMemberChangeMessageFragment(reportAction).html; + const logMessage = ReportActionsUtils.getMemberChangeMessageFragment(reportAction).html ?? ''; setClipboardMessage(logMessage); } else if (ReportActionsUtils.isSubmittedExpenseAction(reportAction)) { - const submittedMessage = _.reduce(reportAction.message, (acc, curr) => `${acc}${curr.text}`, ''); - Clipboard.setString(submittedMessage); + const submittedMessage = reportAction?.message?.reduce((acc, curr) => `${acc}${curr.text}`, ''); + Clipboard.setString(submittedMessage ?? ''); } else if (content) { setClipboardMessage(content); } @@ -325,12 +357,12 @@ export default [ const isAttachment = ReportActionsUtils.isReportActionAttachment(reportAction); // Only hide the copylink menu item when context menu is opened over img element. - const isAttachmentTarget = lodashGet(menuTarget, 'tagName') === 'IMG' && isAttachment; + const isAttachmentTarget = menuTarget?.tagName === 'IMG' && isAttachment; return Permissions.canUseCommentLinking(betas) && type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && !isAttachmentTarget && !ReportActionsUtils.isMessageDeleted(reportAction); }, onPress: (closePopover, {reportAction, reportID}) => { Environment.getEnvironmentURL().then((environmentURL) => { - const reportActionID = lodashGet(reportAction, 'reportActionID'); + const reportActionID = reportAction?.reportActionID; Clipboard.setString(`${environmentURL}/r/${reportID}/${reportActionID}`); }); hideContextMenu(true, ReportActionComposeFocusManager.focus); @@ -378,7 +410,7 @@ export default [ onPress: (closePopover, {reportID, reportAction, draftMessage}) => { if (ReportActionsUtils.isMoneyRequestAction(reportAction)) { hideContextMenu(false); - const childReportID = lodashGet(reportAction, 'childReportID', 0); + const childReportID = reportAction?.childReportID ?? 0; if (!childReportID) { const thread = ReportUtils.buildTransactionThread(reportAction, reportID); const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs); @@ -390,7 +422,7 @@ export default [ Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(childReportID)); return; } - const editAction = () => Report.saveReportActionDraft(reportID, reportAction, _.isEmpty(draftMessage) ? getActionText(reportAction) : ''); + const editAction = () => Report.saveReportActionDraft(reportID, reportAction, !draftMessage ? getActionText(reportAction) : ''); if (closePopover) { // Hide popover, then call editAction @@ -474,3 +506,5 @@ export default [ getDescription: () => {}, }, ]; + +export default ContextMenuActions; From d36fb5bbb1269d90d795f6113833dd888690b82c Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Fri, 29 Dec 2023 15:12:05 +0100 Subject: [PATCH 05/62] Fix ContextMenuActions type errors --- .../report/ContextMenu/ContextMenuActions.tsx | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index ab28e741e6bc..e0dbd6e94159 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -144,19 +144,18 @@ const ContextMenuActions: ContextMenuAction[] = [ successIcon: Expensicons.Download, shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat, isOffline) => { const isAttachment = ReportActionsUtils.isReportActionAttachment(reportAction); - const messageHtml = getActionText; + const messageHtml = reportAction?.message?.at(0)?.html; return ( isAttachment && messageHtml !== CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML && !!reportAction.reportActionID && !ReportActionsUtils.isMessageDeleted(reportAction) && !isOffline ); }, onPress: (closePopover, {reportAction}) => { const html = getActionText(reportAction); - const attachmentDetails = getAttachmentDetails(html); - const {originalFileName, sourceURL} = attachmentDetails; - const sourceURLWithAuth = addEncryptedAuthTokenToURL(sourceURL); - const sourceID = (sourceURL.match(CONST.REGEX.ATTACHMENT_ID) || [])[1]; + const {originalFileName, sourceURL} = getAttachmentDetails(html); + const sourceURLWithAuth = addEncryptedAuthTokenToURL(sourceURL ?? ''); + const sourceID = (sourceURL?.match(CONST.REGEX.ATTACHMENT_ID) ?? [])[1]; Download.setDownload(sourceID, true); - fileDownload(sourceURLWithAuth, originalFileName).then(() => Download.setDownload(sourceID, false)); + fileDownload(sourceURLWithAuth, originalFileName ?? '').then(() => Download.setDownload(sourceID, false)); if (closePopover) { hideContextMenu(true, ReportActionComposeFocusManager.focus); } @@ -214,7 +213,7 @@ const ContextMenuActions: ContextMenuAction[] = [ return !subscribed && !isWhisperAction && (isCommentAction || isReportPreviewAction || isIOUAction); }, onPress: (closePopover, {reportAction, reportID}) => { - let childReportNotificationPreference = reportAction?.childReportNotificationPreference ?? ''; + let childReportNotificationPreference = reportAction?.childReportNotificationPreference; if (!childReportNotificationPreference) { const isActionCreator = ReportUtils.isActionCreator(reportAction); childReportNotificationPreference = isActionCreator ? CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS : CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; @@ -237,7 +236,7 @@ const ContextMenuActions: ContextMenuAction[] = [ textTranslateKey: 'reportActionContextMenu.unsubscribeFromThread', icon: Expensicons.BellSlash, shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID) => { - let childReportNotificationPreference = reportAction?.childReportNotificationPreference ?? ''; + let childReportNotificationPreference = reportAction?.childReportNotificationPreference; if (!childReportNotificationPreference) { const isActionCreator = ReportUtils.isActionCreator(reportAction); childReportNotificationPreference = isActionCreator ? CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS : CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; @@ -252,7 +251,7 @@ const ContextMenuActions: ContextMenuAction[] = [ return subscribed && (isCommentAction || isReportPreviewAction || isIOUAction); }, onPress: (closePopover, {reportAction, reportID}) => { - let childReportNotificationPreference = reportAction?.childReportNotificationPreference ?? ''; + let childReportNotificationPreference = reportAction?.childReportNotificationPreference; if (!childReportNotificationPreference) { const isActionCreator = ReportUtils.isActionCreator(reportAction); childReportNotificationPreference = isActionCreator ? CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS : CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; @@ -294,7 +293,7 @@ const ContextMenuActions: ContextMenuAction[] = [ Clipboard.setString(EmailUtils.trimMailTo(selection)); hideContextMenu(true, ReportActionComposeFocusManager.focus); }, - getDescription: (selection) => EmailUtils.prefixMailSeparatorsWithBreakOpportunities(EmailUtils.trimMailTo(selection)), + getDescription: (selection) => EmailUtils.prefixMailSeparatorsWithBreakOpportunities(EmailUtils.trimMailTo(selection ?? '')), }, { isAnonymousAction: true, @@ -413,7 +412,7 @@ const ContextMenuActions: ContextMenuAction[] = [ const childReportID = reportAction?.childReportID ?? 0; if (!childReportID) { const thread = ReportUtils.buildTransactionThread(reportAction, reportID); - const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs); + const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs ?? []); Report.openReport(thread.reportID, userLogins, thread, reportAction.reportActionID); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(thread.reportID)); return; @@ -493,7 +492,6 @@ const ContextMenuActions: ContextMenuAction[] = [ ReportUtils.canFlagReportAction(reportAction, reportID) && !isArchivedRoom && !isChronosReport && - !ReportUtils.isConciergeChatReport(reportID) && reportAction.actorAccountID !== CONST.ACCOUNT_ID.CONCIERGE, onPress: (closePopover, {reportID, reportAction}) => { if (closePopover) { From e395228009d7589dea28a288d5eb825805ff331d Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Fri, 29 Dec 2023 21:28:03 +0100 Subject: [PATCH 06/62] Improve ContextMenu types --- src/libs/actions/IOU.js | 2 +- .../BaseReportActionContextMenu.tsx | 24 ++--- .../report/ContextMenu/ContextMenuActions.tsx | 62 +++++------ .../PopoverReportActionContextMenu.tsx | 101 ++++++++++-------- .../ContextMenu/ReportActionContextMenu.ts | 1 + 5 files changed, 102 insertions(+), 88 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index d43fefca20bc..0cb3a3277b55 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -2277,7 +2277,7 @@ function updateMoneyRequestAmountAndCurrency(transactionID, transactionThreadRep } /** - * @param {String} transactionID + * @param {String | undefined} transactionID * @param {Object} reportAction - the money request reportAction we are deleting * @param {Boolean} isSingleTransactionView */ diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx index 08d1e7a0c28a..96fc5ba1c944 100755 --- a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx @@ -1,8 +1,7 @@ import lodashIsEqual from 'lodash/isEqual'; -import React, {memo, useMemo, useRef, useState} from 'react'; +import React, {memo, RefObject, useMemo, useRef, useState} from 'react'; import {InteractionManager, View} from 'react-native'; import {OnyxEntry, withOnyx} from 'react-native-onyx'; -import {ValueOf} from 'type-fest'; import ContextMenuItem from '@components/ContextMenuItem'; import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; @@ -13,9 +12,10 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import {Beta, ReportActions} from '@src/types/onyx'; +import {Beta, ReportAction, ReportActions} from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import ContextMenuActions from './ContextMenuActions'; -import {hideContextMenu} from './ReportActionContextMenu'; +import {ContextMenuType, hideContextMenu} from './ReportActionContextMenu'; type BaseReportActionContextMenuOnyxProps = { /** Beta features list */ @@ -50,10 +50,10 @@ type BaseReportActionContextMenuProps = BaseReportActionContextMenuOnyxProps & { draftMessage?: string; /** String representing the context menu type [LINK, REPORT_ACTION] which controls context menu choices */ - type?: ValueOf; + type?: ContextMenuType; /** Target node which is the target of ContentMenu */ - anchor: any; + anchor: HTMLElement; /** Flag to check if the chat participant is Chronos */ isChronosReport: boolean; @@ -68,7 +68,7 @@ type BaseReportActionContextMenuProps = BaseReportActionContextMenuOnyxProps & { isUnreadChat?: boolean; /** Content Ref */ - contentRef: any; + contentRef?: RefObject; }; function BaseReportActionContextMenu({ @@ -96,16 +96,16 @@ function BaseReportActionContextMenu({ const wrapperStyle = StyleUtils.getReportActionContextMenuStyles(isMini, isSmallScreenWidth); const {isOffline} = useNetwork(); - const reportAction = useMemo(() => { - if (_.isEmpty(reportActions) || reportActionID === '0') { - return {}; + const reportAction: OnyxEntry = useMemo(() => { + if (isEmptyObject(reportActions) || reportActionID === '0') { + return null; } - return reportActions[reportActionID] || {}; + return reportActions[reportActionID] ?? null; }, [reportActions, reportActionID]); const shouldEnableArrowNavigation = !isMini && (isVisible || shouldKeepOpen); const filteredContextMenuActions = ContextMenuActions.filter((contextAction) => - contextAction.shouldShow(type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat, isOffline), + contextAction.shouldShow(type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat, !!isOffline), ); // Context menu actions that are not rendered as menu items are excluded from arrow navigation diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index e0dbd6e94159..eecf5a975684 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -47,15 +47,15 @@ function setClipboardMessage(content: string) { type ShouldShow = ( type: string, - reportAction: ReportAction, + reportAction: OnyxEntry, isArchivedRoom: boolean, - betas: Beta[], + betas: OnyxEntry, menuTarget: HTMLElement, isChronosReport: boolean, reportID: string, isPinnedChat: boolean, isUnreadChat: boolean, - anchor: string, + isOffline: boolean, ) => boolean; type Payload = { @@ -90,7 +90,8 @@ type ContextMenuAction = { const ContextMenuActions: ContextMenuAction[] = [ { isAnonymousAction: false, - shouldShow: (type, reportAction) => type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && 'message' in reportAction && !ReportActionsUtils.isMessageDeleted(reportAction), + shouldShow: (type, reportAction) => + type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && !!reportAction && 'message' in reportAction && !ReportActionsUtils.isMessageDeleted(reportAction), renderContent: (closePopover, {reportID, reportAction, close: closeManually, openContextMenu}) => { const isMini = !closePopover; @@ -118,7 +119,7 @@ const ContextMenuActions: ContextMenuAction[] = [ onEmojiSelected={toggleEmojiAndCloseMenu} onPressOpenPicker={openContextMenu} onEmojiPickerClosed={closeContextMenu} - reportActionID={reportAction.reportActionID} + reportActionID={reportAction?.reportActionID} reportAction={reportAction} /> ); @@ -130,7 +131,7 @@ const ContextMenuActions: ContextMenuAction[] = [ closeContextMenu={closeContextMenu} onEmojiSelected={toggleEmojiAndCloseMenu} // @ts-expect-error TODO: Remove this once Reactions (https://github.com/Expensify/App/issues/25153) is migrated to TypeScript. - reportActionID={reportAction.reportActionID} + reportActionID={reportAction?.reportActionID} reportAction={reportAction} /> ); @@ -142,11 +143,11 @@ const ContextMenuActions: ContextMenuAction[] = [ icon: Expensicons.Download, successTextTranslateKey: 'common.download', successIcon: Expensicons.Download, - shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat, isOffline) => { + shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport, reportID, isPinnedChat, isUnreadChat, isOffline) => { const isAttachment = ReportActionsUtils.isReportActionAttachment(reportAction); const messageHtml = reportAction?.message?.at(0)?.html; return ( - isAttachment && messageHtml !== CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML && !!reportAction.reportActionID && !ReportActionsUtils.isMessageDeleted(reportAction) && !isOffline + isAttachment && messageHtml !== CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML && !!reportAction?.reportActionID && !ReportActionsUtils.isMessageDeleted(reportAction) && !isOffline ); }, onPress: (closePopover, {reportAction}) => { @@ -166,13 +167,13 @@ const ContextMenuActions: ContextMenuAction[] = [ isAnonymousAction: false, textTranslateKey: 'reportActionContextMenu.replyInThread', icon: Expensicons.ChatBubble, - shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID) => { + shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport, reportID) => { if (type !== CONST.CONTEXT_MENU_TYPES.REPORT_ACTION) { return false; } - const isCommentAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT; - const isReportPreviewAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW; - const isIOUAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && !ReportActionsUtils.isSplitBillAction(reportAction); + const isCommentAction = reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT; + const isReportPreviewAction = reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW; + const isIOUAction = reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && !ReportActionsUtils.isSplitBillAction(reportAction); const isModifiedExpenseAction = ReportActionsUtils.isModifiedExpenseAction(reportAction); const isTaskAction = ReportActionsUtils.isTaskAction(reportAction); const isWhisperAction = ReportActionsUtils.isWhisperAction(reportAction); @@ -199,16 +200,16 @@ const ContextMenuActions: ContextMenuAction[] = [ isAnonymousAction: false, textTranslateKey: 'reportActionContextMenu.subscribeToThread', icon: Expensicons.Bell, - shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID) => { + shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport, reportID) => { let childReportNotificationPreference = reportAction?.childReportNotificationPreference ?? ''; if (!childReportNotificationPreference) { const isActionCreator = ReportUtils.isActionCreator(reportAction); childReportNotificationPreference = isActionCreator ? CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS : CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; } const subscribed = childReportNotificationPreference !== 'hidden'; - const isCommentAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && !ReportUtils.isThreadFirstChat(reportAction, reportID); - const isReportPreviewAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW; - const isIOUAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && !ReportActionsUtils.isSplitBillAction(reportAction); + const isCommentAction = reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && !ReportUtils.isThreadFirstChat(reportAction, reportID); + const isReportPreviewAction = reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW; + const isIOUAction = reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && !ReportActionsUtils.isSplitBillAction(reportAction); const isWhisperAction = ReportActionsUtils.isWhisperAction(reportAction); return !subscribed && !isWhisperAction && (isCommentAction || isReportPreviewAction || isIOUAction); }, @@ -235,7 +236,7 @@ const ContextMenuActions: ContextMenuAction[] = [ isAnonymousAction: false, textTranslateKey: 'reportActionContextMenu.unsubscribeFromThread', icon: Expensicons.BellSlash, - shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID) => { + shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport, reportID) => { let childReportNotificationPreference = reportAction?.childReportNotificationPreference; if (!childReportNotificationPreference) { const isActionCreator = ReportUtils.isActionCreator(reportAction); @@ -245,9 +246,9 @@ const ContextMenuActions: ContextMenuAction[] = [ if (type !== CONST.CONTEXT_MENU_TYPES.REPORT_ACTION) { return false; } - const isCommentAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && !ReportUtils.isThreadFirstChat(reportAction, reportID); - const isReportPreviewAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW; - const isIOUAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && !ReportActionsUtils.isSplitBillAction(reportAction); + const isCommentAction = reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && !ReportUtils.isThreadFirstChat(reportAction, reportID); + const isReportPreviewAction = reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW; + const isIOUAction = reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && !ReportActionsUtils.isSplitBillAction(reportAction); return subscribed && (isCommentAction || isReportPreviewAction || isIOUAction); }, onPress: (closePopover, {reportAction, reportID}) => { @@ -310,7 +311,7 @@ const ContextMenuActions: ContextMenuAction[] = [ onPress: (closePopover, {reportAction, selection}) => { const isTaskAction = ReportActionsUtils.isTaskAction(reportAction); const isReportPreviewAction = ReportActionsUtils.isReportPreviewAction(reportAction); - const messageHtml = isTaskAction ? TaskUtils.getTaskReportActionMessage(reportAction.actionName) : getActionText(reportAction); + const messageHtml = isTaskAction ? TaskUtils.getTaskReportActionMessage(reportAction?.actionName) : getActionText(reportAction); const isAttachment = ReportActionsUtils.isReportActionAttachment(reportAction); if (!isAttachment) { @@ -374,10 +375,10 @@ const ContextMenuActions: ContextMenuAction[] = [ textTranslateKey: 'reportActionContextMenu.markAsUnread', icon: Expensicons.Mail, successIcon: Expensicons.Checkmark, - shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat) => + shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport, reportID, isPinnedChat, isUnreadChat) => type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION || (type === CONST.CONTEXT_MENU_TYPES.REPORT && !isUnreadChat), onPress: (closePopover, {reportAction, reportID}) => { - Report.markCommentAsUnread(reportID, reportAction.created); + Report.markCommentAsUnread(reportID, reportAction?.created); if (closePopover) { hideContextMenu(true, ReportActionComposeFocusManager.focus); } @@ -390,7 +391,8 @@ const ContextMenuActions: ContextMenuAction[] = [ textTranslateKey: 'reportActionContextMenu.markAsRead', icon: Expensicons.Mail, successIcon: Expensicons.Checkmark, - shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat, isUnreadChat) => type === CONST.CONTEXT_MENU_TYPES.REPORT && isUnreadChat, + shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport, reportID, isPinnedChat, isUnreadChat) => + type === CONST.CONTEXT_MENU_TYPES.REPORT && isUnreadChat, onPress: (closePopover, {reportID}) => { Report.readNewestAction(reportID); if (closePopover) { @@ -413,7 +415,7 @@ const ContextMenuActions: ContextMenuAction[] = [ if (!childReportID) { const thread = ReportUtils.buildTransactionThread(reportAction, reportID); const userLogins = PersonalDetailsUtils.getLoginsByAccountIDs(thread.participantAccountIDs ?? []); - Report.openReport(thread.reportID, userLogins, thread, reportAction.reportActionID); + Report.openReport(thread.reportID, userLogins, thread, reportAction?.reportActionID); Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(thread.reportID)); return; } @@ -461,7 +463,7 @@ const ContextMenuActions: ContextMenuAction[] = [ isAnonymousAction: false, textTranslateKey: 'common.pin', icon: Expensicons.Pin, - shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat) => type === CONST.CONTEXT_MENU_TYPES.REPORT && !isPinnedChat, + shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport, reportID, isPinnedChat) => type === CONST.CONTEXT_MENU_TYPES.REPORT && !isPinnedChat, onPress: (closePopover, {reportID}) => { Report.togglePinnedState(reportID, false); if (closePopover) { @@ -474,7 +476,7 @@ const ContextMenuActions: ContextMenuAction[] = [ isAnonymousAction: false, textTranslateKey: 'common.unPin', icon: Expensicons.Pin, - shouldShow: (type, reportAction, isArchivedRoom, betas, anchor, isChronosReport, reportID, isPinnedChat) => type === CONST.CONTEXT_MENU_TYPES.REPORT && isPinnedChat, + shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport, reportID, isPinnedChat) => type === CONST.CONTEXT_MENU_TYPES.REPORT && isPinnedChat, onPress: (closePopover, {reportID}) => { Report.togglePinnedState(reportID, true); if (closePopover) { @@ -492,14 +494,14 @@ const ContextMenuActions: ContextMenuAction[] = [ ReportUtils.canFlagReportAction(reportAction, reportID) && !isArchivedRoom && !isChronosReport && - reportAction.actorAccountID !== CONST.ACCOUNT_ID.CONCIERGE, + reportAction?.actorAccountID !== CONST.ACCOUNT_ID.CONCIERGE, onPress: (closePopover, {reportID, reportAction}) => { if (closePopover) { - hideContextMenu(false, () => Navigation.navigate(ROUTES.FLAG_COMMENT.getRoute(reportID, reportAction.reportActionID))); + hideContextMenu(false, () => Navigation.navigate(ROUTES.FLAG_COMMENT.getRoute(reportID, reportAction?.reportActionID))); return; } - Navigation.navigate(ROUTES.FLAG_COMMENT.getRoute(reportID, reportAction.reportActionID)); + Navigation.navigate(ROUTES.FLAG_COMMENT.getRoute(reportID, reportAction?.reportActionID)); }, getDescription: () => {}, }, diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx index 7f60b9d9b4d5..5533fe48aefc 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -1,19 +1,34 @@ -import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; -import {Dimensions} from 'react-native'; -import _ from 'underscore'; +import React, {ForwardedRef, forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; +import {Dimensions, EmitterSubscription} from 'react-native'; +import {OnyxEntry} from 'react-native-onyx'; import ConfirmModal from '@components/ConfirmModal'; import PopoverWithMeasuredContent from '@components/PopoverWithMeasuredContent'; import useLocalize from '@hooks/useLocalize'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as IOU from '@userActions/IOU'; import * as Report from '@userActions/Report'; +import CONST from '@src/CONST'; +import {ReportAction} from '@src/types/onyx'; import BaseReportActionContextMenu from './BaseReportActionContextMenu'; - -function PopoverReportActionContextMenu(_props, ref) { +import {ContextMenuType} from './ReportActionContextMenu'; + +type PopoverReportActionContextMenuRef = { + showContextMenu: () => void; + hideContextMenu: () => void; + showDeleteModal: () => void; + hideDeleteModal: () => void; + isActiveReportAction: () => void; + instanceID: () => void; + runAndResetOnPopoverHide: () => void; + clearActiveReportAction: () => void; + contentRef: () => void; +}; + +function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef) { const {translate} = useLocalize(); const reportIDRef = useRef('0'); - const typeRef = useRef(undefined); - const reportActionRef = useRef({}); + const typeRef = useRef(undefined); + const reportActionRef = useRef>(null); const reportActionIDRef = useRef('0'); const originalReportIDRef = useRef('0'); const selectionRef = useRef(''); @@ -43,7 +58,7 @@ function PopoverReportActionContextMenu(_props, ref) { const contentRef = useRef(null); const anchorRef = useRef(null); - const dimensionsEventListener = useRef(null); + const dimensionsEventListener = useRef(null); const contextMenuAnchorRef = useRef(null); const contextMenuTargetNode = useRef(null); @@ -87,8 +102,8 @@ function PopoverReportActionContextMenu(_props, ref) { } popoverAnchorPosition.current = { - horizontal: cursorRelativePosition.horizontal + x, - vertical: cursorRelativePosition.vertical + y, + horizontal: cursorRelativePosition.current.horizontal + x, + vertical: cursorRelativePosition.current.vertical + y, }; }); }, [isPopoverVisible, getContextMenuMeasuredLocation]); @@ -104,36 +119,31 @@ function PopoverReportActionContextMenu(_props, ref) { }; }, [measureContextMenuAnchorPosition]); - /** - * Whether Context Menu is active for the Report Action. - * - * @param {Number|String} actionID - * @return {Boolean} - */ - const isActiveReportAction = (actionID) => Boolean(actionID) && (reportActionIDRef.current === actionID || reportActionRef.current.reportActionID === actionID); + /** Whether Context Menu is active for the Report Action. */ + const isActiveReportAction = (actionID: string): boolean => !!actionID && (reportActionIDRef.current === actionID || reportActionRef.current.reportActionID === actionID); const clearActiveReportAction = () => { reportActionIDRef.current = '0'; - reportActionRef.current = {}; + reportActionRef.current = null; }; /** * Show the ReportActionContextMenu modal popover. * - * @param {string} type - context menu type [EMAIL, LINK, REPORT_ACTION] - * @param {Object} [event] - A press event. - * @param {String} [selection] - Copied content. - * @param {Element} contextMenuAnchor - popoverAnchor - * @param {String} reportID - Active Report Id - * @param {Object} reportActionID - ReportAction for ContextMenu - * @param {String} originalReportID - The currrent Report Id of the reportAction - * @param {String} draftMessage - ReportAction Draftmessage - * @param {Function} [onShow] - Run a callback when Menu is shown - * @param {Function} [onHide] - Run a callback when Menu is hidden - * @param {Boolean} isArchivedRoom - Whether the provided report is an archived room - * @param {Boolean} isChronosReport - Flag to check if the chat participant is Chronos - * @param {Boolean} isPinnedChat - Flag to check if the chat is pinned in the LHN. Used for the Pin/Unpin action - * @param {Boolean} isUnreadChat - Flag to check if the chat is unread in the LHN. Used for the Mark as Read/Unread action + * @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 currrent Report Id of the reportAction + * @param draftMessage - ReportAction Draftmessage + * @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 = ( type, @@ -196,8 +206,8 @@ function PopoverReportActionContextMenu(_props, ref) { /** * Run the callback and return a noop function to reset it - * @param {Function} callback - * @returns {Function} + * @param callback + * @returns */ const runAndResetCallback = (callback) => { callback(); @@ -218,10 +228,10 @@ function PopoverReportActionContextMenu(_props, ref) { /** * Hide the ReportActionContextMenu modal popover. - * @param {Function} onHideActionCallback Callback to be called after popover is completely hidden + * @param onHideActionCallback Callback to be called after popover is completely hidden */ const hideContextMenu = (onHideActionCallback) => { - if (_.isFunction(onHideActionCallback)) { + if (onHideActionCallback === 'function') { onPopoverHideActionCallback.current = onHideActionCallback; } @@ -232,10 +242,11 @@ function PopoverReportActionContextMenu(_props, ref) { const confirmDeleteAndHideModal = useCallback(() => { callbackWhenDeleteModalHide.current = () => (onComfirmDeleteModal.current = runAndResetCallback(onComfirmDeleteModal.current)); - if (ReportActionsUtils.isMoneyRequestAction(reportActionRef.current)) { - IOU.deleteMoneyRequest(reportActionRef.current.originalMessage.IOUTransactionID, reportActionRef.current); - } else { - Report.deleteReportComment(reportIDRef.current, reportActionRef.current); + const reportAction = reportActionRef.current; + if (ReportActionsUtils.isMoneyRequestAction(reportAction) && reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) { + IOU.deleteMoneyRequest(reportAction?.originalMessage?.IOUTransactionID, reportAction); + } else if (reportAction) { + Report.deleteReportComment(reportIDRef.current, reportAction); } setIsDeleteCommentConfirmModalVisible(false); }, []); @@ -252,11 +263,11 @@ function PopoverReportActionContextMenu(_props, ref) { /** * Opens the Confirm delete action modal - * @param {String} reportID - * @param {Object} reportAction - * @param {Boolean} [shouldSetModalVisibility] - * @param {Function} [onConfirm] - * @param {Function} [onCancel] + * @param reportID + * @param reportAction + * @param [shouldSetModalVisibility] + * @param [onConfirm] + * @param [onCancel] */ const showDeleteModal = (reportID, reportAction, shouldSetModalVisibility = true, onConfirm = () => {}, onCancel = () => {}) => { onCancelDeleteModal.current = onCancel; diff --git a/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts b/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts index b269bc276b55..7ba4a1e04283 100644 --- a/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts +++ b/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts @@ -173,3 +173,4 @@ function clearActiveReportAction() { } export {contextMenuRef, showContextMenu, hideContextMenu, isActiveReportAction, clearActiveReportAction, showDeleteModal, hideDeleteModal}; +export type {ContextMenuType}; From d0c910e7ddce28a923aa400694f800013e1a5cbd Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Tue, 2 Jan 2024 16:36:04 +0100 Subject: [PATCH 07/62] Use report.lastMessageText as the all-case fallback for getLastMessageTextForReport --- src/libs/OptionsListUtils.js | 10 +++++----- src/libs/PersonalDetailsUtils.ts | 32 ++++++++++++++++++++++++++++++++ src/libs/SidebarUtils.ts | 2 +- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 0d5162399fcb..1e8429810671 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -379,9 +379,10 @@ function getAllReportErrors(report, reportActions) { /** * Get the last message text from the report directly or from other sources for special cases. * @param {Object} report + * @param {Object[]} personalDetails - list of personal details of the report participants * @returns {String} */ -function getLastMessageTextForReport(report) { +function getLastMessageTextForReport(report, personalDetails) { const lastReportAction = _.find(allSortedReportActions[report.reportID], (reportAction) => ReportActionUtils.shouldReportActionBeVisibleAsLastAction(reportAction)); let lastMessageTextFromReport = ''; const lastActionName = lodashGet(lastReportAction, 'actionName', ''); @@ -418,10 +419,9 @@ function getLastMessageTextForReport(report) { lastMessageTextFromReport = lodashGet(lastReportAction, 'message[0].text', ''); } else if (ReportActionUtils.isCreatedTaskReportAction(lastReportAction)) { lastMessageTextFromReport = TaskUtils.getTaskCreatedMessage(lastReportAction); - } else { - lastMessageTextFromReport = report ? report.lastMessageText || '' : ''; } - return lastMessageTextFromReport; + + return lastMessageTextFromReport || PersonalDetailsUtils.replaceLoginsWithDisplayNames(lodashGet(report, 'lastMessageText', ''), personalDetails); } /** @@ -509,7 +509,7 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { hasMultipleParticipants = personalDetailList.length > 1 || result.isChatRoom || result.isPolicyExpenseChat; subtitle = ReportUtils.getChatRoomSubtitle(report); - const lastMessageTextFromReport = getLastMessageTextForReport(report); + const lastMessageTextFromReport = getLastMessageTextForReport(report, personalDetailList); const lastActorDetails = personalDetailMap[report.lastActorAccountID] || null; const lastActorDisplayName = hasMultipleParticipants && lastActorDetails && lastActorDetails.accountID !== currentUserAccountID ? lastActorDetails.firstName || lastActorDetails.displayName : ''; diff --git a/src/libs/PersonalDetailsUtils.ts b/src/libs/PersonalDetailsUtils.ts index a5a033797c7b..00007b5e04d2 100644 --- a/src/libs/PersonalDetailsUtils.ts +++ b/src/libs/PersonalDetailsUtils.ts @@ -6,13 +6,28 @@ import * as LocalePhoneNumber from './LocalePhoneNumber'; import * as Localize from './Localize'; import * as UserUtils from './UserUtils'; +let currentUserAccountID: number | undefined; +Onyx.connect({ + key: ONYXKEYS.SESSION, + callback: (value) => { + // When signed out, val is undefined + if (!value) { + return; + } + + currentUserAccountID = value.accountID; + }, +}); + let personalDetails: Array = []; let allPersonalDetails: OnyxEntry = {}; +let currentUserPersonalDetails: OnyxEntry; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, callback: (val) => { personalDetails = Object.values(val ?? {}); allPersonalDetails = val; + currentUserPersonalDetails = val?.[currentUserAccountID ?? -1] ?? null; }, }); @@ -27,6 +42,22 @@ function getDisplayNameOrDefault(displayName?: string, defaultValue = ''): strin return displayName || defaultValue || Localize.translateLocal('common.hidden'); } +function replaceLoginsWithDisplayNames(text: string, details: PersonalDetails[], includeCurrentAccount = true): string { + const result = details.reduce((replacedText, detail) => { + if (!detail.login) { + return replacedText; + } + + return replacedText.replaceAll(detail.login, getDisplayNameOrDefault(detail?.displayName)); + }, text); + + if (!includeCurrentAccount || !currentUserPersonalDetails) { + return result; + } + + return result.replaceAll(currentUserPersonalDetails.login ?? '', getDisplayNameOrDefault(currentUserPersonalDetails?.displayName)); +} + /** * Given a list of account IDs (as number) it will return an array of personal details objects. * @param accountIDs - Array of accountIDs @@ -212,4 +243,5 @@ export { getFormattedStreet, getStreetLines, getEffectiveDisplayName, + replaceLoginsWithDisplayNames, }; diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index c4fad1a86906..cf90f87ab5e3 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -319,7 +319,7 @@ function getOptionData( // We only create tooltips for the first 10 users or so since some reports have hundreds of users, causing performance to degrade. const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips((participantPersonalDetailList || []).slice(0, 10), hasMultipleParticipants); - const lastMessageTextFromReport = OptionsListUtils.getLastMessageTextForReport(report); + const lastMessageTextFromReport = OptionsListUtils.getLastMessageTextForReport(report, displayNamesWithTooltips); // If the last actor's details are not currently saved in Onyx Collection, // then try to get that from the last report action if that action is valid From 7ae56a326cd6f29ce32a84c2bbbec0ca554c8c59 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 2 Jan 2024 19:51:57 +0100 Subject: [PATCH 08/62] Fix almost all type errors --- src/components/Modal/index.android.tsx | 3 +- src/components/Modal/index.ios.tsx | 3 +- src/components/Modal/index.tsx | 4 +- src/components/Modal/types.ts | 74 +++++++------- src/components/Popover/types.ts | 2 +- .../PopoverWithoutOverlay/index.tsx | 2 +- src/components/ShowContextMenuContext.tsx | 10 +- src/libs/ReportUtils.ts | 6 +- .../BaseReportActionContextMenu.tsx | 23 ++--- .../report/ContextMenu/ContextMenuActions.tsx | 17 ++-- .../PopoverReportActionContextMenu.tsx | 96 ++++++++----------- .../ContextMenu/ReportActionContextMenu.ts | 2 +- 12 files changed, 118 insertions(+), 124 deletions(-) diff --git a/src/components/Modal/index.android.tsx b/src/components/Modal/index.android.tsx index 2343cb4c70a9..eb5582e3c2e8 100644 --- a/src/components/Modal/index.android.tsx +++ b/src/components/Modal/index.android.tsx @@ -1,6 +1,5 @@ import React from 'react'; import {AppState} from 'react-native'; -import withWindowDimensions from '@components/withWindowDimensions'; import ComposerFocusManager from '@libs/ComposerFocusManager'; import BaseModal from './BaseModal'; import BaseModalProps from './types'; @@ -28,4 +27,4 @@ function Modal({useNativeDriver = true, ...rest}: BaseModalProps) { } Modal.displayName = 'Modal'; -export default withWindowDimensions(Modal); +export default Modal; diff --git a/src/components/Modal/index.ios.tsx b/src/components/Modal/index.ios.tsx index f780775ec216..6be171a5de16 100644 --- a/src/components/Modal/index.ios.tsx +++ b/src/components/Modal/index.ios.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import withWindowDimensions from '@components/withWindowDimensions'; import BaseModal from './BaseModal'; import BaseModalProps from './types'; @@ -15,4 +14,4 @@ function Modal({children, ...rest}: BaseModalProps) { } Modal.displayName = 'Modal'; -export default withWindowDimensions(Modal); +export default Modal; diff --git a/src/components/Modal/index.tsx b/src/components/Modal/index.tsx index 4269420dcd7f..8c55f37d4888 100644 --- a/src/components/Modal/index.tsx +++ b/src/components/Modal/index.tsx @@ -1,5 +1,4 @@ import React, {useState} from 'react'; -import withWindowDimensions from '@components/withWindowDimensions'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import StatusBar from '@libs/StatusBar'; @@ -11,6 +10,7 @@ function Modal({fullscreen = true, onModalHide = () => {}, type, onModalShow = ( const theme = useTheme(); const StyleUtils = useStyleUtils(); const [previousStatusBarColor, setPreviousStatusBarColor] = useState(); + const setStatusBarColor = (color = theme.appBG) => { if (!fullscreen) { @@ -55,4 +55,4 @@ function Modal({fullscreen = true, onModalHide = () => {}, type, onModalShow = ( } Modal.displayName = 'Modal'; -export default withWindowDimensions(Modal); +export default Modal; diff --git a/src/components/Modal/types.ts b/src/components/Modal/types.ts index 461a5935eda9..ccc5db2e7792 100644 --- a/src/components/Modal/types.ts +++ b/src/components/Modal/types.ts @@ -1,7 +1,6 @@ import {ViewStyle} from 'react-native'; import {ModalProps} from 'react-native-modal'; import {ValueOf} from 'type-fest'; -import {WindowDimensionsProps} from '@components/withWindowDimensions/types'; import CONST from '@src/CONST'; type PopoverAnchorPosition = { @@ -11,57 +10,56 @@ type PopoverAnchorPosition = { left?: number; }; -type BaseModalProps = WindowDimensionsProps & - Partial & { - /** Decides whether the modal should cover fullscreen. FullScreen modal has backdrop */ - fullscreen?: boolean; +type BaseModalProps = Partial & { + /** Decides whether the modal should cover fullscreen. FullScreen modal has backdrop */ + fullscreen?: boolean; - /** Should we close modal on outside click */ - shouldCloseOnOutsideClick?: boolean; + /** Should we close modal on outside click */ + shouldCloseOnOutsideClick?: boolean; - /** Should we announce the Modal visibility changes? */ - shouldSetModalVisibility?: boolean; + /** Should we announce the Modal visibility changes? */ + shouldSetModalVisibility?: boolean; - /** Callback method fired when the user requests to close the modal */ - onClose: (ref?: React.RefObject) => void; + /** Callback method fired when the user requests to close the modal */ + onClose: () => void; - /** State that determines whether to display the modal or not */ - isVisible: boolean; + /** State that determines whether to display the modal or not */ + isVisible: boolean; - /** Callback method fired when the user requests to submit the modal content. */ - onSubmit?: () => void; + /** Callback method fired when the user requests to submit the modal content. */ + onSubmit?: () => void; - /** Callback method fired when the modal is hidden */ - onModalHide?: () => void; + /** Callback method fired when the modal is hidden */ + onModalHide?: () => void; - /** Callback method fired when the modal is shown */ - onModalShow?: () => void; + /** Callback method fired when the modal is shown */ + onModalShow?: () => void; - /** Style of modal to display */ - type?: ValueOf; + /** Style of modal to display */ + type?: ValueOf; - /** The anchor position of a popover modal. Has no effect on other modal types. */ - popoverAnchorPosition?: PopoverAnchorPosition; + /** The anchor position of a popover modal. Has no effect on other modal types. */ + popoverAnchorPosition?: PopoverAnchorPosition; - outerStyle?: ViewStyle; + outerStyle?: ViewStyle; - /** Whether the modal should go under the system statusbar */ - statusBarTranslucent?: boolean; + /** Whether the modal should go under the system statusbar */ + statusBarTranslucent?: boolean; - /** Whether the modal should avoid the keyboard */ - avoidKeyboard?: boolean; + /** Whether the modal should avoid the keyboard */ + avoidKeyboard?: boolean; - /** Modal container styles */ - innerContainerStyle?: ViewStyle; + /** Modal container styles */ + innerContainerStyle?: ViewStyle; - /** - * Whether the modal should hide its content while animating. On iOS, set to true - * if `useNativeDriver` is also true, to avoid flashes in the UI. - * - * See: https://github.com/react-native-modal/react-native-modal/pull/116 - * */ - hideModalContentWhileAnimating?: boolean; - }; + /** + * Whether the modal should hide its content while animating. On iOS, set to true + * if `useNativeDriver` is also true, to avoid flashes in the UI. + * + * See: https://github.com/react-native-modal/react-native-modal/pull/116 + * */ + hideModalContentWhileAnimating?: boolean; +}; export default BaseModalProps; export type {PopoverAnchorPosition}; diff --git a/src/components/Popover/types.ts b/src/components/Popover/types.ts index 7f7e2829770c..6a8ae8423d2c 100644 --- a/src/components/Popover/types.ts +++ b/src/components/Popover/types.ts @@ -13,7 +13,7 @@ type PopoverProps = BaseModalProps & { anchorPosition?: PopoverAnchorPosition; /** The anchor alignment of the popover */ - anchorAlignment: AnchorAlignment; + anchorAlignment?: AnchorAlignment; /** The anchor ref of the popover */ anchorRef: React.RefObject; diff --git a/src/components/PopoverWithoutOverlay/index.tsx b/src/components/PopoverWithoutOverlay/index.tsx index f83949bcbe9d..dd43c5d332aa 100644 --- a/src/components/PopoverWithoutOverlay/index.tsx +++ b/src/components/PopoverWithoutOverlay/index.tsx @@ -51,7 +51,7 @@ function PopoverWithoutOverlay( close: onClose, anchorRef, }); - removeOnClose = Modal.setCloseModal(() => onClose(anchorRef)); + removeOnClose = Modal.setCloseModal(() => onClose()); } else { onModalHide(); close(anchorRef); diff --git a/src/components/ShowContextMenuContext.tsx b/src/components/ShowContextMenuContext.tsx index bbc7abf64e5b..c152289115c8 100644 --- a/src/components/ShowContextMenuContext.tsx +++ b/src/components/ShowContextMenuContext.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import {GestureResponderEvent, Text as RNText} from 'react-native'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as ReportUtils from '@libs/ReportUtils'; import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu'; @@ -24,7 +25,14 @@ ShowContextMenuContext.displayName = 'ShowContextMenuContext'; * @param checkIfContextMenuActive Callback to update context menu active state * @param isArchivedRoom - Is the report an archived room */ -function showContextMenuForReport(event: Event, anchor: HTMLElement, reportID: string, action: ReportAction, checkIfContextMenuActive: () => void, isArchivedRoom = false) { +function showContextMenuForReport( + event: GestureResponderEvent | MouseEvent, + anchor: RNText | null, + reportID: string, + action: ReportAction, + checkIfContextMenuActive: () => void, + isArchivedRoom = false, +) { if (!DeviceCapabilities.canUseTouchScreen()) { return; } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 0c1911401432..f67918931200 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4247,7 +4247,7 @@ function navigateToPrivateNotes(report: Report, session: Session) { * - The action is a whisper action and it's neither a report preview nor IOU action * - The action is the thread's first chat */ -function shouldDisableThread(reportAction: ReportAction, reportID: string) { +function shouldDisableThread(reportAction: OnyxEntry, reportID: string) { const isSplitBillAction = ReportActionsUtils.isSplitBillAction(reportAction); const isDeletedAction = ReportActionsUtils.isDeletedAction(reportAction); const isReportPreviewAction = ReportActionsUtils.isReportPreviewAction(reportAction); @@ -4255,9 +4255,9 @@ function shouldDisableThread(reportAction: ReportAction, reportID: string) { const isWhisperAction = ReportActionsUtils.isWhisperAction(reportAction); return ( - CONST.REPORT.ACTIONS.THREAD_DISABLED.some((action: string) => action === reportAction.actionName) || + CONST.REPORT.ACTIONS.THREAD_DISABLED.some((action: string) => action === reportAction?.actionName) || isSplitBillAction || - (isDeletedAction && !reportAction.childVisibleActionCount) || + (isDeletedAction && !reportAction?.childVisibleActionCount) || (isWhisperAction && !isReportPreviewAction && !isIOUAction) || isThreadFirstChat(reportAction, reportID) ); diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx index 96fc5ba1c944..e1e409d24afc 100755 --- a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx @@ -1,5 +1,5 @@ import lodashIsEqual from 'lodash/isEqual'; -import React, {memo, RefObject, useMemo, useRef, useState} from 'react'; +import React, {memo, MutableRefObject, RefObject, useMemo, useRef, useState} from 'react'; import {InteractionManager, View} from 'react-native'; import {OnyxEntry, withOnyx} from 'react-native-onyx'; import ContextMenuItem from '@components/ContextMenuItem'; @@ -14,7 +14,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {Beta, ReportAction, ReportActions} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import ContextMenuActions from './ContextMenuActions'; +import ContextMenuActions, {ContextMenuActionPayload} from './ContextMenuActions'; import {ContextMenuType, hideContextMenu} from './ReportActionContextMenu'; type BaseReportActionContextMenuOnyxProps = { @@ -53,7 +53,7 @@ type BaseReportActionContextMenuProps = BaseReportActionContextMenuOnyxProps & { type?: ContextMenuType; /** Target node which is the target of ContentMenu */ - anchor: HTMLElement; + anchor: MutableRefObject; /** Flag to check if the chat participant is Chronos */ isChronosReport: boolean; @@ -73,8 +73,8 @@ type BaseReportActionContextMenuProps = BaseReportActionContextMenuOnyxProps & { function BaseReportActionContextMenu({ type = CONST.CONTEXT_MENU_TYPES.REPORT_ACTION, - anchor = null, - contentRef = null, + anchor, + contentRef, isChronosReport = false, isArchivedRoom = false, isMini = false, @@ -161,8 +161,8 @@ function BaseReportActionContextMenu({ > {filteredContextMenuActions.map((contextAction, index) => { const closePopup = !isMini; - const payload = { - reportAction, + const payload: ContextMenuActionPayload = { + reportAction: reportAction as ReportAction, reportID, draftMessage, selection, @@ -173,7 +173,7 @@ function BaseReportActionContextMenu({ if (contextAction.renderContent) { // make sure that renderContent isn't mixed with unsupported props - if (__DEV__ && (contextAction.text != null || contextAction.icon != null)) { + if (__DEV__ && contextAction.icon != null) { throw new Error('Dev error: renderContent() and text/icon cannot be used together.'); } @@ -185,14 +185,15 @@ function BaseReportActionContextMenu({ ref={(ref) => { menuItemRefs.current[index] = ref; }} + // @ts-expect-error TODO: Remove this once ContextMenuItem (https://github.com/Expensify/App/issues/25056) is migrated to TypeScript. icon={contextAction.icon} - text={translate(contextAction.textTranslateKey, {action: reportAction})} + text={contextAction.textTranslateKey ? translate(contextAction.textTranslateKey, {action: reportAction} as never) : undefined} successIcon={contextAction.successIcon} successText={contextAction.successTextTranslateKey ? translate(contextAction.successTextTranslateKey) : undefined} isMini={isMini} key={contextAction.textTranslateKey} - onPress={() => interceptAnonymousUser(() => contextAction.onPress(closePopup, payload), contextAction.isAnonymousAction)} - description={contextAction.getDescription(selection, isSmallScreenWidth)} + onPress={() => interceptAnonymousUser(() => contextAction.onPress?.(closePopup, payload), contextAction.isAnonymousAction)} + description={contextAction.getDescription?.(selection)} isAnonymousAction={contextAction.isAnonymousAction} isFocused={focusedIndex === index} /> diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index 00ccbc4e64ab..4cd407471649 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -1,5 +1,5 @@ import ExpensiMark from 'expensify-common/lib/ExpensiMark'; -import React from 'react'; +import React, {MutableRefObject} from 'react'; import {OnyxEntry} from 'react-native-onyx'; import {Emoji} from '@assets/emojis/types'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -50,7 +50,7 @@ type ShouldShow = ( reportAction: OnyxEntry, isArchivedRoom: boolean, betas: OnyxEntry, - menuTarget: HTMLElement, + menuTarget: MutableRefObject, isChronosReport: boolean, reportID: string, isPinnedChat: boolean, @@ -58,7 +58,7 @@ type ShouldShow = ( isOffline: boolean, ) => boolean; -type Payload = { +type ContextMenuActionPayload = { reportAction: ReportAction; reportID: string; draftMessage: string; @@ -68,9 +68,9 @@ type Payload = { interceptAnonymousUser: (callback: () => void, isAnonymousAction?: boolean) => void; }; -type OnPress = (closePopover: boolean, payload: Payload, selection: string | undefined, reportID: string, draftMessage: string) => void; +type OnPress = (closePopover: boolean, payload: ContextMenuActionPayload, selection?: string, reportID?: string, draftMessage?: string) => void; -type RenderContent = (closePopover: boolean, payload: Payload) => React.ReactElement; +type RenderContent = (closePopover: boolean, payload: ContextMenuActionPayload) => React.ReactElement; type GetDescription = (selection?: string) => string | void; @@ -90,7 +90,7 @@ type ContextMenuAction = { const ContextMenuActions: ContextMenuAction[] = [ { isAnonymousAction: false, - shouldShow: (type, reportAction) => + shouldShow: (type, reportAction): reportAction is ReportAction => type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && !!reportAction && 'message' in reportAction && !ReportActionsUtils.isMessageDeleted(reportAction), renderContent: (closePopover, {reportID, reportAction, close: closeManually, openContextMenu}) => { const isMini = !closePopover; @@ -143,7 +143,7 @@ const ContextMenuActions: ContextMenuAction[] = [ icon: Expensicons.Download, successTextTranslateKey: 'common.download', successIcon: Expensicons.Download, - shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport, reportID, isPinnedChat, isUnreadChat, isOffline) => { + shouldShow: (type, reportAction, isArchivedRoom, betas, menuTarget, isChronosReport, reportID, isPinnedChat, isUnreadChat, isOffline): reportAction is ReportAction => { const isAttachment = ReportActionsUtils.isReportActionAttachment(reportAction); const messageHtml = reportAction?.message?.at(0)?.html; return ( @@ -347,7 +347,7 @@ const ContextMenuActions: ContextMenuAction[] = [ const isAttachment = ReportActionsUtils.isReportActionAttachment(reportAction); // Only hide the copylink menu item when context menu is opened over img element. - const isAttachmentTarget = menuTarget?.tagName === 'IMG' && isAttachment; + const isAttachmentTarget = menuTarget.current?.tagName === 'IMG' && isAttachment; return Permissions.canUseCommentLinking(betas) && type === CONST.CONTEXT_MENU_TYPES.REPORT_ACTION && !isAttachmentTarget && !ReportActionsUtils.isMessageDeleted(reportAction); }, onPress: (closePopover, {reportAction, reportID}) => { @@ -504,3 +504,4 @@ const ContextMenuActions: ContextMenuAction[] = [ ]; export default ContextMenuActions; +export type {ContextMenuActionPayload}; diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx index 39e7c66ddb0c..86a7cf7937f3 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -1,5 +1,5 @@ -import React, {ForwardedRef, forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; -import {Dimensions, EmitterSubscription} from 'react-native'; +import React, {ForwardedRef, forwardRef, RefObject, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; +import {Dimensions, EmitterSubscription, NativeTouchEvent, View} from 'react-native'; import {OnyxEntry} from 'react-native-onyx'; import ConfirmModal from '@components/ConfirmModal'; import PopoverWithMeasuredContent from '@components/PopoverWithMeasuredContent'; @@ -10,20 +10,30 @@ import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import {ReportAction} from '@src/types/onyx'; import BaseReportActionContextMenu from './BaseReportActionContextMenu'; -import {ContextMenuType} from './ReportActionContextMenu'; +import {ContextMenuType, ShowContextMenu} from './ReportActionContextMenu'; + +type HideContextMenu = (onHideActionCallback?: () => void) => void; + +type ShowDeleteModal = (reportID: string, reportAction: ReportAction, shouldSetModalVisibility?: boolean, onConfirm?: () => void, onCancel?: () => void) => void; + +type IsActiveReportAction = (actionID: string) => boolean; type PopoverReportActionContextMenuRef = { - showContextMenu: () => void; - hideContextMenu: () => void; - showDeleteModal: () => void; + showContextMenu: ShowContextMenu; + hideContextMenu: HideContextMenu; + showDeleteModal: ShowDeleteModal; hideDeleteModal: () => void; - isActiveReportAction: () => void; - instanceID: () => void; + isActiveReportAction: IsActiveReportAction; + instanceID: string; runAndResetOnPopoverHide: () => void; clearActiveReportAction: () => void; - contentRef: () => void; + contentRef: RefObject; }; +type ContextMenuAnchorCallback = (x: number, y: number) => void; +type ContextMenuAnchor = {measureInWindow: (callback: ContextMenuAnchorCallback) => void}; + +// eslint-disable-next-line @typescript-eslint/naming-convention function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef) { const {translate} = useLocalize(); const reportIDRef = useRef('0'); @@ -32,7 +42,7 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef(undefined); const cursorRelativePosition = useRef({ horizontal: 0, @@ -56,11 +66,11 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef(null); + const anchorRef = useRef(null); const dimensionsEventListener = useRef(null); - const contextMenuAnchorRef = useRef(null); - const contextMenuTargetNode = useRef(null); + const contextMenuAnchorRef = useRef(null); + const contextMenuTargetNode = useRef(null); const onPopoverShow = useRef(() => {}); const onPopoverHide = useRef(() => {}); @@ -70,16 +80,11 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef {}); const callbackWhenDeleteModalHide = useRef(() => {}); - /** - * Get the Context menu anchor position - * We calculate the achor coordinates from measureInWindow async method - * - * @returns {Promise} - */ + /** Get the Context menu anchor position. We calculate the anchor coordinates from measureInWindow async method */ const getContextMenuMeasuredLocation = useCallback( () => - new Promise((resolve) => { - if (contextMenuAnchorRef.current && _.isFunction(contextMenuAnchorRef.current.measureInWindow)) { + new Promise<{x: number; y: number}>((resolve) => { + if (contextMenuAnchorRef.current && typeof contextMenuAnchorRef.current.measureInWindow === 'function') { contextMenuAnchorRef.current.measureInWindow((x, y) => resolve({x, y})); } else { resolve({x: 0, y: 0}); @@ -88,9 +93,7 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef { if (!isPopoverVisible) { return; @@ -120,7 +123,7 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef !!actionID && (reportActionIDRef.current === actionID || reportActionRef.current.reportActionID === actionID); + const isActiveReportAction: IsActiveReportAction = (actionID) => !!actionID && (reportActionIDRef.current === actionID || reportActionRef.current?.reportActionID === actionID); const clearActiveReportAction = () => { reportActionIDRef.current = '0'; @@ -145,7 +148,7 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef { - const nativeEvent = event.nativeEvent || {}; + const nativeEvent = 'nativeEvent' in event ? event.nativeEvent : ({pageX: 0, pageY: 0} as NativeTouchEvent); contextMenuAnchorRef.current = contextMenuAnchor; contextMenuTargetNode.current = nativeEvent.target; @@ -181,9 +184,9 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef { onPopoverShow.current(); @@ -204,19 +205,13 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef {}; }; - /** - * Run the callback and return a noop function to reset it - * @param callback - * @returns - */ - const runAndResetCallback = (callback) => { + /** 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 - */ + /** After Popover hides, call the registered onPopoverHide & onPopoverHideActionCallback callback and reset it */ const runAndResetOnPopoverHide = () => { reportIDRef.current = '0'; reportActionIDRef.current = '0'; @@ -230,8 +225,8 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef