diff --git a/package.json b/package.json index dffcede7..e061bcf1 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/InfiniteScroll/index.tsx b/src/components/InfiniteScroll/index.tsx index b8bd7a1b..93a24a7e 100644 --- a/src/components/InfiniteScroll/index.tsx +++ b/src/components/InfiniteScroll/index.tsx @@ -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 | ((instance: HTMLDivElement | null) => void) | null + lastItemId?: string + setLastItemId: (itemId?: string) => void +}>({ setLastItemId: () => {}, ref: null }) -export const SCROLLABLE_TARGET_ID = 'scrollableDiv' +export const InfiniteScrollProvider = forwardRef( + ({ children }, ref): ReactElement => { + const [lastItemId, _setLastItemId] = useState() -type InfiniteScrollProps = Overwrite + const setLastItemId = (itemId?: string) => { + setTimeout(() => _setLastItemId(itemId), 0) + } -export const InfiniteScroll = ({ dataLength, next, hasMore, ...props }: InfiniteScrollProps): ReactElement => { - return ( - - - - } - scrollThreshold="120px" - scrollableTarget={SCROLLABLE_TARGET_ID} - > - {props.children} - - ) + return ( + + {children} + + ) + }, +) + +InfiniteScrollProvider.displayName = 'InfiniteScrollProvider' + +type InfiniteScrollProps = { + children: ReactNode + hasMore: boolean + next: () => Promise + 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 {children} } diff --git a/src/logic/collectibles/sources/Gnosis.ts b/src/logic/collectibles/sources/Gnosis.ts index c916a653..65d7868d 100644 --- a/src/logic/collectibles/sources/Gnosis.ts +++ b/src/logic/collectibles/sources/Gnosis.ts @@ -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 => {} - _fetch = async (safeAddress: string): Promise => { 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 { 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 } } } diff --git a/src/logic/collectibles/sources/index.ts b/src/logic/collectibles/sources/index.ts index 439cf05f..fa495de8 100644 --- a/src/logic/collectibles/sources/index.ts +++ b/src/logic/collectibles/sources/index.ts @@ -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 }), } diff --git a/src/logic/collectibles/store/actions/fetchCollectibles.ts b/src/logic/collectibles/store/actions/fetchCollectibles.ts index 6c70bba4..b1ff6726 100644 --- a/src/logic/collectibles/store/actions/fetchCollectibles.ts +++ b/src/logic/collectibles/store/actions/fetchCollectibles.ts @@ -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) } diff --git a/src/logic/safe/store/selectors/gatewayTransactions.ts b/src/logic/safe/store/selectors/gatewayTransactions.ts index d994f7ff..25b52cf0 100644 --- a/src/logic/safe/store/selectors/gatewayTransactions.ts +++ b/src/logic/safe/store/selectors/gatewayTransactions.ts @@ -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 => { diff --git a/src/logic/safe/store/selectors/utils.ts b/src/logic/safe/store/selectors/utils.ts index b636cb94..4fec53c4 100644 --- a/src/logic/safe/store/selectors/utils.ts +++ b/src/logic/safe/store/selectors/utils.ts @@ -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) diff --git a/src/logic/tokens/api/fetchErc20AndErc721AssetsList.ts b/src/logic/tokens/api/fetchErc20AndErc721AssetsList.ts index c2c72bf3..6321f706 100644 --- a/src/logic/tokens/api/fetchErc20AndErc721AssetsList.ts +++ b/src/logic/tokens/api/fetchErc20AndErc721AssetsList.ts @@ -11,10 +11,10 @@ export type TokenResult = { type: string } -export const fetchErc20AndErc721AssetsList = async (): Promise> => { +export const fetchErc20AndErc721AssetsList = (): Promise> => { const url = getTokensServiceBaseUrl() - return axios.get<{ results: TokenResult[] }>(`${url}/`, { + return axios.get<{ results: TokenResult[] }, AxiosResponse<{ results: TokenResult[] }>>(`${url}/`, { params: { limit: 3000, }, diff --git a/src/logic/tokens/api/fetchSafeCollectibles.ts b/src/logic/tokens/api/fetchSafeCollectibles.ts index 3b023b0f..18dad328 100644 --- a/src/logic/tokens/api/fetchSafeCollectibles.ts +++ b/src/logic/tokens/api/fetchSafeCollectibles.ts @@ -16,9 +16,11 @@ export type CollectibleResult = { uri: string | null } -export const fetchSafeCollectibles = async (safeAddress: string): Promise> => { +export const fetchSafeCollectibles = async ( + safeAddress: string, +): Promise> => { const address = checksumAddress(safeAddress) const url = `${getSafeServiceBaseUrl(address)}/collectibles/` - return axios.get(url) + return axios.get>(url) } diff --git a/src/logic/tokens/store/actions/fetchSafeTokens.ts b/src/logic/tokens/store/actions/fetchSafeTokens.ts index 826cc265..6957080d 100644 --- a/src/logic/tokens/store/actions/fetchSafeTokens.ts +++ b/src/logic/tokens/store/actions/fetchSafeTokens.ts @@ -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) } diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/HistoryTransactions.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/HistoryTransactions.tsx new file mode 100644 index 00000000..00be4d7b --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/HistoryTransactions.tsx @@ -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 ( + + + + ) + } + + return ( + + + + ) +} diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/HistoryTxList.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/HistoryTxList.tsx index 4b2ee88c..b57f7347 100644 --- a/src/routes/safe/components/Transactions/GatewayTransactions/HistoryTxList.tsx +++ b/src/routes/safe/components/Transactions/GatewayTransactions/HistoryTxList.tsx @@ -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 ( - - - - ) + const [, lastTransactionsGroup] = transactions[transactions.length - 1] + const lastTransaction = lastTransactionsGroup[lastTransactionsGroup.length - 1] + + if (!sameString(lastItemId, lastTransaction.id)) { + setLastItemId(lastTransaction.id) } return ( - - - {transactions?.map(([timestamp, txs]) => ( - - {formatWithSchema(Number(timestamp), 'MMM d, yyyy')} - - {txs.map((transaction) => ( - - ))} - - - ))} - - + {transactions?.map(([timestamp, txs]) => ( + + {formatWithSchema(Number(timestamp), 'MMM d, yyyy')} + + {txs.map((transaction) => ( + + ))} + + + ))} ) } diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/QueueTransactions.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/QueueTransactions.tsx index 87e0804a..f5e55f62 100644 --- a/src/routes/safe/components/Transactions/GatewayTransactions/QueueTransactions.tsx +++ b/src/routes/safe/components/Transactions/GatewayTransactions/QueueTransactions.tsx @@ -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 ( @@ -42,19 +42,17 @@ export const QueueTransactions = (): ReactElement => { return ( - - - {/* Next list */} - - {transactions.next.count !== 0 && } - + + {/* Next list */} + + {transactions.next.count !== 0 && } + - {/* Queue list */} - - {transactions.queue.count !== 0 && } - - - + {/* Queue list */} + + {transactions.queue.count !== 0 && } + + ) diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/QueueTxList.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/QueueTxList.tsx index 0565bdaa..8b683170 100644 --- a/src/routes/safe/components/Transactions/GatewayTransactions/QueueTxList.tsx +++ b/src/routes/safe/components/Transactions/GatewayTransactions/QueueTxList.tsx @@ -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

{firstElement ? : null}

@@ -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 ? ( @@ -70,7 +72,6 @@ const QueueTransaction = ({ nonce, transactions }: QueueTransactionProps): React ) : ( ) -} 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 ( {title} diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/TxCollapsed.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/TxCollapsed.tsx index 1c70df7a..4223be90 100644 --- a/src/routes/safe/components/Transactions/GatewayTransactions/TxCollapsed.tsx +++ b/src/routes/safe/components/Transactions/GatewayTransactions/TxCollapsed.tsx @@ -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 = ({ ) + // attaching ref to a div element as it was causing troubles to add a `ref` to a FunctionComponent const txCollapsedStatus = ( -
+
{transaction?.txStatus === 'PENDING' || transaction?.txStatus === 'PENDING_FAILED' ? ( diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/TxCollapsedActions.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/TxCollapsedActions.tsx index 0375101b..1e753ecd 100644 --- a/src/routes/safe/components/Transactions/GatewayTransactions/TxCollapsedActions.tsx +++ b/src/routes/safe/components/Transactions/GatewayTransactions/TxCollapsedActions.tsx @@ -32,22 +32,26 @@ export const TxCollapsedActions = ({ transaction }: TxCollapsedActionsProps): Re return ( <> - - - + + + + + {canCancel && ( - - - + + + + + )} diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/TxHistoryCollapsed.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/TxHistoryCollapsed.tsx index e7e35167..2187d90d 100644 --- a/src/routes/safe/components/Transactions/GatewayTransactions/TxHistoryCollapsed.tsx +++ b/src/routes/safe/components/Transactions/GatewayTransactions/TxHistoryCollapsed.tsx @@ -12,5 +12,14 @@ export const TxHistoryCollapsed = ({ transaction }: { transaction: Transaction } const info = useAssetInfo(transaction.txInfo) const status = useTransactionStatus(transaction) - return + return ( + + ) } diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/TxsInfiniteScroll.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/TxsInfiniteScroll.tsx new file mode 100644 index 00000000..c3d2d148 --- /dev/null +++ b/src/routes/safe/components/Transactions/GatewayTransactions/TxsInfiniteScroll.tsx @@ -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 + hasMore: boolean + isLoading: boolean +} + +export const TxsInfiniteScroll = ({ children, next, hasMore, isLoading }: TxsInfiniteScrollProps): ReactElement => { + return ( + + + {children} + + + + + + ) +} + +export { InfiniteScrollContext as TxsInfiniteScrollContext } from 'src/components/InfiniteScroll' diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/hooks/usePagedHistoryTransactions.ts b/src/routes/safe/components/Transactions/GatewayTransactions/hooks/usePagedHistoryTransactions.ts index fcad65d9..8fe7d570 100644 --- a/src/routes/safe/components/Transactions/GatewayTransactions/hooks/usePagedHistoryTransactions.ts +++ b/src/routes/safe/components/Transactions/GatewayTransactions/hooks/usePagedHistoryTransactions.ts @@ -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 + 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 } } diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/hooks/usePagedQueuedTransactions.ts b/src/routes/safe/components/Transactions/GatewayTransactions/hooks/usePagedQueuedTransactions.ts index 0dbe18c4..50db99dc 100644 --- a/src/routes/safe/components/Transactions/GatewayTransactions/hooks/usePagedQueuedTransactions.ts +++ b/src/routes/safe/components/Transactions/GatewayTransactions/hooks/usePagedQueuedTransactions.ts @@ -8,7 +8,7 @@ import { QueueTransactionsInfo, useQueueTransactions } from './useQueueTransacti type PagedQueuedTransactions = { count: number - loading: boolean + isLoading: boolean transactions?: QueueTransactionsInfo hasMore: boolean next: () => Promise @@ -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 } } diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/index.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/index.tsx index 910f5aaf..9336aa69 100644 --- a/src/routes/safe/components/Transactions/GatewayTransactions/index.tsx +++ b/src/routes/safe/components/Transactions/GatewayTransactions/index.tsx @@ -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 === 'queue' && } - {tab === 'history' && } + {tab === 'history' && } ) diff --git a/src/routes/safe/components/Transactions/GatewayTransactions/styled.tsx b/src/routes/safe/components/Transactions/GatewayTransactions/styled.tsx index 6add4c8d..10afad0f 100644 --- a/src/routes/safe/components/Transactions/GatewayTransactions/styled.tsx +++ b/src/routes/safe/components/Transactions/GatewayTransactions/styled.tsx @@ -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 { diff --git a/yarn.lock b/yarn.lock index c5906e4c..1962f9c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"