(Fix) Transctions infinite scroll (#1931)

* install `react-intersection-observer` dependency

- also, remove `react-infinite-scroll-component`

* refactor `InfiniteScroll` to be used with `react-intersection-observer`

* build an infinite scroll wrapper for transactions based on `InfiniteScroll`

* recover `TxsInfiniteScrollContext` information to identify the last item in a list

- a new component was created for History transactions: `HistoryTransactions` as a wrapper

* refactor lists to use `TxsInfiniteScrollContext` and identify the last item in the list

* allow to pass config to the InfiniteScroll component

 - also changed default bottom margin so the txs loading starts a bit earlier

* fix memory consumption issue based on nft retrieval/update data

* delay `lastItemId` set to next tick, to prevent multiple updates during the same render phase

* Set triggerOnce to infinitescroll

Co-authored-by: nicosampler <nf.dominguez.87@gmail.com>
Co-authored-by: nicolas <nicosampler@users.noreply.github.com>
Co-authored-by: Daniel Sanchez <daniel.sanchez@gnosis.pm>
This commit is contained in:
Fernando 2021-02-26 18:46:31 -03:00 committed by GitHub
parent 16c347e97e
commit ae8175aae2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 276 additions and 194 deletions

View File

@ -215,7 +215,7 @@
"react-final-form-listeners": "^1.0.2",
"react-ga": "3.3.0",
"react-hot-loader": "4.13.0",
"react-infinite-scroll-component": "^5.1.0",
"react-intersection-observer": "^8.31.0",
"react-qr-reader": "^2.2.1",
"react-redux": "7.2.2",
"react-router-dom": "5.2.0",

View File

@ -1,39 +1,53 @@
import { Loader } from '@gnosis.pm/safe-react-components'
import React, { ReactElement } from 'react'
import { default as ReactInfiniteScroll, Props as ReactInfiniteScrollProps } from 'react-infinite-scroll-component'
import styled from 'styled-components'
import React, { createContext, forwardRef, MutableRefObject, ReactElement, ReactNode, useEffect, useState } from 'react'
import { InViewHookResponse, useInView } from 'react-intersection-observer'
import { Overwrite } from 'src/types/helpers'
export const INFINITE_SCROLL_CONTAINER = 'infinite-scroll-container'
export const Centered = styled.div<{ padding?: number }>`
width: 100%;
height: 100%;
display: flex;
padding: ${({ padding }) => `${padding}px`};
justify-content: center;
align-items: center;
`
export const InfiniteScrollContext = createContext<{
ref: MutableRefObject<HTMLDivElement | null> | ((instance: HTMLDivElement | null) => void) | null
lastItemId?: string
setLastItemId: (itemId?: string) => void
}>({ setLastItemId: () => {}, ref: null })
export const SCROLLABLE_TARGET_ID = 'scrollableDiv'
export const InfiniteScrollProvider = forwardRef<HTMLDivElement, { children: ReactNode }>(
({ children }, ref): ReactElement => {
const [lastItemId, _setLastItemId] = useState<string>()
type InfiniteScrollProps = Overwrite<ReactInfiniteScrollProps, { loader?: ReactInfiniteScrollProps['loader'] }>
const setLastItemId = (itemId?: string) => {
setTimeout(() => _setLastItemId(itemId), 0)
}
export const InfiniteScroll = ({ dataLength, next, hasMore, ...props }: InfiniteScrollProps): ReactElement => {
return (
<ReactInfiniteScroll
style={{ overflow: 'hidden' }}
dataLength={dataLength}
next={next}
hasMore={hasMore}
loader={
<Centered>
<Loader size="md" />
</Centered>
}
scrollThreshold="120px"
scrollableTarget={SCROLLABLE_TARGET_ID}
>
{props.children}
</ReactInfiniteScroll>
)
return (
<InfiniteScrollContext.Provider value={{ ref, lastItemId, setLastItemId }}>
{children}
</InfiniteScrollContext.Provider>
)
},
)
InfiniteScrollProvider.displayName = 'InfiniteScrollProvider'
type InfiniteScrollProps = {
children: ReactNode
hasMore: boolean
next: () => Promise<void>
config?: InViewHookResponse
}
export const InfiniteScroll = ({ children, hasMore, next, config }: InfiniteScrollProps): ReactElement => {
const { ref, inView } = useInView({
threshold: 0,
root: document.querySelector(`#${INFINITE_SCROLL_CONTAINER}`),
rootMargin: '0px 0px 200px 0px',
triggerOnce: true,
...config,
})
useEffect(() => {
if (inView && hasMore) {
next()
}
}, [inView, hasMore, next])
return <InfiniteScrollProvider ref={ref}>{children}</InfiniteScrollProvider>
}

View File

@ -1,10 +1,10 @@
import { RateLimit } from 'async-sema'
import { Collectibles, NFTAsset, NFTAssets, NFTTokens } from 'src/logic/collectibles/sources/collectibles.d'
import { sameAddress } from 'src/logic/wallets/ethAddresses'
import NFTIcon from 'src/routes/safe/components/Balances/assets/nft_icon.png'
import { fetchErc20AndErc721AssetsList, fetchSafeCollectibles } from 'src/logic/tokens/api'
import { TokenResult } from 'src/logic/tokens/api/fetchErc20AndErc721AssetsList'
import { CollectibleResult } from 'src/logic/tokens/api/fetchSafeCollectibles'
import { sameString } from 'src/utils/strings'
type FetchResult = {
erc721Assets: TokenResult[]
@ -12,67 +12,70 @@ type FetchResult = {
}
class Gnosis {
_rateLimit = async (): Promise<void> => {}
_fetch = async (safeAddress: string): Promise<FetchResult> => {
const collectibles: FetchResult = {
erc721Assets: [],
erc721Tokens: [],
}
const [assets, tokens] = await Promise.allSettled([
fetchErc20AndErc721AssetsList(),
fetchSafeCollectibles(safeAddress),
])
try {
const {
data: { results: assets = [] },
} = await fetchErc20AndErc721AssetsList()
collectibles.erc721Assets = assets.filter((token) => token.type.toLowerCase() === 'erc721')
} catch (e) {
console.error('no erc721 assets could be fetched', e)
switch (assets.status) {
case 'fulfilled':
const {
data: { results = [] },
} = assets.value
collectibles.erc721Assets = results.filter((token) => sameString(token.type, 'erc721'))
break
case 'rejected':
console.error('no erc721 assets could be fetched', assets.reason)
break
}
try {
const { data: tokens = [] } = await fetchSafeCollectibles(safeAddress)
collectibles.erc721Tokens = tokens
} catch (e) {
console.error('no erc721 tokens for the current safe', e)
switch (tokens.status) {
case 'fulfilled':
const {
data: { results = [] },
} = tokens.value
collectibles.erc721Tokens = results
break
case 'rejected':
console.error('no erc721 tokens for the current safe', tokens.reason)
break
}
return collectibles
}
/**
* OpenSea class constructor
* @param {object} options
* @param {number} options.rps - requests per second
*/
constructor(options: { rps: number }) {
// eslint-disable-next-line no-underscore-dangle
this._rateLimit = RateLimit(options.rps, { timeUnit: 60 * 1000, uniformDistribution: true })
static extractNFTAsset = (asset: TokenResult, nftTokens: NFTTokens): NFTAsset => {
const mainAssetAddress = asset.address
const numberOfTokens = nftTokens.filter(({ assetAddress }) => sameAddress(assetAddress, mainAssetAddress)).length
return {
address: mainAssetAddress,
description: asset.name,
image: asset.logoUri || NFTIcon,
name: asset.name,
numberOfTokens,
slug: `${mainAssetAddress}_${asset.name}`,
symbol: asset.symbol,
}
}
static extractAssets(assets: TokenResult[], nftTokens: NFTTokens): NFTAssets {
const extractNFTAsset = (asset: TokenResult): NFTAsset => {
const numberOfTokens = nftTokens.filter(({ assetAddress }) => assetAddress === asset.address).length
const extractedAssets = {}
return {
address: asset.address,
description: asset.name,
image: asset.logoUri || NFTIcon,
name: asset.name,
numberOfTokens,
slug: `${asset.address}_${asset.name}`,
symbol: asset.symbol,
}
}
return assets.reduce((acc, asset) => {
assets.forEach((asset) => {
const address = asset.address
if (acc[address] === undefined) {
acc[address] = extractNFTAsset(asset)
if (extractedAssets[address] === undefined) {
extractedAssets[address] = Gnosis.extractNFTAsset(asset, nftTokens)
}
})
return acc
}, {})
return extractedAssets
}
static extractTokens(tokens: CollectibleResult[]): NFTTokens {
@ -94,12 +97,11 @@ class Gnosis {
*/
async fetchCollectibles(safeAddress: string): Promise<Collectibles> {
const { erc721Assets, erc721Tokens } = await this._fetch(safeAddress)
const nftTokens = Gnosis.extractTokens(erc721Tokens)
return {
nftTokens,
nftAssets: Gnosis.extractAssets(erc721Assets, nftTokens),
}
const nftTokens = Gnosis.extractTokens(erc721Tokens)
const nftAssets = Gnosis.extractAssets(erc721Assets, nftTokens)
return { nftTokens, nftAssets }
}
}

View File

@ -5,7 +5,7 @@ import { COLLECTIBLES_SOURCE } from 'src/utils/constants'
const SOURCES = {
opensea: new OpenSea({ rps: 4 }),
gnosis: new Gnosis({ rps: 4 }),
gnosis: new Gnosis(),
mockedopensea: new MockedOpenSea({ rps: 4 }),
}

View File

@ -1,4 +1,3 @@
import { batch } from 'react-redux'
import { Dispatch } from 'redux'
import { getConfiguredSource } from 'src/logic/collectibles/sources'
@ -9,10 +8,8 @@ export const fetchCollectibles = (safeAddress: string) => async (dispatch: Dispa
const source = getConfiguredSource()
const collectibles = await source.fetchCollectibles(safeAddress)
batch(() => {
dispatch(addNftAssets(collectibles.nftAssets))
dispatch(addNftTokens(collectibles.nftTokens))
})
dispatch(addNftAssets(collectibles.nftAssets))
dispatch(addNftTokens(collectibles.nftTokens))
} catch (error) {
console.log('Error fetching collectibles:', error)
}

View File

@ -11,7 +11,7 @@ export const gatewayTransactions = (state: AppReduxState): AppReduxState['gatewa
return state[GATEWAY_TRANSACTIONS_ID]
}
export const historyTransactions = createSelector(
export const historyTransactions = createHashBasedSelector(
gatewayTransactions,
safeParamAddressFromStateSelector,
(gatewayTransactions, safeAddress): StoreStructure['history'] | undefined => {

View File

@ -8,6 +8,6 @@ import { AppReduxState } from 'src/store'
export const createIsEqualSelector = createSelectorCreator(defaultMemoize, isEqual)
const hashFn = (gatewayTransactions: AppReduxState['gatewayTransactions'], safeAddress: string): string =>
hash(gatewayTransactions[safeAddress])
hash(gatewayTransactions[safeAddress] ?? {})
export const createHashBasedSelector = createSelectorCreator(memoize as any, hashFn)

View File

@ -11,10 +11,10 @@ export type TokenResult = {
type: string
}
export const fetchErc20AndErc721AssetsList = async (): Promise<AxiosResponse<{ results: TokenResult[] }>> => {
export const fetchErc20AndErc721AssetsList = (): Promise<AxiosResponse<{ results: TokenResult[] }>> => {
const url = getTokensServiceBaseUrl()
return axios.get<{ results: TokenResult[] }>(`${url}/`, {
return axios.get<{ results: TokenResult[] }, AxiosResponse<{ results: TokenResult[] }>>(`${url}/`, {
params: {
limit: 3000,
},

View File

@ -16,9 +16,11 @@ export type CollectibleResult = {
uri: string | null
}
export const fetchSafeCollectibles = async (safeAddress: string): Promise<AxiosResponse<CollectibleResult[]>> => {
export const fetchSafeCollectibles = async (
safeAddress: string,
): Promise<AxiosResponse<{ results: CollectibleResult[] }>> => {
const address = checksumAddress(safeAddress)
const url = `${getSafeServiceBaseUrl(address)}/collectibles/`
return axios.get(url)
return axios.get<CollectibleResult[], AxiosResponse<{ results: CollectibleResult[] }>>(url)
}

View File

@ -1,6 +1,5 @@
import { backOff } from 'exponential-backoff'
import { List, Map } from 'immutable'
import { batch } from 'react-redux'
import { Dispatch } from 'redux'
import {
@ -84,11 +83,9 @@ const fetchSafeTokens = (safeAddress: string) => async (
balances.keySeq().toSet().subtract(blacklistedTokens),
)
batch(() => {
dispatch(updateSafe({ address: safeAddress, activeTokens, balances, ethBalance }))
dispatch(setCurrencyBalances(safeAddress, currencyList))
dispatch(addTokens(tokens))
})
dispatch(updateSafe({ address: safeAddress, activeTokens, balances, ethBalance }))
dispatch(setCurrencyBalances(safeAddress, currencyList))
dispatch(addTokens(tokens))
} catch (err) {
console.error('Error fetching active token list', err)
}

View File

@ -0,0 +1,25 @@
import { Loader } from '@gnosis.pm/safe-react-components'
import React, { ReactElement } from 'react'
import { usePagedHistoryTransactions } from './hooks/usePagedHistoryTransactions'
import { Centered } from './styled'
import { HistoryTxList } from './HistoryTxList'
import { TxsInfiniteScroll } from './TxsInfiniteScroll'
export const HistoryTransactions = (): ReactElement => {
const { count, hasMore, next, transactions, isLoading } = usePagedHistoryTransactions()
if (count === 0) {
return (
<Centered>
<Loader size="md" />
</Centered>
)
}
return (
<TxsInfiniteScroll next={next} hasMore={hasMore} isLoading={isLoading}>
<HistoryTxList transactions={transactions} />
</TxsInfiniteScroll>
)
}

View File

@ -1,46 +1,35 @@
import { Loader } from '@gnosis.pm/safe-react-components'
import React, { ReactElement } from 'react'
import React, { ReactElement, useContext } from 'react'
import { InfiniteScroll, SCROLLABLE_TARGET_ID } from 'src/components/InfiniteScroll'
import { usePagedHistoryTransactions } from './hooks/usePagedHistoryTransactions'
import {
SubTitle,
ScrollableTransactionsContainer,
StyledTransactions,
StyledTransactionsGroup,
Centered,
} from './styled'
import { TransactionDetails } from 'src/logic/safe/store/models/types/gateway.d'
import { TxsInfiniteScrollContext } from 'src/routes/safe/components/Transactions/GatewayTransactions/TxsInfiniteScroll'
import { formatWithSchema } from 'src/utils/date'
import { sameString } from 'src/utils/strings'
import { StyledTransactions, StyledTransactionsGroup, SubTitle } from './styled'
import { TxHistoryRow } from './TxHistoryRow'
import { TxLocationContext } from './TxLocationProvider'
import { formatWithSchema } from 'src/utils/date'
export const HistoryTxList = (): ReactElement => {
const { count, hasMore, next, transactions } = usePagedHistoryTransactions()
export const HistoryTxList = ({ transactions }: { transactions: TransactionDetails['transactions'] }): ReactElement => {
const { lastItemId, setLastItemId } = useContext(TxsInfiniteScrollContext)
if (count === 0) {
return (
<Centered>
<Loader size="md" />
</Centered>
)
const [, lastTransactionsGroup] = transactions[transactions.length - 1]
const lastTransaction = lastTransactionsGroup[lastTransactionsGroup.length - 1]
if (!sameString(lastItemId, lastTransaction.id)) {
setLastItemId(lastTransaction.id)
}
return (
<TxLocationContext.Provider value={{ txLocation: 'history' }}>
<ScrollableTransactionsContainer id={SCROLLABLE_TARGET_ID}>
<InfiniteScroll dataLength={transactions.length} next={next} hasMore={hasMore}>
{transactions?.map(([timestamp, txs]) => (
<StyledTransactionsGroup key={timestamp}>
<SubTitle size="lg">{formatWithSchema(Number(timestamp), 'MMM d, yyyy')}</SubTitle>
<StyledTransactions>
{txs.map((transaction) => (
<TxHistoryRow key={transaction.id} transaction={transaction} />
))}
</StyledTransactions>
</StyledTransactionsGroup>
))}
</InfiniteScroll>
</ScrollableTransactionsContainer>
{transactions?.map(([timestamp, txs]) => (
<StyledTransactionsGroup key={timestamp}>
<SubTitle size="lg">{formatWithSchema(Number(timestamp), 'MMM d, yyyy')}</SubTitle>
<StyledTransactions>
{txs.map((transaction) => (
<TxHistoryRow key={transaction.id} transaction={transaction} />
))}
</StyledTransactions>
</StyledTransactionsGroup>
))}
</TxLocationContext.Provider>
)
}

View File

@ -2,15 +2,15 @@ import { Loader, Title } from '@gnosis.pm/safe-react-components'
import React, { ReactElement } from 'react'
import style from 'styled-components'
import { InfiniteScroll, SCROLLABLE_TARGET_ID } from 'src/components/InfiniteScroll'
import Img from 'src/components/layout/Img'
import { usePagedQueuedTransactions } from './hooks/usePagedQueuedTransactions'
import { ActionModal } from './ActionModal'
import { TxActionProvider } from './TxActionProvider'
import { TxLocationContext } from './TxLocationProvider'
import { QueueTxList } from './QueueTxList'
import { ScrollableTransactionsContainer, Centered } from './styled'
import NoTransactionsImage from './assets/no-transactions.svg'
import { usePagedQueuedTransactions } from './hooks/usePagedQueuedTransactions'
import { QueueTxList } from './QueueTxList'
import { Centered } from './styled'
import { TxActionProvider } from './TxActionProvider'
import { TxsInfiniteScroll } from './TxsInfiniteScroll'
import { TxLocationContext } from './TxLocationProvider'
const NoTransactions = style.div`
display: flex;
@ -19,11 +19,11 @@ const NoTransactions = style.div`
`
export const QueueTransactions = (): ReactElement => {
const { count, loading, hasMore, next, transactions } = usePagedQueuedTransactions()
const { count, isLoading, hasMore, next, transactions } = usePagedQueuedTransactions()
// `loading` is, actually `!transactions`
// added the `transaction` verification to prevent `Object is possibly 'undefined'` error
if (loading || !transactions) {
if (isLoading || !transactions) {
return (
<Centered>
<Loader size="md" />
@ -42,19 +42,17 @@ export const QueueTransactions = (): ReactElement => {
return (
<TxActionProvider>
<ScrollableTransactionsContainer id={SCROLLABLE_TARGET_ID}>
<InfiniteScroll dataLength={count} next={next} hasMore={hasMore}>
{/* Next list */}
<TxLocationContext.Provider value={{ txLocation: 'queued.next' }}>
{transactions.next.count !== 0 && <QueueTxList transactions={transactions.next.transactions} />}
</TxLocationContext.Provider>
<TxsInfiniteScroll next={next} hasMore={hasMore} isLoading={isLoading}>
{/* Next list */}
<TxLocationContext.Provider value={{ txLocation: 'queued.next' }}>
{transactions.next.count !== 0 && <QueueTxList transactions={transactions.next.transactions} />}
</TxLocationContext.Provider>
{/* Queue list */}
<TxLocationContext.Provider value={{ txLocation: 'queued.queued' }}>
{transactions.queue.count !== 0 && <QueueTxList transactions={transactions.queue.transactions} />}
</TxLocationContext.Provider>
</InfiniteScroll>
</ScrollableTransactionsContainer>
{/* Queue list */}
<TxLocationContext.Provider value={{ txLocation: 'queued.queued' }}>
{transactions.queue.count !== 0 && <QueueTxList transactions={transactions.queue.transactions} />}
</TxLocationContext.Provider>
</TxsInfiniteScroll>
<ActionModal />
</TxActionProvider>
)

View File

@ -2,6 +2,7 @@ import { Icon, Link, Text } from '@gnosis.pm/safe-react-components'
import React, { Fragment, ReactElement, useContext } from 'react'
import { Transaction, TransactionDetails } from 'src/logic/safe/store/models/types/gateway.d'
import { sameString } from 'src/utils/strings'
import {
DisclaimerContainer,
GroupedTransactions,
@ -14,6 +15,7 @@ import {
import { TxHoverProvider } from './TxHoverProvider'
import { TxLocationContext } from './TxLocationProvider'
import { TxQueueRow } from './TxQueueRow'
import { TxsInfiniteScrollContext } from './TxsInfiniteScroll'
const TreeView = ({ firstElement }: { firstElement: boolean }): ReactElement => {
return <p className="tree-lines">{firstElement ? <span className="first-node" /> : null}</p>
@ -52,8 +54,8 @@ type QueueTransactionProps = {
transactions: Transaction[]
}
const QueueTransaction = ({ nonce, transactions }: QueueTransactionProps): ReactElement => {
return transactions.length > 1 ? (
const QueueTransaction = ({ nonce, transactions }: QueueTransactionProps): ReactElement =>
transactions.length > 1 ? (
<GroupedTransactionsCard>
<TxHoverProvider>
<Disclaimer nonce={nonce} />
@ -70,7 +72,6 @@ const QueueTransaction = ({ nonce, transactions }: QueueTransactionProps): React
) : (
<TxQueueRow transaction={transactions[0]} />
)
}
type QueueTxListProps = {
transactions: TransactionDetails['transactions']
@ -80,6 +81,14 @@ export const QueueTxList = ({ transactions }: QueueTxListProps): ReactElement =>
const { txLocation } = useContext(TxLocationContext)
const title = txLocation === 'queued.next' ? 'NEXT TRANSACTION' : 'QUEUE'
const { lastItemId, setLastItemId } = useContext(TxsInfiniteScrollContext)
const [, lastTransactionsGroup] = transactions[transactions.length - 1]
const lastTransaction = lastTransactionsGroup[lastTransactionsGroup.length - 1]
if (txLocation === 'queued.queued' && !sameString(lastItemId, lastTransaction.id)) {
setLastItemId(lastTransaction.id)
}
return (
<StyledTransactionsGroup>
<SubTitle size="lg">{title}</SubTitle>

View File

@ -11,7 +11,7 @@ import {
isSettingsChangeTxInfo,
Transaction,
} from 'src/logic/safe/store/models/types/gateway.d'
import { TxCollapsedActions } from 'src/routes/safe/components/Transactions/GatewayTransactions/TxCollapsedActions'
import { TxCollapsedActions } from './TxCollapsedActions'
import { formatDateTime, formatTime, formatTimeInWords } from 'src/utils/date'
import { KNOWN_MODULES } from 'src/utils/constants'
import { sameString } from 'src/utils/strings'
@ -21,6 +21,7 @@ import { TransactionStatusProps } from './hooks/useTransactionStatus'
import { TxTypeProps } from './hooks/useTransactionType'
import { StyledGroupedTransactions, StyledTransaction } from './styled'
import { TokenTransferAmount } from './TokenTransferAmount'
import { TxsInfiniteScrollContext } from './TxsInfiniteScroll'
import { TxLocationContext } from './TxLocationProvider'
import { CalculatedVotes } from './TxQueueCollapsed'
@ -89,7 +90,7 @@ const TooltipContent = styled.div`
`
type TxCollapsedProps = {
transaction?: Transaction
transaction: Transaction
isGrouped?: boolean
nonce?: number
type: TxTypeProps
@ -112,6 +113,7 @@ export const TxCollapsed = ({
status,
}: TxCollapsedProps): ReactElement => {
const { txLocation } = useContext(TxLocationContext)
const { ref, lastItemId } = useContext(TxsInfiniteScrollContext)
const willBeReplaced = transaction?.txStatus === 'WILL_BE_REPLACED' ? ' will-be-replaced' : ''
@ -161,8 +163,9 @@ export const TxCollapsed = ({
</div>
)
// attaching ref to a div element as it was causing troubles to add a `ref` to a FunctionComponent
const txCollapsedStatus = (
<div className="tx-status">
<div className="tx-status" ref={sameString(lastItemId, transaction.id) ? ref : null}>
{transaction?.txStatus === 'PENDING' || transaction?.txStatus === 'PENDING_FAILED' ? (
<CircularProgressPainter color={status.color}>
<CircularProgress size={14} color="inherit" />

View File

@ -32,22 +32,26 @@ export const TxCollapsedActions = ({ transaction }: TxCollapsedActionsProps): Re
return (
<>
<Tooltip title={transaction.txStatus === 'AWAITING_EXECUTION' ? 'Execute' : 'Confirm'} placement="top">
<IconButton
size="small"
type="button"
onClick={handleConfirmButtonClick}
disabled={disabledActions}
onMouseEnter={handleOnMouseEnter}
onMouseLeave={handleOnMouseLeave}
>
<Icon type={transaction.txStatus === 'AWAITING_EXECUTION' ? 'rocket' : 'check'} color="primary" size="sm" />
</IconButton>
<span>
<IconButton
size="small"
type="button"
onClick={handleConfirmButtonClick}
disabled={disabledActions}
onMouseEnter={handleOnMouseEnter}
onMouseLeave={handleOnMouseLeave}
>
<Icon type={transaction.txStatus === 'AWAITING_EXECUTION' ? 'rocket' : 'check'} color="primary" size="sm" />
</IconButton>
</span>
</Tooltip>
{canCancel && (
<Tooltip title="Cancel" placement="top">
<IconButton size="small" type="button" onClick={handleCancelButtonClick} disabled={isPending}>
<Icon type="circleCross" color="error" size="sm" />
</IconButton>
<span>
<IconButton size="small" type="button" onClick={handleCancelButtonClick} disabled={isPending}>
<Icon type="circleCross" color="error" size="sm" />
</IconButton>
</span>
</Tooltip>
)}
</>

View File

@ -12,5 +12,14 @@ export const TxHistoryCollapsed = ({ transaction }: { transaction: Transaction }
const info = useAssetInfo(transaction.txInfo)
const status = useTransactionStatus(transaction)
return <TxCollapsed nonce={nonce} type={type} info={info} time={transaction.timestamp} status={status} />
return (
<TxCollapsed
nonce={nonce}
type={type}
info={info}
time={transaction.timestamp}
status={status}
transaction={transaction}
/>
)
}

View File

@ -0,0 +1,27 @@
import { Loader } from '@gnosis.pm/safe-react-components'
import React, { ReactElement, ReactNode } from 'react'
import { INFINITE_SCROLL_CONTAINER, InfiniteScroll } from 'src/components/InfiniteScroll'
import { HorizontallyCentered, ScrollableTransactionsContainer } from './styled'
type TxsInfiniteScrollProps = {
children: ReactNode
next: () => Promise<void>
hasMore: boolean
isLoading: boolean
}
export const TxsInfiniteScroll = ({ children, next, hasMore, isLoading }: TxsInfiniteScrollProps): ReactElement => {
return (
<InfiniteScroll next={next} hasMore={hasMore}>
<ScrollableTransactionsContainer id={INFINITE_SCROLL_CONTAINER}>
{children}
<HorizontallyCentered isVisible={isLoading}>
<Loader size="md" />
</HorizontallyCentered>
</ScrollableTransactionsContainer>
</InfiniteScroll>
)
}
export { InfiniteScrollContext as TxsInfiniteScrollContext } from 'src/components/InfiniteScroll'

View File

@ -1,4 +1,4 @@
import { useState } from 'react'
import { useCallback, useRef, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { loadPagedHistoryTransactions } from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadGatewayTransactions'
@ -12,17 +12,20 @@ type PagedTransactions = {
transactions: TransactionDetails['transactions']
hasMore: boolean
next: () => Promise<void>
isLoading: boolean
}
export const usePagedHistoryTransactions = (): PagedTransactions => {
const { count, transactions } = useHistoryTransactions()
const dispatch = useDispatch()
const safeAddress = useSelector(safeParamAddressFromStateSelector)
const dispatch = useRef(useDispatch())
const safeAddress = useRef(useSelector(safeParamAddressFromStateSelector))
const [hasMore, setHasMore] = useState(true)
const [isLoading, setIsLoading] = useState(false)
const next = async () => {
const results = await loadPagedHistoryTransactions(safeAddress)
const next = useCallback(async () => {
setIsLoading(true)
const results = await loadPagedHistoryTransactions(safeAddress.current)
if (!results) {
setHasMore(false)
@ -36,11 +39,12 @@ export const usePagedHistoryTransactions = (): PagedTransactions => {
}
if (values) {
dispatch(addHistoryTransactions({ safeAddress, values, isTail: true }))
dispatch.current(addHistoryTransactions({ safeAddress: safeAddress.current, values, isTail: true }))
} else {
setHasMore(false)
}
}
setIsLoading(false)
}, [])
return { count, transactions, hasMore, next }
return { count, transactions, hasMore, next, isLoading }
}

View File

@ -8,7 +8,7 @@ import { QueueTransactionsInfo, useQueueTransactions } from './useQueueTransacti
type PagedQueuedTransactions = {
count: number
loading: boolean
isLoading: boolean
transactions?: QueueTransactionsInfo
hasMore: boolean
next: () => Promise<void>
@ -46,7 +46,7 @@ export const usePagedQueuedTransactions = (): PagedQueuedTransactions => {
count = transactions.next.count + transactions.queue.count
}
const loading = typeof transactions === 'undefined' || typeof count === 'undefined'
const isLoading = typeof transactions === 'undefined' || typeof count === 'undefined'
return { count, loading, transactions, hasMore, next: nextPage }
return { count, isLoading, transactions, hasMore, next: nextPage }
}

View File

@ -3,7 +3,7 @@ import { Item } from '@gnosis.pm/safe-react-components/dist/navigation/Tab'
import React, { ReactElement, useState } from 'react'
import styled from 'styled-components'
import { HistoryTxList } from './HistoryTxList'
import { HistoryTransactions } from './HistoryTransactions'
import { QueueTransactions } from './QueueTransactions'
import { Breadcrumb, ContentWrapper, Wrapper } from './styled'
@ -27,7 +27,7 @@ const GatewayTransactions = (): ReactElement => {
<Tab items={items} onChange={setTab} selectedTab={tab} />
<ContentWrapper>
{tab === 'queue' && <QueueTransactions />}
{tab === 'history' && <HistoryTxList />}
{tab === 'history' && <HistoryTransactions />}
</ContentWrapper>
</Wrapper>
)

View File

@ -91,7 +91,6 @@ export const StyledTransactions = styled.div`
overflow: hidden;
width: 100%;
// '^' to prevent applying rules to the 'Actions' accordion components
& > .MuiAccordion-root {
&:first-child {
border-top: none;
@ -485,6 +484,11 @@ export const Centered = styled.div<{ padding?: number }>`
align-items: center;
`
export const HorizontallyCentered = styled(Centered)<{ isVisible: boolean }>`
visibility: ${({ isVisible }) => (isVisible ? 'visible' : 'hidden')};
height: auto;
`
export const StyledAccordionSummary = styled(AccordionSummary)`
height: 52px;
.tx-nonce {

View File

@ -17191,13 +17191,6 @@ react-hotkeys@2.0.0:
dependencies:
prop-types "^15.6.1"
react-infinite-scroll-component@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/react-infinite-scroll-component/-/react-infinite-scroll-component-5.1.0.tgz#3c0043cd17c6857c25d3ca7aa4171e0c33ce8023"
integrity sha512-ZQ7lYlLByil1ZVdvt57UJeBIj/tSOwKyds+B0LIm/xGmxzJJZwW/hYGKKs74sHO3LoIDwcq2ci6YXqrkWSalLQ==
dependencies:
throttle-debounce "^2.1.0"
react-inspector@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/react-inspector/-/react-inspector-4.0.1.tgz#0f888f78ff7daccbc7be5d452b20c96dc6d5fbb8"
@ -17207,6 +17200,11 @@ react-inspector@^4.0.0:
is-dom "^1.0.9"
prop-types "^15.6.1"
react-intersection-observer@^8.31.0:
version "8.31.0"
resolved "https://registry.yarnpkg.com/react-intersection-observer/-/react-intersection-observer-8.31.0.tgz#0ed21aaf93c4c0475b22b0ccaba6169076d01605"
integrity sha512-XraIC/tkrD9JtrmVA7ypEN1QIpKc52mXBH1u/bz/aicRLo8QQEJQAMUTb8mz4B6dqpPwyzgjrr7Ljv/2ACDtqw==
react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.9.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"