lbry-desktop/ui/redux/selectors/search.js
jessopb 049fb2878e
recsys v0.2 (#6977)
* recsys wip

better logging

fix floating player popout playing uri bug with recsys

lint

add empty entries to create

use beacon; fire on visibilitychange

cleanup, not record recs if not seen

ifweb recsys beacon

recsys handle embeds, cleanup

use history.listen to trigger events

fix recsys embed bug

bugfix

more default data

cleaner

cleaner

* remove tentative

* disable recsys debug logging
2021-09-02 18:39:40 -04:00

211 lines
7.1 KiB
JavaScript

// @flow
import { getSearchQueryString } from 'util/query-params';
import { selectShowMatureContent } from 'redux/selectors/settings';
import {
parseURI,
makeSelectClaimForUri,
makeSelectClaimForClaimId,
makeSelectClaimIsNsfw,
buildURI,
isClaimNsfw,
makeSelectPendingClaimForUri,
makeSelectIsUriResolving,
} from 'lbry-redux';
import { createSelector } from 'reselect';
import { createNormalizedSearchKey } from 'util/search';
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 selectIsSearching: (state: State) => boolean = createSelector(selectState, (state) => state.searching);
export const selectSearchResultByQuery: (state: State) => { [string]: Array<string> } = createSelector(
selectState,
(state) => state.resultsByQuery
);
export const selectHasReachedMaxResultsLength: (state: State) => { [boolean]: Array<boolean> } = createSelector(
selectState,
(state) => state.hasReachedMaxResultsLength
);
export const makeSelectSearchUrisForQuery = (query: string): ((state: State) => Array<string>) =>
// replace statement below is kind of ugly, and repeated in doSearch action
createSelector(selectSearchResultByQuery, (byQuery) => {
if (query) {
query = query.replace(/^lbry:\/\//i, '').replace(/\//, ' ');
const normalizedQuery = createNormalizedSearchKey(query);
return byQuery[normalizedQuery] && byQuery[normalizedQuery]['uris'];
}
return byQuery[query] && byQuery[query]['uris'];
});
export const makeSelectHasReachedMaxResultsLength = (query: string): ((state: State) => boolean) =>
createSelector(selectHasReachedMaxResultsLength, (hasReachedMaxResultsLength) => {
if (query) {
query = query.replace(/^lbry:\/\//i, '').replace(/\//, ' ');
const normalizedQuery = createNormalizedSearchKey(query);
return hasReachedMaxResultsLength[normalizedQuery];
}
return hasReachedMaxResultsLength[query];
});
export const makeSelectRecommendedContentForUri = (uri: string) =>
createSelector(
makeSelectClaimForUri(uri),
selectSearchResultByQuery,
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 };
options['nsfw'] = isMature;
const searchQuery = getSearchQueryString(title.replace(/\//, ' '), options);
const normalizedSearchQuery = createNormalizedSearchKey(searchQuery);
let searchResult = searchUrisByQuery[normalizedSearchQuery];
if (searchResult) {
recommendedContent = searchResult['uris'].filter((searchUri) => searchUri !== currentUri);
}
}
return recommendedContent;
}
);
export const makeSelectRecommendedRecsysIdForClaimId = (claimId: string) =>
createSelector(makeSelectClaimForClaimId(claimId), selectSearchResultByQuery, (claim, searchUrisByQuery) => {
// TODO: DRY this out.
let poweredBy;
if (claim && claimId) {
const isMature = isClaimNsfw(claim);
const { title } = claim.value;
if (!title) {
return;
}
const options: {
related_to?: string,
nsfw?: boolean,
isBackgroundSearch?: boolean,
} = { related_to: claim.claim_id, isBackgroundSearch: true };
options['nsfw'] = isMature;
const searchQuery = getSearchQueryString(title.replace(/\//, ' '), options);
const normalizedSearchQuery = createNormalizedSearchKey(searchQuery);
let searchResult = searchUrisByQuery[normalizedSearchQuery];
if (searchResult) {
poweredBy = searchResult.recsys;
} else {
return normalizedSearchQuery;
}
}
return poweredBy;
});
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(
selectShowMatureContent,
makeSelectPendingClaimForUri(uriFromQuery),
makeSelectClaimForUri(uriFromQuery),
makeSelectClaimForUri(channelUriFromQuery),
(matureEnabled, pendingClaim, claim1, claim2) => {
const claim1Mature = claim1 && isClaimNsfw(claim1);
const claim2Mature = claim2 && isClaimNsfw(claim2);
let pendingAmount = pendingClaim && pendingClaim.amount;
if (!claim1 && !claim2) {
return undefined;
} else if (!claim1 && claim2) {
return matureEnabled ? claim2.canonical_url : claim2Mature ? undefined : claim2.canonical_url;
} else if (claim1 && !claim2) {
return matureEnabled
? claim1.repost_url || claim1.canonical_url
: claim1Mature
? undefined
: claim1.repost_url || claim1.canonical_url;
}
const effectiveAmount1 = claim1 && (claim1.repost_bid_amount || claim1.meta.effective_amount);
// claim2 will never have a repost_bid_amount because reposts never start with "@"
const effectiveAmount2 = claim2 && claim2.meta.effective_amount;
if (!matureEnabled) {
if (claim1Mature && !claim2Mature) {
return claim2.canonical_url;
} else if (claim2Mature && !claim1Mature) {
return claim1.repost_url || claim1.canonical_url;
} else if (claim1Mature && claim2Mature) {
return undefined;
}
}
const returnBeforePending =
Number(effectiveAmount1) > Number(effectiveAmount2)
? claim1.repost_url || claim1.canonical_url
: claim2.canonical_url;
if (pendingAmount && pendingAmount > effectiveAmount1 && pendingAmount > effectiveAmount2) {
return pendingAmount.permanent_url;
} else {
return returnBeforePending;
}
}
);
};
export const makeSelectIsResolvingWinningUri = (query: string = '') => {
const uriFromQuery = `lbry://${query}`;
let channelUriFromQuery;
try {
const { isChannel } = parseURI(uriFromQuery);
if (!isChannel) {
channelUriFromQuery = `lbry://@${query}`;
}
} catch (e) {}
return createSelector(
makeSelectIsUriResolving(uriFromQuery),
channelUriFromQuery ? makeSelectIsUriResolving(channelUriFromQuery) : () => {},
(claim1IsResolving, claim2IsResolving) => {
return claim1IsResolving || claim2IsResolving;
}
);
};
export const makeSelectUrlForClaimId = (claimId: string) =>
createSelector(
makeSelectClaimForClaimId(claimId), (claim) => claim ? claim.canonical_url || claim.permanent_url : null
);