diff --git a/.gitignore b/.gitignore index ccfec0983..d1b68b9b7 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ dist /app/node_modules /build/venv /lbry-app-venv +/lbry-venv /daemon/build /daemon/venv /daemon/requirements.txt diff --git a/app/main.js b/app/main.js index 23239145b..4b9a4664f 100644 --- a/app/main.js +++ b/app/main.js @@ -62,9 +62,9 @@ function getPidsForProcessName(name) { } function createWindow () { - win = new BrowserWindow({backgroundColor: '#155B4A'}) //$color-primary + win = new BrowserWindow({backgroundColor: '#155B4A', minWidth: 800, minHeight: 600 }) //$color-primary win.maximize() - //win.webContents.openDevTools() + win.webContents.openDevTools(); win.loadURL(`file://${__dirname}/dist/index.html`) win.on('closed', () => { win = null diff --git a/doitagain.sh b/doitagain.sh new file mode 100755 index 000000000..37564e1dd --- /dev/null +++ b/doitagain.sh @@ -0,0 +1,4 @@ +#!/bin/bash +rm -rf ~/.lbrynet/ +rm -rf ~/.lbryum/ +./node_modules/.bin/electron app diff --git a/lbry b/lbry index e8bccec71..043e2d0ab 160000 --- a/lbry +++ b/lbry @@ -1 +1 @@ -Subproject commit e8bccec71c7424bf06d057904e4722d2d734fa3f +Subproject commit 043e2d0ab96030468d53d02e311fd848f35c2dc1 diff --git a/lbryum b/lbryum index 39ace3737..121bda396 160000 --- a/lbryum +++ b/lbryum @@ -1 +1 @@ -Subproject commit 39ace3737509ff2b09fabaaa64d1525843de1325 +Subproject commit 121bda3963ee94f0c9c027813c55b71b38219739 diff --git a/ui/js/app.js b/ui/js/app.js index 7e8559be7..9cfb43137 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -7,13 +7,12 @@ import HelpPage from './page/help.js'; import WatchPage from './page/watch.js'; import ReportPage from './page/report.js'; import StartPage from './page/start.js'; -import ClaimCodePage from './page/claim_code.js'; -import ReferralPage from './page/referral.js'; +import RewardsPage from './page/rewards.js'; +import RewardPage from './page/reward.js'; import WalletPage from './page/wallet.js'; import ShowPage from './page/show.js'; import PublishPage from './page/publish.js'; import DiscoverPage from './page/discover.js'; -import SplashScreen from './component/splash.js'; import DeveloperPage from './page/developer.js'; import {FileListDownloaded, FileListPublished} from './page/file-list.js'; import Drawer from './component/drawer.js'; @@ -38,17 +37,11 @@ var App = React.createClass({ message: 'Error message', data: 'Error data', }, + _fullScreenPages: ['watch'], _upgradeDownloadItem: null, _isMounted: false, _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() { switch (process.platform) { case 'darwin': @@ -87,7 +80,7 @@ var App = React.createClass({ var match, param, val, viewingPage, pageArgs, 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, errorInfo: null, modal: null, @@ -112,6 +105,8 @@ var App = React.createClass({ if (target.matches('a[href^="?"]')) { event.preventDefault(); if (this._isMounted) { + history.pushState({}, document.title, target.getAttribute('href')); + this.registerHistoryPop(); this.setState(this.getViewingPageAndArgs(target.getAttribute('href'))); } } @@ -153,6 +148,11 @@ var App = React.createClass({ componentWillUnmount: function() { this._isMounted = false; }, + registerHistoryPop: function() { + window.addEventListener("popstate", function() { + this.setState(this.getViewingPageAndArgs(location.pathname)); + }.bind(this)); + }, handleUpgradeClicked: function() { // 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); @@ -231,14 +231,12 @@ var App = React.createClass({ case 'wallet': case 'send': case 'receive': - case 'claim': - case 'referral': + case 'rewards': return { - '?wallet' : 'Overview', - '?send' : 'Send', - '?receive' : 'Receive', - '?claim' : 'Claim Beta Code', - '?referral' : 'Check Referral Credit', + '?wallet': 'Overview', + '?send': 'Send', + '?receive': 'Receive', + '?rewards': 'Rewards', }; case 'downloaded': case 'published': @@ -258,8 +256,6 @@ var App = React.createClass({ return ; case 'help': return ; - case 'watch': - return ; case 'report': return ; case 'downloaded': @@ -268,10 +264,8 @@ var App = React.createClass({ return ; case 'start': return ; - case 'claim': - return ; - case 'referral': - return ; + case 'rewards': + return ; case 'wallet': case 'send': case 'receive': @@ -284,16 +278,16 @@ var App = React.createClass({ return ; case 'discover': default: - return ; + return ; } }, render: function() { var mainContent = this.getMainContent(), headerLinks = this.getHeaderLinks(), searchQuery = this.state.viewingPage == 'discover' && this.state.pageArgs ? this.state.pageArgs : ''; - + return ( - this.state.viewingPage == 'watch' ? + this._fullScreenPages.includes(this.state.viewingPage) ? mainContent :
diff --git a/ui/js/component/auth.js b/ui/js/component/auth.js new file mode 100644 index 000000000..197fc0b38 --- /dev/null +++ b/ui/js/component/auth.js @@ -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 ( +
+
+ { this._emailRow = ref }} type="text" label="Email" placeholder="admin@toplbryfan.com" + name="email" value={this.state.email} + onChange={this.handleEmailChanged} /> +
+ +
+ +
+ ); + } +}); + +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 ( +
+
+ { 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."/> +
+ +
+ +
+ ); + } +}); + +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 ? + +
+

Welcome to LBRY.

+

Using LBRY is like dating a centaur. Totally normal up top, and way different underneath.

+

On the upper level, LBRY is like other popular video and media sites.

+

Below, LBRY is controlled by its users -- you -- through the power of blockchain and decentralization.

+

Thanks for making it possible! Here's a nickel, kid.

+
+ +
+
+
: + +
+

About Your Reward

+

You earned a reward of LBRY credits, or LBC.

+

This reward will show in your Wallet momentarily, probably while you are reading this message.

+

LBC is used to compensate creators, to publish, and to have say in how the network works.

+

No need to understand it all just yet! Try watching or downloading something next.

+
+
+ ); + } +}); + + +const ErrorStage = React.createClass({ + render: function() { + return ( +
+

An error was encountered that we cannot continue from.

+

At least we're earning the name beta.

+ { window.location.reload() } } /> +
+ ); + } +}); + +const PendingStage = React.createClass({ + render: function() { + return ( +
+

Preparing for first access

+
+ ); + } +}); + +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" ? + +

LBRY Early Access

+ +
: + + ); + } +}); \ No newline at end of file diff --git a/ui/js/component/channel-indicator.js b/ui/js/component/channel-indicator.js index 37897cbcd..2f30d5755 100644 --- a/ui/js/component/channel-indicator.js +++ b/ui/js/component/channel-indicator.js @@ -3,20 +3,18 @@ import lbry from '../lbry.js'; import uri from '../uri.js'; import {Icon} from './common.js'; -const ChannelIndicator = React.createClass({ +const UriIndicator = React.createClass({ propTypes: { uri: React.PropTypes.string.isRequired, hasSignature: React.PropTypes.bool.isRequired, signatureIsValid: React.PropTypes.bool, }, render: function() { - if (!this.props.hasSignature) { - return null; - } const uriObj = uri.parseLbryUri(this.props.uri); - if (!uriObj.isChannel) { - return null; + + if (!this.props.hasSignature || !uriObj.isChannel) { + return Anonymous; } const channelUriObj = Object.assign({}, uriObj); @@ -25,7 +23,6 @@ const ChannelIndicator = React.createClass({ let icon, modifier; if (this.props.signatureIsValid) { - icon = 'icon-check-circle'; modifier = 'valid'; } else { icon = 'icon-times-circle'; @@ -33,11 +30,13 @@ const ChannelIndicator = React.createClass({ } return ( - by {channelUri} {' '} - + {channelUri} {' '} + { !this.props.signatureIsValid ? + : + '' } ); } }); -export default ChannelIndicator; \ No newline at end of file +export default UriIndicator; \ No newline at end of file diff --git a/ui/js/component/common.js b/ui/js/component/common.js index 9e163476c..d8b0fc052 100644 --- a/ui/js/component/common.js +++ b/ui/js/component/common.js @@ -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({ render: function() { return LBC; } }); export let CreditAmount = React.createClass({ propTypes: { - amount: React.PropTypes.number, - precision: React.PropTypes.number + amount: React.PropTypes.number.isRequired, + precision: React.PropTypes.number, + label: React.PropTypes.bool + }, + getDefaultProps: function() { + return { + precision: 1, + label: true, + } }, 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 ( - {formattedAmount} {parseFloat(formattedAmount) == 1.0 ? 'credit' : 'credits'} - { this.props.isEstimate ? (est) : null } + + {formattedAmount} + {this.props.label ? + (parseFloat(formattedAmount) == 1.0 ? ' credit' : ' credits') : '' } + + { this.props.isEstimate ? * : null } ); } }); +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 free*; + } else { + if (this.props.metadata.fee.currency === "LBC") { + return + } else if (this.props.metadata.fee.currency === "USD") { + return ???; + } + } + } + return ( + this.state.cost !== null ? + : + ??? + ); + } +}); + var addressStyle = { fontFamily: '"Consolas", "Lucida Console", "Adobe Source Code Pro", monospace', }; @@ -131,6 +186,9 @@ export let Thumbnail = React.createClass({ this._isMounted = false; }, render: function() { - return + const className = this.props.className ? this.props.className : '', + otherProps = Object.assign({}, this.props) + delete otherProps.className; + return }, }); diff --git a/ui/js/component/drawer.js b/ui/js/component/drawer.js index eaf11506b..e719af073 100644 --- a/ui/js/component/drawer.js +++ b/ui/js/component/drawer.js @@ -55,7 +55,7 @@ var Drawer = React.createClass({ - + diff --git a/ui/js/component/file-actions.js b/ui/js/component/file-actions.js index 1a535099a..fff8191e2 100644 --- a/ui/js/component/file-actions.js +++ b/ui/js/component/file-actions.js @@ -2,66 +2,13 @@ import React from 'react'; import lbry from '../lbry.js'; import {Link} from '../component/link.js'; import {Icon} from '../component/common.js'; -import Modal from './modal.js'; -import FormField from './form.js'; +import {Modal} from './modal.js'; +import {FormField} from './form.js'; import {ToolTip} from '../component/tooltip.js'; import {DropDownMenu, DropDownMenuItem} from './menu.js'; 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 ( -
- - - You don't have enough LBRY credits to pay for this stream. - -
- ); - } -}); - let FileActionsRow = React.createClass({ _isMounted: false, _fileInfoSubscribeId: null, @@ -79,7 +26,7 @@ let FileActionsRow = React.createClass({ menuOpen: false, deleteChecked: false, attemptingDownload: false, - attemptingRemove: false + attemptingRemove: false, } }, onFileInfoUpdate: function(fileInfo) { @@ -95,14 +42,14 @@ let FileActionsRow = React.createClass({ attemptingDownload: true, attemptingRemove: false }); - lbry.getCostInfo(this.props.uri, ({cost}) => { + lbry.getCostInfo(this.props.uri).then(({cost}) => { lbry.getBalance((balance) => { if (cost > balance) { this.setState({ modal: 'notEnoughCredits', attemptingDownload: false, }); - } else { + } else if (this.state.affirmedPurchase) { lbry.get({uri: this.props.uri}).then((streamInfo) => { if (streamInfo === null || typeof streamInfo !== 'object') { 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 }); }, + onAffirmPurchase: function() { + this.setState({ + affirmedPurchase: true, + modal: null + }); + this.tryDownload(); + }, openMenu: function() { this.setState({ menuOpen: !this.state.menuOpen, @@ -198,9 +157,6 @@ let FileActionsRow = React.createClass({ return (
- {this.props.contentType && this.props.contentType.startsWith('video/') - ? - : null} {this.state.fileInfo !== null || this.state.fileInfo.isMine ? linkBlock : null} @@ -209,6 +165,10 @@ let FileActionsRow = React.createClass({ : '' } + + Confirm you want to purchase this bro. + You don't have enough LBRY credits to pay for this stream. @@ -220,7 +180,7 @@ let FileActionsRow = React.createClass({ -

Are you sure you'd like to remove {this.props.metadata.title} from LBRY?

+

Are you sure you'd like to remove {this.props.metadata ? this.props.metadata.title : this.props.uri} from LBRY?

@@ -261,6 +221,7 @@ export let FileActions = React.createClass({ componentDidMount: function() { this._isMounted = true; this._fileInfoSubscribeId = lbry.fileInfoSubscribe(this.props.outpoint, this.onFileInfoUpdate); + lbry.get_availability({uri: this.props.uri}, (availability) => { if (this._isMounted) { this.setState({ @@ -294,7 +255,7 @@ export let FileActions = React.createClass({ ? :
-
This file is not currently available.
+
Content unavailable.
diff --git a/ui/js/component/file-tile.js b/ui/js/component/file-tile.js index b65434bac..b2477a7be 100644 --- a/ui/js/component/file-tile.js +++ b/ui/js/component/file-tile.js @@ -3,56 +3,8 @@ import lbry from '../lbry.js'; import uri from '../uri.js'; import {Link} from '../component/link.js'; import {FileActions} from '../component/file-actions.js'; -import {Thumbnail, TruncatedText, CreditAmount} from '../component/common.js'; -import ChannelIndicator 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 ( - - - - ); - } -}); +import {Thumbnail, TruncatedText, FilePrice} from '../component/common.js'; +import UriIndicator from '../component/channel-indicator.js'; /*should be merged into FileTile once FileTile is refactored to take a single id*/ export let FileTileStream = React.createClass({ @@ -61,7 +13,7 @@ export let FileTileStream = React.createClass({ propTypes: { uri: React.PropTypes.string, - metadata: React.PropTypes.object.isRequired, + metadata: React.PropTypes.object, contentType: React.PropTypes.string.isRequired, outpoint: React.PropTypes.string, hasSignature: React.PropTypes.bool, @@ -117,47 +69,47 @@ export let FileTileStream = React.createClass({ } }, render: function() { - console.log('rendering.') if (this.state.isHidden) { - console.log('hidden, so returning null') return null; } - console.log("inside FileTileStream. metadata is", this.props.metadata) - const lbryUri = uri.normalizeLbryUri(this.props.uri); const metadata = this.props.metadata; - const isConfirmed = typeof metadata == 'object'; + const isConfirmed = !!metadata; const title = isConfirmed ? metadata.title : lbryUri; const obscureNsfw = this.props.obscureNsfw && isConfirmed && metadata.nsfw; return ( -
-
-
- +
+
+
+
- { !this.props.hidePrice - ? - : null} - -

- - - {title} + +
+ +
+
+

+ + {isConfirmed + ? metadata.description + : This file is pending confirmation.} - -

- - -

- - {isConfirmed - ? metadata.description - : This file is pending confirmation.} - -

+

+
{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 ( +
+
+ +
+
{title}
+
+ { !this.props.hidePrice ? : null} + +
+
+
+
+ + {isConfirmed + ? metadata.description + : This file is pending confirmation.} + +
+
+ {this.state.showNsfwHelp && this.state.hovered + ?
+

+ This content is Not Safe For Work. + To view adult content, please change your . +

+
+ : null} +
+
+ ); + } +}); + export let FileTile = React.createClass({ _isMounted: false, @@ -191,11 +245,12 @@ export let FileTile = React.createClass({ componentDidMount: function() { this._isMounted = true; - lbry.resolve({uri: this.props.uri}).then(({claim: claimInfo}) => { - if (this._isMounted && claimInfo.value.stream.metadata) { + lbry.resolve({uri: this.props.uri}).then((resolutionInfo) => { + 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 this.setState({ - claimInfo: claimInfo, + claimInfo: resolutionInfo.claim, }); } }); @@ -210,7 +265,11 @@ export let FileTile = React.createClass({ const {txid, nout, has_signature, signature_is_valid, value: {stream: {metadata, source: {contentType}}}} = this.state.claimInfo; - return ; + + return this.props.displayStyle == 'card' ? + : + ; } }); diff --git a/ui/js/component/form.js b/ui/js/component/form.js index 33e4aee66..f75310c92 100644 --- a/ui/js/component/form.js +++ b/ui/js/component/form.js @@ -1,28 +1,32 @@ import React from 'react'; import {Icon} from './common.js'; -var requiredFieldWarningStyle = { - color: '#cc0000', - transition: 'opacity 400ms ease-in', -}; +var formFieldCounter = 0, + formFieldNestedLabelTypes = ['radio', 'checkbox']; -var FormField = React.createClass({ +function formFieldId() { + return "form-field-" + (++formFieldCounter); +} + +export let FormField = React.createClass({ _fieldRequiredText: 'This field is required', _type: null, _element: null, propTypes: { type: React.PropTypes.string.isRequired, - hidden: React.PropTypes.bool, + prefix: React.PropTypes.string, + postfix: React.PropTypes.string, + hasError: React.PropTypes.bool }, getInitialState: function() { return { - adviceState: 'hidden', - adviceText: null, + isError: null, + errorMessage: null, } }, 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._type = this.props.type; } else if (this.props.type == 'text-number') { @@ -33,25 +37,11 @@ var FormField = React.createClass({ this._element = this.props.type; } }, - showAdvice: function(text) { + showError: function(text) { this.setState({ - adviceState: 'shown', - adviceText: text, + isError: true, + errorMessage: text, }); - - setTimeout(() => { - this.setState({ - adviceState: 'fading', - }); - setTimeout(() => { - this.setState({ - adviceState: 'hidden', - }); - }, 450); - }, 5000); - }, - warnRequired: function() { - this.showAdvice(this._fieldRequiredText); }, focus: function() { this.refs.field.focus(); @@ -60,7 +50,8 @@ var FormField = React.createClass({ if (this.props.type == 'checkbox') { return this.refs.field.checked; } 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 { return this.refs.field.value; } @@ -70,45 +61,94 @@ var FormField = React.createClass({ }, render: function() { // 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.hidden; + delete otherProps.label; + delete otherProps.hasError; + delete otherProps.className; + delete otherProps.postfix; + delete otherProps.prefix; - return ( - !this.props.hidden - ?
- - {this.props.children} - - {this.state.adviceText} -
- : null - ); + const element = + {this.props.children} + ; + + return
+ { this.props.prefix ? {this.props.prefix} : '' } + { renderElementInsideLabel ? + : + element } + { this.props.postfix ? {this.props.postfix} : '' } + { isError && this.state.errorMessage ?
{this.state.errorMessage}
: '' } +
} -}); +}) -var FormFieldAdvice = React.createClass({ +export let FormRow = React.createClass({ + _fieldRequiredText: 'This field is required', 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() { - return ( - this.props.state != 'hidden' - ?
-
- -
- - {this.props.children} - -
-
-
- : null - ); - } -}); + const fieldProps = Object.assign({}, this.props), + elementId = formFieldId(), + renderLabelInFormField = formFieldNestedLabelTypes.includes(this.props.type); -export default FormField; + if (!renderLabelInFormField) { + delete fieldProps.label; + } + delete fieldProps.helper; + + return
+ { this.props.label && !renderLabelInFormField ? +
+ +
: '' } + + { !this.state.isError && this.props.helper ?
{this.props.helper}
: '' } + { this.state.isError ?
{this.state.errorMessage}
: '' } +
+ } +}) diff --git a/ui/js/component/header.js b/ui/js/component/header.js index fb64e9ece..463042cff 100644 --- a/ui/js/component/header.js +++ b/ui/js/component/header.js @@ -1,5 +1,6 @@ import React from 'react'; import {Link} from './link.js'; +import {Icon} from './common.js'; var Header = React.createClass({ getInitialState: function() { @@ -52,6 +53,7 @@ var Header = React.createClass({

{ this.state.title }

+
diff --git a/ui/js/component/link.js b/ui/js/component/link.js index 8a4d76f76..55c0060dd 100644 --- a/ui/js/component/link.js +++ b/ui/js/component/link.js @@ -1,5 +1,7 @@ import React from 'react'; import {Icon} from './common.js'; +import Modal from '../component/modal.js'; +import rewards from '../rewards.js'; export let Link = React.createClass({ propTypes: { @@ -52,4 +54,80 @@ export let Link = React.createClass({ ); } +}); + +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 ( +
+ {this.props.claimed + ? Reward claimed. + : } + {this.state.errorMessage ? + + {this.state.errorMessage} + + : ''} +
+ ); + } }); \ No newline at end of file diff --git a/ui/js/component/modal-page.js b/ui/js/component/modal-page.js new file mode 100644 index 000000000..12826a81e --- /dev/null +++ b/ui/js/component/modal-page.js @@ -0,0 +1,18 @@ +import React from 'react'; +import ReactModal from 'react-modal'; + +export const ModalPage = React.createClass({ + render: function() { + return ( + +
+ {this.props.children} +
+
+ ); + } +}); + +export default ModalPage; \ No newline at end of file diff --git a/ui/js/component/modal.js b/ui/js/component/modal.js index bd534ecea..dbb8ff646 100644 --- a/ui/js/component/modal.js +++ b/ui/js/component/modal.js @@ -6,6 +6,7 @@ import {Link} from './link.js'; export const Modal = React.createClass({ propTypes: { type: React.PropTypes.oneOf(['alert', 'confirm', 'custom']), + overlay: React.PropTypes.bool, onConfirmed: React.PropTypes.func, onAborted: React.PropTypes.func, confirmButtonLabel: React.PropTypes.string, @@ -16,6 +17,7 @@ export const Modal = React.createClass({ getDefaultProps: function() { return { type: 'alert', + overlay: true, confirmButtonLabel: 'OK', abortButtonLabel: 'Cancel', confirmButtonDisabled: false, @@ -26,7 +28,7 @@ export const Modal = React.createClass({ return ( + overlayClassName={[null, undefined, ""].indexOf(this.props.overlayClassName) === -1 ? this.props.overlayClassName : 'modal-overlay'}>
{this.props.children}
diff --git a/ui/js/component/notice.js b/ui/js/component/notice.js new file mode 100644 index 000000000..068b545b5 --- /dev/null +++ b/ui/js/component/notice.js @@ -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 ( +
+ {this.props.children} +
+ ); + }, +}); + +export default Notice; \ No newline at end of file diff --git a/ui/js/component/snack-bar.js b/ui/js/component/snack-bar.js new file mode 100644 index 000000000..a993c3b75 --- /dev/null +++ b/ui/js/component/snack-bar.js @@ -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 ( +
+ {snack.message} + {snack.linkText && snack.linkTarget ? + {snack.linkText} : ''} +
+ ); + }, +}); + +export default SnackBar; \ No newline at end of file diff --git a/ui/js/component/splash.js b/ui/js/component/splash.js index 8de7e1bbf..a156718b4 100644 --- a/ui/js/component/splash.js +++ b/ui/js/component/splash.js @@ -13,11 +13,12 @@ var SplashScreen = React.createClass({ isLagging: false, } }, - updateStatus: function(was_lagging=false) { - lbry.getDaemonStatus(this._updateStatusCallback); + updateStatus: function() { + lbry.status().then(this._updateStatusCallback); }, _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 // that we are done. // TODO: This is a hack, and the logic should live in the daemon @@ -34,20 +35,28 @@ var SplashScreen = React.createClass({ return; } this.setState({ - details: status.message + (status.is_lagging ? '' : '...'), - isLagging: status.is_lagging, + details: startupStatus.message + (startupStatus.is_lagging ? '' : '...'), + isLagging: startupStatus.is_lagging, }); setTimeout(() => { - this.updateStatus(status.is_lagging); + this.updateStatus(); }, 500); }, componentDidMount: function() { - lbry.connect((connected) => { - this.updateStatus(); - }); + lbry.connect().then((isConnected) => { + 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() { - return ; + return } }); diff --git a/ui/js/lbry.js b/ui/js/lbry.js index f2f201de6..9e8b1565e 100644 --- a/ui/js/lbry.js +++ b/ui/js/lbry.js @@ -1,7 +1,7 @@ import lighthouse from './lighthouse.js'; import jsonrpc from './jsonrpc.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 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 * 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 newPendingPublish = { - claim_id: 'pending_claim_' + name, - txid: 'pending_' + name, + name, channel_name, + claim_id: 'pending_claim_' + lbryUri, + txid: 'pending_' + lbryUri, nout: 0, - outpoint: 'pending_' + name + ':0', - name: name, + outpoint: 'pending_' + lbryUri + ':0', time: Date.now(), }; setLocal('pendingPublishes', [...pendingPublishes, 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() { const pendingPublishes = getLocal('pendingPublishes') || []; - - const newPendingPublishes = []; - for (let pendingPublish of pendingPublishes) { - if (Date.now() - pendingPublish.time <= lbry.pendingPublishTimeout) { - newPendingPublishes.push(pendingPublish); - } - } + const newPendingPublishes = pendingPublishes.filter(pub => Date.now() - pub.time <= lbry.pendingPublishTimeout); 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 - * but it has timed out), returns null. + * Gets a pending publish attempt by its name or (fake) outpoint. A channel name can also be + * 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 pendingPublishIndex = pendingPublishes.findIndex( - ({name: itemName, outpoint: itemOutpoint}) => itemName == name || itemOutpoint == outpoint - ); - 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; - } + return pendingPublishes.find( + pub => pub.outpoint === outpoint || (pub.name === name && (!channel_name || pub.channel_name === channel_name)) + ) || null; } -function pendingPublishToDummyClaim({name, outpoint, claim_id, txid, nout}) { - return { - name: name, - outpoint: outpoint, - claim_id: claim_id, - txid: txid, - nout: nout, - }; +function pendingPublishToDummyClaim({channel_name, name, outpoint, claim_id, txid, nout}) { + return {name, outpoint, claim_id, txid, nout, channel_name}; } function pendingPublishToDummyFileInfo({name, outpoint, claim_id}) { - return { - name: name, - outpoint: outpoint, - claim_id: claim_id, - metadata: "Attempting publication", - }; + return {name, outpoint, claim_id, metadata: null}; } - +window.pptdfi = pendingPublishToDummyFileInfo; let lbry = { isConnected: false, @@ -116,30 +98,37 @@ lbry.call = function (method, params, callback, errorCallback, connectFailedCall jsonrpc.call(lbry.daemonConnectionString, method, [params], callback, errorCallback, connectFailedCallback); } - //core -lbry.connect = function(callback) -{ - // Check every half second to see if the daemon is accepting connections - // Once this returns True, can call getDaemonStatus to see where - // we are in the startup process - function checkDaemonStarted(tryNum=0) { - lbry.isDaemonAcceptingConnections(function (runningStatus) { - if (runningStatus) { - lbry.isConnected = true; - callback(true); - } else { - if (tryNum <= 600) { // Move # of tries into constant or config option - setTimeout(function () { - checkDaemonStarted(tryNum + 1); - }, 500); - } else { - callback(false); - } +lbry._connectPromise = null; +lbry.connect = function() { + if (lbry._connectPromise === null) { + + lbry._connectPromise = new Promise((resolve, reject) => { + + // Check every half second to see if the daemon is accepting connections + function checkDaemonStarted(tryNum = 0) { + lbry.isDaemonAcceptingConnections(function (runningStatus) { + if (runningStatus) { + resolve(true); + } + else { + if (tryNum <= 600) { // Move # of tries into constant or config option + setTimeout(function () { + checkDaemonStarted(tryNum + 1); + }, tryNum < 100 ? 200 : 1000); + } + else { + reject(new Error("Unable to connect to LBRY")); + } + } + }); } + + checkDaemonStarted(); }); } - checkDaemonStarted(); + + return lbry._connectPromise; } lbry.isDaemonAcceptingConnections = function (callback) { @@ -147,10 +136,6 @@ lbry.isDaemonAcceptingConnections = function (callback) { lbry.call('status', {}, () => callback(true), null, () => callback(false)) }; -lbry.getDaemonStatus = function (callback) { - lbry.call('daemon_status', {}, callback); -}; - lbry.checkFirstRun = function(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.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) { if (!name) { 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 - * Lighthouse. If Lighthouse can't be reached, it just retrives the - * key fee. - * - * Returns an object with members: - * - cost: Number; the calculated cost of the name - * - includes_data: Boolean; indicates whether or not the data fee info - * from Lighthouse is included. - */ - if (!name) { - throw new Error(`Name required.`); - } +/** + * Takes a LBRY URI; will first try and calculate a total cost using + * Lighthouse. If Lighthouse can't be reached, it just retrives the + * key fee. + * + * Returns an object with members: + * - cost: Number; the calculated cost of the name + * - includes_data: Boolean; indicates whether or not the data fee info + * from Lighthouse is included. + */ +lbry.costPromiseCache = {} +lbry.getCostInfo = function(lbryUri) { + 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) { - lbry.stream_cost_estimate({name, size}).then((cost) => { - 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.')); + if (!lbryUri) { + reject(new Error(`URI required.`)); } - }; - 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) { @@ -365,12 +321,13 @@ lbry.publish = function(params, fileListedCallback, publishedCallback, errorCall returnedPending = true; if (publishedCallback) { - savePendingPublish(params.name); + savePendingPublish({name: params.name, channel_name: params.channel_name}); publishedCallback(true); } if (fileListedCallback) { - savePendingPublish(params.name); + const {name, channel_name} = params; + savePendingPublish({name: params.name, channel_name: params.channel_name}); fileListedCallback(true); } }, 2000); @@ -430,6 +387,10 @@ lbry.getClientSettings = function() { lbry.getClientSetting = function(setting) { var localStorageVal = localStorage.getItem('setting_' + setting); + if (setting == 'showDeveloperMenu') + { + return true; + } return (localStorageVal === null ? lbry.defaultClientSettings[setting] : JSON.parse(localStorageVal)); } @@ -522,7 +483,7 @@ lbry.stop = function(callback) { lbry.fileInfo = {}; lbry._subscribeIdCount = 0; lbry._fileInfoSubscribeCallbacks = {}; -lbry._fileInfoSubscribeInterval = 5000; +lbry._fileInfoSubscribeInterval = 500000; lbry._balanceSubscribeCallbacks = {}; lbry._balanceSubscribeInterval = 5000; lbry._removedFiles = []; @@ -534,6 +495,7 @@ lbry._updateClaimOwnershipCache = function(claimId) { return match || claimInfo.claim_id == claimId; }); }); + }; lbry._updateFileInfoSubscribers = function(outpoint) { @@ -629,14 +591,14 @@ lbry.showMenuIfNeeded = function() { */ lbry.file_list = function(params={}) { 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. * Pending publishes use their own faux outpoints that are always unique, so we don't need * to check if there's a real file. */ - if (outpoint !== undefined) { + if (outpoint) { const pendingPublish = getPendingPublish({outpoint}); if (pendingPublish) { resolve([pendingPublishToDummyFileInfo(pendingPublish)]); @@ -645,14 +607,8 @@ lbry.file_list = function(params={}) { } 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); resolve([...fileInfos, ...dummyFileInfos]); }, reject, reject); @@ -662,16 +618,43 @@ lbry.file_list = function(params={}) { lbry.claim_list_mine = function(params={}) { return new Promise((resolve, reject) => { lbry.call('claim_list_mine', params, (claims) => { - // Filter out pending publishes when the name is already in the file manager - const dummyClaims = getPendingPublishes().filter( - (pub) => !claims.find(({name}) => name == pub.name) - ).map(pendingPublishToDummyClaim); + for (let {name, channel_name, txid, nout} of claims) { + removePendingPublishIfNeeded({name, channel_name, outpoint: txid + ':' + nout}); + } + const dummyClaims = getPendingPublishes().map(pendingPublishToDummyClaim); 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, { get: function(target, name) { if (name in target) { diff --git a/ui/js/lbryio.js b/ui/js/lbryio.js new file mode 100644 index 000000000..d662354e6 --- /dev/null +++ b/ui/js/lbryio.js @@ -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; diff --git a/ui/js/lighthouse.js b/ui/js/lighthouse.js index a8b60f0fa..faa5b5b67 100644 --- a/ui/js/lighthouse.js +++ b/ui/js/lighthouse.js @@ -1,8 +1,8 @@ import lbry from './lbry.js'; import jsonrpc from './jsonrpc.js'; -const queryTimeout = 5000; -const maxQueryTries = 5; +const queryTimeout = 3000; +const maxQueryTries = 2; const defaultServers = [ 'http://lighthouse4.lbry.io:50005', 'http://lighthouse5.lbry.io:50005', @@ -20,12 +20,9 @@ function getServers() { } function call(method, params, callback, errorCallback) { - if (connectTryNum > maxQueryTries) { - if (connectFailedCallback) { - connectFailedCallback(); - } else { - throw new Error(`Could not connect to Lighthouse server. Last server attempted: ${server}`); - } + if (connectTryNum >= maxQueryTries) { + errorCallback(new Error(`Could not connect to Lighthouse server. Last server attempted: ${server}`)); + return; } /** @@ -48,7 +45,7 @@ function call(method, params, callback, errorCallback) { }, () => { connectTryNum++; call(method, params, callback, errorCallback); - }); + }, queryTimeout); } const lighthouse = new Proxy({}, { diff --git a/ui/js/main.js b/ui/js/main.js index f00c49a69..610ca8594 100644 --- a/ui/js/main.js +++ b/ui/js/main.js @@ -1,9 +1,12 @@ import React from 'react'; import ReactDOM from 'react-dom'; import lbry from './lbry.js'; +import lbryio from './lbryio.js'; import lighthouse from './lighthouse.js'; import App from './app.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 contextMenu = remote.require('./menu/context-menu'); @@ -16,31 +19,24 @@ window.addEventListener('contextmenu', (event) => { event.preventDefault(); }); -var init = function() { +let init = function() { window.lbry = lbry; 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(
{ lbryio.enabled ? : '' }
, canvas) + } - var canvas = document.getElementById('canvas'); if (window.sessionStorage.getItem('loaded') == 'y') { - ReactDOM.render(, canvas) + onDaemonReady(); } else { - ReactDOM.render( - { - if (balance <= 0) { - window.location.href = '?claim'; - } else { - ReactDOM.render(, canvas); - } - }); - } else { - ReactDOM.render(, canvas); - } - }}/>, - canvas - ); + ReactDOM.render(, canvas); } }; diff --git a/ui/js/page/claim_code.js b/ui/js/page/claim_code.js deleted file mode 100644 index 7a9976824..000000000 --- a/ui/js/page/claim_code.js +++ /dev/null @@ -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 ( -
-
-
-

Claim your beta invitation code

-
-

Thanks for beta testing LBRY! Enter your invitation code and email address below to receive your initial - LBRY credits.

-

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.

-
-
-
-
-
-
- - - -
-
-
- - Please enter an invitation code or choose "Skip." - - - Please enter an email address or choose "Skip." - - - {this.state.failureReason} - - - 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.')} - - - Welcome to LBRY! You can visit the Wallet page to redeem an invite code at any time. - - -

LBRY couldn't connect to our servers to confirm your invitation code. Please check your internet connection.

- If you continue to have problems, you can still browse LBRY and visit the Settings page to redeem your code later. -
-
- ); - } -}); - -export default ClaimCodePage; diff --git a/ui/js/page/developer.js b/ui/js/page/developer.js index 93eb1cc11..377204852 100644 --- a/ui/js/page/developer.js +++ b/ui/js/page/developer.js @@ -1,6 +1,6 @@ import lbry from '../lbry.js'; import React from 'react'; -import FormField from '../component/form.js'; +import {FormField} from '../component/form.js'; import {Link} from '../component/link.js'; const fs = require('fs'); diff --git a/ui/js/page/discover.js b/ui/js/page/discover.js index 678310338..8aadd2df4 100644 --- a/ui/js/page/discover.js +++ b/ui/js/page/discover.js @@ -1,5 +1,6 @@ import React from 'react'; import lbry from '../lbry.js'; +import lbryio from '../lbryio.js'; import lighthouse from '../lighthouse.js'; import {FileTile} from '../component/file-tile.js'; import {Link} from '../component/link.js'; @@ -58,46 +59,59 @@ var SearchResults = React.createClass({ } }); -var featuredContentLegendStyle = { - fontSize: '12px', - color: '#aaa', - verticalAlign: '15%', -}; +const communityCategoryToolTipText = ('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!'); + +var FeaturedCategory = React.createClass({ + render: function() { + return (
+ { this.props.category ? +

{this.props.category} + { this.props.category == "community" ? + + : '' }

+ : '' } + { this.props.names.map((name) => { return }) } +
) + } +}) var FeaturedContent = React.createClass({ getInitialState: function() { return { - featuredNames: [], + featuredUris: {}, + failed: false }; }, componentWillMount: function() { - lbry.getFeaturedDiscoverNames().then((featuredNames) => { - this.setState({ featuredNames: featuredNames }); + lbryio.call('discover', 'list', { version: "early-access" } ).then(({Categories, Uris}) => { + 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() { - 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 ( -
-
-

Featured Content

- { this.state.featuredNames.map(name => ) } + this.state.failed ? +
Failed to load landing content.
: +
+ { + Object.keys(this.state.featuredUris).map(function(category) { + return this.state.featuredUris[category].length ? + : + ''; + }.bind(this)) + }
-
-

- Community Content - -

- - - - - -
-
); } }); @@ -105,6 +119,10 @@ var FeaturedContent = React.createClass({ var DiscoverPage = React.createClass({ userTypingTimer: null, + propTypes: { + showWelcome: React.PropTypes.bool.isRequired, + }, + componentDidUpdate: function() { if (this.props.query != this.state.query) { @@ -112,6 +130,12 @@ var DiscoverPage = React.createClass({ } }, + getDefaultProps: function() { + return { + showWelcome: false, + } + }, + componentWillReceiveProps: function(nextProps, nextState) { if (nextProps.query != nextState.query) { @@ -128,8 +152,15 @@ var DiscoverPage = React.createClass({ lighthouse.search(query).then(this.searchCallback); }, + handleWelcomeDone: function() { + this.setState({ + welcomeComplete: true, + }); + }, + componentWillMount: function() { document.title = "Discover"; + if (this.props.query) { // Rendering with a query already typed this.handleSearchChanged(this.props.query); @@ -138,6 +169,7 @@ var DiscoverPage = React.createClass({ getInitialState: function() { return { + welcomeComplete: false, results: [], query: this.props.query, searching: ('query' in this.props) && (this.props.query.length > 0) diff --git a/ui/js/page/file-list.js b/ui/js/page/file-list.js index ba91835e7..e746f9d77 100644 --- a/ui/js/page/file-list.js +++ b/ui/js/page/file-list.js @@ -2,8 +2,10 @@ import React from 'react'; import lbry from '../lbry.js'; import uri from '../uri.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 rewards from '../rewards.js'; +import lbryio from '../lbryio.js'; import {BusyMessage, Thumbnail} from '../component/common.js'; @@ -32,6 +34,9 @@ export let FileListDownloaded = React.createClass({ }); }); }, + componentWillUnmount: function() { + this._isMounted = false; + }, render: function() { if (this.state.fileInfos === null) { return ( @@ -63,8 +68,22 @@ export let FileListPublished = React.createClass({ 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 () { this._isMounted = true; + this._requestPublishReward(); document.title = "Published Files"; lbry.claim_list_mine().then((claimInfos) => { @@ -80,6 +99,9 @@ export let FileListPublished = React.createClass({ }); }); }, + componentWillUnmount: function() { + this._isMounted = false; + }, render: function () { if (this.state.fileInfos === null) { return ( @@ -161,20 +183,28 @@ export let FileList = React.createClass({ seenUris = {}; 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) { - if (!metadata || seenUris[name]) { + for (let {outpoint, name, channel_name, metadata, mime_type, claim_id, has_signature, signature_is_valid} of fileInfosSorted) { + if (seenUris[name] || !claim_id) { continue; } + let streamMetadata; + if (metadata) { + streamMetadata = metadata.stream.metadata; + } else { + streamMetadata = null; + } + + let fileUri; - if (channel_name === undefined) { + if (!channel_name) { fileUri = uri.buildLbryUri({name}); } else { fileUri = uri.buildLbryUri({name: channel_name, path: name}); } seenUris[name] = true; content.push(); } diff --git a/ui/js/page/help.js b/ui/js/page/help.js index 632c3abd0..99e6ad0d7 100644 --- a/ui/js/page/help.js +++ b/ui/js/page/help.js @@ -51,56 +51,68 @@ var HelpPage = React.createClass({ return (
-

Read the FAQ

-

Our FAQ answers many common questions.

-

+
+

Read the FAQ

+
+
+

Our FAQ answers many common questions.

+

+
-

Get Live Help

-

- Live help is available most hours in the #help channel of our Slack chat room. -

-

- -

+
+

Get Live Help

+
+
+

+ Live help is available most hours in the #help channel of our Slack chat room. +

+

+ +

+
-

Report a Bug

-

Did you find something wrong?

-

-
Thanks! LBRY is made by its users.
+

Report a Bug

+
+

Did you find something wrong?

+

+
Thanks! LBRY is made by its users.
+
{!ver ? null :
-

About

- {ver.lbrynet_update_available || ver.lbryum_update_available ? -

A newer version of LBRY is available.

- :

Your copy of LBRY is up to date.

- } - - - - - - - - - - - - - - - - - - - - - - - -
daemon (lbrynet){ver.lbrynet_version}
wallet (lbryum){ver.lbryum_version}
interface{uiVersion}
Platform{platform}
Installation ID{this.state.lbryId}
+

About

+
+ {ver.lbrynet_update_available || ver.lbryum_update_available ? +

A newer version of LBRY is available.

+ :

Your copy of LBRY is up to date.

+ } + + + + + + + + + + + + + + + + + + + + + + + +
daemon (lbrynet){ver.lbrynet_version}
wallet (lbryum){ver.lbryum_version}
interface{uiVersion}
Platform{platform}
Installation ID{this.state.lbryId}
+
}
diff --git a/ui/js/page/publish.js b/ui/js/page/publish.js index 82ef6bdf8..b424e07ee 100644 --- a/ui/js/page/publish.js +++ b/ui/js/page/publish.js @@ -1,17 +1,20 @@ import React from 'react'; import lbry from '../lbry.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 rewards from '../rewards.js'; +import lbryio from '../lbryio.js'; import Modal from '../component/modal.js'; 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) { // 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) lbry.channel_list_mine().then((channels) => { + rewards.claimReward(rewards.TYPE_FIRST_CHANNEL).then(() => {}, () => {}) this.setState({ channels: channels, ... channel ? {channel} : {} @@ -27,19 +30,23 @@ var PublishPage = React.createClass({ submitting: true, }); - var checkFields = this._requiredFields.slice(); + let checkFields = this._requiredFields; if (!this.state.myClaimExists) { - checkFields.push('file'); + checkFields.unshift('file'); } - var missingFieldFound = false; + let missingFieldFound = false; for (let fieldName of checkFields) { - var field = this.refs[fieldName]; - if (field.getValue() === '') { - field.warnRequired(); - if (!missingFieldFound) { - field.focus(); - missingFieldFound = true; + const field = this.refs[fieldName]; + if (field) { + if (field.getValue() === '' || field.getValue() === false) { + field.showRequiredError(); + if (!missingFieldFound) { + field.focus(); + missingFieldFound = true; + } + } else { + field.clearError(); } } } @@ -61,14 +68,16 @@ var PublishPage = React.createClass({ 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(); if (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) { metadata.license_url = licenseUrl; } @@ -82,9 +91,9 @@ var PublishPage = React.createClass({ }; if (this.refs.file.getValue() !== '') { - publishArgs.file_path = this.refs.file.getValue(); + publishArgs.file_path = this.refs.file.getValue(); } - + lbry.publish(publishArgs, (message) => { this.handlePublishStarted(); }, null, (error) => { @@ -111,17 +120,18 @@ var PublishPage = React.createClass({ channels: null, rawName: '', name: '', - bid: '', + bid: 1, + hasFile: false, feeAmount: '', feeCurrency: 'USD', channel: 'anonymous', newChannelName: '@', - newChannelBid: '', - nameResolved: false, + newChannelBid: 10, + nameResolved: null, + myClaimExists: null, topClaimValue: 0.0, myClaimValue: 0.0, myClaimMetadata: null, - myClaimExists: null, copyrightNotice: '', otherLicenseDescription: '', otherLicenseUrl: '', @@ -162,35 +172,39 @@ var PublishPage = React.createClass({ } 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; } + const name = rawName.toLowerCase(); this.setState({ rawName: rawName, + name: name, + nameResolved: null, + myClaimExists: null, }); - const name = rawName.toLowerCase(); 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 return; } + + this.setState({ + myClaimExists: !!myClaimInfo, + }); lbry.resolve({uri: name}).then((claimInfo) => { - if (name != this.refs.name.getValue()) { + if (name != this.state.name) { return; } if (!claimInfo) { this.setState({ - name: name, nameResolved: false, - myClaimExists: false, }); } else { const topClaimIsMine = (myClaimInfo && myClaimInfo.claim.amount >= claimInfo.claim.amount); const newState = { - name: name, nameResolved: true, topClaimValue: parseFloat(claimInfo.claim.amount), myClaimExists: !!myClaimInfo, @@ -237,7 +251,7 @@ var PublishPage = React.createClass({ isFee: feeEnabled }); }, - handeLicenseChange: function(event) { + handleLicenseChange: function(event) { var licenseType = event.target.options[event.target.selectedIndex].getAttribute('data-license-type'); var newState = { copyrightChosen: licenseType == 'copyright', @@ -245,8 +259,7 @@ var PublishPage = React.createClass({ }; if (licenseType == 'copyright') { - var author = this.refs.meta_author.getValue(); - newState.copyrightNotice = 'Copyright ' + (new Date().getFullYear()) + (author ? ' ' + author : ''); + newState.copyrightNotice = 'All rights reserved.' } this.setState(newState); @@ -277,8 +290,10 @@ var PublishPage = React.createClass({ const newChannelName = (event.target.value.startsWith('@') ? event.target.value : '@' + event.target.value); 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; + } else { + this.refs.newChannelName.clearError() } this.setState({ @@ -290,9 +305,14 @@ var PublishPage = React.createClass({ newChannelBid: event.target.value, }); }, + handleTOSChange: function(event) { + this.setState({ + TOSAgreed: event.target.checked, + }); + }, handleCreateChannelClick: function (event) { 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; } @@ -311,7 +331,7 @@ var PublishPage = React.createClass({ }, 5000); }, (error) => { // 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({ creatingChannel: false, }); @@ -334,159 +354,199 @@ var PublishPage = React.createClass({ }, 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 A deposit of at least {this.state.topClaimValue} {this.state.topClaimValue == 1 ? 'credit ' : 'credits '} + is required to win {this.state.name}. However, you can still get a permanent URL for any amount. + } else { + return ''; + } + }, + closeModal: function() { + this.setState({ + modal: null, + }); + }, render: function() { if (this.state.channels === null) { return null; } + const lbcInputHelp = "This LBC remains yours and the deposit can be undone at any time." + return (
-

LBRY Name

-
- - { - (!this.state.name - ? null - : (!this.state.nameResolved - ? The name {this.state.name} is available. - : (this.state.myClaimExists - ? You already have a claim on the name {this.state.name}. You can use this page to update your claim. - : The name {this.state.name} is currently claimed for {this.state.topClaimValue} {this.state.topClaimValue == 1 ? 'credit' : 'credits'}.))) - } -
What LBRY name would you like to claim for this file?
+
+

Content

+
+ What are you publishing? +
+
+
+ +
+ { !this.state.hasFile ? '' : +
+
+ +
+
+ +
+
+ +
+
+ + + + + + + + + +
+
+ + {/* */} + + + +
+
} +
+ +
+
+

Access

+
+ How much does this content cost? +
+
+
+
+ +
+ { this.handleFeePrefChange(false) } } defaultChecked={!this.state.isFee} /> + { this.handleFeePrefChange(true) } } defaultChecked={this.state.isFee} /> + + + + + + + { this.state.isFee ? +
+ 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. +
: '' } + + + + + + + + + + + + + + {this.state.copyrightChosen + ? + : null} + {this.state.otherLicenseChosen ? + + : null} + {this.state.otherLicenseChosen ? + + : null}
-

Channel

-
- +
+

Identity

+
+ Who created this content? +
+
+
+ {this.state.channels.map(({name}) => )} - - - {this.state.channel == 'new' - ?
- - - -
- : null} -
What channel would you like to publish this file under?
+ +
-
- -
-

Choose File

- - { this.state.myClaimExists ?
If you don't choose a file, the file from your existing claim will be used.
: null } -
- -
-

Bid Amount

-
- Credits -
How much would you like to bid for this name? - { !this.state.nameResolved ? 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. - : (this.state.topClaimIsMine ? You currently control this name with a bid of {this.state.myClaimValue} {this.state.myClaimValue == 1 ? 'credit' : 'credits'}. - : (this.state.myClaimExists ? You have a non-winning bid on this name for {this.state.myClaimValue} {this.state.myClaimValue == 1 ? 'credit' : 'credits'}. - To control this name, you'll need to increase your bid to more than {this.state.topClaimValue} {this.state.topClaimValue == 1 ? 'credit' : 'credits'}. - : You must bid over {this.state.topClaimValue} {this.state.topClaimValue == 1 ? 'credit' : 'credits'} to claim this name.)) } -
-
-
- -
-

Fee

-
- - -
-

How much would you like to charge for this file?

- 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. -
-
-
- - -
-

Your Content

- -
- -
-
- -
-
- - - - - - - - - - - - -
- {this.state.copyrightChosen - ?
- + {this.state.channel == 'new' ? +
+ { this.refs.newChannelName = newChannelName }} + value={this.state.newChannelName} /> + +
+ +
: null} - {this.state.otherLicenseChosen - ?
- -
- : null} - {this.state.otherLicenseChosen - ?
- -
- : null} - -
- - - - - - - - - -
-
- -
-
- -
+
+
+

Address

+
Where should this content permanently reside? .
+
+
+ +
+ { this.state.rawName ? +
+ +
: '' } +
-

Additional Content Information (Optional)

-
- +
+

Terms of Service

+
+
+ I agree to the + } type="checkbox" name="tos_agree" ref={(field) => { this.refs.tos_agree = field }} onChange={this.handleTOSChange} />
@@ -500,7 +560,7 @@ var PublishPage = React.createClass({

Your file has been published to LBRY at the address lbry://{this.state.name}!

- 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." +

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.

diff --git a/ui/js/page/referral.js b/ui/js/page/referral.js deleted file mode 100644 index 1f98e49ff..000000000 --- a/ui/js/page/referral.js +++ /dev/null @@ -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 ( -
- -
-

Check your referral credits

-
-

Have you referred others to LBRY? Enter your referral code and email address below to check how many credits you've earned!

-

As a reminder, your referral code is the same as your LBRY invitation code.

-
-
-
-
-
-
- - -
-
- - - {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.'} - - - {this.state.failureReason} - - - LBRY couldn't connect to our servers to confirm your referral code. Please check your internet connection. - -
- ); - } -}); - -export default ReferralPage; diff --git a/ui/js/page/reward.js b/ui/js/page/reward.js new file mode 100644 index 000000000..2fb5b3e64 --- /dev/null +++ b/ui/js/page/reward.js @@ -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 ( +//
+//

+//
+//

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.

+//

Once you're finished, you may confirm you've linked the account to receive your reward.

+//
+// {this.state.error +// ? +// {this.state.error.message} +// +// : null} +// +// +//
+// ); +// } +// }); +// +// 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 ( +//
+//
+//

{title}

+// +//

{this.state.rewardType.claimed +// ? This reward has been claimed. +// : description}

+// +//
+//
+// ); +// } +// }); +// +// export default RewardPage; diff --git a/ui/js/page/rewards.js b/ui/js/page/rewards.js new file mode 100644 index 000000000..0e0ac80e4 --- /dev/null +++ b/ui/js/page/rewards.js @@ -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 ( +
+
+
+ +

{this.props.title}

+
+
+ {this.props.claimed + ? Reward claimed. + : } +
+
{this.props.description}
+
+
+ ); + } +}); + +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 ( +
+
+ {!this.state.userRewards + ? (this.state.failed ?
Failed to load rewards.
: '') + : this.state.userRewards.map(({RewardType, RewardTitle, RewardDescription, TransactionID, RewardAmount}) => { + return ; + })} + +
+ ); + } +}); + +export default RewardsPage; diff --git a/ui/js/page/settings.js b/ui/js/page/settings.js index 508a8a84d..d05be08fe 100644 --- a/ui/js/page/settings.js +++ b/ui/js/page/settings.js @@ -1,52 +1,53 @@ import React from 'react'; +import {FormField, FormRow} from '../component/form.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({ + _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) { - lbry.setDaemonSetting('run_on_startup', event.target.checked); + this.setDaemonSetting('run_on_startup', event.target.checked); }, onShareDataChange: function (event) { - lbry.setDaemonSetting('share_debug_info', event.target.checked); + this.setDaemonSetting('share_debug_info', event.target.checked); }, onDownloadDirChange: function(event) { - lbry.setDaemonSetting('download_directory', event.target.value); + this.setDaemonSetting('download_directory', event.target.value); }, onMaxUploadPrefChange: function(isLimited) { if (!isLimited) { - lbry.setDaemonSetting('max_upload', 0.0); + this.setDaemonSetting('max_upload', 0.0); } this.setState({ isMaxUpload: isLimited }); }, onMaxUploadFieldChange: function(event) { - lbry.setDaemonSetting('max_upload', Number(event.target.value)); + this.setDaemonSetting('max_upload', Number(event.target.value)); }, onMaxDownloadPrefChange: function(isLimited) { if (!isLimited) { - lbry.setDaemonSetting('max_download', 0.0); + this.setDaemonSetting('max_download', 0.0); } this.setState({ isMaxDownload: isLimited }); }, onMaxDownloadFieldChange: function(event) { - lbry.setDaemonSetting('max_download', Number(event.target.value)); + this.setDaemonSetting('max_download', Number(event.target.value)); }, getInitialState: function() { return { @@ -71,84 +72,130 @@ var SettingsPage = React.createClass({ lbry.setClientSetting('showNsfw', event.target.checked); }, onShowUnavailableChange: function(event) { - lbry.setClientSetting('showUnavailable', event.target.checked); + }, render: function() { if (!this.state.daemonSettings) { return null; } - +/* +
+
+

Run on Startup

+
+
+ +
+
+ */ return (
-

Run on Startup

- -
-
-

Download Directory

-
Where would you like the files you download from LBRY to be saved?
- -
-
-

Bandwidth Limits

-
-

Max Upload

- - +
+

Download Directory

-
-

Max Download

- - +
+
-

Content

-
- -
- 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. +
+

Bandwidth Limits

+
+
+
Max Upload
+ +
+ + { this.state.isMaxUpload ? + + : '' + + } + { this.state.isMaxUpload ? MB/s : '' } +
+
+
+
Max Download
+ +
+ + { this.state.isMaxDownload ? + + : '' + + } + { this.state.isMaxDownload ? MB/s : '' }
-

Search

-
-
- Would you like search results to include items that are not currently available for download? +
+

Content

- +
+ +
+
+
-

Share Diagnostic Data

- +
+

Share Diagnostic Data

+
+
+ +
); } }); - export default SettingsPage; diff --git a/ui/js/page/show.js b/ui/js/page/show.js index 49aff7569..d38e9dc75 100644 --- a/ui/js/page/show.js +++ b/ui/js/page/show.js @@ -2,104 +2,45 @@ import React from 'react'; import lbry from '../lbry.js'; import lighthouse from '../lighthouse.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 {Link} from '../component/link.js'; - -var formatItemImgStyle = { - maxWidth: '100%', - maxHeight: '100%', - display: 'block', - marginLeft: 'auto', - marginRight: 'auto', - marginTop: '5px', -}; +import UriIndicator from '../component/channel-indicator.js'; var FormatItem = React.createClass({ propTypes: { metadata: React.PropTypes.object, contentType: React.PropTypes.string, - cost: React.PropTypes.number, uri: React.PropTypes.string, outpoint: React.PropTypes.string, - costIncludesData: React.PropTypes.bool, }, render: function() { const {thumbnail, author, title, description, language, license} = this.props.metadata; const mediaType = lbry.getMediaType(this.props.contentType); - var costIncludesData = this.props.costIncludesData; - var cost = this.props.cost || 0.0; return ( -
-
- -
-
-

{description}

-
- - - - - - - - - - - - - - - - - - -
Content-Type{this.props.contentType}
Cost
Author{author}
Language{language}
License{license}
-
- -
- -
-
-
- ); + + + + + + + + + + + + + + + +
Content-Type{this.props.contentType}
Author{author}
Language{language}
License{license}
+ ); } }); -var FormatsSection = 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 ( -
-

Sorry, no results found for "{name}".

-
); - } - - return ( -
-
{this.props.uri}
-

{this.props.metadata.title}

- {/* In future, anticipate multiple formats, just a guess at what it could look like - // var formats = this.props.metadata.formats - // return ({formats.map(function(format,i){ */} - - {/* })}); */} -
); - } -}); - -var ShowPage = React.createClass({ +let ShowPage = React.createClass({ _uri: null, propTypes: { @@ -109,6 +50,8 @@ var ShowPage = React.createClass({ return { metadata: null, contentType: null, + hasSignature: false, + signatureIsValid: false, cost: null, costIncludesData: null, uriLookupComplete: null, @@ -118,16 +61,18 @@ var ShowPage = React.createClass({ this._uri = uri.normalizeLbryUri(this.props.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({ outpoint: txid + ':' + nout, metadata: metadata, + hasSignature: has_signature, + signatureIsValid: signature_is_valid, contentType: contentType, uriLookupComplete: true, }); }); - lbry.getCostInfo(this._uri, ({cost, includesData}) => { + lbry.getCostInfo(this._uri).then(({cost, includesData}) => { this.setState({ cost: cost, costIncludesData: includesData, @@ -135,23 +80,50 @@ var ShowPage = React.createClass({ }); }, render: function() { - if (this.state.metadata == null) { - return null; - } + const + metadata = this.state.uriLookupComplete ? this.state.metadata : null, + title = this.state.uriLookupComplete ? metadata.title : this._uri; return ( -
-
- {this.state.uriLookupComplete ? ( - - ) : ( -
-

No content

- There is no content available at {this._uri}. If you reached this page from a link within the LBRY interface, please . Thanks! -
- )} +
+
+ { this.state.contentType && this.state.contentType.startsWith('video/') ? +
-
); +
+
+
+ +

{title}

+ { this.state.uriLookupComplete ? +
+
+ +
+
+ +
+
: '' } +
+ { this.state.uriLookupComplete ? +
+
+ {metadata.description} +
+
+ :
} +
+ { metadata ? +
+ +
: '' } +
+ +
+
+
+ ); } }); diff --git a/ui/js/page/wallet.js b/ui/js/page/wallet.js index 2ace64c27..8540e5469 100644 --- a/ui/js/page/wallet.js +++ b/ui/js/page/wallet.js @@ -2,12 +2,9 @@ import React from 'react'; import lbry from '../lbry.js'; import {Link} from '../component/link.js'; import Modal from '../component/modal.js'; +import {FormField, FormRow} from '../component/form.js'; import {Address, BusyMessage, CreditAmount} from '../component/common.js'; - -var addressRefreshButtonStyle = { - fontSize: '11pt', -}; var AddressSection = React.createClass({ _refreshAddress: function(event) { if (typeof event !== 'undefined') { @@ -27,12 +24,12 @@ var AddressSection = React.createClass({ event.preventDefault(); } - lbry.getNewAddress((address) => { + lbry.wallet_new_address().then(function(address) { window.localStorage.setItem('wallet_address', address); this.setState({ address: address, }); - }); + }.bind(this)) }, getInitialState: function() { @@ -60,12 +57,20 @@ var AddressSection = React.createClass({ render: function() { return (
-

Wallet Address

-
- -
-

Other LBRY users may send credits to you by entering this address on the "Send" page.

- 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. +
+

Wallet Address

+
+
+
+
+
+ +
+
+
+

Other LBRY users may send credits to you by entering this address on the "Send" page.

+

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.

+
); @@ -143,27 +148,26 @@ var SendToAddressSection = React.createClass({ return (
-

Send Credits

-
- - +
+

Send Credits

-
- - +
+
-
+
+ +
+
0.0) || this.state.address == ""} />
- { - this.state.results ? -
-

Results

- {this.state.results} -
- : '' - } + { + this.state.results ? +
+

Results

+ {this.state.results} +
: '' + } @@ -231,25 +235,29 @@ var TransactionList = React.createClass({ } return (
-

Transaction History

- { this.state.transactionItems === null ? : '' } - { this.state.transactionItems && rows.length === 0 ?
You have no transactions.
: '' } - { this.state.transactionItems && rows.length > 0 ? - - - - - - - - - - - {rows} - -
AmountDateTimeTransaction
+
+

Transaction History

+
+
+ { this.state.transactionItems === null ? : '' } + { this.state.transactionItems && rows.length === 0 ?
You have no transactions.
: '' } + { this.state.transactionItems && rows.length > 0 ? + + + + + + + + + + + {rows} + +
AmountDateTimeTransaction
: '' - } + } +
); } @@ -290,9 +298,13 @@ var WalletPage = React.createClass({ return (
-

Balance

- { this.state.balance === null ? : ''} - { this.state.balance !== null ? : '' } +
+

Balance

+
+
+ { this.state.balance === null ? : ''} + { this.state.balance !== null ? : '' } +
{ this.props.viewingPage === 'wallet' ? : '' } { this.props.viewingPage === 'send' ? : '' } diff --git a/ui/js/page/watch.js b/ui/js/page/watch.js index ac270d77a..76e96cada 100644 --- a/ui/js/page/watch.js +++ b/ui/js/page/watch.js @@ -1,14 +1,97 @@ import React from 'react'; -import {Icon} from '../component/common.js'; +import {Icon, Thumbnail} from '../component/common.js'; import {Link} from '../component/link.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' const fs = require('fs'); 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 (
+ + + You don't have enough LBRY credits to pay for this stream. + + + Confirm you want to purchase this bro. + +
); + } +}); + + +export let Video = React.createClass({ _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 _controlsHideTimeout: null, @@ -21,19 +104,26 @@ var WatchPage = React.createClass({ return { downloadStarted: 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, controlsShown: false, }; }, - componentDidMount: function() { + onGet: function() { lbry.get({uri: this.props.uri}).then((fileInfo) => { this._outpoint = fileInfo.outpoint; this.updateLoadStatus(); }); + this.setState({ + isPlaying: true + }) }, - handleBackClicked: function() { - history.back(); + componentDidMount: function() { + if (this.props.autoplay) { + this.start() + } }, handleMouseMove: function() { if (this._controlsTimeout) { @@ -93,6 +183,9 @@ var WatchPage = React.createClass({ return fs.createReadStream(status.download_path, opts) } }; + + rewards.claimNextPurchaseReward() + var elem = this.refs.video; var videostream = VideoStream(mediaFile, elem); elem.play(); @@ -101,26 +194,15 @@ var WatchPage = React.createClass({ }, render: function() { return ( - !this.state.readyToPlay - ? - :
- - {this.state.controlsShown - ?
-
- -
- -
- Back to LBRY -
-
-
-
- : null} -
+
{ + this.state.isPlaying ? + !this.state.readyToPlay ? + this is the world's world loading screen and we shipped our software with it anyway...

{this.state.loadStatusMessage}
: + : +
+ +
+ }
); } -}); - -export default WatchPage; +}) diff --git a/ui/js/rewards.js b/ui/js/rewards.js new file mode 100644 index 000000000..168070627 --- /dev/null +++ b/ui/js/rewards.js @@ -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; \ No newline at end of file diff --git a/ui/js/utils.js b/ui/js/utils.js index 5b5cf246a..b24eb25b6 100644 --- a/ui/js/utils.js +++ b/ui/js/utils.js @@ -13,3 +13,19 @@ export function getLocal(key) { export function setLocal(key, 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)); +} \ No newline at end of file diff --git a/ui/scss/_canvas.scss b/ui/scss/_canvas.scss index a5082d0d9..25eb836fc 100644 --- a/ui/scss/_canvas.scss +++ b/ui/scss/_canvas.scss @@ -11,7 +11,7 @@ body line-height: $font-line-height; } -$drawer-width: 240px; +$drawer-width: 220px; #drawer { @@ -39,12 +39,8 @@ $drawer-width: 240px; .badge { float: right; - background: $color-money; - display: inline-block; - padding: 2px; - color: white; margin-top: $spacing-vertical * 0.25 - 2; - border-radius: 2px; + background: $color-money; } } .drawer-item-selected @@ -53,6 +49,23 @@ $drawer-width: 240px; 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 { padding: $spacing-vertical / 2; @@ -60,6 +73,11 @@ $drawer-width: 240px; text-align: center; } +#window +{ + position: relative; /*window has it's own z-index inside of it*/ + z-index: 1; +} #window.drawer-closed { #drawer { display: none } @@ -100,12 +118,28 @@ $drawer-width: 240px; .header-search { margin-left: 60px; + $padding-adjust: 36px; text-align: center; + .icon { + position: absolute; + top: $spacing-vertical * 1.5 / 2 + 2px; //hacked + margin-left: -$padding-adjust + 14px; //hacked + } input[type="search"] { + position: relative; + left: -$padding-adjust; background: rgba(255, 255, 255, 0.3); color: white; 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); + &:focus { + box-shadow: $focus-box-shadow; + } } } @@ -158,25 +192,11 @@ nav.sub-header main { padding: $spacing-vertical; - } - h2 - { - margin-bottom: $spacing-vertical; - } - h3, h4 - { - margin-bottom: $spacing-vertical / 2; - margin-top: $spacing-vertical; - &:first-child + &.constrained-page { - margin-top: 0; - } - } - .meta - { - + h2, + h3, + h4 - { - margin-top: 0; + max-width: $width-page-constrained; + margin-left: auto; + margin-right: auto; } } } @@ -197,48 +217,6 @@ $header-icon-size: 1.5em; 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 { width: 100%; diff --git a/ui/scss/_global.scss b/ui/scss/_global.scss index 201409835..d829d8245 100644 --- a/ui/scss/_global.scss +++ b/ui/scss/_global.scss @@ -6,17 +6,20 @@ $padding-button: 12px; $padding-text-link: 4px; $color-primary: #155B4A; +$color-primary-light: saturate(lighten($color-primary, 50%), 20%); $color-light-alt: hsl(hue($color-primary), 15, 85); $color-text-dark: #000; +$color-black-transparent: rgba(32,32,32,0.9); $color-help: rgba(0,0,0,.6); -$color-notice: #921010; -$color-warning: #ffffff; +$color-notice: #8a6d3b; +$color-error: #a94442; $color-load-screen-text: #c3c3c3; $color-canvas: #f5f5f5; $color-bg: #ffffff; $color-bg-alt: #D9D9D9; $color-money: #216C2A; $color-meta-light: #505050; +$color-form-border: rgba(160,160,160,.5); $font-size: 16px; $font-line-height: 1.3333; @@ -25,10 +28,16 @@ $mobile-width-threshold: 801px; $max-content-width: 1000px; $max-text-width: 660px; +$width-page-constrained: 800px; + $height-header: $spacing-vertical * 2.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); +$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; diff --git a/ui/scss/_gui.scss b/ui/scss/_gui.scss index 1fb53790c..da32d3ac5 100644 --- a/ui/scss/_gui.scss +++ b/ui/scss/_gui.scss @@ -21,7 +21,7 @@ &:hover { opacity: $hover-opacity; - transition: opacity .225s ease; + transition: opacity $transition-standard; text-decoration: underline; .icon { text-decoration: none; @@ -38,6 +38,7 @@ text-align: center; } +/* section { margin-bottom: $spacing-vertical; @@ -46,17 +47,10 @@ section margin-bottom: 0; } &: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; } } - -main h1 { - font-size: 2.0em; - margin-bottom: $spacing-vertical; - margin-top: $spacing-vertical*2; - font-family: 'Raleway', sans-serif; -} +*/ h2 { font-size: 1.75em; @@ -76,11 +70,6 @@ sup, sub { sup { top: -0.4em; } sub { top: 0.4em; } -label { - cursor: default; - display: block; -} - code { font: 0.8em Consolas, 'Lucida Console', 'Source Sans', monospace; background-color: #eee; @@ -104,23 +93,6 @@ p 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 { 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 { position: relative; top: 0.16em; @@ -235,87 +138,6 @@ input[type="text"], input[type="search"] 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 { display: block; margin-bottom: 5px; @@ -325,79 +147,3 @@ input[type="text"], input[type="search"] font-size: 0.85em; 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; -} diff --git a/ui/scss/_reset.scss b/ui/scss/_reset.scss index 66d0b0f1e..e951875a8 100644 --- a/ui/scss/_reset.scss +++ b/ui/scss/_reset.scss @@ -3,20 +3,24 @@ body, div, dl, dt, dd, ul, ol, li, h1, h2, h3, h4, h5, h6, pre, code, form, fiel margin: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-spacing:0; } -fieldset, img, iframe +fieldset, img, iframe { border: 0; } -h1, h2, h3, h4, h5, h6 +h1, h2, h3, h4, h5, h6 { font-weight:normal; } @@ -25,11 +29,12 @@ ol, ul list-style-position: inside; > li { list-style-position: inside; } } -input, textarea, select +input, textarea, select { - font-family:inherit; - font-size:inherit; - font-weight:inherit; + font-family:inherit; + font-size:inherit; + font-weight:inherit; + border: 0 none; } img { width: auto\9; diff --git a/ui/scss/all.scss b/ui/scss/all.scss index 6012fc3ee..b4c6611a6 100644 --- a/ui/scss/all.scss +++ b/ui/scss/all.scss @@ -5,11 +5,21 @@ @import "_canvas"; @import "_gui"; @import "component/_table"; +@import "component/_button.scss"; +@import "component/_card.scss"; @import "component/_file-actions.scss"; @import "component/_file-tile.scss"; +@import "component/_form-field.scss"; @import "component/_menu.scss"; @import "component/_tooltip.scss"; @import "component/_load-screen.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/_watch.scss"; \ No newline at end of file +@import "page/_watch.scss"; +@import "page/_reward.scss"; +@import "page/_show.scss"; diff --git a/ui/scss/component/_button.scss b/ui/scss/component/_button.scss new file mode 100644 index 000000000..e3c5fe8e8 --- /dev/null +++ b/ui/scss/component/_button.scss @@ -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; +} diff --git a/ui/scss/component/_card.scss b/ui/scss/component/_card.scss new file mode 100644 index 000000000..e019d7342 --- /dev/null +++ b/ui/scss/component/_card.scss @@ -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; +} \ No newline at end of file diff --git a/ui/scss/component/_channel-indicator.scss b/ui/scss/component/_channel-indicator.scss index 06446e23f..52a0baed6 100644 --- a/ui/scss/component/_channel-indicator.scss +++ b/ui/scss/component/_channel-indicator.scss @@ -1,5 +1,5 @@ @import "../global"; .channel-indicator__icon--invalid { - color: #b01c2e; + color: $color-error; } diff --git a/ui/scss/component/_file-tile.scss b/ui/scss/component/_file-tile.scss index a5c73a175..eb768bbf0 100644 --- a/ui/scss/component/_file-tile.scss +++ b/ui/scss/component/_file-tile.scss @@ -1,29 +1,35 @@ @import "../global"; +$height-file-tile: $spacing-vertical * 8; .file-tile__row { - height: $spacing-vertical * 7; -} - -.file-tile__row--unavailable { - opacity: 0.5; + height: $height-file-tile; + .credit-amount { + float: right; + } + //Hack! Remove below! + .card__title-primary { + margin-top: $spacing-vertical * 2/3; + } } .file-tile__thumbnail { max-width: 100%; - max-height: $spacing-vertical * 7; + max-height: $height-file-tile; + vertical-align: middle; display: block; margin-left: auto; margin-right: auto; } +.file-tile__thumbnail-container +{ + height: $height-file-tile; + @include absolute-center(); +} .file-tile__title { font-weight: bold; } -.file-tile__cost { - float: right; -} - .file-tile__description { color: #444; margin-top: 12px; diff --git a/ui/scss/component/_form-field.scss b/ui/scss/component/_form-field.scss new file mode 100644 index 000000000..c9f00b141 --- /dev/null +++ b/ui/scss/component/_form-field.scss @@ -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; +} \ No newline at end of file diff --git a/ui/scss/component/_load-screen.scss b/ui/scss/component/_load-screen.scss index e56eb12c0..0caa74f65 100644 --- a/ui/scss/component/_load-screen.scss +++ b/ui/scss/component/_load-screen.scss @@ -23,7 +23,7 @@ } .load-screen__details--warning { - color: $color-warning; + color: white; } .load-screen__cancel-link { diff --git a/ui/scss/component/_modal-page.scss b/ui/scss/component/_modal-page.scss new file mode 100644 index 000000000..d9bd0d8d5 --- /dev/null +++ b/ui/scss/component/_modal-page.scss @@ -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; + } +} \ No newline at end of file diff --git a/ui/scss/component/_modal.scss b/ui/scss/component/_modal.scss new file mode 100644 index 000000000..13284c7ff --- /dev/null +++ b/ui/scss/component/_modal.scss @@ -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; +} \ No newline at end of file diff --git a/ui/scss/component/_notice.scss b/ui/scss/component/_notice.scss new file mode 100644 index 000000000..b77ba2a5a --- /dev/null +++ b/ui/scss/component/_notice.scss @@ -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; +} diff --git a/ui/scss/component/_snack-bar.scss b/ui/scss/component/_snack-bar.scss new file mode 100644 index 000000000..c3df3ab92 --- /dev/null +++ b/ui/scss/component/_snack-bar.scss @@ -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; + } +} diff --git a/ui/scss/component/_video.scss b/ui/scss/component/_video.scss new file mode 100644 index 000000000..c41815dca --- /dev/null +++ b/ui/scss/component/_video.scss @@ -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; +} \ No newline at end of file diff --git a/ui/scss/page/_reward.scss b/ui/scss/page/_reward.scss new file mode 100644 index 000000000..a550c01c3 --- /dev/null +++ b/ui/scss/page/_reward.scss @@ -0,0 +1,5 @@ +@import "../global"; + +.reward-page__details { + background-color: lighten($color-canvas, 1.5%); +} \ No newline at end of file diff --git a/ui/scss/page/_show.scss b/ui/scss/page/_show.scss new file mode 100644 index 000000000..48b82d065 --- /dev/null +++ b/ui/scss/page/_show.scss @@ -0,0 +1,9 @@ +@import "../global"; + +.show-page-media { + text-align: center; + margin-bottom: $spacing-vertical; + img { + max-width: 100%; + } +} \ No newline at end of file diff --git a/ui/scss/page/_watch.scss b/ui/scss/page/_watch.scss index 23fbcc171..59a614d31 100644 --- a/ui/scss/page/_watch.scss +++ b/ui/scss/page/_watch.scss @@ -1,6 +1,3 @@ -.video { - background: #000; -} .video__overlay { position: absolute;