FX: ManageFlatlist fixes
This commit is contained in:
parent
92a0d10ad8
commit
487a2828cd
@ -1,6 +1,7 @@
|
||||
import React, { useCallback, useRef, useEffect } from 'react';
|
||||
import { Text, Animated, StyleSheet, Platform, TextStyle } from 'react-native';
|
||||
import React, { useCallback, useMemo, useEffect } from 'react';
|
||||
import { Text, Animated, StyleSheet, Platform, TextStyle, View } from 'react-native';
|
||||
import { useTheme } from './themes';
|
||||
import useBounceAnimation from '../hooks/useBounceAnimation';
|
||||
|
||||
interface HighlightedTextProps {
|
||||
text: string;
|
||||
@ -13,6 +14,11 @@ interface HighlightedTextProps {
|
||||
highlightOnlyFirstMatch?: boolean;
|
||||
}
|
||||
|
||||
interface TextPart {
|
||||
text: string;
|
||||
isMatch: boolean;
|
||||
}
|
||||
|
||||
const HighlightedText: React.FC<HighlightedTextProps> = ({
|
||||
text,
|
||||
query,
|
||||
@ -24,244 +30,171 @@ const HighlightedText: React.FC<HighlightedTextProps> = ({
|
||||
highlightOnlyFirstMatch = false,
|
||||
}) => {
|
||||
const { colors } = useTheme();
|
||||
const internalBounceAnim = useRef(new Animated.Value(1)).current;
|
||||
|
||||
const internalBounceAnim = useBounceAnimation(query);
|
||||
const bounceAnim = externalBounceAnim || internalBounceAnim;
|
||||
|
||||
useEffect(() => {
|
||||
if (!externalBounceAnim && Platform.OS === 'ios' && query) {
|
||||
Animated.sequence([
|
||||
Animated.timing(bounceAnim, {
|
||||
toValue: 1.07,
|
||||
duration: 100,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.spring(bounceAnim, {
|
||||
toValue: 1,
|
||||
friction: 4,
|
||||
tension: 20,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start();
|
||||
}
|
||||
}, [query, bounceAnim, externalBounceAnim]);
|
||||
// Create the highlighted style object
|
||||
const highlightedStyle = useMemo(() => [
|
||||
styles.highlight,
|
||||
{
|
||||
backgroundColor: '#FFF5C0',
|
||||
color: colors.foregroundColor,
|
||||
borderColor: 'rgba(255, 255, 255, 0.5)',
|
||||
transform: Platform.OS === 'ios' ? [{ scale: bounceAnim }] : undefined,
|
||||
shadowColor: '#000000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 1,
|
||||
elevation: 2,
|
||||
},
|
||||
highlightStyle
|
||||
], [colors.foregroundColor, bounceAnim, highlightStyle]);
|
||||
|
||||
const renderHighlightedText = useCallback(() => {
|
||||
if (!query) {
|
||||
// Render an individual text part (highlighted or plain)
|
||||
const renderTextPart = useCallback((part: TextPart, index: number) => {
|
||||
if (part.isMatch) {
|
||||
return (
|
||||
<Text style={[styles.defaultText, style]} numberOfLines={numberOfLines}>
|
||||
{text}
|
||||
<Animated.View
|
||||
key={`highlight-container-${index}-${query}`} // Add query to key to force re-render when query changes
|
||||
style={[
|
||||
styles.highlightContainer,
|
||||
{
|
||||
transform: [{ scale: bounceAnim }],
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Animated.Text
|
||||
key={`highlight-${index}-${query}`} // Add query to key to force re-render when query changes
|
||||
style={highlightedStyle}
|
||||
>
|
||||
{part.text}
|
||||
</Animated.Text>
|
||||
</Animated.View>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Text
|
||||
key={`text-${index}-${query}`} // Add query to key to force re-render when query changes
|
||||
style={query ? styles.nonHighlightedText : undefined}
|
||||
>
|
||||
{part.text}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
}, [query, highlightedStyle, bounceAnim]);
|
||||
|
||||
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').trim();
|
||||
|
||||
if (escapedQuery === '') {
|
||||
return (
|
||||
<Text style={[styles.defaultText, style]} numberOfLines={numberOfLines}>
|
||||
{text}
|
||||
</Text>
|
||||
);
|
||||
// Process the text to find parts that should be highlighted
|
||||
const textParts = useMemo((): TextPart[] => {
|
||||
// If no query, return the full text as a single non-matched part
|
||||
if (!query || query.trim() === '') {
|
||||
return [{ text, isMatch: false }];
|
||||
}
|
||||
|
||||
try {
|
||||
if (escapedQuery.length === 1 && highlightOnlyFirstMatch) {
|
||||
const searchChar = caseSensitive ? escapedQuery : escapedQuery.toLowerCase();
|
||||
const processedText = caseSensitive ? text : text.toLowerCase();
|
||||
|
||||
const index = processedText.indexOf(searchChar);
|
||||
|
||||
if (index === -1) {
|
||||
return (
|
||||
<Text style={[styles.defaultText, style]} numberOfLines={numberOfLines}>
|
||||
{text}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
const before = text.substring(0, index);
|
||||
const match = text.substring(index, index + 1);
|
||||
const after = text.substring(index + 1);
|
||||
|
||||
return (
|
||||
<Text numberOfLines={numberOfLines} style={style || styles.defaultText}>
|
||||
{before}
|
||||
<Animated.View style={[styles.highlightedContainer, styles.singleCharacterContainer, { transform: [{ scale: bounceAnim }] }]}>
|
||||
<Text
|
||||
style={[
|
||||
styles.highlighted,
|
||||
Platform.OS === 'ios' ? styles.iOSHighlight : styles.androidHighlight,
|
||||
styles.singleCharacterText,
|
||||
highlightStyle,
|
||||
]}
|
||||
>
|
||||
{match}
|
||||
</Text>
|
||||
</Animated.View>
|
||||
{after}
|
||||
</Text>
|
||||
);
|
||||
const trimmedQuery = query.trim();
|
||||
if (trimmedQuery === '') {
|
||||
return [{ text, isMatch: false }];
|
||||
}
|
||||
|
||||
const queryWithFlexibleWhitespace = escapedQuery.replace(/\s+/g, '\\s+');
|
||||
|
||||
let regex: RegExp;
|
||||
try {
|
||||
regex = new RegExp(`(${queryWithFlexibleWhitespace})`, caseSensitive ? 'g' : 'gi');
|
||||
} catch (error) {
|
||||
return (
|
||||
<Text style={[styles.defaultText, style]} numberOfLines={numberOfLines}>
|
||||
{text}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
const parts = text.split(regex).filter(Boolean);
|
||||
|
||||
const isExactMatch = (part: string): boolean => {
|
||||
if (caseSensitive) {
|
||||
regex.lastIndex = 0;
|
||||
return regex.test(part);
|
||||
} else {
|
||||
return part.toLowerCase().includes(escapedQuery.toLowerCase());
|
||||
|
||||
const searchQuery = caseSensitive ? trimmedQuery : trimmedQuery.toLowerCase();
|
||||
const processedText = caseSensitive ? text : text.toLowerCase();
|
||||
|
||||
const parts: TextPart[] = [];
|
||||
let lastIndex = 0;
|
||||
let searchStartIndex = 0;
|
||||
|
||||
// Find all occurrences of the query
|
||||
while (true) {
|
||||
const matchIndex = processedText.indexOf(searchQuery, searchStartIndex);
|
||||
|
||||
// If no more matches, break out of the loop
|
||||
if (matchIndex === -1) {
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Text numberOfLines={numberOfLines} style={style}>
|
||||
{parts.map((part, index) => {
|
||||
const isMatch = isExactMatch(part);
|
||||
|
||||
if (isMatch) {
|
||||
const isFirstHighlight = index === 0 || !isExactMatch(parts[index - 1]);
|
||||
const isLastHighlight = index === parts.length - 1 || !isExactMatch(parts[index + 1]);
|
||||
|
||||
const highlightContainerStyle = [
|
||||
styles.highlightedContainer,
|
||||
isFirstHighlight && styles.firstHighlightContainer,
|
||||
isLastHighlight && styles.lastHighlightContainer,
|
||||
!isFirstHighlight && !isLastHighlight && styles.middleHighlightContainer,
|
||||
Platform.OS === 'ios' ? { transform: [{ scale: bounceAnim }] } : { backgroundColor: colors.brandingColor + '30' },
|
||||
];
|
||||
|
||||
const highlightTextStyle = [
|
||||
styles.highlighted,
|
||||
Platform.OS === 'ios' ? styles.iOSHighlight : styles.androidHighlight,
|
||||
isFirstHighlight && styles.firstHighlightText,
|
||||
isLastHighlight && styles.lastHighlightText,
|
||||
!isFirstHighlight && !isLastHighlight && styles.middleHighlightText,
|
||||
highlightStyle,
|
||||
];
|
||||
|
||||
return (
|
||||
<Animated.View key={`${index}-${part}`} style={highlightContainerStyle}>
|
||||
<Text style={highlightTextStyle}>{part}</Text>
|
||||
</Animated.View>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Text key={`${index}-${part}`} style={[query ? styles.dimmedText : styles.defaultText, style]}>
|
||||
{part}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</Text>
|
||||
);
|
||||
|
||||
// Add the text before the match
|
||||
if (matchIndex > lastIndex) {
|
||||
parts.push({
|
||||
text: text.substring(lastIndex, matchIndex),
|
||||
isMatch: false
|
||||
});
|
||||
}
|
||||
|
||||
// Add the exact matching text portion (using the original casing)
|
||||
parts.push({
|
||||
text: text.substring(matchIndex, matchIndex + searchQuery.length),
|
||||
isMatch: true
|
||||
});
|
||||
|
||||
// Update indices
|
||||
lastIndex = matchIndex + searchQuery.length;
|
||||
searchStartIndex = lastIndex;
|
||||
|
||||
// If we only want the first match, break
|
||||
if (highlightOnlyFirstMatch) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Add any remaining text
|
||||
if (lastIndex < text.length) {
|
||||
parts.push({
|
||||
text: text.substring(lastIndex),
|
||||
isMatch: false
|
||||
});
|
||||
}
|
||||
|
||||
return parts.length > 0 ? parts : [{ text, isMatch: false }];
|
||||
} catch (e) {
|
||||
console.warn('Error in HighlightedText:', e);
|
||||
return (
|
||||
<Text style={[styles.defaultText, style]} numberOfLines={numberOfLines}>
|
||||
{text}
|
||||
</Text>
|
||||
);
|
||||
console.warn('Error processing text for highlighting:', e);
|
||||
return [{ text, isMatch: false }];
|
||||
}
|
||||
}, [query, text, bounceAnim, colors.brandingColor, numberOfLines, style, highlightStyle, caseSensitive, highlightOnlyFirstMatch]);
|
||||
}, [text, query, caseSensitive, highlightOnlyFirstMatch]);
|
||||
|
||||
return renderHighlightedText();
|
||||
// If only one part and it's not a match, render a simple Text component
|
||||
if (textParts.length === 1 && !textParts[0].isMatch) {
|
||||
return (
|
||||
<Text style={[styles.text, style]} numberOfLines={numberOfLines}>
|
||||
{text}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
// Render the text with highlighted parts
|
||||
return (
|
||||
<Text
|
||||
numberOfLines={numberOfLines}
|
||||
style={[styles.text, style]}
|
||||
key={`highlighted-wrapper-${query}`} // Add query to key to force re-render when query changes
|
||||
>
|
||||
{textParts.map(renderTextPart)}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
dimmedText: {
|
||||
text: {
|
||||
fontSize: 16,
|
||||
},
|
||||
nonHighlightedText: {
|
||||
opacity: 0.8,
|
||||
},
|
||||
defaultText: {
|
||||
fontSize: 19,
|
||||
highlightContainer: {
|
||||
display: 'inline',
|
||||
overflow: 'hidden',
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
},
|
||||
highlight: {
|
||||
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: 4,
|
||||
borderWidth: 1,
|
||||
borderColor: 'black',
|
||||
backgroundColor: 'white',
|
||||
},
|
||||
highlightedContainer: {
|
||||
alignSelf: 'flex-start',
|
||||
},
|
||||
iOSHighlight: {
|
||||
backgroundColor: 'white',
|
||||
borderWidth: 1,
|
||||
borderColor: 'black',
|
||||
textShadowRadius: 1,
|
||||
textShadowOffset: { width: 1, height: 1 },
|
||||
textShadowColor: '#000',
|
||||
},
|
||||
androidHighlight: {
|
||||
backgroundColor: 'transparent',
|
||||
borderWidth: 0,
|
||||
color: 'black',
|
||||
fontWeight: '700',
|
||||
textDecorationLine: 'underline',
|
||||
textShadowRadius: 0,
|
||||
},
|
||||
singleCharacterContainer: {
|
||||
borderRadius: 5,
|
||||
},
|
||||
singleCharacterText: {
|
||||
borderRadius: 5,
|
||||
},
|
||||
firstHighlightContainer: {
|
||||
borderTopLeftRadius: 5,
|
||||
borderBottomLeftRadius: 5,
|
||||
},
|
||||
lastHighlightContainer: {
|
||||
borderTopRightRadius: 5,
|
||||
borderBottomRightRadius: 5,
|
||||
},
|
||||
middleHighlightContainer: {
|
||||
borderRadius: 0,
|
||||
},
|
||||
firstHighlightText: {
|
||||
borderTopLeftRadius: 5,
|
||||
borderBottomLeftRadius: 5,
|
||||
borderLeftWidth: 1,
|
||||
marginLeft: 0,
|
||||
borderRadius: 0,
|
||||
},
|
||||
lastHighlightText: {
|
||||
borderTopRightRadius: 5,
|
||||
borderBottomRightRadius: 5,
|
||||
borderRightWidth: 1,
|
||||
marginRight: 0,
|
||||
borderRadius: 0,
|
||||
},
|
||||
middleHighlightText: {
|
||||
borderRadius: 0,
|
||||
borderLeftWidth: 0,
|
||||
borderRightWidth: 0,
|
||||
marginLeft: -1,
|
||||
marginRight: -1,
|
||||
},
|
||||
paddingHorizontal: 3,
|
||||
paddingVertical: 1,
|
||||
marginHorizontal: 1,
|
||||
overflow: 'hidden',
|
||||
textDecorationLine: Platform.OS === 'android' ? 'underline' : 'none',
|
||||
}
|
||||
});
|
||||
|
||||
export default HighlightedText;
|
||||
|
||||
@ -291,7 +291,9 @@ const ManageWalletsListItem: React.FC<ManageWalletsListItemProps> = ({
|
||||
balanceUnit: wallet.getPreferredBalanceUnit() || BitcoinUnit.BTC,
|
||||
walletID: item.data.walletID,
|
||||
allowSignVerifyMessage: wallet.allowSignVerifyMessage ? wallet.allowSignVerifyMessage() : false,
|
||||
onPress: () => navigateToAddress && navigateToAddress(item.data.address, item.data.walletID),
|
||||
onPress: navigateToAddress ? () => navigateToAddress(item.data.address, item.data.walletID) : undefined,
|
||||
searchQuery: state.searchQuery,
|
||||
renderHighlightedText: renderHighlightedText,
|
||||
};
|
||||
|
||||
return (
|
||||
@ -478,7 +480,10 @@ const WalletGroupComponent: React.FC<WalletGroupProps> = ({
|
||||
balanceUnit: wallet.getPreferredBalanceUnit() || BitcoinUnit.BTC,
|
||||
walletID: address.data.walletID,
|
||||
allowSignVerifyMessage: wallet.allowSignVerifyMessage ? wallet.allowSignVerifyMessage() : false,
|
||||
onPress: () => navigateToAddress && navigateToAddress(address.data.address, address.data.walletID),
|
||||
// Use the onPress function returned by navigateToAddress instead of calling it directly
|
||||
onPress: navigateToAddress ? () => navigateToAddress(address.data.address, address.data.walletID) : undefined,
|
||||
searchQuery: state.searchQuery,
|
||||
renderHighlightedText: renderHighlightedText,
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@ -34,12 +34,13 @@ interface TransactionListItemProps {
|
||||
searchQuery?: string;
|
||||
style?: ViewStyle;
|
||||
renderHighlightedText?: (text: string, query: string) => JSX.Element;
|
||||
onPress?: () => void;
|
||||
}
|
||||
|
||||
type NavigationProps = NativeStackNavigationProp<DetailViewStackParamList>;
|
||||
|
||||
export const TransactionListItem: React.FC<TransactionListItemProps> = memo(
|
||||
({ item, itemPriceUnit = BitcoinUnit.BTC, walletID, searchQuery, style, renderHighlightedText }) => {
|
||||
({ item, itemPriceUnit = BitcoinUnit.BTC, walletID, searchQuery, style, renderHighlightedText, onPress: customOnPress }) => {
|
||||
const [subtitleNumberOfLines, setSubtitleNumberOfLines] = useState(1);
|
||||
const { colors } = useTheme();
|
||||
const { navigate } = useExtendedNavigation<NavigationProps>();
|
||||
@ -221,6 +222,12 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = memo(
|
||||
|
||||
const onPress = useCallback(async () => {
|
||||
menuRef?.current?.dismissMenu?.();
|
||||
// If a custom onPress handler was provided, use it and return
|
||||
if (customOnPress) {
|
||||
customOnPress();
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.hash) {
|
||||
if (renderHighlightedText) {
|
||||
pop();
|
||||
@ -258,7 +265,7 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = memo(
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [item, renderHighlightedText, navigate, walletID, wallets]);
|
||||
}, [item, renderHighlightedText, navigate, walletID, wallets, customOnPress]);
|
||||
|
||||
const handleOnExpandNote = useCallback(() => {
|
||||
setSubtitleNumberOfLines(0);
|
||||
@ -403,7 +410,8 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = memo(
|
||||
prevProps.item.hash === nextProps.item.hash &&
|
||||
prevProps.item.received === nextProps.item.received &&
|
||||
prevProps.itemPriceUnit === nextProps.itemPriceUnit &&
|
||||
prevProps.walletID === nextProps.walletID
|
||||
prevProps.walletID === nextProps.walletID &&
|
||||
prevProps.searchQuery === nextProps.searchQuery
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@ -18,6 +18,7 @@ import { useStorage } from '../../hooks/context/useStorage';
|
||||
import ToolTipMenu from '../TooltipMenu';
|
||||
import { CommonToolTipActions } from '../../typings/CommonToolTipActions';
|
||||
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
|
||||
import HighlightedText from '../HighlightedText';
|
||||
|
||||
interface AddressItemProps {
|
||||
item: any;
|
||||
@ -25,11 +26,21 @@ interface AddressItemProps {
|
||||
walletID: string;
|
||||
allowSignVerifyMessage: boolean;
|
||||
onPress?: () => void; // example: ManageWallets uses this
|
||||
searchQuery?: string;
|
||||
renderHighlightedText?: (text: string, query: string) => JSX.Element;
|
||||
}
|
||||
|
||||
type NavigationProps = NativeStackNavigationProp<DetailViewStackParamList>;
|
||||
|
||||
const AddressItem = ({ item, balanceUnit, walletID, allowSignVerifyMessage, onPress }: AddressItemProps) => {
|
||||
const AddressItem = ({
|
||||
item,
|
||||
balanceUnit,
|
||||
walletID,
|
||||
allowSignVerifyMessage,
|
||||
onPress,
|
||||
searchQuery = '',
|
||||
renderHighlightedText
|
||||
}: AddressItemProps) => {
|
||||
const { wallets } = useStorage();
|
||||
const { colors } = useTheme();
|
||||
const { isBiometricUseCapableAndEnabled } = useBiometrics();
|
||||
@ -59,12 +70,9 @@ const AddressItem = ({ item, balanceUnit, walletID, allowSignVerifyMessage, onPr
|
||||
if (onPress) {
|
||||
onPress();
|
||||
} else {
|
||||
navigate('ReceiveDetailsRoot', {
|
||||
screen: 'ReceiveDetails',
|
||||
params: {
|
||||
walletID,
|
||||
address: item.address,
|
||||
},
|
||||
navigate('ReceiveDetails', {
|
||||
walletID,
|
||||
address: item.address,
|
||||
});
|
||||
}
|
||||
}, [navigate, walletID, item.address, onPress]);
|
||||
@ -149,6 +157,30 @@ const AddressItem = ({ item, balanceUnit, walletID, allowSignVerifyMessage, onPr
|
||||
|
||||
const renderPreview = useCallback(() => <QRCodeComponent value={item.address} isMenuAvailable={false} />, [item.address]);
|
||||
|
||||
// Render address with highlighting if a search query is provided
|
||||
const renderAddressContent = () => {
|
||||
if (searchQuery && searchQuery.length > 0) {
|
||||
if (renderHighlightedText) {
|
||||
return renderHighlightedText(item.address, searchQuery);
|
||||
}
|
||||
return (
|
||||
<HighlightedText
|
||||
text={item.address}
|
||||
query={searchQuery}
|
||||
caseSensitive={false}
|
||||
highlightOnlyFirstMatch={searchQuery.length === 1}
|
||||
style={[stylesHook.address, styles.address]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Text style={[stylesHook.address, styles.address]} numberOfLines={1} ellipsizeMode="middle">
|
||||
{item.address}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ToolTipMenu
|
||||
title={item.address}
|
||||
@ -166,9 +198,7 @@ const AddressItem = ({ item, balanceUnit, walletID, allowSignVerifyMessage, onPr
|
||||
<Text style={[styles.index, stylesHook.index]}>{item.index}</Text>
|
||||
</View>
|
||||
<View style={styles.middleSection}>
|
||||
<Text style={[stylesHook.address, styles.address]} numberOfLines={1} ellipsizeMode="middle">
|
||||
{item.address}
|
||||
</Text>
|
||||
{renderAddressContent()}
|
||||
<Text style={[stylesHook.balance, styles.balance]}>{balance}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@ -7,7 +7,7 @@ const useBounceAnimation = (query: string) => {
|
||||
useEffect(() => {
|
||||
if (query) {
|
||||
Animated.timing(bounceAnim, {
|
||||
toValue: 1.2,
|
||||
toValue: 1.08, // Reduced from 1.2 to 1.08 for more subtle animation
|
||||
duration: 150,
|
||||
useNativeDriver: true,
|
||||
}).start(() => {
|
||||
|
||||
@ -27,23 +27,18 @@ export const useExtendedNavigation = <T extends NavigationProp<ParamListBase>>()
|
||||
| [string, object | undefined, { merge?: boolean }]
|
||||
| [{ name: string; params?: object; path?: string; merge?: boolean }]
|
||||
) => {
|
||||
let screenOrOptions: any;
|
||||
let screenName: string;
|
||||
let params: any;
|
||||
let options: { merge?: boolean } | undefined;
|
||||
|
||||
if (typeof args[0] === 'string') {
|
||||
screenOrOptions = args[0];
|
||||
screenName = args[0];
|
||||
params = args[1];
|
||||
options = args[2];
|
||||
} else {
|
||||
screenOrOptions = args[0];
|
||||
}
|
||||
let screenName: string;
|
||||
if (typeof screenOrOptions === 'string') {
|
||||
screenName = screenOrOptions;
|
||||
} else if (typeof screenOrOptions === 'object' && 'name' in screenOrOptions) {
|
||||
screenName = screenOrOptions.name;
|
||||
params = screenOrOptions.params; // Assign params from object if present
|
||||
} else if (typeof args[0] === 'object' && 'name' in args[0]) {
|
||||
screenName = args[0].name;
|
||||
params = args[0].params;
|
||||
options = { merge: args[0].merge };
|
||||
} else {
|
||||
throw new Error('Invalid navigation options');
|
||||
}
|
||||
@ -54,30 +49,15 @@ export const useExtendedNavigation = <T extends NavigationProp<ParamListBase>>()
|
||||
const proceedWithNavigation = () => {
|
||||
console.log('Proceeding with navigation to', screenName);
|
||||
|
||||
// Navigation logic based on current route and target screen
|
||||
if (navigationRef.current?.isReady()) {
|
||||
// Get the current route - we need to know which navigator we're in
|
||||
const currentRoute = navigationRef.current.getCurrentRoute();
|
||||
const currentRouteName = currentRoute?.name;
|
||||
|
||||
// Handle specific cases for nested navigation
|
||||
if (currentRouteName === 'DrawerRoot') {
|
||||
// If we're in DrawerRoot and trying to navigate to a screen that exists in DetailViewStackScreensStack
|
||||
originalNavigation.navigate('DrawerRoot', {
|
||||
screen: 'DetailViewStackScreensStack',
|
||||
params: {
|
||||
screen: screenName,
|
||||
params,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Normal navigation
|
||||
if (typeof screenOrOptions === 'string') {
|
||||
originalNavigation.navigate({ name: screenOrOptions, params, merge: options?.merge });
|
||||
} else {
|
||||
originalNavigation.navigate({ ...screenOrOptions, params, merge: options?.merge });
|
||||
}
|
||||
}
|
||||
// Normal navigation handling
|
||||
if (typeof args[0] === 'string') {
|
||||
originalNavigation.navigate(screenName, params, options);
|
||||
} else {
|
||||
originalNavigation.navigate({
|
||||
name: screenName,
|
||||
params,
|
||||
merge: options?.merge,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -102,6 +82,7 @@ export const useExtendedNavigation = <T extends NavigationProp<ParamListBase>>()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isRequiresWalletExportIsSaved) {
|
||||
console.log('Checking if wallet export is saved');
|
||||
let walletID: string | undefined;
|
||||
|
||||
@ -54,6 +54,7 @@ import { useSizeClass, SizeClass } from '../blue_modules/sizeClass';
|
||||
import getWalletTransactionsOptions from './helpers/getWalletTransactionsOptions';
|
||||
import { isDesktop } from '../blue_modules/environment';
|
||||
import ManageWallets from '../screen/wallets/ManageWallets';
|
||||
import ReceiveDetails from '../screen/receive/details';
|
||||
|
||||
const DetailViewStackScreensStack = () => {
|
||||
const theme = useTheme();
|
||||
@ -318,6 +319,17 @@ const DetailViewStackScreensStack = () => {
|
||||
headerShown: true,
|
||||
}}
|
||||
/>
|
||||
<DetailViewStack.Screen
|
||||
name="ReceiveDetails"
|
||||
component={ReceiveDetails}
|
||||
options={navigationStyle({
|
||||
title: loc.receive.header,
|
||||
closeButtonPosition: CloseButtonPosition.Left,
|
||||
statusBarStyle: 'light',
|
||||
headerShown: true,
|
||||
presentation: 'modal',
|
||||
})(theme)}
|
||||
/>
|
||||
</DetailViewStack.Navigator>
|
||||
);
|
||||
};
|
||||
|
||||
@ -90,13 +90,6 @@ export type DetailViewStackParamList = {
|
||||
address: string;
|
||||
};
|
||||
};
|
||||
ReceiveDetailsRoot: {
|
||||
screen: 'ReceiveDetails';
|
||||
params: {
|
||||
walletID?: string;
|
||||
address: string;
|
||||
};
|
||||
};
|
||||
ReceiveDetails: {
|
||||
walletID?: string;
|
||||
address: string;
|
||||
|
||||
@ -19,7 +19,6 @@ const WalletExportStack = lazy(() => import('./WalletExportStack'));
|
||||
const ExportMultisigCoordinationSetupStack = lazy(() => import('./ExportMultisigCoordinationSetupStack'));
|
||||
const WalletXpubStackRoot = lazy(() => import('./WalletXpubStack'));
|
||||
const SignVerifyStackRoot = lazy(() => import('./SignVerifyStack'));
|
||||
const ReceiveDetailsStackRoot = lazy(() => import('./ReceiveDetailsStack'));
|
||||
const ScanQRCode = lazy(() => import('../screen/send/ScanQRCode'));
|
||||
const ViewEditMultisigCosigners = lazy(() => import('../screen/wallets/ViewEditMultisigCosigners'));
|
||||
|
||||
@ -110,12 +109,6 @@ const LazySignVerifyStackRoot = () => (
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
const LazyReceiveDetailsStackRoot = () => (
|
||||
<Suspense fallback={<LazyLoadingIndicator />}>
|
||||
<ReceiveDetailsStackRoot />
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
const LazyScanQRCodeComponent = () => (
|
||||
<Suspense fallback={<LazyLoadingIndicator />}>
|
||||
<ScanQRCode />
|
||||
@ -171,7 +164,6 @@ const MainRoot = () => {
|
||||
component={LazySignVerifyStackRoot}
|
||||
options={{ ...NavigationDefaultOptions, ...StatusBarLightOptions }}
|
||||
/>
|
||||
<DetailViewStack.Screen name="ReceiveDetailsRoot" component={LazyReceiveDetailsStackRoot} options={NavigationDefaultOptions} />
|
||||
|
||||
<DetailViewStack.Screen
|
||||
name="ScanQRCode"
|
||||
|
||||
@ -43,7 +43,7 @@ interface TransactionItem {
|
||||
|
||||
interface AddressItem {
|
||||
type: ItemType.AddressSection;
|
||||
data: AddressItemData;
|
||||
data: Omit<AddressItemData, 'label'>;
|
||||
}
|
||||
|
||||
interface WalletGroupItem {
|
||||
@ -57,9 +57,8 @@ type Item = WalletItem | TransactionItem | AddressItem | WalletGroupItem;
|
||||
|
||||
const SET_SEARCH_QUERY = 'SET_SEARCH_QUERY';
|
||||
const SET_IS_SEARCH_FOCUSED = 'SET_IS_SEARCH_FOCUSED';
|
||||
const SET_INITIAL_ORDER = 'SET_INITIAL_ORDER';
|
||||
const SET_FILTERED_ORDER = 'SET_FILTERED_ORDER';
|
||||
const SET_TEMP_ORDER = 'SET_TEMP_ORDER';
|
||||
const SET_INITIAL_DATA = 'SET_INITIAL_DATA';
|
||||
const SET_MANAGED_DATA = 'SET_MANAGED_DATA';
|
||||
const REMOVE_WALLET = 'REMOVE_WALLET';
|
||||
const SAVE_CHANGES = 'SAVE_CHANGES';
|
||||
|
||||
@ -78,18 +77,13 @@ interface SetIsSearchFocusedAction {
|
||||
payload: boolean;
|
||||
}
|
||||
|
||||
interface SetInitialOrderAction {
|
||||
type: typeof SET_INITIAL_ORDER;
|
||||
interface SetInitialDataAction {
|
||||
type: typeof SET_INITIAL_DATA;
|
||||
payload: { wallets: TWallet[]; txMetadata: TTXMetadata };
|
||||
}
|
||||
|
||||
interface SetFilteredOrderAction {
|
||||
type: typeof SET_FILTERED_ORDER;
|
||||
payload: string;
|
||||
}
|
||||
|
||||
interface SetTempOrderAction {
|
||||
type: typeof SET_TEMP_ORDER;
|
||||
interface SetManagedDataAction {
|
||||
type: typeof SET_MANAGED_DATA;
|
||||
payload: Item[];
|
||||
}
|
||||
|
||||
@ -101,30 +95,25 @@ interface RemoveWalletAction {
|
||||
type Action =
|
||||
| SetSearchQueryAction
|
||||
| SetIsSearchFocusedAction
|
||||
| SetInitialOrderAction
|
||||
| SetFilteredOrderAction
|
||||
| SetTempOrderAction
|
||||
| SetInitialDataAction
|
||||
| SetManagedDataAction
|
||||
| SaveChangesAction
|
||||
| RemoveWalletAction;
|
||||
|
||||
interface State {
|
||||
searchQuery: string;
|
||||
isSearchFocused: boolean;
|
||||
originalWalletsOrder: Item[];
|
||||
currentWalletsOrder: Item[];
|
||||
availableWallets: TWallet[];
|
||||
originalWallets: TWallet[];
|
||||
managedWalletsData: Item[];
|
||||
txMetadata: TTXMetadata;
|
||||
initialWalletsBackup: TWallet[];
|
||||
}
|
||||
|
||||
const initialState: State = {
|
||||
searchQuery: '',
|
||||
isSearchFocused: false,
|
||||
originalWalletsOrder: [],
|
||||
currentWalletsOrder: [],
|
||||
availableWallets: [],
|
||||
originalWallets: [],
|
||||
managedWalletsData: [],
|
||||
txMetadata: {},
|
||||
initialWalletsBackup: [],
|
||||
};
|
||||
|
||||
const deepCopyWallets = (wallets: TWallet[]): TWallet[] => {
|
||||
@ -137,187 +126,37 @@ const reducer = (state: State, action: Action): State => {
|
||||
return { ...state, searchQuery: action.payload };
|
||||
case SET_IS_SEARCH_FOCUSED:
|
||||
return { ...state, isSearchFocused: action.payload };
|
||||
case SET_INITIAL_ORDER: {
|
||||
const initialWalletsOrder: WalletItem[] = deepCopyWallets(action.payload.wallets).map(wallet => ({
|
||||
case SET_INITIAL_DATA: {
|
||||
const managedWalletsData: WalletItem[] = deepCopyWallets(action.payload.wallets).map(wallet => ({
|
||||
type: ItemType.WalletSection,
|
||||
data: wallet,
|
||||
}));
|
||||
return {
|
||||
...state,
|
||||
availableWallets: action.payload.wallets,
|
||||
originalWallets: deepCopyWallets(action.payload.wallets),
|
||||
txMetadata: action.payload.txMetadata,
|
||||
originalWalletsOrder: initialWalletsOrder,
|
||||
currentWalletsOrder: initialWalletsOrder,
|
||||
initialWalletsBackup: deepCopyWallets(action.payload.wallets),
|
||||
managedWalletsData,
|
||||
};
|
||||
}
|
||||
case SET_FILTERED_ORDER: {
|
||||
const query = action.payload.toLowerCase();
|
||||
|
||||
// For tracking unique results across all categories
|
||||
const finalOrder: Item[] = [];
|
||||
const walletGroups: Record<string, WalletGroupItem> = {};
|
||||
const processedWalletIds = new Set<string>();
|
||||
|
||||
// First pass: Find direct wallet matches
|
||||
state.availableWallets.forEach(wallet => {
|
||||
const walletLabel = wallet.getLabel()?.toLowerCase() || '';
|
||||
const walletId = wallet.getID();
|
||||
|
||||
// Direct wallet match - will be shown as standalone wallet
|
||||
if (walletLabel.includes(query)) {
|
||||
processedWalletIds.add(walletId);
|
||||
finalOrder.push({
|
||||
type: ItemType.WalletSection,
|
||||
data: wallet,
|
||||
} as WalletItem);
|
||||
}
|
||||
});
|
||||
|
||||
// Second pass: Find matching transactions and addresses
|
||||
state.availableWallets.forEach(wallet => {
|
||||
const walletId = wallet.getID();
|
||||
const matchingTransactions: TransactionItem[] = [];
|
||||
const matchingAddresses: AddressItem[] = [];
|
||||
|
||||
// Process transactions
|
||||
try {
|
||||
const walletTransactions = wallet.getTransactions() || [];
|
||||
walletTransactions
|
||||
.filter((tx: Transaction) => {
|
||||
try {
|
||||
if (!tx) return false;
|
||||
|
||||
const hasMemoMatch =
|
||||
tx.hash &&
|
||||
Object.entries(state.txMetadata).some(([txid, meta]) => tx.hash === txid && meta?.memo?.toLowerCase().includes(query));
|
||||
|
||||
const hashMatch = tx.hash?.toLowerCase().includes(query);
|
||||
const valueMatch = tx.value !== undefined && tx.value.toString().includes(query);
|
||||
|
||||
return hasMemoMatch || hashMatch || valueMatch;
|
||||
} catch (e) {
|
||||
console.warn('Error filtering transaction:', e);
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.forEach((tx: Transaction) => {
|
||||
try {
|
||||
matchingTransactions.push({
|
||||
type: ItemType.TransactionSection,
|
||||
data: tx as ExtendedTransaction & LightningTransaction,
|
||||
} as TransactionItem);
|
||||
} catch (e) {
|
||||
console.warn('Error mapping transaction:', e);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('Error processing wallet transactions:', e);
|
||||
}
|
||||
|
||||
// Process addresses
|
||||
try {
|
||||
if ('_getExternalAddressByIndex' in wallet && '_getInternalAddressByIndex' in wallet) {
|
||||
const externalLimit = wallet.getNextFreeAddressIndex ? wallet.getNextFreeAddressIndex() + 20 : 20;
|
||||
for (let i = 0; i < externalLimit; i++) {
|
||||
try {
|
||||
const address = wallet._getExternalAddressByIndex(i);
|
||||
if (address?.toLowerCase().includes(query)) {
|
||||
matchingAddresses.push({
|
||||
type: ItemType.AddressSection,
|
||||
data: {
|
||||
address,
|
||||
walletID: wallet.getID(),
|
||||
index: i,
|
||||
isInternal: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
const internalLimit = wallet.getNextFreeChangeAddressIndex ? wallet.getNextFreeChangeAddressIndex() + 20 : 20;
|
||||
for (let i = 0; i < internalLimit; i++) {
|
||||
try {
|
||||
const address = wallet._getInternalAddressByIndex(i);
|
||||
if (address?.toLowerCase().includes(query)) {
|
||||
matchingAddresses.push({
|
||||
type: ItemType.AddressSection,
|
||||
data: {
|
||||
address,
|
||||
walletID: wallet.getID(),
|
||||
index: i,
|
||||
isInternal: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
} else if ('getAddress' in wallet) {
|
||||
const address = wallet.getAddress();
|
||||
if (address && typeof address === 'string' && address.toLowerCase().includes(query)) {
|
||||
matchingAddresses.push({
|
||||
type: ItemType.AddressSection,
|
||||
data: {
|
||||
address,
|
||||
walletID: wallet.getID(),
|
||||
index: 0,
|
||||
isInternal: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error processing wallet addresses:', e);
|
||||
}
|
||||
|
||||
// If we have matching transactions or addresses, create a wallet group
|
||||
if (matchingTransactions.length > 0 || matchingAddresses.length > 0) {
|
||||
// Only add a wallet group if the wallet isn't already shown as a direct match
|
||||
if (!processedWalletIds.has(walletId)) {
|
||||
walletGroups[walletId] = {
|
||||
type: ItemType.WalletGroupSection,
|
||||
wallet,
|
||||
transactions: matchingTransactions,
|
||||
addresses: matchingAddresses,
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add all wallet groups to the final results
|
||||
Object.values(walletGroups).forEach(group => {
|
||||
finalOrder.push(group);
|
||||
});
|
||||
|
||||
case SET_MANAGED_DATA: {
|
||||
return {
|
||||
...state,
|
||||
currentWalletsOrder: finalOrder,
|
||||
managedWalletsData: action.payload,
|
||||
};
|
||||
}
|
||||
case SAVE_CHANGES: {
|
||||
const savedWallets = deepCopyWallets(action.payload);
|
||||
return {
|
||||
...state,
|
||||
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,
|
||||
),
|
||||
originalWallets: deepCopyWallets(action.payload),
|
||||
};
|
||||
}
|
||||
case SET_TEMP_ORDER: {
|
||||
return { ...state, currentWalletsOrder: action.payload };
|
||||
}
|
||||
case REMOVE_WALLET: {
|
||||
const updatedOrder = state.currentWalletsOrder.filter(
|
||||
const updatedOrder = state.managedWalletsData.filter(
|
||||
item => item.type !== ItemType.WalletSection || item.data.getID() !== action.payload,
|
||||
);
|
||||
return {
|
||||
...state,
|
||||
currentWalletsOrder: updatedOrder,
|
||||
managedWalletsData: updatedOrder,
|
||||
};
|
||||
}
|
||||
default:
|
||||
@ -345,83 +184,214 @@ const ManageWallets: React.FC = () => {
|
||||
color: colors.foregroundColor,
|
||||
},
|
||||
};
|
||||
const [uiData, setUiData] = useState(state.currentWalletsOrder);
|
||||
const [uiData, setUiData] = useState(state.managedWalletsData);
|
||||
const [noResultsOpacity] = useState(new Animated.Value(0));
|
||||
|
||||
const listRef = useRef<FlatList<Item> | null>(null);
|
||||
const [saveInProgress, setSaveInProgress] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setUiData(state.currentWalletsOrder);
|
||||
}, [state.currentWalletsOrder]);
|
||||
const getFilteredWalletsData = useCallback(() => {
|
||||
if (debouncedSearchQuery) {
|
||||
const lowerQuery = debouncedSearchQuery.toLowerCase();
|
||||
|
||||
// Track which wallets have matching items (transactions, addresses, or memos)
|
||||
const walletsWithMatches = new Map<string, {
|
||||
wallet: TWallet,
|
||||
transactions: TransactionItem[],
|
||||
addresses: AddressItem[]
|
||||
}>();
|
||||
|
||||
// First check for transaction memos in txMetadata
|
||||
Object.entries(state.txMetadata).forEach(([txid, metadata]) => {
|
||||
if (metadata.memo && metadata.memo.toLowerCase().includes(lowerQuery)) {
|
||||
// Find which wallet has this transaction
|
||||
state.originalWallets.forEach(wallet => {
|
||||
try {
|
||||
// Check if the wallet contains this transaction
|
||||
const tx = wallet.getTransactions().find((t: Transaction) => t.hash === txid || t.txid === txid);
|
||||
if (tx) {
|
||||
const walletID = wallet.getID();
|
||||
|
||||
if (!walletsWithMatches.has(walletID)) {
|
||||
walletsWithMatches.set(walletID, {
|
||||
wallet,
|
||||
transactions: [],
|
||||
addresses: []
|
||||
});
|
||||
}
|
||||
|
||||
// Add the transaction to the matching group
|
||||
const group = walletsWithMatches.get(walletID)!;
|
||||
|
||||
// Only add if it's not already in the array
|
||||
const alreadyAdded = group.transactions.some(
|
||||
item => (item.data.hash === txid || item.data.txid === txid)
|
||||
);
|
||||
|
||||
if (!alreadyAdded) {
|
||||
group.transactions.push({
|
||||
type: ItemType.TransactionSection,
|
||||
data: tx as ExtendedTransaction & LightningTransaction
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Skip if the wallet doesn't have getTransactions method or other errors
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Search through all wallets for addresses and additional transactions
|
||||
state.originalWallets.forEach(wallet => {
|
||||
const walletID = wallet.getID();
|
||||
const walletLabel = wallet.getLabel() || '';
|
||||
|
||||
// Check if wallet label matches
|
||||
const walletLabelMatches = walletLabel.toLowerCase().includes(lowerQuery);
|
||||
|
||||
// If wallet label matches, make sure it's in our map
|
||||
if (walletLabelMatches && !walletsWithMatches.has(walletID)) {
|
||||
walletsWithMatches.set(walletID, {
|
||||
wallet,
|
||||
transactions: [],
|
||||
addresses: []
|
||||
});
|
||||
}
|
||||
|
||||
// Search through transactions
|
||||
try {
|
||||
const transactions = wallet.getTransactions() || [];
|
||||
|
||||
transactions.forEach((tx: Transaction) => {
|
||||
const txid = tx.hash || tx.txid;
|
||||
// Using value instead of amount since that's what the Transaction type has
|
||||
const txAmount = tx.value?.toString() || '';
|
||||
const txDate = tx.received?.toString() || '';
|
||||
|
||||
// Check if any transaction data matches the search query
|
||||
const txMemoMatches = txid &&
|
||||
state.txMetadata[txid] &&
|
||||
state.txMetadata[txid].memo &&
|
||||
state.txMetadata[txid].memo.toLowerCase().includes(lowerQuery);
|
||||
|
||||
// Check if the transaction ID matches the search query
|
||||
const txIdMatches = txid && txid.toLowerCase().includes(lowerQuery);
|
||||
|
||||
const txDataMatches = txAmount.includes(lowerQuery) || txDate.includes(lowerQuery);
|
||||
|
||||
if (txMemoMatches || txDataMatches || txIdMatches) {
|
||||
if (!walletsWithMatches.has(walletID)) {
|
||||
walletsWithMatches.set(walletID, {
|
||||
wallet,
|
||||
transactions: [],
|
||||
addresses: []
|
||||
});
|
||||
}
|
||||
|
||||
const group = walletsWithMatches.get(walletID)!;
|
||||
|
||||
// Only add if it's not already in the array
|
||||
const alreadyAdded = group.transactions.some(
|
||||
item => (item.data.hash === txid || item.data.txid === txid)
|
||||
);
|
||||
|
||||
if (!alreadyAdded) {
|
||||
group.transactions.push({
|
||||
type: ItemType.TransactionSection,
|
||||
data: tx as ExtendedTransaction & LightningTransaction
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
// Skip if wallet doesn't support getTransactions
|
||||
}
|
||||
|
||||
// Search through addresses
|
||||
try {
|
||||
const addresses = wallet.getAllExternalAddresses() || [];
|
||||
|
||||
// Check if any address matches
|
||||
addresses.forEach((address: any, index: number) => {
|
||||
const addressValue = typeof address === 'string' ? address : address.address;
|
||||
const addressLabel = typeof address === 'string' ? '' : (address.label || '');
|
||||
|
||||
if (addressValue.toLowerCase().includes(lowerQuery) ||
|
||||
addressLabel.toLowerCase().includes(lowerQuery)) {
|
||||
|
||||
if (!walletsWithMatches.has(walletID)) {
|
||||
walletsWithMatches.set(walletID, {
|
||||
wallet,
|
||||
transactions: [],
|
||||
addresses: []
|
||||
});
|
||||
}
|
||||
|
||||
const group = walletsWithMatches.get(walletID)!;
|
||||
|
||||
const addressItem: AddressItem = {
|
||||
type: ItemType.AddressSection,
|
||||
data: {
|
||||
address: addressValue,
|
||||
walletID,
|
||||
index,
|
||||
isInternal: false,
|
||||
}
|
||||
};
|
||||
|
||||
// Check if this address is already in the array
|
||||
const alreadyAdded = group.addresses.some(item => item.data.address === addressValue);
|
||||
|
||||
if (!alreadyAdded) {
|
||||
group.addresses.push(addressItem);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
// Skip if wallet doesn't support getAllExternalAddresses
|
||||
}
|
||||
});
|
||||
|
||||
// Now convert the map to the data format needed for the list
|
||||
const result: Item[] = [];
|
||||
|
||||
// Process matches by wallet
|
||||
walletsWithMatches.forEach((matchData, walletID) => {
|
||||
const { wallet, transactions, addresses } = matchData;
|
||||
|
||||
// If this wallet has transactions or addresses that match, create a group
|
||||
if (transactions.length > 0 || addresses.length > 0) {
|
||||
// Add a wallet group item
|
||||
result.push({
|
||||
type: ItemType.WalletGroupSection,
|
||||
wallet,
|
||||
transactions,
|
||||
addresses
|
||||
});
|
||||
} else {
|
||||
// If it's just the wallet label that matched, add a regular wallet item
|
||||
result.push({
|
||||
type: ItemType.WalletSection,
|
||||
data: wallet
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// When not searching, return the original managed data
|
||||
return state.managedWalletsData;
|
||||
}, [debouncedSearchQuery, state.managedWalletsData, state.originalWallets, state.txMetadata]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch({ type: SET_INITIAL_ORDER, payload: { wallets: initialWalletsRef.current, txMetadata } });
|
||||
dispatch({ type: SET_INITIAL_DATA, payload: { wallets: initialWalletsRef.current, txMetadata } });
|
||||
}, [txMetadata]);
|
||||
|
||||
// Handle no results animation
|
||||
useEffect(() => {
|
||||
if (debouncedSearchQuery) {
|
||||
if (Platform.OS === 'ios') {
|
||||
LayoutAnimation.configureNext({
|
||||
duration: 300,
|
||||
create: {
|
||||
type: LayoutAnimation.Types.spring,
|
||||
property: LayoutAnimation.Properties.opacity,
|
||||
springDamping: 0.85,
|
||||
},
|
||||
update: {
|
||||
type: LayoutAnimation.Types.spring,
|
||||
springDamping: 0.85,
|
||||
},
|
||||
delete: {
|
||||
type: LayoutAnimation.Types.spring,
|
||||
property: LayoutAnimation.Properties.opacity,
|
||||
springDamping: 0.85,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
LayoutAnimation.configureNext({
|
||||
duration: 250,
|
||||
create: {
|
||||
type: LayoutAnimation.Types.easeInEaseOut,
|
||||
property: LayoutAnimation.Properties.opacity,
|
||||
},
|
||||
update: {
|
||||
type: LayoutAnimation.Types.easeInEaseOut,
|
||||
},
|
||||
delete: {
|
||||
duration: 200,
|
||||
type: LayoutAnimation.Types.easeInEaseOut,
|
||||
property: LayoutAnimation.Properties.opacity,
|
||||
},
|
||||
});
|
||||
}
|
||||
dispatch({ type: SET_FILTERED_ORDER, payload: debouncedSearchQuery });
|
||||
} else {
|
||||
if (Platform.OS === 'ios') {
|
||||
LayoutAnimation.configureNext({
|
||||
duration: 250,
|
||||
create: {
|
||||
type: LayoutAnimation.Types.easeInEaseOut,
|
||||
property: LayoutAnimation.Properties.opacity,
|
||||
},
|
||||
update: {
|
||||
type: LayoutAnimation.Types.easeInEaseOut,
|
||||
},
|
||||
delete: {
|
||||
type: LayoutAnimation.Types.easeInEaseOut,
|
||||
property: LayoutAnimation.Properties.opacity,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
||||
}
|
||||
dispatch({ type: SET_TEMP_ORDER, payload: state.originalWalletsOrder });
|
||||
}
|
||||
}, [debouncedSearchQuery, state.originalWalletsOrder]);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.searchQuery && state.currentWalletsOrder.length === 0) {
|
||||
if (state.searchQuery && getFilteredWalletsData().length === 0) {
|
||||
Animated.timing(noResultsOpacity, {
|
||||
toValue: 1,
|
||||
duration: 300,
|
||||
@ -430,18 +400,22 @@ const ManageWallets: React.FC = () => {
|
||||
} else {
|
||||
noResultsOpacity.setValue(0);
|
||||
}
|
||||
}, [state.searchQuery, state.currentWalletsOrder.length, noResultsOpacity]);
|
||||
}, [getFilteredWalletsData, state.searchQuery, state.managedWalletsData.length, noResultsOpacity]);
|
||||
|
||||
useEffect(() => {
|
||||
setUiData(getFilteredWalletsData());
|
||||
}, [state.managedWalletsData, debouncedSearchQuery]);
|
||||
|
||||
const hasUnsavedChanges = useMemo(() => {
|
||||
if (state.searchQuery.length > 0 || state.isSearchFocused) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentWalletIds = state.currentWalletsOrder
|
||||
const currentWalletIds = state.managedWalletsData
|
||||
.filter((item): item is WalletItem => item.type === ItemType.WalletSection)
|
||||
.map(item => item.data.getID());
|
||||
|
||||
const originalWalletIds = state.initialWalletsBackup.map(wallet => wallet.getID());
|
||||
const originalWalletIds = state.originalWallets.map(wallet => wallet.getID());
|
||||
|
||||
if (currentWalletIds.length !== originalWalletIds.length) {
|
||||
return true;
|
||||
@ -453,19 +427,19 @@ const ManageWallets: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const modifiedWallets = state.currentWalletsOrder
|
||||
const modifiedWallets = state.managedWalletsData
|
||||
.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());
|
||||
const originalWallet = state.originalWallets.find(w => w.getID() === modifiedWallet.getID());
|
||||
if (originalWallet && originalWallet.hideBalance !== modifiedWallet.hideBalance) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}, [state.currentWalletsOrder, state.initialWalletsBackup, state.searchQuery, state.isSearchFocused]);
|
||||
}, [state.managedWalletsData, state.originalWallets, state.searchQuery, state.isSearchFocused]);
|
||||
|
||||
usePreventRemove(hasUnsavedChanges && !saveInProgress, ({ data: preventRemoveData }) => {
|
||||
Alert.alert(loc._.discard_changes, loc._.discard_changes_explain, [
|
||||
@ -487,11 +461,11 @@ const ManageWallets: React.FC = () => {
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
if (state.searchQuery.length === 0 && !state.isSearchFocused) {
|
||||
const reorderedWallets = state.currentWalletsOrder
|
||||
const reorderedWallets = state.managedWalletsData
|
||||
.filter((item): item is WalletItem => item.type === ItemType.WalletSection)
|
||||
.map(item => item.data);
|
||||
|
||||
const walletsToDelete = state.initialWalletsBackup.filter(
|
||||
const walletsToDelete = state.originalWallets.filter(
|
||||
originalWallet => !reorderedWallets.some(wallet => wallet.getID() === originalWallet.getID()),
|
||||
);
|
||||
|
||||
@ -512,8 +486,8 @@ const ManageWallets: React.FC = () => {
|
||||
setWalletsWithNewOrder,
|
||||
state.searchQuery,
|
||||
state.isSearchFocused,
|
||||
state.currentWalletsOrder,
|
||||
state.initialWalletsBackup,
|
||||
state.managedWalletsData,
|
||||
state.originalWallets,
|
||||
handleWalletDeletion,
|
||||
]);
|
||||
|
||||
@ -604,7 +578,7 @@ const ManageWallets: React.FC = () => {
|
||||
|
||||
const handleToggleHideBalance = useCallback(
|
||||
(wallet: TWallet) => {
|
||||
const updatedOrder = state.currentWalletsOrder.map(item => {
|
||||
const updatedOrder = state.managedWalletsData.map(item => {
|
||||
if (item.type === ItemType.WalletSection && item.data.getID() === wallet.getID()) {
|
||||
item.data.hideBalance = !item.data.hideBalance;
|
||||
return {
|
||||
@ -615,9 +589,9 @@ const ManageWallets: React.FC = () => {
|
||||
return item;
|
||||
});
|
||||
|
||||
dispatch({ type: SET_TEMP_ORDER, payload: updatedOrder });
|
||||
dispatch({ type: SET_MANAGED_DATA, payload: updatedOrder });
|
||||
},
|
||||
[state.currentWalletsOrder],
|
||||
[state.managedWalletsData],
|
||||
);
|
||||
|
||||
const navigateToWallet = useCallback(
|
||||
@ -635,12 +609,17 @@ const ManageWallets: React.FC = () => {
|
||||
|
||||
const navigateToAddress = useCallback(
|
||||
(address: string, walletID: string) => {
|
||||
// First dismiss the modal and then navigate with a slight delay
|
||||
Keyboard.dismiss();
|
||||
goBack();
|
||||
navigate('ReceiveDetails', {
|
||||
walletID,
|
||||
address,
|
||||
});
|
||||
|
||||
// Use setTimeout to ensure the modal is fully dismissed before navigation
|
||||
setTimeout(() => {
|
||||
navigate('ReceiveDetails', {
|
||||
walletID,
|
||||
address,
|
||||
});
|
||||
}, 300);
|
||||
},
|
||||
[goBack, navigate],
|
||||
);
|
||||
@ -650,7 +629,7 @@ const ManageWallets: React.FC = () => {
|
||||
const { item, onDragStart, isActive } = info;
|
||||
|
||||
const compatibleState = {
|
||||
wallets: state.availableWallets,
|
||||
wallets: state.originalWallets,
|
||||
searchQuery: state.searchQuery,
|
||||
isSearchFocused: state.isSearchFocused,
|
||||
};
|
||||
@ -688,7 +667,7 @@ const ManageWallets: React.FC = () => {
|
||||
);
|
||||
},
|
||||
[
|
||||
state.availableWallets,
|
||||
state.originalWallets,
|
||||
state.searchQuery,
|
||||
state.isSearchFocused,
|
||||
navigateToWallet,
|
||||
@ -706,13 +685,13 @@ const ManageWallets: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedOrder = [...state.currentWalletsOrder];
|
||||
const updatedOrder = [...state.managedWalletsData];
|
||||
const removed = updatedOrder.splice(fromIndex, 1);
|
||||
updatedOrder.splice(toIndex, 0, removed[0]);
|
||||
|
||||
dispatch({ type: SET_TEMP_ORDER, payload: updatedOrder });
|
||||
dispatch({ type: SET_MANAGED_DATA, payload: updatedOrder });
|
||||
},
|
||||
[state.currentWalletsOrder, state.searchQuery, state.isSearchFocused],
|
||||
[state.managedWalletsData, state.searchQuery, state.isSearchFocused],
|
||||
);
|
||||
|
||||
const keyExtractor = useCallback((item: Item, index: number) => index.toString(), []);
|
||||
@ -720,7 +699,7 @@ const ManageWallets: React.FC = () => {
|
||||
return (
|
||||
<Suspense fallback={<ActivityIndicator size="large" color={colors.brandingColor} />}>
|
||||
<GestureHandlerRootView style={[{ backgroundColor: colors.background }, styles.root]}>
|
||||
{state.searchQuery && state.currentWalletsOrder.length === 0 ? (
|
||||
{state.searchQuery && getFilteredWalletsData().length === 0 ? (
|
||||
<Animated.View style={[styles.noResultsContainer, { opacity: noResultsOpacity }]}>
|
||||
<Animated.Text style={[styles.noResultsText, stylesHook.noResultsText]}>{loc.wallets.no_results_found}</Animated.Text>
|
||||
</Animated.View>
|
||||
@ -730,12 +709,13 @@ const ManageWallets: React.FC = () => {
|
||||
automaticallyAdjustKeyboardInsets
|
||||
automaticallyAdjustsScrollIndicatorInsets
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
data={uiData}
|
||||
data={getFilteredWalletsData()}
|
||||
containerStyle={[{ backgroundColor: colors.background }, styles.root]}
|
||||
keyExtractor={keyExtractor}
|
||||
onReordered={onReordered}
|
||||
renderItem={renderItem}
|
||||
ref={listRef}
|
||||
extraData={debouncedSearchQuery} // Use extraData instead of key for re-renders
|
||||
/>
|
||||
)}
|
||||
</GestureHandlerRootView>
|
||||
|
||||
@ -290,11 +290,8 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }) => {
|
||||
selectWallet(navigate, name, Chain.ONCHAIN).then(onWalletSelect);
|
||||
}
|
||||
} else if (id === actionKeys.RefillWithExternalWallet) {
|
||||
navigate('ReceiveDetailsRoot', {
|
||||
screen: 'ReceiveDetails',
|
||||
params: {
|
||||
walletID,
|
||||
},
|
||||
navigate('ReceiveDetails', {
|
||||
walletID,
|
||||
});
|
||||
}
|
||||
},
|
||||
@ -617,7 +614,9 @@ const WalletTransactions: React.FC<WalletTransactionsProps> = ({ route }) => {
|
||||
if (wallet.chain === Chain.OFFCHAIN) {
|
||||
navigate('LNDCreateInvoiceRoot', { screen: 'LNDCreateInvoice', params: { walletID } });
|
||||
} else {
|
||||
navigate('ReceiveDetailsRoot', { screen: 'ReceiveDetails', params: { walletID } });
|
||||
navigate('ReceiveDetails', {
|
||||
walletID,
|
||||
});
|
||||
}
|
||||
}}
|
||||
icon={
|
||||
|
||||
Loading…
Reference in New Issue
Block a user