Add back and improved support for searching while mentioning
This commit is contained in:
parent
6faaf78fc0
commit
db5f24ae28
8 changed files with 87 additions and 6 deletions
1
flow-typed/search.js
vendored
1
flow-typed/search.js
vendored
|
@ -29,6 +29,7 @@ declare type SearchOptions = {
|
|||
declare type SearchState = {
|
||||
options: SearchOptions,
|
||||
resultsByQuery: {},
|
||||
results: Array<string>,
|
||||
hasReachedMaxResultsLength: {},
|
||||
searching: boolean,
|
||||
};
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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) */
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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}`);
|
||||
|
|
|
@ -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) => {
|
||||
|
|
Loading…
Reference in a new issue