Skip to content

Commit 24e294b

Browse files
fix: hasMore limitations (#2242)
### 🎯 Goal A bug that would prevent users from loading older messages within message lists if they paginated on channel B, switched to channel A and then back to channel B and tried paginating again. ### 🛠 Implementation details Moved from `startReached` to `atTopStateChange` and from `endReached` to `atBottomStateChange` callbacks for pagination as `startReached` [is distinct](https://github.com/petyosi/react-virtuoso/blob/efdf50b8c4ee18e1c9d2fa4866e47ae9fd64cae1/src/listStateSystem.ts#L386) and called only once per set of data (I assume, not sure why as scrolling back down should ideally reset it) - but if you were to hit the top - and the data load would fail for some reason - scrolling a bit down and back up would not trigger the data load again. This was one of my ways to try to resolve the original problem before I was able to find reliable reproduction steps but in the end decided to keep it as "futureproofing". Though I'm worried that some of our integrators migh've hooked into these properties overriding whatever we newly have making their pagination not work.
1 parent 47f4eaf commit 24e294b

File tree

4 files changed

+81
-56
lines changed

4 files changed

+81
-56
lines changed

src/components/Channel/Channel.tsx

Lines changed: 50 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,13 @@ type ChannelPropsForwardedToComponentContext<
188188
VirtualMessage?: ComponentContextValue<StreamChatGenerics>['VirtualMessage'];
189189
};
190190

191+
const isUserResponseArray = <
192+
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
193+
>(
194+
output: string[] | UserResponse<StreamChatGenerics>[],
195+
): output is UserResponse<StreamChatGenerics>[] =>
196+
(output as UserResponse<StreamChatGenerics>[])[0]?.id != null;
197+
191198
export type ChannelProps<
192199
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
193200
V extends CustomTrigger = CustomTrigger
@@ -591,16 +598,17 @@ const ChannelInner = <
591598
// Adds a temporary notification to message list, will be removed after 5 seconds
592599
const addNotification = makeAddNotifications(setNotifications, notificationTimeouts);
593600

594-
const loadMoreFinished = debounce(
595-
(hasMore: boolean, messages: ChannelState<StreamChatGenerics>['messages']) => {
596-
if (!isMounted.current) return;
597-
dispatch({ hasMore, messages, type: 'loadMoreFinished' });
598-
},
599-
2000,
600-
{
601-
leading: true,
602-
trailing: true,
603-
},
601+
// eslint-disable-next-line react-hooks/exhaustive-deps
602+
const loadMoreFinished = useCallback(
603+
debounce(
604+
(hasMore: boolean, messages: ChannelState<StreamChatGenerics>['messages']) => {
605+
if (!isMounted.current) return;
606+
dispatch({ hasMore, messages, type: 'loadMoreFinished' });
607+
},
608+
2000,
609+
{ leading: true, trailing: true },
610+
),
611+
[],
604612
);
605613

606614
const loadMore = async (limit = DEFAULT_NEXT_CHANNEL_PAGE_SIZE) => {
@@ -637,7 +645,7 @@ const ChannelInner = <
637645
};
638646

639647
const loadMoreNewer = async (limit = 100) => {
640-
if (!online.current || !window.navigator.onLine) return 0;
648+
if (!online.current || !window.navigator.onLine || !state.hasMoreNewer) return 0;
641649

642650
const newestMessage = state?.messages?.[state?.messages?.length - 1];
643651
if (state.loadingMore || state.loadingMoreNewer) return 0;
@@ -659,9 +667,13 @@ const ChannelInner = <
659667
return 0;
660668
}
661669

662-
const hasMoreNewer = channel.state.messages !== channel.state.latestMessages;
670+
const hasMoreNewerMessages = channel.state.messages !== channel.state.latestMessages;
663671

664-
dispatch({ hasMoreNewer, messages: channel.state.messages, type: 'loadMoreNewerFinished' });
672+
dispatch({
673+
hasMoreNewer: hasMoreNewerMessages,
674+
messages: channel.state.messages,
675+
type: 'loadMoreNewerFinished',
676+
});
665677
return queryResponse.messages.length;
666678
};
667679

@@ -738,11 +750,6 @@ const ChannelInner = <
738750
});
739751
};
740752

741-
const isUserResponseArray = (
742-
output: string[] | UserResponse<StreamChatGenerics>[],
743-
): output is UserResponse<StreamChatGenerics>[] =>
744-
(output as UserResponse<StreamChatGenerics>[])[0]?.id != null;
745-
746753
const doSendMessage = async (
747754
message: MessageToSend<StreamChatGenerics> | StreamMessage<StreamChatGenerics>,
748755
customMessageData?: Partial<Message<StreamChatGenerics>>,
@@ -751,7 +758,7 @@ const ChannelInner = <
751758
const { attachments, id, mentioned_users = [], parent_id, text } = message;
752759

753760
// channel.sendMessage expects an array of user id strings
754-
const mentions = isUserResponseArray(mentioned_users)
761+
const mentions = isUserResponseArray<StreamChatGenerics>(mentioned_users)
755762
? mentioned_users.map(({ id }) => id)
756763
: mentioned_users;
757764

@@ -894,43 +901,47 @@ const ChannelInner = <
894901
dispatch({ type: 'closeThread' });
895902
};
896903

897-
const loadMoreThreadFinished = debounce(
898-
(
899-
threadHasMore: boolean,
900-
threadMessages: Array<ReturnType<ChannelState<StreamChatGenerics>['formatMessage']>>,
901-
) => {
902-
dispatch({
903-
threadHasMore,
904-
threadMessages,
905-
type: 'loadMoreThreadFinished',
906-
});
907-
},
908-
2000,
909-
{ leading: true, trailing: true },
904+
// eslint-disable-next-line react-hooks/exhaustive-deps
905+
const loadMoreThreadFinished = useCallback(
906+
debounce(
907+
(
908+
threadHasMore: boolean,
909+
threadMessages: Array<ReturnType<ChannelState<StreamChatGenerics>['formatMessage']>>,
910+
) => {
911+
dispatch({
912+
threadHasMore,
913+
threadMessages,
914+
type: 'loadMoreThreadFinished',
915+
});
916+
},
917+
2000,
918+
{ leading: true, trailing: true },
919+
),
920+
[],
910921
);
911922

912923
const loadMoreThread = async (limit: number = DEFAULT_THREAD_PAGE_SIZE) => {
913924
// FIXME: should prevent loading more, if state.thread.reply_count === channel.state.threads[parentID].length
914925
if (state.threadLoadingMore || !state.thread) return;
915926

916927
dispatch({ type: 'startLoadingThread' });
917-
const parentID = state.thread.id;
928+
const parentId = state.thread.id;
918929

919-
if (!parentID) {
930+
if (!parentId) {
920931
return dispatch({ type: 'closeThread' });
921932
}
922933

923-
const oldMessages = channel.state.threads[parentID] || [];
924-
const oldestMessageID = oldMessages[0]?.id;
934+
const oldMessages = channel.state.threads[parentId] || [];
935+
const oldestMessageId = oldMessages[0]?.id;
925936

926937
try {
927-
const queryResponse = await channel.getReplies(parentID, {
928-
id_lt: oldestMessageID,
938+
const queryResponse = await channel.getReplies(parentId, {
939+
id_lt: oldestMessageId,
929940
limit,
930941
});
931942

932943
const threadHasMoreMessages = hasMoreMessagesProbably(queryResponse.messages.length, limit);
933-
const newThreadMessages = channel.state.threads[parentID] || [];
944+
const newThreadMessages = channel.state.threads[parentId] || [];
934945

935946
// next set loadingMore to false so we can start asking for more data
936947
loadMoreThreadFinished(threadHasMoreMessages, newThreadMessages);

src/components/Channel/__tests__/Channel.test.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,28 @@ describe('Channel', () => {
322322
});
323323
});
324324

325+
// this will only happen if we:
326+
// load with channel A
327+
// switch to channel B and paginate (loadMore - older)
328+
// switch back to channel A (reset hasMore)
329+
// switch back to channel B - messages are already cached and there's more than page size amount
330+
it('should set hasMore state to true if the initial channel query returns more messages than the default initial page size', async () => {
331+
const { channel, chatClient } = await initClient();
332+
useMockedApis(chatClient, [
333+
queryChannelWithNewMessages(Array.from({ length: 26 }, generateMessage), channel),
334+
]);
335+
let hasMore;
336+
await act(() => {
337+
renderComponent({ channel, chatClient }, ({ hasMore: contextHasMore }) => {
338+
hasMore = contextHasMore;
339+
});
340+
});
341+
342+
await waitFor(() => {
343+
expect(hasMore).toBe(true);
344+
});
345+
});
346+
325347
it('should set hasMore state to true if the initial channel query returns count of messages equal to the default initial page size', async () => {
326348
const { channel, chatClient } = await initClient();
327349
useMockedApis(chatClient, [

src/components/MessageList/VirtualizedMessageList.tsx

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,6 @@ const VirtualizedMessageListWithContext = <
151151
defaultItemHeight,
152152
disableDateSeparator = true,
153153
groupStyles,
154-
hasMore,
155154
hasMoreNewer,
156155
head,
157156
hideDeletedMessages = false,
@@ -333,21 +332,14 @@ const VirtualizedMessageListWithContext = <
333332
const atBottomStateChange = (isAtBottom: boolean) => {
334333
atBottom.current = isAtBottom;
335334
setIsMessageListScrolledToBottom(isAtBottom);
336-
if (isAtBottom && newMessagesNotification) {
337-
setNewMessagesNotification(false);
338-
}
339-
};
340335

341-
const startReached = () => {
342-
if (hasMore && loadMore) {
343-
loadMore(messageLimit);
336+
if (isAtBottom) {
337+
loadMoreNewer?.(messageLimit);
338+
setNewMessagesNotification?.(false);
344339
}
345340
};
346-
347-
const endReached = () => {
348-
if (hasMoreNewer && loadMoreNewer) {
349-
loadMoreNewer(messageLimit);
350-
}
341+
const atTopStateChange = (isAtTop: boolean) => {
342+
if (isAtTop) loadMore?.(messageLimit);
351343
};
352344

353345
useEffect(() => {
@@ -368,7 +360,9 @@ const VirtualizedMessageListWithContext = <
368360
<div className={customClasses?.virtualizedMessageList || 'str-chat__virtual-list'}>
369361
<Virtuoso<UnknownType, VirtuosoContext<StreamChatGenerics>>
370362
atBottomStateChange={atBottomStateChange}
371-
atBottomThreshold={200}
363+
atBottomThreshold={100}
364+
atTopStateChange={atTopStateChange}
365+
atTopThreshold={100}
372366
className='str-chat__message-list-scroll'
373367
components={{
374368
EmptyPlaceholder,
@@ -399,7 +393,6 @@ const VirtualizedMessageListWithContext = <
399393
threadList,
400394
virtuosoRef: virtuoso,
401395
}}
402-
endReached={endReached}
403396
firstItemIndex={calculateFirstItemIndex(numItemsPrepended)}
404397
followOutput={followOutput}
405398
increaseViewportBy={{ bottom: 200, top: 0 }}
@@ -412,7 +405,6 @@ const VirtualizedMessageListWithContext = <
412405
key={messageSetKey}
413406
overscan={overscan}
414407
ref={virtuoso}
415-
startReached={startReached}
416408
style={{ overflowX: 'hidden' }}
417409
totalCount={processedMessages.length}
418410
{...overridingVirtuosoProps}

src/components/MessageList/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ export const getGroupStyles = <
316316
// The MessageList should have configurable the limit for performing the requests.
317317
// This parameter would then be used within these functions
318318
export const hasMoreMessagesProbably = (returnedCountMessages: number, limit: number) =>
319-
returnedCountMessages === limit;
319+
returnedCountMessages >= limit;
320320

321321
// @deprecated
322322
export const hasNotMoreMessages = (returnedCountMessages: number, limit: number) =>

0 commit comments

Comments
 (0)