Skip to content

Commit ae1a7a7

Browse files
HoonBaeksravan-s
andauthored
feat: Provide fetchChannelList method via ChannelListContext (#708)
### What was the issue Customers could fetch the channel list using the `channelSource` of ChannelListContext but it couldn't be able to update the `allChannels` because the state is only updated by an internal dispatcher. ### Fix Add fetchChannelList to the ChannelListContext * Make a custom hook function `useFetchChanneelList` * Use it for fetching channel list inside of the ChannelListUI component * Add tests for this function * Provide the method through the ChannelListContext: fetchChannelList ### Usage Here's an example of using `getFetchChannelList` 1. import below ```javascript import SendbirdProvider from '@sendbird/uikit-react/SendbirdProvider' import useSendbirdStateContext from '@sendbird/uikit-react/useSendbirdStateContext' import { ChannelListProvider, useChannelListContext } from '@sendbird/uikit-react/ChannelList/context' ``` 2. implement a custom channel list ```javascript const isAboutSame = (a, b, px) => (Math.abs(a - b) <= px); export const CustomChannelList = () => { const { allChannels, fetchChannelList, } = useChannelListContext(); <div className="custom-channel-list" onScroll={(e) => { const target = e.target; if (isAboutSame(target.clientHeight + target.scrollTop, target.scrollHeight, 10)) { fetchChannelList(); } }} > {allChannels.map((channel) => { return // custom channel list item })} </div> } ``` 3. apply it to the custom app ```javascript import { CustomChannelList } from '../customChannelList.jsx' const CustomApp = () => { return ( <div className="custom-app"> <SendbirdProvider ... > <ChannelListProvider ... > <CustomChannelList /> </ChannelListProvider> </SendbirdProvider> </div> ) } ``` [CLNP-348](https://sendbird.atlassian.net/browse/CLNP-348) [CLNP-348]: https://sendbird.atlassian.net/browse/CLNP-348?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --------- Co-authored-by: Sravan S <[email protected]>
1 parent 21f9b97 commit ae1a7a7

File tree

8 files changed

+519
-43
lines changed

8 files changed

+519
-43
lines changed

scripts/index_d_ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,8 @@ declare module "SendbirdUIKitGlobal" {
230230
) => Promise<UserMessage>;
231231
export type GetResendFileMessage = (
232232
channel: GroupChannel | OpenChannel,
233-
failedMessage: FileMessage
233+
failedMessage: FileMessage,
234+
blob: Blob,
234235
) => Promise<FileMessage>;
235236

236237
export interface sendbirdSelectorsInterface {
@@ -415,6 +416,7 @@ declare module "SendbirdUIKitGlobal" {
415416
// channelListDispatcher: CustomUseReducerDispatcher;
416417
channelSource: GroupChannelListQuery;
417418
typingChannels: GroupChannel[];
419+
fetchChannelList: () => void;
418420
}
419421

420422

src/index.d.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,8 @@ type GetResendUserMessage = (
249249
) => Promise<UserMessage>;
250250
type GetResendFileMessage = (
251251
channel: GroupChannel | OpenChannel,
252-
failedMessage: FileMessage
252+
failedMessage: FileMessage,
253+
blob: Blob,
253254
) => Promise<FileMessage>;
254255

255256
interface sendbirdSelectorsInterface {
@@ -438,6 +439,8 @@ export interface ChannelListProviderInterface extends ChannelListProviderProps {
438439
currentUserId: string;
439440
// channelListDispatcher: CustomUseReducerDispatcher;
440441
channelSource: GroupChannelListQuery;
442+
typingChannels: GroupChannel[];
443+
fetchChannelList: () => void;
441444
}
442445

443446
interface RenderChannelPreviewProps {

src/modules/ChannelList/components/ChannelListUI/index.tsx

Lines changed: 4 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import './channel-list-ui.scss';
22

33
import React, { useState } from 'react';
4-
import type { GroupChannel, Member, SendbirdGroupChat } from '@sendbird/chat/groupChannel';
4+
import type { GroupChannel, Member } from '@sendbird/chat/groupChannel';
55
import type { User } from '@sendbird/chat';
66

77
import ChannelListHeader from '../ChannelListHeader';
@@ -16,8 +16,6 @@ import EditUserProfile from '../../../EditUserProfile';
1616
import PlaceHolder, { PlaceHolderTypes } from '../../../../ui/PlaceHolder';
1717
import { isAboutSame } from '../utils';
1818

19-
const DELIVERY_RECIPT = 'delivery_receipt';
20-
2119
interface RenderChannelPreviewProps {
2220
channel: GroupChannel;
2321
onLeaveChannel(
@@ -59,23 +57,18 @@ const ChannelListUI: React.FC<ChannelListUIProps> = (props: ChannelListUIProps)
5957
loading,
6058
currentChannel,
6159
channelListDispatcher,
62-
channelSource,
6360
typingChannels,
6461
initialized,
62+
fetchChannelList,
6563
} = useChannelListContext();
6664

6765
const state = useSendbirdStateContext();
68-
6966
const sdkStore = state?.stores?.sdkStore;
7067
const config = state?.config;
7168
const {
7269
logger,
7370
isOnline = false,
74-
markAsDeliveredScheduler,
75-
disableMarkAsDelivered,
7671
} = config;
77-
78-
const sdk = sdkStore?.sdk as SendbirdGroupChat;
7972
const sdkError = sdkStore?.error;
8073

8174
return (
@@ -112,38 +105,8 @@ const ChannelListUI: React.FC<ChannelListUIProps> = (props: ChannelListUIProps)
112105
className="sendbird-channel-list__body"
113106
onScroll={(e) => {
114107
const target = e?.target as HTMLDivElement;
115-
const fetchMore = isAboutSame(target.clientHeight + target.scrollTop, target.scrollHeight, 10);
116-
if (fetchMore && channelSource?.hasNext) {
117-
logger.info('ChannelList: Fetching more channels');
118-
channelListDispatcher({
119-
type: channelListActions.FETCH_CHANNELS_START,
120-
payload: null,
121-
});
122-
channelSource.next().then((channelList) => {
123-
logger.info('ChannelList: Fetching channels successful', channelList);
124-
channelListDispatcher({
125-
type: channelListActions.FETCH_CHANNELS_SUCCESS,
126-
payload: channelList,
127-
});
128-
const canSetMarkAsDelivered = sdk?.appInfo?.premiumFeatureList
129-
?.find((feature) => (feature === DELIVERY_RECIPT));
130-
131-
if (canSetMarkAsDelivered && !disableMarkAsDelivered) {
132-
logger.info('ChannelList: Marking all channels as read');
133-
// eslint-disable-next-line no-unused-expressions
134-
channelList?.forEach((channel) => {
135-
if (channel?.unreadMessageCount > 0) {
136-
markAsDeliveredScheduler.push(channel);
137-
}
138-
});
139-
}
140-
}).catch((err) => {
141-
logger.info('ChannelList: Fetching channels failed', err);
142-
channelListDispatcher({
143-
type: channelListActions.FETCH_CHANNELS_FAILURE,
144-
payload: err,
145-
});
146-
});
108+
if (isAboutSame(target.clientHeight + target.scrollTop, target.scrollHeight, 10)) {
109+
fetchChannelList();
147110
}
148111
}}
149112
>

src/modules/ChannelList/context/ChannelListProvider.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import channelListReducers from '../dux/reducers';
3434
import channelListInitialState from '../dux/initialState';
3535
import { CHANNEL_TYPE } from '../../CreateChannel/types';
3636
import useActiveChannelUrl from './hooks/useActiveChannelUrl';
37+
import { useFetchChannelList } from './hooks/useFetchChannelList';
3738

3839
interface ApplicationUserListQuery {
3940
limit?: number;
@@ -104,6 +105,7 @@ export interface ChannelListProviderInterface extends ChannelListProviderProps {
104105
currentUserId: string;
105106
channelListDispatcher: CustomUseReducerDispatcher;
106107
channelSource: GroupChannelListQuerySb | null;
108+
fetchChannelList: () => void;
107109
}
108110

109111
interface ChannelListStoreInterface {
@@ -169,6 +171,7 @@ const ChannelListProvider: React.FC<ChannelListProviderProps> = (props: ChannelL
169171
isMessageReceiptStatusEnabledOnChannelList = false,
170172
} = config;
171173
const sdk = sdkStore?.sdk as SendbirdGroupChat;
174+
const { premiumFeatureList = [] } = sdk?.appInfo ?? {};
172175

173176
// derive some variables
174177
// enable if it is true atleast once(both are flase by default)
@@ -345,6 +348,16 @@ const ChannelListProvider: React.FC<ChannelListProviderProps> = (props: ChannelL
345348
channelListDispatcher,
346349
});
347350

351+
const fetchChannelList = useFetchChannelList({
352+
channelSource,
353+
premiumFeatureList,
354+
disableMarkAsDelivered,
355+
}, {
356+
channelListDispatcher,
357+
logger,
358+
markAsDeliveredScheduler,
359+
});
360+
348361
return (
349362
<ChannelListContext.Provider value={{
350363
className,
@@ -364,6 +377,7 @@ const ChannelListProvider: React.FC<ChannelListProviderProps> = (props: ChannelL
364377
typingChannels,
365378
isTypingIndicatorEnabled: (isTypingIndicatorEnabled !== null) ? isTypingIndicatorEnabled : isTypingIndicatorEnabledOnChannelList,
366379
isMessageReceiptStatusEnabled: (isMessageReceiptStatusEnabled !== null) ? isMessageReceiptStatusEnabled : isMessageReceiptStatusEnabledOnChannelList,
380+
fetchChannelList,
367381
}}>
368382
<UserProfileProvider
369383
disableUserProfile={userDefinedDisableUserProfile ?? config?.disableUserProfile}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import { act } from 'react-dom/test-utils';
2+
import { renderHook } from '@testing-library/react';
3+
import { GroupChannelListQuery } from '@sendbird/chat/groupChannel';
4+
5+
import { useFetchChannelList } from '../useFetchChannelList';
6+
import { mockChannelList } from '../channelList.mock';
7+
import { DELIVERY_RECEIPT } from '../../../../../utils/consts';
8+
import { CustomUseReducerDispatcher, Logger } from '../../../../../lib/SendbirdState';
9+
import { Nullable } from '../../../../../types';
10+
import { MarkAsDeliveredSchedulerType } from '../../../../../lib/hooks/useMarkAsDeliveredScheduler';
11+
import * as channelListActions from '../../../dux/actionTypes';
12+
13+
interface GlobalContextType {
14+
mockChannelSource: Nullable<GroupChannelListQuery>,
15+
channelListDispatcher: Nullable<CustomUseReducerDispatcher>,
16+
markAsDeliveredScheduler: Nullable<MarkAsDeliveredSchedulerType>,
17+
logger: Nullable<Logger>,
18+
}
19+
const mockPremiumFeatureList = [DELIVERY_RECEIPT];
20+
const globalContext = {} as GlobalContextType;
21+
22+
describe('useFetchChannelList', () => {
23+
beforeEach(() => {
24+
globalContext.mockChannelSource = {
25+
hasNext: true,
26+
next: jest.fn(() => Promise.resolve(mockChannelList)),
27+
} as unknown as GroupChannelListQuery;
28+
globalContext.channelListDispatcher = jest.fn() as CustomUseReducerDispatcher;
29+
globalContext.markAsDeliveredScheduler = {
30+
push: jest.fn(),
31+
clear: jest.fn(),
32+
getQueue: jest.fn(),
33+
};
34+
globalContext.logger = {
35+
info: jest.fn(),
36+
warning: jest.fn(),
37+
error: jest.fn(),
38+
};
39+
});
40+
afterEach(() => {
41+
globalContext.mockChannelSource = null;
42+
globalContext.channelListDispatcher = null;
43+
globalContext.markAsDeliveredScheduler = null;
44+
globalContext.logger = null;
45+
});
46+
47+
it('should update allChannels after successful fetch channel list', async () => {
48+
const {
49+
mockChannelSource,
50+
channelListDispatcher,
51+
markAsDeliveredScheduler,
52+
logger,
53+
} = globalContext;
54+
const hook = renderHook(
55+
() => useFetchChannelList({
56+
channelSource: mockChannelSource,
57+
premiumFeatureList: mockPremiumFeatureList,
58+
disableMarkAsDelivered: false,
59+
}, {
60+
channelListDispatcher: channelListDispatcher as CustomUseReducerDispatcher,
61+
logger: logger as Logger,
62+
markAsDeliveredScheduler: markAsDeliveredScheduler as MarkAsDeliveredSchedulerType,
63+
}),
64+
);
65+
const resultCallback = hook.result.current as unknown as () => void;
66+
await act(async () => {
67+
await resultCallback();
68+
});
69+
70+
expect(channelListDispatcher).toHaveBeenCalledTimes(2);
71+
expect(mockChannelSource?.next).toHaveBeenCalledOnce();
72+
expect(channelListDispatcher).toHaveBeenNthCalledWith(1, {
73+
type: channelListActions.FETCH_CHANNELS_START,
74+
payload: null,
75+
});
76+
expect(channelListDispatcher).toHaveBeenNthCalledWith(2, {
77+
type: channelListActions.FETCH_CHANNELS_SUCCESS,
78+
payload: mockChannelList,
79+
});
80+
});
81+
82+
it('should expect failure when failed fetching channel list', async () => {
83+
const mockError = { code: 0, message: 'Error message' };
84+
const {
85+
mockChannelSource,
86+
channelListDispatcher,
87+
markAsDeliveredScheduler,
88+
logger,
89+
} = globalContext;
90+
const hook = renderHook(
91+
() => useFetchChannelList({
92+
channelSource: {
93+
...mockChannelSource,
94+
next: jest.fn(() => Promise.reject(mockError)),
95+
} as unknown as GroupChannelListQuery,
96+
premiumFeatureList: mockPremiumFeatureList,
97+
disableMarkAsDelivered: false,
98+
}, {
99+
channelListDispatcher: channelListDispatcher as CustomUseReducerDispatcher,
100+
logger: logger as Logger,
101+
markAsDeliveredScheduler: markAsDeliveredScheduler as MarkAsDeliveredSchedulerType,
102+
}),
103+
);
104+
const resultCallback = hook.result.current as unknown as () => void;
105+
await act(async () => {
106+
await resultCallback();
107+
});
108+
109+
expect(channelListDispatcher).toHaveBeenCalledTimes(2);
110+
expect(mockChannelSource?.next).not.toHaveBeenCalled();
111+
expect(channelListDispatcher).toHaveBeenNthCalledWith(1, {
112+
type: channelListActions.FETCH_CHANNELS_START,
113+
payload: null,
114+
});
115+
expect(channelListDispatcher).toHaveBeenNthCalledWith(2, {
116+
type: channelListActions.FETCH_CHANNELS_FAILURE,
117+
payload: mockError,
118+
});
119+
});
120+
121+
it('should not try to fetch channel list when hasNext is false', async () => {
122+
const {
123+
mockChannelSource,
124+
channelListDispatcher,
125+
markAsDeliveredScheduler,
126+
logger,
127+
} = globalContext;
128+
const hook = renderHook(
129+
() => useFetchChannelList({
130+
channelSource: {
131+
...mockChannelSource,
132+
hasNext: false,
133+
} as unknown as GroupChannelListQuery,
134+
premiumFeatureList: mockPremiumFeatureList,
135+
disableMarkAsDelivered: false,
136+
}, {
137+
channelListDispatcher: channelListDispatcher as CustomUseReducerDispatcher,
138+
logger: logger as Logger,
139+
markAsDeliveredScheduler: markAsDeliveredScheduler as MarkAsDeliveredSchedulerType,
140+
}),
141+
);
142+
const resultCallback = hook.result.current as unknown as () => void;
143+
await act(async () => {
144+
await resultCallback();
145+
});
146+
147+
expect(mockChannelSource?.next).not.toHaveBeenCalled();
148+
expect(channelListDispatcher).not.toHaveBeenCalled();
149+
});
150+
});

0 commit comments

Comments
 (0)