Add back and improved support for searching while mentioning

This commit is contained in:
Rafael 2021-12-06 16:39:39 -03:00 committed by Thomas Zarebczan
parent 6faaf78fc0
commit db5f24ae28
8 changed files with 87 additions and 6 deletions

View file

@ -29,6 +29,7 @@ declare type SearchOptions = {
declare type SearchState = { declare type SearchState = {
options: SearchOptions, options: SearchOptions,
resultsByQuery: {}, resultsByQuery: {},
results: Array<string>,
hasReachedMaxResultsLength: {}, hasReachedMaxResultsLength: {},
searching: boolean, searching: boolean,
}; };

View file

@ -3,6 +3,7 @@ import { doResolveUris } from 'redux/actions/claims';
import { MAX_LIVESTREAM_COMMENTS } from 'constants/livestream'; import { MAX_LIVESTREAM_COMMENTS } from 'constants/livestream';
import { selectChannelMentionData } from 'redux/selectors/comments'; import { selectChannelMentionData } from 'redux/selectors/comments';
import { selectShowMatureContent } from 'redux/selectors/settings'; import { selectShowMatureContent } from 'redux/selectors/settings';
import { doSetSearchResults } from 'redux/actions/search';
import { withRouter } from 'react-router'; import { withRouter } from 'react-router';
import TextareaWithSuggestions from './view'; import TextareaWithSuggestions from './view';
@ -12,11 +13,12 @@ const select = (state, props) => {
const uri = `lbry:/${pathname.replaceAll(':', '#')}`; const uri = `lbry:/${pathname.replaceAll(':', '#')}`;
const data = selectChannelMentionData(state, uri, maxComments); const data = selectChannelMentionData(state, uri, maxComments);
const { canonicalCommentors, canonicalCreatorUri, canonicalSubscriptions, commentorUris } = data; const { canonicalCommentors, canonicalCreatorUri, canonicalSubscriptions, commentorUris, canonicalSearch } = data;
return { return {
canonicalCommentors, canonicalCommentors,
canonicalCreatorUri, canonicalCreatorUri,
canonicalSearch,
canonicalSubscriptions, canonicalSubscriptions,
commentorUris, commentorUris,
showMature: selectShowMatureContent(state), showMature: selectShowMatureContent(state),
@ -25,6 +27,7 @@ const select = (state, props) => {
const perform = (dispatch) => ({ const perform = (dispatch) => ({
doResolveUris: (uris) => dispatch(doResolveUris(uris, true)), doResolveUris: (uris) => dispatch(doResolveUris(uris, true)),
doSetSearchResults: (uris) => dispatch(doSetSearchResults(uris)),
}); });
export default withRouter(connect(select, perform)(TextareaWithSuggestions)); export default withRouter(connect(select, perform)(TextareaWithSuggestions));

View file

@ -1,11 +1,13 @@
// @flow // @flow
import { EMOTES_48px as EMOTES } from 'constants/emotes'; import { EMOTES_48px as EMOTES } from 'constants/emotes';
import { matchSorter } from 'match-sorter'; import { matchSorter } from 'match-sorter';
import { SEARCH_OPTIONS } from 'constants/search';
import * as KEYCODES from 'constants/keycodes'; import * as KEYCODES from 'constants/keycodes';
import Autocomplete from '@mui/material/Autocomplete'; import Autocomplete from '@mui/material/Autocomplete';
import React from 'react'; import React from 'react';
import TextareaSuggestionsItem from 'component/textareaSuggestionsItem'; import TextareaSuggestionsItem from 'component/textareaSuggestionsItem';
import TextField from '@mui/material/TextField'; import TextField from '@mui/material/TextField';
import useLighthouse from 'effects/use-lighthouse';
import useThrottle from 'effects/use-throttle'; import useThrottle from 'effects/use-throttle';
const SUGGESTION_REGEX = new RegExp( const SUGGESTION_REGEX = new RegExp(
@ -24,9 +26,14 @@ const SUGGESTION_REGEX = new RegExp(
* *
*/ */
const SEARCH_SIZE = 10;
const LIGHTHOUSE_MIN_CHARACTERS = 3;
const INPUT_DEBOUNCE_MS = 1000;
type Props = { type Props = {
canonicalCommentors?: Array<string>, canonicalCommentors?: Array<string>,
canonicalCreatorUri?: string, canonicalCreatorUri?: string,
canonicalSearch?: Array<string>,
canonicalSubscriptions?: Array<string>, canonicalSubscriptions?: Array<string>,
className?: string, className?: string,
commentorUris?: Array<string>, commentorUris?: Array<string>,
@ -36,10 +43,12 @@ type Props = {
isLivestream?: boolean, isLivestream?: boolean,
maxLength?: number, maxLength?: number,
placeholder?: string, placeholder?: string,
showMature: boolean,
type?: string, type?: string,
uri?: string, uri?: string,
value: any, value: any,
doResolveUris: (Array<string>) => void, doResolveUris: (Array<string>) => void,
doSetSearchResults: (Array<string>) => void,
onBlur: (any) => any, onBlur: (any) => any,
onChange: (any) => any, onChange: (any) => any,
onFocus: (any) => any, onFocus: (any) => any,
@ -49,6 +58,7 @@ export default function TextareaWithSuggestions(props: Props) {
const { const {
canonicalCommentors, canonicalCommentors,
canonicalCreatorUri, canonicalCreatorUri,
canonicalSearch,
canonicalSubscriptions: canonicalSubs, canonicalSubscriptions: canonicalSubs,
className, className,
commentorUris, commentorUris,
@ -58,9 +68,11 @@ export default function TextareaWithSuggestions(props: Props) {
isLivestream, isLivestream,
maxLength, maxLength,
placeholder, placeholder,
showMature,
type, type,
value: messageValue, value: messageValue,
doResolveUris, doResolveUris,
doSetSearchResults,
onBlur, onBlur,
onChange, onChange,
onFocus, onFocus,
@ -72,13 +84,27 @@ export default function TextareaWithSuggestions(props: Props) {
const [selectedValue, setSelectedValue] = React.useState(undefined); const [selectedValue, setSelectedValue] = React.useState(undefined);
const [highlightedSuggestion, setHighlightedSuggestion] = React.useState(''); const [highlightedSuggestion, setHighlightedSuggestion] = React.useState('');
const [shouldClose, setClose] = React.useState(); const [shouldClose, setClose] = React.useState();
const [debouncedTerm, setDebouncedTerm] = React.useState('');
// const [mostSupported, setMostSupported] = React.useState('');
const suggestionTerm = suggestionValue && suggestionValue.term; const suggestionTerm = suggestionValue && suggestionValue.term;
const isEmote = suggestionValue && suggestionValue.isEmote; const isEmote = suggestionValue && suggestionValue.isEmote;
const isMention = suggestionValue && !suggestionValue.isEmote;
const additionalOptions = { isBackgroundSearch: false, [SEARCH_OPTIONS.CLAIM_TYPE]: SEARCH_OPTIONS.INCLUDE_CHANNELS };
const { results, loading } = useLighthouse(debouncedTerm, showMature, SEARCH_SIZE, additionalOptions, 0);
const stringifiedResults = JSON.stringify(results);
const hasMinLength = suggestionTerm && isMention && suggestionTerm.length >= LIGHTHOUSE_MIN_CHARACTERS;
const isTyping = isMention && debouncedTerm !== suggestionTerm;
const showPlaceholder = isMention && (isTyping || loading);
const shouldFilter = (uri, previous) => uri !== canonicalCreatorUri && (!previous || !previous.includes(uri)); const shouldFilter = (uri, previous) => uri !== canonicalCreatorUri && (!previous || !previous.includes(uri));
const filteredCommentors = canonicalCommentors && canonicalCommentors.filter((uri) => shouldFilter(uri)); const filteredCommentors = canonicalCommentors && canonicalCommentors.filter((uri) => shouldFilter(uri));
const filteredSubs = canonicalSubs && canonicalSubs.filter((uri) => shouldFilter(uri, filteredCommentors)); const filteredSubs = canonicalSubs && canonicalSubs.filter((uri) => shouldFilter(uri, filteredCommentors));
const filteredSearch =
canonicalSearch &&
canonicalSearch.filter((uri) => shouldFilter(uri, filteredSubs) && shouldFilter(uri, filteredCommentors));
const allOptions = []; const allOptions = [];
if (isEmote) { if (isEmote) {
@ -88,6 +114,7 @@ export default function TextareaWithSuggestions(props: Props) {
if (canonicalCreatorUri) allOptions.push(canonicalCreatorUri); if (canonicalCreatorUri) allOptions.push(canonicalCreatorUri);
if (filteredSubs) allOptions.push(...filteredSubs); if (filteredSubs) allOptions.push(...filteredSubs);
if (filteredCommentors) allOptions.push(...filteredCommentors); if (filteredCommentors) allOptions.push(...filteredCommentors);
if (filteredSearch) allOptions.push(...filteredSearch);
} }
const allOptionsGrouped = const allOptionsGrouped =
@ -97,7 +124,8 @@ export default function TextareaWithSuggestions(props: Props) {
? __('Emotes') ? __('Emotes')
: (canonicalCreatorUri === option && __('Creator')) || : (canonicalCreatorUri === option && __('Creator')) ||
(filteredSubs && filteredSubs.includes(option) && __('Following')) || (filteredSubs && filteredSubs.includes(option) && __('Following')) ||
(filteredCommentors && filteredCommentors.includes(option) && __('From comments')); (filteredCommentors && filteredCommentors.includes(option) && __('From Comments')) ||
(filteredSearch && filteredSearch.includes(option) && __('From Search'));
return { return {
label: isEmote ? option : option.replace('lbry://', '').replace('#', ':'), label: isEmote ? option : option.replace('lbry://', '').replace('#', ':'),
@ -211,6 +239,26 @@ export default function TextareaWithSuggestions(props: Props) {
/** Effects **/ /** Effects **/
/** ------- **/ /** ------- **/
React.useEffect(() => {
if (!isMention) return;
const timer = setTimeout(() => {
if (isTyping && suggestionTerm) setDebouncedTerm(!hasMinLength ? '' : suggestionTerm);
}, INPUT_DEBOUNCE_MS);
return () => clearTimeout(timer);
}, [hasMinLength, isMention, isTyping, suggestionTerm]);
React.useEffect(() => {
if (!stringifiedResults) return;
const arrayResults = JSON.parse(stringifiedResults);
if (arrayResults && arrayResults.length > 0) {
doResolveUris(arrayResults);
doSetSearchResults(arrayResults);
}
}, [doResolveUris, doSetSearchResults, stringifiedResults]);
// Disable sending on Enter on Livestream chat // Disable sending on Enter on Livestream chat
React.useEffect(() => { React.useEffect(() => {
if (!isLivestream) return; if (!isLivestream) return;
@ -288,8 +336,8 @@ export default function TextareaWithSuggestions(props: Props) {
groupBy={(option) => option.group} groupBy={(option) => option.group}
id={id} id={id}
inputValue={messageValue} inputValue={messageValue}
loading={!allMatches || allMatches.length === 0} loading={!allMatches || allMatches.length === 0 || showPlaceholder}
loadingText={__('Nothing found')} loadingText={results || showPlaceholder ? __('Searching...') : __('Nothing found')}
onBlur={() => onBlur && onBlur()} onBlur={() => onBlur && onBlur()}
/* Different from onInputChange, onChange is only used for the selected value, /* Different from onInputChange, onChange is only used for the selected value,
so here it is acting simply as a selection handler (see it as onSelect) */ so here it is acting simply as a selection handler (see it as onSelect) */

View file

@ -230,6 +230,7 @@ export const SEARCH_SUCCESS = 'SEARCH_SUCCESS';
export const SEARCH_FAIL = 'SEARCH_FAIL'; export const SEARCH_FAIL = 'SEARCH_FAIL';
export const UPDATE_SEARCH_OPTIONS = 'UPDATE_SEARCH_OPTIONS'; export const UPDATE_SEARCH_OPTIONS = 'UPDATE_SEARCH_OPTIONS';
export const UPDATE_SEARCH_SUGGESTIONS = 'UPDATE_SEARCH_SUGGESTIONS'; export const UPDATE_SEARCH_SUGGESTIONS = 'UPDATE_SEARCH_SUGGESTIONS';
export const SET_SEARCH_RESULTS = 'SET_SEARCH_RESULTS';
// Settings // Settings
export const DAEMON_SETTINGS_RECEIVED = 'DAEMON_SETTINGS_RECEIVED'; export const DAEMON_SETTINGS_RECEIVED = 'DAEMON_SETTINGS_RECEIVED';

View file

@ -147,6 +147,13 @@ export const doUpdateSearchOptions = (newOptions: SearchOptions, additionalOptio
} }
}; };
export const doSetSearchResults = (uris: Array<string>) => (dispatch: Dispatch) => {
dispatch({
type: ACTIONS.SET_SEARCH_RESULTS,
data: { uris },
});
};
export const doFetchRecommendedContent = (uri: string) => (dispatch: Dispatch, getState: GetState) => { export const doFetchRecommendedContent = (uri: string) => (dispatch: Dispatch, getState: GetState) => {
const state = getState(); const state = getState();
const claim = selectClaimForUri(state, uri); const claim = selectClaimForUri(state, uri);

View file

@ -20,6 +20,7 @@ const defaultState: SearchState = {
resultsByQuery: {}, resultsByQuery: {},
hasReachedMaxResultsLength: {}, hasReachedMaxResultsLength: {},
searching: false, searching: false,
results: [],
}; };
export default handleActions( export default handleActions(
@ -66,6 +67,11 @@ export default handleActions(
options, options,
}; };
}, },
[ACTIONS.SET_SEARCH_RESULTS]: (state: SearchState, action: SearchSuccess): SearchState => ({
...state,
results: action.data.uris,
}),
}, },
defaultState defaultState
); );

View file

@ -3,6 +3,7 @@ import { createSelector } from 'reselect';
import { createCachedSelector } from 're-reselect'; import { createCachedSelector } from 're-reselect';
import { selectMutedChannels } from 'redux/selectors/blocked'; import { selectMutedChannels } from 'redux/selectors/blocked';
import { selectShowMatureContent } from 'redux/selectors/settings'; import { selectShowMatureContent } from 'redux/selectors/settings';
import { selectSearchResults } from 'redux/selectors/search';
import { selectBlacklistedOutpointMap, selectFilteredOutpointMap } from 'lbryinc'; import { selectBlacklistedOutpointMap, selectFilteredOutpointMap } from 'lbryinc';
import { import {
selectClaimsById, selectClaimsById,
@ -397,11 +398,13 @@ export const selectChannelMentionData = createCachedSelector(
selectClaimsById, selectClaimsById,
selectTopLevelCommentsForUri, selectTopLevelCommentsForUri,
selectSubscriptionUris, selectSubscriptionUris,
(uri, claimIdsByUri, claimsById, topLevelComments, subscriptionUris) => { selectSearchResults,
(uri, claimIdsByUri, claimsById, topLevelComments, subscriptionUris, searchUris) => {
let canonicalCreatorUri; let canonicalCreatorUri;
const commentorUris = []; const commentorUris = [];
const canonicalCommentors = []; const canonicalCommentors = [];
const canonicalSubscriptions = []; const canonicalSubscriptions = [];
const canonicalSearch = [];
if (uri) { if (uri) {
const claimId = claimIdsByUri[uri]; const claimId = claimIdsByUri[uri];
@ -434,6 +437,17 @@ export const selectChannelMentionData = createCachedSelector(
} }
}); });
return { canonicalCommentors, canonicalCreatorUri, canonicalSubscriptions, commentorUris }; if (searchUris && searchUris.length > 0) {
searchUris.forEach((uri) => {
// Update: canonicalSubscriptions
const claimId = claimIdsByUri[uri];
const claim = claimsById[claimId];
if (claim && claim.canonical_url) {
canonicalSearch.push(claim.canonical_url);
}
});
}
return { canonicalCommentors, canonicalCreatorUri, canonicalSubscriptions, commentorUris, canonicalSearch };
} }
)((state, uri, maxCount) => `${String(uri)}:${maxCount}`); )((state, uri, maxCount) => `${String(uri)}:${maxCount}`);

View file

@ -32,6 +32,7 @@ export const selectSearchResultByQuery: (state: State) => { [string]: Array<stri
selectState(state).resultsByQuery; selectState(state).resultsByQuery;
export const selectHasReachedMaxResultsLength: (state: State) => { [boolean]: Array<boolean> } = (state) => export const selectHasReachedMaxResultsLength: (state: State) => { [boolean]: Array<boolean> } = (state) =>
selectState(state).hasReachedMaxResultsLength; selectState(state).hasReachedMaxResultsLength;
export const selectSearchResults: (state: State) => Array<string> = (state) => selectState(state).results;
export const makeSelectSearchUrisForQuery = (query: string): ((state: State) => Array<string>) => export const makeSelectSearchUrisForQuery = (query: string): ((state: State) => Array<string>) =>
createSelector(selectSearchResultByQuery, (byQuery) => { createSelector(selectSearchResultByQuery, (byQuery) => {