import { useEffect, useMemo, useRef, useState } from 'react'; import { MAX_QUERY_CHANNELS_LIMIT } from '../utils'; import { useChatContext } from '../../../contexts/chatContext/ChatContext'; import { useIsMountedRef } from '../../../hooks/useIsMountedRef'; import type { Channel, ChannelFilters, ChannelOptions, ChannelSort } from 'stream-chat'; import type { DefaultAttachmentType, DefaultChannelType, DefaultCommandType, DefaultEventType, DefaultMessageType, DefaultReactionType, DefaultUserType, UnknownType, } from '../../../types/types'; const wait = (ms: number) => new Promise((resolve) => { setTimeout(resolve, ms); }); type Parameters< Ch extends UnknownType = DefaultChannelType, Co extends string = DefaultCommandType, Us extends UnknownType = DefaultUserType, > = { filters: ChannelFilters; options: ChannelOptions; sort: ChannelSort; }; const DEFAULT_OPTIONS = { message_limit: 10, }; export const usePaginatedChannels = < At extends UnknownType = DefaultAttachmentType, Ch extends UnknownType = DefaultChannelType, Co extends string = DefaultCommandType, Ev extends UnknownType = DefaultEventType, Me extends UnknownType = DefaultMessageType, Re extends UnknownType = DefaultReactionType, Us extends UnknownType = DefaultUserType, >({ filters = {}, options = DEFAULT_OPTIONS, sort = {}, }: Parameters) => { const { client } = useChatContext(); const [channels, setChannels] = useState[]>([]); const [error, setError] = useState(false); const [hasNextPage, setHasNextPage] = useState(true); const lastRefresh = useRef(Date.now()); const [loadingChannels, setLoadingChannels] = useState(false); const [loadingNextPage, setLoadingNextPage] = useState(false); const [refreshing, setRefreshing] = useState(false); const isMounted = useIsMountedRef(); const queryChannels = async (queryType = '', retryCount = 0): Promise => { if (!client || loadingChannels || loadingNextPage || refreshing || !isMounted.current) return; if (queryType === 'reload') { setLoadingChannels(true); } else if (queryType === 'refresh') { setRefreshing(true); } else if (!queryType) { setLoadingNextPage(true); } const newOptions = { limit: options?.limit ?? MAX_QUERY_CHANNELS_LIMIT, offset: queryType === 'reload' || queryType === 'refresh' ? 0 : channels.length, ...options, }; try { const channelQueryResponse = await client.queryChannels(filters, sort, newOptions); if (!isMounted.current) return; channelQueryResponse.forEach((channel) => channel.state.setIsUpToDate(true)); const newChannels = queryType === 'reload' || queryType === 'refresh' ? channelQueryResponse : [...channels, ...channelQueryResponse]; setChannels(newChannels); setHasNextPage(channelQueryResponse.length >= newOptions.limit); setError(false); } catch (err) { await wait(2000); if (!isMounted.current) return; if (retryCount === 3) { setLoadingChannels(false); setLoadingNextPage(false); setRefreshing(false); console.warn(err); return setError(true); } return queryChannels(queryType, retryCount + 1); } setLoadingChannels(false); setLoadingNextPage(false); setRefreshing(false); }; const loadNextPage = hasNextPage ? queryChannels : undefined; const refreshList = () => { const now = Date.now(); // Only allow pull-to-refresh 5 seconds after last successful refresh. if (now - lastRefresh.current < 5000 && !error) { return; } lastRefresh.current = Date.now(); return queryChannels('refresh'); }; const reloadList = () => queryChannels('reload'); /** * Equality check using stringified filters/sort ensure that we don't make un-necessary queryChannels api calls * for the scenario: * * * * Here we have passed filters as inline object, which means on every re-render of * parent component, ChannelList will receive new object reference (even though value is same), which * in return will trigger useEffect. To avoid this, we can add a value check. */ const filterStr = useMemo(() => JSON.stringify(filters), [filters]); const sortStr = useMemo(() => JSON.stringify(sort), [sort]); useEffect(() => { reloadList(); }, [filterStr, sortStr]); return { channels, error, hasNextPage, loadingChannels, loadingNextPage, loadNextPage, refreshing, refreshList, reloadList, setChannels, }; };