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:
infinite-persistence 2022-03-07 20:11:28 +08:00 committed by Thomas Zarebczan
parent e2f73a30c6
commit 0143b63c74
12 changed files with 270 additions and 232 deletions

View file

@ -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">

View file

@ -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>
)) ))

View file

@ -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,

View file

@ -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 />;

View file

@ -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')}
/> />
)} )}

View 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;
}

View file

@ -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

View file

@ -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 && (

View file

@ -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,
}
} }
/> />

View file

@ -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

View file

@ -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 };

View file

@ -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 };