parent
a34e07e970
commit
fb3a73d8a7
96 changed files with 3032 additions and 347 deletions
extras/lbryinc/redux/selectors
flow-typed
ui
component
channelContent
channelSelector
channelThumbnail
claimList
claimListDiscover
claimPreview
claimPreviewSubtitle
claimPreviewTile
claimTilesDiscover
comment
commentsList
common
headerMenuButtons
headerProfileMenuButton
livestreamChatLayout
livestreamComment
membershipSplash
publishForm
recommendedContent
router
sideNavigation
textareaSuggestionsItem
uriIndicator
wunderbarSuggestion
constants
effects
modal
modalConfirmOdyseeMembership
modalMembershipSplash
modalRouter
page
channel
channels
discover
home
livestreamSetup
odyseeMembership
redux
actions
reducers
selectors
scss
web/component/ads
|
@ -1,7 +1,7 @@
|
|||
// @flow
|
||||
import { selectClaimIdForUri } from 'redux/selectors/claims';
|
||||
|
||||
type State = { claims: any, stats: any };
|
||||
type State = { claims: any, stats: any, user: User };
|
||||
|
||||
const selectState = (state: State) => state.stats || {};
|
||||
export const selectViewCount = (state: State) => selectState(state).viewCountById;
|
||||
|
|
1
flow-typed/user.js
vendored
1
flow-typed/user.js
vendored
|
@ -32,4 +32,5 @@ declare type User = {
|
|||
odysee_live_enabled: boolean,
|
||||
odysee_live_disabled: boolean,
|
||||
global_mod: boolean,
|
||||
odyseeMembershipsPerClaimIds: ?{},
|
||||
};
|
||||
|
|
|
@ -11,7 +11,7 @@ import { doResolveUris } from 'redux/actions/claims';
|
|||
import * as SETTINGS from 'constants/settings';
|
||||
import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
|
||||
import { withRouter } from 'react-router';
|
||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
import { selectOdyseeMembershipIsPremiumPlus } from 'redux/selectors/user';
|
||||
import { selectClientSetting, selectShowMatureContent } from 'redux/selectors/settings';
|
||||
import { doFetchChannelLiveStatus } from 'redux/actions/livestream';
|
||||
import { selectActiveLivestreamForChannel, selectActiveLivestreamInitialized } from 'redux/selectors/livestream';
|
||||
|
@ -32,11 +32,11 @@ const select = (state, props) => {
|
|||
channelIsMine: selectClaimIsMine(state, claim),
|
||||
channelIsBlocked: makeSelectChannelIsMuted(props.uri)(state),
|
||||
claim,
|
||||
isAuthenticated: selectUserVerifiedEmail(state),
|
||||
showMature: selectShowMatureContent(state),
|
||||
tileLayout: selectClientSetting(state, SETTINGS.TILE_LAYOUT),
|
||||
activeLivestreamForChannel: selectActiveLivestreamForChannel(state, channelClaimId),
|
||||
activeLivestreamInitialized: selectActiveLivestreamInitialized(state),
|
||||
userHasPremiumPlus: selectOdyseeMembershipIsPremiumPlus(state),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -39,6 +39,7 @@ type Props = {
|
|||
doFetchChannelLiveStatus: (string) => void,
|
||||
activeLivestreamForChannel: any,
|
||||
activeLivestreamInitialized: boolean,
|
||||
userHasPremiumPlus: boolean,
|
||||
};
|
||||
|
||||
function ChannelContent(props: Props) {
|
||||
|
@ -49,7 +50,6 @@ function ChannelContent(props: Props) {
|
|||
channelIsBlocked,
|
||||
channelIsBlackListed,
|
||||
claim,
|
||||
isAuthenticated,
|
||||
defaultPageSize = CS.PAGE_SIZE,
|
||||
defaultInfiniteScroll = true,
|
||||
showMature,
|
||||
|
@ -61,8 +61,10 @@ function ChannelContent(props: Props) {
|
|||
doFetchChannelLiveStatus,
|
||||
activeLivestreamForChannel,
|
||||
activeLivestreamInitialized,
|
||||
userHasPremiumPlus,
|
||||
} = props;
|
||||
// const claimsInChannel = (claim && claim.meta.claims_in_channel) || 0;
|
||||
|
||||
const claimsInChannel = 9999;
|
||||
const [searchQuery, setSearchQuery] = React.useState('');
|
||||
const [isSearching, setIsSearching] = React.useState(false);
|
||||
|
@ -160,7 +162,7 @@ function ChannelContent(props: Props) {
|
|||
defaultOrderBy={CS.ORDER_BY_NEW}
|
||||
pageSize={defaultPageSize}
|
||||
infiniteScroll={defaultInfiniteScroll}
|
||||
injectedItem={SHOW_ADS && !isAuthenticated && { node: <Ads type="video" tileLayout={tileLayout} small /> }}
|
||||
injectedItem={SHOW_ADS && !userHasPremiumPlus && { node: <Ads type="video" tileLayout={tileLayout} small /> }}
|
||||
meta={
|
||||
showFilters && (
|
||||
<Form onSubmit={() => {}} className="wunderbar--inline">
|
||||
|
|
|
@ -1,16 +1,24 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectMyChannelClaims } from 'redux/selectors/claims';
|
||||
import { selectMyChannelClaims, selectClaimsByUri, selectOdyseeMembershipForUri } from 'redux/selectors/claims';
|
||||
import { selectActiveChannelClaim, selectIncognito } from 'redux/selectors/app';
|
||||
import { doSetActiveChannel, doSetIncognito } from 'redux/actions/app';
|
||||
import { doFetchUserMemberships } from 'redux/actions/user';
|
||||
import ChannelSelector from './view';
|
||||
|
||||
const select = (state) => ({
|
||||
const select = (state, props) => {
|
||||
const activeChannelClaim = selectActiveChannelClaim(state);
|
||||
|
||||
return {
|
||||
channels: selectMyChannelClaims(state),
|
||||
activeChannelClaim: selectActiveChannelClaim(state),
|
||||
activeChannelClaim,
|
||||
incognito: selectIncognito(state),
|
||||
});
|
||||
odyseeMembershipByUri: (uri) => selectOdyseeMembershipForUri(state, uri),
|
||||
claimsByUri: selectClaimsByUri(state),
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(select, {
|
||||
doSetActiveChannel,
|
||||
doSetIncognito,
|
||||
doFetchUserMemberships,
|
||||
})(ChannelSelector);
|
||||
|
|
|
@ -8,6 +8,8 @@ import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button';
|
|||
import ChannelTitle from 'component/channelTitle';
|
||||
import Icon from 'component/common/icon';
|
||||
import { useHistory } from 'react-router';
|
||||
import useGetUserMemberships from 'effects/use-get-user-memberships';
|
||||
import PremiumBadge from 'component/common/premium-badge';
|
||||
|
||||
type Props = {
|
||||
selectedChannelUrl: string, // currently selected channel
|
||||
|
@ -18,20 +20,32 @@ type Props = {
|
|||
doSetActiveChannel: (string) => void,
|
||||
incognito: boolean,
|
||||
doSetIncognito: (boolean) => void,
|
||||
claimsByUri: { [string]: any },
|
||||
doFetchUserMemberships: (claimIdCsv: string) => void,
|
||||
odyseeMembershipByUri: (uri: string) => string,
|
||||
};
|
||||
|
||||
type ListItemProps = {
|
||||
uri: string,
|
||||
isSelected?: boolean,
|
||||
claimsByUri: { [string]: any },
|
||||
doFetchUserMemberships: (claimIdCsv: string) => void,
|
||||
odyseeMembershipByUri: (uri: string) => string,
|
||||
};
|
||||
|
||||
function ChannelListItem(props: ListItemProps) {
|
||||
const { uri, isSelected = false } = props;
|
||||
const { uri, isSelected = false, claimsByUri, doFetchUserMemberships, odyseeMembershipByUri } = props;
|
||||
|
||||
const membership = odyseeMembershipByUri(uri);
|
||||
|
||||
const shouldFetchUserMemberships = true;
|
||||
useGetUserMemberships(shouldFetchUserMemberships, [uri], claimsByUri, doFetchUserMemberships, [uri]);
|
||||
|
||||
return (
|
||||
<div className={classnames('channel__list-item', { 'channel__list-item--selected': isSelected })}>
|
||||
<ChannelThumbnail uri={uri} hideStakedIndicator xsmall noLazyLoad />
|
||||
<ChannelTitle uri={uri} />
|
||||
<PremiumBadge membership={membership} />
|
||||
{isSelected && <Icon icon={ICONS.DOWN} />}
|
||||
</div>
|
||||
);
|
||||
|
@ -52,11 +66,23 @@ function IncognitoSelector(props: IncognitoSelectorProps) {
|
|||
}
|
||||
|
||||
function ChannelSelector(props: Props) {
|
||||
const { channels, activeChannelClaim, doSetActiveChannel, hideAnon = false, incognito, doSetIncognito } = props;
|
||||
const {
|
||||
channels,
|
||||
activeChannelClaim,
|
||||
doSetActiveChannel,
|
||||
hideAnon = false,
|
||||
incognito,
|
||||
doSetIncognito,
|
||||
odyseeMembershipByUri,
|
||||
claimsByUri,
|
||||
doFetchUserMemberships,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
push,
|
||||
location: { pathname },
|
||||
} = useHistory();
|
||||
|
||||
const activeChannelUrl = activeChannelClaim && activeChannelClaim.permanent_url;
|
||||
|
||||
function handleChannelSelect(channelClaim) {
|
||||
|
@ -71,14 +97,26 @@ function ChannelSelector(props: Props) {
|
|||
{(incognito && !hideAnon) || !activeChannelUrl ? (
|
||||
<IncognitoSelector isSelected />
|
||||
) : (
|
||||
<ChannelListItem uri={activeChannelUrl} isSelected />
|
||||
<ChannelListItem
|
||||
odyseeMembershipByUri={odyseeMembershipByUri}
|
||||
uri={activeChannelUrl}
|
||||
isSelected
|
||||
claimsByUri={claimsByUri}
|
||||
doFetchUserMemberships={doFetchUserMemberships}
|
||||
/>
|
||||
)}
|
||||
</MenuButton>
|
||||
|
||||
<MenuList className="menu__list channel__list">
|
||||
{channels &&
|
||||
channels.map((channel) => (
|
||||
<MenuItem key={channel.permanent_url} onSelect={() => handleChannelSelect(channel)}>
|
||||
<ChannelListItem uri={channel.permanent_url} />
|
||||
<ChannelListItem
|
||||
odyseeMembershipByUri={odyseeMembershipByUri}
|
||||
uri={channel.permanent_url}
|
||||
claimsByUri={claimsByUri}
|
||||
doFetchUserMemberships={doFetchUserMemberships}
|
||||
/>
|
||||
</MenuItem>
|
||||
))}
|
||||
{!hideAnon && (
|
||||
|
|
|
@ -1,14 +1,24 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectThumbnailForUri, selectClaimForUri, selectIsUriResolving } from 'redux/selectors/claims';
|
||||
import {
|
||||
selectThumbnailForUri,
|
||||
selectClaimForUri,
|
||||
selectIsUriResolving,
|
||||
selectClaimsByUri,
|
||||
selectOdyseeMembershipForUri,
|
||||
} from 'redux/selectors/claims';
|
||||
import { doResolveUri } from 'redux/actions/claims';
|
||||
import { doFetchUserMemberships } from 'redux/actions/user';
|
||||
import ChannelThumbnail from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
thumbnail: selectThumbnailForUri(state, props.uri),
|
||||
claim: selectClaimForUri(state, props.uri),
|
||||
isResolving: selectIsUriResolving(state, props.uri),
|
||||
odyseeMembership: selectOdyseeMembershipForUri(state, props.uri),
|
||||
claimsByUri: selectClaimsByUri(state),
|
||||
});
|
||||
|
||||
export default connect(select, {
|
||||
doResolveUri,
|
||||
doFetchUserMemberships,
|
||||
})(ChannelThumbnail);
|
||||
|
|
|
@ -4,9 +4,10 @@ import { parseURI } from 'util/lbryURI';
|
|||
import classnames from 'classnames';
|
||||
import Gerbil from './gerbil.png';
|
||||
import FreezeframeWrapper from 'component/fileThumbnail/FreezeframeWrapper';
|
||||
import ChannelStakedIndicator from 'component/channelStakedIndicator';
|
||||
import OptimizedImage from 'component/optimizedImage';
|
||||
import { AVATAR_DEFAULT } from 'config';
|
||||
import useGetUserMemberships from 'effects/use-get-user-memberships';
|
||||
import PremiumBadge from 'component/common/premium-badge';
|
||||
|
||||
type Props = {
|
||||
thumbnail: ?string,
|
||||
|
@ -26,6 +27,12 @@ type Props = {
|
|||
noOptimization?: boolean,
|
||||
setThumbUploadError: (boolean) => void,
|
||||
ThumbUploadError: boolean,
|
||||
claimsByUri: { [string]: any },
|
||||
odyseeMembership: string,
|
||||
doFetchUserMemberships: (claimIdCsv: string) => void,
|
||||
showMemberBadge?: boolean,
|
||||
isChannel?: boolean,
|
||||
checkMembership: boolean,
|
||||
};
|
||||
|
||||
function ChannelThumbnail(props: Props) {
|
||||
|
@ -42,10 +49,15 @@ function ChannelThumbnail(props: Props) {
|
|||
doResolveUri,
|
||||
isResolving,
|
||||
noLazyLoad,
|
||||
hideStakedIndicator = false,
|
||||
hideTooltip,
|
||||
setThumbUploadError,
|
||||
ThumbUploadError,
|
||||
claimsByUri,
|
||||
odyseeMembership,
|
||||
doFetchUserMemberships,
|
||||
showMemberBadge,
|
||||
isChannel,
|
||||
checkMembership = true,
|
||||
} = props;
|
||||
const [thumbLoadError, setThumbLoadError] = React.useState(ThumbUploadError);
|
||||
const shouldResolve = !isResolving && claim === undefined;
|
||||
|
@ -56,6 +68,16 @@ function ChannelThumbnail(props: Props) {
|
|||
const isGif = channelThumbnail && channelThumbnail.endsWith('gif');
|
||||
const showThumb = (!obscure && !!thumbnail) || thumbnailPreview;
|
||||
|
||||
const badgeProps = {
|
||||
membership: odyseeMembership,
|
||||
linkPage: isChannel,
|
||||
placement: isChannel ? 'bottom' : undefined,
|
||||
hideTooltip,
|
||||
className: isChannel ? 'profile-badge__tooltip' : undefined,
|
||||
};
|
||||
|
||||
useGetUserMemberships(checkMembership, [uri], claimsByUri, doFetchUserMemberships, [uri]);
|
||||
|
||||
// Generate a random color class based on the first letter of the channel name
|
||||
const { channelName } = parseURI(uri);
|
||||
let initializer;
|
||||
|
@ -76,7 +98,7 @@ function ChannelThumbnail(props: Props) {
|
|||
if (isGif && !allowGifs) {
|
||||
return (
|
||||
<FreezeframeWrapper src={channelThumbnail} className={classnames('channel-thumbnail', className)}>
|
||||
{!hideStakedIndicator && <ChannelStakedIndicator uri={uri} claim={claim} hideTooltip={hideTooltip} />}
|
||||
{showMemberBadge && <PremiumBadge {...badgeProps} />}
|
||||
</FreezeframeWrapper>
|
||||
);
|
||||
}
|
||||
|
@ -103,7 +125,7 @@ function ChannelThumbnail(props: Props) {
|
|||
}
|
||||
}}
|
||||
/>
|
||||
{!hideStakedIndicator && <ChannelStakedIndicator uri={uri} claim={claim} hideTooltip={hideTooltip} />}
|
||||
{showMemberBadge && <PremiumBadge {...badgeProps} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -58,6 +58,7 @@ type Props = {
|
|||
showEdit?: boolean,
|
||||
droppableProvided?: any,
|
||||
unavailableUris?: Array<string>,
|
||||
showMemberBadge?: boolean,
|
||||
};
|
||||
|
||||
export default function ClaimList(props: Props) {
|
||||
|
@ -96,6 +97,7 @@ export default function ClaimList(props: Props) {
|
|||
showEdit,
|
||||
droppableProvided,
|
||||
unavailableUris,
|
||||
showMemberBadge,
|
||||
} = props;
|
||||
|
||||
const [currentSort, setCurrentSort] = usePersistedState(persistedStorageKey, SORT_NEW);
|
||||
|
@ -196,6 +198,7 @@ export default function ClaimList(props: Props) {
|
|||
showEdit={showEdit}
|
||||
dragHandleProps={draggableProvided && draggableProvided.dragHandleProps}
|
||||
unavailableUris={unavailableUris}
|
||||
showMemberBadge={showMemberBadge}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import { doClaimSearch } from 'redux/actions/claims';
|
|||
import * as SETTINGS from 'constants/settings';
|
||||
import { selectFollowedTags } from 'redux/selectors/tags';
|
||||
import { selectMutedChannels } from 'redux/selectors/blocked';
|
||||
import { doFetchUserMemberships } from 'redux/actions/user';
|
||||
import { selectClientSetting, selectShowMatureContent, selectLanguage } from 'redux/selectors/settings';
|
||||
import { selectModerationBlockList } from 'redux/selectors/comments';
|
||||
import ClaimListDiscover from './view';
|
||||
|
@ -31,6 +32,7 @@ const select = (state, props) => ({
|
|||
const perform = {
|
||||
doClaimSearch,
|
||||
doFetchViewCount,
|
||||
doFetchUserMemberships,
|
||||
};
|
||||
|
||||
export default connect(select, perform)(ClaimListDiscover);
|
||||
|
|
|
@ -19,6 +19,7 @@ import LangFilterIndicator from 'component/langFilterIndicator';
|
|||
import ClaimListHeader from 'component/claimListHeader';
|
||||
import useFetchViewCount from 'effects/use-fetch-view-count';
|
||||
import { useIsLargeScreen } from 'effects/use-screensize';
|
||||
import useGetUserMemberships from 'effects/use-get-user-memberships';
|
||||
|
||||
type Props = {
|
||||
uris: Array<string>,
|
||||
|
@ -98,6 +99,7 @@ type Props = {
|
|||
// --- perform ---
|
||||
doClaimSearch: ({}) => void,
|
||||
doFetchViewCount: (claimIdCsv: string) => void,
|
||||
doFetchUserMemberships: (claimIdCsv: string) => void,
|
||||
|
||||
hideLayoutButton?: boolean,
|
||||
loadedCallback?: (number) => void,
|
||||
|
@ -177,6 +179,7 @@ function ClaimListDiscover(props: Props) {
|
|||
maxClaimRender,
|
||||
useSkeletonScreen = true,
|
||||
excludeUris = [],
|
||||
doFetchUserMemberships,
|
||||
swipeLayout = false,
|
||||
} = props;
|
||||
const didNavigateForward = history.action === 'PUSH';
|
||||
|
@ -608,9 +611,14 @@ function ClaimListDiscover(props: Props) {
|
|||
|
||||
// **************************************************************************
|
||||
// **************************************************************************
|
||||
|
||||
useFetchViewCount(fetchViewCount, renderUris, claimsByUri, doFetchViewCount);
|
||||
|
||||
const shouldFetchUserMemberships = true;
|
||||
const arrayOfContentUris = renderUris;
|
||||
const convertClaimUrlsToIds = claimsByUri;
|
||||
|
||||
useGetUserMemberships(shouldFetchUserMemberships, arrayOfContentUris, convertClaimUrlsToIds, doFetchUserMemberships);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (shouldPerformSearch) {
|
||||
const searchOptions = JSON.parse(optionsStringForEffect);
|
||||
|
|
|
@ -90,6 +90,7 @@ type Props = {
|
|||
showEdit?: boolean,
|
||||
dragHandleProps?: any,
|
||||
unavailableUris?: Array<string>,
|
||||
showMemberBadge?: boolean,
|
||||
};
|
||||
|
||||
const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||
|
@ -152,6 +153,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
showEdit,
|
||||
dragHandleProps,
|
||||
unavailableUris,
|
||||
showMemberBadge,
|
||||
} = props;
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
@ -366,7 +368,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
|
||||
{isChannelUri && claim ? (
|
||||
<UriIndicator focusable={false} uri={uri} link>
|
||||
<ChannelThumbnail uri={uri} small={type === 'inline'} />
|
||||
<ChannelThumbnail uri={uri} small={type === 'inline'} showMemberBadge={showMemberBadge} checkMembership={false} />
|
||||
</UriIndicator>
|
||||
) : (
|
||||
<>
|
||||
|
@ -411,11 +413,16 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
{!isChannelUri && signingChannel && (
|
||||
<div className="claim-preview__channel-staked">
|
||||
<UriIndicator focusable={false} uri={uri} link hideAnonymous>
|
||||
<ChannelThumbnail uri={signingChannel.permanent_url} xsmall />
|
||||
<ChannelThumbnail uri={signingChannel.permanent_url} xsmall showMemberBadge={showMemberBadge} checkMembership={false} />
|
||||
</UriIndicator>
|
||||
</div>
|
||||
)}
|
||||
<ClaimPreviewSubtitle uri={uri} type={type} showAtSign={isChannelUri} />
|
||||
<ClaimPreviewSubtitle
|
||||
uri={uri}
|
||||
type={type}
|
||||
showAtSign={isChannelUri}
|
||||
showMemberBadge={!showMemberBadge}
|
||||
/>
|
||||
{(pending || !!reflectingProgress) && <PublishPending uri={uri} />}
|
||||
{channelSubscribers}
|
||||
</div>
|
||||
|
|
|
@ -21,11 +21,24 @@ type Props = {
|
|||
lang: string,
|
||||
fetchSubCount: (string) => void,
|
||||
subCount: number,
|
||||
showMemberBadge?: boolean,
|
||||
};
|
||||
|
||||
// previews used in channel overview and homepage (and other places?)
|
||||
function ClaimPreviewSubtitle(props: Props) {
|
||||
const { pending, uri, claim, type, beginPublish, isLivestream, fetchSubCount, subCount, showAtSign, lang } = props;
|
||||
const {
|
||||
pending,
|
||||
uri,
|
||||
claim,
|
||||
type,
|
||||
beginPublish,
|
||||
isLivestream,
|
||||
fetchSubCount,
|
||||
subCount,
|
||||
showAtSign,
|
||||
lang,
|
||||
showMemberBadge,
|
||||
} = props;
|
||||
const isChannel = claim && claim.value_type === 'channel';
|
||||
const claimsInChannel = (claim && claim.meta.claims_in_channel) || 0;
|
||||
|
||||
|
@ -47,7 +60,7 @@ function ClaimPreviewSubtitle(props: Props) {
|
|||
<div className="media__subtitle">
|
||||
{claim ? (
|
||||
<React.Fragment>
|
||||
<UriIndicator uri={uri} showAtSign={showAtSign} link />{' '}
|
||||
<UriIndicator uri={uri} showAtSign={showAtSign} showMemberBadge={showMemberBadge} link />{' '}
|
||||
{!pending && claim && (
|
||||
<>
|
||||
{isChannel && type !== 'inline' && (
|
||||
|
|
|
@ -243,7 +243,7 @@ function ClaimPreviewTile(props: Props) {
|
|||
) : (
|
||||
<React.Fragment>
|
||||
<UriIndicator focusable={false} uri={uri} link hideAnonymous>
|
||||
<ChannelThumbnail uri={channelUri} xsmall />
|
||||
<ChannelThumbnail uri={channelUri} xsmall checkMembership={false} />
|
||||
</UriIndicator>
|
||||
|
||||
<div className="claim-tile__about">
|
||||
|
|
|
@ -3,6 +3,7 @@ import { connect } from 'react-redux';
|
|||
import { withRouter } from 'react-router';
|
||||
import { selectClaimSearchByQuery, selectFetchingClaimSearchByQuery, selectClaimsByUri } from 'redux/selectors/claims';
|
||||
import { doClaimSearch } from 'redux/actions/claims';
|
||||
import { doFetchUserMemberships } from 'redux/actions/user';
|
||||
import * as SETTINGS from 'constants/settings';
|
||||
import { MATURE_TAGS } from 'constants/tags';
|
||||
import { doFetchViewCount } from 'lbryinc';
|
||||
|
@ -37,6 +38,7 @@ const select = (state, props) => {
|
|||
const perform = {
|
||||
doClaimSearch,
|
||||
doFetchViewCount,
|
||||
doFetchUserMemberships,
|
||||
};
|
||||
|
||||
export default withRouter(connect(select, perform)(ClaimListDiscover));
|
||||
|
|
|
@ -4,6 +4,7 @@ import React from 'react';
|
|||
import ClaimPreviewTile from 'component/claimPreviewTile';
|
||||
import useFetchViewCount from 'effects/use-fetch-view-count';
|
||||
import useLastVisibleItem from 'effects/use-last-visible-item';
|
||||
import useGetUserMemberships from 'effects/use-get-user-memberships';
|
||||
|
||||
function urisEqual(prev: ?Array<string>, next: ?Array<string>) {
|
||||
if (!prev || !next) {
|
||||
|
@ -56,6 +57,7 @@ type Props = {
|
|||
// --- perform ---
|
||||
doClaimSearch: ({}) => void,
|
||||
doFetchViewCount: (claimIdCsv: string) => void,
|
||||
doFetchUserMemberships: (claimIdCsv: string) => void,
|
||||
};
|
||||
|
||||
function ClaimTilesDiscover(props: Props) {
|
||||
|
@ -74,6 +76,7 @@ function ClaimTilesDiscover(props: Props) {
|
|||
doFetchViewCount,
|
||||
pageSize = 8,
|
||||
optionsStringified,
|
||||
doFetchUserMemberships,
|
||||
} = props;
|
||||
|
||||
// reference to the claim-grid
|
||||
|
@ -117,6 +120,10 @@ function ClaimTilesDiscover(props: Props) {
|
|||
// populate the view counts for the current claim uris
|
||||
useFetchViewCount(fetchViewCount, uris, claimsByUri, doFetchViewCount);
|
||||
|
||||
const shouldFetchUserMemberships = true;
|
||||
|
||||
useGetUserMemberships(shouldFetchUserMemberships, uris, claimsByUri, doFetchUserMemberships);
|
||||
|
||||
// Run `doClaimSearch`
|
||||
React.useEffect(() => {
|
||||
if (shouldPerformSearch) {
|
||||
|
|
|
@ -5,12 +5,12 @@ import {
|
|||
selectThumbnailForUri,
|
||||
selectHasChannels,
|
||||
selectMyClaimIdsRaw,
|
||||
selectOdyseeMembershipForUri,
|
||||
} from 'redux/selectors/claims';
|
||||
import { doCommentUpdate, doCommentList } from 'redux/actions/comments';
|
||||
import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
|
||||
import { doToast } from 'redux/actions/notifications';
|
||||
import { doClearPlayingUri } from 'redux/actions/content';
|
||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
import {
|
||||
selectLinkedCommentAncestors,
|
||||
selectOthersReactsForComment,
|
||||
|
@ -18,6 +18,9 @@ import {
|
|||
} from 'redux/selectors/comments';
|
||||
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||
import { selectPlayingUri } from 'redux/selectors/content';
|
||||
import {
|
||||
selectUserVerifiedEmail,
|
||||
} from 'redux/selectors/user';
|
||||
import Comment from './view';
|
||||
|
||||
const select = (state, props) => {
|
||||
|
@ -33,7 +36,7 @@ const select = (state, props) => {
|
|||
claim: makeSelectClaimForUri(uri)(state),
|
||||
thumbnail: channel_url && selectThumbnailForUri(state, channel_url),
|
||||
channelIsBlocked: channel_url && makeSelectChannelIsMuted(channel_url)(state),
|
||||
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
|
||||
commentingEnabled: Boolean(selectUserVerifiedEmail(state)),
|
||||
othersReacts: selectOthersReactsForComment(state, reactionKey),
|
||||
activeChannelClaim,
|
||||
hasChannels: selectHasChannels(state),
|
||||
|
@ -41,6 +44,7 @@ const select = (state, props) => {
|
|||
stakedLevel: selectStakedLevelForChannelUri(state, channel_url),
|
||||
linkedCommentAncestors: selectLinkedCommentAncestors(state),
|
||||
totalReplyPages: makeSelectTotalReplyPagesForParentId(comment_id)(state),
|
||||
selectOdyseeMembershipForUri: channel_url && selectOdyseeMembershipForUri(state, channel_url),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@ import OptimizedImage from 'component/optimizedImage';
|
|||
import { getChannelFromClaim } from 'util/claim';
|
||||
import { parseSticker } from 'util/comments';
|
||||
import { useIsMobile } from 'effects/use-screensize';
|
||||
import PremiumBadge from 'component/common/premium-badge';
|
||||
|
||||
const AUTO_EXPAND_ALL_REPLIES = false;
|
||||
|
||||
|
@ -64,6 +65,7 @@ type Props = {
|
|||
supportDisabled: boolean,
|
||||
setQuickReply: (any) => void,
|
||||
quickReply: any,
|
||||
selectOdyseeMembershipForUri: string,
|
||||
};
|
||||
|
||||
const LENGTH_TO_COLLAPSE = 300;
|
||||
|
@ -93,6 +95,7 @@ function CommentView(props: Props) {
|
|||
supportDisabled,
|
||||
setQuickReply,
|
||||
quickReply,
|
||||
selectOdyseeMembershipForUri,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
|
@ -256,18 +259,15 @@ function CommentView(props: Props) {
|
|||
>
|
||||
<div className="comment__thumbnail-wrapper">
|
||||
{authorUri ? (
|
||||
<ChannelThumbnail uri={authorUri} obscure={channelIsBlocked} xsmall className="comment__author-thumbnail" />
|
||||
<ChannelThumbnail uri={authorUri} obscure={channelIsBlocked} xsmall className="comment__author-thumbnail" checkMembership={false} />
|
||||
) : (
|
||||
<ChannelThumbnail xsmall className="comment__author-thumbnail" />
|
||||
<ChannelThumbnail xsmall className="comment__author-thumbnail" checkMembership={false} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="comment__body-container">
|
||||
<div className="comment__meta">
|
||||
<div className="comment__meta-information">
|
||||
{isGlobalMod && <CommentBadge label={__('Admin')} icon={ICONS.BADGE_MOD} />}
|
||||
{isModerator && <CommentBadge label={__('Moderator')} icon={ICONS.BADGE_MOD} />}
|
||||
|
||||
{!author ? (
|
||||
<span className="comment__author">{__('Anonymous')}</span>
|
||||
) : (
|
||||
|
@ -277,9 +277,13 @@ function CommentView(props: Props) {
|
|||
})}
|
||||
link
|
||||
uri={authorUri}
|
||||
comment
|
||||
showAtSign
|
||||
/>
|
||||
)}
|
||||
{isGlobalMod && <CommentBadge label={__('Admin')} icon={ICONS.BADGE_ADMIN} />}
|
||||
{isModerator && <CommentBadge label={__('Moderator')} icon={ICONS.BADGE_MOD} />}
|
||||
<PremiumBadge membership={selectOdyseeMembershipForUri} linkPage />
|
||||
<Button
|
||||
className="comment__time"
|
||||
onClick={handleTimeClick}
|
||||
|
@ -358,6 +362,7 @@ function CommentView(props: Props) {
|
|||
promptLinks
|
||||
parentCommentId={commentId}
|
||||
stakedLevel={stakedLevel}
|
||||
hasMembership={selectOdyseeMembershipForUri}
|
||||
/>
|
||||
</Expandable>
|
||||
) : (
|
||||
|
@ -366,6 +371,7 @@ function CommentView(props: Props) {
|
|||
promptLinks
|
||||
parentCommentId={commentId}
|
||||
stakedLevel={stakedLevel}
|
||||
hasMembership={selectOdyseeMembershipForUri}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectClaimForUri, selectClaimIsMine, selectFetchingMyChannels } from 'redux/selectors/claims';
|
||||
import { selectClaimForUri,
|
||||
selectClaimIsMine,
|
||||
selectFetchingMyChannels,
|
||||
selectClaimsByUri,
|
||||
} from 'redux/selectors/claims';
|
||||
import {
|
||||
selectTopLevelCommentsForUri,
|
||||
makeSelectTopLevelTotalPagesForUri,
|
||||
|
@ -16,6 +20,7 @@ import {
|
|||
import { doCommentReset, doCommentList, doCommentById, doCommentReactList } from 'redux/actions/comments';
|
||||
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||
import { getChannelIdFromClaim } from 'util/claim';
|
||||
import { doFetchUserMemberships } from 'redux/actions/user';
|
||||
import CommentsList from './view';
|
||||
|
||||
const select = (state, props) => {
|
||||
|
@ -41,6 +46,7 @@ const select = (state, props) => {
|
|||
myReactsByCommentId: selectMyReacts(state),
|
||||
othersReactsById: selectOthersReacts(state),
|
||||
activeChannelId: activeChannelClaim && activeChannelClaim.claim_id,
|
||||
claimsByUri: selectClaimsByUri(state),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -49,6 +55,7 @@ const perform = {
|
|||
fetchComment: doCommentById,
|
||||
fetchReacts: doCommentReactList,
|
||||
resetComments: doCommentReset,
|
||||
doFetchUserMemberships,
|
||||
};
|
||||
|
||||
export default connect(select, perform)(CommentsList);
|
||||
|
|
|
@ -15,6 +15,7 @@ import Empty from 'component/common/empty';
|
|||
import React, { useEffect } from 'react';
|
||||
import Spinner from 'component/spinner';
|
||||
import usePersistedState from 'effects/use-persisted-state';
|
||||
import useGetUserMemberships from 'effects/use-get-user-memberships';
|
||||
|
||||
const DEBOUNCE_SCROLL_HANDLER_MS = 200;
|
||||
|
||||
|
@ -50,6 +51,8 @@ type Props = {
|
|||
fetchComment: (commentId: string) => void,
|
||||
fetchReacts: (commentIds: Array<string>) => Promise<any>,
|
||||
resetComments: (claimId: string) => void,
|
||||
claimsByUri: { [string]: any },
|
||||
doFetchUserMemberships: (claimIdCsv: string) => void,
|
||||
};
|
||||
|
||||
export default function CommentList(props: Props) {
|
||||
|
@ -76,6 +79,8 @@ export default function CommentList(props: Props) {
|
|||
fetchComment,
|
||||
fetchReacts,
|
||||
resetComments,
|
||||
claimsByUri,
|
||||
doFetchUserMemberships,
|
||||
} = props;
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
@ -100,6 +105,22 @@ export default function CommentList(props: Props) {
|
|||
Boolean(othersReactsById) || !ENABLE_COMMENT_REACTIONS
|
||||
);
|
||||
|
||||
// get commenter claim ids for checking premium status
|
||||
const commenterClaimIds = topLevelComments.map(function(comment) {
|
||||
return comment.channel_id;
|
||||
});
|
||||
|
||||
// update premium status
|
||||
const shouldFetchUserMemberships = true;
|
||||
useGetUserMemberships(
|
||||
shouldFetchUserMemberships,
|
||||
commenterClaimIds,
|
||||
claimsByUri,
|
||||
doFetchUserMemberships,
|
||||
[topLevelComments],
|
||||
true,
|
||||
);
|
||||
|
||||
function changeSort(newSort) {
|
||||
if (sort !== newSort) {
|
||||
setSort(newSort);
|
||||
|
|
|
@ -1,35 +1,38 @@
|
|||
// @flow
|
||||
import 'scss/component/_comment-badge.scss';
|
||||
|
||||
import classnames from 'classnames';
|
||||
import Icon from 'component/common/icon';
|
||||
import React from 'react';
|
||||
import Tooltip from 'component/common/tooltip';
|
||||
|
||||
const LABEL_TYPES = {
|
||||
ADMIN: 'Admin',
|
||||
MOD: 'Moderator',
|
||||
};
|
||||
|
||||
type Props = {
|
||||
icon: string,
|
||||
label: string,
|
||||
size?: number,
|
||||
placement?: string,
|
||||
hideTooltip?: boolean,
|
||||
className?: string,
|
||||
};
|
||||
|
||||
export default function CommentBadge(props: Props) {
|
||||
const { icon, label, size = 20 } = props;
|
||||
const { icon, label, size = 20, placement = 'top', hideTooltip, className } = props;
|
||||
|
||||
return (
|
||||
<Tooltip title={label} placement="top">
|
||||
<span
|
||||
className={classnames('comment__badge', {
|
||||
'comment__badge--globalMod': label === LABEL_TYPES.ADMIN,
|
||||
'comment__badge--mod': label === LABEL_TYPES.MOD,
|
||||
})}
|
||||
>
|
||||
<BadgeWrapper title={label} placement={placement} hideTooltip={hideTooltip} className={className}>
|
||||
<span className="comment__badge">
|
||||
<Icon icon={icon} size={size} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</BadgeWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
type WrapperProps = {
|
||||
hideTooltip?: boolean,
|
||||
children: any,
|
||||
};
|
||||
|
||||
const BadgeWrapper = (props: WrapperProps) => {
|
||||
const { hideTooltip, children, ...tooltipProps } = props;
|
||||
|
||||
return !hideTooltip ? <Tooltip {...tooltipProps}>{children}</Tooltip> : children;
|
||||
};
|
||||
|
|
|
@ -2537,21 +2537,58 @@ export const icons = {
|
|||
viewBox="0 0 24 24"
|
||||
xmlSpace="preserve"
|
||||
>
|
||||
<style type="text/css">{'.st0{fill:FF3850}.st1{fill:#181021}.st2{fill:#FFFFFF}'}</style>
|
||||
<style type="text/css">
|
||||
{'.st0--badge-mod{fill:#ff3850}.st1--badge-mod{fill:#181021}.st2--badge-mod{fill:#FFFFFF}'}
|
||||
</style>
|
||||
<g>
|
||||
<g>
|
||||
<path
|
||||
className="st0"
|
||||
className="st0--badge-mod"
|
||||
d="M11.69,6.77c4.86,0,7.55,0.9,8.52,1.31c1.29-1.46,3.28-4.14,3.28-6.76c0,0-4.17,4.86-6.92,5.12 c-1.25-0.87-2.77-1.38-4.41-1.38c0,0-3.21-0.06-4.63,1.31C4.81,6.44,0.51,1.32,0.51,1.32c0,2.61,1.97,5.27,3.25,6.74 C4.71,7.59,7.03,6.77,11.69,6.77z M19.87,19.38c0.02-0.13,0.04-0.27,0.04-0.4V12.8c0-1.03-0.21-2.02-0.58-2.92 c-0.83-0.33-3.25-1.11-7.64-1.11c-4.29,0-6.33,0.75-7,1.06c-0.38,0.91-0.6,1.91-0.6,2.97v6.18c0,0.13,0.02,0.26,0.04,0.39 C1.6,19.73,0,22.54,0,22.54L12,24l12-1.46C24,22.54,22.36,19.79,19.87,19.38z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<path
|
||||
className="st1"
|
||||
className="st1--badge-mod"
|
||||
d="M13,18.57H11c-2.27,0-4.12-0.82-4.12-2.88v-2.46c0-2.77,2.17-3.94,5.11-3.94s5.11,1.17,5.11,3.94v2.46 C17.11,17.75,15.27,18.57,13,18.57z"
|
||||
/>
|
||||
<path
|
||||
className="st2"
|
||||
className="st2--badge-mod"
|
||||
d="M15.06,15.25c-0.28,0-0.5-0.22-0.5-0.5v-1.42c0-0.32,0-1.31-1.63-1.31c-0.28,0-0.5-0.22-0.5-0.5 s0.22-0.5,0.5-0.5c1.65,0,2.63,0.86,2.63,2.31v1.42C15.56,15.02,15.33,15.25,15.06,15.25z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
[ICONS.BADGE_ADMIN]: (props: IconProps) => (
|
||||
<svg
|
||||
{...props}
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
x="0px"
|
||||
y="0px"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
xmlSpace="preserve"
|
||||
>
|
||||
<style type="text/css">
|
||||
{'.st0--badge-admin{fill:#fe7500}.st1--badge-admin{fill:#181021}.st2--badge-admin{fill:#FFFFFF}'}
|
||||
</style>
|
||||
<g>
|
||||
<g>
|
||||
<path
|
||||
className="st0--badge-admin"
|
||||
d="M11.69,6.77c4.86,0,7.55,0.9,8.52,1.31c1.29-1.46,3.28-4.14,3.28-6.76c0,0-4.17,4.86-6.92,5.12 c-1.25-0.87-2.77-1.38-4.41-1.38c0,0-3.21-0.06-4.63,1.31C4.81,6.44,0.51,1.32,0.51,1.32c0,2.61,1.97,5.27,3.25,6.74 C4.71,7.59,7.03,6.77,11.69,6.77z M19.87,19.38c0.02-0.13,0.04-0.27,0.04-0.4V12.8c0-1.03-0.21-2.02-0.58-2.92 c-0.83-0.33-3.25-1.11-7.64-1.11c-4.29,0-6.33,0.75-7,1.06c-0.38,0.91-0.6,1.91-0.6,2.97v6.18c0,0.13,0.02,0.26,0.04,0.39 C1.6,19.73,0,22.54,0,22.54L12,24l12-1.46C24,22.54,22.36,19.79,19.87,19.38z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<path
|
||||
className="st1--badge-admin"
|
||||
d="M13,18.57H11c-2.27,0-4.12-0.82-4.12-2.88v-2.46c0-2.77,2.17-3.94,5.11-3.94s5.11,1.17,5.11,3.94v2.46 C17.11,17.75,15.27,18.57,13,18.57z"
|
||||
/>
|
||||
<path
|
||||
className="st2--badge-admin"
|
||||
d="M15.06,15.25c-0.28,0-0.5-0.22-0.5-0.5v-1.42c0-0.32,0-1.31-1.63-1.31c-0.28,0-0.5-0.22-0.5-0.5 s0.22-0.5,0.5-0.5c1.65,0,2.63,0.86,2.63,2.31v1.42C15.56,15.02,15.33,15.25,15.06,15.25z"
|
||||
/>
|
||||
</svg>
|
||||
|
@ -2570,26 +2607,30 @@ export const icons = {
|
|||
viewBox="-1182 401 24 24"
|
||||
xmlSpace="preserve"
|
||||
>
|
||||
<style type="text/css">{'.st0{fill:#FF5490}.st1{fill:#81BBB9}.st2{fill:#2E2A2F}.st3{fill:#FFFFFF}'}</style>
|
||||
<style type="text/css">
|
||||
{
|
||||
'.st0--badge-streamer{fill:#FF5490}.st1--badge-streamer{fill:#81BBB9}.st2--badge-streamer{fill:#2E2A2F}.st3--badge-streamer{fill:#FFFFFF}'
|
||||
}
|
||||
</style>
|
||||
<path
|
||||
className="st0"
|
||||
className="st0--badge-streamer"
|
||||
d="M-1169.8,406.4c-4.3,0-7.8,3.5-7.8,7.8c0,0.4,0,0.8,0.1,1.1h1c-0.1-0.4-0.1-0.7-0.1-1.1c0-3.7,3-6.8,6.8-6.8 s6.8,3,6.8,6.8c0,0.4,0,0.8-0.1,1.1h1c0.1-0.4,0.1-0.7,0.1-1.1C-1162.1,409.9-1165.5,406.4-1169.8,406.4z"
|
||||
/>
|
||||
<path
|
||||
className="st0"
|
||||
className="st0--badge-streamer"
|
||||
d="M-1180,414.2c0-5.6,4.6-10.2,10.2-10.2c5.6,0,10.2,4.6,10.2,10.2c0,2.2-0.7,4.3-1.9,5.9l0.8,0.6 c1.3-1.8,2.1-4.1,2.1-6.5c0-6.2-5-11.2-11.2-11.2c-6.2,0-11.2,5-11.2,11.2c0,2.1,0.6,4.1,1.6,5.8l1-0.3 C-1179.4,418-1180,416.2-1180,414.2z"
|
||||
/>
|
||||
<path className="st1" d="M-1163.7,419.4" />
|
||||
<path className="st1--badge-streamer" d="M-1163.7,419.4" />
|
||||
<path
|
||||
className="st1"
|
||||
className="st1--badge-streamer"
|
||||
d="M-1165.6,418.5c0-0.1,0-3.6,0-3.6c0-1.9-1-4.3-4.4-4.3s-4.4,2.4-4.4,4.3c0,0,0,3.6,0,3.6 c-1.4,0.2-1.8,0.7-1.8,0.7s2.2,2.7,6.2,2.7s6.2-2.7,6.2-2.7S-1164.2,418.7-1165.6,418.5z"
|
||||
/>
|
||||
<path
|
||||
className="st2"
|
||||
className="st2--badge-streamer"
|
||||
d="M-1169.2,418.5h-1.5c-1.7,0-3.1-0.6-3.1-2.2v-1.9c0-2.1,1.6-3,3.9-3s3.9,0.9,3.9,3v1.9 C-1166.1,417.8-1167.5,418.5-1169.2,418.5z"
|
||||
/>
|
||||
<path
|
||||
className="st3"
|
||||
className="st3--badge-streamer"
|
||||
d="M-1167.8,416.2c-0.2,0-0.4-0.2-0.4-0.4v-1.1c0-0.2,0-1-1.2-1c-0.2,0-0.4-0.2-0.4-0.4s0.2-0.4,0.4-0.4 c1.2,0,2,0.6,2,1.7v1.1C-1167.4,416.1-1167.6,416.2-1167.8,416.2z"
|
||||
/>
|
||||
</svg>
|
||||
|
@ -2717,6 +2758,368 @@ export const icons = {
|
|||
<path d="M23.5,12.11a7,7,0,0,1-3.27,5.59.26.26,0,0,1-.32,0,.27.27,0,0,1-.05-.31A2.71,2.71,0,0,0,20,17c.65-1.4.5-2.85-.34-3.25s-2,.41-2.67,1.77c.06-.93-.26-1.7-.86-1.88-1.27-.4-1.77,1.24-4.17,5.44-2.44-4.27-2.9-5.84-4.17-5.44-.6.18-.92.95-.86,1.88-.66-1.36-1.84-2.15-2.67-1.77S3.31,15.63,4,17a2.71,2.71,0,0,0,.18.34.27.27,0,0,1,0,.31.26.26,0,0,1-.32,0A7,7,0,0,1,.5,12.11C.5,8.93,3.17,6.18,7,4.9a.25.25,0,0,1,.32.3L7,6.73a3.37,3.37,0,0,0,.78,3,1,1,0,0,0,1.1.28,1,1,0,0,0,.65-.94V5.61a.25.25,0,0,1,.4-.2l1.6,1.2h1l1.6-1.2a.25.25,0,0,1,.4.2V9.05a1,1,0,0,0,.65.94,1,1,0,0,0,1.1-.28,3.35,3.35,0,0,0,.78-3L16.65,5.2A.25.25,0,0,1,17,4.9C20.83,6.18,23.5,8.93,23.5,12.11Z" />
|
||||
</g>
|
||||
),
|
||||
[ICONS.EARLY_ACCESS]: (props: CustomProps) => (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 35 30"
|
||||
width={'40'}
|
||||
height={'40'}
|
||||
fill="none"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<style type="text/css">
|
||||
{
|
||||
'.early-access--st0{fill:none;stroke:#DCBDA2;stroke-width:1.4173;stroke-miterlimit:10;}.early-access--st1{fill:#DCBEA2;}'
|
||||
}
|
||||
</style>
|
||||
|
||||
<circle className="early-access--st0" cx="304.9" cy="346.6" r="13.7" />
|
||||
<g>
|
||||
<ellipse className="early-access--st0" cx="301.2" cy="346.5" rx="3.5" ry="3.8" />
|
||||
<line className="early-access--st0" x1="304.7" y1="346.5" x2="312.5" y2="346.5" />
|
||||
<line className="early-access--st0" x1="310.3" y1="346.6" x2="310.3" y2="349.3" />
|
||||
</g>
|
||||
<circle className="early-access--st0" cx="304.9" cy="390.6" r="13.7" />
|
||||
<path
|
||||
className="early-access--st0"
|
||||
d="M316.2,296.7v6.4c0,0.9-0.5,1.8-1.3,2.3l-9,5.2c-0.8,0.5-1.8,0.5-2.6,0l-9-5.2c-0.8-0.5-1.3-1.3-1.3-2.3v-10.4c0-0.9,0.5-1.8,1.3-2.3l9-5.2c0.8-0.5,1.8-0.5,2.6,0l9,5.2"
|
||||
/>
|
||||
<polyline
|
||||
className="early-access--st0"
|
||||
points="318.7,290.8 304.4,306.8 301.3,301.3 295.8,298.2 301.3,295 304.4,289.5 307.5,295 "
|
||||
/>
|
||||
<polyline className="early-access--st0" points="299,310.2 299,316.4 304.8,313.1 309.9,316.4 309.9,310.2 " />
|
||||
<line className="early-access--st0" x1="314.7" y1="380.8" x2="295.1" y2="400.5" />
|
||||
<text
|
||||
transform="matrix(1 0 0 1 294.7307 394.0567)"
|
||||
style={{ fill: '#DCBDA2', 'fontFamily': 'Roboto-Bold', 'fontSize': '10.1968px' }}
|
||||
>
|
||||
ADS
|
||||
</text>
|
||||
<g id="XMLID_53_">
|
||||
<g id="XMLID_493_">
|
||||
<path
|
||||
id="XMLID_494_"
|
||||
className="early-access--st1"
|
||||
d="M16,1.6C8,1.6,1.6,8,1.6,16S8,30.4,16,30.4S30.4,24,30.4,16S24,1.6,16,1.6z M16,28.9C8.9,28.9,3.1,23.1,3.1,16S8.9,3.1,16,3.1S28.9,8.9,28.9,16S23.1,28.9,16,28.9z M12.3,11.4c-2.3,0-4.2,2-4.2,4.5s1.9,4.5,4.2,4.5c2.1,0,3.8-1.6,4.1-3.8h4.2v2h1.5v-2h1.4v-1.5h-7.2C16.1,13,14.4,11.4,12.3,11.4z M12.3,18.9c-1.5,0-2.7-1.3-2.7-3s1.2-3,2.7-3c1.5,0,2.7,1.3,2.7,3S13.8,18.9,12.3,18.9z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
[ICONS.MEMBER_BADGE]: (props: CustomProps) => (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 35 30"
|
||||
width={'40'}
|
||||
height={'40'}
|
||||
fill="none"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<style type="text/css">
|
||||
{
|
||||
'.member-bage--st0{fill:none;stroke:#DCBDA2;stroke-width:1.4173;stroke-miterlimit:10;}.member-bage--st1{fill:#DCBEA2;}'
|
||||
}
|
||||
</style>
|
||||
<circle className="member-bage--st0" cx="304.9" cy="399.4" r="13.7" />
|
||||
<g>
|
||||
<ellipse className="member-bage--st0" cx="301.2" cy="399.3" rx="3.5" ry="3.8" />
|
||||
<line className="member-bage--st0" x1="304.7" y1="399.3" x2="312.5" y2="399.3" />
|
||||
<line className="member-bage--st0" x1="310.3" y1="399.3" x2="310.3" y2="402" />
|
||||
</g>
|
||||
<circle className="member-bage--st0" cx="304.9" cy="443.4" r="13.7" />
|
||||
<path
|
||||
className="member-bage--st0"
|
||||
d="M316.2,349.5v6.4c0,0.9-0.5,1.8-1.3,2.3l-9,5.2c-0.8,0.5-1.8,0.5-2.6,0l-9-5.2c-0.8-0.5-1.3-1.3-1.3-2.3v-10.4
|
||||
c0-0.9,0.5-1.8,1.3-2.3l9-5.2c0.8-0.5,1.8-0.5,2.6,0l9,5.2"
|
||||
/>
|
||||
<polyline
|
||||
className="member-bage--st0"
|
||||
points="318.7,343.5 304.4,359.6 301.3,354 295.8,350.9 301.3,347.8 304.4,342.2 307.5,347.8 "
|
||||
/>
|
||||
<polyline className="member-bage--st0" points="299,363 299,369.1 304.8,365.9 309.9,369.1 309.9,363 " />
|
||||
<line className="member-bage--st0" x1="314.7" y1="433.6" x2="295.1" y2="453.2" />
|
||||
<text
|
||||
transform="matrix(1 0 0 1 294.7307 446.8067)"
|
||||
style={{ fill: '#DCBDA2', 'fontFamily': 'Roboto-Bold', 'fontSize': '10.1968px' }}
|
||||
>
|
||||
ADS
|
||||
</text>
|
||||
<g id="XMLID_187_">
|
||||
<g id="XMLID_250_">
|
||||
<path
|
||||
id="XMLID_251_"
|
||||
className="member-bage--st1"
|
||||
d="M26.7,19c0,0.7-0.4,1.3-0.9,1.6l-9,5.2c-0.6,0.3-1.3,0.3-1.9,0l-9-5.2C5.3,20.3,5,19.6,5,19
|
||||
V8.6c0-0.7,0.4-1.3,0.9-1.6l9-5.2c0.6-0.3,1.3-0.3,1.9,0l9,5.2l0.8-1.3l-9-5.2c-1-0.6-2.3-0.6-3.4,0l-9,5.2
|
||||
c-1,0.6-1.7,1.7-1.7,2.9V19c0,1.2,0.6,2.3,1.7,2.9l9,5.2c0.5,0.3,1.1,0.5,1.7,0.5s1.2-0.2,1.7-0.5l9-5.2c1-0.6,1.7-1.7,1.7-2.9
|
||||
v-6.4h-1.5V19z M13.1,16.5L8.5,14l4.6-2.6l2.6-4.6l2.5,4.4l1.3-0.7l-3.8-6.7L12,10.3L5.5,14l6.5,3.7l3.5,6.3l15-16.8l-1.1-1
|
||||
L15.8,21.3L13.1,16.5z M20.4,29.7L16,27.8l-4.7,2v-4.1H9.8v6.3l6.2-2.5l5.9,2.6v-6.3h-1.5V29.7z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
[ICONS.NO_ADS]: (props: CustomProps) => (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 35 30"
|
||||
width={'40'}
|
||||
height={'40'}
|
||||
fill="none"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<style type="text/css">
|
||||
{'.st0--no-ads{fill:none;stroke:#DCBDA2;stroke-width:1.4173;stroke-miterlimit:10;}.st1--no-ads{fill:#DCBEA2;}'}
|
||||
</style>
|
||||
<circle className="st0--no-ads" cx="304.9" cy="297.6" r="13.7" />
|
||||
<g>
|
||||
<ellipse className="st0--no-ads" cx="301.2" cy="297.5" rx="3.5" ry="3.8" />
|
||||
<line className="st0--no-ads" x1="304.7" y1="297.5" x2="312.5" y2="297.5" />
|
||||
<line className="st0--no-ads" x1="310.3" y1="297.6" x2="310.3" y2="300.3" />
|
||||
</g>
|
||||
<circle className="st0--no-ads" cx="304.9" cy="341.6" r="13.7" />
|
||||
<path
|
||||
className="st0--no-ads"
|
||||
d="M316.2,247.7v6.4c0,0.9-0.5,1.8-1.3,2.3l-9,5.2c-0.8,0.5-1.8,0.5-2.6,0l-9-5.2c-0.8-0.5-1.3-1.3-1.3-2.3v-10.4
|
||||
c0-0.9,0.5-1.8,1.3-2.3l9-5.2c0.8-0.5,1.8-0.5,2.6,0l9,5.2"
|
||||
/>
|
||||
<polyline
|
||||
className="st0--no-ads"
|
||||
points="318.7,241.8 304.4,257.8 301.3,252.3 295.8,249.2 301.3,246 304.4,240.5 307.5,246 "
|
||||
/>
|
||||
<polyline className="st0--no-ads" points="299,261.2 299,267.4 304.8,264.1 309.9,267.4 309.9,261.2 " />
|
||||
<line className="st0--no-ads" x1="314.7" y1="331.8" x2="295.1" y2="351.5" />
|
||||
<text
|
||||
transform="matrix(1 0 0 1 294.7307 345.0567)"
|
||||
style={{ fill: '#DCBDA2', 'fontFamily': 'Roboto-Bold', 'fontSize': '10.1968px' }}
|
||||
>
|
||||
ADS
|
||||
</text>
|
||||
<g id="XMLID_109_">
|
||||
<path
|
||||
id="XMLID_190_"
|
||||
className="st1--no-ads"
|
||||
d="M16,1.6C8,1.6,1.6,8,1.6,16S8,30.4,16,30.4S30.4,24,30.4,16S24,1.6,16,1.6z M16,3.1
|
||||
c3.3,0,6.3,1.3,8.6,3.3L18,13c-0.2-0.1-0.3-0.3-0.5-0.4c-0.5-0.3-1.1-0.4-1.7-0.4h-2.2v5.3l-1.2,1.2l-2.4-6.4H8.5l-2.7,7.2h1.6
|
||||
l0.5-1.5h2.6l0.5,1.5h0.5l-5.2,5.2c-2-2.3-3.3-5.3-3.3-8.6C3.1,8.9,8.9,3.1,16,3.1z M17.6,15.6V16c0,0.7-0.2,1.3-0.5,1.6
|
||||
s-0.8,0.6-1.3,0.6H15V18l2.5-2.5C17.6,15.5,17.6,15.6,17.6,15.6z M15,15.9v-2.5h0.7c0.6,0,1,0.2,1.3,0.5L15,15.9z M10.2,16.7H8.3
|
||||
L9.2,14L10.2,16.7z M16,28.9c-3.3,0-6.3-1.3-8.6-3.3l6.2-6.2h2.1c0.6,0,1.2-0.1,1.7-0.4c0.5-0.3,0.9-0.7,1.2-1.2
|
||||
c0.3-0.5,0.4-1.1,0.4-1.8v-0.3c0-0.5-0.1-0.9-0.3-1.4l6.8-6.8c2,2.3,3.3,5.3,3.3,8.6C28.9,23.1,23.1,28.9,16,28.9z"
|
||||
/>
|
||||
<path
|
||||
id="XMLID_195_"
|
||||
className="st1--no-ads"
|
||||
d="M23.2,15.1c-0.5-0.1-0.8-0.3-1-0.4c-0.2-0.2-0.4-0.4-0.4-0.6c0-0.3,0.1-0.5,0.3-0.6
|
||||
c0.2-0.2,0.5-0.2,0.9-0.2c0.4,0,0.7,0.1,0.9,0.3c0.2,0.2,0.3,0.4,0.3,0.8h1.5c0-0.4-0.1-0.8-0.3-1.2s-0.5-0.6-0.9-0.8
|
||||
c-0.4-0.2-0.9-0.3-1.4-0.3c-0.5,0-1,0.1-1.4,0.3s-0.7,0.4-1,0.7c-0.2,0.3-0.3,0.7-0.3,1c0,0.8,0.4,1.4,1.2,1.8
|
||||
c0.3,0.2,0.7,0.3,1.2,0.5c0.5,0.2,0.9,0.3,1.1,0.5c0.2,0.2,0.3,0.4,0.3,0.6c0,0.3-0.1,0.5-0.3,0.6c-0.2,0.1-0.5,0.2-0.8,0.2
|
||||
c-1,0-1.4-0.4-1.4-1.2h-1.5c0,0.5,0.1,0.9,0.4,1.2c0.2,0.4,0.6,0.6,1,0.8s1,0.3,1.5,0.3c0.8,0,1.4-0.2,1.9-0.5s0.7-0.8,0.7-1.5
|
||||
c0-0.6-0.2-1-0.6-1.4C24.6,15.7,24,15.4,23.2,15.1z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
[ICONS.PREMIUM]: (props: CustomProps) => (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 35 30"
|
||||
width={props.size || '40'}
|
||||
height={props.size || '40'}
|
||||
fill="none"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<style type="text/css">
|
||||
{'.premium--st0{fill:#898DB3;}'}
|
||||
{'.premium--st1{fill:#D8D2E8;}'}
|
||||
{'.premium--st2{fill:#CAC2DF;}'}
|
||||
{'.premium--st3{opacity:0.27;fill:#74749A;}'}
|
||||
{'.premium--st4{fill:none;stroke:#CAC2DF;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;}'}
|
||||
{'.premium--st5{fill:#626092;}.premium--st6{opacity:0.2;fill:#FFFFFF;}'}
|
||||
</style>
|
||||
|
||||
<path
|
||||
id="XMLID_122_"
|
||||
className="premium--st0"
|
||||
d="M0,12.7v0.8c0,2.3,2,4.2,4.4,4.2h23.2c2.4,0,4.4-1.9,4.4-4.2v-0.8H0z"
|
||||
/>
|
||||
<path
|
||||
id="XMLID_20_"
|
||||
className="premium--st1"
|
||||
d="M1.8,14.4c0.1,0.3,0.3,0.6,0.5,0.8c0.5,0.6,1.2,0.9,2.1,0.9h23.2c0.8,0,1.6-0.4,2.1-0.9
|
||||
c0.2-0.2,0.4-0.5,0.5-0.8H1.8z"
|
||||
/>
|
||||
<path
|
||||
id="XMLID_27_"
|
||||
className="premium--st2"
|
||||
d="M2.3,15.2c0.5,0.6,1.2,0.9,2.1,0.9h23.2c0.8,0,1.6-0.4,2.1-0.9H2.3z"
|
||||
/>
|
||||
<rect id="XMLID_28_" x="5.2" y="12.7" className="premium--st3" width="21.7" height="5.1" />
|
||||
<path
|
||||
id="XMLID_125_"
|
||||
className="premium--st0"
|
||||
d="M1.4,16.1v0.8c0,2.3,2,4.2,4.4,4.2h20.4c2.4,0,4.4-1.9,4.4-4.2v-0.8H1.4z"
|
||||
/>
|
||||
<path
|
||||
id="XMLID_237_"
|
||||
className="premium--st1"
|
||||
d="M3.2,17.8c0.1,0.3,0.3,0.6,0.5,0.8c0.5,0.6,1.2,0.9,2.1,0.9h20.4c0.8,0,1.6-0.4,2.1-0.9
|
||||
c0.2-0.2,0.4-0.5,0.5-0.8H3.2z"
|
||||
/>
|
||||
<path
|
||||
id="XMLID_120_"
|
||||
className="premium--st2"
|
||||
d="M3.7,18.6c0.5,0.6,1.2,0.9,2.1,0.9h20.4c0.8,0,1.6-0.4,2.1-0.9H3.7z"
|
||||
/>
|
||||
<rect id="XMLID_29_" x="5.2" y="16.1" className="premium--st3" width="21.7" height="5.1" />
|
||||
<path id="XMLID_75_" className="premium--st4" d="M6.4,14.3" />
|
||||
<path id="XMLID_25_" className="premium--st4" d="M2.7,14.3" />
|
||||
<path
|
||||
id="XMLID_124_"
|
||||
className="premium--st0"
|
||||
d="M25.1,10.4l-7.9-4.6c-0.7-0.4-1.6-0.4-2.3,0l-7.9,4.6c-0.7,0.4-1.2,1.2-1.2,2v9.1
|
||||
c0,0.8,0.4,1.6,1.2,2l7.9,4.6c0.4,0.2,0.8,0.3,1.2,0.3c0.4,0,0.8-0.1,1.2-0.3l7.9-4.6c0.7-0.4,1.2-1.2,1.2-2v-9.1
|
||||
C26.2,11.6,25.8,10.8,25.1,10.4z"
|
||||
/>
|
||||
<path
|
||||
id="XMLID_123_"
|
||||
className="premium--st2"
|
||||
d="M16.3,7.3c-0.1-0.1-0.2-0.1-0.3-0.1c-0.1,0-0.2,0-0.3,0.1l-7.9,4.6c-0.2,0.1-0.3,0.3-0.3,0.5
|
||||
v9.1c0,0.2,0.1,0.4,0.3,0.5l7.9,4.6c0.2,0.1,0.4,0.1,0.6,0l7.9-4.6c0.2-0.1,0.3-0.3,0.3-0.5v-9.1c0-0.2-0.1-0.4-0.3-0.5L16.3,7.3z"
|
||||
/>
|
||||
<polygon
|
||||
id="XMLID_19_"
|
||||
className="premium--st5"
|
||||
points="20.5,20.1 11.6,20.1 11.2,14.3 14.2,15.8 16,12.4 17.9,15.8 21,14.2 "
|
||||
/>
|
||||
<polygon
|
||||
id="XMLID_18_"
|
||||
className="premium--st6"
|
||||
points="16.1,20.8 21.1,20.8 21.7,13.2 18.1,14.9 16.1,11.3 16.1,7.8 23.9,12.3 23.9,21.6 16.1,25.9 "
|
||||
/>
|
||||
<polygon id="XMLID_17_" className="premium--st6" points="16.1,13.4 16.1,19.6 20.1,19.6 20.5,15.2 17.8,16.8 " />
|
||||
</svg>
|
||||
),
|
||||
[ICONS.PREMIUM_PLUS]: (props: CustomProps) => (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 35 30"
|
||||
width={props.size || '40'}
|
||||
height={props.size || '40'}
|
||||
fill="none"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<style type="text/css">
|
||||
{'.premium-plus--st0{fill:#C36017;}'}
|
||||
{'.premium-plus--st1{fill:#FAC65D;}'}
|
||||
{'.premium-plus--st2{fill:#F9B915;}'}
|
||||
{'.premium-plus--st3{opacity:0.3;fill:#955000;}'}
|
||||
{'.premium-plus--st4{fill:none;stroke:#CAC2DF;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;}'}
|
||||
{'.premium-plus--st5{fill:#C95B16;}'}
|
||||
{'.premium-plus--st6{opacity:0.2;fill:#FFFFFF;}'}
|
||||
</style>
|
||||
|
||||
<path
|
||||
id="XMLID_141_"
|
||||
className="premium-plus--st0"
|
||||
d="M0,11.7v0.8c0,2.3,2,4.2,4.4,4.2h23.2c2.4,0,4.4-1.9,4.4-4.2v-0.8H0z"
|
||||
/>
|
||||
<path
|
||||
id="XMLID_133_"
|
||||
className="premium-plus--st1"
|
||||
d="M1.8,13.4c0.1,0.3,0.3,0.6,0.5,0.8c0.5,0.6,1.2,0.9,2.1,0.9h23.2c0.8,0,1.6-0.4,2.1-0.9c0.2-0.2,0.4-0.5,0.5-0.8H1.8z"
|
||||
/>
|
||||
<path
|
||||
id="XMLID_139_"
|
||||
className="premium-plus--st2"
|
||||
d="M2.3,14.2c0.5,0.6,1.2,0.9,2.1,0.9h23.2c0.8,0,1.6-0.4,2.1-0.9H2.3z"
|
||||
/>
|
||||
<path
|
||||
id="XMLID_136_"
|
||||
className="premium-plus--st0"
|
||||
d="M1.4,15.1v0.8c0,2.3,2,4.2,4.4,4.2h20.4c2.4,0,4.4-1.9,4.4-4.2v-0.8H1.4z"
|
||||
/>
|
||||
<path
|
||||
id="XMLID_131_"
|
||||
className="premium-plus--st1"
|
||||
d="M3.2,16.8c0.1,0.3,0.3,0.6,0.5,0.8c0.5,0.6,1.2,0.9,2.1,0.9h20.4c0.8,0,1.6-0.4,2.1-0.9c0.2-0.2,0.4-0.5,0.5-0.8H3.2z"
|
||||
/>
|
||||
<path
|
||||
id="XMLID_134_"
|
||||
className="premium-plus--st2"
|
||||
d="M3.7,17.6c0.5,0.6,1.2,0.9,2.1,0.9h20.4c0.8,0,1.6-0.4,2.1-0.9H3.7z"
|
||||
/>
|
||||
<path
|
||||
id="XMLID_260_"
|
||||
className="premium-plus--st0"
|
||||
d="M2.6,18.6v0.8c0,2.3,2,4.2,4.4,4.2h18.2c2.4,0,4.1-1.9,4.1-4.2v-0.8H2.6z"
|
||||
/>
|
||||
<path
|
||||
id="XMLID_137_"
|
||||
className="premium-plus--st3"
|
||||
d="M7.4,23.6h17.4c0.7,0,1.9-1.6,1.9-2.3l0.1-9.6H5.3v9.7C5.3,22.1,6.7,23.6,7.4,23.6z"
|
||||
/>
|
||||
<path
|
||||
id="XMLID_257_"
|
||||
className="premium-plus--st1"
|
||||
d="M4.5,20.3c0.1,0.3,0.3,0.6,0.5,0.8C5.5,21.6,6.2,22,7,22h18.2c0.8,0,1.2-0.4,1.7-0.9c0.2-0.2,0.4-0.5,0.5-0.8H4.5z"
|
||||
/>
|
||||
<path
|
||||
id="XMLID_254_"
|
||||
className="premium-plus--st2"
|
||||
d="M5,21.1C5.5,21.6,6.2,22,7,22h18.2c0.8,0,1.2-0.4,1.7-0.9H5z"
|
||||
/>
|
||||
<path id="XMLID_130_" className="premium-plus--st4" d="M6.4,14.3" />
|
||||
<path id="XMLID_129_" className="premium-plus--st4" d="M2.7,14.3" />
|
||||
<path
|
||||
id="XMLID_128_"
|
||||
className="premium-plus--st0"
|
||||
d="M25.1,10.4l-7.9-4.6c-0.7-0.4-1.6-0.4-2.3,0l-7.9,4.6c-0.7,0.4-1.2,1.2-1.2,2v9.1c0,0.8,0.4,1.6,1.2,2l7.9,4.6c0.4,0.2,0.8,0.3,1.2,0.3c0.4,0,0.8-0.1,1.2-0.3l7.9-4.6c0.7-0.4,1.2-1.2,1.2-2v-9.1C26.2,11.6,25.8,10.8,25.1,10.4z"
|
||||
/>
|
||||
<path
|
||||
id="XMLID_127_"
|
||||
className="premium-plus--st2"
|
||||
d="M16.3,7.3c-0.1-0.1-0.2-0.1-0.3-0.1c-0.1,0-0.2,0-0.3,0.1l-7.9,4.6c-0.2,0.1-0.3,0.3-0.3,0.5v9.1c0,0.2,0.1,0.4,0.3,0.5l7.9,4.6c0.2,0.1,0.4,0.1,0.6,0l7.9-4.6c0.2-0.1,0.3-0.3,0.3-0.5v-9.1c0-0.2-0.1-0.4-0.3-0.5L16.3,7.3z"
|
||||
/>
|
||||
<polygon
|
||||
id="XMLID_126_"
|
||||
className="premium-plus--st5"
|
||||
points="20.5,20.1 11.6,20.1 11.2,14.3 14.2,15.8 16,12.4 17.9,15.8 21,14.2 "
|
||||
/>
|
||||
<polygon
|
||||
id="XMLID_24_"
|
||||
className="premium-plus--st6"
|
||||
points="16.1,20.8 21.1,20.8 21.7,13.2 18.1,14.9 16.1,11.3 16.1,7.8 23.9,12.3 23.9,21.616.1,25.9 "
|
||||
/>
|
||||
<polygon
|
||||
id="XMLID_23_"
|
||||
className="premium-plus--st6"
|
||||
points="16.1,13.4 16.1,19.6 20.1,19.6 20.5,15.2 17.8,16.8 "
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
[ICONS.UPGRADE]: buildIcon(
|
||||
<g>
|
||||
<path d="m2 6 10-5 10 5M2 6v12l10 5 10-5V6" />
|
||||
<circle cx={12} cy={10} r={5.25} />
|
||||
<path d="M8.5 14.5 6 17h3l1.5 2.5 1-4h1l1 4L15 17h3l-2-2.5" />
|
||||
</g>
|
||||
),
|
||||
[ICONS.FEATURED]: (props: IconProps) => {
|
||||
const { size = 24, color = 'currentColor', ...otherProps } = props;
|
||||
|
||||
|
|
|
@ -58,6 +58,7 @@ type MarkdownProps = {
|
|||
disableTimestamps?: boolean,
|
||||
stakedLevel?: number,
|
||||
setUserMention?: (boolean) => void,
|
||||
hasMembership?: string,
|
||||
};
|
||||
|
||||
// ****************************************************************************
|
||||
|
@ -156,6 +157,7 @@ export default React.memo<MarkdownProps>(function MarkdownPreview(props: Markdow
|
|||
disableTimestamps,
|
||||
stakedLevel,
|
||||
setUserMention,
|
||||
hasMembership,
|
||||
} = props;
|
||||
|
||||
const strippedContent = content
|
||||
|
@ -189,7 +191,7 @@ export default React.memo<MarkdownProps>(function MarkdownPreview(props: Markdow
|
|||
parentCommentId={parentCommentId}
|
||||
isMarkdownPost={isMarkdownPost}
|
||||
simpleLinks={simpleLinks}
|
||||
allowPreview={isStakeEnoughForPreview(stakedLevel)}
|
||||
allowPreview={isStakeEnoughForPreview(stakedLevel) || hasMembership}
|
||||
setUserMention={setUserMention}
|
||||
/>
|
||||
),
|
||||
|
@ -198,7 +200,7 @@ export default React.memo<MarkdownProps>(function MarkdownPreview(props: Markdow
|
|||
img: (imgProps) => {
|
||||
const imageCdnUrl =
|
||||
getThumbnailCdnUrl({ thumbnail: imgProps.src, width: 0, height: 0, quality: 85 }) || MISSING_THUMB_DEFAULT;
|
||||
if (isStakeEnoughForPreview(stakedLevel) && !isEmote(imgProps.title, imgProps.src)) {
|
||||
if ((isStakeEnoughForPreview(stakedLevel) || hasMembership) && !isEmote(imgProps.title, imgProps.src)) {
|
||||
return <ZoomableImage {...imgProps} src={imageCdnUrl} />;
|
||||
} else {
|
||||
return (
|
||||
|
@ -206,7 +208,7 @@ export default React.memo<MarkdownProps>(function MarkdownPreview(props: Markdow
|
|||
src={imageCdnUrl}
|
||||
alt={imgProps.alt}
|
||||
title={imgProps.title}
|
||||
helpText={__("This channel isn't staking enough Credits for inline image previews.")}
|
||||
helpText={__('Odysee Premium required to enable image previews')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
52
ui/component/common/premium-badge.jsx
Normal file
52
ui/component/common/premium-badge.jsx
Normal file
|
@ -0,0 +1,52 @@
|
|||
// @flow
|
||||
import 'scss/component/_comment-badge.scss';
|
||||
|
||||
import * as ICONS from 'constants/icons';
|
||||
import React from 'react';
|
||||
import CommentBadge from './comment-badge';
|
||||
import Button from 'component/button';
|
||||
|
||||
type Props = {
|
||||
membership: ?string,
|
||||
linkPage?: boolean,
|
||||
placement?: string,
|
||||
className?: string,
|
||||
hideTooltip?: boolean,
|
||||
};
|
||||
|
||||
export default function PremiumBadge(props: Props) {
|
||||
const { membership, linkPage, placement, className, hideTooltip } = props;
|
||||
|
||||
const badgeToShow = membership === 'Premium' ? 'silver' : membership === 'Premium+' ? 'gold' : null;
|
||||
|
||||
if (!badgeToShow) return null;
|
||||
|
||||
const badgeProps = { size: 40, placement, hideTooltip, className };
|
||||
|
||||
return (
|
||||
<BadgeWrapper linkPage={linkPage}>
|
||||
{badgeToShow === 'silver' ? (
|
||||
<CommentBadge label={__('Premium')} icon={ICONS.PREMIUM} {...badgeProps} />
|
||||
) : (
|
||||
badgeToShow === 'gold' && <CommentBadge label={__('Premium +')} icon={ICONS.PREMIUM_PLUS} {...badgeProps} />
|
||||
)}
|
||||
</BadgeWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
type WrapperProps = {
|
||||
linkPage?: boolean,
|
||||
children: any,
|
||||
};
|
||||
|
||||
const BadgeWrapper = (props: WrapperProps) => {
|
||||
const { linkPage, children } = props;
|
||||
|
||||
return linkPage ? (
|
||||
<Button navigate="/$/membership">
|
||||
{children}
|
||||
</Button>
|
||||
) : (
|
||||
children
|
||||
);
|
||||
};
|
|
@ -9,6 +9,7 @@ type Props = {
|
|||
disableInteractive?: boolean,
|
||||
enterDelay?: number,
|
||||
title?: string | Node,
|
||||
className?: string,
|
||||
followCursor?: boolean,
|
||||
placement?: string, // https://mui.com/api/tooltip/
|
||||
};
|
||||
|
@ -20,6 +21,7 @@ function Tooltip(props: Props) {
|
|||
disableInteractive = true,
|
||||
enterDelay = 300,
|
||||
title,
|
||||
className,
|
||||
followCursor = false,
|
||||
placement = 'bottom',
|
||||
} = props;
|
||||
|
@ -33,6 +35,7 @@ function Tooltip(props: Props) {
|
|||
title={title}
|
||||
followCursor={followCursor}
|
||||
placement={placement}
|
||||
classes={{ tooltip: className }}
|
||||
>
|
||||
{children}
|
||||
</MUITooltip>
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doSetClientSetting } from 'redux/actions/settings';
|
||||
import { selectActiveChannelStakedLevel } from 'redux/selectors/app';
|
||||
import { selectClientSetting } from 'redux/selectors/settings';
|
||||
import * as SETTINGS from 'constants/settings';
|
||||
import HeaderMenuButtons from './view';
|
||||
import { selectUserVerifiedEmail, selectUser } from 'redux/selectors/user';
|
||||
import { selectUserVerifiedEmail, selectUser, selectOdyseeMembershipName } from 'redux/selectors/user';
|
||||
import { doOpenModal } from 'redux/actions/app';
|
||||
|
||||
const select = (state) => ({
|
||||
activeChannelStakedLevel: selectActiveChannelStakedLevel(state),
|
||||
authenticated: selectUserVerifiedEmail(state),
|
||||
automaticDarkModeEnabled: selectClientSetting(state, SETTINGS.AUTOMATIC_DARK_MODE_ENABLED),
|
||||
currentTheme: selectClientSetting(state, SETTINGS.THEME),
|
||||
user: selectUser(state),
|
||||
odyseeMembership: selectOdyseeMembershipName(state),
|
||||
});
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
|
@ -19,6 +19,7 @@ const perform = (dispatch) => ({
|
|||
if (automaticDarkModeEnabled) dispatch(doSetClientSetting(SETTINGS.AUTOMATIC_DARK_MODE_ENABLED, false));
|
||||
dispatch(doSetClientSetting(SETTINGS.THEME, currentTheme === 'dark' ? 'light' : 'dark', true));
|
||||
},
|
||||
doOpenModal: (id, params) => dispatch(doOpenModal(id, params)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(HeaderMenuButtons);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// @flow
|
||||
import 'scss/component/_header.scss';
|
||||
|
||||
import { ENABLE_UI_NOTIFICATIONS, ENABLE_NO_SOURCE_CLAIMS, CHANNEL_STAKED_LEVEL_LIVESTREAM } from 'config';
|
||||
import { ENABLE_UI_NOTIFICATIONS, ENABLE_NO_SOURCE_CLAIMS } from 'config';
|
||||
import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import * as PAGES from 'constants/pages';
|
||||
|
@ -18,25 +18,17 @@ type HeaderMenuButtonProps = {
|
|||
currentTheme: string,
|
||||
user: ?User,
|
||||
handleThemeToggle: (boolean, string) => void,
|
||||
doOpenModal: (string, {}) => void,
|
||||
odyseeMembership: string,
|
||||
};
|
||||
|
||||
export default function HeaderMenuButtons(props: HeaderMenuButtonProps) {
|
||||
const {
|
||||
authenticated,
|
||||
automaticDarkModeEnabled,
|
||||
currentTheme,
|
||||
activeChannelStakedLevel,
|
||||
user,
|
||||
handleThemeToggle,
|
||||
} = props;
|
||||
const { authenticated, automaticDarkModeEnabled, currentTheme, user, handleThemeToggle, odyseeMembership } = props;
|
||||
|
||||
const isOnMembershipPage = window.location.pathname === `/$/${PAGES.ODYSEE_MEMBERSHIP}`;
|
||||
|
||||
const notificationsEnabled = ENABLE_UI_NOTIFICATIONS || (user && user.experimental_ui);
|
||||
const livestreamEnabled = Boolean(
|
||||
ENABLE_NO_SOURCE_CLAIMS &&
|
||||
user &&
|
||||
!user.odysee_live_disabled &&
|
||||
(activeChannelStakedLevel >= CHANNEL_STAKED_LEVEL_LIVESTREAM || user.odysee_live_enabled)
|
||||
);
|
||||
const livestreamEnabled = Boolean(ENABLE_NO_SOURCE_CLAIMS && user && !user.odysee_live_disabled);
|
||||
|
||||
return (
|
||||
<div className="header__buttons">
|
||||
|
@ -68,6 +60,10 @@ export default function HeaderMenuButtons(props: HeaderMenuButtonProps) {
|
|||
|
||||
<MenuList className="menu__list--header">
|
||||
<HeaderMenuLink page={PAGES.SETTINGS} icon={ICONS.SETTINGS} name={__('Settings')} />
|
||||
{/* don't show upgrade button if on membership page or already have a membership */}
|
||||
{!isOnMembershipPage && !odyseeMembership && (
|
||||
<HeaderMenuLink page={PAGES.ODYSEE_MEMBERSHIP} icon={ICONS.UPGRADE} name={__('Odysee Premium')} />
|
||||
)}
|
||||
<HeaderMenuLink page={PAGES.HELP} icon={ICONS.HELP} name={__('Help')} />
|
||||
|
||||
<MenuItem className="menu__link" onSelect={() => handleThemeToggle(automaticDarkModeEnabled, currentTheme)}>
|
||||
|
|
|
@ -35,14 +35,13 @@ export default function HeaderProfileMenuButton(props: HeaderMenuButtonProps) {
|
|||
) : (
|
||||
<MenuButton
|
||||
aria-label={__('Your account')}
|
||||
title={__('Your account')}
|
||||
className={classnames('header__navigationItem', {
|
||||
'header__navigationItem--icon': !activeChannelUrl,
|
||||
'header__navigationItem--profilePic': activeChannelUrl,
|
||||
})}
|
||||
>
|
||||
{activeChannelUrl ? (
|
||||
<ChannelThumbnail uri={activeChannelUrl} hideTooltip small noLazyLoad />
|
||||
<ChannelThumbnail uri={activeChannelUrl} hideTooltip small noLazyLoad showMemberBadge />
|
||||
) : (
|
||||
<Icon size={18} icon={ICONS.ACCOUNT} aria-hidden />
|
||||
)}
|
||||
|
@ -57,6 +56,7 @@ export default function HeaderProfileMenuButton(props: HeaderMenuButtonProps) {
|
|||
<HeaderMenuLink page={PAGES.CREATOR_DASHBOARD} icon={ICONS.ANALYTICS} name={__('Creator Analytics')} />
|
||||
<HeaderMenuLink page={PAGES.REWARDS} icon={ICONS.REWARDS} name={__('Rewards')} />
|
||||
<HeaderMenuLink page={PAGES.INVITE} icon={ICONS.INVITE} name={__('Invites')} />
|
||||
<HeaderMenuLink page={PAGES.ODYSEE_MEMBERSHIP} icon={ICONS.UPGRADE} name={__('Odysee Premium')} />
|
||||
|
||||
<MenuItem onSelect={signOut}>
|
||||
<div className="menu__link">
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { MAX_LIVESTREAM_COMMENTS } from 'constants/livestream';
|
||||
import { doResolveUris } from 'redux/actions/claims';
|
||||
import { selectClaimForUri } from 'redux/selectors/claims';
|
||||
import { selectClaimForUri, selectClaimsByUri } from 'redux/selectors/claims';
|
||||
import { doCommentList, doSuperChatList } from 'redux/actions/comments';
|
||||
import {
|
||||
selectTopLevelCommentsForUri,
|
||||
|
@ -9,6 +9,7 @@ import {
|
|||
selectPinnedCommentsForUri,
|
||||
} from 'redux/selectors/comments';
|
||||
import { selectThemePath } from 'redux/selectors/settings';
|
||||
import { doFetchUserMemberships } from 'redux/actions/user';
|
||||
import LivestreamChatLayout from './view';
|
||||
|
||||
const select = (state, props) => {
|
||||
|
@ -21,6 +22,7 @@ const select = (state, props) => {
|
|||
pinnedComments: selectPinnedCommentsForUri(state, uri),
|
||||
superChats: selectSuperChatsForUri(state, uri),
|
||||
theme: selectThemePath(state),
|
||||
claimsByUri: selectClaimsByUri(state),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -28,6 +30,7 @@ const perform = {
|
|||
doCommentList,
|
||||
doSuperChatList,
|
||||
doResolveUris,
|
||||
doFetchUserMemberships,
|
||||
};
|
||||
|
||||
export default connect(select, perform)(LivestreamChatLayout);
|
||||
|
|
|
@ -18,6 +18,7 @@ import React from 'react';
|
|||
import Yrbl from 'component/yrbl';
|
||||
import { getTipValues } from 'util/livestream';
|
||||
import Slide from '@mui/material/Slide';
|
||||
import useGetUserMemberships from 'effects/use-get-user-memberships';
|
||||
|
||||
export const VIEW_MODES = {
|
||||
CHAT: 'chat',
|
||||
|
@ -49,6 +50,8 @@ type Props = {
|
|||
) => void,
|
||||
doResolveUris: (uris: Array<string>, cache: boolean) => void,
|
||||
doSuperChatList: (uri: string) => void,
|
||||
claimsByUri: { [string]: any },
|
||||
doFetchUserMemberships: (claimIdCsv: string) => void,
|
||||
};
|
||||
|
||||
export default function LivestreamChatLayout(props: Props) {
|
||||
|
@ -68,6 +71,8 @@ export default function LivestreamChatLayout(props: Props) {
|
|||
doCommentList,
|
||||
doResolveUris,
|
||||
doSuperChatList,
|
||||
doFetchUserMemberships,
|
||||
claimsByUri,
|
||||
} = props;
|
||||
|
||||
const isMobile = useIsMobile() && !isPopoutWindow;
|
||||
|
@ -96,6 +101,22 @@ export default function LivestreamChatLayout(props: Props) {
|
|||
superChatsByChronologicalOrder.sort((a, b) => b.timestamp - a.timestamp);
|
||||
}
|
||||
|
||||
// get commenter claim ids for checking premium status
|
||||
const commenterClaimIds = commentsByChronologicalOrder.map(function(comment) {
|
||||
return comment.channel_id;
|
||||
});
|
||||
|
||||
// update premium status
|
||||
const shouldFetchUserMemberships = true;
|
||||
useGetUserMemberships(
|
||||
shouldFetchUserMemberships,
|
||||
commenterClaimIds,
|
||||
claimsByUri,
|
||||
doFetchUserMemberships,
|
||||
[commentsByChronologicalOrder],
|
||||
true,
|
||||
);
|
||||
|
||||
const commentsToDisplay =
|
||||
viewMode === VIEW_MODES.CHAT ? commentsByChronologicalOrder : superChatsByChronologicalOrder;
|
||||
const commentsLength = commentsToDisplay && commentsToDisplay.length;
|
||||
|
|
|
@ -1,16 +1,27 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectStakedLevelForChannelUri, selectClaimForUri, selectMyClaimIdsRaw } from 'redux/selectors/claims';
|
||||
import {
|
||||
selectStakedLevelForChannelUri,
|
||||
selectClaimForUri,
|
||||
selectMyClaimIdsRaw,
|
||||
selectClaimsByUri,
|
||||
selectOdyseeMembershipForChannelId,
|
||||
} from 'redux/selectors/claims';
|
||||
|
||||
import LivestreamComment from './view';
|
||||
|
||||
const select = (state, props) => {
|
||||
const { uri, comment } = props;
|
||||
const { channel_url: authorUri } = comment;
|
||||
const { channel_url: authorUri, channel_id: channelId } = comment;
|
||||
|
||||
return {
|
||||
claim: selectClaimForUri(state, uri),
|
||||
stakedLevel: selectStakedLevelForChannelUri(state, authorUri),
|
||||
myChannelIds: selectMyClaimIdsRaw(state),
|
||||
claimsByUri: selectClaimsByUri(state),
|
||||
odyseeMembership: selectOdyseeMembershipForChannelId(state, channelId),
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(select)(LivestreamComment);
|
||||
const perform = {};
|
||||
|
||||
export default connect(select, perform)(LivestreamComment);
|
||||
|
|
|
@ -17,6 +17,7 @@ import Icon from 'component/common/icon';
|
|||
import MarkdownPreview from 'component/common/markdown-preview';
|
||||
import OptimizedImage from 'component/optimizedImage';
|
||||
import React from 'react';
|
||||
import PremiumBadge from 'component/common/premium-badge';
|
||||
|
||||
type Props = {
|
||||
comment: Comment,
|
||||
|
@ -27,8 +28,10 @@ type Props = {
|
|||
myChannelIds: ?Array<string>,
|
||||
stakedLevel: number,
|
||||
isMobile?: boolean,
|
||||
odyseeMembership: string,
|
||||
handleDismissPin?: () => void,
|
||||
restoreScrollPos?: () => void,
|
||||
claimsByUri: { [string]: any },
|
||||
};
|
||||
|
||||
export default function LivestreamComment(props: Props) {
|
||||
|
@ -42,6 +45,7 @@ export default function LivestreamComment(props: Props) {
|
|||
isMobile,
|
||||
handleDismissPin,
|
||||
restoreScrollPos,
|
||||
odyseeMembership,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
|
@ -98,10 +102,6 @@ export default function LivestreamComment(props: Props) {
|
|||
{supportAmount > 0 && <ChannelThumbnail uri={authorUri} xsmall />}
|
||||
|
||||
<div className="livestreamComment__info">
|
||||
{isGlobalMod && <CommentBadge label={__('Admin')} icon={ICONS.BADGE_MOD} size={16} />}
|
||||
{isModerator && <CommentBadge label={__('Moderator')} icon={ICONS.BADGE_MOD} size={16} />}
|
||||
{isStreamer && <CommentBadge label={__('Streamer')} icon={ICONS.BADGE_STREAMER} size={16} />}
|
||||
|
||||
<Button
|
||||
className={classnames('button--uri-indicator comment__author', { 'comment__author--creator': isStreamer })}
|
||||
target="_blank"
|
||||
|
@ -117,6 +117,11 @@ export default function LivestreamComment(props: Props) {
|
|||
</span>
|
||||
)}
|
||||
|
||||
{isGlobalMod && <CommentBadge label={__('Moderator')} icon={ICONS.BADGE_ADMIN} size={16} />}
|
||||
{isModerator && <CommentBadge label={__('Admin')} icon={ICONS.BADGE_MOD} size={16} />}
|
||||
{isStreamer && <CommentBadge label={__('Streamer')} icon={ICONS.BADGE_STREAMER} size={16} />}
|
||||
<PremiumBadge membership={odyseeMembership} />
|
||||
|
||||
{/* Use key to force timestamp update */}
|
||||
<DateTime date={timePosted} timeAgo key={forceUpdate} genericSeconds />
|
||||
|
||||
|
@ -135,6 +140,7 @@ export default function LivestreamComment(props: Props) {
|
|||
stakedLevel={stakedLevel}
|
||||
disableTimestamps
|
||||
setUserMention={setUserMention}
|
||||
hasMembership={odyseeMembership}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
BIN
ui/component/membershipSplash/astronaut_n_friends.png
Normal file
BIN
ui/component/membershipSplash/astronaut_n_friends.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 218 KiB |
BIN
ui/component/membershipSplash/badge_premium-plus.png
Normal file
BIN
ui/component/membershipSplash/badge_premium-plus.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 13 KiB |
BIN
ui/component/membershipSplash/badge_premium.png
Normal file
BIN
ui/component/membershipSplash/badge_premium.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 12 KiB |
35
ui/component/membershipSplash/index.js
Normal file
35
ui/component/membershipSplash/index.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doOpenModal } from 'redux/actions/app';
|
||||
import {
|
||||
selectTitleForUri,
|
||||
makeSelectClaimForUri,
|
||||
selectClaimIsMineForUri,
|
||||
selectFetchingMyChannels,
|
||||
} from 'redux/selectors/claims';
|
||||
|
||||
import { selectClientSetting } from 'redux/selectors/settings';
|
||||
import { selectActiveChannelClaim, selectIncognito } from 'redux/selectors/app';
|
||||
import { selectBalance, selectIsSendingSupport } from 'redux/selectors/wallet';
|
||||
import { withRouter } from 'react-router';
|
||||
import * as SETTINGS from 'constants/settings';
|
||||
import WalletSendTip from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
activeChannelClaim: selectActiveChannelClaim(state),
|
||||
balance: selectBalance(state),
|
||||
claim: makeSelectClaimForUri(props.uri, false)(state),
|
||||
claimIsMine: selectClaimIsMineForUri(state, props.uri),
|
||||
fetchingChannels: selectFetchingMyChannels(state),
|
||||
incognito: selectIncognito(state),
|
||||
instantTipEnabled: selectClientSetting(state, SETTINGS.INSTANT_PURCHASE_ENABLED),
|
||||
instantTipMax: selectClientSetting(state, SETTINGS.INSTANT_PURCHASE_MAX),
|
||||
isPending: selectIsSendingSupport(state),
|
||||
title: selectTitleForUri(state, props.uri),
|
||||
});
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
doOpenModal,
|
||||
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
||||
});
|
||||
|
||||
export default withRouter(connect(select, perform)(WalletSendTip));
|
BIN
ui/component/membershipSplash/odysee_premium.png
Normal file
BIN
ui/component/membershipSplash/odysee_premium.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 12 KiB |
139
ui/component/membershipSplash/view.jsx
Normal file
139
ui/component/membershipSplash/view.jsx
Normal file
|
@ -0,0 +1,139 @@
|
|||
// @flow
|
||||
import * as ICONS from 'constants/icons';
|
||||
import * as PAGES from 'constants/pages';
|
||||
|
||||
import Icon from 'component/common/icon';
|
||||
import Button from 'component/button';
|
||||
|
||||
import React from 'react';
|
||||
import AstronautAndFriends from './astronaut_n_friends.png';
|
||||
import BadgePremium from './badge_premium.png';
|
||||
import BadgePremiumPlus from './badge_premium-plus.png';
|
||||
import OdyseePremium from './odysee_premium.png';
|
||||
import I18nMessage from 'component/i18nMessage';
|
||||
|
||||
type Props = {
|
||||
pageLocation: string,
|
||||
currencyToUse: string,
|
||||
};
|
||||
|
||||
export default function MembershipSplash(props: Props) {
|
||||
const { pageLocation, currencyToUse } = props;
|
||||
|
||||
const premiumDisplayAmounts = {
|
||||
eur: '€0.89',
|
||||
usd: '99¢',
|
||||
};
|
||||
|
||||
const premiumPlusDisplayAmounts = {
|
||||
eur: '€2.68',
|
||||
usd: '$2.99',
|
||||
};
|
||||
|
||||
// const logo = <Icon className="header__logo" icon={ICONS.ODYSEE_WHITE_TEXT} />;
|
||||
|
||||
const earlyAcessInfo = (
|
||||
<div className="membership-splash__info-content">
|
||||
<Icon icon={ICONS.EARLY_ACCESS} />
|
||||
{__('Exclusive and early access to features')}
|
||||
</div>
|
||||
);
|
||||
const badgeInfo = (
|
||||
<div className="membership-splash__info-content">
|
||||
<Icon icon={ICONS.MEMBER_BADGE} />
|
||||
{__('Badge on profile')}
|
||||
</div>
|
||||
);
|
||||
const noAdsInfo = (
|
||||
<div className="membership-splash__info-content">
|
||||
<Icon icon={ICONS.NO_ADS} />
|
||||
{__('No ads')}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="membership-splash">
|
||||
<div className="membership-splash__banner">
|
||||
<img src={AstronautAndFriends} />
|
||||
<section className="membership-splash__title">
|
||||
<section>
|
||||
<img src={OdyseePremium} />
|
||||
</section>
|
||||
<section>
|
||||
<I18nMessage
|
||||
tokens={{ early_access: <b>{__('early access')}</b>, site_wide_badge: <b>{__('site-wide badge')}</b> }}
|
||||
>
|
||||
Get %early_access% features and a %site_wide_badge%
|
||||
</I18nMessage>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className="membership-splash__info-wrapper">
|
||||
<div className="membership-splash__info">
|
||||
<I18nMessage>
|
||||
Creating a revolutionary video platform for everyone is something we're proud to be doing, but it isn't
|
||||
something that can happen without support. If you believe in Odysee's mission, please consider becoming a
|
||||
Premium member. As a Premium member, you'll be helping us build the best platform in the universe and we'll
|
||||
give you some cool perks!
|
||||
</I18nMessage>
|
||||
</div>
|
||||
|
||||
<div className="membership-splash__info">
|
||||
<section className="membership-splash__info-header">
|
||||
<div className="membership-splash__info-price">
|
||||
<img src={BadgePremium} />
|
||||
|
||||
<section>
|
||||
<I18nMessage
|
||||
tokens={{
|
||||
date_range: <div className="membership-splash__info-range">{__('A MONTH')}</div>,
|
||||
currencyToUseString: premiumDisplayAmounts[currencyToUse],
|
||||
}}
|
||||
>
|
||||
%currencyToUseString% %date_range%
|
||||
</I18nMessage>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{badgeInfo}
|
||||
|
||||
{earlyAcessInfo}
|
||||
|
||||
<div className="membership-splash__info-button">
|
||||
<Button
|
||||
button="primary"
|
||||
label={__('Join')}
|
||||
navigate={`/$/${PAGES.ODYSEE_MEMBERSHIP}?interval=year&plan=Premium&pageLocation=${pageLocation}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="membership-splash__info">
|
||||
<section className="membership-splash__info-header">
|
||||
<div className="membership-splash__info-price">
|
||||
<img src={BadgePremiumPlus} />
|
||||
<section>
|
||||
{premiumPlusDisplayAmounts[currencyToUse]}
|
||||
<div className="membership-splash__info-range">{__('A MONTH')}</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
{badgeInfo}
|
||||
|
||||
{earlyAcessInfo}
|
||||
|
||||
{noAdsInfo}
|
||||
<div className="membership-splash__info-button">
|
||||
<Button
|
||||
button="primary"
|
||||
label={__('Join')}
|
||||
navigate={`/$/${PAGES.ODYSEE_MEMBERSHIP}?interval=year&plan=Premium%2b&pageLocation=${pageLocation}&`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -32,7 +32,7 @@ import {
|
|||
} from 'redux/selectors/app';
|
||||
import { selectClientSetting } from 'redux/selectors/settings';
|
||||
import { makeSelectFileRenderModeForUri } from 'redux/selectors/content';
|
||||
import { selectUser } from 'redux/selectors/user';
|
||||
import { selectUser, selectOdyseeMembershipName } from 'redux/selectors/user';
|
||||
import PublishForm from './view';
|
||||
|
||||
const select = (state) => {
|
||||
|
@ -65,6 +65,7 @@ const select = (state) => {
|
|||
activeChannelStakedLevel: selectActiveChannelStakedLevel(state),
|
||||
isClaimingInitialRewards: selectIsClaimingInitialRewards(state),
|
||||
hasClaimedInitialRewards: selectHasClaimedInitialRewards(state),
|
||||
odyseeMembership: selectOdyseeMembershipName(state),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -97,6 +97,7 @@ type Props = {
|
|||
isClaimingInitialRewards: boolean,
|
||||
claimInitialRewards: () => void,
|
||||
hasClaimedInitialRewards: boolean,
|
||||
odyseeMembership: string,
|
||||
};
|
||||
|
||||
function PublishForm(props: Props) {
|
||||
|
@ -138,6 +139,7 @@ function PublishForm(props: Props) {
|
|||
isClaimingInitialRewards,
|
||||
claimInitialRewards,
|
||||
hasClaimedInitialRewards,
|
||||
odyseeMembership,
|
||||
} = props;
|
||||
|
||||
const inEditMode = Boolean(editingURI);
|
||||
|
@ -146,11 +148,15 @@ function PublishForm(props: Props) {
|
|||
const TYPE_PARAM = 'type';
|
||||
const uploadType = urlParams.get(TYPE_PARAM);
|
||||
const _uploadType = uploadType && uploadType.toLowerCase();
|
||||
|
||||
const userHasEnoughLBCForStreaming = activeChannelStakedLevel >= CHANNEL_STAKED_LEVEL_LIVESTREAM;
|
||||
|
||||
const enableLivestream =
|
||||
ENABLE_NO_SOURCE_CLAIMS &&
|
||||
user &&
|
||||
!user.odysee_live_disabled &&
|
||||
(activeChannelStakedLevel >= CHANNEL_STAKED_LEVEL_LIVESTREAM || user.odysee_live_enabled);
|
||||
(userHasEnoughLBCForStreaming || user.odysee_live_enabled || odyseeMembership);
|
||||
|
||||
// $FlowFixMe
|
||||
const AVAILABLE_MODES = Object.values(PUBLISH_MODES).filter((mode) => {
|
||||
// $FlowFixMe
|
||||
|
|
|
@ -2,7 +2,7 @@ import { connect } from 'react-redux';
|
|||
import { selectClaimForUri } from 'redux/selectors/claims';
|
||||
import { doFetchRecommendedContent } from 'redux/actions/search';
|
||||
import { selectRecommendedContentForUri, selectIsSearching } from 'redux/selectors/search';
|
||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
import { selectOdyseeMembershipIsPremiumPlus } from 'redux/selectors/user';
|
||||
import RecommendedContent from './view';
|
||||
|
||||
const select = (state, props) => {
|
||||
|
@ -14,7 +14,7 @@ const select = (state, props) => {
|
|||
recommendedContentUris,
|
||||
nextRecommendedUri,
|
||||
isSearching: selectIsSearching(state),
|
||||
isAuthenticated: selectUserVerifiedEmail(state),
|
||||
userHasPremiumPlus: selectOdyseeMembershipIsPremiumPlus(state),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -23,8 +23,10 @@ type Props = {
|
|||
nextRecommendedUri: string,
|
||||
isSearching: boolean,
|
||||
doFetchRecommendedContent: (string) => void,
|
||||
isAuthenticated: boolean,
|
||||
claim: ?StreamClaim,
|
||||
claimId: string,
|
||||
metadata: any,
|
||||
userHasPremiumPlus: boolean,
|
||||
};
|
||||
|
||||
export default React.memo<Props>(function RecommendedContent(props: Props) {
|
||||
|
@ -34,12 +36,12 @@ export default React.memo<Props>(function RecommendedContent(props: Props) {
|
|||
recommendedContentUris,
|
||||
nextRecommendedUri,
|
||||
isSearching,
|
||||
isAuthenticated,
|
||||
claim,
|
||||
userHasPremiumPlus,
|
||||
} = props;
|
||||
|
||||
const claimId: ?string = claim && claim.claim_id;
|
||||
const injectAds = SHOW_ADS && IS_WEB && !isAuthenticated;
|
||||
const injectAds = SHOW_ADS && IS_WEB && !userHasPremiumPlus;
|
||||
|
||||
function claimContainsBlockedWords(claim: ?StreamClaim) {
|
||||
if (BLOCKED_WORDS) {
|
||||
|
@ -177,7 +179,6 @@ function areEqual(prevProps: Props, nextProps: Props) {
|
|||
if (
|
||||
a.uri !== b.uri ||
|
||||
a.nextRecommendedUri !== b.nextRecommendedUri ||
|
||||
a.isAuthenticated !== b.isAuthenticated ||
|
||||
a.isSearching !== b.isSearching ||
|
||||
(a.recommendedContentUris && !b.recommendedContentUris) ||
|
||||
(!a.recommendedContentUris && b.recommendedContentUris) ||
|
||||
|
|
|
@ -72,6 +72,9 @@ const LiveStreamSetupPage = lazyImport(() => import('page/livestreamSetup' /* we
|
|||
const LivestreamCurrentPage = lazyImport(() =>
|
||||
import('page/livestreamCurrent' /* webpackChunkName: "livestreamCurrent" */)
|
||||
);
|
||||
const OdyseeMembershipPage = lazyImport(() =>
|
||||
import('page/odyseeMembership' /* webpackChunkName: "odyseeMembership" */)
|
||||
);
|
||||
const OwnComments = lazyImport(() => import('page/ownComments' /* webpackChunkName: "ownComments" */));
|
||||
const PasswordResetPage = lazyImport(() => import('page/passwordReset' /* webpackChunkName: "passwordReset" */));
|
||||
const PasswordSetPage = lazyImport(() => import('page/passwordSet' /* webpackChunkName: "passwordSet" */));
|
||||
|
@ -365,6 +368,7 @@ function AppRouter(props: Props) {
|
|||
<PrivateRoute {...props} path={`/$/${PAGES.NOTIFICATIONS}`} component={NotificationsPage} />
|
||||
<PrivateRoute {...props} path={`/$/${PAGES.AUTH_WALLET_PASSWORD}`} component={SignInWalletPasswordPage} />
|
||||
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_OWN_COMMENTS}`} component={OwnComments} />
|
||||
<PrivateRoute {...props} path={`/$/${PAGES.ODYSEE_MEMBERSHIP}`} component={OdyseeMembershipPage} />
|
||||
|
||||
<Route path={`/$/${PAGES.POPOUT}/:channelName/:streamName`} component={PopoutChatPage} />
|
||||
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doFetchLastActiveSubs } from 'redux/actions/subscriptions';
|
||||
import { selectActiveChannelStakedLevel } from 'redux/selectors/app';
|
||||
import { selectLastActiveSubscriptions, selectSubscriptions } from 'redux/selectors/subscriptions';
|
||||
import { doClearClaimSearch } from 'redux/actions/claims';
|
||||
import { doClearPurchasedUriSuccess } from 'redux/actions/file';
|
||||
import { selectFollowedTags } from 'redux/selectors/tags';
|
||||
import { selectUserVerifiedEmail, selectUser } from 'redux/selectors/user';
|
||||
import { selectUserVerifiedEmail, selectUser, selectOdyseeMembershipName } from 'redux/selectors/user';
|
||||
import { selectHomepageData, selectWildWestDisabled } from 'redux/selectors/settings';
|
||||
import { doSignOut } from 'redux/actions/app';
|
||||
import { selectUnseenNotificationCount } from 'redux/selectors/notifications';
|
||||
import { selectPurchaseUriSuccess } from 'redux/selectors/claims';
|
||||
import { selectPurchaseUriSuccess, selectOdyseeMembershipForUri } from 'redux/selectors/claims';
|
||||
|
||||
import SideNavigation from './view';
|
||||
|
||||
|
@ -22,8 +21,9 @@ const select = (state) => ({
|
|||
unseenCount: selectUnseenNotificationCount(state),
|
||||
user: selectUser(state),
|
||||
homepageData: selectHomepageData(state),
|
||||
activeChannelStakedLevel: selectActiveChannelStakedLevel(state),
|
||||
wildWestDisabled: selectWildWestDisabled(state),
|
||||
odyseeMembership: selectOdyseeMembershipName(state),
|
||||
odyseeMembershipByUri: (uri) => selectOdyseeMembershipForUri(state, uri),
|
||||
});
|
||||
|
||||
export default connect(select, {
|
||||
|
|
|
@ -15,7 +15,8 @@ import I18nMessage from 'component/i18nMessage';
|
|||
import ChannelThumbnail from 'component/channelThumbnail';
|
||||
import { useIsMobile, useIsLargeScreen, isTouch } from 'effects/use-screensize';
|
||||
import { GetLinksData } from 'util/buildHomepage';
|
||||
import { DOMAIN, ENABLE_UI_NOTIFICATIONS, ENABLE_NO_SOURCE_CLAIMS, CHANNEL_STAKED_LEVEL_LIVESTREAM } from 'config';
|
||||
import { DOMAIN, ENABLE_UI_NOTIFICATIONS, ENABLE_NO_SOURCE_CLAIMS } from 'config';
|
||||
import PremiumBadge from 'component/common/premium-badge';
|
||||
|
||||
const touch = isTouch();
|
||||
|
||||
|
@ -70,6 +71,13 @@ const PLAYLISTS = {
|
|||
hideForUnauth: true,
|
||||
};
|
||||
|
||||
const PREMIUM = {
|
||||
title: 'Premium',
|
||||
link: `/$/${PAGES.ODYSEE_MEMBERSHIP}`,
|
||||
icon: ICONS.UPGRADE,
|
||||
hideForUnauth: true,
|
||||
};
|
||||
|
||||
const UNAUTH_LINKS: Array<SideNavLink> = [
|
||||
{
|
||||
title: 'Log In',
|
||||
|
@ -118,9 +126,10 @@ type Props = {
|
|||
doClearPurchasedUriSuccess: () => void,
|
||||
user: ?User,
|
||||
homepageData: any,
|
||||
activeChannelStakedLevel: number,
|
||||
wildWestDisabled: boolean,
|
||||
doClearClaimSearch: () => void,
|
||||
odyseeMembership: string,
|
||||
odyseeMembershipByUri: (uri: string) => string,
|
||||
doFetchLastActiveSubs: (force?: boolean, count?: number) => void,
|
||||
};
|
||||
|
||||
|
@ -140,9 +149,10 @@ function SideNavigation(props: Props) {
|
|||
homepageData,
|
||||
user,
|
||||
followedTags,
|
||||
activeChannelStakedLevel,
|
||||
wildWestDisabled,
|
||||
doClearClaimSearch,
|
||||
odyseeMembership,
|
||||
odyseeMembershipByUri,
|
||||
doFetchLastActiveSubs,
|
||||
} = props;
|
||||
|
||||
|
@ -228,12 +238,7 @@ function SideNavigation(props: Props) {
|
|||
const notificationsEnabled = ENABLE_UI_NOTIFICATIONS || (user && user.experimental_ui);
|
||||
const isAuthenticated = Boolean(email);
|
||||
|
||||
const livestreamEnabled = Boolean(
|
||||
ENABLE_NO_SOURCE_CLAIMS &&
|
||||
user &&
|
||||
!user.odysee_live_disabled &&
|
||||
(activeChannelStakedLevel >= CHANNEL_STAKED_LEVEL_LIVESTREAM || user.odysee_live_enabled)
|
||||
);
|
||||
const livestreamEnabled = Boolean(ENABLE_NO_SOURCE_CLAIMS && user && !user.odysee_live_disabled);
|
||||
|
||||
const [pulseLibrary, setPulseLibrary] = React.useState(false);
|
||||
const [expandTags, setExpandTags] = React.useState(false);
|
||||
|
@ -325,7 +330,11 @@ function SideNavigation(props: Props) {
|
|||
</li>
|
||||
)}
|
||||
{displayedSubscriptions.map((subscription) => (
|
||||
<SubscriptionListItem key={subscription.uri} subscription={subscription} />
|
||||
<SubscriptionListItem
|
||||
key={subscription.uri}
|
||||
subscription={subscription}
|
||||
odyseeMembershipByUri={odyseeMembershipByUri}
|
||||
/>
|
||||
))}
|
||||
{!!subscriptionFilter && !displayedSubscriptions.length && (
|
||||
<li>
|
||||
|
@ -494,6 +503,7 @@ function SideNavigation(props: Props) {
|
|||
{getLink(getHomeButton(doClearClaimSearch))}
|
||||
{getLink(RECENT_FROM_FOLLOWING)}
|
||||
{getLink(PLAYLISTS)}
|
||||
{!odyseeMembership && getLink(PREMIUM)}
|
||||
</ul>
|
||||
|
||||
<ul
|
||||
|
@ -533,8 +543,17 @@ function SideNavigation(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
function SubscriptionListItem({ subscription }: { subscription: Subscription }) {
|
||||
type SubItemProps = {
|
||||
subscription: Subscription,
|
||||
odyseeMembershipByUri: (uri: string) => string,
|
||||
}
|
||||
|
||||
function SubscriptionListItem(props: SubItemProps) {
|
||||
const { subscription, odyseeMembershipByUri } = props;
|
||||
const { uri, channelName } = subscription;
|
||||
|
||||
const membership = odyseeMembershipByUri(uri);
|
||||
|
||||
return (
|
||||
<li className="navigation-link__wrapper navigation__subscription">
|
||||
<Button
|
||||
|
@ -547,6 +566,7 @@ function SubscriptionListItem({ subscription }: { subscription: Subscription })
|
|||
<ClaimPreviewTitle uri={uri} />
|
||||
<span dir="auto" className="channel-name">
|
||||
{channelName}
|
||||
<PremiumBadge membership={membership} />
|
||||
</span>
|
||||
</div>
|
||||
</Button>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectClaimForUri } from 'redux/selectors/claims';
|
||||
import { selectClaimForUri, selectOdyseeMembershipForUri } from 'redux/selectors/claims';
|
||||
import TextareaSuggestionsItem from './view';
|
||||
import { formatLbryChannelName } from 'util/url';
|
||||
import { getClaimTitle } from 'util/claim';
|
||||
|
@ -12,6 +12,7 @@ const select = (state, props) => {
|
|||
return {
|
||||
claimLabel: claim && formatLbryChannelName(claim.canonical_url),
|
||||
claimTitle: claim && getClaimTitle(claim),
|
||||
odyseeMembershipByUri: selectOdyseeMembershipForUri(state, uri),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
// @flow
|
||||
import ChannelThumbnail from 'component/channelThumbnail';
|
||||
import React from 'react';
|
||||
import PremiumBadge from 'component/common/premium-badge';
|
||||
|
||||
type Props = {
|
||||
claimLabel?: string,
|
||||
claimTitle?: string,
|
||||
emote?: any,
|
||||
uri?: string,
|
||||
odyseeMembershipByUri: ?string,
|
||||
};
|
||||
|
||||
export default function TextareaSuggestionsItem(props: Props) {
|
||||
const { claimLabel, claimTitle, emote, uri, ...autocompleteProps } = props;
|
||||
const { claimLabel, claimTitle, emote, uri, odyseeMembershipByUri, ...autocompleteProps } = props;
|
||||
|
||||
if (emote) {
|
||||
const { name: value, url, unicode } = emote;
|
||||
|
@ -37,7 +39,10 @@ export default function TextareaSuggestionsItem(props: Props) {
|
|||
|
||||
<div className="textarea-suggestion__label">
|
||||
<span className="textarea-suggestion__title">{claimTitle || value}</span>
|
||||
<span className="textarea-suggestion__value">{value}</span>
|
||||
<span className="textarea-suggestion__value">
|
||||
{value}
|
||||
<PremiumBadge membership={odyseeMembershipByUri} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { normalizeURI } from 'util/lbryURI';
|
||||
import { doResolveUri } from 'redux/actions/claims';
|
||||
import { selectIsUriResolving, selectClaimForUri } from 'redux/selectors/claims';
|
||||
import { selectIsUriResolving, selectClaimForUri, selectOdyseeMembershipForUri } from 'redux/selectors/claims';
|
||||
import UriIndicator from './view';
|
||||
|
||||
const select = (state, props) => {
|
||||
|
@ -14,6 +14,7 @@ const select = (state, props) => {
|
|||
claim: selectClaimForUri(state, props.uri),
|
||||
isResolvingUri: selectIsUriResolving(state, props.uri),
|
||||
uri,
|
||||
odyseeMembership: selectOdyseeMembershipForUri(state, props.uri),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import type { Node } from 'react';
|
|||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import Button from 'component/button';
|
||||
import PremiumBadge from 'component/common/premium-badge';
|
||||
import { stripLeadingAtSign } from 'util/string';
|
||||
|
||||
type ChannelInfo = { uri: string, name: string, title: string };
|
||||
|
@ -17,10 +18,13 @@ type Props = {
|
|||
inline?: boolean,
|
||||
showAtSign?: boolean,
|
||||
className?: string,
|
||||
showMemberBadge?: boolean,
|
||||
children: ?Node, // to allow for other elements to be nested within the UriIndicator (commit: 1e82586f).
|
||||
// --- redux ---
|
||||
claim: ?Claim,
|
||||
isResolvingUri: boolean,
|
||||
odyseeMembership: string,
|
||||
comment?: boolean,
|
||||
resolveUri: (string) => void,
|
||||
};
|
||||
|
||||
|
@ -90,6 +94,9 @@ class UriIndicator extends React.PureComponent<Props> {
|
|||
hideAnonymous = false,
|
||||
showAtSign,
|
||||
className,
|
||||
odyseeMembership,
|
||||
comment,
|
||||
showMemberBadge = true,
|
||||
} = this.props;
|
||||
|
||||
if (!channelInfo && !claim) {
|
||||
|
@ -119,7 +126,8 @@ class UriIndicator extends React.PureComponent<Props> {
|
|||
|
||||
const inner = (
|
||||
<span dir="auto" className={classnames('channel-name', { 'channel-name--inline': inline })}>
|
||||
{showAtSign ? channelName : stripLeadingAtSign(channelTitle)}
|
||||
<p>{showAtSign ? channelName : stripLeadingAtSign(channelTitle)}</p>
|
||||
{!comment && showMemberBadge && <PremiumBadge membership={odyseeMembership} />}
|
||||
</span>
|
||||
);
|
||||
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectClaimForUri, selectIsUriResolving } from 'redux/selectors/claims';
|
||||
import { selectClaimForUri, selectIsUriResolving, selectOdyseeMembershipForUri } from 'redux/selectors/claims';
|
||||
import WunderbarSuggestion from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
claim: selectClaimForUri(state, props.uri),
|
||||
isResolvingUri: selectIsUriResolving(state, props.uri),
|
||||
});
|
||||
const select = (state, props) => {
|
||||
const { uri } = props;
|
||||
|
||||
return {
|
||||
claim: selectClaimForUri(state, uri),
|
||||
isResolvingUri: selectIsUriResolving(state, uri),
|
||||
odyseeMembershipByUri: selectOdyseeMembershipForUri(state, uri),
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(select)(WunderbarSuggestion);
|
||||
|
|
|
@ -6,15 +6,17 @@ import FileThumbnail from 'component/fileThumbnail';
|
|||
import ChannelThumbnail from 'component/channelThumbnail';
|
||||
import FileProperties from 'component/previewOverlayProperties';
|
||||
import ClaimProperties from 'component/claimProperties';
|
||||
import PremiumBadge from 'component/common/premium-badge';
|
||||
|
||||
type Props = {
|
||||
claim: ?Claim,
|
||||
uri: string,
|
||||
isResolvingUri: boolean,
|
||||
odyseeMembershipByUri: ?string,
|
||||
};
|
||||
|
||||
export default function WunderbarSuggestion(props: Props) {
|
||||
const { claim, uri, isResolvingUri } = props;
|
||||
const { claim, uri, isResolvingUri, odyseeMembershipByUri } = props;
|
||||
|
||||
if (isResolvingUri) {
|
||||
return (
|
||||
|
@ -61,6 +63,7 @@ export default function WunderbarSuggestion(props: Props) {
|
|||
<div className="wunderbar__suggestion-title">{claim.value.title}</div>
|
||||
<div className="wunderbar__suggestion-name">
|
||||
{isChannel ? claim.name : (claim.signing_channel && claim.signing_channel.name) || __('Anonymous')}
|
||||
<PremiumBadge membership={odyseeMembershipByUri} />
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
@ -298,6 +298,8 @@ export const USER_SET_REFERRER_SUCCESS = 'USER_SET_REFERRER_SUCCESS';
|
|||
export const USER_SET_REFERRER_FAILURE = 'USER_SET_REFERRER_FAILURE';
|
||||
export const USER_SET_REFERRER_RESET = 'USER_SET_REFERRER_RESET';
|
||||
export const USER_EMAIL_VERIFY_RETRY = 'USER_EMAIL_VERIFY_RETRY';
|
||||
export const ADD_ODYSEE_MEMBERSHIP_DATA = 'ADD_ODYSEE_MEMBERSHIP_DATA';
|
||||
export const ADD_CLAIMIDS_MEMBERSHIP_DATA = 'ADD_CLAIMIDS_MEMBERSHIP_DATA';
|
||||
|
||||
// Rewards
|
||||
export const FETCH_REWARDS_STARTED = 'FETCH_REWARDS_STARTED';
|
||||
|
|
|
@ -178,6 +178,7 @@ export const CONTENT = 'Content';
|
|||
export const STAR = 'star';
|
||||
export const MUSIC = 'MusicCategory';
|
||||
export const BADGE_MOD = 'BadgeMod';
|
||||
export const BADGE_ADMIN = 'BadgeAdmin';
|
||||
export const BADGE_STREAMER = 'BadgeStreamer';
|
||||
export const REPLAY = 'Replay';
|
||||
export const REPEAT = 'Repeat';
|
||||
|
@ -195,6 +196,12 @@ export const ODYSEE_LOGO = 'OdyseeLogo';
|
|||
export const ODYSEE_WHITE_TEXT = 'OdyseeLogoWhiteText';
|
||||
export const ODYSEE_DARK_TEXT = 'OdyseeLogoDarkText';
|
||||
export const FEATURED = 'Featured';
|
||||
export const EARLY_ACCESS = 'EarlyAccess';
|
||||
export const MEMBER_BADGE = 'MemberBadge';
|
||||
export const NO_ADS = 'NoAds';
|
||||
export const PREMIUM = 'Premium';
|
||||
export const PREMIUM_PLUS = 'PremiumPlus';
|
||||
export const UPGRADE = 'Upgrade';
|
||||
export const DISMISS_ALL = 'DismissAll';
|
||||
export const SUBMIT = 'Submit';
|
||||
export const FILTERED_BY_LANG = 'FilteredByLang';
|
||||
|
|
|
@ -47,3 +47,5 @@ export const COLLECTION_ADD = 'collection_add';
|
|||
export const COLLECTION_DELETE = 'collection_delete';
|
||||
export const CONFIRM_REMOVE_CARD = 'CONFIRM_REMOVE_CARD';
|
||||
export const CONFIRM_REMOVE_COMMENT = 'CONFIRM_REMOVE_COMMENT';
|
||||
export const CONFIRM_ODYSEE_MEMBERSHIP = 'CONFIRM_ODYSEE_MEMBERSHIP';
|
||||
export const MEMBERSHIP_SPLASH = 'MEMBERSHIP_SPLASH';
|
||||
|
|
|
@ -86,4 +86,5 @@ exports.LIVESTREAM = 'livestream';
|
|||
exports.LIVESTREAM_CURRENT = 'live';
|
||||
exports.GENERAL = 'general';
|
||||
exports.LIST = 'list';
|
||||
exports.ODYSEE_MEMBERSHIP = 'membership';
|
||||
exports.POPOUT = 'popout';
|
||||
|
|
|
@ -1,10 +1,17 @@
|
|||
// @flow
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {boolean} shouldFetch - Whether to get the views, not needed for some pages
|
||||
* @param {array} uris - Array of the LBRY uris of content to fetch views for
|
||||
* @param {object} claimsByUri - Function to get claimIds from claim uris
|
||||
* @param {function} doFetchViewCount - Get views account per a string of comma separated Claim Ids
|
||||
*/
|
||||
export default function useFetchViewCount(
|
||||
shouldFetch: ?boolean,
|
||||
uris: Array<string>,
|
||||
claimsByUri: any,
|
||||
claimsByUri: {},
|
||||
doFetchViewCount: (string) => void
|
||||
) {
|
||||
const [fetchedUris, setFetchedUris] = useState([]);
|
||||
|
|
62
ui/effects/use-get-user-memberships.js
Normal file
62
ui/effects/use-get-user-memberships.js
Normal file
|
@ -0,0 +1,62 @@
|
|||
// @flow
|
||||
import { useState, useEffect } from 'react';
|
||||
import { getChannelFromClaim } from 'util/claim';
|
||||
|
||||
export default function useGetUserMemberships(
|
||||
shouldFetchUserMemberships: ?boolean,
|
||||
arrayOfContentUris: ?Array<string>,
|
||||
convertClaimUrlsToIds: any,
|
||||
doFetchUserMemberships: (string) => void, // fetch membership values and save in redux
|
||||
dependency?: any,
|
||||
alreadyClaimIds?: boolean,
|
||||
) {
|
||||
const [userMemberships, setUserMemberships] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldFetchUserMemberships && arrayOfContentUris && arrayOfContentUris.length > 0) {
|
||||
const urisToFetch = arrayOfContentUris;
|
||||
|
||||
let claimIds;
|
||||
if (!alreadyClaimIds) {
|
||||
claimIds = urisToFetch.map((uri) => {
|
||||
// get claim id from array
|
||||
const claimUrlsToId = convertClaimUrlsToIds[uri];
|
||||
|
||||
if (claimUrlsToId) {
|
||||
const { claim_id: claimId } = getChannelFromClaim(claimUrlsToId) || {};
|
||||
return claimId;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
claimIds = arrayOfContentUris;
|
||||
}
|
||||
|
||||
const dedupedChannelIds = [...new Set(claimIds)];
|
||||
|
||||
const channelClaimIdsToCheck = dedupedChannelIds.filter(
|
||||
// not in fetched claims but exists in array
|
||||
(claimId) => claimId && !userMemberships.includes(claimId)
|
||||
);
|
||||
|
||||
const channelsToFetch = channelClaimIdsToCheck.filter(
|
||||
// not in fetched claims but exists in array
|
||||
(uri) => uri && !userMemberships.includes(uri)
|
||||
);
|
||||
|
||||
const commaSeparatedStringOfIds = channelsToFetch.join(',');
|
||||
|
||||
if (channelsToFetch && channelsToFetch.length > 0) {
|
||||
// hit membership/check and save it in redux
|
||||
|
||||
const combinedArray = [...userMemberships, ...channelsToFetch];
|
||||
|
||||
setUserMemberships(combinedArray);
|
||||
|
||||
if (doFetchUserMemberships) {
|
||||
doFetchUserMemberships(commaSeparatedStringOfIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, dependency || [arrayOfContentUris]);
|
||||
}
|
11
ui/modal/modalConfirmOdyseeMembership/index.js
Normal file
11
ui/modal/modalConfirmOdyseeMembership/index.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doHideModal } from 'redux/actions/app';
|
||||
import { doToast } from 'redux/actions/notifications';
|
||||
import ModalConfirmOdyseeMembership from './view';
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
closeModal: () => dispatch(doHideModal()),
|
||||
doToast: (params) => dispatch(doToast(params)),
|
||||
});
|
||||
|
||||
export default connect(null, perform)(ModalConfirmOdyseeMembership);
|
169
ui/modal/modalConfirmOdyseeMembership/view.jsx
Normal file
169
ui/modal/modalConfirmOdyseeMembership/view.jsx
Normal file
|
@ -0,0 +1,169 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { Modal } from 'modal/modal';
|
||||
import Card from 'component/common/card';
|
||||
import Button from 'component/button';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import { Lbryio } from 'lbryinc';
|
||||
import { getStripeEnvironment } from 'util/stripe';
|
||||
let stripeEnvironment = getStripeEnvironment();
|
||||
|
||||
type Props = {
|
||||
closeModal: () => void,
|
||||
paymentMethodId: string,
|
||||
setAsConfirmingCard: () => void, // ?
|
||||
hasMembership: boolean, // user already has purchased --> invoke Cancel then
|
||||
membershipId: string,
|
||||
populateMembershipData: () => void,
|
||||
userChannelClaimId: string,
|
||||
userChannelName: string,
|
||||
priceId: string,
|
||||
purchaseString: string,
|
||||
plan: string,
|
||||
setMembershipOptions: (any) => void,
|
||||
doToast: ({ message: string }) => void,
|
||||
updateUserOdyseeMembershipStatus: ({}) => void,
|
||||
user: ?User,
|
||||
};
|
||||
|
||||
export default function ConfirmOdyseeMembershipPurchase(props: Props) {
|
||||
const {
|
||||
closeModal,
|
||||
membershipId,
|
||||
populateMembershipData,
|
||||
userChannelClaimId,
|
||||
userChannelName,
|
||||
hasMembership,
|
||||
priceId,
|
||||
purchaseString,
|
||||
plan,
|
||||
setMembershipOptions,
|
||||
doToast,
|
||||
updateUserOdyseeMembershipStatus,
|
||||
user,
|
||||
} = props;
|
||||
|
||||
const [waitingForBackend, setWaitingForBackend] = React.useState();
|
||||
const [statusText, setStatusText] = React.useState();
|
||||
|
||||
async function purchaseMembership() {
|
||||
try {
|
||||
setWaitingForBackend(true);
|
||||
setStatusText(__('Completing your purchase...'));
|
||||
|
||||
// show the memberships the user is subscribed to
|
||||
await Lbryio.call(
|
||||
'membership',
|
||||
'buy',
|
||||
{
|
||||
environment: stripeEnvironment,
|
||||
membership_id: membershipId,
|
||||
channel_id: userChannelClaimId,
|
||||
channel_name: userChannelName,
|
||||
price_id: priceId,
|
||||
},
|
||||
'post'
|
||||
);
|
||||
|
||||
// cleary query params
|
||||
// $FlowFixMe
|
||||
let newURL = location.href.split('?')[0];
|
||||
window.history.pushState('object', document.title, newURL);
|
||||
|
||||
setStatusText(__('Membership was successful'));
|
||||
|
||||
// populate the new data and update frontend
|
||||
await populateMembershipData();
|
||||
|
||||
// clear the other membership options after making a purchase
|
||||
setMembershipOptions(false);
|
||||
|
||||
if (user) updateUserOdyseeMembershipStatus(user);
|
||||
|
||||
closeModal();
|
||||
} catch (err) {
|
||||
const errorMessage = err.message;
|
||||
|
||||
const subscriptionFailedBackendError = 'failed to create subscription with default card';
|
||||
|
||||
// wait a bit to show the message so it's not jarring for the user
|
||||
let errorMessageTimeout = 1150;
|
||||
|
||||
// don't do an error delay if there's already a network error
|
||||
if (errorMessage === subscriptionFailedBackendError) {
|
||||
errorMessageTimeout = 0;
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
const genericErrorMessage = __(
|
||||
"Sorry, your purchase wasn't able to completed. Please contact support for possible next steps"
|
||||
);
|
||||
|
||||
doToast({
|
||||
message: genericErrorMessage,
|
||||
isError: true,
|
||||
});
|
||||
|
||||
closeModal();
|
||||
}, errorMessageTimeout);
|
||||
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel
|
||||
async function cancelMembership() {
|
||||
try {
|
||||
setWaitingForBackend(true);
|
||||
setStatusText(__('Canceling your membership...'));
|
||||
|
||||
// show the memberships the user is subscribed to
|
||||
await Lbryio.call(
|
||||
'membership',
|
||||
'cancel',
|
||||
{
|
||||
environment: stripeEnvironment,
|
||||
membership_id: membershipId,
|
||||
},
|
||||
'post'
|
||||
);
|
||||
|
||||
setStatusText(__('Membership successfully canceled'));
|
||||
|
||||
// populate the new data and update frontend
|
||||
await populateMembershipData();
|
||||
|
||||
closeModal();
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal ariaHideApp={false} isOpen contentLabel={'Confirm Membership Purchase'} type="card" onAborted={closeModal}>
|
||||
<Card
|
||||
className="stripe__confirm-remove-membership"
|
||||
title={hasMembership ? __('Confirm Membership Cancellation') : __(`Confirm %plan% Membership`, { plan })}
|
||||
subtitle={purchaseString}
|
||||
actions={
|
||||
<div className="section__actions">
|
||||
{!waitingForBackend ? (
|
||||
<>
|
||||
<Button
|
||||
className="stripe__confirm-remove-card"
|
||||
button="primary"
|
||||
icon={ICONS.FINANCE}
|
||||
label={hasMembership ? __('Confirm Cancellation') : __('Confirm Purchase')}
|
||||
onClick={() => (hasMembership ? cancelMembership() : purchaseMembership())}
|
||||
/>
|
||||
<Button button="link" label={__('Cancel')} onClick={closeModal} />
|
||||
</>
|
||||
) : (
|
||||
<h1 style={{ fontSize: '18px' }}>{statusText}</h1>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
9
ui/modal/modalMembershipSplash/index.js
Normal file
9
ui/modal/modalMembershipSplash/index.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doHideModal } from 'redux/actions/app';
|
||||
import ModalMembershipSplash from './view';
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
closeModal: () => dispatch(doHideModal()),
|
||||
});
|
||||
|
||||
export default connect(null, perform)(ModalMembershipSplash);
|
25
ui/modal/modalMembershipSplash/view.jsx
Normal file
25
ui/modal/modalMembershipSplash/view.jsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { Modal } from 'modal/modal';
|
||||
import MembershipSplash from 'component/membershipSplash';
|
||||
|
||||
type Props = {
|
||||
closeModal: () => void,
|
||||
uri: string,
|
||||
claimIsMine: boolean,
|
||||
isSupport: boolean,
|
||||
};
|
||||
|
||||
class ModalSendTip extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const { closeModal, uri, claimIsMine } = this.props;
|
||||
|
||||
return (
|
||||
<Modal onAborted={closeModal} isOpen type="card">
|
||||
<MembershipSplash uri={uri} claimIsMine={claimIsMine} onCancel={closeModal} />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ModalSendTip;
|
|
@ -65,6 +65,12 @@ const ModalPublishPreview = lazyImport(() =>
|
|||
import('modal/modalPublishPreview' /* webpackChunkName: "modalPublishPreview" */)
|
||||
);
|
||||
const ModalRemoveCard = lazyImport(() => import('modal/modalRemoveCard' /* webpackChunkName: "modalRemoveCard" */));
|
||||
const ModalConfirmOdyseeMembership = lazyImport(() =>
|
||||
import('modal/modalConfirmOdyseeMembership' /* webpackChunkName: "modalConfirmOdyseeMembership" */)
|
||||
);
|
||||
const OdyseeMembershipSplash = lazyImport(() =>
|
||||
import('modal/modalMembershipSplash' /* webpackChunkName: "modalMembershipSplash" */)
|
||||
);
|
||||
const ModalRemoveComment = lazyImport(() =>
|
||||
import('modal/modalRemoveComment' /* webpackChunkName: "modalRemoveComment" */)
|
||||
);
|
||||
|
@ -126,6 +132,8 @@ function getModal(id) {
|
|||
return ModalPhoneCollection;
|
||||
case MODALS.SEND_TIP:
|
||||
return ModalSendTip;
|
||||
case MODALS.MEMBERSHIP_SPLASH:
|
||||
return OdyseeMembershipSplash;
|
||||
case MODALS.SOCIAL_SHARE:
|
||||
return ModalSocialShare;
|
||||
case MODALS.PUBLISH:
|
||||
|
@ -180,6 +188,8 @@ function getModal(id) {
|
|||
return ModalDeleteCollection;
|
||||
case MODALS.CONFIRM_REMOVE_CARD:
|
||||
return ModalRemoveCard;
|
||||
case MODALS.CONFIRM_ODYSEE_MEMBERSHIP:
|
||||
return ModalConfirmOdyseeMembership;
|
||||
case MODALS.CONFIRM_REMOVE_COMMENT:
|
||||
return ModalRemoveComment;
|
||||
default:
|
||||
|
|
|
@ -237,7 +237,14 @@ function ChannelPage(props: Props) {
|
|||
{cover && <img className={classnames('channel-cover__custom')} src={PlaceholderTx} />}
|
||||
{cover && <OptimizedImage className={classnames('channel-cover__custom')} src={cover} objectFit="cover" />}
|
||||
<div className="channel__primary-info">
|
||||
<ChannelThumbnail className="channel__thumbnail--channel-page" uri={uri} allowGifs />
|
||||
<ChannelThumbnail
|
||||
className="channel__thumbnail--channel-page"
|
||||
uri={uri}
|
||||
allowGifs
|
||||
showMemberBadge
|
||||
isChannel
|
||||
hideStakedIndicator
|
||||
/>
|
||||
<h1 className="channel__title">
|
||||
<TruncatedText lines={2} showTooltip>
|
||||
{title || (channelName && '@' + channelName)}
|
||||
|
|
|
@ -4,7 +4,9 @@ import {
|
|||
selectFetchingMyChannels,
|
||||
makeSelectClaimIsPending,
|
||||
selectPendingIds,
|
||||
selectClaimsByUri,
|
||||
} from 'redux/selectors/claims';
|
||||
import { doFetchUserMemberships } from 'redux/actions/user';
|
||||
import { doFetchChannelListMine } from 'redux/actions/claims';
|
||||
import { doSetActiveChannel } from 'redux/actions/app';
|
||||
import { selectYoutubeChannels } from 'redux/selectors/user';
|
||||
|
@ -30,12 +32,14 @@ const select = (state) => {
|
|||
fetchingChannels: selectFetchingMyChannels(state),
|
||||
youtubeChannels: selectYoutubeChannels(state),
|
||||
pendingChannels,
|
||||
claimsByUri: selectClaimsByUri(state),
|
||||
};
|
||||
};
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
fetchChannelListMine: () => dispatch(doFetchChannelListMine()),
|
||||
doSetActiveChannel: (claimId) => dispatch(doSetActiveChannel(claimId)),
|
||||
doFetchUserMemberships: (claimIds) => dispatch(doFetchUserMemberships(claimIds)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(ChannelsPage);
|
||||
|
|
|
@ -12,6 +12,7 @@ import LbcSymbol from 'component/common/lbc-symbol';
|
|||
import * as PAGES from 'constants/pages';
|
||||
import HelpLink from 'component/common/help-link';
|
||||
import { useHistory } from 'react-router';
|
||||
import useGetUserMemberships from 'effects/use-get-user-memberships';
|
||||
|
||||
type Props = {
|
||||
channelUrls: Array<string>,
|
||||
|
@ -20,6 +21,8 @@ type Props = {
|
|||
youtubeChannels: ?Array<any>,
|
||||
doSetActiveChannel: (string) => void,
|
||||
pendingChannels: Array<string>,
|
||||
claimsByUri: { [string]: any },
|
||||
doFetchUserMemberships: (claimIdCsv: string) => void,
|
||||
};
|
||||
|
||||
export default function ChannelsPage(props: Props) {
|
||||
|
@ -30,10 +33,15 @@ export default function ChannelsPage(props: Props) {
|
|||
youtubeChannels,
|
||||
doSetActiveChannel,
|
||||
pendingChannels,
|
||||
claimsByUri,
|
||||
doFetchUserMemberships,
|
||||
} = props;
|
||||
const [rewardData, setRewardData] = React.useState();
|
||||
const hasYoutubeChannels = youtubeChannels && Boolean(youtubeChannels.length);
|
||||
|
||||
const shouldFetchUserMemberships = true;
|
||||
useGetUserMemberships(shouldFetchUserMemberships, channelUrls, claimsByUri, doFetchUserMemberships);
|
||||
|
||||
const { push } = useHistory();
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -51,6 +59,7 @@ export default function ChannelsPage(props: Props) {
|
|||
|
||||
{channelUrls && Boolean(channelUrls.length) && (
|
||||
<ClaimList
|
||||
showMemberBadge
|
||||
header={<h1 className="section__title">{__('Your channels')}</h1>}
|
||||
headerAltControls={
|
||||
<Button
|
||||
|
|
|
@ -5,7 +5,7 @@ import { makeSelectClaimForUri } from 'redux/selectors/claims';
|
|||
import * as SETTINGS from 'constants/settings';
|
||||
import { doFetchActiveLivestreams } from 'redux/actions/livestream';
|
||||
import { selectActiveLivestreams } from 'redux/selectors/livestream';
|
||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
import { selectOdyseeMembershipIsPremiumPlus } from 'redux/selectors/user';
|
||||
import { selectFollowedTags } from 'redux/selectors/tags';
|
||||
import { doToggleTagFollowDesktop } from 'redux/actions/tags';
|
||||
import { selectClientSetting, selectLanguage } from 'redux/selectors/settings';
|
||||
|
@ -20,9 +20,9 @@ const select = (state, props) => {
|
|||
followedTags: selectFollowedTags(state),
|
||||
repostedUri: repostedUri,
|
||||
repostedClaim: repostedUri ? makeSelectClaimForUri(repostedUri)(state) : null,
|
||||
isAuthenticated: selectUserVerifiedEmail(state),
|
||||
tileLayout: selectClientSetting(state, SETTINGS.TILE_LAYOUT),
|
||||
activeLivestreams: selectActiveLivestreams(state),
|
||||
userHasPremiumPlus: selectOdyseeMembershipIsPremiumPlus(state),
|
||||
languageSetting: selectLanguage(state),
|
||||
searchInLanguage: selectClientSetting(state, SETTINGS.SEARCH_IN_LANGUAGE),
|
||||
};
|
||||
|
|
|
@ -28,10 +28,10 @@ type Props = {
|
|||
searchInLanguage: boolean,
|
||||
doToggleTagFollowDesktop: (string) => void,
|
||||
doResolveUri: (string) => void,
|
||||
isAuthenticated: boolean,
|
||||
tileLayout: boolean,
|
||||
activeLivestreams: ?LivestreamInfo,
|
||||
doFetchActiveLivestreams: (orderBy: ?Array<string>, lang: ?Array<string>) => void,
|
||||
userHasPremiumPlus: boolean,
|
||||
};
|
||||
|
||||
function DiscoverPage(props: Props) {
|
||||
|
@ -44,11 +44,11 @@ function DiscoverPage(props: Props) {
|
|||
searchInLanguage,
|
||||
doToggleTagFollowDesktop,
|
||||
doResolveUri,
|
||||
isAuthenticated,
|
||||
tileLayout,
|
||||
activeLivestreams,
|
||||
doFetchActiveLivestreams,
|
||||
dynamicRouteProps,
|
||||
userHasPremiumPlus,
|
||||
} = props;
|
||||
|
||||
const buttonRef = useRef();
|
||||
|
@ -191,7 +191,7 @@ function DiscoverPage(props: Props) {
|
|||
hiddenNsfwMessage={<HiddenNsfw type="page" />}
|
||||
repostedClaimId={repostedClaim ? repostedClaim.claim_id : null}
|
||||
injectedItem={
|
||||
SHOW_ADS && !isAuthenticated && !isWildWest && { node: <Ads small type="video" tileLayout={tileLayout} /> }
|
||||
SHOW_ADS && !userHasPremiumPlus && !isWildWest && { node: <Ads small type="video" tileLayout={tileLayout} /> }
|
||||
}
|
||||
// Assume wild west page if no dynamicRouteProps
|
||||
// Not a very good solution, but just doing it for now
|
||||
|
|
|
@ -3,7 +3,7 @@ import * as SETTINGS from 'constants/settings';
|
|||
import { doFetchActiveLivestreams } from 'redux/actions/livestream';
|
||||
import { selectActiveLivestreams, selectFetchingActiveLivestreams } from 'redux/selectors/livestream';
|
||||
import { selectFollowedTags } from 'redux/selectors/tags';
|
||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
import { selectUserVerifiedEmail, selectOdyseeMembershipIsPremiumPlus } from 'redux/selectors/user';
|
||||
import { selectSubscriptions } from 'redux/selectors/subscriptions';
|
||||
import { selectShowMatureContent, selectHomepageData, selectClientSetting } from 'redux/selectors/settings';
|
||||
|
||||
|
@ -18,6 +18,7 @@ const select = (state) => ({
|
|||
activeLivestreams: selectActiveLivestreams(state),
|
||||
fetchingActiveLivestreams: selectFetchingActiveLivestreams(state),
|
||||
hideScheduledLivestreams: selectClientSetting(state, SETTINGS.HIDE_SCHEDULED_LIVESTREAMS),
|
||||
userHasPremiumPlus: selectOdyseeMembershipIsPremiumPlus(state),
|
||||
});
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
|
|
|
@ -31,6 +31,7 @@ type Props = {
|
|||
doFetchActiveLivestreams: () => void,
|
||||
fetchingActiveLivestreams: boolean,
|
||||
hideScheduledLivestreams: boolean,
|
||||
userHasPremiumPlus: boolean,
|
||||
};
|
||||
|
||||
function HomePage(props: Props) {
|
||||
|
@ -44,6 +45,7 @@ function HomePage(props: Props) {
|
|||
doFetchActiveLivestreams,
|
||||
fetchingActiveLivestreams,
|
||||
hideScheduledLivestreams,
|
||||
userHasPremiumPlus,
|
||||
} = props;
|
||||
const showPersonalizedChannels = (authenticated || !IS_WEB) && subscribedChannels && subscribedChannels.length > 0;
|
||||
const showPersonalizedTags = (authenticated || !IS_WEB) && followedTags && followedTags.length > 0;
|
||||
|
@ -100,7 +102,9 @@ function HomePage(props: Props) {
|
|||
prefixUris={getLivestreamUris(activeLivestreams, options.channelIds)}
|
||||
pinUrls={pinUrls}
|
||||
injectedItem={
|
||||
index === 0 && SHOW_ADS && !authenticated && { node: <Ads small type="video" tileLayout />, replace: true }
|
||||
index === 0 &&
|
||||
SHOW_ADS &&
|
||||
!userHasPremiumPlus && { node: <Ads small type="video" tileLayout />, replace: true }
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectHasChannels, selectFetchingMyChannels } from 'redux/selectors/claims';
|
||||
import { doClearPublish } from 'redux/actions/publish';
|
||||
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||
import { selectActiveChannelClaim, selectActiveChannelStakedLevel } from 'redux/selectors/app';
|
||||
import { doFetchNoSourceClaims } from 'redux/actions/livestream';
|
||||
import { selectUser, selectOdyseeMembershipName } from 'redux/selectors/user';
|
||||
import {
|
||||
makeSelectPendingLivestreamsForChannelId,
|
||||
makeSelectLivestreamsForChannelId,
|
||||
|
@ -23,6 +24,9 @@ const select = (state) => {
|
|||
myLivestreamClaims: makeSelectLivestreamsForChannelId(channelId)(state),
|
||||
pendingClaims: makeSelectPendingLivestreamsForChannelId(channelId)(state),
|
||||
fetchingLivestreams: makeSelectIsFetchingLivestreams(channelId)(state),
|
||||
user: selectUser(state),
|
||||
odyseeMembership: selectOdyseeMembershipName(state),
|
||||
activeChannelStakedLevel: selectActiveChannelStakedLevel(state),
|
||||
};
|
||||
};
|
||||
const perform = (dispatch) => ({
|
||||
|
|
|
@ -17,6 +17,7 @@ import Card from 'component/common/card';
|
|||
import ClaimList from 'component/claimList';
|
||||
import usePersistedState from 'effects/use-persisted-state';
|
||||
import { LIVESTREAM_RTMP_URL } from 'constants/livestream';
|
||||
import { ENABLE_NO_SOURCE_CLAIMS, CHANNEL_STAKED_LEVEL_LIVESTREAM } from '../../../config';
|
||||
|
||||
type Props = {
|
||||
hasChannels: boolean,
|
||||
|
@ -29,6 +30,9 @@ type Props = {
|
|||
fetchingLivestreams: boolean,
|
||||
channelId: ?string,
|
||||
channelName: ?string,
|
||||
user: ?User,
|
||||
activeChannelStakedLevel: number,
|
||||
odyseeMembership: string,
|
||||
};
|
||||
|
||||
export default function LivestreamSetupPage(props: Props) {
|
||||
|
@ -44,6 +48,9 @@ export default function LivestreamSetupPage(props: Props) {
|
|||
fetchingLivestreams,
|
||||
channelId,
|
||||
channelName,
|
||||
user,
|
||||
odyseeMembership,
|
||||
activeChannelStakedLevel,
|
||||
} = props;
|
||||
|
||||
const [sigData, setSigData] = React.useState({ signature: undefined, signing_ts: undefined });
|
||||
|
@ -51,6 +58,22 @@ export default function LivestreamSetupPage(props: Props) {
|
|||
|
||||
const hasLivestreamClaims = Boolean(myLivestreamClaims.length || pendingClaims.length);
|
||||
|
||||
const hasEnoughLBCToStream = activeChannelStakedLevel >= CHANNEL_STAKED_LEVEL_LIVESTREAM;
|
||||
const { odysee_live_disabled: liveDisabled, odysee_live_enabled: liveEnabled } = user || {};
|
||||
|
||||
const livestreamEnabled = Boolean(
|
||||
ENABLE_NO_SOURCE_CLAIMS && user && !liveDisabled && (liveEnabled || odyseeMembership || hasEnoughLBCToStream)
|
||||
);
|
||||
|
||||
let reasonAllowedToStream = '';
|
||||
if (odyseeMembership) {
|
||||
reasonAllowedToStream = 'you purchased Odysee Premium';
|
||||
} else if (liveEnabled) {
|
||||
reasonAllowedToStream = 'your livestreaming was turned on manually';
|
||||
} else if (hasEnoughLBCToStream) {
|
||||
reasonAllowedToStream = 'you have enough staked LBC';
|
||||
}
|
||||
|
||||
function createStreamKey() {
|
||||
if (!channelId || !channelName || !sigData.signature || !sigData.signing_ts) return null;
|
||||
return `${channelId}?d=${toHex(channelName)}&s=${sigData.signature}&t=${sigData.signing_ts}`;
|
||||
|
@ -169,12 +192,32 @@ export default function LivestreamSetupPage(props: Props) {
|
|||
|
||||
return (
|
||||
<Page>
|
||||
{/* no livestreaming privs because no premium membership */}
|
||||
{!livestreamEnabled && !odyseeMembership && (
|
||||
<div>
|
||||
<h2 className={''}>Join Odysee Premium to be able to livestream</h2>
|
||||
|
||||
<Button
|
||||
button="primary"
|
||||
label={__('Join Odysee Premium')}
|
||||
icon={ICONS.FINANCE}
|
||||
navigate={`/$/${PAGES.ODYSEE_MEMBERSHIP}`}
|
||||
className="membership_button"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* show livestreaming frontend */}
|
||||
{livestreamEnabled && (
|
||||
<div className="card-stack">
|
||||
{/* getting channel data */}
|
||||
{fetchingChannels && (
|
||||
<div className="main--empty">
|
||||
<Spinner delayed />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* no channels yet */}
|
||||
{!fetchingChannels && !hasChannels && (
|
||||
<Yrbl
|
||||
type="happy"
|
||||
|
@ -186,6 +229,8 @@ export default function LivestreamSetupPage(props: Props) {
|
|||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* channel selector */}
|
||||
{!fetchingChannels && (
|
||||
<>
|
||||
<div className="section__actions--between">
|
||||
|
@ -194,20 +239,27 @@ export default function LivestreamSetupPage(props: Props) {
|
|||
</>
|
||||
)}
|
||||
|
||||
{/* getting livestreams */}
|
||||
{fetchingLivestreams && !fetchingChannels && !hasLivestreamClaims && (
|
||||
<div className="main--empty">
|
||||
<Spinner delayed />
|
||||
</div>
|
||||
)}
|
||||
<div className="card-stack">
|
||||
|
||||
{!fetchingChannels && channelId && (
|
||||
<>
|
||||
<Card
|
||||
titleActions={
|
||||
<Button button="close" icon={showHelp ? ICONS.UP : ICONS.DOWN} onClick={() => setShowHelp(!showHelp)} />
|
||||
<Button
|
||||
button="close"
|
||||
icon={showHelp ? ICONS.UP : ICONS.DOWN}
|
||||
onClick={() => setShowHelp(!showHelp)}
|
||||
/>
|
||||
}
|
||||
title={__('Go Live on Odysee')}
|
||||
subtitle={<>{__(`You're invited to try out our new livestreaming service while in beta!`)} </>}
|
||||
subtitle={
|
||||
<>{__(`Congratulations, you have access to livestreaming because ${reasonAllowedToStream}!`)} </>
|
||||
}
|
||||
actions={showHelp && helpText}
|
||||
/>
|
||||
{streamKey && totalLivestreamClaims.length > 0 && (
|
||||
|
@ -364,6 +416,7 @@ export default function LivestreamSetupPage(props: Props) {
|
|||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
|
27
ui/page/odyseeMembership/index.js
Normal file
27
ui/page/odyseeMembership/index.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doOpenModal } from 'redux/actions/app';
|
||||
import OdyseeMembership from './view';
|
||||
import { selectActiveChannelClaim, selectIncognito } from 'redux/selectors/app';
|
||||
import { selectMyChannelClaims, selectClaimsByUri } from 'redux/selectors/claims';
|
||||
import { doFetchUserMemberships, doCheckUserOdyseeMemberships } from 'redux/actions/user';
|
||||
import { selectUser } from 'redux/selectors/user';
|
||||
|
||||
const select = (state) => {
|
||||
const activeChannelClaim = selectActiveChannelClaim(state);
|
||||
|
||||
return {
|
||||
activeChannelClaim,
|
||||
channels: selectMyChannelClaims(state),
|
||||
claimsByUri: selectClaimsByUri(state),
|
||||
incognito: selectIncognito(state),
|
||||
user: selectUser(state),
|
||||
};
|
||||
};
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
||||
fetchUserMemberships: (claimIds) => dispatch(doFetchUserMemberships(claimIds)),
|
||||
updateUserOdyseeMembershipStatus: (user) => dispatch(doCheckUserOdyseeMemberships(user)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(OdyseeMembership);
|
646
ui/page/odyseeMembership/view.jsx
Normal file
646
ui/page/odyseeMembership/view.jsx
Normal file
|
@ -0,0 +1,646 @@
|
|||
/* eslint-disable no-console */
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
import Page from 'component/page';
|
||||
import Spinner from 'component/spinner';
|
||||
import { Lbryio } from 'lbryinc';
|
||||
import { getStripeEnvironment } from 'util/stripe';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import * as PAGES from 'constants/pages';
|
||||
import * as MODALS from 'constants/modal_types';
|
||||
import Card from 'component/common/card';
|
||||
import MembershipSplash from 'component/membershipSplash';
|
||||
import Button from 'component/button';
|
||||
import ChannelSelector from 'component/channelSelector';
|
||||
import PremiumBadge from 'component/common/premium-badge';
|
||||
import useGetUserMemberships from 'effects/use-get-user-memberships';
|
||||
import usePersistedState from 'effects/use-persisted-state';
|
||||
|
||||
let stripeEnvironment = getStripeEnvironment();
|
||||
|
||||
const isDev = process.env.NODE_ENV !== 'production';
|
||||
|
||||
// odysee channel information since the memberships are only for Odysee
|
||||
const odyseeChannelId = '80d2590ad04e36fb1d077a9b9e3a8bba76defdf8';
|
||||
const odyseeChannelName = '@odysee';
|
||||
|
||||
type Props = {
|
||||
history: { action: string, push: (string) => void, replace: (string) => void },
|
||||
location: { search: string, pathname: string },
|
||||
totalBalance: ?number,
|
||||
openModal: (string, {}) => void,
|
||||
activeChannelClaim: ?ChannelClaim,
|
||||
channels: ?Array<ChannelClaim>,
|
||||
claimsByUri: { [string]: any },
|
||||
fetchUserMemberships: (claimIdCsv: string) => void,
|
||||
incognito: boolean,
|
||||
updateUserOdyseeMembershipStatus: () => void,
|
||||
user: ?User,
|
||||
};
|
||||
|
||||
const OdyseeMembershipPage = (props: Props) => {
|
||||
const {
|
||||
openModal,
|
||||
activeChannelClaim,
|
||||
channels,
|
||||
claimsByUri,
|
||||
fetchUserMemberships,
|
||||
updateUserOdyseeMembershipStatus,
|
||||
incognito,
|
||||
user,
|
||||
} = props;
|
||||
|
||||
const shouldUseEuro = localStorage.getItem('gdprRequired');
|
||||
let currencyToUse;
|
||||
if (shouldUseEuro === 'true') {
|
||||
currencyToUse = 'eur';
|
||||
} else {
|
||||
currencyToUse = 'usd';
|
||||
}
|
||||
|
||||
const userChannelName = activeChannelClaim ? activeChannelClaim.name : '';
|
||||
const userChannelClaimId = activeChannelClaim && activeChannelClaim.claim_id;
|
||||
|
||||
const [cardSaved, setCardSaved] = React.useState();
|
||||
const [membershipOptions, setMembershipOptions] = React.useState();
|
||||
const [userMemberships, setUserMemberships] = React.useState();
|
||||
const [canceledMemberships, setCanceledMemberships] = React.useState();
|
||||
const [activeMemberships, setActiveMemberships] = React.useState();
|
||||
const [purchasedMemberships, setPurchasedMemberships] = React.useState([]);
|
||||
const [hasShownModal, setHasShownModal] = React.useState(false);
|
||||
const [shouldFetchUserMemberships, setFetchUserMemberships] = React.useState(true);
|
||||
|
||||
const [showHelp, setShowHelp] = usePersistedState('premium-help-seen', true);
|
||||
|
||||
const hasMembership = activeMemberships && activeMemberships.length > 0;
|
||||
|
||||
const channelUrls = channels && channels.map((channel) => channel.permanent_url);
|
||||
|
||||
// check if membership data for user is already fetched, if it's needed then fetch it
|
||||
useGetUserMemberships(shouldFetchUserMemberships, channelUrls, claimsByUri, (value) => {
|
||||
fetchUserMemberships(value);
|
||||
setFetchUserMemberships(false);
|
||||
});
|
||||
|
||||
async function populateMembershipData() {
|
||||
try {
|
||||
// show the memberships the user is subscribed to
|
||||
const response = await Lbryio.call(
|
||||
'membership',
|
||||
'mine',
|
||||
{
|
||||
environment: stripeEnvironment,
|
||||
},
|
||||
'post'
|
||||
);
|
||||
|
||||
let activeMemberships = [];
|
||||
let canceledMemberships = [];
|
||||
let purchasedMemberships = [];
|
||||
|
||||
for (const membership of response) {
|
||||
// if it's autorenewing it's considered 'active'
|
||||
const isActive = membership.Membership.auto_renew;
|
||||
if (isActive) {
|
||||
activeMemberships.push(membership);
|
||||
} else {
|
||||
canceledMemberships.push(membership);
|
||||
}
|
||||
purchasedMemberships.push(membership.Membership.membership_id);
|
||||
}
|
||||
|
||||
// hide the other membership options if there's already a purchased membership
|
||||
if (activeMemberships.length > 0) {
|
||||
setMembershipOptions(false);
|
||||
}
|
||||
|
||||
setActiveMemberships(activeMemberships);
|
||||
setCanceledMemberships(canceledMemberships);
|
||||
setPurchasedMemberships(purchasedMemberships);
|
||||
|
||||
// update the state to show the badge
|
||||
fetchUserMemberships(userChannelClaimId || '');
|
||||
|
||||
setUserMemberships(response);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
setFetchUserMemberships(false);
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!shouldFetchUserMemberships) setFetchUserMemberships(true);
|
||||
}, [shouldFetchUserMemberships]);
|
||||
|
||||
React.useEffect(function () {
|
||||
(async function () {
|
||||
try {
|
||||
// check if there is a payment method
|
||||
const response = await Lbryio.call(
|
||||
'customer',
|
||||
'status',
|
||||
{
|
||||
environment: stripeEnvironment,
|
||||
},
|
||||
'post'
|
||||
);
|
||||
// hardcoded to first card
|
||||
const hasAPaymentCard = Boolean(response && response.PaymentMethods && response.PaymentMethods[0]);
|
||||
|
||||
setCardSaved(hasAPaymentCard);
|
||||
} catch (err) {
|
||||
const customerDoesntExistError = 'user as customer is not setup yet';
|
||||
if (err.message === customerDoesntExistError) {
|
||||
setCardSaved(false);
|
||||
} else {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// check the available membership for odysee.com
|
||||
const response = await Lbryio.call(
|
||||
'membership',
|
||||
'list',
|
||||
{
|
||||
environment: stripeEnvironment,
|
||||
channel_id: odyseeChannelId,
|
||||
channel_name: odyseeChannelName,
|
||||
},
|
||||
'post'
|
||||
);
|
||||
|
||||
// hide other options if there's already a membership
|
||||
if (activeMemberships && activeMemberships.length > 0) {
|
||||
setMembershipOptions(false);
|
||||
} else {
|
||||
setMembershipOptions(response);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
populateMembershipData();
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const stillWaitingFromBackend =
|
||||
purchasedMemberships === undefined ||
|
||||
cardSaved === undefined ||
|
||||
membershipOptions === undefined ||
|
||||
userMemberships === undefined;
|
||||
|
||||
const formatDate = function (date) {
|
||||
return moment(new Date(date)).format('MMMM DD YYYY');
|
||||
};
|
||||
|
||||
const deleteData = async function () {
|
||||
await Lbryio.call('membership', 'clear', {}, 'post');
|
||||
// $FlowFixMe
|
||||
location.reload();
|
||||
};
|
||||
|
||||
// dont pass channel name and id when calling purchase
|
||||
const noChannelsOrIncognitoMode = incognito || !channels;
|
||||
|
||||
// TODO: can clean this up, some repeating text
|
||||
function buildPurchaseString(price, interval, plan) {
|
||||
let featureString = '';
|
||||
// generate different strings depending on other conditions
|
||||
if (plan === 'Premium' && !noChannelsOrIncognitoMode) {
|
||||
featureString =
|
||||
'Your badge will be shown for your ' +
|
||||
userChannelName +
|
||||
' channel in all areas of the app, and can be added to two additional channels in the future for free. ';
|
||||
} else if (plan === 'Premium+' && !noChannelsOrIncognitoMode) {
|
||||
featureString =
|
||||
'The no ads feature applies site-wide for all channels and your badge will be shown for your ' +
|
||||
userChannelName +
|
||||
' channel in all areas of the app, and can be added to two additional channels in the future for free. ';
|
||||
} else if (plan === 'Premium' && !channels) {
|
||||
featureString =
|
||||
'You currently have no channels. To show your badge on a channel, please create a channel first. ' +
|
||||
'If you register a channel later you will be able to show a badge for up to three channels.';
|
||||
} else if (plan === 'Premium+' && !channels) {
|
||||
featureString =
|
||||
'The no ads feature applies site-wide. You currently have no channels. To show your badge on a channel, please create a channel first. ' +
|
||||
'If you register a channel later you will be able to show a badge for up to three channels.';
|
||||
} else if (plan === 'Premium' && incognito) {
|
||||
featureString =
|
||||
'You currently have no channel selected and will not have a badge be visible, if you want to show a badge you can select a channel now, ' +
|
||||
'or you can show a badge for up to three channels in the future for free.';
|
||||
} else if (plan === 'Premium+' && incognito) {
|
||||
featureString =
|
||||
'The no ads feature applies site-wide. You currently have no channel selected and will not have a badge be visible, ' +
|
||||
'if you want to show a badge you can select a channel now, or you can show a badge for up to three channels in the future for free.';
|
||||
}
|
||||
|
||||
let purchaseString =
|
||||
`You are purchasing a ${interval}ly membership, that is active immediately ` +
|
||||
`and will renew ${interval}ly at a price of ${currencyToUse.toUpperCase()} ${
|
||||
currencyToUse === 'usd' ? '$' : '€'
|
||||
}${price / 100}. ` +
|
||||
featureString +
|
||||
'You can cancel Premium at any time (no refunds) and you can also close this window and choose a different membership option.';
|
||||
|
||||
return __(purchaseString);
|
||||
}
|
||||
|
||||
const purchaseMembership = function (e, membershipOption, price) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const planName = membershipOption.Membership.name;
|
||||
|
||||
const membershipId = e.currentTarget.getAttribute('membership-id');
|
||||
const priceId = e.currentTarget.getAttribute('price-id');
|
||||
const purchaseString = buildPurchaseString(price.unit_amount, price.recurring.interval, planName);
|
||||
|
||||
openModal(MODALS.CONFIRM_ODYSEE_MEMBERSHIP, {
|
||||
membershipId,
|
||||
userChannelClaimId: noChannelsOrIncognitoMode ? undefined : userChannelClaimId,
|
||||
userChannelName: noChannelsOrIncognitoMode ? undefined : userChannelName,
|
||||
priceId,
|
||||
purchaseString,
|
||||
plan: planName,
|
||||
populateMembershipData,
|
||||
setMembershipOptions,
|
||||
updateUserOdyseeMembershipStatus,
|
||||
user,
|
||||
});
|
||||
};
|
||||
|
||||
const cancelMembership = async function (e, membership) {
|
||||
const membershipId = e.currentTarget.getAttribute('membership-id');
|
||||
|
||||
const cancellationString =
|
||||
'You are cancelling your Odysee Premium. You will still have access to all the paid ' +
|
||||
'features until the point of the expiration of your current membership, at which point you will not be charged ' +
|
||||
'again and your membership will no longer be active. At this time, there is no way to subscribe to another membership if you cancel and there are no refunds.';
|
||||
|
||||
openModal(MODALS.CONFIRM_ODYSEE_MEMBERSHIP, {
|
||||
membershipId,
|
||||
hasMembership,
|
||||
purchaseString: cancellationString,
|
||||
populateMembershipData,
|
||||
});
|
||||
};
|
||||
|
||||
function convertPriceToString(price) {
|
||||
const interval = price.recurring.interval;
|
||||
|
||||
if (interval === 'year') {
|
||||
return 'Yearly';
|
||||
} else if (interval === 'month') {
|
||||
return 'Monthly';
|
||||
}
|
||||
}
|
||||
|
||||
function capitalizeWord(string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
}
|
||||
|
||||
function buildCurrencyDisplay(priceObject) {
|
||||
let currencySymbol;
|
||||
if (priceObject.currency === 'eur') {
|
||||
currencySymbol = '€';
|
||||
} else if (priceObject.currency === 'usd') {
|
||||
currencySymbol = '$';
|
||||
}
|
||||
|
||||
const currency = priceObject.currency.toUpperCase();
|
||||
|
||||
return currency + ' ' + currencySymbol;
|
||||
}
|
||||
|
||||
const urlSearchParams = new URLSearchParams(window.location.search);
|
||||
const params = Object.fromEntries(urlSearchParams.entries());
|
||||
|
||||
const { interval, plan } = params;
|
||||
|
||||
const planValue = params.plan;
|
||||
|
||||
// description to be shown under plan name
|
||||
function getPlanDescription(plan) {
|
||||
if (plan === 'Premium') {
|
||||
return 'Badge on profile, exclusive and early access to features';
|
||||
|
||||
// if there's more plans added this needs to be expanded
|
||||
} else {
|
||||
return 'All Premium features, and no ads';
|
||||
}
|
||||
}
|
||||
|
||||
// add a bit of a delay otherwise it's a bit jarring
|
||||
const timeoutValue = 300;
|
||||
|
||||
// if user already selected plan, wait a bit (so it's not jarring) and open modal
|
||||
React.useEffect(() => {
|
||||
if (!stillWaitingFromBackend && planValue && cardSaved) {
|
||||
setTimeout(function () {
|
||||
// clear query params
|
||||
window.history.replaceState(null, null, window.location.pathname);
|
||||
|
||||
setHasShownModal(true);
|
||||
|
||||
// open confirm purchase
|
||||
// $FlowFixMe
|
||||
document.querySelector('[plan="' + plan + '"][interval="' + interval + '"]').click();
|
||||
}, timeoutValue);
|
||||
}
|
||||
}, [stillWaitingFromBackend, planValue, cardSaved]);
|
||||
|
||||
const helpText = (
|
||||
<div className="section__subtitle">
|
||||
<p>
|
||||
{__(
|
||||
`First of all, thank you for considering or purchasing a membership, it means a ton to us! A few important details to know: `
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<ul>
|
||||
<li>
|
||||
{__(
|
||||
`Early access and exclusive features include: livestreaming and the ability to post odysee hyperlinks and images in comments + blogs. More to come later.`
|
||||
)}
|
||||
<li>
|
||||
{__(
|
||||
`The yearly Premium+ membership has a discount compared to monthly, and Premium is only available yearly.`
|
||||
)}
|
||||
</li>
|
||||
<li>{__(`These are limited time rates, so get in early!`)}</li>
|
||||
</li>
|
||||
<li>
|
||||
{__(
|
||||
`There may be higher tiers available in the future for creators and anyone else who wants to support us.`
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{__(`Badges will be displayed on a single channel to start, with an option to add on two more later on.`)}
|
||||
</li>
|
||||
<li>
|
||||
{__(`Cannot upgrade or downgrade a membership at this time. Refunds are not available. Choose wisely.`)}
|
||||
</li>
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Page className="premium-wrapper">
|
||||
{/** splash frontend **/}
|
||||
{!stillWaitingFromBackend && purchasedMemberships.length === 0 && !planValue && !hasShownModal ? (
|
||||
<MembershipSplash pageLocation={'confirmPage'} currencyToUse={currencyToUse} />
|
||||
) : (
|
||||
/** odysee membership page **/
|
||||
<div className={'card-stack'}>
|
||||
{!stillWaitingFromBackend && cardSaved !== false && (
|
||||
<>
|
||||
<h1 style={{ fontSize: '23px' }}>{__('Odysee Premium')}</h1>
|
||||
{/* let user switch channel */}
|
||||
<div style={{ marginTop: '10px' }}>
|
||||
<ChannelSelector uri={activeChannelClaim && activeChannelClaim.permanent_url} />
|
||||
|
||||
{/* explainer help text */}
|
||||
<Card
|
||||
titleActions={
|
||||
<Button
|
||||
button="close"
|
||||
icon={showHelp ? ICONS.UP : ICONS.DOWN}
|
||||
onClick={() => setShowHelp(!showHelp)}
|
||||
/>
|
||||
}
|
||||
title={__('Get More Information')}
|
||||
subtitle={<>{__(`Expand to learn more about how Odysee Premium works`)} </>}
|
||||
actions={showHelp && helpText}
|
||||
className={'premium-explanation-text'}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/** available memberships **/}
|
||||
{/* if they have a card and don't have a membership yet */}
|
||||
{!stillWaitingFromBackend && membershipOptions && purchasedMemberships.length < 1 && cardSaved !== false && (
|
||||
<>
|
||||
<div className="card__title-section">
|
||||
<h2 className="card__title">{__('Available Memberships')}</h2>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
{membershipOptions.map((membershipOption, i) => (
|
||||
<>
|
||||
<div key={i}>
|
||||
{purchasedMemberships && !purchasedMemberships.includes(membershipOption.Membership.id) && (
|
||||
<>
|
||||
<div className="premium-option">
|
||||
<h4 className="membership_title">
|
||||
{membershipOption.Membership.name}
|
||||
<PremiumBadge membership={membershipOption.Membership.name} />
|
||||
</h4>
|
||||
|
||||
{/* plan description */}
|
||||
<h4 className="membership_subtitle">
|
||||
{getPlanDescription(membershipOption.Membership.name)}
|
||||
</h4>
|
||||
<>
|
||||
{membershipOption.Prices.map((price) => (
|
||||
<>
|
||||
{/* dont show a monthly Premium membership option */}
|
||||
{!(
|
||||
price.recurring.interval === 'month' &&
|
||||
membershipOption.Membership.name === 'Premium'
|
||||
) && (
|
||||
<>
|
||||
{price.currency === currencyToUse && (
|
||||
<div>
|
||||
<h4 className="membership_info">
|
||||
<b>Interval:</b> {convertPriceToString(price)}
|
||||
</h4>
|
||||
<h4 className="membership_info">
|
||||
<b>Price:</b> {buildCurrencyDisplay(price)}
|
||||
{price.unit_amount / 100}/{capitalizeWord(price.recurring.interval)}
|
||||
</h4>
|
||||
<Button
|
||||
button="primary"
|
||||
onClick={(e) => purchaseMembership(e, membershipOption, price)}
|
||||
membership-id={membershipOption.Membership.id}
|
||||
membership-subscription-period={membershipOption.Membership.type}
|
||||
price-id={price.id}
|
||||
className="membership_button"
|
||||
label={__('Join via ' + price.recurring.interval + 'ly membership')}
|
||||
icon={ICONS.FINANCE}
|
||||
interval={price.recurring.interval}
|
||||
plan={membershipOption.Membership.name}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
</>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
))}
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
{!stillWaitingFromBackend && cardSaved === true && (
|
||||
<>
|
||||
<div className="card__title-section">
|
||||
<h2 className="card__title">{__('Your Active Memberships')}</h2>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
{/** * list of active memberships from user ***/}
|
||||
<div>
|
||||
{/* <h1 style={{ fontSize: '19px' }}>Active Memberships</h1> */}
|
||||
{!stillWaitingFromBackend && activeMemberships && activeMemberships.length === 0 && (
|
||||
<>
|
||||
<h4>{__('You currently have no active memberships')}</h4>
|
||||
</>
|
||||
)}
|
||||
{/** active memberships **/}
|
||||
{!stillWaitingFromBackend &&
|
||||
activeMemberships &&
|
||||
activeMemberships.map((membership) => (
|
||||
<>
|
||||
<div className="premium-option">
|
||||
{/* membership name */}
|
||||
<h4 className="membership_title">
|
||||
{membership.MembershipDetails.name}
|
||||
<PremiumBadge membership={membership.MembershipDetails.name} />
|
||||
</h4>
|
||||
|
||||
{/* description section */}
|
||||
<h4 className="membership_subtitle">
|
||||
{getPlanDescription(membership.MembershipDetails.name)}
|
||||
</h4>
|
||||
|
||||
<h4 className="membership_info">
|
||||
<b>{__('Registered On:')}</b> {formatDate(membership.Membership.created_at)}
|
||||
</h4>
|
||||
<h4 className="membership_info">
|
||||
<b>{__('Auto-Renews On')}:</b>{' '}
|
||||
{formatDate(membership.Subscription.current_period_end * 1000)}
|
||||
</h4>
|
||||
{!stillWaitingFromBackend && membership.type === 'yearly' && (
|
||||
<>
|
||||
<h4 className="membership_info">
|
||||
<b>{__('Membership Period Options:')}</b> {__('Yearly')}
|
||||
</h4>
|
||||
<h4 className="membership_info">
|
||||
${(membership.cost_usd * 12) / 100} {__('USD For A One Year Membership')} ($
|
||||
{membership.cost_usd / 100} {__('Per Month')})
|
||||
</h4>
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
button="alt"
|
||||
membership-id={membership.Membership.membership_id}
|
||||
onClick={(e) => cancelMembership(e, membership)}
|
||||
className="cancel-membership-button"
|
||||
label={__('Cancel membership')}
|
||||
icon={ICONS.FINANCE}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
<>
|
||||
{/** canceled memberships **/}
|
||||
<div className="card__title-section">
|
||||
<h2 className="card__title">{__('Canceled Memberships')}</h2>
|
||||
</div>
|
||||
<Card>
|
||||
{canceledMemberships && canceledMemberships.length === 0 && (
|
||||
<>
|
||||
<h4>{__('You currently have no canceled memberships')}</h4>
|
||||
</>
|
||||
)}
|
||||
{canceledMemberships &&
|
||||
canceledMemberships.map((membership) => (
|
||||
<>
|
||||
<h4 className="membership_title">
|
||||
{membership.MembershipDetails.name}
|
||||
<PremiumBadge membership={membership.MembershipDetails.name} />
|
||||
</h4>
|
||||
|
||||
<div className="premium-option">
|
||||
<h4 className="membership_info">
|
||||
<b>{__('Registered On:')}</b> {formatDate(membership.Membership.created_at)}
|
||||
</h4>
|
||||
<h4 className="membership_info">
|
||||
<b>{__('Canceled On:')}</b> {formatDate(membership.Subscription.canceled_at * 1000)}
|
||||
</h4>
|
||||
<h4 className="membership_info">
|
||||
<b>{__('Still Valid Until:')}</b> {formatDate(membership.Membership.expires)}
|
||||
</h4>
|
||||
</div>
|
||||
</>
|
||||
))}
|
||||
</Card>
|
||||
</>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/** send user to add card if they don't have one yet */}
|
||||
{!stillWaitingFromBackend && cardSaved === false && (
|
||||
<div>
|
||||
<br />
|
||||
<h2 className={'getPaymentCard'}>
|
||||
{__(
|
||||
'Please save a card as a payment method so you can join Odysee Premium. After the card is added, click Back.'
|
||||
)}
|
||||
</h2>
|
||||
|
||||
<Button
|
||||
button="primary"
|
||||
label={__('Add A Card')}
|
||||
icon={ICONS.SETTINGS}
|
||||
navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`}
|
||||
className="membership_button"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/** loading section **/}
|
||||
{stillWaitingFromBackend && (
|
||||
<div className="main--empty">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/** clear membership data (only available on dev) **/}
|
||||
{isDev && cardSaved && purchasedMemberships.length > 0 && (
|
||||
<>
|
||||
<h1 style={{ marginTop: '30px', fontSize: '20px' }}>
|
||||
{__('Clear Membership Data (Only Available On Dev)')}
|
||||
</h1>
|
||||
<div>
|
||||
<Button
|
||||
button="primary"
|
||||
label={__('Clear Membership Data')}
|
||||
icon={ICONS.SETTINGS}
|
||||
className="membership_button"
|
||||
onClick={deleteData}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Page>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default OdyseeMembershipPage;
|
|
@ -22,7 +22,7 @@ import {
|
|||
} from 'redux/selectors/file_info';
|
||||
|
||||
type Dispatch = (action: any) => any;
|
||||
type GetState = () => { claims: any, file: FileState, content: any };
|
||||
type GetState = () => { claims: any, file: FileState, content: any, user: User };
|
||||
export function doOpenFileInFolder(path: string) {
|
||||
return () => {
|
||||
shell.showItemInFolder(path);
|
||||
|
|
|
@ -13,7 +13,7 @@ import { SEARCH_SERVER_API, SEARCH_SERVER_API_ALT } from 'config';
|
|||
import { SEARCH_OPTIONS } from 'constants/search';
|
||||
|
||||
type Dispatch = (action: any) => any;
|
||||
type GetState = () => { claims: any, search: SearchState };
|
||||
type GetState = () => { claims: any, search: SearchState, user: User };
|
||||
|
||||
type SearchOptions = {
|
||||
size?: number,
|
||||
|
|
|
@ -3,6 +3,7 @@ import { makeSelectClaimForUri } from 'redux/selectors/claims';
|
|||
import { doFetchChannelListMine } from 'redux/actions/claims';
|
||||
import { isURIValid, normalizeURI } from 'util/lbryURI';
|
||||
import { batchActions } from 'util/batch-actions';
|
||||
import { getStripeEnvironment } from 'util/stripe';
|
||||
|
||||
import * as ACTIONS from 'constants/action_types';
|
||||
import { doClaimRewardType, doRewardList } from 'redux/actions/rewards';
|
||||
|
@ -16,6 +17,9 @@ const AUTH_IN_PROGRESS = 'authInProgress';
|
|||
export let sessionStorageAvailable = false;
|
||||
const CHECK_INTERVAL = 200;
|
||||
const AUTH_WAIT_TIMEOUT = 10000;
|
||||
const stripeEnvironment = getStripeEnvironment();
|
||||
|
||||
const ODYSEE_CHANNEL_ID = '80d2590ad04e36fb1d077a9b9e3a8bba76defdf8';
|
||||
|
||||
export function doFetchInviteStatus(shouldCallRewardList = true) {
|
||||
return (dispatch) => {
|
||||
|
@ -101,6 +105,59 @@ function checkAuthBusy() {
|
|||
});
|
||||
}
|
||||
|
||||
/***
|
||||
* Given a user, return their highest ranking Odysee membership (Premium or Premium Plus)
|
||||
* @param dispatch
|
||||
* @param user
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export function doCheckUserOdyseeMemberships(user) {
|
||||
return async (dispatch) => {
|
||||
// get memberships for a given user
|
||||
// TODO: in the future, can we specify this just to @odysee?
|
||||
|
||||
const response = await Lbryio.call(
|
||||
'membership',
|
||||
'mine',
|
||||
{
|
||||
environment: stripeEnvironment,
|
||||
},
|
||||
'post'
|
||||
);
|
||||
|
||||
let savedMemberships = [];
|
||||
let highestMembershipRanking;
|
||||
|
||||
// TODO: this will work for now, but it should be adjusted
|
||||
// TODO: to check if it's active, or if it's cancelled if it's still valid past current date
|
||||
// loop through all memberships and save the @odysee ones
|
||||
// maybe in the future we can only hit @odysee in the API call
|
||||
for (const membership of response) {
|
||||
if (membership.MembershipDetails && membership.MembershipDetails.channel_name === '@odysee') {
|
||||
savedMemberships.push(membership.MembershipDetails.name);
|
||||
}
|
||||
}
|
||||
|
||||
// determine highest ranking membership based on returned data
|
||||
// note: this is from an odd state in the API where a user can be both premium/Premium + at the same time
|
||||
// I expect this can change once upgrade/downgrade is implemented
|
||||
if (savedMemberships.length > 0) {
|
||||
// if premium plus is a membership, return that, otherwise it's only premium
|
||||
const premiumPlusExists = savedMemberships.includes('Premium+');
|
||||
if (premiumPlusExists) {
|
||||
highestMembershipRanking = 'Premium+';
|
||||
} else {
|
||||
highestMembershipRanking = 'Premium';
|
||||
}
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: ACTIONS.ADD_ODYSEE_MEMBERSHIP_DATA,
|
||||
data: { user, odyseeMembershipName: highestMembershipRanking },
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Call doInstallNew separately so we don't have to pass appVersion and os_system params?
|
||||
export function doAuthenticate(
|
||||
appVersion,
|
||||
|
@ -124,6 +181,11 @@ export function doAuthenticate(
|
|||
data: { user, accessToken: token },
|
||||
});
|
||||
|
||||
// if user is an Odysee member, get the membership details
|
||||
if (user.odysee_member) {
|
||||
dispatch(doCheckUserOdyseeMemberships(user));
|
||||
}
|
||||
|
||||
if (shareUsageData) {
|
||||
dispatch(doRewardList());
|
||||
|
||||
|
@ -153,6 +215,11 @@ export function doUserFetch() {
|
|||
|
||||
Lbryio.getCurrentUser()
|
||||
.then((user) => {
|
||||
// get user membership status
|
||||
if (user.odysee_member) {
|
||||
dispatch(doCheckUserOdyseeMemberships(user));
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: ACTIONS.USER_FETCH_SUCCESS,
|
||||
data: { user },
|
||||
|
@ -174,6 +241,11 @@ export function doUserCheckEmailVerified() {
|
|||
return (dispatch) => {
|
||||
Lbryio.getCurrentUser().then((user) => {
|
||||
if (user.has_verified_email) {
|
||||
// check premium membership
|
||||
if (user.odysee_member) {
|
||||
dispatch(doCheckUserOdyseeMemberships(user));
|
||||
}
|
||||
|
||||
dispatch(doRewardList());
|
||||
|
||||
dispatch({
|
||||
|
@ -347,16 +419,19 @@ export function doUserCheckIfEmailExists(email) {
|
|||
|
||||
Lbryio.call('user', 'exists', { email }, 'post')
|
||||
.catch((error) => {
|
||||
// no email
|
||||
if (error.response && error.response.status === 404) {
|
||||
dispatch({
|
||||
type: ACTIONS.USER_EMAIL_NEW_DOES_NOT_EXIST,
|
||||
});
|
||||
// sign in by email
|
||||
} else if (error.response && error.response.status === 412) {
|
||||
triggerEmailFlow(false);
|
||||
}
|
||||
|
||||
throw error;
|
||||
})
|
||||
// sign the user in
|
||||
.then(success, failure);
|
||||
};
|
||||
}
|
||||
|
@ -784,3 +859,40 @@ export function doCheckYoutubeTransfer() {
|
|||
});
|
||||
};
|
||||
}
|
||||
|
||||
/***
|
||||
* Receives a csv of channel claim ids, hits the backend and returns nicely formatted object with relevant info
|
||||
* @param claimIdCsv
|
||||
* @returns {(function(*): Promise<void>)|*}
|
||||
*/
|
||||
export function doFetchUserMemberships(claimIdCsv) {
|
||||
return async (dispatch) => {
|
||||
// check if users have odysee memberships (premium/premium+)
|
||||
const response = await Lbryio.call('membership', 'check', {
|
||||
channel_id: ODYSEE_CHANNEL_ID,
|
||||
claim_ids: claimIdCsv,
|
||||
});
|
||||
|
||||
let updatedResponse = {};
|
||||
|
||||
// loop through returned users
|
||||
for (const user in response) {
|
||||
// if array was returned for a user (indicating a membership exists), otherwise is null
|
||||
if (response[user] && response[user].length) {
|
||||
// get membership for user
|
||||
// note: a for loop is kind of odd, indicates there may be multiple memberships?
|
||||
// probably not needed depending on what we do with the frontend, should revisit
|
||||
for (const membership of response[user]) {
|
||||
if (membership.channel_name) {
|
||||
updatedResponse[user] = membership.name;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// note the user has been fetched but is null
|
||||
updatedResponse[user] = null;
|
||||
}
|
||||
}
|
||||
|
||||
dispatch({ type: ACTIONS.ADD_CLAIMIDS_MEMBERSHIP_DATA, data: { response: updatedResponse } });
|
||||
};
|
||||
}
|
||||
|
|
|
@ -363,6 +363,28 @@ reducers[ACTIONS.USER_PASSWORD_SET_FAILURE] = (state, action) =>
|
|||
passwordSetError: action.data.error,
|
||||
});
|
||||
|
||||
reducers[ACTIONS.ADD_ODYSEE_MEMBERSHIP_DATA] = (state, action) => {
|
||||
return Object.assign({}, state, {
|
||||
odyseeMembershipName: action.data.odyseeMembershipName,
|
||||
});
|
||||
};
|
||||
|
||||
reducers[ACTIONS.ADD_CLAIMIDS_MEMBERSHIP_DATA] = (state, action) => {
|
||||
let latestData = {};
|
||||
|
||||
// add additional user membership value
|
||||
if (state.odyseeMembershipsPerClaimIds) {
|
||||
latestData = Object.assign({}, state.odyseeMembershipsPerClaimIds, action.data.response);
|
||||
} else {
|
||||
// otherwise just send the current data because nothing is saved yet
|
||||
latestData = action.data.response;
|
||||
}
|
||||
|
||||
return Object.assign({}, state, {
|
||||
odyseeMembershipsPerClaimIds: latestData,
|
||||
});
|
||||
};
|
||||
|
||||
export default function userReducer(state = defaultState, action) {
|
||||
const handler = reducers[action.type];
|
||||
if (handler) return handler(state, action);
|
||||
|
|
|
@ -9,7 +9,7 @@ import { isClaimNsfw, filterClaims, getChannelIdFromClaim, isStreamPlaceholderCl
|
|||
import * as CLAIM from 'constants/claim';
|
||||
import { INTERNAL_TAGS } from 'constants/tags';
|
||||
|
||||
type State = { claims: any };
|
||||
type State = { claims: any, user: User };
|
||||
|
||||
const selectState = (state: State) => state.claims || {};
|
||||
|
||||
|
@ -791,3 +791,41 @@ export const selectIsMyChannelCountOverLimit = createSelector(
|
|||
return false;
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Given a uri of a channel, check if there an Odysee membership value
|
||||
* @param state
|
||||
* @param uri
|
||||
* @returns {*}
|
||||
*/
|
||||
export const selectOdyseeMembershipForUri = function (state: State, uri: string) {
|
||||
const claim = selectClaimForUri(state, uri);
|
||||
|
||||
const uploaderChannelClaimId = getChannelIdFromClaim(claim);
|
||||
|
||||
// looks for the uploader id
|
||||
if (uploaderChannelClaimId) {
|
||||
const matchingMembershipOfUser =
|
||||
state.user &&
|
||||
state.user.odyseeMembershipsPerClaimIds &&
|
||||
state.user.odyseeMembershipsPerClaimIds[uploaderChannelClaimId];
|
||||
|
||||
return matchingMembershipOfUser;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a uri of a channel, check if there an Odysee membership value
|
||||
* @param state
|
||||
* @param channelId
|
||||
* @returns {*}
|
||||
*/
|
||||
export const selectOdyseeMembershipForChannelId = function (state: State, channelId: string) {
|
||||
// looks for the uploader id
|
||||
const matchingMembershipOfUser =
|
||||
state.user && state.user.odyseeMembershipsPerClaimIds && state.user.odyseeMembershipsPerClaimIds[channelId];
|
||||
|
||||
return matchingMembershipOfUser;
|
||||
};
|
||||
|
|
|
@ -16,7 +16,7 @@ import { isClaimNsfw, getChannelFromClaim } from 'util/claim';
|
|||
import { selectSubscriptionUris } from 'redux/selectors/subscriptions';
|
||||
import { getCommentsListTitle } from 'util/comments';
|
||||
|
||||
type State = { claims: any, comments: CommentsState };
|
||||
type State = { claims: any, comments: CommentsState, user: User };
|
||||
|
||||
const selectState = (state) => state.comments || {};
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ import { FORCE_CONTENT_TYPE_PLAYER, FORCE_CONTENT_TYPE_COMIC } from 'constants/c
|
|||
const RECENT_HISTORY_AMOUNT = 10;
|
||||
const HISTORY_ITEMS_PER_PAGE = 50;
|
||||
|
||||
type State = { claims: any, content: any };
|
||||
type State = { claims: any, content: any, user: User };
|
||||
|
||||
export const selectState = (state: State) => state.content || {};
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ import { selectHistory } from 'redux/selectors/content';
|
|||
import { selectAllCostInfoByUri } from 'lbryinc';
|
||||
import { SIMPLE_SITE } from 'config';
|
||||
|
||||
type State = { claims: any, search: SearchState };
|
||||
type State = { claims: any, search: SearchState, user: User };
|
||||
|
||||
export const selectState = (state: State): SearchState => state.search;
|
||||
|
||||
|
|
|
@ -104,6 +104,14 @@ export const selectYouTubeImportError = (state) => selectState(state).youtubeCha
|
|||
export const selectSetReferrerPending = (state) => selectState(state).referrerSetIsPending;
|
||||
export const selectSetReferrerError = (state) => selectState(state).referrerSetError;
|
||||
|
||||
export const selectOdyseeMembershipName = (state) => selectState(state).odyseeMembershipName;
|
||||
|
||||
export const selectOdyseeMembershipIsPremiumPlus = (state) => {
|
||||
const odyseeMembershipName = selectState(state).odyseeMembershipName;
|
||||
if (!odyseeMembershipName) return undefined;
|
||||
return selectState(state).odyseeMembershipName === 'Premium+';
|
||||
};
|
||||
|
||||
export const selectYouTubeImportVideosComplete = createSelector(selectState, (state) => {
|
||||
const total = state.youtubeChannelImportTotal;
|
||||
const complete = state.youtubeChannelImportComplete || 0;
|
||||
|
|
|
@ -36,6 +36,7 @@
|
|||
@import 'component/markdown-editor';
|
||||
@import 'component/markdown-preview';
|
||||
@import 'component/media';
|
||||
@import 'component/membership';
|
||||
@import 'component/menu-button';
|
||||
@import 'component/modal';
|
||||
@import 'component/nag';
|
||||
|
|
|
@ -285,12 +285,27 @@ a.button--alt {
|
|||
max-width: 100%;
|
||||
text-align: left;
|
||||
|
||||
.comment__badge {
|
||||
padding-right: 0px;
|
||||
|
||||
svg {
|
||||
height: 1.2rem;
|
||||
width: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.channel-name {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
font-size: var(--font-xsmall);
|
||||
color: rgba(var(--color-text-base), 0.6);
|
||||
|
||||
.icon {
|
||||
margin-left: var(--spacing-xxs);
|
||||
}
|
||||
}
|
||||
|
||||
.markdown-preview & {
|
||||
|
|
|
@ -329,9 +329,10 @@
|
|||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
@media (min-width: $breakpoint-small) {
|
||||
// padding: var(--spacing-s);
|
||||
// padding-right: 0px;
|
||||
@media (max-width: $breakpoint-small) {
|
||||
.button--close {
|
||||
top: calc(var(--spacing-m) * -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -199,6 +199,37 @@ $actions-z-index: 2;
|
|||
width: 5rem;
|
||||
background-size: cover;
|
||||
margin-right: var(--spacing-m);
|
||||
|
||||
.button {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
|
||||
width: 80%;
|
||||
bottom: -16%;
|
||||
left: unset !important;
|
||||
background-color: unset !important;
|
||||
right: -1rem;
|
||||
}
|
||||
|
||||
.link--small {
|
||||
right: -3rem !important;
|
||||
}
|
||||
|
||||
.comment__badge {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
width: 80%;
|
||||
left: 22%;
|
||||
bottom: -30%;
|
||||
|
||||
svg {
|
||||
stroke: unset;
|
||||
overflow: visible;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.channel-thumbnail--resolving {
|
||||
|
@ -237,6 +268,31 @@ $actions-z-index: 2;
|
|||
top: 0;
|
||||
margin-top: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.comment__badge {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
width: 60%;
|
||||
left: 0;
|
||||
|
||||
svg {
|
||||
stroke: unset;
|
||||
overflow: visible;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.claim-preview .channel-thumbnail .comment__badge {
|
||||
padding: 0px;
|
||||
left: 12%;
|
||||
bottom: -20%;
|
||||
|
||||
svg {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.channel-thumbnail__custom {
|
||||
|
@ -332,6 +388,15 @@ $actions-z-index: 2;
|
|||
@media (max-width: $breakpoint-small) {
|
||||
padding-top: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.comment__badge {
|
||||
padding-right: 0px;
|
||||
|
||||
svg {
|
||||
height: 2.5rem;
|
||||
width: 2.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.channel__meta {
|
||||
|
@ -501,6 +566,15 @@ $actions-z-index: 2;
|
|||
padding: var(--spacing-s);
|
||||
overflow: hidden;
|
||||
|
||||
.comment__badge {
|
||||
padding-right: 0px;
|
||||
|
||||
svg {
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.channel-thumbnail {
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
|
@ -634,6 +708,19 @@ $actions-z-index: 2;
|
|||
}
|
||||
}
|
||||
|
||||
.icon__wrapper--PremiumPlusBadge {
|
||||
margin-left: 3px;
|
||||
height: 22px !important;
|
||||
width: 22px !important;
|
||||
padding: 0 !important;
|
||||
display: inline-block !important;
|
||||
margin-bottom: -9px !important;
|
||||
}
|
||||
|
||||
.icon__wrapper--PremiumPlusBadge > svg {
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
.channelsPage-wrapper {
|
||||
.claim-preview__wrapper--channel {
|
||||
position: relative;
|
||||
|
|
|
@ -8,16 +8,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
.comment__badge--globalMod {
|
||||
.st0 {
|
||||
// @see: ICONS.BADGE_MOD
|
||||
fill: #fe7500;
|
||||
}
|
||||
// fix contrast on hover of channel selector, couldn't come up with a better way
|
||||
div[role='menuitem'] .channel__list-item .comment__badge svg {
|
||||
stroke: unset !important;
|
||||
}
|
||||
|
||||
.comment__badge--mod {
|
||||
.st0 {
|
||||
// @see: ICONS.BADGE_MOD
|
||||
fill: #ff3850;
|
||||
}
|
||||
// icon is a bit bright and loud
|
||||
.icon--PremiumPlus {
|
||||
filter: brightness(0.92);
|
||||
}
|
||||
|
|
|
@ -228,6 +228,11 @@ $thumbnailWidthSmall: 2rem;
|
|||
color: rgba(var(--color-secondary-dynamic), 1);
|
||||
}
|
||||
}
|
||||
|
||||
.comment__badge svg {
|
||||
height: 1.4rem;
|
||||
width: 1.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.comment__pin {
|
||||
|
|
|
@ -779,6 +779,17 @@ $recent-msg-button__height: 2rem;
|
|||
color: var(--color-black);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.channel-name {
|
||||
max-width: unset;
|
||||
|
||||
p {
|
||||
max-width: 5rem;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.livestreamSuperchat__info--notSticker {
|
||||
|
|
|
@ -86,6 +86,11 @@
|
|||
|
||||
.livestreamComment__info {
|
||||
overflow: hidden;
|
||||
|
||||
.comment__badge svg {
|
||||
height: 1.4rem;
|
||||
width: 1.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.livestream__comment--superchat {
|
||||
|
|
68
ui/scss/component/_membership.scss
Normal file
68
ui/scss/component/_membership.scss
Normal file
|
@ -0,0 +1,68 @@
|
|||
.membership_title {
|
||||
margin-top: var(--spacing-l);
|
||||
font-size: 18px;
|
||||
font-weight: var(--font-weight-bold);
|
||||
|
||||
.comment__badge {
|
||||
margin-left: var(--spacing-s);
|
||||
|
||||
svg {
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.membership_title:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.membership_subtitle {
|
||||
font-size: var(--font-small);
|
||||
color: var(--color-text-subtitle);
|
||||
margin-bottom: var(--spacing-s);
|
||||
}
|
||||
|
||||
.membership_info {
|
||||
font-size: var(--font-small);
|
||||
}
|
||||
|
||||
.membership_button {
|
||||
display: block;
|
||||
margin-bottom: 18px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.cancel-membership-button {
|
||||
display: block;
|
||||
margin-bottom: 18px;
|
||||
margin-top: 13px;
|
||||
}
|
||||
|
||||
.explanation-text {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.premium-explanation-text {
|
||||
.icon--ChevronDown,
|
||||
.icon--ChevronUp {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
.card__main-actions {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.button--close {
|
||||
margin-top: 3px;
|
||||
left: 223px;
|
||||
width: 34px;
|
||||
}
|
||||
|
||||
.section__subtitle {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
margin-bottom: 21px;
|
||||
}
|
|
@ -49,3 +49,353 @@
|
|||
.doodle {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
// Membership Splash
|
||||
.membership-splash {
|
||||
padding-left: 10rem;
|
||||
padding-right: 10rem;
|
||||
display: flex;
|
||||
flex-flow: column wrap;
|
||||
justify-content: space-around;
|
||||
transition: padding 0.6s;
|
||||
|
||||
::selection {
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
@media (min-width: $breakpoint-xlarge) {
|
||||
padding-left: 0rem;
|
||||
padding-right: 0rem;
|
||||
margin-left: -16rem;
|
||||
margin-right: -16rem;
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-medium) {
|
||||
padding-left: 4rem;
|
||||
padding-right: 4rem;
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
padding-left: unset;
|
||||
padding-right: unset;
|
||||
}
|
||||
|
||||
.membership-splash__banner {
|
||||
display: flex;
|
||||
flex-basis: 100%;
|
||||
flex: auto;
|
||||
align-items: center;
|
||||
background: #283263;
|
||||
margin-bottom: var(--spacing-xxs);
|
||||
img {
|
||||
display: flex;
|
||||
width: 50%;
|
||||
flex-basis: 50%;
|
||||
}
|
||||
@media (max-width: $breakpoint-small) {
|
||||
flex-flow: column-reverse;
|
||||
img {
|
||||
width: 100%;
|
||||
flex-basis: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.membership-splash__title {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
flex-basis: 50%;
|
||||
padding: var(--spacing-l);
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
color: #fff;
|
||||
// font-size: 2.6rem;
|
||||
font-size: 2.3vw;
|
||||
line-height: 2.8vw;
|
||||
font-weight: 100;
|
||||
b {
|
||||
font-weight: 900;
|
||||
}
|
||||
section {
|
||||
&:first-of-type {
|
||||
margin-bottom: 2rem;
|
||||
img {
|
||||
width: 96%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
font-size: 2rem;
|
||||
line-height: 2.1rem;
|
||||
margin-bottom: 1.6rem;
|
||||
section {
|
||||
&:first-of-type {
|
||||
img {
|
||||
margin-top: 1.6rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.membership-splash__info-wrapper {
|
||||
display: flex;
|
||||
flex-basis: 100%;
|
||||
.membership-splash__info {
|
||||
position: relative;
|
||||
background: #fff;
|
||||
flex-basis: 33%;
|
||||
color: #000;
|
||||
padding-bottom: 4.2rem;
|
||||
.membership-splash__info-content {
|
||||
padding-left: var(--spacing-m);
|
||||
}
|
||||
}
|
||||
@media (max-width: $breakpoint-small) {
|
||||
flex-flow: column;
|
||||
.membership-splash__info {
|
||||
flex-basis: 100%;
|
||||
margin-bottom: var(--spacing-xxs);
|
||||
}
|
||||
|
||||
// "creating a revolutionary platform.." copy
|
||||
.membership-splash__info:nth-of-type(1) {
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.membership-splash__info:nth-child(1) {
|
||||
// padding: var(--spacing-l);
|
||||
padding: 3%;
|
||||
font-size: 1vw;
|
||||
@media (min-width: $breakpoint-xlarge) {
|
||||
font-size: 1vw;
|
||||
}
|
||||
@media (max-width: $breakpoint-medium) {
|
||||
font-size: 1.3vw;
|
||||
}
|
||||
@media (max-width: $breakpoint-small) {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
.membership-splash__info:nth-child(2) {
|
||||
margin-left: var(--spacing-xxs);
|
||||
margin-right: var(--spacing-xxs);
|
||||
.membership-splash__info-header {
|
||||
background-color: #d5cee5;
|
||||
color: #626092;
|
||||
}
|
||||
@media (max-width: $breakpoint-small) {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
.membership-splash__info:nth-child(3) {
|
||||
.membership-splash__info-header {
|
||||
background-color: #ffd976;
|
||||
color: #c95b16;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.membership-splash__info-header {
|
||||
margin-bottom: 18px;
|
||||
.membership-splash__info-price {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 2.6rem;
|
||||
font-size: 2.6vw;
|
||||
font-weight: 900;
|
||||
img {
|
||||
display: inline-block;
|
||||
width: 36%;
|
||||
//margin-left: 1rem;
|
||||
margin-left: 7%;
|
||||
//margin-right: 1.4rem;
|
||||
margin-right: 7%;
|
||||
}
|
||||
section {
|
||||
display: inline-block;
|
||||
}
|
||||
.membership-splash__info-range {
|
||||
font-size: 1rem;
|
||||
margin-top: -10px;
|
||||
}
|
||||
@media (min-width: $breakpoint-xlarge) {
|
||||
img {
|
||||
//margin-left: 2%;
|
||||
width: 33%;
|
||||
}
|
||||
.membership-splash__info-range {
|
||||
font-size: 1.8rem;
|
||||
margin-top: -1.6rem;
|
||||
}
|
||||
}
|
||||
@media (max-width: $breakpoint-medium) {
|
||||
img {
|
||||
//margin-left: 2%;
|
||||
width: 30%;
|
||||
}
|
||||
}
|
||||
@media (max-width: $breakpoint-small) {
|
||||
font-size: 2.2rem;
|
||||
img {
|
||||
margin-left: 1.4rem;
|
||||
margin-right: 1.4rem;
|
||||
width: 5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.membership-splash__info-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1vw;
|
||||
font-weight: 900;
|
||||
margin-top: 8px;
|
||||
.icon {
|
||||
width: 2rem;
|
||||
margin-right: var(--spacing-xs);
|
||||
}
|
||||
@media (max-width: $breakpoint-medium) {
|
||||
font-size: 1.3vw;
|
||||
}
|
||||
@media (max-width: $breakpoint-small) {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
.membership-splash__info-button {
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
width: calc(100% - (var(--spacing-m) * 2));
|
||||
bottom: 0;
|
||||
margin-left: var(--spacing-m);
|
||||
margin-right: var(--spacing-m);
|
||||
display: inline-block;
|
||||
margin-bottom: var(--spacing-m);
|
||||
.button--primary {
|
||||
display: inline-block;
|
||||
border: 2px solid #debca0;
|
||||
border-radius: 20px;
|
||||
padding: 8px 20px 8px 20px;
|
||||
background-color: unset !important;
|
||||
text-align: center;
|
||||
.button__label {
|
||||
align-self: center;
|
||||
display: inline-block;
|
||||
color: #debca0;
|
||||
text-transform: uppercase;
|
||||
font-size: 17px;
|
||||
line-height: 2rem;
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
&:hover {
|
||||
background-color: unset;
|
||||
}
|
||||
@media (min-width: $breakpoint-xlarge) {
|
||||
padding: 18px 40px 18px 40px;
|
||||
height: 4.8rem;
|
||||
border-radius: 2.4rem;
|
||||
margin-bottom: 3%;
|
||||
.button__label {
|
||||
line-height: 4rem;
|
||||
}
|
||||
}
|
||||
@media (max-width: $breakpoint-small) {
|
||||
padding: 8px 10px 8px 10px;
|
||||
box-sizing: border-box;
|
||||
.button__label {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal {
|
||||
@media (min-width: $breakpoint-small) {
|
||||
.membership-splash {
|
||||
padding-left: unset;
|
||||
padding-right: unset;
|
||||
|
||||
.membership-splash__banner {
|
||||
.membership-splash__title {
|
||||
padding-left: var(--spacing-m);
|
||||
padding-right: var(--spacing-m);
|
||||
font-size: 1.6rem;
|
||||
line-height: 1.7rem;
|
||||
section:first-of-type {
|
||||
margin-bottom: var(--spacing-m);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.membership-splash__info-wrapper {
|
||||
.membership-splash__info {
|
||||
.membership-splash__info-header {
|
||||
.membership-splash__info-price {
|
||||
img {
|
||||
margin-left: var(--spacing-s);
|
||||
margin-right: var(--spacing-s);
|
||||
}
|
||||
section {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
.membership-splash__info-range {
|
||||
font-size: 0.8rem;
|
||||
margin-top: -10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.membership-splash__info-button {
|
||||
margin-left: var(--spacing-xs);
|
||||
.button {
|
||||
padding: 4px 6px 4px 6px;
|
||||
.button__content {
|
||||
.button__label {
|
||||
font-size: 0.74rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.membership-splash__info-content {
|
||||
padding-left: var(--spacing-xxs);
|
||||
font-size: 0.7rem;
|
||||
.icon {
|
||||
width: 1.7rem;
|
||||
height: 1.7rem;
|
||||
margin-right: var(--spacing-xxs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.membership-splash__info:nth-child(1) {
|
||||
padding: var(--spacing-m);
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (max-width: $breakpoint-small) {
|
||||
.membership-splash {
|
||||
.membership-splash__banner {
|
||||
.membership-splash__title {
|
||||
img {
|
||||
margin-top: var(--spacing-l);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stripe__confirm-remove-membership {
|
||||
.card__subtitle {
|
||||
line-height: 39px;
|
||||
margin-top: 5px;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,3 +43,8 @@
|
|||
color: var(--color-text) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-badge__tooltip {
|
||||
margin-top: 0px !important;
|
||||
right: 0.5rem !important;
|
||||
}
|
||||
|
|
|
@ -1177,6 +1177,22 @@ img {
|
|||
}
|
||||
}
|
||||
|
||||
.premium-wrapper {
|
||||
.membership_title {
|
||||
.comment__badge {
|
||||
.icon {
|
||||
margin-bottom: -6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.premium-option {
|
||||
background-color: rgba(var(--color-header-background-base), 0.4);
|
||||
border-radius: var(--border-radius);
|
||||
padding: var(--spacing-m);
|
||||
margin-bottom: var(--spacing-m);
|
||||
}
|
||||
}
|
||||
|
||||
// Temporary master classes
|
||||
.date_time {
|
||||
font-size: var(--font-xsmall);
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectTheme } from 'redux/selectors/settings';
|
||||
import { makeSelectClaimForUri, selectClaimIsNsfwForUri } from 'redux/selectors/claims';
|
||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
import { selectOdyseeMembershipIsPremiumPlus } from 'redux/selectors/user';
|
||||
import Ads from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
theme: selectTheme(state),
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
isMature: selectClaimIsNsfwForUri(state, props.uri),
|
||||
authenticated: selectUserVerifiedEmail(state),
|
||||
userHasPremiumPlus: selectOdyseeMembershipIsPremiumPlus(state),
|
||||
});
|
||||
|
||||
export default connect(select)(Ads);
|
||||
|
|
|
@ -40,7 +40,8 @@ type Props = {
|
|||
small: boolean,
|
||||
claim: Claim,
|
||||
isMature: boolean,
|
||||
authenticated: boolean,
|
||||
triggerBlacklist: boolean,
|
||||
userHasPremiumPlus: boolean,
|
||||
className?: string,
|
||||
};
|
||||
|
||||
|
@ -64,16 +65,9 @@ function clearAdElements() {
|
|||
}
|
||||
|
||||
function Ads(props: Props) {
|
||||
const {
|
||||
location: { pathname },
|
||||
type = 'video',
|
||||
tileLayout,
|
||||
small,
|
||||
authenticated,
|
||||
className,
|
||||
} = props;
|
||||
const { type = 'video', tileLayout, small, userHasPremiumPlus, className } = props;
|
||||
|
||||
const shouldShowAds = SHOW_ADS && !authenticated;
|
||||
const shouldShowAds = SHOW_ADS && !userHasPremiumPlus;
|
||||
const mobileAds = IS_ANDROID || IS_IOS;
|
||||
|
||||
// this is populated from app based on location
|
||||
|
@ -106,8 +100,8 @@ function Ads(props: Props) {
|
|||
log_in_to_domain: (
|
||||
<Button
|
||||
button="link"
|
||||
label={__('Log in to %domain%', { domain: DOMAIN })}
|
||||
navigate={`/$/${PAGES.AUTH}?redirect=${pathname}`}
|
||||
label={__('Get Odysee Premium+', { domain: DOMAIN })}
|
||||
navigate={`/$/${PAGES.ODYSEE_MEMBERSHIP}`}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue