2018-07-25 02:50:04 +02:00
|
|
|
// @flow
|
2022-03-15 20:07:31 +01:00
|
|
|
import { v4 as Uuidv4 } from 'uuid';
|
2021-12-06 19:01:40 +01:00
|
|
|
import { SHOW_ADS, AD_KEYWORD_BLOCKLIST, AD_KEYWORD_BLOCKLIST_CHECK_DESCRIPTION } from 'config';
|
2018-07-25 02:50:04 +02:00
|
|
|
import React from 'react';
|
2019-06-19 07:05:43 +02:00
|
|
|
import ClaimList from 'component/claimList';
|
2021-04-06 20:59:12 +02:00
|
|
|
import ClaimListDiscover from 'component/claimListDiscover';
|
2022-03-02 06:43:25 +01:00
|
|
|
import Spinner from 'component/spinner';
|
2020-05-07 20:44:11 +02:00
|
|
|
import Ads from 'web/component/ads';
|
2020-04-29 21:31:11 +02:00
|
|
|
import Card from 'component/common/card';
|
2020-08-25 15:49:19 +02:00
|
|
|
import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize';
|
2021-04-06 20:59:12 +02:00
|
|
|
import Button from 'component/button';
|
2022-03-15 20:07:31 +01:00
|
|
|
import { FYP_ID } from 'constants/urlParams';
|
2021-04-08 17:21:45 +02:00
|
|
|
import classnames from 'classnames';
|
2021-09-03 00:39:40 +02:00
|
|
|
import RecSys from 'recsys';
|
2022-03-02 16:10:29 +01:00
|
|
|
import { getClaimMetadata } from 'util/claim';
|
2021-04-08 17:21:45 +02:00
|
|
|
|
|
|
|
const VIEW_ALL_RELATED = 'view_all_related';
|
|
|
|
const VIEW_MORE_FROM = 'view_more_from';
|
2022-03-02 16:10:29 +01:00
|
|
|
const BLOCKED_WORDS: ?Array<string> = AD_KEYWORD_BLOCKLIST && AD_KEYWORD_BLOCKLIST.toLowerCase().split(',');
|
|
|
|
const CHECK_DESCRIPTION: boolean = AD_KEYWORD_BLOCKLIST_CHECK_DESCRIPTION === 'true';
|
2018-07-25 02:50:04 +02:00
|
|
|
|
|
|
|
type Props = {
|
2018-07-25 06:45:24 +02:00
|
|
|
uri: string,
|
2021-09-03 00:39:40 +02:00
|
|
|
recommendedContentUris: Array<string>,
|
2021-03-19 04:16:05 +01:00
|
|
|
nextRecommendedUri: string,
|
2018-08-27 05:18:57 +02:00
|
|
|
isSearching: boolean,
|
2022-03-15 20:07:31 +01:00
|
|
|
doFetchRecommendedContent: (string, ?FypParam) => void,
|
2021-04-08 17:21:45 +02:00
|
|
|
claim: ?StreamClaim,
|
2022-03-09 19:05:37 +01:00
|
|
|
claimId: string,
|
|
|
|
metadata: any,
|
2022-03-15 20:07:31 +01:00
|
|
|
location: UrlLocation,
|
2022-05-18 20:51:15 +02:00
|
|
|
hasPremiumPlus: boolean,
|
2018-07-25 02:50:04 +02:00
|
|
|
};
|
|
|
|
|
2021-06-28 10:38:48 +02:00
|
|
|
export default React.memo<Props>(function RecommendedContent(props: Props) {
|
2021-03-19 16:04:12 +01:00
|
|
|
const {
|
|
|
|
uri,
|
|
|
|
doFetchRecommendedContent,
|
2021-09-03 00:39:40 +02:00
|
|
|
recommendedContentUris,
|
2021-03-19 16:04:12 +01:00
|
|
|
nextRecommendedUri,
|
|
|
|
isSearching,
|
2021-04-08 17:21:45 +02:00
|
|
|
claim,
|
2022-03-15 20:07:31 +01:00
|
|
|
location,
|
2022-05-18 20:51:15 +02:00
|
|
|
hasPremiumPlus,
|
2021-03-19 16:04:12 +01:00
|
|
|
} = props;
|
2021-12-06 19:01:40 +01:00
|
|
|
|
2022-03-02 16:10:29 +01:00
|
|
|
const claimId: ?string = claim && claim.claim_id;
|
2022-05-18 20:51:15 +02:00
|
|
|
const injectAds = SHOW_ADS && IS_WEB && !hasPremiumPlus;
|
2022-03-02 16:10:29 +01:00
|
|
|
|
|
|
|
function claimContainsBlockedWords(claim: ?StreamClaim) {
|
|
|
|
if (BLOCKED_WORDS) {
|
|
|
|
const hasBlockedWords = (str) => BLOCKED_WORDS.some((bw) => str.includes(bw));
|
|
|
|
const metadata = getClaimMetadata(claim);
|
|
|
|
// $FlowFixMe - flow does not support chaining yet, but we know for sure these fields are '?string'.
|
|
|
|
const title = metadata?.title?.toLowerCase();
|
|
|
|
// $FlowFixMe
|
|
|
|
const description = metadata?.description?.toLowerCase();
|
|
|
|
// $FlowFixMe
|
|
|
|
const name = claim?.name?.toLowerCase();
|
|
|
|
|
|
|
|
return Boolean(
|
|
|
|
(title && hasBlockedWords(title)) ||
|
|
|
|
(name && hasBlockedWords(name)) ||
|
|
|
|
(CHECK_DESCRIPTION && description && hasBlockedWords(description))
|
|
|
|
);
|
2021-12-06 19:01:40 +01:00
|
|
|
}
|
2022-03-02 16:10:29 +01:00
|
|
|
return false;
|
2021-12-06 19:01:40 +01:00
|
|
|
}
|
|
|
|
|
2022-03-07 08:12:49 +01:00
|
|
|
const blacklistTriggered = React.useMemo(() => injectAds && claimContainsBlockedWords(claim), [injectAds, claim]);
|
2022-03-02 16:10:29 +01:00
|
|
|
|
2021-04-08 17:21:45 +02:00
|
|
|
const [viewMode, setViewMode] = React.useState(VIEW_ALL_RELATED);
|
|
|
|
const signingChannel = claim && claim.signing_channel;
|
|
|
|
const channelName = signingChannel ? signingChannel.name : null;
|
2020-08-25 15:49:19 +02:00
|
|
|
const isMobile = useIsMobile();
|
|
|
|
const isMedium = useIsMediumScreen();
|
2021-09-03 00:39:40 +02:00
|
|
|
const { onRecsLoaded: onRecommendationsLoaded, onClickedRecommended: onRecommendationClicked } = RecSys;
|
2021-03-19 04:16:05 +01:00
|
|
|
|
2022-03-07 13:11:28 +01:00
|
|
|
const InjectedAd =
|
2022-05-18 20:51:15 +02:00
|
|
|
injectAds && !blacklistTriggered && !hasPremiumPlus
|
2022-03-07 13:11:28 +01:00
|
|
|
? {
|
2022-06-02 17:28:40 +02:00
|
|
|
node: <Ads small type="video" filePage className="ads__claim-item--recommended" noFallback />,
|
2022-03-07 13:11:28 +01:00
|
|
|
index: isMobile ? 0 : 3,
|
|
|
|
}
|
|
|
|
: null;
|
|
|
|
|
2022-03-15 20:07:31 +01:00
|
|
|
// Assume this component always resides in a page where the `uri` matches
|
|
|
|
// e.g. never in a floating popup. With that, we can grab the FYP ID from
|
|
|
|
// the search param directly. Otherwise, the parent component would need to
|
|
|
|
// pass it.
|
|
|
|
const { search } = location;
|
|
|
|
const urlParams = new URLSearchParams(search);
|
|
|
|
const fypId = urlParams.get(FYP_ID);
|
|
|
|
const [uuid] = React.useState(fypId ? Uuidv4() : '');
|
|
|
|
|
2020-08-25 15:49:19 +02:00
|
|
|
React.useEffect(() => {
|
2022-03-15 20:07:31 +01:00
|
|
|
const fypParam = fypId && uuid ? { gid: fypId, uuid } : null;
|
|
|
|
doFetchRecommendedContent(uri, fypParam);
|
|
|
|
}, [uri, doFetchRecommendedContent, fypId, uuid]);
|
2018-08-09 18:41:14 +02:00
|
|
|
|
2021-09-03 00:39:40 +02:00
|
|
|
React.useEffect(() => {
|
|
|
|
// Right now we only want to record the recs if they actually saw them.
|
2021-09-16 22:00:44 +02:00
|
|
|
if (
|
2022-03-02 16:10:29 +01:00
|
|
|
claimId &&
|
2021-09-16 22:00:44 +02:00
|
|
|
recommendedContentUris &&
|
|
|
|
recommendedContentUris.length &&
|
|
|
|
nextRecommendedUri &&
|
|
|
|
viewMode === VIEW_ALL_RELATED
|
|
|
|
) {
|
2022-06-01 15:55:38 +02:00
|
|
|
onRecommendationsLoaded(claimId, recommendedContentUris, uuid);
|
2021-09-03 00:39:40 +02:00
|
|
|
}
|
2022-06-01 15:55:38 +02:00
|
|
|
}, [recommendedContentUris, onRecommendationsLoaded, claimId, nextRecommendedUri, viewMode, uuid]);
|
2021-09-03 00:39:40 +02:00
|
|
|
|
2021-09-16 22:00:44 +02:00
|
|
|
function handleRecommendationClicked(e, clickedClaim) {
|
Fill in remaining Recsys fields
## Issue
6366 Recsys Evaluation Telemetry
The recommended list from lighthouse is obtained from `makeSelectRecommendedContentForUri`. This list is further tweaked by the GUI (e.g. move autoplay next item to top, remove blocked content, etc.). Recsys wants the final recommendation list and the clicked index (in exact order), so we need pass these info to the videojs recsys plugin somehow. Also, Recsys wants a recommendation list ID as well as the parent (referrer) ID, we so need to track the clicks and navigation.
## General Approach
- It seems easiest to just spew back the final (displayed) list and all the required info to Redux, and the recsys plugin (or anyone else in the future) can grab it.
- Try to touch few files as possible. The dirty work should all reside in `<RecommendedContent>` only.
## Changes
- `ClaimPreview`: add optional parameters to store an ID of the container that it is in (for this case, it is `ClaimList`) as well as the index within the container.
- When clicked, we store the container ID in the navigation history `state` object.
- For general cases, anyone can check this state from `history.location.state` to know which container referred/navigated to the current page. For the recsys use case, we can use this as the `parentUUID`.
- `ClaimList`: just relay `onClick` and set IDs.
- `RecommendedContent`: now handles the uuid generation (for both parent and child) and stores the data in Redux.
2021-07-28 14:07:49 +02:00
|
|
|
if (claim) {
|
2021-09-03 00:39:40 +02:00
|
|
|
onRecommendationClicked(claim.claim_id, clickedClaim.claim_id);
|
Fill in remaining Recsys fields
## Issue
6366 Recsys Evaluation Telemetry
The recommended list from lighthouse is obtained from `makeSelectRecommendedContentForUri`. This list is further tweaked by the GUI (e.g. move autoplay next item to top, remove blocked content, etc.). Recsys wants the final recommendation list and the clicked index (in exact order), so we need pass these info to the videojs recsys plugin somehow. Also, Recsys wants a recommendation list ID as well as the parent (referrer) ID, we so need to track the clicks and navigation.
## General Approach
- It seems easiest to just spew back the final (displayed) list and all the required info to Redux, and the recsys plugin (or anyone else in the future) can grab it.
- Try to touch few files as possible. The dirty work should all reside in `<RecommendedContent>` only.
## Changes
- `ClaimPreview`: add optional parameters to store an ID of the container that it is in (for this case, it is `ClaimList`) as well as the index within the container.
- When clicked, we store the container ID in the navigation history `state` object.
- For general cases, anyone can check this state from `history.location.state` to know which container referred/navigated to the current page. For the recsys use case, we can use this as the `parentUUID`.
- `ClaimList`: just relay `onClick` and set IDs.
- `RecommendedContent`: now handles the uuid generation (for both parent and child) and stores the data in Redux.
2021-07-28 14:07:49 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-25 15:49:19 +02:00
|
|
|
return (
|
|
|
|
<Card
|
|
|
|
isBodyList
|
|
|
|
smallTitle={!isMobile && !isMedium}
|
|
|
|
className="file-page__recommended"
|
|
|
|
title={__('Related')}
|
2021-04-08 17:21:45 +02:00
|
|
|
titleActions={
|
|
|
|
signingChannel && (
|
2022-02-11 19:50:55 +01:00
|
|
|
<div className="recommended-content__bubble">
|
2021-04-08 17:21:45 +02:00
|
|
|
<Button
|
2022-02-11 19:50:55 +01:00
|
|
|
className={classnames('button-bubble', {
|
|
|
|
'button-bubble--active': viewMode === VIEW_ALL_RELATED,
|
2021-04-08 17:21:45 +02:00
|
|
|
})}
|
2022-02-11 19:50:55 +01:00
|
|
|
label={__('Related')}
|
2021-04-08 17:21:45 +02:00
|
|
|
onClick={() => setViewMode(VIEW_ALL_RELATED)}
|
|
|
|
/>
|
|
|
|
|
2021-04-06 20:59:12 +02:00
|
|
|
<Button
|
2022-02-11 19:50:55 +01:00
|
|
|
className={classnames('button-bubble', {
|
|
|
|
'button-bubble--active': viewMode === VIEW_MORE_FROM,
|
2021-04-08 17:21:45 +02:00
|
|
|
})}
|
2021-04-06 20:59:12 +02:00
|
|
|
label={__('More from %claim_name%', { claim_name: channelName })}
|
2021-04-08 17:21:45 +02:00
|
|
|
onClick={() => setViewMode(VIEW_MORE_FROM)}
|
2021-04-06 20:59:12 +02:00
|
|
|
/>
|
2021-04-08 17:21:45 +02:00
|
|
|
</div>
|
|
|
|
)
|
2021-04-06 20:59:12 +02:00
|
|
|
}
|
|
|
|
body={
|
|
|
|
<div>
|
2022-03-02 06:43:25 +01:00
|
|
|
{isSearching && (
|
|
|
|
<div className="empty empty--centered-tight">
|
|
|
|
<Spinner type="small" />
|
|
|
|
</div>
|
|
|
|
)}
|
2021-04-08 17:21:45 +02:00
|
|
|
{viewMode === VIEW_ALL_RELATED && (
|
2021-04-06 20:59:12 +02:00
|
|
|
<ClaimList
|
|
|
|
type="small"
|
|
|
|
loading={isSearching}
|
2021-09-16 22:00:44 +02:00
|
|
|
uris={recommendedContentUris}
|
2021-04-06 20:59:12 +02:00
|
|
|
hideMenu={isMobile}
|
2022-03-07 13:11:28 +01:00
|
|
|
injectedItem={InjectedAd}
|
2021-04-06 20:59:12 +02:00
|
|
|
empty={__('No related content found')}
|
Fill in remaining Recsys fields
## Issue
6366 Recsys Evaluation Telemetry
The recommended list from lighthouse is obtained from `makeSelectRecommendedContentForUri`. This list is further tweaked by the GUI (e.g. move autoplay next item to top, remove blocked content, etc.). Recsys wants the final recommendation list and the clicked index (in exact order), so we need pass these info to the videojs recsys plugin somehow. Also, Recsys wants a recommendation list ID as well as the parent (referrer) ID, we so need to track the clicks and navigation.
## General Approach
- It seems easiest to just spew back the final (displayed) list and all the required info to Redux, and the recsys plugin (or anyone else in the future) can grab it.
- Try to touch few files as possible. The dirty work should all reside in `<RecommendedContent>` only.
## Changes
- `ClaimPreview`: add optional parameters to store an ID of the container that it is in (for this case, it is `ClaimList`) as well as the index within the container.
- When clicked, we store the container ID in the navigation history `state` object.
- For general cases, anyone can check this state from `history.location.state` to know which container referred/navigated to the current page. For the recsys use case, we can use this as the `parentUUID`.
- `ClaimList`: just relay `onClick` and set IDs.
- `RecommendedContent`: now handles the uuid generation (for both parent and child) and stores the data in Redux.
2021-07-28 14:07:49 +02:00
|
|
|
onClick={handleRecommendationClicked}
|
2021-04-06 20:59:12 +02:00
|
|
|
/>
|
|
|
|
)}
|
2021-04-08 17:21:45 +02:00
|
|
|
{viewMode === VIEW_MORE_FROM && signingChannel && (
|
2021-04-06 20:59:12 +02:00
|
|
|
<ClaimListDiscover
|
2021-04-08 17:21:45 +02:00
|
|
|
hideAdvancedFilter
|
2021-04-06 20:59:12 +02:00
|
|
|
tileLayout={false}
|
|
|
|
showHeader={false}
|
|
|
|
type="small"
|
|
|
|
claimType={['stream']}
|
|
|
|
orderBy="new"
|
|
|
|
pageSize={20}
|
|
|
|
infiniteScroll={false}
|
2021-04-08 17:21:45 +02:00
|
|
|
hideFilters
|
2021-04-06 20:59:12 +02:00
|
|
|
channelIds={[signingChannel.claim_id]}
|
|
|
|
loading={isSearching}
|
|
|
|
hideMenu={isMobile}
|
2022-03-07 13:11:28 +01:00
|
|
|
injectedItem={InjectedAd}
|
2021-04-06 20:59:12 +02:00
|
|
|
empty={__('No related content found')}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
</div>
|
2020-08-25 15:49:19 +02:00
|
|
|
}
|
|
|
|
/>
|
|
|
|
);
|
2021-06-28 10:38:48 +02:00
|
|
|
}, areEqual);
|
|
|
|
|
|
|
|
function areEqual(prevProps: Props, nextProps: Props) {
|
|
|
|
const a = prevProps;
|
|
|
|
const b = nextProps;
|
|
|
|
|
|
|
|
if (
|
|
|
|
a.uri !== b.uri ||
|
|
|
|
a.nextRecommendedUri !== b.nextRecommendedUri ||
|
|
|
|
a.isSearching !== b.isSearching ||
|
2021-09-03 00:39:40 +02:00
|
|
|
(a.recommendedContentUris && !b.recommendedContentUris) ||
|
|
|
|
(!a.recommendedContentUris && b.recommendedContentUris) ||
|
2021-06-28 10:38:48 +02:00
|
|
|
(a.claim && !b.claim) ||
|
|
|
|
(!a.claim && b.claim)
|
|
|
|
) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (a.claim && b.claim && a.claim.claim_id !== b.claim.claim_id) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2021-09-03 00:39:40 +02:00
|
|
|
if (a.recommendedContentUris && b.recommendedContentUris) {
|
|
|
|
if (a.recommendedContentUris.length !== b.recommendedContentUris.length) {
|
2021-06-28 10:38:48 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2021-09-03 00:39:40 +02:00
|
|
|
let i = a.recommendedContentUris.length;
|
2021-06-28 10:38:48 +02:00
|
|
|
while (i--) {
|
2021-09-03 00:39:40 +02:00
|
|
|
if (a.recommendedContentUris[i] !== b.recommendedContentUris[i]) {
|
2021-06-28 10:38:48 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
2018-07-25 02:50:04 +02:00
|
|
|
}
|