New search settings for filtering #114

Merged
neb-b merged 1 commit from search-filters into master 2019-02-18 19:19:37 +01:00
12 changed files with 708 additions and 432 deletions

772
dist/bundle.js vendored

File diff suppressed because it is too large Load diff

View file

@ -117,6 +117,7 @@ export const SEARCH_START = 'SEARCH_START';
export const SEARCH_SUCCESS = 'SEARCH_SUCCESS';
export const SEARCH_FAIL = 'SEARCH_FAIL';
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 SEARCH_FOCUS = 'SEARCH_FOCUS';
export const SEARCH_BLUR = 'SEARCH_BLUR';

View file

@ -1,3 +1,18 @@
export const FILE = 'file';
export const CHANNEL = 'channel';
export const SEARCH = 'search';
export const SEARCH_TYPES = {
FILE: 'file',
CHANNEL: 'channel',
SEARCH: 'search',
};
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

@ -1,10 +1,10 @@
import * as ACTIONS from 'constants/action_types';
import * as THUMBNAIL_STATUSES from 'constants/thumbnail_upload_statuses';
import * as SEARCH_TYPES from 'constants/search';
import * as SETTINGS from 'constants/settings';
import * as TRANSACTIONS from 'constants/transaction_types';
import * as SORT_OPTIONS from 'constants/sort_options';
import * as PAGES from 'constants/pages';
import { SEARCH_TYPES, SEARCH_OPTIONS } from 'constants/search';
import Lbry from 'lbry';
import Lbryapi from 'lbryapi';
@ -14,7 +14,16 @@ import { selectState as selectSearchState } from 'redux/selectors/search';
export { Toast } from 'types/Notification';
// constants
export { ACTIONS, THUMBNAIL_STATUSES, SEARCH_TYPES, SETTINGS, TRANSACTIONS, SORT_OPTIONS, PAGES };
export {
ACTIONS,
THUMBNAIL_STATUSES,
SEARCH_TYPES,
SEARCH_OPTIONS,
SETTINGS,
TRANSACTIONS,
SORT_OPTIONS,
PAGES,
};
// common
export { Lbry, Lbryapi };
@ -59,6 +68,7 @@ export {
doFocusSearchInput,
doBlurSearchInput,
setSearchApi,
doUpdateSearchOptions,
} from 'redux/actions/search';
export { doBlackListedOutpointsSubscribe } from 'redux/actions/blacklist';
@ -191,11 +201,13 @@ export {
makeSelectSearchUris,
selectSearchQuery,
selectSearchValue,
selectSearchOptions,
selectIsSearching,
selectSearchUrisByQuery,
selectWunderBarAddress,
selectSearchBarFocused,
selectSearchSuggestions,
makeSelectQueryWithOptions,
} from 'redux/selectors/search';
export {

View file

@ -1,32 +1,35 @@
// @flow
import type { SearchState, SearchOptions } from 'types/Search';
import * as ACTIONS from 'constants/action_types';
import { buildURI } from 'lbryURI';
import { doResolveUri } from 'redux/actions/claims';
import { makeSelectSearchUris, selectSuggestions } from 'redux/selectors/search';
import {
makeSelectSearchUris,
selectSuggestions,
makeSelectQueryWithOptions,
selectSearchQuery,
} from 'redux/selectors/search';
import { batchActions } from 'util/batchActions';
import debounce from 'util/debounce';
import handleFetchResponse from 'util/handle-fetch';
const DEFAULTSEARCHRESULTSIZE = 10;
const DEFAULTSEARCHRESULTFROM = 0;
const DEBOUNCED_SEARCH_SUGGESTION_MS = 300;
type Dispatch = (action: any) => any;
type GetState = () => {};
type GetState = () => { search: SearchState };
// We can't use env's because they aren't passed into node_modules
let CONNECTION_STRING = 'https://lighthouse.lbry.io/';
export const setSearchApi = endpoint => {
export const setSearchApi = (endpoint: string) => {
CONNECTION_STRING = endpoint.replace(/\/*$/, '/'); // exactly one slash at the end;
};
export const doSearch = (
rawQuery: string,
size: number = DEFAULTSEARCHRESULTSIZE,
from: number = DEFAULTSEARCHRESULTFROM,
rawQuery: string, // pass in a query if you don't want to search for what's in the search bar
size: ?number, // only pass in if you don't want to use the users setting (ex: related content)
from: ?number,
isBackgroundSearch: boolean = false
) => (dispatch: Dispatch, getState: GetState) => {
const state = getState();
const query = rawQuery.replace(/^lbry:\/\//i, '').replace(/\//, ' ');
if (!query) {
@ -36,8 +39,11 @@ export const doSearch = (
return;
}
const state = getState();
const queryWithOptions = makeSelectQueryWithOptions(query, size, from, isBackgroundSearch)(state);
// If we have already searched for something, we don't need to do anything
const urisForQuery = makeSelectSearchUris(query)(state);
const urisForQuery = makeSelectSearchUris(queryWithOptions)(state);
if (urisForQuery && !!urisForQuery.length) {
return;
}
@ -57,8 +63,7 @@ export const doSearch = (
});
}
const encodedQuery = encodeURIComponent(query);
fetch(`${CONNECTION_STRING}search?s=${encodedQuery}&size=${size}&from=${from}`)
fetch(`${CONNECTION_STRING}search?${queryWithOptions}`)
.then(handleFetchResponse)
.then(data => {
const uris = [];
@ -76,7 +81,7 @@ export const doSearch = (
actions.push({
type: ACTIONS.SEARCH_SUCCESS,
data: {
query,
query: queryWithOptions,
uris,
},
});
@ -149,3 +154,21 @@ export const doBlurSearchInput = () => (dispatch: Dispatch) =>
dispatch({
type: ACTIONS.SEARCH_BLUR,
});
export const doUpdateSearchOptions = (newOptions: SearchOptions) => (
dispatch: Dispatch,
getState: GetState
) => {
const state = getState();
const searchQuery = selectSearchQuery(state);
dispatch({
type: ACTIONS.UPDATE_SEARCH_OPTIONS,
data: newOptions,
});
if (searchQuery) {
// After updating, perform a search with the new options
dispatch(doSearch(searchQuery));
}
};

View file

@ -2,6 +2,7 @@
import type {
NotificationState,
DoToast,
DoError,
DoNotification,
DoEditNotification,
DoDeleteNotification,
@ -55,7 +56,7 @@ const notificationsReducer = handleActions(
let notifications = state.notifications.slice();
notifications = notifications.map(
(pastNotification) =>
pastNotification =>
pastNotification.id === notification.id ? notification : pastNotification
);
@ -67,7 +68,7 @@ const notificationsReducer = handleActions(
[ACTIONS.DELETE_NOTIFICATION]: (state: NotificationState, action: DoDeleteNotification) => {
const { id } = action.data;
let newNotifications = state.notifications.slice();
newNotifications = newNotifications.filter((notification) => notification.id !== id);
newNotifications = newNotifications.filter(notification => notification.id !== id);
return {
...state,
@ -76,7 +77,7 @@ const notificationsReducer = handleActions(
},
// Errors
[ACTIONS.CREATE_ERROR]: (state: NotificationState, action: DoToast) => {
[ACTIONS.CREATE_ERROR]: (state: NotificationState, action: DoError) => {
const error = action.data;
const newErrors = state.errors.slice();
newErrors.push(error);

View file

@ -1,56 +1,29 @@
// @flow
import * as ACTIONS from 'constants/action_types';
import { handleActions } from 'util/redux-utils';
type SearchSuccess = {
type: ACTIONS.SEARCH_SUCCESS,
data: {
query: string,
uris: Array<string>,
},
};
type UpdateSearchQuery = {
type: ACTIONS.UPDATE_SEARCH_QUERY,
data: {
query: string,
},
};
type SearchSuggestion = {
value: string,
shorthand: string,
type: string,
};
type UpdateSearchSuggestions = {
type: ACTIONS.UPDATE_SEARCH_SUGGESTIONS,
data: {
query: string,
suggestions: Array<SearchSuggestion>,
},
};
type SearchState = {
isActive: boolean,
searchQuery: string,
suggestions: Array<SearchSuggestion>,
urisByQuery: {},
};
type HistoryNavigate = {
type: ACTIONS.HISTORY_NAVIGATE,
data: {
url: string,
index?: number,
scrollY?: number,
},
};
import { SEARCH_OPTIONS } from 'constants/search';
import type {
SearchState,
SearchSuccess,
UpdateSearchQuery,
UpdateSearchSuggestions,
HistoryNavigate,
UpdateSearchOptions,
} from 'types/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: {},
};
@ -123,6 +96,18 @@ export const searchReducer = handleActions(
...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

@ -17,7 +17,7 @@ type ActionResult = {
type WalletState = {
balance: any,
blocks: any,
latestBlock: number,
latestBlock: ?number,
transactions: any,
fetchingTransactions: boolean,
gettingNewAddress: boolean,
@ -73,7 +73,7 @@ reducers[ACTIONS.FETCH_TRANSACTIONS_COMPLETED] = (state: WalletState, action) =>
const { transactions } = action.data;
transactions.forEach((transaction) => {
transactions.forEach(transaction => {
byId[transaction.txid] = transaction;
});

View file

@ -3,6 +3,7 @@ import { makeSelectCurrentParam } from 'redux/selectors/navigation';
import { selectSearchUrisByQuery } from 'redux/selectors/search';
import { createSelector } from 'reselect';
import { isClaimNsfw } from 'util/claim';
import { getSearchQueryString } from 'util/query_params';
const selectState = state => state.claims || {};
@ -297,7 +298,9 @@ export const makeSelectRecommendedContentForUri = uri =>
const { title } = claim.value.stream.metadata;
let searchUris = searchUrisByQuery[title.replace(/\//, ' ')];
const searchQuery = getSearchQueryString(title.replace(/\//, ' '));
let searchUris = searchUrisByQuery[searchQuery];
if (searchUris) {
searchUris = searchUris.filter(searchUri => searchUri !== currentUri);
recommendedContent = searchUris;

View file

@ -1,25 +1,48 @@
import * as SEARCH_TYPES from 'constants/search';
// @flow
import type { SearchState, SearchOptions, SearchSuggestion } from 'types/Search';
import { SEARCH_TYPES, SEARCH_OPTIONS } from 'constants/search';
import { getSearchQueryString } from 'util/query_params';
import { normalizeURI, parseURI } from 'lbryURI';
import { selectCurrentPage, selectCurrentParams } from 'redux/selectors/navigation';
import { createSelector } from 'reselect';
export const selectState = state => state.search || {};
type State = { search: SearchState };
export const selectSearchValue = createSelector(selectState, state => state.searchQuery);
export const selectState = (state: State): SearchState => state.search;
export const selectSuggestions = createSelector(selectState, state => state.suggestions);
export const selectSearchQuery = createSelector(
selectCurrentPage,
selectCurrentParams,
(page, params) => (page === 'search' ? params && params.query : null)
export const selectSearchValue: (state: State) => string = createSelector(
selectState,
state => state.searchQuery
);
export const selectIsSearching = createSelector(selectState, state => state.searching);
export const selectSearchOptions: (state: State) => SearchOptions = createSelector(
selectState,
state => state.options
);
export const selectSearchUrisByQuery = createSelector(selectState, state => state.urisByQuery);
export const selectSuggestions: (
state: State
) => { [string]: Array<SearchSuggestion> } = createSelector(
selectState,
state => state.suggestions
);
export const makeSelectSearchUris = query =>
export const selectSearchQuery: (state: State) => ?string = createSelector(
selectCurrentPage,
selectCurrentParams,
(page: string, params: ?{ query: string }) => (page === 'search' ? params && params.query : null)
);
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,
@ -30,7 +53,7 @@ export const selectWunderBarAddress = createSelector(
selectCurrentPage,
selectSearchQuery,
selectCurrentParams,
(page, query, params) => {
(page: string, query: string, params: { uri: string }) => {
// only populate the wunderbar address if we are on the file/channel pages
// or show the search query
if (page === 'show') {
@ -40,13 +63,12 @@ export const selectWunderBarAddress = createSelector(
}
);
export const selectSearchBarFocused = createSelector(selectState, state => state.focused);
// export const selectSear
export const selectSearchBarFocused: boolean = createSelector(selectState, state => state.focused);
export const selectSearchSuggestions = createSelector(
export const selectSearchSuggestions: Array<SearchSuggestion> = createSelector(
selectSearchValue,
selectSuggestions,
(query, suggestions) => {
(query: string, suggestions: { [string]: Array<string> }) => {
if (!query) {
return [];
}
@ -117,3 +139,23 @@ export const selectSearchSuggestions = createSelector(
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
export const makeSelectQueryWithOptions = (
customQuery: ?string,
customSize: ?number,
customFrom: ?number,
isBackgroundSearch: boolean = false // If it's a background search, don't use the users settings
) =>
createSelector(selectSearchQuery, selectSearchOptions, (query, options) => {
const size = customSize || options[SEARCH_OPTIONS.RESULT_COUNT];
const queryString = getSearchQueryString(
customQuery || query,
{ ...options, size, from: customFrom },
!isBackgroundSearch
);
return queryString;
});

68
src/types/Search.js Normal file
View file

@ -0,0 +1,68 @@
// @flow
import * as ACTIONS from 'constants/action_types';
export type SearchSuggestion = {
value: string,
shorthand: string,
type: string,
};
export 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,
};
export type SearchState = {
isActive: boolean,
searchQuery: string,
options: SearchOptions,
suggestions: { [string]: Array<SearchSuggestion> },
urisByQuery: {},
};
export type SearchSuccess = {
type: ACTIONS.SEARCH_SUCCESS,
data: {
query: string,
uris: Array<string>,
},
};
export type UpdateSearchQuery = {
type: ACTIONS.UPDATE_SEARCH_QUERY,
data: {
query: string,
},
};
export type UpdateSearchSuggestions = {
type: ACTIONS.UPDATE_SEARCH_SUGGESTIONS,
data: {
query: string,
suggestions: Array<SearchSuggestion>,
},
};
export type HistoryNavigate = {
type: ACTIONS.HISTORY_NAVIGATE,
data: {
url: string,
index?: number,
scrollY?: number,
},
};
export type UpdateSearchOptions = {
type: ACTIONS.UPDATE_SEARCH_OPTIONS,
data: SearchOptions,
};

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 {};
const parts = queryString
.split('?')
@ -14,7 +20,7 @@ export function parseQueryParams(queryString) {
return params;
}
export function toQueryString(params) {
export function toQueryString(params: { [string]: string | number }) {
if (!params) return '';
const parts = [];
@ -26,3 +32,39 @@ export function toQueryString(params) {
return parts.join('&');
}
export const getSearchQueryString = (
query: string,
options: any = {},
includeUserOptions: boolean = false
) => {
const encodedQuery = encodeURIComponent(query);
const queryParams = [
`s=${encodedQuery}`,
`size=${options.size || DEFAULT_SEARCH_SIZE}`,
`from=${options.from || DEFAULT_SEARCH_RESULT_FROM}`,
];
if (includeUserOptions) {
queryParams.push(`claimType=${options[SEARCH_OPTIONS.CLAIM_TYPE]}`);
// If they are only searching for channels, strip out the media info
if (options[SEARCH_OPTIONS.CLAIM_TYPE] !== 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),
''
)}`
);
}
}
return queryParams.join('&');
};