add createChannel to first run flow

This commit is contained in:
Sean Yesmunt 2019-08-27 10:43:42 -04:00
parent 3311e1af1f
commit 30d396fce9
28 changed files with 589 additions and 254 deletions

24
flow-typed/user.js vendored Normal file
View file

@ -0,0 +1,24 @@
// @flow
// Move this to lbryinc
declare type User = {
created_at: string,
family_name: ?string,
given_name: ?string,
groups: Array<string>,
has_verified_email: boolean,
id: number,
invite_reward_claimed: boolean,
invited_at: ?number,
invited_by_id: number,
invites_remaining: number,
is_email_enabled: boolean,
is_identity_verified: boolean,
is_reward_approved: boolean,
language: string,
manual_approval_user_id: ?number,
primary_email: string,
reward_status_change_trigger: string,
updated_at: string,
youtube_channels: ?Array<string>,
};

View file

@ -6,9 +6,12 @@ import { Lbry, buildURI, parseURI } from 'lbry-redux';
import Router from 'component/router/index'; import Router from 'component/router/index';
import ModalRouter from 'modal/modalRouter'; import ModalRouter from 'modal/modalRouter';
import ReactModal from 'react-modal'; import ReactModal from 'react-modal';
<<<<<<< HEAD
import SideBar from 'component/sideBar'; import SideBar from 'component/sideBar';
import Header from 'component/header'; import Header from 'component/header';
import Button from 'component/button'; import Button from 'component/button';
=======
>>>>>>> add createChannel to first run flow
import { openContextMenu } from 'util/context-menu'; import { openContextMenu } from 'util/context-menu';
import useKonamiListener from 'util/enhanced-layout'; import useKonamiListener from 'util/enhanced-layout';
import Yrbl from 'component/yrbl'; import Yrbl from 'component/yrbl';
@ -111,13 +114,7 @@ function App(props: Props) {
return ( return (
<div className={MAIN_WRAPPER_CLASS} ref={appRef} onContextMenu={e => openContextMenu(e)}> <div className={MAIN_WRAPPER_CLASS} ref={appRef} onContextMenu={e => openContextMenu(e)}>
<Header />
<div className="main-wrapper__inner">
<Router /> <Router />
<SideBar />
</div>
<ModalRouter /> <ModalRouter />
<FileViewer pageUri={uri} /> <FileViewer pageUri={uri} />

View file

@ -1,7 +1,7 @@
// @flow // @flow
import React from 'react'; import React from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import { formatCredits, formatFullPrice } from 'util/format-credits'; import { formatCredits, formatFullPrice } from 'lbry-redux';
type Props = { type Props = {
amount: number, amount: number,
@ -39,7 +39,7 @@ class CreditAmount extends React.PureComponent<Props> {
formattedAmount = formattedAmount =
amount > 0 && amount < minimumRenderableAmount amount > 0 && amount < minimumRenderableAmount
? `<${minimumRenderableAmount}` ? `<${minimumRenderableAmount}`
: formatCredits(amount, precision); : formatCredits(amount, precision, true);
} }
let amountText; let amountText;

View file

@ -1,16 +1,15 @@
import * as SETTINGS from 'constants/settings'; import * as SETTINGS from 'constants/settings';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { selectBalance, SETTINGS as LBRY_REDUX_SETTINGS } from 'lbry-redux'; import { selectBalance, formatCredits } from 'lbry-redux';
import { selectUserEmail } from 'lbryinc'; import { selectUserEmail } from 'lbryinc';
import { formatCredits } from 'util/format-credits';
import { doSetClientSetting } from 'redux/actions/settings'; import { doSetClientSetting } from 'redux/actions/settings';
import { makeSelectClientSetting } from 'redux/selectors/settings'; import { makeSelectClientSetting } from 'redux/selectors/settings';
import Header from './view'; import Header from './view';
const select = state => ({ const select = state => ({
balance: selectBalance(state), balance: selectBalance(state),
language: makeSelectClientSetting(LBRY_REDUX_SETTINGS.LANGUAGE)(state), // trigger redraw on language change language: makeSelectClientSetting(SETTINGS.LANGUAGE)(state), // trigger redraw on language change
roundedBalance: formatCredits(selectBalance(state) || 0, 2), roundedBalance: formatCredits(selectBalance(state) || 0, 2, true),
currentTheme: makeSelectClientSetting(SETTINGS.THEME)(state), currentTheme: makeSelectClientSetting(SETTINGS.THEME)(state),
automaticDarkModeEnabled: makeSelectClientSetting(SETTINGS.AUTOMATIC_DARK_MODE_ENABLED)(state), automaticDarkModeEnabled: makeSelectClientSetting(SETTINGS.AUTOMATIC_DARK_MODE_ENABLED)(state),
hideBalance: makeSelectClientSetting(SETTINGS.HIDE_BALANCE)(state), hideBalance: makeSelectClientSetting(SETTINGS.HIDE_BALANCE)(state),

View file

@ -4,11 +4,13 @@ import * as SETTINGS from 'constants/settings';
import * as PAGES from 'constants/pages'; import * as PAGES from 'constants/pages';
import React, { Fragment } from 'react'; import React, { Fragment } from 'react';
import { withRouter } from 'react-router'; import { withRouter } from 'react-router';
import classnames from 'classnames';
import Button from 'component/button'; import Button from 'component/button';
import LbcSymbol from 'component/common/lbc-symbol'; import LbcSymbol from 'component/common/lbc-symbol';
import WunderBar from 'component/wunderbar'; import WunderBar from 'component/wunderbar';
import Icon from 'component/common/icon'; import Icon from 'component/common/icon';
import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button'; import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button';
import Tooltip from 'component/common/tooltip';
// Move this into jessops password util // Move this into jessops password util
import cookie from 'cookie'; import cookie from 'cookie';
@ -38,6 +40,7 @@ type Props = {
setClientSetting: (string, boolean | string) => void, setClientSetting: (string, boolean | string) => void,
hideBalance: boolean, hideBalance: boolean,
email: ?string, email: ?string,
minimal: boolean,
}; };
const Header = (props: Props) => { const Header = (props: Props) => {
@ -49,8 +52,12 @@ const Header = (props: Props) => {
automaticDarkModeEnabled, automaticDarkModeEnabled,
hideBalance, hideBalance,
email, email,
minimal,
} = props; } = props;
const showUpgradeButton = autoUpdateDownloaded || (process.platform === 'linux' && isUpgradeAvailable);
const isAuthenticated = Boolean(email); const isAuthenticated = Boolean(email);
// Auth is optional in the desktop app
const showFullHeader = IS_WEB ? isAuthenticated : true;
function handleThemeToggle() { function handleThemeToggle() {
if (automaticDarkModeEnabled) { if (automaticDarkModeEnabled) {
@ -64,35 +71,50 @@ const Header = (props: Props) => {
} }
} }
function getAccountTitle() {
if (roundedBalance > 0 && !hideBalance) {
return (
<React.Fragment>
{roundedBalance} <LbcSymbol />
</React.Fragment>
);
}
return __('Account');
}
function signOut() { function signOut() {
// Replace this with actual clearUser function // Replace this with actual clearUser function
window.store.dispatch({ type: 'USER_FETCH_FAILURE' }); window.store.dispatch({ type: 'USER_FETCH_FAILURE' });
deleteAuthToken(); deleteAuthToken();
location.reload();
}
const accountMenuButtons = [
{
path: `/$/account`,
icon: ICONS.OVERVIEW,
label: __('Overview'),
},
{
path: `/$/rewards`,
icon: ICONS.FEATURED,
label: __('Rewards'),
},
{
path: `/$/wallet`,
icon: ICONS.WALLET,
label: __('Wallet'),
},
{
path: `/$/publish`,
icon: ICONS.PUBLISH,
label: __('Publish'),
},
];
if (!isAuthenticated) {
accountMenuButtons.push({
onClick: signOut,
icon: ICONS.PUBLISH,
label: __('Publish'),
});
} }
return ( return (
<header className="header"> <header className={classnames('header', { 'header--minimal': minimal })}>
<div className="header__contents"> <div className="header__contents">
<div className="header__navigation"> <div className="header__navigation">
<Button
className="header__navigation-item header__navigation-item--lbry"
label={__('LBRY')}
icon={ICONS.LBRY}
navigate="/"
/>
{/* @if TARGET='app' */} {/* @if TARGET='app' */}
{!minimal && (
<div className="header__navigation-arrows"> <div className="header__navigation-arrows">
<Button <Button
className="header__navigation-item header__navigation-item--back" className="header__navigation-item header__navigation-item--back"
@ -110,39 +132,63 @@ const Header = (props: Props) => {
iconSize={18} iconSize={18}
/> />
</div> </div>
)}
{/* @endif */} {/* @endif */}
<WunderBar />
<Button
className="header__navigation-item header__navigation-item--lbry"
label={__('LBRY')}
icon={ICONS.LBRY}
navigate="/"
/>
{!minimal && <WunderBar />}
</div> </div>
{!minimal ? (
<div className="header__menu"> <div className="header__menu">
{isAuthenticated ? ( {showFullHeader ? (
<Fragment> <Fragment>
<Menu> <Menu>
<MenuButton className="header__navigation-item menu__title"> <MenuButton className="header__navigation-item menu__title">
<Icon icon={ICONS.ACCOUNT} /> {roundedBalance} <LbcSymbol />
{getAccountTitle()}
</MenuButton> </MenuButton>
<MenuList className="menu__list--header"> <MenuList className="menu__list--header">
<MenuItem className="menu__link" onSelect={() => history.push(`/$/account`)}> <MenuItem className="menu__link" onSelect={() => history.push(`/$/wallet`)}>
<Icon aria-hidden icon={ICONS.OVERVIEW} /> <Icon aria-hidden icon={ICONS.WALLET} />
{__('Overview')} {__('Wallet')}
</MenuItem> </MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/rewards`)}> <MenuItem className="menu__link" onSelect={() => history.push(`/$/rewards`)}>
<Icon aria-hidden icon={ICONS.FEATURED} /> <Icon aria-hidden icon={ICONS.FEATURED} />
{__('Rewards')} {__('Rewards')}
</MenuItem> </MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/wallet`)}> </MenuList>
<Icon aria-hidden icon={ICONS.WALLET} /> </Menu>
{__('Wallet')} <Menu>
<MenuButton className="header__navigation-item menu__title">
<Icon size={18} icon={ICONS.ACCOUNT} />
</MenuButton>
<MenuList className="menu__list--header">
<MenuItem
className="menu__link"
onSelect={() => history.push(isAuthenticated ? `/$/account` : `/$/auth/signup`)}
>
<Icon aria-hidden icon={ICONS.OVERVIEW} />
{__('Overview')}
</MenuItem> </MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/publish`)}> <MenuItem className="menu__link" onSelect={() => history.push(`/$/publish`)}>
<Icon aria-hidden icon={ICONS.PUBLISH} /> <Icon aria-hidden icon={ICONS.PUBLISH} />
{__('Publish')} {__('Publish')}
</MenuItem> </MenuItem>
{isAuthenticated ? (
<MenuItem className="menu__link" onSelect={signOut}> <MenuItem className="menu__link" onSelect={signOut}>
<Icon aria-hidden icon={ICONS.SIGN_OUT} /> <Icon aria-hidden icon={ICONS.SIGN_OUT} />
{__('Sign Out')} {__('Sign Out')}
</MenuItem> </MenuItem>
) : (
<MenuItem onSelect={() => {}} />
)}
</MenuList> </MenuList>
</Menu> </Menu>
@ -168,11 +214,18 @@ const Header = (props: Props) => {
</Fragment> </Fragment>
) : ( ) : (
<Fragment> <Fragment>
<Button navigate={`/$/${PAGES.AUTH}/signin`} button="link" label={__('Sign In')} /> <span />
<Button navigate={`/$/${PAGES.AUTH}/signup`} button="primary" label={__('Sign Up')} /> <Button navigate={`/$/${PAGES.AUTH}/signin`} button="primary" label={__('Sign In')} />
</Fragment> </Fragment>
)} )}
</div> </div>
) : (
<div className="header__menu">
<Tooltip label={__('Go Back')}>
<Button icon={ICONS.REMOVE} onClick={() => history.goBack()} />
</Tooltip>
</div>
)}
</div> </div>
</header> </header>
); );

View file

@ -1,16 +1,50 @@
// @flow // @flow
import * as React from 'react'; import type { Node } from 'react';
import * as ICONS from 'constants/icons';
import React, { Fragment } from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import SideBar from 'component/sideBar';
import Header from 'component/header';
import Button from 'component/button';
type Props = { type Props = {
children: React.Node | Array<React.Node>, children: Node | Array<Node>,
className: ?string, className: ?string,
autoUpdateDownloaded: boolean,
isUpgradeAvailable: boolean,
fullscreen: boolean,
doDownloadUpgradeRequested: () => void,
}; };
function Page(props: Props) { function Page(props: Props) {
const { children, className } = props; const {
children,
className,
fullscreen = false,
autoUpdateDownloaded,
isUpgradeAvailable,
doDownloadUpgradeRequested,
} = props;
const showUpgradeButton = autoUpdateDownloaded || (process.platform === 'linux' && isUpgradeAvailable);
return <main className={classnames('main', className)}>{children}</main>; return (
<Fragment>
<Header minimal={fullscreen} />
<div className={classnames('main-wrapper__inner')}>
{/* @if TARGET='app' */}
{showUpgradeButton && (
<div className="main__status">
{__('Upgrade is ready')}
<Button button="alt" icon={ICONS.DOWNLOAD} label={__('Install now')} onClick={doDownloadUpgradeRequested} />
</div>
)}
{/* @endif */}
<main className={classnames('main', className)}>{children}</main>
{!fullscreen && <SideBar />}
</div>
</Fragment>
);
} }
export default Page; export default Page;

View file

@ -21,10 +21,9 @@ import WalletPage from 'page/wallet';
import NavigationHistory from 'page/navigationHistory'; import NavigationHistory from 'page/navigationHistory';
import TagsPage from 'page/tags'; import TagsPage from 'page/tags';
import FollowingPage from 'page/following'; import FollowingPage from 'page/following';
import ListBlocked from 'page/listBlocked'; import ListBlockedPage from 'page/listBlocked';
import FourOhFourPage from 'page/fourOhFour'; import FourOhFourPage from 'page/fourOhFour';
import SignInPage from 'page/signIn';
import UserEmail from 'component/userEmail';
// Tell the browser we are handling scroll restoration // Tell the browser we are handling scroll restoration
if ('scrollRestoration' in history) { if ('scrollRestoration' in history) {
@ -36,14 +35,6 @@ type Props = {
location: { pathname: string, search: string }, location: { pathname: string, search: string },
}; };
function SignInPage() {
return <UserEmail />;
}
function SignUpPage() {
return <h1>Sign In</h1>;
}
function AppRouter(props: Props) { function AppRouter(props: Props) {
const { currentScroll } = props; const { currentScroll } = props;
@ -57,7 +48,6 @@ function AppRouter(props: Props) {
<Route path={`/$/${PAGES.DISCOVER}`} exact component={DiscoverPage} /> <Route path={`/$/${PAGES.DISCOVER}`} exact component={DiscoverPage} />
<Route path={`/$/${PAGES.AUTH}`} exact component={AuthPage} /> <Route path={`/$/${PAGES.AUTH}`} exact component={AuthPage} />
<Route path={`/$/${PAGES.AUTH}/signin`} exact component={SignInPage} /> <Route path={`/$/${PAGES.AUTH}/signin`} exact component={SignInPage} />
<Route path={`/$/${PAGES.AUTH}/signup`} exact component={SignUpPage} />
<Route path={`/$/${PAGES.INVITE}`} exact component={InvitePage} /> <Route path={`/$/${PAGES.INVITE}`} exact component={InvitePage} />
<Route path={`/$/${PAGES.DOWNLOADED}`} exact component={FileListDownloaded} /> <Route path={`/$/${PAGES.DOWNLOADED}`} exact component={FileListDownloaded} />
<Route path={`/$/${PAGES.PUBLISHED}`} exact component={FileListPublished} /> <Route path={`/$/${PAGES.PUBLISHED}`} exact component={FileListPublished} />
@ -74,7 +64,7 @@ function AppRouter(props: Props) {
<Route path={`/$/${PAGES.TAGS}`} exact component={TagsPage} /> <Route path={`/$/${PAGES.TAGS}`} exact component={TagsPage} />
<Route path={`/$/${PAGES.FOLLOWING}`} exact component={FollowingPage} /> <Route path={`/$/${PAGES.FOLLOWING}`} exact component={FollowingPage} />
<Route path={`/$/${PAGES.WALLET}`} exact component={WalletPage} /> <Route path={`/$/${PAGES.WALLET}`} exact component={WalletPage} />
<Route path={`/$/${PAGES.BLOCKED}`} exact component={ListBlocked} /> <Route path={`/$/${PAGES.BLOCKED}`} exact component={ListBlockedPage} />
{/* Below need to go at the end to make sure we don't match any of our pages first */} {/* Below need to go at the end to make sure we don't match any of our pages first */}
<Route path="/:claimName" exact component={ShowPage} /> <Route path="/:claimName" exact component={ShowPage} />
<Route path="/:claimName/:streamName" exact component={ShowPage} /> <Route path="/:claimName/:streamName" exact component={ShowPage} />

View file

@ -1,7 +1,7 @@
// @flow // @flow
import * as PAGES from 'constants/pages'; import * as PAGES from 'constants/pages';
import * as ICONS from 'constants/icons'; import * as ICONS from 'constants/icons';
import React from 'react'; import React, { Fragment } from 'react';
import Button from 'component/button'; import Button from 'component/button';
import Tag from 'component/tag'; import Tag from 'component/tag';
import StickyBox from 'react-sticky-box/dist/esnext'; import StickyBox from 'react-sticky-box/dist/esnext';
@ -14,6 +14,7 @@ type Props = {
function SideBar(props: Props) { function SideBar(props: Props) {
const { subscriptions, followedTags, email } = props; const { subscriptions, followedTags, email } = props;
const showSideBar = IS_WEB ? Boolean(email) : true;
function buildLink(path, label, icon, guide) { function buildLink(path, label, icon, guide) {
return { return {
@ -26,7 +27,7 @@ function SideBar(props: Props) {
return ( return (
<StickyBox offsetTop={100} offsetBottom={20}> <StickyBox offsetTop={100} offsetBottom={20}>
{email ? ( {showSideBar ? (
<nav className="navigation"> <nav className="navigation">
<ul className="navigation-links"> <ul className="navigation-links">
{[ {[
@ -41,15 +42,22 @@ function SideBar(props: Props) {
{ {
...buildLink(PAGES.PUBLISHED, __('Publishes'), ICONS.PUBLISH), ...buildLink(PAGES.PUBLISHED, __('Publishes'), ICONS.PUBLISH),
}, },
{
...buildLink(PAGES.FOLLOWING, __('Customize'), ICONS.EDIT),
},
].map(linkProps => ( ].map(linkProps => (
<li key={linkProps.label}> <li key={linkProps.label}>
<Button {...linkProps} className="navigation-link" activeClass="navigation-link--active" /> <Button {...linkProps} className="navigation-link" activeClass="navigation-link--active" />
</li> </li>
))} ))}
</ul> </ul>
{email ? (
<Fragment>
<Button
navigate={`/$/${PAGES.FOLLOWING}`}
label={__('Customize')}
icon={ICONS.EDIT}
className="navigation-link"
activeClass="navigation-link--active"
/>
<ul className="navigation-links tags--vertical"> <ul className="navigation-links tags--vertical">
{followedTags.map(({ name }, key) => ( {followedTags.map(({ name }, key) => (
<li className="navigation-link__wrapper" key={name}> <li className="navigation-link__wrapper" key={name}>
@ -69,6 +77,12 @@ function SideBar(props: Props) {
</li> </li>
))} ))}
</ul> </ul>
</Fragment>
) : (
<div className="navigation--placeholder" style={{ height: '20vh', marginTop: '1rem', padding: '1rem' }}>
Something about logging up to customize here
</div>
)}
</nav> </nav>
) : ( ) : (
<div className="navigation--placeholder" /> <div className="navigation--placeholder" />

View file

@ -1,43 +1,23 @@
// @flow // @flow
import * as React from 'react'; import React, { useState } from 'react';
import { FormField, Form } from 'component/common/form'; import { FormField, Form } from 'component/common/form';
import Button from 'component/button'; import Button from 'component/button';
import { Lbryio } from 'lbryinc'; import { Lbryio } from 'lbryinc';
import analytics from 'analytics'; import analytics from 'analytics';
type Props = { type Props = {
cancelButton: React.Node,
errorMessage: ?string, errorMessage: ?string,
isPending: boolean, isPending: boolean,
addUserEmail: string => void, addUserEmail: string => void,
}; };
type State = { function UserEmailNew(props: Props) {
email: string, const { errorMessage, isPending, addUserEmail } = props;
}; const [newEmail, setEmail] = useState('');
const [sync, setSync] = useState(false);
class UserEmailNew extends React.PureComponent<Props, State> { function handleSubmit() {
constructor() { addUserEmail(newEmail);
super();
this.state = {
email: '',
};
(this: any).handleSubmit = this.handleSubmit.bind(this);
(this: any).handleEmailChanged = this.handleEmailChanged.bind(this);
}
handleEmailChanged(event: SyntheticInputEvent<*>) {
this.setState({
email: event.target.value,
});
}
handleSubmit() {
const { email } = this.state;
const { addUserEmail } = this.props;
addUserEmail(email);
analytics.emailProvidedEvent(); analytics.emailProvidedEvent();
// @if TARGET='web' // @if TARGET='web'
@ -45,40 +25,28 @@ class UserEmailNew extends React.PureComponent<Props, State> {
// @endif // @endif
} }
render() {
const { cancelButton, errorMessage, isPending } = this.props;
return ( return (
<React.Fragment> <Form onSubmit={handleSubmit}>
<h2 className="card__title">{__('Verify Your Email')}</h2>
<p className="card__subtitle">
{/* @if TARGET='app' */}
{__("We'll let you know about LBRY updates, security issues, and great new content.")}
{/* @endif */}
{/* @if TARGET='web' */}
{__('Stay up to date with lbry.tv and be the first to know about the progress we make.')}
{/* @endif */}
</p>
<Form onSubmit={this.handleSubmit}>
<FormField <FormField
type="email" type="email"
label="Email" id="sign_up_email"
placeholder="youremail@example.org" label={__('Email')}
name="email" value={newEmail}
value={this.state.email}
error={errorMessage} error={errorMessage}
onChange={this.handleEmailChanged} onChange={e => setEmail(e.target.value)}
inputButton={
<Button type="submit" button="inverse" label="Submit" disabled={isPending || !this.state.email} />
}
/> />
<FormField
type="checkbox"
id="sign_up_sync"
label={__('Sync my bidnezz on this device')}
helper={__('Maybe some additional text with this field')}
checked={sync}
onChange={() => setSync(!sync)}
/>
<Button button="primary" type="submit" label={__('Continue')} disabled={isPending} />
</Form> </Form>
<div className="card__actions">{cancelButton}</div>
<p className="help">{__('Your email address will never be sold and you can unsubscribe at any time.')}</p>
</React.Fragment>
); );
} }
}
export default UserEmailNew; export default UserEmailNew;

View file

@ -18,7 +18,7 @@ function UserEmailResetButton(props: Props) {
} }
: { href: 'https://lbry.com/faq/how-to-change-email' }; : { href: 'https://lbry.com/faq/how-to-change-email' };
return <Button button={button} label={__('Change')} {...buttonsProps} />; return <Button button={button} label={__('Change... fix me sean')} {...buttonsProps} />;
} }
export default UserEmailResetButton; export default UserEmailResetButton;

View file

@ -24,7 +24,8 @@ class UserEmailVerify extends React.PureComponent<Props> {
} }
componentDidUpdate() { componentDidUpdate() {
if (this.emailVerifyCheckInterval && this.props.user.has_verified_email) { const { user } = this.props;
if (this.emailVerifyCheckInterval && user && user.has_verified_email) {
clearInterval(this.emailVerifyCheckInterval); clearInterval(this.emailVerifyCheckInterval);
} }
} }
@ -51,8 +52,6 @@ class UserEmailVerify extends React.PureComponent<Props> {
return ( return (
<React.Fragment> <React.Fragment>
<h2 className="card__title">{__('Waiting For Verification')}</h2>
<p className="card__subtitle"> <p className="card__subtitle">
{__('An email was sent to')} {email}.{' '} {__('An email was sent to')} {email}.{' '}
{__('Follow the link and you will be good to go. This will update automatically.')} {__('Follow the link and you will be good to go. This will update automatically.')}
@ -61,7 +60,7 @@ class UserEmailVerify extends React.PureComponent<Props> {
<div className="card__actions"> <div className="card__actions">
<Button <Button
button="primary" button="primary"
label={__('Resend verification email')} label={__('Resend Verification Email')}
onClick={this.handleResendVerificationEmail} onClick={this.handleResendVerificationEmail}
/> />
<UserEmailResetButton /> <UserEmailResetButton />

View file

@ -0,0 +1,28 @@
import { connect } from 'react-redux';
import { selectUser, selectEmailToVerify, rewards as REWARD_TYPES, doClaimRewardType } from 'lbryinc';
import { doCreateChannel, selectCreatingChannel, selectMyChannelClaims } from 'lbry-redux';
import UserSignUp from './view';
const select = state => ({
email: selectEmailToVerify(state),
user: selectUser(state),
creatingChannel: selectCreatingChannel(state),
channels: selectMyChannelClaims(state),
});
const perform = dispatch => ({
createChannel: (name, amount) => dispatch(doCreateChannel(name, amount)),
claimReward: cb =>
dispatch(
doClaimRewardType(REWARD_TYPES.TYPE_REWARD_CODE, {
notifyError: true,
successCallback: cb,
params: { code: 'sean-test' },
})
),
});
export default connect(
select,
perform
)(UserSignUp);

View file

@ -0,0 +1,65 @@
// @flow
import React, { useState } from 'react';
import { isNameValid } from 'lbry-redux';
import Button from 'component/button';
import { Form, FormField } from 'component/common/form';
const DEFAULT_BID_FOR_FIRST_CHANNEL = 0.1;
type Props = {
createChannel: (string, number) => void,
claimReward: (() => void) => void,
creatingChannel: boolean,
};
function UserFirstChannel(props: Props) {
const { createChannel, creatingChannel, claimReward } = props;
const [channel, setChannel] = useState('');
const [nameError, setNameError] = useState();
function handleCreateChannel() {
claimReward(() => {
createChannel(`@${channel}`, DEFAULT_BID_FOR_FIRST_CHANNEL);
});
}
function handleChannelChange(e) {
const { value } = e.target;
setChannel(value);
if (!isNameValid(value, false)) {
setNameError(__('LBRY names cannot contain spaces or reserved symbols ($#@;/"<>%{}|^~[]`)'));
} else {
setNameError();
}
}
return (
<Form onSubmit={handleCreateChannel}>
<h1 className="card__title--large">{__('Choose Your Channel Name')}</h1>
<p className="card__subtitle">
{__("Normally this would cost LBRY credits, but we're hooking you up for your first one.")}
</p>
<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 type="text" name="auth_first_channel" value={channel} onChange={handleChannelChange} />
</fieldset-group>
<div className="card__actions">
<Button
button="primary"
type="submit"
disabled={nameError || !channel || creatingChannel}
label={__('Create')}
onClick={handleCreateChannel}
/>
</div>
</Form>
);
}
export default UserFirstChannel;

View file

@ -0,0 +1,27 @@
import { connect } from 'react-redux';
import {
selectEmailToVerify,
doUserResendVerificationEmail,
doUserCheckEmailVerified,
selectUser,
doFetchAccessToken,
selectAccessToken,
} from 'lbryinc';
import UserSignUp from './view';
const select = state => ({
email: selectEmailToVerify(state),
user: selectUser(state),
accessToken: selectAccessToken(state),
});
const perform = dispatch => ({
resendVerificationEmail: email => dispatch(doUserResendVerificationEmail(email)),
checkEmailVerified: () => dispatch(doUserCheckEmailVerified()),
fetchAccessToken: () => dispatch(doFetchAccessToken()),
});
export default connect(
select,
perform
)(UserSignUp);

View file

@ -0,0 +1,32 @@
// @flow
import React from 'react';
import UserEmailNew from 'component/userEmailNew';
import UserEmailVerify from 'component/userEmailVerify';
type Props = {
user: ?User,
email: ?string,
};
function UserSignUp(props: Props) {
const { email, user } = props;
const verifiedEmail = user && email && user.has_verified_email;
function getTitle() {
if (!email) {
return __('Get Rockin');
} else if (email && !verifiedEmail) {
return __('We Sent You An Email');
}
}
return (
<section>
<h1 className="card__title--large">{getTitle()}</h1>
{!email && <UserEmailNew />}
{email && !verifiedEmail && <UserEmailVerify />}
</section>
);
}
export default UserSignUp;

View file

@ -50,12 +50,11 @@ class UserVerify extends React.PureComponent<Props> {
button="inverse" button="inverse"
label={__('Submit Phone Number')} label={__('Submit Phone Number')}
/> />
</div>
<div className="help"> <div className="help">
{__('Standard messaging rates apply. LBRY will not text or call you otherwise. Having trouble?')}{' '} {__('Standard messaging rates apply. LBRY will not text or call you otherwise. Having trouble?')}{' '}
<Button button="link" href="https://lbry.com/faq/phone" label={__('Read more.')} /> <Button button="link" href="https://lbry.com/faq/phone" label={__('Read more.')} />
</div> </div>
</div>
</section> </section>
<section className="card card--section"> <section className="card card--section">

View file

@ -8,7 +8,7 @@ import * as ACTIONS from 'constants/action_types';
import { ipcRenderer, remote, shell } from 'electron'; import { ipcRenderer, remote, shell } from 'electron';
import moment from 'moment'; import moment from 'moment';
import * as MODALS from 'constants/modal_types'; import * as MODALS from 'constants/modal_types';
import React, { useState, useEffect } from 'react'; import React, { Fragment, useState, useEffect } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { doConditionalAuthNavigate, doDaemonReady, doAutoUpdate, doOpenModal, doHideModal } from 'redux/actions/app'; import { doConditionalAuthNavigate, doDaemonReady, doAutoUpdate, doOpenModal, doHideModal } from 'redux/actions/app';
@ -237,7 +237,7 @@ function AppWrapper() {
return ( return (
<Provider store={store}> <Provider store={store}>
<PersistGate persistor={persistor} loading={<div className="main--launching" />}> <PersistGate persistor={persistor} loading={<div className="main--launching" />}>
<div> <Fragment>
{readyToLaunch ? ( {readyToLaunch ? (
<ConnectedRouter history={history}> <ConnectedRouter history={history}>
<ErrorBoundary> <ErrorBoundary>
@ -251,7 +251,7 @@ function AppWrapper() {
onReadyToLaunch={() => setReadyToLaunch(true)} onReadyToLaunch={() => setReadyToLaunch(true)}
/> />
)} )}
</div> </Fragment>
</PersistGate> </PersistGate>
</Provider> </Provider>
); );

View file

@ -1,4 +1,5 @@
// @flow // @flow
import * as PAGES from 'constants/pages';
import React, { PureComponent, Fragment } from 'react'; import React, { PureComponent, Fragment } from 'react';
import BusyIndicator from 'component/common/busy-indicator'; import BusyIndicator from 'component/common/busy-indicator';
import RewardListClaimed from 'component/rewardListClaimed'; import RewardListClaimed from 'component/rewardListClaimed';
@ -39,15 +40,18 @@ class RewardsPage extends PureComponent<Props> {
return ( return (
!IS_WEB && ( !IS_WEB && (
<section className="card card--section"> <section className="card card--section">
<h2 className="card__title">{__('Rewards Approval to Earn Credits (LBC)')}</h2> <h2 className="card__title">{__('Sign In To Unlock Rewards')}</h2>
<p className="card__subtitle"> <p className="card__subtitle">
{__( {__(
'This step is optional. You can continue to use this app without rewards, but LBC may be needed for some tasks.' 'This is optional. You can continue to use this app without rewards, but LBC may be needed for some tasks.'
)}{' '} )}
<Button button="link" label={__('Learn more')} href="https://lbry.com/faq/rewards" />.
</p> </p>
<Button navigate="/$/auth?redirect=rewards" button="primary" label="Prove Humanity" /> <Button
navigate={`/$/${PAGES.AUTH}/signin?redirect=rewards`}
button="primary"
label={__('Unlock Rewards')}
/>
</section> </section>
) )
); );

View file

@ -0,0 +1,15 @@
import { connect } from 'react-redux';
import { selectEmailToVerify, selectUser } from 'lbryinc';
import { selectMyChannelClaims } from 'lbry-redux';
import SignUpPage from './view';
const select = state => ({
email: selectEmailToVerify(state),
user: selectUser(state),
channels: selectMyChannelClaims(state),
});
export default connect(
select,
null
)(SignUpPage);

View file

@ -0,0 +1,42 @@
// @flow
import React from 'react';
import UserSignIn from 'component/userSignIn';
import UserFirstChannel from 'component/userFirstChannel';
import UserVerify from 'component/userVerify';
import Page from 'component/page';
type Props = {
user: ?User,
channels: ?Array<ChannelClaim>,
location: { search: string },
history: { replace: string => void },
};
export default function SignInPage(props: Props) {
const { user, channels, location, history } = props;
const { search } = location;
const urlParams = new URLSearchParams(search);
const redirect = urlParams.get('redirect');
const hasVerifiedEmail = user && user.has_verified_email;
const rewardsApproved = user && user.is_reward_approved;
const channelCount = channels ? channels.length : 0;
const showWelcome = !hasVerifiedEmail || channelCount === 0;
if (rewardsApproved && channelCount > 0) {
history.replace(redirect ? `/$/${redirect}` : '/');
}
return (
<Page fullscreen className="main--auth-page">
{showWelcome && (
<div className="columns">
{!hasVerifiedEmail && <UserSignIn />}
{hasVerifiedEmail && channelCount === 0 && <UserFirstChannel />}
<div style={{ width: '100%', height: '20rem', borderRadius: 20, backgroundColor: '#ffc7e6' }} />
</div>
)}
{hasVerifiedEmail && channelCount > 0 && <UserVerify />}
</Page>
);
}

View file

@ -49,6 +49,12 @@
box-shadow: none; box-shadow: none;
} }
.card--fill {
min-width: 25rem;
margin-left: auto;
margin-right: auto;
}
// C A R D // C A R D
// A C T I O N S // A C T I O N S
@ -189,6 +195,12 @@
} }
} }
.card__title--large {
@extend .card__title;
font-weight: 700;
font-size: var(--font-heading);
}
.card__title--between { .card__title--between {
@extend .card__title; @extend .card__title;
justify-content: space-between; justify-content: space-between;

View file

@ -85,6 +85,10 @@ fieldset-section:last-child {
checkbox-element, checkbox-element,
radio-element { radio-element {
&:hover {
cursor: pointer !important;
}
label { label {
color: lighten($lbry-black, 20%); color: lighten($lbry-black, 20%);
margin-bottom: 0; margin-bottom: 0;
@ -102,6 +106,9 @@ radio-element {
} }
checkbox-toggle { checkbox-toggle {
border-width: 1px;
border-radius: var(--input-border-radius);
&:before { &:before {
background-position: center; background-position: center;
background-repeat: no-repeat; background-repeat: no-repeat;
@ -155,6 +162,10 @@ fieldset-group {
label { label {
min-height: 18px; min-height: 18px;
white-space: nowrap;
// Set width 0 and overflow visible so the label can act as if it's the input label and not a random text node in a side by side div
overflow: visible;
width: 0;
} }
fieldset-section:first-child .form-field__prefix, fieldset-section:first-child .form-field__prefix,

View file

@ -20,6 +20,12 @@
} }
} }
.header--minimal {
box-shadow: none;
background-color: transparent;
border-bottom: none;
}
.header__contents { .header__contents {
max-width: var(--page-max-width); max-width: var(--page-max-width);
height: calc(var(--header-height) - 1px); height: calc(var(--header-height) - 1px);
@ -47,6 +53,10 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
.button {
margin-left: auto;
}
} }
.header__navigation-arrows { .header__navigation-arrows {

View file

@ -1,6 +1,6 @@
.main-wrapper { .main-wrapper {
position: relative; position: relative;
min-height: 100vh; min-height: 80vh;
[data-mode='dark'] & { [data-mode='dark'] & {
background-color: var(--dm-color-08); background-color: var(--dm-color-08);
@ -41,6 +41,13 @@
} }
} }
.main--auth-page {
max-width: 60rem;
margin-top: calc(var(--spacing-main-padding) * 2);
margin-left: auto;
margin-right: auto;
}
.main--empty { .main--empty {
align-items: center; align-items: center;
display: flex; display: flex;
@ -56,6 +63,24 @@
background-color: var(--color-background); background-color: var(--color-background);
} }
.main--fullscreen {
width: 100vw;
height: 100vh;
z-index: 9999;
background-color: $lbry-white;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: 0;
margin-top: 0;
* {
z-index: 10000;
}
}
.main__status { .main__status {
@extend .card__subtitle; @extend .card__subtitle;
display: flex; display: flex;

View file

@ -89,7 +89,6 @@
color: lighten($lbry-black, 20%); color: lighten($lbry-black, 20%);
.icon { .icon {
margin-right: var(--spacing-small);
stroke: $lbry-gray-5; stroke: $lbry-gray-5;
} }
@ -101,3 +100,16 @@
} }
} }
} }
.menu__link {
.icon {
margin-right: var(--spacing-small);
stroke: $lbry-gray-5;
}
}
.menu__title {
span {
margin-left: var(--spacing-miniscule);
}
}

View file

@ -24,7 +24,6 @@ body {
cursor: default; cursor: default;
font-size: 1rem; font-size: 1rem;
font-weight: 400; font-weight: 400;
height: 100%;
line-height: 1.5; line-height: 1.5;
background-color: mix($lbry-white, $lbry-gray-1, 70%); background-color: mix($lbry-white, $lbry-gray-1, 70%);
@ -107,10 +106,13 @@ a {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: flex-start; align-items: flex-start;
flex-wrap: wrap;
> * { > * {
flex-grow: 1; flex-grow: 1;
flex-basis: 0; flex-basis: 0;
min-width: 15rem;
margin-bottom: var(--spacing-large);
&:first-child { &:first-child {
margin-right: 1.5rem; margin-right: 1.5rem;

View file

@ -1,26 +0,0 @@
export function formatCredits(amount, precision = 1) {
return parseFloat(amount)
.toFixed(precision || 1)
.replace(/\.?0+$/, '');
}
export function formatFullPrice(amount, precision = 1) {
if (!amount) {
return 0;
}
let formated = '';
const quantity = amount.toString().split('.');
const fraction = quantity[1];
if (fraction) {
const decimals = fraction.split('');
const first = decimals.filter(number => number !== '0')[0];
const index = decimals.indexOf(first);
// Set format fraction
formated = `.${fraction.substring(0, index + precision)}`;
}
return parseFloat(quantity[0] + formated);
}

View file

@ -638,7 +638,6 @@
"Refreshed!": "Refreshed!", "Refreshed!": "Refreshed!",
"Starting...": "Starting...", "Starting...": "Starting...",
"Spin Spin Sugar": "Spin Spin Sugar", "Spin Spin Sugar": "Spin Spin Sugar",
"URI does not include name.": "URI does not include name.",
"You're not following any channels.": "You're not following any channels.", "You're not following any channels.": "You're not following any channels.",
"Look what's trending for everyone": "Look what's trending for everyone", "Look what's trending for everyone": "Look what's trending for everyone",
"or": "or", "or": "or",