mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2025-03-13 11:09:20 +01:00
Compare commits
9 commits
Author | SHA1 | Date | |
---|---|---|---|
|
5f18540ca7 | ||
|
751c7d6f45 | ||
|
0b1c3dd9f7 | ||
|
ae89a59794 | ||
|
10b3432e0e | ||
|
c67eea8155 | ||
|
9421511f74 | ||
|
9ec0ef51e4 | ||
|
1cada11c50 |
14 changed files with 543 additions and 286 deletions
|
@ -87,7 +87,7 @@ android {
|
|||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "7.1.4"
|
||||
versionName "7.1.5"
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
}
|
||||
|
|
|
@ -89,6 +89,8 @@ const ManageWalletsListItem: React.FC<ManageWalletsListItemProps> = ({
|
|||
}) => {
|
||||
const { colors } = useTheme();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSwipeActive, setIsSwipeActive] = useState(false);
|
||||
const resetFunctionRef = useRef<(() => void) | null>(null);
|
||||
|
||||
const CARD_SORT_ACTIVE = 1.06;
|
||||
const INACTIVE_SCALE_WHEN_ACTIVE = 0.9;
|
||||
|
@ -125,24 +127,39 @@ const ManageWalletsListItem: React.FC<ManageWalletsListItemProps> = ({
|
|||
reset();
|
||||
};
|
||||
|
||||
const leftContent = (reset: () => void) => (
|
||||
<LeftSwipeContent onPress={() => handleLeftPress(reset)} hideBalance={(item.data as TWallet).hideBalance} colors={colors} />
|
||||
);
|
||||
|
||||
const handleRightPress = (reset: () => void) => {
|
||||
handleDeleteWallet(item.data as TWallet);
|
||||
reset();
|
||||
const leftContent = (reset: () => void) => {
|
||||
resetFunctionRef.current = reset;
|
||||
return <LeftSwipeContent onPress={() => handleLeftPress(reset)} hideBalance={(item.data as TWallet).hideBalance} colors={colors} />;
|
||||
};
|
||||
|
||||
const rightContent = (reset: () => void) => <RightSwipeContent onPress={() => handleRightPress(reset)} />;
|
||||
const handleRightPress = (reset: () => void) => {
|
||||
reset();
|
||||
|
||||
setTimeout(() => {
|
||||
handleDeleteWallet(item.data as TWallet);
|
||||
}, 100); // short delay to allow swipe reset animation to complete
|
||||
};
|
||||
|
||||
const rightContent = (reset: () => void) => {
|
||||
resetFunctionRef.current = reset;
|
||||
return <RightSwipeContent onPress={() => handleRightPress(reset)} />;
|
||||
};
|
||||
|
||||
const startDrag = useCallback(() => {
|
||||
if (isSwipeActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (resetFunctionRef.current) {
|
||||
resetFunctionRef.current();
|
||||
}
|
||||
|
||||
scaleValue.setValue(CARD_SORT_ACTIVE);
|
||||
triggerHapticFeedback(HapticFeedbackTypes.ImpactMedium);
|
||||
if (drag) {
|
||||
drag();
|
||||
}
|
||||
}, [CARD_SORT_ACTIVE, drag, scaleValue]);
|
||||
}, [CARD_SORT_ACTIVE, drag, scaleValue, isSwipeActive]);
|
||||
|
||||
if (isLoading) {
|
||||
return <ActivityIndicator size="large" color={colors.brandingColor} />;
|
||||
|
@ -155,23 +172,38 @@ const ManageWalletsListItem: React.FC<ManageWalletsListItemProps> = ({
|
|||
};
|
||||
|
||||
const backgroundColor = isActive || globalDragActive ? colors.brandingColor : colors.background;
|
||||
|
||||
const swipeDisabled = isActive || globalDragActive;
|
||||
|
||||
return (
|
||||
<Animated.View style={animatedStyle}>
|
||||
<ListItem.Swipeable
|
||||
leftWidth={80}
|
||||
rightWidth={90}
|
||||
containerStyle={[style, { backgroundColor }, isActive || globalDragActive ? styles.transparentBackground : {}]}
|
||||
leftContent={globalDragActive ? null : isActive ? null : leftContent}
|
||||
rightContent={globalDragActive ? null : isActive ? null : rightContent}
|
||||
leftWidth={swipeDisabled ? 0 : 80}
|
||||
rightWidth={swipeDisabled ? 0 : 90}
|
||||
containerStyle={[style, { backgroundColor }, swipeDisabled ? styles.transparentBackground : {}]}
|
||||
leftContent={swipeDisabled ? null : leftContent}
|
||||
rightContent={swipeDisabled ? null : rightContent}
|
||||
onPressOut={onPressOut}
|
||||
minSlideWidth={80}
|
||||
minSlideWidth={swipeDisabled ? 0 : 80}
|
||||
onPressIn={onPressIn}
|
||||
style={isActive || globalDragActive ? styles.transparentBackground : {}}
|
||||
style={swipeDisabled ? styles.transparentBackground : {}}
|
||||
onSwipeBegin={direction => {
|
||||
if (!swipeDisabled) {
|
||||
console.debug(`Swipe began: ${direction}`);
|
||||
setIsSwipeActive(true);
|
||||
}
|
||||
}}
|
||||
onSwipeEnd={() => {
|
||||
if (!swipeDisabled) {
|
||||
console.debug('Swipe ended');
|
||||
setIsSwipeActive(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ListItem.Content>
|
||||
<WalletCarouselItem
|
||||
item={item.data}
|
||||
handleLongPress={isDraggingDisabled ? undefined : startDrag}
|
||||
handleLongPress={isDraggingDisabled || isSwipeActive ? undefined : startDrag}
|
||||
onPress={onPress}
|
||||
onPressIn={onPressIn}
|
||||
onPressOut={onPressOut}
|
||||
|
|
|
@ -22,11 +22,11 @@ import ActionSheet from '../screen/ActionSheet';
|
|||
import { useStorage } from './context/useStorage';
|
||||
import RNQRGenerator from 'rn-qr-generator';
|
||||
import presentAlert from '../components/Alert';
|
||||
import useMenuElements from './useMenuElements';
|
||||
import useWidgetCommunication from './useWidgetCommunication';
|
||||
import useWatchConnectivity from './useWatchConnectivity';
|
||||
import useDeviceQuickActions from './useDeviceQuickActions';
|
||||
import useHandoffListener from './useHandoffListener';
|
||||
import useMenuElements from './useMenuElements';
|
||||
|
||||
const ClipboardContentType = Object.freeze({
|
||||
BITCOIN: 'BITCOIN',
|
||||
|
|
|
@ -1,140 +1,167 @@
|
|||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useEffect, useCallback } from 'react';
|
||||
import { NativeEventEmitter, NativeModules, Platform } from 'react-native';
|
||||
import { navigationRef } from '../NavigationService';
|
||||
import { CommonActions } from '@react-navigation/native';
|
||||
import * as NavigationService from '../NavigationService';
|
||||
import { useStorage } from './context/useStorage';
|
||||
|
||||
/*
|
||||
Hook for managing iPadOS and macOS menu actions with keyboard shortcuts.
|
||||
Uses MenuElementsEmitter for event handling.
|
||||
Uses MenuElementsEmitter for event handling and navigation state.
|
||||
*/
|
||||
|
||||
type MenuEventHandler = () => void;
|
||||
type MenuActionHandler = () => void;
|
||||
|
||||
// Singleton setup - initialize once at module level
|
||||
const { MenuElementsEmitter } = NativeModules;
|
||||
let eventEmitter: NativeEventEmitter | null = null;
|
||||
let listenersInitialized = false;
|
||||
|
||||
let globalReloadTransactionsFunction: MenuEventHandler | null = null;
|
||||
// Registry for transaction handlers by screen ID
|
||||
const handlerRegistry = new Map<string, MenuActionHandler>();
|
||||
|
||||
// Only create the emitter if the module exists and we're on iOS/macOS
|
||||
// Store subscription references for proper cleanup
|
||||
let subscriptions: { remove: () => void }[] = [];
|
||||
|
||||
// Create a more robust emitter with error handling
|
||||
try {
|
||||
if ((Platform.OS === 'ios' || Platform.OS === 'macos') && MenuElementsEmitter) {
|
||||
if (Platform.OS === 'ios' && MenuElementsEmitter) {
|
||||
eventEmitter = new NativeEventEmitter(MenuElementsEmitter);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[MenuElements] Failed to initialize event emitter: ', error);
|
||||
eventEmitter = null;
|
||||
}
|
||||
|
||||
// Empty function that does nothing - used as default
|
||||
const noop = () => {};
|
||||
|
||||
const useMenuElements = () => {
|
||||
const { walletsInitialized } = useStorage();
|
||||
const reloadTransactionsMenuActionRef = useRef<MenuEventHandler>(noop);
|
||||
// Track if listeners have been set up
|
||||
const listenersInitialized = useRef<boolean>(false);
|
||||
const listenersRef = useRef<any[]>([]);
|
||||
|
||||
const setReloadTransactionsMenuActionFunction = useCallback((handler: MenuEventHandler) => {
|
||||
if (typeof handler !== 'function') {
|
||||
/**
|
||||
* Safely navigate using multiple fallback approaches
|
||||
*/
|
||||
function safeNavigate(routeName: string, params?: Record<string, any>): void {
|
||||
try {
|
||||
if (navigationRef.current?.isReady()) {
|
||||
navigationRef.current.navigate(routeName as never, params as never);
|
||||
return;
|
||||
}
|
||||
|
||||
reloadTransactionsMenuActionRef.current = handler;
|
||||
globalReloadTransactionsFunction = handler;
|
||||
}, []);
|
||||
|
||||
const clearReloadTransactionsMenuAction = useCallback(() => {
|
||||
reloadTransactionsMenuActionRef.current = noop;
|
||||
}, []);
|
||||
|
||||
const dispatchNavigate = useCallback((routeName: string, screen?: string) => {
|
||||
try {
|
||||
NavigationService.dispatch(CommonActions.navigate({ name: routeName, params: screen ? { screen } : undefined }));
|
||||
} catch (error) {
|
||||
// Navigation failed silently
|
||||
if (navigationRef.isReady()) {
|
||||
navigationRef.dispatch(
|
||||
CommonActions.navigate({
|
||||
name: routeName,
|
||||
params,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
} catch (error) {
|
||||
console.error(`[MenuElements] Navigation error:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
const eventActions = useMemo(
|
||||
() => ({
|
||||
openSettings: () => {
|
||||
dispatchNavigate('Settings');
|
||||
},
|
||||
addWallet: () => {
|
||||
dispatchNavigate('AddWalletRoot');
|
||||
},
|
||||
importWallet: () => {
|
||||
dispatchNavigate('AddWalletRoot', 'ImportWallet');
|
||||
},
|
||||
reloadTransactions: () => {
|
||||
try {
|
||||
const handler = reloadTransactionsMenuActionRef.current || globalReloadTransactionsFunction || noop;
|
||||
handler();
|
||||
} catch (error) {
|
||||
// Execution failed silently
|
||||
}
|
||||
},
|
||||
}),
|
||||
[dispatchNavigate],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Skip if emitter doesn't exist or wallets aren't initialized yet
|
||||
if (!eventEmitter || !walletsInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (listenersInitialized.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (listenersRef.current.length > 0) {
|
||||
listenersRef.current.forEach(listener => listener?.remove?.());
|
||||
listenersRef.current = [];
|
||||
// Cleanup event listeners to prevent memory leaks
|
||||
function cleanupListeners(): void {
|
||||
if (subscriptions.length > 0) {
|
||||
subscriptions.forEach(subscription => {
|
||||
try {
|
||||
subscription.remove();
|
||||
} catch (e) {
|
||||
console.warn('[MenuElements] Error removing subscription:', e);
|
||||
}
|
||||
});
|
||||
subscriptions = [];
|
||||
listenersInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
eventEmitter.removeAllListeners('openSettings');
|
||||
eventEmitter.removeAllListeners('addWalletMenuAction');
|
||||
eventEmitter.removeAllListeners('importWalletMenuAction');
|
||||
eventEmitter.removeAllListeners('reloadTransactionsMenuAction');
|
||||
} catch (error) {
|
||||
// Error cleanup silently ignored
|
||||
}
|
||||
function initializeListeners(): void {
|
||||
if (!eventEmitter || listenersInitialized) return;
|
||||
|
||||
try {
|
||||
const listeners = [
|
||||
eventEmitter.addListener('openSettings', eventActions.openSettings),
|
||||
eventEmitter.addListener('addWalletMenuAction', eventActions.addWallet),
|
||||
eventEmitter.addListener('importWalletMenuAction', eventActions.importWallet),
|
||||
eventEmitter.addListener('reloadTransactionsMenuAction', eventActions.reloadTransactions),
|
||||
];
|
||||
cleanupListeners();
|
||||
|
||||
listenersRef.current = listeners;
|
||||
listenersInitialized.current = true;
|
||||
} catch (error) {
|
||||
// Listener setup failed silently
|
||||
}
|
||||
// Navigation actions
|
||||
const globalActions = {
|
||||
navigateToSettings: (): void => {
|
||||
safeNavigate('Settings');
|
||||
},
|
||||
|
||||
navigateToAddWallet: (): void => {
|
||||
safeNavigate('AddWalletRoot');
|
||||
},
|
||||
|
||||
navigateToImportWallet: (): void => {
|
||||
safeNavigate('AddWalletRoot', { screen: 'ImportWallet' });
|
||||
},
|
||||
|
||||
executeReloadTransactions: (): void => {
|
||||
const currentRoute = navigationRef.current?.getCurrentRoute();
|
||||
if (!currentRoute) return;
|
||||
|
||||
const screenName = currentRoute.name;
|
||||
const params = (currentRoute.params as { walletID?: string }) || {};
|
||||
const walletID = params.walletID;
|
||||
|
||||
const specificKey = walletID ? `${screenName}-${walletID}` : null;
|
||||
|
||||
const specificHandler = specificKey ? handlerRegistry.get(specificKey) : undefined;
|
||||
const genericHandler = handlerRegistry.get(screenName);
|
||||
const handler = specificHandler || genericHandler;
|
||||
|
||||
if (typeof handler === 'function') {
|
||||
handler();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
subscriptions.push(eventEmitter.addListener('openSettings', globalActions.navigateToSettings));
|
||||
subscriptions.push(eventEmitter.addListener('addWalletMenuAction', globalActions.navigateToAddWallet));
|
||||
subscriptions.push(eventEmitter.addListener('importWalletMenuAction', globalActions.navigateToImportWallet));
|
||||
subscriptions.push(eventEmitter.addListener('reloadTransactionsMenuAction', globalActions.executeReloadTransactions));
|
||||
} catch (error) {
|
||||
console.error('[MenuElements] Error setting up event listeners:', error);
|
||||
}
|
||||
|
||||
listenersInitialized = true;
|
||||
}
|
||||
|
||||
interface MenuElementsHook {
|
||||
registerTransactionsHandler: (handler: MenuActionHandler, screenKey?: string) => boolean;
|
||||
unregisterTransactionsHandler: (screenKey: string) => void;
|
||||
isMenuElementsSupported: boolean;
|
||||
}
|
||||
|
||||
const mountedComponents = new Set<string>();
|
||||
|
||||
const useMenuElements = (): MenuElementsHook => {
|
||||
useEffect(() => {
|
||||
initializeListeners();
|
||||
|
||||
const unsubscribe = navigationRef.addListener('state', () => {});
|
||||
|
||||
return () => {
|
||||
try {
|
||||
listenersRef.current.forEach(listener => {
|
||||
if (listener && typeof listener.remove === 'function') {
|
||||
listener.remove();
|
||||
}
|
||||
});
|
||||
listenersRef.current = [];
|
||||
listenersInitialized.current = false;
|
||||
} catch (error) {
|
||||
// Cleanup error silently ignored
|
||||
}
|
||||
unsubscribe();
|
||||
};
|
||||
}, [walletsInitialized, eventActions]);
|
||||
}, []);
|
||||
|
||||
const registerTransactionsHandler = useCallback((handler: MenuActionHandler, screenKey?: string): boolean => {
|
||||
if (typeof handler !== 'function') return false;
|
||||
|
||||
const key = screenKey || navigationRef.current?.getCurrentRoute()?.name;
|
||||
if (!key) return false;
|
||||
|
||||
mountedComponents.add(key);
|
||||
|
||||
handlerRegistry.set(key, handler);
|
||||
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
const unregisterTransactionsHandler = useCallback((screenKey: string): void => {
|
||||
if (!screenKey) return;
|
||||
|
||||
handlerRegistry.delete(screenKey);
|
||||
mountedComponents.delete(screenKey);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
setReloadTransactionsMenuActionFunction,
|
||||
clearReloadTransactionsMenuAction,
|
||||
registerTransactionsHandler,
|
||||
unregisterTransactionsHandler,
|
||||
isMenuElementsSupported: !!eventEmitter,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,8 +1,28 @@
|
|||
const useMenuElements = () => {
|
||||
import { useCallback } from 'react';
|
||||
|
||||
type MenuActionHandler = () => void;
|
||||
|
||||
interface MenuElementsHook {
|
||||
registerTransactionsHandler: (handler: MenuActionHandler, screenKey?: string) => boolean;
|
||||
unregisterTransactionsHandler: (screenKey: string) => void;
|
||||
isMenuElementsSupported: boolean;
|
||||
}
|
||||
|
||||
// Default implementation for platforms other than iOS
|
||||
const useMenuElements = (): MenuElementsHook => {
|
||||
const registerTransactionsHandler = useCallback((_handler: MenuActionHandler, _screenKey?: string): boolean => {
|
||||
// Non-functional stub for non-iOS platforms
|
||||
return false;
|
||||
}, []);
|
||||
|
||||
const unregisterTransactionsHandler = useCallback((_screenKey: string): void => {
|
||||
// No-op for non-supported platforms
|
||||
}, []);
|
||||
|
||||
return {
|
||||
setReloadTransactionsMenuActionFunction: (_func: any) => {},
|
||||
clearReloadTransactionsMenuAction: () => {},
|
||||
isMenuElementsSupported: true,
|
||||
registerTransactionsHandler,
|
||||
unregisterTransactionsHandler,
|
||||
isMenuElementsSupported: false, // Not supported on platforms other than iOS
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -1483,7 +1483,7 @@
|
|||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||
"$(inherited)",
|
||||
);
|
||||
MARKETING_VERSION = 7.1.4;
|
||||
MARKETING_VERSION = 7.1.5;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
|
@ -1541,7 +1541,7 @@
|
|||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||
"$(inherited)",
|
||||
);
|
||||
MARKETING_VERSION = 7.1.4;
|
||||
MARKETING_VERSION = 7.1.5;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
|
@ -1590,7 +1590,7 @@
|
|||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||
"$(inherited)",
|
||||
);
|
||||
MARKETING_VERSION = 7.1.4;
|
||||
MARKETING_VERSION = 7.1.5;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
||||
|
@ -1633,7 +1633,7 @@
|
|||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||
"$(inherited)",
|
||||
);
|
||||
MARKETING_VERSION = 7.1.4;
|
||||
MARKETING_VERSION = 7.1.5;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.Stickers;
|
||||
|
@ -1683,7 +1683,7 @@
|
|||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||
"$(inherited)",
|
||||
);
|
||||
MARKETING_VERSION = 7.1.4;
|
||||
MARKETING_VERSION = 7.1.5;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
||||
|
@ -1739,7 +1739,7 @@
|
|||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||
"$(inherited)",
|
||||
);
|
||||
MARKETING_VERSION = 7.1.4;
|
||||
MARKETING_VERSION = 7.1.5;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.MarketWidget;
|
||||
|
@ -1927,7 +1927,7 @@
|
|||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||
"$(inherited)",
|
||||
);
|
||||
MARKETING_VERSION = 7.1.4;
|
||||
MARKETING_VERSION = 7.1.5;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
||||
|
@ -1980,7 +1980,7 @@
|
|||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||
"$(inherited)",
|
||||
);
|
||||
MARKETING_VERSION = 7.1.4;
|
||||
MARKETING_VERSION = 7.1.5;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.watch.extension;
|
||||
|
@ -2026,7 +2026,7 @@
|
|||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||
"$(inherited)",
|
||||
);
|
||||
MARKETING_VERSION = 7.1.4;
|
||||
MARKETING_VERSION = 7.1.5;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
||||
|
@ -2075,7 +2075,7 @@
|
|||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||
"$(inherited)",
|
||||
);
|
||||
MARKETING_VERSION = 7.1.4;
|
||||
MARKETING_VERSION = 7.1.5;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.watch;
|
||||
|
|
|
@ -257,11 +257,13 @@
|
|||
// Safely access the MenuElementsEmitter
|
||||
MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
|
||||
if (emitter) {
|
||||
NSLog(@"[MenuElements] AppDelegate: openSettings called, calling emitter");
|
||||
// Force on main thread for consistency
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[emitter openSettings];
|
||||
});
|
||||
} else {
|
||||
NSLog(@"MenuElementsEmitter not available");
|
||||
NSLog(@"[MenuElements] AppDelegate: MenuElementsEmitter not available for openSettings");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -269,11 +271,13 @@
|
|||
// Safely access the MenuElementsEmitter
|
||||
MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
|
||||
if (emitter) {
|
||||
NSLog(@"[MenuElements] AppDelegate: addWalletAction called, calling emitter");
|
||||
// Force on main thread for consistency
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[emitter addWalletMenuAction];
|
||||
});
|
||||
} else {
|
||||
NSLog(@"MenuElementsEmitter not available");
|
||||
NSLog(@"[MenuElements] AppDelegate: MenuElementsEmitter not available for addWalletAction");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -281,11 +285,13 @@
|
|||
// Safely access the MenuElementsEmitter
|
||||
MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
|
||||
if (emitter) {
|
||||
NSLog(@"[MenuElements] AppDelegate: importWalletAction called, calling emitter");
|
||||
// Force on main thread for consistency
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[emitter importWalletMenuAction];
|
||||
});
|
||||
} else {
|
||||
NSLog(@"MenuElementsEmitter not available");
|
||||
NSLog(@"[MenuElements] AppDelegate: MenuElementsEmitter not available for importWalletAction");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -293,11 +299,13 @@
|
|||
// Safely access the MenuElementsEmitter
|
||||
MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
|
||||
if (emitter) {
|
||||
NSLog(@"[MenuElements] AppDelegate: reloadTransactionsAction called, calling emitter");
|
||||
// Force on main thread for consistency
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[emitter reloadTransactionsMenuAction];
|
||||
});
|
||||
} else {
|
||||
NSLog(@"MenuElementsEmitter not available");
|
||||
NSLog(@"[MenuElements] AppDelegate: MenuElementsEmitter not available for reloadTransactionsAction");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,14 +3,27 @@ import React
|
|||
|
||||
@objc(MenuElementsEmitter)
|
||||
class MenuElementsEmitter: RCTEventEmitter {
|
||||
private static var _sharedInstance: MenuElementsEmitter?
|
||||
// Use a weak reference for the singleton to prevent retain cycles
|
||||
private static weak var sharedInstance: MenuElementsEmitter?
|
||||
|
||||
// Use LRU cache with a max size to prevent unbounded growth
|
||||
private var lastEventTime: [String: TimeInterval] = [:]
|
||||
private let throttleInterval: TimeInterval = 0.3 // 300ms throttle
|
||||
private let maxCacheSize = 10 // Limit the cache size
|
||||
|
||||
// Track listener state without needing constant bridge access
|
||||
private var hasListeners = false
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
MenuElementsEmitter._sharedInstance = self
|
||||
NSLog("[MenuElements] Swift: Initialized MenuElementsEmitter instance")
|
||||
MenuElementsEmitter.sharedInstance = self
|
||||
NSLog("[MenuElements] MenuElementsEmitter initialized")
|
||||
}
|
||||
|
||||
deinit {
|
||||
NSLog("[MenuElements] MenuElementsEmitter deallocated")
|
||||
// Ensure all event listeners are removed in deinit
|
||||
self.removeAllListeners()
|
||||
}
|
||||
|
||||
override class func requiresMainQueueSetup() -> Bool {
|
||||
|
@ -22,54 +35,100 @@ class MenuElementsEmitter: RCTEventEmitter {
|
|||
}
|
||||
|
||||
@objc static func shared() -> MenuElementsEmitter? {
|
||||
return _sharedInstance
|
||||
if sharedInstance == nil {
|
||||
NSLog("[MenuElements] Warning: Attempting to use sharedInstance when it's nil")
|
||||
}
|
||||
return sharedInstance
|
||||
}
|
||||
|
||||
override func startObserving() {
|
||||
hasListeners = true
|
||||
NSLog("[MenuElements] Swift: Started observing events")
|
||||
NSLog("[MenuElements] Started observing events, bridge: \(self.bridge != nil ? "available" : "unavailable")")
|
||||
}
|
||||
|
||||
override func stopObserving() {
|
||||
hasListeners = false
|
||||
NSLog("[MenuElements] Swift: Stopped observing events")
|
||||
NSLog("[MenuElements] Stopped observing events")
|
||||
// Clear cache when stopping observation
|
||||
lastEventTime.removeAll()
|
||||
}
|
||||
|
||||
private func safelyEmitEvent(withName name: String) {
|
||||
if hasListeners && self.bridge != nil {
|
||||
NSLog("[MenuElements] Swift: Emitting event: %@", name)
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.sendEvent(withName: name, body: nil)
|
||||
private func limitCacheSize() {
|
||||
if lastEventTime.count > maxCacheSize {
|
||||
// Remove oldest entries if cache is too large
|
||||
let sortedKeys = lastEventTime.sorted(by: { $0.value < $1.value })
|
||||
for i in 0..<(lastEventTime.count - maxCacheSize) {
|
||||
lastEventTime.removeValue(forKey: sortedKeys[i].key)
|
||||
}
|
||||
} else {
|
||||
NSLog("[MenuElements] Swift: Cannot emit %@ event. %@", name, !hasListeners ? "No listeners" : "Bridge not ready")
|
||||
}
|
||||
}
|
||||
|
||||
private func canEmitEvent(named eventName: String) -> Bool {
|
||||
let now = Date().timeIntervalSince1970
|
||||
|
||||
if let lastTime = lastEventTime[eventName], now - lastTime < throttleInterval {
|
||||
NSLog("[MenuElements] Throttling event: \(eventName)")
|
||||
return false
|
||||
}
|
||||
|
||||
lastEventTime[eventName] = now
|
||||
limitCacheSize() // Keep cache size in check
|
||||
|
||||
let canEmit = hasListeners && bridge != nil
|
||||
if (!canEmit) {
|
||||
NSLog("[MenuElements] Cannot emit event: \(eventName), hasListeners: \(hasListeners), bridge: \(bridge != nil ? "available" : "unavailable")")
|
||||
}
|
||||
|
||||
return canEmit
|
||||
}
|
||||
|
||||
private func safelyEmitEvent(withName name: String) {
|
||||
guard canEmitEvent(named: name) else { return }
|
||||
|
||||
NSLog("[MenuElements] Emitting event: \(name)")
|
||||
|
||||
// Use weak self to avoid retain cycles
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self, self.bridge != nil, self.hasListeners else {
|
||||
NSLog("[MenuElements] Failed to emit event: \(name) - bridge or listeners not available")
|
||||
return
|
||||
}
|
||||
self.sendEvent(withName: name, body: nil)
|
||||
NSLog("[MenuElements] Event sent: \(name)")
|
||||
}
|
||||
}
|
||||
|
||||
func removeAllListeners() {
|
||||
NSLog("[MenuElements] Removing all listeners")
|
||||
// Clean up resources
|
||||
lastEventTime.removeAll()
|
||||
}
|
||||
|
||||
@objc func openSettings() {
|
||||
NSLog("[MenuElements] Swift: openSettings called")
|
||||
NSLog("[MenuElements] openSettings method called")
|
||||
safelyEmitEvent(withName: "openSettings")
|
||||
}
|
||||
|
||||
@objc func addWalletMenuAction() {
|
||||
NSLog("[MenuElements] Swift: addWalletMenuAction called")
|
||||
NSLog("[MenuElements] addWalletMenuAction method called")
|
||||
safelyEmitEvent(withName: "addWalletMenuAction")
|
||||
}
|
||||
|
||||
@objc func importWalletMenuAction() {
|
||||
NSLog("[MenuElements] Swift: importWalletMenuAction called")
|
||||
NSLog("[MenuElements] importWalletMenuAction method called")
|
||||
safelyEmitEvent(withName: "importWalletMenuAction")
|
||||
}
|
||||
|
||||
@objc func reloadTransactionsMenuAction() {
|
||||
NSLog("[MenuElements] Swift: reloadTransactionsMenuAction called")
|
||||
safelyEmitEvent(withName: "reloadTransactionsMenuAction")
|
||||
}
|
||||
|
||||
override func invalidate() {
|
||||
NSLog("[MenuElements] Swift: Module invalidated")
|
||||
MenuElementsEmitter._sharedInstance = nil
|
||||
NSLog("[MenuElements] Module invalidated")
|
||||
if MenuElementsEmitter.sharedInstance === self {
|
||||
MenuElementsEmitter.sharedInstance = nil
|
||||
}
|
||||
removeAllListeners()
|
||||
super.invalidate()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -364,7 +364,7 @@ const DetailViewStackScreensStack = () => {
|
|||
options={navigationStyle({
|
||||
headerBackVisible: false,
|
||||
gestureEnabled: false,
|
||||
presentation: 'containedModal',
|
||||
presentation: 'fullScreenModal',
|
||||
title: loc.wallets.manage_title,
|
||||
statusBarStyle: 'auto',
|
||||
})(theme)}
|
||||
|
|
6
package-lock.json
generated
6
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "bluewallet",
|
||||
"version": "7.1.4",
|
||||
"version": "7.1.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "bluewallet",
|
||||
"version": "7.1.4",
|
||||
"version": "7.1.5",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
@ -72,7 +72,7 @@
|
|||
"react-native-default-preference": "https://github.com/BlueWallet/react-native-default-preference.git#6338a1f1235e4130b8cfc2dd3b53015eeff2870c",
|
||||
"react-native-device-info": "14.0.4",
|
||||
"react-native-document-picker": "9.3.1",
|
||||
"react-native-draglist": "github:BlueWallet/react-native-draglist#8c52785",
|
||||
"react-native-draglist": "github:BlueWallet/react-native-draglist#2fb0c1f",
|
||||
"react-native-fs": "2.20.0",
|
||||
"react-native-gesture-handler": "2.23.1",
|
||||
"react-native-handoff": "github:BlueWallet/react-native-handoff#v0.0.4",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "bluewallet",
|
||||
"version": "7.1.4",
|
||||
"version": "7.1.5",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -140,7 +140,7 @@
|
|||
"react-native-default-preference": "https://github.com/BlueWallet/react-native-default-preference.git#6338a1f1235e4130b8cfc2dd3b53015eeff2870c",
|
||||
"react-native-device-info": "14.0.4",
|
||||
"react-native-document-picker": "9.3.1",
|
||||
"react-native-draglist": "github:BlueWallet/react-native-draglist#8c52785",
|
||||
"react-native-draglist": "github:BlueWallet/react-native-draglist#2fb0c1f",
|
||||
"react-native-fs": "2.20.0",
|
||||
"react-native-gesture-handler": "2.23.1",
|
||||
"react-native-handoff": "github:BlueWallet/react-native-handoff#v0.0.4",
|
||||
|
|
|
@ -7,9 +7,11 @@ import {
|
|||
Alert,
|
||||
I18nManager,
|
||||
Animated,
|
||||
LayoutAnimation,
|
||||
FlatList,
|
||||
ActivityIndicator,
|
||||
LayoutAnimation,
|
||||
UIManager,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
import { useFocusEffect, usePreventRemove } from '@react-navigation/native';
|
||||
|
@ -99,19 +101,21 @@ type Action =
|
|||
interface State {
|
||||
searchQuery: string;
|
||||
isSearchFocused: boolean;
|
||||
order: Item[];
|
||||
tempOrder: Item[];
|
||||
wallets: TWallet[];
|
||||
originalWalletsOrder: Item[];
|
||||
currentWalletsOrder: Item[];
|
||||
availableWallets: TWallet[];
|
||||
txMetadata: TTXMetadata;
|
||||
initialWalletsBackup: TWallet[];
|
||||
}
|
||||
|
||||
const initialState: State = {
|
||||
searchQuery: '',
|
||||
isSearchFocused: false,
|
||||
order: [],
|
||||
tempOrder: [],
|
||||
wallets: [],
|
||||
originalWalletsOrder: [],
|
||||
currentWalletsOrder: [],
|
||||
availableWallets: [],
|
||||
txMetadata: {},
|
||||
initialWalletsBackup: [],
|
||||
};
|
||||
|
||||
const deepCopyWallets = (wallets: TWallet[]): TWallet[] => {
|
||||
|
@ -131,21 +135,22 @@ const reducer = (state: State, action: Action): State => {
|
|||
}));
|
||||
return {
|
||||
...state,
|
||||
wallets: action.payload.wallets,
|
||||
availableWallets: action.payload.wallets,
|
||||
txMetadata: action.payload.txMetadata,
|
||||
order: initialWalletsOrder,
|
||||
tempOrder: initialWalletsOrder,
|
||||
originalWalletsOrder: initialWalletsOrder,
|
||||
currentWalletsOrder: initialWalletsOrder,
|
||||
initialWalletsBackup: deepCopyWallets(action.payload.wallets),
|
||||
};
|
||||
}
|
||||
case SET_FILTERED_ORDER: {
|
||||
const query = action.payload.toLowerCase();
|
||||
const filteredWallets = state.wallets
|
||||
const filteredWallets = state.availableWallets
|
||||
.filter(wallet => wallet.getLabel()?.toLowerCase().includes(query))
|
||||
.map(wallet => ({ type: ItemType.WalletSection, data: wallet }));
|
||||
|
||||
const filteredTxMetadata = Object.entries(state.txMetadata).filter(([_, tx]) => tx.memo?.toLowerCase().includes(query));
|
||||
|
||||
const filteredTransactions = state.wallets.flatMap(wallet =>
|
||||
const filteredTransactions = state.availableWallets.flatMap(wallet =>
|
||||
wallet
|
||||
.getTransactions()
|
||||
.filter((tx: Transaction) =>
|
||||
|
@ -158,14 +163,16 @@ const reducer = (state: State, action: Action): State => {
|
|||
|
||||
return {
|
||||
...state,
|
||||
tempOrder: filteredOrder,
|
||||
currentWalletsOrder: filteredOrder,
|
||||
};
|
||||
}
|
||||
case SAVE_CHANGES: {
|
||||
const savedWallets = deepCopyWallets(action.payload);
|
||||
return {
|
||||
...state,
|
||||
wallets: deepCopyWallets(action.payload),
|
||||
tempOrder: state.tempOrder.map(item =>
|
||||
availableWallets: savedWallets,
|
||||
initialWalletsBackup: savedWallets,
|
||||
currentWalletsOrder: state.currentWalletsOrder.map(item =>
|
||||
item.type === ItemType.WalletSection
|
||||
? { ...item, data: action.payload.find(wallet => wallet.getID() === item.data.getID())! }
|
||||
: item,
|
||||
|
@ -173,13 +180,15 @@ const reducer = (state: State, action: Action): State => {
|
|||
};
|
||||
}
|
||||
case SET_TEMP_ORDER: {
|
||||
return { ...state, tempOrder: action.payload };
|
||||
return { ...state, currentWalletsOrder: action.payload };
|
||||
}
|
||||
case REMOVE_WALLET: {
|
||||
const updatedOrder = state.tempOrder.filter(item => item.type !== ItemType.WalletSection || item.data.getID() !== action.payload);
|
||||
const updatedOrder = state.currentWalletsOrder.filter(
|
||||
item => item.type !== ItemType.WalletSection || item.data.getID() !== action.payload,
|
||||
);
|
||||
return {
|
||||
...state,
|
||||
tempOrder: updatedOrder,
|
||||
currentWalletsOrder: updatedOrder,
|
||||
};
|
||||
}
|
||||
default:
|
||||
|
@ -187,12 +196,16 @@ const reducer = (state: State, action: Action): State => {
|
|||
}
|
||||
};
|
||||
|
||||
if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
|
||||
UIManager.setLayoutAnimationEnabledExperimental(true);
|
||||
}
|
||||
|
||||
const ManageWallets: React.FC = () => {
|
||||
const { colors, closeImage } = useTheme();
|
||||
const { wallets: storedWallets, setWalletsWithNewOrder, txMetadata, handleWalletDeletion } = useStorage();
|
||||
const { wallets: persistedWallets, setWalletsWithNewOrder, txMetadata, handleWalletDeletion } = useStorage();
|
||||
const { setIsDrawerShouldHide } = useSettings();
|
||||
const walletsRef = useRef<TWallet[]>(deepCopyWallets(storedWallets)); // Create a deep copy of wallets for the DraggableFlatList
|
||||
const { navigate, setOptions, goBack } = useExtendedNavigation();
|
||||
const initialWalletsRef = useRef<TWallet[]>(deepCopyWallets(persistedWallets));
|
||||
const { navigate, setOptions, goBack, dispatch: navigationDispatch } = useExtendedNavigation();
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
const debouncedSearchQuery = useDebounce(state.searchQuery, 300);
|
||||
const bounceAnim = useBounceAnimation(state.searchQuery);
|
||||
|
@ -204,76 +217,123 @@ const ManageWallets: React.FC = () => {
|
|||
color: colors.foregroundColor,
|
||||
},
|
||||
};
|
||||
const [data, setData] = useState(state.tempOrder);
|
||||
const [uiData, setUiData] = useState(state.currentWalletsOrder);
|
||||
|
||||
const listRef = useRef<FlatList<Item> | null>(null);
|
||||
const [saveInProgress, setSaveInProgress] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setData(state.tempOrder);
|
||||
}, [state.tempOrder]);
|
||||
setUiData(state.currentWalletsOrder);
|
||||
}, [state.currentWalletsOrder]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch({ type: SET_INITIAL_ORDER, payload: { wallets: walletsRef.current, txMetadata } });
|
||||
dispatch({ type: SET_INITIAL_ORDER, payload: { wallets: initialWalletsRef.current, txMetadata } });
|
||||
}, [txMetadata]);
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedSearchQuery) {
|
||||
dispatch({ type: SET_FILTERED_ORDER, payload: debouncedSearchQuery });
|
||||
} else {
|
||||
dispatch({ type: SET_TEMP_ORDER, payload: state.order });
|
||||
dispatch({ type: SET_TEMP_ORDER, payload: state.originalWalletsOrder });
|
||||
}
|
||||
}, [debouncedSearchQuery, state.order]);
|
||||
}, [debouncedSearchQuery, state.originalWalletsOrder]);
|
||||
|
||||
const hasUnsavedChanges = useMemo(() => {
|
||||
return JSON.stringify(walletsRef.current) !== JSON.stringify(state.tempOrder.map(item => item.data));
|
||||
}, [state.tempOrder]);
|
||||
const currentWalletIds = state.currentWalletsOrder
|
||||
.filter((item): item is WalletItem => item.type === ItemType.WalletSection)
|
||||
.map(item => item.data.getID());
|
||||
|
||||
usePreventRemove(hasUnsavedChanges, async () => {
|
||||
await new Promise<void>(resolve => {
|
||||
Alert.alert(loc._.discard_changes, loc._.discard_changes_explain, [
|
||||
{ text: loc._.cancel, style: 'cancel', onPress: () => resolve() },
|
||||
{ text: loc._.ok, style: 'default', onPress: () => resolve() },
|
||||
]);
|
||||
});
|
||||
const originalWalletIds = state.initialWalletsBackup.map(wallet => wallet.getID());
|
||||
|
||||
if (currentWalletIds.length !== originalWalletIds.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (let i = 0; i < currentWalletIds.length; i++) {
|
||||
if (currentWalletIds[i] !== originalWalletIds[i]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const modifiedWallets = state.currentWalletsOrder
|
||||
.filter((item): item is WalletItem => item.type === ItemType.WalletSection)
|
||||
.map(item => item.data);
|
||||
|
||||
for (const modifiedWallet of modifiedWallets) {
|
||||
const originalWallet = state.initialWalletsBackup.find(w => w.getID() === modifiedWallet.getID());
|
||||
if (originalWallet && originalWallet.hideBalance !== modifiedWallet.hideBalance) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}, [state.currentWalletsOrder, state.initialWalletsBackup]);
|
||||
|
||||
usePreventRemove(hasUnsavedChanges && !saveInProgress, ({ data: preventRemoveData }) => {
|
||||
Alert.alert(loc._.discard_changes, loc._.discard_changes_explain, [
|
||||
{ text: loc._.cancel, style: 'cancel' },
|
||||
{
|
||||
text: loc._.ok,
|
||||
style: 'destructive',
|
||||
onPress: () => navigationDispatch(preventRemoveData.action),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (saveInProgress) {
|
||||
goBack();
|
||||
setSaveInProgress(false);
|
||||
}
|
||||
}, [saveInProgress, goBack]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
if (state.searchQuery.length === 0 && !state.isSearchFocused) {
|
||||
const newWalletOrder = state.tempOrder
|
||||
const reorderedWallets = state.currentWalletsOrder
|
||||
.filter((item): item is WalletItem => item.type === ItemType.WalletSection)
|
||||
.map(item => item.data);
|
||||
|
||||
setWalletsWithNewOrder(newWalletOrder);
|
||||
const walletsToDelete = state.initialWalletsBackup.filter(
|
||||
originalWallet => !reorderedWallets.some(wallet => wallet.getID() === originalWallet.getID()),
|
||||
);
|
||||
|
||||
dispatch({ type: SAVE_CHANGES, payload: newWalletOrder });
|
||||
setWalletsWithNewOrder(reorderedWallets);
|
||||
dispatch({ type: SAVE_CHANGES, payload: reorderedWallets });
|
||||
initialWalletsRef.current = deepCopyWallets(reorderedWallets);
|
||||
|
||||
walletsRef.current = deepCopyWallets(newWalletOrder);
|
||||
|
||||
state.tempOrder.forEach(item => {
|
||||
if (item.type === ItemType.WalletSection && !newWalletOrder.some(wallet => wallet.getID() === item.data.getID())) {
|
||||
handleWalletDeletion(item.data.getID());
|
||||
}
|
||||
walletsToDelete.forEach(wallet => {
|
||||
handleWalletDeletion(wallet.getID());
|
||||
});
|
||||
|
||||
goBack();
|
||||
setSaveInProgress(true);
|
||||
} else {
|
||||
dispatch({ type: SET_SEARCH_QUERY, payload: '' });
|
||||
dispatch({ type: SET_IS_SEARCH_FOCUSED, payload: false });
|
||||
}
|
||||
}, [goBack, setWalletsWithNewOrder, state.searchQuery, state.isSearchFocused, state.tempOrder, handleWalletDeletion]);
|
||||
}, [
|
||||
setWalletsWithNewOrder,
|
||||
state.searchQuery,
|
||||
state.isSearchFocused,
|
||||
state.currentWalletsOrder,
|
||||
state.initialWalletsBackup,
|
||||
handleWalletDeletion,
|
||||
]);
|
||||
|
||||
const buttonOpacity = useMemo(() => ({ opacity: saveInProgress ? 0.5 : 1 }), [saveInProgress]);
|
||||
const HeaderLeftButton = useMemo(
|
||||
() => (
|
||||
<TouchableOpacity
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={loc._.close}
|
||||
style={styles.button}
|
||||
style={[styles.button, buttonOpacity]}
|
||||
onPress={goBack}
|
||||
disabled={saveInProgress}
|
||||
testID="NavigationCloseButton"
|
||||
>
|
||||
<Image source={closeImage} />
|
||||
</TouchableOpacity>
|
||||
),
|
||||
[goBack, closeImage],
|
||||
[buttonOpacity, goBack, saveInProgress, closeImage],
|
||||
);
|
||||
|
||||
const SaveButton = useMemo(
|
||||
|
@ -290,7 +350,6 @@ const ManageWallets: React.FC = () => {
|
|||
onBlur: () => dispatch({ type: SET_IS_SEARCH_FOCUSED, payload: false }),
|
||||
placeholder: loc.wallets.manage_wallets_search_placeholder,
|
||||
};
|
||||
|
||||
setOptions({
|
||||
headerLeft: () => HeaderLeftButton,
|
||||
headerRight: () => SaveButton,
|
||||
|
@ -329,19 +388,30 @@ const ManageWallets: React.FC = () => {
|
|||
[bounceAnim],
|
||||
);
|
||||
|
||||
const handleDeleteWallet = useCallback(
|
||||
async (wallet: TWallet) => {
|
||||
const deletionSucceeded = await handleWalletDeletion(wallet.getID());
|
||||
if (deletionSucceeded) {
|
||||
dispatch({ type: REMOVE_WALLET, payload: wallet.getID() });
|
||||
}
|
||||
},
|
||||
[handleWalletDeletion],
|
||||
);
|
||||
const handleDeleteWallet = useCallback(async (wallet: TWallet) => {
|
||||
LayoutAnimation.configureNext({
|
||||
duration: 300,
|
||||
create: {
|
||||
type: LayoutAnimation.Types.easeInEaseOut,
|
||||
property: LayoutAnimation.Properties.opacity,
|
||||
},
|
||||
update: {
|
||||
type: LayoutAnimation.Types.easeInEaseOut,
|
||||
property: LayoutAnimation.Properties.opacity,
|
||||
},
|
||||
delete: {
|
||||
type: LayoutAnimation.Types.easeInEaseOut,
|
||||
property: LayoutAnimation.Properties.opacity,
|
||||
duration: 200,
|
||||
},
|
||||
});
|
||||
|
||||
dispatch({ type: REMOVE_WALLET, payload: wallet.getID() });
|
||||
}, []);
|
||||
|
||||
const handleToggleHideBalance = useCallback(
|
||||
(wallet: TWallet) => {
|
||||
const updatedOrder = state.tempOrder.map(item => {
|
||||
const updatedOrder = state.currentWalletsOrder.map(item => {
|
||||
if (item.type === ItemType.WalletSection && item.data.getID() === wallet.getID()) {
|
||||
item.data.hideBalance = !item.data.hideBalance;
|
||||
return {
|
||||
|
@ -351,11 +421,10 @@ const ManageWallets: React.FC = () => {
|
|||
}
|
||||
return item;
|
||||
});
|
||||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
||||
|
||||
dispatch({ type: SET_TEMP_ORDER, payload: updatedOrder });
|
||||
},
|
||||
[state.tempOrder],
|
||||
[state.currentWalletsOrder],
|
||||
);
|
||||
|
||||
const navigateToWallet = useCallback(
|
||||
|
@ -373,13 +442,19 @@ const ManageWallets: React.FC = () => {
|
|||
const renderItem = useCallback(
|
||||
(info: DragListRenderItemInfo<Item>) => {
|
||||
const { item, onDragStart, isActive } = info;
|
||||
|
||||
const compatibleState = {
|
||||
wallets: state.availableWallets,
|
||||
searchQuery: state.searchQuery,
|
||||
};
|
||||
|
||||
return (
|
||||
<ManageWalletsListItem
|
||||
item={item}
|
||||
onPressIn={undefined}
|
||||
onPressOut={undefined}
|
||||
isDraggingDisabled={state.searchQuery.length > 0 || state.isSearchFocused}
|
||||
state={state}
|
||||
state={compatibleState}
|
||||
navigateToWallet={navigateToWallet}
|
||||
renderHighlightedText={renderHighlightedText}
|
||||
handleDeleteWallet={handleDeleteWallet}
|
||||
|
@ -389,33 +464,33 @@ const ManageWallets: React.FC = () => {
|
|||
/>
|
||||
);
|
||||
},
|
||||
[state, navigateToWallet, renderHighlightedText, handleDeleteWallet, handleToggleHideBalance],
|
||||
[
|
||||
state.availableWallets,
|
||||
state.searchQuery,
|
||||
state.isSearchFocused,
|
||||
navigateToWallet,
|
||||
renderHighlightedText,
|
||||
handleDeleteWallet,
|
||||
handleToggleHideBalance,
|
||||
],
|
||||
);
|
||||
|
||||
const onReordered = useCallback(
|
||||
(fromIndex: number, toIndex: number) => {
|
||||
const copy = [...state.order];
|
||||
const removed = copy.splice(fromIndex, 1);
|
||||
copy.splice(toIndex, 0, removed[0]);
|
||||
const updatedOrder = [...state.currentWalletsOrder];
|
||||
const removed = updatedOrder.splice(fromIndex, 1);
|
||||
updatedOrder.splice(toIndex, 0, removed[0]);
|
||||
|
||||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
||||
dispatch({ type: SET_TEMP_ORDER, payload: copy });
|
||||
dispatch({
|
||||
type: SET_INITIAL_ORDER,
|
||||
payload: {
|
||||
wallets: copy.filter(item => item.type === ItemType.WalletSection).map(item => item.data as TWallet),
|
||||
txMetadata: state.txMetadata,
|
||||
},
|
||||
});
|
||||
dispatch({ type: SET_TEMP_ORDER, payload: updatedOrder });
|
||||
},
|
||||
[state.order, state.txMetadata],
|
||||
[state.currentWalletsOrder],
|
||||
);
|
||||
|
||||
const keyExtractor = useCallback((item: Item, index: number) => index.toString(), []);
|
||||
|
||||
const renderHeader = useMemo(() => {
|
||||
if (!state.searchQuery) return null;
|
||||
const hasWallets = state.wallets.length > 0;
|
||||
const hasWallets = state.availableWallets.length > 0;
|
||||
const filteredTxMetadata = Object.entries(state.txMetadata).filter(([_, tx]) =>
|
||||
tx.memo?.toLowerCase().includes(state.searchQuery.toLowerCase()),
|
||||
);
|
||||
|
@ -425,7 +500,7 @@ const ManageWallets: React.FC = () => {
|
|||
!hasWallets &&
|
||||
!hasTransactions && <Text style={[styles.noResultsText, stylesHook.noResultsText]}>{loc.wallets.no_results_found}</Text>
|
||||
);
|
||||
}, [state.searchQuery, state.wallets.length, state.txMetadata, stylesHook.noResultsText]);
|
||||
}, [state.searchQuery, state.availableWallets.length, state.txMetadata, stylesHook.noResultsText]);
|
||||
|
||||
return (
|
||||
<Suspense fallback={<ActivityIndicator size="large" color={colors.brandingColor} />}>
|
||||
|
@ -437,7 +512,7 @@ const ManageWallets: React.FC = () => {
|
|||
automaticallyAdjustKeyboardInsets
|
||||
automaticallyAdjustsScrollIndicatorInsets
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
data={data}
|
||||
data={uiData}
|
||||
containerStyle={[{ backgroundColor: colors.background }, styles.root]}
|
||||
keyExtractor={keyExtractor}
|
||||
onReordered={onReordered}
|
||||
|
@ -460,35 +535,37 @@ const styles = StyleSheet.create({
|
|||
padding: 16,
|
||||
},
|
||||
noResultsText: {
|
||||
fontSize: 19,
|
||||
fontWeight: 'bold',
|
||||
writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr',
|
||||
textAlign: 'center',
|
||||
justifyContent: 'center',
|
||||
marginTop: 34,
|
||||
},
|
||||
highlightedContainer: {
|
||||
backgroundColor: 'white',
|
||||
borderColor: 'black',
|
||||
borderWidth: 1,
|
||||
borderRadius: 5,
|
||||
padding: 2,
|
||||
alignSelf: 'flex-start',
|
||||
textDecorationLine: 'underline',
|
||||
textDecorationStyle: 'double',
|
||||
textShadowColor: '#000',
|
||||
textShadowOffset: { width: 1, height: 1 },
|
||||
textShadowRadius: 1,
|
||||
},
|
||||
highlighted: {
|
||||
color: 'black',
|
||||
fontSize: 19,
|
||||
fontWeight: '600',
|
||||
},
|
||||
defaultText: {
|
||||
writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr',
|
||||
fontWeight: 'bold',
|
||||
fontSize: 19,
|
||||
},
|
||||
dimmedText: {
|
||||
opacity: 0.8,
|
||||
},
|
||||
defaultText: {
|
||||
fontSize: 19,
|
||||
fontWeight: '600',
|
||||
},
|
||||
highlighted: {
|
||||
fontSize: 19,
|
||||
fontWeight: '600',
|
||||
color: 'black',
|
||||
textShadowRadius: 1,
|
||||
textShadowOffset: { width: 1, height: 1 },
|
||||
textShadowColor: '#000',
|
||||
textDecorationStyle: 'double',
|
||||
textDecorationLine: 'underline',
|
||||
alignSelf: 'flex-start',
|
||||
padding: 2,
|
||||
borderRadius: 5,
|
||||
borderWidth: 1,
|
||||
borderColor: 'black',
|
||||
backgroundColor: 'white',
|
||||
},
|
||||
highlightedContainer: {
|
||||
alignSelf: 'flex-start',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -7,7 +7,6 @@ import {
|
|||
findNodeHandle,
|
||||
FlatList,
|
||||
I18nManager,
|
||||
InteractionManager,
|
||||
LayoutAnimation,
|
||||
PixelRatio,
|
||||
ScrollView,
|
||||
|
@ -53,12 +52,14 @@ const buttonFontSize =
|
|||
? 22
|
||||
: PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26);
|
||||
|
||||
type WalletTransactionsProps = NativeStackScreenProps<DetailViewStackParamList, 'WalletTransactions'>;
|
||||
type RouteProps = RouteProp<DetailViewStackParamList, 'WalletTransactions'>;
|
||||
|
||||
type WalletTransactionsProps = NativeStackScreenProps<DetailViewStackParamList, 'WalletTransactions'>;
|
||||
|
||||
type TransactionListItem = Transaction & { type: 'transaction' | 'header' };
|
||||
const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }) => {
|
||||
const { wallets, saveToDisk, setSelectedWalletID } = useStorage();
|
||||
const { setReloadTransactionsMenuActionFunction } = useMenuElements();
|
||||
const { registerTransactionsHandler, unregisterTransactionsHandler } = useMenuElements();
|
||||
const { isBiometricUseCapableAndEnabled } = useBiometrics();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { params, name } = useRoute<RouteProps>();
|
||||
|
@ -384,17 +385,27 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }) => {
|
|||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (wallet) {
|
||||
const screenKey = `WalletTransactions-${walletID}`;
|
||||
registerTransactionsHandler(() => refreshTransactions(true), screenKey);
|
||||
|
||||
return () => {
|
||||
unregisterTransactionsHandler(screenKey);
|
||||
};
|
||||
}
|
||||
}, [wallet, walletID, refreshTransactions, registerTransactionsHandler, unregisterTransactionsHandler]);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
const task = InteractionManager.runAfterInteractions(() => {
|
||||
setReloadTransactionsMenuActionFunction(() => refreshTransactions);
|
||||
});
|
||||
return () => {
|
||||
task.cancel();
|
||||
console.debug('Next screen is focused, clearing reloadTransactionsMenuActionFunction');
|
||||
setReloadTransactionsMenuActionFunction(() => {});
|
||||
};
|
||||
}, [setReloadTransactionsMenuActionFunction, refreshTransactions]),
|
||||
if (wallet) {
|
||||
const screenKey = `WalletTransactions-${walletID}`;
|
||||
|
||||
return () => {
|
||||
unregisterTransactionsHandler(screenKey);
|
||||
};
|
||||
}
|
||||
}, [wallet, walletID, unregisterTransactionsHandler]),
|
||||
);
|
||||
|
||||
const [balance, setBalance] = useState(wallet ? wallet.getBalance() : 0);
|
||||
|
|
|
@ -98,7 +98,7 @@ const WalletsList: React.FC = () => {
|
|||
const { isLargeScreen } = useIsLargeScreen();
|
||||
const walletsCarousel = useRef<any>();
|
||||
const currentWalletIndex = useRef<number>(0);
|
||||
const { setReloadTransactionsMenuActionFunction, clearReloadTransactionsMenuAction } = useMenuElements();
|
||||
const { registerTransactionsHandler, unregisterTransactionsHandler } = useMenuElements();
|
||||
const { wallets, getTransactions, getBalance, refreshAllWalletTransactions, setSelectedWalletID } = useStorage();
|
||||
const { isTotalBalanceEnabled, isElectrumDisabled } = useSettings();
|
||||
const { width } = useWindowDimensions();
|
||||
|
@ -159,19 +159,41 @@ const WalletsList: React.FC = () => {
|
|||
}
|
||||
}, [getBalance]);
|
||||
|
||||
useEffect(() => {
|
||||
const screenKey = route.name;
|
||||
console.log(`[WalletsList] Registering handler with key: ${screenKey}`);
|
||||
registerTransactionsHandler(onRefresh, screenKey);
|
||||
|
||||
return () => {
|
||||
console.log(`[WalletsList] Unmounting - cleaning up handler for: ${screenKey}`);
|
||||
unregisterTransactionsHandler(screenKey);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [onRefresh, registerTransactionsHandler, unregisterTransactionsHandler]);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
const screenKey = route.name;
|
||||
|
||||
return () => {
|
||||
console.log(`[WalletsList] Blurred - cleaning up handler for: ${screenKey}`);
|
||||
unregisterTransactionsHandler(screenKey);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [unregisterTransactionsHandler]),
|
||||
);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
const task = InteractionManager.runAfterInteractions(() => {
|
||||
setReloadTransactionsMenuActionFunction(onRefresh);
|
||||
verifyBalance();
|
||||
setSelectedWalletID(undefined);
|
||||
});
|
||||
|
||||
return () => {
|
||||
task.cancel();
|
||||
clearReloadTransactionsMenuAction();
|
||||
};
|
||||
}, [onRefresh, setReloadTransactionsMenuActionFunction, clearReloadTransactionsMenuAction, verifyBalance, setSelectedWalletID]),
|
||||
}, [verifyBalance, setSelectedWalletID]),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -207,6 +229,7 @@ const WalletsList: React.FC = () => {
|
|||
|
||||
useEffect(() => {
|
||||
refreshTransactions();
|
||||
// es-lint-disable-next-line react-hooks/exhaustive-deps
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue