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 = {
|
declare type SearchState = {
|
||||||
options: SearchOptions,
|
options: SearchOptions,
|
||||||
resultsByQuery: {},
|
resultsByQuery: {},
|
||||||
|
results: Array<string>,
|
||||||
hasReachedMaxResultsLength: {},
|
hasReachedMaxResultsLength: {},
|
||||||
searching: boolean,
|
searching: boolean,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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) */
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
);
|
);
|
||||||
|
|
|
@ -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}`);
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
Loading…
Reference in a new issue