Recommended changes #7089

Merged
saltrafael merged 2 commits from recommended-changes into master 2021-09-16 22:00:44 +02:00
8 changed files with 143 additions and 188 deletions
Showing only changes of commit 920a64d74b - Show all commits

View file

@ -1,31 +1,24 @@
import { connect } from 'react-redux';
import { makeSelectClaimIsNsfw, makeSelectClaimForUri } from 'lbry-redux';
import { doRecommendationUpdate, doRecommendationClicked } from 'redux/actions/content';
import { makeSelectClaimForUri } from 'lbry-redux';
import { doFetchRecommendedContent } from 'redux/actions/search';
import { makeSelectRecommendedContentForUri, selectIsSearching } from 'redux/selectors/search';
import { selectUserVerifiedEmail } from 'redux/selectors/user';
import { makeSelectNextUnplayedRecommended } from 'redux/selectors/content';
import RecommendedContent from './view';
const select = (state, props) => {
const claim = makeSelectClaimForUri(props.uri)(state);
const { claim_id: claimId } = claim;
const recommendedContentUris = makeSelectRecommendedContentForUri(props.uri)(state);
const nextRecommendedUri = recommendedContentUris && recommendedContentUris[0];
return {
mature: makeSelectClaimIsNsfw(props.uri)(state),
recommendedContentUris: makeSelectRecommendedContentForUri(props.uri)(state),
nextRecommendedUri: makeSelectNextUnplayedRecommended(props.uri)(state),
isSearching: selectIsSearching(state),
isAuthenticated: selectUserVerifiedEmail(state),
claim,
claimId,
recommendedContentUris,
nextRecommendedUri,
isSearching: selectIsSearching(state),
isAuthenticated: selectUserVerifiedEmail(state),
};
};
const perform = (dispatch) => ({
doFetchRecommendedContent: (uri, mature) => dispatch(doFetchRecommendedContent(uri, mature)),
doRecommendationUpdate: (claimId, urls, id, parentId) =>
dispatch(doRecommendationUpdate(claimId, urls, id, parentId)),
doRecommendationClicked: (claimId, index) => dispatch(doRecommendationClicked(claimId, index)),
});
export default connect(select, perform)(RecommendedContent);
export default connect(select, { doFetchRecommendedContent })(RecommendedContent);

View file

@ -18,11 +18,9 @@ type Props = {
recommendedContentUris: Array<string>,
nextRecommendedUri: string,
isSearching: boolean,
doFetchRecommendedContent: (string, boolean) => void,
mature: boolean,
doFetchRecommendedContent: (string) => void,
isAuthenticated: boolean,
claim: ?StreamClaim,
doRecommendationUpdate: (claimId: string, urls: Array<string>, id: string, parentId: string) => void,
claimId: string,
};
@ -30,7 +28,6 @@ export default React.memo<Props>(function RecommendedContent(props: Props) {
const {
uri,
doFetchRecommendedContent,
mature,
recommendedContentUris,
nextRecommendedUri,
isSearching,
@ -39,53 +36,29 @@ export default React.memo<Props>(function RecommendedContent(props: Props) {
claimId,
} = props;
const [viewMode, setViewMode] = React.useState(VIEW_ALL_RELATED);
const [recommendationUrls, setRecommendationUrls] = React.useState();
const signingChannel = claim && claim.signing_channel;
const channelName = signingChannel ? signingChannel.name : null;
const isMobile = useIsMobile();
const isMedium = useIsMediumScreen();
const { onRecsLoaded: onRecommendationsLoaded, onClickedRecommended: onRecommendationClicked } = RecSys;
React.useEffect(() => {
function moveAutoplayNextItemToTop(recommendedContent) {
let newList = recommendedContent;
if (newList) {
const index = newList.indexOf(nextRecommendedUri);
if (index > 0) {
const a = newList[0];
newList[0] = nextRecommendedUri;
newList[index] = a;
}
}
return newList;
}
function listEq(prev, next) {
if (prev && next) {
return prev.length === next.length && prev.every((value, index) => value === next[index]);
} else {
return prev === next;
}
}
const newRecommendationUrls = moveAutoplayNextItemToTop(recommendedContentUris);
if (claim && !listEq(recommendationUrls, newRecommendationUrls)) {
setRecommendationUrls(newRecommendationUrls);
}
}, [recommendedContentUris, nextRecommendedUri, recommendationUrls, setRecommendationUrls, claim]);
React.useEffect(() => {
doFetchRecommendedContent(uri, mature);
}, [uri, mature, doFetchRecommendedContent]);
doFetchRecommendedContent(uri);
}, [uri, doFetchRecommendedContent]);
React.useEffect(() => {
// Right now we only want to record the recs if they actually saw them.
if (recommendationUrls && recommendationUrls.length && nextRecommendedUri && viewMode === VIEW_ALL_RELATED) {
onRecommendationsLoaded(claimId, recommendationUrls);
if (
recommendedContentUris &&
recommendedContentUris.length &&
nextRecommendedUri &&
viewMode === VIEW_ALL_RELATED
) {
onRecommendationsLoaded(claimId, recommendedContentUris);
}
}, [recommendationUrls, onRecommendationsLoaded, claimId, nextRecommendedUri, viewMode]);
}, [recommendedContentUris, onRecommendationsLoaded, claimId, nextRecommendedUri, viewMode]);
function handleRecommendationClicked(e, clickedClaim, index: number) {
function handleRecommendationClicked(e, clickedClaim) {
if (claim) {
onRecommendationClicked(claim.claim_id, clickedClaim.claim_id);
}
@ -124,7 +97,7 @@ export default React.memo<Props>(function RecommendedContent(props: Props) {
<ClaimList
type="small"
loading={isSearching}
uris={recommendationUrls}
uris={recommendedContentUris}
hideMenu={isMobile}
injectedItem={SHOW_ADS && IS_WEB && !isAuthenticated && <Ads small type={'video'} />}
empty={__('No related content found')}
@ -164,7 +137,6 @@ function areEqual(prevProps: Props, nextProps: Props) {
a.nextRecommendedUri !== b.nextRecommendedUri ||
a.isAuthenticated !== b.isAuthenticated ||
a.isSearching !== b.isSearching ||
a.mature !== b.mature ||
(a.recommendedContentUris && !b.recommendedContentUris) ||
(!a.recommendedContentUris && b.recommendedContentUris) ||
(a.claim && !b.claim) ||

View file

@ -16,12 +16,8 @@ import {
} from 'redux/actions/app';
import { selectVolume, selectMute } from 'redux/selectors/app';
import { savePosition, clearPosition, doPlayUri, doSetPlayingUri } from 'redux/actions/content';
import {
makeSelectContentPositionForUri,
makeSelectIsPlayerFloating,
makeSelectNextUnplayedRecommended,
selectPlayingUri,
} from 'redux/selectors/content';
import { makeSelectContentPositionForUri, makeSelectIsPlayerFloating, selectPlayingUri } from 'redux/selectors/content';
import { makeSelectRecommendedContentForUri } from 'redux/selectors/search';
import VideoViewer from './view';
import { withRouter } from 'react-router';
import { doClaimEligiblePurchaseRewards } from 'redux/actions/rewards';
@ -47,7 +43,8 @@ const select = (state, props) => {
nextRecommendedUri = makeSelectNextUrlForCollectionAndUrl(collectionId, uri)(state);
previousListUri = makeSelectPreviousUrlForCollectionAndUrl(collectionId, uri)(state);
} else {
nextRecommendedUri = makeSelectNextUnplayedRecommended(uri)(state);
const recommendedContent = makeSelectRecommendedContentForUri(uri)(state);
nextRecommendedUri = recommendedContent && recommendedContent[0];
}
return {

View file

@ -13,9 +13,11 @@ export const SEARCH_TYPES = {
export const SEARCH_OPTIONS = {
RESULT_COUNT: 'size',
CLAIM_TYPE: 'claimType',
RELATED_TO: 'related_to',
INCLUDE_FILES: 'file',
INCLUDE_CHANNELS: 'channel',
INCLUDE_FILES_AND_CHANNELS: 'file,channel',
INCLUDE_MATURE: 'nsfw',
jessopb commented 2021-09-16 17:02:25 +02:00 (Migrated from github.com)
Review

this is good - did you use it anywhere?

this is good - did you use it anywhere?
saltrafael commented 2021-09-16 20:01:45 +02:00 (Migrated from github.com)
Review

now I did 👍

now I did 👍
MEDIA_AUDIO: 'audio',
MEDIA_VIDEO: 'video',
MEDIA_TEXT: 'text',
@ -25,6 +27,7 @@ export const SEARCH_OPTIONS = {
SORT_ACCENDING: '^release_time',
SORT_DESCENDING: 'release_time',
EXACT: 'exact',
PRICE_FILTER_FREE: 'free_only',
TIME_FILTER: 'time_filter',
TIME_FILTER_LAST_HOUR: 'lasthour',
TIME_FILTER_TODAY: 'today',

View file

@ -1,7 +1,15 @@
// @flow
import * as ACTIONS from 'constants/action_types';
import { SEARCH_OPTIONS } from 'constants/search';
import { buildURI, doResolveUris, batchActions, isURIValid, makeSelectClaimForUri } from 'lbry-redux';
import { selectShowMatureContent } from 'redux/selectors/settings';
import {
buildURI,
doResolveUris,
batchActions,
isURIValid,
makeSelectClaimForUri,
makeSelectClaimIsNsfw,
} from 'lbry-redux';
import { makeSelectSearchUrisForQuery, selectSearchValue } from 'redux/selectors/search';
import handleFetchResponse from 'util/handle-fetch';
import { getSearchQueryString } from 'util/query-params';
@ -125,21 +133,26 @@ export const doUpdateSearchOptions = (newOptions: SearchOptions, additionalOptio
}
};
export const doFetchRecommendedContent = (uri: string, mature: boolean) => (dispatch: Dispatch, getState: GetState) => {
export const doFetchRecommendedContent = (uri: string) => (dispatch: Dispatch, getState: GetState) => {
const state = getState();
const claim = makeSelectClaimForUri(uri)(state);
const matureEnabled = selectShowMatureContent(state);
const claimIsMature = makeSelectClaimIsNsfw(uri)(state);
if (claim && claim.value && claim.claim_id) {
const options: SearchOptions = { size: 20, related_to: claim.claim_id, isBackgroundSearch: true };
if (!mature) {
options['nsfw'] = false;
}
const options: SearchOptions = { size: 20, nsfw: matureEnabled, isBackgroundSearch: true };
if (SIMPLE_SITE) {
options[SEARCH_OPTIONS.CLAIM_TYPE] = SEARCH_OPTIONS.INCLUDE_FILES;
options[SEARCH_OPTIONS.MEDIA_VIDEO] = true;
options[SEARCH_OPTIONS.PRICE_FILTER_FREE] = true;
}
if (matureEnabled || !claimIsMature) {
options[SEARCH_OPTIONS.RELATED_TO] = claim.claim_id;
}
const { title } = claim.value;
if (title && options) {
dispatch(doSearch(title, options));
}

View file

@ -7,13 +7,10 @@ import {
makeSelectClaimIsMine,
makeSelectMediaTypeForUri,
selectBalance,
parseURI,
makeSelectContentTypeForUri,
makeSelectFileNameForUri,
} from 'lbry-redux';
import { makeSelectRecommendedContentForUri } from 'redux/selectors/search';
import { selectMutedChannels } from 'redux/selectors/blocked';
import { selectAllCostInfoByUri, makeSelectCostInfoForUri } from 'lbryinc';
import { makeSelectCostInfoForUri } from 'lbryinc';
import { selectShowMatureContent } from 'redux/selectors/settings';
import * as RENDER_MODES from 'constants/file_render_modes';
import path from 'path';
@ -78,78 +75,6 @@ export const makeSelectHistoryForUri = (uri: string) =>
export const makeSelectHasVisitedUri = (uri: string) =>
createSelector(makeSelectHistoryForUri(uri), (history) => Boolean(history));
export const makeSelectNextUnplayedRecommended = (uri: string) =>
createSelector(
makeSelectRecommendedContentForUri(uri),
selectHistory,
selectClaimsByUri,
selectAllCostInfoByUri,
selectMutedChannels,
(
recommendedForUri: Array<string>,
history: Array<{ uri: string }>,
claimsByUri: { [string]: ?Claim },
costInfoByUri: { [string]: { cost: 0 | string } },
blockedChannels: Array<string>
) => {
if (recommendedForUri) {
// Make sure we don't autoplay paid content, channels, or content from blocked channels
for (let i = 0; i < recommendedForUri.length; i++) {
const recommendedUri = recommendedForUri[i];
const claim = claimsByUri[recommendedUri];
if (!claim) {
continue;
}
const { isChannel } = parseURI(recommendedUri);
if (isChannel) {
continue;
}
const costInfo = costInfoByUri[recommendedUri];
if (!costInfo || costInfo.cost !== 0) {
continue;
}
// We already check if it's a channel above
// $FlowFixMe
const isVideo = claim.value && claim.value.stream_type === 'video';
// $FlowFixMe
const isAudio = claim.value && claim.value.stream_type === 'audio';
if (!isVideo && !isAudio) {
continue;
}
const channel = claim && claim.signing_channel;
if (channel && blockedChannels.some((blockedUri) => blockedUri === channel.permanent_url)) {
continue;
}
const recommendedUriInfo = parseURI(recommendedUri);
const recommendedUriShort = recommendedUriInfo.claimName + '#' + recommendedUriInfo.claimId.substring(0, 1);
if (claimsByUri[uri] && claimsByUri[uri].claim_id === recommendedUriInfo.claimId) {
// Skip myself (same claim ID)
continue;
}
if (
!history.some((h) => {
const directMatch = h.uri === recommendedForUri[i];
const shortUriMatch = h.uri.includes(recommendedUriShort);
const idMatch = claimsByUri[h.uri] && claimsByUri[h.uri].claim_id === recommendedUriInfo.claimId;
return directMatch || shortUriMatch || idMatch;
})
) {
return recommendedForUri[i];
}
}
}
}
);
export const selectRecentHistory = createSelector(selectHistory, (history) => {
return history.slice(0, RECENT_HISTORY_AMOUNT);
});

View file

@ -1,18 +1,23 @@
// @flow
import { getSearchQueryString } from 'util/query-params';
import { selectShowMatureContent } from 'redux/selectors/settings';
import { SEARCH_OPTIONS } from 'constants/search';
import {
parseURI,
selectClaimsByUri,
makeSelectClaimForUri,
makeSelectClaimForClaimId,
makeSelectClaimIsNsfw,
buildURI,
isClaimNsfw,
makeSelectPendingClaimForUri,
makeSelectIsUriResolving,
} from 'lbry-redux';
import { createSelector } from 'reselect';
import { createNormalizedSearchKey } from 'util/search';
import { selectMutedChannels } from 'redux/selectors/blocked';
import { selectHistory } from 'redux/selectors/content';
import { selectAllCostInfoByUri } from 'lbryinc';
import { SIMPLE_SITE } from 'config';
type State = { search: SearchState };
@ -38,14 +43,12 @@ export const selectHasReachedMaxResultsLength: (state: State) => { [boolean]: Ar
);
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'];
if (!query) return;
// replace statement below is kind of ugly, and repeated in doSearch action
query = query.replace(/^lbry:\/\//i, '').replace(/\//, ' ');
const normalizedQuery = createNormalizedSearchKey(query);
return byQuery[normalizedQuery] && byQuery[normalizedQuery]['uris'];
});
export const makeSelectHasReachedMaxResultsLength = (query: string): ((state: State) => boolean) =>
@ -60,34 +63,91 @@ export const makeSelectHasReachedMaxResultsLength = (query: string): ((state: St
export const makeSelectRecommendedContentForUri = (uri: string) =>
createSelector(
makeSelectClaimForUri(uri),
selectHistory,
selectClaimsByUri,
selectShowMatureContent,
selectMutedChannels,
selectAllCostInfoByUri,
selectSearchResultByQuery,
makeSelectClaimIsNsfw(uri),
(claim, searchUrisByQuery, isMature) => {
(history, claimsByUri, matureEnabled, blockedChannels, costInfoByUri, searchUrisByQuery, isMature) => {
const claim = claimsByUri[uri];
if (!claim) return;
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 });
// always grab the claimId - this value won't change for filtering
const currentClaimId = claim.claim_id;
const { title } = claim.value;
const { title } = claim.value;
if (!title) {
return;
}
if (!title) return;
const options: {
related_to?: string,
nsfw?: boolean,
isBackgroundSearch?: boolean,
} = { related_to: claim.claim_id, isBackgroundSearch: true };
const options: {
size: number,
nsfw?: boolean,
isBackgroundSearch?: boolean,
} = { size: 20, nsfw: matureEnabled, isBackgroundSearch: true };
options['nsfw'] = isMature;
const searchQuery = getSearchQueryString(title.replace(/\//, ' '), options);
const normalizedSearchQuery = createNormalizedSearchKey(searchQuery);
if (SIMPLE_SITE) {
options[SEARCH_OPTIONS.CLAIM_TYPE] = SEARCH_OPTIONS.INCLUDE_FILES;
options[SEARCH_OPTIONS.MEDIA_VIDEO] = true;
options[SEARCH_OPTIONS.PRICE_FILTER_FREE] = true;
}
if (matureEnabled || (!matureEnabled && !isMature)) {
options[SEARCH_OPTIONS.RELATED_TO] = claim.claim_id;
}
let searchResult = searchUrisByQuery[normalizedSearchQuery];
if (searchResult) {
recommendedContent = searchResult['uris'].filter((searchUri) => searchUri !== currentUri);
const searchQuery = getSearchQueryString(title.replace(/\//, ' '), options);
const normalizedSearchQuery = createNormalizedSearchKey(searchQuery);
let searchResult = searchUrisByQuery[normalizedSearchQuery];
if (searchResult) {
// Filter from recommended: The same claim and blocked channels
recommendedContent = searchResult['uris'].filter((searchUri) => {
const searchClaim = claimsByUri[searchUri];
if (!searchClaim) return;
const signingChannel = searchClaim && searchClaim.signing_channel;
const channelUri = signingChannel && signingChannel.canonical_url;
const blockedMatch = blockedChannels.some((blockedUri) => blockedUri.includes(channelUri));
let isEqualUri;
try {
const { claimId: searchId } = parseURI(searchUri);
isEqualUri = searchId === currentClaimId;
} catch (e) {}
return !isEqualUri && !blockedMatch;
});
// Claim to play next: playable and free claims not played before in history
const nextUriToPlay = recommendedContent.filter((nextRecommendedUri) => {
jessopb commented 2021-09-15 16:39:59 +02:00 (Migrated from github.com)
Review

nice

nice
const costInfo = costInfoByUri[nextRecommendedUri] && costInfoByUri[nextRecommendedUri].cost;
const recommendedClaim = claimsByUri[nextRecommendedUri];
const isVideo = recommendedClaim && recommendedClaim.value && recommendedClaim.value.stream_type === 'video';
const isAudio = recommendedClaim && recommendedClaim.value && recommendedClaim.value.stream_type === 'audio';
let historyMatch = false;
try {
const { claimId: nextRecommendedId } = parseURI(nextRecommendedUri);
historyMatch = history.some(
(historyItem) =>
(claimsByUri[historyItem.uri] && claimsByUri[historyItem.uri].claim_id) === nextRecommendedId
);
} catch (e) {}
return !historyMatch && costInfo === 0 && (isVideo || isAudio);
})[0];
const index = recommendedContent.indexOf(nextUriToPlay);
if (index > 0) {
const a = recommendedContent[0];
recommendedContent[0] = nextUriToPlay;
recommendedContent[index] = a;
}
}
return recommendedContent;
@ -206,6 +266,6 @@ export const makeSelectIsResolvingWinningUri = (query: string = '') => {
};
export const makeSelectUrlForClaimId = (claimId: string) =>
createSelector(
makeSelectClaimForClaimId(claimId), (claim) => claim ? claim.canonical_url || claim.permanent_url : null
createSelector(makeSelectClaimForClaimId(claimId), (claim) =>
claim ? claim.canonical_url || claim.permanent_url : null
);

View file

@ -1,6 +1,5 @@
// @flow
import { SEARCH_OPTIONS } from 'constants/search';
import { SIMPLE_SITE } from 'config';
const DEFAULT_SEARCH_RESULT_FROM = 0;
const DEFAULT_SEARCH_SIZE = 20;
@ -33,7 +32,6 @@ export function updateQueryParam(uri: string, key: string, value: string) {
}
export const getSearchQueryString = (query: string, options: any = {}) => {
const FORCE_FREE_ONLY = SIMPLE_SITE;
const isSurroundedByQuotes = (str) => str[0] === '"' && str[str.length - 1] === '"';
const encodedQuery = encodeURIComponent(query);
const queryParams = [
@ -81,9 +79,11 @@ export const getSearchQueryString = (query: string, options: any = {}) => {
const additionalOptions = {};
const { related_to } = options;
const { nsfw } = options;
const { free_only } = options;
if (related_to) additionalOptions['related_to'] = related_to;
if (nsfw === false) additionalOptions['nsfw'] = false;
if (related_to) additionalOptions[SEARCH_OPTIONS.RELATED_TO] = related_to;
if (free_only) additionalOptions[SEARCH_OPTIONS.PRICE_FILTER_FREE] = true;
if (nsfw === false) additionalOptions[SEARCH_OPTIONS.INCLUDE_MATURE] = false;
if (additionalOptions) {
Object.keys(additionalOptions).forEach((key) => {
@ -92,13 +92,5 @@ export const getSearchQueryString = (query: string, options: any = {}) => {
});
}
if (FORCE_FREE_ONLY) {
const index = queryParams.findIndex((q) => q.startsWith('free_only'));
if (index > -1) {
queryParams.splice(index, 1);
}
queryParams.push(`free_only=true`);
}
return queryParams.join('&');
};