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 = {
options: SearchOptions,
resultsByQuery: {},
results: Array<string>,
hasReachedMaxResultsLength: {},
searching: boolean,
};

View file

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

View file

@ -1,11 +1,13 @@
// @flow
import { EMOTES_48px as EMOTES } from 'constants/emotes';
import { matchSorter } from 'match-sorter';
import { SEARCH_OPTIONS } from 'constants/search';
import * as KEYCODES from 'constants/keycodes';
import Autocomplete from '@mui/material/Autocomplete';
import React from 'react';
import TextareaSuggestionsItem from 'component/textareaSuggestionsItem';
import TextField from '@mui/material/TextField';
import useLighthouse from 'effects/use-lighthouse';
import useThrottle from 'effects/use-throttle';
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 = {
canonicalCommentors?: Array<string>,
canonicalCreatorUri?: string,
canonicalSearch?: Array<string>,
canonicalSubscriptions?: Array<string>,
className?: string,
commentorUris?: Array<string>,
@ -36,10 +43,12 @@ type Props = {
isLivestream?: boolean,
maxLength?: number,
placeholder?: string,
showMature: boolean,
type?: string,
uri?: string,
value: any,
doResolveUris: (Array<string>) => void,
doSetSearchResults: (Array<string>) => void,
onBlur: (any) => any,
onChange: (any) => any,
onFocus: (any) => any,
@ -49,6 +58,7 @@ export default function TextareaWithSuggestions(props: Props) {
const {
canonicalCommentors,
canonicalCreatorUri,
canonicalSearch,
canonicalSubscriptions: canonicalSubs,
className,
commentorUris,
@ -58,9 +68,11 @@ export default function TextareaWithSuggestions(props: Props) {
isLivestream,
maxLength,
placeholder,
showMature,
type,
value: messageValue,
doResolveUris,
doSetSearchResults,
onBlur,
onChange,
onFocus,
@ -72,13 +84,27 @@ export default function TextareaWithSuggestions(props: Props) {
const [selectedValue, setSelectedValue] = React.useState(undefined);
const [highlightedSuggestion, setHighlightedSuggestion] = React.useState('');
const [shouldClose, setClose] = React.useState();
const [debouncedTerm, setDebouncedTerm] = React.useState('');
// const [mostSupported, setMostSupported] = React.useState('');
const suggestionTerm = suggestionValue && suggestionValue.term;
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 filteredCommentors = canonicalCommentors && canonicalCommentors.filter((uri) => shouldFilter(uri));
const filteredSubs = canonicalSubs && canonicalSubs.filter((uri) => shouldFilter(uri, filteredCommentors));
const filteredSearch =
canonicalSearch &&
canonicalSearch.filter((uri) => shouldFilter(uri, filteredSubs) && shouldFilter(uri, filteredCommentors));
const allOptions = [];
if (isEmote) {
@ -88,6 +114,7 @@ export default function TextareaWithSuggestions(props: Props) {
if (canonicalCreatorUri) allOptions.push(canonicalCreatorUri);
if (filteredSubs) allOptions.push(...filteredSubs);
if (filteredCommentors) allOptions.push(...filteredCommentors);
if (filteredSearch) allOptions.push(...filteredSearch);
}
const allOptionsGrouped =
@ -97,7 +124,8 @@ export default function TextareaWithSuggestions(props: Props) {
? __('Emotes')
: (canonicalCreatorUri === option && __('Creator')) ||
(filteredSubs && filteredSubs.includes(option) && __('Following')) ||
(filteredCommentors && filteredCommentors.includes(option) && __('From comments'));
(filteredCommentors && filteredCommentors.includes(option) && __('From Comments')) ||
(filteredSearch && filteredSearch.includes(option) && __('From Search'));
return {
label: isEmote ? option : option.replace('lbry://', '').replace('#', ':'),
@ -211,6 +239,26 @@ export default function TextareaWithSuggestions(props: Props) {
/** 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
React.useEffect(() => {
if (!isLivestream) return;
@ -288,8 +336,8 @@ export default function TextareaWithSuggestions(props: Props) {
groupBy={(option) => option.group}
id={id}
inputValue={messageValue}
loading={!allMatches || allMatches.length === 0}
loadingText={__('Nothing found')}
loading={!allMatches || allMatches.length === 0 || showPlaceholder}
loadingText={results || showPlaceholder ? __('Searching...') : __('Nothing found')}
onBlur={() => onBlur && onBlur()}
/* 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) */

View file

@ -230,6 +230,7 @@ export const SEARCH_SUCCESS = 'SEARCH_SUCCESS';
export const SEARCH_FAIL = 'SEARCH_FAIL';
export const UPDATE_SEARCH_OPTIONS = 'UPDATE_SEARCH_OPTIONS';
export const UPDATE_SEARCH_SUGGESTIONS = 'UPDATE_SEARCH_SUGGESTIONS';
export const SET_SEARCH_RESULTS = 'SET_SEARCH_RESULTS';
// Settings
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) => {
const state = getState();
const claim = selectClaimForUri(state, uri);

View file

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

View file

@ -3,6 +3,7 @@ import { createSelector } from 'reselect';
import { createCachedSelector } from 're-reselect';
import { selectMutedChannels } from 'redux/selectors/blocked';
import { selectShowMatureContent } from 'redux/selectors/settings';
import { selectSearchResults } from 'redux/selectors/search';
import { selectBlacklistedOutpointMap, selectFilteredOutpointMap } from 'lbryinc';
import {
selectClaimsById,
@ -397,11 +398,13 @@ export const selectChannelMentionData = createCachedSelector(
selectClaimsById,
selectTopLevelCommentsForUri,
selectSubscriptionUris,
(uri, claimIdsByUri, claimsById, topLevelComments, subscriptionUris) => {
selectSearchResults,
(uri, claimIdsByUri, claimsById, topLevelComments, subscriptionUris, searchUris) => {
let canonicalCreatorUri;
const commentorUris = [];
const canonicalCommentors = [];
const canonicalSubscriptions = [];
const canonicalSearch = [];
if (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}`);

View file

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