diff --git a/flow-typed/search.js b/flow-typed/search.js index adfb768f6..b795f9c12 100644 --- a/flow-typed/search.js +++ b/flow-typed/search.js @@ -28,7 +28,7 @@ declare type SearchOptions = { declare type SearchState = { options: SearchOptions, - urisByQuery: {}, + resultsByQuery: {}, hasReachedMaxResultsLength: {}, searching: boolean, }; @@ -40,6 +40,7 @@ declare type SearchSuccess = { from: number, size: number, uris: Array, + recsys: string, }, }; diff --git a/ui/component/channelContent/view.jsx b/ui/component/channelContent/view.jsx index 3498e5799..c069428a5 100644 --- a/ui/component/channelContent/view.jsx +++ b/ui/component/channelContent/view.jsx @@ -81,7 +81,7 @@ function ChannelContent(props: Props) { !showMature ? '&nsfw=false&size=50&from=0' : '' }` ) - .then((results) => { + .then(({ body: results }) => { const urls = results.map(({ name, claimId }) => { return `lbry://${name}#${claimId}`; }); diff --git a/ui/component/viewers/videoViewer/internal/plugins/videojs-recsys/plugin.js b/ui/component/viewers/videoViewer/internal/plugins/videojs-recsys/plugin.js index 5ee3f3c8d..01fd9a117 100644 --- a/ui/component/viewers/videoViewer/internal/plugins/videojs-recsys/plugin.js +++ b/ui/component/viewers/videoViewer/internal/plugins/videojs-recsys/plugin.js @@ -6,6 +6,7 @@ import { makeSelectRecommendedClaimIds, makeSelectRecommendationClicks, } from 'redux/selectors/content'; +import { makeSelectRecommendedRecsysIdForClaimId } from 'redux/selectors/search'; const VERSION = '0.0.1'; @@ -36,7 +37,7 @@ function createRecsys(claimId, userId, events, loadedAt, isEmbed) { claimId: claimId, pageLoadedAt: pageLoadedAt, pageExitedAt: pageExitedAt, - recsysId: recsysId, + recsysId: makeSelectRecommendedRecsysIdForClaimId(claimId)(state) || recsysId, recClaimIds: makeSelectRecommendedClaimIds(claimId)(state), recClickedVideoIdx: makeSelectRecommendationClicks(claimId)(state), events: events, diff --git a/ui/effects/use-lighthouse.js b/ui/effects/use-lighthouse.js index 029668c20..b5b9f31e5 100644 --- a/ui/effects/use-lighthouse.js +++ b/ui/effects/use-lighthouse.js @@ -25,7 +25,7 @@ export default function useLighthouse( let isSubscribed = true; lighthouse .search(throttledQuery) - .then((results) => { + .then(({ body: results }) => { if (isSubscribed) { setResults( results.map((result) => `lbry://${result.name}#${result.claimId}`).filter((uri) => isURIValid(uri)) diff --git a/ui/page/search/index.js b/ui/page/search/index.js index 30558e61a..d1c140fd1 100644 --- a/ui/page/search/index.js +++ b/ui/page/search/index.js @@ -3,7 +3,7 @@ import { withRouter } from 'react-router'; import { doSearch } from 'redux/actions/search'; import { selectIsSearching, - makeSelectSearchUris, + makeSelectSearchUrisForQuery, selectSearchOptions, makeSelectHasReachedMaxResultsLength, } from 'redux/selectors/search'; @@ -28,7 +28,7 @@ const select = (state, props) => { }; const query = getSearchQueryString(urlQuery, searchOptions); - const uris = makeSelectSearchUris(query)(state); + const uris = makeSelectSearchUrisForQuery(query)(state); const hasReachedMaxResultsLength = makeSelectHasReachedMaxResultsLength(query)(state); return { diff --git a/ui/redux/actions/search.js b/ui/redux/actions/search.js index 0da0039d0..28b1a724c 100644 --- a/ui/redux/actions/search.js +++ b/ui/redux/actions/search.js @@ -2,7 +2,7 @@ import * as ACTIONS from 'constants/action_types'; import { SEARCH_OPTIONS } from 'constants/search'; import { buildURI, doResolveUris, batchActions, isURIValid, makeSelectClaimForUri } from 'lbry-redux'; -import { makeSelectSearchUris, selectSearchValue } from 'redux/selectors/search'; +import { makeSelectSearchUrisForQuery, selectSearchValue } from 'redux/selectors/search'; import handleFetchResponse from 'util/handle-fetch'; import { getSearchQueryString } from 'util/query-params'; import { SIMPLE_SITE, SEARCH_SERVER_API } from 'config'; @@ -48,7 +48,7 @@ export const doSearch = (rawQuery: string, searchOptions: SearchOptions) => ( const from = searchOptions.from; // If we have already searched for something, we don't need to do anything - const urisForQuery = makeSelectSearchUris(queryWithOptions)(state); + const urisForQuery = makeSelectSearchUrisForQuery(queryWithOptions)(state); if (urisForQuery && !!urisForQuery.length) { if (!size || !from || from + size < urisForQuery.length) { return; @@ -61,13 +61,14 @@ export const doSearch = (rawQuery: string, searchOptions: SearchOptions) => ( lighthouse .search(queryWithOptions) - .then((data: Array<{ name: string, claimId: string }>) => { + .then((data: { body: Array<{ name: string, claimId: string }>, poweredBy: string }) => { + const { body: result, poweredBy } = data; const uris = []; const actions = []; - data.forEach((result) => { - if (result) { - const { name, claimId } = result; + result.forEach((item) => { + if (item) { + const { name, claimId } = item; const urlObj: LbryUrlObj = {}; if (name.startsWith('@')) { @@ -94,6 +95,7 @@ export const doSearch = (rawQuery: string, searchOptions: SearchOptions) => ( from: from, size: size, uris, + recsys: poweredBy, }, }); dispatch(batchActions(...actions)); diff --git a/ui/redux/reducers/search.js b/ui/redux/reducers/search.js index 7730eeaa5..e97f868ec 100644 --- a/ui/redux/reducers/search.js +++ b/ui/redux/reducers/search.js @@ -17,7 +17,7 @@ const defaultState: SearchState = { [SEARCH_OPTIONS.MEDIA_IMAGE]: defaultSearchTypes.includes(SEARCH_OPTIONS.MEDIA_IMAGE), [SEARCH_OPTIONS.MEDIA_APPLICATION]: defaultSearchTypes.includes(SEARCH_OPTIONS.MEDIA_APPLICATION), }, - urisByQuery: {}, + resultsByQuery: {}, hasReachedMaxResultsLength: {}, searching: false, }; @@ -29,21 +29,23 @@ export default handleActions( searching: true, }), [ACTIONS.SEARCH_SUCCESS]: (state: SearchState, action: SearchSuccess): SearchState => { - const { query, uris, from, size } = action.data; + const { query, uris, from, size, recsys } = action.data; const normalizedQuery = createNormalizedSearchKey(query); + const urisForQuery = state.resultsByQuery[normalizedQuery] && state.resultsByQuery[normalizedQuery]['uris']; let newUris = uris; - if (from !== 0 && state.urisByQuery[normalizedQuery]) { - newUris = Array.from(new Set(state.urisByQuery[normalizedQuery].concat(uris))); + if (from !== 0 && urisForQuery) { + newUris = Array.from(new Set(urisForQuery.concat(uris))); } // The returned number of urls is less than the page size, so we're on the last page const noMoreResults = size && uris.length < size; + const results = { uris: newUris, recsys }; return { ...state, searching: false, - urisByQuery: Object.assign({}, state.urisByQuery, { [normalizedQuery]: newUris }), + resultsByQuery: Object.assign({}, state.resultsByQuery, { [normalizedQuery]: results }), hasReachedMaxResultsLength: Object.assign({}, state.hasReachedMaxResultsLength, { [normalizedQuery]: noMoreResults, }), diff --git a/ui/redux/selectors/search.js b/ui/redux/selectors/search.js index d864ea00d..1d97b1976 100644 --- a/ui/redux/selectors/search.js +++ b/ui/redux/selectors/search.js @@ -4,6 +4,7 @@ import { selectShowMatureContent } from 'redux/selectors/settings'; import { parseURI, makeSelectClaimForUri, + makeSelectClaimForClaimId, makeSelectClaimIsNsfw, buildURI, isClaimNsfw, @@ -26,9 +27,9 @@ export const selectSearchOptions: (state: State) => SearchOptions = createSelect export const selectIsSearching: (state: State) => boolean = createSelector(selectState, (state) => state.searching); -export const selectSearchUrisByQuery: (state: State) => { [string]: Array } = createSelector( +export const selectSearchResultByQuery: (state: State) => { [string]: Array } = createSelector( selectState, - (state) => state.urisByQuery + (state) => state.resultsByQuery ); export const selectHasReachedMaxResultsLength: (state: State) => { [boolean]: Array } = createSelector( @@ -36,15 +37,15 @@ export const selectHasReachedMaxResultsLength: (state: State) => { [boolean]: Ar (state) => state.hasReachedMaxResultsLength ); -export const makeSelectSearchUris = (query: string): ((state: State) => Array) => +export const makeSelectSearchUrisForQuery = (query: string): ((state: State) => Array) => // replace statement below is kind of ugly, and repeated in doSearch action - createSelector(selectSearchUrisByQuery, (byQuery) => { + createSelector(selectSearchResultByQuery, (byQuery) => { if (query) { query = query.replace(/^lbry:\/\//i, '').replace(/\//, ' '); const normalizedQuery = createNormalizedSearchKey(query); - return byQuery[normalizedQuery]; + return byQuery[normalizedQuery] && byQuery[normalizedQuery]['uris']; } - return byQuery[query]; + return byQuery[query] && byQuery[query]['uris']; }); export const makeSelectHasReachedMaxResultsLength = (query: string): ((state: State) => boolean) => @@ -60,7 +61,7 @@ export const makeSelectHasReachedMaxResultsLength = (query: string): ((state: St export const makeSelectRecommendedContentForUri = (uri: string) => createSelector( makeSelectClaimForUri(uri), - selectSearchUrisByQuery, + selectSearchResultByQuery, makeSelectClaimIsNsfw(uri), (claim, searchUrisByQuery, isMature) => { let recommendedContent; @@ -84,16 +85,47 @@ export const makeSelectRecommendedContentForUri = (uri: string) => const searchQuery = getSearchQueryString(title.replace(/\//, ' '), options); const normalizedSearchQuery = createNormalizedSearchKey(searchQuery); - let searchUris = searchUrisByQuery[normalizedSearchQuery]; - if (searchUris) { - searchUris = searchUris.filter((searchUri) => searchUri !== currentUri); - recommendedContent = searchUris; + 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) { + 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}`; diff --git a/ui/util/handle-fetch.js b/ui/util/handle-fetch.js index 83ce10513..4686b9b1a 100644 --- a/ui/util/handle-fetch.js +++ b/ui/util/handle-fetch.js @@ -1,4 +1,9 @@ // @flow export default function handleFetchResponse(response: Response): Promise { - return response.status === 200 ? Promise.resolve(response.json()) : Promise.reject(new Error(response.statusText)); + const headers = response.headers; + const poweredBy = headers.get('x-powered-by'); + + return response.status === 200 + ? response.json().then((body) => ({ body, poweredBy })) + : Promise.reject(new Error(response.statusText)); }