Skip to content

Commit d65b7b9

Browse files
MartinCupelapetyosiarnautov-anton
authored
fix: unify paginator interface (#1803)
* feat: add deprecation warning logger utility function * feat: unify paginator interface * feat: add React SDK scoped classes to ChannelListMessenger elements * feat: add popper tooltip to SimpleReactionList items (#1801) * fix: log deprecation warnings only on mount * refactor: do not use LoadingIndicator prop in InfiniteScroll * style: do not state the exact version for removal of deprecated props * docs: describe how to use InfiniteScroll with ChannelList Co-authored-by: Petyo Ivanov <[email protected]> Co-authored-by: Anton Arnautov <[email protected]>
1 parent 518fd54 commit d65b7b9

File tree

14 files changed

+300
-56
lines changed

14 files changed

+300
-56
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
---
2+
id: channel-list-infinite-scroll
3+
sidebar_position: 24
4+
title: Channel List Infinite Scroll
5+
keywords: [channel list, infinite scroll]
6+
---
7+
8+
import GHComponentLink from '../_docusaurus-components/GHComponentLink';
9+
10+
This example demonstrates how to implement infinite scroll with existing SDK components. By default, the SDK's `ChannelList` component uses `LoadMorePaginator` to load more channels into the list. More channels are loaded every time the `LoadMoreButton` is clicked. The infinite scroll instead loads more channels based on the channel list container's scroll position. The request to load more channels is automatically triggered, once the scroller approaches the bottom scroll threshold of the container.
11+
12+
## How to plug in the infinite scroll
13+
14+
The SDK provides own [`InfiniteScroll`](https://github.com/GetStream/stream-chat-react/blob/master/src/components/InfiniteScrollPaginator/InfiniteScroll.tsx) component. This component implements the [`PaginatorProps`](https://github.com/GetStream/stream-chat-react/blob/master/src/types/types.ts) interface. As this interface is implemented by the [`LoadMorePaginator`](https://github.com/GetStream/stream-chat-react/blob/master/src/components/LoadMore/LoadMorePaginator.ts) too, we can just pass the `InfiniteScroll` into the `ChannelList` prop `Paginator`.
15+
16+
```tsx
17+
import {
18+
ChannelList,
19+
InfiniteScroll,
20+
} from 'stream-chat-react';
21+
22+
<ChannelList filters={filters} sort={sort} options={options}
23+
Paginator={InfiniteScroll}
24+
showChannelSearch
25+
/>
26+
```
27+
28+
If you would like to adjust the configuration parameters like `threshold`, `reverse` (`PaginatorProps`) or `useCapture`, etc. (`InfiniteScrollProps`), you can create a wrapper component where these props can be set:
29+
30+
```tsx
31+
import {
32+
ChannelList,
33+
InfiniteScroll,
34+
InfiniteScrollProps
35+
} from 'stream-chat-react';
36+
37+
38+
const Paginator = (props: InfiniteScrollProps) => <InfiniteScroll {...props} threshold={50} />;
39+
40+
...
41+
<ChannelList filters={filters} sort={sort} options={options}
42+
Paginator={Paginator}
43+
showChannelSearch
44+
/>
45+
```
46+
47+
Especially the `threshold` prop may need to be set as the default is 250px. That may be too soon to load more channels.
48+
49+
## What to take into consideration
50+
51+
For the infinite scroll to work, the element containing the `ChannelPreview` list has to be forced to display the scroll bar with the initial channel list load. This is achieved by:
52+
53+
**1. adjusting the initial number of loaded channels**
54+
55+
Set a reasonable number of channels to be initially loaded. If loading 10 channels leads to them being visible without having to scroll, then increase the number to e.g. 15:
56+
57+
```tsx
58+
import type { ChannelOptions } from 'stream-chat';
59+
const options: ChannelOptions = { state: true, presence: true, limit: 15 };
60+
```
61+
62+
**2. adjusting the container height**
63+
64+
You can change the container height so that not all channels are visible at once. You should target the container with class `.str-chat__channel-list-messenger-react__main`
65+
66+
```css
67+
.str-chat__channel-list-messenger-react__main {
68+
max-height: 50%;
69+
}
70+
```
71+
72+

src/components/ChannelList/ChannelList.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,8 @@ const UnMemoizedChannelList = <
317317
const className = clsx(
318318
customClasses?.chat ?? 'str-chat',
319319
theme,
320-
customClasses?.channelList ?? 'str-chat-channel-list str-chat__channel-list',
320+
customClasses?.channelList ??
321+
'str-chat-channel-list str-chat__channel-list str-chat__channel-list-react',
321322
{
322323
'str-chat--windows-flags': useImageFlagEmojisOnWindows && navigator.userAgent.match(/Win/),
323324
'str-chat-channel-list--open': navOpen,
@@ -350,8 +351,8 @@ const UnMemoizedChannelList = <
350351
) : (
351352
<Paginator
352353
hasNextPage={hasNextPage}
354+
isLoading={channelsQueryState.queryInProgress === 'load-more'}
353355
loadNextPage={loadNextPage}
354-
refreshing={channelsQueryState.queryInProgress === 'load-more'}
355356
>
356357
{renderChannels
357358
? renderChannels(loadedChannels, renderChannel)

src/components/ChannelList/ChannelListMessenger.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,10 @@ export const ChannelListMessenger = <
4949
}
5050

5151
return (
52-
<div className='str-chat__channel-list-messenger'>
52+
<div className='str-chat__channel-list-messenger str-chat__channel-list-messenger-react'>
5353
<div
5454
aria-label='Channel list'
55-
className='str-chat__channel-list-messenger__main'
55+
className='str-chat__channel-list-messenger__main str-chat__channel-list-messenger-react__main'
5656
role='listbox'
5757
>
5858
{children}

src/components/ChannelList/__tests__/__snapshots__/ChannelListMessenger.test.js.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
exports[`ChannelListMessenger by default, children should be rendered 1`] = `
44
<div
5-
className="str-chat__channel-list-messenger"
5+
className="str-chat__channel-list-messenger str-chat__channel-list-messenger-react"
66
>
77
<div
88
aria-label="Channel list"
9-
className="str-chat__channel-list-messenger__main"
9+
className="str-chat__channel-list-messenger__main str-chat__channel-list-messenger-react__main"
1010
role="listbox"
1111
>
1212
<div>

src/components/InfiniteScrollPaginator/InfiniteScroll.tsx

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import React, { PropsWithChildren, useCallback, useEffect, useRef } from 'react';
2+
import type { PaginatorProps } from '../../types/types';
3+
import { deprecationAndReplacementWarning } from '../../utils/deprecationWarning';
24

35
/**
46
* Prevents Chrome hangups
@@ -10,43 +12,66 @@ const mousewheelListener = (event: Event) => {
1012
}
1113
};
1214

13-
export type InfiniteScrollProps = {
15+
export type InfiniteScrollProps = PaginatorProps & {
1416
className?: string;
1517
element?: React.ElementType;
18+
/**
19+
* @desc Flag signalling whether more pages with older items can be loaded
20+
* @deprecated Use hasPreviousPage prop instead. Planned for removal: https://github.com/GetStream/stream-chat-react/issues/1804
21+
*/
1622
hasMore?: boolean;
23+
/**
24+
* @desc Flag signalling whether more pages with newer items can be loaded
25+
* @deprecated Use hasNextPage prop instead. Planned for removal: https://github.com/GetStream/stream-chat-react/issues/1804
26+
*/
1727
hasMoreNewer?: boolean;
18-
/** Element to be rendered at the top of the thread message list. By default Message and ThreadStart components */
28+
/** Element to be rendered at the top of the thread message list. By default, Message and ThreadStart components */
1929
head?: React.ReactNode;
2030
initialLoad?: boolean;
2131
isLoading?: boolean;
2232
listenToScroll?: (offset: number, reverseOffset: number, threshold: number) => void;
2333
loader?: React.ReactNode;
24-
loading?: React.ReactNode;
34+
/**
35+
* @desc Function that loads previous page with older items
36+
* @deprecated Use loadPreviousPage prop instead. Planned for removal: https://github.com/GetStream/stream-chat-react/issues/1804
37+
*/
2538
loadMore?: () => void;
39+
/**
40+
* @desc Function that loads next page with newer items
41+
* @deprecated Use loadNextPage prop instead. Planned for removal: https://github.com/GetStream/stream-chat-react/issues/1804
42+
*/
2643
loadMoreNewer?: () => void;
2744
pageStart?: number;
28-
threshold?: number;
2945
useCapture?: boolean;
3046
};
3147

3248
export const InfiniteScroll = (props: PropsWithChildren<InfiniteScrollProps>) => {
3349
const {
3450
children,
3551
element = 'div',
36-
hasMore = false,
37-
hasMoreNewer = false,
52+
hasMore,
53+
hasMoreNewer,
54+
hasNextPage,
55+
hasPreviousPage,
3856
head,
3957
initialLoad = true,
40-
isLoading = false,
58+
isLoading,
4159
listenToScroll,
4260
loader,
4361
loadMore,
4462
loadMoreNewer,
63+
loadNextPage,
64+
loadPreviousPage,
4565
threshold = 250,
4666
useCapture = false,
4767
...elementProps
4868
} = props;
4969

70+
const loadNextPageFn = loadNextPage || loadMoreNewer;
71+
const loadPreviousPageFn = loadPreviousPage || loadMore;
72+
const hasNextPageFlag = hasNextPage || hasMoreNewer;
73+
const hasPreviousPageFlag = hasPreviousPage || hasMore;
74+
5075
const scrollComponent = useRef<HTMLElement>();
5176

5277
const scrollListener = useCallback(() => {
@@ -66,14 +91,37 @@ export const InfiniteScroll = (props: PropsWithChildren<InfiniteScrollProps>) =>
6691
listenToScroll(offset, reverseOffset, threshold);
6792
}
6893

69-
if (reverseOffset < Number(threshold) && typeof loadMore === 'function' && hasMore) {
70-
loadMore();
94+
if (
95+
reverseOffset < Number(threshold) &&
96+
typeof loadPreviousPageFn === 'function' &&
97+
hasPreviousPageFlag
98+
) {
99+
loadPreviousPageFn();
71100
}
72101

73-
if (offset < Number(threshold) && typeof loadMoreNewer === 'function' && hasMoreNewer) {
74-
loadMoreNewer();
102+
if (offset < Number(threshold) && typeof loadNextPageFn === 'function' && hasNextPageFlag) {
103+
loadNextPageFn();
75104
}
76-
}, [hasMore, hasMoreNewer, threshold, listenToScroll, loadMore, loadMoreNewer]);
105+
}, [
106+
hasPreviousPageFlag,
107+
hasNextPageFlag,
108+
threshold,
109+
listenToScroll,
110+
loadPreviousPageFn,
111+
loadNextPageFn,
112+
]);
113+
114+
useEffect(() => {
115+
deprecationAndReplacementWarning(
116+
[
117+
[{ hasMoreNewer }, { hasNextPage }],
118+
[{ loadMoreNewer }, { loadNextPage }],
119+
[{ hasMore }, { hasPreviousPage }],
120+
[{ loadMore }, { loadPreviousPage }],
121+
],
122+
'InfiniteScroll',
123+
);
124+
}, []);
77125

78126
useEffect(() => {
79127
const scrollElement = scrollComponent.current?.parentNode;

src/components/InfiniteScrollPaginator/__tests__/InfiniteScroll.test.js

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import React from 'react';
2-
import { render } from '@testing-library/react';
2+
import { fireEvent, render } from '@testing-library/react';
33
import '@testing-library/jest-dom';
44
import renderer from 'react-test-renderer';
55

66
import { InfiniteScroll } from '../';
77

8-
const loadMore = jest.fn().mockImplementation(() => Promise.resolve());
8+
const loadPreviousPage = jest.fn().mockImplementation(() => Promise.resolve());
99

1010
// Note: testing actual infinite scroll behavior is very tricky / pointless because Jest does not
1111
// really implement offsetHeight / offsetTop / offsetParent etc. This means we'd have to mock basically everything,
@@ -24,7 +24,7 @@ describe('InfiniteScroll', () => {
2424
const renderComponent = (props) => {
2525
const renderResult = render(
2626
<div data-testid='scroll-parent'>
27-
<InfiniteScroll loadMore={loadMore} {...props} />
27+
<InfiniteScroll loadPreviousPage={loadPreviousPage} {...props} />
2828
</div>,
2929
);
3030
const scrollParent = renderResult.getByTestId('scroll-parent');
@@ -35,7 +35,7 @@ describe('InfiniteScroll', () => {
3535
'should bind scroll, mousewheel and resize events to the right target with useCapture as %s',
3636
(useCapture) => {
3737
renderComponent({
38-
hasMore: true,
38+
hasPreviousPage: true,
3939
useCapture,
4040
});
4141

@@ -53,7 +53,7 @@ describe('InfiniteScroll', () => {
5353
'should unbind scroll, mousewheel and resize events from the right target with useCapture as %s',
5454
(useCapture) => {
5555
const { unmount } = renderComponent({
56-
hasMore: true,
56+
hasPreviousPage: true,
5757
useCapture,
5858
});
5959

@@ -77,11 +77,48 @@ describe('InfiniteScroll', () => {
7777
},
7878
);
7979

80+
it.each([
81+
['hasMoreNewer', 'loadMoreNewer', 'hasNextPage', 'loadNextPage'],
82+
['hasMore', 'loadMore', 'hasPreviousPage', 'loadPreviousPage'],
83+
])(
84+
'deprecates %s and %s in favor of %s and %s',
85+
(deprecatedFlag, deprecatedLoader, newFlag, newLoader) => {
86+
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => null);
87+
const oldLoaderSpy = jest.fn();
88+
const newLoaderSpy = jest.fn();
89+
90+
const { scrollParent } = renderComponent({
91+
[deprecatedFlag]: false,
92+
[deprecatedLoader]: oldLoaderSpy,
93+
[newFlag]: true,
94+
[newLoader]: newLoaderSpy,
95+
threshold: Infinity,
96+
});
97+
98+
Object.defineProperty(HTMLElement.prototype, 'offsetParent', {
99+
get() {
100+
return this.parentNode;
101+
},
102+
});
103+
fireEvent.scroll(scrollParent);
104+
105+
consoleWarnSpy.mockRestore();
106+
107+
expect(oldLoaderSpy).not.toHaveBeenCalled();
108+
// eslint-disable-next-line jest/prefer-called-with
109+
expect(newLoaderSpy).toHaveBeenCalled();
110+
},
111+
);
112+
80113
describe('Rendering loader', () => {
81114
const getRenderResult = () =>
82115
renderer
83116
.create(
84-
<InfiniteScroll isLoading loader={<div key='loader'>loader</div>} loadMore={loadMore}>
117+
<InfiniteScroll
118+
isLoading
119+
loader={<div key='loader'>loader</div>}
120+
loadPreviousPage={loadPreviousPage}
121+
>
85122
Content
86123
</InfiniteScroll>,
87124
)

src/components/LoadMore/LoadMoreButton.tsx

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,41 @@
1-
import React, { PropsWithChildren } from 'react';
1+
import React, { PropsWithChildren, useEffect } from 'react';
22
import { LoadingIndicator } from '../Loading';
3+
import { deprecationAndReplacementWarning } from '../../utils/deprecationWarning';
34

45
export type LoadMoreButtonProps = {
56
/** onClick handler load more button. Pagination logic should be executed in this handler. */
67
onClick: React.MouseEventHandler<HTMLButtonElement>;
7-
/** If true, LoadingIndicator is displayed instead of button */
8+
/** indicates whether a loading request is in progress */
9+
isLoading?: boolean;
10+
/**
11+
* @desc If true, LoadingIndicator is displayed instead of button
12+
* @deprecated Use loading prop instead of refreshing. Planned for removal: https://github.com/GetStream/stream-chat-react/issues/1804
13+
*/
814
refreshing?: boolean;
915
};
1016

11-
const UnMemoizedLoadMoreButton = (props: PropsWithChildren<LoadMoreButtonProps>) => {
12-
const { children = 'Load more', onClick, refreshing } = props;
17+
const UnMemoizedLoadMoreButton = ({
18+
children = 'Load more',
19+
isLoading,
20+
onClick,
21+
refreshing,
22+
}: PropsWithChildren<LoadMoreButtonProps>) => {
23+
const loading = typeof isLoading !== 'undefined' ? isLoading : refreshing;
24+
25+
useEffect(() => {
26+
deprecationAndReplacementWarning([[{ refreshing }, { isLoading }]], 'LoadMoreButton');
27+
}, []);
1328

1429
return (
1530
<div className='str-chat__load-more-button'>
1631
<button
1732
aria-label='Load More Channels'
1833
className='str-chat__load-more-button__button str-chat__cta-button'
1934
data-testid='load-more-button'
20-
disabled={refreshing}
35+
disabled={loading}
2136
onClick={onClick}
2237
>
23-
{refreshing ? <LoadingIndicator /> : children}
38+
{loading ? <LoadingIndicator /> : children}
2439
</button>
2540
</div>
2641
);

0 commit comments

Comments
 (0)