recsys v0.2 #6977
13 changed files with 340 additions and 238 deletions
|
@ -31,6 +31,7 @@ module.name_mapper='^modal\(.*\)$' -> '<PROJECT_ROOT>/ui/modal\1'
|
||||||
module.name_mapper='^app\(.*\)$' -> '<PROJECT_ROOT>/ui/app\1'
|
module.name_mapper='^app\(.*\)$' -> '<PROJECT_ROOT>/ui/app\1'
|
||||||
module.name_mapper='^native\(.*\)$' -> '<PROJECT_ROOT>/ui/native\1'
|
module.name_mapper='^native\(.*\)$' -> '<PROJECT_ROOT>/ui/native\1'
|
||||||
module.name_mapper='^analytics\(.*\)$' -> '<PROJECT_ROOT>/ui/analytics\1'
|
module.name_mapper='^analytics\(.*\)$' -> '<PROJECT_ROOT>/ui/analytics\1'
|
||||||
|
module.name_mapper='^recsys\(.*\)$' -> '<PROJECT_ROOT>/ui/recsys\1'
|
||||||
module.name_mapper='^rewards\(.*\)$' -> '<PROJECT_ROOT>/ui/rewards\1'
|
module.name_mapper='^rewards\(.*\)$' -> '<PROJECT_ROOT>/ui/rewards\1'
|
||||||
module.name_mapper='^i18n\(.*\)$' -> '<PROJECT_ROOT>/ui/i18n\1'
|
module.name_mapper='^i18n\(.*\)$' -> '<PROJECT_ROOT>/ui/i18n\1'
|
||||||
module.name_mapper='^effects\(.*\)$' -> '<PROJECT_ROOT>/ui/effects\1'
|
module.name_mapper='^effects\(.*\)$' -> '<PROJECT_ROOT>/ui/effects\1'
|
||||||
|
|
|
@ -27,7 +27,6 @@ type Props = {
|
||||||
onScrollBottom?: (any) => void,
|
onScrollBottom?: (any) => void,
|
||||||
page?: number,
|
page?: number,
|
||||||
pageSize?: number,
|
pageSize?: number,
|
||||||
id?: string,
|
|
||||||
// If using the default header, this is a unique ID needed to persist the state of the filter setting
|
// If using the default header, this is a unique ID needed to persist the state of the filter setting
|
||||||
persistedStorageKey?: string,
|
persistedStorageKey?: string,
|
||||||
showHiddenByUser: boolean,
|
showHiddenByUser: boolean,
|
||||||
|
@ -47,7 +46,7 @@ type Props = {
|
||||||
searchOptions?: any,
|
searchOptions?: any,
|
||||||
collectionId?: string,
|
collectionId?: string,
|
||||||
showNoSourceClaims?: boolean,
|
showNoSourceClaims?: boolean,
|
||||||
onClick?: (e: any, index?: number) => void,
|
onClick?: (e: any, claim?: ?Claim, index?: number) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ClaimList(props: Props) {
|
export default function ClaimList(props: Props) {
|
||||||
|
@ -56,7 +55,6 @@ export default function ClaimList(props: Props) {
|
||||||
uris,
|
uris,
|
||||||
headerAltControls,
|
headerAltControls,
|
||||||
loading,
|
loading,
|
||||||
id,
|
|
||||||
persistedStorageKey,
|
persistedStorageKey,
|
||||||
empty,
|
empty,
|
||||||
defaultSort,
|
defaultSort,
|
||||||
|
@ -110,9 +108,9 @@ export default function ClaimList(props: Props) {
|
||||||
setCurrentSort(currentSort === SORT_NEW ? SORT_OLD : SORT_NEW);
|
setCurrentSort(currentSort === SORT_NEW ? SORT_OLD : SORT_NEW);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClaimClicked(e, index) {
|
function handleClaimClicked(e, claim, index) {
|
||||||
if (onClick) {
|
if (onClick) {
|
||||||
onClick(e, index);
|
onClick(e, claim, index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -200,7 +198,6 @@ export default function ClaimList(props: Props) {
|
||||||
{injectedItem && index === 4 && <li>{injectedItem}</li>}
|
{injectedItem && index === 4 && <li>{injectedItem}</li>}
|
||||||
<ClaimPreview
|
<ClaimPreview
|
||||||
uri={uri}
|
uri={uri}
|
||||||
containerId={id}
|
|
||||||
indexInContainer={index}
|
indexInContainer={index}
|
||||||
type={type}
|
type={type}
|
||||||
active={activeUri && uri === activeUri}
|
active={activeUri && uri === activeUri}
|
||||||
|
@ -220,7 +217,7 @@ export default function ClaimList(props: Props) {
|
||||||
return claim.name.length === 24 && !claim.name.includes(' ') && claim.value.author === 'Spee.ch';
|
return claim.name.length === 24 && !claim.name.includes(' ') && claim.value.author === 'Spee.ch';
|
||||||
}}
|
}}
|
||||||
live={resolveLive(index)}
|
live={resolveLive(index)}
|
||||||
onClick={handleClaimClicked}
|
onClick={(e, claim, index) => handleClaimClicked(e, claim, index)}
|
||||||
/>
|
/>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -583,7 +583,6 @@ function ClaimListDiscover(props: Props) {
|
||||||
)}
|
)}
|
||||||
<ClaimList
|
<ClaimList
|
||||||
tileLayout
|
tileLayout
|
||||||
id={mainSearchKey}
|
|
||||||
loading={loading}
|
loading={loading}
|
||||||
uris={finalUris}
|
uris={finalUris}
|
||||||
onScrollBottom={handleScrollBottom}
|
onScrollBottom={handleScrollBottom}
|
||||||
|
@ -617,7 +616,6 @@ function ClaimListDiscover(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<ClaimList
|
<ClaimList
|
||||||
id={mainSearchKey}
|
|
||||||
type={type}
|
type={type}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
uris={finalUris}
|
uris={finalUris}
|
||||||
|
|
|
@ -29,7 +29,6 @@ import ClaimPreviewNoContent from './claim-preview-no-content';
|
||||||
import { ENABLE_NO_SOURCE_CLAIMS } from 'config';
|
import { ENABLE_NO_SOURCE_CLAIMS } from 'config';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import { CONTAINER_ID } from 'constants/navigation';
|
|
||||||
|
|
||||||
const AbandonedChannelPreview = lazyImport(() =>
|
const AbandonedChannelPreview = lazyImport(() =>
|
||||||
import('component/abandonedChannelPreview' /* webpackChunkName: "abandonedChannelPreview" */)
|
import('component/abandonedChannelPreview' /* webpackChunkName: "abandonedChannelPreview" */)
|
||||||
|
@ -67,7 +66,7 @@ type Props = {
|
||||||
actions: boolean | Node | string | number,
|
actions: boolean | Node | string | number,
|
||||||
properties: boolean | Node | string | number | ((Claim) => Node),
|
properties: boolean | Node | string | number | ((Claim) => Node),
|
||||||
empty?: Node,
|
empty?: Node,
|
||||||
onClick?: (e: any, index?: number) => any,
|
onClick?: (e: any, claim?: ?Claim, index?: number) => any,
|
||||||
streamingUrl: ?string,
|
streamingUrl: ?string,
|
||||||
getFile: (string) => void,
|
getFile: (string) => void,
|
||||||
customShouldHide?: (Claim) => boolean,
|
customShouldHide?: (Claim) => boolean,
|
||||||
|
@ -90,7 +89,6 @@ type Props = {
|
||||||
disableNavigation?: boolean,
|
disableNavigation?: boolean,
|
||||||
mediaDuration?: string,
|
mediaDuration?: string,
|
||||||
date?: any,
|
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'.
|
indexInContainer?: number, // The index order of this component within 'containerId'.
|
||||||
channelSubCount?: number,
|
channelSubCount?: number,
|
||||||
};
|
};
|
||||||
|
@ -154,7 +152,6 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
isCollectionMine,
|
isCollectionMine,
|
||||||
collectionUris,
|
collectionUris,
|
||||||
disableNavigation,
|
disableNavigation,
|
||||||
containerId,
|
|
||||||
indexInContainer,
|
indexInContainer,
|
||||||
channelSubCount,
|
channelSubCount,
|
||||||
} = props;
|
} = props;
|
||||||
|
@ -223,7 +220,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
|
|
||||||
const handleNavLinkClick = (e) => {
|
const handleNavLinkClick = (e) => {
|
||||||
if (onClick) {
|
if (onClick) {
|
||||||
onClick(e, indexInContainer);
|
onClick(e, claim, indexInContainer); // not sure indexInContainer is used for anything.
|
||||||
}
|
}
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
};
|
};
|
||||||
|
@ -232,10 +229,9 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
to: {
|
to: {
|
||||||
pathname: navigateUrl,
|
pathname: navigateUrl,
|
||||||
search: navigateSearch.toString() ? '?' + navigateSearch.toString() : '',
|
search: navigateSearch.toString() ? '?' + navigateSearch.toString() : '',
|
||||||
state: containerId ? { [CONTAINER_ID]: containerId } : undefined,
|
|
||||||
},
|
},
|
||||||
onClick: (e) => handleNavLinkClick(e),
|
onClick: handleNavLinkClick,
|
||||||
onAuxClick: (e) => handleNavLinkClick(e),
|
onAuxClick: handleNavLinkClick,
|
||||||
};
|
};
|
||||||
|
|
||||||
// do not block abandoned and nsfw claims if showUserBlocked is passed
|
// do not block abandoned and nsfw claims if showUserBlocked is passed
|
||||||
|
@ -281,14 +277,13 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
|
|
||||||
function handleOnClick(e) {
|
function handleOnClick(e) {
|
||||||
if (onClick) {
|
if (onClick) {
|
||||||
onClick(e, indexInContainer);
|
onClick(e, claim, indexInContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (claim && !pending && !disableNavigation) {
|
if (claim && !pending && !disableNavigation) {
|
||||||
history.push({
|
history.push({
|
||||||
pathname: navigateUrl,
|
pathname: navigateUrl,
|
||||||
search: navigateSearch.toString() ? '?' + navigateSearch.toString() : '',
|
search: navigateSearch.toString() ? '?' + navigateSearch.toString() : '',
|
||||||
state: containerId ? { [CONTAINER_ID]: containerId } : undefined,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,14 +7,19 @@ import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||||
import { makeSelectNextUnplayedRecommended } from 'redux/selectors/content';
|
import { makeSelectNextUnplayedRecommended } from 'redux/selectors/content';
|
||||||
import RecommendedContent from './view';
|
import RecommendedContent from './view';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => {
|
||||||
|
const claim = makeSelectClaimForUri(props.uri)(state);
|
||||||
|
const { claim_id: claimId } = claim;
|
||||||
|
return {
|
||||||
mature: makeSelectClaimIsNsfw(props.uri)(state),
|
mature: makeSelectClaimIsNsfw(props.uri)(state),
|
||||||
recommendedContent: makeSelectRecommendedContentForUri(props.uri)(state),
|
recommendedContentUris: makeSelectRecommendedContentForUri(props.uri)(state),
|
||||||
nextRecommendedUri: makeSelectNextUnplayedRecommended(props.uri)(state),
|
nextRecommendedUri: makeSelectNextUnplayedRecommended(props.uri)(state),
|
||||||
isSearching: selectIsSearching(state),
|
isSearching: selectIsSearching(state),
|
||||||
isAuthenticated: selectUserVerifiedEmail(state),
|
isAuthenticated: selectUserVerifiedEmail(state),
|
||||||
claim: makeSelectClaimForUri(props.uri)(state),
|
claim,
|
||||||
});
|
claimId,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const perform = (dispatch) => ({
|
const perform = (dispatch) => ({
|
||||||
doFetchRecommendedContent: (uri, mature) => dispatch(doFetchRecommendedContent(uri, mature)),
|
doFetchRecommendedContent: (uri, mature) => dispatch(doFetchRecommendedContent(uri, mature)),
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { SHOW_ADS } from 'config';
|
import { SHOW_ADS } from 'config';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useHistory } from 'react-router-dom';
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
import ClaimList from 'component/claimList';
|
import ClaimList from 'component/claimList';
|
||||||
import ClaimListDiscover from 'component/claimListDiscover';
|
import ClaimListDiscover from 'component/claimListDiscover';
|
||||||
import Ads from 'web/component/ads';
|
import Ads from 'web/component/ads';
|
||||||
|
@ -10,14 +8,14 @@ import Card from 'component/common/card';
|
||||||
import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize';
|
import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { CONTAINER_ID } from 'constants/navigation';
|
import RecSys from 'recsys';
|
||||||
|
|
||||||
const VIEW_ALL_RELATED = 'view_all_related';
|
const VIEW_ALL_RELATED = 'view_all_related';
|
||||||
const VIEW_MORE_FROM = 'view_more_from';
|
const VIEW_MORE_FROM = 'view_more_from';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: string,
|
uri: string,
|
||||||
recommendedContent: Array<string>,
|
recommendedContentUris: Array<string>,
|
||||||
nextRecommendedUri: string,
|
nextRecommendedUri: string,
|
||||||
isSearching: boolean,
|
isSearching: boolean,
|
||||||
doFetchRecommendedContent: (string, boolean) => void,
|
doFetchRecommendedContent: (string, boolean) => void,
|
||||||
|
@ -25,7 +23,7 @@ type Props = {
|
||||||
isAuthenticated: boolean,
|
isAuthenticated: boolean,
|
||||||
claim: ?StreamClaim,
|
claim: ?StreamClaim,
|
||||||
doRecommendationUpdate: (claimId: string, urls: Array<string>, id: string, parentId: string) => void,
|
doRecommendationUpdate: (claimId: string, urls: Array<string>, id: string, parentId: string) => void,
|
||||||
doRecommendationClicked: (claimId: string, index: number) => void,
|
claimId: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default React.memo<Props>(function RecommendedContent(props: Props) {
|
export default React.memo<Props>(function RecommendedContent(props: Props) {
|
||||||
|
@ -33,23 +31,20 @@ export default React.memo<Props>(function RecommendedContent(props: Props) {
|
||||||
uri,
|
uri,
|
||||||
doFetchRecommendedContent,
|
doFetchRecommendedContent,
|
||||||
mature,
|
mature,
|
||||||
recommendedContent,
|
recommendedContentUris,
|
||||||
nextRecommendedUri,
|
nextRecommendedUri,
|
||||||
isSearching,
|
isSearching,
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
claim,
|
claim,
|
||||||
doRecommendationUpdate,
|
claimId,
|
||||||
doRecommendationClicked,
|
|
||||||
} = props;
|
} = props;
|
||||||
const [viewMode, setViewMode] = React.useState(VIEW_ALL_RELATED);
|
const [viewMode, setViewMode] = React.useState(VIEW_ALL_RELATED);
|
||||||
const [recommendationId, setRecommendationId] = React.useState('');
|
|
||||||
const [recommendationUrls, setRecommendationUrls] = React.useState();
|
const [recommendationUrls, setRecommendationUrls] = React.useState();
|
||||||
const history = useHistory();
|
|
||||||
const signingChannel = claim && claim.signing_channel;
|
const signingChannel = claim && claim.signing_channel;
|
||||||
const channelName = signingChannel ? signingChannel.name : null;
|
const channelName = signingChannel ? signingChannel.name : null;
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const isMedium = useIsMediumScreen();
|
const isMedium = useIsMediumScreen();
|
||||||
|
const { onRecsLoaded: onRecommendationsLoaded, onClickedRecommended: onRecommendationClicked } = RecSys;
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
function moveAutoplayNextItemToTop(recommendedContent) {
|
function moveAutoplayNextItemToTop(recommendedContent) {
|
||||||
let newList = recommendedContent;
|
let newList = recommendedContent;
|
||||||
|
@ -72,33 +67,27 @@ export default React.memo<Props>(function RecommendedContent(props: Props) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const newRecommendationUrls = moveAutoplayNextItemToTop(recommendedContent);
|
const newRecommendationUrls = moveAutoplayNextItemToTop(recommendedContentUris);
|
||||||
|
|
||||||
if (claim && !listEq(recommendationUrls, newRecommendationUrls)) {
|
if (claim && !listEq(recommendationUrls, newRecommendationUrls)) {
|
||||||
const parentId = (history.location.state && history.location.state[CONTAINER_ID]) || '';
|
|
||||||
const id = uuidv4();
|
|
||||||
setRecommendationId(id);
|
|
||||||
setRecommendationUrls(newRecommendationUrls);
|
setRecommendationUrls(newRecommendationUrls);
|
||||||
|
|
||||||
doRecommendationUpdate(claim.claim_id, newRecommendationUrls, id, parentId);
|
|
||||||
}
|
}
|
||||||
}, [
|
}, [recommendedContentUris, nextRecommendedUri, recommendationUrls, setRecommendationUrls, claim]);
|
||||||
recommendedContent,
|
|
||||||
nextRecommendedUri,
|
|
||||||
recommendationUrls,
|
|
||||||
setRecommendationUrls,
|
|
||||||
claim,
|
|
||||||
doRecommendationUpdate,
|
|
||||||
history.location.state,
|
|
||||||
]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
doFetchRecommendedContent(uri, mature);
|
doFetchRecommendedContent(uri, mature);
|
||||||
}, [uri, mature, doFetchRecommendedContent]);
|
}, [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) {
|
if (claim) {
|
||||||
doRecommendationClicked(claim.claim_id, index);
|
onRecommendationClicked(claim.claim_id, clickedClaim.claim_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,7 +122,6 @@ export default React.memo<Props>(function RecommendedContent(props: Props) {
|
||||||
<div>
|
<div>
|
||||||
{viewMode === VIEW_ALL_RELATED && (
|
{viewMode === VIEW_ALL_RELATED && (
|
||||||
<ClaimList
|
<ClaimList
|
||||||
id={recommendationId}
|
|
||||||
type="small"
|
type="small"
|
||||||
loading={isSearching}
|
loading={isSearching}
|
||||||
uris={recommendationUrls}
|
uris={recommendationUrls}
|
||||||
|
@ -177,8 +165,8 @@ function areEqual(prevProps: Props, nextProps: Props) {
|
||||||
a.isAuthenticated !== b.isAuthenticated ||
|
a.isAuthenticated !== b.isAuthenticated ||
|
||||||
a.isSearching !== b.isSearching ||
|
a.isSearching !== b.isSearching ||
|
||||||
a.mature !== b.mature ||
|
a.mature !== b.mature ||
|
||||||
(a.recommendedContent && !b.recommendedContent) ||
|
(a.recommendedContentUris && !b.recommendedContentUris) ||
|
||||||
(!a.recommendedContent && b.recommendedContent) ||
|
(!a.recommendedContentUris && b.recommendedContentUris) ||
|
||||||
(a.claim && !b.claim) ||
|
(a.claim && !b.claim) ||
|
||||||
(!a.claim && b.claim)
|
(!a.claim && b.claim)
|
||||||
) {
|
) {
|
||||||
|
@ -189,14 +177,14 @@ function areEqual(prevProps: Props, nextProps: Props) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (a.recommendedContent && b.recommendedContent) {
|
if (a.recommendedContentUris && b.recommendedContentUris) {
|
||||||
if (a.recommendedContent.length !== b.recommendedContent.length) {
|
if (a.recommendedContentUris.length !== b.recommendedContentUris.length) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let i = a.recommendedContent.length;
|
let i = a.recommendedContentUris.length;
|
||||||
while (i--) {
|
while (i--) {
|
||||||
if (a.recommendedContent[i] !== b.recommendedContent[i]) {
|
if (a.recommendedContentUris[i] !== b.recommendedContentUris[i]) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,54 +1,20 @@
|
||||||
// Created by xander on 6/21/2021
|
// Created by xander on 6/21/2021
|
||||||
import videojs from 'video.js';
|
import videojs from 'video.js';
|
||||||
import {
|
|
||||||
makeSelectRecommendationId,
|
|
||||||
makeSelectRecommendationParentId,
|
|
||||||
makeSelectRecommendedClaimIds,
|
|
||||||
makeSelectRecommendationClicks,
|
|
||||||
} from 'redux/selectors/content';
|
|
||||||
import { makeSelectRecommendedRecsysIdForClaimId } from 'redux/selectors/search';
|
|
||||||
|
|
||||||
|
import RecSys from 'recsys';
|
||||||
const VERSION = '0.0.1';
|
const VERSION = '0.0.1';
|
||||||
|
|
||||||
const recsysEndpoint = 'https://clickstream.odysee.com/log/video/view';
|
|
||||||
const recsysId = 'lighthouse-v0';
|
|
||||||
|
|
||||||
/* RecSys */
|
/* RecSys */
|
||||||
const RecsysData = {
|
const PlayerEvent = {
|
||||||
event: {
|
event: {
|
||||||
start: 0,
|
start: 0, // event types
|
||||||
stop: 1,
|
stop: 1,
|
||||||
scrub: 2,
|
scrub: 2,
|
||||||
speed: 3,
|
speed: 3,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function createRecsys(claimId, userId, events, loadedAt, isEmbed) {
|
function newRecsysPlayerEvent(eventType, offset, arg) {
|
||||||
const pageLoadedAt = loadedAt;
|
|
||||||
const pageExitedAt = Date.now();
|
|
||||||
|
|
||||||
if (window.store) {
|
|
||||||
const state = window.store.getState();
|
|
||||||
|
|
||||||
return {
|
|
||||||
uuid: makeSelectRecommendationId(claimId)(state),
|
|
||||||
parentUuid: makeSelectRecommendationParentId(claimId)(state),
|
|
||||||
uid: userId,
|
|
||||||
claimId: claimId,
|
|
||||||
pageLoadedAt: pageLoadedAt,
|
|
||||||
pageExitedAt: pageExitedAt,
|
|
||||||
recsysId: makeSelectRecommendedRecsysIdForClaimId(claimId)(state) || recsysId,
|
|
||||||
recClaimIds: makeSelectRecommendedClaimIds(claimId)(state),
|
|
||||||
recClickedVideoIdx: makeSelectRecommendationClicks(claimId)(state),
|
|
||||||
events: events,
|
|
||||||
isEmbed: isEmbed,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function newRecsysEvent(eventType, offset, arg) {
|
|
||||||
if (arg) {
|
if (arg) {
|
||||||
return {
|
return {
|
||||||
event: eventType,
|
event: eventType,
|
||||||
|
@ -63,30 +29,11 @@ function newRecsysEvent(eventType, offset, arg) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendRecsysEvents(recsys) {
|
|
||||||
const requestOptions = {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'text/plain' }, // application/json
|
|
||||||
body: JSON.stringify(recsys),
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
fetch(recsysEndpoint, requestOptions)
|
|
||||||
.then((response) => response.json())
|
|
||||||
.then((data) => {
|
|
||||||
// console.log(`Recsys response data:`, data);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
// console.error(`Recsys Error`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaults = {
|
const defaults = {
|
||||||
endpoint: recsysEndpoint,
|
|
||||||
recsysId: recsysId,
|
|
||||||
videoId: null,
|
videoId: null,
|
||||||
userId: 0,
|
userId: 0,
|
||||||
debug: false,
|
debug: false,
|
||||||
|
embedded: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Component = videojs.getComponent('Component');
|
const Component = videojs.getComponent('Component');
|
||||||
|
@ -98,16 +45,13 @@ class RecsysPlugin extends Component {
|
||||||
|
|
||||||
// Plugin started
|
// Plugin started
|
||||||
if (options.debug) {
|
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
|
// To help with debugging, we'll add a global vjs object with the video js player
|
||||||
window.vjs = player;
|
window.vjs = player;
|
||||||
|
|
||||||
this.player = player;
|
this.player = player;
|
||||||
|
|
||||||
this.recsysEvents = [];
|
|
||||||
this.loadedAt = Date.now();
|
|
||||||
this.lastTimeUpdate = null;
|
this.lastTimeUpdate = null;
|
||||||
this.currentTimeUpdate = null;
|
this.currentTimeUpdate = null;
|
||||||
this.inPause = false;
|
this.inPause = false;
|
||||||
|
@ -124,57 +68,37 @@ class RecsysPlugin extends Component {
|
||||||
player.on('dispose', (event) => this.onDispose(event));
|
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) {
|
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.log('onPlay', recsysEvent);
|
||||||
this.addRecsysEvent(recsysEvent);
|
RecSys.onRecsysPlayerEvent(this.options_.videoId, recsysEvent, this.options_.embedded);
|
||||||
|
|
||||||
this.inPause = false;
|
this.inPause = false;
|
||||||
this.lastTimeUpdate = recsysEvent.offset;
|
this.lastTimeUpdate = recsysEvent.offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
onPause(event) {
|
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.log('onPause', recsysEvent);
|
||||||
this.addRecsysEvent(recsysEvent);
|
RecSys.onRecsysPlayerEvent(this.options_.videoId, recsysEvent);
|
||||||
|
|
||||||
this.inPause = true;
|
this.inPause = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
onEnded(event) {
|
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.log('onEnded', recsysEvent);
|
||||||
this.addRecsysEvent(recsysEvent);
|
RecSys.onRecsysPlayerEvent(this.options_.videoId, recsysEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
onRateChange(event) {
|
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.log('onRateChange', recsysEvent);
|
||||||
this.addRecsysEvent(recsysEvent);
|
RecSys.onRecsysPlayerEvent(this.options_.videoId, recsysEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
onTimeUpdate(event) {
|
onTimeUpdate(event) {
|
||||||
|
@ -212,19 +136,19 @@ class RecsysPlugin extends Component {
|
||||||
|
|
||||||
if (fromTime !== curTime) {
|
if (fromTime !== curTime) {
|
||||||
// This removes duplicates that aren't useful.
|
// 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.log('onSeeked', recsysEvent);
|
||||||
this.addRecsysEvent(recsysEvent);
|
RecSys.onRecsysPlayerEvent(this.options_.videoId, recsysEvent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onDispose(event) {
|
onDispose(event) {
|
||||||
this.sendRecsysEvents();
|
RecSys.onPlayerDispose(this.options_.videoId, this.options_.embedded);
|
||||||
}
|
}
|
||||||
|
|
||||||
log(...args) {
|
log(...args) {
|
||||||
if (this.options_.debug) {
|
if (this.options_.debug) {
|
||||||
// console.log(`Recsys Debug:`, JSON.stringify(args));
|
console.log(`Recsys Player Debug:`, JSON.stringify(args));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,6 +54,7 @@ type Props = {
|
||||||
startMuted: boolean,
|
startMuted: boolean,
|
||||||
autoplay: boolean,
|
autoplay: boolean,
|
||||||
autoplaySetting: boolean,
|
autoplaySetting: boolean,
|
||||||
|
embedded: boolean,
|
||||||
toggleVideoTheaterMode: () => void,
|
toggleVideoTheaterMode: () => void,
|
||||||
adUrl: ?string,
|
adUrl: ?string,
|
||||||
claimId: ?string,
|
claimId: ?string,
|
||||||
|
@ -194,6 +195,7 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
||||||
const {
|
const {
|
||||||
autoplay,
|
autoplay,
|
||||||
autoplaySetting,
|
autoplaySetting,
|
||||||
|
embedded,
|
||||||
startMuted,
|
startMuted,
|
||||||
source,
|
source,
|
||||||
sourceType,
|
sourceType,
|
||||||
|
@ -590,6 +592,7 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
||||||
player.recsys({
|
player.recsys({
|
||||||
videoId: claimId,
|
videoId: claimId,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
|
embedded: embedded,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -125,7 +125,7 @@ function VideoViewer(props: Props) {
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
const [ended, setEnded] = useState(false);
|
const [ended, setEnded] = useState(false);
|
||||||
const [showAutoplayCountdown, setShowAutoplayCountdown] = useState(false);
|
const [showAutoplayCountdown, setShowAutoplayCountdown] = useState(false);
|
||||||
const [isEndededEmbed, setIsEndededEmbed] = useState(false);
|
const [isEndedEmbed, setIsEndedEmbed] = useState(false);
|
||||||
const vjsCallbackDataRef: any = React.useRef();
|
const vjsCallbackDataRef: any = React.useRef();
|
||||||
const previousUri = usePrevious(uri);
|
const previousUri = usePrevious(uri);
|
||||||
const embedded = useContext(EmbedContext);
|
const embedded = useContext(EmbedContext);
|
||||||
|
@ -142,7 +142,7 @@ function VideoViewer(props: Props) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (uri && previousUri && uri !== previousUri) {
|
if (uri && previousUri && uri !== previousUri) {
|
||||||
setShowAutoplayCountdown(false);
|
setShowAutoplayCountdown(false);
|
||||||
setIsEndededEmbed(false);
|
setIsEndedEmbed(false);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [uri, previousUri]);
|
}, [uri, previousUri]);
|
||||||
|
@ -236,7 +236,7 @@ function VideoViewer(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (embedded) {
|
if (embedded) {
|
||||||
setIsEndededEmbed(true);
|
setIsEndedEmbed(true);
|
||||||
} else if (!collectionId && autoplayNext) {
|
} else if (!collectionId && autoplayNext) {
|
||||||
setShowAutoplayCountdown(true);
|
setShowAutoplayCountdown(true);
|
||||||
} else if (collectionId) {
|
} else if (collectionId) {
|
||||||
|
@ -247,7 +247,7 @@ function VideoViewer(props: Props) {
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
embedded,
|
embedded,
|
||||||
setIsEndededEmbed,
|
setIsEndedEmbed,
|
||||||
autoplayMedia,
|
autoplayMedia,
|
||||||
setShowAutoplayCountdown,
|
setShowAutoplayCountdown,
|
||||||
adUrl,
|
adUrl,
|
||||||
|
@ -264,7 +264,7 @@ function VideoViewer(props: Props) {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setIsPlaying(true);
|
setIsPlaying(true);
|
||||||
setShowAutoplayCountdown(false);
|
setShowAutoplayCountdown(false);
|
||||||
setIsEndededEmbed(false);
|
setIsEndedEmbed(false);
|
||||||
setReplay(false);
|
setReplay(false);
|
||||||
setDoNavigate(false);
|
setDoNavigate(false);
|
||||||
analytics.videoIsPlaying(true, player);
|
analytics.videoIsPlaying(true, player);
|
||||||
|
@ -391,7 +391,7 @@ function VideoViewer(props: Props) {
|
||||||
<div
|
<div
|
||||||
className={classnames('file-viewer', {
|
className={classnames('file-viewer', {
|
||||||
'file-viewer--is-playing': isPlaying,
|
'file-viewer--is-playing': isPlaying,
|
||||||
'file-viewer--ended-embed': isEndededEmbed,
|
'file-viewer--ended-embed': isEndedEmbed,
|
||||||
})}
|
})}
|
||||||
onContextMenu={stopContextMenu}
|
onContextMenu={stopContextMenu}
|
||||||
>
|
>
|
||||||
|
@ -403,8 +403,8 @@ function VideoViewer(props: Props) {
|
||||||
doReplay={() => setReplay(true)}
|
doReplay={() => setReplay(true)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isEndededEmbed && <FileViewerEmbeddedEnded uri={uri} />}
|
{isEndedEmbed && <FileViewerEmbeddedEnded uri={uri} />}
|
||||||
{embedded && !isEndededEmbed && <FileViewerEmbeddedTitle uri={uri} />}
|
{embedded && !isEndedEmbed && <FileViewerEmbeddedTitle uri={uri} />}
|
||||||
{/* disable this loading behavior because it breaks when player.play() promise hangs */}
|
{/* disable this loading behavior because it breaks when player.play() promise hangs */}
|
||||||
{isLoading && <LoadingScreen status={__('Loading')} />}
|
{isLoading && <LoadingScreen status={__('Loading')} />}
|
||||||
|
|
||||||
|
@ -458,6 +458,7 @@ function VideoViewer(props: Props) {
|
||||||
videoTheaterMode={videoTheaterMode}
|
videoTheaterMode={videoTheaterMode}
|
||||||
playNext={doPlayNext}
|
playNext={doPlayNext}
|
||||||
playPrevious={doPlayPrevious}
|
playPrevious={doPlayPrevious}
|
||||||
|
embedded={embedded}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
254
ui/recsys.js
Normal file
254
ui/recsys.js
Normal file
|
@ -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;
|
|
@ -116,43 +116,6 @@ reducers[ACTIONS.CLEAR_CONTENT_HISTORY_URI] = (state, action) => {
|
||||||
|
|
||||||
reducers[ACTIONS.CLEAR_CONTENT_HISTORY_ALL] = (state) => ({ ...state, history: [] });
|
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) => {
|
// reducers[LBRY_REDUX_ACTIONS.PURCHASE_URI_FAILED] = (state, action) => {
|
||||||
// return {
|
// return {
|
||||||
// ...state,
|
// ...state,
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { createSelector } from 'reselect';
|
||||||
import {
|
import {
|
||||||
makeSelectClaimForUri,
|
makeSelectClaimForUri,
|
||||||
selectClaimsByUri,
|
selectClaimsByUri,
|
||||||
makeSelectClaimsInChannelForCurrentPageState,
|
|
||||||
makeSelectClaimIsNsfw,
|
makeSelectClaimIsNsfw,
|
||||||
makeSelectClaimIsMine,
|
makeSelectClaimIsMine,
|
||||||
makeSelectMediaTypeForUri,
|
makeSelectMediaTypeForUri,
|
||||||
|
@ -11,7 +10,6 @@ import {
|
||||||
parseURI,
|
parseURI,
|
||||||
makeSelectContentTypeForUri,
|
makeSelectContentTypeForUri,
|
||||||
makeSelectFileNameForUri,
|
makeSelectFileNameForUri,
|
||||||
selectClaimIdsByUri,
|
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { makeSelectRecommendedContentForUri } from 'redux/selectors/search';
|
import { makeSelectRecommendedContentForUri } from 'redux/selectors/search';
|
||||||
import { selectMutedChannels } from 'redux/selectors/blocked';
|
import { selectMutedChannels } from 'redux/selectors/blocked';
|
||||||
|
@ -155,18 +153,6 @@ export const selectRecentHistory = createSelector(selectHistory, (history) => {
|
||||||
return history.slice(0, RECENT_HISTORY_AMOUNT);
|
return history.slice(0, RECENT_HISTORY_AMOUNT);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const makeSelectCategoryListUris = (uris: ?Array<string>, 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) =>
|
export const makeSelectShouldObscurePreview = (uri: string) =>
|
||||||
createSelector(selectShowMatureContent, makeSelectClaimIsNsfw(uri), (showMatureContent, isClaimMature) => {
|
createSelector(selectShowMatureContent, makeSelectClaimIsNsfw(uri), (showMatureContent, isClaimMature) => {
|
||||||
return isClaimMature && !showMatureContent;
|
return isClaimMature && !showMatureContent;
|
||||||
|
@ -247,21 +233,3 @@ export const makeSelectInsufficientCreditsForUri = (uri: string) =>
|
||||||
return !isMine && costInfo && costInfo.cost > 0 && costInfo.cost > balance;
|
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]);
|
|
||||||
|
|
|
@ -98,7 +98,7 @@ export const makeSelectRecommendedRecsysIdForClaimId = (claimId: string) =>
|
||||||
createSelector(makeSelectClaimForClaimId(claimId), selectSearchResultByQuery, (claim, searchUrisByQuery) => {
|
createSelector(makeSelectClaimForClaimId(claimId), selectSearchResultByQuery, (claim, searchUrisByQuery) => {
|
||||||
// TODO: DRY this out.
|
// TODO: DRY this out.
|
||||||
let poweredBy;
|
let poweredBy;
|
||||||
if (claim) {
|
if (claim && claimId) {
|
||||||
const isMature = isClaimNsfw(claim);
|
const isMature = isClaimNsfw(claim);
|
||||||
const { title } = claim.value;
|
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
|
||||||
|
);
|
||||||
|
|
Loading…
Reference in a new issue