parent
a34e07e970
commit
fb3a73d8a7
96 changed files with 3032 additions and 347 deletions
|
@ -1,7 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { selectClaimIdForUri } from 'redux/selectors/claims';
|
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 || {};
|
const selectState = (state: State) => state.stats || {};
|
||||||
export const selectViewCount = (state: State) => selectState(state).viewCountById;
|
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_enabled: boolean,
|
||||||
odysee_live_disabled: boolean,
|
odysee_live_disabled: boolean,
|
||||||
global_mod: boolean,
|
global_mod: boolean,
|
||||||
|
odyseeMembershipsPerClaimIds: ?{},
|
||||||
};
|
};
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { doResolveUris } from 'redux/actions/claims';
|
||||||
import * as SETTINGS from 'constants/settings';
|
import * as SETTINGS from 'constants/settings';
|
||||||
import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
|
import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
import { selectOdyseeMembershipIsPremiumPlus } from 'redux/selectors/user';
|
||||||
import { selectClientSetting, selectShowMatureContent } from 'redux/selectors/settings';
|
import { selectClientSetting, selectShowMatureContent } from 'redux/selectors/settings';
|
||||||
import { doFetchChannelLiveStatus } from 'redux/actions/livestream';
|
import { doFetchChannelLiveStatus } from 'redux/actions/livestream';
|
||||||
import { selectActiveLivestreamForChannel, selectActiveLivestreamInitialized } from 'redux/selectors/livestream';
|
import { selectActiveLivestreamForChannel, selectActiveLivestreamInitialized } from 'redux/selectors/livestream';
|
||||||
|
@ -32,11 +32,11 @@ const select = (state, props) => {
|
||||||
channelIsMine: selectClaimIsMine(state, claim),
|
channelIsMine: selectClaimIsMine(state, claim),
|
||||||
channelIsBlocked: makeSelectChannelIsMuted(props.uri)(state),
|
channelIsBlocked: makeSelectChannelIsMuted(props.uri)(state),
|
||||||
claim,
|
claim,
|
||||||
isAuthenticated: selectUserVerifiedEmail(state),
|
|
||||||
showMature: selectShowMatureContent(state),
|
showMature: selectShowMatureContent(state),
|
||||||
tileLayout: selectClientSetting(state, SETTINGS.TILE_LAYOUT),
|
tileLayout: selectClientSetting(state, SETTINGS.TILE_LAYOUT),
|
||||||
activeLivestreamForChannel: selectActiveLivestreamForChannel(state, channelClaimId),
|
activeLivestreamForChannel: selectActiveLivestreamForChannel(state, channelClaimId),
|
||||||
activeLivestreamInitialized: selectActiveLivestreamInitialized(state),
|
activeLivestreamInitialized: selectActiveLivestreamInitialized(state),
|
||||||
|
userHasPremiumPlus: selectOdyseeMembershipIsPremiumPlus(state),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -39,6 +39,7 @@ type Props = {
|
||||||
doFetchChannelLiveStatus: (string) => void,
|
doFetchChannelLiveStatus: (string) => void,
|
||||||
activeLivestreamForChannel: any,
|
activeLivestreamForChannel: any,
|
||||||
activeLivestreamInitialized: boolean,
|
activeLivestreamInitialized: boolean,
|
||||||
|
userHasPremiumPlus: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
function ChannelContent(props: Props) {
|
function ChannelContent(props: Props) {
|
||||||
|
@ -49,7 +50,6 @@ function ChannelContent(props: Props) {
|
||||||
channelIsBlocked,
|
channelIsBlocked,
|
||||||
channelIsBlackListed,
|
channelIsBlackListed,
|
||||||
claim,
|
claim,
|
||||||
isAuthenticated,
|
|
||||||
defaultPageSize = CS.PAGE_SIZE,
|
defaultPageSize = CS.PAGE_SIZE,
|
||||||
defaultInfiniteScroll = true,
|
defaultInfiniteScroll = true,
|
||||||
showMature,
|
showMature,
|
||||||
|
@ -61,8 +61,10 @@ function ChannelContent(props: Props) {
|
||||||
doFetchChannelLiveStatus,
|
doFetchChannelLiveStatus,
|
||||||
activeLivestreamForChannel,
|
activeLivestreamForChannel,
|
||||||
activeLivestreamInitialized,
|
activeLivestreamInitialized,
|
||||||
|
userHasPremiumPlus,
|
||||||
} = props;
|
} = props;
|
||||||
// const claimsInChannel = (claim && claim.meta.claims_in_channel) || 0;
|
// const claimsInChannel = (claim && claim.meta.claims_in_channel) || 0;
|
||||||
|
|
||||||
const claimsInChannel = 9999;
|
const claimsInChannel = 9999;
|
||||||
const [searchQuery, setSearchQuery] = React.useState('');
|
const [searchQuery, setSearchQuery] = React.useState('');
|
||||||
const [isSearching, setIsSearching] = React.useState(false);
|
const [isSearching, setIsSearching] = React.useState(false);
|
||||||
|
@ -160,7 +162,7 @@ function ChannelContent(props: Props) {
|
||||||
defaultOrderBy={CS.ORDER_BY_NEW}
|
defaultOrderBy={CS.ORDER_BY_NEW}
|
||||||
pageSize={defaultPageSize}
|
pageSize={defaultPageSize}
|
||||||
infiniteScroll={defaultInfiniteScroll}
|
infiniteScroll={defaultInfiniteScroll}
|
||||||
injectedItem={SHOW_ADS && !isAuthenticated && { node: <Ads type="video" tileLayout={tileLayout} small /> }}
|
injectedItem={SHOW_ADS && !userHasPremiumPlus && { node: <Ads type="video" tileLayout={tileLayout} small /> }}
|
||||||
meta={
|
meta={
|
||||||
showFilters && (
|
showFilters && (
|
||||||
<Form onSubmit={() => {}} className="wunderbar--inline">
|
<Form onSubmit={() => {}} className="wunderbar--inline">
|
||||||
|
|
|
@ -1,16 +1,24 @@
|
||||||
import { connect } from 'react-redux';
|
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 { selectActiveChannelClaim, selectIncognito } from 'redux/selectors/app';
|
||||||
import { doSetActiveChannel, doSetIncognito } from 'redux/actions/app';
|
import { doSetActiveChannel, doSetIncognito } from 'redux/actions/app';
|
||||||
|
import { doFetchUserMemberships } from 'redux/actions/user';
|
||||||
import ChannelSelector from './view';
|
import ChannelSelector from './view';
|
||||||
|
|
||||||
const select = (state) => ({
|
const select = (state, props) => {
|
||||||
channels: selectMyChannelClaims(state),
|
const activeChannelClaim = selectActiveChannelClaim(state);
|
||||||
activeChannelClaim: selectActiveChannelClaim(state),
|
|
||||||
incognito: selectIncognito(state),
|
return {
|
||||||
});
|
channels: selectMyChannelClaims(state),
|
||||||
|
activeChannelClaim,
|
||||||
|
incognito: selectIncognito(state),
|
||||||
|
odyseeMembershipByUri: (uri) => selectOdyseeMembershipForUri(state, uri),
|
||||||
|
claimsByUri: selectClaimsByUri(state),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default connect(select, {
|
export default connect(select, {
|
||||||
doSetActiveChannel,
|
doSetActiveChannel,
|
||||||
doSetIncognito,
|
doSetIncognito,
|
||||||
|
doFetchUserMemberships,
|
||||||
})(ChannelSelector);
|
})(ChannelSelector);
|
||||||
|
|
|
@ -8,6 +8,8 @@ import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button';
|
||||||
import ChannelTitle from 'component/channelTitle';
|
import ChannelTitle from 'component/channelTitle';
|
||||||
import Icon from 'component/common/icon';
|
import Icon from 'component/common/icon';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
|
import useGetUserMemberships from 'effects/use-get-user-memberships';
|
||||||
|
import PremiumBadge from 'component/common/premium-badge';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
selectedChannelUrl: string, // currently selected channel
|
selectedChannelUrl: string, // currently selected channel
|
||||||
|
@ -18,20 +20,32 @@ type Props = {
|
||||||
doSetActiveChannel: (string) => void,
|
doSetActiveChannel: (string) => void,
|
||||||
incognito: boolean,
|
incognito: boolean,
|
||||||
doSetIncognito: (boolean) => void,
|
doSetIncognito: (boolean) => void,
|
||||||
|
claimsByUri: { [string]: any },
|
||||||
|
doFetchUserMemberships: (claimIdCsv: string) => void,
|
||||||
|
odyseeMembershipByUri: (uri: string) => string,
|
||||||
};
|
};
|
||||||
|
|
||||||
type ListItemProps = {
|
type ListItemProps = {
|
||||||
uri: string,
|
uri: string,
|
||||||
isSelected?: boolean,
|
isSelected?: boolean,
|
||||||
|
claimsByUri: { [string]: any },
|
||||||
|
doFetchUserMemberships: (claimIdCsv: string) => void,
|
||||||
|
odyseeMembershipByUri: (uri: string) => string,
|
||||||
};
|
};
|
||||||
|
|
||||||
function ChannelListItem(props: ListItemProps) {
|
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 (
|
return (
|
||||||
<div className={classnames('channel__list-item', { 'channel__list-item--selected': isSelected })}>
|
<div className={classnames('channel__list-item', { 'channel__list-item--selected': isSelected })}>
|
||||||
<ChannelThumbnail uri={uri} hideStakedIndicator xsmall noLazyLoad />
|
<ChannelThumbnail uri={uri} hideStakedIndicator xsmall noLazyLoad />
|
||||||
<ChannelTitle uri={uri} />
|
<ChannelTitle uri={uri} />
|
||||||
|
<PremiumBadge membership={membership} />
|
||||||
{isSelected && <Icon icon={ICONS.DOWN} />}
|
{isSelected && <Icon icon={ICONS.DOWN} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -52,11 +66,23 @@ function IncognitoSelector(props: IncognitoSelectorProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChannelSelector(props: Props) {
|
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 {
|
const {
|
||||||
push,
|
push,
|
||||||
location: { pathname },
|
location: { pathname },
|
||||||
} = useHistory();
|
} = useHistory();
|
||||||
|
|
||||||
const activeChannelUrl = activeChannelClaim && activeChannelClaim.permanent_url;
|
const activeChannelUrl = activeChannelClaim && activeChannelClaim.permanent_url;
|
||||||
|
|
||||||
function handleChannelSelect(channelClaim) {
|
function handleChannelSelect(channelClaim) {
|
||||||
|
@ -71,14 +97,26 @@ function ChannelSelector(props: Props) {
|
||||||
{(incognito && !hideAnon) || !activeChannelUrl ? (
|
{(incognito && !hideAnon) || !activeChannelUrl ? (
|
||||||
<IncognitoSelector isSelected />
|
<IncognitoSelector isSelected />
|
||||||
) : (
|
) : (
|
||||||
<ChannelListItem uri={activeChannelUrl} isSelected />
|
<ChannelListItem
|
||||||
|
odyseeMembershipByUri={odyseeMembershipByUri}
|
||||||
|
uri={activeChannelUrl}
|
||||||
|
isSelected
|
||||||
|
claimsByUri={claimsByUri}
|
||||||
|
doFetchUserMemberships={doFetchUserMemberships}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
|
|
||||||
<MenuList className="menu__list channel__list">
|
<MenuList className="menu__list channel__list">
|
||||||
{channels &&
|
{channels &&
|
||||||
channels.map((channel) => (
|
channels.map((channel) => (
|
||||||
<MenuItem key={channel.permanent_url} onSelect={() => handleChannelSelect(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>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
{!hideAnon && (
|
{!hideAnon && (
|
||||||
|
|
|
@ -1,14 +1,24 @@
|
||||||
import { connect } from 'react-redux';
|
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 { doResolveUri } from 'redux/actions/claims';
|
||||||
|
import { doFetchUserMemberships } from 'redux/actions/user';
|
||||||
import ChannelThumbnail from './view';
|
import ChannelThumbnail from './view';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => ({
|
||||||
thumbnail: selectThumbnailForUri(state, props.uri),
|
thumbnail: selectThumbnailForUri(state, props.uri),
|
||||||
claim: selectClaimForUri(state, props.uri),
|
claim: selectClaimForUri(state, props.uri),
|
||||||
isResolving: selectIsUriResolving(state, props.uri),
|
isResolving: selectIsUriResolving(state, props.uri),
|
||||||
|
odyseeMembership: selectOdyseeMembershipForUri(state, props.uri),
|
||||||
|
claimsByUri: selectClaimsByUri(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, {
|
export default connect(select, {
|
||||||
doResolveUri,
|
doResolveUri,
|
||||||
|
doFetchUserMemberships,
|
||||||
})(ChannelThumbnail);
|
})(ChannelThumbnail);
|
||||||
|
|
|
@ -4,9 +4,10 @@ import { parseURI } from 'util/lbryURI';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import Gerbil from './gerbil.png';
|
import Gerbil from './gerbil.png';
|
||||||
import FreezeframeWrapper from 'component/fileThumbnail/FreezeframeWrapper';
|
import FreezeframeWrapper from 'component/fileThumbnail/FreezeframeWrapper';
|
||||||
import ChannelStakedIndicator from 'component/channelStakedIndicator';
|
|
||||||
import OptimizedImage from 'component/optimizedImage';
|
import OptimizedImage from 'component/optimizedImage';
|
||||||
import { AVATAR_DEFAULT } from 'config';
|
import { AVATAR_DEFAULT } from 'config';
|
||||||
|
import useGetUserMemberships from 'effects/use-get-user-memberships';
|
||||||
|
import PremiumBadge from 'component/common/premium-badge';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
thumbnail: ?string,
|
thumbnail: ?string,
|
||||||
|
@ -26,6 +27,12 @@ type Props = {
|
||||||
noOptimization?: boolean,
|
noOptimization?: boolean,
|
||||||
setThumbUploadError: (boolean) => void,
|
setThumbUploadError: (boolean) => void,
|
||||||
ThumbUploadError: boolean,
|
ThumbUploadError: boolean,
|
||||||
|
claimsByUri: { [string]: any },
|
||||||
|
odyseeMembership: string,
|
||||||
|
doFetchUserMemberships: (claimIdCsv: string) => void,
|
||||||
|
showMemberBadge?: boolean,
|
||||||
|
isChannel?: boolean,
|
||||||
|
checkMembership: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
function ChannelThumbnail(props: Props) {
|
function ChannelThumbnail(props: Props) {
|
||||||
|
@ -42,10 +49,15 @@ function ChannelThumbnail(props: Props) {
|
||||||
doResolveUri,
|
doResolveUri,
|
||||||
isResolving,
|
isResolving,
|
||||||
noLazyLoad,
|
noLazyLoad,
|
||||||
hideStakedIndicator = false,
|
|
||||||
hideTooltip,
|
hideTooltip,
|
||||||
setThumbUploadError,
|
setThumbUploadError,
|
||||||
ThumbUploadError,
|
ThumbUploadError,
|
||||||
|
claimsByUri,
|
||||||
|
odyseeMembership,
|
||||||
|
doFetchUserMemberships,
|
||||||
|
showMemberBadge,
|
||||||
|
isChannel,
|
||||||
|
checkMembership = true,
|
||||||
} = props;
|
} = props;
|
||||||
const [thumbLoadError, setThumbLoadError] = React.useState(ThumbUploadError);
|
const [thumbLoadError, setThumbLoadError] = React.useState(ThumbUploadError);
|
||||||
const shouldResolve = !isResolving && claim === undefined;
|
const shouldResolve = !isResolving && claim === undefined;
|
||||||
|
@ -56,6 +68,16 @@ function ChannelThumbnail(props: Props) {
|
||||||
const isGif = channelThumbnail && channelThumbnail.endsWith('gif');
|
const isGif = channelThumbnail && channelThumbnail.endsWith('gif');
|
||||||
const showThumb = (!obscure && !!thumbnail) || thumbnailPreview;
|
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
|
// Generate a random color class based on the first letter of the channel name
|
||||||
const { channelName } = parseURI(uri);
|
const { channelName } = parseURI(uri);
|
||||||
let initializer;
|
let initializer;
|
||||||
|
@ -76,7 +98,7 @@ function ChannelThumbnail(props: Props) {
|
||||||
if (isGif && !allowGifs) {
|
if (isGif && !allowGifs) {
|
||||||
return (
|
return (
|
||||||
<FreezeframeWrapper src={channelThumbnail} className={classnames('channel-thumbnail', className)}>
|
<FreezeframeWrapper src={channelThumbnail} className={classnames('channel-thumbnail', className)}>
|
||||||
{!hideStakedIndicator && <ChannelStakedIndicator uri={uri} claim={claim} hideTooltip={hideTooltip} />}
|
{showMemberBadge && <PremiumBadge {...badgeProps} />}
|
||||||
</FreezeframeWrapper>
|
</FreezeframeWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -103,7 +125,7 @@ function ChannelThumbnail(props: Props) {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{!hideStakedIndicator && <ChannelStakedIndicator uri={uri} claim={claim} hideTooltip={hideTooltip} />}
|
{showMemberBadge && <PremiumBadge {...badgeProps} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,6 +58,7 @@ type Props = {
|
||||||
showEdit?: boolean,
|
showEdit?: boolean,
|
||||||
droppableProvided?: any,
|
droppableProvided?: any,
|
||||||
unavailableUris?: Array<string>,
|
unavailableUris?: Array<string>,
|
||||||
|
showMemberBadge?: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ClaimList(props: Props) {
|
export default function ClaimList(props: Props) {
|
||||||
|
@ -96,6 +97,7 @@ export default function ClaimList(props: Props) {
|
||||||
showEdit,
|
showEdit,
|
||||||
droppableProvided,
|
droppableProvided,
|
||||||
unavailableUris,
|
unavailableUris,
|
||||||
|
showMemberBadge,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [currentSort, setCurrentSort] = usePersistedState(persistedStorageKey, SORT_NEW);
|
const [currentSort, setCurrentSort] = usePersistedState(persistedStorageKey, SORT_NEW);
|
||||||
|
@ -196,6 +198,7 @@ export default function ClaimList(props: Props) {
|
||||||
showEdit={showEdit}
|
showEdit={showEdit}
|
||||||
dragHandleProps={draggableProvided && draggableProvided.dragHandleProps}
|
dragHandleProps={draggableProvided && draggableProvided.dragHandleProps}
|
||||||
unavailableUris={unavailableUris}
|
unavailableUris={unavailableUris}
|
||||||
|
showMemberBadge={showMemberBadge}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { doClaimSearch } from 'redux/actions/claims';
|
||||||
import * as SETTINGS from 'constants/settings';
|
import * as SETTINGS from 'constants/settings';
|
||||||
import { selectFollowedTags } from 'redux/selectors/tags';
|
import { selectFollowedTags } from 'redux/selectors/tags';
|
||||||
import { selectMutedChannels } from 'redux/selectors/blocked';
|
import { selectMutedChannels } from 'redux/selectors/blocked';
|
||||||
|
import { doFetchUserMemberships } from 'redux/actions/user';
|
||||||
import { selectClientSetting, selectShowMatureContent, selectLanguage } from 'redux/selectors/settings';
|
import { selectClientSetting, selectShowMatureContent, selectLanguage } from 'redux/selectors/settings';
|
||||||
import { selectModerationBlockList } from 'redux/selectors/comments';
|
import { selectModerationBlockList } from 'redux/selectors/comments';
|
||||||
import ClaimListDiscover from './view';
|
import ClaimListDiscover from './view';
|
||||||
|
@ -31,6 +32,7 @@ const select = (state, props) => ({
|
||||||
const perform = {
|
const perform = {
|
||||||
doClaimSearch,
|
doClaimSearch,
|
||||||
doFetchViewCount,
|
doFetchViewCount,
|
||||||
|
doFetchUserMemberships,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(select, perform)(ClaimListDiscover);
|
export default connect(select, perform)(ClaimListDiscover);
|
||||||
|
|
|
@ -19,6 +19,7 @@ import LangFilterIndicator from 'component/langFilterIndicator';
|
||||||
import ClaimListHeader from 'component/claimListHeader';
|
import ClaimListHeader from 'component/claimListHeader';
|
||||||
import useFetchViewCount from 'effects/use-fetch-view-count';
|
import useFetchViewCount from 'effects/use-fetch-view-count';
|
||||||
import { useIsLargeScreen } from 'effects/use-screensize';
|
import { useIsLargeScreen } from 'effects/use-screensize';
|
||||||
|
import useGetUserMemberships from 'effects/use-get-user-memberships';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uris: Array<string>,
|
uris: Array<string>,
|
||||||
|
@ -98,6 +99,7 @@ type Props = {
|
||||||
// --- perform ---
|
// --- perform ---
|
||||||
doClaimSearch: ({}) => void,
|
doClaimSearch: ({}) => void,
|
||||||
doFetchViewCount: (claimIdCsv: string) => void,
|
doFetchViewCount: (claimIdCsv: string) => void,
|
||||||
|
doFetchUserMemberships: (claimIdCsv: string) => void,
|
||||||
|
|
||||||
hideLayoutButton?: boolean,
|
hideLayoutButton?: boolean,
|
||||||
loadedCallback?: (number) => void,
|
loadedCallback?: (number) => void,
|
||||||
|
@ -177,6 +179,7 @@ function ClaimListDiscover(props: Props) {
|
||||||
maxClaimRender,
|
maxClaimRender,
|
||||||
useSkeletonScreen = true,
|
useSkeletonScreen = true,
|
||||||
excludeUris = [],
|
excludeUris = [],
|
||||||
|
doFetchUserMemberships,
|
||||||
swipeLayout = false,
|
swipeLayout = false,
|
||||||
} = props;
|
} = props;
|
||||||
const didNavigateForward = history.action === 'PUSH';
|
const didNavigateForward = history.action === 'PUSH';
|
||||||
|
@ -608,9 +611,14 @@ function ClaimListDiscover(props: Props) {
|
||||||
|
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
useFetchViewCount(fetchViewCount, renderUris, claimsByUri, doFetchViewCount);
|
useFetchViewCount(fetchViewCount, renderUris, claimsByUri, doFetchViewCount);
|
||||||
|
|
||||||
|
const shouldFetchUserMemberships = true;
|
||||||
|
const arrayOfContentUris = renderUris;
|
||||||
|
const convertClaimUrlsToIds = claimsByUri;
|
||||||
|
|
||||||
|
useGetUserMemberships(shouldFetchUserMemberships, arrayOfContentUris, convertClaimUrlsToIds, doFetchUserMemberships);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (shouldPerformSearch) {
|
if (shouldPerformSearch) {
|
||||||
const searchOptions = JSON.parse(optionsStringForEffect);
|
const searchOptions = JSON.parse(optionsStringForEffect);
|
||||||
|
|
|
@ -90,6 +90,7 @@ type Props = {
|
||||||
showEdit?: boolean,
|
showEdit?: boolean,
|
||||||
dragHandleProps?: any,
|
dragHandleProps?: any,
|
||||||
unavailableUris?: Array<string>,
|
unavailableUris?: Array<string>,
|
||||||
|
showMemberBadge?: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
|
@ -152,6 +153,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
showEdit,
|
showEdit,
|
||||||
dragHandleProps,
|
dragHandleProps,
|
||||||
unavailableUris,
|
unavailableUris,
|
||||||
|
showMemberBadge,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
@ -366,7 +368,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
|
|
||||||
{isChannelUri && claim ? (
|
{isChannelUri && claim ? (
|
||||||
<UriIndicator focusable={false} uri={uri} link>
|
<UriIndicator focusable={false} uri={uri} link>
|
||||||
<ChannelThumbnail uri={uri} small={type === 'inline'} />
|
<ChannelThumbnail uri={uri} small={type === 'inline'} showMemberBadge={showMemberBadge} checkMembership={false} />
|
||||||
</UriIndicator>
|
</UriIndicator>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
@ -411,11 +413,16 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
{!isChannelUri && signingChannel && (
|
{!isChannelUri && signingChannel && (
|
||||||
<div className="claim-preview__channel-staked">
|
<div className="claim-preview__channel-staked">
|
||||||
<UriIndicator focusable={false} uri={uri} link hideAnonymous>
|
<UriIndicator focusable={false} uri={uri} link hideAnonymous>
|
||||||
<ChannelThumbnail uri={signingChannel.permanent_url} xsmall />
|
<ChannelThumbnail uri={signingChannel.permanent_url} xsmall showMemberBadge={showMemberBadge} checkMembership={false} />
|
||||||
</UriIndicator>
|
</UriIndicator>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<ClaimPreviewSubtitle uri={uri} type={type} showAtSign={isChannelUri} />
|
<ClaimPreviewSubtitle
|
||||||
|
uri={uri}
|
||||||
|
type={type}
|
||||||
|
showAtSign={isChannelUri}
|
||||||
|
showMemberBadge={!showMemberBadge}
|
||||||
|
/>
|
||||||
{(pending || !!reflectingProgress) && <PublishPending uri={uri} />}
|
{(pending || !!reflectingProgress) && <PublishPending uri={uri} />}
|
||||||
{channelSubscribers}
|
{channelSubscribers}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -21,11 +21,24 @@ type Props = {
|
||||||
lang: string,
|
lang: string,
|
||||||
fetchSubCount: (string) => void,
|
fetchSubCount: (string) => void,
|
||||||
subCount: number,
|
subCount: number,
|
||||||
|
showMemberBadge?: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
// previews used in channel overview and homepage (and other places?)
|
// previews used in channel overview and homepage (and other places?)
|
||||||
function ClaimPreviewSubtitle(props: Props) {
|
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 isChannel = claim && claim.value_type === 'channel';
|
||||||
const claimsInChannel = (claim && claim.meta.claims_in_channel) || 0;
|
const claimsInChannel = (claim && claim.meta.claims_in_channel) || 0;
|
||||||
|
|
||||||
|
@ -47,7 +60,7 @@ function ClaimPreviewSubtitle(props: Props) {
|
||||||
<div className="media__subtitle">
|
<div className="media__subtitle">
|
||||||
{claim ? (
|
{claim ? (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<UriIndicator uri={uri} showAtSign={showAtSign} link />{' '}
|
<UriIndicator uri={uri} showAtSign={showAtSign} showMemberBadge={showMemberBadge} link />{' '}
|
||||||
{!pending && claim && (
|
{!pending && claim && (
|
||||||
<>
|
<>
|
||||||
{isChannel && type !== 'inline' && (
|
{isChannel && type !== 'inline' && (
|
||||||
|
|
|
@ -243,7 +243,7 @@ function ClaimPreviewTile(props: Props) {
|
||||||
) : (
|
) : (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<UriIndicator focusable={false} uri={uri} link hideAnonymous>
|
<UriIndicator focusable={false} uri={uri} link hideAnonymous>
|
||||||
<ChannelThumbnail uri={channelUri} xsmall />
|
<ChannelThumbnail uri={channelUri} xsmall checkMembership={false} />
|
||||||
</UriIndicator>
|
</UriIndicator>
|
||||||
|
|
||||||
<div className="claim-tile__about">
|
<div className="claim-tile__about">
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { connect } from 'react-redux';
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
import { selectClaimSearchByQuery, selectFetchingClaimSearchByQuery, selectClaimsByUri } from 'redux/selectors/claims';
|
import { selectClaimSearchByQuery, selectFetchingClaimSearchByQuery, selectClaimsByUri } from 'redux/selectors/claims';
|
||||||
import { doClaimSearch } from 'redux/actions/claims';
|
import { doClaimSearch } from 'redux/actions/claims';
|
||||||
|
import { doFetchUserMemberships } from 'redux/actions/user';
|
||||||
import * as SETTINGS from 'constants/settings';
|
import * as SETTINGS from 'constants/settings';
|
||||||
import { MATURE_TAGS } from 'constants/tags';
|
import { MATURE_TAGS } from 'constants/tags';
|
||||||
import { doFetchViewCount } from 'lbryinc';
|
import { doFetchViewCount } from 'lbryinc';
|
||||||
|
@ -37,6 +38,7 @@ const select = (state, props) => {
|
||||||
const perform = {
|
const perform = {
|
||||||
doClaimSearch,
|
doClaimSearch,
|
||||||
doFetchViewCount,
|
doFetchViewCount,
|
||||||
|
doFetchUserMemberships,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withRouter(connect(select, perform)(ClaimListDiscover));
|
export default withRouter(connect(select, perform)(ClaimListDiscover));
|
||||||
|
|
|
@ -4,6 +4,7 @@ import React from 'react';
|
||||||
import ClaimPreviewTile from 'component/claimPreviewTile';
|
import ClaimPreviewTile from 'component/claimPreviewTile';
|
||||||
import useFetchViewCount from 'effects/use-fetch-view-count';
|
import useFetchViewCount from 'effects/use-fetch-view-count';
|
||||||
import useLastVisibleItem from 'effects/use-last-visible-item';
|
import useLastVisibleItem from 'effects/use-last-visible-item';
|
||||||
|
import useGetUserMemberships from 'effects/use-get-user-memberships';
|
||||||
|
|
||||||
function urisEqual(prev: ?Array<string>, next: ?Array<string>) {
|
function urisEqual(prev: ?Array<string>, next: ?Array<string>) {
|
||||||
if (!prev || !next) {
|
if (!prev || !next) {
|
||||||
|
@ -56,6 +57,7 @@ type Props = {
|
||||||
// --- perform ---
|
// --- perform ---
|
||||||
doClaimSearch: ({}) => void,
|
doClaimSearch: ({}) => void,
|
||||||
doFetchViewCount: (claimIdCsv: string) => void,
|
doFetchViewCount: (claimIdCsv: string) => void,
|
||||||
|
doFetchUserMemberships: (claimIdCsv: string) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
function ClaimTilesDiscover(props: Props) {
|
function ClaimTilesDiscover(props: Props) {
|
||||||
|
@ -74,6 +76,7 @@ function ClaimTilesDiscover(props: Props) {
|
||||||
doFetchViewCount,
|
doFetchViewCount,
|
||||||
pageSize = 8,
|
pageSize = 8,
|
||||||
optionsStringified,
|
optionsStringified,
|
||||||
|
doFetchUserMemberships,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
// reference to the claim-grid
|
// reference to the claim-grid
|
||||||
|
@ -117,6 +120,10 @@ function ClaimTilesDiscover(props: Props) {
|
||||||
// populate the view counts for the current claim uris
|
// populate the view counts for the current claim uris
|
||||||
useFetchViewCount(fetchViewCount, uris, claimsByUri, doFetchViewCount);
|
useFetchViewCount(fetchViewCount, uris, claimsByUri, doFetchViewCount);
|
||||||
|
|
||||||
|
const shouldFetchUserMemberships = true;
|
||||||
|
|
||||||
|
useGetUserMemberships(shouldFetchUserMemberships, uris, claimsByUri, doFetchUserMemberships);
|
||||||
|
|
||||||
// Run `doClaimSearch`
|
// Run `doClaimSearch`
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (shouldPerformSearch) {
|
if (shouldPerformSearch) {
|
||||||
|
|
|
@ -5,12 +5,12 @@ import {
|
||||||
selectThumbnailForUri,
|
selectThumbnailForUri,
|
||||||
selectHasChannels,
|
selectHasChannels,
|
||||||
selectMyClaimIdsRaw,
|
selectMyClaimIdsRaw,
|
||||||
|
selectOdyseeMembershipForUri,
|
||||||
} from 'redux/selectors/claims';
|
} from 'redux/selectors/claims';
|
||||||
import { doCommentUpdate, doCommentList } from 'redux/actions/comments';
|
import { doCommentUpdate, doCommentList } from 'redux/actions/comments';
|
||||||
import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
|
import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
|
||||||
import { doToast } from 'redux/actions/notifications';
|
import { doToast } from 'redux/actions/notifications';
|
||||||
import { doClearPlayingUri } from 'redux/actions/content';
|
import { doClearPlayingUri } from 'redux/actions/content';
|
||||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
|
||||||
import {
|
import {
|
||||||
selectLinkedCommentAncestors,
|
selectLinkedCommentAncestors,
|
||||||
selectOthersReactsForComment,
|
selectOthersReactsForComment,
|
||||||
|
@ -18,6 +18,9 @@ import {
|
||||||
} from 'redux/selectors/comments';
|
} from 'redux/selectors/comments';
|
||||||
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||||
import { selectPlayingUri } from 'redux/selectors/content';
|
import { selectPlayingUri } from 'redux/selectors/content';
|
||||||
|
import {
|
||||||
|
selectUserVerifiedEmail,
|
||||||
|
} from 'redux/selectors/user';
|
||||||
import Comment from './view';
|
import Comment from './view';
|
||||||
|
|
||||||
const select = (state, props) => {
|
const select = (state, props) => {
|
||||||
|
@ -33,7 +36,7 @@ const select = (state, props) => {
|
||||||
claim: makeSelectClaimForUri(uri)(state),
|
claim: makeSelectClaimForUri(uri)(state),
|
||||||
thumbnail: channel_url && selectThumbnailForUri(state, channel_url),
|
thumbnail: channel_url && selectThumbnailForUri(state, channel_url),
|
||||||
channelIsBlocked: channel_url && makeSelectChannelIsMuted(channel_url)(state),
|
channelIsBlocked: channel_url && makeSelectChannelIsMuted(channel_url)(state),
|
||||||
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
|
commentingEnabled: Boolean(selectUserVerifiedEmail(state)),
|
||||||
othersReacts: selectOthersReactsForComment(state, reactionKey),
|
othersReacts: selectOthersReactsForComment(state, reactionKey),
|
||||||
activeChannelClaim,
|
activeChannelClaim,
|
||||||
hasChannels: selectHasChannels(state),
|
hasChannels: selectHasChannels(state),
|
||||||
|
@ -41,6 +44,7 @@ const select = (state, props) => {
|
||||||
stakedLevel: selectStakedLevelForChannelUri(state, channel_url),
|
stakedLevel: selectStakedLevelForChannelUri(state, channel_url),
|
||||||
linkedCommentAncestors: selectLinkedCommentAncestors(state),
|
linkedCommentAncestors: selectLinkedCommentAncestors(state),
|
||||||
totalReplyPages: makeSelectTotalReplyPagesForParentId(comment_id)(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 { getChannelFromClaim } from 'util/claim';
|
||||||
import { parseSticker } from 'util/comments';
|
import { parseSticker } from 'util/comments';
|
||||||
import { useIsMobile } from 'effects/use-screensize';
|
import { useIsMobile } from 'effects/use-screensize';
|
||||||
|
import PremiumBadge from 'component/common/premium-badge';
|
||||||
|
|
||||||
const AUTO_EXPAND_ALL_REPLIES = false;
|
const AUTO_EXPAND_ALL_REPLIES = false;
|
||||||
|
|
||||||
|
@ -64,6 +65,7 @@ type Props = {
|
||||||
supportDisabled: boolean,
|
supportDisabled: boolean,
|
||||||
setQuickReply: (any) => void,
|
setQuickReply: (any) => void,
|
||||||
quickReply: any,
|
quickReply: any,
|
||||||
|
selectOdyseeMembershipForUri: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
const LENGTH_TO_COLLAPSE = 300;
|
const LENGTH_TO_COLLAPSE = 300;
|
||||||
|
@ -93,6 +95,7 @@ function CommentView(props: Props) {
|
||||||
supportDisabled,
|
supportDisabled,
|
||||||
setQuickReply,
|
setQuickReply,
|
||||||
quickReply,
|
quickReply,
|
||||||
|
selectOdyseeMembershipForUri,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -256,18 +259,15 @@ function CommentView(props: Props) {
|
||||||
>
|
>
|
||||||
<div className="comment__thumbnail-wrapper">
|
<div className="comment__thumbnail-wrapper">
|
||||||
{authorUri ? (
|
{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>
|
||||||
|
|
||||||
<div className="comment__body-container">
|
<div className="comment__body-container">
|
||||||
<div className="comment__meta">
|
<div className="comment__meta">
|
||||||
<div className="comment__meta-information">
|
<div className="comment__meta-information">
|
||||||
{isGlobalMod && <CommentBadge label={__('Admin')} icon={ICONS.BADGE_MOD} />}
|
|
||||||
{isModerator && <CommentBadge label={__('Moderator')} icon={ICONS.BADGE_MOD} />}
|
|
||||||
|
|
||||||
{!author ? (
|
{!author ? (
|
||||||
<span className="comment__author">{__('Anonymous')}</span>
|
<span className="comment__author">{__('Anonymous')}</span>
|
||||||
) : (
|
) : (
|
||||||
|
@ -277,9 +277,13 @@ function CommentView(props: Props) {
|
||||||
})}
|
})}
|
||||||
link
|
link
|
||||||
uri={authorUri}
|
uri={authorUri}
|
||||||
|
comment
|
||||||
showAtSign
|
showAtSign
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{isGlobalMod && <CommentBadge label={__('Admin')} icon={ICONS.BADGE_ADMIN} />}
|
||||||
|
{isModerator && <CommentBadge label={__('Moderator')} icon={ICONS.BADGE_MOD} />}
|
||||||
|
<PremiumBadge membership={selectOdyseeMembershipForUri} linkPage />
|
||||||
<Button
|
<Button
|
||||||
className="comment__time"
|
className="comment__time"
|
||||||
onClick={handleTimeClick}
|
onClick={handleTimeClick}
|
||||||
|
@ -358,6 +362,7 @@ function CommentView(props: Props) {
|
||||||
promptLinks
|
promptLinks
|
||||||
parentCommentId={commentId}
|
parentCommentId={commentId}
|
||||||
stakedLevel={stakedLevel}
|
stakedLevel={stakedLevel}
|
||||||
|
hasMembership={selectOdyseeMembershipForUri}
|
||||||
/>
|
/>
|
||||||
</Expandable>
|
</Expandable>
|
||||||
) : (
|
) : (
|
||||||
|
@ -366,6 +371,7 @@ function CommentView(props: Props) {
|
||||||
promptLinks
|
promptLinks
|
||||||
parentCommentId={commentId}
|
parentCommentId={commentId}
|
||||||
stakedLevel={stakedLevel}
|
stakedLevel={stakedLevel}
|
||||||
|
hasMembership={selectOdyseeMembershipForUri}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { selectClaimForUri, selectClaimIsMine, selectFetchingMyChannels } from 'redux/selectors/claims';
|
import { selectClaimForUri,
|
||||||
|
selectClaimIsMine,
|
||||||
|
selectFetchingMyChannels,
|
||||||
|
selectClaimsByUri,
|
||||||
|
} from 'redux/selectors/claims';
|
||||||
import {
|
import {
|
||||||
selectTopLevelCommentsForUri,
|
selectTopLevelCommentsForUri,
|
||||||
makeSelectTopLevelTotalPagesForUri,
|
makeSelectTopLevelTotalPagesForUri,
|
||||||
|
@ -16,6 +20,7 @@ import {
|
||||||
import { doCommentReset, doCommentList, doCommentById, doCommentReactList } from 'redux/actions/comments';
|
import { doCommentReset, doCommentList, doCommentById, doCommentReactList } from 'redux/actions/comments';
|
||||||
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||||
import { getChannelIdFromClaim } from 'util/claim';
|
import { getChannelIdFromClaim } from 'util/claim';
|
||||||
|
import { doFetchUserMemberships } from 'redux/actions/user';
|
||||||
import CommentsList from './view';
|
import CommentsList from './view';
|
||||||
|
|
||||||
const select = (state, props) => {
|
const select = (state, props) => {
|
||||||
|
@ -41,6 +46,7 @@ const select = (state, props) => {
|
||||||
myReactsByCommentId: selectMyReacts(state),
|
myReactsByCommentId: selectMyReacts(state),
|
||||||
othersReactsById: selectOthersReacts(state),
|
othersReactsById: selectOthersReacts(state),
|
||||||
activeChannelId: activeChannelClaim && activeChannelClaim.claim_id,
|
activeChannelId: activeChannelClaim && activeChannelClaim.claim_id,
|
||||||
|
claimsByUri: selectClaimsByUri(state),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -49,6 +55,7 @@ const perform = {
|
||||||
fetchComment: doCommentById,
|
fetchComment: doCommentById,
|
||||||
fetchReacts: doCommentReactList,
|
fetchReacts: doCommentReactList,
|
||||||
resetComments: doCommentReset,
|
resetComments: doCommentReset,
|
||||||
|
doFetchUserMemberships,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(select, perform)(CommentsList);
|
export default connect(select, perform)(CommentsList);
|
||||||
|
|
|
@ -15,6 +15,7 @@ import Empty from 'component/common/empty';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import Spinner from 'component/spinner';
|
import Spinner from 'component/spinner';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
|
import useGetUserMemberships from 'effects/use-get-user-memberships';
|
||||||
|
|
||||||
const DEBOUNCE_SCROLL_HANDLER_MS = 200;
|
const DEBOUNCE_SCROLL_HANDLER_MS = 200;
|
||||||
|
|
||||||
|
@ -50,6 +51,8 @@ type Props = {
|
||||||
fetchComment: (commentId: string) => void,
|
fetchComment: (commentId: string) => void,
|
||||||
fetchReacts: (commentIds: Array<string>) => Promise<any>,
|
fetchReacts: (commentIds: Array<string>) => Promise<any>,
|
||||||
resetComments: (claimId: string) => void,
|
resetComments: (claimId: string) => void,
|
||||||
|
claimsByUri: { [string]: any },
|
||||||
|
doFetchUserMemberships: (claimIdCsv: string) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function CommentList(props: Props) {
|
export default function CommentList(props: Props) {
|
||||||
|
@ -76,6 +79,8 @@ export default function CommentList(props: Props) {
|
||||||
fetchComment,
|
fetchComment,
|
||||||
fetchReacts,
|
fetchReacts,
|
||||||
resetComments,
|
resetComments,
|
||||||
|
claimsByUri,
|
||||||
|
doFetchUserMemberships,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
@ -100,6 +105,22 @@ export default function CommentList(props: Props) {
|
||||||
Boolean(othersReactsById) || !ENABLE_COMMENT_REACTIONS
|
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) {
|
function changeSort(newSort) {
|
||||||
if (sort !== newSort) {
|
if (sort !== newSort) {
|
||||||
setSort(newSort);
|
setSort(newSort);
|
||||||
|
|
|
@ -1,35 +1,38 @@
|
||||||
// @flow
|
// @flow
|
||||||
import 'scss/component/_comment-badge.scss';
|
import 'scss/component/_comment-badge.scss';
|
||||||
|
|
||||||
import classnames from 'classnames';
|
|
||||||
import Icon from 'component/common/icon';
|
import Icon from 'component/common/icon';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Tooltip from 'component/common/tooltip';
|
import Tooltip from 'component/common/tooltip';
|
||||||
|
|
||||||
const LABEL_TYPES = {
|
|
||||||
ADMIN: 'Admin',
|
|
||||||
MOD: 'Moderator',
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
icon: string,
|
icon: string,
|
||||||
label: string,
|
label: string,
|
||||||
size?: number,
|
size?: number,
|
||||||
|
placement?: string,
|
||||||
|
hideTooltip?: boolean,
|
||||||
|
className?: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function CommentBadge(props: Props) {
|
export default function CommentBadge(props: Props) {
|
||||||
const { icon, label, size = 20 } = props;
|
const { icon, label, size = 20, placement = 'top', hideTooltip, className } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip title={label} placement="top">
|
<BadgeWrapper title={label} placement={placement} hideTooltip={hideTooltip} className={className}>
|
||||||
<span
|
<span className="comment__badge">
|
||||||
className={classnames('comment__badge', {
|
|
||||||
'comment__badge--globalMod': label === LABEL_TYPES.ADMIN,
|
|
||||||
'comment__badge--mod': label === LABEL_TYPES.MOD,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Icon icon={icon} size={size} />
|
<Icon icon={icon} size={size} />
|
||||||
</span>
|
</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"
|
viewBox="0 0 24 24"
|
||||||
xmlSpace="preserve"
|
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>
|
||||||
<g>
|
<g>
|
||||||
<path
|
<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"
|
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>
|
||||||
</g>
|
</g>
|
||||||
<path
|
<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"
|
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
|
<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"
|
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>
|
</svg>
|
||||||
|
@ -2570,26 +2607,30 @@ export const icons = {
|
||||||
viewBox="-1182 401 24 24"
|
viewBox="-1182 401 24 24"
|
||||||
xmlSpace="preserve"
|
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
|
<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"
|
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
|
<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"
|
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
|
<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"
|
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
|
<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"
|
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
|
<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"
|
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>
|
</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" />
|
<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>
|
</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) => {
|
[ICONS.FEATURED]: (props: IconProps) => {
|
||||||
const { size = 24, color = 'currentColor', ...otherProps } = props;
|
const { size = 24, color = 'currentColor', ...otherProps } = props;
|
||||||
|
|
||||||
|
|
|
@ -58,6 +58,7 @@ type MarkdownProps = {
|
||||||
disableTimestamps?: boolean,
|
disableTimestamps?: boolean,
|
||||||
stakedLevel?: number,
|
stakedLevel?: number,
|
||||||
setUserMention?: (boolean) => void,
|
setUserMention?: (boolean) => void,
|
||||||
|
hasMembership?: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ****************************************************************************
|
// ****************************************************************************
|
||||||
|
@ -156,6 +157,7 @@ export default React.memo<MarkdownProps>(function MarkdownPreview(props: Markdow
|
||||||
disableTimestamps,
|
disableTimestamps,
|
||||||
stakedLevel,
|
stakedLevel,
|
||||||
setUserMention,
|
setUserMention,
|
||||||
|
hasMembership,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const strippedContent = content
|
const strippedContent = content
|
||||||
|
@ -189,7 +191,7 @@ export default React.memo<MarkdownProps>(function MarkdownPreview(props: Markdow
|
||||||
parentCommentId={parentCommentId}
|
parentCommentId={parentCommentId}
|
||||||
isMarkdownPost={isMarkdownPost}
|
isMarkdownPost={isMarkdownPost}
|
||||||
simpleLinks={simpleLinks}
|
simpleLinks={simpleLinks}
|
||||||
allowPreview={isStakeEnoughForPreview(stakedLevel)}
|
allowPreview={isStakeEnoughForPreview(stakedLevel) || hasMembership}
|
||||||
setUserMention={setUserMention}
|
setUserMention={setUserMention}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
@ -198,7 +200,7 @@ export default React.memo<MarkdownProps>(function MarkdownPreview(props: Markdow
|
||||||
img: (imgProps) => {
|
img: (imgProps) => {
|
||||||
const imageCdnUrl =
|
const imageCdnUrl =
|
||||||
getThumbnailCdnUrl({ thumbnail: imgProps.src, width: 0, height: 0, quality: 85 }) || MISSING_THUMB_DEFAULT;
|
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} />;
|
return <ZoomableImage {...imgProps} src={imageCdnUrl} />;
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
|
@ -206,7 +208,7 @@ export default React.memo<MarkdownProps>(function MarkdownPreview(props: Markdow
|
||||||
src={imageCdnUrl}
|
src={imageCdnUrl}
|
||||||
alt={imgProps.alt}
|
alt={imgProps.alt}
|
||||||
title={imgProps.title}
|
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,
|
disableInteractive?: boolean,
|
||||||
enterDelay?: number,
|
enterDelay?: number,
|
||||||
title?: string | Node,
|
title?: string | Node,
|
||||||
|
className?: string,
|
||||||
followCursor?: boolean,
|
followCursor?: boolean,
|
||||||
placement?: string, // https://mui.com/api/tooltip/
|
placement?: string, // https://mui.com/api/tooltip/
|
||||||
};
|
};
|
||||||
|
@ -20,6 +21,7 @@ function Tooltip(props: Props) {
|
||||||
disableInteractive = true,
|
disableInteractive = true,
|
||||||
enterDelay = 300,
|
enterDelay = 300,
|
||||||
title,
|
title,
|
||||||
|
className,
|
||||||
followCursor = false,
|
followCursor = false,
|
||||||
placement = 'bottom',
|
placement = 'bottom',
|
||||||
} = props;
|
} = props;
|
||||||
|
@ -33,6 +35,7 @@ function Tooltip(props: Props) {
|
||||||
title={title}
|
title={title}
|
||||||
followCursor={followCursor}
|
followCursor={followCursor}
|
||||||
placement={placement}
|
placement={placement}
|
||||||
|
classes={{ tooltip: className }}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</MUITooltip>
|
</MUITooltip>
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { doSetClientSetting } from 'redux/actions/settings';
|
import { doSetClientSetting } from 'redux/actions/settings';
|
||||||
import { selectActiveChannelStakedLevel } from 'redux/selectors/app';
|
|
||||||
import { selectClientSetting } from 'redux/selectors/settings';
|
import { selectClientSetting } from 'redux/selectors/settings';
|
||||||
import * as SETTINGS from 'constants/settings';
|
import * as SETTINGS from 'constants/settings';
|
||||||
import HeaderMenuButtons from './view';
|
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) => ({
|
const select = (state) => ({
|
||||||
activeChannelStakedLevel: selectActiveChannelStakedLevel(state),
|
|
||||||
authenticated: selectUserVerifiedEmail(state),
|
authenticated: selectUserVerifiedEmail(state),
|
||||||
automaticDarkModeEnabled: selectClientSetting(state, SETTINGS.AUTOMATIC_DARK_MODE_ENABLED),
|
automaticDarkModeEnabled: selectClientSetting(state, SETTINGS.AUTOMATIC_DARK_MODE_ENABLED),
|
||||||
currentTheme: selectClientSetting(state, SETTINGS.THEME),
|
currentTheme: selectClientSetting(state, SETTINGS.THEME),
|
||||||
user: selectUser(state),
|
user: selectUser(state),
|
||||||
|
odyseeMembership: selectOdyseeMembershipName(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = (dispatch) => ({
|
const perform = (dispatch) => ({
|
||||||
|
@ -19,6 +19,7 @@ const perform = (dispatch) => ({
|
||||||
if (automaticDarkModeEnabled) dispatch(doSetClientSetting(SETTINGS.AUTOMATIC_DARK_MODE_ENABLED, false));
|
if (automaticDarkModeEnabled) dispatch(doSetClientSetting(SETTINGS.AUTOMATIC_DARK_MODE_ENABLED, false));
|
||||||
dispatch(doSetClientSetting(SETTINGS.THEME, currentTheme === 'dark' ? 'light' : 'dark', true));
|
dispatch(doSetClientSetting(SETTINGS.THEME, currentTheme === 'dark' ? 'light' : 'dark', true));
|
||||||
},
|
},
|
||||||
|
doOpenModal: (id, params) => dispatch(doOpenModal(id, params)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, perform)(HeaderMenuButtons);
|
export default connect(select, perform)(HeaderMenuButtons);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import 'scss/component/_header.scss';
|
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 { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import * as PAGES from 'constants/pages';
|
import * as PAGES from 'constants/pages';
|
||||||
|
@ -18,25 +18,17 @@ type HeaderMenuButtonProps = {
|
||||||
currentTheme: string,
|
currentTheme: string,
|
||||||
user: ?User,
|
user: ?User,
|
||||||
handleThemeToggle: (boolean, string) => void,
|
handleThemeToggle: (boolean, string) => void,
|
||||||
|
doOpenModal: (string, {}) => void,
|
||||||
|
odyseeMembership: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function HeaderMenuButtons(props: HeaderMenuButtonProps) {
|
export default function HeaderMenuButtons(props: HeaderMenuButtonProps) {
|
||||||
const {
|
const { authenticated, automaticDarkModeEnabled, currentTheme, user, handleThemeToggle, odyseeMembership } = props;
|
||||||
authenticated,
|
|
||||||
automaticDarkModeEnabled,
|
const isOnMembershipPage = window.location.pathname === `/$/${PAGES.ODYSEE_MEMBERSHIP}`;
|
||||||
currentTheme,
|
|
||||||
activeChannelStakedLevel,
|
|
||||||
user,
|
|
||||||
handleThemeToggle,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const notificationsEnabled = ENABLE_UI_NOTIFICATIONS || (user && user.experimental_ui);
|
const notificationsEnabled = ENABLE_UI_NOTIFICATIONS || (user && user.experimental_ui);
|
||||||
const livestreamEnabled = Boolean(
|
const livestreamEnabled = Boolean(ENABLE_NO_SOURCE_CLAIMS && user && !user.odysee_live_disabled);
|
||||||
ENABLE_NO_SOURCE_CLAIMS &&
|
|
||||||
user &&
|
|
||||||
!user.odysee_live_disabled &&
|
|
||||||
(activeChannelStakedLevel >= CHANNEL_STAKED_LEVEL_LIVESTREAM || user.odysee_live_enabled)
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="header__buttons">
|
<div className="header__buttons">
|
||||||
|
@ -68,6 +60,10 @@ export default function HeaderMenuButtons(props: HeaderMenuButtonProps) {
|
||||||
|
|
||||||
<MenuList className="menu__list--header">
|
<MenuList className="menu__list--header">
|
||||||
<HeaderMenuLink page={PAGES.SETTINGS} icon={ICONS.SETTINGS} name={__('Settings')} />
|
<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')} />
|
<HeaderMenuLink page={PAGES.HELP} icon={ICONS.HELP} name={__('Help')} />
|
||||||
|
|
||||||
<MenuItem className="menu__link" onSelect={() => handleThemeToggle(automaticDarkModeEnabled, currentTheme)}>
|
<MenuItem className="menu__link" onSelect={() => handleThemeToggle(automaticDarkModeEnabled, currentTheme)}>
|
||||||
|
|
|
@ -35,14 +35,13 @@ export default function HeaderProfileMenuButton(props: HeaderMenuButtonProps) {
|
||||||
) : (
|
) : (
|
||||||
<MenuButton
|
<MenuButton
|
||||||
aria-label={__('Your account')}
|
aria-label={__('Your account')}
|
||||||
title={__('Your account')}
|
|
||||||
className={classnames('header__navigationItem', {
|
className={classnames('header__navigationItem', {
|
||||||
'header__navigationItem--icon': !activeChannelUrl,
|
'header__navigationItem--icon': !activeChannelUrl,
|
||||||
'header__navigationItem--profilePic': activeChannelUrl,
|
'header__navigationItem--profilePic': activeChannelUrl,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{activeChannelUrl ? (
|
{activeChannelUrl ? (
|
||||||
<ChannelThumbnail uri={activeChannelUrl} hideTooltip small noLazyLoad />
|
<ChannelThumbnail uri={activeChannelUrl} hideTooltip small noLazyLoad showMemberBadge />
|
||||||
) : (
|
) : (
|
||||||
<Icon size={18} icon={ICONS.ACCOUNT} aria-hidden />
|
<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.CREATOR_DASHBOARD} icon={ICONS.ANALYTICS} name={__('Creator Analytics')} />
|
||||||
<HeaderMenuLink page={PAGES.REWARDS} icon={ICONS.REWARDS} name={__('Rewards')} />
|
<HeaderMenuLink page={PAGES.REWARDS} icon={ICONS.REWARDS} name={__('Rewards')} />
|
||||||
<HeaderMenuLink page={PAGES.INVITE} icon={ICONS.INVITE} name={__('Invites')} />
|
<HeaderMenuLink page={PAGES.INVITE} icon={ICONS.INVITE} name={__('Invites')} />
|
||||||
|
<HeaderMenuLink page={PAGES.ODYSEE_MEMBERSHIP} icon={ICONS.UPGRADE} name={__('Odysee Premium')} />
|
||||||
|
|
||||||
<MenuItem onSelect={signOut}>
|
<MenuItem onSelect={signOut}>
|
||||||
<div className="menu__link">
|
<div className="menu__link">
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { MAX_LIVESTREAM_COMMENTS } from 'constants/livestream';
|
import { MAX_LIVESTREAM_COMMENTS } from 'constants/livestream';
|
||||||
import { doResolveUris } from 'redux/actions/claims';
|
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 { doCommentList, doSuperChatList } from 'redux/actions/comments';
|
||||||
import {
|
import {
|
||||||
selectTopLevelCommentsForUri,
|
selectTopLevelCommentsForUri,
|
||||||
|
@ -9,6 +9,7 @@ import {
|
||||||
selectPinnedCommentsForUri,
|
selectPinnedCommentsForUri,
|
||||||
} from 'redux/selectors/comments';
|
} from 'redux/selectors/comments';
|
||||||
import { selectThemePath } from 'redux/selectors/settings';
|
import { selectThemePath } from 'redux/selectors/settings';
|
||||||
|
import { doFetchUserMemberships } from 'redux/actions/user';
|
||||||
import LivestreamChatLayout from './view';
|
import LivestreamChatLayout from './view';
|
||||||
|
|
||||||
const select = (state, props) => {
|
const select = (state, props) => {
|
||||||
|
@ -21,6 +22,7 @@ const select = (state, props) => {
|
||||||
pinnedComments: selectPinnedCommentsForUri(state, uri),
|
pinnedComments: selectPinnedCommentsForUri(state, uri),
|
||||||
superChats: selectSuperChatsForUri(state, uri),
|
superChats: selectSuperChatsForUri(state, uri),
|
||||||
theme: selectThemePath(state),
|
theme: selectThemePath(state),
|
||||||
|
claimsByUri: selectClaimsByUri(state),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -28,6 +30,7 @@ const perform = {
|
||||||
doCommentList,
|
doCommentList,
|
||||||
doSuperChatList,
|
doSuperChatList,
|
||||||
doResolveUris,
|
doResolveUris,
|
||||||
|
doFetchUserMemberships,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(select, perform)(LivestreamChatLayout);
|
export default connect(select, perform)(LivestreamChatLayout);
|
||||||
|
|
|
@ -18,6 +18,7 @@ import React from 'react';
|
||||||
import Yrbl from 'component/yrbl';
|
import Yrbl from 'component/yrbl';
|
||||||
import { getTipValues } from 'util/livestream';
|
import { getTipValues } from 'util/livestream';
|
||||||
import Slide from '@mui/material/Slide';
|
import Slide from '@mui/material/Slide';
|
||||||
|
import useGetUserMemberships from 'effects/use-get-user-memberships';
|
||||||
|
|
||||||
export const VIEW_MODES = {
|
export const VIEW_MODES = {
|
||||||
CHAT: 'chat',
|
CHAT: 'chat',
|
||||||
|
@ -49,6 +50,8 @@ type Props = {
|
||||||
) => void,
|
) => void,
|
||||||
doResolveUris: (uris: Array<string>, cache: boolean) => void,
|
doResolveUris: (uris: Array<string>, cache: boolean) => void,
|
||||||
doSuperChatList: (uri: string) => void,
|
doSuperChatList: (uri: string) => void,
|
||||||
|
claimsByUri: { [string]: any },
|
||||||
|
doFetchUserMemberships: (claimIdCsv: string) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function LivestreamChatLayout(props: Props) {
|
export default function LivestreamChatLayout(props: Props) {
|
||||||
|
@ -68,6 +71,8 @@ export default function LivestreamChatLayout(props: Props) {
|
||||||
doCommentList,
|
doCommentList,
|
||||||
doResolveUris,
|
doResolveUris,
|
||||||
doSuperChatList,
|
doSuperChatList,
|
||||||
|
doFetchUserMemberships,
|
||||||
|
claimsByUri,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const isMobile = useIsMobile() && !isPopoutWindow;
|
const isMobile = useIsMobile() && !isPopoutWindow;
|
||||||
|
@ -96,6 +101,22 @@ export default function LivestreamChatLayout(props: Props) {
|
||||||
superChatsByChronologicalOrder.sort((a, b) => b.timestamp - a.timestamp);
|
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 =
|
const commentsToDisplay =
|
||||||
viewMode === VIEW_MODES.CHAT ? commentsByChronologicalOrder : superChatsByChronologicalOrder;
|
viewMode === VIEW_MODES.CHAT ? commentsByChronologicalOrder : superChatsByChronologicalOrder;
|
||||||
const commentsLength = commentsToDisplay && commentsToDisplay.length;
|
const commentsLength = commentsToDisplay && commentsToDisplay.length;
|
||||||
|
|
|
@ -1,16 +1,27 @@
|
||||||
import { connect } from 'react-redux';
|
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';
|
import LivestreamComment from './view';
|
||||||
|
|
||||||
const select = (state, props) => {
|
const select = (state, props) => {
|
||||||
const { uri, comment } = props;
|
const { uri, comment } = props;
|
||||||
const { channel_url: authorUri } = comment;
|
const { channel_url: authorUri, channel_id: channelId } = comment;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
claim: selectClaimForUri(state, uri),
|
claim: selectClaimForUri(state, uri),
|
||||||
stakedLevel: selectStakedLevelForChannelUri(state, authorUri),
|
stakedLevel: selectStakedLevelForChannelUri(state, authorUri),
|
||||||
myChannelIds: selectMyClaimIdsRaw(state),
|
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 MarkdownPreview from 'component/common/markdown-preview';
|
||||||
import OptimizedImage from 'component/optimizedImage';
|
import OptimizedImage from 'component/optimizedImage';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PremiumBadge from 'component/common/premium-badge';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
comment: Comment,
|
comment: Comment,
|
||||||
|
@ -27,8 +28,10 @@ type Props = {
|
||||||
myChannelIds: ?Array<string>,
|
myChannelIds: ?Array<string>,
|
||||||
stakedLevel: number,
|
stakedLevel: number,
|
||||||
isMobile?: boolean,
|
isMobile?: boolean,
|
||||||
|
odyseeMembership: string,
|
||||||
handleDismissPin?: () => void,
|
handleDismissPin?: () => void,
|
||||||
restoreScrollPos?: () => void,
|
restoreScrollPos?: () => void,
|
||||||
|
claimsByUri: { [string]: any },
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function LivestreamComment(props: Props) {
|
export default function LivestreamComment(props: Props) {
|
||||||
|
@ -42,6 +45,7 @@ export default function LivestreamComment(props: Props) {
|
||||||
isMobile,
|
isMobile,
|
||||||
handleDismissPin,
|
handleDismissPin,
|
||||||
restoreScrollPos,
|
restoreScrollPos,
|
||||||
|
odyseeMembership,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -98,10 +102,6 @@ export default function LivestreamComment(props: Props) {
|
||||||
{supportAmount > 0 && <ChannelThumbnail uri={authorUri} xsmall />}
|
{supportAmount > 0 && <ChannelThumbnail uri={authorUri} xsmall />}
|
||||||
|
|
||||||
<div className="livestreamComment__info">
|
<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
|
<Button
|
||||||
className={classnames('button--uri-indicator comment__author', { 'comment__author--creator': isStreamer })}
|
className={classnames('button--uri-indicator comment__author', { 'comment__author--creator': isStreamer })}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
@ -117,6 +117,11 @@ export default function LivestreamComment(props: Props) {
|
||||||
</span>
|
</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 */}
|
{/* Use key to force timestamp update */}
|
||||||
<DateTime date={timePosted} timeAgo key={forceUpdate} genericSeconds />
|
<DateTime date={timePosted} timeAgo key={forceUpdate} genericSeconds />
|
||||||
|
|
||||||
|
@ -135,6 +140,7 @@ export default function LivestreamComment(props: Props) {
|
||||||
stakedLevel={stakedLevel}
|
stakedLevel={stakedLevel}
|
||||||
disableTimestamps
|
disableTimestamps
|
||||||
setUserMention={setUserMention}
|
setUserMention={setUserMention}
|
||||||
|
hasMembership={odyseeMembership}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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';
|
} from 'redux/selectors/app';
|
||||||
import { selectClientSetting } from 'redux/selectors/settings';
|
import { selectClientSetting } from 'redux/selectors/settings';
|
||||||
import { makeSelectFileRenderModeForUri } from 'redux/selectors/content';
|
import { makeSelectFileRenderModeForUri } from 'redux/selectors/content';
|
||||||
import { selectUser } from 'redux/selectors/user';
|
import { selectUser, selectOdyseeMembershipName } from 'redux/selectors/user';
|
||||||
import PublishForm from './view';
|
import PublishForm from './view';
|
||||||
|
|
||||||
const select = (state) => {
|
const select = (state) => {
|
||||||
|
@ -65,6 +65,7 @@ const select = (state) => {
|
||||||
activeChannelStakedLevel: selectActiveChannelStakedLevel(state),
|
activeChannelStakedLevel: selectActiveChannelStakedLevel(state),
|
||||||
isClaimingInitialRewards: selectIsClaimingInitialRewards(state),
|
isClaimingInitialRewards: selectIsClaimingInitialRewards(state),
|
||||||
hasClaimedInitialRewards: selectHasClaimedInitialRewards(state),
|
hasClaimedInitialRewards: selectHasClaimedInitialRewards(state),
|
||||||
|
odyseeMembership: selectOdyseeMembershipName(state),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -97,6 +97,7 @@ type Props = {
|
||||||
isClaimingInitialRewards: boolean,
|
isClaimingInitialRewards: boolean,
|
||||||
claimInitialRewards: () => void,
|
claimInitialRewards: () => void,
|
||||||
hasClaimedInitialRewards: boolean,
|
hasClaimedInitialRewards: boolean,
|
||||||
|
odyseeMembership: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
function PublishForm(props: Props) {
|
function PublishForm(props: Props) {
|
||||||
|
@ -138,6 +139,7 @@ function PublishForm(props: Props) {
|
||||||
isClaimingInitialRewards,
|
isClaimingInitialRewards,
|
||||||
claimInitialRewards,
|
claimInitialRewards,
|
||||||
hasClaimedInitialRewards,
|
hasClaimedInitialRewards,
|
||||||
|
odyseeMembership,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const inEditMode = Boolean(editingURI);
|
const inEditMode = Boolean(editingURI);
|
||||||
|
@ -146,11 +148,15 @@ function PublishForm(props: Props) {
|
||||||
const TYPE_PARAM = 'type';
|
const TYPE_PARAM = 'type';
|
||||||
const uploadType = urlParams.get(TYPE_PARAM);
|
const uploadType = urlParams.get(TYPE_PARAM);
|
||||||
const _uploadType = uploadType && uploadType.toLowerCase();
|
const _uploadType = uploadType && uploadType.toLowerCase();
|
||||||
|
|
||||||
|
const userHasEnoughLBCForStreaming = activeChannelStakedLevel >= CHANNEL_STAKED_LEVEL_LIVESTREAM;
|
||||||
|
|
||||||
const enableLivestream =
|
const enableLivestream =
|
||||||
ENABLE_NO_SOURCE_CLAIMS &&
|
ENABLE_NO_SOURCE_CLAIMS &&
|
||||||
user &&
|
user &&
|
||||||
!user.odysee_live_disabled &&
|
!user.odysee_live_disabled &&
|
||||||
(activeChannelStakedLevel >= CHANNEL_STAKED_LEVEL_LIVESTREAM || user.odysee_live_enabled);
|
(userHasEnoughLBCForStreaming || user.odysee_live_enabled || odyseeMembership);
|
||||||
|
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
const AVAILABLE_MODES = Object.values(PUBLISH_MODES).filter((mode) => {
|
const AVAILABLE_MODES = Object.values(PUBLISH_MODES).filter((mode) => {
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { connect } from 'react-redux';
|
||||||
import { selectClaimForUri } from 'redux/selectors/claims';
|
import { selectClaimForUri } from 'redux/selectors/claims';
|
||||||
import { doFetchRecommendedContent } from 'redux/actions/search';
|
import { doFetchRecommendedContent } from 'redux/actions/search';
|
||||||
import { selectRecommendedContentForUri, selectIsSearching } from 'redux/selectors/search';
|
import { selectRecommendedContentForUri, selectIsSearching } from 'redux/selectors/search';
|
||||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
import { selectOdyseeMembershipIsPremiumPlus } from 'redux/selectors/user';
|
||||||
import RecommendedContent from './view';
|
import RecommendedContent from './view';
|
||||||
|
|
||||||
const select = (state, props) => {
|
const select = (state, props) => {
|
||||||
|
@ -14,7 +14,7 @@ const select = (state, props) => {
|
||||||
recommendedContentUris,
|
recommendedContentUris,
|
||||||
nextRecommendedUri,
|
nextRecommendedUri,
|
||||||
isSearching: selectIsSearching(state),
|
isSearching: selectIsSearching(state),
|
||||||
isAuthenticated: selectUserVerifiedEmail(state),
|
userHasPremiumPlus: selectOdyseeMembershipIsPremiumPlus(state),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -23,8 +23,10 @@ type Props = {
|
||||||
nextRecommendedUri: string,
|
nextRecommendedUri: string,
|
||||||
isSearching: boolean,
|
isSearching: boolean,
|
||||||
doFetchRecommendedContent: (string) => void,
|
doFetchRecommendedContent: (string) => void,
|
||||||
isAuthenticated: boolean,
|
|
||||||
claim: ?StreamClaim,
|
claim: ?StreamClaim,
|
||||||
|
claimId: string,
|
||||||
|
metadata: any,
|
||||||
|
userHasPremiumPlus: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default React.memo<Props>(function RecommendedContent(props: Props) {
|
export default React.memo<Props>(function RecommendedContent(props: Props) {
|
||||||
|
@ -34,12 +36,12 @@ export default React.memo<Props>(function RecommendedContent(props: Props) {
|
||||||
recommendedContentUris,
|
recommendedContentUris,
|
||||||
nextRecommendedUri,
|
nextRecommendedUri,
|
||||||
isSearching,
|
isSearching,
|
||||||
isAuthenticated,
|
|
||||||
claim,
|
claim,
|
||||||
|
userHasPremiumPlus,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const claimId: ?string = claim && claim.claim_id;
|
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) {
|
function claimContainsBlockedWords(claim: ?StreamClaim) {
|
||||||
if (BLOCKED_WORDS) {
|
if (BLOCKED_WORDS) {
|
||||||
|
@ -177,7 +179,6 @@ function areEqual(prevProps: Props, nextProps: Props) {
|
||||||
if (
|
if (
|
||||||
a.uri !== b.uri ||
|
a.uri !== b.uri ||
|
||||||
a.nextRecommendedUri !== b.nextRecommendedUri ||
|
a.nextRecommendedUri !== b.nextRecommendedUri ||
|
||||||
a.isAuthenticated !== b.isAuthenticated ||
|
|
||||||
a.isSearching !== b.isSearching ||
|
a.isSearching !== b.isSearching ||
|
||||||
(a.recommendedContentUris && !b.recommendedContentUris) ||
|
(a.recommendedContentUris && !b.recommendedContentUris) ||
|
||||||
(!a.recommendedContentUris && b.recommendedContentUris) ||
|
(!a.recommendedContentUris && b.recommendedContentUris) ||
|
||||||
|
|
|
@ -72,6 +72,9 @@ const LiveStreamSetupPage = lazyImport(() => import('page/livestreamSetup' /* we
|
||||||
const LivestreamCurrentPage = lazyImport(() =>
|
const LivestreamCurrentPage = lazyImport(() =>
|
||||||
import('page/livestreamCurrent' /* webpackChunkName: "livestreamCurrent" */)
|
import('page/livestreamCurrent' /* webpackChunkName: "livestreamCurrent" */)
|
||||||
);
|
);
|
||||||
|
const OdyseeMembershipPage = lazyImport(() =>
|
||||||
|
import('page/odyseeMembership' /* webpackChunkName: "odyseeMembership" */)
|
||||||
|
);
|
||||||
const OwnComments = lazyImport(() => import('page/ownComments' /* webpackChunkName: "ownComments" */));
|
const OwnComments = lazyImport(() => import('page/ownComments' /* webpackChunkName: "ownComments" */));
|
||||||
const PasswordResetPage = lazyImport(() => import('page/passwordReset' /* webpackChunkName: "passwordReset" */));
|
const PasswordResetPage = lazyImport(() => import('page/passwordReset' /* webpackChunkName: "passwordReset" */));
|
||||||
const PasswordSetPage = lazyImport(() => import('page/passwordSet' /* webpackChunkName: "passwordSet" */));
|
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.NOTIFICATIONS}`} component={NotificationsPage} />
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.AUTH_WALLET_PASSWORD}`} component={SignInWalletPasswordPage} />
|
<PrivateRoute {...props} path={`/$/${PAGES.AUTH_WALLET_PASSWORD}`} component={SignInWalletPasswordPage} />
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_OWN_COMMENTS}`} component={OwnComments} />
|
<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} />
|
<Route path={`/$/${PAGES.POPOUT}/:channelName/:streamName`} component={PopoutChatPage} />
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { doFetchLastActiveSubs } from 'redux/actions/subscriptions';
|
import { doFetchLastActiveSubs } from 'redux/actions/subscriptions';
|
||||||
import { selectActiveChannelStakedLevel } from 'redux/selectors/app';
|
|
||||||
import { selectLastActiveSubscriptions, selectSubscriptions } from 'redux/selectors/subscriptions';
|
import { selectLastActiveSubscriptions, selectSubscriptions } from 'redux/selectors/subscriptions';
|
||||||
import { doClearClaimSearch } from 'redux/actions/claims';
|
import { doClearClaimSearch } from 'redux/actions/claims';
|
||||||
import { doClearPurchasedUriSuccess } from 'redux/actions/file';
|
import { doClearPurchasedUriSuccess } from 'redux/actions/file';
|
||||||
import { selectFollowedTags } from 'redux/selectors/tags';
|
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 { selectHomepageData, selectWildWestDisabled } from 'redux/selectors/settings';
|
||||||
import { doSignOut } from 'redux/actions/app';
|
import { doSignOut } from 'redux/actions/app';
|
||||||
import { selectUnseenNotificationCount } from 'redux/selectors/notifications';
|
import { selectUnseenNotificationCount } from 'redux/selectors/notifications';
|
||||||
import { selectPurchaseUriSuccess } from 'redux/selectors/claims';
|
import { selectPurchaseUriSuccess, selectOdyseeMembershipForUri } from 'redux/selectors/claims';
|
||||||
|
|
||||||
import SideNavigation from './view';
|
import SideNavigation from './view';
|
||||||
|
|
||||||
|
@ -22,8 +21,9 @@ const select = (state) => ({
|
||||||
unseenCount: selectUnseenNotificationCount(state),
|
unseenCount: selectUnseenNotificationCount(state),
|
||||||
user: selectUser(state),
|
user: selectUser(state),
|
||||||
homepageData: selectHomepageData(state),
|
homepageData: selectHomepageData(state),
|
||||||
activeChannelStakedLevel: selectActiveChannelStakedLevel(state),
|
|
||||||
wildWestDisabled: selectWildWestDisabled(state),
|
wildWestDisabled: selectWildWestDisabled(state),
|
||||||
|
odyseeMembership: selectOdyseeMembershipName(state),
|
||||||
|
odyseeMembershipByUri: (uri) => selectOdyseeMembershipForUri(state, uri),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, {
|
export default connect(select, {
|
||||||
|
|
|
@ -15,7 +15,8 @@ import I18nMessage from 'component/i18nMessage';
|
||||||
import ChannelThumbnail from 'component/channelThumbnail';
|
import ChannelThumbnail from 'component/channelThumbnail';
|
||||||
import { useIsMobile, useIsLargeScreen, isTouch } from 'effects/use-screensize';
|
import { useIsMobile, useIsLargeScreen, isTouch } from 'effects/use-screensize';
|
||||||
import { GetLinksData } from 'util/buildHomepage';
|
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();
|
const touch = isTouch();
|
||||||
|
|
||||||
|
@ -70,6 +71,13 @@ const PLAYLISTS = {
|
||||||
hideForUnauth: true,
|
hideForUnauth: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const PREMIUM = {
|
||||||
|
title: 'Premium',
|
||||||
|
link: `/$/${PAGES.ODYSEE_MEMBERSHIP}`,
|
||||||
|
icon: ICONS.UPGRADE,
|
||||||
|
hideForUnauth: true,
|
||||||
|
};
|
||||||
|
|
||||||
const UNAUTH_LINKS: Array<SideNavLink> = [
|
const UNAUTH_LINKS: Array<SideNavLink> = [
|
||||||
{
|
{
|
||||||
title: 'Log In',
|
title: 'Log In',
|
||||||
|
@ -118,9 +126,10 @@ type Props = {
|
||||||
doClearPurchasedUriSuccess: () => void,
|
doClearPurchasedUriSuccess: () => void,
|
||||||
user: ?User,
|
user: ?User,
|
||||||
homepageData: any,
|
homepageData: any,
|
||||||
activeChannelStakedLevel: number,
|
|
||||||
wildWestDisabled: boolean,
|
wildWestDisabled: boolean,
|
||||||
doClearClaimSearch: () => void,
|
doClearClaimSearch: () => void,
|
||||||
|
odyseeMembership: string,
|
||||||
|
odyseeMembershipByUri: (uri: string) => string,
|
||||||
doFetchLastActiveSubs: (force?: boolean, count?: number) => void,
|
doFetchLastActiveSubs: (force?: boolean, count?: number) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -140,9 +149,10 @@ function SideNavigation(props: Props) {
|
||||||
homepageData,
|
homepageData,
|
||||||
user,
|
user,
|
||||||
followedTags,
|
followedTags,
|
||||||
activeChannelStakedLevel,
|
|
||||||
wildWestDisabled,
|
wildWestDisabled,
|
||||||
doClearClaimSearch,
|
doClearClaimSearch,
|
||||||
|
odyseeMembership,
|
||||||
|
odyseeMembershipByUri,
|
||||||
doFetchLastActiveSubs,
|
doFetchLastActiveSubs,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
@ -228,12 +238,7 @@ function SideNavigation(props: Props) {
|
||||||
const notificationsEnabled = ENABLE_UI_NOTIFICATIONS || (user && user.experimental_ui);
|
const notificationsEnabled = ENABLE_UI_NOTIFICATIONS || (user && user.experimental_ui);
|
||||||
const isAuthenticated = Boolean(email);
|
const isAuthenticated = Boolean(email);
|
||||||
|
|
||||||
const livestreamEnabled = Boolean(
|
const livestreamEnabled = Boolean(ENABLE_NO_SOURCE_CLAIMS && user && !user.odysee_live_disabled);
|
||||||
ENABLE_NO_SOURCE_CLAIMS &&
|
|
||||||
user &&
|
|
||||||
!user.odysee_live_disabled &&
|
|
||||||
(activeChannelStakedLevel >= CHANNEL_STAKED_LEVEL_LIVESTREAM || user.odysee_live_enabled)
|
|
||||||
);
|
|
||||||
|
|
||||||
const [pulseLibrary, setPulseLibrary] = React.useState(false);
|
const [pulseLibrary, setPulseLibrary] = React.useState(false);
|
||||||
const [expandTags, setExpandTags] = React.useState(false);
|
const [expandTags, setExpandTags] = React.useState(false);
|
||||||
|
@ -325,7 +330,11 @@ function SideNavigation(props: Props) {
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
{displayedSubscriptions.map((subscription) => (
|
{displayedSubscriptions.map((subscription) => (
|
||||||
<SubscriptionListItem key={subscription.uri} subscription={subscription} />
|
<SubscriptionListItem
|
||||||
|
key={subscription.uri}
|
||||||
|
subscription={subscription}
|
||||||
|
odyseeMembershipByUri={odyseeMembershipByUri}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
{!!subscriptionFilter && !displayedSubscriptions.length && (
|
{!!subscriptionFilter && !displayedSubscriptions.length && (
|
||||||
<li>
|
<li>
|
||||||
|
@ -494,6 +503,7 @@ function SideNavigation(props: Props) {
|
||||||
{getLink(getHomeButton(doClearClaimSearch))}
|
{getLink(getHomeButton(doClearClaimSearch))}
|
||||||
{getLink(RECENT_FROM_FOLLOWING)}
|
{getLink(RECENT_FROM_FOLLOWING)}
|
||||||
{getLink(PLAYLISTS)}
|
{getLink(PLAYLISTS)}
|
||||||
|
{!odyseeMembership && getLink(PREMIUM)}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<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 { uri, channelName } = subscription;
|
||||||
|
|
||||||
|
const membership = odyseeMembershipByUri(uri);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className="navigation-link__wrapper navigation__subscription">
|
<li className="navigation-link__wrapper navigation__subscription">
|
||||||
<Button
|
<Button
|
||||||
|
@ -547,6 +566,7 @@ function SubscriptionListItem({ subscription }: { subscription: Subscription })
|
||||||
<ClaimPreviewTitle uri={uri} />
|
<ClaimPreviewTitle uri={uri} />
|
||||||
<span dir="auto" className="channel-name">
|
<span dir="auto" className="channel-name">
|
||||||
{channelName}
|
{channelName}
|
||||||
|
<PremiumBadge membership={membership} />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { selectClaimForUri } from 'redux/selectors/claims';
|
import { selectClaimForUri, selectOdyseeMembershipForUri } from 'redux/selectors/claims';
|
||||||
import TextareaSuggestionsItem from './view';
|
import TextareaSuggestionsItem from './view';
|
||||||
import { formatLbryChannelName } from 'util/url';
|
import { formatLbryChannelName } from 'util/url';
|
||||||
import { getClaimTitle } from 'util/claim';
|
import { getClaimTitle } from 'util/claim';
|
||||||
|
@ -12,6 +12,7 @@ const select = (state, props) => {
|
||||||
return {
|
return {
|
||||||
claimLabel: claim && formatLbryChannelName(claim.canonical_url),
|
claimLabel: claim && formatLbryChannelName(claim.canonical_url),
|
||||||
claimTitle: claim && getClaimTitle(claim),
|
claimTitle: claim && getClaimTitle(claim),
|
||||||
|
odyseeMembershipByUri: selectOdyseeMembershipForUri(state, uri),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
// @flow
|
// @flow
|
||||||
import ChannelThumbnail from 'component/channelThumbnail';
|
import ChannelThumbnail from 'component/channelThumbnail';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PremiumBadge from 'component/common/premium-badge';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
claimLabel?: string,
|
claimLabel?: string,
|
||||||
claimTitle?: string,
|
claimTitle?: string,
|
||||||
emote?: any,
|
emote?: any,
|
||||||
uri?: string,
|
uri?: string,
|
||||||
|
odyseeMembershipByUri: ?string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function TextareaSuggestionsItem(props: Props) {
|
export default function TextareaSuggestionsItem(props: Props) {
|
||||||
const { claimLabel, claimTitle, emote, uri, ...autocompleteProps } = props;
|
const { claimLabel, claimTitle, emote, uri, odyseeMembershipByUri, ...autocompleteProps } = props;
|
||||||
|
|
||||||
if (emote) {
|
if (emote) {
|
||||||
const { name: value, url, unicode } = emote;
|
const { name: value, url, unicode } = emote;
|
||||||
|
@ -37,7 +39,10 @@ export default function TextareaSuggestionsItem(props: Props) {
|
||||||
|
|
||||||
<div className="textarea-suggestion__label">
|
<div className="textarea-suggestion__label">
|
||||||
<span className="textarea-suggestion__title">{claimTitle || value}</span>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { normalizeURI } from 'util/lbryURI';
|
import { normalizeURI } from 'util/lbryURI';
|
||||||
import { doResolveUri } from 'redux/actions/claims';
|
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';
|
import UriIndicator from './view';
|
||||||
|
|
||||||
const select = (state, props) => {
|
const select = (state, props) => {
|
||||||
|
@ -14,6 +14,7 @@ const select = (state, props) => {
|
||||||
claim: selectClaimForUri(state, props.uri),
|
claim: selectClaimForUri(state, props.uri),
|
||||||
isResolvingUri: selectIsUriResolving(state, props.uri),
|
isResolvingUri: selectIsUriResolving(state, props.uri),
|
||||||
uri,
|
uri,
|
||||||
|
odyseeMembership: selectOdyseeMembershipForUri(state, props.uri),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ import type { Node } from 'react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
|
import PremiumBadge from 'component/common/premium-badge';
|
||||||
import { stripLeadingAtSign } from 'util/string';
|
import { stripLeadingAtSign } from 'util/string';
|
||||||
|
|
||||||
type ChannelInfo = { uri: string, name: string, title: string };
|
type ChannelInfo = { uri: string, name: string, title: string };
|
||||||
|
@ -17,10 +18,13 @@ type Props = {
|
||||||
inline?: boolean,
|
inline?: boolean,
|
||||||
showAtSign?: boolean,
|
showAtSign?: boolean,
|
||||||
className?: string,
|
className?: string,
|
||||||
|
showMemberBadge?: boolean,
|
||||||
children: ?Node, // to allow for other elements to be nested within the UriIndicator (commit: 1e82586f).
|
children: ?Node, // to allow for other elements to be nested within the UriIndicator (commit: 1e82586f).
|
||||||
// --- redux ---
|
// --- redux ---
|
||||||
claim: ?Claim,
|
claim: ?Claim,
|
||||||
isResolvingUri: boolean,
|
isResolvingUri: boolean,
|
||||||
|
odyseeMembership: string,
|
||||||
|
comment?: boolean,
|
||||||
resolveUri: (string) => void,
|
resolveUri: (string) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -90,6 +94,9 @@ class UriIndicator extends React.PureComponent<Props> {
|
||||||
hideAnonymous = false,
|
hideAnonymous = false,
|
||||||
showAtSign,
|
showAtSign,
|
||||||
className,
|
className,
|
||||||
|
odyseeMembership,
|
||||||
|
comment,
|
||||||
|
showMemberBadge = true,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (!channelInfo && !claim) {
|
if (!channelInfo && !claim) {
|
||||||
|
@ -119,7 +126,8 @@ class UriIndicator extends React.PureComponent<Props> {
|
||||||
|
|
||||||
const inner = (
|
const inner = (
|
||||||
<span dir="auto" className={classnames('channel-name', { 'channel-name--inline': inline })}>
|
<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>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { selectClaimForUri, selectIsUriResolving } from 'redux/selectors/claims';
|
import { selectClaimForUri, selectIsUriResolving, selectOdyseeMembershipForUri } from 'redux/selectors/claims';
|
||||||
import WunderbarSuggestion from './view';
|
import WunderbarSuggestion from './view';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => {
|
||||||
claim: selectClaimForUri(state, props.uri),
|
const { uri } = props;
|
||||||
isResolvingUri: selectIsUriResolving(state, props.uri),
|
|
||||||
});
|
return {
|
||||||
|
claim: selectClaimForUri(state, uri),
|
||||||
|
isResolvingUri: selectIsUriResolving(state, uri),
|
||||||
|
odyseeMembershipByUri: selectOdyseeMembershipForUri(state, uri),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export default connect(select)(WunderbarSuggestion);
|
export default connect(select)(WunderbarSuggestion);
|
||||||
|
|
|
@ -6,15 +6,17 @@ import FileThumbnail from 'component/fileThumbnail';
|
||||||
import ChannelThumbnail from 'component/channelThumbnail';
|
import ChannelThumbnail from 'component/channelThumbnail';
|
||||||
import FileProperties from 'component/previewOverlayProperties';
|
import FileProperties from 'component/previewOverlayProperties';
|
||||||
import ClaimProperties from 'component/claimProperties';
|
import ClaimProperties from 'component/claimProperties';
|
||||||
|
import PremiumBadge from 'component/common/premium-badge';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
claim: ?Claim,
|
claim: ?Claim,
|
||||||
uri: string,
|
uri: string,
|
||||||
isResolvingUri: boolean,
|
isResolvingUri: boolean,
|
||||||
|
odyseeMembershipByUri: ?string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function WunderbarSuggestion(props: Props) {
|
export default function WunderbarSuggestion(props: Props) {
|
||||||
const { claim, uri, isResolvingUri } = props;
|
const { claim, uri, isResolvingUri, odyseeMembershipByUri } = props;
|
||||||
|
|
||||||
if (isResolvingUri) {
|
if (isResolvingUri) {
|
||||||
return (
|
return (
|
||||||
|
@ -61,6 +63,7 @@ export default function WunderbarSuggestion(props: Props) {
|
||||||
<div className="wunderbar__suggestion-title">{claim.value.title}</div>
|
<div className="wunderbar__suggestion-title">{claim.value.title}</div>
|
||||||
<div className="wunderbar__suggestion-name">
|
<div className="wunderbar__suggestion-name">
|
||||||
{isChannel ? claim.name : (claim.signing_channel && claim.signing_channel.name) || __('Anonymous')}
|
{isChannel ? claim.name : (claim.signing_channel && claim.signing_channel.name) || __('Anonymous')}
|
||||||
|
<PremiumBadge membership={odyseeMembershipByUri} />
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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_FAILURE = 'USER_SET_REFERRER_FAILURE';
|
||||||
export const USER_SET_REFERRER_RESET = 'USER_SET_REFERRER_RESET';
|
export const USER_SET_REFERRER_RESET = 'USER_SET_REFERRER_RESET';
|
||||||
export const USER_EMAIL_VERIFY_RETRY = 'USER_EMAIL_VERIFY_RETRY';
|
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
|
// Rewards
|
||||||
export const FETCH_REWARDS_STARTED = 'FETCH_REWARDS_STARTED';
|
export const FETCH_REWARDS_STARTED = 'FETCH_REWARDS_STARTED';
|
||||||
|
|
|
@ -178,6 +178,7 @@ export const CONTENT = 'Content';
|
||||||
export const STAR = 'star';
|
export const STAR = 'star';
|
||||||
export const MUSIC = 'MusicCategory';
|
export const MUSIC = 'MusicCategory';
|
||||||
export const BADGE_MOD = 'BadgeMod';
|
export const BADGE_MOD = 'BadgeMod';
|
||||||
|
export const BADGE_ADMIN = 'BadgeAdmin';
|
||||||
export const BADGE_STREAMER = 'BadgeStreamer';
|
export const BADGE_STREAMER = 'BadgeStreamer';
|
||||||
export const REPLAY = 'Replay';
|
export const REPLAY = 'Replay';
|
||||||
export const REPEAT = 'Repeat';
|
export const REPEAT = 'Repeat';
|
||||||
|
@ -195,6 +196,12 @@ export const ODYSEE_LOGO = 'OdyseeLogo';
|
||||||
export const ODYSEE_WHITE_TEXT = 'OdyseeLogoWhiteText';
|
export const ODYSEE_WHITE_TEXT = 'OdyseeLogoWhiteText';
|
||||||
export const ODYSEE_DARK_TEXT = 'OdyseeLogoDarkText';
|
export const ODYSEE_DARK_TEXT = 'OdyseeLogoDarkText';
|
||||||
export const FEATURED = 'Featured';
|
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 DISMISS_ALL = 'DismissAll';
|
||||||
export const SUBMIT = 'Submit';
|
export const SUBMIT = 'Submit';
|
||||||
export const FILTERED_BY_LANG = 'FilteredByLang';
|
export const FILTERED_BY_LANG = 'FilteredByLang';
|
||||||
|
|
|
@ -47,3 +47,5 @@ export const COLLECTION_ADD = 'collection_add';
|
||||||
export const COLLECTION_DELETE = 'collection_delete';
|
export const COLLECTION_DELETE = 'collection_delete';
|
||||||
export const CONFIRM_REMOVE_CARD = 'CONFIRM_REMOVE_CARD';
|
export const CONFIRM_REMOVE_CARD = 'CONFIRM_REMOVE_CARD';
|
||||||
export const CONFIRM_REMOVE_COMMENT = 'CONFIRM_REMOVE_COMMENT';
|
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.LIVESTREAM_CURRENT = 'live';
|
||||||
exports.GENERAL = 'general';
|
exports.GENERAL = 'general';
|
||||||
exports.LIST = 'list';
|
exports.LIST = 'list';
|
||||||
|
exports.ODYSEE_MEMBERSHIP = 'membership';
|
||||||
exports.POPOUT = 'popout';
|
exports.POPOUT = 'popout';
|
||||||
|
|
|
@ -1,10 +1,17 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { useState, useEffect } from 'react';
|
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(
|
export default function useFetchViewCount(
|
||||||
shouldFetch: ?boolean,
|
shouldFetch: ?boolean,
|
||||||
uris: Array<string>,
|
uris: Array<string>,
|
||||||
claimsByUri: any,
|
claimsByUri: {},
|
||||||
doFetchViewCount: (string) => void
|
doFetchViewCount: (string) => void
|
||||||
) {
|
) {
|
||||||
const [fetchedUris, setFetchedUris] = useState([]);
|
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" */)
|
import('modal/modalPublishPreview' /* webpackChunkName: "modalPublishPreview" */)
|
||||||
);
|
);
|
||||||
const ModalRemoveCard = lazyImport(() => import('modal/modalRemoveCard' /* webpackChunkName: "modalRemoveCard" */));
|
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(() =>
|
const ModalRemoveComment = lazyImport(() =>
|
||||||
import('modal/modalRemoveComment' /* webpackChunkName: "modalRemoveComment" */)
|
import('modal/modalRemoveComment' /* webpackChunkName: "modalRemoveComment" */)
|
||||||
);
|
);
|
||||||
|
@ -126,6 +132,8 @@ function getModal(id) {
|
||||||
return ModalPhoneCollection;
|
return ModalPhoneCollection;
|
||||||
case MODALS.SEND_TIP:
|
case MODALS.SEND_TIP:
|
||||||
return ModalSendTip;
|
return ModalSendTip;
|
||||||
|
case MODALS.MEMBERSHIP_SPLASH:
|
||||||
|
return OdyseeMembershipSplash;
|
||||||
case MODALS.SOCIAL_SHARE:
|
case MODALS.SOCIAL_SHARE:
|
||||||
return ModalSocialShare;
|
return ModalSocialShare;
|
||||||
case MODALS.PUBLISH:
|
case MODALS.PUBLISH:
|
||||||
|
@ -180,6 +188,8 @@ function getModal(id) {
|
||||||
return ModalDeleteCollection;
|
return ModalDeleteCollection;
|
||||||
case MODALS.CONFIRM_REMOVE_CARD:
|
case MODALS.CONFIRM_REMOVE_CARD:
|
||||||
return ModalRemoveCard;
|
return ModalRemoveCard;
|
||||||
|
case MODALS.CONFIRM_ODYSEE_MEMBERSHIP:
|
||||||
|
return ModalConfirmOdyseeMembership;
|
||||||
case MODALS.CONFIRM_REMOVE_COMMENT:
|
case MODALS.CONFIRM_REMOVE_COMMENT:
|
||||||
return ModalRemoveComment;
|
return ModalRemoveComment;
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -237,7 +237,14 @@ function ChannelPage(props: Props) {
|
||||||
{cover && <img className={classnames('channel-cover__custom')} src={PlaceholderTx} />}
|
{cover && <img className={classnames('channel-cover__custom')} src={PlaceholderTx} />}
|
||||||
{cover && <OptimizedImage className={classnames('channel-cover__custom')} src={cover} objectFit="cover" />}
|
{cover && <OptimizedImage className={classnames('channel-cover__custom')} src={cover} objectFit="cover" />}
|
||||||
<div className="channel__primary-info">
|
<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">
|
<h1 className="channel__title">
|
||||||
<TruncatedText lines={2} showTooltip>
|
<TruncatedText lines={2} showTooltip>
|
||||||
{title || (channelName && '@' + channelName)}
|
{title || (channelName && '@' + channelName)}
|
||||||
|
|
|
@ -4,7 +4,9 @@ import {
|
||||||
selectFetchingMyChannels,
|
selectFetchingMyChannels,
|
||||||
makeSelectClaimIsPending,
|
makeSelectClaimIsPending,
|
||||||
selectPendingIds,
|
selectPendingIds,
|
||||||
|
selectClaimsByUri,
|
||||||
} from 'redux/selectors/claims';
|
} from 'redux/selectors/claims';
|
||||||
|
import { doFetchUserMemberships } from 'redux/actions/user';
|
||||||
import { doFetchChannelListMine } from 'redux/actions/claims';
|
import { doFetchChannelListMine } from 'redux/actions/claims';
|
||||||
import { doSetActiveChannel } from 'redux/actions/app';
|
import { doSetActiveChannel } from 'redux/actions/app';
|
||||||
import { selectYoutubeChannels } from 'redux/selectors/user';
|
import { selectYoutubeChannels } from 'redux/selectors/user';
|
||||||
|
@ -30,12 +32,14 @@ const select = (state) => {
|
||||||
fetchingChannels: selectFetchingMyChannels(state),
|
fetchingChannels: selectFetchingMyChannels(state),
|
||||||
youtubeChannels: selectYoutubeChannels(state),
|
youtubeChannels: selectYoutubeChannels(state),
|
||||||
pendingChannels,
|
pendingChannels,
|
||||||
|
claimsByUri: selectClaimsByUri(state),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const perform = (dispatch) => ({
|
const perform = (dispatch) => ({
|
||||||
fetchChannelListMine: () => dispatch(doFetchChannelListMine()),
|
fetchChannelListMine: () => dispatch(doFetchChannelListMine()),
|
||||||
doSetActiveChannel: (claimId) => dispatch(doSetActiveChannel(claimId)),
|
doSetActiveChannel: (claimId) => dispatch(doSetActiveChannel(claimId)),
|
||||||
|
doFetchUserMemberships: (claimIds) => dispatch(doFetchUserMemberships(claimIds)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, perform)(ChannelsPage);
|
export default connect(select, perform)(ChannelsPage);
|
||||||
|
|
|
@ -12,6 +12,7 @@ import LbcSymbol from 'component/common/lbc-symbol';
|
||||||
import * as PAGES from 'constants/pages';
|
import * as PAGES from 'constants/pages';
|
||||||
import HelpLink from 'component/common/help-link';
|
import HelpLink from 'component/common/help-link';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
|
import useGetUserMemberships from 'effects/use-get-user-memberships';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
channelUrls: Array<string>,
|
channelUrls: Array<string>,
|
||||||
|
@ -20,6 +21,8 @@ type Props = {
|
||||||
youtubeChannels: ?Array<any>,
|
youtubeChannels: ?Array<any>,
|
||||||
doSetActiveChannel: (string) => void,
|
doSetActiveChannel: (string) => void,
|
||||||
pendingChannels: Array<string>,
|
pendingChannels: Array<string>,
|
||||||
|
claimsByUri: { [string]: any },
|
||||||
|
doFetchUserMemberships: (claimIdCsv: string) => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ChannelsPage(props: Props) {
|
export default function ChannelsPage(props: Props) {
|
||||||
|
@ -30,10 +33,15 @@ export default function ChannelsPage(props: Props) {
|
||||||
youtubeChannels,
|
youtubeChannels,
|
||||||
doSetActiveChannel,
|
doSetActiveChannel,
|
||||||
pendingChannels,
|
pendingChannels,
|
||||||
|
claimsByUri,
|
||||||
|
doFetchUserMemberships,
|
||||||
} = props;
|
} = props;
|
||||||
const [rewardData, setRewardData] = React.useState();
|
const [rewardData, setRewardData] = React.useState();
|
||||||
const hasYoutubeChannels = youtubeChannels && Boolean(youtubeChannels.length);
|
const hasYoutubeChannels = youtubeChannels && Boolean(youtubeChannels.length);
|
||||||
|
|
||||||
|
const shouldFetchUserMemberships = true;
|
||||||
|
useGetUserMemberships(shouldFetchUserMemberships, channelUrls, claimsByUri, doFetchUserMemberships);
|
||||||
|
|
||||||
const { push } = useHistory();
|
const { push } = useHistory();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -51,6 +59,7 @@ export default function ChannelsPage(props: Props) {
|
||||||
|
|
||||||
{channelUrls && Boolean(channelUrls.length) && (
|
{channelUrls && Boolean(channelUrls.length) && (
|
||||||
<ClaimList
|
<ClaimList
|
||||||
|
showMemberBadge
|
||||||
header={<h1 className="section__title">{__('Your channels')}</h1>}
|
header={<h1 className="section__title">{__('Your channels')}</h1>}
|
||||||
headerAltControls={
|
headerAltControls={
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { makeSelectClaimForUri } from 'redux/selectors/claims';
|
||||||
import * as SETTINGS from 'constants/settings';
|
import * as SETTINGS from 'constants/settings';
|
||||||
import { doFetchActiveLivestreams } from 'redux/actions/livestream';
|
import { doFetchActiveLivestreams } from 'redux/actions/livestream';
|
||||||
import { selectActiveLivestreams } from 'redux/selectors/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 { selectFollowedTags } from 'redux/selectors/tags';
|
||||||
import { doToggleTagFollowDesktop } from 'redux/actions/tags';
|
import { doToggleTagFollowDesktop } from 'redux/actions/tags';
|
||||||
import { selectClientSetting, selectLanguage } from 'redux/selectors/settings';
|
import { selectClientSetting, selectLanguage } from 'redux/selectors/settings';
|
||||||
|
@ -20,9 +20,9 @@ const select = (state, props) => {
|
||||||
followedTags: selectFollowedTags(state),
|
followedTags: selectFollowedTags(state),
|
||||||
repostedUri: repostedUri,
|
repostedUri: repostedUri,
|
||||||
repostedClaim: repostedUri ? makeSelectClaimForUri(repostedUri)(state) : null,
|
repostedClaim: repostedUri ? makeSelectClaimForUri(repostedUri)(state) : null,
|
||||||
isAuthenticated: selectUserVerifiedEmail(state),
|
|
||||||
tileLayout: selectClientSetting(state, SETTINGS.TILE_LAYOUT),
|
tileLayout: selectClientSetting(state, SETTINGS.TILE_LAYOUT),
|
||||||
activeLivestreams: selectActiveLivestreams(state),
|
activeLivestreams: selectActiveLivestreams(state),
|
||||||
|
userHasPremiumPlus: selectOdyseeMembershipIsPremiumPlus(state),
|
||||||
languageSetting: selectLanguage(state),
|
languageSetting: selectLanguage(state),
|
||||||
searchInLanguage: selectClientSetting(state, SETTINGS.SEARCH_IN_LANGUAGE),
|
searchInLanguage: selectClientSetting(state, SETTINGS.SEARCH_IN_LANGUAGE),
|
||||||
};
|
};
|
||||||
|
|
|
@ -28,10 +28,10 @@ type Props = {
|
||||||
searchInLanguage: boolean,
|
searchInLanguage: boolean,
|
||||||
doToggleTagFollowDesktop: (string) => void,
|
doToggleTagFollowDesktop: (string) => void,
|
||||||
doResolveUri: (string) => void,
|
doResolveUri: (string) => void,
|
||||||
isAuthenticated: boolean,
|
|
||||||
tileLayout: boolean,
|
tileLayout: boolean,
|
||||||
activeLivestreams: ?LivestreamInfo,
|
activeLivestreams: ?LivestreamInfo,
|
||||||
doFetchActiveLivestreams: (orderBy: ?Array<string>, lang: ?Array<string>) => void,
|
doFetchActiveLivestreams: (orderBy: ?Array<string>, lang: ?Array<string>) => void,
|
||||||
|
userHasPremiumPlus: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
function DiscoverPage(props: Props) {
|
function DiscoverPage(props: Props) {
|
||||||
|
@ -44,11 +44,11 @@ function DiscoverPage(props: Props) {
|
||||||
searchInLanguage,
|
searchInLanguage,
|
||||||
doToggleTagFollowDesktop,
|
doToggleTagFollowDesktop,
|
||||||
doResolveUri,
|
doResolveUri,
|
||||||
isAuthenticated,
|
|
||||||
tileLayout,
|
tileLayout,
|
||||||
activeLivestreams,
|
activeLivestreams,
|
||||||
doFetchActiveLivestreams,
|
doFetchActiveLivestreams,
|
||||||
dynamicRouteProps,
|
dynamicRouteProps,
|
||||||
|
userHasPremiumPlus,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const buttonRef = useRef();
|
const buttonRef = useRef();
|
||||||
|
@ -191,7 +191,7 @@ function DiscoverPage(props: Props) {
|
||||||
hiddenNsfwMessage={<HiddenNsfw type="page" />}
|
hiddenNsfwMessage={<HiddenNsfw type="page" />}
|
||||||
repostedClaimId={repostedClaim ? repostedClaim.claim_id : null}
|
repostedClaimId={repostedClaim ? repostedClaim.claim_id : null}
|
||||||
injectedItem={
|
injectedItem={
|
||||||
SHOW_ADS && !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
|
// Assume wild west page if no dynamicRouteProps
|
||||||
// Not a very good solution, but just doing it for now
|
// Not a very good solution, but just doing it for now
|
||||||
|
|
|
@ -3,7 +3,7 @@ import * as SETTINGS from 'constants/settings';
|
||||||
import { doFetchActiveLivestreams } from 'redux/actions/livestream';
|
import { doFetchActiveLivestreams } from 'redux/actions/livestream';
|
||||||
import { selectActiveLivestreams, selectFetchingActiveLivestreams } from 'redux/selectors/livestream';
|
import { selectActiveLivestreams, selectFetchingActiveLivestreams } from 'redux/selectors/livestream';
|
||||||
import { selectFollowedTags } from 'redux/selectors/tags';
|
import { selectFollowedTags } from 'redux/selectors/tags';
|
||||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
import { selectUserVerifiedEmail, selectOdyseeMembershipIsPremiumPlus } from 'redux/selectors/user';
|
||||||
import { selectSubscriptions } from 'redux/selectors/subscriptions';
|
import { selectSubscriptions } from 'redux/selectors/subscriptions';
|
||||||
import { selectShowMatureContent, selectHomepageData, selectClientSetting } from 'redux/selectors/settings';
|
import { selectShowMatureContent, selectHomepageData, selectClientSetting } from 'redux/selectors/settings';
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ const select = (state) => ({
|
||||||
activeLivestreams: selectActiveLivestreams(state),
|
activeLivestreams: selectActiveLivestreams(state),
|
||||||
fetchingActiveLivestreams: selectFetchingActiveLivestreams(state),
|
fetchingActiveLivestreams: selectFetchingActiveLivestreams(state),
|
||||||
hideScheduledLivestreams: selectClientSetting(state, SETTINGS.HIDE_SCHEDULED_LIVESTREAMS),
|
hideScheduledLivestreams: selectClientSetting(state, SETTINGS.HIDE_SCHEDULED_LIVESTREAMS),
|
||||||
|
userHasPremiumPlus: selectOdyseeMembershipIsPremiumPlus(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = (dispatch) => ({
|
const perform = (dispatch) => ({
|
||||||
|
|
|
@ -31,6 +31,7 @@ type Props = {
|
||||||
doFetchActiveLivestreams: () => void,
|
doFetchActiveLivestreams: () => void,
|
||||||
fetchingActiveLivestreams: boolean,
|
fetchingActiveLivestreams: boolean,
|
||||||
hideScheduledLivestreams: boolean,
|
hideScheduledLivestreams: boolean,
|
||||||
|
userHasPremiumPlus: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
function HomePage(props: Props) {
|
function HomePage(props: Props) {
|
||||||
|
@ -44,6 +45,7 @@ function HomePage(props: Props) {
|
||||||
doFetchActiveLivestreams,
|
doFetchActiveLivestreams,
|
||||||
fetchingActiveLivestreams,
|
fetchingActiveLivestreams,
|
||||||
hideScheduledLivestreams,
|
hideScheduledLivestreams,
|
||||||
|
userHasPremiumPlus,
|
||||||
} = props;
|
} = props;
|
||||||
const showPersonalizedChannels = (authenticated || !IS_WEB) && subscribedChannels && subscribedChannels.length > 0;
|
const showPersonalizedChannels = (authenticated || !IS_WEB) && subscribedChannels && subscribedChannels.length > 0;
|
||||||
const showPersonalizedTags = (authenticated || !IS_WEB) && followedTags && followedTags.length > 0;
|
const showPersonalizedTags = (authenticated || !IS_WEB) && followedTags && followedTags.length > 0;
|
||||||
|
@ -100,7 +102,9 @@ function HomePage(props: Props) {
|
||||||
prefixUris={getLivestreamUris(activeLivestreams, options.channelIds)}
|
prefixUris={getLivestreamUris(activeLivestreams, options.channelIds)}
|
||||||
pinUrls={pinUrls}
|
pinUrls={pinUrls}
|
||||||
injectedItem={
|
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 { connect } from 'react-redux';
|
||||||
import { selectHasChannels, selectFetchingMyChannels } from 'redux/selectors/claims';
|
import { selectHasChannels, selectFetchingMyChannels } from 'redux/selectors/claims';
|
||||||
import { doClearPublish } from 'redux/actions/publish';
|
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 { doFetchNoSourceClaims } from 'redux/actions/livestream';
|
||||||
|
import { selectUser, selectOdyseeMembershipName } from 'redux/selectors/user';
|
||||||
import {
|
import {
|
||||||
makeSelectPendingLivestreamsForChannelId,
|
makeSelectPendingLivestreamsForChannelId,
|
||||||
makeSelectLivestreamsForChannelId,
|
makeSelectLivestreamsForChannelId,
|
||||||
|
@ -23,6 +24,9 @@ const select = (state) => {
|
||||||
myLivestreamClaims: makeSelectLivestreamsForChannelId(channelId)(state),
|
myLivestreamClaims: makeSelectLivestreamsForChannelId(channelId)(state),
|
||||||
pendingClaims: makeSelectPendingLivestreamsForChannelId(channelId)(state),
|
pendingClaims: makeSelectPendingLivestreamsForChannelId(channelId)(state),
|
||||||
fetchingLivestreams: makeSelectIsFetchingLivestreams(channelId)(state),
|
fetchingLivestreams: makeSelectIsFetchingLivestreams(channelId)(state),
|
||||||
|
user: selectUser(state),
|
||||||
|
odyseeMembership: selectOdyseeMembershipName(state),
|
||||||
|
activeChannelStakedLevel: selectActiveChannelStakedLevel(state),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const perform = (dispatch) => ({
|
const perform = (dispatch) => ({
|
||||||
|
|
|
@ -17,6 +17,7 @@ import Card from 'component/common/card';
|
||||||
import ClaimList from 'component/claimList';
|
import ClaimList from 'component/claimList';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
import { LIVESTREAM_RTMP_URL } from 'constants/livestream';
|
import { LIVESTREAM_RTMP_URL } from 'constants/livestream';
|
||||||
|
import { ENABLE_NO_SOURCE_CLAIMS, CHANNEL_STAKED_LEVEL_LIVESTREAM } from '../../../config';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
hasChannels: boolean,
|
hasChannels: boolean,
|
||||||
|
@ -29,6 +30,9 @@ type Props = {
|
||||||
fetchingLivestreams: boolean,
|
fetchingLivestreams: boolean,
|
||||||
channelId: ?string,
|
channelId: ?string,
|
||||||
channelName: ?string,
|
channelName: ?string,
|
||||||
|
user: ?User,
|
||||||
|
activeChannelStakedLevel: number,
|
||||||
|
odyseeMembership: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function LivestreamSetupPage(props: Props) {
|
export default function LivestreamSetupPage(props: Props) {
|
||||||
|
@ -44,6 +48,9 @@ export default function LivestreamSetupPage(props: Props) {
|
||||||
fetchingLivestreams,
|
fetchingLivestreams,
|
||||||
channelId,
|
channelId,
|
||||||
channelName,
|
channelName,
|
||||||
|
user,
|
||||||
|
odyseeMembership,
|
||||||
|
activeChannelStakedLevel,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const [sigData, setSigData] = React.useState({ signature: undefined, signing_ts: undefined });
|
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 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() {
|
function createStreamKey() {
|
||||||
if (!channelId || !channelName || !sigData.signature || !sigData.signing_ts) return null;
|
if (!channelId || !channelName || !sigData.signature || !sigData.signing_ts) return null;
|
||||||
return `${channelId}?d=${toHex(channelName)}&s=${sigData.signature}&t=${sigData.signing_ts}`;
|
return `${channelId}?d=${toHex(channelName)}&s=${sigData.signature}&t=${sigData.signing_ts}`;
|
||||||
|
@ -169,201 +192,231 @@ export default function LivestreamSetupPage(props: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
{fetchingChannels && (
|
{/* no livestreaming privs because no premium membership */}
|
||||||
<div className="main--empty">
|
{!livestreamEnabled && !odyseeMembership && (
|
||||||
<Spinner delayed />
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!fetchingChannels && !hasChannels && (
|
{/* show livestreaming frontend */}
|
||||||
<Yrbl
|
{livestreamEnabled && (
|
||||||
type="happy"
|
<div className="card-stack">
|
||||||
title={__("You haven't created a channel yet, let's fix that!")}
|
{/* getting channel data */}
|
||||||
actions={
|
{fetchingChannels && (
|
||||||
<div className="section__actions">
|
<div className="main--empty">
|
||||||
<Button button="primary" navigate={`/$/${PAGES.CHANNEL_NEW}`} label={__('Create A Channel')} />
|
<Spinner delayed />
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{!fetchingChannels && (
|
|
||||||
<>
|
|
||||||
<div className="section__actions--between">
|
|
||||||
<ChannelSelector hideAnon />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{fetchingLivestreams && !fetchingChannels && !hasLivestreamClaims && (
|
{/* no channels yet */}
|
||||||
<div className="main--empty">
|
{!fetchingChannels && !hasChannels && (
|
||||||
<Spinner delayed />
|
<Yrbl
|
||||||
</div>
|
type="happy"
|
||||||
)}
|
title={__("You haven't created a channel yet, let's fix that!")}
|
||||||
<div className="card-stack">
|
actions={
|
||||||
{!fetchingChannels && channelId && (
|
<div className="section__actions">
|
||||||
<>
|
<Button button="primary" navigate={`/$/${PAGES.CHANNEL_NEW}`} label={__('Create A Channel')} />
|
||||||
<Card
|
</div>
|
||||||
titleActions={
|
|
||||||
<Button button="close" icon={showHelp ? ICONS.UP : ICONS.DOWN} onClick={() => setShowHelp(!showHelp)} />
|
|
||||||
}
|
}
|
||||||
title={__('Go Live on Odysee')}
|
|
||||||
subtitle={<>{__(`You're invited to try out our new livestreaming service while in beta!`)} </>}
|
|
||||||
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 ? (
|
{/* channel selector */}
|
||||||
<>
|
{!fetchingChannels && (
|
||||||
{Boolean(pendingClaims.length) && (
|
<>
|
||||||
<div className="section">
|
<div className="section__actions--between">
|
||||||
<ClaimList
|
<ChannelSelector hideAnon />
|
||||||
header={__('Your pending livestreams uploads')}
|
</div>
|
||||||
uris={pendingClaims.map((claim) => claim.permanent_url)}
|
</>
|
||||||
/>
|
)}
|
||||||
</div>
|
|
||||||
)}
|
{/* getting livestreams */}
|
||||||
{Boolean(myLivestreamClaims.length) && (
|
{fetchingLivestreams && !fetchingChannels && !hasLivestreamClaims && (
|
||||||
<>
|
<div className="main--empty">
|
||||||
{Boolean(upcomingStreams.length) && (
|
<Spinner delayed />
|
||||||
<div className="section">
|
</div>
|
||||||
<ClaimList
|
)}
|
||||||
header={<ListHeader title={__('Your Scheduled Livestreams')} />}
|
|
||||||
uris={upcomingStreams.map((claim) => claim.permanent_url)}
|
{!fetchingChannels && channelId && (
|
||||||
/>
|
<>
|
||||||
</div>
|
<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">
|
<div className="section">
|
||||||
<ClaimList
|
<ClaimList
|
||||||
header={
|
header={__('Your pending livestreams uploads')}
|
||||||
<ListHeader title={__('Your Past Livestreams')} hideBtn={Boolean(upcomingStreams.length)} />
|
uris={pendingClaims.map((claim) => claim.permanent_url)}
|
||||||
}
|
|
||||||
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>
|
</div>
|
||||||
</>
|
)}
|
||||||
)}
|
{Boolean(myLivestreamClaims.length) && (
|
||||||
</>
|
<>
|
||||||
) : (
|
{Boolean(upcomingStreams.length) && (
|
||||||
<Yrbl
|
<div className="section">
|
||||||
className="livestream__publish-intro"
|
<ClaimList
|
||||||
title={__('No livestream publishes found')}
|
header={<ListHeader title={__('Your Scheduled Livestreams')} />}
|
||||||
subtitle={__(
|
uris={upcomingStreams.map((claim) => claim.permanent_url)}
|
||||||
'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.'
|
/>
|
||||||
)}
|
</div>
|
||||||
actions={
|
)}
|
||||||
<div className="section__actions">
|
<div className="section">
|
||||||
<Button
|
<ClaimList
|
||||||
button="primary"
|
header={
|
||||||
onClick={() =>
|
<ListHeader title={__('Your Past Livestreams')} hideBtn={Boolean(upcomingStreams.length)} />
|
||||||
doNewLivestream(`/$/${PAGES.UPLOAD}?type=${PUBLISH_MODES.LIVESTREAM.toLowerCase()}`)
|
}
|
||||||
}
|
empty={
|
||||||
label={__('Create A Livestream')}
|
<I18nMessage
|
||||||
/>
|
tokens={{
|
||||||
<Button
|
check_again: (
|
||||||
button="alt"
|
<Button
|
||||||
onClick={() => {
|
button="link"
|
||||||
fetchNoSourceClaims(channelId);
|
onClick={() => fetchNoSourceClaims(channelId)}
|
||||||
}}
|
label={__('Check again')}
|
||||||
label={__('Check again...')}
|
/>
|
||||||
/>
|
),
|
||||||
</div>
|
}}
|
||||||
}
|
>
|
||||||
/>
|
Nothing here yet. %check_again%
|
||||||
)}
|
</I18nMessage>
|
||||||
|
}
|
||||||
{/* Debug Stuff */}
|
uris={pastStreams.map((claim) => claim.permanent_url)}
|
||||||
{streamKey && false && activeChannelClaim && (
|
/>
|
||||||
<div style={{ marginTop: 'var(--spacing-l)' }}>
|
</div>
|
||||||
<h3>Debug Info</h3>
|
</>
|
||||||
|
)}
|
||||||
{/* Channel ID */}
|
</>
|
||||||
<FormField
|
) : (
|
||||||
name={'channelId'}
|
<Yrbl
|
||||||
label={'Channel ID'}
|
className="livestream__publish-intro"
|
||||||
type={'text'}
|
title={__('No livestream publishes found')}
|
||||||
defaultValue={activeChannelClaim.claim_id}
|
subtitle={__(
|
||||||
readOnly
|
'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 */}
|
{/* Debug Stuff */}
|
||||||
<FormField
|
{streamKey && false && activeChannelClaim && (
|
||||||
name={'signature'}
|
<div style={{ marginTop: 'var(--spacing-l)' }}>
|
||||||
label={'Signature'}
|
<h3>Debug Info</h3>
|
||||||
type={'text'}
|
|
||||||
defaultValue={sigData.signature}
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Signature TS */}
|
{/* Channel ID */}
|
||||||
<FormField
|
<FormField
|
||||||
name={'signaturets'}
|
name={'channelId'}
|
||||||
label={'Signature Timestamp'}
|
label={'Channel ID'}
|
||||||
type={'text'}
|
type={'text'}
|
||||||
defaultValue={sigData.signing_ts}
|
defaultValue={activeChannelClaim.claim_id}
|
||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Hex Data */}
|
{/* Signature */}
|
||||||
<FormField
|
<FormField
|
||||||
name={'datahex'}
|
name={'signature'}
|
||||||
label={'Hex Data'}
|
label={'Signature'}
|
||||||
type={'text'}
|
type={'text'}
|
||||||
defaultValue={toHex(activeChannelClaim.name)}
|
defaultValue={sigData.signature}
|
||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Channel Public Key */}
|
{/* Signature TS */}
|
||||||
<FormField
|
<FormField
|
||||||
name={'channelpublickey'}
|
name={'signaturets'}
|
||||||
label={'Public Key'}
|
label={'Signature Timestamp'}
|
||||||
type={'text'}
|
type={'text'}
|
||||||
defaultValue={activeChannelClaim.value.public_key}
|
defaultValue={sigData.signing_ts}
|
||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
)}
|
{/* Hex Data */}
|
||||||
</>
|
<FormField
|
||||||
)}
|
name={'datahex'}
|
||||||
</div>
|
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>
|
</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';
|
} from 'redux/selectors/file_info';
|
||||||
|
|
||||||
type Dispatch = (action: any) => any;
|
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) {
|
export function doOpenFileInFolder(path: string) {
|
||||||
return () => {
|
return () => {
|
||||||
shell.showItemInFolder(path);
|
shell.showItemInFolder(path);
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { SEARCH_SERVER_API, SEARCH_SERVER_API_ALT } from 'config';
|
||||||
import { SEARCH_OPTIONS } from 'constants/search';
|
import { SEARCH_OPTIONS } from 'constants/search';
|
||||||
|
|
||||||
type Dispatch = (action: any) => any;
|
type Dispatch = (action: any) => any;
|
||||||
type GetState = () => { claims: any, search: SearchState };
|
type GetState = () => { claims: any, search: SearchState, user: User };
|
||||||
|
|
||||||
type SearchOptions = {
|
type SearchOptions = {
|
||||||
size?: number,
|
size?: number,
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { makeSelectClaimForUri } from 'redux/selectors/claims';
|
||||||
import { doFetchChannelListMine } from 'redux/actions/claims';
|
import { doFetchChannelListMine } from 'redux/actions/claims';
|
||||||
import { isURIValid, normalizeURI } from 'util/lbryURI';
|
import { isURIValid, normalizeURI } from 'util/lbryURI';
|
||||||
import { batchActions } from 'util/batch-actions';
|
import { batchActions } from 'util/batch-actions';
|
||||||
|
import { getStripeEnvironment } from 'util/stripe';
|
||||||
|
|
||||||
import * as ACTIONS from 'constants/action_types';
|
import * as ACTIONS from 'constants/action_types';
|
||||||
import { doClaimRewardType, doRewardList } from 'redux/actions/rewards';
|
import { doClaimRewardType, doRewardList } from 'redux/actions/rewards';
|
||||||
|
@ -16,6 +17,9 @@ const AUTH_IN_PROGRESS = 'authInProgress';
|
||||||
export let sessionStorageAvailable = false;
|
export let sessionStorageAvailable = false;
|
||||||
const CHECK_INTERVAL = 200;
|
const CHECK_INTERVAL = 200;
|
||||||
const AUTH_WAIT_TIMEOUT = 10000;
|
const AUTH_WAIT_TIMEOUT = 10000;
|
||||||
|
const stripeEnvironment = getStripeEnvironment();
|
||||||
|
|
||||||
|
const ODYSEE_CHANNEL_ID = '80d2590ad04e36fb1d077a9b9e3a8bba76defdf8';
|
||||||
|
|
||||||
export function doFetchInviteStatus(shouldCallRewardList = true) {
|
export function doFetchInviteStatus(shouldCallRewardList = true) {
|
||||||
return (dispatch) => {
|
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?
|
// TODO: Call doInstallNew separately so we don't have to pass appVersion and os_system params?
|
||||||
export function doAuthenticate(
|
export function doAuthenticate(
|
||||||
appVersion,
|
appVersion,
|
||||||
|
@ -124,6 +181,11 @@ export function doAuthenticate(
|
||||||
data: { user, accessToken: token },
|
data: { user, accessToken: token },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// if user is an Odysee member, get the membership details
|
||||||
|
if (user.odysee_member) {
|
||||||
|
dispatch(doCheckUserOdyseeMemberships(user));
|
||||||
|
}
|
||||||
|
|
||||||
if (shareUsageData) {
|
if (shareUsageData) {
|
||||||
dispatch(doRewardList());
|
dispatch(doRewardList());
|
||||||
|
|
||||||
|
@ -153,6 +215,11 @@ export function doUserFetch() {
|
||||||
|
|
||||||
Lbryio.getCurrentUser()
|
Lbryio.getCurrentUser()
|
||||||
.then((user) => {
|
.then((user) => {
|
||||||
|
// get user membership status
|
||||||
|
if (user.odysee_member) {
|
||||||
|
dispatch(doCheckUserOdyseeMemberships(user));
|
||||||
|
}
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.USER_FETCH_SUCCESS,
|
type: ACTIONS.USER_FETCH_SUCCESS,
|
||||||
data: { user },
|
data: { user },
|
||||||
|
@ -174,6 +241,11 @@ export function doUserCheckEmailVerified() {
|
||||||
return (dispatch) => {
|
return (dispatch) => {
|
||||||
Lbryio.getCurrentUser().then((user) => {
|
Lbryio.getCurrentUser().then((user) => {
|
||||||
if (user.has_verified_email) {
|
if (user.has_verified_email) {
|
||||||
|
// check premium membership
|
||||||
|
if (user.odysee_member) {
|
||||||
|
dispatch(doCheckUserOdyseeMemberships(user));
|
||||||
|
}
|
||||||
|
|
||||||
dispatch(doRewardList());
|
dispatch(doRewardList());
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
|
@ -347,16 +419,19 @@ export function doUserCheckIfEmailExists(email) {
|
||||||
|
|
||||||
Lbryio.call('user', 'exists', { email }, 'post')
|
Lbryio.call('user', 'exists', { email }, 'post')
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
// no email
|
||||||
if (error.response && error.response.status === 404) {
|
if (error.response && error.response.status === 404) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.USER_EMAIL_NEW_DOES_NOT_EXIST,
|
type: ACTIONS.USER_EMAIL_NEW_DOES_NOT_EXIST,
|
||||||
});
|
});
|
||||||
|
// sign in by email
|
||||||
} else if (error.response && error.response.status === 412) {
|
} else if (error.response && error.response.status === 412) {
|
||||||
triggerEmailFlow(false);
|
triggerEmailFlow(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
})
|
})
|
||||||
|
// sign the user in
|
||||||
.then(success, failure);
|
.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,
|
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) {
|
export default function userReducer(state = defaultState, action) {
|
||||||
const handler = reducers[action.type];
|
const handler = reducers[action.type];
|
||||||
if (handler) return handler(state, action);
|
if (handler) return handler(state, action);
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { isClaimNsfw, filterClaims, getChannelIdFromClaim, isStreamPlaceholderCl
|
||||||
import * as CLAIM from 'constants/claim';
|
import * as CLAIM from 'constants/claim';
|
||||||
import { INTERNAL_TAGS } from 'constants/tags';
|
import { INTERNAL_TAGS } from 'constants/tags';
|
||||||
|
|
||||||
type State = { claims: any };
|
type State = { claims: any, user: User };
|
||||||
|
|
||||||
const selectState = (state: State) => state.claims || {};
|
const selectState = (state: State) => state.claims || {};
|
||||||
|
|
||||||
|
@ -791,3 +791,41 @@ export const selectIsMyChannelCountOverLimit = createSelector(
|
||||||
return false;
|
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 { selectSubscriptionUris } from 'redux/selectors/subscriptions';
|
||||||
import { getCommentsListTitle } from 'util/comments';
|
import { getCommentsListTitle } from 'util/comments';
|
||||||
|
|
||||||
type State = { claims: any, comments: CommentsState };
|
type State = { claims: any, comments: CommentsState, user: User };
|
||||||
|
|
||||||
const selectState = (state) => state.comments || {};
|
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 RECENT_HISTORY_AMOUNT = 10;
|
||||||
const HISTORY_ITEMS_PER_PAGE = 50;
|
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 || {};
|
export const selectState = (state: State) => state.content || {};
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ import { selectHistory } from 'redux/selectors/content';
|
||||||
import { selectAllCostInfoByUri } from 'lbryinc';
|
import { selectAllCostInfoByUri } from 'lbryinc';
|
||||||
import { SIMPLE_SITE } from 'config';
|
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;
|
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 selectSetReferrerPending = (state) => selectState(state).referrerSetIsPending;
|
||||||
export const selectSetReferrerError = (state) => selectState(state).referrerSetError;
|
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) => {
|
export const selectYouTubeImportVideosComplete = createSelector(selectState, (state) => {
|
||||||
const total = state.youtubeChannelImportTotal;
|
const total = state.youtubeChannelImportTotal;
|
||||||
const complete = state.youtubeChannelImportComplete || 0;
|
const complete = state.youtubeChannelImportComplete || 0;
|
||||||
|
|
|
@ -36,6 +36,7 @@
|
||||||
@import 'component/markdown-editor';
|
@import 'component/markdown-editor';
|
||||||
@import 'component/markdown-preview';
|
@import 'component/markdown-preview';
|
||||||
@import 'component/media';
|
@import 'component/media';
|
||||||
|
@import 'component/membership';
|
||||||
@import 'component/menu-button';
|
@import 'component/menu-button';
|
||||||
@import 'component/modal';
|
@import 'component/modal';
|
||||||
@import 'component/nag';
|
@import 'component/nag';
|
||||||
|
|
|
@ -285,12 +285,27 @@ a.button--alt {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
|
.comment__badge {
|
||||||
|
padding-right: 0px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
height: 1.2rem;
|
||||||
|
width: 1.2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.channel-name {
|
.channel-name {
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
font-size: var(--font-xsmall);
|
font-size: var(--font-xsmall);
|
||||||
color: rgba(var(--color-text-base), 0.6);
|
color: rgba(var(--color-text-base), 0.6);
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin-left: var(--spacing-xxs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-preview & {
|
.markdown-preview & {
|
||||||
|
|
|
@ -329,9 +329,10 @@
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (min-width: $breakpoint-small) {
|
@media (max-width: $breakpoint-small) {
|
||||||
// padding: var(--spacing-s);
|
.button--close {
|
||||||
// padding-right: 0px;
|
top: calc(var(--spacing-m) * -1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -199,6 +199,37 @@ $actions-z-index: 2;
|
||||||
width: 5rem;
|
width: 5rem;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
margin-right: var(--spacing-m);
|
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 {
|
.channel-thumbnail--resolving {
|
||||||
|
@ -237,6 +268,31 @@ $actions-z-index: 2;
|
||||||
top: 0;
|
top: 0;
|
||||||
margin-top: var(--spacing-xl);
|
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 {
|
.channel-thumbnail__custom {
|
||||||
|
@ -332,6 +388,15 @@ $actions-z-index: 2;
|
||||||
@media (max-width: $breakpoint-small) {
|
@media (max-width: $breakpoint-small) {
|
||||||
padding-top: var(--spacing-xl);
|
padding-top: var(--spacing-xl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.comment__badge {
|
||||||
|
padding-right: 0px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
height: 2.5rem;
|
||||||
|
width: 2.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.channel__meta {
|
.channel__meta {
|
||||||
|
@ -501,6 +566,15 @@ $actions-z-index: 2;
|
||||||
padding: var(--spacing-s);
|
padding: var(--spacing-s);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
.comment__badge {
|
||||||
|
padding-right: 0px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
height: 2rem;
|
||||||
|
width: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.channel-thumbnail {
|
.channel-thumbnail {
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
width: 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 {
|
.channelsPage-wrapper {
|
||||||
.claim-preview__wrapper--channel {
|
.claim-preview__wrapper--channel {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
|
@ -8,16 +8,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment__badge--globalMod {
|
// fix contrast on hover of channel selector, couldn't come up with a better way
|
||||||
.st0 {
|
div[role='menuitem'] .channel__list-item .comment__badge svg {
|
||||||
// @see: ICONS.BADGE_MOD
|
stroke: unset !important;
|
||||||
fill: #fe7500;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment__badge--mod {
|
// icon is a bit bright and loud
|
||||||
.st0 {
|
.icon--PremiumPlus {
|
||||||
// @see: ICONS.BADGE_MOD
|
filter: brightness(0.92);
|
||||||
fill: #ff3850;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -228,6 +228,11 @@ $thumbnailWidthSmall: 2rem;
|
||||||
color: rgba(var(--color-secondary-dynamic), 1);
|
color: rgba(var(--color-secondary-dynamic), 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.comment__badge svg {
|
||||||
|
height: 1.4rem;
|
||||||
|
width: 1.4rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment__pin {
|
.comment__pin {
|
||||||
|
|
|
@ -779,6 +779,17 @@ $recent-msg-button__height: 2rem;
|
||||||
color: var(--color-black);
|
color: var(--color-black);
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.channel-name {
|
||||||
|
max-width: unset;
|
||||||
|
|
||||||
|
p {
|
||||||
|
max-width: 5rem;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.livestreamSuperchat__info--notSticker {
|
.livestreamSuperchat__info--notSticker {
|
||||||
|
|
|
@ -86,6 +86,11 @@
|
||||||
|
|
||||||
.livestreamComment__info {
|
.livestreamComment__info {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
.comment__badge svg {
|
||||||
|
height: 1.4rem;
|
||||||
|
width: 1.4rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.livestream__comment--superchat {
|
.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 {
|
.doodle {
|
||||||
position: fixed;
|
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;
|
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
|
// Temporary master classes
|
||||||
.date_time {
|
.date_time {
|
||||||
font-size: var(--font-xsmall);
|
font-size: var(--font-xsmall);
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { selectTheme } from 'redux/selectors/settings';
|
import { selectTheme } from 'redux/selectors/settings';
|
||||||
import { makeSelectClaimForUri, selectClaimIsNsfwForUri } from 'redux/selectors/claims';
|
import { makeSelectClaimForUri, selectClaimIsNsfwForUri } from 'redux/selectors/claims';
|
||||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
import { selectOdyseeMembershipIsPremiumPlus } from 'redux/selectors/user';
|
||||||
import Ads from './view';
|
import Ads from './view';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => ({
|
||||||
theme: selectTheme(state),
|
theme: selectTheme(state),
|
||||||
claim: makeSelectClaimForUri(props.uri)(state),
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
isMature: selectClaimIsNsfwForUri(state, props.uri),
|
isMature: selectClaimIsNsfwForUri(state, props.uri),
|
||||||
authenticated: selectUserVerifiedEmail(state),
|
userHasPremiumPlus: selectOdyseeMembershipIsPremiumPlus(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select)(Ads);
|
export default connect(select)(Ads);
|
||||||
|
|
|
@ -40,7 +40,8 @@ type Props = {
|
||||||
small: boolean,
|
small: boolean,
|
||||||
claim: Claim,
|
claim: Claim,
|
||||||
isMature: boolean,
|
isMature: boolean,
|
||||||
authenticated: boolean,
|
triggerBlacklist: boolean,
|
||||||
|
userHasPremiumPlus: boolean,
|
||||||
className?: string,
|
className?: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -64,16 +65,9 @@ function clearAdElements() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function Ads(props: Props) {
|
function Ads(props: Props) {
|
||||||
const {
|
const { type = 'video', tileLayout, small, userHasPremiumPlus, className } = props;
|
||||||
location: { pathname },
|
|
||||||
type = 'video',
|
|
||||||
tileLayout,
|
|
||||||
small,
|
|
||||||
authenticated,
|
|
||||||
className,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const shouldShowAds = SHOW_ADS && !authenticated;
|
const shouldShowAds = SHOW_ADS && !userHasPremiumPlus;
|
||||||
const mobileAds = IS_ANDROID || IS_IOS;
|
const mobileAds = IS_ANDROID || IS_IOS;
|
||||||
|
|
||||||
// this is populated from app based on location
|
// this is populated from app based on location
|
||||||
|
@ -106,8 +100,8 @@ function Ads(props: Props) {
|
||||||
log_in_to_domain: (
|
log_in_to_domain: (
|
||||||
<Button
|
<Button
|
||||||
button="link"
|
button="link"
|
||||||
label={__('Log in to %domain%', { domain: DOMAIN })}
|
label={__('Get Odysee Premium+', { domain: DOMAIN })}
|
||||||
navigate={`/$/${PAGES.AUTH}?redirect=${pathname}`}
|
navigate={`/$/${PAGES.ODYSEE_MEMBERSHIP}`}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
|
|
Loading…
Reference in a new issue