// @flow import { Combobox, ComboboxInput, ComboboxPopover, ComboboxList } from '@reach/combobox'; import { Form } from 'component/common/form'; import { parseURI } from 'lbry-redux'; import { SEARCH_OPTIONS } from 'constants/search'; import * as KEYCODES from 'constants/keycodes'; import ChannelMentionSuggestion from 'component/channelMentionSuggestion'; import ChannelMentionTopSuggestion from 'component/channelMentionTopSuggestion'; import React from 'react'; import Spinner from 'component/spinner'; import type { ElementRef } from 'react'; import useLighthouse from 'effects/use-lighthouse'; const INPUT_DEBOUNCE_MS = 1000; const LIGHTHOUSE_MIN_CHARACTERS = 3; type Props = { inputRef: any, mentionTerm: string, noTopSuggestion?: boolean, showMature: boolean, creatorUri: string, isLivestream: boolean, commentorUris: Array, unresolvedCommentors: Array, subscriptionUris: Array, unresolvedSubscriptions: Array, doResolveUris: (Array) => void, customSelectAction?: (string) => void, }; export default function ChannelMentionSuggestions(props: Props) { const { unresolvedCommentors, unresolvedSubscriptions, isLivestream, creatorUri, inputRef, showMature, noTopSuggestion, mentionTerm, doResolveUris, customSelectAction, } = props; const comboboxInputRef: ElementRef = React.useRef(); const comboboxListRef: ElementRef = React.useRef(); const [debouncedTerm, setDebouncedTerm] = React.useState(''); const isRefFocused = (ref) => ref && ref.current && ref.current === document.activeElement; let subscriptionUris = props.subscriptionUris.filter((uri) => uri !== creatorUri); let commentorUris = props.commentorUris.filter((uri) => uri !== creatorUri && !subscriptionUris.includes(uri)); const termToMatch = mentionTerm && mentionTerm.replace('@', '').toLowerCase(); const allShownUris = [creatorUri, ...subscriptionUris, ...commentorUris]; const possibleMatches = allShownUris.filter((uri) => { try { const { channelName } = parseURI(uri); return channelName.toLowerCase().includes(termToMatch); } catch (e) {} }); const hasSubscriptionsResolved = subscriptionUris && !subscriptionUris.every((uri) => unresolvedSubscriptions && unresolvedSubscriptions.includes(uri)); const hasCommentorsShown = commentorUris.length > 0 && commentorUris.some((uri) => possibleMatches && possibleMatches.includes(uri)); const searchSize = 5; const additionalOptions = { isBackgroundSearch: false, [SEARCH_OPTIONS.CLAIM_TYPE]: SEARCH_OPTIONS.INCLUDE_CHANNELS }; const { results, loading } = useLighthouse(debouncedTerm, showMature, searchSize, additionalOptions, 0); const stringifiedResults = JSON.stringify(results); const hasMinLength = mentionTerm && mentionTerm.length >= LIGHTHOUSE_MIN_CHARACTERS; const isTyping = debouncedTerm !== mentionTerm; const showPlaceholder = isTyping || loading; const handleSelect = React.useCallback( (value) => { if (customSelectAction) { // Give them full results, as our resolved one might truncate the claimId. customSelectAction(value || (results && results.find((r) => r.startsWith(value))) || ''); } if (inputRef && inputRef.current) inputRef.current.focus(); }, [customSelectAction, inputRef, results] ); React.useEffect(() => { const timer = setTimeout(() => { if (isTyping) setDebouncedTerm(!hasMinLength ? '' : mentionTerm); }, INPUT_DEBOUNCE_MS); return () => clearTimeout(timer); }, [isTyping, mentionTerm, hasMinLength, possibleMatches.length]); React.useEffect(() => { if (!inputRef) return; if (mentionTerm) { inputRef.current.classList.add('textarea-mention'); } else { inputRef.current.classList.remove('textarea-mention'); } }, [inputRef, mentionTerm]); React.useEffect(() => { if (!inputRef || !comboboxInputRef || !mentionTerm) return; function handleKeyDown(event) { const { keyCode } = event; const activeElement = document.activeElement; if (keyCode === KEYCODES.UP || keyCode === KEYCODES.DOWN) { if (isRefFocused(comboboxInputRef)) { const selectedId = activeElement && activeElement.getAttribute('aria-activedescendant'); const selectedItem = selectedId && document.querySelector(`li[id="${selectedId}"]`); if (selectedItem) selectedItem.scrollIntoView({ block: 'nearest', inline: 'nearest' }); } else { comboboxInputRef.current.focus(); } } else { if (keyCode === KEYCODES.TAB) { event.preventDefault(); const activeValue = activeElement && activeElement.getAttribute('value'); if (activeValue) { handleSelect(activeValue); } else if (possibleMatches.length) { handleSelect(possibleMatches[0]); } else if (results) { handleSelect(mentionTerm); } } inputRef.current.focus(); } } window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [handleSelect, inputRef, mentionTerm, possibleMatches, results]); React.useEffect(() => { if (!stringifiedResults) return; const arrayResults = JSON.parse(stringifiedResults); if (arrayResults && arrayResults.length > 0) doResolveUris(arrayResults); }, [doResolveUris, stringifiedResults]); // Only resolve commentors on Livestreams if actually mentioning/looking for it React.useEffect(() => { if (isLivestream && unresolvedCommentors && mentionTerm) doResolveUris(unresolvedCommentors); }, [doResolveUris, isLivestream, mentionTerm, unresolvedCommentors]); // Only resolve the subscriptions that match the mention term, instead of all React.useEffect(() => { if (isTyping) return; let urisToResolve = []; subscriptionUris.map( (uri) => hasMinLength && possibleMatches.includes(uri) && unresolvedSubscriptions.includes(uri) && urisToResolve.push(uri) ); if (urisToResolve.length > 0) doResolveUris(urisToResolve); }, [doResolveUris, hasMinLength, isTyping, possibleMatches, subscriptionUris, unresolvedSubscriptions]); const suggestionsRow = (label: string, suggestions: Array, hasSuggestionsBelow: boolean) => { if (mentionTerm !== '@' && suggestions !== results) { suggestions = suggestions.filter((uri) => possibleMatches.includes(uri)); } else if (suggestions === results) { suggestions = suggestions.filter((uri) => !allShownUris.includes(uri)); } return !suggestions.length ? null : ( <>
{label}
{suggestions.map((uri) => ( ))} {hasSuggestionsBelow &&
} ); }; return (
handleSelect(mentionTerm)}> {mentionTerm && ( {creatorUri && suggestionsRow( __('Creator'), [creatorUri], hasSubscriptionsResolved || hasCommentorsShown || !showPlaceholder )} {hasSubscriptionsResolved && suggestionsRow(__('Following'), subscriptionUris, hasCommentorsShown || !showPlaceholder)} {commentorUris.length > 0 && suggestionsRow(__('From comments'), commentorUris, !showPlaceholder)} {showPlaceholder ? hasMinLength && : results && ( <> {!noTopSuggestion && } {suggestionsRow(__('From search'), results, false)} )} )}
); }