add search code from lbry-redux

This commit is contained in:
Sean Yesmunt 2020-07-27 16:04:12 -04:00
parent 142e695150
commit 0df388280e
19 changed files with 672 additions and 53 deletions

84
flow-typed/search.js vendored Normal file
View file

@ -0,0 +1,84 @@
// @flow
import * as ACTIONS from 'constants/action_types';
declare type SearchSuggestion = {
value: string,
shorthand: string,
type: string,
};
declare type SearchOptions = {
// :(
// https://github.com/facebook/flow/issues/6492
RESULT_COUNT: number,
CLAIM_TYPE: string,
INCLUDE_FILES: string,
INCLUDE_CHANNELS: string,
INCLUDE_FILES_AND_CHANNELS: string,
MEDIA_AUDIO: string,
MEDIA_VIDEO: string,
MEDIA_TEXT: string,
MEDIA_IMAGE: string,
MEDIA_APPLICATION: string,
};
declare type SearchState = {
isActive: boolean,
searchQuery: string,
options: SearchOptions,
suggestions: { [string]: Array<SearchSuggestion> },
urisByQuery: {},
resolvedResultsByQuery: {},
resolvedResultsByQueryLastPageReached: {},
};
declare type SearchSuccess = {
type: ACTIONS.SEARCH_SUCCESS,
data: {
query: string,
uris: Array<string>,
},
};
declare type UpdateSearchQuery = {
type: ACTIONS.UPDATE_SEARCH_QUERY,
data: {
query: string,
},
};
declare type UpdateSearchSuggestions = {
type: ACTIONS.UPDATE_SEARCH_SUGGESTIONS,
data: {
query: string,
suggestions: Array<SearchSuggestion>,
},
};
declare type UpdateSearchOptions = {
type: ACTIONS.UPDATE_SEARCH_OPTIONS,
data: SearchOptions,
};
declare type ResolvedSearchResult = {
channel: string,
channel_claim_id: string,
claimId: string,
duration: number,
fee: number,
name: string,
nsfw: boolean,
release_time: string,
thumbnail_url: string,
title: string,
};
declare type ResolvedSearchSuccess = {
type: ACTIONS.RESOLVED_SEARCH_SUCCESS,
data: {
append: boolean,
pageSize: number,
results: Array<ResolvedSearchResult>,
query: string,
},
};

View file

@ -135,7 +135,7 @@
"imagesloaded": "^4.1.4", "imagesloaded": "^4.1.4",
"json-loader": "^0.5.4", "json-loader": "^0.5.4",
"lbry-format": "https://github.com/lbryio/lbry-format.git", "lbry-format": "https://github.com/lbryio/lbry-format.git",
"lbry-redux": "lbryio/lbry-redux#a1d5ce7e7e854c1c11e630609ef06a98e3b100c1", "lbry-redux": "lbryio/lbry-redux#021e7e3b798efeea467ce6437cd181dfa2e6badc",
"lbryinc": "lbryio/lbryinc#cff5dd60934c4c6080e135f47ebbece1548c658c", "lbryinc": "lbryio/lbryinc#cff5dd60934c4c6080e135f47ebbece1548c658c",
"lint-staged": "^7.0.2", "lint-staged": "^7.0.2",
"localforage": "^1.7.1", "localforage": "^1.7.1",

View file

@ -1,11 +1,7 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import { makeSelectClaimForUri, makeSelectClaimIsNsfw } from 'lbry-redux';
makeSelectClaimForUri, import { doSearch } from 'redux/actions/search';
makeSelectClaimIsNsfw, import { makeSelectRecommendedContentForUri, selectIsSearching } from 'redux/selectors/search';
doSearch,
makeSelectRecommendedContentForUri,
selectIsSearching,
} from 'lbry-redux';
import { selectUserVerifiedEmail } from 'redux/selectors/user'; import { selectUserVerifiedEmail } from 'redux/selectors/user';
import RecommendedVideos from './view'; import RecommendedVideos from './view';

View file

@ -1,5 +1,6 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { selectSearchOptions, doUpdateSearchOptions, makeSelectQueryWithOptions } from 'lbry-redux'; import { doUpdateSearchOptions } from 'redux/actions/search';
import { selectSearchOptions, makeSelectQueryWithOptions } from 'redux/selectors/search';
import { doToggleSearchExpanded } from 'redux/actions/app'; import { doToggleSearchExpanded } from 'redux/actions/app';
import { selectSearchOptionsExpanded } from 'redux/selectors/app'; import { selectSearchOptionsExpanded } from 'redux/selectors/app';
import SearchOptions from './view'; import SearchOptions from './view';
@ -18,7 +19,4 @@ const perform = (dispatch, ownProps) => {
}; };
}; };
export default connect( export default connect(select, perform)(SearchOptions);
select,
perform
)(SearchOptions);

View file

@ -1,7 +1,7 @@
// @flow // @flow
import { SEARCH_OPTIONS } from 'constants/search';
import * as ICONS from 'constants/icons'; import * as ICONS from 'constants/icons';
import React from 'react'; import React from 'react';
import { SEARCH_OPTIONS } from 'lbry-redux';
import { Form, FormField } from 'component/common/form'; import { Form, FormField } from 'component/common/form';
import Button from 'component/button'; import Button from 'component/button';

View file

@ -1,13 +1,7 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import { SETTINGS } from 'lbry-redux';
doFocusSearchInput, import { doFocusSearchInput, doBlurSearchInput, doUpdateSearchQuery } from 'redux/actions/search';
doBlurSearchInput, import { selectSearchValue, selectSearchSuggestions, selectSearchBarFocused } from 'redux/selectors/search';
doUpdateSearchQuery,
selectSearchValue,
selectSearchSuggestions,
selectSearchBarFocused,
SETTINGS,
} from 'lbry-redux';
import { makeSelectClientSetting } from 'redux/selectors/settings'; import { makeSelectClientSetting } from 'redux/selectors/settings';
import { doToast } from 'redux/actions/notifications'; import { doToast } from 'redux/actions/notifications';
import analytics from 'analytics'; import analytics from 'analytics';

View file

@ -1,10 +1,11 @@
// @flow // @flow
import { URL, URL_LOCAL, URL_DEV } from 'config'; import { URL, URL_LOCAL, URL_DEV } from 'config';
import { SEARCH_TYPES } from 'constants/search';
import * as PAGES from 'constants/pages'; import * as PAGES from 'constants/pages';
import * as ICONS from 'constants/icons'; import * as ICONS from 'constants/icons';
import React from 'react'; import React from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import { normalizeURI, SEARCH_TYPES, isURIValid } from 'lbry-redux'; import { normalizeURI, isURIValid } from 'lbry-redux';
import { withRouter } from 'react-router'; import { withRouter } from 'react-router';
import Icon from 'component/common/icon'; import Icon from 'component/common/icon';
import Autocomplete from './internal/autocomplete'; import Autocomplete from './internal/autocomplete';

View file

@ -116,8 +116,14 @@ export const FILE_DELETE = 'FILE_DELETE';
export const SEARCH_START = 'SEARCH_START'; export const SEARCH_START = 'SEARCH_START';
export const SEARCH_SUCCESS = 'SEARCH_SUCCESS'; export const SEARCH_SUCCESS = 'SEARCH_SUCCESS';
export const SEARCH_FAIL = 'SEARCH_FAIL'; export const SEARCH_FAIL = 'SEARCH_FAIL';
export const RESOLVED_SEARCH_START = 'RESOLVED_SEARCH_START';
export const RESOLVED_SEARCH_SUCCESS = 'RESOLVED_SEARCH_SUCCESS';
export const RESOLVED_SEARCH_FAIL = 'RESOLVED_SEARCH_FAIL';
export const UPDATE_SEARCH_QUERY = 'UPDATE_SEARCH_QUERY'; export const UPDATE_SEARCH_QUERY = 'UPDATE_SEARCH_QUERY';
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 SEARCH_FOCUS = 'SEARCH_FOCUS';
export const SEARCH_BLUR = 'SEARCH_BLUR';
// Settings // Settings
export const DAEMON_SETTINGS_RECEIVED = 'DAEMON_SETTINGS_RECEIVED'; export const DAEMON_SETTINGS_RECEIVED = 'DAEMON_SETTINGS_RECEIVED';

View file

@ -2,3 +2,23 @@ export const FILE = 'file';
export const CHANNEL = 'channel'; export const CHANNEL = 'channel';
export const SEARCH = 'search'; export const SEARCH = 'search';
export const DEBOUNCE_WAIT_DURATION_MS = 250; export const DEBOUNCE_WAIT_DURATION_MS = 250;
export const SEARCH_TYPES = {
FILE: 'file',
CHANNEL: 'channel',
SEARCH: 'search',
TAG: 'tag',
};
export const SEARCH_OPTIONS = {
RESULT_COUNT: 'size',
CLAIM_TYPE: 'claimType',
INCLUDE_FILES: 'file',
INCLUDE_CHANNELS: 'channel',
INCLUDE_FILES_AND_CHANNELS: 'file,channel',
MEDIA_AUDIO: 'audio',
MEDIA_VIDEO: 'video',
MEDIA_TEXT: 'text',
MEDIA_IMAGE: 'image',
MEDIA_APPLICATION: 'application',
};

View file

@ -15,7 +15,8 @@ import React, { Fragment, useState, useEffect } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { doDaemonReady, doAutoUpdate, doOpenModal, doHideModal, doToggle3PAnalytics } from 'redux/actions/app'; import { doDaemonReady, doAutoUpdate, doOpenModal, doHideModal, doToggle3PAnalytics } from 'redux/actions/app';
import { Lbry, isURIValid, setSearchApi, apiCall } from 'lbry-redux'; import { Lbry, isURIValid, apiCall } from 'lbry-redux';
import { setSearchApi } from 'redux/actions/search';
import { doSetLanguage, doFetchLanguage, doUpdateIsNightAsync } from 'redux/actions/settings'; import { doSetLanguage, doFetchLanguage, doUpdateIsNightAsync } from 'redux/actions/settings';
import { Lbryio, doBlackListedOutpointsSubscribe, doFilteredOutpointsSubscribe } from 'lbryinc'; import { Lbryio, doBlackListedOutpointsSubscribe, doFilteredOutpointsSubscribe } from 'lbryinc';
import rewards from 'rewards'; import rewards from 'rewards';

View file

@ -1,12 +1,7 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import { doToast, SETTINGS } from 'lbry-redux';
doSearch, import { doSearch } from 'redux/actions/search';
selectIsSearching, import { selectIsSearching, makeSelectSearchUris, makeSelectQueryWithOptions } from 'redux/selectors/search';
makeSelectSearchUris,
makeSelectQueryWithOptions,
doToast,
SETTINGS,
} from 'lbry-redux';
import { makeSelectClientSetting } from 'redux/selectors/settings'; import { makeSelectClientSetting } from 'redux/selectors/settings';
import { selectUserVerifiedEmail } from 'redux/selectors/user'; import { selectUserVerifiedEmail } from 'redux/selectors/user';
import analytics from 'analytics'; import analytics from 'analytics';

View file

@ -1,6 +1,6 @@
import { combineReducers } from 'redux'; import { combineReducers } from 'redux';
import { connectRouter } from 'connected-react-router'; import { connectRouter } from 'connected-react-router';
import { claimsReducer, fileInfoReducer, searchReducer, walletReducer, tagsReducer, publishReducer } from 'lbry-redux'; import { claimsReducer, fileInfoReducer, walletReducer, tagsReducer, publishReducer } from 'lbry-redux';
import { import {
costInfoReducer, costInfoReducer,
blacklistReducer, blacklistReducer,
@ -19,6 +19,7 @@ import rewardsReducer from 'redux/reducers/rewards';
import userReducer from 'redux/reducers/user'; import userReducer from 'redux/reducers/user';
import commentsReducer from 'redux/reducers/comments'; import commentsReducer from 'redux/reducers/comments';
import blockedReducer from 'redux/reducers/blocked'; import blockedReducer from 'redux/reducers/blocked';
import searchReducer from 'redux/reducers/search';
export default history => export default history =>
combineReducers({ combineReducers({

188
ui/redux/actions/search.js Normal file
View file

@ -0,0 +1,188 @@
// @flow
import * as ACTIONS from 'constants/action_types';
import { buildURI, doResolveUri, batchActions } from 'lbry-redux';
import {
makeSelectSearchUris,
selectSuggestions,
makeSelectQueryWithOptions,
selectSearchValue,
} from 'redux/selectors/search';
import debounce from 'util/debounce';
import handleFetchResponse from 'util/handle-fetch';
const DEBOUNCED_SEARCH_SUGGESTION_MS = 300;
type Dispatch = (action: any) => any;
type GetState = () => { search: SearchState };
type SearchOptions = {
size?: number,
from?: number,
related_to?: string,
nsfw?: boolean,
isBackgroundSearch?: boolean,
resolveResults?: boolean,
};
// We can't use env's because they aren't passed into node_modules
let CONNECTION_STRING = 'https://lighthouse.lbry.com/';
export const setSearchApi = (endpoint: string) => {
CONNECTION_STRING = endpoint.replace(/\/*$/, '/'); // exactly one slash at the end;
};
export const getSearchSuggestions = (value: string) => (dispatch: Dispatch, getState: GetState) => {
const query = value.trim();
// strip out any basic stuff for more accurate search results
let searchValue = query.replace(/lbry:\/\//g, '').replace(/-/g, ' ');
if (searchValue.includes('#')) {
// This should probably be more robust, but I think it's fine for now
// Remove everything after # to get rid of the claim id
searchValue = searchValue.substring(0, searchValue.indexOf('#'));
}
const suggestions = selectSuggestions(getState());
if (suggestions[searchValue]) {
return;
}
fetch(`${CONNECTION_STRING}autocomplete?s=${searchValue}`)
.then(handleFetchResponse)
.then(apiSuggestions => {
dispatch({
type: ACTIONS.UPDATE_SEARCH_SUGGESTIONS,
data: {
query: searchValue,
suggestions: apiSuggestions,
},
});
})
.catch(() => {
// If the fetch fails, do nothing
// Basic search suggestions are already populated at this point
});
};
const throttledSearchSuggestions = debounce((dispatch, query) => {
dispatch(getSearchSuggestions(query));
}, DEBOUNCED_SEARCH_SUGGESTION_MS);
export const doUpdateSearchQuery = (query: string, shouldSkipSuggestions: ?boolean) => (dispatch: Dispatch) => {
dispatch({
type: ACTIONS.UPDATE_SEARCH_QUERY,
data: { query },
});
// Don't fetch new suggestions if the user just added a space
if (!query.endsWith(' ') || !shouldSkipSuggestions) {
throttledSearchSuggestions(dispatch, query);
}
};
export const doSearch = (rawQuery: string, searchOptions: SearchOptions) => (
dispatch: Dispatch,
getState: GetState
) => {
const query = rawQuery.replace(/^lbry:\/\//i, '').replace(/\//, ' ');
const resolveResults = searchOptions && searchOptions.resolveResults;
const isBackgroundSearch = (searchOptions && searchOptions.isBackgroundSearch) || false;
if (!query) {
dispatch({
type: ACTIONS.SEARCH_FAIL,
});
return;
}
const state = getState();
let queryWithOptions = makeSelectQueryWithOptions(query, searchOptions)(state);
// If we have already searched for something, we don't need to do anything
const urisForQuery = makeSelectSearchUris(queryWithOptions)(state);
if (urisForQuery && !!urisForQuery.length) {
return;
}
dispatch({
type: ACTIONS.SEARCH_START,
});
// If the user is on the file page with a pre-populated uri and they select
// the search option without typing anything, searchQuery will be empty
// We need to populate it so the input is filled on the search page
// isBackgroundSearch means the search is happening in the background, don't update the search query
if (!state.search.searchQuery && !isBackgroundSearch) {
dispatch(doUpdateSearchQuery(query));
}
fetch(`${CONNECTION_STRING}search?${queryWithOptions}`)
.then(handleFetchResponse)
.then((data: Array<{ name: string, claimId: string }>) => {
const uris = [];
const actions = [];
data.forEach(result => {
if (result) {
const { name, claimId } = result;
const urlObj: LbryUrlObj = {};
if (name.startsWith('@')) {
urlObj.channelName = name;
urlObj.channelClaimId = claimId;
} else {
urlObj.streamName = name;
urlObj.streamClaimId = claimId;
}
const url = buildURI(urlObj);
if (resolveResults) {
actions.push(doResolveUri(url));
}
uris.push(url);
}
});
actions.push({
type: ACTIONS.SEARCH_SUCCESS,
data: {
query: queryWithOptions,
uris,
},
});
dispatch(batchActions(...actions));
})
.catch(e => {
dispatch({
type: ACTIONS.SEARCH_FAIL,
});
});
};
export const doFocusSearchInput = () => (dispatch: Dispatch) =>
dispatch({
type: ACTIONS.SEARCH_FOCUS,
});
export const doBlurSearchInput = () => (dispatch: Dispatch) =>
dispatch({
type: ACTIONS.SEARCH_BLUR,
});
export const doUpdateSearchOptions = (newOptions: SearchOptions, additionalOptions: SearchOptions) => (
dispatch: Dispatch,
getState: GetState
) => {
const state = getState();
const searchValue = selectSearchValue(state);
dispatch({
type: ACTIONS.UPDATE_SEARCH_OPTIONS,
data: newOptions,
});
if (searchValue) {
// After updating, perform a search with the new options
dispatch(doSearch(searchValue, additionalOptions));
}
};

117
ui/redux/reducers/search.js Normal file
View file

@ -0,0 +1,117 @@
// @flow
import * as ACTIONS from 'constants/action_types';
import { handleActions } from 'util/redux-utils';
import { SEARCH_OPTIONS } from 'constants/search';
const defaultState = {
isActive: false, // does the user have any typed text in the search input
focused: false, // is the search input focused
searchQuery: '', // needs to be an empty string for input focusing
options: {
[SEARCH_OPTIONS.RESULT_COUNT]: 30,
[SEARCH_OPTIONS.CLAIM_TYPE]: SEARCH_OPTIONS.INCLUDE_FILES_AND_CHANNELS,
[SEARCH_OPTIONS.MEDIA_AUDIO]: true,
[SEARCH_OPTIONS.MEDIA_VIDEO]: true,
[SEARCH_OPTIONS.MEDIA_TEXT]: true,
[SEARCH_OPTIONS.MEDIA_IMAGE]: true,
[SEARCH_OPTIONS.MEDIA_APPLICATION]: true,
},
suggestions: {},
urisByQuery: {},
resolvedResultsByQuery: {},
resolvedResultsByQueryLastPageReached: {},
};
export default handleActions(
{
[ACTIONS.SEARCH_START]: (state: SearchState): SearchState => ({
...state,
searching: true,
}),
[ACTIONS.SEARCH_SUCCESS]: (state: SearchState, action: SearchSuccess): SearchState => {
const { query, uris } = action.data;
return {
...state,
searching: false,
urisByQuery: Object.assign({}, state.urisByQuery, { [query]: uris }),
};
},
[ACTIONS.SEARCH_FAIL]: (state: SearchState): SearchState => ({
...state,
searching: false,
}),
[ACTIONS.RESOLVED_SEARCH_START]: (state: SearchState): SearchState => ({
...state,
searching: true,
}),
[ACTIONS.RESOLVED_SEARCH_SUCCESS]: (state: SearchState, action: ResolvedSearchSuccess): SearchState => {
const resolvedResultsByQuery = Object.assign({}, state.resolvedResultsByQuery);
const resolvedResultsByQueryLastPageReached = Object.assign({}, state.resolvedResultsByQueryLastPageReached);
const { append, query, results, pageSize } = action.data;
if (append) {
// todo: check for duplicates when concatenating?
resolvedResultsByQuery[query] =
resolvedResultsByQuery[query] && resolvedResultsByQuery[query].length
? resolvedResultsByQuery[query].concat(results)
: results;
} else {
resolvedResultsByQuery[query] = results;
}
// the returned number of urls is less than the page size, so we're on the last page
resolvedResultsByQueryLastPageReached[query] = results.length < pageSize;
return {
...state,
searching: false,
resolvedResultsByQuery,
resolvedResultsByQueryLastPageReached,
};
},
[ACTIONS.RESOLVED_SEARCH_FAIL]: (state: SearchState): SearchState => ({
...state,
searching: false,
}),
[ACTIONS.UPDATE_SEARCH_QUERY]: (state: SearchState, action: UpdateSearchQuery): SearchState => ({
...state,
searchQuery: action.data.query,
isActive: true,
}),
[ACTIONS.UPDATE_SEARCH_SUGGESTIONS]: (state: SearchState, action: UpdateSearchSuggestions): SearchState => ({
...state,
suggestions: {
...state.suggestions,
[action.data.query]: action.data.suggestions,
},
}),
// sets isActive to false so the uri will be populated correctly if the
// user is on a file page. The search query will still be present on any
// other page
[ACTIONS.SEARCH_FOCUS]: (state: SearchState): SearchState => ({
...state,
focused: true,
}),
[ACTIONS.SEARCH_BLUR]: (state: SearchState): SearchState => ({
...state,
focused: false,
}),
[ACTIONS.UPDATE_SEARCH_OPTIONS]: (state: SearchState, action: UpdateSearchOptions): SearchState => {
const { options: oldOptions } = state;
const newOptions = action.data;
const options = { ...oldOptions, ...newOptions };
return {
...state,
options,
};
},
},
defaultState
);

View file

@ -6,7 +6,6 @@ import {
makeSelectClaimsInChannelForCurrentPageState, makeSelectClaimsInChannelForCurrentPageState,
makeSelectClaimIsNsfw, makeSelectClaimIsNsfw,
makeSelectClaimIsMine, makeSelectClaimIsMine,
makeSelectRecommendedContentForUri,
makeSelectMediaTypeForUri, makeSelectMediaTypeForUri,
selectBalance, selectBalance,
parseURI, parseURI,
@ -14,6 +13,7 @@ import {
makeSelectContentTypeForUri, makeSelectContentTypeForUri,
makeSelectFileNameForUri, makeSelectFileNameForUri,
} from 'lbry-redux'; } from 'lbry-redux';
import { makeSelectRecommendedContentForUri } from 'redux/selectors/search';
import { selectBlockedChannels } from 'redux/selectors/blocked'; import { selectBlockedChannels } from 'redux/selectors/blocked';
import { selectAllCostInfoByUri, makeSelectCostInfoForUri } from 'lbryinc'; import { selectAllCostInfoByUri, makeSelectCostInfoForUri } from 'lbryinc';
import { selectShowMatureContent } from 'redux/selectors/settings'; import { selectShowMatureContent } from 'redux/selectors/settings';

View file

@ -0,0 +1,177 @@
// @flow
import { SEARCH_TYPES } from 'constants/search';
import { getSearchQueryString } from 'util/query-params';
import { normalizeURI, parseURI, makeSelectClaimForUri, makeSelectClaimIsNsfw, buildURI } from 'lbry-redux';
import { createSelector } from 'reselect';
type State = { search: SearchState };
export const selectState = (state: State): SearchState => state.search;
export const selectSearchValue: (state: State) => string = createSelector(selectState, state => state.searchQuery);
export const selectSearchOptions: (state: State) => SearchOptions = createSelector(selectState, state => state.options);
export const selectSuggestions: (state: State) => { [string]: Array<SearchSuggestion> } = createSelector(
selectState,
state => state.suggestions
);
export const selectIsSearching: (state: State) => boolean = createSelector(selectState, state => state.searching);
export const selectSearchUrisByQuery: (state: State) => { [string]: Array<string> } = createSelector(
selectState,
state => state.urisByQuery
);
export const makeSelectSearchUris = (query: string): ((state: State) => Array<string>) =>
// replace statement below is kind of ugly, and repeated in doSearch action
createSelector(
selectSearchUrisByQuery,
byQuery => byQuery[query ? query.replace(/^lbry:\/\//i, '').replace(/\//, ' ') : query]
);
export const selectResolvedSearchResultsByQuery: (
state: State
) => { [string]: Array<ResolvedSearchResult> } = createSelector(selectState, state => state.resolvedResultsByQuery);
export const selectSearchBarFocused: boolean = createSelector(selectState, state => state.focused);
export const selectSearchSuggestions: Array<SearchSuggestion> = createSelector(
selectSearchValue,
selectSuggestions,
(query: string, suggestions: { [string]: Array<string> }) => {
if (!query) {
return [];
}
const queryIsPrefix = query === 'lbry:' || query === 'lbry:/' || query === 'lbry://' || query === 'lbry://@';
if (queryIsPrefix) {
// If it is a prefix, wait until something else comes to figure out what to do
return [];
} else if (query.startsWith('lbry://')) {
// If it starts with a prefix, don't show any autocomplete results
// They are probably typing/pasting in a lbry uri
return [
{
value: query,
type: query[7] === '@' ? SEARCH_TYPES.CHANNEL : SEARCH_TYPES.FILE,
},
];
}
let searchSuggestions = [];
try {
const uri = normalizeURI(query);
const { channelName, streamName, isChannel } = parseURI(uri);
searchSuggestions.push(
{
value: query,
type: SEARCH_TYPES.SEARCH,
},
{
value: uri,
shorthand: isChannel ? channelName : streamName,
type: isChannel ? SEARCH_TYPES.CHANNEL : SEARCH_TYPES.FILE,
}
);
} catch (e) {
searchSuggestions.push({
value: query,
type: SEARCH_TYPES.SEARCH,
});
}
searchSuggestions.push({
value: query,
type: SEARCH_TYPES.TAG,
});
const apiSuggestions = suggestions[query] || [];
if (apiSuggestions.length) {
searchSuggestions = searchSuggestions.concat(
apiSuggestions
.filter(suggestion => suggestion !== query)
.map(suggestion => {
// determine if it's a channel
try {
const uri = normalizeURI(suggestion);
const { channelName, streamName, isChannel } = parseURI(uri);
return {
value: uri,
shorthand: isChannel ? channelName : streamName,
type: isChannel ? SEARCH_TYPES.CHANNEL : SEARCH_TYPES.FILE,
};
} catch (e) {
// search result includes some character that isn't valid in claim names
return {
value: suggestion,
type: SEARCH_TYPES.SEARCH,
};
}
})
);
}
return searchSuggestions;
}
);
// Creates a query string based on the state in the search reducer
// Can be overrided by passing in custom sizes/from values for other areas pagination
type CustomOptions = {
isBackgroundSearch?: boolean,
size?: number,
from?: number,
related_to?: string,
nsfw?: boolean,
};
export const makeSelectQueryWithOptions = (customQuery: ?string, options: CustomOptions) =>
createSelector(selectSearchValue, selectSearchOptions, (query, defaultOptions) => {
const searchOptions = { ...defaultOptions, ...options };
const queryString = getSearchQueryString(customQuery || query, searchOptions);
return queryString;
});
export const makeSelectRecommendedContentForUri = (uri: string) =>
createSelector(
makeSelectClaimForUri(uri),
selectSearchUrisByQuery,
makeSelectClaimIsNsfw(uri),
(claim, searchUrisByQuery, isMature) => {
let recommendedContent;
if (claim) {
// always grab full URL - this can change once search returns canonical
const currentUri = buildURI({ streamClaimId: claim.claim_id, streamName: claim.name });
const { title } = claim.value;
if (!title) {
return;
}
const options: {
related_to?: string,
nsfw?: boolean,
isBackgroundSearch?: boolean,
} = { related_to: claim.claim_id, isBackgroundSearch: true };
if (!isMature) {
options['nsfw'] = false;
}
const searchQuery = getSearchQueryString(title.replace(/\//, ' '), options);
let searchUris = searchUrisByQuery[searchQuery];
if (searchUris) {
searchUris = searchUris.filter(searchUri => searchUri !== currentUri);
recommendedContent = searchUris;
}
}
return recommendedContent;
}
);

View file

@ -1,3 +1,4 @@
export default function handleFetchResponse(response) { // @flow
export default function handleFetchResponse(response: Response): Promise<any> {
return response.status === 200 ? Promise.resolve(response.json()) : Promise.reject(new Error(response.statusText)); return response.status === 200 ? Promise.resolve(response.json()) : Promise.reject(new Error(response.statusText));
} }

View file

@ -1,4 +1,10 @@
export function parseQueryParams(queryString) { // @flow
import { SEARCH_OPTIONS } from 'constants/search';
const DEFAULT_SEARCH_RESULT_FROM = 0;
const DEFAULT_SEARCH_SIZE = 20;
export function parseQueryParams(queryString: string) {
if (queryString === '') return {}; if (queryString === '') return {};
const parts = queryString const parts = queryString
.split('?') .split('?')
@ -14,21 +20,8 @@ export function parseQueryParams(queryString) {
return params; return params;
} }
export function toQueryString(params) {
if (!params) return '';
const parts = [];
Object.keys(params).forEach(key => {
if (Object.prototype.hasOwnProperty.call(params, key) && params[key]) {
parts.push(`${key}=${params[key]}`);
}
});
return parts.join('&');
}
// https://stackoverflow.com/questions/5999118/how-can-i-add-or-update-a-query-string-parameter // https://stackoverflow.com/questions/5999118/how-can-i-add-or-update-a-query-string-parameter
export function updateQueryParam(uri, key, value) { export function updateQueryParam(uri: string, key: string, value: string) {
const re = new RegExp('([?&])' + key + '=.*?(&|$)', 'i'); const re = new RegExp('([?&])' + key + '=.*?(&|$)', 'i');
const separator = uri.includes('?') ? '&' : '?'; const separator = uri.includes('?') ? '&' : '?';
if (uri.match(re)) { if (uri.match(re)) {
@ -37,3 +30,50 @@ export function updateQueryParam(uri, key, value) {
return uri + separator + key + '=' + value; return uri + separator + key + '=' + value;
} }
} }
export const getSearchQueryString = (query: string, options: any = {}) => {
const encodedQuery = encodeURIComponent(query);
const queryParams = [
`s=${encodedQuery}`,
`size=${options.size || DEFAULT_SEARCH_SIZE}`,
`from=${options.from || DEFAULT_SEARCH_RESULT_FROM}`,
];
const { isBackgroundSearch } = options;
const includeUserOptions = typeof isBackgroundSearch === 'undefined' ? false : !isBackgroundSearch;
if (includeUserOptions) {
const claimType = options[SEARCH_OPTIONS.CLAIM_TYPE];
if (claimType) {
queryParams.push(`claimType=${claimType}`);
// If they are only searching for channels, strip out the media info
if (!claimType.includes(SEARCH_OPTIONS.INCLUDE_CHANNELS)) {
queryParams.push(
`mediaType=${[
SEARCH_OPTIONS.MEDIA_FILE,
SEARCH_OPTIONS.MEDIA_AUDIO,
SEARCH_OPTIONS.MEDIA_VIDEO,
SEARCH_OPTIONS.MEDIA_TEXT,
SEARCH_OPTIONS.MEDIA_IMAGE,
SEARCH_OPTIONS.MEDIA_APPLICATION,
].reduce((acc, currentOption) => (options[currentOption] ? `${acc}${currentOption},` : acc), '')}`
);
}
}
}
const additionalOptions = {};
const { related_to } = options;
const { nsfw } = options;
if (related_to) additionalOptions['related_to'] = related_to;
if (typeof nsfw !== 'undefined') additionalOptions['nsfw'] = nsfw;
if (additionalOptions) {
Object.keys(additionalOptions).forEach(key => {
const option = additionalOptions[key];
queryParams.push(`${key}=${option}`);
});
}
return queryParams.join('&');
};

View file

@ -6385,9 +6385,9 @@ lazy-val@^1.0.4:
yargs "^13.2.2" yargs "^13.2.2"
zstd-codec "^0.1.1" zstd-codec "^0.1.1"
lbry-redux@lbryio/lbry-redux#a1d5ce7e7e854c1c11e630609ef06a98e3b100c1: lbry-redux@lbryio/lbry-redux#021e7e3b798efeea467ce6437cd181dfa2e6badc:
version "0.0.1" version "0.0.1"
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/a1d5ce7e7e854c1c11e630609ef06a98e3b100c1" resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/021e7e3b798efeea467ce6437cd181dfa2e6badc"
dependencies: dependencies:
proxy-polyfill "0.1.6" proxy-polyfill "0.1.6"
reselect "^3.0.0" reselect "^3.0.0"