From 64c1114dbc25cc51aebc9b6b7b1671357a25a28f Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Wed, 24 Jul 2024 17:42:09 -0400 Subject: [PATCH 1/8] ADD: Search wallets, memos and transactions --- components/ListItem.tsx | 2 +- components/TransactionListItem.tsx | 603 ++++++++++++++++------------- components/WalletsCarousel.tsx | 16 +- loc/en.json | 2 +- screen/wallets/ReorderWallets.tsx | 212 ++++++++-- 5 files changed, 532 insertions(+), 303 deletions(-) diff --git a/components/ListItem.tsx b/components/ListItem.tsx index de781c26d..bc507532f 100644 --- a/components/ListItem.tsx +++ b/components/ListItem.tsx @@ -21,7 +21,7 @@ interface ListItemProps { switch?: object; // Define more specific type if needed leftIcon?: any; // Define more specific type if needed title: string; - subtitle?: string; + subtitle?: string | React.ReactNode; subtitleNumberOfLines?: number; rightTitle?: string; rightTitleStyle?: object; diff --git a/components/TransactionListItem.tsx b/components/TransactionListItem.tsx index b3a13be03..9ea847d5a 100644 --- a/components/TransactionListItem.tsx +++ b/components/TransactionListItem.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import AsyncStorage from '@react-native-async-storage/async-storage'; import Clipboard from '@react-native-clipboard/clipboard'; -import { Linking, View } from 'react-native'; +import { Linking, View, Text, StyleSheet, Animated, ViewStyle } from 'react-native'; import Lnurl from '../class/lnurl'; import { LightningTransaction, Transaction } from '../class/wallets/types'; import TransactionExpiredIcon from '../components/icons/TransactionExpiredIcon'; @@ -28,307 +28,364 @@ interface TransactionListItemProps { itemPriceUnit: BitcoinUnit; walletID: string; item: Transaction & LightningTransaction; // using type intersection to have less issues with ts + searchQuery?: string; + style?: ViewStyle; } type NavigationProps = NativeStackNavigationProp; -export const TransactionListItem: React.FC = React.memo(({ item, itemPriceUnit = BitcoinUnit.BTC, walletID }) => { - const [subtitleNumberOfLines, setSubtitleNumberOfLines] = useState(1); - const { colors } = useTheme(); - const { navigate } = useExtendedNavigation(); - const menuRef = useRef(); - const { txMetadata, counterpartyMetadata, wallets } = useStorage(); - const { language } = useSettings(); - const containerStyle = useMemo( - () => ({ - backgroundColor: 'transparent', - borderBottomColor: colors.lightBorder, - }), - [colors.lightBorder], - ); +const useBounceAnimation = (query: string) => { + const bounceAnim = useRef(new Animated.Value(1.0)).current; - const shortenContactName = (name: string): string => { - if (name.length < 16) return name; - return name.substr(0, 7) + '...' + name.substr(name.length - 7, 7); - }; - - const title = useMemo(() => { - if (item.confirmations === 0) { - return loc.transactions.pending; - } else { - return transactionTimeToReadable(item.received!); + useEffect(() => { + if (query) { + Animated.spring(bounceAnim, { + toValue: 1.2, + useNativeDriver: true, + friction: 3, + tension: 100, + }).start(() => { + Animated.spring(bounceAnim, { + toValue: 1.0, + useNativeDriver: true, + friction: 3, + tension: 100, + }).start(); + }); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [item.confirmations, item.received, language]); + }, [query, bounceAnim]); - let counterparty; - if (item.counterparty) { - counterparty = counterpartyMetadata?.[item.counterparty]?.label ?? item.counterparty; - } - const txMemo = (counterparty ? `[${shortenContactName(counterparty)}] ` : '') + (txMetadata[item.hash]?.memo ?? ''); - const subtitle = useMemo(() => { - let sub = Number(item.confirmations) < 7 ? loc.formatString(loc.transactions.list_conf, { number: item.confirmations }) : ''; - if (sub !== '') sub += ' '; - sub += txMemo; - if (item.memo) sub += item.memo; - return sub || undefined; - }, [txMemo, item.confirmations, item.memo]); + return bounceAnim; +}; - const rowTitle = useMemo(() => { - if (item.type === 'user_invoice' || item.type === 'payment_request') { - if (isNaN(Number(item.value))) { - item.value = 0; - } - const currentDate = new Date(); - const now = (currentDate.getTime() / 1000) | 0; // eslint-disable-line no-bitwise - const invoiceExpiration = item.timestamp! + item.expire_time!; +export const TransactionListItem: React.FC = React.memo( + ({ item, itemPriceUnit = BitcoinUnit.BTC, walletID, searchQuery, style }) => { + const [subtitleNumberOfLines, setSubtitleNumberOfLines] = useState(1); + const { colors } = useTheme(); + const { navigate } = useExtendedNavigation(); + const menuRef = useRef(); + const { txMetadata, counterpartyMetadata, wallets } = useStorage(); + const { language } = useSettings(); + const containerStyle = useMemo( + () => ({ + backgroundColor: 'transparent', + borderBottomColor: colors.lightBorder, + }), + [colors.lightBorder], + ); - if (invoiceExpiration > now) { - return formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString(); + const shortenContactName = (name: string): string => { + if (name.length < 16) return name; + return name.substr(0, 7) + '...' + name.substr(name.length - 7, 7); + }; + + const title = useMemo(() => { + if (item.confirmations === 0) { + return loc.transactions.pending; } else { - if (item.ispaid) { + return transactionTimeToReadable(item.received!); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [item.confirmations, item.received, language]); + + let counterparty; + if (item.counterparty) { + counterparty = counterpartyMetadata?.[item.counterparty]?.label ?? item.counterparty; + } + const txMemo = (counterparty ? `[${shortenContactName(counterparty)}] ` : '') + (txMetadata[item.hash]?.memo ?? ''); + const subtitle = useMemo(() => { + let sub = Number(item.confirmations) < 7 ? loc.formatString(loc.transactions.list_conf, { number: item.confirmations }) : ''; + if (sub !== '') sub += ' '; + sub += txMemo; + if (item.memo) sub += item.memo; + return sub || undefined; + }, [txMemo, item.confirmations, item.memo]); + + const rowTitle = useMemo(() => { + if (item.type === 'user_invoice' || item.type === 'payment_request') { + if (isNaN(Number(item.value))) { + item.value = 0; + } + const currentDate = new Date(); + const now = (currentDate.getTime() / 1000) | 0; // eslint-disable-line no-bitwise + const invoiceExpiration = item.timestamp! + item.expire_time!; + + if (invoiceExpiration > now) { return formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString(); } else { - return loc.lnd.expired; + if (item.ispaid) { + return formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString(); + } else { + return loc.lnd.expired; + } } + } else { + return formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString(); } - } else { - return formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString(); - } - }, [item, itemPriceUnit]); + }, [item, itemPriceUnit]); - const rowTitleStyle = useMemo(() => { - let color = colors.successColor; + const rowTitleStyle = useMemo(() => { + let color = colors.successColor; - if (item.type === 'user_invoice' || item.type === 'payment_request') { - const currentDate = new Date(); - const now = (currentDate.getTime() / 1000) | 0; // eslint-disable-line no-bitwise - const invoiceExpiration = item.timestamp! + item.expire_time!; + if (item.type === 'user_invoice' || item.type === 'payment_request') { + const currentDate = new Date(); + const now = (currentDate.getTime() / 1000) | 0; // eslint-disable-line no-bitwise + const invoiceExpiration = item.timestamp! + item.expire_time!; - if (invoiceExpiration > now) { - color = colors.successColor; - } else if (invoiceExpiration < now) { - if (item.ispaid) { + if (invoiceExpiration > now) { color = colors.successColor; + } else if (invoiceExpiration < now) { + if (item.ispaid) { + color = colors.successColor; + } else { + color = '#9AA0AA'; + } + } + } else if (item.value! / 100000000 < 0) { + color = colors.foregroundColor; + } + + return { + color, + fontSize: 14, + fontWeight: '600', + textAlign: 'right', + }; + }, [item, colors.foregroundColor, colors.successColor]); + + const determineTransactionTypeAndAvatar = () => { + if (item.category === 'receive' && item.confirmations! < 3) { + return { + label: loc.transactions.pending_transaction, + icon: , + }; + } + + if (item.type && item.type === 'bitcoind_tx') { + return { + label: loc.transactions.onchain, + icon: , + }; + } + + if (item.type === 'paid_invoice') { + return { + label: loc.transactions.offchain, + icon: , + }; + } + + if (item.type === 'user_invoice' || item.type === 'payment_request') { + const currentDate = new Date(); + const now = (currentDate.getTime() / 1000) | 0; // eslint-disable-line no-bitwise + const invoiceExpiration = item.timestamp! + item.expire_time!; + if (!item.ispaid && invoiceExpiration < now) { + return { + label: loc.transactions.expired_transaction, + icon: , + }; } else { - color = '#9AA0AA'; + return { + label: loc.transactions.incoming_transaction, + icon: , + }; } } - } else if (item.value! / 100000000 < 0) { - color = colors.foregroundColor; - } - return { - color, - fontSize: 14, - fontWeight: '600', - textAlign: 'right', - }; - }, [item, colors.foregroundColor, colors.successColor]); - - const determineTransactionTypeAndAvatar = () => { - if (item.category === 'receive' && item.confirmations! < 3) { - return { - label: loc.transactions.pending_transaction, - icon: , - }; - } - - if (item.type && item.type === 'bitcoind_tx') { - return { - label: loc.transactions.onchain, - icon: , - }; - } - - if (item.type === 'paid_invoice') { - return { - label: loc.transactions.offchain, - icon: , - }; - } - - if (item.type === 'user_invoice' || item.type === 'payment_request') { - const currentDate = new Date(); - const now = (currentDate.getTime() / 1000) | 0; // eslint-disable-line no-bitwise - const invoiceExpiration = item.timestamp! + item.expire_time!; - if (!item.ispaid && invoiceExpiration < now) { + if (!item.confirmations) { return { - label: loc.transactions.expired_transaction, - icon: , + label: loc.transactions.pending_transaction, + icon: , + }; + } else if (item.value! < 0) { + return { + label: loc.transactions.outgoing_transaction, + icon: , }; } else { return { label: loc.transactions.incoming_transaction, - icon: , + icon: , }; } - } - - if (!item.confirmations) { - return { - label: loc.transactions.pending_transaction, - icon: , - }; - } else if (item.value! < 0) { - return { - label: loc.transactions.outgoing_transaction, - icon: , - }; - } else { - return { - label: loc.transactions.incoming_transaction, - icon: , - }; - } - }; - - const { label: transactionTypeLabel, icon: avatar } = determineTransactionTypeAndAvatar(); - - const amountWithUnit = useMemo(() => { - const amount = formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString(); - const unit = itemPriceUnit === BitcoinUnit.BTC || itemPriceUnit === BitcoinUnit.SATS ? ` ${itemPriceUnit}` : ' '; - return `${amount}${unit}`; - }, [item.value, itemPriceUnit]); - - useEffect(() => { - setSubtitleNumberOfLines(1); - }, [subtitle]); - - const onPress = useCallback(async () => { - menuRef?.current?.dismissMenu?.(); - if (item.hash) { - navigate('TransactionStatus', { hash: item.hash, walletID }); - } else if (item.type === 'user_invoice' || item.type === 'payment_request' || item.type === 'paid_invoice') { - const lightningWallet = wallets.filter(wallet => wallet?.getID() === item.walletID); - if (lightningWallet.length === 1) { - try { - // is it a successful lnurl-pay? - const LN = new Lnurl(false, AsyncStorage); - let paymentHash = item.payment_hash!; - if (typeof paymentHash === 'object') { - paymentHash = Buffer.from(paymentHash.data).toString('hex'); - } - const loaded = await LN.loadSuccessfulPayment(paymentHash); - if (loaded) { - navigate('ScanLndInvoiceRoot', { - screen: 'LnurlPaySuccess', - params: { - paymentHash, - justPaid: false, - fromWalletID: lightningWallet[0].getID(), - }, - }); - return; - } - } catch (e) { - console.log(e); - } - - navigate('LNDViewInvoice', { - invoice: item, - walletID: lightningWallet[0].getID(), - }); - } - } - }, [item, wallets, navigate, walletID]); - - const handleOnExpandNote = useCallback(() => { - setSubtitleNumberOfLines(0); - }, []); - - const subtitleProps = useMemo(() => ({ numberOfLines: subtitleNumberOfLines }), [subtitleNumberOfLines]); - - const handleOnCopyAmountTap = useCallback(() => Clipboard.setString(rowTitle.replace(/[\s\\-]/g, '')), [rowTitle]); - const handleOnCopyTransactionID = useCallback(() => Clipboard.setString(item.hash), [item.hash]); - const handleOnCopyNote = useCallback(() => Clipboard.setString(subtitle ?? ''), [subtitle]); - const handleOnViewOnBlockExplorer = useCallback(() => { - const url = `https://mempool.space/tx/${item.hash}`; - Linking.canOpenURL(url).then(supported => { - if (supported) { - Linking.openURL(url); - } - }); - }, [item.hash]); - const handleCopyOpenInBlockExplorerPress = useCallback(() => { - Clipboard.setString(`https://mempool.space/tx/${item.hash}`); - }, [item.hash]); - - const onToolTipPress = useCallback( - (id: any) => { - if (id === CommonToolTipActions.CopyAmount.id) { - handleOnCopyAmountTap(); - } else if (id === CommonToolTipActions.CopyNote.id) { - handleOnCopyNote(); - } else if (id === CommonToolTipActions.OpenInBlockExplorer.id) { - handleOnViewOnBlockExplorer(); - } else if (id === CommonToolTipActions.ExpandNote.id) { - handleOnExpandNote(); - } else if (id === CommonToolTipActions.CopyBlockExplorerLink.id) { - handleCopyOpenInBlockExplorerPress(); - } else if (id === CommonToolTipActions.CopyTXID.id) { - handleOnCopyTransactionID(); - } - }, - [ - handleCopyOpenInBlockExplorerPress, - handleOnCopyAmountTap, - handleOnCopyNote, - handleOnCopyTransactionID, - handleOnExpandNote, - handleOnViewOnBlockExplorer, - ], - ); - const toolTipActions = useMemo((): Action[] | Action[][] => { - const actions: (Action | Action[])[] = []; - - if (rowTitle !== loc.lnd.expired) { - actions.push(CommonToolTipActions.CopyAmount); - } - - if (subtitle) { - actions.push(CommonToolTipActions.CopyNote); - } - - if (item.hash) { - actions.push(CommonToolTipActions.CopyTXID, CommonToolTipActions.CopyBlockExplorerLink, [CommonToolTipActions.OpenInBlockExplorer]); - } - - if (subtitle && subtitleNumberOfLines === 1) { - actions.push([CommonToolTipActions.ExpandNote]); - } - - return actions as Action[] | Action[][]; - }, [item.hash, subtitle, rowTitle, subtitleNumberOfLines]); - - const accessibilityState = useMemo(() => { - return { - expanded: subtitleNumberOfLines === 0, }; - }, [subtitleNumberOfLines]); - return ( - - - - ); + const { label: transactionTypeLabel, icon: avatar } = determineTransactionTypeAndAvatar(); + + const amountWithUnit = useMemo(() => { + const amount = formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString(); + const unit = itemPriceUnit === BitcoinUnit.BTC || itemPriceUnit === BitcoinUnit.SATS ? ` ${itemPriceUnit}` : ' '; + return `${amount}${unit}`; + }, [item.value, itemPriceUnit]); + + useEffect(() => { + setSubtitleNumberOfLines(1); + }, [subtitle]); + + const onPress = useCallback(async () => { + menuRef?.current?.dismissMenu?.(); + if (item.hash) { + navigate('TransactionStatus', { hash: item.hash, walletID }); + } else if (item.type === 'user_invoice' || item.type === 'payment_request' || item.type === 'paid_invoice') { + const lightningWallet = wallets.filter(wallet => wallet?.getID() === item.walletID); + if (lightningWallet.length === 1) { + try { + // is it a successful lnurl-pay? + const LN = new Lnurl(false, AsyncStorage); + let paymentHash = item.payment_hash!; + if (typeof paymentHash === 'object') { + paymentHash = Buffer.from(paymentHash.data).toString('hex'); + } + const loaded = await LN.loadSuccessfulPayment(paymentHash); + if (loaded) { + navigate('ScanLndInvoiceRoot', { + screen: 'LnurlPaySuccess', + params: { + paymentHash, + justPaid: false, + fromWalletID: lightningWallet[0].getID(), + }, + }); + return; + } + } catch (e) { + console.log(e); + } + + navigate('LNDViewInvoice', { + invoice: item, + walletID: lightningWallet[0].getID(), + }); + } + } + }, [item, wallets, navigate, walletID]); + + const handleOnExpandNote = useCallback(() => { + setSubtitleNumberOfLines(0); + }, []); + + const subtitleProps = useMemo(() => ({ numberOfLines: subtitleNumberOfLines }), [subtitleNumberOfLines]); + + const handleOnCopyAmountTap = useCallback(() => Clipboard.setString(rowTitle.replace(/[\s\\-]/g, '')), [rowTitle]); + const handleOnCopyTransactionID = useCallback(() => Clipboard.setString(item.hash), [item.hash]); + const handleOnCopyNote = useCallback(() => Clipboard.setString(subtitle ?? ''), [subtitle]); + const handleOnViewOnBlockExplorer = useCallback(() => { + const url = `https://mempool.space/tx/${item.hash}`; + Linking.canOpenURL(url).then(supported => { + if (supported) { + Linking.openURL(url); + } + }); + }, [item.hash]); + const handleCopyOpenInBlockExplorerPress = useCallback(() => { + Clipboard.setString(`https://mempool.space/tx/${item.hash}`); + }, [item.hash]); + + const onToolTipPress = useCallback( + (id: any) => { + if (id === CommonToolTipActions.CopyAmount.id) { + handleOnCopyAmountTap(); + } else if (id === CommonToolTipActions.CopyNote.id) { + handleOnCopyNote(); + } else if (id === CommonToolTipActions.OpenInBlockExplorer.id) { + handleOnViewOnBlockExplorer(); + } else if (id === CommonToolTipActions.ExpandNote.id) { + handleOnExpandNote(); + } else if (id === CommonToolTipActions.CopyBlockExplorerLink.id) { + handleCopyOpenInBlockExplorerPress(); + } else if (id === CommonToolTipActions.CopyTXID.id) { + handleOnCopyTransactionID(); + } + }, + [ + handleCopyOpenInBlockExplorerPress, + handleOnCopyAmountTap, + handleOnCopyNote, + handleOnCopyTransactionID, + handleOnExpandNote, + handleOnViewOnBlockExplorer, + ], + ); + const toolTipActions = useMemo((): Action[] | Action[][] => { + const actions: (Action | Action[])[] = []; + + if (rowTitle !== loc.lnd.expired) { + actions.push(CommonToolTipActions.CopyAmount); + } + + if (subtitle) { + actions.push(CommonToolTipActions.CopyNote); + } + + if (item.hash) { + actions.push(CommonToolTipActions.CopyTXID, CommonToolTipActions.CopyBlockExplorerLink, [CommonToolTipActions.OpenInBlockExplorer]); + } + + if (subtitle && subtitleNumberOfLines === 1) { + actions.push([CommonToolTipActions.ExpandNote]); + } + + return actions as Action[] | Action[][]; + }, [item.hash, subtitle, rowTitle, subtitleNumberOfLines]); + + const accessibilityState = useMemo(() => { + return { + expanded: subtitleNumberOfLines === 0, + }; + }, [subtitleNumberOfLines]); + + const bounceAnim = useBounceAnimation(searchQuery ?? ''); + + const renderHighlightedText = (text: string, query: string) => { + const parts = text.split(new RegExp(`(${query})`, 'gi')); + return ( + <> + {parts.map((part, index) => + part.toLowerCase() === query.toLowerCase() ? ( + + {part} + + ) : ( + {part} + ), + )} + + ); + }; + + return ( + + + + ); + }, +); + +const styles = StyleSheet.create({ + highlighted: { + backgroundColor: 'white', + borderColor: 'black', + borderWidth: 1, + borderRadius: 5, + color: 'black', + }, }); diff --git a/components/WalletsCarousel.tsx b/components/WalletsCarousel.tsx index 8793271f4..775ecf1f6 100644 --- a/components/WalletsCarousel.tsx +++ b/components/WalletsCarousel.tsx @@ -106,6 +106,8 @@ interface WalletCarouselItemProps { horizontal?: boolean; isActive?: boolean; allowOnPressAnimation?: boolean; + searchQuery?: string; + renderHighlightedText?: (text: string, query: string) => JSX.Element; } const iStyles = StyleSheet.create({ @@ -168,7 +170,17 @@ const iStyles = StyleSheet.create({ }); export const WalletCarouselItem: React.FC = React.memo( - ({ item, onPress, handleLongPress, isSelectedWallet, customStyle, horizontal, allowOnPressAnimation = true }) => { + ({ + item, + onPress, + handleLongPress, + isSelectedWallet, + customStyle, + horizontal, + allowOnPressAnimation = true, + searchQuery, + renderHighlightedText, + }) => { const scaleValue = useRef(new Animated.Value(1.0)).current; const { colors } = useTheme(); const { walletTransactionUpdateStatus } = useStorage(); @@ -246,7 +258,7 @@ export const WalletCarouselItem: React.FC = React.memo( - {item.getLabel()} + {renderHighlightedText ? renderHighlightedText(item.getLabel(), searchQuery ?? '') : item.getLabel()} {item.hideBalance ? ( diff --git a/loc/en.json b/loc/en.json index c0d51731d..75c39f1db 100644 --- a/loc/en.json +++ b/loc/en.json @@ -495,7 +495,7 @@ "add_ln_wallet_first": "You must first add a Lightning wallet.", "identity_pubkey": "Identity Pubkey", "xpub_title": "Wallet XPUB", - "search_wallets": "Search Wallets" + "search_wallets": "Search wallets, memos, and transactions", }, "multisig": { "multisig_vault": "Vault", diff --git a/screen/wallets/ReorderWallets.tsx b/screen/wallets/ReorderWallets.tsx index c9c1ac207..a709c07c7 100644 --- a/screen/wallets/ReorderWallets.tsx +++ b/screen/wallets/ReorderWallets.tsx @@ -1,21 +1,24 @@ -import React, { useEffect, useLayoutEffect, useRef, useReducer, useCallback } from 'react'; -import { Platform, StyleSheet, useColorScheme } from 'react-native'; +import React, { useEffect, useLayoutEffect, useRef, useReducer, useCallback, useMemo } from 'react'; +import { Platform, StyleSheet, useColorScheme, TouchableOpacity, Image, Animated, Text, I18nManager } from 'react-native'; // @ts-ignore: fix later import DraggableFlatList, { ScaleDecorator } from 'react-native-draggable-flatlist'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback'; import { useTheme } from '../../components/themes'; import { WalletCarouselItem } from '../../components/WalletsCarousel'; +import { TransactionListItem } from '../../components/TransactionListItem'; import { useExtendedNavigation } from '../../hooks/useExtendedNavigation'; import loc from '../../loc'; import { useStorage } from '../../hooks/context/useStorage'; +import useDebounce from '../../hooks/useDebounce'; +import { Header } from '../../components/Header'; -// Action Types const SET_SEARCH_QUERY = 'SET_SEARCH_QUERY'; const SET_IS_SEARCH_FOCUSED = 'SET_IS_SEARCH_FOCUSED'; const SET_WALLET_DATA = 'SET_WALLET_DATA'; +const SET_TX_METADATA = 'SET_TX_METADATA'; +const SET_ORDER = 'SET_ORDER'; -// Action Interfaces interface SetSearchQueryAction { type: typeof SET_SEARCH_QUERY; payload: string; @@ -31,23 +34,34 @@ interface SetWalletDataAction { payload: any[]; } -type Action = SetSearchQueryAction | SetIsSearchFocusedAction | SetWalletDataAction; +interface SetTxMetadataAction { + type: typeof SET_TX_METADATA; + payload: { [key: string]: { memo?: string } }; +} + +interface SetOrderAction { + type: typeof SET_ORDER; + payload: any[]; +} + +type Action = SetSearchQueryAction | SetIsSearchFocusedAction | SetWalletDataAction | SetTxMetadataAction | SetOrderAction; -// State Interface interface State { searchQuery: string; isSearchFocused: boolean; walletData: any[]; + txMetadata: { [key: string]: { memo?: string } }; + order: any[]; } -// Initial State const initialState: State = { searchQuery: '', isSearchFocused: false, walletData: [], + txMetadata: {}, + order: [], }; -// Reducer const reducer = (state: State, action: Action): State => { switch (action.type) { case SET_SEARCH_QUERY: @@ -56,15 +70,41 @@ const reducer = (state: State, action: Action): State => { return { ...state, isSearchFocused: action.payload }; case SET_WALLET_DATA: return { ...state, walletData: action.payload }; + case SET_TX_METADATA: + return { ...state, txMetadata: action.payload }; + case SET_ORDER: + return { ...state, order: action.payload }; default: return state; } }; +const useBounceAnimation = (query: string) => { + const bounceAnim = useRef(new Animated.Value(1.0)).current; + + useEffect(() => { + if (query) { + Animated.timing(bounceAnim, { + toValue: 1.2, + duration: 150, + useNativeDriver: true, + }).start(() => { + Animated.timing(bounceAnim, { + toValue: 1.0, + duration: 150, + useNativeDriver: true, + }).start(); + }); + } + }, [bounceAnim, query]); + + return bounceAnim; +}; + const ReorderWallets: React.FC = () => { const sortableList = useRef(null); - const { colors } = useTheme(); - const { wallets, setWalletsWithNewOrder } = useStorage(); + const { colors, closeImage } = useTheme(); + const { wallets, setWalletsWithNewOrder, txMetadata } = useStorage(); const colorScheme = useColorScheme(); const { navigate, setOptions, goBack } = useExtendedNavigation(); const [state, dispatch] = useReducer(reducer, initialState); @@ -80,18 +120,53 @@ const ReorderWallets: React.FC = () => { useEffect(() => { dispatch({ type: SET_WALLET_DATA, payload: wallets }); - }, [wallets]); + dispatch({ type: SET_TX_METADATA, payload: txMetadata }); + dispatch({ type: SET_ORDER, payload: wallets }); + }, [wallets, txMetadata]); + + const handleClose = useCallback(() => { + const walletOrder = state.order.filter(item => item.type === 'wallet').map(item => item.data); + setWalletsWithNewOrder(walletOrder); + goBack(); + }, [goBack, setWalletsWithNewOrder, state.order]); + + const HeaderRightButton = useMemo( + () => ( + + + + ), + [handleClose, closeImage], + ); useEffect(() => { setOptions({ statusBarStyle: Platform.select({ ios: 'light', default: colorScheme === 'dark' ? 'light' : 'dark' }), + headerRight: () => HeaderRightButton, }); - }, [colorScheme, setOptions]); + }, [colorScheme, setOptions, HeaderRightButton]); + + const debouncedSearchQuery = useDebounce(state.searchQuery, 300); useEffect(() => { - const filteredWallets = wallets.filter(wallet => wallet.getLabel().toLowerCase().includes(state.searchQuery.toLowerCase())); - dispatch({ type: SET_WALLET_DATA, payload: filteredWallets }); - }, [wallets, state.searchQuery]); + if (debouncedSearchQuery) { + const filteredWallets = wallets.filter(wallet => wallet.getLabel()?.toLowerCase().includes(debouncedSearchQuery.toLowerCase())); + const filteredTxMetadata = Object.entries(txMetadata).filter(([_, tx]) => + tx.memo?.toLowerCase().includes(debouncedSearchQuery.toLowerCase()), + ); + dispatch({ type: SET_WALLET_DATA, payload: filteredWallets }); + dispatch({ type: SET_TX_METADATA, payload: Object.fromEntries(filteredTxMetadata) }); + } else { + dispatch({ type: SET_WALLET_DATA, payload: wallets }); + dispatch({ type: SET_TX_METADATA, payload: {} }); + } + }, [wallets, txMetadata, debouncedSearchQuery]); useLayoutEffect(() => { setOptions({ @@ -120,23 +195,61 @@ const ReorderWallets: React.FC = () => { const isDraggingDisabled = state.searchQuery.length > 0 || state.isSearchFocused; + const bounceAnim = useBounceAnimation(state.searchQuery); + + const renderHighlightedText = useCallback( + (text: string, query: string) => { + const parts = text.split(new RegExp(`(${query})`, 'gi')); + return ( + + {parts.map((part, index) => + part.toLowerCase() === query.toLowerCase() ? ( + + {part} + + ) : ( + + {part} + + ), + )} + + ); + }, + [bounceAnim], + ); + const renderItem = useCallback( ({ item, drag, isActive }: any) => { - const itemOpacity = isActive ? 1 : 0.5; + const itemOpacity = isActive ? 1 : state.searchQuery ? 0.5 : 1; + + if (item.type === 'transaction') { + return ( + + ); + } return ( ); }, - [isDraggingDisabled, navigateToWallet], + [isDraggingDisabled, navigateToWallet, state.searchQuery, renderHighlightedText], ); const onChangeOrder = useCallback(() => { @@ -151,23 +264,40 @@ const ReorderWallets: React.FC = () => { triggerHapticFeedback(HapticFeedbackTypes.ImpactLight); }, []); - const onDragEnd = useCallback( - ({ data }: any) => { - setWalletsWithNewOrder(data); - dispatch({ type: SET_WALLET_DATA, payload: data }); - }, - [setWalletsWithNewOrder], - ); + const onDragEnd = useCallback(({ data }: any) => { + dispatch({ type: SET_ORDER, payload: data }); + }, []); const _keyExtractor = useCallback((_item: any, index: number) => index.toString(), []); + const data = state.searchQuery + ? [ + ...state.walletData.map(wallet => ({ type: 'wallet', data: wallet })), + ...Object.entries(state.txMetadata).map(([txid, tx]) => ({ type: 'transaction', data: { txid, ...tx } })), + ] + : state.walletData.map(wallet => ({ type: 'wallet', data: wallet })); + + const renderHeader = useMemo(() => { + if (!state.searchQuery) return null; + const hasWallets = state.walletData.length > 0; + const hasTransactions = Object.keys(state.txMetadata).length > 0; + + return ( + <> + {hasWallets &&
} + {hasTransactions &&
} + {!hasWallets && !hasTransactions && No results found} + + ); + }, [state.searchQuery, state.walletData, state.txMetadata]); + return ( { onRelease={onRelease} onDragEnd={onDragEnd} containerStyle={styles.root} + ListHeaderComponent={renderHeader} /> ); @@ -189,4 +320,33 @@ const styles = StyleSheet.create({ padding16: { padding: 16, }, + button: { + padding: 16, + }, + noResultsText: { + fontSize: 18, + textAlign: 'center', + marginTop: 20, + }, +}); + +const iStyles = StyleSheet.create({ + highlightedContainer: { + backgroundColor: 'white', + borderColor: 'black', + borderWidth: 1, + borderRadius: 5, + padding: 2, + }, + highlighted: { + color: 'black', + }, + defaultText: { + fontSize: 19, + fontWeight: 'bold', + writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr', + }, + dimmedText: { + opacity: 0.5, + }, }); From 6289485ff9378012ae06364d5ce6f55618e46d5e Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Wed, 24 Jul 2024 20:22:06 -0400 Subject: [PATCH 2/8] REF: ManageWallets --- loc/en.json | 5 +++-- navigation/DetailViewScreensStack.tsx | 6 +++--- navigation/DetailViewStackParamList.ts | 2 +- ...alletsStack.tsx => ManageWalletsStack.tsx} | 12 ++++++------ navigation/SendDetailsStackParamList.ts | 2 +- screen/wallets/DrawerList.tsx | 2 +- .../{ReorderWallets.tsx => ManageWallets.tsx} | 19 ++++++++++++------- screen/wallets/WalletsList.tsx | 2 +- 8 files changed, 28 insertions(+), 22 deletions(-) rename navigation/{ReorderWalletsStack.tsx => ManageWalletsStack.tsx} (68%) rename screen/wallets/{ReorderWallets.tsx => ManageWallets.tsx} (94%) diff --git a/loc/en.json b/loc/en.json index 75c39f1db..f339087ed 100644 --- a/loc/en.json +++ b/loc/en.json @@ -484,7 +484,8 @@ "list_tryagain": "Try again", "no_ln_wallet_error": "Before paying a Lightning invoice, you must first add a Lightning wallet.", "looks_like_bip38": "This looks like a password-protected private key (BIP38).", - "reorder_title": "Re-order Wallets", + "manage_title": "Manage Wallets", + "no_results_found": "No results found.", "please_continue_scanning": "Please continue scanning.", "select_no_bitcoin": "There are currently no Bitcoin wallets available.", "select_no_bitcoin_exp": "A Bitcoin wallet is required to refill Lightning wallets. Please create or import one.", @@ -495,7 +496,7 @@ "add_ln_wallet_first": "You must first add a Lightning wallet.", "identity_pubkey": "Identity Pubkey", "xpub_title": "Wallet XPUB", - "search_wallets": "Search wallets, memos, and transactions", + "search_wallets": "Search wallets, memos, and transactions" }, "multisig": { "multisig_vault": "Vault", diff --git a/navigation/DetailViewScreensStack.tsx b/navigation/DetailViewScreensStack.tsx index d9a3a818f..131a65f72 100644 --- a/navigation/DetailViewScreensStack.tsx +++ b/navigation/DetailViewScreensStack.tsx @@ -55,7 +55,6 @@ import PaymentCodesListComponent from './LazyLoadPaymentCodeStack'; import LDKOpenChannelRoot from './LDKOpenChannelStack'; import LNDCreateInvoiceRoot from './LNDCreateInvoiceStack'; import ReceiveDetailsStackRoot from './ReceiveDetailsStack'; -import ReorderWalletsStackRoot from './ReorderWalletsStack'; import ScanLndInvoiceRoot from './ScanLndInvoiceStack'; import ScanQRCodeStackRoot from './ScanQRCodeStack'; import SendDetailsStack from './SendDetailsStack'; @@ -66,6 +65,7 @@ import WalletXpubStackRoot from './WalletXpubStack'; import PlusIcon from '../components/icons/PlusIcon'; import SettingsButton from '../components/icons/SettingsButton'; import ExportMultisigCoordinationSetupStack from './ExportMultisigCoordinationSetupStack'; +import ManageWalletsStackRoot from './ManageWalletsStack'; const DetailViewStackScreensStack = () => { const theme = useTheme(); @@ -379,8 +379,8 @@ const DetailViewStackScreensStack = () => { }} /> { +const ManageWalletsStackRoot = () => { const theme = useTheme(); return ( ); }; -export default ReorderWalletsStackRoot; +export default ManageWalletsStackRoot; diff --git a/navigation/SendDetailsStackParamList.ts b/navigation/SendDetailsStackParamList.ts index 7a8f1bbb6..038ad79e3 100644 --- a/navigation/SendDetailsStackParamList.ts +++ b/navigation/SendDetailsStackParamList.ts @@ -213,5 +213,5 @@ export type DetailViewStackParamList = { paymentCode: string; walletID: string; }; - ReorderWallets: undefined; + ManageWallets: undefined; }; diff --git a/screen/wallets/DrawerList.tsx b/screen/wallets/DrawerList.tsx index f34b0210e..31515ebd1 100644 --- a/screen/wallets/DrawerList.tsx +++ b/screen/wallets/DrawerList.tsx @@ -124,7 +124,7 @@ const DrawerList: React.FC = memo(({ navigation }) => { const handleLongPress = useCallback(() => { if (state.wallets.length > 1) { - navigation.navigate('ReorderWallets'); + navigation.navigate('ManageWalletsRoot'); } else { triggerHapticFeedback(HapticFeedbackTypes.NotificationError); } diff --git a/screen/wallets/ReorderWallets.tsx b/screen/wallets/ManageWallets.tsx similarity index 94% rename from screen/wallets/ReorderWallets.tsx rename to screen/wallets/ManageWallets.tsx index a709c07c7..c0d276f3a 100644 --- a/screen/wallets/ReorderWallets.tsx +++ b/screen/wallets/ManageWallets.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useLayoutEffect, useRef, useReducer, useCallback, useMemo } from 'react'; import { Platform, StyleSheet, useColorScheme, TouchableOpacity, Image, Animated, Text, I18nManager } from 'react-native'; -// @ts-ignore: fix later +// @ts-ignore: no declaration file import DraggableFlatList, { ScaleDecorator } from 'react-native-draggable-flatlist'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback'; @@ -101,7 +101,7 @@ const useBounceAnimation = (query: string) => { return bounceAnim; }; -const ReorderWallets: React.FC = () => { +const ManageWallets: React.FC = () => { const sortableList = useRef(null); const { colors, closeImage } = useTheme(); const { wallets, setWalletsWithNewOrder, txMetadata } = useStorage(); @@ -116,6 +116,9 @@ const ReorderWallets: React.FC = () => { tip: { backgroundColor: colors.ballOutgoingExpired, }, + noResultsText: { + color: colors.foregroundColor, + }, }; useEffect(() => { @@ -284,12 +287,14 @@ const ReorderWallets: React.FC = () => { return ( <> - {hasWallets &&
} - {hasTransactions &&
} - {!hasWallets && !hasTransactions && No results found} + {hasWallets &&
} + {hasTransactions &&
} + {!hasWallets && !hasTransactions && ( + {loc.wallets.no_results_found} + )} ); - }, [state.searchQuery, state.walletData, state.txMetadata]); + }, [state.searchQuery, state.walletData.length, state.txMetadata, stylesHook.noResultsText]); return ( @@ -311,7 +316,7 @@ const ReorderWallets: React.FC = () => { ); }; -export default ReorderWallets; +export default ManageWallets; const styles = StyleSheet.create({ root: { diff --git a/screen/wallets/WalletsList.tsx b/screen/wallets/WalletsList.tsx index db3143cac..5bda72255 100644 --- a/screen/wallets/WalletsList.tsx +++ b/screen/wallets/WalletsList.tsx @@ -229,7 +229,7 @@ const WalletsList: React.FC = () => { const handleLongPress = useCallback(() => { if (wallets.length > 1) { - navigate('ReorderWallets'); + navigate('ManageWalletsRoot'); } else { triggerHapticFeedback(HapticFeedbackTypes.NotificationError); } From ecfd52393ea8828396ed5c40ca687bfcb673a59d Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Wed, 24 Jul 2024 20:53:40 -0400 Subject: [PATCH 3/8] wip --- package-lock.json | 4 ++-- package.json | 2 +- screen/wallets/ManageWallets.tsx | 34 ++++++++++++++------------------ 3 files changed, 18 insertions(+), 22 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0e782d03b..14429b49f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -69,7 +69,7 @@ "react-native-default-preference": "1.4.4", "react-native-device-info": "11.1.0", "react-native-document-picker": "https://github.com/BlueWallet/react-native-document-picker#6033c4e1b0dd0a6760b5f5a5a2c3b2e5d07f2ae4", - "react-native-draggable-flatlist": "github:BlueWallet/react-native-draggable-flatlist#ebfddc4", + "react-native-draggable-flatlist": "github:BlueWallet/react-native-draggable-flatlist#3061e30", "react-native-fs": "2.20.0", "react-native-gesture-handler": "2.17.1", "react-native-handoff": "https://github.com/BlueWallet/react-native-handoff#31d005f93d31099d0e564590a3bbd052b8a02b39", @@ -37441,7 +37441,7 @@ }, "react-native-draggable-flatlist": { "version": "git+ssh://git@github.com/BlueWallet/react-native-draggable-flatlist.git#ebfddc4877e8f65d5391a748db61b9cd030430ba", - "from": "react-native-draggable-flatlist@github:BlueWallet/react-native-draggable-flatlist#ebfddc4", + "from": "react-native-draggable-flatlist@github:BlueWallet/react-native-draggable-flatlist#3061e30", "requires": { "@babel/preset-typescript": "^7.17.12" } diff --git a/package.json b/package.json index 9ba9e229c..201a0ba35 100644 --- a/package.json +++ b/package.json @@ -163,7 +163,7 @@ "react-native-default-preference": "1.4.4", "react-native-device-info": "11.1.0", "react-native-document-picker": "https://github.com/BlueWallet/react-native-document-picker#6033c4e1b0dd0a6760b5f5a5a2c3b2e5d07f2ae4", - "react-native-draggable-flatlist": "github:BlueWallet/react-native-draggable-flatlist#ebfddc4", + "react-native-draggable-flatlist": "github:BlueWallet/react-native-draggable-flatlist#3061e30", "react-native-fs": "2.20.0", "react-native-gesture-handler": "2.17.1", "react-native-handoff": "https://github.com/BlueWallet/react-native-handoff#31d005f93d31099d0e564590a3bbd052b8a02b39", diff --git a/screen/wallets/ManageWallets.tsx b/screen/wallets/ManageWallets.tsx index c0d276f3a..a31802eda 100644 --- a/screen/wallets/ManageWallets.tsx +++ b/screen/wallets/ManageWallets.tsx @@ -12,6 +12,8 @@ import loc from '../../loc'; import { useStorage } from '../../hooks/context/useStorage'; import useDebounce from '../../hooks/useDebounce'; import { Header } from '../../components/Header'; +import { TTXMetadata } from '../../class'; +import { TWallet } from '../../class/wallets/types'; const SET_SEARCH_QUERY = 'SET_SEARCH_QUERY'; const SET_IS_SEARCH_FOCUSED = 'SET_IS_SEARCH_FOCUSED'; @@ -49,8 +51,8 @@ type Action = SetSearchQueryAction | SetIsSearchFocusedAction | SetWalletDataAct interface State { searchQuery: string; isSearchFocused: boolean; - walletData: any[]; - txMetadata: { [key: string]: { memo?: string } }; + walletData: TWallet[]; + txMetadata: TTXMetadata; order: any[]; } @@ -122,9 +124,10 @@ const ManageWallets: React.FC = () => { }; useEffect(() => { + const initialOrder = wallets.map(wallet => ({ type: 'wallet', data: wallet })); dispatch({ type: SET_WALLET_DATA, payload: wallets }); dispatch({ type: SET_TX_METADATA, payload: txMetadata }); - dispatch({ type: SET_ORDER, payload: wallets }); + dispatch({ type: SET_ORDER, payload: initialOrder }); }, [wallets, txMetadata]); const handleClose = useCallback(() => { @@ -163,11 +166,18 @@ const ManageWallets: React.FC = () => { const filteredTxMetadata = Object.entries(txMetadata).filter(([_, tx]) => tx.memo?.toLowerCase().includes(debouncedSearchQuery.toLowerCase()), ); + const filteredOrder = [ + ...filteredWallets.map(wallet => ({ type: 'wallet', data: wallet })), + ...Object.entries(filteredTxMetadata).map(([txid, tx]) => ({ type: 'transaction', data: { txid, ...tx } })), + ]; dispatch({ type: SET_WALLET_DATA, payload: filteredWallets }); dispatch({ type: SET_TX_METADATA, payload: Object.fromEntries(filteredTxMetadata) }); + dispatch({ type: SET_ORDER, payload: filteredOrder }); } else { + const initialOrder = wallets.map(wallet => ({ type: 'wallet', data: wallet })); dispatch({ type: SET_WALLET_DATA, payload: wallets }); dispatch({ type: SET_TX_METADATA, payload: {} }); + dispatch({ type: SET_ORDER, payload: initialOrder }); } }, [wallets, txMetadata, debouncedSearchQuery]); @@ -273,13 +283,6 @@ const ManageWallets: React.FC = () => { const _keyExtractor = useCallback((_item: any, index: number) => index.toString(), []); - const data = state.searchQuery - ? [ - ...state.walletData.map(wallet => ({ type: 'wallet', data: wallet })), - ...Object.entries(state.txMetadata).map(([txid, tx]) => ({ type: 'transaction', data: { txid, ...tx } })), - ] - : state.walletData.map(wallet => ({ type: 'wallet', data: wallet })); - const renderHeader = useMemo(() => { if (!state.searchQuery) return null; const hasWallets = state.walletData.length > 0; @@ -289,9 +292,7 @@ const ManageWallets: React.FC = () => { <> {hasWallets &&
} {hasTransactions &&
} - {!hasWallets && !hasTransactions && ( - {loc.wallets.no_results_found} - )} + {!hasWallets && !hasTransactions && {loc.wallets.no_results_found}} ); }, [state.searchQuery, state.walletData.length, state.txMetadata, stylesHook.noResultsText]); @@ -302,7 +303,7 @@ const ManageWallets: React.FC = () => { ref={sortableList} contentInsetAdjustmentBehavior="automatic" automaticallyAdjustContentInsets - data={data} + data={state.order} keyExtractor={_keyExtractor} renderItem={renderItem} onChangeOrder={onChangeOrder} @@ -328,11 +329,6 @@ const styles = StyleSheet.create({ button: { padding: 16, }, - noResultsText: { - fontSize: 18, - textAlign: 'center', - marginTop: 20, - }, }); const iStyles = StyleSheet.create({ From 9a31d729f407c220f0112b69475a68e0112c7c15 Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Thu, 25 Jul 2024 01:54:51 -0400 Subject: [PATCH 4/8] wip --- components/WalletsCarousel.tsx | 267 ++++++++++++++++--------------- loc/en.json | 2 +- screen/wallets/ManageWallets.tsx | 46 ++++-- 3 files changed, 170 insertions(+), 145 deletions(-) diff --git a/components/WalletsCarousel.tsx b/components/WalletsCarousel.tsx index 775ecf1f6..b67ff5279 100644 --- a/components/WalletsCarousel.tsx +++ b/components/WalletsCarousel.tsx @@ -105,11 +105,128 @@ interface WalletCarouselItemProps { customStyle?: ViewStyle; horizontal?: boolean; isActive?: boolean; - allowOnPressAnimation?: boolean; searchQuery?: string; renderHighlightedText?: (text: string, query: string) => JSX.Element; } +export const WalletCarouselItem: React.FC = ({ + item, + onPress, + handleLongPress, + isSelectedWallet, + customStyle, + horizontal, + isActive, + searchQuery, + renderHighlightedText, +}) => { + const scaleValue = useRef(new Animated.Value(1.0)).current; + const { colors } = useTheme(); + const { width } = useWindowDimensions(); + const itemWidth = width * 0.82 > 375 ? 375 : width * 0.82; + const isLargeScreen = useIsLargeScreen(); + + const onPressedIn = useCallback(() => { + Animated.spring(scaleValue, { + toValue: 0.95, + useNativeDriver: true, + friction: 3, + tension: 100, + }).start(); + }, [scaleValue]); + + const onPressedOut = useCallback(() => { + Animated.spring(scaleValue, { + toValue: 1.0, + useNativeDriver: true, + friction: 3, + tension: 100, + }).start(); + }, [scaleValue]); + + const handlePress = useCallback(() => { + onPressedOut(); + onPress(item); + }, [item, onPress, onPressedOut]); + + const opacity = isSelectedWallet === false ? 0.5 : 1.0; + let image; + switch (item.type) { + case LightningLdkWallet.type: + case LightningCustodianWallet.type: + image = I18nManager.isRTL ? require('../img/lnd-shape-rtl.png') : require('../img/lnd-shape.png'); + break; + case MultisigHDWallet.type: + image = I18nManager.isRTL ? require('../img/vault-shape-rtl.png') : require('../img/vault-shape.png'); + break; + default: + image = I18nManager.isRTL ? require('../img/btc-shape-rtl.png') : require('../img/btc-shape.png'); + } + + const latestTransactionText = + item.getBalance() !== 0 && item.getLatestTransactionTime() === 0 + ? loc.wallets.pull_to_refresh + : item.getTransactions().find((tx: Transaction) => tx.confirmations === 0) + ? loc.transactions.pending + : transactionTimeToReadable(item.getLatestTransactionTime()); + + const balance = !item.hideBalance && formatBalance(Number(item.getBalance()), item.getPreferredBalanceUnit(), true); + + return ( + + { + if (handleLongPress) handleLongPress(); + }} + onPress={handlePress} + > + + + + + + {renderHighlightedText && searchQuery ? renderHighlightedText(item.getLabel(), searchQuery) : item.getLabel()} + + + {item.hideBalance ? ( + <> + + + + ) : ( + + {`${balance} `} + + )} + + + + {loc.wallets.list_latest_transaction} + + + {latestTransactionText} + + + + + + ); +}; + const iStyles = StyleSheet.create({ root: { paddingRight: 20 }, rootLargeDevice: { marginVertical: 20 }, @@ -169,129 +286,6 @@ const iStyles = StyleSheet.create({ }, }); -export const WalletCarouselItem: React.FC = React.memo( - ({ - item, - onPress, - handleLongPress, - isSelectedWallet, - customStyle, - horizontal, - allowOnPressAnimation = true, - searchQuery, - renderHighlightedText, - }) => { - const scaleValue = useRef(new Animated.Value(1.0)).current; - const { colors } = useTheme(); - const { walletTransactionUpdateStatus } = useStorage(); - const { width } = useWindowDimensions(); - const itemWidth = width * 0.82 > 375 ? 375 : width * 0.82; - const isLargeScreen = useIsLargeScreen(); - - const onPressedIn = useCallback(() => { - Animated.spring(scaleValue, { - toValue: 0.95, - useNativeDriver: true, - friction: 3, - tension: 100, - }).start(); - }, [scaleValue]); - - const onPressedOut = useCallback(() => { - Animated.spring(scaleValue, { - toValue: 1.0, - useNativeDriver: true, - friction: 3, - tension: 100, - }).start(); - }, [scaleValue]); - - const handlePress = useCallback(() => { - onPressedOut(); - onPress(item); - }, [item, onPress, onPressedOut]); - - const opacity = isSelectedWallet === false ? 0.5 : 1.0; - let image; - switch (item.type) { - case LightningLdkWallet.type: - case LightningCustodianWallet.type: - image = I18nManager.isRTL ? require('../img/lnd-shape-rtl.png') : require('../img/lnd-shape.png'); - break; - case MultisigHDWallet.type: - image = I18nManager.isRTL ? require('../img/vault-shape-rtl.png') : require('../img/vault-shape.png'); - break; - default: - image = I18nManager.isRTL ? require('../img/btc-shape-rtl.png') : require('../img/btc-shape.png'); - } - - const latestTransactionText = - walletTransactionUpdateStatus === WalletTransactionsStatus.ALL || walletTransactionUpdateStatus === item.getID() - ? loc.transactions.updating - : item.getBalance() !== 0 && item.getLatestTransactionTime() === 0 - ? loc.wallets.pull_to_refresh - : item.getTransactions().find((tx: Transaction) => tx.confirmations === 0) - ? loc.transactions.pending - : transactionTimeToReadable(item.getLatestTransactionTime()); - - const balance = !item.hideBalance && formatBalance(Number(item.getBalance()), item.getPreferredBalanceUnit(), true); - - return ( - - { - if (handleLongPress) handleLongPress(); - }} - onPress={handlePress} - > - - - - - - {renderHighlightedText ? renderHighlightedText(item.getLabel(), searchQuery ?? '') : item.getLabel()} - - - {item.hideBalance ? ( - <> - - - - ) : ( - - {`${balance} `} - - )} - - - - {loc.wallets.list_latest_transaction} - - - {latestTransactionText} - - - - - - ); - }, -); - interface WalletsCarouselProps extends Partial> { horizontal?: boolean; selectedWallet?: string; @@ -300,7 +294,8 @@ interface WalletsCarouselProps extends Partial> { handleLongPress?: () => void; data: TWallet[]; scrollEnabled?: boolean; - showNewWalletPanel?: boolean; // New prop + renderHighlightedText?: (text: string, query: string) => JSX.Element; + searchQuery?: string; } type FlatListRefType = FlatList & { @@ -329,7 +324,17 @@ const cStyles = StyleSheet.create({ const ListHeaderComponent: React.FC = () => ; const WalletsCarousel = forwardRef((props, ref) => { - const { horizontal, data, handleLongPress, onPress, selectedWallet, scrollEnabled, showNewWalletPanel, onNewWalletPress } = props; + const { + horizontal, + data, + handleLongPress, + onPress, + selectedWallet, + scrollEnabled, + onNewWalletPress, + searchQuery, + renderHighlightedText, + } = props; const renderItem = useCallback( ({ item, index }: ListRenderItemInfo) => item ? ( @@ -339,9 +344,11 @@ const WalletsCarousel = forwardRef((props handleLongPress={handleLongPress} onPress={onPress} horizontal={horizontal} + searchQuery={searchQuery} + renderHighlightedText={renderHighlightedText} /> ) : null, - [horizontal, selectedWallet, handleLongPress, onPress], + [horizontal, selectedWallet, handleLongPress, onPress, searchQuery, renderHighlightedText], ); const flatListRef = useRef>(null); @@ -406,7 +413,7 @@ const WalletsCarousel = forwardRef((props ListHeaderComponent={ListHeaderComponent} style={{ minHeight: sliderHeight + 12 }} onScrollToIndexFailed={onScrollToIndexFailed} - ListFooterComponent={showNewWalletPanel && onNewWalletPress ? : null} + ListFooterComponent={onNewWalletPress ? : null} {...props} /> ) : ( @@ -419,10 +426,12 @@ const WalletsCarousel = forwardRef((props handleLongPress={handleLongPress} onPress={onPress} key={index} + searchQuery={props.searchQuery} + renderHighlightedText={props.renderHighlightedText} /> ) : null, )} - {showNewWalletPanel && onNewWalletPress && } + {onNewWalletPress && } ); }); diff --git a/loc/en.json b/loc/en.json index f339087ed..bf7e45a70 100644 --- a/loc/en.json +++ b/loc/en.json @@ -496,7 +496,7 @@ "add_ln_wallet_first": "You must first add a Lightning wallet.", "identity_pubkey": "Identity Pubkey", "xpub_title": "Wallet XPUB", - "search_wallets": "Search wallets, memos, and transactions" + "search_wallets": "Search wallets, memos" }, "multisig": { "multisig_vault": "Vault", diff --git a/screen/wallets/ManageWallets.tsx b/screen/wallets/ManageWallets.tsx index a31802eda..3ffd05ccc 100644 --- a/screen/wallets/ManageWallets.tsx +++ b/screen/wallets/ManageWallets.tsx @@ -1,11 +1,10 @@ import React, { useEffect, useLayoutEffect, useRef, useReducer, useCallback, useMemo } from 'react'; -import { Platform, StyleSheet, useColorScheme, TouchableOpacity, Image, Animated, Text, I18nManager } from 'react-native'; +import { Platform, StyleSheet, useColorScheme, TouchableOpacity, Image, Animated, Text, I18nManager, View } from 'react-native'; // @ts-ignore: no declaration file import DraggableFlatList, { ScaleDecorator } from 'react-native-draggable-flatlist'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback'; import { useTheme } from '../../components/themes'; -import { WalletCarouselItem } from '../../components/WalletsCarousel'; import { TransactionListItem } from '../../components/TransactionListItem'; import { useExtendedNavigation } from '../../hooks/useExtendedNavigation'; import loc from '../../loc'; @@ -14,6 +13,8 @@ import useDebounce from '../../hooks/useDebounce'; import { Header } from '../../components/Header'; import { TTXMetadata } from '../../class'; import { TWallet } from '../../class/wallets/types'; +import { BitcoinUnit } from '../../models/bitcoinUnits'; +import { WalletCarouselItem } from '../../components/WalletsCarousel'; const SET_SEARCH_QUERY = 'SET_SEARCH_QUERY'; const SET_IS_SEARCH_FOCUSED = 'SET_IS_SEARCH_FOCUSED'; @@ -86,19 +87,21 @@ const useBounceAnimation = (query: string) => { useEffect(() => { if (query) { - Animated.timing(bounceAnim, { + Animated.spring(bounceAnim, { toValue: 1.2, - duration: 150, useNativeDriver: true, + friction: 3, + tension: 100, }).start(() => { - Animated.timing(bounceAnim, { + Animated.spring(bounceAnim, { toValue: 1.0, - duration: 150, useNativeDriver: true, + friction: 3, + tension: 100, }).start(); }); } - }, [bounceAnim, query]); + }, [query]); return bounceAnim; }; @@ -238,13 +241,16 @@ const ManageWallets: React.FC = () => { if (item.type === 'transaction') { return ( - + + + ); } @@ -292,7 +298,9 @@ const ManageWallets: React.FC = () => { <> {hasWallets &&
} {hasTransactions &&
} - {!hasWallets && !hasTransactions && {loc.wallets.no_results_found}} + {!hasWallets && !hasTransactions && ( + {loc.wallets.no_results_found} + )} ); }, [state.searchQuery, state.walletData.length, state.txMetadata, stylesHook.noResultsText]); @@ -329,6 +337,14 @@ const styles = StyleSheet.create({ button: { padding: 16, }, + noResultsText: { + fontSize: 19, + fontWeight: 'bold', + writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr', + textAlign: 'center', + justifyContent: 'center', + marginTop: 34, + }, }); const iStyles = StyleSheet.create({ From e7b41786bd9452872834be32f101fe6a3dd46c22 Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Thu, 25 Jul 2024 18:41:01 -0400 Subject: [PATCH 5/8] wip --- components/TransactionListItem.tsx | 54 ++++------ hooks/useBounceAnimation.ts | 26 +++++ ios/BlueWallet.xcodeproj/project.pbxproj | 4 +- loc/en.json | 2 +- screen/wallets/ManageWallets.tsx | 127 ++++++++++++----------- 5 files changed, 119 insertions(+), 94 deletions(-) create mode 100644 hooks/useBounceAnimation.ts diff --git a/components/TransactionListItem.tsx b/components/TransactionListItem.tsx index 9ea847d5a..a596cddd8 100644 --- a/components/TransactionListItem.tsx +++ b/components/TransactionListItem.tsx @@ -23,6 +23,7 @@ import { DetailViewStackParamList } from '../navigation/DetailViewStackParamList import { useStorage } from '../hooks/context/useStorage'; import ToolTipMenu from './TooltipMenu'; import { CommonToolTipActions } from '../typings/CommonToolTipActions'; +import useBounceAnimation from '../hooks/useBounceAnimation'; interface TransactionListItemProps { itemPriceUnit: BitcoinUnit; @@ -34,30 +35,6 @@ interface TransactionListItemProps { type NavigationProps = NativeStackNavigationProp; -const useBounceAnimation = (query: string) => { - const bounceAnim = useRef(new Animated.Value(1.0)).current; - - useEffect(() => { - if (query) { - Animated.spring(bounceAnim, { - toValue: 1.2, - useNativeDriver: true, - friction: 3, - tension: 100, - }).start(() => { - Animated.spring(bounceAnim, { - toValue: 1.0, - useNativeDriver: true, - friction: 3, - tension: 100, - }).start(); - }); - } - }, [query, bounceAnim]); - - return bounceAnim; -}; - export const TransactionListItem: React.FC = React.memo( ({ item, itemPriceUnit = BitcoinUnit.BTC, walletID, searchQuery, style }) => { const [subtitleNumberOfLines, setSubtitleNumberOfLines] = useState(1); @@ -339,17 +316,17 @@ export const TransactionListItem: React.FC = React.mem const renderHighlightedText = (text: string, query: string) => { const parts = text.split(new RegExp(`(${query})`, 'gi')); return ( - <> + {parts.map((part, index) => part.toLowerCase() === query.toLowerCase() ? ( - - {part} - + + {part} + ) : ( - {part} + {part} ), )} - + ); }; @@ -373,7 +350,7 @@ export const TransactionListItem: React.FC = React.mem chevron={false} rightTitle={rowTitle} rightTitleStyle={rowTitleStyle} - containerStyle={containerStyle} + containerStyle={[containerStyle, style]} /> ); @@ -381,11 +358,24 @@ export const TransactionListItem: React.FC = React.mem ); const styles = StyleSheet.create({ - highlighted: { + highlightedContainer: { backgroundColor: 'white', borderColor: 'black', borderWidth: 1, borderRadius: 5, + padding: 2, + alignSelf: 'flex-start', + }, + highlighted: { color: 'black', + fontSize: 14, + fontWeight: '600', + }, + defaultText: { + fontSize: 14, + fontWeight: '600', + }, + dimmedText: { + opacity: 0.5, }, }); diff --git a/hooks/useBounceAnimation.ts b/hooks/useBounceAnimation.ts new file mode 100644 index 000000000..461346555 --- /dev/null +++ b/hooks/useBounceAnimation.ts @@ -0,0 +1,26 @@ +import { useEffect, useRef } from 'react'; +import { Animated } from 'react-native'; + +const useBounceAnimation = (query: string) => { + const bounceAnim = useRef(new Animated.Value(1.0)).current; + + useEffect(() => { + if (query) { + Animated.timing(bounceAnim, { + toValue: 1.2, + duration: 150, + useNativeDriver: true, + }).start(() => { + Animated.timing(bounceAnim, { + toValue: 1.0, + duration: 150, + useNativeDriver: true, + }).start(); + }); + } + }, [bounceAnim, query]); + + return bounceAnim; +}; + +export default useBounceAnimation; diff --git a/ios/BlueWallet.xcodeproj/project.pbxproj b/ios/BlueWallet.xcodeproj/project.pbxproj index b9edaf205..60804ebb2 100644 --- a/ios/BlueWallet.xcodeproj/project.pbxproj +++ b/ios/BlueWallet.xcodeproj/project.pbxproj @@ -181,7 +181,7 @@ B4EFF7472C3F70010095D655 /* LatestTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B44033DC2BCC36C300162242 /* LatestTransaction.swift */; }; B4EFF7482C3F70090095D655 /* BitcoinUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = B44033BE2BCC32F800162242 /* BitcoinUnit.swift */; }; C59F90CE0D04D3E4BB39BC5D /* libPods-BlueWalletUITests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6F02C2F7CA3591E4E0B06EBA /* libPods-BlueWalletUITests.a */; }; - C978A716948AB7DEC5B6F677 /* (null) in Frameworks */ = {isa = PBXBuildFile; }; + C978A716948AB7DEC5B6F677 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -497,7 +497,7 @@ files = ( 782F075B5DD048449E2DECE9 /* libz.tbd in Frameworks */, 764B49B1420D4AEB8109BF62 /* libsqlite3.0.tbd in Frameworks */, - C978A716948AB7DEC5B6F677 /* (null) in Frameworks */, + C978A716948AB7DEC5B6F677 /* BuildFile in Frameworks */, 773E382FE62E836172AAB98B /* libPods-BlueWallet.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/loc/en.json b/loc/en.json index bf7e45a70..453e3412d 100644 --- a/loc/en.json +++ b/loc/en.json @@ -496,7 +496,7 @@ "add_ln_wallet_first": "You must first add a Lightning wallet.", "identity_pubkey": "Identity Pubkey", "xpub_title": "Wallet XPUB", - "search_wallets": "Search wallets, memos" + "manage_wallets_search_placeholder": "Search wallets, memos" }, "multisig": { "multisig_vault": "Vault", diff --git a/screen/wallets/ManageWallets.tsx b/screen/wallets/ManageWallets.tsx index 3ffd05ccc..bce14da78 100644 --- a/screen/wallets/ManageWallets.tsx +++ b/screen/wallets/ManageWallets.tsx @@ -5,6 +5,7 @@ import DraggableFlatList, { ScaleDecorator } from 'react-native-draggable-flatli import { GestureHandlerRootView } from 'react-native-gesture-handler'; import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback'; import { useTheme } from '../../components/themes'; +import { WalletCarouselItem } from '../../components/WalletsCarousel'; import { TransactionListItem } from '../../components/TransactionListItem'; import { useExtendedNavigation } from '../../hooks/useExtendedNavigation'; import loc from '../../loc'; @@ -12,9 +13,26 @@ import { useStorage } from '../../hooks/context/useStorage'; import useDebounce from '../../hooks/useDebounce'; import { Header } from '../../components/Header'; import { TTXMetadata } from '../../class'; -import { TWallet } from '../../class/wallets/types'; +import { ExtendedTransaction, LightningTransaction, Transaction, TWallet } from '../../class/wallets/types'; import { BitcoinUnit } from '../../models/bitcoinUnits'; -import { WalletCarouselItem } from '../../components/WalletsCarousel'; +import useBounceAnimation from '../../hooks/useBounceAnimation'; + +enum ItemType { + WalletSection = 'wallet', + TransactionSection = 'transaction', +} + +interface WalletItem { + type: ItemType.WalletSection; + data: TWallet; +} + +interface TransactionItem { + type: ItemType.TransactionSection; + data: ExtendedTransaction & LightningTransaction; +} + +type Item = WalletItem | TransactionItem; const SET_SEARCH_QUERY = 'SET_SEARCH_QUERY'; const SET_IS_SEARCH_FOCUSED = 'SET_IS_SEARCH_FOCUSED'; @@ -34,17 +52,17 @@ interface SetIsSearchFocusedAction { interface SetWalletDataAction { type: typeof SET_WALLET_DATA; - payload: any[]; + payload: TWallet[]; } interface SetTxMetadataAction { type: typeof SET_TX_METADATA; - payload: { [key: string]: { memo?: string } }; + payload: TTXMetadata; } interface SetOrderAction { type: typeof SET_ORDER; - payload: any[]; + payload: Item[]; } type Action = SetSearchQueryAction | SetIsSearchFocusedAction | SetWalletDataAction | SetTxMetadataAction | SetOrderAction; @@ -54,7 +72,7 @@ interface State { isSearchFocused: boolean; walletData: TWallet[]; txMetadata: TTXMetadata; - order: any[]; + order: Item[]; } const initialState: State = { @@ -82,30 +100,6 @@ const reducer = (state: State, action: Action): State => { } }; -const useBounceAnimation = (query: string) => { - const bounceAnim = useRef(new Animated.Value(1.0)).current; - - useEffect(() => { - if (query) { - Animated.spring(bounceAnim, { - toValue: 1.2, - useNativeDriver: true, - friction: 3, - tension: 100, - }).start(() => { - Animated.spring(bounceAnim, { - toValue: 1.0, - useNativeDriver: true, - friction: 3, - tension: 100, - }).start(); - }); - } - }, [query]); - - return bounceAnim; -}; - const ManageWallets: React.FC = () => { const sortableList = useRef(null); const { colors, closeImage } = useTheme(); @@ -127,14 +121,14 @@ const ManageWallets: React.FC = () => { }; useEffect(() => { - const initialOrder = wallets.map(wallet => ({ type: 'wallet', data: wallet })); + const initialOrder: Item[] = wallets.map(wallet => ({ type: ItemType.WalletSection, data: wallet })); dispatch({ type: SET_WALLET_DATA, payload: wallets }); dispatch({ type: SET_TX_METADATA, payload: txMetadata }); dispatch({ type: SET_ORDER, payload: initialOrder }); }, [wallets, txMetadata]); const handleClose = useCallback(() => { - const walletOrder = state.order.filter(item => item.type === 'wallet').map(item => item.data); + const walletOrder = state.order.filter(item => item.type === ItemType.WalletSection).map(item => item.data); setWalletsWithNewOrder(walletOrder); goBack(); }, [goBack, setWalletsWithNewOrder, state.order]); @@ -169,15 +163,28 @@ const ManageWallets: React.FC = () => { const filteredTxMetadata = Object.entries(txMetadata).filter(([_, tx]) => tx.memo?.toLowerCase().includes(debouncedSearchQuery.toLowerCase()), ); - const filteredOrder = [ - ...filteredWallets.map(wallet => ({ type: 'wallet', data: wallet })), - ...Object.entries(filteredTxMetadata).map(([txid, tx]) => ({ type: 'transaction', data: { txid, ...tx } })), + + // Filter transactions + const filteredTransactions = wallets.flatMap(wallet => + wallet + .getTransactions() + .filter((tx: Transaction) => + filteredTxMetadata.some( + ([txid, txMeta]) => tx.hash === txid && txMeta.memo?.toLowerCase().includes(debouncedSearchQuery.toLowerCase()), + ), + ), + ); + + const filteredOrder: Item[] = [ + ...filteredWallets.map(wallet => ({ type: ItemType.WalletSection, data: wallet })), + ...filteredTransactions.map(tx => ({ type: ItemType.TransactionSection, data: tx })), ]; + dispatch({ type: SET_WALLET_DATA, payload: filteredWallets }); dispatch({ type: SET_TX_METADATA, payload: Object.fromEntries(filteredTxMetadata) }); dispatch({ type: SET_ORDER, payload: filteredOrder }); } else { - const initialOrder = wallets.map(wallet => ({ type: 'wallet', data: wallet })); + const initialOrder: Item[] = wallets.map(wallet => ({ type: ItemType.WalletSection, data: wallet })); dispatch({ type: SET_WALLET_DATA, payload: wallets }); dispatch({ type: SET_TX_METADATA, payload: {} }); dispatch({ type: SET_ORDER, payload: initialOrder }); @@ -192,13 +199,13 @@ const ManageWallets: React.FC = () => { onClear: () => dispatch({ type: SET_SEARCH_QUERY, payload: '' }), onFocus: () => dispatch({ type: SET_IS_SEARCH_FOCUSED, payload: true }), onBlur: () => dispatch({ type: SET_IS_SEARCH_FOCUSED, payload: false }), - placeholder: loc.wallets.search_wallets, + placeholder: loc.wallets.manage_wallets_search_placeholder, // New placeholder text }, }); }, [setOptions]); const navigateToWallet = useCallback( - (wallet: any) => { + (wallet: TWallet) => { const walletID = wallet.getID(); goBack(); navigate('WalletTransactions', { @@ -236,37 +243,37 @@ const ManageWallets: React.FC = () => { ); const renderItem = useCallback( - ({ item, drag, isActive }: any) => { + ({ item, drag, isActive }: { item: Item; drag: () => void; isActive: boolean }) => { const itemOpacity = isActive ? 1 : state.searchQuery ? 0.5 : 1; - if (item.type === 'transaction') { + if (item.type === ItemType.TransactionSection && item.data) { return ( ); + } else if (item.type === ItemType.WalletSection) { + return ( + + navigateToWallet(item.data)} + customStyle={StyleSheet.flatten([styles.padding16, { opacity: itemOpacity }])} + searchQuery={state.searchQuery} + renderHighlightedText={state.searchQuery ? renderHighlightedText : undefined} + /> + + ); } - - return ( - - - - ); + return null; }, [isDraggingDisabled, navigateToWallet, state.searchQuery, renderHighlightedText], ); @@ -354,14 +361,16 @@ const iStyles = StyleSheet.create({ borderWidth: 1, borderRadius: 5, padding: 2, + alignSelf: 'flex-start', // ensure the container resizes based on its content }, highlighted: { color: 'black', + fontSize: 14, + fontWeight: '600', }, defaultText: { - fontSize: 19, - fontWeight: 'bold', - writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr', + fontSize: 14, + fontWeight: '600', }, dimmedText: { opacity: 0.5, From 85cf3ee10f4cbd739678d849d5f09c72cab9beaf Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Fri, 26 Jul 2024 19:35:53 -0400 Subject: [PATCH 6/8] FIX: Testing memos was broken --- components/TransactionListItem.tsx | 54 +++-------------------- components/WalletsCarousel.tsx | 41 +++++++++++------- navigation/DetailViewScreensStack.tsx | 14 +++--- navigation/DetailViewStackParamList.ts | 2 +- navigation/ManageWalletsStack.tsx | 30 ------------- screen/wallets/DrawerList.tsx | 2 +- screen/wallets/ManageWallets.tsx | 59 ++++++++++++++------------ screen/wallets/WalletsList.tsx | 2 +- 8 files changed, 76 insertions(+), 128 deletions(-) delete mode 100644 navigation/ManageWalletsStack.tsx diff --git a/components/TransactionListItem.tsx b/components/TransactionListItem.tsx index a596cddd8..06420d5dc 100644 --- a/components/TransactionListItem.tsx +++ b/components/TransactionListItem.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import AsyncStorage from '@react-native-async-storage/async-storage'; import Clipboard from '@react-native-clipboard/clipboard'; -import { Linking, View, Text, StyleSheet, Animated, ViewStyle } from 'react-native'; +import { Linking, View, ViewStyle } from 'react-native'; import Lnurl from '../class/lnurl'; import { LightningTransaction, Transaction } from '../class/wallets/types'; import TransactionExpiredIcon from '../components/icons/TransactionExpiredIcon'; @@ -23,7 +23,7 @@ import { DetailViewStackParamList } from '../navigation/DetailViewStackParamList import { useStorage } from '../hooks/context/useStorage'; import ToolTipMenu from './TooltipMenu'; import { CommonToolTipActions } from '../typings/CommonToolTipActions'; -import useBounceAnimation from '../hooks/useBounceAnimation'; +import { pop } from '../NavigationService'; interface TransactionListItemProps { itemPriceUnit: BitcoinUnit; @@ -31,12 +31,13 @@ interface TransactionListItemProps { item: Transaction & LightningTransaction; // using type intersection to have less issues with ts searchQuery?: string; style?: ViewStyle; + renderHighlightedText?: (text: string, query: string) => JSX.Element; } type NavigationProps = NativeStackNavigationProp; export const TransactionListItem: React.FC = React.memo( - ({ item, itemPriceUnit = BitcoinUnit.BTC, walletID, searchQuery, style }) => { + ({ item, itemPriceUnit = BitcoinUnit.BTC, walletID, searchQuery, style, renderHighlightedText }) => { const [subtitleNumberOfLines, setSubtitleNumberOfLines] = useState(1); const { colors } = useTheme(); const { navigate } = useExtendedNavigation(); @@ -202,6 +203,7 @@ export const TransactionListItem: React.FC = React.mem const onPress = useCallback(async () => { menuRef?.current?.dismissMenu?.(); if (item.hash) { + pop(); navigate('TransactionStatus', { hash: item.hash, walletID }); } else if (item.type === 'user_invoice' || item.type === 'payment_request' || item.type === 'paid_invoice') { const lightningWallet = wallets.filter(wallet => wallet?.getID() === item.walletID); @@ -226,7 +228,7 @@ export const TransactionListItem: React.FC = React.mem return; } } catch (e) { - console.log(e); + console.debug(e); } navigate('LNDViewInvoice', { @@ -311,25 +313,6 @@ export const TransactionListItem: React.FC = React.mem }; }, [subtitleNumberOfLines]); - const bounceAnim = useBounceAnimation(searchQuery ?? ''); - - const renderHighlightedText = (text: string, query: string) => { - const parts = text.split(new RegExp(`(${query})`, 'gi')); - return ( - - {parts.map((part, index) => - part.toLowerCase() === query.toLowerCase() ? ( - - {part} - - ) : ( - {part} - ), - )} - - ); - }; - return ( = React.mem leftAvatar={avatar} title={title} subtitleNumberOfLines={subtitleNumberOfLines} - subtitle={subtitle ? renderHighlightedText(subtitle, searchQuery ?? '') : undefined} + subtitle={subtitle ? (renderHighlightedText ? renderHighlightedText(subtitle, searchQuery ?? '') : subtitle) : undefined} Component={View} subtitleProps={subtitleProps} chevron={false} @@ -356,26 +339,3 @@ export const TransactionListItem: React.FC = React.mem ); }, ); - -const styles = StyleSheet.create({ - highlightedContainer: { - backgroundColor: 'white', - borderColor: 'black', - borderWidth: 1, - borderRadius: 5, - padding: 2, - alignSelf: 'flex-start', - }, - highlighted: { - color: 'black', - fontSize: 14, - fontWeight: '600', - }, - defaultText: { - fontSize: 14, - fontWeight: '600', - }, - dimmedText: { - opacity: 0.5, - }, -}); diff --git a/components/WalletsCarousel.tsx b/components/WalletsCarousel.tsx index 2f802bd32..3d0470950 100644 --- a/components/WalletsCarousel.tsx +++ b/components/WalletsCarousel.tsx @@ -8,7 +8,6 @@ import { Pressable, StyleSheet, Text, - TouchableOpacity, useWindowDimensions, View, FlatListProps, @@ -73,12 +72,7 @@ const NewWalletPanel: React.FC = ({ onPress }) => { }); return ( - + = ({ onPress }) => { {loc.wallets.list_create_a_button} - + ); }; @@ -105,7 +99,8 @@ interface WalletCarouselItemProps { customStyle?: ViewStyle; horizontal?: boolean; isActive?: boolean; - allowOnPressAnimation?: boolean; + searchQuery?: string; + renderHighlightedText?: (text: string, query: string) => JSX.Element; } const iStyles = StyleSheet.create({ @@ -168,7 +163,7 @@ const iStyles = StyleSheet.create({ }); export const WalletCarouselItem: React.FC = React.memo( - ({ item, onPress, handleLongPress, isSelectedWallet, customStyle, horizontal, allowOnPressAnimation = true }) => { + ({ item, onPress, handleLongPress, isSelectedWallet, customStyle, horizontal, searchQuery, renderHighlightedText }) => { const scaleValue = useRef(new Animated.Value(1.0)).current; const { colors } = useTheme(); const { walletTransactionUpdateStatus } = useStorage(); @@ -234,8 +229,8 @@ export const WalletCarouselItem: React.FC = React.memo( { if (handleLongPress) handleLongPress(); }} @@ -246,7 +241,7 @@ export const WalletCarouselItem: React.FC = React.memo( - {item.getLabel()} + {renderHighlightedText && searchQuery ? renderHighlightedText(item.getLabel(), searchQuery) : item.getLabel()} {item.hideBalance ? ( @@ -288,6 +283,8 @@ interface WalletsCarouselProps extends Partial> { handleLongPress?: () => void; data: TWallet[]; scrollEnabled?: boolean; + searchQuery?: string; + renderHighlightedText?: (text: string, query: string) => JSX.Element; } type FlatListRefType = FlatList & { @@ -316,7 +313,17 @@ const cStyles = StyleSheet.create({ const ListHeaderComponent: React.FC = () => ; const WalletsCarousel = forwardRef((props, ref) => { - const { horizontal, data, handleLongPress, onPress, selectedWallet, scrollEnabled, onNewWalletPress } = props; + const { + horizontal, + data, + handleLongPress, + onPress, + selectedWallet, + scrollEnabled, + onNewWalletPress, + searchQuery, + renderHighlightedText, + } = props; const renderItem = useCallback( ({ item, index }: ListRenderItemInfo) => item ? ( @@ -326,9 +333,11 @@ const WalletsCarousel = forwardRef((props handleLongPress={handleLongPress} onPress={onPress} horizontal={horizontal} + searchQuery={searchQuery} + renderHighlightedText={renderHighlightedText} /> ) : null, - [horizontal, selectedWallet, handleLongPress, onPress], + [horizontal, selectedWallet, handleLongPress, onPress, searchQuery, renderHighlightedText], ); const flatListRef = useRef>(null); @@ -402,6 +411,8 @@ const WalletsCarousel = forwardRef((props handleLongPress={handleLongPress} onPress={onPress} key={index} + searchQuery={props.searchQuery} + renderHighlightedText={props.renderHighlightedText} /> ) : null, )} diff --git a/navigation/DetailViewScreensStack.tsx b/navigation/DetailViewScreensStack.tsx index 131a65f72..fbb96c56b 100644 --- a/navigation/DetailViewScreensStack.tsx +++ b/navigation/DetailViewScreensStack.tsx @@ -65,7 +65,7 @@ import WalletXpubStackRoot from './WalletXpubStack'; import PlusIcon from '../components/icons/PlusIcon'; import SettingsButton from '../components/icons/SettingsButton'; import ExportMultisigCoordinationSetupStack from './ExportMultisigCoordinationSetupStack'; -import ManageWalletsStackRoot from './ManageWalletsStack'; +import ManageWallets from '../screen/wallets/ManageWallets'; const DetailViewStackScreensStack = () => { const theme = useTheme(); @@ -379,13 +379,15 @@ const DetailViewStackScreensStack = () => { }} /> ); diff --git a/navigation/DetailViewStackParamList.ts b/navigation/DetailViewStackParamList.ts index bb1cd8074..6a5192c61 100644 --- a/navigation/DetailViewStackParamList.ts +++ b/navigation/DetailViewStackParamList.ts @@ -103,5 +103,5 @@ export type DetailViewStackParamList = { paymentCode: string; walletID: string; }; - ManageWalletsRoot: undefined; + ManageWallets: undefined; }; diff --git a/navigation/ManageWalletsStack.tsx b/navigation/ManageWalletsStack.tsx deleted file mode 100644 index f17cfa3ff..000000000 --- a/navigation/ManageWalletsStack.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { createNativeStackNavigator } from '@react-navigation/native-stack'; -import React from 'react'; - -import navigationStyle from '../components/navigationStyle'; -import { useTheme } from '../components/themes'; -import loc from '../loc'; -import ManageWallets from '../screen/wallets/ManageWallets'; - -const Stack = createNativeStackNavigator(); - -const ManageWalletsStackRoot = () => { - const theme = useTheme(); - - return ( - - - - ); -}; - -export default ManageWalletsStackRoot; diff --git a/screen/wallets/DrawerList.tsx b/screen/wallets/DrawerList.tsx index 8a3f15112..a692e222e 100644 --- a/screen/wallets/DrawerList.tsx +++ b/screen/wallets/DrawerList.tsx @@ -124,7 +124,7 @@ const DrawerList: React.FC = memo(({ navigation }) => { const handleLongPress = useCallback(() => { if (state.wallets.length > 1) { - navigation.navigate('ManageWalletsRoot'); + navigation.navigate('ManageWallets'); } else { triggerHapticFeedback(HapticFeedbackTypes.NotificationError); } diff --git a/screen/wallets/ManageWallets.tsx b/screen/wallets/ManageWallets.tsx index bce14da78..9c554da91 100644 --- a/screen/wallets/ManageWallets.tsx +++ b/screen/wallets/ManageWallets.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useLayoutEffect, useRef, useReducer, useCallback, useMemo } from 'react'; -import { Platform, StyleSheet, useColorScheme, TouchableOpacity, Image, Animated, Text, I18nManager, View } from 'react-native'; +import { Platform, StyleSheet, useColorScheme, TouchableOpacity, Image, Animated, Text, I18nManager } from 'react-native'; // @ts-ignore: no declaration file import DraggableFlatList, { ScaleDecorator } from 'react-native-draggable-flatlist'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; @@ -199,7 +199,7 @@ const ManageWallets: React.FC = () => { onClear: () => dispatch({ type: SET_SEARCH_QUERY, payload: '' }), onFocus: () => dispatch({ type: SET_IS_SEARCH_FOCUSED, payload: true }), onBlur: () => dispatch({ type: SET_IS_SEARCH_FOCUSED, payload: false }), - placeholder: loc.wallets.manage_wallets_search_placeholder, // New placeholder text + placeholder: loc.wallets.manage_wallets_search_placeholder, }, }); }, [setOptions]); @@ -226,12 +226,12 @@ const ManageWallets: React.FC = () => { return ( {parts.map((part, index) => - part.toLowerCase() === query.toLowerCase() ? ( - - {part} + query && part.toLowerCase().includes(query.toLowerCase()) ? ( + + {part} ) : ( - + {part} ), @@ -241,22 +241,20 @@ const ManageWallets: React.FC = () => { }, [bounceAnim], ); - const renderItem = useCallback( + // eslint-disable-next-line react/no-unused-prop-types ({ item, drag, isActive }: { item: Item; drag: () => void; isActive: boolean }) => { - const itemOpacity = isActive ? 1 : state.searchQuery ? 0.5 : 1; - if (item.type === ItemType.TransactionSection && item.data) { + const w = wallets.find(wallet => wallet.getTransactions().some((tx: ExtendedTransaction) => tx.hash === item.data.hash)); + const walletID = w ? w.getID() : ''; return ( - - - + ); } else if (item.type === ItemType.WalletSection) { return ( @@ -266,16 +264,16 @@ const ManageWallets: React.FC = () => { handleLongPress={isDraggingDisabled ? undefined : drag} isActive={isActive} onPress={() => navigateToWallet(item.data)} - customStyle={StyleSheet.flatten([styles.padding16, { opacity: itemOpacity }])} + customStyle={styles.padding16} searchQuery={state.searchQuery} - renderHighlightedText={state.searchQuery ? renderHighlightedText : undefined} + renderHighlightedText={renderHighlightedText} /> ); } return null; }, - [isDraggingDisabled, navigateToWallet, state.searchQuery, renderHighlightedText], + [wallets, isDraggingDisabled, navigateToWallet, state.searchQuery, renderHighlightedText], ); const onChangeOrder = useCallback(() => { @@ -299,7 +297,10 @@ const ManageWallets: React.FC = () => { const renderHeader = useMemo(() => { if (!state.searchQuery) return null; const hasWallets = state.walletData.length > 0; - const hasTransactions = Object.keys(state.txMetadata).length > 0; + const filteredTxMetadata = Object.entries(state.txMetadata).filter(([_, tx]) => + tx.memo?.toLowerCase().includes(state.searchQuery.toLowerCase()), + ); + const hasTransactions = filteredTxMetadata.length > 0; return ( <> @@ -361,18 +362,22 @@ const iStyles = StyleSheet.create({ borderWidth: 1, borderRadius: 5, padding: 2, - alignSelf: 'flex-start', // ensure the container resizes based on its content + alignSelf: 'flex-start', + textDecorationLine: 'underline', + textDecorationStyle: 'double', + textShadowColor: '#000', + textShadowOffset: { width: 1, height: 1 }, + textShadowRadius: 1, }, highlighted: { color: 'black', - fontSize: 14, + fontSize: 19, fontWeight: '600', }, defaultText: { - fontSize: 14, - fontWeight: '600', + fontSize: 19, }, dimmedText: { - opacity: 0.5, + opacity: 0.8, }, }); diff --git a/screen/wallets/WalletsList.tsx b/screen/wallets/WalletsList.tsx index 557130eb4..62c93f0e7 100644 --- a/screen/wallets/WalletsList.tsx +++ b/screen/wallets/WalletsList.tsx @@ -239,7 +239,7 @@ const WalletsList: React.FC = () => { const handleLongPress = useCallback(() => { if (wallets.length > 1) { - navigate('ManageWalletsRoot'); + navigate('ManageWallets'); } else { triggerHapticFeedback(HapticFeedbackTypes.NotificationError); } From 824608eeb0346b49e4d39c75dae5b4f74d120539 Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Fri, 26 Jul 2024 19:48:26 -0400 Subject: [PATCH 7/8] Update ManageWallets.tsx --- screen/wallets/ManageWallets.tsx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/screen/wallets/ManageWallets.tsx b/screen/wallets/ManageWallets.tsx index 9c554da91..d1d657350 100644 --- a/screen/wallets/ManageWallets.tsx +++ b/screen/wallets/ManageWallets.tsx @@ -11,7 +11,6 @@ import { useExtendedNavigation } from '../../hooks/useExtendedNavigation'; import loc from '../../loc'; import { useStorage } from '../../hooks/context/useStorage'; import useDebounce from '../../hooks/useDebounce'; -import { Header } from '../../components/Header'; import { TTXMetadata } from '../../class'; import { ExtendedTransaction, LightningTransaction, Transaction, TWallet } from '../../class/wallets/types'; import { BitcoinUnit } from '../../models/bitcoinUnits'; @@ -303,13 +302,8 @@ const ManageWallets: React.FC = () => { const hasTransactions = filteredTxMetadata.length > 0; return ( - <> - {hasWallets &&
} - {hasTransactions &&
} - {!hasWallets && !hasTransactions && ( - {loc.wallets.no_results_found} - )} - + !hasWallets && + !hasTransactions && {loc.wallets.no_results_found} ); }, [state.searchQuery, state.walletData.length, state.txMetadata, stylesHook.noResultsText]); From 1067a435c603cdba532c60c896821935d2bebd35 Mon Sep 17 00:00:00 2001 From: Marcos Rodriguez Velez Date: Fri, 26 Jul 2024 19:53:12 -0400 Subject: [PATCH 8/8] Update TransactionListItem.tsx --- components/TransactionListItem.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/components/TransactionListItem.tsx b/components/TransactionListItem.tsx index 06420d5dc..ad7403df5 100644 --- a/components/TransactionListItem.tsx +++ b/components/TransactionListItem.tsx @@ -52,6 +52,8 @@ export const TransactionListItem: React.FC = React.mem [colors.lightBorder], ); + const combinedStyle = useMemo(() => [containerStyle, style], [containerStyle, style]); + const shortenContactName = (name: string): string => { if (name.length < 16) return name; return name.substr(0, 7) + '...' + name.substr(name.length - 7, 7); @@ -333,7 +335,7 @@ export const TransactionListItem: React.FC = React.mem chevron={false} rightTitle={rowTitle} rightTitleStyle={rowTitleStyle} - containerStyle={[containerStyle, style]} + containerStyle={combinedStyle} /> );