Early Access app #46

Merged
kauffj merged 51 commits from rewards into master 2017-04-17 16:02:53 +02:00
58 changed files with 2975 additions and 1605 deletions

1
.gitignore vendored
View file

@ -6,6 +6,7 @@ dist
/app/node_modules /app/node_modules
/build/venv /build/venv
/lbry-app-venv /lbry-app-venv
/lbry-venv
/daemon/build /daemon/build
/daemon/venv /daemon/venv
/daemon/requirements.txt /daemon/requirements.txt

View file

@ -62,9 +62,9 @@ function getPidsForProcessName(name) {
} }
function createWindow () { function createWindow () {
win = new BrowserWindow({backgroundColor: '#155B4A'}) //$color-primary win = new BrowserWindow({backgroundColor: '#155B4A', minWidth: 800, minHeight: 600 }) //$color-primary
win.maximize() win.maximize()
//win.webContents.openDevTools() win.webContents.openDevTools();
win.loadURL(`file://${__dirname}/dist/index.html`) win.loadURL(`file://${__dirname}/dist/index.html`)
win.on('closed', () => { win.on('closed', () => {
win = null win = null

4
doitagain.sh Executable file
View file

@ -0,0 +1,4 @@
#!/bin/bash
rm -rf ~/.lbrynet/
rm -rf ~/.lbryum/
./node_modules/.bin/electron app

2
lbry

@ -1 +1 @@
Subproject commit e8bccec71c7424bf06d057904e4722d2d734fa3f Subproject commit 043e2d0ab96030468d53d02e311fd848f35c2dc1

2
lbryum

@ -1 +1 @@
Subproject commit 39ace3737509ff2b09fabaaa64d1525843de1325 Subproject commit 121bda3963ee94f0c9c027813c55b71b38219739

View file

@ -7,13 +7,12 @@ 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 RewardsPage from './page/rewards.js';
import ReferralPage from './page/referral.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';
@ -38,17 +37,11 @@ var App = React.createClass({
message: 'Error message', message: 'Error message',
data: 'Error data', data: 'Error data',
}, },
_fullScreenPages: ['watch'],
_upgradeDownloadItem: null, _upgradeDownloadItem: null,
_isMounted: false, _isMounted: false,
_version: null, _version: null,
// Temporary workaround since electron-dl throws errors when you try to get the filename
getDefaultProps: function() {
return {
address: window.location.search
};
},
getUpdateUrl: function() { getUpdateUrl: function() {
switch (process.platform) { switch (process.platform) {
case 'darwin': case 'darwin':
@ -87,7 +80,7 @@ var App = React.createClass({
var match, param, val, viewingPage, pageArgs, var match, param, val, viewingPage, pageArgs,
drawerOpenRaw = sessionStorage.getItem('drawerOpen'); drawerOpenRaw = sessionStorage.getItem('drawerOpen');
return Object.assign(this.getViewingPageAndArgs(this.props.address), { return Object.assign(this.getViewingPageAndArgs(window.location.search), {
drawerOpen: drawerOpenRaw !== null ? JSON.parse(drawerOpenRaw) : true, drawerOpen: drawerOpenRaw !== null ? JSON.parse(drawerOpenRaw) : true,
errorInfo: null, errorInfo: null,
modal: null, modal: null,
@ -112,6 +105,8 @@ var App = React.createClass({
if (target.matches('a[href^="?"]')) { if (target.matches('a[href^="?"]')) {
event.preventDefault(); event.preventDefault();
if (this._isMounted) { if (this._isMounted) {
history.pushState({}, document.title, target.getAttribute('href'));
this.registerHistoryPop();
this.setState(this.getViewingPageAndArgs(target.getAttribute('href'))); this.setState(this.getViewingPageAndArgs(target.getAttribute('href')));
} }
} }
@ -153,6 +148,11 @@ var App = React.createClass({
componentWillUnmount: function() { componentWillUnmount: function() {
this._isMounted = false; this._isMounted = false;
}, },
registerHistoryPop: function() {
window.addEventListener("popstate", function() {
this.setState(this.getViewingPageAndArgs(location.pathname));
}.bind(this));
},
handleUpgradeClicked: function() { handleUpgradeClicked: function() {
// Make a new directory within temp directory so the filename is guaranteed to be available // Make a new directory within temp directory so the filename is guaranteed to be available
const dir = fs.mkdtempSync(app.getPath('temp') + require('path').sep); const dir = fs.mkdtempSync(app.getPath('temp') + require('path').sep);
@ -231,14 +231,12 @@ var App = React.createClass({
case 'wallet': case 'wallet':
case 'send': case 'send':
case 'receive': case 'receive':
case 'claim': case 'rewards':
case 'referral':
return { return {
'?wallet' : 'Overview', '?wallet': 'Overview',
'?send' : 'Send', '?send': 'Send',
'?receive' : 'Receive', '?receive': 'Receive',
'?claim' : 'Claim Beta Code', '?rewards': 'Rewards',
'?referral' : 'Check Referral Credit',
}; };
case 'downloaded': case 'downloaded':
case 'published': case 'published':
@ -258,8 +256,6 @@ var App = React.createClass({
return <SettingsPage />; return <SettingsPage />;
case 'help': case 'help':
return <HelpPage />; return <HelpPage />;
case 'watch':
return <WatchPage uri={this.state.pageArgs} />;
case 'report': case 'report':
return <ReportPage />; return <ReportPage />;
case 'downloaded': case 'downloaded':
@ -268,10 +264,8 @@ var App = React.createClass({
return <FileListPublished />; return <FileListPublished />;
case 'start': case 'start':
return <StartPage />; return <StartPage />;
case 'claim': case 'rewards':
return <ClaimCodePage />; return <RewardsPage />;
case 'referral':
return <ReferralPage />;
case 'wallet': case 'wallet':
case 'send': case 'send':
case 'receive': case 'receive':
@ -284,16 +278,16 @@ var App = React.createClass({
return <DeveloperPage />; return <DeveloperPage />;
case 'discover': case 'discover':
default: default:
return <DiscoverPage {... this.state.pageArgs !== null ? {query: this.state.pageArgs} : {} } />; return <DiscoverPage showWelcome={this.state.justRegistered} {... this.state.pageArgs !== null ? {query: this.state.pageArgs} : {} } />;
} }
}, },
render: function() { render: function() {
var mainContent = this.getMainContent(), var mainContent = this.getMainContent(),
headerLinks = this.getHeaderLinks(), headerLinks = this.getHeaderLinks(),
searchQuery = this.state.viewingPage == 'discover' && this.state.pageArgs ? this.state.pageArgs : ''; searchQuery = this.state.viewingPage == 'discover' && this.state.pageArgs ? this.state.pageArgs : '';
return ( return (
this.state.viewingPage == 'watch' ? this._fullScreenPages.includes(this.state.viewingPage) ?
mainContent : mainContent :
<div id="window" className={ this.state.drawerOpen ? 'drawer-open' : 'drawer-closed' }> <div id="window" className={ this.state.drawerOpen ? 'drawer-open' : 'drawer-closed' }>
<Drawer onCloseDrawer={this.closeDrawer} viewingPage={this.state.viewingPage} /> <Drawer onCloseDrawer={this.closeDrawer} viewingPage={this.state.viewingPage} />

246
ui/js/component/auth.js Normal file
View file

@ -0,0 +1,246 @@
import React from 'react';
import lbryio from '../lbryio.js';
import Modal from './modal.js';
import ModalPage from './modal-page.js';
import {Link, RewardLink} from '../component/link.js';
import {FormField, FormRow} from '../component/form.js';
import {CreditAmount} from '../component/common.js';
import rewards from '../rewards.js';
const SubmitEmailStage = React.createClass({
getInitialState: function() {
return {
rewardType: null,
email: '',
submitting: false
};
},
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.onEmailSaved();
}, (error) => {
if (this._emailRow) {
this._emailRow.showError(error.message)
}
this.setState({ submitting: false });
});
},
render: function() {
return (
<section>
<form onSubmit={this.handleSubmit}>
<FormRow ref={(ref) => { this._emailRow = ref }} type="text" label="Email" placeholder="admin@toplbryfan.com"
name="email" value={this.state.email}
onChange={this.handleEmailChanged} />
<div className="form-row-submit">
<Link button="primary" label="Next" disabled={this.state.submitting} onClick={this.handleSubmit} />
</div>
</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,
});
const onSubmitError = function(error) {
if (this._codeRow) {
this._codeRow.showError(error.message)
}
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() {
return (
<section>
<form onSubmit={this.handleSubmit}>
<FormRow label="Verification Code" ref={(ref) => { this._codeRow = ref }} type="text"
name="code" placeholder="a94bXXXXXXXXXXXXXX" value={this.state.code} onChange={this.handleCodeChanged}
helper="A verification code is required to access this version."/>
<div className="form-row-submit">
<Link button="primary" label="Verify" disabled={this.state.submitting} onClick={this.handleSubmit} />
</div>
</form>
</section>
);
}
});
const WelcomeStage = React.createClass({
propTypes: {
endAuth: React.PropTypes.func,
},
getInitialState: function() {
return {
hasReward: false,
rewardAmount: null,
}
},
onRewardClaim: function(reward) {
console.log(reward);
this.setState({
hasReward: true,
rewardAmount: reward.amount
})
},
render: function() {
return (
!this.state.hasReward ?
<Modal type="custom" isOpen={true} contentLabel="Welcome to LBRY" {...this.props}>
<section>
<h3 className="modal__header">Welcome to LBRY.</h3>
<p>Using LBRY is like dating a centaur. Totally normal up top, and <em>way different</em> underneath.</p>
<p>On the upper level, LBRY is like other popular video and media sites.</p>
<p>Below, LBRY is controlled by its users -- you -- through the power of blockchain and decentralization.</p>
<p>Thanks for making it possible! Here's a nickel, kid.</p>
<div style={{textAlign: "center", marginBottom: "12px"}}>
<RewardLink type="new_user" button="primary" onRewardClaim={this.onRewardClaim} onRewardFailure={this.props.endAuth} />
</div>
</section>
</Modal> :
<Modal type="alert" overlayClassName="modal-overlay modal-overlay--clear" isOpen={true} contentLabel="Welcome to LBRY" {...this.props} onConfirmed={this.props.endAuth}>
<section>
<h3 className="modal__header">About Your Reward</h3>
<p>You earned a reward of <CreditAmount amount={this.state.rewardAmount} label={false} /> LBRY credits, or <em>LBC</em>.</p>
<p>This reward will show in your Wallet momentarily, probably while you are reading this message.</p>
<p>LBC is used to compensate creators, to publish, and to have say in how the network works.</p>
<p>No need to understand it all just yet! Try watching or downloading something next.</p>
</section>
</Modal>
);
}
});
const ErrorStage = React.createClass({
render: function() {
return (
<section>
<p>An error was encountered that we cannot continue from.</p>
<p>At least we're earning the name beta.</p>
<Link button="alt" label="Try Reload" onClick={() => { window.location.reload() } } />
</section>
);
}
});
const PendingStage = React.createClass({
render: function() {
return (
<section>
<p>Preparing for first access <span className="busy-indicator"></span></p>
</section>
);
}
});
export const AuthOverlay = React.createClass({
_stages: {
pending: PendingStage,
error: ErrorStage,
email: SubmitEmailStage,
confirm: ConfirmEmailStage,
welcome: WelcomeStage
},
getInitialState: function() {
return {
stage: null,
stageProps: {}
};
},
endAuth: function() {
this.setState({
stage: null
});
},
componentWillMount: function() {
lbryio.authenticate().then(function(user) {
if (!user.HasVerifiedEmail) { //oops I fucked this up
this.setState({
stage: "email",
stageProps: {
onEmailSaved: function() {
this.setState({
stage: "confirm",
stageProps: {
onEmailConfirmed: function() { this.setState({ stage: "welcome"}) }.bind(this)
}
})
}.bind(this)
}
})
} else {
lbryio.call('reward', 'list', {}).then(function(userRewards) {
userRewards.filter(function(reward) {
return reward.RewardType == "new_user" && reward.TransactionID;
}).length ?
this.endAuth() :
this.setState({ stage: "welcome" })
}.bind(this));
}
}.bind(this)).catch((err) => {
this.setState({
stage: "error",
stageProps: { errorText: err.message }
})
document.dispatchEvent(new CustomEvent('unhandledError', {
detail: {
message: err.message,
data: err.stack
}
}));
})
},
render: function() {
if (!this.state.stage) {
return null;
}
const StageContent = this._stages[this.state.stage];
return (
this.state.stage != "welcome" ?
<ModalPage className="modal-page--full" isOpen={true} contentLabel="Authentication" {...this.props}>
<h1>LBRY Early Access</h1>
<StageContent {...this.state.stageProps} />
</ModalPage> :
<StageContent endAuth={this.endAuth} {...this.state.stageProps} />
);
}
});

View file

@ -3,20 +3,18 @@ import lbry from '../lbry.js';
import uri from '../uri.js'; import uri from '../uri.js';
import {Icon} from './common.js'; import {Icon} from './common.js';
const ChannelIndicator = React.createClass({ const UriIndicator = React.createClass({
propTypes: { propTypes: {
uri: React.PropTypes.string.isRequired, uri: React.PropTypes.string.isRequired,
hasSignature: React.PropTypes.bool.isRequired, hasSignature: React.PropTypes.bool.isRequired,
signatureIsValid: React.PropTypes.bool, signatureIsValid: React.PropTypes.bool,
}, },
render: function() { render: function() {
if (!this.props.hasSignature) {
return null;
}
const uriObj = uri.parseLbryUri(this.props.uri); const uriObj = uri.parseLbryUri(this.props.uri);
if (!uriObj.isChannel) {
return null; if (!this.props.hasSignature || !uriObj.isChannel) {
return <span className="empty">Anonymous</span>;
} }
const channelUriObj = Object.assign({}, uriObj); const channelUriObj = Object.assign({}, uriObj);
@ -25,7 +23,6 @@ const ChannelIndicator = React.createClass({
let icon, modifier; let icon, modifier;
if (this.props.signatureIsValid) { if (this.props.signatureIsValid) {
icon = 'icon-check-circle';
modifier = 'valid'; modifier = 'valid';
} else { } else {
icon = 'icon-times-circle'; icon = 'icon-times-circle';
@ -33,11 +30,13 @@ const ChannelIndicator = React.createClass({
} }
return ( return (
<span> <span>
by <strong>{channelUri}</strong> {' '} {channelUri} {' '}
<Icon icon={icon} className={`channel-indicator__icon channel-indicator__icon--${modifier}`} /> { !this.props.signatureIsValid ?
<Icon icon={icon} className={`channel-indicator__icon channel-indicator__icon--${modifier}`} /> :
'' }
</span> </span>
); );
} }
}); });
export default ChannelIndicator; export default UriIndicator;

View file

@ -54,35 +54,90 @@ export let BusyMessage = React.createClass({
} }
}); });
var creditAmountStyle = {
color: '#216C2A',
fontWeight: 'bold',
fontSize: '0.8em'
}, estimateStyle = {
fontSize: '0.8em',
color: '#aaa',
};
export let CurrencySymbol = React.createClass({ export let CurrencySymbol = React.createClass({
render: function() { return <span>LBC</span>; } render: function() { return <span>LBC</span>; }
}); });
export let CreditAmount = React.createClass({ export let CreditAmount = React.createClass({
propTypes: { propTypes: {
amount: React.PropTypes.number, amount: React.PropTypes.number.isRequired,
precision: React.PropTypes.number precision: React.PropTypes.number,
label: React.PropTypes.bool
},
getDefaultProps: function() {
return {
precision: 1,
label: true,
}
}, },
render: function() { render: function() {
var formattedAmount = lbry.formatCredits(this.props.amount, this.props.precision ? this.props.precision : 1); var formattedAmount = lbry.formatCredits(this.props.amount, this.props.precision);
return ( return (
<span className="credit-amount"> <span className="credit-amount">
<span style={creditAmountStyle}>{formattedAmount} {parseFloat(formattedAmount) == 1.0 ? 'credit' : 'credits'}</span> <span>
{ this.props.isEstimate ? <span style={estimateStyle}> (est)</span> : null } {formattedAmount}
{this.props.label ?
(parseFloat(formattedAmount) == 1.0 ? ' credit' : ' credits') : '' }
</span>
{ this.props.isEstimate ? <span className="credit-amount__estimate" title="This is an estimate and does not include data fees">*</span> : null }
</span> </span>
); );
} }
}); });
export let FilePrice = React.createClass({
_isMounted: false,
propTypes: {
metadata: React.PropTypes.object,
uri: React.PropTypes.string.isRequired,
},
getInitialState: function() {
return {
cost: null,
isEstimate: null,
}
},
componentDidMount: function() {
this._isMounted = true;
lbry.getCostInfo(this.props.uri).then(({cost, includesData}) => {
if (this._isMounted) {
this.setState({
cost: cost,
isEstimate: includesData,
});
}
}, (err) => {
// If we get an error looking up cost information, do nothing
});
},
componentWillUnmount: function() {
this._isMounted = false;
},
render: function() {
if (this.state.cost === null && this.props.metadata) {
if (!this.props.metadata.fee) {
return <span className="credit-amount">free*</span>;
} else {
if (this.props.metadata.fee.currency === "LBC") {
return <CreditAmount label={false} amount={this.props.metadata.fee.amount} isEstimate={true} />
} else if (this.props.metadata.fee.currency === "USD") {
return <span className="credit-amount">???</span>;
}
}
}
return (
this.state.cost !== null ?
<CreditAmount amount={this.state.cost} label={false} isEstimate={!this.state.isEstimate}/> :
<span className="credit-amount">???</span>
);
}
});
var addressStyle = { var addressStyle = {
fontFamily: '"Consolas", "Lucida Console", "Adobe Source Code Pro", monospace', fontFamily: '"Consolas", "Lucida Console", "Adobe Source Code Pro", monospace',
}; };
@ -131,6 +186,9 @@ export let Thumbnail = React.createClass({
this._isMounted = false; this._isMounted = false;
}, },
render: function() { render: function() {
return <img ref="img" onError={this.handleError} {... this.props} src={this.state.imageUri} /> const className = this.props.className ? this.props.className : '',
otherProps = Object.assign({}, this.props)
delete otherProps.className;
return <img ref="img" onError={this.handleError} {...otherProps} className={className} src={this.state.imageUri} />
}, },
}); });

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

@ -2,66 +2,13 @@ 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 {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';
const {shell} = require('electron'); const {shell} = require('electron');
let WatchLink = React.createClass({
propTypes: {
uri: React.PropTypes.string,
downloadStarted: React.PropTypes.bool,
},
startVideo: function() {
window.location = '?watch=' + this.props.uri;
},
handleClick: function() {
this.setState({
loading: true,
});
if (this.props.downloadStarted) {
this.startVideo();
} else {
lbry.getCostInfo(this.props.uri, ({cost}) => {
lbry.getBalance((balance) => {
if (cost > balance) {
this.setState({
modal: 'notEnoughCredits',
loading: false,
});
} else {
this.startVideo();
}
});
});
}
},
getInitialState: function() {
return {
modal: null,
loading: false,
};
},
closeModal: function() {
this.setState({
modal: null,
});
},
render: function() {
return (
<div className="button-set-item">
<Link button="primary" 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}>
You don't have enough LBRY credits to pay for this stream.
</Modal>
</div>
);
}
});
let FileActionsRow = React.createClass({ let FileActionsRow = React.createClass({
_isMounted: false, _isMounted: false,
_fileInfoSubscribeId: null, _fileInfoSubscribeId: null,
@ -79,7 +26,7 @@ let FileActionsRow = React.createClass({
menuOpen: false, menuOpen: false,
deleteChecked: false, deleteChecked: false,
attemptingDownload: false, attemptingDownload: false,
attemptingRemove: false attemptingRemove: false,
} }
}, },
onFileInfoUpdate: function(fileInfo) { onFileInfoUpdate: function(fileInfo) {
@ -95,14 +42,14 @@ let FileActionsRow = React.createClass({
attemptingDownload: true, attemptingDownload: true,
attemptingRemove: false attemptingRemove: false
}); });
lbry.getCostInfo(this.props.uri, ({cost}) => { lbry.getCostInfo(this.props.uri).then(({cost}) => {
lbry.getBalance((balance) => { lbry.getBalance((balance) => {
if (cost > balance) { if (cost > balance) {
this.setState({ this.setState({
modal: 'notEnoughCredits', modal: 'notEnoughCredits',
attemptingDownload: false, attemptingDownload: false,
}); });
} else { } else if (this.state.affirmedPurchase) {
lbry.get({uri: this.props.uri}).then((streamInfo) => { lbry.get({uri: this.props.uri}).then((streamInfo) => {
if (streamInfo === null || typeof streamInfo !== 'object') { if (streamInfo === null || typeof streamInfo !== 'object') {
this.setState({ this.setState({
@ -111,6 +58,11 @@ let FileActionsRow = React.createClass({
}); });
} }
}); });
} else {
this.setState({
attemptingDownload: false,
modal: 'affirmPurchase'
})
} }
}); });
}); });
@ -153,6 +105,13 @@ let FileActionsRow = React.createClass({
attemptingDownload: false attemptingDownload: false
}); });
}, },
onAffirmPurchase: function() {
this.setState({
affirmedPurchase: true,
modal: null
});
this.tryDownload();
},
openMenu: function() { openMenu: function() {
this.setState({ this.setState({
menuOpen: !this.state.menuOpen, menuOpen: !this.state.menuOpen,
@ -198,9 +157,6 @@ let FileActionsRow = React.createClass({
return ( return (
<div> <div>
{this.props.contentType && this.props.contentType.startsWith('video/')
? <WatchLink uri={this.props.uri} downloadStarted={!!this.state.fileInfo} />
: null}
{this.state.fileInfo !== null || this.state.fileInfo.isMine {this.state.fileInfo !== null || this.state.fileInfo.isMine
? linkBlock ? linkBlock
: null} : null}
@ -209,6 +165,10 @@ let FileActionsRow = React.createClass({
<DropDownMenuItem key={0} onClick={this.handleRevealClicked} label={openInFolderMessage} /> <DropDownMenuItem key={0} onClick={this.handleRevealClicked} label={openInFolderMessage} />
<DropDownMenuItem key={1} onClick={this.handleRemoveClicked} label="Remove..." /> <DropDownMenuItem key={1} onClick={this.handleRemoveClicked} label="Remove..." />
</DropDownMenu> : '' } </DropDownMenu> : '' }
<Modal type="confirm" isOpen={this.state.modal == 'affirmPurchase'}
contentLabel="Confirm Purchase" onConfirmed={this.onAffirmPurchase} onAborted={this.closeModal}>
Confirm you want to purchase this bro.
</Modal>
<Modal isOpen={this.state.modal == 'notEnoughCredits'} contentLabel="Not enough credits" <Modal isOpen={this.state.modal == 'notEnoughCredits'} contentLabel="Not enough credits"
onConfirmed={this.closeModal}> 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.
@ -220,7 +180,7 @@ let FileActionsRow = React.createClass({
<Modal isOpen={this.state.modal == 'confirmRemove'} contentLabel="Not enough credits" <Modal isOpen={this.state.modal == 'confirmRemove'} contentLabel="Not enough credits"
type="confirm" confirmButtonLabel="Remove" onConfirmed={this.handleRemoveConfirmed} type="confirm" confirmButtonLabel="Remove" onConfirmed={this.handleRemoveConfirmed}
onAborted={this.closeModal}> onAborted={this.closeModal}>
<p>Are you sure you'd like to remove <cite>{this.props.metadata.title}</cite> from LBRY?</p> <p>Are you sure you'd like to remove <cite>{this.props.metadata ? this.props.metadata.title : this.props.uri}</cite> from LBRY?</p>
<label><FormField type="checkbox" checked={this.state.deleteChecked} onClick={this.handleDeleteCheckboxClicked} /> Delete this file from my computer</label> <label><FormField type="checkbox" checked={this.state.deleteChecked} onClick={this.handleDeleteCheckboxClicked} /> Delete this file from my computer</label>
</Modal> </Modal>
@ -261,6 +221,7 @@ export let FileActions = React.createClass({
componentDidMount: function() { componentDidMount: function() {
this._isMounted = true; this._isMounted = true;
this._fileInfoSubscribeId = lbry.fileInfoSubscribe(this.props.outpoint, this.onFileInfoUpdate); this._fileInfoSubscribeId = lbry.fileInfoSubscribe(this.props.outpoint, this.onFileInfoUpdate);
lbry.get_availability({uri: this.props.uri}, (availability) => { lbry.get_availability({uri: this.props.uri}, (availability) => {
if (this._isMounted) { if (this._isMounted) {
this.setState({ this.setState({
@ -294,7 +255,7 @@ export let FileActions = React.createClass({
? <FileActionsRow outpoint={this.props.outpoint} metadata={this.props.metadata} uri={this.props.uri} ? <FileActionsRow outpoint={this.props.outpoint} metadata={this.props.metadata} uri={this.props.uri}
contentType={this.props.contentType} /> contentType={this.props.contentType} />
: <div> : <div>
<div className="button-set-item empty">This file is not currently available.</div> <div className="button-set-item empty">Content unavailable.</div>
<ToolTip label="Why?" <ToolTip label="Why?"
body="The content on LBRY is hosted by its users. It appears there are no users connected that have this file at the moment." body="The content on LBRY is hosted by its users. It appears there are no users connected that have this file at the moment."
className="button-set-item" /> className="button-set-item" />

View file

@ -3,56 +3,8 @@ 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 {FileActions} from '../component/file-actions.js'; import {FileActions} from '../component/file-actions.js';
import {Thumbnail, TruncatedText, CreditAmount} from '../component/common.js'; import {Thumbnail, TruncatedText, FilePrice} from '../component/common.js';
import ChannelIndicator from '../component/channel-indicator.js'; import UriIndicator from '../component/channel-indicator.js';
let FilePrice = React.createClass({
_isMounted: false,
propTypes: {
uri: React.PropTypes.string
},
getInitialState: function() {
return {
cost: null,
costIncludesData: null,
}
},
componentDidMount: function() {
this._isMounted = true;
lbry.getCostInfo(this.props.uri, ({cost, includesData}) => {
if (this._isMounted) {
this.setState({
cost: cost,
costIncludesData: includesData,
});
}
}, (err) => {
console.log('error from getCostInfo callback:', err)
// If we get an error looking up cost information, do nothing
});
},
componentWillUnmount: function() {
this._isMounted = false;
},
render: function() {
if (this.state.cost === null)
{
return null;
}
return (
<span className="file-tile__cost">
<CreditAmount amount={this.state.cost} isEstimate={!this.state.costIncludesData}/>
</span>
);
}
});
/*should be merged into FileTile once FileTile is refactored to take a single id*/ /*should be merged into FileTile once FileTile is refactored to take a single id*/
export let FileTileStream = React.createClass({ export let FileTileStream = React.createClass({
@ -61,7 +13,7 @@ export let FileTileStream = React.createClass({
propTypes: { propTypes: {
uri: React.PropTypes.string, uri: React.PropTypes.string,
metadata: React.PropTypes.object.isRequired, metadata: React.PropTypes.object,
contentType: React.PropTypes.string.isRequired, contentType: React.PropTypes.string.isRequired,
outpoint: React.PropTypes.string, outpoint: React.PropTypes.string,
hasSignature: React.PropTypes.bool, hasSignature: React.PropTypes.bool,
@ -117,47 +69,47 @@ export let FileTileStream = React.createClass({
} }
}, },
render: function() { render: function() {
console.log('rendering.')
if (this.state.isHidden) { if (this.state.isHidden) {
console.log('hidden, so returning null')
return null; return null;
} }
console.log("inside FileTileStream. metadata is", this.props.metadata)
const lbryUri = uri.normalizeLbryUri(this.props.uri); const lbryUri = uri.normalizeLbryUri(this.props.uri);
const metadata = this.props.metadata; const metadata = this.props.metadata;
const isConfirmed = typeof metadata == 'object'; const isConfirmed = !!metadata;
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 file-tile__thumbnail-container">
<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" {... metadata && metadata.thumbnail ? {src: metadata.thumbnail} : {}} alt={'Photo for ' + (title || this.props.uri)} /></a>
</div> </div>
<div className="span9"> <div className="span9">
{ !this.props.hidePrice <div className="card__title-primary">
? <FilePrice uri={this.props.uri} /> { !this.props.hidePrice
: null} ? <FilePrice uri={this.props.uri} />
<div className="meta"><a href={'?show=' + this.props.uri}>{lbryUri}</a></div> : null}
<h3 className="file-tile__title"> <div className="meta"><a href={'?show=' + this.props.uri}>{lbryUri}</a></div>
<a href={'?show=' + this.props.uri}> <h3>
<TruncatedText lines={1}> <a href={'?show=' + this.props.uri}>
{title} <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={2}>
{isConfirmed
? metadata.description
: <span className="empty">This file is pending confirmation.</span>}
</TruncatedText> </TruncatedText>
</a> </p>
</h3> </div>
<ChannelIndicator uri={lbryUri} metadata={metadata} contentType={this.props.contentType}
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">
<TruncatedText lines={3}>
{isConfirmed
? metadata.description
: <span className="empty">This file is pending confirmation.</span>}
</TruncatedText>
</p>
</div> </div>
</div> </div>
{this.state.showNsfwHelp {this.state.showNsfwHelp
@ -173,6 +125,108 @@ export let FileTileStream = React.createClass({
} }
}); });
export let FileCardStream = React.createClass({
_fileInfoSubscribeId: null,
_isMounted: null,
_metadata: null,
propTypes: {
uri: React.PropTypes.string,
claimInfo: 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,
hasSignature: 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() {
this.setState({
hovered: true,
});
},
handleMouseOut: function() {
this.setState({
hovered: false,
});
},
render: function() {
if (this.state.isHidden) {
return null;
}
const lbryUri = uri.normalizeLbryUri(this.props.uri);
const metadata = this.props.metadata;
const isConfirmed = !!metadata;
const title = isConfirmed ? metadata.title : lbryUri;
const obscureNsfw = this.props.obscureNsfw && isConfirmed && metadata.nsfw;
const primaryUrl = '?show=' + lbryUri;
return (
<section className={ 'card card--small card--link ' + (obscureNsfw ? 'card--obscured ' : '') } onMouseEnter={this.handleMouseOver} onMouseLeave={this.handleMouseOut}>
<div className="card__inner">
<a href={primaryUrl} className="card__link">
<div className="card__title-identity">
<h5><TruncatedText lines={1}>{title}</TruncatedText></h5>
<div className="card__subtitle">
{ !this.props.hidePrice ? <span style={{float: "right"}}><FilePrice uri={this.props.uri} metadata={metadata} /></span> : null}
<UriIndicator uri={lbryUri} metadata={metadata} contentType={this.props.contentType}
hasSignature={this.props.hasSignature} signatureIsValid={this.props.signatureIsValid} />
</div>
</div>
<div className="card__media" style={{ backgroundImage: "url('" + metadata.thumbnail + "')" }}></div>
<div className="card__content card__subtext card__subtext--two-lines">
<TruncatedText lines={2}>
{isConfirmed
? metadata.description
: <span className="empty">This file is pending confirmation.</span>}
</TruncatedText>
</div>
</a>
{this.state.showNsfwHelp && this.state.hovered
? <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}
</div>
</section>
);
}
});
export let FileTile = React.createClass({ export let FileTile = React.createClass({
_isMounted: false, _isMounted: false,
@ -191,11 +245,12 @@ export let FileTile = React.createClass({
componentDidMount: function() { componentDidMount: function() {
this._isMounted = true; this._isMounted = true;
lbry.resolve({uri: this.props.uri}).then(({claim: claimInfo}) => { lbry.resolve({uri: this.props.uri}).then((resolutionInfo) => {
if (this._isMounted && claimInfo.value.stream.metadata) { if (this._isMounted && resolutionInfo && resolutionInfo.claim && resolutionInfo.claim.value &&
resolutionInfo.claim.value.stream && resolutionInfo.claim.value.stream.metadata) {
// In case of a failed lookup, metadata will be null, in which case the component will never display // In case of a failed lookup, metadata will be null, in which case the component will never display
this.setState({ this.setState({
claimInfo: claimInfo, claimInfo: resolutionInfo.claim,
}); });
} }
}); });
@ -210,7 +265,11 @@ export let FileTile = React.createClass({
const {txid, nout, has_signature, signature_is_valid, const {txid, nout, has_signature, signature_is_valid,
value: {stream: {metadata, source: {contentType}}}} = this.state.claimInfo; value: {stream: {metadata, source: {contentType}}}} = this.state.claimInfo;
return <FileTileStream outpoint={txid + ':' + nout} metadata={metadata} contentType={contentType}
hasSignature={has_signature} signatureIsValid={signature_is_valid} {... this.props} />; return this.props.displayStyle == 'card' ?
<FileCardStream outpoint={txid + ':' + nout} metadata={metadata} contentType={contentType}
hasSignature={has_signature} signatureIsValid={signature_is_valid} {... this.props}/> :
<FileTileStream outpoint={txid + ':' + nout} metadata={metadata} contentType={contentType}
hasSignature={has_signature} signatureIsValid={signature_is_valid} {... this.props} />;
} }
}); });

View file

@ -1,28 +1,32 @@
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 FormField = React.createClass({ function formFieldId() {
return "form-field-" + (++formFieldCounter);
}
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,
hidden: React.PropTypes.bool, prefix: React.PropTypes.string,
postfix: React.PropTypes.string,
hasError: React.PropTypes.bool
}, },
getInitialState: function() { getInitialState: function() {
return { return {
adviceState: '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') {
@ -33,25 +37,11 @@ var FormField = React.createClass({
this._element = this.props.type; this._element = this.props.type;
} }
}, },
showAdvice: function(text) { showError: function(text) {
this.setState({ this.setState({
adviceState: 'shown', isError: true,
adviceText: text, errorMessage: text,
}); });
setTimeout(() => {
this.setState({
adviceState: 'fading',
});
setTimeout(() => {
this.setState({
adviceState: 'hidden',
});
}, 450);
}, 5000);
},
warnRequired: function() {
this.showAdvice(this._fieldRequiredText);
}, },
focus: function() { focus: function() {
this.refs.field.focus(); this.refs.field.focus();
@ -60,7 +50,8 @@ var FormField = React.createClass({
if (this.props.type == 'checkbox') { if (this.props.type == 'checkbox') {
return this.refs.field.checked; return this.refs.field.checked;
} else if (this.props.type == 'file') { } else if (this.props.type == 'file') {
return this.refs.field.files[0].path; return this.refs.field.files.length && this.refs.field.files[0].path ?
this.refs.field.files[0].path : null;
} else { } else {
return this.refs.field.value; return this.refs.field.value;
} }
@ -70,45 +61,94 @@ 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),
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.hasError;
delete otherProps.className;
delete otherProps.postfix;
delete otherProps.prefix;
return ( const element = <this._element id={elementId} type={this._type} name={this.props.name} ref="field" placeholder={this.props.placeholder}
!this.props.hidden className={'form-field__input form-field__input-' + this.props.type + ' ' + (this.props.className || '') + (isError ? 'form-field__input--error' : '')}
? <div className="form-field-container"> {...otherProps}>
<this._element type={this._type} className="form-field" name={this.props.name} ref="field" placeholder={this.props.placeholder} {this.props.children}
className={'form-field--' + this.props.type + ' ' + (this.props.className || '')} </this._element>;
{...otherProps}>
{this.props.children} return <div className="form-field">
</this._element> { this.props.prefix ? <span className="form-field__prefix">{this.props.prefix}</span> : '' }
<FormFieldAdvice field={this.refs.field} state={this.state.adviceState}>{this.state.adviceText}</FormFieldAdvice> { renderElementInsideLabel ?
</div> <label htmlFor={elementId} className={"form-field__label " + (isError ? 'form-field__label--error' : '')}>
: null {element}
); {this.props.label}
</label> :
element }
{ this.props.postfix ? <span className="form-field__postfix">{this.props.postfix}</span> : '' }
{ isError && this.state.errorMessage ? <div className="form-field__error">{this.state.errorMessage}</div> : '' }
</div>
} }
}); })
var FormFieldAdvice = React.createClass({ export let FormRow = React.createClass({
_fieldRequiredText: 'This field is required',
propTypes: { propTypes: {
state: React.PropTypes.string.isRequired, label: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.element])
// helper: React.PropTypes.html,
},
getInitialState: function() {
return {
isError: false,
errorMessage: null,
}
},
showError: function(text) {
this.setState({
isError: true,
errorMessage: text,
});
},
showRequiredError: function() {
this.showError(this._fieldRequiredText);
},
clearError: function(text) {
this.setState({
isError: false,
errorMessage: ''
});
},
getValue: function() {
return this.refs.field.getValue();
},
getSelectedElement: function() {
return this.refs.field.getSelectedElement();
},
focus: function() {
this.refs.field.focus();
}, },
render: function() { render: function() {
return ( const fieldProps = Object.assign({}, this.props),
this.props.state != 'hidden' elementId = formFieldId(),
? <div className="form-field-advice-container"> renderLabelInFormField = formFieldNestedLabelTypes.includes(this.props.type);
<div className={'form-field-advice' + (this.props.state == 'fading' ? ' form-field-advice--fading' : '')}>
<Icon icon="icon-caret-up" className="form-field-advice__arrow" />
<div className="form-field-advice__content-container">
<span className="form-field-advice__content">
{this.props.children}
</span>
</div>
</div>
</div>
: null
);
}
});
export default FormField; 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,5 +1,6 @@
import React from 'react'; import React from 'react';
import {Link} from './link.js'; import {Link} from './link.js';
import {Icon} from './common.js';
var Header = React.createClass({ var Header = React.createClass({
getInitialState: function() { getInitialState: function() {
@ -52,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>

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: {
@ -52,4 +54,80 @@ export let Link = React.createClass({
</a> </a>
); );
} }
});
export let RewardLink = React.createClass({
propTypes: {
type: React.PropTypes.string.isRequired,
claimed: React.PropTypes.bool,
onRewardClaim: React.PropTypes.func,
onRewardFailure: 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((reward) => {
this.setState({
pending: false,
errorMessage: null
})
if (this.props.onRewardClaim) {
this.props.onRewardClaim(reward);
}
}).catch((error) => {
this.setState({
errorMessage: error.message,
pending: false
})
})
},
clearError: function() {
if (this.props.onRewardFailure) {
this.props.onRewardFailure()
}
this.setState({
errorMessage: null
})
},
render: function() {
return (
<div className="reward-link">
{this.props.claimed
? <span><Icon icon="icon-check" /> Reward claimed.</span>
: <Link button={this.props.button ? this.props.button : 'alt'} disabled={this.state.pending || !this.state.claimable }
label={ this.state.pending ? "Claiming..." : "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

@ -0,0 +1,18 @@
import React from 'react';
import ReactModal from 'react-modal';
export const ModalPage = React.createClass({
render: function() {
return (
<ReactModal onCloseRequested={this.props.onAborted || this.props.onConfirmed} {...this.props}
className={(this.props.className || '') + ' modal-page'}
overlayClassName="modal-overlay">
<div className="modal-page__content">
{this.props.children}
</div>
</ReactModal>
);
}
});
export default ModalPage;

View file

@ -6,6 +6,7 @@ import {Link} from './link.js';
export const Modal = React.createClass({ export const Modal = React.createClass({
propTypes: { propTypes: {
type: React.PropTypes.oneOf(['alert', 'confirm', 'custom']), type: React.PropTypes.oneOf(['alert', 'confirm', 'custom']),
overlay: React.PropTypes.bool,
onConfirmed: React.PropTypes.func, onConfirmed: React.PropTypes.func,
onAborted: React.PropTypes.func, onAborted: React.PropTypes.func,
confirmButtonLabel: React.PropTypes.string, confirmButtonLabel: React.PropTypes.string,
@ -16,6 +17,7 @@ export const Modal = React.createClass({
getDefaultProps: function() { getDefaultProps: function() {
return { return {
type: 'alert', type: 'alert',
overlay: true,
confirmButtonLabel: 'OK', confirmButtonLabel: 'OK',
abortButtonLabel: 'Cancel', abortButtonLabel: 'Cancel',
confirmButtonDisabled: false, confirmButtonDisabled: false,
@ -26,7 +28,7 @@ export const Modal = React.createClass({
return ( return (
<ReactModal onCloseRequested={this.props.onAborted || this.props.onConfirmed} {...this.props} <ReactModal onCloseRequested={this.props.onAborted || this.props.onConfirmed} {...this.props}
className={(this.props.className || '') + ' modal'} className={(this.props.className || '') + ' modal'}
overlayClassName={(this.props.overlayClassName || '') + ' modal-overlay'}> overlayClassName={[null, undefined, ""].indexOf(this.props.overlayClassName) === -1 ? this.props.overlayClassName : 'modal-overlay'}>
<div> <div>
{this.props.children} {this.props.children}
</div> </div>

21
ui/js/component/notice.js Normal file
View file

@ -0,0 +1,21 @@
import React from 'react';
export const Notice = React.createClass({
propTypes: {
isError: React.PropTypes.bool,
},
getDefaultProps: function() {
return {
isError: false,
};
},
render: function() {
return (
<section className={'notice ' + (this.props.isError ? 'notice--error ' : '') + (this.props.className || '')}>
{this.props.children}
</section>
);
},
});
export default Notice;

View file

@ -0,0 +1,57 @@
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) {
// 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

@ -13,11 +13,12 @@ var SplashScreen = React.createClass({
isLagging: false, isLagging: false,
} }
}, },
updateStatus: function(was_lagging=false) { updateStatus: function() {
lbry.getDaemonStatus(this._updateStatusCallback); lbry.status().then(this._updateStatusCallback);
}, },
_updateStatusCallback: function(status) { _updateStatusCallback: function(status) {
if (status.code == 'started') { const startupStatus = status.startup_status
if (startupStatus.code == 'started') {
// Wait until we are able to resolve a name before declaring // Wait until we are able to resolve a name before declaring
// that we are done. // that we are done.
// TODO: This is a hack, and the logic should live in the daemon // TODO: This is a hack, and the logic should live in the daemon
@ -34,20 +35,28 @@ var SplashScreen = React.createClass({
return; return;
} }
this.setState({ this.setState({
details: status.message + (status.is_lagging ? '' : '...'), details: startupStatus.message + (startupStatus.is_lagging ? '' : '...'),
isLagging: status.is_lagging, isLagging: startupStatus.is_lagging,
}); });
setTimeout(() => { setTimeout(() => {
this.updateStatus(status.is_lagging); this.updateStatus();
}, 500); }, 500);
}, },
componentDidMount: function() { componentDidMount: function() {
lbry.connect((connected) => { lbry.connect().then((isConnected) => {
this.updateStatus(); if (isConnected) {
}); this.updateStatus();
} else {
this.setState({
isLagging: true,
message: "Failed to connect to LBRY",
details: "LBRY was unable to start and connect properly."
})
}
})
}, },
render: function() { render: function() {
return <LoadScreen message={this.props.message} details={this.state.details} isWarning={this.state.isLagging} />; return <LoadScreen message={this.props.message} details={this.state.details} isWarning={this.state.isLagging} />
} }
}); });

View file

@ -1,7 +1,7 @@
import lighthouse from './lighthouse.js'; import lighthouse from './lighthouse.js';
import jsonrpc from './jsonrpc.js'; import jsonrpc from './jsonrpc.js';
import uri from './uri.js'; import uri from './uri.js';
import {getLocal, setLocal} from './utils.js'; import {getLocal, getSession, setSession, setLocal} from './utils.js';
const {remote} = require('electron'); const {remote} = require('electron');
const menu = remote.require('./menu/main-menu'); const menu = remote.require('./menu/main-menu');
@ -10,24 +10,37 @@ const menu = remote.require('./menu/main-menu');
* Records a publish attempt in local storage. Returns a dictionary with all the data needed to * Records a publish attempt in local storage. Returns a dictionary with all the data needed to
* needed to make a dummy claim or file info object. * needed to make a dummy claim or file info object.
*/ */
function savePendingPublish(name) { function savePendingPublish({name, channel_name}) {
let lbryUri;
if (channel_name) {
lbryUri = uri.buildLbryUri({name: channel_name, path: name}, false);
} else {
lbryUri = uri.buildLbryUri({name: name}, false);
}
const pendingPublishes = getLocal('pendingPublishes') || []; const pendingPublishes = getLocal('pendingPublishes') || [];
const newPendingPublish = { const newPendingPublish = {
claim_id: 'pending_claim_' + name, name, channel_name,
txid: 'pending_' + name, claim_id: 'pending_claim_' + lbryUri,
txid: 'pending_' + lbryUri,
nout: 0, nout: 0,
outpoint: 'pending_' + name + ':0', outpoint: 'pending_' + lbryUri + ':0',
name: name,
time: Date.now(), time: Date.now(),
}; };
setLocal('pendingPublishes', [...pendingPublishes, newPendingPublish]); setLocal('pendingPublishes', [...pendingPublishes, newPendingPublish]);
return newPendingPublish; return newPendingPublish;
} }
function removePendingPublish({name, outpoint}) {
setLocal('pendingPublishes', getPendingPublishes().filter( /**
(pub) => pub.name != name && pub.outpoint != outpoint * If there is a pending publish with the given name or outpoint, remove it.
)); * A channel name may also be provided along with name.
*/
function removePendingPublishIfNeeded({name, channel_name, outpoint}) {
function pubMatches(pub) {
return pub.outpoint === outpoint || (pub.name === name && (!channel_name || pub.channel_name === channel_name));
}
setLocal('pendingPublishes', getPendingPublishes().filter(pub => !pubMatches(pub)));
} }
/** /**
@ -36,61 +49,30 @@ function removePendingPublish({name, outpoint}) {
*/ */
function getPendingPublishes() { function getPendingPublishes() {
const pendingPublishes = getLocal('pendingPublishes') || []; const pendingPublishes = getLocal('pendingPublishes') || [];
const newPendingPublishes = pendingPublishes.filter(pub => Date.now() - pub.time <= lbry.pendingPublishTimeout);
const newPendingPublishes = [];
for (let pendingPublish of pendingPublishes) {
if (Date.now() - pendingPublish.time <= lbry.pendingPublishTimeout) {
newPendingPublishes.push(pendingPublish);
}
}
setLocal('pendingPublishes', newPendingPublishes); setLocal('pendingPublishes', newPendingPublishes);
return newPendingPublishes return newPendingPublishes;
} }
/** /**
* Gets a pending publish attempt by its name or (fake) outpoint. If none is found (or one is found * Gets a pending publish attempt by its name or (fake) outpoint. A channel name can also be
* but it has timed out), returns null. * provided along withe the name. If no pending publish is found, returns null.
*/ */
function getPendingPublish({name, outpoint}) { function getPendingPublish({name, channel_name, outpoint}) {
const pendingPublishes = getPendingPublishes(); const pendingPublishes = getPendingPublishes();
const pendingPublishIndex = pendingPublishes.findIndex( return pendingPublishes.find(
({name: itemName, outpoint: itemOutpoint}) => itemName == name || itemOutpoint == outpoint pub => pub.outpoint === outpoint || (pub.name === name && (!channel_name || pub.channel_name === channel_name))
); ) || null;
const pendingPublish = pendingPublishes[pendingPublishIndex];
if (pendingPublishIndex == -1) {
return null;
} else if (Date.now() - pendingPublish.time > lbry.pendingPublishTimeout) {
// Pending publish timed out, so remove it from the stored list and don't match
const newPendingPublishes = pendingPublishes.slice();
newPendingPublishes.splice(pendingPublishIndex, 1);
setLocal('pendingPublishes', newPendingPublishes);
return null;
} else {
return pendingPublish;
}
} }
function pendingPublishToDummyClaim({name, outpoint, claim_id, txid, nout}) { function pendingPublishToDummyClaim({channel_name, name, outpoint, claim_id, txid, nout}) {
return { return {name, outpoint, claim_id, txid, nout, channel_name};
name: name,
outpoint: outpoint,
claim_id: claim_id,
txid: txid,
nout: nout,
};
} }
function pendingPublishToDummyFileInfo({name, outpoint, claim_id}) { function pendingPublishToDummyFileInfo({name, outpoint, claim_id}) {
return { return {name, outpoint, claim_id, metadata: null};
name: name,
outpoint: outpoint,
claim_id: claim_id,
metadata: "Attempting publication",
};
} }
window.pptdfi = pendingPublishToDummyFileInfo;
let lbry = { let lbry = {
isConnected: false, isConnected: false,
@ -116,30 +98,37 @@ lbry.call = function (method, params, callback, errorCallback, connectFailedCall
jsonrpc.call(lbry.daemonConnectionString, method, [params], callback, errorCallback, connectFailedCallback); jsonrpc.call(lbry.daemonConnectionString, method, [params], callback, errorCallback, connectFailedCallback);
} }
//core //core
lbry.connect = function(callback) lbry._connectPromise = null;
{ lbry.connect = function() {
// Check every half second to see if the daemon is accepting connections if (lbry._connectPromise === null) {
// Once this returns True, can call getDaemonStatus to see where
// we are in the startup process lbry._connectPromise = new Promise((resolve, reject) => {
function checkDaemonStarted(tryNum=0) {
lbry.isDaemonAcceptingConnections(function (runningStatus) { // Check every half second to see if the daemon is accepting connections
if (runningStatus) { function checkDaemonStarted(tryNum = 0) {
lbry.isConnected = true; lbry.isDaemonAcceptingConnections(function (runningStatus) {
callback(true); if (runningStatus) {
} else { resolve(true);
if (tryNum <= 600) { // Move # of tries into constant or config option }
setTimeout(function () { else {
checkDaemonStarted(tryNum + 1); if (tryNum <= 600) { // Move # of tries into constant or config option
}, 500); setTimeout(function () {
} else { checkDaemonStarted(tryNum + 1);
callback(false); }, tryNum < 100 ? 200 : 1000);
} }
else {
reject(new Error("Unable to connect to LBRY"));
}
}
});
} }
checkDaemonStarted();
}); });
} }
checkDaemonStarted();
return lbry._connectPromise;
} }
lbry.isDaemonAcceptingConnections = function (callback) { lbry.isDaemonAcceptingConnections = function (callback) {
@ -147,10 +136,6 @@ lbry.isDaemonAcceptingConnections = function (callback) {
lbry.call('status', {}, () => callback(true), null, () => callback(false)) lbry.call('status', {}, () => callback(true), null, () => callback(false))
}; };
lbry.getDaemonStatus = function (callback) {
lbry.call('daemon_status', {}, callback);
};
lbry.checkFirstRun = function(callback) { lbry.checkFirstRun = function(callback) {
lbry.call('is_first_run', {}, callback); lbry.call('is_first_run', {}, callback);
} }
@ -190,23 +175,6 @@ lbry.sendToAddress = function(amount, address, callback, errorCallback) {
lbry.call("send_amount_to_address", { "amount" : amount, "address": address }, callback, errorCallback); lbry.call("send_amount_to_address", { "amount" : amount, "address": address }, callback, errorCallback);
} }
lbry.resolveName = function(name, callback) {
if (!name) {
throw new Error(`Name required.`);
}
lbry.call('resolve_name', { 'name': name }, callback, () => {
// For now, assume any error means the name was not resolved
callback(null);
});
}
lbry.getStream = function(name, callback) {
if (!name) {
throw new Error(`Name required.`);
}
lbry.call('get', { 'name': name }, callback);
};
lbry.getClaimInfo = function(name, callback) { lbry.getClaimInfo = function(name, callback) {
if (!name) { if (!name) {
throw new Error(`Name required.`); throw new Error(`Name required.`);
@ -235,70 +203,58 @@ lbry.getPeersForBlobHash = function(blobHash, callback) {
}); });
} }
lbry.getCostInfo = function(lbryUri, callback, errorCallback) { /**
/** * Takes a LBRY URI; will first try and calculate a total cost using
* Takes a LBRY URI; will first try and calculate a total cost using * Lighthouse. If Lighthouse can't be reached, it just retrives the
* Lighthouse. If Lighthouse can't be reached, it just retrives the * key fee.
* key fee. *
* * Returns an object with members:
* Returns an object with members: * - cost: Number; the calculated cost of the name
* - cost: Number; the calculated cost of the name * - includes_data: Boolean; indicates whether or not the data fee info
* - includes_data: Boolean; indicates whether or not the data fee info * from Lighthouse is included.
* from Lighthouse is included. */
*/ lbry.costPromiseCache = {}
if (!name) { lbry.getCostInfo = function(lbryUri) {
throw new Error(`Name required.`); if (lbry.costPromiseCache[lbryUri] === undefined) {
} const COST_INFO_CACHE_KEY = 'cost_info_cache';
lbry.costPromiseCache[lbryUri] = new Promise((resolve, reject) => {
let costInfoCache = getSession(COST_INFO_CACHE_KEY, {})
function getCostWithData(name, size, callback, errorCallback) { if (!lbryUri) {
lbry.stream_cost_estimate({name, size}).then((cost) => { reject(new Error(`URI required.`));
callback({
cost: cost,
includesData: true,
});
}, errorCallback);
}
function getCostNoData(name, callback, errorCallback) {
lbry.stream_cost_estimate({name}).then((cost) => {
callback({
cost: cost,
includesData: false,
});
}, errorCallback);
}
const uriObj = uri.parseLbryUri(lbryUri);
const name = uriObj.path || uriObj.name;
lighthouse.get_size_for_name(name).then((size) => {
getCostWithData(name, size, callback, errorCallback);
}, () => {
getCostNoData(name, 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(); if (costInfoCache[lbryUri] && costInfoCache[lbryUri].cost) {
}); return resolve(costInfoCache[lbryUri])
}
function getCost(lbryUri, size) {
lbry.stream_cost_estimate({uri: lbryUri, ... size !== null ? {size} : {}}).then((cost) => {
costInfoCache[lbryUri] = {
cost: cost,
includesData: size !== null,
};
setSession(COST_INFO_CACHE_KEY, costInfoCache);
resolve(costInfoCache[lbryUri]);
}, reject);
}
const uriObj = uri.parseLbryUri(lbryUri);
const name = uriObj.path || uriObj.name;
lighthouse.get_size_for_name(name).then((size) => {
if (size) {
getCost(name, size);
}
else {
getCost(name, null);
}
}, () => {
getCost(name, null);
});
});
}
return lbry.costPromiseCache[lbryUri];
} }
lbry.getMyClaims = function(callback) { lbry.getMyClaims = function(callback) {
@ -365,12 +321,13 @@ lbry.publish = function(params, fileListedCallback, publishedCallback, errorCall
returnedPending = true; returnedPending = true;
if (publishedCallback) { if (publishedCallback) {
savePendingPublish(params.name); savePendingPublish({name: params.name, channel_name: params.channel_name});
publishedCallback(true); publishedCallback(true);
} }
if (fileListedCallback) { if (fileListedCallback) {
savePendingPublish(params.name); const {name, channel_name} = params;
savePendingPublish({name: params.name, channel_name: params.channel_name});
fileListedCallback(true); fileListedCallback(true);
} }
}, 2000); }, 2000);
@ -430,6 +387,10 @@ lbry.getClientSettings = function() {
lbry.getClientSetting = function(setting) { lbry.getClientSetting = function(setting) {
var localStorageVal = localStorage.getItem('setting_' + setting); var localStorageVal = localStorage.getItem('setting_' + setting);
if (setting == 'showDeveloperMenu')
{
return true;
}
return (localStorageVal === null ? lbry.defaultClientSettings[setting] : JSON.parse(localStorageVal)); return (localStorageVal === null ? lbry.defaultClientSettings[setting] : JSON.parse(localStorageVal));
} }
@ -522,7 +483,7 @@ lbry.stop = function(callback) {
lbry.fileInfo = {}; lbry.fileInfo = {};
lbry._subscribeIdCount = 0; lbry._subscribeIdCount = 0;
lbry._fileInfoSubscribeCallbacks = {}; lbry._fileInfoSubscribeCallbacks = {};
lbry._fileInfoSubscribeInterval = 5000; lbry._fileInfoSubscribeInterval = 500000;
lbry._balanceSubscribeCallbacks = {}; lbry._balanceSubscribeCallbacks = {};
lbry._balanceSubscribeInterval = 5000; lbry._balanceSubscribeInterval = 5000;
lbry._removedFiles = []; lbry._removedFiles = [];
@ -534,6 +495,7 @@ lbry._updateClaimOwnershipCache = function(claimId) {
return match || claimInfo.claim_id == claimId; return match || claimInfo.claim_id == claimId;
}); });
}); });
}; };
lbry._updateFileInfoSubscribers = function(outpoint) { lbry._updateFileInfoSubscribers = function(outpoint) {
@ -629,14 +591,14 @@ lbry.showMenuIfNeeded = function() {
*/ */
lbry.file_list = function(params={}) { lbry.file_list = function(params={}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const {name, outpoint} = params; const {name, channel_name, outpoint} = params;
/** /**
* If we're searching by outpoint, check first to see if there's a matching pending publish. * If we're searching by outpoint, check first to see if there's a matching pending publish.
* Pending publishes use their own faux outpoints that are always unique, so we don't need * Pending publishes use their own faux outpoints that are always unique, so we don't need
* to check if there's a real file. * to check if there's a real file.
*/ */
if (outpoint !== undefined) { if (outpoint) {
const pendingPublish = getPendingPublish({outpoint}); const pendingPublish = getPendingPublish({outpoint});
if (pendingPublish) { if (pendingPublish) {
resolve([pendingPublishToDummyFileInfo(pendingPublish)]); resolve([pendingPublishToDummyFileInfo(pendingPublish)]);
@ -645,14 +607,8 @@ lbry.file_list = function(params={}) {
} }
lbry.call('file_list', params, (fileInfos) => { lbry.call('file_list', params, (fileInfos) => {
// Remove any pending publications that are now listed in the file manager removePendingPublishIfNeeded({name, channel_name, outpoint});
const pendingPublishes = getPendingPublishes();
for (let {name: itemName} of fileInfos) {
if (pendingPublishes.find(() => name == itemName)) {
removePendingPublish({name: name});
}
}
const dummyFileInfos = getPendingPublishes().map(pendingPublishToDummyFileInfo); const dummyFileInfos = getPendingPublishes().map(pendingPublishToDummyFileInfo);
resolve([...fileInfos, ...dummyFileInfos]); resolve([...fileInfos, ...dummyFileInfos]);
}, reject, reject); }, reject, reject);
@ -662,16 +618,43 @@ lbry.file_list = function(params={}) {
lbry.claim_list_mine = function(params={}) { lbry.claim_list_mine = function(params={}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
lbry.call('claim_list_mine', params, (claims) => { lbry.call('claim_list_mine', params, (claims) => {
// Filter out pending publishes when the name is already in the file manager for (let {name, channel_name, txid, nout} of claims) {
const dummyClaims = getPendingPublishes().filter( removePendingPublishIfNeeded({name, channel_name, outpoint: txid + ':' + nout});
(pub) => !claims.find(({name}) => name == pub.name) }
).map(pendingPublishToDummyClaim);
const dummyClaims = getPendingPublishes().map(pendingPublishToDummyClaim);
resolve([...claims, ...dummyClaims]); resolve([...claims, ...dummyClaims]);
}, reject, reject); }, reject, reject)
}); });
} }
lbry.resolve = function(params={}) {
const claimCacheKey = 'resolve_claim_cache',
claimCache = getSession(claimCacheKey, {})
return new Promise((resolve, reject) => {
if (!params.uri) {
throw "Resolve has hacked cache on top of it that requires a URI"
}
if (params.uri && claimCache[params.uri]) {
resolve(claimCache[params.uri]);
} else {
lbry.call('resolve', params, function(data) {
claimCache[params.uri] = data;
setSession(claimCacheKey, claimCache)
resolve(data)
}, reject)
}
});
}
// lbry.get = function(params={}) {
// return function(params={}) {
// return new Promise((resolve, reject) => {
// jsonrpc.call(lbry.daemonConnectionString, "get", [params], resolve, reject, reject);
// });
// };
// }
lbry = new Proxy(lbry, { lbry = new Proxy(lbry, {
get: function(target, name) { get: function(target, name) {
if (name in target) { if (name in target) {

170
ui/js/lbryio.js Normal file
View file

@ -0,0 +1,170 @@
import {getLocal, getSession, setSession, setLocal} from './utils.js';
import lbry from './lbry.js';
const querystring = require('querystring');
const lbryio = {
_accessToken: getLocal('accessToken'),
_authenticationPromise: null,
_user : null,
enabled: false
};
const CONNECTION_STRING = 'https://api.lbry.io/';
const mocks = {
'reward_type.get': ({name}) => {
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') {
return new Promise((resolve, reject) => {
if (!lbryio.enabled && (resource != 'discover' || action != 'list')) {
reject(new Error("LBRY interal API is disabled"))
return
}
/* temp code for mocks */
if (`${resource}.${action}` in mocks) {
resolve(mocks[`${resource}.${action}`](params));
return;
}
/* end temp */
const xhr = new XMLHttpRequest;
xhr.addEventListener('error', function (event) {
reject(new Error("Something went wrong making an internal API call."));
});
xhr.addEventListener('timeout', function() {
reject(new Error('XMLHttpRequest connection timed out'));
});
xhr.addEventListener('load', function() {
const response = JSON.parse(xhr.responseText);
if (!response.success) {
if (reject) {
reject(new Error(response.error));
} else {
document.dispatchEvent(new CustomEvent('unhandledError', {
detail: {
connectionString: connectionString,
method: action,
params: params,
message: response.error.message,
... response.error.data ? {data: response.error.data} : {},
}
}));
}
} else {
resolve(response.data);
}
});
// For social media auth:
//const accessToken = localStorage.getItem('accessToken');
//const fullParams = {...params, ... accessToken ? {access_token: accessToken} : {}};
// Temp app ID based auth:
const fullParams = {app_id: lbryio._accessToken, ...params};
if (method == 'get') {
xhr.open('get', CONNECTION_STRING + resource + '/' + action + '?' + querystring.stringify(fullParams), true);
xhr.send();
} else if (method == 'post') {
xhr.open('post', CONNECTION_STRING + resource + '/' + action, true);
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhr.send(querystring.stringify(fullParams));
}
});
};
lbryio.setAccessToken = (token) => {
setLocal('accessToken', token)
lbryio._accessToken = token
}
lbryio.authenticate = function() {
if (!lbryio.enabled) {
return new Promise((resolve, reject) => {
resolve({
ID: 1,
HasVerifiedEmail: true
})
})
}
if (lbryio._authenticationPromise === null) {
lbryio._authenticationPromise = new Promise((resolve, reject) => {
lbry.status().then(({installation_id}) => {
//temp hack for installation_ids being wrong
installation_id += "Y".repeat(96 - installation_id.length)
function setCurrentUser() {
lbryio.call('user', 'me').then((data) => {
lbryio.user = data
resolve(data)
}).catch(function(err) {
lbryio.setAccessToken(null);
if (!getSession('reloadedOnFailedAuth')) {
setSession('reloadedOnFailedAuth', true)
window.location.reload();
} else {
reject(err);
}
})
}
if (!lbryio._accessToken) {
lbryio.call('user', 'new', {
language: 'en',
app_id: installation_id,
}, 'post').then(function(responseData) {
if (!responseData.ID) {
reject(new Error("Received invalid authentication response."));
}
lbryio.setAccessToken(installation_id)
setCurrentUser()
}).catch(function(error) {
/*
until we have better error code format, assume all errors are duplicate application id
if we're wrong, this will be caught by later attempts to make a valid call
*/
lbryio.setAccessToken(installation_id)
setCurrentUser()
})
} else {
setCurrentUser()
}
// if (!lbryio._
//(data) => {
// resolve(data)
// localStorage.setItem('accessToken', ID);
// localStorage.setItem('appId', installation_id);
// this.setState({
// registrationCheckComplete: true,
// justRegistered: true,
// });
//});
// lbryio.call('user_install', 'exists', {app_id: installation_id}).then((userExists) => {
// // TODO: deal with case where user exists already with the same app ID, but we have no access token.
// // Possibly merge in to the existing user with the same app ID.
// })
}).catch(reject);
});
}
return lbryio._authenticationPromise;
}
export default lbryio;

View file

@ -1,8 +1,8 @@
import lbry from './lbry.js'; import lbry from './lbry.js';
import jsonrpc from './jsonrpc.js'; import jsonrpc from './jsonrpc.js';
const queryTimeout = 5000; const queryTimeout = 3000;
const maxQueryTries = 5; const maxQueryTries = 2;
const defaultServers = [ const defaultServers = [
'http://lighthouse4.lbry.io:50005', 'http://lighthouse4.lbry.io:50005',
'http://lighthouse5.lbry.io:50005', 'http://lighthouse5.lbry.io:50005',
@ -20,12 +20,9 @@ function getServers() {
} }
function call(method, params, callback, errorCallback) { function call(method, params, callback, errorCallback) {
if (connectTryNum > maxQueryTries) { if (connectTryNum >= maxQueryTries) {
if (connectFailedCallback) { errorCallback(new Error(`Could not connect to Lighthouse server. Last server attempted: ${server}`));
connectFailedCallback(); return;
} else {
throw new Error(`Could not connect to Lighthouse server. Last server attempted: ${server}`);
}
} }
/** /**
@ -48,7 +45,7 @@ function call(method, params, callback, errorCallback) {
}, () => { }, () => {
connectTryNum++; connectTryNum++;
call(method, params, callback, errorCallback); call(method, params, callback, errorCallback);
}); }, queryTimeout);
} }
const lighthouse = new Proxy({}, { const lighthouse = new Proxy({}, {

View file

@ -1,9 +1,12 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
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 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';
const {remote} = require('electron'); const {remote} = require('electron');
const contextMenu = remote.require('./menu/context-menu'); const contextMenu = remote.require('./menu/context-menu');
@ -16,31 +19,24 @@ window.addEventListener('contextmenu', (event) => {
event.preventDefault(); event.preventDefault();
}); });
var init = function() { let init = function() {
window.lbry = lbry; window.lbry = lbry;
window.lighthouse = lighthouse; window.lighthouse = lighthouse;
let canvas = document.getElementById('canvas');
lbry.connect().then(function(isConnected) {
lbryio.authenticate() //start auth process as soon as soon as we can get an install ID
})
function onDaemonReady() {
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>{ lbryio.enabled ? <AuthOverlay/> : '' }<App /><SnackBar /></div>, canvas)
}
var canvas = document.getElementById('canvas');
if (window.sessionStorage.getItem('loaded') == 'y') { if (window.sessionStorage.getItem('loaded') == 'y') {
ReactDOM.render(<App/>, canvas) onDaemonReady();
} else { } else {
ReactDOM.render( ReactDOM.render(<SplashScreen message="Connecting" onLoadDone={onDaemonReady} />, canvas);
<SplashScreen message="Connecting" onLoadDone={function() {
// Redirect to the claim code page if needed. Find somewhere better for this logic
if (!localStorage.getItem('claimCodeDone') && window.location.search == '' || window.location.search == '?' || window.location.search == 'discover') {
lbry.getBalance((balance) => {
if (balance <= 0) {
window.location.href = '?claim';
} else {
ReactDOM.render(<App/>, canvas);
}
});
} else {
ReactDOM.render(<App/>, canvas);
}
}}/>,
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,46 +59,59 @@ 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 card-row--small">
{ 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" uri={name} /> }) }
</div>)
}
})
var FeaturedContent = React.createClass({ var FeaturedContent = React.createClass({
getInitialState: function() { getInitialState: function() {
return { return {
featuredNames: [], featuredUris: {},
failed: false
}; };
}, },
componentWillMount: function() { componentWillMount: function() {
lbry.getFeaturedDiscoverNames().then((featuredNames) => { lbryio.call('discover', 'list', { version: "early-access" } ).then(({Categories, Uris}) => {
this.setState({ featuredNames: featuredNames }); let featuredUris = {}
Categories.forEach((category) => {
if (Uris[category] && Uris[category].length) {
featuredUris[category] = Uris[category]
}
})
this.setState({ featuredUris: featuredUris });
}, () => {
this.setState({
failed: true
})
}); });
}, },
render: function() { render: function() {
const toolTipText = ('Community Content is a public space where anyone can share content with the ' +
'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"> this.state.failed ?
<div className="span6"> <div className="empty">Failed to load landing content.</div> :
<h3>Featured Content</h3> <div>
{ this.state.featuredNames.map(name => <FileTile key={name} uri={name} />) } {
Object.keys(this.state.featuredUris).map(function(category) {
return this.state.featuredUris[category].length ?
<FeaturedCategory key={category} category={category} names={this.state.featuredUris[category]} /> :
'';
}.bind(this))
}
</div> </div>
<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>
); );
} }
}); });
@ -105,6 +119,10 @@ var FeaturedContent = React.createClass({
var DiscoverPage = React.createClass({ var DiscoverPage = React.createClass({
userTypingTimer: null, userTypingTimer: null,
propTypes: {
showWelcome: React.PropTypes.bool.isRequired,
},
componentDidUpdate: function() { componentDidUpdate: function() {
if (this.props.query != this.state.query) if (this.props.query != this.state.query)
{ {
@ -112,6 +130,12 @@ var DiscoverPage = React.createClass({
} }
}, },
getDefaultProps: function() {
return {
showWelcome: false,
}
},
componentWillReceiveProps: function(nextProps, nextState) { componentWillReceiveProps: function(nextProps, nextState) {
if (nextProps.query != nextState.query) if (nextProps.query != nextState.query)
{ {
@ -128,8 +152,15 @@ var DiscoverPage = React.createClass({
lighthouse.search(query).then(this.searchCallback); lighthouse.search(query).then(this.searchCallback);
}, },
handleWelcomeDone: function() {
this.setState({
welcomeComplete: true,
});
},
componentWillMount: function() { componentWillMount: function() {
document.title = "Discover"; document.title = "Discover";
if (this.props.query) { if (this.props.query) {
// Rendering with a query already typed // Rendering with a query already typed
this.handleSearchChanged(this.props.query); this.handleSearchChanged(this.props.query);
@ -138,6 +169,7 @@ var DiscoverPage = React.createClass({
getInitialState: function() { getInitialState: function() {
return { return {
welcomeComplete: false,
results: [], results: [],
query: this.props.query, query: this.props.query,
searching: ('query' in this.props) && (this.props.query.length > 0) searching: ('query' in this.props) && (this.props.query.length > 0)

View file

@ -2,8 +2,10 @@ 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 rewards from '../rewards.js';
import lbryio from '../lbryio.js';
import {BusyMessage, Thumbnail} from '../component/common.js'; import {BusyMessage, Thumbnail} from '../component/common.js';
@ -32,6 +34,9 @@ export let FileListDownloaded = React.createClass({
}); });
}); });
}, },
componentWillUnmount: function() {
this._isMounted = false;
},
render: function() { render: function() {
if (this.state.fileInfos === null) { if (this.state.fileInfos === null) {
return ( return (
@ -63,8 +68,22 @@ export let FileListPublished = React.createClass({
fileInfos: null, fileInfos: null,
}; };
}, },
_requestPublishReward: function() {
lbryio.call('reward', 'list', {}).then(function(userRewards) {
//already rewarded
if (userRewards.filter(function (reward) {
return reward.RewardType == rewards.TYPE_FIRST_PUBLISH && reward.TransactionID;
}).length) {
return;
}
else {
rewards.claimReward(rewards.TYPE_FIRST_PUBLISH).catch(() => {})
}
});
},
componentDidMount: function () { componentDidMount: function () {
this._isMounted = true; this._isMounted = true;
this._requestPublishReward();
document.title = "Published Files"; document.title = "Published Files";
lbry.claim_list_mine().then((claimInfos) => { lbry.claim_list_mine().then((claimInfos) => {
@ -80,6 +99,9 @@ export let FileListPublished = React.createClass({
}); });
}); });
}, },
componentWillUnmount: function() {
this._isMounted = false;
},
render: function () { render: function () {
if (this.state.fileInfos === null) { if (this.state.fileInfos === null) {
return ( return (
@ -161,20 +183,28 @@ export let FileList = React.createClass({
seenUris = {}; seenUris = {};
const fileInfosSorted = this._sortFunctions[this.state.sortBy](this.props.fileInfos); const fileInfosSorted = this._sortFunctions[this.state.sortBy](this.props.fileInfos);
for (let {outpoint, name, channel_name, metadata: {stream: {metadata}}, mime_type, claim_id, has_signature, signature_is_valid} of fileInfosSorted) { for (let {outpoint, name, channel_name, metadata, mime_type, claim_id, has_signature, signature_is_valid} of fileInfosSorted) {
if (!metadata || seenUris[name]) { if (seenUris[name] || !claim_id) {
continue; continue;
} }
let streamMetadata;
if (metadata) {
streamMetadata = metadata.stream.metadata;
} else {
streamMetadata = null;
}
let fileUri; let fileUri;
if (channel_name === undefined) { if (!channel_name) {
fileUri = uri.buildLbryUri({name}); fileUri = uri.buildLbryUri({name});
} else { } else {
fileUri = uri.buildLbryUri({name: channel_name, path: name}); fileUri = uri.buildLbryUri({name: channel_name, path: name});
} }
seenUris[name] = true; seenUris[name] = true;
content.push(<FileTileStream key={outpoint} outpoint={outpoint} uri={fileUri} hideOnRemove={true} content.push(<FileTileStream key={outpoint} outpoint={outpoint} uri={fileUri} hideOnRemove={true}
hidePrice={this.props.hidePrices} metadata={metadata} contentType={mime_type} hidePrice={this.props.hidePrices} metadata={streamMetadata} contentType={mime_type}
hasSignature={has_signature} signatureIsValid={signature_is_valid} />); hasSignature={has_signature} signatureIsValid={signature_is_valid} />);
} }

View file

@ -51,56 +51,68 @@ var HelpPage = React.createClass({
return ( return (
<main className="page"> <main className="page">
<section className="card"> <section className="card">
<h3>Read the FAQ</h3> <div className="card__title-primary">
<p>Our FAQ answers many common questions.</p> <h3>Read the FAQ</h3>
<p><Link href="https://lbry.io/faq" label="Read the FAQ" icon="icon-question" button="alt"/></p> </div>
<div className="card__content">
<p>Our FAQ answers many common questions.</p>
<p><Link href="https://lbry.io/faq" label="Read the FAQ" icon="icon-question" button="alt"/></p>
</div>
</section> </section>
<section className="card"> <section className="card">
<h3>Get Live Help</h3> <div className="card__title-primary">
<p> <h3>Get Live Help</h3>
Live help is available most hours in the <strong>#help</strong> channel of our Slack chat room. </div>
</p> <div className="card__content">
<p> <p>
<Link button="alt" label="Join Our Slack" icon="icon-slack" href="https://slack.lbry.io" /> Live help is available most hours in the <strong>#help</strong> channel of our Slack chat room.
</p> </p>
<p>
<Link button="alt" label="Join Our Slack" icon="icon-slack" href="https://slack.lbry.io" />
</p>
</div>
</section> </section>
<section className="card"> <section className="card">
<h3>Report a Bug</h3> <div className="card__title-primary"><h3>Report a Bug</h3></div>
<p>Did you find something wrong?</p> <div className="card__content">
<p><Link href="?report" label="Submit a Bug Report" icon="icon-bug" button="alt" /></p> <p>Did you find something wrong?</p>
<div className="meta">Thanks! LBRY is made by its users.</div> <p><Link href="?report" label="Submit a Bug Report" icon="icon-bug" button="alt" /></p>
<div className="meta">Thanks! LBRY is made by its users.</div>
</div>
</section> </section>
{!ver ? null : {!ver ? null :
<section className="card"> <section className="card">
<h3>About</h3> <div className="card__title-primary"><h3>About</h3></div>
{ver.lbrynet_update_available || ver.lbryum_update_available ? <div className="card__content">
<p>A newer version of LBRY is available. <Link href={newVerLink} label={`Download LBRY ${ver.remote_lbrynet} now!`} /></p> {ver.lbrynet_update_available || ver.lbryum_update_available ?
: <p>Your copy of LBRY is up to date.</p> <p>A newer version of LBRY is available. <Link href={newVerLink} label={`Download LBRY ${ver.remote_lbrynet} now!`} /></p>
} : <p>Your copy of LBRY is up to date.</p>
<table className="table-standard"> }
<tbody> <table className="table-standard">
<tr> <tbody>
<th>daemon (lbrynet)</th> <tr>
<td>{ver.lbrynet_version}</td> <th>daemon (lbrynet)</th>
</tr> <td>{ver.lbrynet_version}</td>
<tr> </tr>
<th>wallet (lbryum)</th> <tr>
<td>{ver.lbryum_version}</td> <th>wallet (lbryum)</th>
</tr> <td>{ver.lbryum_version}</td>
<tr> </tr>
<th>interface</th> <tr>
<td>{uiVersion}</td> <th>interface</th>
</tr> <td>{uiVersion}</td>
<tr> </tr>
<th>Platform</th> <tr>
<td>{platform}</td> <th>Platform</th>
</tr> <td>{platform}</td>
<tr> </tr>
<th>Installation ID</th> <tr>
<td>{this.state.lbryId}</td> <th>Installation ID</th>
</tr> <td>{this.state.lbryId}</td>
</tbody> </tr>
</table> </tbody>
</table>
</div>
</section> </section>
} }
</main> </main>

View file

@ -1,17 +1,20 @@
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 rewards from '../rewards.js';
import lbryio from '../lbryio.js';
import Modal from '../component/modal.js'; import Modal from '../component/modal.js';
var PublishPage = React.createClass({ var PublishPage = React.createClass({
_requiredFields: ['name', 'bid', 'meta_title', 'meta_author', 'meta_license', 'meta_description'], _requiredFields: ['meta_title', 'name', 'bid', 'tos_agree'],
_updateChannelList: function(channel) { _updateChannelList: function(channel) {
// Calls API to update displayed list of channels. If a channel name is provided, will select // Calls API to update displayed list of channels. If a channel name is provided, will select
// that channel at the same time (used immediately after creating a channel) // that channel at the same time (used immediately after creating a channel)
lbry.channel_list_mine().then((channels) => { lbry.channel_list_mine().then((channels) => {
rewards.claimReward(rewards.TYPE_FIRST_CHANNEL).then(() => {}, () => {})
this.setState({ this.setState({
channels: channels, channels: channels,
... channel ? {channel} : {} ... channel ? {channel} : {}
@ -27,19 +30,23 @@ var PublishPage = React.createClass({
submitting: true, submitting: true,
}); });
var checkFields = this._requiredFields.slice(); let checkFields = this._requiredFields;
if (!this.state.myClaimExists) { if (!this.state.myClaimExists) {
checkFields.push('file'); checkFields.unshift('file');
} }
var missingFieldFound = false; let missingFieldFound = false;
for (let fieldName of checkFields) { for (let fieldName of checkFields) {
var field = this.refs[fieldName]; const field = this.refs[fieldName];
if (field.getValue() === '') { if (field) {
field.warnRequired(); if (field.getValue() === '' || field.getValue() === false) {
if (!missingFieldFound) { field.showRequiredError();
field.focus(); if (!missingFieldFound) {
missingFieldFound = true; field.focus();
missingFieldFound = true;
}
} else {
field.clearError();
} }
} }
} }
@ -61,14 +68,16 @@ var PublishPage = React.createClass({
var metadata = {}; var metadata = {};
} }
for (let metaField of ['title', 'author', 'description', 'thumbnail', 'license', 'license_url', 'language', 'nsfw']) { for (let metaField of ['title', 'description', 'thumbnail', 'license', 'license_url', 'language']) {
var value = this.refs['meta_' + metaField].getValue(); var value = this.refs['meta_' + metaField].getValue();
if (value !== '') { if (value !== '') {
metadata[metaField] = value; metadata[metaField] = value;
} }
} }
var licenseUrl = this.refs.meta_license_url.getValue(); metadata.nsfw = Boolean(parseInt(!!this.refs.meta_nsfw.getValue()));
const licenseUrl = this.refs.meta_license_url.getValue();
if (licenseUrl) { if (licenseUrl) {
metadata.license_url = licenseUrl; metadata.license_url = licenseUrl;
} }
@ -82,9 +91,9 @@ var PublishPage = React.createClass({
}; };
if (this.refs.file.getValue() !== '') { if (this.refs.file.getValue() !== '') {
publishArgs.file_path = this.refs.file.getValue(); publishArgs.file_path = this.refs.file.getValue();
} }
lbry.publish(publishArgs, (message) => { lbry.publish(publishArgs, (message) => {
this.handlePublishStarted(); this.handlePublishStarted();
}, null, (error) => { }, null, (error) => {
@ -111,17 +120,18 @@ var PublishPage = React.createClass({
channels: null, channels: null,
rawName: '', rawName: '',
name: '', name: '',
bid: '', bid: 1,
hasFile: false,
feeAmount: '', feeAmount: '',
feeCurrency: 'USD', feeCurrency: 'USD',
channel: 'anonymous', channel: 'anonymous',
newChannelName: '@', newChannelName: '@',
newChannelBid: '', newChannelBid: 10,
nameResolved: false, nameResolved: null,
myClaimExists: null,
topClaimValue: 0.0, topClaimValue: 0.0,
myClaimValue: 0.0, myClaimValue: 0.0,
myClaimMetadata: null, myClaimMetadata: null,
myClaimExists: null,
copyrightNotice: '', copyrightNotice: '',
otherLicenseDescription: '', otherLicenseDescription: '',
otherLicenseUrl: '', otherLicenseUrl: '',
@ -162,35 +172,39 @@ var PublishPage = React.createClass({
} }
if (!lbry.nameIsValid(rawName, false)) { if (!lbry.nameIsValid(rawName, false)) {
this.refs.name.showAdvice('LBRY names must contain only letters, numbers and dashes.'); this.refs.name.showError('LBRY names must contain only letters, numbers and dashes.');
return; return;
} }
const name = rawName.toLowerCase();
this.setState({ this.setState({
rawName: rawName, rawName: rawName,
name: name,
nameResolved: null,
myClaimExists: null,
}); });
const name = rawName.toLowerCase();
lbry.getMyClaim(name, (myClaimInfo) => { lbry.getMyClaim(name, (myClaimInfo) => {
if (name != this.refs.name.getValue().toLowerCase()) { if (name != this.state.name) {
// A new name has been typed already, so bail // A new name has been typed already, so bail
return; return;
} }
this.setState({
myClaimExists: !!myClaimInfo,
});
lbry.resolve({uri: name}).then((claimInfo) => { lbry.resolve({uri: name}).then((claimInfo) => {
if (name != this.refs.name.getValue()) { if (name != this.state.name) {
return; return;
} }
if (!claimInfo) { if (!claimInfo) {
this.setState({ this.setState({
name: name,
nameResolved: false, nameResolved: false,
myClaimExists: false,
}); });
} else { } else {
const topClaimIsMine = (myClaimInfo && myClaimInfo.claim.amount >= claimInfo.claim.amount); const topClaimIsMine = (myClaimInfo && myClaimInfo.claim.amount >= claimInfo.claim.amount);
const newState = { const newState = {
name: name,
nameResolved: true, nameResolved: true,
topClaimValue: parseFloat(claimInfo.claim.amount), topClaimValue: parseFloat(claimInfo.claim.amount),
myClaimExists: !!myClaimInfo, myClaimExists: !!myClaimInfo,
@ -237,7 +251,7 @@ var PublishPage = React.createClass({
isFee: feeEnabled isFee: feeEnabled
}); });
}, },
handeLicenseChange: function(event) { handleLicenseChange: function(event) {
var licenseType = event.target.options[event.target.selectedIndex].getAttribute('data-license-type'); var licenseType = event.target.options[event.target.selectedIndex].getAttribute('data-license-type');
var newState = { var newState = {
copyrightChosen: licenseType == 'copyright', copyrightChosen: licenseType == 'copyright',
@ -245,8 +259,7 @@ var PublishPage = React.createClass({
}; };
if (licenseType == 'copyright') { if (licenseType == 'copyright') {
var author = this.refs.meta_author.getValue(); newState.copyrightNotice = 'All rights reserved.'
newState.copyrightNotice = 'Copyright ' + (new Date().getFullYear()) + (author ? ' ' + author : '');
} }
this.setState(newState); this.setState(newState);
@ -277,8 +290,10 @@ var PublishPage = React.createClass({
const newChannelName = (event.target.value.startsWith('@') ? event.target.value : '@' + event.target.value); const newChannelName = (event.target.value.startsWith('@') ? event.target.value : '@' + event.target.value);
if (newChannelName.length > 1 && !lbry.nameIsValid(newChannelName.substr(1), false)) { if (newChannelName.length > 1 && !lbry.nameIsValid(newChannelName.substr(1), false)) {
this.refs.newChannelName.showAdvice('LBRY channel names must contain only letters, numbers and dashes.'); this.refs.newChannelName.showError('LBRY channel names must contain only letters, numbers and dashes.');
return; return;
} else {
this.refs.newChannelName.clearError()
} }
this.setState({ this.setState({
@ -290,9 +305,14 @@ var PublishPage = React.createClass({
newChannelBid: event.target.value, newChannelBid: event.target.value,
}); });
}, },
handleTOSChange: function(event) {
this.setState({
TOSAgreed: event.target.checked,
});
},
handleCreateChannelClick: function (event) { handleCreateChannelClick: function (event) {
if (this.state.newChannelName.length < 5) { if (this.state.newChannelName.length < 5) {
this.refs.newChannelName.showAdvice('LBRY channel names must be at least 4 characters in length.'); this.refs.newChannelName.showError('LBRY channel names must be at least 4 characters in length.');
return; return;
} }
@ -311,7 +331,7 @@ var PublishPage = React.createClass({
}, 5000); }, 5000);
}, (error) => { }, (error) => {
// TODO: better error handling // TODO: better error handling
this.refs.newChannelName.showAdvice('Unable to create channel due to an internal error.'); this.refs.newChannelName.showError('Unable to create channel due to an internal error.');
this.setState({ this.setState({
creatingChannel: false, creatingChannel: false,
}); });
@ -334,159 +354,199 @@ var PublishPage = React.createClass({
}, },
componentDidUpdate: function() { componentDidUpdate: function() {
}, },
// Also getting a type warning here too onFileChange: function() {
if (this.refs.file.getValue()) {
this.setState({ hasFile: true })
} else {
this.setState({ hasFile: false })
}
},
getNameBidHelpText: function() {
if (!this.state.name) {
return "Select a URL for this publish.";
} else if (this.state.nameResolved === false) {
return "This URL is unused.";
} else if (this.state.myClaimExists) {
return "You have already used this URL. Publishing to it again will update your previous publish."
} else if (this.state.topClaimValue) {
return <span>A deposit of at least <strong>{this.state.topClaimValue}</strong> {this.state.topClaimValue == 1 ? 'credit ' : 'credits '}
is required to win <strong>{this.state.name}</strong>. However, you can still get a permanent URL for any amount.</span>
} else {
return '';
}
},
closeModal: function() {
this.setState({
modal: null,
});
},
render: function() { render: function() {
if (this.state.channels === null) { if (this.state.channels === null) {
return null; return null;
} }
const lbcInputHelp = "This LBC remains yours and the deposit can be undone at any time."
return ( return (
<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>Content</h4>
<FormField type="text" ref="name" value={this.state.rawName} onChange={this.handleNameChange} /> <div className="card__subtitle">
{ What are you publishing?
(!this.state.name </div>
? null </div>
: (!this.state.nameResolved <div className="card__content">
? <em> The name <strong>{this.state.name}</strong> is available.</em> <FormRow name="file" label="File" ref="file" type="file" onChange={this.onFileChange}
: (this.state.myClaimExists helper={this.state.myClaimExists ? "If you don't choose a file, the file from your existing claim will be used." : null}/>
? <em> You already have a claim on the name <strong>{this.state.name}</strong>. You can use this page to update your claim.</em> </div>
: <em> The name <strong>{this.state.name}</strong> is currently claimed for <strong>{this.state.topClaimValue}</strong> {this.state.topClaimValue == 1 ? 'credit' : 'credits'}.</em>))) { !this.state.hasFile ? '' :
} <div>
<div className="help">What LBRY name would you like to claim for this file?</div> <div className="card__content">
<FormRow label="Title" type="text" ref="meta_title" name="title" placeholder="Titular Title" />
</div>
<div className="card__content">
<FormRow type="text" label="Thumbnail URL" ref="meta_thumbnail" name="thumbnail" placeholder="http://spee.ch/mylogo" />
</div>
<div className="card__content">
<FormRow label="Description" type="textarea" ref="meta_description" name="description" placeholder="Description of your content" />
</div>
<div className="card__content">
<FormRow label="Language" type="select" defaultValue="en" ref="meta_language" name="language">
<option value="en">English</option>
<option value="zh">Chinese</option>
<option value="fr">French</option>
<option value="de">German</option>
<option value="jp">Japanese</option>
<option value="ru">Russian</option>
<option value="es">Spanish</option>
</FormRow>
</div>
<div className="card__content">
<FormRow type="select" label="Maturity" defaultValue="en" ref="meta_nsfw" name="nsfw">
{/* <option value=""></option> */}
<option value="0">All Ages</option>
<option value="1">Adults Only</option>
</FormRow>
</div>
</div>}
</section>
<section className="card">
<div className="card__title-primary">
<h4>Access</h4>
<div className="card__subtitle">
How much does this content cost?
</div>
</div>
<div className="card__content">
<div className="form-row__label-row">
<label className="form-row__label">Price</label>
</div>
<FormRow label="Free" type="radio" name="isFree" value="1" onChange={ () => { this.handleFeePrefChange(false) } } defaultChecked={!this.state.isFee} />
<FormField type="radio" name="isFree" label={!this.state.isFee ? 'Choose price...' : 'Price ' }
onChange={ () => { this.handleFeePrefChange(true) } } defaultChecked={this.state.isFee} />
<span className={!this.state.isFee ? 'hidden' : ''}>
<FormField type="number" className="form-field__input--inline" step="0.01" placeholder="1.00" onChange={this.handleFeeAmountChange} /> <FormField type="select" onChange={this.handleFeeCurrencyChange}>
<option value="USD">US Dollars</option>
<option value="LBC">LBRY credits</option>
</FormField>
</span>
{ this.state.isFee ?
<div className="form-field__helper">
If you choose to price this content in dollars, the number of credits charged will be adjusted based on the value of LBRY credits at the time of purchase.
</div> : '' }
<FormRow label="License" type="select" ref="meta_license" name="license" onChange={this.handleLicenseChange}>
<option></option>
<option>Public Domain</option>
<option data-url="https://creativecommons.org/licenses/by/4.0/legalcode">Creative Commons Attribution 4.0 International</option>
<option data-url="https://creativecommons.org/licenses/by-sa/4.0/legalcode">Creative Commons Attribution-ShareAlike 4.0 International</option>
<option data-url="https://creativecommons.org/licenses/by-nd/4.0/legalcode">Creative Commons Attribution-NoDerivatives 4.0 International</option>
<option data-url="https://creativecommons.org/licenses/by-nc/4.0/legalcode">Creative Commons Attribution-NonCommercial 4.0 International</option>
<option data-url="https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode">Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International</option>
<option data-url="https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode">Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International</option>
<option data-license-type="copyright" {... this.state.copyrightChosen ? {value: this.state.copyrightNotice} : {}}>Copyrighted...</option>
<option data-license-type="other" {... this.state.otherLicenseChosen ? {value: this.state.otherLicenseDescription} : {}}>Other...</option>
</FormRow>
<FormField type="hidden" ref="meta_license_url" name="license_url" value={this.getLicenseUrl()} />
{this.state.copyrightChosen
? <FormRow label="Copyright notice" type="text" name="copyright-notice"
value={this.state.copyrightNotice} onChange={this.handleCopyrightNoticeChange} />
: null}
{this.state.otherLicenseChosen ?
<FormRow label="License description" type="text" name="other-license-description" onChange={this.handleOtherLicenseDescriptionChange} />
: null}
{this.state.otherLicenseChosen ?
<FormRow label="License URL" type="text" name="other-license-url" onChange={this.handleOtherLicenseUrlChange} />
: null}
</div> </div>
</section> </section>
<section className="card"> <section className="card">
<h4>Channel</h4> <div className="card__title-primary">
<div className="form-row"> <h4>Identity</h4>
<FormField type="select" onChange={this.handleChannelChange} value={this.state.channel}> <div className="card__subtitle">
Who created this content?
</div>
</div>
<div className="card__content">
<FormRow type="select" tabIndex="1" onChange={this.handleChannelChange} value={this.state.channel}>
<option key="anonymous" value="anonymous">Anonymous</option> <option key="anonymous" value="anonymous">Anonymous</option>
{this.state.channels.map(({name}) => <option key={name} value={name}>{name}</option>)} {this.state.channels.map(({name}) => <option key={name} value={name}>{name}</option>)}
<option key="new" value="new">New channel...</option> <option key="new" value="new">New identity...</option>
</FormField> </FormRow>
{this.state.channel == 'new'
? <section>
<label>Name <FormField type="text" onChange={this.handleNewChannelNameChange} ref={newChannelName => { this.refs.newChannelName = newChannelName }}
value={this.state.newChannelName} /></label>
<label>Bid amount <FormField type="text-number" onChange={this.handleNewChannelBidChange} value={this.state.newChannelBid} /> LBC</label>
<Link button="primary" label={!this.state.creatingChannel ? 'Create channel' : 'Creating channel...'} onClick={this.handleCreateChannelClick} disabled={this.state.creatingChannel} />
</section>
: null}
<div className="help">What channel would you like to publish this file under?</div>
</div> </div>
</section> {this.state.channel == 'new' ?
<div className="card__content">
<section className="card"> <FormRow label="Name" type="text" onChange={this.handleNewChannelNameChange} ref={newChannelName => { this.refs.newChannelName = newChannelName }}
<h4>Choose File</h4> value={this.state.newChannelName} />
<FormField name="file" ref="file" type="file" /> <FormRow label="Deposit"
{ this.state.myClaimExists ? <div className="help">If you don't choose a file, the file from your existing claim will be used.</div> : null } postfix="LBC"
</section> step="0.01"
type="number"
<section className="card"> helper={lbcInputHelp}
<h4>Bid Amount</h4> onChange={this.handleNewChannelBidChange}
<div className="form-row"> value={this.state.newChannelBid} />
Credits <FormField ref="bid" type="text-number" onChange={this.handleBidChange} value={this.state.bid} placeholder={this.state.nameResolved ? this.state.topClaimValue + 10 : 100} /> <div className="form-row-submit">
<div className="help">How much would you like to bid for this name? <Link button="primary" label={!this.state.creatingChannel ? 'Create identity' : 'Creating identity...'} onClick={this.handleCreateChannelClick} disabled={this.state.creatingChannel} />
{ !this.state.nameResolved ? <span> Since this name is not currently resolved, you may bid as low as you want, but higher bids help prevent others from claiming your name.</span> </div>
: (this.state.topClaimIsMine ? <span> You currently control this name with a bid of <strong>{this.state.myClaimValue}</strong> {this.state.myClaimValue == 1 ? 'credit' : 'credits'}.</span>
: (this.state.myClaimExists ? <span> You have a non-winning bid on this name for <strong>{this.state.myClaimValue}</strong> {this.state.myClaimValue == 1 ? 'credit' : 'credits'}.
To control this name, you'll need to increase your bid to more than <strong>{this.state.topClaimValue}</strong> {this.state.topClaimValue == 1 ? 'credit' : 'credits'}.</span>
: <span> You must bid over <strong>{this.state.topClaimValue}</strong> {this.state.topClaimValue == 1 ? 'credit' : 'credits'} to claim this name.</span>)) }
</div>
</div>
</section>
<section className="card">
<h4>Fee</h4>
<div className="form-row">
<label>
<FormField type="radio" onChange={ () => { this.handleFeePrefChange(false) } } checked={!this.state.isFee} /> No fee
</label>
<label>
<FormField type="radio" onChange={ () => { this.handleFeePrefChange(true) } } checked={this.state.isFee} /> { !this.state.isFee ? 'Choose fee...' : 'Fee ' }
<span className={!this.state.isFee ? 'hidden' : ''}>
<FormField type="text-number" onChange={this.handleFeeAmountChange} /> <FormField type="select" onChange={this.handleFeeCurrencyChange}>
<option value="USD">US Dollars</option>
<option value="LBC">LBRY credits</option>
</FormField>
</span>
</label>
<div className="help">
<p>How much would you like to charge for this file?</p>
If you choose to price this content in dollars, the number of credits charged will be adjusted based on the value of LBRY credits at the time of purchase.
</div>
</div>
</section>
<section className="card">
<h4>Your Content</h4>
<div className="form-row">
<label htmlFor="title">Title</label><FormField type="text" ref="meta_title" name="title" placeholder="My Show, Episode 1" />
</div>
<div className="form-row">
<label htmlFor="author">Author</label><FormField type="text" ref="meta_author" name="author" placeholder="My Company, Inc." />
</div>
<div className="form-row">
<label htmlFor="license">License</label><FormField type="select" ref="meta_license" name="license" onChange={this.handeLicenseChange}>
<option data-url="https://creativecommons.org/licenses/by/4.0/legalcode">Creative Commons Attribution 4.0 International</option>
<option data-url="https://creativecommons.org/licenses/by-sa/4.0/legalcode">Creative Commons Attribution-ShareAlike 4.0 International</option>
<option data-url="https://creativecommons.org/licenses/by-nd/4.0/legalcode">Creative Commons Attribution-NoDerivatives 4.0 International</option>
<option data-url="https://creativecommons.org/licenses/by-nc/4.0/legalcode">Creative Commons Attribution-NonCommercial 4.0 International</option>
<option data-url="https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode">Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International</option>
<option data-url="https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode">Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International</option>
<option>Public Domain</option>
<option data-license-type="copyright" {... this.state.copyrightChosen ? {value: this.state.copyrightNotice} : {}}>Copyrighted...</option>
<option data-license-type="other" {... this.state.otherLicenseChosen ? {value: this.state.otherLicenseDescription} : {}}>Other...</option>
</FormField>
<FormField type="hidden" ref="meta_license_url" name="license_url" value={this.getLicenseUrl()} />
</div>
{this.state.copyrightChosen
? <div className="form-row">
<label htmlFor="copyright-notice" value={this.state.copyrightNotice}>Copyright notice</label><FormField type="text" name="copyright-notice" value={this.state.copyrightNotice} onChange={this.handleCopyrightNoticeChange} />
</div> </div>
: null} : null}
{this.state.otherLicenseChosen
? <div className="form-row">
<label htmlFor="other-license-description">License description</label><FormField type="text" name="other-license-description" onChange={this.handleOtherLicenseDescriptionChange} />
</div>
: null}
{this.state.otherLicenseChosen
? <div className="form-row">
<label htmlFor="other-license-url">License URL</label> <FormField type="text" name="other-license-url" onChange={this.handleOtherLicenseUrlChange} />
</div>
: null}
<div className="form-row">
<label htmlFor="language">Language</label> <FormField type="select" defaultValue="en" ref="meta_language" name="language">
<option value="en">English</option>
<option value="zh">Chinese</option>
<option value="fr">French</option>
<option value="de">German</option>
<option value="jp">Japanese</option>
<option value="ru">Russian</option>
<option value="es">Spanish</option>
</FormField>
</div>
<div className="form-row">
<label htmlFor="description">Description</label> <FormField type="textarea" ref="meta_description" name="description" placeholder="Description of your content" />
</div>
<div className="form-row">
<label><FormField type="checkbox" ref="meta_nsfw" name="nsfw" placeholder="Description of your content" /> Not Safe For Work</label>
</div>
</section> </section>
<section className="card">
<div className="card__title-primary">
<h4>Address</h4>
<div className="card__subtitle">Where should this content permanently reside? <Link label="Read more" href="https://lbry.io/faq/naming" />.</div>
</div>
<div className="card__content">
<FormRow prefix="lbry://" type="text" ref="name" placeholder="myname" value={this.state.rawName} onChange={this.handleNameChange}
helper={this.getNameBidHelpText()} />
</div>
{ this.state.rawName ?
<div className="card__content">
<FormRow ref="bid"
type="number"
step="0.01"
label="Deposit"
postfix="LBC"
onChange={this.handleBidChange}
value={this.state.bid}
placeholder={this.state.nameResolved ? this.state.topClaimValue + 10 : 100}
helper={lbcInputHelp} />
</div> : '' }
</section>
<section className="card"> <section className="card">
<h4>Additional Content Information (Optional)</h4> <div className="card__title-primary">
<div className="form-row"> <h4>Terms of Service</h4>
<label htmlFor="meta_thumbnail">Thumbnail URL</label> <FormField type="text" ref="meta_thumbnail" name="thumbnail" placeholder="http://mycompany.com/images/ep_1.jpg" /> </div>
<div className="card__content">
<FormRow label={
<span>I agree to the <Link href="https://www.lbry.io/termsofservice" label="LBRY terms of service" checked={this.state.TOSAgreed} /></span>
} type="checkbox" name="tos_agree" ref={(field) => { this.refs.tos_agree = field }} onChange={this.handleTOSChange} />
</div> </div>
</section> </section>
@ -500,7 +560,7 @@ var PublishPage = React.createClass({
<Modal isOpen={this.state.modal == 'publishStarted'} contentLabel="File published" <Modal isOpen={this.state.modal == 'publishStarted'} contentLabel="File published"
onConfirmed={this.handlePublishStartedConfirmed}> onConfirmed={this.handlePublishStartedConfirmed}>
<p>Your file has been published to LBRY at the address <code>lbry://{this.state.name}</code>!</p> <p>Your file has been published to LBRY at the address <code>lbry://{this.state.name}</code>!</p>
You will now be taken to your My Files page, where your newly published file will be listed. The file will take a few minutes to appear for other LBRY users; until then it will be listed as "pending." <p>The file will take a few minutes to appear for other LBRY users. Until then it will be listed as "pending" under your published files.</p>
</Modal> </Modal>
<Modal isOpen={this.state.modal == 'error'} contentLabel="Error publishing file" <Modal isOpen={this.state.modal == 'error'} contentLabel="Error publishing file"
onConfirmed={this.closeModal}> onConfirmed={this.closeModal}>

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;

126
ui/js/page/reward.js Normal file
View file

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

73
ui/js/page/rewards.js Normal file
View file

@ -0,0 +1,73 @@
import React from 'react';
import lbry from '../lbry.js';
import lbryio from '../lbryio.js';
import {CreditAmount, Icon} from '../component/common.js';
import rewards from '../rewards.js';
import Modal from '../component/modal.js';
import {RewardLink} from '../component/link.js';
const RewardTile = React.createClass({
propTypes: {
type: React.PropTypes.string.isRequired,
title: React.PropTypes.string.isRequired,
description: React.PropTypes.string.isRequired,
claimed: React.PropTypes.bool.isRequired,
value: React.PropTypes.number.isRequired,
onRewardClaim: React.PropTypes.func
},
render: function() {
return (
<section className="card">
<div className="card__inner">
<div className="card__title-primary">
<CreditAmount amount={this.props.value} />
<h3>{this.props.title}</h3>
</div>
<div className="card__actions">
{this.props.claimed
? <span><Icon icon="icon-check" /> Reward claimed.</span>
: <RewardLink {...this.props} />}
</div>
<div className="card__content">{this.props.description}</div>
</div>
</section>
);
}
});
var RewardsPage = React.createClass({
componentWillMount: function() {
this.loadRewards()
},
getInitialState: function() {
return {
userRewards: null,
failed: null
};
},
loadRewards: function() {
lbryio.call('reward', 'list', {}).then((userRewards) => {
this.setState({
userRewards: userRewards,
});
}, () => {
this.setState({failed: true })
});
},
render: function() {
console.log(this.state.userRewards);
return (
<main>
<form onSubmit={this.handleSubmit}>
{!this.state.userRewards
? (this.state.failed ? <div className="empty">Failed to load rewards.</div> : '')
: this.state.userRewards.map(({RewardType, RewardTitle, RewardDescription, TransactionID, RewardAmount}) => {
return <RewardTile key={RewardType} onRewardClaim={this.loadRewards} type={RewardType} title={RewardTitle} description={RewardDescription} claimed={!!TransactionID} value={RewardAmount} />;
})}
</form>
</main>
);
}
});
export default RewardsPage;

View file

@ -1,52 +1,53 @@
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({
_onSettingSaveSuccess: function() {
// This is bad.
// document.dispatchEvent(new CustomEvent('globalNotice', {
// detail: {
// message: "Settings saved",
// },
// }))
},
setDaemonSetting: function(name, value) {
lbry.setDaemonSetting(name, value, this._onSettingSaveSuccess)
},
setClientSetting: function(name, value) {
lbry.setClientSetting(name, value)
this._onSettingSaveSuccess()
},
onRunOnStartChange: function (event) { onRunOnStartChange: function (event) {
lbry.setDaemonSetting('run_on_startup', event.target.checked); this.setDaemonSetting('run_on_startup', event.target.checked);
}, },
onShareDataChange: function (event) { onShareDataChange: function (event) {
lbry.setDaemonSetting('share_debug_info', event.target.checked); this.setDaemonSetting('share_debug_info', event.target.checked);
}, },
onDownloadDirChange: function(event) { onDownloadDirChange: function(event) {
lbry.setDaemonSetting('download_directory', event.target.value); this.setDaemonSetting('download_directory', event.target.value);
}, },
onMaxUploadPrefChange: function(isLimited) { onMaxUploadPrefChange: function(isLimited) {
if (!isLimited) { if (!isLimited) {
lbry.setDaemonSetting('max_upload', 0.0); this.setDaemonSetting('max_upload', 0.0);
} }
this.setState({ this.setState({
isMaxUpload: isLimited isMaxUpload: isLimited
}); });
}, },
onMaxUploadFieldChange: function(event) { onMaxUploadFieldChange: function(event) {
lbry.setDaemonSetting('max_upload', Number(event.target.value)); this.setDaemonSetting('max_upload', Number(event.target.value));
}, },
onMaxDownloadPrefChange: function(isLimited) { onMaxDownloadPrefChange: function(isLimited) {
if (!isLimited) { if (!isLimited) {
lbry.setDaemonSetting('max_download', 0.0); this.setDaemonSetting('max_download', 0.0);
} }
this.setState({ this.setState({
isMaxDownload: isLimited isMaxDownload: isLimited
}); });
}, },
onMaxDownloadFieldChange: function(event) { onMaxDownloadFieldChange: function(event) {
lbry.setDaemonSetting('max_download', Number(event.target.value)); this.setDaemonSetting('max_download', Number(event.target.value));
}, },
getInitialState: function() { getInitialState: function() {
return { return {
@ -71,84 +72,130 @@ var SettingsPage = React.createClass({
lbry.setClientSetting('showNsfw', event.target.checked); lbry.setClientSetting('showNsfw', event.target.checked);
}, },
onShowUnavailableChange: function(event) { onShowUnavailableChange: function(event) {
lbry.setClientSetting('showUnavailable', event.target.checked);
}, },
render: function() { render: function() {
if (!this.state.daemonSettings) { if (!this.state.daemonSettings) {
return null; return null;
} }
/*
<section className="card">
<div className="card__content">
<h3>Run on Startup</h3>
</div>
<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>
*/
return ( return (
<main> <main>
<section className="card"> <section className="card">
<h3>Run on Startup</h3> <div className="card__content">
<label style={settingsCheckBoxOptionStyles}> <h3>Download Directory</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">
<h4>Max Download</h4> <FormRow type="text"
<label style={settingsRadioOptionStyles}> name="download_directory"
<input type="radio" name="max_download_pref" onChange={this.onMaxDownloadPrefChange.bind(this, false)} defaultChecked={!this.state.isMaxDownload}/> Unlimited defaultValue={this.state.daemonSettings.download_directory}
</label> helper="LBRY downloads will be saved here."
<label style={settingsRadioOptionStyles}> onChange={this.onDownloadDirChange} />
<input type="radio" name="max_download_pref" onChange={this.onMaxDownloadPrefChange.bind(this, true)} defaultChecked={this.state.isMaxDownload}/> { this.state.isMaxDownload ? 'Up to' : 'Choose limit...' }
<span className={ this.state.isMaxDownload ? '' : 'hidden'}> <input type="number" min="0" step=".5" defaultValue={this.state.daemonSettings.max_download} style={settingsNumberFieldStyles} onChange={this.onMaxDownloadFieldChange}/> MB/s</span>
</label>
</div> </div>
</section> </section>
<section className="card"> <section className="card">
<h3>Content</h3> <div className="card__content">
<div className="form-row"> <h3>Bandwidth Limits</h3>
<label style={settingsCheckBoxOptionStyles}> </div>
<input type="checkbox" onChange={this.onShowNsfwChange} defaultChecked={this.state.showNsfw} /> Show NSFW content <div className="card__content">
</label> <div className="form-row__label-row"><div className="form-field__label">Max Upload</div></div>
<div className="help"> <FormRow type="radio"
NSFW content may include nudity, intense sexuality, profanity, or other adult content. name="max_upload_pref"
By displaying NSFW content, you are affirming you are of legal age to view mature content in your country or jurisdiction. onChange={this.onMaxUploadPrefChange.bind(this, false)}
defaultChecked={!this.state.isMaxUpload}
label="Unlimited" />
<div className="form-row">
<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"
defaultValue={this.state.daemonSettings.max_upload}
placeholder="10"
className="form-field__input--inline"
onChange={this.onMaxUploadFieldChange}
/>
: ''
}
{ this.state.isMaxUpload ? <span className="form-field__label">MB/s</span> : '' }
</div>
</div>
<div className="card__content">
<div className="form-row__label-row"><div className="form-field__label">Max Download</div></div>
<FormRow label="Unlimited"
type="radio"
name="max_download_pref"
onChange={this.onMaxDownloadPrefChange.bind(this, false)}
defaultChecked={!this.state.isMaxDownload} />
<div className="form-row">
<FormField type="radio"
name="max_download_pref"
onChange={this.onMaxDownloadPrefChange.bind(this, true)}
defaultChecked={this.state.isMaxDownload}
label={ this.state.isMaxDownload ? 'Up to' : 'Choose limit...' } />
{ this.state.isMaxDownload ?
<FormField type="number"
min="0"
step=".5"
defaultValue={this.state.daemonSettings.max_download}
placeholder="10"
className="form-field__input--inline"
onChange={this.onMaxDownloadFieldChange}
/>
: ''
}
{ this.state.isMaxDownload ? <span className="form-field__label">MB/s</span> : '' }
</div> </div>
</div> </div>
</section> </section>
<section className="card"> <section className="card">
<h3>Search</h3> <div className="card__content">
<div className="form-row"> <h3>Content</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 className="card__content">
<input type="checkbox" onChange={this.onShowUnavailableChange} defaultChecked={this.state.showUnavailable} /> <FormRow type="checkbox"
Show unavailable content in search results onChange={this.onShowUnavailableChange}
</label> defaultChecked={this.state.showUnavailable}
label="Show unavailable content in search results" />
</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>Share Diagnostic Data</h3> <div className="card__content">
<label style={settingsCheckBoxOptionStyles}> <h3>Share Diagnostic Data</h3>
<input type="checkbox" onChange={this.onShareDataChange} defaultChecked={this.state.daemonSettings.share_debug_info} /> </div>
Help make LBRY better by contributing diagnostic data about my usage <div className="card__content">
</label> <FormRow type="checkbox"
onChange={this.onShareDataChange}
defaultChecked={this.state.daemonSettings.share_debug_info}
label="Help make LBRY better by contributing diagnostic data about my usage" />
</div>
</section> </section>
</main> </main>
); );
} }
}); });
export default SettingsPage; export default SettingsPage;

View file

@ -2,104 +2,45 @@ import React from 'react';
import lbry from '../lbry.js'; import lbry from '../lbry.js';
import lighthouse from '../lighthouse.js'; import lighthouse from '../lighthouse.js';
import uri from '../uri.js'; import uri from '../uri.js';
import {CreditAmount, Thumbnail} from '../component/common.js'; import {Video} from '../page/watch.js'
import {TruncatedText, Thumbnail, FilePrice, BusyMessage} from '../component/common.js';
import {FileActions} from '../component/file-actions.js'; import {FileActions} from '../component/file-actions.js';
import {Link} from '../component/link.js'; import {Link} from '../component/link.js';
import UriIndicator from '../component/channel-indicator.js';
var formatItemImgStyle = {
maxWidth: '100%',
maxHeight: '100%',
display: 'block',
marginLeft: 'auto',
marginRight: 'auto',
marginTop: '5px',
};
var FormatItem = React.createClass({ var FormatItem = React.createClass({
propTypes: { propTypes: {
metadata: React.PropTypes.object, metadata: React.PropTypes.object,
contentType: React.PropTypes.string, contentType: React.PropTypes.string,
cost: React.PropTypes.number,
uri: React.PropTypes.string, uri: React.PropTypes.string,
outpoint: React.PropTypes.string, outpoint: React.PropTypes.string,
costIncludesData: React.PropTypes.bool,
}, },
render: function() { render: function() {
const {thumbnail, author, title, description, language, license} = this.props.metadata; const {thumbnail, author, title, description, language, license} = this.props.metadata;
const mediaType = lbry.getMediaType(this.props.contentType); const mediaType = lbry.getMediaType(this.props.contentType);
var costIncludesData = this.props.costIncludesData;
var cost = this.props.cost || 0.0;
return ( return (
<div className="row-fluid"> <table className="table-standard">
<div className="span4"> <tbody>
<Thumbnail src={thumbnail} alt={'Photo for ' + title} style={formatItemImgStyle} /> <tr>
</div> <td>Content-Type</td><td>{this.props.contentType}</td>
<div className="span8"> </tr>
<p>{description}</p> <tr>
<section> <td>Author</td><td>{author}</td>
<table className="table-standard"> </tr>
<tbody> <tr>
<tr> <td>Language</td><td>{language}</td>
<td>Content-Type</td><td>{this.props.contentType}</td> </tr>
</tr> <tr>
<tr> <td>License</td><td>{license}</td>
<td>Cost</td><td><CreditAmount amount={cost} isEstimate={!costIncludesData}/></td> </tr>
</tr> </tbody>
<tr> </table>
<td>Author</td><td>{author}</td> );
</tr>
<tr>
<td>Language</td><td>{language}</td>
</tr>
<tr>
<td>License</td><td>{license}</td>
</tr>
</tbody>
</table>
</section>
<FileActions uri={this._uri} outpoint={this.props.outpoint} metadata={this.props.metadata} contentType={this.props.contentType} />
<section>
<Link href="https://lbry.io/dmca" label="report" className="button-text-help" />
</section>
</div>
</div>
);
} }
}); });
var FormatsSection = React.createClass({ let ShowPage = React.createClass({
propTypes: {
uri: React.PropTypes.string,
outpoint: React.PropTypes.string,
metadata: React.PropTypes.object,
contentType: React.PropTypes.string,
cost: React.PropTypes.number,
costIncludesData: React.PropTypes.bool,
},
render: function() {
if(this.props.metadata == null)
{
return (
<div>
<h2>Sorry, no results found for "{name}".</h2>
</div>);
}
return (
<div>
<div className="meta">{this.props.uri}</div>
<h2>{this.props.metadata.title}</h2>
{/* In future, anticipate multiple formats, just a guess at what it could look like
// var formats = this.props.metadata.formats
// return (<tbody>{formats.map(function(format,i){ */}
<FormatItem metadata={this.props.metadata} contentType={this.props.contentType} cost={this.props.cost} uri={this.props.uri} outpoint={this.props.outpoint} costIncludesData={this.props.costIncludesData} />
{/* })}</tbody>); */}
</div>);
}
});
var ShowPage = React.createClass({
_uri: null, _uri: null,
propTypes: { propTypes: {
@ -109,6 +50,8 @@ var ShowPage = React.createClass({
return { return {
metadata: null, metadata: null,
contentType: null, contentType: null,
hasSignature: false,
signatureIsValid: false,
cost: null, cost: null,
costIncludesData: null, costIncludesData: null,
uriLookupComplete: null, uriLookupComplete: null,
@ -118,16 +61,18 @@ var ShowPage = React.createClass({
this._uri = uri.normalizeLbryUri(this.props.uri); this._uri = uri.normalizeLbryUri(this.props.uri);
document.title = this._uri; document.title = this._uri;
lbry.resolve({uri: this._uri}).then(({txid, nout, claim: {value: {stream: {metadata, source: {contentType}}}}}) => { lbry.resolve({uri: this._uri}).then(({ claim: {txid, nout, has_signature, signature_is_valid, value: {stream: {metadata, source: {contentType}}}}}) => {
this.setState({ this.setState({
outpoint: txid + ':' + nout, outpoint: txid + ':' + nout,
metadata: metadata, metadata: metadata,
hasSignature: has_signature,
signatureIsValid: signature_is_valid,
contentType: contentType, contentType: contentType,
uriLookupComplete: true, uriLookupComplete: true,
}); });
}); });
lbry.getCostInfo(this._uri, ({cost, includesData}) => { lbry.getCostInfo(this._uri).then(({cost, includesData}) => {
this.setState({ this.setState({
cost: cost, cost: cost,
costIncludesData: includesData, costIncludesData: includesData,
@ -135,23 +80,50 @@ var ShowPage = React.createClass({
}); });
}, },
render: function() { render: function() {
if (this.state.metadata == null) { const
return null; metadata = this.state.uriLookupComplete ? this.state.metadata : null,
} title = this.state.uriLookupComplete ? metadata.title : this._uri;
return ( return (
<main> <main className="constrained-page">
<section className="card"> <section className="show-page-media">
{this.state.uriLookupComplete ? ( { this.state.contentType && this.state.contentType.startsWith('video/') ?
<FormatsSection uri={this._uri} outpoint={this.state.outpoint} metadata={this.state.metadata} cost={this.state.cost} costIncludesData={this.state.costIncludesData} contentType={this.state.contentType} /> <Video className="video-embedded" uri={this._uri} metadata={metadata} /> :
) : ( (metadata ? <Thumbnail src={metadata.thumbnail} /> : <Thumbnail />) }
<div>
<h2>No content</h2>
There is no content available at <strong>{this._uri}</strong>. If you reached this page from a link within the LBRY interface, please <Link href="?report" label="report a bug" />. Thanks!
</div>
)}
</section> </section>
</main>); <section className="card">
<div className="card__inner">
<div className="card__title-identity">
<span style={{float: "right"}}><FilePrice uri={this._uri} metadata={metadata} /></span>
<h1>{title}</h1>
{ this.state.uriLookupComplete ?
<div>
<div className="card__subtitle">
<UriIndicator uri={this._uri} hasSignature={this.state.hasSignature} signatureIsValid={this.state.signatureIsValid} />
</div>
<div className="card__actions">
<FileActions uri={this._uri} outpoint={this.state.outpoint} metadata={metadata} contentType={this.state.contentType} />
</div>
</div> : '' }
</div>
{ this.state.uriLookupComplete ?
<div>
<div className="card__content card__subtext card__subtext card__subtext--allow-newlines">
{metadata.description}
</div>
</div>
: <div className="card__content"><BusyMessage message="Loading magic decentralized data..." /></div> }
</div>
{ metadata ?
<div className="card__content">
<FormatItem metadata={metadata} contentType={this.state.contentType} cost={this.state.cost} uri={this._uri} outpoint={this.state.outpoint} costIncludesData={this.state.costIncludesData} />
</div> : '' }
<div className="card__content">
<Link href="https://lbry.io/dmca" label="report" className="button-text-help" />
</div>
</section>
</main>
);
} }
}); });

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(function(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" postfix="LBC" step="0.01" 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,14 +1,97 @@
import React from 'react'; import React from 'react';
import {Icon} from '../component/common.js'; import {Icon, Thumbnail} from '../component/common.js';
import {Link} from '../component/link.js'; import {Link} from '../component/link.js';
import lbry from '../lbry.js'; import lbry from '../lbry.js';
import Modal from '../component/modal.js';
import lbryio from '../lbryio.js';
import rewards from '../rewards.js';
import LoadScreen from '../component/load_screen.js' import LoadScreen from '../component/load_screen.js'
const fs = require('fs'); const fs = require('fs');
const VideoStream = require('videostream'); const VideoStream = require('videostream');
export let WatchLink = React.createClass({
propTypes: {
uri: React.PropTypes.string,
downloadStarted: React.PropTypes.bool,
onGet: React.PropTypes.func
},
getInitialState: function() {
affirmedPurchase: false
},
onAffirmPurchase: function() {
lbry.get({uri: this.props.uri}).then((streamInfo) => {
if (streamInfo === null || typeof streamInfo !== 'object') {
this.setState({
modal: 'timedOut',
attemptingDownload: false,
});
}
var WatchPage = React.createClass({ lbryio.call('file', 'view', {
uri: this.props.uri,
outpoint: streamInfo.outpoint,
claimId: streamInfo.claim_id
})
});
if (this.props.onGet) {
this.props.onGet()
}
},
onWatchClick: function() {
this.setState({
loading: true
});
lbry.getCostInfo(this.props.uri).then(({cost}) => {
lbry.getBalance((balance) => {
if (cost > balance) {
this.setState({
modal: 'notEnoughCredits',
attemptingDownload: false,
});
} else if (cost <= 0.01) {
this.onAffirmPurchase()
} else {
this.setState({
modal: 'affirmPurchase'
});
}
});
});
},
getInitialState: function() {
return {
modal: null,
loading: false,
};
},
closeModal: function() {
this.setState({
loading: false,
modal: null,
});
},
render: function() {
return (<div>
<Link button={ this.props.button ? this.props.button : null }
disabled={this.state.loading}
label={this.props.label ? this.props.label : ""}
className={this.props.className}
icon="icon-play"
onClick={this.onWatchClick} />
<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.
</Modal>
<Modal type="confirm" isOpen={this.state.modal == 'affirmPurchase'}
contentLabel="Confirm Purchase" onConfirmed={this.onAffirmPurchase} onAborted={this.closeModal}>
Confirm you want to purchase this bro.
</Modal>
</div>);
}
});
export let Video = React.createClass({
_isMounted: false, _isMounted: false,
_controlsHideDelay: 3000, // Note: this needs to be shorter than the built-in delay in Electron, or Electron will hide the controls before us _controlsHideDelay: 3000, // Note: this needs to be shorter than the built-in delay in Electron, or Electron will hide the controls before us
_controlsHideTimeout: null, _controlsHideTimeout: null,
@ -21,19 +104,26 @@ var WatchPage = React.createClass({
return { return {
downloadStarted: false, downloadStarted: false,
readyToPlay: false, readyToPlay: false,
loadStatusMessage: "Requesting stream", isPlaying: false,
isPurchased: false,
loadStatusMessage: "Requesting stream... it may sit here for like 15-20 seconds in a really awkward way... we're working on it",
mimeType: null, mimeType: null,
controlsShown: false, controlsShown: false,
}; };
}, },
componentDidMount: function() { onGet: function() {
lbry.get({uri: this.props.uri}).then((fileInfo) => { lbry.get({uri: this.props.uri}).then((fileInfo) => {
this._outpoint = fileInfo.outpoint; this._outpoint = fileInfo.outpoint;
this.updateLoadStatus(); this.updateLoadStatus();
}); });
this.setState({
isPlaying: true
})
}, },
handleBackClicked: function() { componentDidMount: function() {
history.back(); if (this.props.autoplay) {
this.start()
}
}, },
handleMouseMove: function() { handleMouseMove: function() {
if (this._controlsTimeout) { if (this._controlsTimeout) {
@ -93,6 +183,9 @@ var WatchPage = React.createClass({
return fs.createReadStream(status.download_path, opts) return fs.createReadStream(status.download_path, opts)
} }
}; };
rewards.claimNextPurchaseReward()
var elem = this.refs.video; var elem = this.refs.video;
var videostream = VideoStream(mediaFile, elem); var videostream = VideoStream(mediaFile, elem);
elem.play(); elem.play();
@ -101,26 +194,15 @@ var WatchPage = React.createClass({
}, },
render: function() { render: function() {
return ( return (
!this.state.readyToPlay <div className={"video " + this.props.className + (this.state.isPlaying && this.state.readyToPlay ? " video--active" : " video--hidden")}>{
? <LoadScreen message={'Loading video...'} details={this.state.loadStatusMessage} /> this.state.isPlaying ?
: <main className="video full-screen" onMouseMove={this.handleMouseMove} onMouseLeave={this.handleMouseLeave}> !this.state.readyToPlay ?
<video controls width="100%" height="100%" id="video" ref="video"></video> <span>this is the world's world loading screen and we shipped our software with it anyway... <br/><br/>{this.state.loadStatusMessage}</span> :
{this.state.controlsShown <video controls id="video" ref="video"></video> :
? <div className="video__overlay"> <div className="video__cover" style={{backgroundImage: 'url("' + this.props.metadata.thumbnail + '")'}}>
<div className="video__back"> <WatchLink className="video__play-button" uri={this.props.uri} onGet={this.onGet} icon="icon-play"></WatchLink>
<Link icon="icon-arrow-circle-o-left" className="video__back-link" onClick={this.handleBackClicked}/> </div>
<div className="video__back-label"> }</div>
<Icon icon="icon-caret-left" className="video__back-label-arrow" />
<div className="video__back-label-content">
Back to LBRY
</div>
</div>
</div>
</div>
: null}
</main>
); );
} }
}); })
export default WatchPage;

125
ui/js/rewards.js Normal file
View file

@ -0,0 +1,125 @@
import lbry from './lbry.js';
import lbryio from './lbryio.js';
function rewardMessage(type, amount) {
return {
new_developer: `You earned ${amount} for registering as a new developer.`,
new_user: `You earned ${amount} LBC new user reward.`,
confirm_email: `You earned ${amount} LBC for verifying your email address.`,
new_channel: `You earned ${amount} LBC for creating a publisher identity.`,
first_stream: `You earned ${amount} LBC for streaming your first video.`,
many_downloads: `You earned ${amount} LBC for downloading some of the things.`,
first_publish: `You earned ${amount} LBC for making your first publication.`,
}[type];
}
const rewards = {};
rewards.TYPE_NEW_DEVELOPER = "new_developer",
rewards.TYPE_NEW_USER = "new_user",
rewards.TYPE_CONFIRM_EMAIL = "confirm_email",
rewards.TYPE_FIRST_CHANNEL = "new_channel",
rewards.TYPE_FIRST_STREAM = "first_stream",
rewards.TYPE_MANY_DOWNLOADS = "many_downloads",
rewards.TYPE_FIRST_PUBLISH = "first_publish";
rewards.claimReward = function (type) {
function requestReward(resolve, reject, params) {
if (!lbryio.enabled) {
reject(new Error("Rewards are not enabled."))
return;
}
lbryio.call('reward', 'new', params, 'post').then(({RewardAmount}) => {
const
message = rewardMessage(type, RewardAmount),
result = {
type: type,
amount: RewardAmount,
message: message
};
// Display global notice
document.dispatchEvent(new CustomEvent('globalNotice', {
detail: {
message: message,
linkText: "Show All",
linkTarget: "?rewards",
isError: false,
},
}));
// Add more events here to display other places
resolve(result);
}, reject);
}
return new Promise((resolve, reject) => {
lbry.get_new_address().then((address) => {
const params = {
reward_type: type,
wallet_address: address,
};
switch (type) {
case rewards.TYPE_FIRST_CHANNEL:
lbry.claim_list_mine().then(function(claims) {
let claim = claims.find(function(claim) {
return claim.name.length && claim.name[0] == '@' && claim.txid.length
})
console.log(claim);
if (claim) {
params.transaction_id = claim.txid;
requestReward(resolve, reject, params)
} else {
reject(new Error("Please create a channel identity first."))
}
}).catch(reject)
break;
case rewards.TYPE_FIRST_PUBLISH:
lbry.claim_list_mine().then((claims) => {
let claim = claims.find(function(claim) {
return claim.name.length && claim.name[0] != '@' && claim.txid.length
})
if (claim) {
params.transaction_id = claim.txid
requestReward(resolve, reject, params)
} else {
reject(claims.length ?
new Error("Please publish something and wait for confirmation by the network to claim this reward.") :
new Error("Please publish something to claim this reward."))
}
}).catch(reject)
break;
case rewards.TYPE_FIRST_STREAM:
case rewards.TYPE_NEW_USER:
default:
requestReward(resolve, reject, params);
}
});
});
}
rewards.claimNextPurchaseReward = function() {
let types = {}
types[rewards.TYPE_FIRST_STREAM] = false
types[rewards.TYPE_MANY_DOWNLOADS] = false
lbryio.call('reward', 'list', {}).then((userRewards) => {
userRewards.forEach((reward) => {
if (types[reward.RewardType] === false && reward.TransactionID) {
types[reward.RewardType] = true
}
})
let unclaimedType = Object.keys(types).find((type) => {
return types[type] === false;
})
if (unclaimedType) {
rewards.claimReward(unclaimedType);
}
}, () => { });
}
export default rewards;

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, fallback=undefined) {
const itemRaw = sessionStorage.getItem(key);
return itemRaw === null ? fallback : 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

@ -11,7 +11,7 @@ body
line-height: $font-line-height; line-height: $font-line-height;
} }
$drawer-width: 240px; $drawer-width: 220px;
#drawer #drawer
{ {
@ -39,12 +39,8 @@ $drawer-width: 240px;
.badge .badge
{ {
float: right; float: right;
background: $color-money;
display: inline-block;
padding: 2px;
color: white;
margin-top: $spacing-vertical * 0.25 - 2; margin-top: $spacing-vertical * 0.25 - 2;
border-radius: 2px; background: $color-money;
} }
} }
.drawer-item-selected .drawer-item-selected
@ -53,6 +49,23 @@ $drawer-width: 240px;
color: $color-primary; color: $color-primary;
} }
} }
.badge
{
background: $color-money;
display: inline-block;
padding: 2px;
color: white;
border-radius: 2px;
}
.credit-amount
{
font-weight: bold;
color: $color-money;
}
.credit-amount--estimate {
font-style: italic;
color: $color-meta-light;
}
#drawer-handle #drawer-handle
{ {
padding: $spacing-vertical / 2; padding: $spacing-vertical / 2;
@ -60,6 +73,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 +118,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;
}
} }
} }
@ -158,25 +192,11 @@ nav.sub-header
main main
{ {
padding: $spacing-vertical; padding: $spacing-vertical;
} &.constrained-page
h2
{
margin-bottom: $spacing-vertical;
}
h3, h4
{
margin-bottom: $spacing-vertical / 2;
margin-top: $spacing-vertical;
&:first-child
{ {
margin-top: 0; max-width: $width-page-constrained;
} margin-left: auto;
} margin-right: auto;
.meta
{
+ h2, + h3, + h4
{
margin-top: 0;
} }
} }
} }
@ -197,48 +217,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

@ -6,17 +6,20 @@ $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: #921010; $color-notice: #8a6d3b;
$color-warning: #ffffff; $color-error: #a94442;
$color-load-screen-text: #c3c3c3; $color-load-screen-text: #c3c3c3;
$color-canvas: #f5f5f5; $color-canvas: #f5f5f5;
$color-bg: #ffffff; $color-bg: #ffffff;
$color-bg-alt: #D9D9D9; $color-bg-alt: #D9D9D9;
$color-money: #216C2A; $color-money: #216C2A;
$color-meta-light: #505050; $color-meta-light: #505050;
$color-form-border: rgba(160,160,160,.5);
$font-size: 16px; $font-size: 16px;
$font-line-height: 1.3333; $font-line-height: 1.3333;
@ -25,10 +28,16 @@ $mobile-width-threshold: 801px;
$max-content-width: 1000px; $max-content-width: 1000px;
$max-text-width: 660px; $max-text-width: 660px;
$width-page-constrained: 800px;
$height-header: $spacing-vertical * 2.5; $height-header: $spacing-vertical * 2.5;
$height-button: $spacing-vertical * 1.5; $height-button: $spacing-vertical * 1.5;
$height-video-embedded: $width-page-constrained * 9 / 16;
$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);
$transition-standard: .225s ease;
$blur-intensity: 8px; $blur-intensity: 8px;

View file

@ -21,7 +21,7 @@
&:hover &:hover
{ {
opacity: $hover-opacity; opacity: $hover-opacity;
transition: opacity .225s ease; transition: opacity $transition-standard;
text-decoration: underline; text-decoration: underline;
.icon { .icon {
text-decoration: none; text-decoration: none;
@ -38,6 +38,7 @@
text-align: center; text-align: center;
} }
/*
section section
{ {
margin-bottom: $spacing-vertical; margin-bottom: $spacing-vertical;
@ -46,17 +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 {
font-size: 2.0em;
margin-bottom: $spacing-vertical;
margin-top: $spacing-vertical*2;
font-family: 'Raleway', sans-serif;
}
h2 { h2 {
font-size: 1.75em; font-size: 1.75em;
@ -76,11 +70,6 @@ sup, sub {
sup { top: -0.4em; } sup { top: -0.4em; }
sub { top: 0.4em; } sub { top: 0.4em; }
label {
cursor: default;
display: block;
}
code { code {
font: 0.8em Consolas, 'Lucida Console', 'Source Sans', monospace; font: 0.8em Consolas, 'Lucida Console', 'Source Sans', monospace;
background-color: #eee; background-color: #eee;
@ -104,23 +93,6 @@ p
opacity: 0.7; opacity: 0.7;
} }
input[type="text"], input[type="search"], textarea
{
@include placeholder {
color: lighten($color-text-dark, 60%);
}
border: 2px solid rgba(160,160,160,.5);
padding-left: 5px;
padding-right: 5px;
box-sizing: border-box;
-webkit-appearance: none;
}
input[type="text"], input[type="search"]
{
line-height: $spacing-vertical - 4;
height: $spacing-vertical * 1.5;
}
.truncated-text { .truncated-text {
display: inline-block; display: inline-block;
} }
@ -144,75 +116,6 @@ input[type="text"], input[type="search"]
} }
} }
.button-set-item {
position: relative;
display: inline-block;
+ .button-set-item
{
margin-left: $padding-button;
}
}
.button-block, .faux-button-block
{
display: inline-block;
height: $height-button;
line-height: $height-button;
text-decoration: none;
border: 0 none;
text-align: center;
border-radius: 2px;
text-transform: uppercase;
.icon
{
top: 0em;
}
.icon:first-child
{
padding-right: 5px;
}
.icon:last-child
{
padding-left: 5px;
}
}
.button-block
{
cursor: pointer;
}
.button__content {
margin: 0 $padding-button;
}
.button-primary
{
color: white;
background-color: $color-primary;
box-shadow: $default-box-shadow;
}
.button-alt
{
background-color: $color-bg-alt;
box-shadow: $default-box-shadow;
}
.button-text
{
@include text-link();
display: inline-block;
.button__content {
margin: 0 $padding-text-link;
}
}
.button-text-help
{
@include text-link(#aaa);
font-size: 0.8em;
}
.icon:only-child { .icon:only-child {
position: relative; position: relative;
top: 0.16em; top: 0.16em;
@ -235,87 +138,6 @@ input[type="text"], input[type="search"]
font-style: italic; font-style: italic;
} }
.form-row
{
+ .form-row
{
margin-top: $spacing-vertical / 2;
}
.help
{
margin-top: $spacing-vertical / 2;
}
+ .form-row-submit
{
margin-top: $spacing-vertical;
}
}
.form-field-container {
display: inline-block;
}
.form-field--text {
width: 330px;
}
.form-field--text-number {
width: 50px;
}
.form-field-advice-container {
position: relative;
}
.form-field-advice {
position: absolute;
top: 0px;
left: 0px;
display: flex;
flex-direction: column;
white-space: nowrap;
transition: opacity 400ms ease-in;
}
.form-field-advice--fading {
opacity: 0;
}
.form-field-advice__arrow {
text-align: left;
padding-left: 18px;
font-size: 22px;
line-height: 0.3;
color: darken($color-primary, 5%);
}
.form-field-advice__content-container {
display: inline-block;
}
.form-field-advice__content {
display: inline-block;
padding: 5px;
border-radius: 2px;
background-color: darken($color-primary, 5%);
color: #fff;
}
.form-field-label {
width: 118px;
text-align: right;
vertical-align: top;
display: inline-block;
}
.sort-section { .sort-section {
display: block; display: block;
margin-bottom: 5px; margin-bottom: 5px;
@ -325,79 +147,3 @@ input[type="text"], input[type="search"]
font-size: 0.85em; font-size: 0.85em;
color: $color-help; color: $color-help;
} }
.modal-overlay {
position: fixed;
display: flex;
justify-content: center;
align-items: center;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
background-color: rgba(255, 255, 255, 0.74902);
z-index: 9999;
}
.modal {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border: 1px solid rgb(204, 204, 204);
background: rgb(255, 255, 255);
overflow: auto;
border-radius: 4px;
outline: none;
padding: 36px;
max-width: 250px;
}
.modal__header {
margin-bottom: 5px;
text-align: center;
}
.modal__buttons {
display: flex;
flex-direction: row;
justify-content: center;
margin-top: 15px;
}
.modal__button {
margin: 0px 6px;
}
.error-modal-overlay {
background: rgba(#000, .88);
}
.error-modal__content {
display: flex;
padding: 0px 8px 10px 10px;
}
.error-modal__warning-symbol {
margin-top: 6px;
margin-right: 7px;
}
.download-started-modal__file-path {
word-break: break-all;
}
.error-modal {
max-width: none;
width: 400px;
}
.error-modal__error-list { /*shitty hack/temp fix for long errors making modals unusable*/
border: 1px solid #eee;
padding: 8px;
list-style: none;
max-height: 400px;
max-width: 400px;
overflow-y: hidden;
}

View file

@ -3,20 +3,24 @@ body, div, dl, dt, dd, ul, ol, li, h1, h2, h3, h4, h5, h6, pre, code, form, fiel
margin:0; margin:0;
padding:0; padding:0;
} }
input:focus, textarea:focus :focus
{ {
outline: 0; outline: 0;
} }
table input::-webkit-search-cancel-button {
/* Remove default */
-webkit-appearance: none;
}
table
{ {
border-collapse: collapse; border-collapse: collapse;
border-spacing:0; border-spacing:0;
} }
fieldset, img, iframe fieldset, img, iframe
{ {
border: 0; border: 0;
} }
h1, h2, h3, h4, h5, h6 h1, h2, h3, h4, h5, h6
{ {
font-weight:normal; font-weight:normal;
} }
@ -25,11 +29,12 @@ ol, ul
list-style-position: inside; list-style-position: inside;
> li { list-style-position: inside; } > li { list-style-position: inside; }
} }
input, textarea, select input, textarea, select
{ {
font-family:inherit; font-family:inherit;
font-size:inherit; font-size:inherit;
font-weight:inherit; font-weight:inherit;
border: 0 none;
} }
img { img {
width: auto\9; width: auto\9;

View file

@ -5,11 +5,21 @@
@import "_canvas"; @import "_canvas";
@import "_gui"; @import "_gui";
@import "component/_table"; @import "component/_table";
@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/_menu.scss"; @import "component/_menu.scss";
@import "component/_tooltip.scss"; @import "component/_tooltip.scss";
@import "component/_load-screen.scss"; @import "component/_load-screen.scss";
@import "component/_channel-indicator.scss"; @import "component/_channel-indicator.scss";
@import "component/_notice.scss";
@import "component/_modal.scss";
@import "component/_modal-page.scss";
@import "component/_snack-bar.scss";
@import "component/_video.scss";
@import "page/_developer.scss"; @import "page/_developer.scss";
@import "page/_watch.scss"; @import "page/_watch.scss";
@import "page/_reward.scss";
@import "page/_show.scss";

View file

@ -0,0 +1,78 @@
@import "../global";
$button-focus-shift: 12%;
.button-set-item {
position: relative;
display: inline-block;
+ .button-set-item
{
margin-left: $padding-button;
}
}
.button-block, .faux-button-block
{
display: inline-block;
height: $height-button;
line-height: $height-button;
text-decoration: none;
border: 0 none;
text-align: center;
border-radius: 2px;
text-transform: uppercase;
.icon
{
top: 0em;
}
.icon:first-child
{
padding-right: 5px;
}
.icon:last-child
{
padding-left: 5px;
}
}
.button-block
{
cursor: pointer;
}
.button__content {
margin: 0 $padding-button;
}
.button-primary
{
$color-button-text: white;
color: darken($color-button-text, $button-focus-shift * 0.5);
background-color: $color-primary;
box-shadow: $default-box-shadow;
&:focus {
color: $color-button-text;
//box-shadow: $focus-box-shadow;
background-color: mix(black, $color-primary, $button-focus-shift)
}
}
.button-alt
{
background-color: $color-bg-alt;
box-shadow: $default-box-shadow;
}
.button-text
{
@include text-link();
display: inline-block;
.button__content {
margin: 0 $padding-text-link;
}
}
.button-text-help
{
@include text-link(#aaa);
font-size: 0.8em;
}

View file

@ -0,0 +1,147 @@
@import "../global";
$padding-card-horizontal: $spacing-vertical * 2/3;
.card {
margin-left: auto;
margin-right: auto;
max-width: $width-page-constrained;
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--allow-newlines {
white-space: pre-wrap;
}
.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-link-scaling: 1.1;
.card__link {
display: block;
}
.card--link:hover {
position: relative;
z-index: 1;
box-shadow: $focus-box-shadow;
transform: scale($card-link-scaling);
transform-origin: 50% 50%;
overflow-x: visible;
overflow-y: visible;
}
.card__media {
background-size: cover;
background-repeat: no-repeat;
background-position: 50% 50%;
}
$width-card-small: $spacing-vertical * 12;
$height-card-small: $spacing-vertical * 15;
.card--small {
width: $width-card-small;
overflow-x: hidden;
white-space: normal;
}
.card--small .card__media {
height: $width-card-small * 9 / 16;
}
.card__subtitle {
color: $color-help;
font-size: 0.85em;
line-height: $font-line-height * 1 / 0.85;
}
.card-series-submit
{
margin-left: auto;
margin-right: auto;
max-width: $width-page-constrained;
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--small {
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
padding-left: 20px;
margin-left: -20px; /*hacky way to give space for hover */
}
.card-row__header {
margin-bottom: $spacing-vertical / 3;
}

View file

@ -1,5 +1,5 @@
@import "../global"; @import "../global";
.channel-indicator__icon--invalid { .channel-indicator__icon--invalid {
color: #b01c2e; color: $color-error;
} }

View file

@ -1,29 +1,35 @@
@import "../global"; @import "../global";
$height-file-tile: $spacing-vertical * 8;
.file-tile__row { .file-tile__row {
height: $spacing-vertical * 7; height: $height-file-tile;
} .credit-amount {
float: right;
.file-tile__row--unavailable { }
opacity: 0.5; //Hack! Remove below!
.card__title-primary {
margin-top: $spacing-vertical * 2/3;
}
} }
.file-tile__thumbnail { .file-tile__thumbnail {
max-width: 100%; max-width: 100%;
max-height: $spacing-vertical * 7; max-height: $height-file-tile;
vertical-align: middle;
display: block; display: block;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
} }
.file-tile__thumbnail-container
{
height: $height-file-tile;
@include absolute-center();
}
.file-tile__title { .file-tile__title {
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

@ -0,0 +1,141 @@
@import "../global";
$width-input-border: 2px;
.form-row-submit
{
margin-top: $spacing-vertical;
}
.form-row__label-row {
margin-top: $spacing-vertical * 5/6;
margin-bottom: $spacing-vertical * 1/6;
line-height: 1;
font-size: 0.9 * $font-size;
}
.form-row__label-row--prefix {
float: left;
margin-right: 5px;
}
.form-field {
display: inline-block;
input[type="checkbox"],
input[type="radio"] {
cursor: pointer;
}
select {
transition: outline $transition-standard;
cursor: pointer;
box-sizing: border-box;
padding-left: 5px;
padding-right: 5px;
height: $spacing-vertical;
&:focus {
outline: $width-input-border solid $color-primary;
}
}
textarea,
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: $width-input-border solid $color-form-border;
line-height: 1px;
padding-top: $spacing-vertical * 1/3;
padding-bottom: $spacing-vertical * 1/3;
&.form-field__input--error {
border-color: $color-error;
}
&.form-field__input--inline {
padding-top: 0;
padding-bottom: 0;
border-bottom-width: 1px;
margin-left: 8px;
margin-right: 8px;
}
}
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: $width-input-border solid $color-form-border;
}
}
.form-field__label {
&[for] { cursor: pointer; }
> input[type="checkbox"], input[type="radio"] {
margin-right: 6px;
}
}
.form-field__label--error {
color: $color-error;
}
.form-field__input-text {
width: 330px;
}
.form-field__prefix {
margin-right: 4px;
}
.form-field__postfix {
margin-left: 4px;
}
.form-field__input-number {
width: 70px;
text-align: right;
}
.form-field__input-textarea {
width: 330px;
}
.form-field__error, .form-field__helper {
margin-top: $spacing-vertical * 1/3;
font-size: 0.8em;
transition: opacity $transition-standard;
}
.form-field__error {
color: $color-error;
}
.form-field__helper {
color: $color-help;
}

View file

@ -23,7 +23,7 @@
} }
.load-screen__details--warning { .load-screen__details--warning {
color: $color-warning; color: white;
} }
.load-screen__cancel-link { .load-screen__cancel-link {

View file

@ -0,0 +1,51 @@
@import "../global";
.modal-page {
position: fixed;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border: 1px solid rgb(204, 204, 204);
background: rgb(255, 255, 255);
overflow: auto;
}
.modal-page--full {
left: 0;
right: 0;
top: 0;
bottom: 0;
}
/*
.modal-page {
position: fixed;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border: 1px solid rgb(204, 204, 204);
background: rgb(255, 255, 255);
overflow: auto;
border-radius: 4px;
outline: none;
padding: 36px;
top: 25px;
left: 25px;
right: 25px;
bottom: 25px;
}
*/
.modal-page__content {
h1, h2 {
margin-bottom: $spacing-vertical / 2;
}
h3, h4 {
margin-bottom: $spacing-vertical / 4;
}
}

View file

@ -0,0 +1,81 @@
@import "../global";
.modal-overlay {
position: fixed;
display: flex;
justify-content: center;
align-items: center;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
background-color: rgba(255, 255, 255, 0.74902);
z-index: 9999;
}
.modal-overlay--clear {
background-color: transparent;
}
.modal {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border: 1px solid rgb(204, 204, 204);
background: rgb(255, 255, 255);
overflow: auto;
border-radius: 4px;
padding: $spacing-vertical;
box-shadow: $default-box-shadow;
max-width: 400px;
}
.modal__header {
margin-bottom: 5px;
text-align: center;
}
.modal__buttons {
display: flex;
flex-direction: row;
justify-content: center;
margin-top: 15px;
}
.modal__button {
margin: 0px 6px;
}
.error-modal-overlay {
background: rgba(#000, .88);
}
.error-modal__content {
display: flex;
padding: 0px 8px 10px 10px;
}
.error-modal__warning-symbol {
margin-top: 6px;
margin-right: 7px;
}
.download-started-modal__file-path {
word-break: break-all;
}
.error-modal {
max-width: none;
width: 400px;
}
.error-modal__error-list { /*shitty hack/temp fix for long errors making modals unusable*/
border: 1px solid #eee;
padding: 8px;
list-style: none;
max-height: 400px;
max-width: 400px;
overflow-y: hidden;
}

View file

@ -0,0 +1,18 @@
@import "../global";
.notice {
padding: 10px 20px;
border: 1px solid #000;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
border-radius: 5px;
color: #468847;
background-color: #dff0d8;
border-color: #d6e9c6;
}
.notice--error {
color: #b94a48;
background-color: #f2dede;
border-color: #eed3d7;
}

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;
}
}

View file

@ -0,0 +1,53 @@
video {
object-fit: contain;
box-sizing: border-box;
max-height: 100%;
max-width: 100%;
}
.video {
background: #000;
color: white;
}
.video-embedded {
max-width: $width-page-constrained;
max-height: $height-video-embedded;
height: $height-video-embedded;
video {
height: 100%;
}
&.video--hidden {
height: $height-video-embedded;
}
&.video--active {
/*background: none;*/
}
}
.video__cover {
text-align: center;
height: 100%;
width: 100%;
background-size: auto 100%;
background-position: center center;
background-repeat: no-repeat;
position: relative;
&:hover {
.video__play-button { @include absolute-center(); }
}
}
.video__play-button {
position: absolute;
width: 100%;
height: 100%;
cursor: pointer;
display: none;
font-size: $spacing-vertical * 3;
color: white;
z-index: 1;
background: $color-black-transparent;
left: 0;
top: 0;
}

View file

@ -0,0 +1,5 @@
@import "../global";
.reward-page__details {
background-color: lighten($color-canvas, 1.5%);
}

9
ui/scss/page/_show.scss Normal file
View file

@ -0,0 +1,9 @@
@import "../global";
.show-page-media {
text-align: center;
margin-bottom: $spacing-vertical;
img {
max-width: 100%;
}
}

View file

@ -1,6 +1,3 @@
.video {
background: #000;
}
.video__overlay { .video__overlay {
position: absolute; position: absolute;