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:
parent
96e704e5d9
commit
ffdb5abf63
24 changed files with 439 additions and 58 deletions
1
flow-typed/gui.js
vendored
1
flow-typed/gui.js
vendored
|
@ -2,5 +2,4 @@
|
|||
declare type ListInjectedItem = {
|
||||
node: Node | (index: number, lastVisibleIndex: ?number, pageSize: ?number) => Node,
|
||||
index?: number,
|
||||
replace?: boolean
|
||||
};
|
||||
|
|
|
@ -7,11 +7,13 @@ import {
|
|||
makeSelectTotalPagesInChannelSearch,
|
||||
selectClaimForUri,
|
||||
} from 'redux/selectors/claims';
|
||||
import { selectOdyseeMembershipIsPremiumPlus } from 'redux/selectors/user';
|
||||
import { doResolveUris } from 'redux/actions/claims';
|
||||
import * as SETTINGS from 'constants/settings';
|
||||
import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
|
||||
import { withRouter } from 'react-router';
|
||||
import { selectClientSetting, selectShowMatureContent } from 'redux/selectors/settings';
|
||||
import { selectAdBlockerFound } from 'redux/selectors/app';
|
||||
import { doFetchChannelLiveStatus } from 'redux/actions/livestream';
|
||||
import { selectActiveLivestreamForChannel, selectActiveLivestreamInitialized } from 'redux/selectors/livestream';
|
||||
import { getChannelIdFromClaim } from 'util/claim';
|
||||
|
@ -35,6 +37,8 @@ const select = (state, props) => {
|
|||
tileLayout: selectClientSetting(state, SETTINGS.TILE_LAYOUT),
|
||||
activeLivestreamForChannel: selectActiveLivestreamForChannel(state, channelClaimId),
|
||||
activeLivestreamInitialized: selectActiveLivestreamInitialized(state),
|
||||
adBlockerFound: selectAdBlockerFound(state),
|
||||
hasPremiumPlus: selectOdyseeMembershipIsPremiumPlus(state),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import ScheduledStreams from 'component/scheduledStreams';
|
|||
import { SearchResults } from './internal/searchResults';
|
||||
import useFetchLiveStatus from 'effects/use-fetch-live';
|
||||
import { useIsLargeScreen } from 'effects/use-screensize';
|
||||
import PremiumPlusTile from 'component/premiumPlusTile';
|
||||
|
||||
const TYPES_TO_ALLOW_FILTER = ['stream', 'repost'];
|
||||
|
||||
|
@ -41,6 +42,8 @@ type Props = {
|
|||
doFetchChannelLiveStatus: (string) => void,
|
||||
activeLivestreamForChannel: any,
|
||||
activeLivestreamInitialized: boolean,
|
||||
adBlockerFound: ?boolean,
|
||||
hasPremiumPlus: ?boolean,
|
||||
};
|
||||
|
||||
function ChannelContent(props: Props) {
|
||||
|
@ -62,6 +65,8 @@ function ChannelContent(props: Props) {
|
|||
doFetchChannelLiveStatus,
|
||||
activeLivestreamForChannel,
|
||||
activeLivestreamInitialized,
|
||||
adBlockerFound,
|
||||
hasPremiumPlus,
|
||||
} = props;
|
||||
// const claimsInChannel = (claim && claim.meta.claims_in_channel) || 0;
|
||||
|
||||
|
@ -159,7 +164,15 @@ function ChannelContent(props: Props) {
|
|||
defaultOrderBy={CS.ORDER_BY_NEW}
|
||||
pageSize={dynamicPageSize}
|
||||
infiniteScroll={defaultInfiniteScroll}
|
||||
injectedItem={{ node: <Ads type="video" tileLayout={tileLayout} small /> }}
|
||||
injectedItem={
|
||||
!hasPremiumPlus && {
|
||||
node: adBlockerFound ? (
|
||||
<PremiumPlusTile tileLayout={tileLayout} />
|
||||
) : (
|
||||
<Ads small type="video" tileLayout />
|
||||
),
|
||||
}
|
||||
}
|
||||
meta={
|
||||
showFilters && (
|
||||
<Form onSubmit={() => {}} className="wunderbar--inline">
|
||||
|
|
|
@ -27,7 +27,7 @@ export default function CollectionAddButton(props: Props) {
|
|||
button={!fileAction ? 'alt' : undefined}
|
||||
className={classnames({ 'button--file-action': fileAction })}
|
||||
icon={fileAction ? (!isSaved ? ICONS.ADD : ICONS.STACK) : ICONS.LIBRARY}
|
||||
iconSize={fileAction ? 22 : undefined}
|
||||
iconSize={fileAction ? 16 : undefined}
|
||||
label={uri ? (!isSaved ? __('Save') : __('Saved')) : __('New List')}
|
||||
requiresAuth
|
||||
onClick={(e) => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// @flow
|
||||
import { MAIN_CLASS } from 'constants/classnames';
|
||||
import type { Node } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import ClaimPreview from 'component/claimPreview';
|
||||
import Spinner from 'component/spinner';
|
||||
|
@ -61,6 +61,7 @@ type Props = {
|
|||
unavailableUris?: Array<string>,
|
||||
showMemberBadge?: boolean,
|
||||
inWatchHistory?: boolean,
|
||||
onHidden: string,
|
||||
};
|
||||
|
||||
export default function ClaimList(props: Props) {
|
||||
|
@ -102,9 +103,11 @@ export default function ClaimList(props: Props) {
|
|||
unavailableUris,
|
||||
showMemberBadge,
|
||||
inWatchHistory,
|
||||
onHidden,
|
||||
} = props;
|
||||
|
||||
const [currentSort, setCurrentSort] = usePersistedState(persistedStorageKey, SORT_NEW);
|
||||
const [uriBuffer, setUriBuffer] = useState([]);
|
||||
|
||||
// Resolve the index for injectedItem, if provided; else injectedIndex will be 'undefined'.
|
||||
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';
|
||||
}, []);
|
||||
|
||||
// @if process.env.NODE_ENV!='production'
|
||||
if (injectedItem && injectedItem.replace) {
|
||||
throw new Error('claimList: "injectedItem.replace" is not implemented yet');
|
||||
}
|
||||
// @endif
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = debounce((e) => {
|
||||
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) => {
|
||||
if (injectedItem && injectedItem.node) {
|
||||
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 })}>
|
||||
{urisLength > 0 &&
|
||||
tileUris.map((uri, index) => (
|
||||
<React.Fragment key={uri}>
|
||||
{getInjectedItem(index)}
|
||||
<ClaimPreviewTile
|
||||
uri={uri}
|
||||
showHiddenByUser={showHiddenByUser}
|
||||
showUnresolvedClaims={showUnresolvedClaims}
|
||||
properties={renderProperties}
|
||||
collectionId={collectionId}
|
||||
fypId={fypId}
|
||||
showNoSourceClaims={showNoSourceClaims}
|
||||
swipeLayout={swipeLayout}
|
||||
/>
|
||||
</React.Fragment>
|
||||
))}
|
||||
tileUris.map((uri, index) => {
|
||||
if (uri) {
|
||||
const inj = getInjectedItem(index);
|
||||
return (
|
||||
<React.Fragment key={uri}>
|
||||
{inj && inj}
|
||||
{(index < tileUris.length - uriBuffer.length ||
|
||||
(pageSize && index < pageSize - uriBuffer.length)) && (
|
||||
<ClaimPreviewTile
|
||||
uri={uri}
|
||||
showHiddenByUser={showHiddenByUser}
|
||||
showUnresolvedClaims={showUnresolvedClaims}
|
||||
properties={renderProperties}
|
||||
collectionId={collectionId}
|
||||
fypId={fypId}
|
||||
showNoSourceClaims={showNoSourceClaims}
|
||||
swipeLayout={swipeLayout}
|
||||
onHidden={onHidden}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
})}
|
||||
{!timedOut && urisLength === 0 && !loading && !noEmpty && (
|
||||
<div className="empty main--empty">{empty || noResultMsg}</div>
|
||||
)}
|
||||
|
|
|
@ -732,7 +732,7 @@ function ClaimListDiscover(props: Props) {
|
|||
{loading && useSkeletonScreen && (
|
||||
<div className="claim-grid">
|
||||
{new Array(dynamicPageSize).fill(1).map((x, i) => (
|
||||
<ClaimPreviewTile key={i} placeholder="loading" />
|
||||
<ClaimPreviewTile key={i} placeholder="loading" pulse />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -55,6 +55,8 @@ type Props = {
|
|||
isLivestreamActive: boolean,
|
||||
livestreamViewerCount: ?number,
|
||||
swipeLayout: boolean,
|
||||
onHidden?: (string) => void,
|
||||
pulse?: boolean,
|
||||
};
|
||||
|
||||
// preview image cards used in related video functionality, channel overview page and homepage
|
||||
|
@ -86,6 +88,8 @@ function ClaimPreviewTile(props: Props) {
|
|||
mediaDuration,
|
||||
viewCount,
|
||||
swipeLayout = false,
|
||||
onHidden,
|
||||
pulse,
|
||||
} = props;
|
||||
const isRepost = claim && claim.repost_channel_url;
|
||||
const isCollection = claim && claim.value_type === 'collection';
|
||||
|
@ -158,6 +162,7 @@ function ClaimPreviewTile(props: Props) {
|
|||
banState.filtered ||
|
||||
(!showHiddenByUser && (banState.muted || banState.blocked)) ||
|
||||
(isAbandoned && !showUnresolvedClaims));
|
||||
if (onHidden && shouldHide) onHidden(props.uri);
|
||||
}
|
||||
|
||||
if (shouldHide || (isLivestream && !showNoSourceClaims)) {
|
||||
|
@ -173,6 +178,7 @@ function ClaimPreviewTile(props: Props) {
|
|||
<li
|
||||
className={classnames('placeholder claim-preview--tile', {
|
||||
'swipe-list__item claim-preview--horizontal-tile': swipeLayout,
|
||||
pulse: pulse,
|
||||
})}
|
||||
>
|
||||
<div className="media__thumb">
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// @flow
|
||||
import type { Node } from 'react';
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import Button from 'component/button';
|
||||
import ClaimPreviewTile from 'component/claimPreviewTile';
|
||||
import I18nMessage from 'component/i18nMessage';
|
||||
|
@ -52,6 +52,7 @@ type Props = {
|
|||
hasSource?: boolean,
|
||||
hasNoSource?: boolean,
|
||||
forceShowReposts?: boolean, // overrides SETTINGS.HIDE_REPOSTS
|
||||
loading: boolean,
|
||||
// --- select ---
|
||||
location: { search: string },
|
||||
claimSearchResults: Array<string>,
|
||||
|
@ -90,6 +91,7 @@ function ClaimTilesDiscover(props: Props) {
|
|||
doFetchUserMemberships,
|
||||
doResolveClaimIds,
|
||||
doResolveUris,
|
||||
loading,
|
||||
} = props;
|
||||
|
||||
const listRef = React.useRef();
|
||||
|
@ -100,6 +102,7 @@ function ClaimTilesDiscover(props: Props) {
|
|||
const claimSearchUris = claimSearchResults || [];
|
||||
const isUnfetchedClaimSearch = claimSearchResults === undefined;
|
||||
const resolvedPinUris = useResolvePins({ pins, claimsById, doResolveClaimIds, doResolveUris });
|
||||
const [uriBuffer, setUriBuffer] = useState([]);
|
||||
|
||||
const timedOut = claimSearchResults === null;
|
||||
const shouldPerformSearch = !fetchingClaimSearch && !timedOut && claimSearchUris.length === 0;
|
||||
|
@ -172,6 +175,19 @@ function ClaimTilesDiscover(props: Props) {
|
|||
}
|
||||
}, [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 (
|
||||
<ul ref={listRef} className="claim-grid">
|
||||
{finalUris && finalUris.length
|
||||
{!loading && finalUris && finalUris.length
|
||||
? finalUris.map((uri, i) => {
|
||||
if (uri) {
|
||||
const inj = getInjectedItem(i);
|
||||
return (
|
||||
<React.Fragment key={uri}>
|
||||
{inj && inj}
|
||||
{(!inj || !injectedItem || !injectedItem.replace) && (
|
||||
{(i < finalUris.length - uriBuffer.length || i < pageSize - uriBuffer.length) && (
|
||||
<ClaimPreviewTile
|
||||
showNoSourceClaims={hasNoSource || showNoSourceClaims}
|
||||
uri={uri}
|
||||
|
@ -217,13 +233,15 @@ function ClaimTilesDiscover(props: Props) {
|
|||
</React.Fragment>
|
||||
);
|
||||
} else {
|
||||
return <ClaimPreviewTile showNoSourceClaims={hasNoSource || showNoSourceClaims} key={i} placeholder />;
|
||||
return (
|
||||
<ClaimPreviewTile showNoSourceClaims={hasNoSource || showNoSourceClaims} key={i} placeholder pulse />
|
||||
);
|
||||
}
|
||||
})
|
||||
: new Array(pageSize)
|
||||
.fill(1)
|
||||
.map((x, i) => (
|
||||
<ClaimPreviewTile showNoSourceClaims={hasNoSource || showNoSourceClaims} key={i} placeholder />
|
||||
<ClaimPreviewTile showNoSourceClaims={hasNoSource || showNoSourceClaims} key={i} placeholder pulse />
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
|
@ -236,6 +254,7 @@ export default React.memo<Props>(ClaimTilesDiscover, areEqual);
|
|||
|
||||
function trace(key, value) {
|
||||
// @if process.env.DEBUG_TILE_RENDER
|
||||
|
||||
// $FlowFixMe "cannot coerce certain types".
|
||||
console.log(`[claimTilesDiscover] ${key}: ${value}`); // eslint-disable-line no-console
|
||||
// @endif
|
||||
|
|
6
ui/component/premiumPlusTile/index.js
Normal file
6
ui/component/premiumPlusTile/index.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { connect } from 'react-redux';
|
||||
import PremiumPlusTile from './view';
|
||||
|
||||
const select = (state) => ({});
|
||||
|
||||
export default connect(select, {})(PremiumPlusTile);
|
68
ui/component/premiumPlusTile/view.jsx
Normal file
68
ui/component/premiumPlusTile/view.jsx
Normal 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;
|
|
@ -1,5 +1,5 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import Button from 'component/button';
|
||||
import ClaimList from 'component/claimList';
|
||||
import ClaimPreviewTile from 'component/claimPreviewTile';
|
||||
|
@ -45,6 +45,22 @@ export default function RecommendedPersonal(props: Props) {
|
|||
const count = personalRecommendations.uris.length;
|
||||
const countCollapsed = getSuitablePageSizeForScreen(12, isLargeScreen, isMediumScreen);
|
||||
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
|
||||
|
@ -147,8 +163,9 @@ export default function RecommendedPersonal(props: Props) {
|
|||
|
||||
<ClaimList
|
||||
tileLayout
|
||||
uris={personalRecommendations.uris.slice(0, finalCount)}
|
||||
uris={personalRecommendations.uris.slice(0, finalCount + getHidden())}
|
||||
fypId={personalRecommendations.gid}
|
||||
onHidden={onClaimHidden}
|
||||
/>
|
||||
|
||||
{view !== VIEW.ALL_VISIBLE && (
|
||||
|
|
|
@ -23,6 +23,7 @@ type Props = {
|
|||
doShowSnackBar: (string) => void,
|
||||
};
|
||||
|
||||
/* NEKO MARK */
|
||||
const ScheduledStreams = (props: Props) => {
|
||||
const {
|
||||
channelIds,
|
||||
|
@ -34,6 +35,7 @@ const ScheduledStreams = (props: Props) => {
|
|||
onLoad,
|
||||
showHideSetting = true,
|
||||
} = props;
|
||||
|
||||
const isMobileScreen = useIsMobile();
|
||||
const isLargeScreen = useIsLargeScreen();
|
||||
|
||||
|
|
|
@ -8,6 +8,8 @@ import { selectActiveLivestreams } from 'redux/selectors/livestream';
|
|||
import { selectFollowedTags } from 'redux/selectors/tags';
|
||||
import { doToggleTagFollowDesktop } from 'redux/actions/tags';
|
||||
import { selectClientSetting, selectLanguage } from 'redux/selectors/settings';
|
||||
import { selectAdBlockerFound } from 'redux/selectors/app';
|
||||
import { selectOdyseeMembershipIsPremiumPlus } from 'redux/selectors/user';
|
||||
import DiscoverPage from './view';
|
||||
|
||||
const select = (state, props) => {
|
||||
|
@ -23,6 +25,8 @@ const select = (state, props) => {
|
|||
activeLivestreams: selectActiveLivestreams(state),
|
||||
languageSetting: selectLanguage(state),
|
||||
searchInLanguage: selectClientSetting(state, SETTINGS.SEARCH_IN_LANGUAGE),
|
||||
adBlockerFound: selectAdBlockerFound(state),
|
||||
hasPremiumPlus: selectOdyseeMembershipIsPremiumPlus(state),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ import LbcSymbol from 'component/common/lbc-symbol';
|
|||
import I18nMessage from 'component/i18nMessage';
|
||||
import moment from 'moment';
|
||||
import LivestreamSection from './livestreamSection';
|
||||
import PremiumPlusTile from 'component/premiumPlusTile';
|
||||
|
||||
const CATEGORY_CONTENT_TYPES_FILTER = CS.CONTENT_TYPES.filter((x) => x !== CS.CLAIM_REPOST);
|
||||
|
||||
|
@ -35,6 +36,8 @@ type Props = {
|
|||
tileLayout: boolean,
|
||||
activeLivestreams: ?LivestreamInfo,
|
||||
doFetchActiveLivestreams: (orderBy: ?Array<string>, lang: ?Array<string>) => void,
|
||||
adBlockerFound: ?boolean,
|
||||
hasPremiumPlus: ?boolean,
|
||||
};
|
||||
|
||||
function DiscoverPage(props: Props) {
|
||||
|
@ -51,6 +54,8 @@ function DiscoverPage(props: Props) {
|
|||
activeLivestreams,
|
||||
doFetchActiveLivestreams,
|
||||
dynamicRouteProps,
|
||||
adBlockerFound,
|
||||
hasPremiumPlus,
|
||||
} = props;
|
||||
|
||||
const buttonRef = useRef();
|
||||
|
@ -221,7 +226,16 @@ function DiscoverPage(props: Props) {
|
|||
tags={tags}
|
||||
hiddenNsfwMessage={<HiddenNsfw type="page" />}
|
||||
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
|
||||
// for now including && !tags so that
|
||||
releaseTime={releaseTime || undefined}
|
||||
|
|
|
@ -5,7 +5,12 @@ import { doFetchActiveLivestreams } from 'redux/actions/livestream';
|
|||
import { selectAdBlockerFound } from 'redux/selectors/app';
|
||||
import { selectActiveLivestreams, selectFetchingActiveLivestreams } from 'redux/selectors/livestream';
|
||||
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 {
|
||||
selectShowMatureContent,
|
||||
|
@ -30,6 +35,7 @@ const select = (state) => ({
|
|||
adBlockerFound: selectAdBlockerFound(state),
|
||||
homepageOrder: selectClientSetting(state, SETTINGS.HOMEPAGE_ORDER),
|
||||
hasMembership: selectHasOdyseeMembership(state),
|
||||
hasPremiumPlus: selectOdyseeMembershipIsPremiumPlus(state),
|
||||
});
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
|
|
|
@ -20,6 +20,7 @@ import { splitBySeparator } from 'util/lbryURI';
|
|||
import classnames from 'classnames';
|
||||
import Ads from 'web/component/ads';
|
||||
import Meme from 'web/component/meme';
|
||||
import PremiumPlusTile from 'component/premiumPlusTile';
|
||||
|
||||
const FYP_SECTION: RowDataItem = {
|
||||
id: 'FYP',
|
||||
|
@ -46,6 +47,7 @@ type Props = {
|
|||
homepageOrder: HomepageOrder,
|
||||
doOpenModal: (id: string, ?{}) => void,
|
||||
hasMembership: ?boolean,
|
||||
hasPremiumPlus: ?boolean,
|
||||
};
|
||||
|
||||
function HomePage(props: Props) {
|
||||
|
@ -65,6 +67,7 @@ function HomePage(props: Props) {
|
|||
homepageOrder,
|
||||
doOpenModal,
|
||||
hasMembership,
|
||||
hasPremiumPlus,
|
||||
} = props;
|
||||
|
||||
const showPersonalizedChannels = (authenticated || !IS_WEB) && subscribedChannels && subscribedChannels.length > 0;
|
||||
|
@ -163,12 +166,14 @@ function HomePage(props: Props) {
|
|||
prefixUris={getLivestreamUris(activeLivestreams, options.channelIds)}
|
||||
pins={{ urls: pinUrls, claimIds: pinnedClaimIds }}
|
||||
injectedItem={
|
||||
index === 0 && {
|
||||
node: <Ads small type="video" tileLayout />,
|
||||
replace: adBlockerFound === false && isLargeScreen,
|
||||
index === 0 &&
|
||||
!hasPremiumPlus && {
|
||||
node: adBlockerFound ? <PremiumPlusTile tileLayout /> : <Ads small type="video" tileLayout />,
|
||||
}
|
||||
}
|
||||
forceShowReposts={id !== 'FOLLOWING'}
|
||||
loading={id === 'FOLLOWING' ? fetchingActiveLivestreams : false}
|
||||
adBlockerFound={adBlockerFound}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -33,10 +33,17 @@
|
|||
aspect-ratio: 16 / 9;
|
||||
$minWidth: calc(var(--file-list-thumbnail-width) * 0.8);
|
||||
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 {
|
||||
width: 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) {
|
||||
|
@ -93,6 +100,33 @@
|
|||
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 {
|
||||
|
@ -105,10 +139,30 @@
|
|||
border-bottom: unset;
|
||||
|
||||
.ads__claim-text {
|
||||
margin: var(--spacing-s) 0 0 0;
|
||||
margin: var(--spacing-xs) 0 0 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
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 {
|
||||
|
@ -118,10 +172,52 @@
|
|||
.ads__claim-text {
|
||||
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 {
|
||||
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) {
|
||||
margin-bottom: 0;
|
||||
|
@ -203,8 +299,66 @@
|
|||
// 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 {
|
||||
padding-left: 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;
|
||||
}
|
||||
|
|
|
@ -397,7 +397,7 @@
|
|||
|
||||
flex-shrink: 0;
|
||||
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 {
|
||||
|
@ -413,7 +413,7 @@
|
|||
// show watch later button and duration divs when hovered
|
||||
&:hover {
|
||||
.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 {
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -938,11 +961,20 @@
|
|||
}
|
||||
}
|
||||
|
||||
.comment__badge {
|
||||
display: inherit;
|
||||
|
||||
.icon {
|
||||
margin-bottom: -1px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
|
||||
.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%;
|
||||
}
|
||||
|
||||
|
@ -965,7 +997,7 @@
|
|||
color: var(--color-primary);
|
||||
}
|
||||
.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-left: 4px;
|
||||
margin-right: var(--spacing-xxs);
|
||||
text-overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
@ -1349,7 +1381,7 @@ $claim-preview-progress-bar-height: 5px;
|
|||
}
|
||||
|
||||
.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 {
|
||||
|
@ -1371,14 +1403,13 @@ $claim-preview-progress-bar-height: 5px;
|
|||
}
|
||||
|
||||
.claim-preview__live {
|
||||
// margin-bottom: 0 !important;
|
||||
.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);
|
||||
background-color: var(--color-live);
|
||||
.claim-preview__overlay-properties {
|
||||
margin-bottom: -2px;
|
||||
color: white;
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -84,7 +84,7 @@
|
|||
align-items: center;
|
||||
|
||||
.icon {
|
||||
margin-left: var(--spacing-xs);
|
||||
margin-left: var(--spacing-xxs);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
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
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
.section__header--actions {
|
||||
padding-top: var(--spacing-m);
|
||||
margin-top: var(--spacing-m);
|
||||
margin-bottom: var(--spacing-m);
|
||||
margin-bottom: var(--spacing-l);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
|
|
|
@ -188,6 +188,10 @@
|
|||
border-width: 0;
|
||||
}
|
||||
|
||||
.pulse {
|
||||
animation: pulse 2s infinite ease-in-out;
|
||||
}
|
||||
|
||||
@mixin mediaThumbHoverZoom {
|
||||
.media__thumb,
|
||||
img {
|
||||
|
|
|
@ -160,8 +160,8 @@
|
|||
--color-scrollbar-track-bg: transparent;
|
||||
--color-body-scrollbar-track-bg: rgba(0, 0, 0, 0.5);
|
||||
|
||||
--background-shade: linear-gradient(-180deg, rgba(25, 25, 25, 0.2) 2%, #202020),
|
||||
radial-gradient(circle at 50% 117%, rgba(25, 25, 25, 0.2) 0, #202020 100%);
|
||||
--background-shade: linear-gradient(-180deg, rgba(25, 25, 25, 0.3) 2%, #202020),
|
||||
radial-gradient(circle at 50% 117%, rgba(25, 25, 25, 0.4) 0, #202020 100%);
|
||||
|
||||
--mui-background: #000;
|
||||
}
|
||||
|
|
|
@ -6,6 +6,8 @@ import I18nMessage from 'component/i18nMessage';
|
|||
import Button from 'component/button';
|
||||
import classnames from 'classnames';
|
||||
import { platform } from 'util/platform';
|
||||
import Icon from 'component/common/icon';
|
||||
import * as ICONS from 'constants/icons';
|
||||
|
||||
// prettier-ignore
|
||||
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>
|
||||
);
|
||||
|
||||
|
@ -143,8 +145,16 @@ function Ads(props: Props) {
|
|||
'ads__claim-text--small': small,
|
||||
})}
|
||||
>
|
||||
<div>Ad</div>
|
||||
<p>{adsSignInDriver}</p>
|
||||
<div className="ads__title">
|
||||
{__('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>
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue