From 7644566ef5ba9630cc88fd342489bebde36c3d06 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Mon, 17 Apr 2017 03:59:06 -0400 Subject: [PATCH 1/9] Add ability to display non-styled prices and credit amounts --- ui/js/component/common.js | 21 +++++++++++++++------ ui/scss/_canvas.scss | 6 +----- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/ui/js/component/common.js b/ui/js/component/common.js index d8b0fc052..21db05865 100644 --- a/ui/js/component/common.js +++ b/ui/js/component/common.js @@ -62,18 +62,20 @@ export let CreditAmount = React.createClass({ propTypes: { amount: React.PropTypes.number.isRequired, precision: React.PropTypes.number, - label: React.PropTypes.bool + label: React.PropTypes.bool, + look: React.PropTypes.oneOf(['indicator', 'plain']), }, getDefaultProps: function() { return { precision: 1, label: true, + look: 'indicator', } }, render: function() { var formattedAmount = lbry.formatCredits(this.props.amount, this.props.precision); return ( - + {formattedAmount} {this.props.label ? @@ -91,6 +93,13 @@ export let FilePrice = React.createClass({ propTypes: { metadata: React.PropTypes.object, uri: React.PropTypes.string.isRequired, + look: React.PropTypes.oneOf(['indicator', 'plain']), + }, + + getDefaultProps: function() { + return { + look: 'indicator', + } }, getInitialState: function() { @@ -121,19 +130,19 @@ export let FilePrice = React.createClass({ render: function() { if (this.state.cost === null && this.props.metadata) { if (!this.props.metadata.fee) { - return free*; + return free*; } else { if (this.props.metadata.fee.currency === "LBC") { return } else if (this.props.metadata.fee.currency === "USD") { - return ???; + return ???; } } } return ( this.state.cost !== null ? - : - ??? + : + ??? ); } }); 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; From 5fe9f076ebf815d1242dcd93d8ff53b5f5e59771 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Mon, 17 Apr 2017 05:10:45 -0400 Subject: [PATCH 2/9] Update handling of file prices - Until Lighthouse results come back, display just the key fee - Add support for displaying prices without special formatting - Refactor and simplify FilePrice and CreditAmount --- ui/js/component/common.js | 46 +++++++++++++++++++-------------------- ui/js/lbry.js | 1 + 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/ui/js/component/common.js b/ui/js/component/common.js index 21db05865..ba6b81561 100644 --- a/ui/js/component/common.js +++ b/ui/js/component/common.js @@ -63,23 +63,32 @@ export let CreditAmount = React.createClass({ amount: React.PropTypes.number.isRequired, precision: React.PropTypes.number, 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 } @@ -102,11 +111,11 @@ export let FilePrice = React.createClass({ } }, - getInitialState: function() { - return { - cost: null, - isEstimate: null, - } + componentWillMount: function() { + this.setState({ + cost: this.props.metadata ? this.props.metadata.fee : null, + isEstimate: this.props.metadata ? true : null, + }); }, componentDidMount: function() { @@ -128,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/lbry.js b/ui/js/lbry.js index 9e8b1565e..f69839f57 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'; From 94b60beebc90c985292a23797075a115dfa4dffe Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Mon, 17 Apr 2017 05:18:41 -0400 Subject: [PATCH 3/9] Update download notice modal to include price --- ui/js/component/file-actions.js | 11 +++++++---- ui/js/page/watch.js | 9 +++++---- 2 files changed, 12 insertions(+), 8 deletions(-) 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/page/watch.js b/ui/js/page/watch.js index 315e19a9c..5332df5ff 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,8 +13,9 @@ 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 @@ -84,7 +85,7 @@ export let WatchLink = React.createClass({ - Do you want to purchase this? + Are you sure you'd like to buy {this.props.metadata.title} for credits?
); } @@ -200,7 +201,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}
: :
- +
} ); From bbab1f064a2a7944c9a787eb894c5a97ae1675f4 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Mon, 17 Apr 2017 06:03:00 -0400 Subject: [PATCH 4/9] Show: don't display price if file is already downloaded --- ui/js/page/show.js | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) 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 ?
From cf1107050dbcd862466b3cacdcc0147e6fdca1de Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Mon, 17 Apr 2017 06:05:36 -0400 Subject: [PATCH 5/9] Show: prevent prompt for purchase if user already has a copy --- ui/js/page/watch.js | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/ui/js/page/watch.js b/ui/js/page/watch.js index 5332df5ff..164745e10 100644 --- a/ui/js/page/watch.js +++ b/ui/js/page/watch.js @@ -20,7 +20,7 @@ export let WatchLink = React.createClass({ getInitialState: function() { affirmedPurchase: false }, - onAffirmPurchase: function() { + play: function() { lbry.get({uri: this.props.uri}).then((streamInfo) => { if (streamInfo === null || typeof streamInfo !== 'object') { this.setState({ @@ -51,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' + }); + } }); } }); @@ -84,7 +90,7 @@ export let WatchLink = React.createClass({ You don't have enough LBRY credits to pay for this stream. + contentLabel="Confirm Purchase" onConfirmed={this.play} onAborted={this.closeModal}> Are you sure you'd like to buy {this.props.metadata.title} for credits?
); @@ -96,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 { @@ -114,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({ @@ -159,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) { @@ -201,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}
: :
- +
}
); From 9b4ebbab0f7d89b274e01ccfaf2a376a6e81b835 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Mon, 17 Apr 2017 12:18:33 -0400 Subject: [PATCH 6/9] Add caching wrapper for settings_get API method --- ui/js/lbry.js | 17 +++++++++++++++++ ui/js/lbryio.js | 29 +++++++++++++++++++++++++---- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/ui/js/lbry.js b/ui/js/lbry.js index f69839f57..01fa6d3b2 100644 --- a/ui/js/lbry.js +++ b/ui/js/lbry.js @@ -648,6 +648,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..704ba53b0 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 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)); From 757ab11779e7db9c4854c139bd3892c81cd6c1ff Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Mon, 17 Apr 2017 16:22:08 -0400 Subject: [PATCH 7/9] Compute cost estimate on client side when possible --- ui/js/component/common.js | 10 +++---- ui/js/component/file-tile.js | 3 -- ui/js/lbry.js | 53 ++++++++++++++++++++++++++---------- ui/js/lbryio.js | 2 +- 4 files changed, 44 insertions(+), 24 deletions(-) diff --git a/ui/js/component/common.js b/ui/js/component/common.js index ba6b81561..0c8d66ca0 100644 --- a/ui/js/component/common.js +++ b/ui/js/component/common.js @@ -62,6 +62,7 @@ export let CreditAmount = React.createClass({ propTypes: { amount: React.PropTypes.number.isRequired, precision: React.PropTypes.number, + isEstimate: React.PropTypes.bool, label: React.PropTypes.bool, showFree: React.PropTypes.bool, look: React.PropTypes.oneOf(['indicator', 'plain']), @@ -100,7 +101,6 @@ export let FilePrice = React.createClass({ _isMounted: false, propTypes: { - metadata: React.PropTypes.object, uri: React.PropTypes.string.isRequired, look: React.PropTypes.oneOf(['indicator', 'plain']), }, @@ -113,8 +113,8 @@ export let FilePrice = React.createClass({ componentWillMount: function() { this.setState({ - cost: this.props.metadata ? this.props.metadata.fee : null, - isEstimate: this.props.metadata ? true : null, + cost: null, + isEstimate: null, }); }, @@ -124,7 +124,7 @@ export let FilePrice = React.createClass({ if (this._isMounted) { this.setState({ cost: cost, - isEstimate: includesData, + isEstimate: !includesData, }); } }, (err) => { @@ -141,7 +141,7 @@ export let FilePrice = React.createClass({ return ???; } - return + return } }); 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 01fa6d3b2..244b82142 100644 --- a/ui/js/lbry.js +++ b/ui/js/lbry.js @@ -217,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) { @@ -231,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); + }); }); }); } diff --git a/ui/js/lbryio.js b/ui/js/lbryio.js index 704ba53b0..9efeac9b1 100644 --- a/ui/js/lbryio.js +++ b/ui/js/lbryio.js @@ -44,7 +44,7 @@ lbryio.getExchangeRates = function() { }); } -lbryio.call = function(resource, action, params={}, method='get', evenIfDisabled=false) { // evenIfDisabled is for development, when we may have some calls working and some not +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")) From b65753d5438b1ef44502d5ff2ea683bbd49d8f68 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Mon, 17 Apr 2017 17:26:36 -0400 Subject: [PATCH 8/9] Update submodules --- lbry | 2 +- lbryum | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 From 277ba8249d4757c3dcaf1336e599bc1563b370b7 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Mon, 17 Apr 2017 17:28:48 -0400 Subject: [PATCH 9/9] Update changelog for price loading changes --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) 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