From 2172d4a20be64c690f4311410dba44696b152463 Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Date: Fri, 27 Mar 2026 23:40:23 -0500 Subject: [PATCH] REF: Update tooltip --- components/TooltipMenu.tsx | 112 ++++++++++++++--------------- components/TransactionListItem.tsx | 2 +- components/types.ts | 1 + ios/Podfile.lock | 40 ++--------- package-lock.json | 21 +++--- package.json | 2 +- react-native.config.js | 15 ++++ 7 files changed, 90 insertions(+), 103 deletions(-) diff --git a/components/TooltipMenu.tsx b/components/TooltipMenu.tsx index a3d451e0b..6d57975a2 100644 --- a/components/TooltipMenu.tsx +++ b/components/TooltipMenu.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useMemo, useRef } from 'react'; import { Platform, Pressable, StyleSheet, ViewStyle } from 'react-native'; -import { MenuView, MenuAction, NativeActionEvent } from '@react-native-menu/menu'; +import ContextMenu, { ContextMenuAction } from 'react-native-context-menu-view'; import { ToolTipMenuProps, Action } from './types'; import { useSettings } from '../hooks/context/useSettings'; @@ -23,79 +23,73 @@ const ToolTipMenu = (props: ToolTipMenuProps) => { onMenuWillShow, onMenuWillHide, enableAndroidRipple = true, + enableIOSPressOpacity = false, } = props; const { language } = useSettings(); const openedRef = useRef(false); - const menuRef = useRef(null); - const normalizeMenuState = useCallback((menuState?: Action['menuState']): MenuAction['state'] | undefined => { + const handleMenuWillShow = useCallback(() => { + if (openedRef.current) { + return; + } + openedRef.current = true; + onMenuWillShow?.(); + }, [onMenuWillShow]); + + const normalizeMenuState = useCallback((menuState?: Action['menuState']): boolean | undefined => { if (menuState === undefined) { return undefined; } if (menuState === 'mixed') { - return 'mixed'; + return true; } - return menuState ? 'on' : 'off'; - }, []); - - const buildAttributes = useCallback((action: Action): MenuAction['attributes'] | undefined => { - const attributes = { - destructive: Boolean(action.destructive), - disabled: Boolean(action.disabled), - hidden: Boolean(action.hidden), - }; - - if (!attributes.destructive && !attributes.disabled && !attributes.hidden) { - return undefined; - } - - return attributes; + return Boolean(menuState); }, []); const mapMenuItemForMenuView = useCallback( - (action: Action): MenuAction | null => { - if (!action?.id) return null; + (action: Action): ContextMenuAction | null => { + if (!action?.id || action.hidden) return null; const mappedSubactions = (action.subactions || []) .map(subaction => mapMenuItemForMenuView(subaction)) - .filter((item): item is MenuAction => item !== null); + .filter((item): item is ContextMenuAction => item !== null); - const menuItem: MenuAction = { - id: action.id.toString(), + const menuItem: ContextMenuAction = { title: action.text, subtitle: action.subtitle, - image: action.icon?.iconValue ?? action.image, - imageColor: action.imageColor, - attributes: buildAttributes(action), - displayInline: Platform.OS === 'ios' ? action.displayInline : undefined, + systemIcon: Platform.OS === 'ios' ? action.icon?.iconValue ?? action.image : undefined, + icon: Platform.OS === 'android' ? action.icon?.iconValue ?? action.image : undefined, + iconColor: typeof action.imageColor === 'string' ? action.imageColor : undefined, + destructive: Boolean(action.destructive), + disabled: Boolean(action.disabled), + inlineChildren: Platform.OS === 'ios' ? action.displayInline : undefined, }; - const state = normalizeMenuState(action.menuState); - if (state) { - menuItem.state = state; + const selected = normalizeMenuState(action.menuState); + if (selected !== undefined) { + menuItem.selected = selected; } if (mappedSubactions.length > 0) { - menuItem.subactions = mappedSubactions; + menuItem.actions = mappedSubactions; } return menuItem; }, - [buildAttributes, normalizeMenuState], + [normalizeMenuState], ); const menuViewItemsIOS = useMemo(() => { return actions .map(actionGroup => { if (Array.isArray(actionGroup) && actionGroup.length > 0) { - const inlineActions = actionGroup.map(mapMenuItemForMenuView).filter((item): item is MenuAction => item !== null); + const inlineActions = actionGroup.map(mapMenuItemForMenuView).filter((item): item is ContextMenuAction => item !== null); if (inlineActions.length === 0) return null; - const group: MenuAction = { - id: inlineActions[0].id, + const group: ContextMenuAction = { title: '', - subactions: inlineActions, - displayInline: true, + actions: inlineActions, + inlineChildren: true, }; return group; } @@ -106,18 +100,20 @@ const ToolTipMenu = (props: ToolTipMenuProps) => { return null; }) - .filter((item): item is MenuAction => item !== null); + .filter((item): item is ContextMenuAction => item !== null); }, [actions, mapMenuItemForMenuView]); const menuViewItemsAndroid = useMemo(() => { - const mergedActions = actions.flat().filter(action => action.id); - return mergedActions.map(mapMenuItemForMenuView).filter((item): item is MenuAction => item !== null); + const mergedActions = actions.flat().filter(action => action.id && !action.hidden); + return mergedActions.map(mapMenuItemForMenuView).filter((item): item is ContextMenuAction => item !== null); }, [actions, mapMenuItemForMenuView]); - const handlePressMenuItemForMenuView = ({ nativeEvent }: NativeActionEvent) => { - if (nativeEvent?.event) { - onPressMenuItem(nativeEvent.event); + const handlePressMenuItemForMenuView = ({ nativeEvent }: { nativeEvent: { name?: string } }) => { + if (nativeEvent?.name) { + onPressMenuItem(nativeEvent.name); } + openedRef.current = false; + onMenuWillHide?.(); }; const renderMenuView = () => { @@ -137,12 +133,18 @@ const ToolTipMenu = (props: ToolTipMenuProps) => { base.push(buttonStyle); } } - if (pressed && enableAndroidRipple) base.push(styles.pressed); + // Keep visual feedback on Android by default. iOS context-menu preview + // already applies a system press effect; opt in when needed. + const shouldApplyPressedStyle = + pressed && + ((Platform.OS === 'android' && enableAndroidRipple) || (Platform.OS === 'ios' && enableIOSPressOpacity)); + if (shouldApplyPressedStyle) base.push(styles.pressed); return base; }} disabled={disabled} onPress={onPress} - onLongPress={shouldOpenOnLongPress ? () => {} : undefined} + onPressIn={!shouldOpenOnLongPress ? handleMenuWillShow : undefined} + onLongPress={shouldOpenOnLongPress ? handleMenuWillShow : undefined} accessibilityLabel={accessibilityLabel} accessibilityHint={accessibilityHint} accessibilityRole={accessibilityRole} @@ -151,28 +153,24 @@ const ToolTipMenu = (props: ToolTipMenuProps) => { testID={testID} hitSlop={8} > - { - openedRef.current = true; - onMenuWillShow?.(); - }} - onCloseMenu={() => { + previewBackgroundColor="transparent" + onPress={handlePressMenuItemForMenuView} + onCancel={() => { if (!openedRef.current) { return; } openedRef.current = false; onMenuWillHide?.(); }} - onPressAction={handlePressMenuItemForMenuView} actions={Platform.OS === 'ios' ? menuViewItemsIOS : menuViewItemsAndroid} - shouldOpenOnLongPress={shouldOpenOnLongPress} + dropdownMenuMode={!shouldOpenOnLongPress} + disabled={disabled} style={buttonStyle ? styles.menuViewFlex : undefined} > {children} - + ); }; diff --git a/components/TransactionListItem.tsx b/components/TransactionListItem.tsx index 799eb78fe..bec36aa2e 100644 --- a/components/TransactionListItem.tsx +++ b/components/TransactionListItem.tsx @@ -452,7 +452,7 @@ export const TransactionListItem: React.FC = memo( accessibilityLabel={`${transactionTypeLabel}, ${amountWithUnit}, ${subtitle ?? title}`} accessibilityRole="button" > - {/* @ts-ignore - MenuView types can be overly strict about child element props */} + {/* @ts-ignore - Context menu wrapper types can be overly strict about child element props */} void; onPressMenuItem: (id: string) => void; title?: string; diff --git a/ios/Podfile.lock b/ios/Podfile.lock index d673e1b00..b4fc011dd 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1987,6 +1987,8 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga + - react-native-context-menu-view (1.21.0): + - React - react-native-document-picker (12.0.1): - boost - DoubleConversion @@ -2045,34 +2047,6 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - react-native-menu (2.0.0): - - boost - - DoubleConversion - - fast_float - - fmt - - glog - - hermes-engine - - RCT-Folly - - RCT-Folly/Fabric - - RCTRequired - - RCTTypeSafety - - React-Core - - React-debug - - React-Fabric - - React-featureflags - - React-graphics - - React-ImageManager - - React-jsi - - React-NativeModulesApple - - React-RCTFabric - - React-renderercss - - React-rendererdebug - - React-utils - - ReactCodegen - - ReactCommon/turbomodule/bridging - - ReactCommon/turbomodule/core - - SocketRocket - - Yoga - react-native-notifications (5.2.2): - React-Core - react-native-safe-area-context (5.7.0): @@ -3354,10 +3328,10 @@ DEPENDENCIES: - react-native-blue-crypto (from `../node_modules/react-native-blue-crypto`) - react-native-bw-file-access (from `../blue_modules/react-native-bw-file-access`) - react-native-capture-protection (from `../node_modules/react-native-capture-protection`) + - react-native-context-menu-view (from `../node_modules/react-native-context-menu-view`) - "react-native-document-picker (from `../node_modules/@react-native-documents/picker`)" - react-native-get-random-values (from `../node_modules/react-native-get-random-values`) - react-native-image-picker (from `../node_modules/react-native-image-picker`) - - "react-native-menu (from `../node_modules/@react-native-menu/menu`)" - react-native-notifications (from `../node_modules/react-native-notifications`) - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) - react-native-secure-key-store (from `../node_modules/react-native-secure-key-store`) @@ -3534,14 +3508,14 @@ EXTERNAL SOURCES: :path: "../blue_modules/react-native-bw-file-access" react-native-capture-protection: :path: "../node_modules/react-native-capture-protection" + react-native-context-menu-view: + :path: "../node_modules/react-native-context-menu-view" react-native-document-picker: :path: "../node_modules/@react-native-documents/picker" react-native-get-random-values: :path: "../node_modules/react-native-get-random-values" react-native-image-picker: :path: "../node_modules/react-native-image-picker" - react-native-menu: - :path: "../node_modules/@react-native-menu/menu" react-native-notifications: :path: "../node_modules/react-native-notifications" react-native-safe-area-context: @@ -3683,7 +3657,7 @@ SPEC CHECKSUMS: FBLazyVector: 309703e71d3f2f1ed7dc7889d58309c9d77a95a4 fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 - hermes-engine: 1195b00d0f49a367b01a38c87b43227542fc3532 + hermes-engine: b6ee973c4fd6366874d9aabcaf396d53475038eb lottie-ios: 8f959969761e9c45d70353667d00af0e5b9cadb3 lottie-react-native: 983fd0489530e8d40f173de7f04e2f88b9317a15 RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669 @@ -3726,10 +3700,10 @@ SPEC CHECKSUMS: react-native-blue-crypto: de5babd59b17fbf3fc31d2e1e5d59ec859093fbc react-native-bw-file-access: fe925b77dbf48500df0b294c6851f8c84607a203 react-native-capture-protection: 6d8d6a714114decb938d029f88e8df3da636e996 + react-native-context-menu-view: da39a4612c1294d651907162529cfba8421e9b89 react-native-document-picker: dc2d83366e47e89e7c51e8a41eab99c1d54e941c react-native-get-random-values: d16467cf726c618e9c7a8c3c39c31faa2244bbba react-native-image-picker: 0314366753615115fa55c3cc937ac44cb7e75702 - react-native-menu: 8d2c831a735c9e6528c28b26ca14e57591d87e14 react-native-notifications: e2d3c022d6077de7e420ba5c01b4bd9464f3941d react-native-safe-area-context: befb5404eb8a16fdc07fa2bebab3568ecabcbb8a react-native-secure-key-store: eb45b44bdec3f48e9be5cdfca0f49ddf64892ea6 diff --git a/package-lock.json b/package-lock.json index 372feb7a9..a5424b61d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,6 @@ "@react-native-community/cli-platform-android": "20.0.2", "@react-native-community/cli-platform-ios": "20.0.2", "@react-native-documents/picker": "12.0.1", - "@react-native-menu/menu": "2.0.0", "@react-native-vector-icons/entypo": "12.4.2", "@react-native-vector-icons/fontawesome": "12.4.2", "@react-native-vector-icons/fontawesome6": "12.3.2", @@ -76,6 +75,7 @@ "react-native-blue-crypto": "github:BlueWallet/react-native-blue-crypto#3cb5442", "react-native-camera-kit-no-google": "16.2.0", "react-native-capture-protection": "github:BlueWallet/react-native-capture-protection#bb78a40", + "react-native-context-menu-view": "github:BlueWallet/react-native-context-menu-view#main", "react-native-default-preference": "https://github.com/BlueWallet/react-native-default-preference.git#6338a1f1235e4130b8cfc2dd3b53015eeff2870c", "react-native-device-info": "14.1.1", "react-native-draggable-flatlist": "4.0.3", @@ -3624,16 +3624,6 @@ "react-native": ">=0.79.0" } }, - "node_modules/@react-native-menu/menu": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@react-native-menu/menu/-/menu-2.0.0.tgz", - "integrity": "sha512-hb8Mirw6aKPGONhgo52IiNpwHtISVrgCT3rMdFX1qS7eOFNzOcQh8d2UDnaH5zVpxN+QuvWtaaiRMGFpIjzdtA==", - "license": "MIT", - "peerDependencies": { - "react": "*", - "react-native": "*" - } - }, "node_modules/@react-native-vector-icons/common": { "version": "12.4.1", "resolved": "https://registry.npmjs.org/@react-native-vector-icons/common/-/common-12.4.1.tgz", @@ -15989,6 +15979,15 @@ } } }, + "node_modules/react-native-context-menu-view": { + "version": "1.21.0", + "resolved": "git+ssh://git@github.com/BlueWallet/react-native-context-menu-view.git#144110b02afdb11b431741aef5da95e91b942a9b", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-native": ">=0.60.0-rc.0 <1.0.x" + } + }, "node_modules/react-native-default-preference": { "version": "1.5.1", "resolved": "git+ssh://git@github.com/BlueWallet/react-native-default-preference.git#6338a1f1235e4130b8cfc2dd3b53015eeff2870c", diff --git a/package.json b/package.json index f83f7f7c0..62b08221e 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "@react-native-community/cli-platform-android": "20.0.2", "@react-native-community/cli-platform-ios": "20.0.2", "@react-native-documents/picker": "12.0.1", - "@react-native-menu/menu": "2.0.0", + "react-native-context-menu-view": "github:BlueWallet/react-native-context-menu-view#main", "@react-native-vector-icons/entypo": "12.4.2", "@react-native-vector-icons/fontawesome": "12.4.2", "@react-native-vector-icons/fontawesome6": "12.3.2", diff --git a/react-native.config.js b/react-native.config.js index cd0594f52..2fbdadc40 100644 --- a/react-native.config.js +++ b/react-native.config.js @@ -1,8 +1,23 @@ +const path = require('path'); + module.exports = { project: { android: {}, ios: {}, }, + dependencies: { + 'react-native-context-menu-view': { + root: path.resolve(__dirname, 'node_modules/react-native-context-menu-view'), + platforms: { + ios: { + podspecPath: path.resolve(__dirname, 'node_modules/react-native-context-menu-view/react-native-context-menu-view.podspec'), + }, + android: { + sourceDir: path.resolve(__dirname, 'node_modules/react-native-context-menu-view/android'), + }, + }, + }, + }, codegenConfig: { name: 'BlueWalletSpec', type: 'all',