lbry-desktop/ui/redux/selectors/search.js
2020-11-04 16:08:23 -05:00

207 lines
6.6 KiB
JavaScript

// @flow
import { SEARCH_TYPES } from 'constants/search';
import { getSearchQueryString } from 'util/query-params';
import { 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
let type: string;
try {
let { isChannel } = parseURI(query);
type = isChannel ? SEARCH_TYPES.CHANNEL : SEARCH_TYPES.FILE;
} catch (e) {
type = SEARCH_TYPES.SEARCH;
}
return [
{
value: query,
type: type,
},
];
}
let searchSuggestions = [];
searchSuggestions.push({
value: query,
type: SEARCH_TYPES.SEARCH,
});
try {
const uriObj = parseURI(query);
searchSuggestions.push({
value: buildURI(uriObj),
shorthand: uriObj.isChannel ? uriObj.channelName : uriObj.streamName,
type: uriObj.isChannel ? SEARCH_TYPES.CHANNEL : SEARCH_TYPES.FILE,
});
} catch (e) {}
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 uriObj = parseURI(suggestion);
return {
value: buildURI(uriObj),
shorthand: uriObj.isChannel ? uriObj.channelName : uriObj.streamName,
type: uriObj.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;
}
);
export const makeSelectWinningUriForQuery = (query: string) => {
const uriFromQuery = `lbry://${query}`;
let channelUriFromQuery;
try {
const { isChannel } = parseURI(uriFromQuery);
if (!isChannel) {
channelUriFromQuery = `lbry://@${query}`;
}
} catch (e) {}
return createSelector(
makeSelectClaimForUri(uriFromQuery),
makeSelectClaimForUri(channelUriFromQuery),
(claim1, claim2) => {
if (!claim1 && !claim2) {
return undefined;
} else if (!claim1 && claim2) {
return claim2.canonical_url;
} else if (claim1 && !claim2) {
return claim1.canonical_url;
}
const effectiveAmount1 = claim1 && claim1.meta.effective_amount;
const effectiveAmount2 = claim2 && claim2.meta.effective_amount;
return Number(effectiveAmount1) > Number(effectiveAmount2) ? claim1.canonical_url : claim2.canonical_url;
}
);
};