diff --git a/.flowconfig b/.flowconfig index ffc05b99e..9e73c0bd0 100644 --- a/.flowconfig +++ b/.flowconfig @@ -31,6 +31,7 @@ module.name_mapper='^modal\(.*\)$' -> '/ui/modal\1' module.name_mapper='^app\(.*\)$' -> '/ui/app\1' module.name_mapper='^native\(.*\)$' -> '/ui/native\1' module.name_mapper='^analytics\(.*\)$' -> '/ui/analytics\1' +module.name_mapper='^recsys\(.*\)$' -> '/ui/recsys\1' module.name_mapper='^rewards\(.*\)$' -> '/ui/rewards\1' module.name_mapper='^i18n\(.*\)$' -> '/ui/i18n\1' module.name_mapper='^effects\(.*\)$' -> '/ui/effects\1' diff --git a/ui/component/claimList/view.jsx b/ui/component/claimList/view.jsx index 79796ff5e..4da171d4f 100644 --- a/ui/component/claimList/view.jsx +++ b/ui/component/claimList/view.jsx @@ -27,7 +27,6 @@ type Props = { onScrollBottom?: (any) => void, page?: number, pageSize?: number, - id?: string, // If using the default header, this is a unique ID needed to persist the state of the filter setting persistedStorageKey?: string, showHiddenByUser: boolean, @@ -47,7 +46,7 @@ type Props = { searchOptions?: any, collectionId?: string, showNoSourceClaims?: boolean, - onClick?: (e: any, index?: number) => void, + onClick?: (e: any, claim?: ?Claim, index?: number) => void, }; export default function ClaimList(props: Props) { @@ -56,7 +55,6 @@ export default function ClaimList(props: Props) { uris, headerAltControls, loading, - id, persistedStorageKey, empty, defaultSort, @@ -110,9 +108,9 @@ export default function ClaimList(props: Props) { setCurrentSort(currentSort === SORT_NEW ? SORT_OLD : SORT_NEW); } - function handleClaimClicked(e, index) { + function handleClaimClicked(e, claim, index) { if (onClick) { - onClick(e, index); + onClick(e, claim, index); } } @@ -200,7 +198,6 @@ export default function ClaimList(props: Props) { {injectedItem && index === 4 &&
  • {injectedItem}
  • } handleClaimClicked(e, claim, index)} /> ))} diff --git a/ui/component/claimListDiscover/view.jsx b/ui/component/claimListDiscover/view.jsx index 99dfb6196..843541054 100644 --- a/ui/component/claimListDiscover/view.jsx +++ b/ui/component/claimListDiscover/view.jsx @@ -583,7 +583,6 @@ function ClaimListDiscover(props: Props) { )} )} import('component/abandonedChannelPreview' /* webpackChunkName: "abandonedChannelPreview" */) @@ -67,7 +66,7 @@ type Props = { actions: boolean | Node | string | number, properties: boolean | Node | string | number | ((Claim) => Node), empty?: Node, - onClick?: (e: any, index?: number) => any, + onClick?: (e: any, claim?: ?Claim, index?: number) => any, streamingUrl: ?string, getFile: (string) => void, customShouldHide?: (Claim) => boolean, @@ -90,7 +89,6 @@ type Props = { disableNavigation?: boolean, mediaDuration?: string, date?: any, - containerId?: string, // ID or name of the container (e.g. ClaimList, HOC, etc.) that this is in. indexInContainer?: number, // The index order of this component within 'containerId'. channelSubCount?: number, }; @@ -154,7 +152,6 @@ const ClaimPreview = forwardRef((props: Props, ref: any) => { isCollectionMine, collectionUris, disableNavigation, - containerId, indexInContainer, channelSubCount, } = props; @@ -223,7 +220,7 @@ const ClaimPreview = forwardRef((props: Props, ref: any) => { const handleNavLinkClick = (e) => { if (onClick) { - onClick(e, indexInContainer); + onClick(e, claim, indexInContainer); // not sure indexInContainer is used for anything. } e.stopPropagation(); }; @@ -232,10 +229,9 @@ const ClaimPreview = forwardRef((props: Props, ref: any) => { to: { pathname: navigateUrl, search: navigateSearch.toString() ? '?' + navigateSearch.toString() : '', - state: containerId ? { [CONTAINER_ID]: containerId } : undefined, }, - onClick: (e) => handleNavLinkClick(e), - onAuxClick: (e) => handleNavLinkClick(e), + onClick: handleNavLinkClick, + onAuxClick: handleNavLinkClick, }; // do not block abandoned and nsfw claims if showUserBlocked is passed @@ -281,14 +277,13 @@ const ClaimPreview = forwardRef((props: Props, ref: any) => { function handleOnClick(e) { if (onClick) { - onClick(e, indexInContainer); + onClick(e, claim, indexInContainer); } if (claim && !pending && !disableNavigation) { history.push({ pathname: navigateUrl, search: navigateSearch.toString() ? '?' + navigateSearch.toString() : '', - state: containerId ? { [CONTAINER_ID]: containerId } : undefined, }); } } diff --git a/ui/component/recommendedContent/index.js b/ui/component/recommendedContent/index.js index ea5cf0421..efd71ec50 100644 --- a/ui/component/recommendedContent/index.js +++ b/ui/component/recommendedContent/index.js @@ -7,14 +7,19 @@ import { selectUserVerifiedEmail } from 'redux/selectors/user'; import { makeSelectNextUnplayedRecommended } from 'redux/selectors/content'; import RecommendedContent from './view'; -const select = (state, props) => ({ - mature: makeSelectClaimIsNsfw(props.uri)(state), - recommendedContent: makeSelectRecommendedContentForUri(props.uri)(state), - nextRecommendedUri: makeSelectNextUnplayedRecommended(props.uri)(state), - isSearching: selectIsSearching(state), - isAuthenticated: selectUserVerifiedEmail(state), - claim: makeSelectClaimForUri(props.uri)(state), -}); +const select = (state, props) => { + const claim = makeSelectClaimForUri(props.uri)(state); + const { claim_id: claimId } = claim; + return { + mature: makeSelectClaimIsNsfw(props.uri)(state), + recommendedContentUris: makeSelectRecommendedContentForUri(props.uri)(state), + nextRecommendedUri: makeSelectNextUnplayedRecommended(props.uri)(state), + isSearching: selectIsSearching(state), + isAuthenticated: selectUserVerifiedEmail(state), + claim, + claimId, + }; +}; const perform = (dispatch) => ({ doFetchRecommendedContent: (uri, mature) => dispatch(doFetchRecommendedContent(uri, mature)), diff --git a/ui/component/recommendedContent/view.jsx b/ui/component/recommendedContent/view.jsx index 3adfa8fcf..fdf366272 100644 --- a/ui/component/recommendedContent/view.jsx +++ b/ui/component/recommendedContent/view.jsx @@ -1,8 +1,6 @@ // @flow import { SHOW_ADS } from 'config'; import React from 'react'; -import { useHistory } from 'react-router-dom'; -import { v4 as uuidv4 } from 'uuid'; import ClaimList from 'component/claimList'; import ClaimListDiscover from 'component/claimListDiscover'; import Ads from 'web/component/ads'; @@ -10,14 +8,14 @@ import Card from 'component/common/card'; import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize'; import Button from 'component/button'; import classnames from 'classnames'; -import { CONTAINER_ID } from 'constants/navigation'; +import RecSys from 'recsys'; const VIEW_ALL_RELATED = 'view_all_related'; const VIEW_MORE_FROM = 'view_more_from'; type Props = { uri: string, - recommendedContent: Array, + recommendedContentUris: Array, nextRecommendedUri: string, isSearching: boolean, doFetchRecommendedContent: (string, boolean) => void, @@ -25,7 +23,7 @@ type Props = { isAuthenticated: boolean, claim: ?StreamClaim, doRecommendationUpdate: (claimId: string, urls: Array, id: string, parentId: string) => void, - doRecommendationClicked: (claimId: string, index: number) => void, + claimId: string, }; export default React.memo(function RecommendedContent(props: Props) { @@ -33,23 +31,20 @@ export default React.memo(function RecommendedContent(props: Props) { uri, doFetchRecommendedContent, mature, - recommendedContent, + recommendedContentUris, nextRecommendedUri, isSearching, isAuthenticated, claim, - doRecommendationUpdate, - doRecommendationClicked, + claimId, } = props; const [viewMode, setViewMode] = React.useState(VIEW_ALL_RELATED); - const [recommendationId, setRecommendationId] = React.useState(''); const [recommendationUrls, setRecommendationUrls] = React.useState(); - const history = useHistory(); 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; @@ -72,33 +67,27 @@ export default React.memo(function RecommendedContent(props: Props) { } } - const newRecommendationUrls = moveAutoplayNextItemToTop(recommendedContent); + const newRecommendationUrls = moveAutoplayNextItemToTop(recommendedContentUris); if (claim && !listEq(recommendationUrls, newRecommendationUrls)) { - const parentId = (history.location.state && history.location.state[CONTAINER_ID]) || ''; - const id = uuidv4(); - setRecommendationId(id); setRecommendationUrls(newRecommendationUrls); - - doRecommendationUpdate(claim.claim_id, newRecommendationUrls, id, parentId); } - }, [ - recommendedContent, - nextRecommendedUri, - recommendationUrls, - setRecommendationUrls, - claim, - doRecommendationUpdate, - history.location.state, - ]); + }, [recommendedContentUris, nextRecommendedUri, recommendationUrls, setRecommendationUrls, claim]); React.useEffect(() => { doFetchRecommendedContent(uri, mature); }, [uri, mature, doFetchRecommendedContent]); - function handleRecommendationClicked(e: any, index: number) { + 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); + } + }, [recommendationUrls, onRecommendationsLoaded, claimId, nextRecommendedUri, viewMode]); + + function handleRecommendationClicked(e, clickedClaim, index: number) { if (claim) { - doRecommendationClicked(claim.claim_id, index); + onRecommendationClicked(claim.claim_id, clickedClaim.claim_id); } } @@ -133,7 +122,6 @@ export default React.memo(function RecommendedContent(props: Props) {
    {viewMode === VIEW_ALL_RELATED && ( response.json()) - .then((data) => { - // console.log(`Recsys response data:`, data); - }); - } catch (error) { - // console.error(`Recsys Error`, error); - } -} - const defaults = { - endpoint: recsysEndpoint, - recsysId: recsysId, videoId: null, userId: 0, debug: false, + embedded: false, }; const Component = videojs.getComponent('Component'); @@ -98,16 +45,13 @@ class RecsysPlugin extends Component { // Plugin started if (options.debug) { - this.log(`Created recsys plugin for: videoId:${options.videoId}, userId:${options.userId}`); + this.log(`Created recsys plugin for: videoId:${options.videoId}`); } // To help with debugging, we'll add a global vjs object with the video js player window.vjs = player; this.player = player; - - this.recsysEvents = []; - this.loadedAt = Date.now(); this.lastTimeUpdate = null; this.currentTimeUpdate = null; this.inPause = false; @@ -124,57 +68,37 @@ class RecsysPlugin extends Component { player.on('dispose', (event) => this.onDispose(event)); } - addRecsysEvent(recsysEvent) { - // For now, don't do client-side preprocessing. I think there - // are browser inconsistencies and preprocessing loses too much info. - this.recsysEvents.push(recsysEvent); - } - - getRecsysEvents() { - return this.recsysEvents; - } - - sendRecsysEvents() { - const event = createRecsys( - this.options_.videoId, - this.options_.userId, - this.getRecsysEvents(), - this.loadedAt, - false - ); - - if (event) { - sendRecsysEvents(event); - } - } - onPlay(event) { - const recsysEvent = newRecsysEvent(RecsysData.event.start, this.player.currentTime()); + const recsysEvent = newRecsysPlayerEvent(PlayerEvent.event.start, this.player.currentTime()); this.log('onPlay', recsysEvent); - this.addRecsysEvent(recsysEvent); + RecSys.onRecsysPlayerEvent(this.options_.videoId, recsysEvent, this.options_.embedded); this.inPause = false; this.lastTimeUpdate = recsysEvent.offset; } onPause(event) { - const recsysEvent = newRecsysEvent(RecsysData.event.stop, this.player.currentTime()); + const recsysEvent = newRecsysPlayerEvent(PlayerEvent.event.stop, this.player.currentTime()); this.log('onPause', recsysEvent); - this.addRecsysEvent(recsysEvent); + RecSys.onRecsysPlayerEvent(this.options_.videoId, recsysEvent); this.inPause = true; } onEnded(event) { - const recsysEvent = newRecsysEvent(RecsysData.event.stop, this.player.currentTime()); + const recsysEvent = newRecsysPlayerEvent(PlayerEvent.event.stop, this.player.currentTime()); this.log('onEnded', recsysEvent); - this.addRecsysEvent(recsysEvent); + RecSys.onRecsysPlayerEvent(this.options_.videoId, recsysEvent); } onRateChange(event) { - const recsysEvent = newRecsysEvent(RecsysData.event.speed, this.player.currentTime(), this.player.playbackRate()); + const recsysEvent = newRecsysPlayerEvent( + PlayerEvent.event.speed, + this.player.currentTime(), + this.player.playbackRate() + ); this.log('onRateChange', recsysEvent); - this.addRecsysEvent(recsysEvent); + RecSys.onRecsysPlayerEvent(this.options_.videoId, recsysEvent); } onTimeUpdate(event) { @@ -212,19 +136,19 @@ class RecsysPlugin extends Component { if (fromTime !== curTime) { // This removes duplicates that aren't useful. - const recsysEvent = newRecsysEvent(RecsysData.event.scrub, fromTime, curTime); + const recsysEvent = newRecsysPlayerEvent(PlayerEvent.event.scrub, fromTime, curTime); this.log('onSeeked', recsysEvent); - this.addRecsysEvent(recsysEvent); + RecSys.onRecsysPlayerEvent(this.options_.videoId, recsysEvent); } } onDispose(event) { - this.sendRecsysEvents(); + RecSys.onPlayerDispose(this.options_.videoId, this.options_.embedded); } log(...args) { if (this.options_.debug) { - // console.log(`Recsys Debug:`, JSON.stringify(args)); + console.log(`Recsys Player Debug:`, JSON.stringify(args)); } } } diff --git a/ui/component/viewers/videoViewer/internal/videojs.jsx b/ui/component/viewers/videoViewer/internal/videojs.jsx index 41751838c..451bb0b7a 100644 --- a/ui/component/viewers/videoViewer/internal/videojs.jsx +++ b/ui/component/viewers/videoViewer/internal/videojs.jsx @@ -54,6 +54,7 @@ type Props = { startMuted: boolean, autoplay: boolean, autoplaySetting: boolean, + embedded: boolean, toggleVideoTheaterMode: () => void, adUrl: ?string, claimId: ?string, @@ -194,6 +195,7 @@ export default React.memo(function VideoJs(props: Props) { const { autoplay, autoplaySetting, + embedded, startMuted, source, sourceType, @@ -590,6 +592,7 @@ export default React.memo(function VideoJs(props: Props) { player.recsys({ videoId: claimId, userId: userId, + embedded: embedded, }); } diff --git a/ui/component/viewers/videoViewer/view.jsx b/ui/component/viewers/videoViewer/view.jsx index d7e00b965..905187ded 100644 --- a/ui/component/viewers/videoViewer/view.jsx +++ b/ui/component/viewers/videoViewer/view.jsx @@ -125,7 +125,7 @@ function VideoViewer(props: Props) { const [isPlaying, setIsPlaying] = useState(false); const [ended, setEnded] = useState(false); const [showAutoplayCountdown, setShowAutoplayCountdown] = useState(false); - const [isEndededEmbed, setIsEndededEmbed] = useState(false); + const [isEndedEmbed, setIsEndedEmbed] = useState(false); const vjsCallbackDataRef: any = React.useRef(); const previousUri = usePrevious(uri); const embedded = useContext(EmbedContext); @@ -142,7 +142,7 @@ function VideoViewer(props: Props) { useEffect(() => { if (uri && previousUri && uri !== previousUri) { setShowAutoplayCountdown(false); - setIsEndededEmbed(false); + setIsEndedEmbed(false); setIsLoading(false); } }, [uri, previousUri]); @@ -236,7 +236,7 @@ function VideoViewer(props: Props) { } if (embedded) { - setIsEndededEmbed(true); + setIsEndedEmbed(true); } else if (!collectionId && autoplayNext) { setShowAutoplayCountdown(true); } else if (collectionId) { @@ -247,7 +247,7 @@ function VideoViewer(props: Props) { } }, [ embedded, - setIsEndededEmbed, + setIsEndedEmbed, autoplayMedia, setShowAutoplayCountdown, adUrl, @@ -264,7 +264,7 @@ function VideoViewer(props: Props) { setIsLoading(false); setIsPlaying(true); setShowAutoplayCountdown(false); - setIsEndededEmbed(false); + setIsEndedEmbed(false); setReplay(false); setDoNavigate(false); analytics.videoIsPlaying(true, player); @@ -391,7 +391,7 @@ function VideoViewer(props: Props) {
    @@ -403,8 +403,8 @@ function VideoViewer(props: Props) { doReplay={() => setReplay(true)} /> )} - {isEndededEmbed && } - {embedded && !isEndededEmbed && } + {isEndedEmbed && } + {embedded && !isEndedEmbed && } {/* disable this loading behavior because it breaks when player.play() promise hangs */} {isLoading && } @@ -458,6 +458,7 @@ function VideoViewer(props: Props) { videoTheaterMode={videoTheaterMode} playNext={doPlayNext} playPrevious={doPlayPrevious} + embedded={embedded} /> )}
    diff --git a/ui/recsys.js b/ui/recsys.js new file mode 100644 index 000000000..f5d8ae106 --- /dev/null +++ b/ui/recsys.js @@ -0,0 +1,254 @@ +import { selectUser } from 'redux/selectors/user'; +import { makeSelectRecommendedRecsysIdForClaimId } from 'redux/selectors/search'; +import { v4 as Uuidv4 } from 'uuid'; +import { parseURI, SETTINGS, makeSelectClaimForUri } from 'lbry-redux'; +import { selectPlayingUri, selectPrimaryUri } from 'redux/selectors/content'; +import { makeSelectClientSetting, selectDaemonSettings } from 'redux/selectors/settings'; +import { history } from './store'; + +const recsysEndpoint = 'https://clickstream.odysee.com/log/video/view'; +const recsysId = 'lighthouse-v0'; + +const getClaimIdsFromUris = (uris) => { + return uris + ? uris.map((uri) => { + try { + const { claimId } = parseURI(uri); + return claimId; + } catch (e) { + return []; + } + }) + : []; +}; + +const recsys = { + entries: {}, + debug: false, + /** + * Provides for creating, updating, and sending Clickstream data object Entries. + * Entries are Created either when recommendedContent loads, or when recommendedContent is clicked. + * If recommended content is clicked, An Entry with parentUuid is created. + * On page load, find an empty entry with your claimId, or create a new entry and record to it. + * The entry will be populated with the following: + * - parentUuid // optional + * - Uuid + * - claimId + * - recommendedClaims [] // optionally empty + * - playerEvents [] // optionally empty + * - recommendedClaimsIndexClicked [] // optionally empty + * - UserId + * - pageLoadedAt + * - isEmbed + * - pageExitedAt + * - recsysId // optional + */ + + /** + * Function: onClickedRecommended() + * Called when RecommendedContent was clicked. + * Adds index of clicked recommendation to parent entry + * Adds new Entry with parentUuid for destination page + * @param parentClaimId: string, + * @param newClaimId: string, + */ + onClickedRecommended: function (parentClaimId, newClaimId) { + const parentEntry = recsys.entries[parentClaimId] ? recsys.entries[parentClaimId] : null; + const parentUuid = parentEntry['uuid']; + const parentRecommendedClaims = parentEntry['recClaimIds'] || []; + const parentClickedIndexes = parentEntry['recClickedVideoIdx'] || []; + const indexClicked = parentRecommendedClaims.indexOf(newClaimId); + + if (parentUuid) { + recsys.createRecsysEntry(newClaimId, parentUuid); + } + parentClickedIndexes.push(indexClicked); + recsys.log('onClickedRecommended', { parentClaimId, newClaimId }); + }, + + /** + * Page was loaded. Get or Create entry and populate it with default data, plus recommended content, recsysId, etc. + * Called from recommendedContent component + */ + onRecsLoaded: function (claimId, uris) { + if (window.store) { + const state = window.store.getState(); + if (!recsys.entries[claimId]) { + recsys.createRecsysEntry(claimId); + } + const claimIds = getClaimIdsFromUris(uris); + recsys.entries[claimId]['recsysId'] = makeSelectRecommendedRecsysIdForClaimId(claimId)(state) || recsysId; + recsys.entries[claimId]['pageLoadedAt'] = Date.now(); + recsys.entries[claimId]['recClaimIds'] = claimIds; + } + recsys.log('onRecsLoaded', claimId); + }, + + /** + * Creates an Entry with optional parentUuid + * @param: claimId: string + * @param: parentUuid: string (optional) + */ + createRecsysEntry: function (claimId, parentUuid) { + if (window.store && claimId) { + const state = window.store.getState(); + const { id: userId } = selectUser(state); + if (parentUuid) { + // Make a stub entry that will be filled out on page load + recsys.entries[claimId] = { + uuid: Uuidv4(), + parentUuid: parentUuid, + uid: userId || null, // selectUser + claimId: claimId, + recClickedVideoIdx: [], + pageLoadedAt: Date.now(), + events: [], + }; + } else { + recsys.entries[claimId] = { + uuid: Uuidv4(), + uid: userId, // selectUser + claimId: claimId, + pageLoadedAt: Date.now(), + recsysId: makeSelectRecommendedRecsysIdForClaimId(claimId)(state) || recsysId, + recClaimIds: [], + recClickedVideoIdx: [], + events: [], + }; + } + } + recsys.log('createRecsysEntry', claimId); + }, + + /** + * Send event for claimId + * @param claimId + * @param isTentative + */ + sendRecsysEntry: function (claimId, isTentative) { + const shareTelemetry = + IS_WEB || (window && window.store && selectDaemonSettings(window.store.getState()).share_usage_data); + + if (recsys.entries[claimId] && shareTelemetry) { + const data = JSON.stringify(recsys.entries[claimId]); + try { + navigator.sendBeacon(recsysEndpoint, data); + if (!isTentative) { + delete recsys.entries[claimId]; + } + } catch (error) { + console.log('no beacon for you', error); + } + } + recsys.log('sendRecsysEntry', claimId); + }, + + /** + * A player event fired. Get the Entry for the claimId, and add the events + * @param claimId + * @param event + */ + onRecsysPlayerEvent: function (claimId, event, isEmbedded) { + if (!recsys.entries[claimId]) { + recsys.createRecsysEntry(claimId); + // do something to show it's floating or autoplay + } + if (isEmbedded) { + recsys.entries[claimId]['isEmbed'] = true; + } + recsys.entries[claimId].events.push(event); + recsys.log('onRecsysPlayerEvent', claimId); + }, + log: function (callName, claimId) { + if (recsys.debug) { + console.log(`Call: ***${callName}***, ClaimId: ${claimId}, Recsys Entries`, Object.assign({}, recsys.entries)); + } + }, + + /** + * Player closed. Check to see if primaryUri = playingUri + * if so, send the Entry. + */ + onPlayerDispose: function (claimId, isEmbedded) { + if (window.store) { + const state = window.store.getState(); + const playingUri = selectPlayingUri(state); + const primaryUri = selectPrimaryUri(state); + const onFilePage = playingUri === primaryUri; + if (!onFilePage || isEmbedded) { + if (isEmbedded) { + recsys.entries[claimId]['isEmbed'] = true; + } + recsys.sendRecsysEntry(claimId); + } + } + recsys.log('PlayerDispose', claimId); + }, + + // /** + // * File page unmount or change event + // * Check to see if playingUri, floatingEnabled, primaryUri === playingUri + // * If not, send the Entry. + // * If floating enabled, leaving file page will pop out player, leading to + // * more events until player is disposed. Don't send unless floatingPlayer playingUri + // */ + // onLeaveFilePage: function (primaryUri) { + // if (window.store) { + // const state = window.store.getState(); + // const claim = makeSelectClaimForUri(primaryUri)(state); + // const claimId = claim ? claim.claim_id : null; + // const playingUri = selectPlayingUri(state); + // const actualPlayingUri = playingUri && playingUri.uri; + // // const primaryUri = selectPrimaryUri(state); + // const floatingPlayer = makeSelectClientSetting(SETTINGS.FLOATING_PLAYER)(state); + // // When leaving page, if floating player is enabled, play will continue. + // if (claimId) { + // recsys.entries[claimId]['pageExitedAt'] = Date.now(); + // } + // const shouldSend = + // (claimId && floatingPlayer && actualPlayingUri && actualPlayingUri !== primaryUri) || !floatingPlayer || !actualPlayingUri; + // if (shouldSend) { + // recsys.sendRecsysEntry(claimId); + // } + // recsys.log('LeaveFile', claimId); + // } + // }, + + /** + * Navigate event + * Send all claimIds that aren't currently playing. + */ + onNavigate: function () { + if (window.store) { + const state = window.store.getState(); + const playingUri = selectPlayingUri(state); + const actualPlayingUri = playingUri && playingUri.uri; + const claim = makeSelectClaimForUri(actualPlayingUri)(state); + const playingClaimId = claim ? claim.claim_id : null; + // const primaryUri = selectPrimaryUri(state); + const floatingPlayer = makeSelectClientSetting(SETTINGS.FLOATING_PLAYER)(state); + // When leaving page, if floating player is enabled, play will continue. + Object.keys(recsys.entries).forEach((claimId) => { + const shouldSkip = recsys.entries[claimId].parentUuid && !recsys.entries[claimId].recClaimIds; + if (!shouldSkip && ((claimId !== playingClaimId && floatingPlayer) || !floatingPlayer)) { + recsys.entries[claimId]['pageExitedAt'] = Date.now(); + recsys.sendRecsysEntry(claimId); + } + recsys.log('OnNavigate', claimId); + }); + } + }, +}; +// @if TARGET='web' +document.addEventListener('visibilitychange', function logData() { + if (document.visibilityState === 'hidden') { + Object.keys(recsys.entries).map((claimId) => recsys.sendRecsysEntry(claimId, true)); + } +}); +// @endif + +history.listen(() => { + recsys.onNavigate(); +}); + +export default recsys; diff --git a/ui/redux/reducers/content.js b/ui/redux/reducers/content.js index b7580d92c..315df9b2a 100644 --- a/ui/redux/reducers/content.js +++ b/ui/redux/reducers/content.js @@ -116,43 +116,6 @@ reducers[ACTIONS.CLEAR_CONTENT_HISTORY_URI] = (state, action) => { reducers[ACTIONS.CLEAR_CONTENT_HISTORY_ALL] = (state) => ({ ...state, history: [] }); -reducers[ACTIONS.RECOMMENDATION_UPDATED] = (state, action) => { - const { claimId, urls, id, parentId } = action.data; - const recommendationId = Object.assign({}, state.recommendationId); - const recommendationParentId = Object.assign({}, state.recommendationParentId); - const recommendationUrls = Object.assign({}, state.recommendationUrls); - const recommendationClicks = Object.assign({}, state.recommendationClicks); - - if (urls && urls.length > 0) { - recommendationId[claimId] = id; - recommendationParentId[claimId] = parentId; - recommendationUrls[claimId] = urls; - recommendationClicks[claimId] = []; - } else { - delete recommendationId[claimId]; - delete recommendationParentId[claimId]; - delete recommendationUrls[claimId]; - delete recommendationClicks[claimId]; - } - - return { ...state, recommendationId, recommendationParentId, recommendationUrls, recommendationClicks }; -}; - -reducers[ACTIONS.RECOMMENDATION_CLICKED] = (state, action) => { - const { claimId, index } = action.data; - const recommendationClicks = Object.assign({}, state.recommendationClicks); - - if (state.recommendationUrls[claimId] && index >= 0 && index < state.recommendationUrls[claimId].length) { - if (recommendationClicks[claimId]) { - recommendationClicks[claimId].push(index); - } else { - recommendationClicks[claimId] = [index]; - } - } - - return { ...state, recommendationClicks }; -}; - // reducers[LBRY_REDUX_ACTIONS.PURCHASE_URI_FAILED] = (state, action) => { // return { // ...state, diff --git a/ui/redux/selectors/content.js b/ui/redux/selectors/content.js index 03f4025a4..208b9818b 100644 --- a/ui/redux/selectors/content.js +++ b/ui/redux/selectors/content.js @@ -3,7 +3,6 @@ import { createSelector } from 'reselect'; import { makeSelectClaimForUri, selectClaimsByUri, - makeSelectClaimsInChannelForCurrentPageState, makeSelectClaimIsNsfw, makeSelectClaimIsMine, makeSelectMediaTypeForUri, @@ -11,7 +10,6 @@ import { parseURI, makeSelectContentTypeForUri, makeSelectFileNameForUri, - selectClaimIdsByUri, } from 'lbry-redux'; import { makeSelectRecommendedContentForUri } from 'redux/selectors/search'; import { selectMutedChannels } from 'redux/selectors/blocked'; @@ -155,18 +153,6 @@ export const selectRecentHistory = createSelector(selectHistory, (history) => { return history.slice(0, RECENT_HISTORY_AMOUNT); }); -export const makeSelectCategoryListUris = (uris: ?Array, channel: string) => - createSelector(makeSelectClaimsInChannelForCurrentPageState(channel), (channelClaims) => { - if (uris) return uris; - - if (channelClaims) { - const CATEGORY_LIST_SIZE = 10; - return channelClaims.slice(0, CATEGORY_LIST_SIZE).map(({ name, claim_id: claimId }) => `${name}#${claimId}`); - } - - return null; - }); - export const makeSelectShouldObscurePreview = (uri: string) => createSelector(selectShowMatureContent, makeSelectClaimIsNsfw(uri), (showMatureContent, isClaimMature) => { return isClaimMature && !showMatureContent; @@ -247,21 +233,3 @@ export const makeSelectInsufficientCreditsForUri = (uri: string) => return !isMine && costInfo && costInfo.cost > 0 && costInfo.cost > balance; } ); - -export const makeSelectRecommendationId = (claimId: string) => - createSelector(selectState, (state) => state.recommendationId[claimId]); - -export const makeSelectRecommendationParentId = (claimId: string) => - createSelector(selectState, (state) => state.recommendationParentId[claimId]); - -export const makeSelectRecommendedClaimIds = (claimId: string) => - createSelector(selectState, selectClaimIdsByUri, (state, claimIdsByUri) => { - const recommendationUrls = state.recommendationUrls[claimId]; - if (recommendationUrls) { - return recommendationUrls.map((url) => claimIdsByUri[url]); - } - return undefined; - }); - -export const makeSelectRecommendationClicks = (claimId: string) => - createSelector(selectState, (state) => state.recommendationClicks[claimId]); diff --git a/ui/redux/selectors/search.js b/ui/redux/selectors/search.js index 1d97b1976..5005eadfc 100644 --- a/ui/redux/selectors/search.js +++ b/ui/redux/selectors/search.js @@ -98,7 +98,7 @@ export const makeSelectRecommendedRecsysIdForClaimId = (claimId: string) => createSelector(makeSelectClaimForClaimId(claimId), selectSearchResultByQuery, (claim, searchUrisByQuery) => { // TODO: DRY this out. let poweredBy; - if (claim) { + if (claim && claimId) { const isMature = isClaimNsfw(claim); const { title } = claim.value; @@ -204,3 +204,8 @@ export const makeSelectIsResolvingWinningUri = (query: string = '') => { } ); }; + +export const makeSelectUrlForClaimId = (claimId: string) => + createSelector( + makeSelectClaimForClaimId(claimId), (claim) => claim ? claim.canonical_url || claim.permanent_url : null + );