Ads: replace DOM manipulations with React components
- Instead of 2 ways to display ads (DOM injection + React method) and having both of them clash, just do it the predictable React way. - Augment the existing React version to support tile layout + ability to place in last visible slot. - Consolidate styling code to scss ... DOM manipulations were making it even harder to maintain. - Removed the need to check for ad-blockers for now. It was being executed every time an ad is displayed, and now that we are displaying ads in more places, the gains doesn't justify the performance loss. Also, it wasn't being done for Recommended ads anyway, so the inconsistency probably means it's not needed in the first place. Other known issues fixed: - double ad injection when changing language via nag. - additional "total-blocking-time" due to ads at startup removed. - fixed ads not appearing in mobile homepage until navigated away and back to homepage. - enable ads in channel page. - support for both List and Tile layout.
This commit is contained in:
parent
e2f73a30c6
commit
0143b63c74
12 changed files with 270 additions and 232 deletions
|
@ -175,8 +175,6 @@ function ChannelContent(props: Props) {
|
||||||
|
|
||||||
{!channelIsMine && claimsInChannel > 0 && <HiddenNsfwClaims uri={uri} />}
|
{!channelIsMine && claimsInChannel > 0 && <HiddenNsfwClaims uri={uri} />}
|
||||||
|
|
||||||
{/* <Ads type="homepage" /> */}
|
|
||||||
|
|
||||||
{!fetching && (
|
{!fetching && (
|
||||||
<ClaimListDiscover
|
<ClaimListDiscover
|
||||||
ignoreSearchInLanguage
|
ignoreSearchInLanguage
|
||||||
|
@ -196,7 +194,7 @@ function ChannelContent(props: Props) {
|
||||||
defaultOrderBy={CS.ORDER_BY_NEW}
|
defaultOrderBy={CS.ORDER_BY_NEW}
|
||||||
pageSize={defaultPageSize}
|
pageSize={defaultPageSize}
|
||||||
infiniteScroll={defaultInfiniteScroll}
|
infiniteScroll={defaultInfiniteScroll}
|
||||||
injectedItem={SHOW_ADS && !isAuthenticated && IS_WEB && <Ads type="video" />}
|
injectedItem={SHOW_ADS && !isAuthenticated && { node: <Ads type="video" tileLayout={tileLayout} small /> }}
|
||||||
meta={
|
meta={
|
||||||
showFilters && (
|
showFilters && (
|
||||||
<Form onSubmit={() => {}} className="wunderbar--inline">
|
<Form onSubmit={() => {}} className="wunderbar--inline">
|
||||||
|
|
|
@ -7,6 +7,7 @@ import ClaimPreview from 'component/claimPreview';
|
||||||
import Spinner from 'component/spinner';
|
import Spinner from 'component/spinner';
|
||||||
import { FormField } from 'component/common/form';
|
import { FormField } from 'component/common/form';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
|
import useLastVisibleItem from 'effects/use-last-visible-item';
|
||||||
import debounce from 'util/debounce';
|
import debounce from 'util/debounce';
|
||||||
import ClaimPreviewTile from 'component/claimPreviewTile';
|
import ClaimPreviewTile from 'component/claimPreviewTile';
|
||||||
|
|
||||||
|
@ -40,7 +41,7 @@ type Props = {
|
||||||
renderActions?: (Claim) => ?Node,
|
renderActions?: (Claim) => ?Node,
|
||||||
renderProperties?: (Claim) => ?Node,
|
renderProperties?: (Claim) => ?Node,
|
||||||
includeSupportAction?: boolean,
|
includeSupportAction?: boolean,
|
||||||
injectedItem: ?Node,
|
injectedItem?: { node: Node, index?: number, replace?: boolean },
|
||||||
timedOutMessage?: Node,
|
timedOutMessage?: Node,
|
||||||
tileLayout?: boolean,
|
tileLayout?: boolean,
|
||||||
searchInLanguage: boolean,
|
searchInLanguage: boolean,
|
||||||
|
@ -99,6 +100,9 @@ export default function ClaimList(props: Props) {
|
||||||
|
|
||||||
const [currentSort, setCurrentSort] = usePersistedState(persistedStorageKey, SORT_NEW);
|
const [currentSort, setCurrentSort] = usePersistedState(persistedStorageKey, SORT_NEW);
|
||||||
|
|
||||||
|
const listRef = React.useRef();
|
||||||
|
const injectedIndex = useLastVisibleItem(injectedItem, listRef);
|
||||||
|
|
||||||
// Exclude prefix uris in these results variables. We don't want to show
|
// Exclude prefix uris in these results variables. We don't want to show
|
||||||
// anything if the search failed or timed out.
|
// anything if the search failed or timed out.
|
||||||
const timedOut = uris === null;
|
const timedOut = uris === null;
|
||||||
|
@ -142,6 +146,12 @@ 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';
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// @if process.env.NODE_ENV!='production'
|
||||||
|
if (injectedItem && injectedItem.replace) {
|
||||||
|
throw new Error('claimList: "injectedItem.replace" is not implemented yet');
|
||||||
|
}
|
||||||
|
// @endif
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleScroll = debounce((e) => {
|
const handleScroll = debounce((e) => {
|
||||||
if (page && pageSize && onScrollBottom) {
|
if (page && pageSize && onScrollBottom) {
|
||||||
|
@ -188,19 +198,28 @@ export default function ClaimList(props: Props) {
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const getInjectedItem = (index) => {
|
||||||
|
if (injectedItem && injectedItem.node && injectedIndex === index) {
|
||||||
|
return injectedItem.node;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
return tileLayout && !header ? (
|
return tileLayout && !header ? (
|
||||||
<section className={classnames('claim-grid', { 'swipe-list': swipeLayout })}>
|
<section ref={listRef} className={classnames('claim-grid', { 'swipe-list': swipeLayout })}>
|
||||||
{urisLength > 0 &&
|
{urisLength > 0 &&
|
||||||
tileUris.map((uri) => (
|
tileUris.map((uri, index) => (
|
||||||
<ClaimPreviewTile
|
<React.Fragment key={uri}>
|
||||||
key={uri}
|
{getInjectedItem(index)}
|
||||||
uri={uri}
|
<ClaimPreviewTile
|
||||||
showHiddenByUser={showHiddenByUser}
|
uri={uri}
|
||||||
properties={renderProperties}
|
showHiddenByUser={showHiddenByUser}
|
||||||
collectionId={collectionId}
|
properties={renderProperties}
|
||||||
showNoSourceClaims={showNoSourceClaims}
|
collectionId={collectionId}
|
||||||
swipeLayout={swipeLayout}
|
showNoSourceClaims={showNoSourceClaims}
|
||||||
/>
|
swipeLayout={swipeLayout}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
{loading && useLoadingSpinner && <ClaimPreviewTile placeholder="loading" swipeLayout={swipeLayout} />}
|
{loading && useLoadingSpinner && <ClaimPreviewTile placeholder="loading" swipeLayout={swipeLayout} />}
|
||||||
{!timedOut && urisLength === 0 && !loading && <div className="empty main--empty">{empty || noResultMsg}</div>}
|
{!timedOut && urisLength === 0 && !loading && <div className="empty main--empty">{empty || noResultMsg}</div>}
|
||||||
|
@ -248,11 +267,10 @@ export default function ClaimList(props: Props) {
|
||||||
'swipe-list': swipeLayout,
|
'swipe-list': swipeLayout,
|
||||||
})}
|
})}
|
||||||
{...(droppableProvided && droppableProvided.droppableProps)}
|
{...(droppableProvided && droppableProvided.droppableProps)}
|
||||||
ref={droppableProvided && droppableProvided.innerRef}
|
ref={droppableProvided ? droppableProvided.innerRef : listRef}
|
||||||
>
|
>
|
||||||
{droppableProvided ? (
|
{droppableProvided ? (
|
||||||
<>
|
<>
|
||||||
{injectedItem && <li>{injectedItem}</li>}
|
|
||||||
{sortedUris.map((uri, index) => (
|
{sortedUris.map((uri, index) => (
|
||||||
<React.Suspense fallback={null} key={uri}>
|
<React.Suspense fallback={null} key={uri}>
|
||||||
<Draggable draggableId={uri} index={index}>
|
<Draggable draggableId={uri} index={index}>
|
||||||
|
@ -285,7 +303,7 @@ export default function ClaimList(props: Props) {
|
||||||
) : (
|
) : (
|
||||||
sortedUris.map((uri, index) => (
|
sortedUris.map((uri, index) => (
|
||||||
<React.Fragment key={uri}>
|
<React.Fragment key={uri}>
|
||||||
{injectedItem && index === 4 && <li>{injectedItem}</li>}
|
{getInjectedItem(index)}
|
||||||
{getClaimPreview(uri, index)}
|
{getClaimPreview(uri, index)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))
|
))
|
||||||
|
|
|
@ -74,7 +74,7 @@ type Props = {
|
||||||
header?: Node,
|
header?: Node,
|
||||||
headerLabel?: string | Node,
|
headerLabel?: string | Node,
|
||||||
hiddenNsfwMessage?: Node,
|
hiddenNsfwMessage?: Node,
|
||||||
injectedItem: ?Node,
|
injectedItem?: { node: Node, index?: number, replace?: boolean },
|
||||||
meta?: Node,
|
meta?: Node,
|
||||||
subSection?: Node, // Additional section below [Header|Meta]
|
subSection?: Node, // Additional section below [Header|Meta]
|
||||||
renderProperties?: (Claim) => Node,
|
renderProperties?: (Claim) => Node,
|
||||||
|
|
|
@ -3,6 +3,7 @@ import type { Node } from 'react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ClaimPreviewTile from 'component/claimPreviewTile';
|
import ClaimPreviewTile from 'component/claimPreviewTile';
|
||||||
import useFetchViewCount from 'effects/use-fetch-view-count';
|
import useFetchViewCount from 'effects/use-fetch-view-count';
|
||||||
|
import useLastVisibleItem from 'effects/use-last-visible-item';
|
||||||
|
|
||||||
function urisEqual(prev: ?Array<string>, next: ?Array<string>) {
|
function urisEqual(prev: ?Array<string>, next: ?Array<string>) {
|
||||||
if (!prev || !next) {
|
if (!prev || !next) {
|
||||||
|
@ -25,6 +26,7 @@ type Props = {
|
||||||
prefixUris?: Array<string>,
|
prefixUris?: Array<string>,
|
||||||
pinUrls?: Array<string>,
|
pinUrls?: Array<string>,
|
||||||
uris: Array<string>,
|
uris: Array<string>,
|
||||||
|
injectedItem?: { node: Node, index?: number, replace?: boolean },
|
||||||
showNoSourceClaims?: boolean,
|
showNoSourceClaims?: boolean,
|
||||||
renderProperties?: (Claim) => ?Node,
|
renderProperties?: (Claim) => ?Node,
|
||||||
fetchViewCount?: boolean,
|
fetchViewCount?: boolean,
|
||||||
|
@ -67,14 +69,17 @@ function ClaimTilesDiscover(props: Props) {
|
||||||
renderProperties,
|
renderProperties,
|
||||||
pinUrls,
|
pinUrls,
|
||||||
prefixUris,
|
prefixUris,
|
||||||
|
injectedItem,
|
||||||
showNoSourceClaims,
|
showNoSourceClaims,
|
||||||
doFetchViewCount,
|
doFetchViewCount,
|
||||||
pageSize = 8,
|
pageSize = 8,
|
||||||
optionsStringified,
|
optionsStringified,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const prevUris = React.useRef();
|
const sectionRef = React.useRef();
|
||||||
|
const injectedIndex = useLastVisibleItem(injectedItem, sectionRef);
|
||||||
|
|
||||||
|
const prevUris = React.useRef();
|
||||||
const claimSearchUris = claimSearchResults || [];
|
const claimSearchUris = claimSearchResults || [];
|
||||||
const isUnfetchedClaimSearch = claimSearchResults === undefined;
|
const isUnfetchedClaimSearch = claimSearchResults === undefined;
|
||||||
|
|
||||||
|
@ -118,17 +123,23 @@ function ClaimTilesDiscover(props: Props) {
|
||||||
}, [doClaimSearch, shouldPerformSearch, optionsStringified]);
|
}, [doClaimSearch, shouldPerformSearch, optionsStringified]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className="claim-grid">
|
<ul ref={sectionRef} className="claim-grid">
|
||||||
{finalUris && finalUris.length
|
{finalUris && finalUris.length
|
||||||
? finalUris.map((uri, i) => {
|
? finalUris.map((uri, i) => {
|
||||||
if (uri) {
|
if (uri) {
|
||||||
|
if (injectedIndex === i && injectedItem && injectedItem.replace) {
|
||||||
|
return <React.Fragment key={uri}>{injectedItem.node}</React.Fragment>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ClaimPreviewTile
|
<React.Fragment key={uri}>
|
||||||
showNoSourceClaims={hasNoSource || showNoSourceClaims}
|
{injectedIndex === i && injectedItem && injectedItem.node}
|
||||||
key={uri}
|
<ClaimPreviewTile
|
||||||
uri={uri}
|
showNoSourceClaims={hasNoSource || showNoSourceClaims}
|
||||||
properties={renderProperties}
|
uri={uri}
|
||||||
/>
|
properties={renderProperties}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return <ClaimPreviewTile showNoSourceClaims={hasNoSource || showNoSourceClaims} key={i} placeholder />;
|
return <ClaimPreviewTile showNoSourceClaims={hasNoSource || showNoSourceClaims} key={i} placeholder />;
|
||||||
|
|
|
@ -70,6 +70,14 @@ export default React.memo<Props>(function RecommendedContent(props: Props) {
|
||||||
const isMedium = useIsMediumScreen();
|
const isMedium = useIsMediumScreen();
|
||||||
const { onRecsLoaded: onRecommendationsLoaded, onClickedRecommended: onRecommendationClicked } = RecSys;
|
const { onRecsLoaded: onRecommendationsLoaded, onClickedRecommended: onRecommendationClicked } = RecSys;
|
||||||
|
|
||||||
|
const InjectedAd =
|
||||||
|
injectAds && !blacklistTriggered
|
||||||
|
? {
|
||||||
|
node: <Ads small type="video" className="ads__claim-item--recommended" />,
|
||||||
|
index: isMobile ? 0 : 3,
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
doFetchRecommendedContent(uri);
|
doFetchRecommendedContent(uri);
|
||||||
}, [uri, doFetchRecommendedContent]);
|
}, [uri, doFetchRecommendedContent]);
|
||||||
|
@ -133,7 +141,7 @@ export default React.memo<Props>(function RecommendedContent(props: Props) {
|
||||||
loading={isSearching}
|
loading={isSearching}
|
||||||
uris={recommendedContentUris}
|
uris={recommendedContentUris}
|
||||||
hideMenu={isMobile}
|
hideMenu={isMobile}
|
||||||
injectedItem={injectAds && !blacklistTriggered && <Ads small type={'video'} />}
|
injectedItem={InjectedAd}
|
||||||
empty={__('No related content found')}
|
empty={__('No related content found')}
|
||||||
onClick={handleRecommendationClicked}
|
onClick={handleRecommendationClicked}
|
||||||
/>
|
/>
|
||||||
|
@ -152,7 +160,7 @@ export default React.memo<Props>(function RecommendedContent(props: Props) {
|
||||||
channelIds={[signingChannel.claim_id]}
|
channelIds={[signingChannel.claim_id]}
|
||||||
loading={isSearching}
|
loading={isSearching}
|
||||||
hideMenu={isMobile}
|
hideMenu={isMobile}
|
||||||
injectedItem={SHOW_ADS && IS_WEB && !isAuthenticated && <Ads small type={'video'} />}
|
injectedItem={InjectedAd}
|
||||||
empty={__('No related content found')}
|
empty={__('No related content found')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
46
ui/effects/use-last-visible-item.js
Normal file
46
ui/effects/use-last-visible-item.js
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import type { Node } from 'react';
|
||||||
|
|
||||||
|
type InjectedItem = { node: Node, index?: number, replace?: boolean };
|
||||||
|
|
||||||
|
export default function useLastVisibleItem(injectedItem: ?InjectedItem, listRef: any) {
|
||||||
|
const [injectedIndex, setInjectedIndex] = React.useState(injectedItem?.index);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
// Move to default injection index (last visible item)
|
||||||
|
if (injectedItem && injectedItem.index === undefined) {
|
||||||
|
// AD_INJECTION_DELAY_MS = average total-blocking-time incurred for
|
||||||
|
// loading ads. Delay to let higher priority tasks run first. Ideally,
|
||||||
|
// should use 'requestIdleCallback/requestAnimationFrame'.
|
||||||
|
const AD_INJECTION_DELAY_MS = 1500;
|
||||||
|
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
if (listRef.current) {
|
||||||
|
const screenBottom = window.innerHeight;
|
||||||
|
const items = listRef.current.children;
|
||||||
|
|
||||||
|
if (items.length) {
|
||||||
|
let i = 2; // Start from 2, so that the min possible is index-1
|
||||||
|
for (; i < items.length; ++i) {
|
||||||
|
const rect = items[i].getBoundingClientRect();
|
||||||
|
if (rect.top > screenBottom || rect.bottom > screenBottom) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setInjectedIndex(i - 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to index-1 (2nd item) for failures. No retries.
|
||||||
|
setInjectedIndex(1);
|
||||||
|
}, AD_INJECTION_DELAY_MS);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return injectedIndex;
|
||||||
|
}
|
|
@ -12,7 +12,7 @@ import { useIsMobile } from 'effects/use-screensize';
|
||||||
import analytics from 'analytics';
|
import analytics from 'analytics';
|
||||||
import HiddenNsfw from 'component/common/hidden-nsfw';
|
import HiddenNsfw from 'component/common/hidden-nsfw';
|
||||||
import Icon from 'component/common/icon';
|
import Icon from 'component/common/icon';
|
||||||
import Ads, { injectAd } from 'web/component/ads';
|
import Ads from 'web/component/ads';
|
||||||
import LbcSymbol from 'component/common/lbc-symbol';
|
import LbcSymbol from 'component/common/lbc-symbol';
|
||||||
import I18nMessage from 'component/i18nMessage';
|
import I18nMessage from 'component/i18nMessage';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
@ -54,6 +54,7 @@ function DiscoverPage(props: Props) {
|
||||||
const buttonRef = useRef();
|
const buttonRef = useRef();
|
||||||
const isHovering = useHover(buttonRef);
|
const isHovering = useHover(buttonRef);
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
const isWildWest = window.location.pathname === `/$/${PAGES.WILD_WEST}`;
|
||||||
|
|
||||||
const urlParams = new URLSearchParams(search);
|
const urlParams = new URLSearchParams(search);
|
||||||
const langParam = urlParams.get(CS.LANGUAGE_KEY) || null;
|
const langParam = urlParams.get(CS.LANGUAGE_KEY) || null;
|
||||||
|
@ -175,18 +176,8 @@ function DiscoverPage(props: Props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const hasAdOnPage = document.querySelector('.homepageAdContainer');
|
|
||||||
|
|
||||||
if (hasAdOnPage || isAuthenticated || !SHOW_ADS || window.location.pathname === `/$/${PAGES.WILD_WEST}`) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
injectAd();
|
|
||||||
}, [isAuthenticated]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page noFooter fullWidthPage={tileLayout} className="main__discover">
|
<Page noFooter fullWidthPage={tileLayout} className="main__discover">
|
||||||
<Ads type="homepage" />
|
|
||||||
<ClaimListDiscover
|
<ClaimListDiscover
|
||||||
pins={getPins(dynamicRouteProps)}
|
pins={getPins(dynamicRouteProps)}
|
||||||
hideFilters={SIMPLE_SITE ? !(dynamicRouteProps || tags) : undefined}
|
hideFilters={SIMPLE_SITE ? !(dynamicRouteProps || tags) : undefined}
|
||||||
|
@ -200,7 +191,7 @@ function DiscoverPage(props: Props) {
|
||||||
hiddenNsfwMessage={<HiddenNsfw type="page" />}
|
hiddenNsfwMessage={<HiddenNsfw type="page" />}
|
||||||
repostedClaimId={repostedClaim ? repostedClaim.claim_id : null}
|
repostedClaimId={repostedClaim ? repostedClaim.claim_id : null}
|
||||||
injectedItem={
|
injectedItem={
|
||||||
SHOW_ADS && IS_WEB ? (SIMPLE_SITE ? false : !isAuthenticated && <Ads small type={'video'} />) : false
|
SHOW_ADS && !isAuthenticated && !isWildWest && { node: <Ads small type="video" tileLayout={tileLayout} /> }
|
||||||
}
|
}
|
||||||
// Assume wild west page if no dynamicRouteProps
|
// Assume wild west page if no dynamicRouteProps
|
||||||
// Not a very good solution, but just doing it for now
|
// Not a very good solution, but just doing it for now
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import * as PAGES from 'constants/pages';
|
import * as PAGES from 'constants/pages';
|
||||||
import { SITE_NAME, SIMPLE_SITE, ENABLE_NO_SOURCE_CLAIMS, SHOW_ADS } from 'config';
|
import { SHOW_ADS, SITE_NAME, SIMPLE_SITE, ENABLE_NO_SOURCE_CLAIMS } from 'config';
|
||||||
import Ads, { injectAd } from 'web/component/ads';
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import Page from 'component/page';
|
import Page from 'component/page';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
|
@ -16,6 +15,7 @@ import { getLivestreamUris } from 'util/livestream';
|
||||||
import ScheduledStreams from 'component/scheduledStreams';
|
import ScheduledStreams from 'component/scheduledStreams';
|
||||||
import { splitBySeparator } from 'util/lbryURI';
|
import { splitBySeparator } from 'util/lbryURI';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
import Ads from 'web/component/ads';
|
||||||
|
|
||||||
// @if TARGET='web'
|
// @if TARGET='web'
|
||||||
import Meme from 'web/component/meme';
|
import Meme from 'web/component/meme';
|
||||||
|
@ -99,6 +99,9 @@ function HomePage(props: Props) {
|
||||||
hasSource
|
hasSource
|
||||||
prefixUris={getLivestreamUris(activeLivestreams, options.channelIds)}
|
prefixUris={getLivestreamUris(activeLivestreams, options.channelIds)}
|
||||||
pinUrls={pinUrls}
|
pinUrls={pinUrls}
|
||||||
|
injectedItem={
|
||||||
|
index === 0 && SHOW_ADS && !authenticated && { node: <Ads small type="video" tileLayout />, replace: true }
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -139,12 +142,6 @@ function HomePage(props: Props) {
|
||||||
doFetchActiveLivestreams();
|
doFetchActiveLivestreams();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const shouldShowAds = SHOW_ADS && !authenticated;
|
|
||||||
// inject ad into last visible card
|
|
||||||
injectAd(shouldShowAds);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const [hasScheduledStreams, setHasScheduledStreams] = useState(false);
|
const [hasScheduledStreams, setHasScheduledStreams] = useState(false);
|
||||||
const scheduledStreamsLoaded = (total) => setHasScheduledStreams(total > 0);
|
const scheduledStreamsLoaded = (total) => setHasScheduledStreams(total > 0);
|
||||||
|
|
||||||
|
@ -167,7 +164,6 @@ function HomePage(props: Props) {
|
||||||
|
|
||||||
{/* @if TARGET='web' */}
|
{/* @if TARGET='web' */}
|
||||||
{SIMPLE_SITE && <Meme />}
|
{SIMPLE_SITE && <Meme />}
|
||||||
<Ads type="homepage" />
|
|
||||||
{/* @endif */}
|
{/* @endif */}
|
||||||
|
|
||||||
{!fetchingActiveLivestreams && (
|
{!fetchingActiveLivestreams && (
|
||||||
|
|
|
@ -104,7 +104,11 @@ export default function SearchPage(props: Props) {
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
injectedItem={
|
injectedItem={
|
||||||
SHOW_ADS && IS_WEB ? (SIMPLE_SITE ? false : !isAuthenticated && <Ads small type={'video'} />) : false
|
SHOW_ADS &&
|
||||||
|
!isAuthenticated && {
|
||||||
|
node: <Ads small type="video" />,
|
||||||
|
index: 3,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -16,43 +16,145 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inline Video Ads
|
// Inline Video Ads
|
||||||
|
// The default is coded for list-layout;
|
||||||
|
// --tile and other modifiers adjust accordingly.
|
||||||
.ads__claim-item {
|
.ads__claim-item {
|
||||||
border-bottom: 1px solid var(--color-border);
|
border-bottom: 1px solid var(--color-border);
|
||||||
margin-top: var(--spacing-m);
|
margin-top: var(--spacing-m);
|
||||||
|
margin-bottom: var(--spacing-m);
|
||||||
padding: var(--spacing-m);
|
padding: var(--spacing-m);
|
||||||
background-color: var(--color-ads-background);
|
background-color: var(--color-ads-background);
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: wrap;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
> div,
|
|
||||||
ins {
|
|
||||||
width: 100%;
|
|
||||||
position: relative !important;
|
|
||||||
max-width: 30rem;
|
|
||||||
min-width: 15rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ad__container {
|
.ad__container {
|
||||||
aspect-ratio: 16 / 9;
|
aspect-ratio: 16 / 9;
|
||||||
}
|
$minWidth: calc(var(--file-list-thumbnail-width) * 0.8);
|
||||||
|
min-width: $minWidth;
|
||||||
|
|
||||||
.avp-p-gui {
|
video {
|
||||||
z-index: 1 !important;
|
width: 100% !important;
|
||||||
}
|
height: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: $breakpoint-small) {
|
@media (max-width: $breakpoint-small) {
|
||||||
flex-direction: column;
|
$width: calc(var(--file-list-thumbnail-width) * 0.8);
|
||||||
|
width: $width;
|
||||||
|
|
||||||
> div {
|
#aniBox,
|
||||||
width: 100%;
|
#av-container {
|
||||||
|
// Only needed on actual mobile. Their mobile script does something
|
||||||
|
// different that makes it 100%, so have to counter that here.
|
||||||
|
width: unset !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: $breakpoint-small) and (max-width: $breakpoint-large) {
|
||||||
|
$width: calc(var(--file-list-thumbnail-width) * 1.2);
|
||||||
|
width: $width;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: $breakpoint-large) {
|
||||||
|
$width: calc(var(--file-list-thumbnail-width) * 1.2);
|
||||||
|
width: $width;
|
||||||
|
}
|
||||||
|
|
||||||
|
//div[style*='position: fixed; transform: scale(1);'] {
|
||||||
|
// // This is the floating ad (when tile goes off screen).
|
||||||
|
// // The sidebar is covering it, so move to the right a bit:
|
||||||
|
// left: unset !important;
|
||||||
|
//
|
||||||
|
// // It's a bit jarring at times on a busy page, so add border:
|
||||||
|
// background-color: var(--color-ads-background);
|
||||||
|
// border-radius: var(--border-radius);
|
||||||
|
// padding: var(--spacing-s);
|
||||||
|
//}
|
||||||
|
|
||||||
|
div[style*='position: fixed; transform: scale(1);'] {
|
||||||
|
// [Floating ad]
|
||||||
|
// Hide it in entirely. Couldn't reconcile with the changes needed to make
|
||||||
|
// tile layout fit on browser-resize and also with their mobile script
|
||||||
|
// changes. Else, the block above can be used.
|
||||||
|
transform: none !important;
|
||||||
|
transform-origin: unset !important;
|
||||||
|
position: unset !important;
|
||||||
|
|
||||||
|
div:first-child {
|
||||||
|
// [Floating ad close button]
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#aniBox,
|
||||||
|
#av-container {
|
||||||
|
// Handle the scenario of ads not resizing when switching from small to
|
||||||
|
// medium/large layout:
|
||||||
|
height: unset !important;
|
||||||
|
aspect-ratio: 16 / 9 !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ads__claim-item--tile {
|
||||||
|
@extend .card;
|
||||||
|
@extend .claim-preview--tile;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: var(--spacing-s);
|
||||||
|
|
||||||
|
.ads__claim-text {
|
||||||
|
margin: var(--spacing-s) 0 0 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ad__container {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100 !important;
|
||||||
|
min-width: unset !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
|
||||||
|
div:not(#sound):not(.off):not(#timeline) {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: unset !important;
|
||||||
|
min-width: unset !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
video {
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#timeline,
|
||||||
|
#buttons {
|
||||||
|
width: calc(100% - (var(--spacing-s) * 2)) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
width: calc(100% - (var(--spacing-m) * 3)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ads__claim-text {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ads__claim-item--recommended {
|
||||||
|
padding: var(--spacing-s);
|
||||||
|
|
||||||
|
@media (min-width: $breakpoint-medium) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.ads__claim-text {
|
.ads__claim-text {
|
||||||
margin-top: var(--spacing-m);
|
overflow: hidden;
|
||||||
|
max-width: 50%;
|
||||||
|
margin: var(--spacing-m) 0 var(--spacing-m) var(--spacing-s);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -60,6 +162,10 @@
|
||||||
|
|
||||||
.ads__claim-text--small {
|
.ads__claim-text--small {
|
||||||
font-size: var(--font-small);
|
font-size: var(--font-small);
|
||||||
|
|
||||||
|
@media (max-width: $breakpoint-small) {
|
||||||
|
font-size: var(--font-xsmall);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pre-roll ads
|
// Pre-roll ads
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { connect } from 'react-redux';
|
||||||
import { selectTheme } from 'redux/selectors/settings';
|
import { selectTheme } from 'redux/selectors/settings';
|
||||||
import { makeSelectClaimForUri, selectClaimIsNsfwForUri } from 'redux/selectors/claims';
|
import { makeSelectClaimForUri, selectClaimIsNsfwForUri } from 'redux/selectors/claims';
|
||||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||||
import Ads, { injectAd } from './view';
|
import Ads from './view';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => ({
|
||||||
theme: selectTheme(state),
|
theme: selectTheme(state),
|
||||||
|
@ -12,4 +12,3 @@ const select = (state, props) => ({
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select)(Ads);
|
export default connect(select)(Ads);
|
||||||
export { injectAd };
|
|
||||||
|
|
|
@ -28,9 +28,7 @@ const IS_IOS =
|
||||||
// for iOS 13+ , platform is MacIntel, so use this to test
|
// for iOS 13+ , platform is MacIntel, so use this to test
|
||||||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)) &&
|
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)) &&
|
||||||
!window.MSStream;
|
!window.MSStream;
|
||||||
|
|
||||||
const IS_ANDROID = /Android/i.test(navigator.userAgent);
|
const IS_ANDROID = /Android/i.test(navigator.userAgent);
|
||||||
|
|
||||||
const IS_FIREFOX = /Firefox/i.test(navigator.userAgent);
|
const IS_FIREFOX = /Firefox/i.test(navigator.userAgent);
|
||||||
|
|
||||||
const isFirefoxAndroid = IS_ANDROID && IS_FIREFOX;
|
const isFirefoxAndroid = IS_ANDROID && IS_FIREFOX;
|
||||||
|
@ -38,10 +36,12 @@ const isFirefoxAndroid = IS_ANDROID && IS_FIREFOX;
|
||||||
type Props = {
|
type Props = {
|
||||||
location: { pathname: string },
|
location: { pathname: string },
|
||||||
type: string,
|
type: string,
|
||||||
|
tileLayout?: boolean,
|
||||||
small: boolean,
|
small: boolean,
|
||||||
claim: Claim,
|
claim: Claim,
|
||||||
isMature: boolean,
|
isMature: boolean,
|
||||||
authenticated: boolean,
|
authenticated: boolean,
|
||||||
|
className?: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
function removeIfExists(querySelector) {
|
function removeIfExists(querySelector) {
|
||||||
|
@ -53,11 +53,13 @@ function Ads(props: Props) {
|
||||||
const {
|
const {
|
||||||
location: { pathname },
|
location: { pathname },
|
||||||
type = 'video',
|
type = 'video',
|
||||||
|
tileLayout,
|
||||||
small,
|
small,
|
||||||
authenticated,
|
authenticated,
|
||||||
|
className,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const shouldShowAds = SHOW_ADS && !authenticated;
|
const shouldShowAds = SHOW_ADS && !authenticated && !isFirefoxAndroid;
|
||||||
const mobileAds = IS_ANDROID || IS_IOS;
|
const mobileAds = IS_ANDROID || IS_IOS;
|
||||||
|
|
||||||
// this is populated from app based on location
|
// this is populated from app based on location
|
||||||
|
@ -66,8 +68,6 @@ function Ads(props: Props) {
|
||||||
|
|
||||||
// add script to DOM
|
// add script to DOM
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isFirefoxAndroid) return;
|
|
||||||
|
|
||||||
if (shouldShowAds) {
|
if (shouldShowAds) {
|
||||||
let script;
|
let script;
|
||||||
try {
|
try {
|
||||||
|
@ -97,7 +97,6 @@ function Ads(props: Props) {
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// display to say "sign up to not see these"
|
|
||||||
const adsSignInDriver = (
|
const adsSignInDriver = (
|
||||||
<I18nMessage
|
<I18nMessage
|
||||||
tokens={{
|
tokens={{
|
||||||
|
@ -114,167 +113,29 @@ function Ads(props: Props) {
|
||||||
</I18nMessage>
|
</I18nMessage>
|
||||||
);
|
);
|
||||||
|
|
||||||
// ad shown in the related videos area
|
if (shouldShowAds && type === 'video') {
|
||||||
const videoAd = (
|
return (
|
||||||
<div className="ads__claim-item">
|
|
||||||
<div className="ad__container">
|
|
||||||
<div id={adConfig.tag} style={{ display: 'none' }} />
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
className={classnames('ads__claim-text', {
|
className={classnames('ads ads__claim-item', className, {
|
||||||
'ads__claim-text--small': small,
|
'ads__claim-item--tile': tileLayout,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div>Ad</div>
|
<div className="ad__container">
|
||||||
<p>{adsSignInDriver}</p>
|
<div id={adConfig.tag} />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={classnames('ads__claim-text', {
|
||||||
|
'ads__claim-text--small': small,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div>Ad</div>
|
||||||
|
<p>{adsSignInDriver}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
|
||||||
|
|
||||||
// homepage ad in a card
|
|
||||||
const homepageCardAd = (
|
|
||||||
<div className="homepageAdContainer media__thumb" style={{ display: 'none' }}>
|
|
||||||
<div id={adConfig.tag} className="homepageAdDiv media__thumb" style={{ display: 'none' }} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!SHOW_ADS) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
// disable ads for firefox android because they don't work properly
|
|
||||||
if (isFirefoxAndroid) return false;
|
|
||||||
|
|
||||||
// sidebar ad (in recommended videos)
|
return null;
|
||||||
if (type === 'video') {
|
|
||||||
return videoAd;
|
|
||||||
}
|
|
||||||
if (type === 'homepage') {
|
|
||||||
return homepageCardAd;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// returns true if passed element is fully visible on screen
|
|
||||||
function isScrolledIntoView(el) {
|
|
||||||
const rect = el.getBoundingClientRect();
|
|
||||||
const elemTop = rect.top;
|
|
||||||
const elemBottom = rect.bottom;
|
|
||||||
|
|
||||||
// Only completely visible elements return true:
|
|
||||||
const isVisible = elemTop >= 0 && elemBottom <= window.innerHeight;
|
|
||||||
return isVisible;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function injectAd(shouldShowAds: boolean) {
|
|
||||||
// don't inject on firefox android or for authenticated users or no ads on instance
|
|
||||||
if (isFirefoxAndroid || !shouldShowAds) return;
|
|
||||||
// test if adblock is enabled
|
|
||||||
let adBlockEnabled = false;
|
|
||||||
const googleAdUrl = 'https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js';
|
|
||||||
try {
|
|
||||||
await fetch(new Request(googleAdUrl)).catch((_) => {
|
|
||||||
adBlockEnabled = true;
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
adBlockEnabled = true;
|
|
||||||
} finally {
|
|
||||||
if (!adBlockEnabled) {
|
|
||||||
// select the cards on page
|
|
||||||
let cards = document.getElementsByClassName('card claim-preview--tile');
|
|
||||||
// eslint-disable-next-line no-inner-declarations
|
|
||||||
function checkFlag() {
|
|
||||||
if (cards.length === 0) {
|
|
||||||
window.setTimeout(checkFlag, 100);
|
|
||||||
} else {
|
|
||||||
// find the last fully visible card
|
|
||||||
let lastCard;
|
|
||||||
|
|
||||||
// width of browser window
|
|
||||||
const windowWidth = window.innerWidth;
|
|
||||||
|
|
||||||
// on small screens, grab the second item
|
|
||||||
if (windowWidth <= 900) {
|
|
||||||
lastCard = cards[1];
|
|
||||||
} else {
|
|
||||||
// otherwise, get the last fully visible card
|
|
||||||
for (const card of cards) {
|
|
||||||
const isFullyVisible = isScrolledIntoView(card);
|
|
||||||
if (!isFullyVisible) break;
|
|
||||||
lastCard = card;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if no last card was found, just exit the function to not cause errors
|
|
||||||
if (!lastCard) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// clone the last card
|
|
||||||
// $FlowFixMe
|
|
||||||
const clonedCard = lastCard.cloneNode(true);
|
|
||||||
|
|
||||||
// insert cloned card
|
|
||||||
// $FlowFixMe
|
|
||||||
lastCard.parentNode.insertBefore(clonedCard, lastCard);
|
|
||||||
|
|
||||||
// change the appearance of the cloned card
|
|
||||||
// $FlowFixMe
|
|
||||||
clonedCard.querySelector('.claim__menu-button').remove();
|
|
||||||
|
|
||||||
// $FlowFixMe
|
|
||||||
clonedCard.querySelector('.truncated-text').innerHTML = __(
|
|
||||||
'Hate these? Login to Odysee for an ad free experience'
|
|
||||||
);
|
|
||||||
|
|
||||||
// $FlowFixMe
|
|
||||||
clonedCard.querySelector('.claim-tile__info').remove();
|
|
||||||
|
|
||||||
// $FlowFixMe
|
|
||||||
clonedCard.querySelector('[role="none"]').removeAttribute('href');
|
|
||||||
|
|
||||||
// $FlowFixMe
|
|
||||||
clonedCard.querySelector('.claim-tile__header').firstChild.href = '/$/signin';
|
|
||||||
|
|
||||||
// $FlowFixMe
|
|
||||||
clonedCard.querySelector('.claim-tile__title').firstChild.removeAttribute('aria-label');
|
|
||||||
|
|
||||||
// $FlowFixMe
|
|
||||||
clonedCard.querySelector('.claim-tile__title').firstChild.removeAttribute('title');
|
|
||||||
|
|
||||||
// $FlowFixMe
|
|
||||||
clonedCard.querySelector('.claim-tile__header').firstChild.removeAttribute('aria-label');
|
|
||||||
|
|
||||||
// $FlowFixMe
|
|
||||||
clonedCard
|
|
||||||
.querySelector('.media__thumb')
|
|
||||||
.replaceWith(document.getElementsByClassName('homepageAdContainer')[0]);
|
|
||||||
|
|
||||||
// show the homepage ad which is not displayed at first
|
|
||||||
document.getElementsByClassName('homepageAdContainer')[0].style.display = 'block';
|
|
||||||
|
|
||||||
const thumbnail = window.getComputedStyle(lastCard.querySelector('.media__thumb'));
|
|
||||||
|
|
||||||
const styles = `#av-container, #AVcontent, #aniBox {
|
|
||||||
height: ${thumbnail.height} !important;
|
|
||||||
width: ${thumbnail.width} !important;
|
|
||||||
}`;
|
|
||||||
|
|
||||||
const styleSheet = document.createElement('style');
|
|
||||||
styleSheet.type = 'text/css';
|
|
||||||
styleSheet.id = 'customAniviewStyling';
|
|
||||||
styleSheet.innerText = styles;
|
|
||||||
|
|
||||||
// $FlowFixMe
|
|
||||||
document.head.appendChild(styleSheet);
|
|
||||||
|
|
||||||
// delete last card to not introduce layout shifts
|
|
||||||
lastCard.remove();
|
|
||||||
|
|
||||||
// addresses bug where ad doesn't show up until a scroll event
|
|
||||||
document.dispatchEvent(new CustomEvent('scroll'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
checkFlag();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withRouter(Ads);
|
export default withRouter(Ads);
|
||||||
export { injectAd };
|
|
||||||
|
|
Loading…
Reference in a new issue