Skip to content

Commit 751c7d6

Browse files
Merge pull request BlueWallet#7682 from BlueWallet/menud
REF: MenuItem memory
2 parents 0b1c3dd + c67eea8 commit 751c7d6

7 files changed

Lines changed: 293 additions & 145 deletions

File tree

hooks/useCompanionListeners.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ import ActionSheet from '../screen/ActionSheet';
2222
import { useStorage } from './context/useStorage';
2323
import RNQRGenerator from 'rn-qr-generator';
2424
import presentAlert from '../components/Alert';
25-
import useMenuElements from './useMenuElements';
2625
import useWidgetCommunication from './useWidgetCommunication';
2726
import useWatchConnectivity from './useWatchConnectivity';
2827
import useDeviceQuickActions from './useDeviceQuickActions';
2928
import useHandoffListener from './useHandoffListener';
29+
import useMenuElements from './useMenuElements';
3030

3131
const ClipboardContentType = Object.freeze({
3232
BITCOIN: 'BITCOIN',

hooks/useMenuElements.ios.ts

Lines changed: 128 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,140 +1,167 @@
1-
import { useCallback, useEffect, useMemo, useRef } from 'react';
1+
import { useEffect, useCallback } from 'react';
22
import { NativeEventEmitter, NativeModules, Platform } from 'react-native';
3+
import { navigationRef } from '../NavigationService';
34
import { CommonActions } from '@react-navigation/native';
4-
import * as NavigationService from '../NavigationService';
5-
import { useStorage } from './context/useStorage';
65

76
/*
87
Hook for managing iPadOS and macOS menu actions with keyboard shortcuts.
9-
Uses MenuElementsEmitter for event handling.
8+
Uses MenuElementsEmitter for event handling and navigation state.
109
*/
1110

12-
type MenuEventHandler = () => void;
11+
type MenuActionHandler = () => void;
1312

13+
// Singleton setup - initialize once at module level
1414
const { MenuElementsEmitter } = NativeModules;
1515
let eventEmitter: NativeEventEmitter | null = null;
16+
let listenersInitialized = false;
1617

17-
let globalReloadTransactionsFunction: MenuEventHandler | null = null;
18+
// Registry for transaction handlers by screen ID
19+
const handlerRegistry = new Map<string, MenuActionHandler>();
1820

19-
// Only create the emitter if the module exists and we're on iOS/macOS
21+
// Store subscription references for proper cleanup
22+
let subscriptions: { remove: () => void }[] = [];
23+
24+
// Create a more robust emitter with error handling
2025
try {
21-
if ((Platform.OS === 'ios' || Platform.OS === 'macos') && MenuElementsEmitter) {
26+
if (Platform.OS === 'ios' && MenuElementsEmitter) {
2227
eventEmitter = new NativeEventEmitter(MenuElementsEmitter);
2328
}
2429
} catch (error) {
30+
console.warn('[MenuElements] Failed to initialize event emitter: ', error);
2531
eventEmitter = null;
2632
}
2733

28-
// Empty function that does nothing - used as default
29-
const noop = () => {};
30-
31-
const useMenuElements = () => {
32-
const { walletsInitialized } = useStorage();
33-
const reloadTransactionsMenuActionRef = useRef<MenuEventHandler>(noop);
34-
// Track if listeners have been set up
35-
const listenersInitialized = useRef<boolean>(false);
36-
const listenersRef = useRef<any[]>([]);
37-
38-
const setReloadTransactionsMenuActionFunction = useCallback((handler: MenuEventHandler) => {
39-
if (typeof handler !== 'function') {
34+
/**
35+
* Safely navigate using multiple fallback approaches
36+
*/
37+
function safeNavigate(routeName: string, params?: Record<string, any>): void {
38+
try {
39+
if (navigationRef.current?.isReady()) {
40+
navigationRef.current.navigate(routeName as never, params as never);
4041
return;
4142
}
4243

43-
reloadTransactionsMenuActionRef.current = handler;
44-
globalReloadTransactionsFunction = handler;
45-
}, []);
44+
if (navigationRef.isReady()) {
45+
navigationRef.dispatch(
46+
CommonActions.navigate({
47+
name: routeName,
48+
params,
49+
}),
50+
);
51+
}
52+
} catch (error) {
53+
console.error(`[MenuElements] Navigation error:`, error);
54+
}
55+
}
4656

47-
const clearReloadTransactionsMenuAction = useCallback(() => {
48-
reloadTransactionsMenuActionRef.current = noop;
49-
}, []);
57+
// Cleanup event listeners to prevent memory leaks
58+
function cleanupListeners(): void {
59+
if (subscriptions.length > 0) {
60+
subscriptions.forEach(subscription => {
61+
try {
62+
subscription.remove();
63+
} catch (e) {
64+
console.warn('[MenuElements] Error removing subscription:', e);
65+
}
66+
});
67+
subscriptions = [];
68+
listenersInitialized = false;
69+
}
70+
}
5071

51-
const dispatchNavigate = useCallback((routeName: string, screen?: string) => {
52-
try {
53-
NavigationService.dispatch(CommonActions.navigate({ name: routeName, params: screen ? { screen } : undefined }));
54-
} catch (error) {
55-
// Navigation failed silently
56-
}
57-
}, []);
72+
function initializeListeners(): void {
73+
if (!eventEmitter || listenersInitialized) return;
5874

59-
const eventActions = useMemo(
60-
() => ({
61-
openSettings: () => {
62-
dispatchNavigate('Settings');
63-
},
64-
addWallet: () => {
65-
dispatchNavigate('AddWalletRoot');
66-
},
67-
importWallet: () => {
68-
dispatchNavigate('AddWalletRoot', 'ImportWallet');
69-
},
70-
reloadTransactions: () => {
71-
try {
72-
const handler = reloadTransactionsMenuActionRef.current || globalReloadTransactionsFunction || noop;
73-
handler();
74-
} catch (error) {
75-
// Execution failed silently
76-
}
77-
},
78-
}),
79-
[dispatchNavigate],
80-
);
75+
cleanupListeners();
8176

82-
useEffect(() => {
83-
// Skip if emitter doesn't exist or wallets aren't initialized yet
84-
if (!eventEmitter || !walletsInitialized) {
85-
return;
86-
}
77+
// Navigation actions
78+
const globalActions = {
79+
navigateToSettings: (): void => {
80+
safeNavigate('Settings');
81+
},
8782

88-
if (listenersInitialized.current) {
89-
return;
90-
}
83+
navigateToAddWallet: (): void => {
84+
safeNavigate('AddWalletRoot');
85+
},
86+
87+
navigateToImportWallet: (): void => {
88+
safeNavigate('AddWalletRoot', { screen: 'ImportWallet' });
89+
},
90+
91+
executeReloadTransactions: (): void => {
92+
const currentRoute = navigationRef.current?.getCurrentRoute();
93+
if (!currentRoute) return;
94+
95+
const screenName = currentRoute.name;
96+
const params = (currentRoute.params as { walletID?: string }) || {};
97+
const walletID = params.walletID;
98+
99+
const specificKey = walletID ? `${screenName}-${walletID}` : null;
100+
101+
const specificHandler = specificKey ? handlerRegistry.get(specificKey) : undefined;
102+
const genericHandler = handlerRegistry.get(screenName);
103+
const handler = specificHandler || genericHandler;
91104

92-
try {
93-
if (listenersRef.current.length > 0) {
94-
listenersRef.current.forEach(listener => listener?.remove?.());
95-
listenersRef.current = [];
105+
if (typeof handler === 'function') {
106+
handler();
96107
}
108+
},
109+
};
97110

98-
eventEmitter.removeAllListeners('openSettings');
99-
eventEmitter.removeAllListeners('addWalletMenuAction');
100-
eventEmitter.removeAllListeners('importWalletMenuAction');
101-
eventEmitter.removeAllListeners('reloadTransactionsMenuAction');
102-
} catch (error) {
103-
// Error cleanup silently ignored
104-
}
111+
try {
112+
subscriptions.push(eventEmitter.addListener('openSettings', globalActions.navigateToSettings));
113+
subscriptions.push(eventEmitter.addListener('addWalletMenuAction', globalActions.navigateToAddWallet));
114+
subscriptions.push(eventEmitter.addListener('importWalletMenuAction', globalActions.navigateToImportWallet));
115+
subscriptions.push(eventEmitter.addListener('reloadTransactionsMenuAction', globalActions.executeReloadTransactions));
116+
} catch (error) {
117+
console.error('[MenuElements] Error setting up event listeners:', error);
118+
}
105119

106-
try {
107-
const listeners = [
108-
eventEmitter.addListener('openSettings', eventActions.openSettings),
109-
eventEmitter.addListener('addWalletMenuAction', eventActions.addWallet),
110-
eventEmitter.addListener('importWalletMenuAction', eventActions.importWallet),
111-
eventEmitter.addListener('reloadTransactionsMenuAction', eventActions.reloadTransactions),
112-
];
113-
114-
listenersRef.current = listeners;
115-
listenersInitialized.current = true;
116-
} catch (error) {
117-
// Listener setup failed silently
118-
}
120+
listenersInitialized = true;
121+
}
122+
123+
interface MenuElementsHook {
124+
registerTransactionsHandler: (handler: MenuActionHandler, screenKey?: string) => boolean;
125+
unregisterTransactionsHandler: (screenKey: string) => void;
126+
isMenuElementsSupported: boolean;
127+
}
128+
129+
const mountedComponents = new Set<string>();
130+
131+
const useMenuElements = (): MenuElementsHook => {
132+
useEffect(() => {
133+
initializeListeners();
134+
135+
const unsubscribe = navigationRef.addListener('state', () => {});
119136

120137
return () => {
121-
try {
122-
listenersRef.current.forEach(listener => {
123-
if (listener && typeof listener.remove === 'function') {
124-
listener.remove();
125-
}
126-
});
127-
listenersRef.current = [];
128-
listenersInitialized.current = false;
129-
} catch (error) {
130-
// Cleanup error silently ignored
131-
}
138+
unsubscribe();
132139
};
133-
}, [walletsInitialized, eventActions]);
140+
}, []);
141+
142+
const registerTransactionsHandler = useCallback((handler: MenuActionHandler, screenKey?: string): boolean => {
143+
if (typeof handler !== 'function') return false;
144+
145+
const key = screenKey || navigationRef.current?.getCurrentRoute()?.name;
146+
if (!key) return false;
147+
148+
mountedComponents.add(key);
149+
150+
handlerRegistry.set(key, handler);
151+
152+
return true;
153+
}, []);
154+
155+
const unregisterTransactionsHandler = useCallback((screenKey: string): void => {
156+
if (!screenKey) return;
157+
158+
handlerRegistry.delete(screenKey);
159+
mountedComponents.delete(screenKey);
160+
}, []);
134161

135162
return {
136-
setReloadTransactionsMenuActionFunction,
137-
clearReloadTransactionsMenuAction,
163+
registerTransactionsHandler,
164+
unregisterTransactionsHandler,
138165
isMenuElementsSupported: !!eventEmitter,
139166
};
140167
};

hooks/useMenuElements.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,28 @@
1-
const useMenuElements = () => {
1+
import { useCallback } from 'react';
2+
3+
type MenuActionHandler = () => void;
4+
5+
interface MenuElementsHook {
6+
registerTransactionsHandler: (handler: MenuActionHandler, screenKey?: string) => boolean;
7+
unregisterTransactionsHandler: (screenKey: string) => void;
8+
isMenuElementsSupported: boolean;
9+
}
10+
11+
// Default implementation for platforms other than iOS
12+
const useMenuElements = (): MenuElementsHook => {
13+
const registerTransactionsHandler = useCallback((_handler: MenuActionHandler, _screenKey?: string): boolean => {
14+
// Non-functional stub for non-iOS platforms
15+
return false;
16+
}, []);
17+
18+
const unregisterTransactionsHandler = useCallback((_screenKey: string): void => {
19+
// No-op for non-supported platforms
20+
}, []);
21+
222
return {
3-
setReloadTransactionsMenuActionFunction: (_func: any) => {},
4-
clearReloadTransactionsMenuAction: () => {},
5-
isMenuElementsSupported: true,
23+
registerTransactionsHandler,
24+
unregisterTransactionsHandler,
25+
isMenuElementsSupported: false, // Not supported on platforms other than iOS
626
};
727
};
828

ios/BlueWallet/AppDelegate.mm

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -257,47 +257,55 @@ - (void)openSettings:(UIKeyCommand *)keyCommand {
257257
// Safely access the MenuElementsEmitter
258258
MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
259259
if (emitter) {
260+
NSLog(@"[MenuElements] AppDelegate: openSettings called, calling emitter");
261+
// Force on main thread for consistency
260262
dispatch_async(dispatch_get_main_queue(), ^{
261263
[emitter openSettings];
262264
});
263265
} else {
264-
NSLog(@"MenuElementsEmitter not available");
266+
NSLog(@"[MenuElements] AppDelegate: MenuElementsEmitter not available for openSettings");
265267
}
266268
}
267269

268270
- (void)addWalletAction:(UIKeyCommand *)keyCommand {
269271
// Safely access the MenuElementsEmitter
270272
MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
271273
if (emitter) {
274+
NSLog(@"[MenuElements] AppDelegate: addWalletAction called, calling emitter");
275+
// Force on main thread for consistency
272276
dispatch_async(dispatch_get_main_queue(), ^{
273277
[emitter addWalletMenuAction];
274278
});
275279
} else {
276-
NSLog(@"MenuElementsEmitter not available");
280+
NSLog(@"[MenuElements] AppDelegate: MenuElementsEmitter not available for addWalletAction");
277281
}
278282
}
279283

280284
- (void)importWalletAction:(UIKeyCommand *)keyCommand {
281285
// Safely access the MenuElementsEmitter
282286
MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
283287
if (emitter) {
288+
NSLog(@"[MenuElements] AppDelegate: importWalletAction called, calling emitter");
289+
// Force on main thread for consistency
284290
dispatch_async(dispatch_get_main_queue(), ^{
285291
[emitter importWalletMenuAction];
286292
});
287293
} else {
288-
NSLog(@"MenuElementsEmitter not available");
294+
NSLog(@"[MenuElements] AppDelegate: MenuElementsEmitter not available for importWalletAction");
289295
}
290296
}
291297

292298
- (void)reloadTransactionsAction:(UIKeyCommand *)keyCommand {
293299
// Safely access the MenuElementsEmitter
294300
MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
295301
if (emitter) {
302+
NSLog(@"[MenuElements] AppDelegate: reloadTransactionsAction called, calling emitter");
303+
// Force on main thread for consistency
296304
dispatch_async(dispatch_get_main_queue(), ^{
297305
[emitter reloadTransactionsMenuAction];
298306
});
299307
} else {
300-
NSLog(@"MenuElementsEmitter not available");
308+
NSLog(@"[MenuElements] AppDelegate: MenuElementsEmitter not available for reloadTransactionsAction");
301309
}
302310
}
303311

0 commit comments

Comments
 (0)