Skip to content

Commit 5921133

Browse files
Infinite Scroll for CLN Offers
1 parent a2a01cc commit 5921133

File tree

3 files changed

+97
-18
lines changed

3 files changed

+97
-18
lines changed

apps/frontend/src/components/cln/CLNOffersList/CLNOffersList.test.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { screen } from '@testing-library/react';
1+
import { screen, within } from '@testing-library/react';
22
import { mockAppStore, mockBKPRStoreData, mockCLNStoreData, mockListOffers, mockRootStoreData } from '../../../utilities/test-utilities/mockData';
33
import { renderWithProviders } from '../../../utilities/test-utilities/mockStore';
44
import CLNOffersList from './CLNOffersList';
@@ -45,9 +45,9 @@ describe('CLNOffersList component ', () => {
4545
it('if it has offers, show the offers list', async () => {
4646
await renderWithProviders(<CLNOffersList />, { preloadedState: mockAppStore, initialRoute: ['/cln'] });
4747
const offersList = screen.getByTestId('cln-offers-list');
48-
48+
const offerHeader = within(offersList).getAllByTestId('cln-offer-header');
4949
expect(offersList).toBeInTheDocument();
50-
expect(offersList.children.length).toBe(1);
50+
expect(offerHeader.length).toBe(1);
5151
});
5252

5353
it('if there are no offers, show the on offers text', async () => {

apps/frontend/src/components/cln/CLNOffersList/CLNOffersList.tsx

Lines changed: 93 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import './CLNOffersList.scss';
2-
import { useState } from 'react';
2+
import { useCallback, useEffect, useRef, useState } from 'react';
33
import { motion, AnimatePresence } from 'framer-motion';
44
import { Row, Col, Spinner, Alert } from 'react-bootstrap';
5+
import PerfectScrollbar from 'react-perfect-scrollbar';
56

67
import { IncomingArrowSVG } from '../../../svgs/IncomingArrow';
78
import Offer from '../CLNOffer/CLNOffer';
8-
import { TRANSITION_DURATION } from '../../../utilities/constants';
9+
import { SCROLL_BATCH_SIZE, SCROLL_THRESHOLD, TRANSITION_DURATION } from '../../../utilities/constants';
910
import { NoCLNTransactionLightSVG } from '../../../svgs/NoCLNTransactionLight';
1011
import { NoCLNTransactionDarkSVG } from '../../../svgs/NoCLNTransactionDark';
1112
import { useSelector } from 'react-redux';
@@ -14,7 +15,7 @@ import { selectIsAuthenticated, selectIsDarkMode } from '../../../store/rootSele
1415

1516
const OfferHeader = ({ offer }) => {
1617
return (
17-
<Row data-testid='cln-offer-header' className="offer-list-item d-flex justify-content-between align-items-center">
18+
<Row className="offer-list-item d-flex justify-content-between align-items-center">
1819
<Col xs={2}>
1920
<IncomingArrowSVG className="me-1" txStatus={offer.used ? 'used' : 'unused'} />
2021
</Col>
@@ -57,6 +58,7 @@ const CLNOffersAccordion = ({
5758
return (
5859
<>
5960
<motion.div
61+
data-testid='cln-offer-header'
6062
className={'cln-offer-header ' + (expanded[i] ? 'expanded' : '')}
6163
initial={false}
6264
animate={{ backgroundColor: (isDarkMode ? (expanded[i] ? '#0C0C0F' : '#2A2A2C') : (expanded[i] ? '#EBEFF9' : '#FFFFFF')) }}
@@ -72,11 +74,11 @@ const CLNOffersAccordion = ({
7274
{expanded[i] && (
7375
<motion.div
7476
data-testid='cln-offer-details'
75-
className="cln-offer-details"
76-
key="content"
77-
initial="collapsed"
78-
animate="open"
79-
exit="collapsed"
77+
className='cln-offer-details'
78+
key='content'
79+
initial='collapsed'
80+
animate='open'
81+
exit='collapsed'
8082
variants={{
8183
open: { opacity: 1, height: 'auto' },
8284
collapsed: { opacity: 0, height: 0 },
@@ -98,6 +100,68 @@ export const CLNOffersList = () => {
98100
const initExpansions = (listOffers.offers?.reduce((acc: boolean[]) => [...acc, false], []) || []);
99101
const [expanded, setExpanded] = useState<boolean[]>(initExpansions);
100102

103+
const [displayedOffers, setDisplayedOffers] = useState<any[]>([]);
104+
const [currentIndex, setCurrentIndex] = useState(0);
105+
const [isLoading, setIsLoading] = useState(false);
106+
const [allOffersLoaded, setAllOffersLoaded] = useState(false);
107+
const containerRef = useRef<HTMLDivElement>(null);
108+
109+
const setContainerRef = useCallback((ref: HTMLElement | null) => {
110+
if (ref) {
111+
(containerRef as React.MutableRefObject<HTMLElement | null>).current = ref;
112+
}
113+
}, []);
114+
115+
useEffect(() => {
116+
if (listOffers && listOffers.offers && listOffers.offers.length > 0) {
117+
const initialBatch = listOffers.offers.slice(0, SCROLL_BATCH_SIZE);
118+
setDisplayedOffers(initialBatch);
119+
setCurrentIndex(SCROLL_BATCH_SIZE);
120+
if (SCROLL_BATCH_SIZE >= listOffers?.offers?.length) {
121+
setAllOffersLoaded(true);
122+
}
123+
}
124+
}, [listOffers]);
125+
126+
const loadMoreTransactions = useCallback(() => {
127+
if (isLoading || allOffersLoaded) return;
128+
setIsLoading(true);
129+
setTimeout(() => {
130+
const nextIndex = currentIndex + SCROLL_BATCH_SIZE;
131+
const newOffers = listOffers?.offers?.slice(
132+
currentIndex,
133+
nextIndex
134+
) || [];
135+
setDisplayedOffers(prev => [...prev, ...newOffers]);
136+
setCurrentIndex(nextIndex);
137+
138+
if (listOffers && listOffers.offers && nextIndex >= listOffers?.offers?.length) {
139+
setAllOffersLoaded(true);
140+
}
141+
142+
setIsLoading(false);
143+
}, 300);
144+
}, [currentIndex, isLoading, allOffersLoaded, listOffers]);
145+
146+
const handleScroll = useCallback((container) => {
147+
if (!container || isLoading || allOffersLoaded) return;
148+
149+
const { scrollTop, scrollHeight, clientHeight } = container;
150+
const bottomOffset = scrollHeight - scrollTop - clientHeight;
151+
152+
if (bottomOffset < SCROLL_THRESHOLD) {
153+
loadMoreTransactions();
154+
}
155+
}, [isLoading, allOffersLoaded, loadMoreTransactions]);
156+
157+
useEffect(() => {
158+
const container = containerRef.current;
159+
if (container) {
160+
container?.addEventListener('scroll', handleScroll);
161+
return () => container?.removeEventListener('scroll', handleScroll);
162+
}
163+
}, [handleScroll]);
164+
101165
return (
102166
isAuthenticated && listOffers.isLoading ?
103167
<span className='h-100 d-flex justify-content-center align-items-center'>
@@ -107,13 +171,28 @@ export const CLNOffersList = () => {
107171
listOffers.error ?
108172
<Alert className='py-0 px-1 fs-7' variant='danger' data-testid='cln-offers-list-error'>{listOffers.error}</Alert> :
109173
listOffers?.offers && listOffers?.offers.length && listOffers?.offers.length > 0 ?
110-
<div className='cln-offers-list' data-testid='cln-offers-list'>
111-
{
112-
listOffers?.offers?.map((offer, i) => (
113-
<CLNOffersAccordion key={i} i={i} expanded={expanded} setExpanded={setExpanded} initExpansions={initExpansions} offer={offer} />
114-
))
174+
<PerfectScrollbar
175+
containerRef={setContainerRef}
176+
onScrollY={handleScroll}
177+
className='cln-offers-list'
178+
data-testid='cln-offers-list'
179+
options={{
180+
suppressScrollX: true,
181+
wheelPropagation: false
182+
}}
183+
>
184+
{displayedOffers.map((offer, i) => (
185+
<CLNOffersAccordion key={i} i={i} expanded={expanded} setExpanded={setExpanded} initExpansions={initExpansions} offer={offer} />
186+
))}
187+
{isLoading && (
188+
<Col xs={12} className='d-flex align-items-center justify-content-center mb-5'>
189+
<Spinner animation='grow' variant='primary' />
190+
</Col>
191+
)}
192+
{allOffersLoaded && listOffers?.offers.length > 100 &&
193+
<h6 className='d-flex align-self-center py-4 text-muted'>No more offers to load!</h6>
115194
}
116-
</div>
195+
</PerfectScrollbar>
117196
:
118197
<Row className='text-light fs-6 h-75 mt-5 align-items-center justify-content-center'>
119198
<Row className='d-flex align-items-center justify-content-center mt-2'>

apps/frontend/src/components/cln/CLNWallet/CLNWallet.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ const CLNWallet = (props) => {
8787
<CLNTransactionsList />
8888
</Suspense>
8989
</Tab.Pane>
90-
<Tab.Pane eventKey="offers">
90+
<Tab.Pane className="h-100 list-scroll-container" eventKey="offers">
9191
<Suspense fallback={<Loading />}>
9292
<CLNOffersList />
9393
</Suspense>

0 commit comments

Comments
 (0)