UNPKG

stream-chat-react

Version:

React components to create chat conversations or livestream style chat

152 lines (151 loc) 9.09 kB
import React, { useCallback, useEffect, useRef, useState } from 'react'; import clsx from 'clsx'; import { useConnectionRecoveredListener } from './hooks/useConnectionRecoveredListener'; import { useMobileNavigation } from './hooks/useMobileNavigation'; import { usePaginatedChannels } from './hooks/usePaginatedChannels'; import { useChannelListShape, usePrepareShapeHandlers, } from './hooks/useChannelListShape'; import { useStateStore } from '../../store'; import { ChannelListMessenger } from './ChannelListMessenger'; import { Avatar as DefaultAvatar } from '../Avatar'; import { ChannelPreview } from '../ChannelPreview/ChannelPreview'; import { ChannelSearch as DefaultChannelSearch } from '../ChannelSearch/ChannelSearch'; import { EmptyStateIndicator as DefaultEmptyStateIndicator } from '../EmptyStateIndicator'; import { LoadingChannels } from '../Loading/LoadingChannels'; import { LoadMorePaginator } from '../LoadMore/LoadMorePaginator'; import { ChannelListContextProvider, useChatContext, useComponentContext, } from '../../context'; import { NullComponent } from '../UtilityComponents'; import { MAX_QUERY_CHANNELS_LIMIT, moveChannelUpwards } from './utils'; const DEFAULT_FILTERS = {}; const DEFAULT_OPTIONS = {}; const DEFAULT_SORT = {}; const searchControllerStateSelector = (nextValue) => ({ searchIsActive: nextValue.isActive, }); const UnMemoizedChannelList = (props) => { const { additionalChannelSearchProps, allowNewMessagesFromUnfilteredChannels = true, Avatar = DefaultAvatar, channelRenderFilterFn, ChannelSearch = DefaultChannelSearch, customActiveChannel, customQueryChannels, EmptyStateIndicator = DefaultEmptyStateIndicator, filters = {}, getLatestMessagePreview, List = ChannelListMessenger, LoadingErrorIndicator = NullComponent, LoadingIndicator = LoadingChannels, lockChannelOrder = false, onAddedToChannel, onChannelDeleted, onChannelHidden, onChannelTruncated, onChannelUpdated, onChannelVisible, onMessageNew, onMessageNewHandler, onRemovedFromChannel, options, Paginator = LoadMorePaginator, Preview, recoveryThrottleIntervalMs, renderChannels, sendChannelsToList = false, setActiveChannelOnMount = true, showChannelSearch = false, sort = DEFAULT_SORT, watchers = {}, } = props; const { channel, channelsQueryState, client, closeMobileNav, customClasses, navOpen = false, searchController, setActiveChannel, theme, useImageFlagEmojisOnWindows, } = useChatContext('ChannelList'); const { Search } = useComponentContext(); // FIXME: us component context to retrieve ChannelPreview UI components too const channelListRef = useRef(null); const [channelUpdateCount, setChannelUpdateCount] = useState(0); const [searchActive, setSearchActive] = useState(false); // Indicator relevant when Search component that relies on SearchController is used const { searchIsActive } = useStateStore(searchController.state, searchControllerStateSelector); /** * Set a channel with id {customActiveChannel} as active and move it to the top of the list. * If customActiveChannel prop is absent, then set the first channel in list as active channel. */ const activeChannelHandler = async (channels, setChannels) => { if (!channels.length || channels.length > (options?.limit || MAX_QUERY_CHANNELS_LIMIT)) { return; } if (customActiveChannel) { // FIXME: this is wrong... let customActiveChannelObject = channels.find((chan) => chan.id === customActiveChannel); if (!customActiveChannelObject) { [customActiveChannelObject] = await client.queryChannels({ id: customActiveChannel, }); } if (customActiveChannelObject) { setActiveChannel(customActiveChannelObject, watchers); const newChannels = moveChannelUpwards({ channels, channelToMove: customActiveChannelObject, sort, }); setChannels(newChannels); } return; } if (setActiveChannelOnMount) { setActiveChannel(channels[0], watchers); } }; /** * For some events, inner properties on the channel will update but the shallow comparison will not * force a re-render. Incrementing this dummy variable ensures the channel previews update. */ const forceUpdate = useCallback(() => setChannelUpdateCount((count) => count + 1), []); const onSearch = useCallback((event) => { setSearchActive(!!event.target.value); additionalChannelSearchProps?.onSearch?.(event); }, [additionalChannelSearchProps]); const onSearchExit = useCallback(() => { setSearchActive(false); additionalChannelSearchProps?.onSearchExit?.(); }, [additionalChannelSearchProps]); const { channels, hasNextPage, loadNextPage, setChannels } = usePaginatedChannels(client, filters || DEFAULT_FILTERS, sort || DEFAULT_SORT, options || DEFAULT_OPTIONS, activeChannelHandler, recoveryThrottleIntervalMs, customQueryChannels); const loadedChannels = channelRenderFilterFn ? channelRenderFilterFn(channels) : channels; useMobileNavigation(channelListRef, navOpen, closeMobileNav); const { customHandler, defaultHandler } = usePrepareShapeHandlers({ allowNewMessagesFromUnfilteredChannels, filters, lockChannelOrder, onAddedToChannel, onChannelDeleted, onChannelHidden, onChannelTruncated, onChannelUpdated, onChannelVisible, onMessageNew, onMessageNewHandler, onRemovedFromChannel, setChannels, sort, // TODO: implement // customHandleChannelListShape }); useChannelListShape(customHandler ?? defaultHandler); // TODO: maybe move this too useConnectionRecoveredListener(forceUpdate); useEffect(() => { const handleEvent = (event) => { if (event.cid === channel?.cid) { setActiveChannel(); } }; client.on('channel.deleted', handleEvent); client.on('channel.hidden', handleEvent); return () => { client.off('channel.deleted', handleEvent); client.off('channel.hidden', handleEvent); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [channel?.cid]); const renderChannel = (item) => { const previewProps = { activeChannel: channel, Avatar, channel: item, // forces the update of preview component on channel update channelUpdateCount, getLatestMessagePreview, key: item.cid, Preview, setActiveChannel, watchers, }; return React.createElement(ChannelPreview, { ...previewProps }); }; const baseClass = 'str-chat__channel-list'; const className = clsx(customClasses?.chat ?? 'str-chat', theme, customClasses?.channelList ?? `${baseClass} ${baseClass}-react`, { 'str-chat--windows-flags': useImageFlagEmojisOnWindows && navigator.userAgent.match(/Win/), [`${baseClass}--open`]: navOpen, }); const showChannelList = (!searchActive && !searchIsActive) || additionalChannelSearchProps?.popupResults; return (React.createElement(ChannelListContextProvider, { value: { channels, hasNextPage, loadNextPage, setChannels } }, React.createElement("div", { className: className, ref: channelListRef }, showChannelSearch && (Search ? (React.createElement(Search, { directMessagingChannelType: additionalChannelSearchProps?.channelType, disabled: additionalChannelSearchProps?.disabled, exitSearchOnInputBlur: additionalChannelSearchProps?.clearSearchOnClickOutside, placeholder: additionalChannelSearchProps?.placeholder })) : (React.createElement(ChannelSearch, { onSearch: onSearch, onSearchExit: onSearchExit, setChannels: setChannels, ...additionalChannelSearchProps }))), showChannelList && (React.createElement(List, { error: channelsQueryState.error, loadedChannels: sendChannelsToList ? loadedChannels : undefined, loading: !!channelsQueryState.queryInProgress && ['reload', 'uninitialized'].includes(channelsQueryState.queryInProgress), LoadingErrorIndicator: LoadingErrorIndicator, LoadingIndicator: LoadingIndicator, setChannels: setChannels }, !loadedChannels?.length ? (React.createElement(EmptyStateIndicator, { listType: 'channel' })) : (React.createElement(Paginator, { hasNextPage: hasNextPage, isLoading: channelsQueryState.queryInProgress === 'load-more', loadNextPage: loadNextPage }, renderChannels ? renderChannels(loadedChannels, renderChannel) : loadedChannels.map((channel) => renderChannel(channel))))))))); }; /** * Renders a preview list of Channels, allowing you to select the Channel you want to open */ export const ChannelList = React.memo(UnMemoizedChannelList);