diff --git a/CHANGELOG.md b/CHANGELOG.md index 947773030..bb336c848 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Web UI version numbers should always match the corresponding version of LBRY App * New publishes now display immediately in My Files, even before they hit the lbrynet file manager. * New welcome flow for new users * Redesigned UI for Discover + * Handle more of price calculations at the daemon layer to improve page load time ### Changed * Update process now easier and more reliable diff --git a/lbry b/lbry index 043e2d0ab..e8bccec71 160000 --- a/lbry +++ b/lbry @@ -1 +1 @@ -Subproject commit 043e2d0ab96030468d53d02e311fd848f35c2dc1 +Subproject commit e8bccec71c7424bf06d057904e4722d2d734fa3f diff --git a/lbryum b/lbryum index 121bda396..39ace3737 160000 --- a/lbryum +++ b/lbryum @@ -1 +1 @@ -Subproject commit 121bda3963ee94f0c9c027813c55b71b38219739 +Subproject commit 39ace3737509ff2b09fabaaa64d1525843de1325 diff --git a/ui/js/component/common.js b/ui/js/component/common.js index d8b0fc052..0c8d66ca0 100644 --- a/ui/js/component/common.js +++ b/ui/js/component/common.js @@ -62,22 +62,34 @@ export let CreditAmount = React.createClass({ propTypes: { amount: React.PropTypes.number.isRequired, precision: React.PropTypes.number, - label: React.PropTypes.bool + isEstimate: React.PropTypes.bool, + label: React.PropTypes.bool, + showFree: React.PropTypes.bool, + look: React.PropTypes.oneOf(['indicator', 'plain']), }, getDefaultProps: function() { return { precision: 1, label: true, + showFree: false, + look: 'indicator', } }, render: function() { - var formattedAmount = lbry.formatCredits(this.props.amount, this.props.precision); + const formattedAmount = lbry.formatCredits(this.props.amount, this.props.precision); + let amountText; + if (this.props.showFree && parseFloat(formattedAmount) == 0) { + amountText = 'free'; + } else if (this.props.label) { + amountText = formattedAmount + (parseFloat(formattedAmount) == 1 ? ' credit' : ' credits'); + } else { + amountText = formattedAmount; + } + return ( - + - {formattedAmount} - {this.props.label ? - (parseFloat(formattedAmount) == 1.0 ? ' credit' : ' credits') : '' } + {amountText} { this.props.isEstimate ? * : null } @@ -89,15 +101,21 @@ export let FilePrice = React.createClass({ _isMounted: false, propTypes: { - metadata: React.PropTypes.object, uri: React.PropTypes.string.isRequired, + look: React.PropTypes.oneOf(['indicator', 'plain']), }, - getInitialState: function() { + getDefaultProps: function() { return { + look: 'indicator', + } + }, + + componentWillMount: function() { + this.setState({ cost: null, isEstimate: null, - } + }); }, componentDidMount: function() { @@ -106,7 +124,7 @@ export let FilePrice = React.createClass({ if (this._isMounted) { this.setState({ cost: cost, - isEstimate: includesData, + isEstimate: !includesData, }); } }, (err) => { @@ -119,22 +137,11 @@ export let FilePrice = React.createClass({ }, 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 ???; - } - } + if (this.state.cost === null) { + return ???; } - return ( - this.state.cost !== null ? - : - ??? - ); + + return } }); diff --git a/ui/js/component/file-actions.js b/ui/js/component/file-actions.js index 2f2a4074d..6c105ed75 100644 --- a/ui/js/component/file-actions.js +++ b/ui/js/component/file-actions.js @@ -1,7 +1,8 @@ import React from 'react'; import lbry from '../lbry.js'; +import uri from '../uri.js'; import {Link} from '../component/link.js'; -import {Icon} from '../component/common.js'; +import {Icon, FilePrice} from '../component/common.js'; import {Modal} from './modal.js'; import {FormField} from './form.js'; import {ToolTip} from '../component/tooltip.js'; @@ -155,6 +156,8 @@ let FileActionsRow = React.createClass({ linkBlock = ; } + const lbryUri = uri.normalizeLbryUri(this.props.uri); + const title = this.props.metadata ? this.props.metadata.title : lbryUri; return (
{this.state.fileInfo !== null || this.state.fileInfo.isMine @@ -167,7 +170,7 @@ let FileActionsRow = React.createClass({ : '' } - Do you want to purchase this? + Are you sure you'd like to buy {title} for credits? @@ -175,12 +178,12 @@ let FileActionsRow = React.createClass({ - LBRY was unable to download the stream lbry://{this.props.uri}. + LBRY was unable to download the stream {lbryUri}. -

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

+

Are you sure you'd like to remove {title} from LBRY?

diff --git a/ui/js/component/file-tile.js b/ui/js/component/file-tile.js index b2477a7be..2abaa5d21 100644 --- a/ui/js/component/file-tile.js +++ b/ui/js/component/file-tile.js @@ -26,7 +26,6 @@ export let FileTileStream = React.createClass({ return { showNsfwHelp: false, isHidden: false, - available: null, } }, getDefaultProps: function() { @@ -143,7 +142,6 @@ export let FileCardStream = React.createClass({ return { showNsfwHelp: false, isHidden: false, - available: null, } }, getDefaultProps: function() { @@ -232,7 +230,6 @@ export let FileTile = React.createClass({ propTypes: { uri: React.PropTypes.string.isRequired, - available: React.PropTypes.bool, }, getInitialState: function() { diff --git a/ui/js/lbry.js b/ui/js/lbry.js index 9e8b1565e..244b82142 100644 --- a/ui/js/lbry.js +++ b/ui/js/lbry.js @@ -1,3 +1,4 @@ +import lbryio from './lbryio.js'; import lighthouse from './lighthouse.js'; import jsonrpc from './jsonrpc.js'; import uri from './uri.js'; @@ -216,12 +217,18 @@ lbry.getPeersForBlobHash = function(blobHash, callback) { 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) => { + const COST_INFO_CACHE_KEY = 'cost_info_cache'; let costInfoCache = getSession(COST_INFO_CACHE_KEY, {}) + function cacheAndResolve(cost, includesData) { + costInfoCache[lbryUri] = {cost, includesData}; + setSession(COST_INFO_CACHE_KEY, costInfoCache); + resolve({cost, includesData}); + } + if (!lbryUri) { - reject(new Error(`URI required.`)); + return reject(new Error(`URI required.`)); } if (costInfoCache[lbryUri] && costInfoCache[lbryUri].cost) { @@ -230,27 +237,44 @@ lbry.getCostInfo = function(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]); + cacheAndResolve(cost, size !== null); }, reject); } + function getCostGenerous(lbryUri) { + // If generous is on, the calculation is simple enough that we might as well do it here in the front end + lbry.resolve({uri: lbryUri}).then((resolutionInfo) => { + const fee = resolutionInfo.claim.value.stream.metadata.fee; + if (fee === undefined) { + cacheAndResolve(0, true); + } else if (fee.currency == 'LBC') { + cacheAndResolve(fee.amount, true); + } else { + lbryio.getExchangeRates().then(({lbc_usd}) => { + cacheAndResolve(fee.amount / lbc_usd, true); + }); + } + }); + } + const uriObj = uri.parseLbryUri(lbryUri); const name = uriObj.path || uriObj.name; - lighthouse.get_size_for_name(name).then((size) => { - if (size) { - getCost(name, size); + lbry.settings_get({allow_cached: true}).then(({is_generous_host}) => { + if (is_generous_host) { + return getCostGenerous(lbryUri); } - else { + + lighthouse.get_size_for_name(name).then((size) => { + if (size) { + getCost(name, size); + } + else { + getCost(name, null); + } + }, () => { getCost(name, null); - } - }, () => { - getCost(name, null); + }); }); }); } @@ -647,6 +671,23 @@ lbry.resolve = function(params={}) { }); } +// Adds caching. +lbry.settings_get = function(params={}) { + return new Promise((resolve, reject) => { + if (params.allow_cached) { + const cached = getSession('settings'); + if (cached) { + return resolve(cached); + } + } + + lbry.call('settings_get', {}, (settings) => { + setSession('settings', settings); + resolve(settings); + }); + }); +} + // lbry.get = function(params={}) { // return function(params={}) { // return new Promise((resolve, reject) => { diff --git a/ui/js/lbryio.js b/ui/js/lbryio.js index 99fcd0e0d..9efeac9b1 100644 --- a/ui/js/lbryio.js +++ b/ui/js/lbryio.js @@ -10,7 +10,8 @@ const lbryio = { enabled: false }; -const CONNECTION_STRING = 'http://localhost:8080/'; +const CONNECTION_STRING = 'https://api.lbry.io/'; +const EXCHANGE_RATE_TIMEOUT = 20 * 60 * 1000; const mocks = { 'reward_type.get': ({name}) => { @@ -24,12 +25,32 @@ const mocks = { } }; -lbryio.call = function(resource, action, params={}, method='get') { + +lbryio.getExchangeRates = function() { return new Promise((resolve, reject) => { - if (!lbryio.enabled && (resource != 'discover' || action != 'list')) { - reject(new Error("LBRY internal API is disabled")) + const cached = getSession('exchangeRateCache'); + if (!cached || Date.now() - cached.time > EXCHANGE_RATE_TIMEOUT) { + lbryio.call('lbc', 'exchange_rate', {}, 'get', true).then(({lbc_usd, lbc_btc, btc_usd}) => { + const rates = {lbc_usd, lbc_btc, btc_usd}; + setSession('exchangeRateCache', { + rates: rates, + time: Date.now(), + }); + resolve(rates); + }); + } else { + resolve(cached.rates); + } + }); +} + +lbryio.call = function(resource, action, params={}, method='get', evenIfDisabled=false) { // evenIfDisabled is just for development, when we may have some calls working and some not + return new Promise((resolve, reject) => { + if (!lbryio.enabled && !evenIfDisabled && (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)); diff --git a/ui/js/page/show.js b/ui/js/page/show.js index d38e9dc75..ea41731af 100644 --- a/ui/js/page/show.js +++ b/ui/js/page/show.js @@ -55,6 +55,7 @@ let ShowPage = React.createClass({ cost: null, costIncludesData: null, uriLookupComplete: null, + isDownloaded: null, }; }, componentWillMount: function() { @@ -62,8 +63,16 @@ let ShowPage = React.createClass({ document.title = this._uri; lbry.resolve({uri: this._uri}).then(({ claim: {txid, nout, has_signature, signature_is_valid, value: {stream: {metadata, source: {contentType}}}}}) => { + const outpoint = txid + ':' + nout; + + lbry.file_list({outpoint}).then((fileInfo) => { + this.setState({ + isDownloaded: fileInfo.length > 0, + }); + }); + this.setState({ - outpoint: txid + ':' + nout, + outpoint: outpoint, metadata: metadata, hasSignature: has_signature, signatureIsValid: signature_is_valid, @@ -80,21 +89,21 @@ let ShowPage = React.createClass({ }); }, render: function() { - const - metadata = this.state.uriLookupComplete ? this.state.metadata : null, - title = this.state.uriLookupComplete ? metadata.title : this._uri; - + const metadata = this.state.metadata; + const title = metadata ? this.state.metadata.title : this._uri; return (
{ this.state.contentType && this.state.contentType.startsWith('video/') ? -
- + {this.state.isDownloaded === false + ? + : null}

{title}

{ this.state.uriLookupComplete ?
diff --git a/ui/js/page/watch.js b/ui/js/page/watch.js index 315e19a9c..164745e10 100644 --- a/ui/js/page/watch.js +++ b/ui/js/page/watch.js @@ -1,5 +1,5 @@ import React from 'react'; -import {Icon, Thumbnail} from '../component/common.js'; +import {Icon, Thumbnail, FilePrice} from '../component/common.js'; import {Link} from '../component/link.js'; import lbry from '../lbry.js'; import Modal from '../component/modal.js'; @@ -13,13 +13,14 @@ const VideoStream = require('videostream'); export let WatchLink = React.createClass({ propTypes: { uri: React.PropTypes.string, + metadata: React.PropTypes.object, downloadStarted: React.PropTypes.bool, - onGet: React.PropTypes.func + onGet: React.PropTypes.func, }, getInitialState: function() { affirmedPurchase: false }, - onAffirmPurchase: function() { + play: function() { lbry.get({uri: this.props.uri}).then((streamInfo) => { if (streamInfo === null || typeof streamInfo !== 'object') { this.setState({ @@ -50,10 +51,16 @@ export let WatchLink = React.createClass({ attemptingDownload: false, }); } else if (cost <= 0.01) { - this.onAffirmPurchase() + this.play() } else { - this.setState({ - modal: 'affirmPurchase' + lbry.file_list({outpoint: this.props.outpoint}).then((fileInfo) => { + if (fileInfo) { // Already downloaded + this.play(); + } else { + this.setState({ + modal: 'affirmPurchase' + }); + } }); } }); @@ -83,8 +90,8 @@ export let WatchLink = React.createClass({ You don't have enough LBRY credits to pay for this stream. - Do you want to purchase this? + contentLabel="Confirm Purchase" onConfirmed={this.play} onAborted={this.closeModal}> + Are you sure you'd like to buy {this.props.metadata.title} for credits?
); } @@ -95,10 +102,11 @@ 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, - _outpoint: null, propTypes: { - uri: React.PropTypes.string, + uri: React.PropTypes.string.isRequired, + metadata: React.PropTypes.object, + outpoint: React.PropTypes.string, }, getInitialState: function() { return { @@ -113,7 +121,6 @@ export let Video = React.createClass({ }, onGet: function() { lbry.get({uri: this.props.uri}).then((fileInfo) => { - this._outpoint = fileInfo.outpoint; this.updateLoadStatus(); }); this.setState({ @@ -158,7 +165,7 @@ export let Video = React.createClass({ }, updateLoadStatus: function() { lbry.file_list({ - outpoint: this._outpoint, + outpoint: this.props.outpoint, full_status: true, }).then(([status]) => { if (!status || status.written_bytes == 0) { @@ -200,7 +207,7 @@ export let Video = React.createClass({ this is the world's world loading screen and we shipped our software with it anyway...

{this.state.loadStatusMessage}
: :
- +
}
); diff --git a/ui/scss/_canvas.scss b/ui/scss/_canvas.scss index 25eb836fc..8aa4227e3 100644 --- a/ui/scss/_canvas.scss +++ b/ui/scss/_canvas.scss @@ -57,15 +57,11 @@ $drawer-width: 220px; color: white; border-radius: 2px; } -.credit-amount +.credit-amount--indicator { font-weight: bold; color: $color-money; } -.credit-amount--estimate { - font-style: italic; - color: $color-meta-light; -} #drawer-handle { padding: $spacing-vertical / 2;