UNPKG

stream-chat-react

Version:

React components to create chat conversations or livestream style chat

196 lines (195 loc) 7.34 kB
import { useCallback, useEffect, useRef, useState } from 'react'; import debounce from 'lodash.debounce'; import uniqBy from 'lodash.uniqby'; import { isChannel } from '../utils'; import { useChatContext } from '../../../context/ChatContext'; export const useChannelSearch = ({ channelType = 'messaging', clearSearchOnClickOutside = true, disabled = false, onSearch: onSearchCallback, onSearchExit, onSelectResult, searchDebounceIntervalMs = 300, searchForChannels = false, searchForUsers = true, searchFunction, searchQueryParams, setChannels, }) => { const { client, setActiveChannel } = useChatContext('useChannelSearch'); const [inputIsFocused, setInputIsFocused] = useState(false); const [query, setQuery] = useState(''); const [results, setResults] = useState([]); const [searching, setSearching] = useState(false); const searchQueryPromiseInProgress = useRef(false); const shouldIgnoreQueryResults = useRef(false); const inputRef = useRef(null); const searchBarRef = useRef(null); const clearState = useCallback(() => { setQuery(''); setResults([]); setSearching(false); shouldIgnoreQueryResults.current = searchQueryPromiseInProgress.current; }, []); const activateSearch = useCallback(() => { setInputIsFocused(true); }, []); const exitSearch = useCallback(() => { setInputIsFocused(false); inputRef.current?.blur(); clearState(); onSearchExit?.(); }, [clearState, onSearchExit]); useEffect(() => { if (disabled) return; const clickListener = (event) => { if (!(event.target instanceof HTMLElement)) return; const isInputClick = searchBarRef.current?.contains(event.target); if (isInputClick) return; if ((inputIsFocused && !query) || clearSearchOnClickOutside) { exitSearch(); } }; document.addEventListener('click', clickListener); return () => document.removeEventListener('click', clickListener); }, [disabled, inputIsFocused, query, exitSearch, clearSearchOnClickOutside]); useEffect(() => { if (!inputRef.current || disabled) return; const handleKeyDown = (event) => { if (event.key === 'Escape') return exitSearch(); }; inputRef.current.addEventListener('keydown', handleKeyDown); return () => { // eslint-disable-next-line react-hooks/exhaustive-deps inputRef.current?.removeEventListener('keydown', handleKeyDown); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [disabled]); const selectResult = useCallback(async (result) => { if (!client.userID) return; if (onSelectResult) { await onSelectResult({ setQuery, setResults, setSearching, }, result); return; } let selectedChannel; if (isChannel(result)) { setActiveChannel(result); selectedChannel = result; } else { const newChannel = client.channel(channelType, { members: [client.userID, result.id], }); await newChannel.watch(); setActiveChannel(newChannel); selectedChannel = newChannel; } setChannels?.((channels) => uniqBy([selectedChannel, ...channels], 'cid')); if (clearSearchOnClickOutside) { exitSearch(); } }, // eslint-disable-next-line react-hooks/exhaustive-deps [ clearSearchOnClickOutside, client, exitSearch, onSelectResult, setActiveChannel, setChannels, ]); const getChannels = useCallback(async (text) => { if (!searchForChannels && !searchForUsers) return; let results = []; const promises = []; try { if (searchForChannels) { promises.push(client.queryChannels({ members: { $in: [client.userID] }, name: { $autocomplete: text }, ...searchQueryParams?.channelFilters?.filters, }, searchQueryParams?.channelFilters?.sort || {}, { limit: 5, ...searchQueryParams?.channelFilters?.options })); } if (searchForUsers) { promises.push(client.queryUsers({ $or: [{ id: { $autocomplete: text } }, { name: { $autocomplete: text } }], ...searchQueryParams?.userFilters?.filters, }, { id: 1, ...searchQueryParams?.userFilters?.sort }, { limit: 8, ...searchQueryParams?.userFilters?.options })); } if (promises.length) { searchQueryPromiseInProgress.current = true; const resolved = await Promise.all(promises); if (searchForChannels && searchForUsers) { const [channels, { users }] = resolved; results = [...channels, ...users.filter((u) => u.id !== client.user?.id)]; } else if (searchForChannels) { const [channels] = resolved; results = [...channels]; } else if (searchForUsers) { const [{ users }] = resolved; results = [...users.filter((u) => u.id !== client.user?.id)]; } } } catch (error) { console.error(error); } setSearching(false); if (!shouldIgnoreQueryResults.current) { setResults(results); } else { shouldIgnoreQueryResults.current = false; } searchQueryPromiseInProgress.current = false; }, [client, searchForChannels, searchForUsers, searchQueryParams]); // eslint-disable-next-line react-hooks/exhaustive-deps const scheduleGetChannels = useCallback(debounce(getChannels, searchDebounceIntervalMs), [getChannels, searchDebounceIntervalMs]); const onSearch = useCallback((event) => { event.preventDefault(); if (disabled) return; if (searchFunction) { searchFunction({ setQuery, setResults, setSearching, }, event); } else if (!searchForChannels && !searchForUsers) { return; } else if (event.target.value) { setSearching(true); setQuery(event.target.value); scheduleGetChannels(event.target.value); } else if (!event.target.value) { clearState(); scheduleGetChannels.cancel(); } onSearchCallback?.(event); }, [ clearState, disabled, scheduleGetChannels, onSearchCallback, searchForChannels, searchForUsers, searchFunction, ]); return { activateSearch, clearState, exitSearch, inputIsFocused, inputRef, onSearch, query, results, searchBarRef, searching, selectResult, }; };