diff --git a/docusaurus/docs/React/components/core-components/message-list.mdx b/docusaurus/docs/React/components/core-components/message-list.mdx index 8b76c63f9..99792cf03 100644 --- a/docusaurus/docs/React/components/core-components/message-list.mdx +++ b/docusaurus/docs/React/components/core-components/message-list.mdx @@ -476,6 +476,10 @@ Array of allowed message actions (ex: 'edit', 'delete', 'reply'). To disable all The limit to use when paginating new messages (the page size). +:::caution +After mounting, the `MessageList` component checks if the list is completely filled with messages. If there is some space left in the list, `MessageList` will load the next page of messages, but it will do so _only once_. This means that if your `messageLimit` is too low, or if your viewport is very large, the list will not be completely filled. Set the limit with this in mind. +::: + | Type | Default | | ------ | ------- | | number | 100 | diff --git a/docusaurus/docs/React/components/core-components/virtualized-list.mdx b/docusaurus/docs/React/components/core-components/virtualized-list.mdx index 7ede49006..48f0a1509 100644 --- a/docusaurus/docs/React/components/core-components/virtualized-list.mdx +++ b/docusaurus/docs/React/components/core-components/virtualized-list.mdx @@ -186,6 +186,10 @@ Custom UI component to display an individual message. The limit to use when paginating messages (the page size). +:::caution +After mounting, the `VirtualizedMessageList` component checks if the list is completely filled with messages. If there is some space left in the list, `VirtualizedMessageList` will load the next page of messages, but it will do so _only once_. This means that if your `messageLimit` is too low, or if your viewport is very large, the list will not be completely filled. Set the limit with this in mind. +::: + | Type | Default | | ------ | ------- | | number | 100 | diff --git a/examples/typescript/src/index.tsx b/examples/typescript/src/index.tsx index 0a9c84979..a6e481519 100644 --- a/examples/typescript/src/index.tsx +++ b/examples/typescript/src/index.tsx @@ -10,7 +10,7 @@ import * as serviceWorker from './serviceWorker'; createRoot(document.getElementById('root')!).render( - + , ); // If you want your app to work offline and load faster, you can change diff --git a/package.json b/package.json index 297008ec1..0def1d0bb 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "isomorphic-ws": "^4.0.1", "linkifyjs": "^4.1.0", "lodash.debounce": "^4.0.8", + "lodash.defaultsdeep": "^4.6.1", "lodash.throttle": "^4.1.1", "lodash.uniqby": "^4.7.0", "nanoid": "^3.3.4", @@ -155,6 +156,7 @@ "@types/jsdom": "^21.1.5", "@types/linkifyjs": "^2.1.3", "@types/lodash.debounce": "^4.0.7", + "@types/lodash.defaultsdeep": "^4.6.9", "@types/lodash.throttle": "^4.1.7", "@types/lodash.uniqby": "^4.7.7", "@types/moment": "^2.13.0", diff --git a/src/components/Channel/Channel.tsx b/src/components/Channel/Channel.tsx index b7c560164..35e41b0e1 100644 --- a/src/components/Channel/Channel.tsx +++ b/src/components/Channel/Channel.tsx @@ -10,6 +10,7 @@ import React, { } from 'react'; import debounce from 'lodash.debounce'; +import defaultsDeep from 'lodash.defaultsdeep'; import throttle from 'lodash.throttle'; import { ChannelAPIResponse, @@ -346,7 +347,7 @@ const ChannelInner = < acceptedFiles, activeUnreadHandler, channel, - channelQueryOptions, + channelQueryOptions: propChannelQueryOptions, children, doDeleteMessageRequest, doMarkReadRequest, @@ -366,6 +367,16 @@ const ChannelInner = < skipMessageDataMemoization, } = props; + const channelQueryOptions: ChannelQueryOptions & { + messages: { limit: number }; + } = useMemo( + () => + defaultsDeep(propChannelQueryOptions, { + messages: { limit: DEFAULT_INITIAL_CHANNEL_PAGE_SIZE }, + }), + [propChannelQueryOptions], + ); + const { client, customClasses, @@ -546,6 +557,7 @@ const ChannelInner = < useLayoutEffect(() => { let errored = false; let done = false; + let channelInitializedExternally = true; (async () => { if (!channel.initialized && initializeOnMount) { @@ -571,6 +583,7 @@ const ChannelInner = < await getChannel({ channel, client, members, options: channelQueryOptions }); const config = channel.getConfig(); setChannelConfig(config); + channelInitializedExternally = false; } catch (e) { dispatch({ error: e as Error, type: 'setError' }); errored = true; @@ -583,10 +596,12 @@ const ChannelInner = < if (!errored) { dispatch({ channel, - hasMore: hasMoreMessagesProbably( - channel.state.messages.length, - channelQueryOptions?.messages?.limit ?? DEFAULT_INITIAL_CHANNEL_PAGE_SIZE, - ), + hasMore: + channelInitializedExternally || + hasMoreMessagesProbably( + channel.state.messages.length, + channelQueryOptions.messages.limit, + ), type: 'initStateFromChannel', }); diff --git a/src/components/Channel/__tests__/Channel.test.js b/src/components/Channel/__tests__/Channel.test.js index d7fdf6bd3..151cebba5 100644 --- a/src/components/Channel/__tests__/Channel.test.js +++ b/src/components/Channel/__tests__/Channel.test.js @@ -290,7 +290,7 @@ describe('Channel', () => { await waitFor(() => { expect(watchSpy).toHaveBeenCalledTimes(1); - expect(watchSpy).toHaveBeenCalledWith(undefined); + expect(watchSpy).toHaveBeenCalledWith({ messages: { limit: 25 } }); }); }); diff --git a/src/components/ChannelList/hooks/usePaginatedChannels.ts b/src/components/ChannelList/hooks/usePaginatedChannels.ts index ad79cdaa9..8387bdc09 100644 --- a/src/components/ChannelList/hooks/usePaginatedChannels.ts +++ b/src/components/ChannelList/hooks/usePaginatedChannels.ts @@ -9,6 +9,7 @@ import { useChatContext } from '../../../context/ChatContext'; import type { DefaultStreamChatGenerics } from '../../../types/types'; import type { ChannelsQueryState } from '../../Chat/hooks/useChannelsQueryState'; +import { DEFAULT_INITIAL_CHANNEL_PAGE_SIZE } from '../../../constants/limits'; const RECOVER_LOADED_CHANNELS_THROTTLE_INTERVAL_IN_MS = 5000; const MIN_RECOVER_LOADED_CHANNELS_THROTTLE_INTERVAL_IN_MS = 2000; @@ -79,6 +80,7 @@ export const usePaginatedChannels = < const newOptions = { limit: options?.limit ?? MAX_QUERY_CHANNELS_LIMIT, + message_limit: options?.message_limit ?? DEFAULT_INITIAL_CHANNEL_PAGE_SIZE, offset, ...options, }; diff --git a/src/components/InfiniteScrollPaginator/InfiniteScroll.tsx b/src/components/InfiniteScrollPaginator/InfiniteScroll.tsx index 156c4f1e9..c8941fb25 100644 --- a/src/components/InfiniteScrollPaginator/InfiniteScroll.tsx +++ b/src/components/InfiniteScrollPaginator/InfiniteScroll.tsx @@ -1,4 +1,4 @@ -import React, { PropsWithChildren, useCallback, useEffect, useLayoutEffect, useRef } from 'react'; +import React, { PropsWithChildren, useEffect, useLayoutEffect, useRef } from 'react'; import type { PaginatorProps } from '../../types/types'; import { deprecationAndReplacementWarning } from '../../utils/deprecationWarning'; @@ -73,7 +73,8 @@ export const InfiniteScroll = (props: PropsWithChildren) => const scrollComponent = useRef(); - const scrollListener = useCallback(() => { + const scrollListenerRef = useRef<() => void>(); + scrollListenerRef.current = () => { const element = scrollComponent.current; if (!element || element.offsetParent === null) { @@ -103,15 +104,7 @@ export const InfiniteScroll = (props: PropsWithChildren) => if (offset < Number(threshold) && typeof loadNextPageFn === 'function' && hasNextPageFlag) { loadNextPageFn(); } - }, [ - hasPreviousPageFlag, - hasNextPageFlag, - isLoading, - listenToScroll, - loadPreviousPageFn, - loadNextPageFn, - threshold, - ]); + }; useEffect(() => { deprecationAndReplacementWarning( @@ -130,14 +123,17 @@ export const InfiniteScroll = (props: PropsWithChildren) => const scrollElement = scrollComponent.current?.parentNode; if (!scrollElement) return; + const scrollListener = () => scrollListenerRef.current?.(); + scrollElement.addEventListener('scroll', scrollListener, useCapture); scrollElement.addEventListener('resize', scrollListener, useCapture); + scrollListener(); return () => { scrollElement.removeEventListener('scroll', scrollListener, useCapture); scrollElement.removeEventListener('resize', scrollListener, useCapture); }; - }, [initialLoad, scrollListener, useCapture]); + }, [initialLoad, useCapture]); useEffect(() => { const scrollElement = scrollComponent.current?.parentNode; diff --git a/src/components/MessageList/MessageList.tsx b/src/components/MessageList/MessageList.tsx index 047fd3390..34fc34dc7 100644 --- a/src/components/MessageList/MessageList.tsx +++ b/src/components/MessageList/MessageList.tsx @@ -38,6 +38,7 @@ import type { MessageProps } from '../Message/types'; import type { StreamMessage } from '../../context/ChannelStateContext'; import type { DefaultStreamChatGenerics } from '../../types/types'; +import { DEFAULT_NEXT_CHANNEL_PAGE_SIZE } from '../../constants/limits'; type MessageListWithContextProps< StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics @@ -68,7 +69,7 @@ const MessageListWithContext = < headerPosition, read, renderMessages = defaultRenderMessages, - messageLimit = 100, + messageLimit = DEFAULT_NEXT_CHANNEL_PAGE_SIZE, loadMore: loadMoreCallback, loadMoreNewer: loadMoreNewerCallback, hasMoreNewer = false, diff --git a/src/components/MessageList/VirtualizedMessageList.tsx b/src/components/MessageList/VirtualizedMessageList.tsx index 5b8a9deae..acc046b2b 100644 --- a/src/components/MessageList/VirtualizedMessageList.tsx +++ b/src/components/MessageList/VirtualizedMessageList.tsx @@ -53,6 +53,7 @@ import { ComponentContextValue, useComponentContext } from '../../context/Compon import type { Channel, ChannelState as StreamChannelState, UserResponse } from 'stream-chat'; import type { DefaultStreamChatGenerics, UnknownType } from '../../types/types'; +import { DEFAULT_NEXT_CHANNEL_PAGE_SIZE } from '../../constants/limits'; type VirtualizedMessageListPropsForContext = | 'additionalMessageInputProps' @@ -184,7 +185,7 @@ const VirtualizedMessageListWithContext = < loadMoreNewer, Message: MessageUIComponentFromProps, messageActions, - messageLimit = 100, + messageLimit = DEFAULT_NEXT_CHANNEL_PAGE_SIZE, messages, notifications, // TODO: refactor to scrollSeekPlaceHolderConfiguration and components.ScrollSeekPlaceholder, like the Virtuoso Component @@ -386,7 +387,9 @@ const VirtualizedMessageListWithContext = < } }; const atTopStateChange = (isAtTop: boolean) => { - if (isAtTop) loadMore?.(messageLimit); + if (isAtTop) { + loadMore?.(messageLimit); + } }; useEffect(() => { diff --git a/yarn.lock b/yarn.lock index c564a5c71..3a76886d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2497,6 +2497,13 @@ dependencies: "@types/lodash" "*" +"@types/lodash.defaultsdeep@^4.6.9": + version "4.6.9" + resolved "https://registry.yarnpkg.com/@types/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.9.tgz#050fbe389a7a6e245b15da9ee83a8a62f047a1c4" + integrity sha512-pLtCFK0YkHfGtGLYLNMTbFB5/G5+RsmQCIbbHH8GOAXjv+gDkVilY98kILfe8JH2Kev0OCReYxp1AjxEjP8ixA== + dependencies: + "@types/lodash" "*" + "@types/lodash.throttle@^4.1.7": version "4.1.7" resolved "https://registry.yarnpkg.com/@types/lodash.throttle/-/lodash.throttle-4.1.7.tgz#4ef379eb4f778068022310ef166625f420b6ba58" @@ -9391,6 +9398,11 @@ lodash.deburr@^4.1.0: resolved "https://registry.yarnpkg.com/lodash.deburr/-/lodash.deburr-4.1.0.tgz#ddb1bbb3ef07458c0177ba07de14422cb033ff9b" integrity sha1-3bG7s+8HRYwBd7oH3hRCLLAz/5s= +lodash.defaultsdeep@^4.6.1: + version "4.6.1" + resolved "https://registry.yarnpkg.com/lodash.defaultsdeep/-/lodash.defaultsdeep-4.6.1.tgz#512e9bd721d272d94e3d3a63653fa17516741ca6" + integrity sha512-3j8wdDzYuWO3lM3Reg03MuQR957t287Rpcxp1njpEa8oDrikb+FwGdW3n+FELh/A6qib6yPit0j/pv9G/yeAqA== + lodash.escaperegexp@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz#64762c48618082518ac3df4ccf5d5886dae20347"