Seed Support #56

Closed
ocnios wants to merge 173 commits from master into build
39 changed files with 1120 additions and 1166 deletions
Showing only changes of commit ecf54f400b - Show all commits

View file

@ -2,22 +2,18 @@ import React from 'react';
import {Line} from 'rc-progress'; import {Line} from 'rc-progress';
import lbry from './lbry.js'; import lbry from './lbry.js';
import lbryio from './lbryio.js';
import EmailPage from './page/email.js'; import EmailPage from './page/email.js';
import SettingsPage from './page/settings.js'; import SettingsPage from './page/settings.js';
import HelpPage from './page/help.js'; import HelpPage from './page/help.js';
import WatchPage from './page/watch.js'; import WatchPage from './page/watch.js';
import ReportPage from './page/report.js'; import ReportPage from './page/report.js';
import StartPage from './page/start.js'; import StartPage from './page/start.js';
import ClaimCodePage from './page/claim_code.js';
import ReferralPage from './page/referral.js';
import RewardsPage from './page/rewards.js'; import RewardsPage from './page/rewards.js';
import RewardPage from './page/reward.js'; import RewardPage from './page/reward.js';
import WalletPage from './page/wallet.js'; import WalletPage from './page/wallet.js';
import ShowPage from './page/show.js'; import ShowPage from './page/show.js';
import PublishPage from './page/publish.js'; import PublishPage from './page/publish.js';
import DiscoverPage from './page/discover.js'; import DiscoverPage from './page/discover.js';
import SplashScreen from './component/splash.js';
import DeveloperPage from './page/developer.js'; import DeveloperPage from './page/developer.js';
import {FileListDownloaded, FileListPublished} from './page/file-list.js'; import {FileListDownloaded, FileListPublished} from './page/file-list.js';
import Drawer from './component/drawer.js'; import Drawer from './component/drawer.js';
@ -229,15 +225,11 @@ var App = React.createClass({
case 'wallet': case 'wallet':
case 'send': case 'send':
case 'receive': case 'receive':
case 'claim':
case 'referral':
case 'rewards': case 'rewards':
return { return {
'?wallet': 'Overview', '?wallet': 'Overview',
'?send': 'Send', '?send': 'Send',
'?receive': 'Receive', '?receive': 'Receive',
'?claim': 'Claim Beta Code',
'?referral': 'Check Referral Credit',
'?rewards': 'Rewards', '?rewards': 'Rewards',
}; };
case 'downloaded': case 'downloaded':
@ -268,14 +260,8 @@ var App = React.createClass({
return <FileListPublished />; return <FileListPublished />;
case 'start': case 'start':
return <StartPage />; return <StartPage />;
case 'claim':
return <ClaimCodePage />;
case 'referral':
return <ReferralPage />;
case 'rewards': case 'rewards':
return <RewardsPage />; return <RewardsPage />;
case 'reward':
return <RewardPage name={this.state.pageArgs} />;
case 'wallet': case 'wallet':
case 'send': case 'send':
case 'receive': case 'receive':

View file

@ -1,10 +1,11 @@
import React from 'react'; import React from 'react';
import lbryio from '../lbryio.js'; import lbryio from '../lbryio.js';
import Modal from './modal.js';
import ModalPage from './modal-page.js'; import ModalPage from './modal-page.js';
import {Link} from '../component/link.js'; import {Link, RewardLink} from '../component/link.js';
import FormField from '../component/form.js'; import {FormField, FormRow} from '../component/form.js';
import Notice from '../component/notice.js' import rewards from '../rewards.js';
const SubmitEmailStage = React.createClass({ const SubmitEmailStage = React.createClass({
@ -29,8 +30,8 @@ const SubmitEmailStage = React.createClass({
lbryio.call('user_email', 'new', {email: this.state.email}, 'post').then(() => { lbryio.call('user_email', 'new', {email: this.state.email}, 'post').then(() => {
this.props.onEmailSaved(); this.props.onEmailSaved();
}, (error) => { }, (error) => {
if (this._emailField) { if (this._emailRow) {
this._emailField.showError(error.message) this._emailRow.showError(error.message)
} }
this.setState({ submitting: false }); this.setState({ submitting: false });
}); });
@ -39,10 +40,10 @@ const SubmitEmailStage = React.createClass({
return ( return (
<section> <section>
<form onSubmit={this.handleSubmit}> <form onSubmit={this.handleSubmit}>
<FormField row={true} ref={(field) => { this._emailField = field }} type="text" label="Email" placeholder="webmaster@toplbryfan.com" <FormRow ref={(ref) => { this._emailRow = ref }} type="text" label="Email" placeholder="webmaster@toplbryfan.com"
name="email" value={this.state.email} name="email" value={this.state.email}
onChange={this.handleEmailChanged} /> onChange={this.handleEmailChanged} />
<div className="form-row"> <div className="form-row-submit">
<Link button="primary" label="Next" disabled={this.state.submitting} onClick={this.handleSubmit} /> <Link button="primary" label="Next" disabled={this.state.submitting} onClick={this.handleSubmit} />
</div> </div>
</form> </form>
@ -71,27 +72,29 @@ const ConfirmEmailStage = React.createClass({
submitting: true, submitting: true,
}); });
lbryio.call('user_email', 'confirm', {verification_token: this.state.code}, 'post').then(() => { const onSubmitError = function(error) {
rewards.claimReward('confirm_email').then(() => { if (this._codeRow) {
this.props.onDone(); this._codeRow.showError(error.message)
}, (err) => {l
this.props.onDone();
});
}, (error) => {
if (this._codeField) {
this._codeField.showError(error.message)
this.setState({ submitting: false })
} }
}); this.setState({ submitting: false });
}.bind(this)
lbryio.call('user_email', 'confirm', {verification_token: this.state.code}, 'post').then((userEmail) => {
if (userEmail.IsVerified) {
this.props.onEmailConfirmed();
} else {
onSubmitError(new Error("Your email is still not verified.")) //shouldn't happen?
}
}, onSubmitError);
}, },
render: function() { render: function() {
return ( return (
<section> <section>
<form onSubmit={this.handleSubmit}> <form onSubmit={this.handleSubmit}>
<FormField row={true} label="Verification Code" ref={(field) => { this._codeField = field }} type="text" <FormRow label="Verification Code" ref={(ref) => { this._codeRow = ref }} type="text"
name="code" placeholder="a94bXXXXXXXXXXXXXX" value={this.state.code} onChange={this.handleCodeChanged} name="code" placeholder="a94bXXXXXXXXXXXXXX" value={this.state.code} onChange={this.handleCodeChanged}
helper="A verification code is required to access this version."/> helper="A verification code is required to access this version."/>
<div className="form-row"> <div className="form-row-submit">
<Link button="primary" label="Verify" disabled={this.state.submitting} onClick={this.handleSubmit} /> <Link button="primary" label="Verify" disabled={this.state.submitting} onClick={this.handleSubmit} />
</div> </div>
</form> </form>
@ -100,9 +103,29 @@ const ConfirmEmailStage = React.createClass({
} }
}); });
const WelcomeStage = React.createClass({
onRewardClaim: function() {
console.log('omg');
},
render: function() {
return (
<section>
<h3 className="modal__header">Welcome to LBRY.</h3>
<p>LBRY is the first community controlled content marketplace.</p>
<p>Since you're new here, we'll toss you some credits.</p>
<div style={{textAlign: "center", marginBottom: "12px"}}>
<RewardLink type="new_user" onRewardClaim={this.onRewardClaim} />
</div>
<p>LBC is blah blah blah.</p>
<p>And remember, LBRY is a beta and be safe!</p>
</section>
);
}
});
const ErrorStage = React.createClass({ const ErrorStage = React.createClass({
render: function() { render: function() {
// <section><Link button="primary" label="OK" onClick={this.props.onDone} /></section>
return ( return (
<section> <section>
<p>An error was encountered that we cannot continue from.</p> <p>An error was encountered that we cannot continue from.</p>
@ -129,68 +152,66 @@ export const AuthOverlay = React.createClass({
error: ErrorStage, error: ErrorStage,
email: SubmitEmailStage, email: SubmitEmailStage,
confirm: ConfirmEmailStage, confirm: ConfirmEmailStage,
welcome: WelcomeStage,
}, },
propTypes: {
// onDone: React.PropTypes.func.isRequired,
},
getInitialState: function() { getInitialState: function() {
return { return {
stage: "pending", stage: "welcome",
stageProps: {} stageProps: {}
}; };
}, },
componentWillMount: function() { endAuth: function() {
lbryio.authenticate().then(function(user) { this.setState({
console.log(user); stage: null
if (!user.HasVerifiedEmail) { });
this.setState({ },
stage: "email", componentWillMount: function() {
stageProps: { // lbryio.authenticate().then(function(user) {
onEmailSaved: function() { // if (!user.HasVerifiedEmail) { //oops I fucked this up
this.setState({ // this.setState({
stage: "confirm" // stage: "email",
}) // stageProps: {
}.bind(this) // onEmailSaved: function() {
} // this.setState({
}) // stage: "confirm",
} else { // stageProps: {
this.setState({ stage: null }) // onEmailConfirmed: function() { this.setState({ stage: "welcome"}) }.bind(this)
} // }
}.bind(this)).catch((err) => { // })
this.setState({ // }.bind(this)
stage: "error", // }
stageProps: { errorText: err.message } // })
}) // } else {
document.dispatchEvent(new CustomEvent('unhandledError', { // this.endAuth()
detail: { // }
message: err.message, // }.bind(this)).catch((err) => {
data: err.stack // this.setState({
} // stage: "error",
})); // stageProps: { errorText: err.message }
}) // })
// document.dispatchEvent(new CustomEvent('unhandledError', {
// detail: {
// message: err.message,
// data: err.stack
// }
// }));
// })
}, },
// handleStageDone: function() {
// if (this.state.stageNum >= this._stages.length - 1) {
// this.props.onDone();
// } else {
// this.setState({
// stageNum: this.state.stageNum + 1,
// });
// }
// },
// <Content onDone={this.handleStageDone} />
render: function() { render: function() {
console.log(lbryio.user);
if (!this.state.stage || lbryio.user && lbryio.user.HasVerifiedEmail) { if (!this.state.stage || lbryio.user && lbryio.user.HasVerifiedEmail) {
return null; return null;
} }
const StageContent = this._stages[this.state.stage]; const StageContent = this._stages[this.state.stage];
return ( return (
<ModalPage isOpen={true} contentLabel="Authentication" {...this.props}> this.state.stage != "welcome" ?
<h1>LBRY Early Access</h1> <ModalPage className="modal-page--full"isOpen={true} contentLabel="Authentication" {...this.props}>
<StageContent {...this.state.stageProps} /> <h1>LBRY Early Access</h1>
</ModalPage> <StageContent {...this.state.stageProps} />
</ModalPage> :
<Modal isOpen={true} contentLabel="Welcome to LBRY" {...this.props} onConfirmed={this.endAuth}>
<StageContent {...this.state.stageProps} />
</Modal>
); );
} }
}); });

View file

@ -55,7 +55,7 @@ var Drawer = React.createClass({
<DrawerItem href='?discover' viewingPage={this.props.viewingPage} label="Discover" icon="icon-search" /> <DrawerItem href='?discover' viewingPage={this.props.viewingPage} label="Discover" icon="icon-search" />
<DrawerItem href='?publish' viewingPage={this.props.viewingPage} label="Publish" icon="icon-upload" /> <DrawerItem href='?publish' viewingPage={this.props.viewingPage} label="Publish" icon="icon-upload" />
<DrawerItem href='?downloaded' subPages={['published']} viewingPage={this.props.viewingPage} label="My Files" icon='icon-cloud-download' /> <DrawerItem href='?downloaded' subPages={['published']} viewingPage={this.props.viewingPage} label="My Files" icon='icon-cloud-download' />
<DrawerItem href="?wallet" subPages={['send', 'receive', 'claim', 'referral']} viewingPage={this.props.viewingPage} label="My Wallet" badge={lbry.formatCredits(this.state.balance) } icon="icon-bank" /> <DrawerItem href="?wallet" subPages={['send', 'receive', 'rewards']} viewingPage={this.props.viewingPage} label="My Wallet" badge={lbry.formatCredits(this.state.balance) } icon="icon-bank" />
<DrawerItem href='?settings' viewingPage={this.props.viewingPage} label="Settings" icon='icon-gear' /> <DrawerItem href='?settings' viewingPage={this.props.viewingPage} label="Settings" icon='icon-gear' />
<DrawerItem href='?help' viewingPage={this.props.viewingPage} label="Help" icon='icon-question-circle' /> <DrawerItem href='?help' viewingPage={this.props.viewingPage} label="Help" icon='icon-question-circle' />
</nav> </nav>

View file

@ -3,7 +3,7 @@ import lbry from '../lbry.js';
import {Link} from '../component/link.js'; import {Link} from '../component/link.js';
import {Icon} from '../component/common.js'; import {Icon} from '../component/common.js';
import Modal from './modal.js'; import Modal from './modal.js';
import FormField from './form.js'; import {FormField} from './form.js';
import {ToolTip} from '../component/tooltip.js'; import {ToolTip} from '../component/tooltip.js';
import {DropDownMenu, DropDownMenuItem} from './menu.js'; import {DropDownMenu, DropDownMenuItem} from './menu.js';
@ -53,7 +53,7 @@ let WatchLink = React.createClass({
render: function() { render: function() {
return ( return (
<div className="button-set-item"> <div className="button-set-item">
<Link button="primary" disabled={this.state.loading} label="Watch" icon="icon-play" onClick={this.handleClick} /> <Link button={ this.props.button ? this.props.button : 'alt' } disabled={this.state.loading} label="Watch" icon="icon-play" onClick={this.handleClick} />
<Modal contentLabel="Not enough credits" isOpen={this.state.modal == 'notEnoughCredits'} onConfirmed={this.closeModal}> <Modal contentLabel="Not enough credits" isOpen={this.state.modal == 'notEnoughCredits'} onConfirmed={this.closeModal}>
You don't have enough LBRY credits to pay for this stream. You don't have enough LBRY credits to pay for this stream.
</Modal> </Modal>

View file

@ -47,7 +47,7 @@ let FilePrice = React.createClass({
} }
return ( return (
<span className="file-tile__cost"> <span className="file-price">
<CreditAmount amount={this.state.cost} isEstimate={!this.state.costIncludesData}/> <CreditAmount amount={this.state.cost} isEstimate={!this.state.costIncludesData}/>
</span> </span>
); );
@ -131,8 +131,8 @@ export let FileTileStream = React.createClass({
const title = isConfirmed ? metadata.title : lbryUri; const title = isConfirmed ? metadata.title : lbryUri;
const obscureNsfw = this.props.obscureNsfw && isConfirmed && metadata.nsfw; const obscureNsfw = this.props.obscureNsfw && isConfirmed && metadata.nsfw;
return ( return (
<section className={ 'file-tile card ' + (obscureNsfw ? 'card-obscured ' : '') } onMouseEnter={this.handleMouseOver} onMouseLeave={this.handleMouseOut}> <section className={ 'file-tile card ' + (obscureNsfw ? 'card--obscured ' : '') } onMouseEnter={this.handleMouseOver} onMouseLeave={this.handleMouseOut}>
<div className={"row-fluid card-content file-tile__row"}> <div className={"row-fluid card__inner file-tile__row"}>
<div className="span3"> <div className="span3">
<a href={'?show=' + lbryUri}><Thumbnail className="file-tile__thumbnail" src={metadata.thumbnail} alt={'Photo for ' + (title || this.props.uri)} /></a> <a href={'?show=' + lbryUri}><Thumbnail className="file-tile__thumbnail" src={metadata.thumbnail} alt={'Photo for ' + (title || this.props.uri)} /></a>
</div> </div>
@ -140,24 +140,139 @@ export let FileTileStream = React.createClass({
{ !this.props.hidePrice { !this.props.hidePrice
? <FilePrice uri={this.props.uri} /> ? <FilePrice uri={this.props.uri} />
: null} : null}
<div className="meta"><a href={'?show=' + this.props.uri}>{lbryUri}</a></div> <div className="card__title-primary">
<h3 className="file-tile__title"> <div className="meta"><a href={'?show=' + this.props.uri}>{lbryUri}</a></div>
<h3>
<a href={'?show=' + this.props.uri}>
<TruncatedText lines={1}>
{title}
</TruncatedText>
</a>
</h3>
</div>
<div className="card__actions">
<FileActions uri={this.props.uri} outpoint={this.props.outpoint} metadata={metadata} contentType={this.props.contentType} />
</div>
<div className="card__content">
<p className="file-tile__description">
<TruncatedText lines={3}>
{isConfirmed
? metadata.description
: <span className="empty">This file is pending confirmation.</span>}
</TruncatedText>
</p>
</div>
</div>
</div>
{this.state.showNsfwHelp
? <div className='card-overlay'>
<p>
This content is Not Safe For Work.
To view adult content, please change your <Link className="button-text" href="?settings" label="Settings" />.
</p>
</div>
: null}
</section>
);
}
});
export let FileCardStream = React.createClass({
_fileInfoSubscribeId: null,
_isMounted: null,
propTypes: {
metadata: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.object]),
outpoint: React.PropTypes.string,
hideOnRemove: React.PropTypes.bool,
hidePrice: React.PropTypes.bool,
obscureNsfw: React.PropTypes.bool
},
getInitialState: function() {
return {
showNsfwHelp: false,
isHidden: false,
available: null,
}
},
getDefaultProps: function() {
return {
obscureNsfw: !lbry.getClientSetting('showNsfw'),
hidePrice: false
}
},
componentDidMount: function() {
this._isMounted = true;
if (this.props.hideOnRemove) {
this._fileInfoSubscribeId = lbry.fileInfoSubscribe(this.props.outpoint, this.onFileInfoUpdate);
}
},
componentWillUnmount: function() {
if (this._fileInfoSubscribeId) {
lbry.fileInfoUnsubscribe(this.props.outpoint, this._fileInfoSubscribeId);
}
},
onFileInfoUpdate: function(fileInfo) {
if (!fileInfo && this._isMounted && this.props.hideOnRemove) {
this.setState({
isHidden: true
});
}
},
handleMouseOver: function() {
if (this.props.obscureNsfw && this.props.metadata && this.props.metadata.nsfw) {
this.setState({
showNsfwHelp: true,
});
}
},
handleMouseOut: function() {
if (this.state.showNsfwHelp) {
this.setState({
showNsfwHelp: false,
});
}
},
render: function() {
if (this.state.isHidden) {
return null;
}
const lbryUri = uri.normalizeLbryUri(this.props.uri);
const metadata = this.props.metadata;
const isConfirmed = typeof metadata == 'object';
const title = isConfirmed ? metadata.title : lbryUri;
const obscureNsfw = this.props.obscureNsfw && isConfirmed && metadata.nsfw;
return (
<section className={ 'card card--small ' + (obscureNsfw ? 'card--obscured ' : '') } onMouseEnter={this.handleMouseOver} onMouseLeave={this.handleMouseOut}>
<div className="card__inner">
<div className="card__title-identity">
<h4>
<a href={'?show=' + this.props.uri}> <a href={'?show=' + this.props.uri}>
<TruncatedText lines={1}> <TruncatedText lines={1}>
{title} {title}
</TruncatedText> </TruncatedText>
</a> </a>
</h3> </h4>
<div className="card__subtitle"><a href={'?show=' + lbryUri}>{lbryUri}</a></div></div>
<ChannelIndicator uri={lbryUri} metadata={metadata} contentType={this.props.contentType} <ChannelIndicator uri={lbryUri} metadata={metadata} contentType={this.props.contentType}
hasSignature={this.props.hasSignature} signatureIsValid={this.props.signatureIsValid} /> hasSignature={this.props.hasSignature} signatureIsValid={this.props.signatureIsValid} />
<FileActions uri={this.props.uri} outpoint={this.props.outpoint} metadata={metadata} contentType={this.props.contentType} />
<p className="file-tile__description"> <div className="card__media">
<TruncatedText lines={3}> <a href={'?show=' + this.props.uri}><Thumbnail src={metadata.thumbnail} alt={'Photo for ' + (title || this.props.uri)} /></a>
</div>
{ !this.props.hidePrice
? <FilePrice uri={this.props.uri} />
: null}
<div className="card__content card__subtext card__subtext--two-lines">
<TruncatedText lines={2}>
{isConfirmed {isConfirmed
? metadata.description ? metadata.description
: <span className="empty">This file is pending confirmation.</span>} : <span className="empty">This file is pending confirmation.</span>}
</TruncatedText> </TruncatedText>
</p> </div>
<div className="card__actions card__actions--bottom">
<FileActions uri={this.props.uri} outpoint={this.props.outpoint} metadata={metadata} contentType={this.props.contentType} />
</div> </div>
</div> </div>
{this.state.showNsfwHelp {this.state.showNsfwHelp

View file

@ -1,31 +1,30 @@
import React from 'react'; import React from 'react';
import {Icon} from './common.js'; import {Icon} from './common.js';
var requiredFieldWarningStyle = { var formFieldCounter = 0,
color: '#cc0000', formFieldNestedLabelTypes = ['radio', 'checkbox'];
transition: 'opacity 400ms ease-in',
};
var formFieldCounter = 0; function formFieldId() {
return "form-field-" + (++formFieldCounter);
}
var FormField = React.createClass({ export let FormField = React.createClass({
_fieldRequiredText: 'This field is required', _fieldRequiredText: 'This field is required',
_type: null, _type: null,
_element: null, _element: null,
propTypes: { propTypes: {
type: React.PropTypes.string.isRequired, type: React.PropTypes.string.isRequired,
row: React.PropTypes.bool, hasError: React.PropTypes.bool
hidden: React.PropTypes.bool,
}, },
getInitialState: function() { getInitialState: function() {
return { return {
errorState: 'hidden', isError: null,
adviceText: null, errorMessage: null,
} }
}, },
componentWillMount: function() { componentWillMount: function() {
if (['text', 'radio', 'checkbox', 'file'].includes(this.props.type)) { if (['text', 'number', 'radio', 'checkbox', 'file'].includes(this.props.type)) {
this._element = 'input'; this._element = 'input';
this._type = this.props.type; this._type = this.props.type;
} else if (this.props.type == 'text-number') { } else if (this.props.type == 'text-number') {
@ -38,22 +37,11 @@ var FormField = React.createClass({
}, },
showError: function(text) { showError: function(text) {
this.setState({ this.setState({
errorState: 'shown', isError: true,
adviceText: text, errorMessage: text,
}); });
// setTimeout(() => {
// this.setState({
// errorState: 'fading',
// });
// setTimeout(() => {
// this.setState({
// errorState: 'hidden',
// });
// }, 450);
// }, 5000);
}, },
warnRequired: function() { showRequiredError: function() {
this.showError(this._fieldRequiredText); this.showError(this._fieldRequiredText);
}, },
focus: function() { focus: function() {
@ -74,33 +62,27 @@ var FormField = React.createClass({
render: function() { render: function() {
// Pass all unhandled props to the field element // Pass all unhandled props to the field element
const otherProps = Object.assign({}, this.props), const otherProps = Object.assign({}, this.props),
hasError = this.state.errorState != 'hidden'; isError = this.state.isError !== null ? this.state.isError : this.props.hasError,
elementId = this.props.id ? this.props.id : formFieldId(),
renderElementInsideLabel = this.props.label && formFieldNestedLabelTypes.includes(this.props.type);
delete otherProps.type; delete otherProps.type;
delete otherProps.hidden;
delete otherProps.label; delete otherProps.label;
delete otherProps.row; delete otherProps.hasError;
delete otherProps.helper;
++formFieldCounter; const element = <this._element id={elementId} type={this._type} name={this.props.name} ref="field" placeholder={this.props.placeholder}
const elementId = "form-field-" + formFieldCounter className={'form-field__input form-field__input-' + this.props.type + ' ' + (this.props.className || '') + (isError ? 'form-field__input--error' : '')}
{...otherProps}>
{this.props.children}
</this._element>;
if (this.props.hidden) { return <div className="form-field">
return null; { renderElementInsideLabel ?
} <label htmlFor={elementId} className={"form-field__label " + (isError ? 'form-field__label--error' : '')}>
{element}
const field = <div className="form-field"> {this.props.label}
{ this.props.label ? </label> : element }
<div className={"form-field__label " + (hasError ? 'form-field__label--error' : '')}> { isError ? <div className="form-field__error">{this.state.errorMessage}</div> : '' }
<label htmlFor={elementId}>{this.props.label}</label>
</div> : ''
}
<this._element id={elementId} type={this._type} name={this.props.name} ref="field" placeholder={this.props.placeholder}
className={'form-field__input form-field__input-' + this.props.type + ' ' + (this.props.className || '') + (hasError ? 'form-field__input--error' : '')}
{...otherProps}>
{this.props.children}
</this._element>
{ !hasError && this.props.helper ? <div className="form-field__helper">{this.props.helper}</div> : '' }
{ hasError ? <div className="form-field__error">{this.state.adviceText}</div> : '' }
</div> </div>
return ( return (
this.props.row ? this.props.row ?
@ -108,6 +90,57 @@ var FormField = React.createClass({
field field
); );
} }
}); })
export default FormField; export let FormRow = React.createClass({
propTypes: {
label: React.PropTypes.string,
// helper: React.PropTypes.html,
},
getValue: function() {
if (this.props.type == 'checkbox') {
return this.refs.field.checked;
} else if (this.props.type == 'file') {
return this.refs.field.files[0].path;
} else {
return this.refs.field.value;
}
},
getInitialState: function() {
return {
isError: false,
errorMessage: null,
}
},
showError: function(text) {
this.setState({
isError: true,
errorMessage: text,
});
},
getValue: function() {
return this.refs.field.getValue();
},
render: function() {
const fieldProps = Object.assign({}, this.props),
elementId = formFieldId(),
renderLabelInFormField = formFieldNestedLabelTypes.includes(this.props.type);
if (!renderLabelInFormField) {
delete fieldProps.label;
}
delete fieldProps.helper;
return <div className="form-row">
{ this.props.label && !renderLabelInFormField ?
<div className={"form-row__label-row " + (this.props.labelPrefix ? "form-row__label-row--prefix" : "") }>
<label htmlFor={elementId} className={"form-field__label " + (this.state.isError ? 'form-field__label--error' : '')}>
{this.props.label}
</label>
</div> : '' }
<FormField ref="field" hasError={this.state.isError} {...fieldProps} />
{ !this.state.isError && this.props.helper ? <div className="form-field__helper">{this.props.helper}</div> : '' }
{ this.state.isError ? <div className="form-field__error">{this.state.errorMessage}</div> : '' }
</div>
}
})

View file

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import {Link} from './link.js'; import {Link} from './link.js';
import NotificationBar from './notification-bar.js'; import {Icon} from './common.js';
var Header = React.createClass({ var Header = React.createClass({
getInitialState: function() { getInitialState: function() {
@ -53,6 +53,7 @@ var Header = React.createClass({
<Link onClick={this.props.onOpenDrawer} icon="icon-bars" className="open-drawer-link" /> <Link onClick={this.props.onOpenDrawer} icon="icon-bars" className="open-drawer-link" />
<h1>{ this.state.title }</h1> <h1>{ this.state.title }</h1>
<div className="header-search"> <div className="header-search">
<Icon icon="icon-search" />
<input type="search" onChange={this.onQueryChange} defaultValue={this.props.initialQuery} <input type="search" onChange={this.onQueryChange} defaultValue={this.props.initialQuery}
placeholder="Find movies, music, games, and more"/> placeholder="Find movies, music, games, and more"/>
</div> </div>
@ -62,7 +63,6 @@ var Header = React.createClass({
<SubHeader links={this.props.links} viewingPage={this.props.viewingPage} /> : <SubHeader links={this.props.links} viewingPage={this.props.viewingPage} /> :
'' ''
} }
<NotificationBar />
</header> </header>
); );
} }

View file

@ -1,5 +1,7 @@
import React from 'react'; import React from 'react';
import {Icon} from './common.js'; import {Icon} from './common.js';
import Modal from '../component/modal.js';
import rewards from '../rewards.js';
export let Link = React.createClass({ export let Link = React.createClass({
propTypes: { propTypes: {
@ -53,3 +55,75 @@ export let Link = React.createClass({
); );
} }
}); });
export let RewardLink = React.createClass({
propTypes: {
type: React.PropTypes.string.isRequired,
claimed: React.PropTypes.bool,
onRewardClaim: React.PropTypes.func
},
refreshClaimable: function() {
switch(this.props.type) {
case 'new_user':
this.setState({ claimable: true });
return;
case 'first_publish':
lbry.claim_list_mine().then(function(list) {
this.setState({
claimable: list.length > 0
})
}.bind(this));
return;
}
},
componentWillMount: function() {
this.refreshClaimable();
},
getInitialState: function() {
return {
claimable: true,
pending: false,
errorMessage: null
}
},
claimReward: function() {
this.setState({
pending: true
})
rewards.claimReward(this.props.type).then(function(reward) {
console.log(reward);
this.setState({
pending: false,
errorMessage: null
})
if (this.props.onRewardClaim) {
this.props.onRewardClaim();
}
}.bind(this)).catch(function(error) {
this.setState({
errorMessage: error.message,
pending: false
})
}.bind(this))
},
clearError: function() {
this.setState({
errorMessage: null
})
},
render: function() {
return (
<div className="reward-link">
{this.props.claimed
? <span><Icon icon="icon-check" /> Reward claimed.</span>
: <Link button="alt" disabled={this.state.pending || !this.state.claimable } label="Claim Reward" onClick={this.claimReward} />}
{this.state.errorMessage ?
<Modal isOpen={true} contentLabel="Reward Claim Error" className="error-modal" onConfirmed={this.clearError}>
{this.state.errorMessage}
</Modal>
: ''}
</div>
);
}
});

View file

@ -1,53 +0,0 @@
import React from 'react';
import lbry from '../lbry.js';
import Notice from '../component/notice.js';
export const NotificationBar = React.createClass({
_displayTime: 8, // in seconds
_hideTimeout: null,
getInitialState: function() {
return {
message: null,
isError: null,
}
},
handleNoticeReceived: function(event) {
if (this._hideTimeout) {
clearTimeout(this._hideTimeout);
}
const {detail: {message, isError}} = event;
this.setState({
message: message,
isError: isError,
});
this._hideTimeout = setTimeout(() => {
this.setState({
message: null,
isError: null,
});
}, this._displayTime * 1000);
},
componentWillMount: function() {
document.addEventListener('globalNotice', this.handleNoticeReceived);
},
componentWillUnmount: function() {
document.removeEventListener('globalNotice', this.handleNoticeReceived);
},
render: function() {
if (!this.state.message) {
return null;
}
return (
<Notice isError={this.state.isError} className="notification-bar">
{this.state.message}
</Notice>
);
},
});
export default NotificationBar;

View file

@ -0,0 +1,58 @@
import React from 'react';
import lbry from '../lbry.js';
export const SnackBar = React.createClass({
_displayTime: 5, // in seconds
_hideTimeout: null,
getInitialState: function() {
return {
snacks: []
}
},
handleSnackReceived: function(event) {
// console.log(event);
// if (this._hideTimeout) {
// clearTimeout(this._hideTimeout);
// }
let snacks = this.state.snacks;
snacks.push(event.detail);
this.setState({ snacks: snacks});
},
componentWillMount: function() {
document.addEventListener('globalNotice', this.handleSnackReceived);
},
componentWillUnmount: function() {
document.removeEventListener('globalNotice', this.handleSnackReceived);
},
render: function() {
if (!this.state.snacks.length) {
this._hideTimeout = null; //should be unmounting anyway, but be safe?
return null;
}
let snack = this.state.snacks[0];
if (this._hideTimeout === null) {
this._hideTimeout = setTimeout(function() {
this._hideTimeout = null;
let snacks = this.state.snacks;
snacks.shift();
this.setState({ snacks: snacks });
}.bind(this), this._displayTime * 1000);
}
return (
<div className="snack-bar">
{snack.message}
{snack.linkText && snack.linkTarget ?
<a className="snack-bar__action" href={snack.linkTarget}>{snack.linkText}</a> : ''}
</div>
);
},
});
export default SnackBar;

View file

@ -1,156 +0,0 @@
import React from 'react';
import lbryio from '../lbryio.js';
import ModalPage from './modal-page.js';
import {Link} from '../component/link.js';
import FormField from '../component/form.js';
import Notice from '../component/notice.js'
const SubmitEmailStage = React.createClass({
getInitialState: function() {
return {
rewardType: null,
email: '',
submitting: false,
errorMessage: null,
};
},
handleEmailChanged: function(event) {
this.setState({
email: event.target.value,
});
},
handleSubmit: function(event) {
event.preventDefault();
this.setState({
submitting: true,
});
lbryio.call('user_email', 'new', {email: this.state.email}, 'post').then(() => {
this.props.onDone();
}, (error) => {
this.setState({
submitting: false,
errorMessage: error.message,
});
});
},
render: function() {
return (
<section>
<h1>Welcome to LBRY</h1>
{this.state.errorMessage
? <Notice isError>
{this.state.errorMessage}
</Notice>
: null}
<p>Copy here explaining what we do with your email, and the reward.</p>
<form onSubmit={this.handleSubmit}>
<section>Email <label><FormField ref={(field) => { this._emailField = field }} type="text" name="email" value={this.state.email} onChange={this.handleEmailChanged} /></label></section>
<section><Link button="primary" label="Submit and Continue" disabled={this.state.submitting} onClick={this.handleSubmit} /></section>
</form>
</section>
);
}
});
const ConfirmEmailStage = React.createClass({
getInitialState: function() {
return {
rewardType: null,
code: '',
submitting: false,
errorMessage: null,
};
},
handleCodeChanged: function(event) {
this.setState({
code: event.target.value,
});
},
handleSubmit: function(event) {
event.preventDefault();
this.setState({
submitting: true,
});
lbryio.call('user_email', 'confirm', {verification_token: this.state.code}, 'post').then(() => {
rewards.claimReward('confirm_email').then(() => {
console.log('succeeded');
this.props.onDone();
}, (err) => {
console.log('failed');
this.props.onDone();
});
}, (error) => {
this.setState({
submitting: false,
errorMessage: error.message,
});
});
},
render: function() {
return (
<section>
<h1>Confirm Your Email Address</h1>
{this.state.errorMessage
? <Notice isError>
{this.state.errorMessage}
</Notice>
: null}
<p>Please enter your verification code to confirm your email address.</p>
<form onSubmit={this.handleSubmit}>
<section><label>Verification Code: <FormField ref={(field) => { this._codeField = field }} type="text" name="code" value={this.state.code} onChange={this.handleCodeChanged} /></label></section>
<section><Link button="primary" label="Verify" disabled={this.state.submitting} onClick={this.handleSubmit} /></section>
</form>
</section>
);
}
});
const FinalMessageStage = React.createClass({
render: function() {
return (
<section>
<h1>Email verified</h1>
<p>Text here about what happens next</p>
<section><Link button="primary" label="OK" onClick={this.props.onDone} /></section>
</section>
);
}
});
export const Welcome = React.createClass({
_stages: [
SubmitEmailStage,
ConfirmEmailStage,
FinalMessageStage,
],
propTypes: {
onDone: React.PropTypes.func.isRequired,
},
getInitialState: function() {
return {
stageNum: 0,
};
},
handleStageDone: function() {
if (this.state.stageNum >= this._stages.length - 1) {
this.props.onDone();
} else {
this.setState({
stageNum: this.state.stageNum + 1,
});
}
},
render: function() {
const Content = this._stages[this.state.stageNum];
return (
<ModalPage contentLabel="Welcome to LBRY" {...this.props}>
<Content onDone={this.handleStageDone} />
</ModalPage>
);
}
});

View file

@ -282,29 +282,6 @@ lbry.getCostInfo = function(lbryUri, callback, errorCallback) {
}); });
} }
lbry.getFeaturedDiscoverNames = function(callback) {
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest;
xhr.open('GET', 'https://api.lbry.io/discover/list', true);
xhr.onload = () => {
if (xhr.status === 200) {
var responseData = JSON.parse(xhr.responseText);
if (responseData.data) //new signature, once api.lbry.io is updated
{
resolve(responseData.data);
}
else
{
resolve(responseData);
}
} else {
reject(Error('Failed to fetch featured names.'));
}
};
xhr.send();
});
}
lbry.getMyClaims = function(callback) { lbry.getMyClaims = function(callback) {
lbry.call('get_name_claims', {}, callback); lbry.call('get_name_claims', {}, callback);
} }

View file

@ -1,4 +1,4 @@
import {getLocal, setLocal} from './utils.js'; import {getLocal, getSession, setSession, setLocal} from './utils.js';
import lbry from './lbry.js'; import lbry from './lbry.js';
const querystring = require('querystring'); const querystring = require('querystring');
@ -20,18 +20,7 @@ const mocks = {
value: 50, value: 50,
claimed: false, claimed: false,
}; };
}, }
'reward_type.list': () => {
return [
{
name: 'link_github',
title: 'Link your GitHub account',
description: 'Link LBRY to your GitHub account',
value: 50,
claimed: false,
},
];
},
}; };
lbryio.call = function(resource, action, params={}, method='get') { lbryio.call = function(resource, action, params={}, method='get') {
@ -103,7 +92,7 @@ lbryio.setAccessToken = (token) => {
lbryio._accessToken = token lbryio._accessToken = token
} }
lbryio.authenticate = () => { lbryio.authenticate = function() {
if (lbryio._authenticationPromise === null) { if (lbryio._authenticationPromise === null) {
lbryio._authenticationPromise = new Promise((resolve, reject) => { lbryio._authenticationPromise = new Promise((resolve, reject) => {
lbry.status().then(({installation_id}) => { lbry.status().then(({installation_id}) => {
@ -117,7 +106,12 @@ lbryio.authenticate = () => {
resolve(data) resolve(data)
}).catch(function(err) { }).catch(function(err) {
lbryio.setAccessToken(null); lbryio.setAccessToken(null);
reject(err); if (!getSession('reloadedOnFailedAuth')) {
setSession('reloadedOnFailedAuth', true)
window.location.reload();
} else {
reject(err);
}
}) })
} }

View file

@ -5,8 +5,8 @@ import lbryio from './lbryio.js';
import lighthouse from './lighthouse.js'; import lighthouse from './lighthouse.js';
import App from './app.js'; import App from './app.js';
import SplashScreen from './component/splash.js'; import SplashScreen from './component/splash.js';
import SnackBar from './component/snack-bar.js';
import {AuthOverlay} from './component/auth.js'; import {AuthOverlay} from './component/auth.js';
import {Welcome} from './component/welcome.js';
const {remote} = require('electron'); const {remote} = require('electron');
const contextMenu = remote.require('./menu/context-menu'); const contextMenu = remote.require('./menu/context-menu');
@ -28,12 +28,12 @@ let init = function() {
lbryio.authenticate() //start auth process as soon as soon as we can get an install ID lbryio.authenticate() //start auth process as soon as soon as we can get an install ID
}) })
async function onDaemonReady() { function onDaemonReady() {
window.sessionStorage.setItem('loaded', 'y'); //once we've made it here once per session, we don't need to show splash again window.sessionStorage.setItem('loaded', 'y'); //once we've made it here once per session, we don't need to show splash again
ReactDOM.render(<div><AuthOverlay /><App /></div>, canvas) ReactDOM.render(<div><AuthOverlay /><App /><SnackBar /></div>, canvas)
} }
if (window.sessionStorage.getItem('loaded') == 'y' && false) { if (window.sessionStorage.getItem('loaded') == 'y') {
onDaemonReady(); onDaemonReady();
} else { } else {
ReactDOM.render(<SplashScreen message="Connecting" onLoadDone={onDaemonReady} />, canvas); ReactDOM.render(<SplashScreen message="Connecting" onLoadDone={onDaemonReady} />, canvas);

View file

@ -1,158 +0,0 @@
import React from 'react';
import lbry from '../lbry.js';
import Modal from '../component/modal.js';
import {Link} from '../component/link.js';
var claimCodeContentStyle = {
display: 'inline-block',
textAlign: 'left',
width: '600px',
}, claimCodeLabelStyle = {
display: 'inline-block',
cursor: 'default',
width: '130px',
textAlign: 'right',
marginRight: '6px',
};
var ClaimCodePage = React.createClass({
getInitialState: function() {
return {
submitting: false,
modal: null,
referralCredits: null,
activationCredits: null,
failureReason: null,
}
},
handleSubmit: function(event) {
if (typeof event !== 'undefined') {
event.preventDefault();
}
if (!this.refs.code.value) {
this.setState({
modal: 'missingCode',
});
return;
} else if (!this.refs.email.value) {
this.setState({
modal: 'missingEmail',
});
return;
}
this.setState({
submitting: true,
});
lbry.getUnusedAddress((address) => {
var code = this.refs.code.value;
var email = this.refs.email.value;
var xhr = new XMLHttpRequest;
xhr.addEventListener('load', () => {
var response = JSON.parse(xhr.responseText);
if (response.success) {
this.setState({
modal: 'codeRedeemed',
referralCredits: response.referralCredits,
activationCredits: response.activationCredits,
});
} else {
this.setState({
submitting: false,
modal: 'codeRedeemFailed',
failureReason: response.reason,
});
}
});
xhr.addEventListener('error', () => {
this.setState({
submitting: false,
modal: 'couldNotConnect',
});
});
xhr.open('POST', 'https://invites.lbry.io', true);
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhr.send('code=' + encodeURIComponent(code) + '&address=' + encodeURIComponent(address) +
'&email=' + encodeURIComponent(email));
});
},
handleSkip: function() {
this.setState({
modal: 'skipped',
});
},
handleFinished: function() {
localStorage.setItem('claimCodeDone', true);
window.location = '?home';
},
closeModal: function() {
this.setState({
modal: null,
});
},
render: function() {
return (
<main>
<form onSubmit={this.handleSubmit}>
<div className="card">
<h2>Claim your beta invitation code</h2>
<section style={claimCodeContentStyle}>
<p>Thanks for beta testing LBRY! Enter your invitation code and email address below to receive your initial
LBRY credits.</p>
<p>You will be added to our mailing list (if you're not already on it) and will be eligible for future rewards for beta testers.</p>
</section>
<section>
<section><label style={claimCodeLabelStyle} htmlFor="code">Invitation code</label><input name="code" ref="code" /></section>
<section><label style={claimCodeLabelStyle} htmlFor="email">Email</label><input name="email" ref="email" /></section>
</section>
<section>
<Link button="primary" label={this.state.submitting ? "Submitting..." : "Submit"}
disabled={this.state.submitting} onClick={this.handleSubmit} />
<Link button="alt" label="Skip" disabled={this.state.submitting} onClick={this.handleSkip} />
<input type='submit' className='hidden' />
</section>
</div>
</form>
<Modal isOpen={this.state.modal == 'missingCode'} contentLabel="Invitation code required"
onConfirmed={this.closeModal}>
Please enter an invitation code or choose "Skip."
</Modal>
<Modal isOpen={this.state.modal == 'missingEmail'} contentLabel="Email required"
onConfirmed={this.closeModal}>
Please enter an email address or choose "Skip."
</Modal>
<Modal isOpen={this.state.modal == 'codeRedeemFailed'} contentLabel="Failed to redeem code"
onConfirmed={this.closeModal}>
{this.state.failureReason}
</Modal>
<Modal isOpen={this.state.modal == 'codeRedeemed'} contentLabel="Code redeemed"
onConfirmed={this.handleFinished}>
Your invite code has been redeemed. { ' ' }
{this.state.referralCredits > 0
? `You have also earned ${referralCredits} credits from referrals. A total of ${activationCredits + referralCredits}
will be added to your balance shortly.`
: (this.state.activationCredits > 0
? `${this.state.activationCredits} credits will be added to your balance shortly.`
: 'The credits will be added to your balance shortly.')}
</Modal>
<Modal isOpen={this.state.modal == 'skipped'} contentLabel="Welcome to LBRY"
onConfirmed={this.handleFinished}>
Welcome to LBRY! You can visit the Wallet page to redeem an invite code at any time.
</Modal>
<Modal isOpen={this.state.modal == 'couldNotConnect'} contentLabel="Could not connect"
onConfirmed={this.closeModal}>
<p>LBRY couldn't connect to our servers to confirm your invitation code. Please check your internet connection.</p>
If you continue to have problems, you can still browse LBRY and visit the Settings page to redeem your code later.
</Modal>
</main>
);
}
});
export default ClaimCodePage;

View file

@ -1,6 +1,6 @@
import lbry from '../lbry.js'; import lbry from '../lbry.js';
import React from 'react'; import React from 'react';
import FormField from '../component/form.js'; import {FormField} from '../component/form.js';
import {Link} from '../component/link.js'; import {Link} from '../component/link.js';
const fs = require('fs'); const fs = require('fs');

View file

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import lbry from '../lbry.js'; import lbry from '../lbry.js';
import lbryio from '../lbryio.js';
import lighthouse from '../lighthouse.js'; import lighthouse from '../lighthouse.js';
import {FileTile} from '../component/file-tile.js'; import {FileTile} from '../component/file-tile.js';
import {Link} from '../component/link.js'; import {Link} from '../component/link.js';
@ -58,45 +59,44 @@ var SearchResults = React.createClass({
} }
}); });
var featuredContentLegendStyle = { const communityCategoryToolTipText = ('Community Content is a public space where anyone can share content with the ' +
fontSize: '12px', 'rest of the LBRY community. Bid on the names "one," "two," "three," "four" and ' +
color: '#aaa', '"five" to put your content here!');
verticalAlign: '15%',
}; var FeaturedCategory = React.createClass({
render: function() {
return (<div className="card-row">
{ this.props.category ?
<h3 className="card-row__header">{this.props.category}
{ this.props.category == "community" ?
<ToolTip label="What's this?" body={communityCategoryToolTipText} className="tooltip--header"/>
: '' }</h3>
: '' }
{ this.props.names.map((name) => { return <FileTile key={name} displayStyle="card" name={name} uri={name} /> }) }
</div>)
}
})
var FeaturedContent = React.createClass({ var FeaturedContent = React.createClass({
getInitialState: function() { getInitialState: function() {
return { return {
featuredNames: [], featuredNames: {},
}; };
}, },
componentWillMount: function() { componentWillMount: function() {
lbry.getFeaturedDiscoverNames().then((featuredNames) => { lbryio.call('discover', 'list', { version: "early-access" } ).then((featuredNames) => {
this.setState({ featuredNames: featuredNames }); this.setState({ featuredNames: featuredNames });
}); });
}, },
render: function() { render: function() {
const toolTipText = ('Community Content is a public space where anyone can share content with the ' + console.log(this.state.featuredNames);
'rest of the LBRY community. Bid on the names "one," "two," "three," "four" and ' +
'"five" to put your content here!');
return ( return (
<div className="row-fluid"> <div>
<div className="span6"> {
<h3>Featured Content</h3> Object.keys(this.state.featuredNames).map(function(category) {
{ this.state.featuredNames.map(name => <FileTile key={name} uri={name} />) } return <FeaturedCategory key={category} category={category} names={this.state.featuredNames[category]} />
</div> }.bind(this))
<div className="span6"> }
<h3>
Community Content
<ToolTip label="What's this?" body={toolTipText} className="tooltip--header"/>
</h3>
<FileTile uri="one" />
<FileTile uri="two" />
<FileTile uri="three" />
<FileTile uri="four" />
<FileTile uri="five" />
</div>
</div> </div>
); );
} }
@ -173,12 +173,12 @@ var DiscoverPage = React.createClass({
}, },
render: function() { render: function() {
//{ !this.props.query && !this.state.searching ? <FeaturedContent /> : null }
return ( return (
<main> <main>
{ this.state.searching ? <SearchActive /> : null } { this.state.searching ? <SearchActive /> : null }
{ !this.state.searching && this.props.query && this.state.results.length ? <SearchResults results={this.state.results} /> : null } { !this.state.searching && this.props.query && this.state.results.length ? <SearchResults results={this.state.results} /> : null }
{ !this.state.searching && this.props.query && !this.state.results.length ? <SearchNoResults query={this.props.query} /> : null } { !this.state.searching && this.props.query && !this.state.results.length ? <SearchNoResults query={this.props.query} /> : null }
{ !this.props.query && !this.state.searching ? <FeaturedContent /> : null }
</main> </main>
); );
} }

View file

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import lbryio from '../lbryio.js'; import lbryio from '../lbryio.js';
import {getLocal, setLocal} from '../utils.js'; import {getLocal, setLocal} from '../utils.js';
import FormField from '../component/form.js' import {FormField} from '../component/form.js'
import {Link} from '../component/link.js' import {Link} from '../component/link.js'
import rewards from '../rewards.js'; import rewards from '../rewards.js';
@ -12,7 +12,7 @@ const EmailPage = React.createClass({
} }
if (!this.state.email) { if (!this.state.email) {
this._emailField.warnRequired(); this._emailField.showRequiredError();
} }
}, },
componentWillMount: function() { componentWillMount: function() {

View file

@ -2,7 +2,7 @@ import React from 'react';
import lbry from '../lbry.js'; import lbry from '../lbry.js';
import uri from '../uri.js'; import uri from '../uri.js';
import {Link} from '../component/link.js'; import {Link} from '../component/link.js';
import FormField from '../component/form.js'; import {FormField} from '../component/form.js';
import {FileTileStream} from '../component/file-tile.js'; import {FileTileStream} from '../component/file-tile.js';
import {BusyMessage, Thumbnail} from '../component/common.js'; import {BusyMessage, Thumbnail} from '../component/common.js';

View file

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import lbry from '../lbry.js'; import lbry from '../lbry.js';
import uri from '../uri.js'; import uri from '../uri.js';
import FormField from '../component/form.js'; import {FormField, FormRow} from '../component/form.js';
import {Link} from '../component/link.js'; import {Link} from '../component/link.js';
import Modal from '../component/modal.js'; import Modal from '../component/modal.js';
@ -36,7 +36,7 @@ var PublishPage = React.createClass({
for (let fieldName of checkFields) { for (let fieldName of checkFields) {
var field = this.refs[fieldName]; var field = this.refs[fieldName];
if (field.getValue() === '') { if (field.getValue() === '') {
field.warnRequired(); field.showRequiredError();
if (!missingFieldFound) { if (!missingFieldFound) {
field.focus(); field.focus();
missingFieldFound = true; missingFieldFound = true;
@ -344,9 +344,12 @@ var PublishPage = React.createClass({
<main ref="page"> <main ref="page">
<form onSubmit={this.handleSubmit}> <form onSubmit={this.handleSubmit}>
<section className="card"> <section className="card">
<h4>LBRY Name</h4> <div className="card__title-primary">
<div className="form-row"> <h4>LBRY Name</h4>
<FormField type="text" ref="name" value={this.state.rawName} onChange={this.handleNameChange} /> </div>
<div className="card__content">
<FormRow label="lbry://" type="text" ref="name" placeholder="lbry://myname" value={this.state.rawName} onChange={this.handleNameChange}
helper={(<div>What LBRY name would you like to claim for this file? <Link label="Read more" href="https://lbry.io/faq/naming" />.</div>)} />
{ {
(!this.state.name (!this.state.name
? null ? null
@ -356,7 +359,6 @@ var PublishPage = React.createClass({
? <em> You already have a claim on the name <strong>{this.state.name}</strong>. You can use this page to update your claim.</em> ? <em> You already have a claim on the name <strong>{this.state.name}</strong>. You can use this page to update your claim.</em>
: <em> The name <strong>{this.state.name}</strong> is currently claimed for <strong>{this.state.topClaimValue}</strong> {this.state.topClaimValue == 1 ? 'credit' : 'credits'}.</em>))) : <em> The name <strong>{this.state.name}</strong> is currently claimed for <strong>{this.state.topClaimValue}</strong> {this.state.topClaimValue == 1 ? 'credit' : 'credits'}.</em>)))
} }
<div className="help">What LBRY name would you like to claim for this file?</div>
</div> </div>
</section> </section>
@ -381,9 +383,11 @@ var PublishPage = React.createClass({
</section> </section>
<section className="card"> <section className="card">
<h4>Choose File</h4> <div className="card__title-primary"><h4>Choose File</h4></div>
<FormField name="file" ref="file" type="file" /> <div className="card__content">
{ this.state.myClaimExists ? <div className="help">If you don't choose a file, the file from your existing claim will be used.</div> : null } <FormField name="file" ref="file" type="file" />
{ this.state.myClaimExists ? <div className="help">If you don't choose a file, the file from your existing claim will be used.</div> : null }
</div>
</section> </section>
<section className="card"> <section className="card">

View file

@ -1,130 +0,0 @@
import React from 'react';
import lbry from '../lbry.js';
import {Link} from '../component/link.js';
import Modal from '../component/modal.js';
var referralCodeContentStyle = {
display: 'inline-block',
textAlign: 'left',
width: '600px',
}, referralCodeLabelStyle = {
display: 'inline-block',
cursor: 'default',
width: '130px',
textAlign: 'right',
marginRight: '6px',
};
var ReferralPage = React.createClass({
getInitialState: function() {
return {
submitting: false,
modal: null,
referralCredits: null,
failureReason: null,
}
},
handleSubmit: function(event) {
if (typeof event !== 'undefined') {
event.preventDefault();
}
if (!this.refs.code.value) {
this.setState({
modal: 'missingCode',
});
} else if (!this.refs.email.value) {
this.setState({
modal: 'missingEmail',
});
}
this.setState({
submitting: true,
});
lbry.getUnusedAddress((address) => {
var code = this.refs.code.value;
var email = this.refs.email.value;
var xhr = new XMLHttpRequest;
xhr.addEventListener('load', () => {
var response = JSON.parse(xhr.responseText);
if (response.success) {
this.setState({
modal: 'referralInfo',
referralCredits: response.referralCredits,
});
} else {
this.setState({
submitting: false,
modal: 'lookupFailed',
failureReason: response.reason,
});
}
});
xhr.addEventListener('error', () => {
this.setState({
submitting: false,
modal: 'couldNotConnect',
});
});
xhr.open('POST', 'https://invites.lbry.io/check', true);
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhr.send('code=' + encodeURIComponent(code) + '&address=' + encodeURIComponent(address) +
'&email=' + encodeURIComponent(email));
});
},
closeModal: function() {
this.setState({
modal: null,
});
},
handleFinished: function() {
localStorage.setItem('claimCodeDone', true);
window.location = '?home';
},
render: function() {
return (
<main>
<form onSubmit={this.handleSubmit}>
<div className="card">
<h2>Check your referral credits</h2>
<section style={referralCodeContentStyle}>
<p>Have you referred others to LBRY? Enter your referral code and email address below to check how many credits you've earned!</p>
<p>As a reminder, your referral code is the same as your LBRY invitation code.</p>
</section>
<section>
<section><label style={referralCodeLabelStyle} htmlFor="code">Referral code</label><input name="code" ref="code" /></section>
<section><label style={referralCodeLabelStyle} htmlFor="email">Email</label><input name="email" ref="email" /></section>
</section>
<section>
<Link button="primary" label={this.state.submitting ? "Submitting..." : "Submit"}
disabled={this.state.submitting} onClick={this.handleSubmit} />
<input type='submit' className='hidden' />
</section>
</div>
</form>
<Modal isOpen={this.state.modal == 'referralInfo'} contentLabel="Credit earnings"
onConfirmed={this.handleFinished}>
{this.state.referralCredits > 0
? `You have earned ${response.referralCredits} credits from referrals. We will credit your account shortly. Thanks!`
: 'You have not earned any new referral credits since the last time you checked. Please check back in a week or two.'}
</Modal>
<Modal isOpen={this.state.modal == 'lookupFailed'} contentLabel={this.state.failureReason}
onConfirmed={this.closeModal}>
{this.state.failureReason}
</Modal>
<Modal isOpen={this.state.modal == 'couldNotConnect'} contentLabel="Couldn't confirm referral code"
onConfirmed={this.closeModal}>
LBRY couldn't connect to our servers to confirm your referral code. Please check your internet connection.
</Modal>
</main>
);
}
});
export default ReferralPage;

View file

@ -3,124 +3,124 @@ import lbryio from '../lbryio.js';
import {Link} from '../component/link.js'; import {Link} from '../component/link.js';
import Notice from '../component/notice.js'; import Notice from '../component/notice.js';
import {CreditAmount} from '../component/common.js'; import {CreditAmount} from '../component/common.js';
//
const {shell} = require('electron'); // const {shell} = require('electron');
const querystring = require('querystring'); // const querystring = require('querystring');
//
const GITHUB_CLIENT_ID = '6baf581d32bad60519'; // const GITHUB_CLIENT_ID = '6baf581d32bad60519';
//
const LinkGithubReward = React.createClass({ // const LinkGithubReward = React.createClass({
propTypes: { // propTypes: {
onClaimed: React.PropTypes.func, // onClaimed: React.PropTypes.func,
}, // },
_launchLinkPage: function() { // _launchLinkPage: function() {
/* const githubAuthParams = { // /* const githubAuthParams = {
client_id: GITHUB_CLIENT_ID, // client_id: GITHUB_CLIENT_ID,
redirect_uri: 'https://lbry.io/', // redirect_uri: 'https://lbry.io/',
scope: 'user:email,public_repo', // scope: 'user:email,public_repo',
allow_signup: false, // allow_signup: false,
} // }
shell.openExternal('https://github.com/login/oauth/authorize?' + querystring.stringify(githubAuthParams)); */ // shell.openExternal('https://github.com/login/oauth/authorize?' + querystring.stringify(githubAuthParams)); */
shell.openExternal('https://lbry.io'); // shell.openExternal('https://lbry.io');
}, // },
handleConfirmClicked: function() { // handleConfirmClicked: function() {
this.setState({ // this.setState({
confirming: true, // confirming: true,
}); // });
//
lbry.get_new_address().then((address) => { // lbry.get_new_address().then((address) => {
lbryio.call('reward', 'new', { // lbryio.call('reward', 'new', {
reward_type: 'new_developer', // reward_type: 'new_developer',
access_token: '**access token here**', // access_token: '**access token here**',
wallet_address: address, // wallet_address: address,
}, 'post').then((response) => { // }, 'post').then((response) => {
console.log('response:', response); // console.log('response:', response);
//
this.props.onClaimed(); // This will trigger another API call to show that we succeeded // this.props.onClaimed(); // This will trigger another API call to show that we succeeded
//
this.setState({ // this.setState({
confirming: false, // confirming: false,
error: null, // error: null,
}); // });
}, (error) => { // }, (error) => {
console.log('failed with error:', error); // console.log('failed with error:', error);
this.setState({ // this.setState({
confirming: false, // confirming: false,
error: error, // error: error,
}); // });
}); // });
}); // });
}, // },
getInitialState: function() { // getInitialState: function() {
return { // return {
confirming: false, // confirming: false,
error: null, // error: null,
}; // };
}, // },
render: function() { // render: function() {
return ( // return (
<section> // <section>
<p><Link button="alt" label="Link with GitHub" onClick={this._launchLinkPage} /></p> // <p><Link button="alt" label="Link with GitHub" onClick={this._launchLinkPage} /></p>
<section className="reward-page__details"> // <section className="reward-page__details">
<p>This will open a browser window where you can authorize GitHub to link your account to LBRY. This will record your email (no spam) and star the LBRY repo.</p> // <p>This will open a browser window where you can authorize GitHub to link your account to LBRY. This will record your email (no spam) and star the LBRY repo.</p>
<p>Once you're finished, you may confirm you've linked the account to receive your reward.</p> // <p>Once you're finished, you may confirm you've linked the account to receive your reward.</p>
</section> // </section>
{this.state.error // {this.state.error
? <Notice isError> // ? <Notice isError>
{this.state.error.message} // {this.state.error.message}
</Notice> // </Notice>
: null} // : null}
//
<Link button="primary" label={this.state.confirming ? 'Confirming...' : 'Confirm'} // <Link button="primary" label={this.state.confirming ? 'Confirming...' : 'Confirm'}
onClick={this.handleConfirmClicked} /> // onClick={this.handleConfirmClicked} />
</section> // </section>
); // );
} // }
}); // });
//
const RewardPage = React.createClass({ // const RewardPage = React.createClass({
propTypes: { // propTypes: {
name: React.PropTypes.string.isRequired, // name: React.PropTypes.string.isRequired,
}, // },
_getRewardType: function() { // _getRewardType: function() {
lbryio.call('reward_type', 'get', this.props.name).then((rewardType) => { // lbryio.call('reward_type', 'get', this.props.name).then((rewardType) => {
this.setState({ // this.setState({
rewardType: rewardType, // rewardType: rewardType,
}); // });
}); // });
}, // },
getInitialState: function() { // getInitialState: function() {
return { // return {
rewardType: null, // rewardType: null,
}; // };
}, // },
componentWillMount: function() { // componentWillMount: function() {
this._getRewardType(); // this._getRewardType();
}, // },
render: function() { // render: function() {
if (!this.state.rewardType) { // if (!this.state.rewardType) {
return null; // return null;
} // }
//
let Reward; // let Reward;
if (this.props.name == 'link_github') { // if (this.props.name == 'link_github') {
Reward = LinkGithubReward; // Reward = LinkGithubReward;
} // }
//
const {title, description, value} = this.state.rewardType; // const {title, description, value} = this.state.rewardType;
return ( // return (
<main> // <main>
<section className="card"> // <section className="card">
<h2>{title}</h2> // <h2>{title}</h2>
<CreditAmount amount={value} /> // <CreditAmount amount={value} />
<p>{this.state.rewardType.claimed // <p>{this.state.rewardType.claimed
? <span class="empty">This reward has been claimed.</span> // ? <span class="empty">This reward has been claimed.</span>
: description}</p> // : description}</p>
<Reward onClaimed={this._getRewardType} /> // <Reward onClaimed={this._getRewardType} />
</section> // </section>
</main> // </main>
); // );
} // }
}); // });
//
export default RewardPage; // export default RewardPage;

View file

@ -1,28 +1,34 @@
import React from 'react'; import React from 'react';
import lbry from '../lbry.js'; import lbry from '../lbry.js';
import lbryio from '../lbryio.js'; import lbryio from '../lbryio.js';
import {CreditAmount} from '../component/common.js'; import {CreditAmount, Icon} from '../component/common.js';
import rewards from '../rewards.js';
import Modal from '../component/modal.js'; import Modal from '../component/modal.js';
import {Link} from '../component/link.js'; import {RewardLink} from '../component/link.js';
const RewardTile = React.createClass({ const RewardTile = React.createClass({
propTypes: { propTypes: {
name: React.PropTypes.string.isRequired, type: React.PropTypes.string.isRequired,
title: React.PropTypes.string.isRequired, title: React.PropTypes.string.isRequired,
description: React.PropTypes.string.isRequired, description: React.PropTypes.string.isRequired,
claimed: React.PropTypes.bool.isRequired, claimed: React.PropTypes.bool.isRequired,
value: React.PropTypes.number.isRequired, value: React.PropTypes.number.isRequired,
onRewardClaim: React.PropTypes.func
}, },
render: function() { render: function() {
return ( return (
<section className="card"> <section className="card">
<div className={"row-fluid card-content"}> <div className="card__inner">
<h3><Link label={this.props.title} href={'?reward=' + this.props.name} /></h3> <div className="card__title-primary">
<CreditAmount amount={this.props.value} /> <CreditAmount amount={this.props.value} />
<section>{this.props.description}</section> <h3>{this.props.title}</h3>
{this.props.claimed </div>
? <span className="empty">This reward has been claimed.</span> <div className="card__actions">
: <Link button="primary" label="Start reward" href={'?reward=' + this.props.name} />} {this.props.claimed
? <span><Icon icon="icon-check" /> Reward claimed.</span>
: <RewardLink {...this.props} />}
</div>
<div className="card__content">{this.props.description}</div>
</div> </div>
</section> </section>
); );
@ -31,29 +37,29 @@ const RewardTile = React.createClass({
var RewardsPage = React.createClass({ var RewardsPage = React.createClass({
componentWillMount: function() { componentWillMount: function() {
lbryio.call('reward_type', 'list', {}).then((rewardTypes) => { this.loadRewards()
this.setState({
rewardTypes: rewardTypes,
});
});
}, },
getInitialState: function() { getInitialState: function() {
return { return {
rewardTypes: null, userRewards: null,
}; };
}, },
loadRewards: function() {
lbryio.call('reward', 'list', {}).then((userRewards) => {
this.setState({
userRewards: userRewards,
});
});
},
render: function() { render: function() {
return ( return (
<main> <main>
<form onSubmit={this.handleSubmit}> <form onSubmit={this.handleSubmit}>
<div className="card"> {!this.state.userRewards
<h2>Rewards</h2> ? null
{!this.state.rewardTypes : this.state.userRewards.map(({RewardType, RewardTitle, RewardDescription, TransactionID, RewardAmount}) => {
? null return <RewardTile key={RewardType} onRewardClaim={this.loadRewards} type={RewardType} title={RewardTitle} description={RewardDescription} claimed={!!TransactionID} value={RewardAmount} />;
: this.state.rewardTypes.map(({name, title, description, claimed, value}) => { })}
return <RewardTile key={name} name={name} title={title} description={description} claimed={claimed} value={value} />;
})}
</div>
</form> </form>
</main> </main>
); );

View file

@ -1,21 +1,7 @@
import React from 'react'; import React from 'react';
import {FormField, FormRow} from '../component/form.js';
import lbry from '../lbry.js'; import lbry from '../lbry.js';
var settingsRadioOptionStyles = {
display: 'block',
marginLeft: '13px'
}, settingsCheckBoxOptionStyles = {
display: 'block',
marginLeft: '13px'
}, settingsNumberFieldStyles = {
width: '40px'
}, downloadDirectoryLabelStyles = {
fontSize: '.9em',
marginLeft: '13px'
}, downloadDirectoryFieldStyles= {
width: '300px'
};
var SettingsPage = React.createClass({ var SettingsPage = React.createClass({
onRunOnStartChange: function (event) { onRunOnStartChange: function (event) {
lbry.setDaemonSetting('run_on_startup', event.target.checked); lbry.setDaemonSetting('run_on_startup', event.target.checked);
@ -81,29 +67,54 @@ var SettingsPage = React.createClass({
return ( return (
<main> <main>
<section className="card"> <section className="card">
<h3>Run on Startup</h3> <div className="card__content">
<label style={settingsCheckBoxOptionStyles}> <h3>Run on Startup</h3>
<input type="checkbox" onChange={this.onRunOnStartChange} defaultChecked={this.state.daemonSettings.run_on_startup} /> Run LBRY automatically when I start my computer
</label>
</section>
<section className="card">
<h3>Download Directory</h3>
<div className="help">Where would you like the files you download from LBRY to be saved?</div>
<input style={downloadDirectoryFieldStyles} type="text" name="download_directory" defaultValue={this.state.daemonSettings.download_directory} onChange={this.onDownloadDirChange}/>
</section>
<section className="card">
<h3>Bandwidth Limits</h3>
<div className="form-row">
<h4>Max Upload</h4>
<label style={settingsRadioOptionStyles}>
<input type="radio" name="max_upload_pref" onChange={this.onMaxUploadPrefChange.bind(this, false)} defaultChecked={!this.state.isMaxUpload}/> Unlimited
</label>
<label style={settingsRadioOptionStyles}>
<input type="radio" name="max_upload_pref" onChange={this.onMaxUploadPrefChange.bind(this, true)} defaultChecked={this.state.isMaxUpload}/> { this.state.isMaxUpload ? 'Up to' : 'Choose limit...' }
<span className={ this.state.isMaxUpload ? '' : 'hidden'}> <input type="number" min="0" step=".5" defaultValue={this.state.daemonSettings.max_upload} style={settingsNumberFieldStyles} onChange={this.onMaxUploadFieldChange}/> MB/s</span>
</label>
</div> </div>
<div className="form-row"> <div className="card__content">
<FormRow type="checkbox"
onChange={this.onRunOnStartChange}
defaultChecked={this.state.daemonSettings.run_on_startup}
label="Run LBRY automatically when I start my computer" />
</div>
</section>
<section className="card">
<div className="card__content">
<h3>Download Directory</h3>
</div>
<div className="card__content">
<FormRow type="text"
name="download_directory"
defaultValue={this.state.daemonSettings.download_directory}
helper="LBRY downloads will be saved here."
onChange={this.onDownloadDirChange} />
</div>
</section>
<section className="card">
<div className="card__content">
<h3>Bandwidth Limits</h3>
</div>
<div className="card__content">
<h4>Max Upload</h4>
<FormField type="radio"
name="max_upload_pref"
onChange={this.onMaxUploadPrefChange.bind(this, false)}
defaultChecked={!this.state.isMaxUpload}
label="Unlimited" />
<FormField type="radio"
name="max_upload_pref"
onChange={this.onMaxUploadPrefChange.bind(this, true)}
defaultChecked={this.state.isMaxUpload}
label={ this.state.isMaxUpload ? 'Up to' : 'Choose limit...' } />
{ this.state.isMaxUpload ?
<FormField type="number"
min="0"
step=".5"
label="MB/s"
onChange={this.onMaxUploadFieldChange}
/> : ''
}
</div>
<div className="card__content">
<h4>Max Download</h4> <h4>Max Download</h4>
<label style={settingsRadioOptionStyles}> <label style={settingsRadioOptionStyles}>
<input type="radio" name="max_download_pref" onChange={this.onMaxDownloadPrefChange.bind(this, false)} defaultChecked={!this.state.isMaxDownload}/> Unlimited <input type="radio" name="max_download_pref" onChange={this.onMaxDownloadPrefChange.bind(this, false)} defaultChecked={!this.state.isMaxDownload}/> Unlimited
@ -115,40 +126,56 @@ var SettingsPage = React.createClass({
</div> </div>
</section> </section>
<section className="card"> <section className="card">
<h3>Content</h3> <div className="card__content">
<div className="form-row"> <h3>Content</h3>
<label style={settingsCheckBoxOptionStyles}> </div>
<input type="checkbox" onChange={this.onShowNsfwChange} defaultChecked={this.state.showNsfw} /> Show NSFW content <div class="card__content">
</label> <FormRow type="checkbox"
<div className="help"> onChange={this.onShowUnavailableChange}
NSFW content may include nudity, intense sexuality, profanity, or other adult content. defaultChecked={this.state.showUnavailable}
By displaying NSFW content, you are affirming you are of legal age to view mature content in your country or jurisdiction. label="Show unavailable content in search results" />
</div> </div>
<div className="card__content">
<FormRow label="Show NSFW content" type="checkbox"
onChange={this.onShowNsfwChange} defaultChecked={this.state.showNsfw}
helper="NSFW content may include nudity, intense sexuality, profanity, or other adult content. By displaying NSFW content, you are affirming you are of legal age to view mature content in your country or jurisdiction. " />
</div> </div>
</section> </section>
<section className="card"> <section className="card">
<h3>Search</h3> <div className="card__content">
<div className="form-row"> <h3>Share Diagnostic Data</h3>
<div className="help">
Would you like search results to include items that are not currently available for download?
</div> </div>
<label style={settingsCheckBoxOptionStyles}> <div class="card__content">
<input type="checkbox" onChange={this.onShowUnavailableChange} defaultChecked={this.state.showUnavailable} /> <FormRow type="checkbox"
Show unavailable content in search results onChange={this.onShareDataChange}
</label> defaultChecked={this.state.daemonSettings.share_debug_info}
label="Help make LBRY better by contributing diagnostic data about my usage" />
</div> </div>
</section> </section>
<section className="card">
<h3>Share Diagnostic Data</h3>
<label style={settingsCheckBoxOptionStyles}>
<input type="checkbox" onChange={this.onShareDataChange} defaultChecked={this.state.daemonSettings.share_debug_info} />
Help make LBRY better by contributing diagnostic data about my usage
</label>
</section>
</main> </main>
); );
} }
}); });
/*
<section className="card">
<h3>Search</h3>
<div className="form-row">
<div className="help">
Would you like search results to include items that are not currently available for download?
</div>
<label style={settingsCheckBoxOptionStyles}>
<input type="checkbox" onChange={this.onShowUnavailableChange} defaultChecked={this.state.showUnavailable} /> Show unavailable content in search results
</label>
</div>
</section>
<section className="card">
<h3>Share Diagnostic Data</h3>
<label style={settingsCheckBoxOptionStyles}>
<input type="checkbox" onChange={this.onShareDataChange} defaultChecked={this.state.daemonSettings.upload_log} /> Help make LBRY better by contributing diagnostic data about my usage
</label>
</section>
*/
export default SettingsPage; export default SettingsPage;

View file

@ -2,12 +2,9 @@ import React from 'react';
import lbry from '../lbry.js'; import lbry from '../lbry.js';
import {Link} from '../component/link.js'; import {Link} from '../component/link.js';
import Modal from '../component/modal.js'; import Modal from '../component/modal.js';
import {FormField, FormRow} from '../component/form.js';
import {Address, BusyMessage, CreditAmount} from '../component/common.js'; import {Address, BusyMessage, CreditAmount} from '../component/common.js';
var addressRefreshButtonStyle = {
fontSize: '11pt',
};
var AddressSection = React.createClass({ var AddressSection = React.createClass({
_refreshAddress: function(event) { _refreshAddress: function(event) {
if (typeof event !== 'undefined') { if (typeof event !== 'undefined') {
@ -27,12 +24,12 @@ var AddressSection = React.createClass({
event.preventDefault(); event.preventDefault();
} }
lbry.getNewAddress((address) => { lbry.wallet_new_address().then((address) => {
window.localStorage.setItem('wallet_address', address); window.localStorage.setItem('wallet_address', address);
this.setState({ this.setState({
address: address, address: address,
}); });
}); }.bind(this));
}, },
getInitialState: function() { getInitialState: function() {
@ -60,12 +57,20 @@ var AddressSection = React.createClass({
render: function() { render: function() {
return ( return (
<section className="card"> <section className="card">
<h3>Wallet Address</h3> <div className="card__title-primary">
<Address address={this.state.address} /> <Link text="Get new address" icon='icon-refresh' onClick={this._getNewAddress} style={addressRefreshButtonStyle} /> <h3>Wallet Address</h3>
<input type='submit' className='hidden' /> </div>
<div className="help"> <div className="card__content">
<p>Other LBRY users may send credits to you by entering this address on the "Send" page.</p> <Address address={this.state.address} />
You can generate a new address at any time, and any previous addresses will continue to work. Using multiple addresses can be helpful for keeping track of incoming payments from multiple sources. </div>
<div className="card__actions">
<Link label="Get New Address" button="primary" icon='icon-refresh' onClick={this._getNewAddress} />
</div>
<div className="card__content">
<div className="help">
<p>Other LBRY users may send credits to you by entering this address on the "Send" page.</p>
<p>You can generate a new address at any time, and any previous addresses will continue to work. Using multiple addresses can be helpful for keeping track of incoming payments from multiple sources.</p>
</div>
</div> </div>
</section> </section>
); );
@ -143,27 +148,26 @@ var SendToAddressSection = React.createClass({
return ( return (
<section className="card"> <section className="card">
<form onSubmit={this.handleSubmit}> <form onSubmit={this.handleSubmit}>
<h3>Send Credits</h3> <div className="card__title-primary">
<div className="form-row"> <h3>Send Credits</h3>
<label htmlFor="amount">Amount</label>
<input id="amount" type="text" size="10" onChange={this.setAmount}></input>
</div> </div>
<div className="form-row"> <div className="card__content">
<label htmlFor="address">Recipient address</label> <FormRow label="Amount" type="number" placeholder="1.23" size="10" onChange={this.setAmount} />
<input id="address" type="text" size="60" onChange={this.setAddress}></input>
</div> </div>
<div className="form-row form-row-submit"> <div className="card__content">
<FormRow label="Recipient Address" placeholder="bbFxRyXXXXXXXXXXXZD8nE7XTLUxYnddTs" type="text" size="60" onChange={this.setAddress} />
</div>
<div className="card__actions card__actions--form-submit">
<Link button="primary" label="Send" onClick={this.handleSubmit} disabled={!(parseFloat(this.state.amount) > 0.0) || this.state.address == ""} /> <Link button="primary" label="Send" onClick={this.handleSubmit} disabled={!(parseFloat(this.state.amount) > 0.0) || this.state.address == ""} />
<input type='submit' className='hidden' /> <input type='submit' className='hidden' />
</div> </div>
{ {
this.state.results ? this.state.results ?
<div className="form-row"> <div className="card__content">
<h4>Results</h4> <h4>Results</h4>
{this.state.results} {this.state.results}
</div> </div> : ''
: '' }
}
</form> </form>
<Modal isOpen={this.state.modal === 'insufficientBalance'} contentLabel="Insufficient balance" <Modal isOpen={this.state.modal === 'insufficientBalance'} contentLabel="Insufficient balance"
onConfirmed={this.closeModal}> onConfirmed={this.closeModal}>
@ -231,25 +235,29 @@ var TransactionList = React.createClass({
} }
return ( return (
<section className="card"> <section className="card">
<h3>Transaction History</h3> <div className="card__title-primary">
{ this.state.transactionItems === null ? <BusyMessage message="Loading transactions" /> : '' } <h3>Transaction History</h3>
{ this.state.transactionItems && rows.length === 0 ? <div className="empty">You have no transactions.</div> : '' } </div>
{ this.state.transactionItems && rows.length > 0 ? <div className="card__content">
<table className="table-standard table-stretch"> { this.state.transactionItems === null ? <BusyMessage message="Loading transactions" /> : '' }
<thead> { this.state.transactionItems && rows.length === 0 ? <div className="empty">You have no transactions.</div> : '' }
<tr> { this.state.transactionItems && rows.length > 0 ?
<th>Amount</th> <table className="table-standard table-stretch">
<th>Date</th> <thead>
<th>Time</th> <tr>
<th>Transaction</th> <th>Amount</th>
</tr> <th>Date</th>
</thead> <th>Time</th>
<tbody> <th>Transaction</th>
{rows} </tr>
</tbody> </thead>
</table> <tbody>
{rows}
</tbody>
</table>
: '' : ''
} }
</div>
</section> </section>
); );
} }
@ -290,9 +298,13 @@ var WalletPage = React.createClass({
return ( return (
<main className="page"> <main className="page">
<section className="card"> <section className="card">
<h3>Balance</h3> <div className="card__title-primary">
{ this.state.balance === null ? <BusyMessage message="Checking balance" /> : ''} <h3>Balance</h3>
{ this.state.balance !== null ? <CreditAmount amount={this.state.balance} precision={8} /> : '' } </div>
<div className="card__content">
{ this.state.balance === null ? <BusyMessage message="Checking balance" /> : ''}
{ this.state.balance !== null ? <CreditAmount amount={this.state.balance} precision={8} /> : '' }
</div>
</section> </section>
{ this.props.viewingPage === 'wallet' ? <TransactionList /> : '' } { this.props.viewingPage === 'wallet' ? <TransactionList /> : '' }
{ this.props.viewingPage === 'send' ? <SendToAddressSection /> : '' } { this.props.viewingPage === 'send' ? <SendToAddressSection /> : '' }

View file

@ -1,11 +1,14 @@
import lbry from './lbry.js'; import lbry from './lbry.js';
import lbryio from './lbryio.js'; import lbryio from './lbryio.js';
const MESSAGES = { function rewardMessage(type, amount) {
new_developer: "Your reward has been confirmed for registering as a new developer.", return {
confirm_email: "Your reward has been confirmed for verifying your email address.", new_developer: "Your reward has been confirmed for registering as a new developer.",
first_publish: "Your reward has been confirmed for making your first publication.", new_user: `You received ${amount} LBC new user reward.`,
}; confirm_email: "Your reward has been confirmed for verifying your email address.",
first_publish: "Your reward has been confirmed for making your first publication.",
}[type];
}
const rewards = {}; const rewards = {};
@ -13,22 +16,25 @@ rewards.claimReward = function(type) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
console.log('top of promise body') console.log('top of promise body')
lbry.get_new_address().then((address) => { lbry.get_new_address().then((address) => {
console.log('top of get_new_address')
const params = { const params = {
reward_type: type, reward_type: type,
wallet_address: address, wallet_address: address,
}; };
lbryio.call('reward', 'new', params, 'post').then(({RewardAmount}) => { lbryio.call('reward', 'new', params, 'post').then(({RewardAmount}) => {
const result = { const
type: type, message = rewardMessage(type, RewardAmount),
amount: RewardAmount, result = {
message: MESSAGES[type], type: type,
}; amount: RewardAmount,
message: message
};
// Display global notice // Display global notice
document.dispatchEvent(new CustomEvent('globalNotice', { document.dispatchEvent(new CustomEvent('globalNotice', {
detail: { detail: {
message: MESSAGES[type], message: message,
linkText: "Show All",
linkTarget: "?rewards",
isError: false, isError: false,
}, },
})); }));
@ -36,16 +42,7 @@ rewards.claimReward = function(type) {
// Add more events here to display other places // Add more events here to display other places
resolve(result); resolve(result);
}, (error) => { }, reject);
document.dispatchEvent(new CustomEvent('globalNotice', {
detail: {
message: `Failed to claim reward: ${error.message}`,
isError: true,
},
}));
document.dispatchEvent(new CustomEvent('rewardFailed', error));
reject(error);
});
}); });
}); });
} }

View file

@ -13,3 +13,19 @@ export function getLocal(key) {
export function setLocal(key, value) { export function setLocal(key, value) {
localStorage.setItem(key, JSON.stringify(value)); localStorage.setItem(key, JSON.stringify(value));
} }
/**
* Thin wrapper around localStorage.getItem(). Parses JSON and returns undefined if the value
* is not set yet.
*/
export function getSession(key) {
const itemRaw = sessionStorage.getItem(key);
return itemRaw === null ? undefined : JSON.parse(itemRaw);
}
/**
* Thin wrapper around localStorage.setItem(). Converts value to JSON.
*/
export function setSession(key, value) {
sessionStorage.setItem(key, JSON.stringify(value));
}

View file

@ -60,6 +60,11 @@ $drawer-width: 240px;
text-align: center; text-align: center;
} }
#window
{
position: relative; /*window has it's own z-index inside of it*/
z-index: 1;
}
#window.drawer-closed #window.drawer-closed
{ {
#drawer { display: none } #drawer { display: none }
@ -100,12 +105,28 @@ $drawer-width: 240px;
.header-search .header-search
{ {
margin-left: 60px; margin-left: 60px;
$padding-adjust: 36px;
text-align: center; text-align: center;
.icon {
position: absolute;
top: $spacing-vertical * 1.5 / 2 + 2px; //hacked
margin-left: -$padding-adjust + 14px; //hacked
}
input[type="search"] { input[type="search"] {
position: relative;
left: -$padding-adjust;
background: rgba(255, 255, 255, 0.3); background: rgba(255, 255, 255, 0.3);
color: white; color: white;
width: 400px; width: 400px;
height: $spacing-vertical * 1.5;
line-height: $spacing-vertical * 1.5;
padding-left: $padding-adjust + 3;
padding-right: 3px;
@include border-radius(2px);
@include placeholder-color(#e8e8e8); @include placeholder-color(#e8e8e8);
&:focus {
box-shadow: $focus-box-shadow;
}
} }
} }
@ -159,26 +180,6 @@ nav.sub-header
{ {
padding: $spacing-vertical; padding: $spacing-vertical;
} }
h2
{
margin-bottom: $spacing-vertical;
}
h3, h4
{
margin-bottom: $spacing-vertical / 2;
margin-top: $spacing-vertical;
&:first-child
{
margin-top: 0;
}
}
.meta
{
+ h2, + h3, + h4
{
margin-top: 0;
}
}
} }
$header-icon-size: 1.5em; $header-icon-size: 1.5em;
@ -197,48 +198,6 @@ $header-icon-size: 1.5em;
padding: 0 6px 0 18px; padding: 0 6px 0 18px;
} }
.card {
margin-left: auto;
margin-right: auto;
max-width: 800px;
padding: $spacing-vertical;
background: $color-bg;
box-shadow: $default-box-shadow;
border-radius: 2px;
}
.card-obscured
{
position: relative;
}
.card-obscured .card-content {
-webkit-filter: blur($blur-intensity);
-moz-filter: blur($blur-intensity);
-o-filter: blur($blur-intensity);
-ms-filter: blur($blur-intensity);
filter: blur($blur-intensity);
}
.card-overlay {
position: absolute;
left: 0px;
right: 0px;
top: 0px;
bottom: 0px;
padding: 20px;
background-color: rgba(128, 128, 128, 0.8);
color: #fff;
display: flex;
align-items: center;
font-weight: 600;
}
.card-series-submit
{
margin-left: auto;
margin-right: auto;
max-width: 800px;
padding: $spacing-vertical / 2;
}
.full-screen .full-screen
{ {
width: 100%; width: 100%;

View file

@ -1,66 +0,0 @@
@import "global";
/* this probably shouldn't exist but it does so here we are - Jeremy */
textarea,
select,
input[type="text"],
input[type="password"],
input[type="email"],
input[type="number"],
input[type="search"],
input[type="date"] {
@include placeholder {
color: lighten($color-text-dark, 60%);
}
transition: all $transition-standard;
cursor: pointer;
padding-left: 1px;
padding-right: 1px;
box-sizing: border-box;
-webkit-appearance: none;
&[readonly] {
background-color: #bbb;
}
}
input[type="text"],
input[type="password"],
input[type="email"],
input[type="number"],
input[type="search"],
input[type="date"] {
border-bottom: 2px solid $color-form-border;
line-height: $spacing-vertical - 4;
height: $spacing-vertical * 1.5;
&.form-field__input--error {
border-color: $color-error;
}
}
textarea:focus,
input[type="text"]:focus,
input[type="password"]:focus,
input[type="email"]:focus,
input[type="number"]:focus,
input[type="search"]:focus,
input[type="date"]:focus {
border-color: $color-primary;
}
textarea {
border: 2px solid $color-form-border;
}
.form-row
{
+ .form-row
{
margin-top: $spacing-vertical;
}
+ .form-row-submit
{
margin-top: $spacing-vertical;
}
}

View file

@ -6,8 +6,10 @@ $padding-button: 12px;
$padding-text-link: 4px; $padding-text-link: 4px;
$color-primary: #155B4A; $color-primary: #155B4A;
$color-primary-light: saturate(lighten($color-primary, 50%), 20%);
$color-light-alt: hsl(hue($color-primary), 15, 85); $color-light-alt: hsl(hue($color-primary), 15, 85);
$color-text-dark: #000; $color-text-dark: #000;
$color-black-transparent: rgba(32,32,32,0.9);
$color-help: rgba(0,0,0,.6); $color-help: rgba(0,0,0,.6);
$color-notice: #8a6d3b; $color-notice: #8a6d3b;
$color-error: #a94442; $color-error: #a94442;
@ -30,7 +32,7 @@ $height-header: $spacing-vertical * 2.5;
$height-button: $spacing-vertical * 1.5; $height-button: $spacing-vertical * 1.5;
$default-box-shadow: 0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12); $default-box-shadow: 0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);
//$focus-box-shadow: 2px 4px 4px 0 rgba(0,0,0,.14),2px 5px 3px -2px rgba(0,0,0,.2),2px 3px 7px 0 rgba(0,0,0,.12); $focus-box-shadow: 2px 4px 4px 0 rgba(0,0,0,.14),2px 5px 3px -2px rgba(0,0,0,.2),2px 3px 7px 0 rgba(0,0,0,.12);
$transition-standard: .225s ease; $transition-standard: .225s ease;

View file

@ -38,6 +38,7 @@
text-align: center; text-align: center;
} }
/*
section section
{ {
margin-bottom: $spacing-vertical; margin-bottom: $spacing-vertical;
@ -46,10 +47,10 @@ section
margin-bottom: 0; margin-bottom: 0;
} }
&:only-child { &:only-child {
/* If it's an only child, assume it's part of a React layout that will handle the last child condition on its own */
margin-bottom: $spacing-vertical; margin-bottom: $spacing-vertical;
} }
} }
*/
main h1 { main h1 {
font-size: 2.0em; font-size: 2.0em;
@ -178,7 +179,8 @@ p
background: rgb(255, 255, 255); background: rgb(255, 255, 255);
overflow: auto; overflow: auto;
border-radius: 4px; border-radius: 4px;
padding: 36px; padding: $spacing-vertical;
box-shadow: $default-box-shadow;
max-width: 250px; max-width: 250px;
} }

View file

@ -7,6 +7,10 @@ body, div, dl, dt, dd, ul, ol, li, h1, h2, h3, h4, h5, h6, pre, code, form, fiel
{ {
outline: 0; outline: 0;
} }
input::-webkit-search-cancel-button {
/* Remove default */
-webkit-appearance: none;
}
table table
{ {
border-collapse: collapse; border-collapse: collapse;

View file

@ -1,12 +1,12 @@
@import "_reset"; @import "_reset";
@import "_grid"; @import "_grid";
@import "_icons"; @import "_icons";
@import "_form";
@import "_mediaelement"; @import "_mediaelement";
@import "_canvas"; @import "_canvas";
@import "_gui"; @import "_gui";
@import "component/_table"; @import "component/_table";
@import "component/_button.scss"; @import "component/_button.scss";
@import "component/_card.scss";
@import "component/_file-actions.scss"; @import "component/_file-actions.scss";
@import "component/_file-tile.scss"; @import "component/_file-tile.scss";
@import "component/_form-field.scss"; @import "component/_form-field.scss";
@ -16,7 +16,7 @@
@import "component/_channel-indicator.scss"; @import "component/_channel-indicator.scss";
@import "component/_notice.scss"; @import "component/_notice.scss";
@import "component/_modal-page.scss"; @import "component/_modal-page.scss";
@import "component/_notification-bar.scss"; @import "component/_snack-bar.scss";
@import "page/_developer.scss"; @import "page/_developer.scss";
@import "page/_watch.scss"; @import "page/_watch.scss";
@import "page/_reward.scss"; @import "page/_reward.scss";

View file

@ -0,0 +1,122 @@
@import "../global";
$padding-card-horizontal: $spacing-vertical * 2/3;
.card {
margin-left: auto;
margin-right: auto;
max-width: 800px;
background: $color-bg;
box-shadow: $default-box-shadow;
border-radius: 2px;
margin-bottom: $spacing-vertical * 2/3;
overflow: auto;
}
.card--obscured
{
position: relative;
}
.card--obscured .card__inner {
-webkit-filter: blur($blur-intensity);
-moz-filter: blur($blur-intensity);
-o-filter: blur($blur-intensity);
-ms-filter: blur($blur-intensity);
filter: blur($blur-intensity);
}
.card__title-primary {
padding: 0 $padding-card-horizontal;
margin-top: $spacing-vertical;
}
.card__title-identity {
padding: 0 $padding-card-horizontal;
margin-top: $spacing-vertical * 1/3;
margin-bottom: $spacing-vertical * 1/3;
}
.card__actions {
padding: 0 $padding-card-horizontal;
}
.card__actions {
margin-top: $spacing-vertical * 2/3;
}
.card__actions--bottom {
margin-top: $spacing-vertical * 1/3;
margin-bottom: $spacing-vertical * 1/3;
}
.card__actions--form-submit {
margin-top: $spacing-vertical;
margin-bottom: $spacing-vertical * 2/3;
}
.card__content {
margin-top: $spacing-vertical * 2/3;
margin-bottom: $spacing-vertical * 2/3;
padding: 0 $padding-card-horizontal;
}
.card__subtext {
color: #444;
margin-top: 12px;
font-size: 0.9em;
margin-top: $spacing-vertical * 2/3;
margin-bottom: $spacing-vertical * 2/3;
padding: 0 $padding-card-horizontal;
}
.card__subtext--two-lines {
height: $font-size * 0.9 * $font-line-height * 2;
}
.card-overlay {
position: absolute;
left: 0px;
right: 0px;
top: 0px;
bottom: 0px;
padding: 20px;
background-color: rgba(128, 128, 128, 0.8);
color: #fff;
display: flex;
align-items: center;
font-weight: 600;
}
.card__media img {
max-width: 100%;;
display: block;
margin-left: auto;
margin-right: auto;
}
$width-card-small: $spacing-vertical * 12;
.card--small {
width: $width-card-small;
}
.card--small .card__media {
max-height: $width-card-small * 9 / 16;
img {
max-height: $width-card-small * 9 / 16;
}
}
.card__subtitle {
color: $color-meta-light;
font-size: 0.85em;
}
.card-series-submit
{
margin-left: auto;
margin-right: auto;
max-width: 800px;
padding: $spacing-vertical / 2;
}
.card-row {
> .card {
vertical-align: top;
display: inline-block;
margin-right: $spacing-vertical / 3;
}
+ .card-row {
margin-top: $spacing-vertical * 1/3;
}
}
.card-row__header {
margin-bottom: $spacing-vertical / 3;
}

View file

@ -2,10 +2,10 @@
.file-tile__row { .file-tile__row {
height: $spacing-vertical * 7; height: $spacing-vertical * 7;
}
.file-tile__row--unavailable { .file-price {
opacity: 0.5; float: right;
}
} }
.file-tile__thumbnail { .file-tile__thumbnail {
@ -20,10 +20,6 @@
font-weight: bold; font-weight: bold;
} }
.file-tile__cost {
float: right;
}
.file-tile__description { .file-tile__description {
color: #444; color: #444;
margin-top: 12px; margin-top: 12px;

View file

@ -1,14 +1,88 @@
@import "../global"; @import "../global";
.form-field { .form-row-submit
display: inline-block; {
margin-top: $spacing-vertical;
} }
.form-field__label { $height-input: $spacing-vertical * 1.5;
.form-row__label-row {
margin-top: $spacing-vertical * 2/3; margin-top: $spacing-vertical * 2/3;
margin-bottom: $spacing-vertical * 1/3; margin-bottom: $spacing-vertical * 1/3;
line-height: 1; line-height: 1;
} }
.form-row__label-row--prefix {
float: left;
margin-right: 5px;
line-height: $height-input;
}
.form-field {
display: inline-block;
input[type="checkbox"],
input[type="radio"] {
cursor: pointer;
}
textarea,
select,
input[type="text"],
input[type="password"],
input[type="email"],
input[type="number"],
input[type="search"],
input[type="date"] {
@include placeholder {
color: lighten($color-text-dark, 60%);
}
transition: all $transition-standard;
cursor: pointer;
padding-left: 1px;
padding-right: 1px;
box-sizing: border-box;
-webkit-appearance: none;
&[readonly] {
background-color: #bbb;
}
}
input[type="text"],
input[type="password"],
input[type="email"],
input[type="number"],
input[type="search"],
input[type="date"] {
border-bottom: 2px solid $color-form-border;
line-height: $spacing-vertical - 4;
height: $height-input;
&.form-field__input--error {
border-color: $color-error;
}
}
textarea:focus,
input[type="text"]:focus,
input[type="password"]:focus,
input[type="email"]:focus,
input[type="number"]:focus,
input[type="search"]:focus,
input[type="date"]:focus {
border-color: $color-primary;
}
textarea {
border: 2px solid $color-form-border;
}
}
.form-field__label {
&[for] { cursor: pointer; }
> input[type="checkbox"], input[type="radio"] {
margin-right: 6px;
}
}
.form-field__label--error { .form-field__label--error {
color: $color-error; color: $color-error;
@ -18,8 +92,8 @@
width: 330px; width: 330px;
} }
.form-field__input-text-number { .form-field__input-number {
width: 50px; width: 100px;
} }
.form-field__error, .form-field__helper { .form-field__error, .form-field__helper {

View file

@ -19,14 +19,16 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
border: 1px solid rgb(204, 204, 204);
background: rgb(255, 255, 255);
overflow: auto;
}
.modal-page--full {
left: 0; left: 0;
right: 0; right: 0;
top: 0; top: 0;
bottom: 0; bottom: 0;
border: 1px solid rgb(204, 204, 204);
background: rgb(255, 255, 255);
overflow: auto;
} }
/* /*

View file

@ -1,6 +0,0 @@
@import "../global";
.notification-bar {
margin-top: 5px;
margin-right: 10px;
}

View file

@ -0,0 +1,42 @@
@import "../global";
$padding-snack-horizontal: $spacing-vertical;
.snack-bar {
$height-snack: $spacing-vertical * 2;
$padding-snack-vertical: $spacing-vertical / 4;
line-height: $height-snack - $padding-snack-vertical * 2;
padding: $padding-snack-vertical $padding-snack-horizontal;
position: fixed;
top: $spacing-vertical;
left: 0;
right: 0;
margin-left: auto;
margin-right: auto;
min-width: 300px;
max-width: 500px;
background: $color-black-transparent;
color: #f0f0f0;
display: flex;
justify-content: space-between;
align-items: center;
border-radius: 2px;
transition: all $transition-standard;
z-index: 10000; /*hack to get it over react modal */
}
.snack-bar__action {
display: inline-block;
text-transform: uppercase;
color: $color-primary-light;
margin: 0px 0px 0px $padding-snack-horizontal;
min-width: min-content;
&:hover {
text-decoration: underline;
}
}