Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TransactionList infinite scroll #11355

Draft
wants to merge 5 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import { TransactionList } from 'src/views/wallet/transactions/TransactionList/TransactionList';
import { useSelector } from 'src/hooks/suite';
import {
selectAccountStakeTypeTransactions,
selectIsLoadingTransactions,
} from '@suite-common/wallet-core';
import { selectAccountTransactions, selectIsLoadingTransactions } from '@suite-common/wallet-core';
import { isStakeTypeTx } from '@suite-common/suite-utils';

export const Transactions = () => {
const transactionsIsLoading = useSelector(selectIsLoadingTransactions);
const selectedAccount = useSelector(state => state.wallet.selectedAccount);
const stakeTxs = useSelector(state =>
selectAccountStakeTypeTransactions(state, selectedAccount.account?.key || ''),
const accountTxs = useSelector(state =>
selectAccountTransactions(state, selectedAccount.account?.key || ''),
);

if (selectedAccount.status !== 'loaded' || stakeTxs.length < 1) {
if (selectedAccount.status !== 'loaded' || accountTxs.length < 1) {
return null;
}

Expand All @@ -21,10 +19,9 @@ export const Transactions = () => {
return (
<TransactionList
account={account}
transactions={stakeTxs}
symbol={account.symbol}
transactions={accountTxs}
transactionFilter={tx => isStakeTypeTx(tx.ethereumSpecific?.parsedData?.methodId)}
isLoading={transactionsIsLoading}
customTotalItems={stakeTxs.length}
isExportable={false}
/>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { useMemo, useState, useEffect, useRef } from 'react';
import { useMemo, useState, useEffect } from 'react';
import styled from 'styled-components';
import useDebounce from 'react-use/lib/useDebounce';

import { fetchTransactionsThunk } from '@suite-common/wallet-core';
import {
groupTransactionsByDate,
advancedSearchTransactions,
Expand All @@ -12,120 +11,118 @@ import {
import { CoinjoinBatchItem } from 'src/components/wallet/TransactionItem/CoinjoinBatchItem';
import { Translation } from 'src/components/suite';
import { DashboardSection } from 'src/components/dashboard';
import { useDispatch, useSelector } from 'src/hooks/suite';
import { useSelector } from 'src/hooks/suite';
import { WalletAccountTransaction, Account } from 'src/types/wallet';
import { TransactionListActions } from './TransactionListActions/TransactionListActions';
import { SearchAction } from './TransactionListActions/SearchAction';
import { ExportAction } from './TransactionListActions/ExportAction';
import { TransactionItem } from 'src/components/wallet/TransactionItem/TransactionItem';
import { Pagination } from 'src/components/wallet';
import { TransactionsGroup } from './TransactionsGroup/TransactionsGroup';
import { SkeletonTransactionItem } from './SkeletonTransactionItem';
import { NoSearchResults } from './NoSearchResults';
import { findAnchorTransactionPage } from 'src/utils/suite/anchor';
import { TransactionCandidates } from './TransactionCandidates';
import { selectLabelingDataForAccount } from 'src/reducers/suite/metadataReducer';
import { getTxsPerPage } from '@suite-common/suite-utils';
import { SkeletonStack } from '@trezor/components';
import { selectLocalCurrency } from 'src/reducers/wallet/settingsReducer';
import { useFetchTransactions } from './useFetchTransactions';

const StyledSection = styled(DashboardSection)`
margin-bottom: 20px;
`;

const PaginationWrapper = styled.div`
margin-top: 20px;
const ActionsWrapper = styled.div`
display: flex;
align-items: center;
`;

const TEMPINFOPANEL = ({
info,
searched,
filtered,
}: {
info: ReturnType<typeof useFetchTransactions>['TEMPINFO'];
searched: WalletAccountTransaction[];
filtered: WalletAccountTransaction[];
}) => (
<div style={{ alignSelf: 'center', marginTop: '16px' }}>
FETCHED {info.pagesFetched}/{info.pagesTotal} PAGES ({info.txFetched}/{info.txTotal} TXS)
<br />
{info.txFetched} -&gt; FILTER -&gt; {filtered.length} -&gt; SEARCH -&gt; {searched.length}
</div>
);

const TEMPBUTTON = ({
fetchedAll,
fetchNext,
}: {
fetchedAll: boolean;
fetchNext: () => unknown;
}) => (
<button
disabled={fetchedAll}
onClick={fetchNext}
style={{ alignSelf: 'center', marginTop: '16px', padding: '12px' }}
>
LOAD MORE
</button>
);

interface TransactionListProps {
account: Account;
transactions: WalletAccountTransaction[];
symbol: WalletAccountTransaction['symbol'];
transactionFilter?: (tx: WalletAccountTransaction) => boolean;
isLoading?: boolean;
account: Account;
customTotalItems?: number;
isExportable?: boolean;
}

export const TransactionList = ({
account,
transactions,
transactionFilter,
isLoading,
account,
symbol,
customTotalItems,
isExportable = true,
}: TransactionListProps) => {
const localCurrency = useSelector(selectLocalCurrency);
const anchor = useSelector(state => state.router.anchor);
const dispatch = useDispatch();
const accountMetadata = useSelector(state => selectLabelingDataForAccount(state, account.key));
const network = getAccountNetwork(account);

// Filter
const filteredTransactions = useMemo(
() => (transactionFilter ? transactions.filter(transactionFilter) : transactions),
[transactions, transactionFilter],
);

// Search
const [searchQuery, setSearchQuery] = useState('');
const [searchedTransactions, setSearchedTransactions] = useState(transactions);
const [hasFetchedAll, setHasFetchedAll] = useState(false);

const sectionRef = useRef<HTMLDivElement>(null);
const [searchedTransactions, setSearchedTransactions] = useState(filteredTransactions);

useDebounce(
() => {
const results = advancedSearchTransactions(transactions, accountMetadata, searchQuery);
const results = advancedSearchTransactions(
filteredTransactions,
accountMetadata,
searchQuery,
);
setSearchedTransactions(results);
},
200,
[transactions, account.metadata, searchQuery, accountMetadata],
[filteredTransactions, account.metadata, searchQuery, accountMetadata],
);

useEffect(() => {
if (anchor && !hasFetchedAll) {
dispatch(
fetchTransactionsThunk({
accountKey: account.key,
page: 2,
perPage: getTxsPerPage(account.networkType),
noLoading: true,
recursive: true,
}),
);
setHasFetchedAll(true);
}
}, [anchor, account, dispatch, hasFetchedAll]);

// Pagination
const perPage = getTxsPerPage(account.networkType);
const startPage = findAnchorTransactionPage(transactions, perPage, anchor);
const [currentPage, setSelectedPage] = useState(startPage);
const { fetchNext, fetchAll, fetchedAll, TEMPINFO } = useFetchTransactions(
account,
transactions,
);

useEffect(() => {
// reset page on account change
setSelectedPage(startPage);
}, [account.descriptor, account.symbol, startPage]);
if (anchor) fetchAll();
}, [anchor, fetchAll]);

const isSearching = searchQuery.trim() !== '';
const defaultTotalItems = customTotalItems ?? account.history.total;
const totalItems = isSearching ? searchedTransactions.length : defaultTotalItems;

const onPageSelected = (page: number) => {
setSelectedPage(page);

if (!isSearching) {
dispatch(fetchTransactionsThunk({ accountKey: account.key, page, perPage }));
}

if (sectionRef.current) {
sectionRef.current.scrollIntoView();
}
};

const startIndex = (currentPage - 1) * perPage;
const stopIndex = startIndex + perPage;

const slicedTransactions = useMemo(
() => searchedTransactions.slice(startIndex, stopIndex),
[searchedTransactions, startIndex, stopIndex],
);

const transactionsByDate = useMemo(
() => groupTransactionsByDate(slicedTransactions),
[slicedTransactions],
() => groupTransactionsByDate(searchedTransactions),
[searchedTransactions],
);

const listItems = useMemo(
Expand All @@ -137,7 +134,7 @@ export const TransactionList = ({
<TransactionsGroup
key={dateKey}
dateKey={dateKey}
symbol={symbol}
symbol={account.symbol}
transactions={value}
localCurrency={localCurrency}
index={groupIndex}
Expand Down Expand Up @@ -165,30 +162,32 @@ export const TransactionList = ({
</TransactionsGroup>
);
}),
[transactionsByDate, account.key, localCurrency, symbol, network, accountMetadata],
[transactionsByDate, account.key, localCurrency, account.symbol, network, accountMetadata],
);

// if total pages cannot be determined check current page and number of txs (XRP)
// Edge case: if there is exactly 25 Ripple txs, pagination will be displayed
const isRipple = account.networkType === 'ripple';
const isLastRipplePage = isRipple && slicedTransactions.length < perPage;
const showRipplePagination = !(isLastRipplePage && currentPage === 1);
const showPagination = isRipple ? showRipplePagination : totalItems > perPage;
const areTransactionsAvailable = transactions.length > 0 && searchedTransactions.length === 0;
const areTransactionsAvailable =
filteredTransactions.length > 0 && searchedTransactions.length === 0;

return (
<StyledSection
ref={sectionRef}
heading={<Translation id="TR_ALL_TRANSACTIONS" />}
actions={
<TransactionListActions
account={account}
searchQuery={searchQuery}
setSearch={setSearchQuery}
setSelectedPage={setSelectedPage}
accountMetadata={accountMetadata}
isExportable={isExportable}
/>
<ActionsWrapper>
<SearchAction
account={account}
searchQuery={searchQuery}
setSearch={setSearchQuery}
fetchAll={fetchAll}
/>
{isExportable && (
<ExportAction
account={account}
searchQuery={searchQuery}
accountMetadata={accountMetadata}
fetchAll={fetchAll}
/>
)}
</ActionsWrapper>
}
data-test="@wallet/accounts/transaction-list"
>
Expand All @@ -207,18 +206,12 @@ export const TransactionList = ({
<>{areTransactionsAvailable ? <NoSearchResults /> : listItems}</>
)}

{showPagination && (
<PaginationWrapper>
<Pagination
hasPages={!isRipple}
currentPage={currentPage}
isLastPage={isLastRipplePage}
perPage={perPage}
totalItems={totalItems}
onPageSelected={onPageSelected}
/>
</PaginationWrapper>
)}
<TEMPINFOPANEL
info={TEMPINFO}
searched={searchedTransactions}
filtered={filteredTransactions}
/>
<TEMPBUTTON fetchNext={fetchNext} fetchedAll={fetchedAll} />
</StyledSection>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import { useDispatch } from 'src/hooks/suite';
import { useTranslation } from 'src/hooks/suite/useTranslation';
import { useSelector } from 'src/hooks/suite/useSelector';
import { notificationsActions } from '@suite-common/toast-notifications';
import { exportTransactionsThunk, fetchTransactionsThunk } from '@suite-common/wallet-core';
import { exportTransactionsThunk } from '@suite-common/wallet-core';
import { ExportFileType } from '@suite-common/wallet-types';
import { Account } from 'src/types/wallet';
import { isFeatureFlagEnabled, getTxsPerPage } from '@suite-common/suite-utils';
import { isFeatureFlagEnabled } from '@suite-common/suite-utils';
import { getTitleForNetwork, getTitleForCoinjoinAccount } from '@suite-common/wallet-utils';
import { selectLabelingDataForSelectedAccount } from 'src/reducers/suite/metadataReducer';
import { AccountLabels } from '@suite-common/metadata-types';
Expand All @@ -18,9 +18,15 @@ export interface ExportActionProps {
account: Account;
searchQuery: string;
accountMetadata: AccountLabels;
fetchAll: () => Promise<void>;
}

export const ExportAction = ({ account, searchQuery, accountMetadata }: ExportActionProps) => {
export const ExportAction = ({
account,
searchQuery,
accountMetadata,
fetchAll,
}: ExportActionProps) => {
const [isExportRunning, setIsExportRunning] = useState(false);
const dispatch = useDispatch();
const { translationString } = useTranslation();
Expand Down Expand Up @@ -54,15 +60,7 @@ export const ExportAction = ({ account, searchQuery, accountMetadata }: ExportAc

setIsExportRunning(true);
try {
await dispatch(
fetchTransactionsThunk({
accountKey: account.key,
page: 2,
perPage: getTxsPerPage(account.networkType),
noLoading: true,
recursive: true,
}),
);
await fetchAll();
const accountName = accountLabel || getAccountTitle();
await dispatch(
exportTransactionsThunk({
Expand All @@ -88,6 +86,7 @@ export const ExportAction = ({ account, searchQuery, accountMetadata }: ExportAc
[
isExportRunning,
account,
fetchAll,
dispatch,
translationString,
getAccountTitle,
Expand Down
Loading
Loading