add youtube sync to first run

This commit is contained in:
Sean Yesmunt 2020-09-03 16:05:38 -04:00
parent dec63d7a2e
commit bc89d774ba
18 changed files with 348 additions and 56 deletions

View file

@ -508,10 +508,10 @@
"Policies": "Policies",
"Confirm your account": "Confirm your account",
"Start Over": "Start Over",
"Your YouTube Channel": "Your YouTube Channel",
"Your YouTube Channels": "Your YouTube Channels",
"Your YouTube channel": "Your YouTube channel",
"Your YouTube channels": "Your YouTube channels",
"Your videos are currently being transferred. There is nothing else for you to do.": "Your videos are currently being transferred. There is nothing else for you to do.",
"Please check back later.": "Please check back later.",
"Please check back later. This may take up to 1 week.": "Please check back later. This may take up to 1 week.",
"%channelName% is not yet ready to be transferred. Please allow up to one week, though it is frequently faster.": "%channelName% is not yet ready to be transferred. Please allow up to one week, though it is frequently faster.",
"here": "here",
"%channelName% is not ready to be transferred. You can check the status %statusLink% or check back later.": "%channelName% is not ready to be transferred. You can check the status %statusLink% or check back later.",

View file

@ -739,6 +739,7 @@ export const icons = {
<polyline points="22 4 12 14.01 9 11.01" />
</g>
),
[ICONS.NOT_COMPLETED]: buildIcon(<circle cx="12" cy="12" r="10" />),
[ICONS.PINNED]: buildIcon(
<g>
<path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" />
@ -819,4 +820,10 @@ export const icons = {
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
</g>
),
[ICONS.YOUTUBE]: buildIcon(
<g>
<path d="M22.54 6.42a2.78 2.78 0 0 0-1.94-2C18.88 4 12 4 12 4s-6.88 0-8.6.46a2.78 2.78 0 0 0-1.94 2A29 29 0 0 0 1 11.75a29 29 0 0 0 .46 5.33A2.78 2.78 0 0 0 3.4 19c1.72.46 8.6.46 8.6.46s6.88 0 8.6-.46a2.78 2.78 0 0 0 1.94-2 29 29 0 0 0 .46-5.25 29 29 0 0 0-.46-5.33z" />
<polygon points="9.75 15.02 15.5 11.75 9.75 8.48 9.75 15.02" />
</g>
),
};

View file

@ -177,7 +177,7 @@ const Header = (props: Props) => {
label={(backLabel && backLabel) || __('Cancel')}
icon={ICONS.ARROW_LEFT}
/>
{backTitle && <h1 className={'card__title'}>{isMobile ? simpleBackTitle || backTitle : backTitle}</h1>}
{backTitle && <h1 className="header__auth-title">{isMobile ? simpleBackTitle || backTitle : backTitle}</h1>}
<Button
aria-label={__('Your wallet')}
navigate={`/$/${PAGES.WALLET}`}

View file

@ -19,7 +19,7 @@ function RewardAuthIntro(props: Props) {
return (
<Card
title={title || __('Log in to %SITE_NAME% to Earn Rewards', { SITE_NAME })}
title={title || __('Log in to %SITE_NAME% to earn rewards', { SITE_NAME })}
subtitle={
<I18nMessage
tokens={{

View file

@ -46,6 +46,7 @@ import ChannelNew from 'page/channelNew';
import BuyPage from 'page/buy';
import NotificationsPage from 'page/notifications';
import SignInWalletPasswordPage from 'page/signInWalletPassword';
import YoutubeSyncPage from 'page/youtubeSync';
import { LINKED_COMMENT_QUERY_PARAM } from 'constants/comment';
import { parseURI } from 'lbry-redux';
import { SITE_TITLE, WELCOME_VERSION } from 'config';
@ -190,6 +191,7 @@ function AppRouter(props: Props) {
<Route path={`/$/${PAGES.AUTH}`} exact component={SignUpPage} />
<Route path={`/$/${PAGES.AUTH}/*`} exact component={SignUpPage} />
<Route path={`/$/${PAGES.WELCOME}`} exact component={Welcome} />
<Route path={`/$/${PAGES.YOUTUBE_SYNC}`} exact component={YoutubeSyncPage} />
<Route path={`/$/${PAGES.HELP}`} exact component={HelpPage} />
{/* @if TARGET='app' */}

View file

@ -1,10 +1,12 @@
// @flow
import * as PAGES from 'constants/pages';
import React, { useState } from 'react';
import { isNameValid } from 'lbry-redux';
import Button from 'component/button';
import { Form, FormField } from 'component/common/form';
import { INVALID_NAME_ERROR } from 'constants/claim';
import Card from 'component/common/card';
import I18nMessage from 'component/i18nMessage';
export const DEFAULT_BID_FOR_FIRST_CHANNEL = 0.01;
type Props = {
@ -78,6 +80,21 @@ function UserFirstChannel(props: Props) {
label={creatingChannel || claimingReward ? __('Creating') : __('Create')}
/>
</div>
<div className="help--card-actions">
<I18nMessage
tokens={{
sync_channel: (
<Button
button="link"
label={__('Sync it and skip this step')}
navigate={`/$/${PAGES.YOUTUBE_SYNC}`}
/>
),
}}
>
Have a YouTube channel? %sync_channel%.
</I18nMessage>
</div>
</Form>
}
/>

View file

@ -1,14 +1,14 @@
// @flow
import * as PAGES from 'constants/pages';
import * as ICONS from 'constants/icons';
import * as React from 'react';
import classnames from 'classnames';
import Button from 'component/button';
import ClaimPreview from 'component/claimPreview';
import Card from 'component/common/card';
import { YOUTUBE_STATUSES } from 'lbryinc';
import { buildURI } from 'lbry-redux';
import I18nMessage from 'component/i18nMessage';
const STATUS_URL = 'https://lbry.com/youtube/status/';
import Spinner from 'component/spinner';
import Icon from 'component/common/icon';
type Props = {
youtubeChannels: Array<any>,
@ -17,7 +17,8 @@ type Props = {
updateUser: () => void,
checkYoutubeTransfer: () => void,
videosImported: ?Array<number>, // [currentAmountImported, totalAmountToImport]
hideChannelLink: boolean,
alwaysShow: boolean,
addNewChannel?: boolean,
};
export default function YoutubeTransferStatus(props: Props) {
@ -28,15 +29,21 @@ export default function YoutubeTransferStatus(props: Props) {
videosImported,
checkYoutubeTransfer,
updateUser,
hideChannelLink = false,
alwaysShow = false,
addNewChannel,
} = props;
const hasChannels = youtubeChannels && youtubeChannels.length;
const hasChannels = youtubeChannels && youtubeChannels.length > 0;
const transferEnabled = youtubeChannels.some(status => status.transferable);
const hasPendingTransfers = youtubeChannels.some(
status => status.transfer_state === YOUTUBE_STATUSES.PENDING_TRANSFER
status => status.transfer_state === YOUTUBE_STATUSES.YOUTUBE_SYNC_PENDING_TRANSFER
);
const isYoutubeTransferComplete =
hasChannels && youtubeChannels.every(channel => channel.transfer_state === YOUTUBE_STATUSES.COMPLETED_TRANSFER);
hasChannels &&
youtubeChannels.every(
channel =>
channel.transfer_state === YOUTUBE_STATUSES.YOUTUBE_SYNC_COMPLETED_TRANSFER ||
channel.sync_status === YOUTUBE_STATUSES.YOUTUBE_SYNC_ABANDONDED
);
let total;
let complete;
@ -49,12 +56,14 @@ export default function YoutubeTransferStatus(props: Props) {
const { transferable, transfer_state: transferState, sync_status: syncStatus } = channel;
if (!transferable) {
switch (transferState) {
case YOUTUBE_STATUSES.NOT_TRANSFERRED:
case YOUTUBE_STATUSES.YOUTUBE_SYNC_NOT_TRANSFERRED:
return syncStatus[0].toUpperCase() + syncStatus.slice(1);
case YOUTUBE_STATUSES.PENDING_TRANSFER:
case YOUTUBE_STATUSES.YOUTUBE_SYNC_PENDING_TRANSFER:
return __('Transfer in progress');
case YOUTUBE_STATUSES.COMPLETED_TRANSFER:
case YOUTUBE_STATUSES.YOUTUBE_SYNC_COMPLETED_TRANSFER:
return __('Completed transfer');
case YOUTUBE_STATUSES.YOUTUBE_SYNC_ABANDONDED:
return __('This channel not eligible to by synced');
}
} else {
return __('Ready to transfer');
@ -74,28 +83,36 @@ export default function YoutubeTransferStatus(props: Props) {
return () => {
clearInterval(interval);
};
} else {
updateUser();
}
}, [hasPendingTransfers, checkYoutubeTransfer, updateUser]);
}, [hasPendingTransfers, checkYoutubeTransfer, updateUser, updateUser]);
return (
hasChannels &&
!isYoutubeTransferComplete && (
(alwaysShow || (hasChannels && !isYoutubeTransferComplete)) && (
<Card
title={youtubeChannels.length > 1 ? __('Your YouTube Channels') : __('Your YouTube Channel')}
title={youtubeChannels.length > 1 ? __('Your YouTube channels') : __('Your YouTube channel')}
subtitle={
<span>
{hasPendingTransfers &&
__('Your videos are currently being transferred. There is nothing else for you to do.')}
{transferEnabled && !hasPendingTransfers && __('Your videos are ready to be transferred.')}
{!transferEnabled && !hasPendingTransfers && __('Please check back later.')}
{!transferEnabled && !hasPendingTransfers && __('Please check back later. This may take up to 1 week.')}
</span>
}
body={
<section>
{youtubeChannels.map((channel, index) => {
const { lbry_channel_name: channelName, channel_claim_id: claimId, status_token: statusToken } = channel;
const { lbry_channel_name: channelName, channel_claim_id: claimId, sync_status: syncStatus } = channel;
const url = buildURI({ channelName, channelClaimId: claimId });
const transferState = getMessage(channel);
const isWaitingForSync =
syncStatus === YOUTUBE_STATUSES.YOUTUBE_SYNC_QUEUED ||
syncStatus === YOUTUBE_STATUSES.YOUTUBE_SYNC_PENDINGUPGRADE ||
syncStatus === YOUTUBE_STATUSES.YOUTUBE_SYNC_SYNCING;
const isNotEligible = syncStatus === YOUTUBE_STATUSES.YOUTUBE_SYNC_ABANDONDED;
return (
<div key={url} className="card--inline">
{claimId ? (
@ -106,26 +123,32 @@ export default function YoutubeTransferStatus(props: Props) {
/>
) : (
<div className="section--padded">
<p>
<I18nMessage
tokens={{
channelName,
}}
>
%channelName% is not yet ready to be transferred. Please allow up to one week, though it is
frequently faster.
</I18nMessage>
</p>
<p className="help">
<I18nMessage
tokens={{
statusLink: <Button button="link" href={STATUS_URL + statusToken} label={__('here')} />,
faqLink: <Button button="link" label={__('FAQ')} href="https://lbry.com/faq/youtube" />,
}}
>
You can check your status %statusLink%. This %faqLink% explains the program in more detail.
</I18nMessage>
</p>
{isNotEligible ? (
<div>{__('%channelName% is not eligible to be synced', { channelName })}</div>
) : (
<div className="progress">
<div className="progress__item">
{__('Claim your handle %handle%', { handle: channelName })}
<Icon icon={ICONS.COMPLETED} className="progress__complete-icon--completed" />
</div>
<div className="progress__item">
{__('Agree to sync')}{' '}
<Icon icon={ICONS.COMPLETED} className="progress__complete-icon--completed" />
</div>
<div className="progress__item">
{__('Wait for your videos to be synced')}
{isWaitingForSync ? (
<Spinner type="small" />
) : (
<Icon icon={ICONS.COMPLETED} className="progress__complete-icon--completed" />
)}
</div>
<div className="progress__item">
{__('Claim your channel')}
<Icon icon={ICONS.NOT_COMPLETED} className={classnames('progress__complete-icon')} />
</div>
</div>
)}
</div>
)}
</div>
@ -137,23 +160,30 @@ export default function YoutubeTransferStatus(props: Props) {
</section>
}
actions={
transferEnabled ? (
<div className="card__actions">
<>
<div className="section__actions">
<Button
button="primary"
disabled={youtubeImportPending}
disabled={youtubeImportPending || !transferEnabled}
onClick={claimChannels}
label={youtubeChannels.length > 1 ? __('Claim Channels') : __('Claim Channel')}
/>
<Button button="link" label={__('Learn more')} href="https://lbry.com/faq/youtube#transfer" />
</div>
) : !hideChannelLink ? (
<div className="card__actions">
<Button button="primary" navigate={`/$/${PAGES.CHANNELS}`} label={__('View your channels')} />
</div>
{addNewChannel ? (
<Button button="link" label={__('Add Another Channel')} onClick={addNewChannel} />
) : (
false
)
<Button button="link" label={__('Learn More')} href="https://lbry.com/faq/youtube#transfer" />
)}
</div>
<p className="help">
{youtubeChannels.length > 1
? __('You will be able to claim your channels once they finish syncing.')
: __('You will be able to claim your channel once it has finished syncing.')}{' '}
{addNewChannel && (
<Button button="link" label={__('Learn More')} href="https://lbry.com/faq/youtube#transfer" />
)}
</p>
</>
}
/>
)

View file

@ -30,6 +30,7 @@ export const WALLET = 'List';
export const PHONE = 'Phone';
export const COMPLETE = 'Check';
export const COMPLETED = 'CheckCircle';
export const NOT_COMPLETED = 'Circle';
export const SUBSCRIBE = 'Heart';
export const UNSUBSCRIBE = 'BrokenHeart';
export const UNLOCK = 'Unlock';
@ -115,3 +116,4 @@ export const LBRY_STATUS = 'BarChart';
export const NOTIFICATION = 'Bell';
export const LAYOUT = 'Layout';
export const REPLY = 'Reply';
export const YOUTUBE = 'Youtube';

View file

@ -46,3 +46,4 @@ exports.CODE_2257 = '2257';
exports.BUY = 'buy';
exports.CHANNEL_NEW = 'channel/new';
exports.NOTIFICATIONS = 'notifications';
exports.YOUTUBE_SYNC = 'youtube';

View file

@ -10,6 +10,7 @@ import {
} from 'lbry-redux';
import { selectChannelIsBlocked } from 'redux/selectors/blocked';
import { selectBlackListedOutpoints, doFetchSubCount, makeSelectSubCountForUri } from 'lbryinc';
import { selectYoutubeChannels } from 'redux/selectors/user';
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
import { doOpenModal } from 'redux/actions/app';
import ChannelPage from './view';
@ -26,6 +27,7 @@ const select = (state, props) => ({
blackListedOutpoints: selectBlackListedOutpoints(state),
subCount: makeSelectSubCountForUri(props.uri)(state),
pending: makeSelectClaimIsPending(props.uri)(state),
youtubeChannels: selectYoutubeChannels(state),
});
const perform = dispatch => ({

View file

@ -1,7 +1,9 @@
// @flow
import * as ICONS from 'constants/icons';
import * as PAGES from 'constants/pages';
import React from 'react';
import { parseURI } from 'lbry-redux';
import { YOUTUBE_STATUSES } from 'lbryinc';
import Page from 'component/page';
import SubscribeButton from 'component/subscribeButton';
import BlockButton from 'component/blockButton';
@ -43,6 +45,7 @@ type Props = {
fetchSubCount: string => void,
subCount: number,
pending: boolean,
youtubeChannels: ?Array<{ channel_claim_id: string, sync_status: string, transfer_state: string }>,
};
function ChannelPage(props: Props) {
@ -59,6 +62,7 @@ function ChannelPage(props: Props) {
fetchSubCount,
subCount,
pending,
youtubeChannels,
} = props;
const {
push,
@ -73,6 +77,18 @@ function ChannelPage(props: Props) {
const { permanent_url: permanentUrl } = claim;
const claimId = claim.claim_id;
const formattedSubCount = Number(subCount).toLocaleString();
const isMyYouTubeChannel =
claim &&
youtubeChannels &&
youtubeChannels.some(({ channel_claim_id, sync_status, transfer_state }) => {
if (
channel_claim_id === claim.claim_id &&
sync_status !== YOUTUBE_STATUSES.YOUTUBE_SYNC_ABANDONDED &&
transfer_state !== YOUTUBE_STATUSES.YOUTUBE_SYNC_COMPLETED_TRANSFER
) {
return true;
}
});
let channelIsBlackListed = false;
if (claim && blackListedOutpoints) {
@ -131,6 +147,14 @@ function ChannelPage(props: Props) {
<YoutubeBadge channelClaimId={claimId} />
<header className="channel-cover">
<div className="channel__quick-actions">
{isMyYouTubeChannel && (
<Button
button="alt"
label={__('Claim Your Channel')}
icon={ICONS.YOUTUBE}
navigate={`/$/${PAGES.CHANNELS}`}
/>
)}
{!channelIsBlocked && !channelIsBlackListed && <ShareButton uri={uri} />}
{!channelIsBlocked && <ClaimSupportButton uri={uri} />}
{!channelIsBlocked && (!channelIsBlackListed || isSubscribed) && <SubscribeButton uri={permanentUrl} />}

View file

@ -33,7 +33,7 @@ class InvitePage extends React.PureComponent<Props> {
<Page>
{!authenticated ? (
<RewardAuthIntro
title={__('Log in to %SITE_NAME% to Earn Rewards From Inviting Your Friends', { SITE_NAME })}
title={__('Log in to %SITE_NAME% to earn rewards From Inviting Your Friends', { SITE_NAME })}
/>
) : (
<React.Fragment>

View file

@ -0,0 +1,12 @@
import { connect } from 'react-redux';
import { selectYoutubeChannels } from 'redux/selectors/user';
import { doUserFetch } from 'redux/actions/user';
import CreatorDashboardPage from './view';
const select = state => ({
youtubeChannels: selectYoutubeChannels(state),
});
export default connect(select, {
doUserFetch,
})(CreatorDashboardPage);

View file

@ -0,0 +1,168 @@
// @flow
import { SITE_NAME, DOMAIN } from 'config';
import * as PAGES from 'constants/pages';
import React from 'react';
import Page from 'component/page';
import Button from 'component/button';
import Card from 'component/common/card';
import I18nMessage from 'component/i18nMessage';
import { Form, FormField } from 'component/common/form';
import { INVALID_NAME_ERROR } from 'constants/claim';
import { isNameValid } from 'lbry-redux';
import { Lbryio } from 'lbryinc';
import { useHistory } from 'react-router';
import YoutubeTransferStatus from 'component/youtubeTransferStatus';
import Nag from 'component/common/nag';
const STATUS_TOKEN_PARAM = 'status_token';
const ERROR_MESSAGE_PARAM = 'error_message';
const NEW_CHANNEL_PARAM = 'new_channel';
type Props = {
youtubeChannels: ?Array<{ transfer_state: string, sync_status: string }>,
doUserFetch: () => void,
};
export default function YoutubeSync(props: Props) {
const { youtubeChannels, doUserFetch } = props;
const {
location: { search, pathname },
push,
} = useHistory();
const urlParams = new URLSearchParams(search);
const statusToken = urlParams.get(STATUS_TOKEN_PARAM);
const errorMessage = urlParams.get(ERROR_MESSAGE_PARAM);
const newChannelParam = urlParams.get(NEW_CHANNEL_PARAM);
const [channel, setChannel] = React.useState('');
const [nameError, setNameError] = React.useState(undefined);
const [acknowledgedTerms, setAcknowledgedTerms] = React.useState(false);
const [addingNewChannel, setAddingNewChannel] = React.useState(newChannelParam);
const hasYoutubeChannels = youtubeChannels && youtubeChannels.length > 0;
React.useEffect(() => {
if (statusToken && !hasYoutubeChannels) {
doUserFetch();
}
}, [statusToken, hasYoutubeChannels, doUserFetch]);
React.useEffect(() => {
if (!newChannelParam) {
setAddingNewChannel(false);
}
}, [newChannelParam]);
function handleCreateChannel() {
Lbryio.call('yt', 'new', {
type: 'sync',
immediate_sync: true,
desired_lbry_channel_name: `@${channel}`,
return_url: `https://${DOMAIN}/$/${PAGES.YOUTUBE_SYNC}`,
}).then(ytAuthUrl => {
// react-router isn't needed since it's a different domain
window.location.href = ytAuthUrl;
});
}
function handleChannelChange(e) {
const { value } = e.target;
setChannel(value);
if (!isNameValid(value, 'false')) {
setNameError(INVALID_NAME_ERROR);
} else {
setNameError();
}
}
function handleNewChannel() {
urlParams.append('new_channel', 'true');
push(`${pathname}?${urlParams.toString()}`);
setAddingNewChannel(true);
}
return (
<Page noSideNavigation authPage>
<div className="main__channel-creation">
{hasYoutubeChannels && !addingNewChannel ? (
<YoutubeTransferStatus alwaysShow addNewChannel={handleNewChannel} />
) : (
<Card
title={__('Connect with your fans while earning rewards')}
subtitle={__('Get your YouTube videos in front of the %site_name% audience.', {
site_name: IS_WEB ? SITE_NAME : 'LBRY',
})}
actions={
<Form onSubmit={handleCreateChannel}>
<fieldset-group class="fieldset-group--smushed fieldset-group--disabled-prefix">
<fieldset-section>
<label htmlFor="auth_first_channel">
{nameError ? <span className="error__text">{nameError}</span> : __('Your Channel')}
</label>
<div className="form-field__prefix">@</div>
</fieldset-section>
<FormField
autoFocus
placeholder={__('channel')}
type="text"
name="yt_sync_channel"
className="form-field--short"
value={channel}
onChange={handleChannelChange}
/>
</fieldset-group>
<FormField
type="checkbox"
name="yt_sync_terms"
checked={acknowledgedTerms}
onChange={() => setAcknowledgedTerms(!acknowledgedTerms)}
label={
<I18nMessage
tokens={{
terms: (
<Button button="link" label={__('these terms')} href="https://lbry.com/faq/youtube-terms" />
),
faq: (
<Button
button="link"
label={__('how the program works')}
href="https://lbry.com/faq/youtube"
/>
),
}}
>
I want to sync my content to the LBRY network and agree to %terms%. I have also read and
understand %faq%.
</I18nMessage>
}
/>
<div className="section__actions">
<Button
button="primary"
type="submit"
disabled={nameError || !channel || !acknowledgedTerms}
label={__('Claim Now')}
/>
{errorMessage && <Button button="link" label={__('Skip')} navigate={`/$/${PAGES.REWARDS}`} />}
</div>
<div className="help--card-actions">
<I18nMessage
tokens={{
learn_more: <Button button="link" label={__('Learn more')} href="https://lbry.com/faq/youtube" />,
}}
>
This will verify you are an active YouTuber. Channel names cannot be changed once chosen, please be
extra careful. Additional instructions will be emailed to you after you verify your email on the
next page. %learn_more%.
</I18nMessage>
</div>
</Form>
}
nag={errorMessage && <Nag message={errorMessage} type="error" relative />}
/>
)}
</div>
</Page>
);
}

View file

@ -40,6 +40,7 @@
@import 'component/pagination';
@import 'component/purchase';
@import 'component/placeholder';
@import 'component/progress';
@import 'component/search';
@import 'component/claim-search';
@import 'component/section';

View file

@ -388,8 +388,11 @@ fieldset-group {
}
.form-field--short {
width: 100%;
@media (min-width: $breakpoint-small) {
width: 25em;
}
}
.form-field--price-amount {
width: 7em;

View file

@ -186,3 +186,9 @@
justify-content: center;
width: 100%;
}
.header__auth-title {
@media (min-width: $breakpoint-small) {
font-size: var(--font-large);
}
}

View file

@ -0,0 +1,17 @@
.progress__item {
display: flex;
align-items: center;
&:not(:first-of-type) {
margin-top: var(--spacing-s);
}
}
.progress__complete-icon {
margin-left: var(--spacing-s);
}
.progress__complete-icon--completed {
@extend .progress__complete-icon;
stroke: var(--color-primary);
}