stream-chat-react
Version:
React components to create chat conversations or livestream style chat
196 lines (195 loc) • 7.34 kB
JavaScript
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,
};
};