Skip to content

Commit

Permalink
Split TransactionListScene
Browse files Browse the repository at this point in the history
  • Loading branch information
Jon-edge committed Jan 29, 2025
1 parent c0eb2b0 commit f50c090
Show file tree
Hide file tree
Showing 12 changed files with 377 additions and 83 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
## Unreleased (develop)

- added: Search bar to `EarnScene`
- added: Price chart to `TransactionListScene`
- added: Add Unizen DEX
- changed: `TransactionListScene` split into two scenes: `TransactionListScene` and `TransactionListScene2`

## 4.21.1 (2025-01-28)

Expand Down
4 changes: 2 additions & 2 deletions src/actions/WalletListMenuActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export type WalletListMenuKey =
| string // for split keys like splitbitcoincash, splitethereum, etc.

export function walletListMenuAction(
navigation: WalletsTabSceneProps<'walletList' | 'transactionList'>['navigation'],
navigation: WalletsTabSceneProps<'walletList' | 'walletDetails'>['navigation'],
walletId: string,
option: WalletListMenuKey,
tokenId: EdgeTokenId,
Expand Down Expand Up @@ -292,7 +292,7 @@ export function walletListMenuAction(
const { currencyWallets } = account
const wallet = currencyWallets[walletId]

navigation.navigate('transactionList', {
navigation.navigate('walletDetails', {
walletId,
tokenId: null,
walletName: getWalletName(wallet)
Expand Down
18 changes: 17 additions & 1 deletion src/components/Main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,10 @@ import { SweepPrivateKeyCompletionScene as SweepPrivateKeyCompletionSceneCompone
import { SweepPrivateKeyProcessingScene as SweepPrivateKeyProcessingSceneComponent } from './scenes/SweepPrivateKeyProcessingScene'
import { SweepPrivateKeySelectCryptoScene as SweepPrivateKeySelectCryptoSceneComponent } from './scenes/SweepPrivateKeySelectCryptoScene'
import { TransactionDetailsScene as TransactionDetailsSceneComponent } from './scenes/TransactionDetailsScene'
import { TransactionList as TransactionListComponent } from './scenes/TransactionListScene'
import { TransactionList as TransactionListComponent } from './scenes/TransactionListScene2'
import { TransactionsExportScene as TransactionsExportSceneComponent } from './scenes/TransactionsExportScene'
import { UpgradeUsernameScene as UpgradeUsernameSceneComponent } from './scenes/UpgradeUsernameScreen'
import { WalletDetails as WalletDetailsComponent } from './scenes/WalletDetailsScene'
import { WalletListScene as WalletListSceneComponent } from './scenes/WalletListScene'
import { WalletRestoreScene as WalletRestoreSceneComponent } from './scenes/WalletRestoreScene'
import { WcConnectionsScene as WcConnectionsSceneComponent } from './scenes/WcConnectionsScene'
Expand Down Expand Up @@ -218,6 +219,7 @@ const SwapSettingsScene = ifLoggedIn(SwapSettingsSceneComponent)
const SwapSuccessScene = ifLoggedIn(SwapSuccessSceneComponent)
const TransactionDetailsScene = ifLoggedIn(TransactionDetailsSceneComponent)
const TransactionList = ifLoggedIn(TransactionListComponent)
const WalletDetails = ifLoggedIn(WalletDetailsComponent)
const TransactionsExportScene = ifLoggedIn(TransactionsExportSceneComponent)
const WalletListScene = ifLoggedIn(WalletListSceneComponent)
const WalletRestoreScene = ifLoggedIn(WalletRestoreSceneComponent)
Expand Down Expand Up @@ -276,6 +278,13 @@ const EdgeWalletsTabScreen = () => {
headerTitle: () => <ParamHeaderTitle<'transactionList'> fromParams={params => params.walletName} />
}}
/>
<WalletsStack.Screen
name="walletDetails"
component={WalletDetails}
options={{
headerTitle: () => <ParamHeaderTitle<'walletDetails'> fromParams={params => params.walletName} />
}}
/>
</WalletsStack.Navigator>
)
}
Expand Down Expand Up @@ -781,6 +790,13 @@ const EdgeAppStack = () => {
headerTitle: () => <TransactionDetailsTitle />
}}
/>
<WalletsStack.Screen
name="transactionList"
component={TransactionList}
options={{
headerTitle: () => <ParamHeaderTitle<'transactionList'> fromParams={params => params.walletName} />
}}
/>
<AppStack.Screen
name="transactionsExport"
component={TransactionsExportScene}
Expand Down
2 changes: 1 addition & 1 deletion src/components/cards/VisaCardCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const ioniaPluginIds = Object.keys(SPECIAL_CURRENCY_INFO).filter(pluginId
interface Props {
wallet: EdgeCurrencyWallet
tokenId: EdgeTokenId
navigation: WalletsTabSceneProps<'transactionList'>['navigation']
navigation: WalletsTabSceneProps<'walletDetails'>['navigation']
}

export const VisaCardCard = (props: Props) => {
Expand Down
2 changes: 1 addition & 1 deletion src/components/modals/WalletListMenuModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ interface Option {

interface Props {
bridge: AirshipBridge<void>
navigation: WalletsTabSceneProps<'walletList' | 'transactionList'>['navigation']
navigation: WalletsTabSceneProps<'walletList' | 'walletDetails'>['navigation']

// Wallet identity:
tokenId: EdgeTokenId
Expand Down
266 changes: 266 additions & 0 deletions src/components/scenes/TransactionListScene2.tsx
Original file line number Diff line number Diff line change
@@ -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 TransactionListParams {
walletId: string
walletName: string
tokenId: EdgeTokenId
countryCode?: string
}

type ListItem = EdgeTransaction | string | null
interface Props extends WalletsTabSceneProps<'transactionList'> {
wallet: EdgeCurrencyWallet
}

function TransactionListComponent(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<Animated.FlatList<ListItem> | null>(null)
const [isSearching, setIsSearching] = React.useState(false)
const [searchText, setSearchText] = React.useState('')
const [footerHeight, setFooterHeight] = React.useState<number | undefined>()

// 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 : (
<RefreshControl
refreshing={false}
enabled={false}
style={{ opacity: 0 }}
// useHandler isn't needed, since we're already in useMemo:
onRefresh={() => {}}
/>
)
}, [])

const emptyComponent = React.useMemo(() => {
if (isTransactionListUnsupported) {
return <ExplorerCard wallet={wallet} tokenId={tokenId} />
} else if (isSearching) {
return <SectionHeaderCentered title={lstrings.transaction_list_search_no_result} />
}
return null
}, [isTransactionListUnsupported, isSearching, wallet, tokenId])

const renderItem = useHandler(({ index, item }: ListRenderItemInfo<ListItem>) => {
if (item == null) {
return <EmptyLoader />
}

const disableAnimation = index >= MAX_LIST_ITEMS_ANIM
if (typeof item === 'string') {
return (
<EdgeAnim disableAnimation={disableAnimation} enter={{ type: 'fadeInDown', distance: 30 * (index + 1) }}>
<SectionHeader title={item} />
</EdgeAnim>
)
}
return (
<EdgeAnim disableAnimation={disableAnimation} enter={{ type: 'fadeInDown', distance: 30 * (index + 1) }}>
<TransactionListRow navigation={navigation as NavigationBase} transaction={item} wallet={wallet} />
</EdgeAnim>
)
})

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 (
<SearchFooter
name="TransactionListScene2-SearchFooter"
placeholder={lstrings.transaction_list_search}
isSearching={isSearching}
searchText={searchText}
noBackground
sceneWrapperInfo={sceneWrapperInfo}
onStartSearching={handleStartSearching}
onDoneSearching={handleDoneSearching}
onChangeText={handleChangeText}
onLayoutHeight={handleFooterLayoutHeight}
/>
)
},
[handleChangeText, handleDoneSearching, handleFooterLayoutHeight, handleStartSearching, isSearching, searchText]
)

return (
<SceneWrapper avoidKeyboard footerHeight={footerHeight} hasTabs hasNotifications renderFooter={renderFooter}>
{({ insetStyle, undoInsetStyle }) => (
<View style={undoInsetStyle}>
<Animated.FlatList
style={styles.flatList}
ref={flashListRef}
contentContainerStyle={{
paddingTop: insetStyle.paddingTop + theme.rem(0.5),
paddingBottom: insetStyle.paddingBottom + theme.rem(0.5),
paddingLeft: insetStyle.paddingLeft + theme.rem(0.5),
paddingRight: insetStyle.paddingRight + theme.rem(0.5)
}}
data={listItems}
keyboardDismissMode="on-drag"
keyboardShouldPersistTaps="handled"
keyExtractor={keyExtractor}
ListEmptyComponent={emptyComponent}
onEndReachedThreshold={0.5}
renderItem={renderItem}
onEndReached={handleScrollEnd}
onScroll={handleScroll}
scrollIndicatorInsets={SCROLL_INDICATOR_INSET_FIX}
refreshControl={refreshControl}
/>
</View>
)}
</SceneWrapper>
)
}

/**
* 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 TransactionList = withWallet(TransactionListComponent)

const getStyles = cacheStyles(() => ({
flatList: {
flex: 1
}
}))
Loading

0 comments on commit f50c090

Please sign in to comment.