parent
a34e07e970
commit
fb3a73d8a7
96 changed files with 3032 additions and 347 deletions
|
@ -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) => ({
|
||||
channels: selectMyChannelClaims(state),
|
||||
activeChannelClaim: selectActiveChannelClaim(state),
|
||||
incognito: selectIncognito(state),
|
||||
});
|
||||
const select = (state, props) => {
|
||||
const activeChannelClaim = selectActiveChannelClaim(state);
|
||||
|
||||
return {
|
||||
channels: selectMyChannelClaims(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 Width: | Height: | 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 Width: | Height: | 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 Width: | Height: | 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 Width: | Height: | 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,201 +192,231 @@ export default function LivestreamSetupPage(props: Props) {
|
|||
|
||||
return (
|
||||
<Page>
|
||||
{fetchingChannels && (
|
||||
<div className="main--empty">
|
||||
<Spinner delayed />
|
||||
{/* 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>
|
||||
)}
|
||||
|
||||
{!fetchingChannels && !hasChannels && (
|
||||
<Yrbl
|
||||
type="happy"
|
||||
title={__("You haven't created a channel yet, let's fix that!")}
|
||||
actions={
|
||||
<div className="section__actions">
|
||||
<Button button="primary" navigate={`/$/${PAGES.CHANNEL_NEW}`} label={__('Create A Channel')} />
|
||||
{/* show livestreaming frontend */}
|
||||
{livestreamEnabled && (
|
||||
<div className="card-stack">
|
||||
{/* getting channel data */}
|
||||
{fetchingChannels && (
|
||||
<div className="main--empty">
|
||||
<Spinner delayed />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{!fetchingChannels && (
|
||||
<>
|
||||
<div className="section__actions--between">
|
||||
<ChannelSelector hideAnon />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
)}
|
||||
|
||||
{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)} />
|
||||
{/* no channels yet */}
|
||||
{!fetchingChannels && !hasChannels && (
|
||||
<Yrbl
|
||||
type="happy"
|
||||
title={__("You haven't created a channel yet, let's fix that!")}
|
||||
actions={
|
||||
<div className="section__actions">
|
||||
<Button button="primary" navigate={`/$/${PAGES.CHANNEL_NEW}`} label={__('Create A Channel')} />
|
||||
</div>
|
||||
}
|
||||
title={__('Go Live on Odysee')}
|
||||
subtitle={<>{__(`You're invited to try out our new livestreaming service while in beta!`)} </>}
|
||||
actions={showHelp && helpText}
|
||||
/>
|
||||
{streamKey && totalLivestreamClaims.length > 0 && (
|
||||
<Card
|
||||
className="section"
|
||||
title={__('Your stream key')}
|
||||
actions={
|
||||
<>
|
||||
<CopyableText
|
||||
primaryButton
|
||||
name="stream-server"
|
||||
label={__('Stream server')}
|
||||
copyable={LIVESTREAM_RTMP_URL}
|
||||
snackMessage={__('Copied stream server URL.')}
|
||||
/>
|
||||
<CopyableText
|
||||
primaryButton
|
||||
enableInputMask
|
||||
name="livestream-key"
|
||||
label={__('Stream key (can be reused)')}
|
||||
copyable={streamKey}
|
||||
snackMessage={__('Copied stream key.')}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
)}
|
||||
|
||||
{totalLivestreamClaims.length > 0 ? (
|
||||
<>
|
||||
{Boolean(pendingClaims.length) && (
|
||||
<div className="section">
|
||||
<ClaimList
|
||||
header={__('Your pending livestreams uploads')}
|
||||
uris={pendingClaims.map((claim) => claim.permanent_url)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{Boolean(myLivestreamClaims.length) && (
|
||||
<>
|
||||
{Boolean(upcomingStreams.length) && (
|
||||
<div className="section">
|
||||
<ClaimList
|
||||
header={<ListHeader title={__('Your Scheduled Livestreams')} />}
|
||||
uris={upcomingStreams.map((claim) => claim.permanent_url)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* channel selector */}
|
||||
{!fetchingChannels && (
|
||||
<>
|
||||
<div className="section__actions--between">
|
||||
<ChannelSelector hideAnon />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* getting livestreams */}
|
||||
{fetchingLivestreams && !fetchingChannels && !hasLivestreamClaims && (
|
||||
<div className="main--empty">
|
||||
<Spinner delayed />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!fetchingChannels && channelId && (
|
||||
<>
|
||||
<Card
|
||||
titleActions={
|
||||
<Button
|
||||
button="close"
|
||||
icon={showHelp ? ICONS.UP : ICONS.DOWN}
|
||||
onClick={() => setShowHelp(!showHelp)}
|
||||
/>
|
||||
}
|
||||
title={__('Go Live on Odysee')}
|
||||
subtitle={
|
||||
<>{__(`Congratulations, you have access to livestreaming because ${reasonAllowedToStream}!`)} </>
|
||||
}
|
||||
actions={showHelp && helpText}
|
||||
/>
|
||||
{streamKey && totalLivestreamClaims.length > 0 && (
|
||||
<Card
|
||||
className="section"
|
||||
title={__('Your stream key')}
|
||||
actions={
|
||||
<>
|
||||
<CopyableText
|
||||
primaryButton
|
||||
name="stream-server"
|
||||
label={__('Stream server')}
|
||||
copyable={LIVESTREAM_RTMP_URL}
|
||||
snackMessage={__('Copied stream server URL.')}
|
||||
/>
|
||||
<CopyableText
|
||||
primaryButton
|
||||
enableInputMask
|
||||
name="livestream-key"
|
||||
label={__('Stream key (can be reused)')}
|
||||
copyable={streamKey}
|
||||
snackMessage={__('Copied stream key.')}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{totalLivestreamClaims.length > 0 ? (
|
||||
<>
|
||||
{Boolean(pendingClaims.length) && (
|
||||
<div className="section">
|
||||
<ClaimList
|
||||
header={
|
||||
<ListHeader title={__('Your Past Livestreams')} hideBtn={Boolean(upcomingStreams.length)} />
|
||||
}
|
||||
empty={
|
||||
<I18nMessage
|
||||
tokens={{
|
||||
check_again: (
|
||||
<Button
|
||||
button="link"
|
||||
onClick={() => fetchNoSourceClaims(channelId)}
|
||||
label={__('Check again')}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
>
|
||||
Nothing here yet. %check_again%
|
||||
</I18nMessage>
|
||||
}
|
||||
uris={pastStreams.map((claim) => claim.permanent_url)}
|
||||
header={__('Your pending livestreams uploads')}
|
||||
uris={pendingClaims.map((claim) => claim.permanent_url)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Yrbl
|
||||
className="livestream__publish-intro"
|
||||
title={__('No livestream publishes found')}
|
||||
subtitle={__(
|
||||
'You need to upload your livestream details before you can go live. Please note: Replays must be published manually after your stream via the Update button on the livestream.'
|
||||
)}
|
||||
actions={
|
||||
<div className="section__actions">
|
||||
<Button
|
||||
button="primary"
|
||||
onClick={() =>
|
||||
doNewLivestream(`/$/${PAGES.UPLOAD}?type=${PUBLISH_MODES.LIVESTREAM.toLowerCase()}`)
|
||||
}
|
||||
label={__('Create A Livestream')}
|
||||
/>
|
||||
<Button
|
||||
button="alt"
|
||||
onClick={() => {
|
||||
fetchNoSourceClaims(channelId);
|
||||
}}
|
||||
label={__('Check again...')}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Debug Stuff */}
|
||||
{streamKey && false && activeChannelClaim && (
|
||||
<div style={{ marginTop: 'var(--spacing-l)' }}>
|
||||
<h3>Debug Info</h3>
|
||||
|
||||
{/* Channel ID */}
|
||||
<FormField
|
||||
name={'channelId'}
|
||||
label={'Channel ID'}
|
||||
type={'text'}
|
||||
defaultValue={activeChannelClaim.claim_id}
|
||||
readOnly
|
||||
)}
|
||||
{Boolean(myLivestreamClaims.length) && (
|
||||
<>
|
||||
{Boolean(upcomingStreams.length) && (
|
||||
<div className="section">
|
||||
<ClaimList
|
||||
header={<ListHeader title={__('Your Scheduled Livestreams')} />}
|
||||
uris={upcomingStreams.map((claim) => claim.permanent_url)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="section">
|
||||
<ClaimList
|
||||
header={
|
||||
<ListHeader title={__('Your Past Livestreams')} hideBtn={Boolean(upcomingStreams.length)} />
|
||||
}
|
||||
empty={
|
||||
<I18nMessage
|
||||
tokens={{
|
||||
check_again: (
|
||||
<Button
|
||||
button="link"
|
||||
onClick={() => fetchNoSourceClaims(channelId)}
|
||||
label={__('Check again')}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
>
|
||||
Nothing here yet. %check_again%
|
||||
</I18nMessage>
|
||||
}
|
||||
uris={pastStreams.map((claim) => claim.permanent_url)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Yrbl
|
||||
className="livestream__publish-intro"
|
||||
title={__('No livestream publishes found')}
|
||||
subtitle={__(
|
||||
'You need to upload your livestream details before you can go live. Please note: Replays must be published manually after your stream via the Update button on the livestream.'
|
||||
)}
|
||||
actions={
|
||||
<div className="section__actions">
|
||||
<Button
|
||||
button="primary"
|
||||
onClick={() =>
|
||||
doNewLivestream(`/$/${PAGES.UPLOAD}?type=${PUBLISH_MODES.LIVESTREAM.toLowerCase()}`)
|
||||
}
|
||||
label={__('Create A Livestream')}
|
||||
/>
|
||||
<Button
|
||||
button="alt"
|
||||
onClick={() => {
|
||||
fetchNoSourceClaims(channelId);
|
||||
}}
|
||||
label={__('Check again...')}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Signature */}
|
||||
<FormField
|
||||
name={'signature'}
|
||||
label={'Signature'}
|
||||
type={'text'}
|
||||
defaultValue={sigData.signature}
|
||||
readOnly
|
||||
/>
|
||||
{/* Debug Stuff */}
|
||||
{streamKey && false && activeChannelClaim && (
|
||||
<div style={{ marginTop: 'var(--spacing-l)' }}>
|
||||
<h3>Debug Info</h3>
|
||||
|
||||
{/* Signature TS */}
|
||||
<FormField
|
||||
name={'signaturets'}
|
||||
label={'Signature Timestamp'}
|
||||
type={'text'}
|
||||
defaultValue={sigData.signing_ts}
|
||||
readOnly
|
||||
/>
|
||||
{/* Channel ID */}
|
||||
<FormField
|
||||
name={'channelId'}
|
||||
label={'Channel ID'}
|
||||
type={'text'}
|
||||
defaultValue={activeChannelClaim.claim_id}
|
||||
readOnly
|
||||
/>
|
||||
|
||||
{/* Hex Data */}
|
||||
<FormField
|
||||
name={'datahex'}
|
||||
label={'Hex Data'}
|
||||
type={'text'}
|
||||
defaultValue={toHex(activeChannelClaim.name)}
|
||||
readOnly
|
||||
/>
|
||||
{/* Signature */}
|
||||
<FormField
|
||||
name={'signature'}
|
||||
label={'Signature'}
|
||||
type={'text'}
|
||||
defaultValue={sigData.signature}
|
||||
readOnly
|
||||
/>
|
||||
|
||||
{/* Channel Public Key */}
|
||||
<FormField
|
||||
name={'channelpublickey'}
|
||||
label={'Public Key'}
|
||||
type={'text'}
|
||||
defaultValue={activeChannelClaim.value.public_key}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{/* Signature TS */}
|
||||
<FormField
|
||||
name={'signaturets'}
|
||||
label={'Signature Timestamp'}
|
||||
type={'text'}
|
||||
defaultValue={sigData.signing_ts}
|
||||
readOnly
|
||||
/>
|
||||
|
||||
{/* Hex Data */}
|
||||
<FormField
|
||||
name={'datahex'}
|
||||
label={'Hex Data'}
|
||||
type={'text'}
|
||||
defaultValue={toHex(activeChannelClaim.name)}
|
||||
readOnly
|
||||
/>
|
||||
|
||||
{/* Channel Public Key */}
|
||||
<FormField
|
||||
name={'channelpublickey'}
|
||||
label={'Public Key'}
|
||||
type={'text'}
|
||||
defaultValue={activeChannelClaim.value.public_key}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</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…
Reference in a new issue