From 65055ef0b1418b1493c193756ce82ba9e17dc801 Mon Sep 17 00:00:00 2001 From: Paul Puey Date: Fri, 17 Jan 2025 14:06:41 -0800 Subject: [PATCH 1/3] WIP on add charts to txlist --- src/app.ts | 3 +- .../scenes/CoinRankingDetailsScene.tsx | 67 +++++++++++++------ .../scenes/TransactionListScene.tsx | 34 +++++++++- src/util/network.ts | 31 ++++++++- 4 files changed, 111 insertions(+), 24 deletions(-) diff --git a/src/app.ts b/src/app.ts index f1aa410ce93..d7e38394015 100644 --- a/src/app.ts +++ b/src/app.ts @@ -17,7 +17,7 @@ import { changeTheme, getTheme } from './components/services/ThemeContext' import { ENV } from './env' import { NumberMap } from './types/types' import { log, logToServer } from './util/logger' -import { initInfoServer } from './util/network' +import { initCoinrankList, initInfoServer } from './util/network' export type Environment = 'development' | 'testing' | 'production' @@ -278,5 +278,6 @@ if (ENV.DEBUG_THEME) { initDeviceSettings().catch(err => console.log(err)) initInfoServer().catch(err => console.log(err)) +initCoinrankList().catch(err => console.log(err)) if (global.Buffer == null) global.Buffer = Buffer diff --git a/src/components/scenes/CoinRankingDetailsScene.tsx b/src/components/scenes/CoinRankingDetailsScene.tsx index 27af9280ca7..29235c24055 100644 --- a/src/components/scenes/CoinRankingDetailsScene.tsx +++ b/src/components/scenes/CoinRankingDetailsScene.tsx @@ -3,13 +3,15 @@ import * as React from 'react' import { View } from 'react-native' import FastImage from 'react-native-fast-image' +import { useAsyncEffect } from '../../hooks/useAsyncEffect' import { formatFiatString } from '../../hooks/useFiatText' import { toLocaleDate, toPercentString } from '../../locales/intl' import { lstrings } from '../../locales/strings' import { getDefaultFiat } from '../../selectors/SettingsSelectors' -import { CoinRankingData, CoinRankingDataPercentChange } from '../../types/coinrankTypes' +import { asCoinRankingData, CoinRankingData, CoinRankingDataPercentChange } from '../../types/coinrankTypes' import { useSelector } from '../../types/reactRedux' import { EdgeAppSceneProps } from '../../types/routerTypes' +import { fetchRates } from '../../util/network' import { formatLargeNumberString as formatLargeNumber } from '../../util/utils' import { SwipeChart } from '../charts/SwipeChart' import { EdgeAnim, fadeInLeft } from '../common/EdgeAnim' @@ -21,7 +23,8 @@ import { COINGECKO_SUPPORTED_FIATS } from './CoinRankingScene' type CoinRankingDataValueType = string | number | CoinRankingDataPercentChange | undefined export interface CoinRankingDetailsParams { - coinRankingData: CoinRankingData + assetId?: string + coinRankingData?: CoinRankingData fiatCurrencyCode: string } @@ -79,9 +82,10 @@ const CoinRankingDetailsSceneComponent = (props: Props) => { const theme = useTheme() const styles = getStyles(theme) const { route, navigation } = props - const { coinRankingData, fiatCurrencyCode } = route.params - const { currencyCode, currencyName } = coinRankingData - const currencyCodeUppercase = currencyCode.toUpperCase() + const { assetId, coinRankingData: initCoinRankingData, fiatCurrencyCode } = route.params + const [coinRankingData, setCoinRankingData] = React.useState(initCoinRankingData) + const { currencyCode, currencyName } = coinRankingData ?? {} + const currencyCodeUppercase = currencyCode?.toUpperCase() ?? '' // In case the user changes their default fiat while viewing this scene, we // want to go back since the parent scene handles fetching data. @@ -98,9 +102,30 @@ const CoinRankingDetailsSceneComponent = (props: Props) => { } }, [supportedFiat, initFiat, isFocused, navigation]) + useAsyncEffect( + async () => { + if (coinRankingData == null) { + if (assetId == null) { + throw new Error('No currencyCode or coinRankingData provided') + } + const response = await fetchRates(`v2/coinrankAsset/${assetId}?fiatCode=${supportedFiat}`) + if (!response.ok) { + const text = await response.text() + throw new Error(`Unable to fetch coin ranking data. ${text}`) + } + + const json = await response.json() + const crData = asCoinRankingData(json.data) + setCoinRankingData(crData) + } + }, + [currencyCode, supportedFiat], + 'CoinRankingDetailsScene' + ) + const imageUrlObject = React.useMemo( () => ({ - uri: coinRankingData.imageUrl ?? '' + uri: coinRankingData?.imageUrl ?? '' }), [coinRankingData] ) @@ -140,19 +165,19 @@ const CoinRankingDetailsSceneComponent = (props: Props) => { case 'rank': return `#${baseString}` case 'marketCapChange24h': - extendedString = coinRankingData.marketCapChangePercent24h != null ? ` (${toPercentString(coinRankingData.marketCapChangePercent24h / 100)})` : '' + extendedString = coinRankingData?.marketCapChangePercent24h != null ? ` (${toPercentString(coinRankingData.marketCapChangePercent24h / 100)})` : '' break case 'allTimeHigh': { const fiatString = `${formatFiatString({ fiatAmount: baseString })} ${supportedFiat}` - return coinRankingData.allTimeHighDate != null ? `${fiatString} - ${toLocaleDate(new Date(coinRankingData.allTimeHighDate))}` : fiatString + return coinRankingData?.allTimeHighDate != null ? `${fiatString} - ${toLocaleDate(new Date(coinRankingData.allTimeHighDate))}` : fiatString } case 'allTimeLow': { const fiatString = `${formatFiatString({ fiatAmount: baseString })} ${supportedFiat}` - return coinRankingData.allTimeLowDate != null ? `${fiatString} - ${toLocaleDate(new Date(coinRankingData.allTimeLowDate))}` : fiatString + return coinRankingData?.allTimeLowDate != null ? `${fiatString} - ${toLocaleDate(new Date(coinRankingData.allTimeLowDate))}` : fiatString } default: // If no special modifications, just return simple data formatting @@ -194,17 +219,21 @@ const CoinRankingDetailsSceneComponent = (props: Props) => { return ( - - - - {`${currencyName} (${currencyCodeUppercase})`} - - - - {renderRows(coinRankingData, COLUMN_LEFT_DATA_KEYS)} - {renderRows(coinRankingData, COLUMN_RIGHT_DATA_KEYS)} + {coinRankingData != null ? ( + + + + {`${currencyName} (${currencyCodeUppercase})`} + + + + {renderRows(coinRankingData, COLUMN_LEFT_DATA_KEYS)} + {renderRows(coinRankingData, COLUMN_RIGHT_DATA_KEYS)} + - + ) : ( + Loading... + )} ) } diff --git a/src/components/scenes/TransactionListScene.tsx b/src/components/scenes/TransactionListScene.tsx index 86953c559e6..436fe1c258f 100644 --- a/src/components/scenes/TransactionListScene.tsx +++ b/src/components/scenes/TransactionListScene.tsx @@ -18,15 +18,18 @@ import { FooterRender } from '../../state/SceneFooterState' import { useSceneScrollHandler } from '../../state/SceneScrollState' import { useDispatch, useSelector } from '../../types/reactRedux' import { NavigationBase, WalletsTabSceneProps } from '../../types/routerTypes' -import { infoServerData } from '../../util/network' +import { coinrankListData, infoServerData } from '../../util/network' import { calculateSpamThreshold, darkenHexColor, unixToLocaleDateTime, zeroString } from '../../util/utils' import { InfoCardCarousel } from '../cards/InfoCardCarousel' +import { SwipeChart } from '../charts/SwipeChart' import { AccentColors } from '../common/DotsBackground' import { EdgeAnim, fadeInDown10, MAX_LIST_ITEMS_ANIM } from '../common/EdgeAnim' +import { EdgeTouchableOpacity } from '../common/EdgeTouchableOpacity' import { SceneWrapper } from '../common/SceneWrapper' import { withWallet } from '../hoc/withWallet' import { cacheStyles, useTheme } from '../services/ThemeContext' import { BuyCrypto } from '../themed/BuyCrypto' +import { EdgeText } from '../themed/EdgeText' import { ExplorerCard } from '../themed/ExplorerCard' import { SearchFooter } from '../themed/SearchFooter' import { EmptyLoader, SectionHeader, SectionHeaderCentered } from '../themed/TransactionListComponents' @@ -66,6 +69,7 @@ function TransactionListComponent(props: Props) { // Selectors: const exchangeDenom = getExchangeDenomByCurrencyCode(wallet.currencyConfig, currencyCode) + const fiatCurrencyCode = useSelector(state => state.ui.settings.defaultIsoFiat).replace('iso:', '') const exchangeRate = useSelector(state => state.exchangeRates[`${currencyCode}_${state.ui.settings.defaultIsoFiat}`]) const spamFilterOn = useSelector(state => state.ui.settings.spamFilterOn) const activeUsername = useSelector(state => state.core.account.username) @@ -202,6 +206,12 @@ function TransactionListComponent(props: Props) { ) }, []) + const assetId = coinrankListData.coins[currencyCode] + + const handlePressCoinRanking = useHandler(() => { + navigation.navigate('coinRankingDetails', { assetId, fiatCurrencyCode }) + }) + const topArea = React.useMemo(() => { return ( <> @@ -222,9 +232,29 @@ function TransactionListComponent(props: Props) { countryCode={route.params.countryCode} screenWidth={screenWidth} /> + {assetId != null && ( + + See More + + )} + {assetId != null && } ) - }, [listItems.length, navigation, isSearching, tokenId, wallet, isLightAccount, pluginId, route.params.countryCode, screenWidth]) + }, [ + listItems.length, + navigation, + isSearching, + tokenId, + wallet, + isLightAccount, + pluginId, + route.params.countryCode, + screenWidth, + assetId, + currencyCode, + fiatCurrencyCode, + handlePressCoinRanking + ]) const emptyComponent = React.useMemo(() => { if (isTransactionListUnsupported) { diff --git a/src/util/network.ts b/src/util/network.ts index 77fea4b52c5..feae2f308ad 100644 --- a/src/util/network.ts +++ b/src/util/network.ts @@ -1,4 +1,4 @@ -import { Cleaner } from 'cleaners' +import { asObject, asString, Cleaner } from 'cleaners' import { EdgeFetchFunction, EdgeFetchOptions, EdgeFetchResponse } from 'edge-core-js' import { asInfoRollup, InfoRollup } from 'edge-info-server' import { Platform } from 'react-native' @@ -7,7 +7,8 @@ import { getVersion } from 'react-native-device-info' import { config } from '../theme/appConfig' import { asyncWaterfall, getOsVersion, shuffleArray } from './utils' const INFO_SERVERS = ['https://info1.edge.app', 'https://info2.edge.app'] -const RATES_SERVERS = ['https://rates1.edge.app', 'https://rates2.edge.app'] +// const RATES_SERVERS = ['https://rates1.edge.app', 'https://rates2.edge.app'] +const RATES_SERVERS = ['http://localhost:8008'] const INFO_FETCH_INTERVAL = 5 * 60 * 1000 // 5 minutes @@ -96,3 +97,29 @@ export const initInfoServer = async () => { await queryInfo() setInterval(queryInfo, INFO_FETCH_INTERVAL) } + +const asCoinrankList = asObject(asString) + +const asCoinGeckoCoinsResponse = asObject({ + data: asCoinrankList +}) + +export type CoinrankList = ReturnType + +export const coinrankListData: { coins: CoinrankList } = { coins: {} } +export const initCoinrankList = async () => { + try { + const response = await fetchRates('v2/coinrankList') + if (!response.ok) { + const text = await response.text() + throw new Error(`initCoinrankList error ${response.status}: ${text}`) + } + const responseJson = await response.json() + const { data } = asCoinGeckoCoinsResponse(responseJson) + + coinrankListData.coins = data + console.log('initCoinrankList: Successfully fetched coingecko list') + } catch (e) { + console.warn('initCoinrankList: Failed to fetch coinrank list', String(e)) + } +} From 8352b026e11c31551e0c0e527897408e64ceb237 Mon Sep 17 00:00:00 2001 From: Jon Tzeng Date: Wed, 22 Jan 2025 16:03:03 -0800 Subject: [PATCH 2/3] WIP --- src/components/charts/SwipeChart.tsx | 12 +++-- .../scenes/CoinRankingDetailsScene.tsx | 3 +- .../scenes/TransactionListScene.tsx | 51 +++++++++++++------ src/locales/en_US.ts | 2 + src/util/network.ts | 2 +- 5 files changed, 50 insertions(+), 20 deletions(-) diff --git a/src/components/charts/SwipeChart.tsx b/src/components/charts/SwipeChart.tsx index 30f2e10d02c..f1e197838c7 100644 --- a/src/components/charts/SwipeChart.tsx +++ b/src/components/charts/SwipeChart.tsx @@ -25,9 +25,12 @@ type Timespan = 'year' | 'month' | 'week' | 'day' | 'hour' type CoinGeckoDataPair = number[] interface Props { - assetId: string // The asset's 'id' as defined by CoinGecko + /** The asset's 'id' as defined by CoinGecko */ + assetId: string currencyCode: string fiatCurrencyCode: string + /** Modify positioning to fit properly in a card */ + cardAdjust?: boolean } interface ChartDataPoint { x: Date @@ -129,7 +132,7 @@ const reduceChartData = (chartData: ChartDataPoint[], timespan: Timespan): Chart const SwipeChartComponent = (params: Props) => { const theme = useTheme() const styles = getStyles(theme) - const { assetId, currencyCode, fiatCurrencyCode } = params + const { assetId, cardAdjust, currencyCode, fiatCurrencyCode } = params // #region Chart setup @@ -567,7 +570,7 @@ const SwipeChartComponent = (params: Props) => { toolTipProps={tooltipProps} // #endregion ToolTip - style={styles.baseChart} + style={[styles.baseChart, cardAdjust ? styles.cardAdjust : undefined]} /> {/* Min/Max price labels */} @@ -614,6 +617,9 @@ const getStyles = cacheStyles((theme: Theme) => { alignSelf: 'flex-start', position: 'absolute' }, + cardAdjust: { + marginLeft: theme.rem(-1) + }, container: { margin: theme.rem(0.5) }, diff --git a/src/components/scenes/CoinRankingDetailsScene.tsx b/src/components/scenes/CoinRankingDetailsScene.tsx index 29235c24055..7579c29e93d 100644 --- a/src/components/scenes/CoinRankingDetailsScene.tsx +++ b/src/components/scenes/CoinRankingDetailsScene.tsx @@ -83,6 +83,7 @@ const CoinRankingDetailsSceneComponent = (props: Props) => { const styles = getStyles(theme) const { route, navigation } = props const { assetId, coinRankingData: initCoinRankingData, fiatCurrencyCode } = route.params + const [coinRankingData, setCoinRankingData] = React.useState(initCoinRankingData) const { currencyCode, currencyName } = coinRankingData ?? {} const currencyCodeUppercase = currencyCode?.toUpperCase() ?? '' @@ -108,7 +109,7 @@ const CoinRankingDetailsSceneComponent = (props: Props) => { if (assetId == null) { throw new Error('No currencyCode or coinRankingData provided') } - const response = await fetchRates(`v2/coinrankAsset/${assetId}?fiatCode=${supportedFiat}`) + const response = await fetchRates(`v2/coinrankAsset/${assetId}?fiatCode=iso:${supportedFiat}`) if (!response.ok) { const text = await response.text() throw new Error(`Unable to fetch coin ranking data. ${text}`) diff --git a/src/components/scenes/TransactionListScene.tsx b/src/components/scenes/TransactionListScene.tsx index 436fe1c258f..d02a32b108a 100644 --- a/src/components/scenes/TransactionListScene.tsx +++ b/src/components/scenes/TransactionListScene.tsx @@ -1,3 +1,4 @@ +import { gt, mul } from 'biggystring' import { EdgeCurrencyWallet, EdgeTokenId, EdgeTokenMap, EdgeTransaction } from 'edge-core-js' import * as React from 'react' import { ListRenderItemInfo, Platform, RefreshControl, View } from 'react-native' @@ -12,6 +13,7 @@ import { useHandler } from '../../hooks/useHandler' import { useIconColor } from '../../hooks/useIconColor' import { useTransactionList } from '../../hooks/useTransactionList' import { useWatch } from '../../hooks/useWatch' +import { formatNumber } from '../../locales/intl' import { lstrings } from '../../locales/strings' import { getExchangeDenomByCurrencyCode } from '../../selectors/DenominationSelectors' import { FooterRender } from '../../state/SceneFooterState' @@ -19,17 +21,18 @@ import { useSceneScrollHandler } from '../../state/SceneScrollState' import { useDispatch, useSelector } from '../../types/reactRedux' import { NavigationBase, WalletsTabSceneProps } from '../../types/routerTypes' import { coinrankListData, infoServerData } from '../../util/network' -import { calculateSpamThreshold, darkenHexColor, unixToLocaleDateTime, zeroString } from '../../util/utils' +import { calculateSpamThreshold, convertNativeToDenomination, darkenHexColor, unixToLocaleDateTime, zeroString } from '../../util/utils' +import { EdgeCard } from '../cards/EdgeCard' import { InfoCardCarousel } from '../cards/InfoCardCarousel' import { SwipeChart } from '../charts/SwipeChart' import { AccentColors } from '../common/DotsBackground' import { EdgeAnim, fadeInDown10, MAX_LIST_ITEMS_ANIM } from '../common/EdgeAnim' -import { EdgeTouchableOpacity } from '../common/EdgeTouchableOpacity' import { SceneWrapper } from '../common/SceneWrapper' +import { SectionHeader as SectionHeaderUi4 } from '../common/SectionHeader' import { withWallet } from '../hoc/withWallet' import { cacheStyles, useTheme } from '../services/ThemeContext' import { BuyCrypto } from '../themed/BuyCrypto' -import { EdgeText } from '../themed/EdgeText' +import { EdgeText, Paragraph } from '../themed/EdgeText' import { ExplorerCard } from '../themed/ExplorerCard' import { SearchFooter } from '../themed/SearchFooter' import { EmptyLoader, SectionHeader, SectionHeaderCentered } from '../themed/TransactionListComponents' @@ -59,6 +62,7 @@ function TransactionListComponent(props: Props) { const tokenId = checkToken(route.params.tokenId, wallet.currencyConfig.allTokens) const { pluginId } = wallet.currencyInfo const { currencyCode } = tokenId == null ? wallet.currencyInfo : wallet.currencyConfig.allTokens[tokenId] + const { displayName } = tokenId == null ? wallet.currencyInfo : wallet.currencyConfig.allTokens[tokenId] // State: const flashListRef = React.useRef | null>(null) @@ -83,6 +87,13 @@ function TransactionListComponent(props: Props) { // Derived values // --------------------------------------------------------------------------- + // Fiat Balance Formatting + const exchangeAmount = convertNativeToDenomination(exchangeDenom.multiplier)(exchangeDenom.multiplier) + const fiatRate = mul(exchangeAmount, exchangeRate) + const fiatRateFormat = `${formatNumber(fiatRate && gt(fiatRate, '0.000001') ? fiatRate : 0, { + toFixed: gt(fiatRate, '1000') ? 0 : 2 + })} ${fiatCurrencyCode}/${currencyCode}` + const spamThreshold = React.useMemo(() => { if (spamFilterOn && !zeroString(exchangeRate)) { return calculateSpamThreshold(exchangeRate, exchangeDenom) @@ -185,6 +196,12 @@ function TransactionListComponent(props: Props) { setFooterHeight(height) }) + const assetId = coinrankListData.coins[currencyCode] + + const handlePressCoinRanking = useHandler(() => { + navigation.navigate('coinRankingDetails', { assetId, fiatCurrencyCode }) + }) + // // Renderers // @@ -206,12 +223,6 @@ function TransactionListComponent(props: Props) { ) }, []) - const assetId = coinrankListData.coins[currencyCode] - - const handlePressCoinRanking = useHandler(() => { - navigation.navigate('coinRankingDetails', { assetId, fiatCurrencyCode }) - }) - const topArea = React.useMemo(() => { return ( <> @@ -232,12 +243,20 @@ function TransactionListComponent(props: Props) { countryCode={route.params.countryCode} screenWidth={screenWidth} /> + {assetId != null && } {assetId != null && ( - - See More - + + + {fiatRateFormat} + + + )} - {assetId != null && } + ) }, [ @@ -251,9 +270,11 @@ function TransactionListComponent(props: Props) { route.params.countryCode, screenWidth, assetId, + displayName, + handlePressCoinRanking, + fiatRateFormat, currencyCode, - fiatCurrencyCode, - handlePressCoinRanking + fiatCurrencyCode ]) const emptyComponent = React.useMemo(() => { diff --git a/src/locales/en_US.ts b/src/locales/en_US.ts index b8840bd4dbd..e103cc34454 100644 --- a/src/locales/en_US.ts +++ b/src/locales/en_US.ts @@ -225,6 +225,7 @@ const strings = { transaction_list_no_tx_support_yet: 'Transaction history is not yet supported', transaction_list_search: 'Search Transactions', transaction_list_search_no_result: 'Search returned no results', + transaction_list_recent_transactions: 'Recent Transactions', fragment_wallets_balance_text: 'Total Balance', fragment_wallets_delete_wallet: 'Archive Wallet', fragment_wallets_delete_token: 'Disable Token', @@ -1532,6 +1533,7 @@ const strings = { coin_rank_week: '7D', coin_rank_month: '30D', coin_rank_year: '1Y', + coin_rank_see_more: 'See More', // #endregion CoinRanking diff --git a/src/util/network.ts b/src/util/network.ts index feae2f308ad..59bbffa7ed8 100644 --- a/src/util/network.ts +++ b/src/util/network.ts @@ -8,7 +8,7 @@ import { config } from '../theme/appConfig' import { asyncWaterfall, getOsVersion, shuffleArray } from './utils' const INFO_SERVERS = ['https://info1.edge.app', 'https://info2.edge.app'] // const RATES_SERVERS = ['https://rates1.edge.app', 'https://rates2.edge.app'] -const RATES_SERVERS = ['http://localhost:8008'] +const RATES_SERVERS = ['http://localhost:8087'] const INFO_FETCH_INTERVAL = 5 * 60 * 1000 // 5 minutes From 073096bceb6a21d670ba02c8b6da404dc5dfa979 Mon Sep 17 00:00:00 2001 From: Jon Tzeng Date: Wed, 22 Jan 2025 18:44:50 -0800 Subject: [PATCH 3/3] WIP --- CHANGELOG.md | 3 + src/components/Main.tsx | 16 ++ .../scenes/TransactionListScene.tsx | 87 ++---- .../scenes/TransactionListScene2.tsx | 266 ++++++++++++++++++ src/components/themed/TransactionListRow.tsx | 52 +++- src/types/routerTypes.tsx | 3 + 6 files changed, 359 insertions(+), 68 deletions(-) create mode 100644 src/components/scenes/TransactionListScene2.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bec491f540..c1b5ddbff2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased (develop) +- added: Price chart to `TransactionListScene` +- changed: `TransactionListScene` split into two scenes: `TransactionListScene` and `TransactionListScene2` + ## 4.21.0 (staging) - added: Add SUI 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 @@ -254,27 +224,35 @@ function TransactionListComponent(props: Props) { )} navigation.navigate('transactionList2', route.params)} /> + + {listItems.map((tx: EdgeTransaction) => ( + + + + ))} + ) }, [ - listItems.length, + listItems, navigation, isSearching, tokenId, wallet, isLightAccount, pluginId, - route.params.countryCode, + route.params, screenWidth, assetId, displayName, handlePressCoinRanking, fiatRateFormat, currencyCode, - fiatCurrencyCode + fiatCurrencyCode, + styles.txRow ]) const emptyComponent = React.useMemo(() => { @@ -292,19 +270,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 +342,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 +369,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