(function RecommendedContent(props: Props) {
{viewMode === VIEW_ALL_RELATED && (
}
empty={__('No related content found')}
+ onClick={handleRecommendationClicked}
/>
)}
{viewMode === VIEW_MORE_FROM && signingChannel && (
diff --git a/ui/component/viewers/videoViewer/internal/plugins/videojs-recsys/plugin.js b/ui/component/viewers/videoViewer/internal/plugins/videojs-recsys/plugin.js
index d7bacdcce..5ee3f3c8d 100644
--- a/ui/component/viewers/videoViewer/internal/plugins/videojs-recsys/plugin.js
+++ b/ui/component/viewers/videoViewer/internal/plugins/videojs-recsys/plugin.js
@@ -1,6 +1,12 @@
// Created by xander on 6/21/2021
import videojs from 'video.js';
-import { v4 as uuidV4 } from 'uuid';
+import {
+ makeSelectRecommendationId,
+ makeSelectRecommendationParentId,
+ makeSelectRecommendedClaimIds,
+ makeSelectRecommendationClicks,
+} from 'redux/selectors/content';
+
const VERSION = '0.0.1';
const recsysEndpoint = 'https://clickstream.odysee.com/log/video/view';
@@ -17,23 +23,28 @@ const RecsysData = {
};
function createRecsys(claimId, userId, events, loadedAt, isEmbed) {
- // TODO: use a UUID generator
- const uuid = uuidV4();
const pageLoadedAt = loadedAt;
const pageExitedAt = Date.now();
- return {
- uuid: uuid,
- parentUuid: null,
- uid: userId,
- claimId: claimId,
- pageLoadedAt: pageLoadedAt,
- pageExitedAt: pageExitedAt,
- recsysId: recsysId,
- recClaimIds: null,
- recClickedVideoIdx: null,
- events: events,
- isEmbed: isEmbed,
- };
+
+ 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: recsysId,
+ recClaimIds: makeSelectRecommendedClaimIds(claimId)(state),
+ recClickedVideoIdx: makeSelectRecommendationClicks(claimId)(state),
+ events: events,
+ isEmbed: isEmbed,
+ };
+ }
+
+ return undefined;
}
function newRecsysEvent(eventType, offset, arg) {
@@ -130,7 +141,10 @@ class RecsysPlugin extends Component {
this.loadedAt,
false
);
- sendRecsysEvents(event);
+
+ if (event) {
+ sendRecsysEvents(event);
+ }
}
onPlay(event) {
@@ -226,7 +240,7 @@ const onPlayerReady = (player, options) => {
* @function plugin
* @param {Object} [options={}]
*/
-const plugin = function(options) {
+const plugin = function (options) {
this.ready(() => {
onPlayerReady(this, videojs.mergeOptions(defaults, options));
});
diff --git a/ui/constants/action_types.js b/ui/constants/action_types.js
index 42d638318..14f10ad6b 100644
--- a/ui/constants/action_types.js
+++ b/ui/constants/action_types.js
@@ -98,6 +98,8 @@ export const CLEAR_CONTENT_POSITION = 'CLEAR_CONTENT_POSITION';
export const SET_CONTENT_LAST_VIEWED = 'SET_CONTENT_LAST_VIEWED';
export const CLEAR_CONTENT_HISTORY_URI = 'CLEAR_CONTENT_HISTORY_URI';
export const CLEAR_CONTENT_HISTORY_ALL = 'CLEAR_CONTENT_HISTORY_ALL';
+export const RECOMMENDATION_UPDATED = 'RECOMMENDATION_UPDATED';
+export const RECOMMENDATION_CLICKED = 'RECOMMENDATION_CLICKED';
// Files
export const FILE_LIST_STARTED = 'FILE_LIST_STARTED';
diff --git a/ui/constants/navigation.js b/ui/constants/navigation.js
new file mode 100644
index 000000000..217945645
--- /dev/null
+++ b/ui/constants/navigation.js
@@ -0,0 +1 @@
+export const CONTAINER_ID = 'CONTAINER_ID';
diff --git a/ui/redux/actions/content.js b/ui/redux/actions/content.js
index 17217a60b..b5bdc6356 100644
--- a/ui/redux/actions/content.js
+++ b/ui/redux/actions/content.js
@@ -43,7 +43,7 @@ export function doUpdateLoadStatus(uri: string, outpoint: string) {
full_status: true,
page: 1,
page_size: 1,
- }).then(result => {
+ }).then((result) => {
const { items: fileInfos } = result;
const fileInfo = fileInfos[0];
if (!fileInfo || fileInfo.written_bytes === 0) {
@@ -261,3 +261,21 @@ export function doClearContentHistoryAll() {
dispatch({ type: ACTIONS.CLEAR_CONTENT_HISTORY_ALL });
};
}
+
+export const doRecommendationUpdate = (claimId: string, urls: Array, id: string, parentId: string) => (
+ dispatch: Dispatch
+) => {
+ dispatch({
+ type: ACTIONS.RECOMMENDATION_UPDATED,
+ data: { claimId, urls, id, parentId },
+ });
+};
+
+export const doRecommendationClicked = (claimId: string, index: number) => (dispatch: Dispatch) => {
+ if (index !== undefined && index !== null) {
+ dispatch({
+ type: ACTIONS.RECOMMENDATION_CLICKED,
+ data: { claimId, index },
+ });
+ }
+};
diff --git a/ui/redux/reducers/content.js b/ui/redux/reducers/content.js
index 576cdf67f..7b0069f30 100644
--- a/ui/redux/reducers/content.js
+++ b/ui/redux/reducers/content.js
@@ -7,6 +7,10 @@ const defaultState = {
channelClaimCounts: {},
positions: {},
history: [],
+ recommendationId: {}, // { "claimId": "recommendationId" }
+ recommendationParentId: {}, // { "claimId": "referrerId" }
+ recommendationUrls: {}, // { "claimId": [lbryUrls...] }
+ recommendationClicks: {}, // { "claimId": [clicked indices...] }
};
reducers[ACTIONS.SET_PRIMARY_URI] = (state, action) =>
@@ -73,7 +77,7 @@ reducers[ACTIONS.SET_CONTENT_LAST_VIEWED] = (state, action) => {
const { uri, lastViewed } = action.data;
const { history } = state;
const historyObj = { uri, lastViewed };
- const index = history.findIndex(i => i.uri === uri);
+ const index = history.findIndex((i) => i.uri === uri);
const newHistory =
index === -1
? [historyObj].concat(history)
@@ -84,7 +88,7 @@ reducers[ACTIONS.SET_CONTENT_LAST_VIEWED] = (state, action) => {
reducers[ACTIONS.CLEAR_CONTENT_HISTORY_URI] = (state, action) => {
const { uri } = action.data;
const { history } = state;
- const index = history.findIndex(i => i.uri === uri);
+ const index = history.findIndex((i) => i.uri === uri);
return index === -1
? state
: {
@@ -93,7 +97,44 @@ 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) => {
// return {
diff --git a/ui/redux/selectors/content.js b/ui/redux/selectors/content.js
index 570ed5c7e..faaa4acc5 100644
--- a/ui/redux/selectors/content.js
+++ b/ui/redux/selectors/content.js
@@ -13,6 +13,7 @@ import {
makeSelectFileNameForUri,
normalizeURI,
selectMyActiveClaims,
+ selectClaimIdsByUri,
} from 'lbry-redux';
import { makeSelectRecommendedContentForUri } from 'redux/selectors/search';
import { selectMutedChannels } from 'redux/selectors/blocked';
@@ -247,19 +248,37 @@ export const makeSelectInsufficientCreditsForUri = (uri: string) =>
);
export const makeSelectSigningIsMine = (rawUri: string) => {
- let uri;
+ let uri;
+ try {
+ uri = normalizeURI(rawUri);
+ } catch (e) {}
+
+ return createSelector(selectClaimsByUri, selectMyActiveClaims, (claims, myClaims) => {
try {
- uri = normalizeURI(rawUri);
- } catch (e) { }
+ parseURI(uri);
+ } catch (e) {
+ return false;
+ }
+ const signingChannel = claims && claims[uri] && (claims[uri].signing_channel || claims[uri]);
- return createSelector(selectClaimsByUri, selectMyActiveClaims, (claims, myClaims) => {
- try {
- parseURI(uri);
- } catch (e) {
- return false;
- }
- const signingChannel = claims && claims[uri] && (claims[uri].signing_channel || claims[uri]);
+ return signingChannel && myClaims.has(signingChannel.claim_id);
+ });
+};
- return signingChannel && myClaims.has(signingChannel.claim_id);
- });
- };
+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]);