diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c04dd16dd9..458615876b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,9 @@ ## Unreleased (develop) +- added: Price chart to `TransactionListScene` - added: Add Unizen DEX +- changed: `TransactionListScene` split into two scenes: `TransactionListScene` and `TransactionListScene2` ## 4.21.0 (2025-01-22) diff --git a/src/components/Main.tsx b/src/components/Main.tsx index dc3a8a75ba1..fbe5eb52fe8 100644 --- a/src/components/Main.tsx +++ b/src/components/Main.tsx @@ -126,6 +126,7 @@ import { SweepPrivateKeyProcessingScene as SweepPrivateKeyProcessingSceneCompone import { SweepPrivateKeySelectCryptoScene as SweepPrivateKeySelectCryptoSceneComponent } from './scenes/SweepPrivateKeySelectCryptoScene' import { TransactionDetailsScene as TransactionDetailsSceneComponent } from './scenes/TransactionDetailsScene' import { TransactionList as TransactionListComponent } from './scenes/TransactionListScene' +import { TransactionList2 as TransactionList2Component } from './scenes/TransactionListScene2' import { TransactionsExportScene as TransactionsExportSceneComponent } from './scenes/TransactionsExportScene' import { UpgradeUsernameScene as UpgradeUsernameSceneComponent } from './scenes/UpgradeUsernameScreen' import { WalletListScene as WalletListSceneComponent } from './scenes/WalletListScene' @@ -218,6 +219,7 @@ const SwapSettingsScene = ifLoggedIn(SwapSettingsSceneComponent) const SwapSuccessScene = ifLoggedIn(SwapSuccessSceneComponent) const TransactionDetailsScene = ifLoggedIn(TransactionDetailsSceneComponent) const TransactionList = ifLoggedIn(TransactionListComponent) +const TransactionList2 = ifLoggedIn(TransactionList2Component) const TransactionsExportScene = ifLoggedIn(TransactionsExportSceneComponent) const WalletListScene = ifLoggedIn(WalletListSceneComponent) const WalletRestoreScene = ifLoggedIn(WalletRestoreSceneComponent) @@ -276,6 +278,13 @@ const EdgeWalletsTabScreen = () => { headerTitle: () => fromParams={params => params.walletName} /> }} /> + fromParams={params => params.walletName} /> + }} + /> ) } @@ -781,6 +790,13 @@ const EdgeAppStack = () => { headerTitle: () => }} /> + fromParams={params => params.walletName} /> + }} + /> { if (isTransactionListUnsupported) return [] - let lastSection = '' - const out: ListItem[] = [] - for (const tx of transactions) { - // Create a new section header if we need one: - const { date } = unixToLocaleDateTime(tx.date) - if (date !== lastSection) { - out.push(date) - lastSection = date - } - - // Add the transaction to the list: - out.push(tx) - } - - // If we are still loading, add a spinner at the end: - if (!atEnd) out.push(null) - - return out - }, [atEnd, isTransactionListUnsupported, transactions]) - - // TODO: Comment out sticky header indices until we figure out how to - // give the headers a background only when they're sticking. - // Figure out where the section headers are located: - // const stickyHeaderIndices = React.useMemo(() => { - // const out: number[] = [] - // for (let i = 0; i < listItems.length; ++i) { - // if (typeof listItems[i] === 'string') out.push(i) - // } - // return out - // }, [listItems]) + // Take only the 5 most recent transactions + const recentTransactions = transactions.slice(0, 5) + return recentTransactions.length > 0 ? recentTransactions : [] + }, [isTransactionListUnsupported, transactions]) // --------------------------------------------------------------------------- // Side-Effects @@ -202,6 +172,10 @@ function TransactionListComponent(props: Props) { navigation.navigate('coinRankingDetails', { assetId, fiatCurrencyCode }) }) + const handlePressSeeAll = useHandler(() => { + navigation.navigate('transactionList2', route.params) + }) + // // Renderers // @@ -254,13 +228,20 @@ function TransactionListComponent(props: Props) { )} + + {listItems.map((tx: EdgeTransaction) => ( + + + + ))} + ) }, [ - listItems.length, + listItems, navigation, isSearching, tokenId, @@ -274,7 +255,9 @@ function TransactionListComponent(props: Props) { handlePressCoinRanking, fiatRateFormat, currencyCode, - fiatCurrencyCode + fiatCurrencyCode, + handlePressSeeAll, + styles.txRow ]) const emptyComponent = React.useMemo(() => { @@ -292,19 +275,7 @@ function TransactionListComponent(props: Props) { return } - const disableAnimation = index >= MAX_LIST_ITEMS_ANIM - if (typeof item === 'string') { - return ( - - - - ) - } - return ( - - - - ) + return null // We're not using the FlatList rendering anymore }) const keyExtractor = useHandler((item: ListItem) => { @@ -376,9 +347,6 @@ function TransactionListComponent(props: Props) { ListHeaderComponent={topArea} onEndReachedThreshold={0.5} renderItem={renderItem} - // TODO: Comment out sticky header indices until we figure out how to - // give the headers a background only when they're sticking. - // stickyHeaderIndices={stickyHeaderIndices} onEndReached={handleScrollEnd} onScroll={handleScroll} scrollIndicatorInsets={SCROLL_INDICATOR_INSET_FIX} @@ -406,7 +374,9 @@ export const TransactionList = withWallet(TransactionListComponent) const getStyles = cacheStyles(() => ({ flatList: { - overflow: 'visible', - flexShrink: 0 + flex: 1 + }, + txRow: { + paddingVertical: 0 } })) diff --git a/src/components/scenes/TransactionListScene2.tsx b/src/components/scenes/TransactionListScene2.tsx new file mode 100644 index 00000000000..3513b2aa11a --- /dev/null +++ b/src/components/scenes/TransactionListScene2.tsx @@ -0,0 +1,266 @@ +import { EdgeCurrencyWallet, EdgeTokenId, EdgeTokenMap, EdgeTransaction } from 'edge-core-js' +import * as React from 'react' +import { ListRenderItemInfo, Platform, RefreshControl, View } from 'react-native' +import Animated from 'react-native-reanimated' + +import { activateWalletTokens } from '../../actions/WalletActions' +import { SCROLL_INDICATOR_INSET_FIX } from '../../constants/constantSettings' +import { SPECIAL_CURRENCY_INFO } from '../../constants/WalletAndCurrencyConstants' +import { useAsyncEffect } from '../../hooks/useAsyncEffect' +import { useHandler } from '../../hooks/useHandler' +import { useTransactionList } from '../../hooks/useTransactionList' +import { useWatch } from '../../hooks/useWatch' +import { lstrings } from '../../locales/strings' +import { FooterRender } from '../../state/SceneFooterState' +import { useSceneScrollHandler } from '../../state/SceneScrollState' +import { useDispatch } from '../../types/reactRedux' +import { NavigationBase, WalletsTabSceneProps } from '../../types/routerTypes' +import { unixToLocaleDateTime } from '../../util/utils' +import { EdgeAnim, MAX_LIST_ITEMS_ANIM } from '../common/EdgeAnim' +import { SceneWrapper } from '../common/SceneWrapper' +import { withWallet } from '../hoc/withWallet' +import { cacheStyles, useTheme } from '../services/ThemeContext' +import { ExplorerCard } from '../themed/ExplorerCard' +import { SearchFooter } from '../themed/SearchFooter' +import { EmptyLoader, SectionHeader, SectionHeaderCentered } from '../themed/TransactionListComponents' +import { TransactionListRow } from '../themed/TransactionListRow' + +export interface TransactionList2Params { + walletId: string + walletName: string + tokenId: EdgeTokenId + countryCode?: string +} + +type ListItem = EdgeTransaction | string | null +interface Props extends WalletsTabSceneProps<'transactionList2'> { + wallet: EdgeCurrencyWallet +} + +function TransactionList2Component(props: Props) { + const { navigation, route, wallet } = props + const theme = useTheme() + const styles = getStyles(theme) + const dispatch = useDispatch() + + const tokenId = checkToken(route.params.tokenId, wallet.currencyConfig.allTokens) + const { pluginId } = wallet.currencyInfo + + // State: + const flashListRef = React.useRef | null>(null) + const [isSearching, setIsSearching] = React.useState(false) + const [searchText, setSearchText] = React.useState('') + const [footerHeight, setFooterHeight] = React.useState() + + // Watchers: + const enabledTokenIds = useWatch(wallet, 'enabledTokenIds') + const unactivatedTokenIds = useWatch(wallet, 'unactivatedTokenIds') + + // Transaction list state machine: + const { + transactions, + atEnd, + requestMore: handleScrollEnd + } = useTransactionList(wallet, tokenId, { + searchString: isSearching ? searchText : undefined + }) + + const { isTransactionListUnsupported = false } = SPECIAL_CURRENCY_INFO[pluginId] ?? {} + + // Assemble the data for the section list: + const listItems = React.useMemo(() => { + if (isTransactionListUnsupported) return [] + + let lastSection = '' + const out: ListItem[] = [] + for (const tx of transactions) { + // Create a new section header if we need one: + const { date } = unixToLocaleDateTime(tx.date) + if (date !== lastSection) { + out.push(date) + lastSection = date + } + + // Add the transaction to the list: + out.push(tx) + } + + // If we are still loading, add a spinner at the end: + if (!atEnd) out.push(null) + + return out + }, [atEnd, isTransactionListUnsupported, transactions]) + + // --------------------------------------------------------------------------- + // Side-Effects + // --------------------------------------------------------------------------- + + // Navigate back if the token is disabled from Archive Wallet action + React.useEffect(() => { + if (tokenId != null && !enabledTokenIds.includes(tokenId)) { + navigation.goBack() + } + }, [enabledTokenIds, navigation, tokenId]) + + // Automatically navigate to the token activation confirmation scene if + // the token appears in the unactivatedTokenIds list once the wallet loads + // this state. + useAsyncEffect( + async () => { + if (unactivatedTokenIds.length > 0) { + if (unactivatedTokenIds.some(unactivatedTokenId => unactivatedTokenId === tokenId)) { + await dispatch(activateWalletTokens(navigation as NavigationBase, wallet, [tokenId])) + } + } + }, + [unactivatedTokenIds], + 'TransactionListScene2 unactivatedTokenIds check' + ) + + // + // Handlers + // + + const handleScroll = useSceneScrollHandler() + + const handleStartSearching = useHandler(() => { + setIsSearching(true) + }) + + const handleDoneSearching = useHandler(() => { + setSearchText('') + setIsSearching(false) + }) + + const handleChangeText = useHandler((value: string) => { + setSearchText(value) + }) + + const handleFooterLayoutHeight = useHandler((height: number) => { + setFooterHeight(height) + }) + + // + // Renderers + // + + /** + * HACK: This `RefreshControl` doesn't actually do anything visually or + * functionally noticeable besides making Android scroll gestures actually + * work for the parent `Animated.FlatList` + */ + const refreshControl = React.useMemo(() => { + return Platform.OS === 'ios' ? undefined : ( + {}} + /> + ) + }, []) + + const emptyComponent = React.useMemo(() => { + if (isTransactionListUnsupported) { + return + } else if (isSearching) { + return + } + return null + }, [isTransactionListUnsupported, isSearching, wallet, tokenId]) + + const renderItem = useHandler(({ index, item }: ListRenderItemInfo) => { + if (item == null) { + return + } + + const disableAnimation = index >= MAX_LIST_ITEMS_ANIM + if (typeof item === 'string') { + return ( + + + + ) + } + return ( + + + + ) + }) + + const keyExtractor = useHandler((item: ListItem) => { + if (item == null) return 'spinner' + if (typeof item === 'string') return item + return item.txid + }) + + const renderFooter: FooterRender = React.useCallback( + sceneWrapperInfo => { + return ( + + ) + }, + [handleChangeText, handleDoneSearching, handleFooterLayoutHeight, handleStartSearching, isSearching, searchText] + ) + + return ( + + {({ insetStyle, undoInsetStyle }) => ( + + + + )} + + ) +} + +/** + * If the token gets deleted, the scene will crash. + * Fall back to the main currency code if this happens. + */ +function checkToken(tokenId: EdgeTokenId, allTokens: EdgeTokenMap): EdgeTokenId { + if (tokenId == null) return null + if (allTokens[tokenId] == null) return null + return tokenId +} + +export const TransactionList2 = withWallet(TransactionList2Component) + +const getStyles = cacheStyles(() => ({ + flatList: { + flex: 1 + } +})) diff --git a/src/components/themed/TransactionListRow.tsx b/src/components/themed/TransactionListRow.tsx index 6d58f9681d5..40a0289fc45 100644 --- a/src/components/themed/TransactionListRow.tsx +++ b/src/components/themed/TransactionListRow.tsx @@ -31,6 +31,7 @@ import { unixToLocaleDateTime } from '../../util/utils' import { EdgeCard } from '../cards/EdgeCard' +import { EdgeTouchableOpacity } from '../common/EdgeTouchableOpacity' import { SectionView } from '../layout/SectionView' import { showError } from '../services/AirshipInstance' import { cacheStyles, Theme, useTheme } from '../services/ThemeContext' @@ -40,13 +41,14 @@ interface Props { navigation: NavigationBase wallet: EdgeCurrencyWallet transaction: EdgeTransaction + noCard?: boolean } export function TransactionListRow(props: Props) { const theme = useTheme() const styles = getStyles(theme) - const { navigation, wallet, transaction } = props + const { navigation, wallet, transaction, noCard } = props const { metadata = {}, currencyCode, tokenId } = transaction const currencyInfo = wallet.currencyInfo @@ -175,7 +177,33 @@ export function TransactionListRow(props: Props) { }) // HACK: Handle 100% of the margins because of SceneHeader usage on this scene - return ( + return noCard ? ( + + + {icon} + + {unixToLocaleDateTime(transaction.date).date} + + + {name} + + {cryptoAmountString} + + + + {unconfirmedOrTimeText} + + {fiatAmountString} + + {categoryText == null ? null : ( + + {categoryText} + + )} + + + + ) : ( <> @@ -191,18 +219,23 @@ export function TransactionListRow(props: Props) { {fiatAmountString} + {categoryText == null ? null : ( + + {categoryText} + + )} - {categoryText == null ? null : ( - - {categoryText} - - )} ) } const getStyles = cacheStyles((theme: Theme) => ({ + cardlessView: { + flexDirection: 'column', + flexGrow: 1, + flexShrink: 1 + }, icon: { // Shadow styles for Android textShadowColor: 'rgba(0, 0, 0, 0.7)', @@ -264,6 +297,11 @@ const getStyles = cacheStyles((theme: Theme) => ({ justifyContent: 'space-between', marginHorizontal: theme.rem(0.5) }, + dateText: { + fontSize: theme.rem(0.75), + marginLeft: theme.rem(0.5), + color: theme.deactivatedText + }, titleText: { alignSelf: 'center', fontFamily: theme.fontFaceMedium, diff --git a/src/types/routerTypes.tsx b/src/types/routerTypes.tsx index 501248479bd..59e79d9140f 100644 --- a/src/types/routerTypes.tsx +++ b/src/types/routerTypes.tsx @@ -63,6 +63,7 @@ import type { SweepPrivateKeyProcessingParams } from '../components/scenes/Sweep import type { SweepPrivateKeySelectCryptoParams } from '../components/scenes/SweepPrivateKeySelectCryptoScene' import type { TransactionDetailsParams } from '../components/scenes/TransactionDetailsScene' import type { TransactionListParams } from '../components/scenes/TransactionListScene' +import type { TransactionList2Params } from '../components/scenes/TransactionListScene2' import type { TransactionsExportParams } from '../components/scenes/TransactionsExportScene' import type { WcConnectionsParams } from '../components/scenes/WcConnectionsScene' import type { WcConnectParams } from '../components/scenes/WcConnectScene' @@ -88,6 +89,7 @@ import type { FiatPluginSepaFormParams } from '../plugins/gui/scenes/SepaFormSce export type WalletsTabParamList = {} & { walletList: undefined transactionList: TransactionListParams + transactionList2: TransactionList2Params transactionDetails: TransactionDetailsParams } @@ -204,6 +206,7 @@ export type EdgeAppStackParamList = {} & { sweepPrivateKeySelectCrypto: SweepPrivateKeySelectCryptoParams testScene: undefined transactionDetails: TransactionDetailsParams + transactionList2: TransactionList2Params transactionsExport: TransactionsExportParams upgradeUsername: undefined walletRestore: undefined