mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2025-03-13 19:16:52 +01:00
Merge pull request #7620 from BlueWallet/usede
REF: use debounce in wallet transactions to avoid rapid reattempts
This commit is contained in:
commit
ad71dccd72
3 changed files with 105 additions and 104 deletions
|
@ -1,23 +1,27 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import debounce from '../blue_modules/debounce';
|
||||
|
||||
const useDebounce = <T>(value: T, delay: number): T => {
|
||||
// Overload signatures
|
||||
function useDebounce<T extends (...args: any[]) => any>(callback: T, delay: number): T;
|
||||
function useDebounce<T>(value: T, delay: number): T;
|
||||
|
||||
function useDebounce<T>(value: T, delay: number): T {
|
||||
const isFn = typeof value === 'function';
|
||||
|
||||
const debouncedFunction = useMemo(() => {
|
||||
return isFn ? debounce(value as unknown as (...args: any[]) => any, delay) : null;
|
||||
}, [isFn, value, delay]);
|
||||
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = debounce((val: T) => {
|
||||
setDebouncedValue(val);
|
||||
}, delay);
|
||||
if (!isFn) {
|
||||
const handler = setTimeout(() => setDebouncedValue(value), delay);
|
||||
return () => clearTimeout(handler);
|
||||
}
|
||||
}, [isFn, value, delay]);
|
||||
|
||||
handler(value);
|
||||
|
||||
|
||||
return () => {
|
||||
handler.cancel();
|
||||
};
|
||||
}, [value, delay]);
|
||||
|
||||
return debouncedValue;
|
||||
};
|
||||
return isFn ? (debouncedFunction as unknown as T) : debouncedValue;
|
||||
}
|
||||
|
||||
export default useDebounce;
|
||||
|
|
|
@ -12,18 +12,18 @@ import {
|
|||
ActivityIndicator,
|
||||
} from 'react-native';
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
import { useFocusEffect, useNavigation } from '@react-navigation/native';
|
||||
import { useFocusEffect, usePreventRemove } from '@react-navigation/native';
|
||||
import { useTheme } from '../../components/themes';
|
||||
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
|
||||
import loc from '../../loc';
|
||||
import { useStorage } from '../../hooks/context/useStorage';
|
||||
import useDebounce from '../../hooks/useDebounce';
|
||||
import { TTXMetadata } from '../../class';
|
||||
import { ExtendedTransaction, LightningTransaction, Transaction, TWallet } from '../../class/wallets/types';
|
||||
import useBounceAnimation from '../../hooks/useBounceAnimation';
|
||||
import HeaderRightButton from '../../components/HeaderRightButton';
|
||||
import { useSettings } from '../../hooks/context/useSettings';
|
||||
import DragList, { DragListRenderItemInfo } from 'react-native-draglist';
|
||||
import useDebounce from '../../hooks/useDebounce';
|
||||
|
||||
const ManageWalletsListItem = lazy(() => import('../../components/ManageWalletsListItem'));
|
||||
|
||||
|
@ -194,10 +194,8 @@ const ManageWallets: React.FC = () => {
|
|||
const walletsRef = useRef<TWallet[]>(deepCopyWallets(storedWallets)); // Create a deep copy of wallets for the DraggableFlatList
|
||||
const { navigate, setOptions, goBack } = useExtendedNavigation();
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
const navigation = useNavigation();
|
||||
const debouncedSearchQuery = useDebounce(state.searchQuery, 300);
|
||||
const bounceAnim = useBounceAnimation(state.searchQuery);
|
||||
const beforeRemoveListenerRef = useRef<(() => void) | null>(null);
|
||||
const stylesHook = {
|
||||
root: {
|
||||
backgroundColor: colors.elevated,
|
||||
|
@ -225,6 +223,19 @@ const ManageWallets: React.FC = () => {
|
|||
}
|
||||
}, [debouncedSearchQuery, state.order]);
|
||||
|
||||
const hasUnsavedChanges = useMemo(() => {
|
||||
return JSON.stringify(walletsRef.current) !== JSON.stringify(state.tempOrder.map(item => item.data));
|
||||
}, [state.tempOrder]);
|
||||
|
||||
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 handleClose = useCallback(() => {
|
||||
if (state.searchQuery.length === 0 && !state.isSearchFocused) {
|
||||
const newWalletOrder = state.tempOrder
|
||||
|
@ -243,20 +254,12 @@ const ManageWallets: React.FC = () => {
|
|||
}
|
||||
});
|
||||
|
||||
if (beforeRemoveListenerRef.current) {
|
||||
navigation.removeListener('beforeRemove', beforeRemoveListenerRef.current);
|
||||
}
|
||||
|
||||
goBack();
|
||||
} else {
|
||||
dispatch({ type: SET_SEARCH_QUERY, payload: '' });
|
||||
dispatch({ type: SET_IS_SEARCH_FOCUSED, payload: false });
|
||||
}
|
||||
}, [goBack, setWalletsWithNewOrder, state.searchQuery, state.isSearchFocused, state.tempOrder, navigation, handleWalletDeletion]);
|
||||
|
||||
const hasUnsavedChanges = useMemo(() => {
|
||||
return JSON.stringify(walletsRef.current) !== JSON.stringify(state.tempOrder.map(item => item.data));
|
||||
}, [state.tempOrder]);
|
||||
}, [goBack, setWalletsWithNewOrder, state.searchQuery, state.isSearchFocused, state.tempOrder, handleWalletDeletion]);
|
||||
|
||||
const HeaderLeftButton = useMemo(
|
||||
() => (
|
||||
|
@ -298,45 +301,12 @@ const ManageWallets: React.FC = () => {
|
|||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
setIsDrawerShouldHide(true);
|
||||
const beforeRemoveListener = (e: { preventDefault: () => void; data: { action: any } }) => {
|
||||
if (!hasUnsavedChanges) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
Alert.alert(loc._.discard_changes, loc._.discard_changes_explain, [
|
||||
{ text: loc._.cancel, style: 'cancel', onPress: () => {} },
|
||||
{
|
||||
text: loc._.ok,
|
||||
style: 'default',
|
||||
onPress: () => navigation.dispatch(e.data.action),
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
// @ts-ignore: fix later
|
||||
beforeRemoveListenerRef.current = beforeRemoveListener;
|
||||
|
||||
navigation.addListener('beforeRemove', beforeRemoveListener);
|
||||
|
||||
return () => {
|
||||
if (beforeRemoveListenerRef.current) {
|
||||
navigation.removeListener('beforeRemove', beforeRemoveListenerRef.current);
|
||||
}
|
||||
setIsDrawerShouldHide(false);
|
||||
};
|
||||
}, [hasUnsavedChanges, navigation, setIsDrawerShouldHide]),
|
||||
}, [setIsDrawerShouldHide]),
|
||||
);
|
||||
|
||||
// Ensure the listener is re-added every time there are unsaved changes
|
||||
useEffect(() => {
|
||||
if (beforeRemoveListenerRef.current) {
|
||||
navigation.removeListener('beforeRemove', beforeRemoveListenerRef.current);
|
||||
navigation.addListener('beforeRemove', beforeRemoveListenerRef.current);
|
||||
}
|
||||
}, [hasUnsavedChanges, navigation]);
|
||||
|
||||
const renderHighlightedText = useCallback(
|
||||
(text: string, query: string) => {
|
||||
const parts = text.split(new RegExp(`(${query})`, 'gi'));
|
||||
|
|
|
@ -71,6 +71,9 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }) => {
|
|||
const { colors } = useTheme();
|
||||
const { isElectrumDisabled } = useSettings();
|
||||
const walletActionButtonsRef = useRef<View>(null);
|
||||
const [lastFetchTimestamp, setLastFetchTimestamp] = useState(() => wallet?._lastTxFetch || 0);
|
||||
const [fetchFailures, setFetchFailures] = useState(0);
|
||||
const MAX_FAILURES = 3;
|
||||
|
||||
const stylesHook = StyleSheet.create({
|
||||
listHeaderText: {
|
||||
|
@ -126,51 +129,75 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }) => {
|
|||
}
|
||||
}, [getTransactions, limit, pageSize]);
|
||||
|
||||
const refreshTransactions = useCallback(async () => {
|
||||
console.debug('refreshTransactions, ', wallet?.getLabel());
|
||||
if (!wallet || isElectrumDisabled || isLoading) return;
|
||||
setIsLoading(true);
|
||||
let smthChanged = false;
|
||||
try {
|
||||
await BlueElectrum.waitTillConnected();
|
||||
if (wallet.allowBIP47() && wallet.isBIP47Enabled() && 'fetchBIP47SenderPaymentCodes' in wallet) {
|
||||
await wallet.fetchBIP47SenderPaymentCodes();
|
||||
}
|
||||
const oldBalance = wallet.getBalance();
|
||||
await wallet.fetchBalance();
|
||||
if (oldBalance !== wallet.getBalance()) smthChanged = true;
|
||||
const oldTxLen = wallet.getTransactions().length;
|
||||
await wallet.fetchTransactions();
|
||||
if ('fetchPendingTransactions' in wallet) {
|
||||
await wallet.fetchPendingTransactions();
|
||||
}
|
||||
if ('fetchUserInvoices' in wallet) {
|
||||
await wallet.fetchUserInvoices();
|
||||
}
|
||||
if (oldTxLen !== wallet.getTransactions().length) smthChanged = true;
|
||||
} catch (err) {
|
||||
presentAlert({ message: (err as Error).message, type: AlertType.Toast });
|
||||
} finally {
|
||||
if (smthChanged) {
|
||||
await saveToDisk();
|
||||
setLimit(prev => prev + pageSize);
|
||||
}
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [wallet, isElectrumDisabled, isLoading, saveToDisk, pageSize]);
|
||||
const refreshTransactions = useCallback(
|
||||
async (isManualRefresh = false) => {
|
||||
console.debug('refreshTransactions, ', wallet?.getLabel());
|
||||
if (!wallet || isElectrumDisabled || isLoading) return;
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
const task = InteractionManager.runAfterInteractions(() => {
|
||||
if (wallet && wallet.getLastTxFetch() === 0) {
|
||||
refreshTransactions();
|
||||
const MIN_REFRESH_INTERVAL = 5000; // 5 seconds
|
||||
if (!isManualRefresh && lastFetchTimestamp !== 0 && Date.now() - lastFetchTimestamp < MIN_REFRESH_INTERVAL) {
|
||||
return; // Prevent auto-refreshing if last fetch was too recent
|
||||
}
|
||||
|
||||
if (fetchFailures >= MAX_FAILURES && !isManualRefresh) {
|
||||
return; // Silently stop auto-retrying, but allow manual refresh
|
||||
}
|
||||
|
||||
// Only show loading indicator on manual refresh or after first successful fetch
|
||||
if (isManualRefresh || lastFetchTimestamp !== 0) {
|
||||
setIsLoading(true);
|
||||
}
|
||||
|
||||
let smthChanged = false;
|
||||
try {
|
||||
await BlueElectrum.waitTillConnected();
|
||||
if (wallet.allowBIP47() && wallet.isBIP47Enabled() && 'fetchBIP47SenderPaymentCodes' in wallet) {
|
||||
await wallet.fetchBIP47SenderPaymentCodes();
|
||||
}
|
||||
});
|
||||
const oldBalance = wallet.getBalance();
|
||||
await wallet.fetchBalance();
|
||||
if (oldBalance !== wallet.getBalance()) smthChanged = true;
|
||||
const oldTxLen = wallet.getTransactions().length;
|
||||
await wallet.fetchTransactions();
|
||||
if ('fetchPendingTransactions' in wallet) {
|
||||
await wallet.fetchPendingTransactions();
|
||||
}
|
||||
if ('fetchUserInvoices' in wallet) {
|
||||
await wallet.fetchUserInvoices();
|
||||
}
|
||||
if (oldTxLen !== wallet.getTransactions().length) smthChanged = true;
|
||||
|
||||
return () => task.cancel();
|
||||
}, [refreshTransactions, wallet]),
|
||||
// Success - reset failure counter and update timestamps
|
||||
setFetchFailures(0);
|
||||
const newTimestamp = Date.now();
|
||||
setLastFetchTimestamp(newTimestamp);
|
||||
wallet._lastTxFetch = newTimestamp;
|
||||
} catch (err) {
|
||||
setFetchFailures(prev => {
|
||||
const newFailures = prev + 1;
|
||||
// Only show error on final attempt for automatic refresh
|
||||
if ((isManualRefresh || newFailures === MAX_FAILURES) && newFailures >= MAX_FAILURES) {
|
||||
presentAlert({ message: (err as Error).message, type: AlertType.Toast });
|
||||
}
|
||||
return newFailures;
|
||||
});
|
||||
} finally {
|
||||
if (smthChanged) {
|
||||
await saveToDisk();
|
||||
setLimit(prev => prev + pageSize);
|
||||
}
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[wallet, isElectrumDisabled, isLoading, saveToDisk, pageSize, lastFetchTimestamp, fetchFailures],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (wallet && lastFetchTimestamp === 0 && !isLoading && !isElectrumDisabled) {
|
||||
refreshTransactions(false).catch(console.error);
|
||||
}
|
||||
}, [wallet, isElectrumDisabled, isLoading, refreshTransactions, lastFetchTimestamp]);
|
||||
|
||||
useEffect(() => {
|
||||
if (wallet) {
|
||||
setSelectedWalletID(walletID);
|
||||
|
@ -373,7 +400,7 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }) => {
|
|||
task.cancel();
|
||||
setReloadTransactionsMenuActionFunction(() => {});
|
||||
};
|
||||
}, [refreshTransactions, setReloadTransactionsMenuActionFunction]),
|
||||
}, [setReloadTransactionsMenuActionFunction, refreshTransactions]),
|
||||
);
|
||||
|
||||
const [balance, setBalance] = useState(wallet ? wallet.getBalance() : 0);
|
||||
|
@ -525,7 +552,7 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }) => {
|
|||
}
|
||||
refreshControl={
|
||||
!isDesktop && !isElectrumDisabled ? (
|
||||
<RefreshControl refreshing={isLoading} onRefresh={refreshTransactions} tintColor={colors.msSuccessCheck} />
|
||||
<RefreshControl refreshing={isLoading} onRefresh={() => refreshTransactions(true)} tintColor={colors.msSuccessCheck} />
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
|
|
Loading…
Add table
Reference in a new issue