Tile Grid Revamp (#1502)

* Save

* Save

* Add pulse

* Adjust footer ad

* Adjust tile ad

* Adjust tile ad hover

* Fix premium badge alignment in tile grid

* Adjust livestream icon

* Adjust livestream icon

* Save scheduled livestreasm & tile ad

* Fix scheduled callback

* Fix playlist icon size on file page

* Fix grid distortion in 3 & 4 column layout

* -

* Fix grid on category & channel page

* Fix Premium Plus Grid

* Add custom tile for adblockers

* Reset env

* Remove collapsed tiles

* Remove setLoaded on scheduled livestreams page

* -

* Make isHidden optional

* Remove px

* Review adjustments

* Inject Premium+ ads

* Fix injection

* Fix injection when using the last tile

* Fix injection when using the last tile

* Enable stripe dev

* Create PremiumPlusTile component and add list view design

* Create PremiumPlusTile component and add list view design

* Adjust ads in list view

* Remove setState from render loop

* Clean code

* Fix livestream margin in list view

* Rewrite & tune some logic - Homepage & Channel page

* Clean details...

* Clean details...

* Requested review changes

Signed-off-by: Raphael Wickihalder <raphael.wickihalder@odysee.com>

* Requested review changes

Signed-off-by: Raphael Wickihalder <raphael.wickihalder@odysee.com>
This commit is contained in:
Rave | 図書館猫 2022-05-18 13:16:35 +02:00 committed by GitHub
parent 96e704e5d9
commit ffdb5abf63
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 439 additions and 58 deletions

1
flow-typed/gui.js vendored
View file

@ -2,5 +2,4 @@
declare type ListInjectedItem = { declare type ListInjectedItem = {
node: Node | (index: number, lastVisibleIndex: ?number, pageSize: ?number) => Node, node: Node | (index: number, lastVisibleIndex: ?number, pageSize: ?number) => Node,
index?: number, index?: number,
replace?: boolean
}; };

View file

@ -7,11 +7,13 @@ import {
makeSelectTotalPagesInChannelSearch, makeSelectTotalPagesInChannelSearch,
selectClaimForUri, selectClaimForUri,
} from 'redux/selectors/claims'; } from 'redux/selectors/claims';
import { selectOdyseeMembershipIsPremiumPlus } from 'redux/selectors/user';
import { doResolveUris } from 'redux/actions/claims'; import { doResolveUris } from 'redux/actions/claims';
import * as SETTINGS from 'constants/settings'; import * as SETTINGS from 'constants/settings';
import { makeSelectChannelIsMuted } from 'redux/selectors/blocked'; import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
import { withRouter } from 'react-router'; import { withRouter } from 'react-router';
import { selectClientSetting, selectShowMatureContent } from 'redux/selectors/settings'; import { selectClientSetting, selectShowMatureContent } from 'redux/selectors/settings';
import { selectAdBlockerFound } from 'redux/selectors/app';
import { doFetchChannelLiveStatus } from 'redux/actions/livestream'; import { doFetchChannelLiveStatus } from 'redux/actions/livestream';
import { selectActiveLivestreamForChannel, selectActiveLivestreamInitialized } from 'redux/selectors/livestream'; import { selectActiveLivestreamForChannel, selectActiveLivestreamInitialized } from 'redux/selectors/livestream';
import { getChannelIdFromClaim } from 'util/claim'; import { getChannelIdFromClaim } from 'util/claim';
@ -35,6 +37,8 @@ const select = (state, props) => {
tileLayout: selectClientSetting(state, SETTINGS.TILE_LAYOUT), tileLayout: selectClientSetting(state, SETTINGS.TILE_LAYOUT),
activeLivestreamForChannel: selectActiveLivestreamForChannel(state, channelClaimId), activeLivestreamForChannel: selectActiveLivestreamForChannel(state, channelClaimId),
activeLivestreamInitialized: selectActiveLivestreamInitialized(state), activeLivestreamInitialized: selectActiveLivestreamInitialized(state),
adBlockerFound: selectAdBlockerFound(state),
hasPremiumPlus: selectOdyseeMembershipIsPremiumPlus(state),
}; };
}; };

View file

@ -15,6 +15,7 @@ import ScheduledStreams from 'component/scheduledStreams';
import { SearchResults } from './internal/searchResults'; import { SearchResults } from './internal/searchResults';
import useFetchLiveStatus from 'effects/use-fetch-live'; import useFetchLiveStatus from 'effects/use-fetch-live';
import { useIsLargeScreen } from 'effects/use-screensize'; import { useIsLargeScreen } from 'effects/use-screensize';
import PremiumPlusTile from 'component/premiumPlusTile';
const TYPES_TO_ALLOW_FILTER = ['stream', 'repost']; const TYPES_TO_ALLOW_FILTER = ['stream', 'repost'];
@ -41,6 +42,8 @@ type Props = {
doFetchChannelLiveStatus: (string) => void, doFetchChannelLiveStatus: (string) => void,
activeLivestreamForChannel: any, activeLivestreamForChannel: any,
activeLivestreamInitialized: boolean, activeLivestreamInitialized: boolean,
adBlockerFound: ?boolean,
hasPremiumPlus: ?boolean,
}; };
function ChannelContent(props: Props) { function ChannelContent(props: Props) {
@ -62,6 +65,8 @@ function ChannelContent(props: Props) {
doFetchChannelLiveStatus, doFetchChannelLiveStatus,
activeLivestreamForChannel, activeLivestreamForChannel,
activeLivestreamInitialized, activeLivestreamInitialized,
adBlockerFound,
hasPremiumPlus,
} = props; } = props;
// const claimsInChannel = (claim && claim.meta.claims_in_channel) || 0; // const claimsInChannel = (claim && claim.meta.claims_in_channel) || 0;
@ -159,7 +164,15 @@ function ChannelContent(props: Props) {
defaultOrderBy={CS.ORDER_BY_NEW} defaultOrderBy={CS.ORDER_BY_NEW}
pageSize={dynamicPageSize} pageSize={dynamicPageSize}
infiniteScroll={defaultInfiniteScroll} infiniteScroll={defaultInfiniteScroll}
injectedItem={{ node: <Ads type="video" tileLayout={tileLayout} small /> }} injectedItem={
!hasPremiumPlus && {
node: adBlockerFound ? (
<PremiumPlusTile tileLayout={tileLayout} />
) : (
<Ads small type="video" tileLayout />
),
}
}
meta={ meta={
showFilters && ( showFilters && (
<Form onSubmit={() => {}} className="wunderbar--inline"> <Form onSubmit={() => {}} className="wunderbar--inline">

View file

@ -27,7 +27,7 @@ export default function CollectionAddButton(props: Props) {
button={!fileAction ? 'alt' : undefined} button={!fileAction ? 'alt' : undefined}
className={classnames({ 'button--file-action': fileAction })} className={classnames({ 'button--file-action': fileAction })}
icon={fileAction ? (!isSaved ? ICONS.ADD : ICONS.STACK) : ICONS.LIBRARY} icon={fileAction ? (!isSaved ? ICONS.ADD : ICONS.STACK) : ICONS.LIBRARY}
iconSize={fileAction ? 22 : undefined} iconSize={fileAction ? 16 : undefined}
label={uri ? (!isSaved ? __('Save') : __('Saved')) : __('New List')} label={uri ? (!isSaved ? __('Save') : __('Saved')) : __('New List')}
requiresAuth requiresAuth
onClick={(e) => { onClick={(e) => {

View file

@ -1,7 +1,7 @@
// @flow // @flow
import { MAIN_CLASS } from 'constants/classnames'; import { MAIN_CLASS } from 'constants/classnames';
import type { Node } from 'react'; import type { Node } from 'react';
import React, { useEffect } from 'react'; import React, { useEffect, useState } from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import ClaimPreview from 'component/claimPreview'; import ClaimPreview from 'component/claimPreview';
import Spinner from 'component/spinner'; import Spinner from 'component/spinner';
@ -61,6 +61,7 @@ type Props = {
unavailableUris?: Array<string>, unavailableUris?: Array<string>,
showMemberBadge?: boolean, showMemberBadge?: boolean,
inWatchHistory?: boolean, inWatchHistory?: boolean,
onHidden: string,
}; };
export default function ClaimList(props: Props) { export default function ClaimList(props: Props) {
@ -102,9 +103,11 @@ export default function ClaimList(props: Props) {
unavailableUris, unavailableUris,
showMemberBadge, showMemberBadge,
inWatchHistory, inWatchHistory,
onHidden,
} = props; } = props;
const [currentSort, setCurrentSort] = usePersistedState(persistedStorageKey, SORT_NEW); const [currentSort, setCurrentSort] = usePersistedState(persistedStorageKey, SORT_NEW);
const [uriBuffer, setUriBuffer] = useState([]);
// Resolve the index for injectedItem, if provided; else injectedIndex will be 'undefined'. // Resolve the index for injectedItem, if provided; else injectedIndex will be 'undefined'.
const listRef = React.useRef(); const listRef = React.useRef();
@ -154,12 +157,6 @@ 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) {
@ -208,6 +205,19 @@ export default function ClaimList(props: Props) {
/> />
); );
React.useEffect(() => {
tileUris.forEach((uri, index) => {
if (uri) {
const inj = getInjectedItem(index);
if (inj) {
if (uriBuffer.indexOf(index) === -1) {
setUriBuffer([index]);
}
}
}
});
}, [tileUris, injectedItem, lastVisibleIndex, pageSize]);
const getInjectedItem = (index) => { const getInjectedItem = (index) => {
if (injectedItem && injectedItem.node) { if (injectedItem && injectedItem.node) {
if (typeof injectedItem.node === 'function') { if (typeof injectedItem.node === 'function') {
@ -228,21 +238,30 @@ export default function ClaimList(props: Props) {
<> <>
<section ref={listRef} className={classnames('claim-grid', { 'swipe-list': swipeLayout })}> <section ref={listRef} className={classnames('claim-grid', { 'swipe-list': swipeLayout })}>
{urisLength > 0 && {urisLength > 0 &&
tileUris.map((uri, index) => ( tileUris.map((uri, index) => {
<React.Fragment key={uri}> if (uri) {
{getInjectedItem(index)} const inj = getInjectedItem(index);
<ClaimPreviewTile return (
uri={uri} <React.Fragment key={uri}>
showHiddenByUser={showHiddenByUser} {inj && inj}
showUnresolvedClaims={showUnresolvedClaims} {(index < tileUris.length - uriBuffer.length ||
properties={renderProperties} (pageSize && index < pageSize - uriBuffer.length)) && (
collectionId={collectionId} <ClaimPreviewTile
fypId={fypId} uri={uri}
showNoSourceClaims={showNoSourceClaims} showHiddenByUser={showHiddenByUser}
swipeLayout={swipeLayout} showUnresolvedClaims={showUnresolvedClaims}
/> properties={renderProperties}
</React.Fragment> collectionId={collectionId}
))} fypId={fypId}
showNoSourceClaims={showNoSourceClaims}
swipeLayout={swipeLayout}
onHidden={onHidden}
/>
)}
</React.Fragment>
);
}
})}
{!timedOut && urisLength === 0 && !loading && !noEmpty && ( {!timedOut && urisLength === 0 && !loading && !noEmpty && (
<div className="empty main--empty">{empty || noResultMsg}</div> <div className="empty main--empty">{empty || noResultMsg}</div>
)} )}

View file

@ -732,7 +732,7 @@ function ClaimListDiscover(props: Props) {
{loading && useSkeletonScreen && ( {loading && useSkeletonScreen && (
<div className="claim-grid"> <div className="claim-grid">
{new Array(dynamicPageSize).fill(1).map((x, i) => ( {new Array(dynamicPageSize).fill(1).map((x, i) => (
<ClaimPreviewTile key={i} placeholder="loading" /> <ClaimPreviewTile key={i} placeholder="loading" pulse />
))} ))}
</div> </div>
)} )}

View file

@ -55,6 +55,8 @@ type Props = {
isLivestreamActive: boolean, isLivestreamActive: boolean,
livestreamViewerCount: ?number, livestreamViewerCount: ?number,
swipeLayout: boolean, swipeLayout: boolean,
onHidden?: (string) => void,
pulse?: boolean,
}; };
// preview image cards used in related video functionality, channel overview page and homepage // preview image cards used in related video functionality, channel overview page and homepage
@ -86,6 +88,8 @@ function ClaimPreviewTile(props: Props) {
mediaDuration, mediaDuration,
viewCount, viewCount,
swipeLayout = false, swipeLayout = false,
onHidden,
pulse,
} = props; } = props;
const isRepost = claim && claim.repost_channel_url; const isRepost = claim && claim.repost_channel_url;
const isCollection = claim && claim.value_type === 'collection'; const isCollection = claim && claim.value_type === 'collection';
@ -158,6 +162,7 @@ function ClaimPreviewTile(props: Props) {
banState.filtered || banState.filtered ||
(!showHiddenByUser && (banState.muted || banState.blocked)) || (!showHiddenByUser && (banState.muted || banState.blocked)) ||
(isAbandoned && !showUnresolvedClaims)); (isAbandoned && !showUnresolvedClaims));
if (onHidden && shouldHide) onHidden(props.uri);
} }
if (shouldHide || (isLivestream && !showNoSourceClaims)) { if (shouldHide || (isLivestream && !showNoSourceClaims)) {
@ -173,6 +178,7 @@ function ClaimPreviewTile(props: Props) {
<li <li
className={classnames('placeholder claim-preview--tile', { className={classnames('placeholder claim-preview--tile', {
'swipe-list__item claim-preview--horizontal-tile': swipeLayout, 'swipe-list__item claim-preview--horizontal-tile': swipeLayout,
pulse: pulse,
})} })}
> >
<div className="media__thumb"> <div className="media__thumb">

View file

@ -1,6 +1,6 @@
// @flow // @flow
import type { Node } from 'react'; import type { Node } from 'react';
import React from 'react'; import React, { useState } from 'react';
import Button from 'component/button'; import Button from 'component/button';
import ClaimPreviewTile from 'component/claimPreviewTile'; import ClaimPreviewTile from 'component/claimPreviewTile';
import I18nMessage from 'component/i18nMessage'; import I18nMessage from 'component/i18nMessage';
@ -52,6 +52,7 @@ type Props = {
hasSource?: boolean, hasSource?: boolean,
hasNoSource?: boolean, hasNoSource?: boolean,
forceShowReposts?: boolean, // overrides SETTINGS.HIDE_REPOSTS forceShowReposts?: boolean, // overrides SETTINGS.HIDE_REPOSTS
loading: boolean,
// --- select --- // --- select ---
location: { search: string }, location: { search: string },
claimSearchResults: Array<string>, claimSearchResults: Array<string>,
@ -90,6 +91,7 @@ function ClaimTilesDiscover(props: Props) {
doFetchUserMemberships, doFetchUserMemberships,
doResolveClaimIds, doResolveClaimIds,
doResolveUris, doResolveUris,
loading,
} = props; } = props;
const listRef = React.useRef(); const listRef = React.useRef();
@ -100,6 +102,7 @@ function ClaimTilesDiscover(props: Props) {
const claimSearchUris = claimSearchResults || []; const claimSearchUris = claimSearchResults || [];
const isUnfetchedClaimSearch = claimSearchResults === undefined; const isUnfetchedClaimSearch = claimSearchResults === undefined;
const resolvedPinUris = useResolvePins({ pins, claimsById, doResolveClaimIds, doResolveUris }); const resolvedPinUris = useResolvePins({ pins, claimsById, doResolveClaimIds, doResolveUris });
const [uriBuffer, setUriBuffer] = useState([]);
const timedOut = claimSearchResults === null; const timedOut = claimSearchResults === null;
const shouldPerformSearch = !fetchingClaimSearch && !timedOut && claimSearchUris.length === 0; const shouldPerformSearch = !fetchingClaimSearch && !timedOut && claimSearchUris.length === 0;
@ -172,6 +175,19 @@ function ClaimTilesDiscover(props: Props) {
} }
}, [doClaimSearch, shouldPerformSearch, optionsStringified]); }, [doClaimSearch, shouldPerformSearch, optionsStringified]);
React.useEffect(() => {
finalUris.forEach((uri, index) => {
if (uri) {
const inj = getInjectedItem(index);
if (inj) {
if (uriBuffer.indexOf(index) === -1) {
setUriBuffer([index]);
}
}
}
});
}, [finalUris, injectedItem, lastVisibleIndex, pageSize]);
// -------------------------------------------------------------------------- // --------------------------------------------------------------------------
// -------------------------------------------------------------------------- // --------------------------------------------------------------------------
@ -200,14 +216,14 @@ function ClaimTilesDiscover(props: Props) {
return ( return (
<ul ref={listRef} className="claim-grid"> <ul ref={listRef} className="claim-grid">
{finalUris && finalUris.length {!loading && finalUris && finalUris.length
? finalUris.map((uri, i) => { ? finalUris.map((uri, i) => {
if (uri) { if (uri) {
const inj = getInjectedItem(i); const inj = getInjectedItem(i);
return ( return (
<React.Fragment key={uri}> <React.Fragment key={uri}>
{inj && inj} {inj && inj}
{(!inj || !injectedItem || !injectedItem.replace) && ( {(i < finalUris.length - uriBuffer.length || i < pageSize - uriBuffer.length) && (
<ClaimPreviewTile <ClaimPreviewTile
showNoSourceClaims={hasNoSource || showNoSourceClaims} showNoSourceClaims={hasNoSource || showNoSourceClaims}
uri={uri} uri={uri}
@ -217,13 +233,15 @@ function ClaimTilesDiscover(props: Props) {
</React.Fragment> </React.Fragment>
); );
} else { } else {
return <ClaimPreviewTile showNoSourceClaims={hasNoSource || showNoSourceClaims} key={i} placeholder />; return (
<ClaimPreviewTile showNoSourceClaims={hasNoSource || showNoSourceClaims} key={i} placeholder pulse />
);
} }
}) })
: new Array(pageSize) : new Array(pageSize)
.fill(1) .fill(1)
.map((x, i) => ( .map((x, i) => (
<ClaimPreviewTile showNoSourceClaims={hasNoSource || showNoSourceClaims} key={i} placeholder /> <ClaimPreviewTile showNoSourceClaims={hasNoSource || showNoSourceClaims} key={i} placeholder pulse />
))} ))}
</ul> </ul>
); );
@ -236,6 +254,7 @@ export default React.memo<Props>(ClaimTilesDiscover, areEqual);
function trace(key, value) { function trace(key, value) {
// @if process.env.DEBUG_TILE_RENDER // @if process.env.DEBUG_TILE_RENDER
// $FlowFixMe "cannot coerce certain types". // $FlowFixMe "cannot coerce certain types".
console.log(`[claimTilesDiscover] ${key}: ${value}`); // eslint-disable-line no-console console.log(`[claimTilesDiscover] ${key}: ${value}`); // eslint-disable-line no-console
// @endif // @endif

View file

@ -0,0 +1,6 @@
import { connect } from 'react-redux';
import PremiumPlusTile from './view';
const select = (state) => ({});
export default connect(select, {})(PremiumPlusTile);

View file

@ -0,0 +1,68 @@
// @flow
import React from 'react';
import * as ICONS from 'constants/icons';
import * as PAGES from 'constants/pages';
import Icon from 'component/common/icon';
type Props = {
tileLayout?: boolean,
};
const PremiumPlusTile = (props: Props) => {
const { tileLayout } = props;
const title = __('No ads and access to exclusive features!');
const channel = __('Get Odysee Premium+');
const time = __('Now');
return tileLayout ? (
<li className="card claim-preview--tile claim-preview--premium-plus">
<a href={`/$/${PAGES.ODYSEE_MEMBERSHIP}`}>
<div className="media__thumb" />
<div className="claim-tile__header">
<h2 className="claim-tile__title">{title}</h2>
</div>
<div>
<div className="claim-tile__info">
<Icon icon={ICONS.UPGRADE} />
<div className="claim-tile__about">
<div className="channel-name">{channel}</div>
<div className="claim-tile__about--counts">
<span className="date_time">{time}</span>
</div>
</div>
</div>
</div>
</a>
</li>
) : (
<li className="claim-preview__wrapper claim-preview--premium-plus">
<a href={`/$/${PAGES.ODYSEE_MEMBERSHIP}`}>
<div className="claim-preview">
<div className="media__thumb" />
<div className="claim-preview__text">
<div className="claim-preview-metadata">
<div className="claim-preview-info">
<div className="claim-preview__title">{title}</div>
</div>
<div className="claim-tile__info">
<div className="claim-preview__channel-staked">
<Icon icon={ICONS.UPGRADE} />
</div>
<div className="media__subtitle">
<div className="button__content">
<span className="channel-name">{channel}</span>
<br />
</div>
<span sclassName="view_count">{time}</span>
</div>
</div>
</div>
</div>
</div>
</a>
</li>
);
};
export default PremiumPlusTile;

View file

@ -1,5 +1,5 @@
// @flow // @flow
import React from 'react'; import React, { useState } from 'react';
import Button from 'component/button'; import Button from 'component/button';
import ClaimList from 'component/claimList'; import ClaimList from 'component/claimList';
import ClaimPreviewTile from 'component/claimPreviewTile'; import ClaimPreviewTile from 'component/claimPreviewTile';
@ -45,6 +45,22 @@ export default function RecommendedPersonal(props: Props) {
const count = personalRecommendations.uris.length; const count = personalRecommendations.uris.length;
const countCollapsed = getSuitablePageSizeForScreen(12, isLargeScreen, isMediumScreen); const countCollapsed = getSuitablePageSizeForScreen(12, isLargeScreen, isMediumScreen);
const finalCount = view === VIEW.ALL_VISIBLE ? count : view === VIEW.COLLAPSED ? countCollapsed : 36; const finalCount = view === VIEW.ALL_VISIBLE ? count : view === VIEW.COLLAPSED ? countCollapsed : 36;
const [hiddenArray, setHiddenArray] = useState([]);
function onClaimHidden(hiddenUri) {
let newArray = hiddenArray;
if (newArray.indexOf(hiddenUri) === -1) {
newArray.push(hiddenUri);
setHiddenArray(newArray);
}
}
function getHidden() {
let hidden = hiddenArray.length;
for (let uri of hiddenArray) {
if (personalRecommendations.uris.indexOf(uri) > finalCount) hidden--;
}
return hidden;
}
// ************************************************************************** // **************************************************************************
// Effects // Effects
@ -147,8 +163,9 @@ export default function RecommendedPersonal(props: Props) {
<ClaimList <ClaimList
tileLayout tileLayout
uris={personalRecommendations.uris.slice(0, finalCount)} uris={personalRecommendations.uris.slice(0, finalCount + getHidden())}
fypId={personalRecommendations.gid} fypId={personalRecommendations.gid}
onHidden={onClaimHidden}
/> />
{view !== VIEW.ALL_VISIBLE && ( {view !== VIEW.ALL_VISIBLE && (

View file

@ -23,6 +23,7 @@ type Props = {
doShowSnackBar: (string) => void, doShowSnackBar: (string) => void,
}; };
/* NEKO MARK */
const ScheduledStreams = (props: Props) => { const ScheduledStreams = (props: Props) => {
const { const {
channelIds, channelIds,
@ -34,6 +35,7 @@ const ScheduledStreams = (props: Props) => {
onLoad, onLoad,
showHideSetting = true, showHideSetting = true,
} = props; } = props;
const isMobileScreen = useIsMobile(); const isMobileScreen = useIsMobile();
const isLargeScreen = useIsLargeScreen(); const isLargeScreen = useIsLargeScreen();

View file

@ -8,6 +8,8 @@ import { selectActiveLivestreams } from 'redux/selectors/livestream';
import { selectFollowedTags } from 'redux/selectors/tags'; import { selectFollowedTags } from 'redux/selectors/tags';
import { doToggleTagFollowDesktop } from 'redux/actions/tags'; import { doToggleTagFollowDesktop } from 'redux/actions/tags';
import { selectClientSetting, selectLanguage } from 'redux/selectors/settings'; import { selectClientSetting, selectLanguage } from 'redux/selectors/settings';
import { selectAdBlockerFound } from 'redux/selectors/app';
import { selectOdyseeMembershipIsPremiumPlus } from 'redux/selectors/user';
import DiscoverPage from './view'; import DiscoverPage from './view';
const select = (state, props) => { const select = (state, props) => {
@ -23,6 +25,8 @@ const select = (state, props) => {
activeLivestreams: selectActiveLivestreams(state), activeLivestreams: selectActiveLivestreams(state),
languageSetting: selectLanguage(state), languageSetting: selectLanguage(state),
searchInLanguage: selectClientSetting(state, SETTINGS.SEARCH_IN_LANGUAGE), searchInLanguage: selectClientSetting(state, SETTINGS.SEARCH_IN_LANGUAGE),
adBlockerFound: selectAdBlockerFound(state),
hasPremiumPlus: selectOdyseeMembershipIsPremiumPlus(state),
}; };
}; };

View file

@ -19,6 +19,7 @@ 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';
import LivestreamSection from './livestreamSection'; import LivestreamSection from './livestreamSection';
import PremiumPlusTile from 'component/premiumPlusTile';
const CATEGORY_CONTENT_TYPES_FILTER = CS.CONTENT_TYPES.filter((x) => x !== CS.CLAIM_REPOST); const CATEGORY_CONTENT_TYPES_FILTER = CS.CONTENT_TYPES.filter((x) => x !== CS.CLAIM_REPOST);
@ -35,6 +36,8 @@ type Props = {
tileLayout: boolean, tileLayout: boolean,
activeLivestreams: ?LivestreamInfo, activeLivestreams: ?LivestreamInfo,
doFetchActiveLivestreams: (orderBy: ?Array<string>, lang: ?Array<string>) => void, doFetchActiveLivestreams: (orderBy: ?Array<string>, lang: ?Array<string>) => void,
adBlockerFound: ?boolean,
hasPremiumPlus: ?boolean,
}; };
function DiscoverPage(props: Props) { function DiscoverPage(props: Props) {
@ -51,6 +54,8 @@ function DiscoverPage(props: Props) {
activeLivestreams, activeLivestreams,
doFetchActiveLivestreams, doFetchActiveLivestreams,
dynamicRouteProps, dynamicRouteProps,
adBlockerFound,
hasPremiumPlus,
} = props; } = props;
const buttonRef = useRef(); const buttonRef = useRef();
@ -221,7 +226,16 @@ function DiscoverPage(props: Props) {
tags={tags} tags={tags}
hiddenNsfwMessage={<HiddenNsfw type="page" />} hiddenNsfwMessage={<HiddenNsfw type="page" />}
repostedClaimId={repostedClaim ? repostedClaim.claim_id : null} repostedClaimId={repostedClaim ? repostedClaim.claim_id : null}
injectedItem={!isWildWest && { node: <Ads small type="video" tileLayout={tileLayout} /> }} injectedItem={
!isWildWest &&
!hasPremiumPlus && {
node: adBlockerFound ? (
<PremiumPlusTile tileLayout={tileLayout} />
) : (
<Ads small type="video" tileLayout />
),
}
}
// TODO: find a better way to determine discover / wild west vs other modes release times // TODO: find a better way to determine discover / wild west vs other modes release times
// for now including && !tags so that // for now including && !tags so that
releaseTime={releaseTime || undefined} releaseTime={releaseTime || undefined}

View file

@ -5,7 +5,12 @@ import { doFetchActiveLivestreams } from 'redux/actions/livestream';
import { selectAdBlockerFound } from 'redux/selectors/app'; import { selectAdBlockerFound } from 'redux/selectors/app';
import { selectActiveLivestreams, selectFetchingActiveLivestreams } from 'redux/selectors/livestream'; import { selectActiveLivestreams, selectFetchingActiveLivestreams } from 'redux/selectors/livestream';
import { selectFollowedTags } from 'redux/selectors/tags'; import { selectFollowedTags } from 'redux/selectors/tags';
import { selectHasOdyseeMembership, selectHomepageFetched, selectUserVerifiedEmail } from 'redux/selectors/user'; import {
selectOdyseeMembershipIsPremiumPlus,
selectHasOdyseeMembership,
selectHomepageFetched,
selectUserVerifiedEmail,
} from 'redux/selectors/user';
import { selectSubscriptions } from 'redux/selectors/subscriptions'; import { selectSubscriptions } from 'redux/selectors/subscriptions';
import { import {
selectShowMatureContent, selectShowMatureContent,
@ -30,6 +35,7 @@ const select = (state) => ({
adBlockerFound: selectAdBlockerFound(state), adBlockerFound: selectAdBlockerFound(state),
homepageOrder: selectClientSetting(state, SETTINGS.HOMEPAGE_ORDER), homepageOrder: selectClientSetting(state, SETTINGS.HOMEPAGE_ORDER),
hasMembership: selectHasOdyseeMembership(state), hasMembership: selectHasOdyseeMembership(state),
hasPremiumPlus: selectOdyseeMembershipIsPremiumPlus(state),
}); });
const perform = (dispatch) => ({ const perform = (dispatch) => ({

View file

@ -20,6 +20,7 @@ import { splitBySeparator } from 'util/lbryURI';
import classnames from 'classnames'; import classnames from 'classnames';
import Ads from 'web/component/ads'; import Ads from 'web/component/ads';
import Meme from 'web/component/meme'; import Meme from 'web/component/meme';
import PremiumPlusTile from 'component/premiumPlusTile';
const FYP_SECTION: RowDataItem = { const FYP_SECTION: RowDataItem = {
id: 'FYP', id: 'FYP',
@ -46,6 +47,7 @@ type Props = {
homepageOrder: HomepageOrder, homepageOrder: HomepageOrder,
doOpenModal: (id: string, ?{}) => void, doOpenModal: (id: string, ?{}) => void,
hasMembership: ?boolean, hasMembership: ?boolean,
hasPremiumPlus: ?boolean,
}; };
function HomePage(props: Props) { function HomePage(props: Props) {
@ -65,6 +67,7 @@ function HomePage(props: Props) {
homepageOrder, homepageOrder,
doOpenModal, doOpenModal,
hasMembership, hasMembership,
hasPremiumPlus,
} = props; } = props;
const showPersonalizedChannels = (authenticated || !IS_WEB) && subscribedChannels && subscribedChannels.length > 0; const showPersonalizedChannels = (authenticated || !IS_WEB) && subscribedChannels && subscribedChannels.length > 0;
@ -163,12 +166,14 @@ function HomePage(props: Props) {
prefixUris={getLivestreamUris(activeLivestreams, options.channelIds)} prefixUris={getLivestreamUris(activeLivestreams, options.channelIds)}
pins={{ urls: pinUrls, claimIds: pinnedClaimIds }} pins={{ urls: pinUrls, claimIds: pinnedClaimIds }}
injectedItem={ injectedItem={
index === 0 && { index === 0 &&
node: <Ads small type="video" tileLayout />, !hasPremiumPlus && {
replace: adBlockerFound === false && isLargeScreen, node: adBlockerFound ? <PremiumPlusTile tileLayout /> : <Ads small type="video" tileLayout />,
} }
} }
forceShowReposts={id !== 'FOLLOWING'} forceShowReposts={id !== 'FOLLOWING'}
loading={id === 'FOLLOWING' ? fetchingActiveLivestreams : false}
adBlockerFound={adBlockerFound}
/> />
); );

View file

@ -33,10 +33,17 @@
aspect-ratio: 16 / 9; aspect-ratio: 16 / 9;
$minWidth: calc(var(--file-list-thumbnail-width) * 0.8); $minWidth: calc(var(--file-list-thumbnail-width) * 0.8);
min-width: $minWidth; min-width: $minWidth;
background-color: #283263;
background-image: url('https://odysee.com/public/img/astronaut_n_friends.png');
background-size: 100%;
border-radius: var(--border-radius);
box-shadow: 0 0 0 1px rgba(var(--color-primary-dynamic), 0.1) inset;
video { video {
width: 100% !important; width: 100% !important;
height: 100% !important; height: 100% !important;
border-radius: var(--border-radius) !important;
border: 1px solid rgba(var(--color-primary-dynamic), 0.1);
} }
@media (max-width: $breakpoint-small) { @media (max-width: $breakpoint-small) {
@ -93,6 +100,33 @@
display: none !important; display: none !important;
} }
} }
&:hover {
.ad__container {
box-shadow: 0 0 0 1px rgba(var(--color-primary-dynamic), 1) inset;
}
video {
border: 1px solid rgba(var(--color-primary-dynamic), 1);
}
}
}
.claim-list {
.ads__claim-item--tile {
background-color: var(--color-ads-background);
margin-left: unset;
margin-bottom: var(--spacing-xxs);
width: 100%;
padding: var(--spacing-m);
flex: 1;
flex-direction: row;
.ad__container {
width: calc(var(--file-list-thumbnail-width) * 1.2);
// margin-left: var(--spacing-s);
margin-right: var(--spacing-s);
}
}
} }
.ads__claim-item--tile { .ads__claim-item--tile {
@ -105,10 +139,30 @@
border-bottom: unset; border-bottom: unset;
.ads__claim-text { .ads__claim-text {
margin: var(--spacing-s) 0 0 0; margin: var(--spacing-xs) 0 0 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
.ads__title {
color: var(--color-text);
}
.ads__subtitle {
margin-top: var(--spacing-s);
color: rgba(var(--color-text-base), 0.6);
font-weight: var(--font-weight-bold);
line-height: 1rem;
font-size: var(--font-xsmall);
.icon {
color: var(--color-text);
width: 2.1rem;
height: 2.1rem;
float: left;
margin-right: var(--spacing-s);
}
}
} }
.ad__container { .ad__container {
@ -118,10 +172,52 @@
.ads__claim-text { .ads__claim-text {
max-width: 100%; max-width: 100%;
} }
#close-btn {
left: unset !important;
right: 0 !important;
border: none !important;
border-radius: 0 var(--border-radius) 0 var(--border-radius) !important;
background-color: var(--color-primary) !important;
}
.visible {
display: none !important;
position: absolute !important;
top: 0px !important;
z-index: 99 !important;
width: 3rem !important;
height: 3rem !important;
background-color: var(--color-primary) !important;
border-radius: 0 var(--border-radius) 0 var(--border-radius) !important;
background-size: 60% !important;
}
} }
.ads__claim-item--recommended { .ads__claim-item--recommended {
padding: var(--spacing-s); // padding: var(--spacing-s);
padding: 0;
margin-top: var(--spacing-m);
border-radius: var(--border-radius);
border-bottom: unset;
.ad__container {
// width:100%;
width: var(--file-list-thumbnail-width);
}
.ads__claim-text {
margin: 0;
padding-left: var(--spacing-s);
.ads__title {
color: white;
}
}
.icon {
display: none;
}
@media (min-width: $breakpoint-medium) { @media (min-width: $breakpoint-medium) {
margin-bottom: 0; margin-bottom: 0;
@ -203,8 +299,66 @@
// Outbrain // Outbrain
// **************************************************************************** // ****************************************************************************
.OUTBRAIN {
border-radius: var(--border-radius) 0 0 0 !important;
height: unset !important;
.ob-widget .ob-unit.ob-rec-text {
font-size: 12px !important;
}
}
.closeButton {
background-color: var(--color-primary) !important;
color: var(--color-primary-contrast) !important;
border-radius: var(--border-radius) var(--border-radius) 0 0 !important;
top: -27px !important;
width: 2rem !important;
padding-top: 3px;
// padding-bottom:4px !important;
height: 27px !important;
}
.ob-widget-footer {
position: absolute !important;
right: 32px;
top: -27px;
background: black;
border-radius: var(--border-radius) var(--border-radius) 0 0 !important;
padding-left: 4px;
padding-right: 6px;
padding-top: 3px;
padding-bottom: 0px;
.ob_what_resp {
padding: unset !important;
}
}
.ob-widget-items-container { .ob-widget-items-container {
padding-left: var(--spacing-xs); padding-left: var(--spacing-xs);
padding-right: var(--spacing-xs); padding-right: var(--spacing-xs);
border-radius: var(--border-radius); border-radius: var(--border-radius) 0 0 0;
}
// ****************************************************************************
// Neko Patch
// ****************************************************************************
.ad__container {
div {
max-width: unset !important;
}
}
#av-container #av-inner #gui::before {
background: unset !important;
#timeline #timeline-progress {
background: var(--color-primary) !important;
}
}
#av-container #av-inner #gui #timeline #timeline-progress {
background: var(--color-primary) !important;
} }

View file

@ -397,7 +397,7 @@
flex-shrink: 0; flex-shrink: 0;
margin-right: var(--spacing-s); margin-right: var(--spacing-s);
box-shadow: 0px 0px 0px 1px rgba(var(--color-primary-dynamic), 0.1) inset; box-shadow: 0 0 0 1px rgba(var(--color-primary-dynamic), 0.1) inset;
} }
.media__thumb-placeholder-text { .media__thumb-placeholder-text {
@ -413,7 +413,7 @@
// show watch later button and duration divs when hovered // show watch later button and duration divs when hovered
&:hover { &:hover {
.media__thumb { .media__thumb {
box-shadow: 0px 0px 0px 1px rgba(var(--color-primary-dynamic), 1) inset; box-shadow: 0 0 0 1px rgba(var(--color-primary-dynamic), 1) inset;
} }
.claim-preview__title { .claim-preview__title {
color: var(--color-link); color: var(--color-link);
@ -447,6 +447,29 @@
} }
} }
.claim-preview--premium-plus {
.media__thumb {
background-color: #283263;
background-image: url('https://odysee.com/public/img/astronaut_n_friends.png');
background-size: 100%;
}
.icon {
color: var(--color-text);
width: 2.1rem;
height: 2.1rem;
float: left;
margin-right: var(--spacing-s);
}
.channel-name {
font-weight: var(--font-weight-bold);
&:hover {
color: var(--color-text);
}
}
}
.claim-preview__empty { .claim-preview__empty {
display: flex; display: flex;
align-items: center; align-items: center;
@ -938,11 +961,20 @@
} }
} }
.comment__badge {
display: inherit;
.icon {
margin-bottom: -1px;
padding-bottom: 4px;
}
}
&:hover { &:hover {
cursor: pointer; cursor: pointer;
.media__thumb { .media__thumb {
box-shadow: 0px 0px 0px 1px rgba(var(--color-primary-dynamic), 1) inset; box-shadow: 0 0 0 1px rgba(var(--color-primary-dynamic), 1) inset;
background-size: 108%; background-size: 108%;
} }
@ -965,7 +997,7 @@
color: var(--color-primary); color: var(--color-primary);
} }
.media__thumb { .media__thumb {
box-shadow: 0px 0px 0px 1px rgba(var(--color-primary-dynamic), 1) inset; box-shadow: 0 0 0 1px rgba(var(--color-primary-dynamic), 1) inset;
} }
} }
} }
@ -1332,7 +1364,7 @@ $claim-preview-progress-bar-height: 5px;
margin-bottom: -4px; margin-bottom: -4px;
margin-left: 4px; margin-left: 4px;
margin-right: var(--spacing-xxs); margin-right: var(--spacing-xxs);
text-overflow: hidden; text-overflow: ellipsis;
} }
&:hover { &:hover {
@ -1349,7 +1381,7 @@ $claim-preview-progress-bar-height: 5px;
} }
.media__thumb { .media__thumb {
box-shadow: 0px 0px 0px 1px rgba(var(--color-primary-dynamic), 1) inset; box-shadow: 0 0 0 1px rgba(var(--color-primary-dynamic), 1) inset;
} }
.claim-preview:not(.claim-preview--collection-mine):before { .claim-preview:not(.claim-preview--collection-mine):before {
@ -1371,14 +1403,13 @@ $claim-preview-progress-bar-height: 5px;
} }
.claim-preview__live { .claim-preview__live {
// margin-bottom: 0 !important;
.claim-preview__file-property-overlay { .claim-preview__file-property-overlay {
opacity: 1; // The original 0.7 is not visible over bright thumbnails opacity: 0.9; // The original 0.7 is not visible over bright thumbnails
color: var(--color-white-alt); color: var(--color-white-alt);
background-color: var(--color-live); background-color: var(--color-live);
.claim-preview__overlay-properties { .claim-preview__overlay-properties {
margin-bottom: -2px;
color: white; color: white;
font-weight: var(--font-weight-bold);
} }
} }

View file

@ -84,7 +84,7 @@
align-items: center; align-items: center;
.icon { .icon {
margin-left: var(--spacing-xs); margin-left: var(--spacing-xxs);
} }
} }

View file

@ -10,7 +10,7 @@
background-position: center; background-position: center;
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: cover; background-size: cover;
box-shadow: 0px 0px 0px 1px rgba(var(--color-primary-dynamic), 0.1) inset; box-shadow: 0 0 0 1px rgba(var(--color-primary-dynamic), 0.1) inset;
} }
// M E D I A // M E D I A

View file

@ -44,7 +44,7 @@
.section__header--actions { .section__header--actions {
padding-top: var(--spacing-m); padding-top: var(--spacing-m);
margin-top: var(--spacing-m); margin-top: var(--spacing-m);
margin-bottom: var(--spacing-m); margin-bottom: var(--spacing-l);
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
justify-content: space-between; justify-content: space-between;

View file

@ -188,6 +188,10 @@
border-width: 0; border-width: 0;
} }
.pulse {
animation: pulse 2s infinite ease-in-out;
}
@mixin mediaThumbHoverZoom { @mixin mediaThumbHoverZoom {
.media__thumb, .media__thumb,
img { img {

View file

@ -160,8 +160,8 @@
--color-scrollbar-track-bg: transparent; --color-scrollbar-track-bg: transparent;
--color-body-scrollbar-track-bg: rgba(0, 0, 0, 0.5); --color-body-scrollbar-track-bg: rgba(0, 0, 0, 0.5);
--background-shade: linear-gradient(-180deg, rgba(25, 25, 25, 0.2) 2%, #202020), --background-shade: linear-gradient(-180deg, rgba(25, 25, 25, 0.3) 2%, #202020),
radial-gradient(circle at 50% 117%, rgba(25, 25, 25, 0.2) 0, #202020 100%); radial-gradient(circle at 50% 117%, rgba(25, 25, 25, 0.4) 0, #202020 100%);
--mui-background: #000; --mui-background: #000;
} }

View file

@ -6,6 +6,8 @@ import I18nMessage from 'component/i18nMessage';
import Button from 'component/button'; import Button from 'component/button';
import classnames from 'classnames'; import classnames from 'classnames';
import { platform } from 'util/platform'; import { platform } from 'util/platform';
import Icon from 'component/common/icon';
import * as ICONS from 'constants/icons';
// prettier-ignore // prettier-ignore
const AD_CONFIGS = Object.freeze({ const AD_CONFIGS = Object.freeze({
@ -124,7 +126,7 @@ function Ads(props: Props) {
), ),
}} }}
> >
Hate these? %sign_up_for_premium% for an ad free experience. %sign_up_for_premium% for an ad free experience.
</I18nMessage> </I18nMessage>
); );
@ -143,8 +145,16 @@ function Ads(props: Props) {
'ads__claim-text--small': small, 'ads__claim-text--small': small,
})} })}
> >
<div>Ad</div> <div className="ads__title">
<p>{adsSignInDriver}</p> {__('Ad')}
<br />
{__('Hate these?')}
{/* __('No ads, a custom badge and access to exclusive features, try Odysee Premium!') */}
</div>
<div className="ads__subtitle">
<Icon icon={ICONS.UPGRADE} />
{adsSignInDriver}
</div>
</div> </div>
</div> </div>
); );