From 839e6293624e8d590d5d74e5e4450b5b425142cd Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Mon, 10 Apr 2017 21:18:37 -0400 Subject: [PATCH 001/158] Fix bid position and claim sequence regexes --- ui/js/uri.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/js/uri.js b/ui/js/uri.js index 98883c6be..1eb86ada0 100644 --- a/ui/js/uri.js +++ b/ui/js/uri.js @@ -74,12 +74,12 @@ uri.parseLbryUri = function(lbryUri, requireProto=false) { throw new Error(`Invalid claim ID ${claimId}.`); } - if (bidPosition && !bidPosition.match(/^-?[1-9][0-9]+$/)) { - throw new Error('Bid position must be a number.'); + if (claimSequence && !claimSequence.match(/^-?[1-9][0-9]*$/)) { + throw new Error('Claim sequence must be a number.'); } - if (claimSequence && !claimSequence.match(/^-?[1-9][0-9]+$/)) { - throw new Error('Claim sequence must be a number.'); + if (bidPosition && !bidPosition.match(/^-?[1-9][0-9]*$/)) { + throw new Error('Bid position must be a number.'); } // Validate path -- 2.45.2 From af32951d1fb4ffaec41730f8e0eead89cab51572 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Mon, 10 Apr 2017 21:18:58 -0400 Subject: [PATCH 002/158] Add uri.normalizeLbryUri() --- ui/js/uri.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ui/js/uri.js b/ui/js/uri.js index 1eb86ada0..67615f7b5 100644 --- a/ui/js/uri.js +++ b/ui/js/uri.js @@ -115,4 +115,10 @@ uri.buildLbryUri = function(uriObj, includeProto=true) { (path ? `/${path}` : ''); } +/* Takes a parseable LBRY URI and converts it to standard, canonical format (currently this just + * consists of making sure it has a lbry:// prefix) */ +uri.normalizeLbryUri = function(lbryUri) { + return uri.buildLbryUri(uri.parseLbryUri(lbryUri)); +} + export default uri; -- 2.45.2 From 3b72938b74490f76c0f4ed00edb046632eaa4505 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Mon, 10 Apr 2017 21:24:35 -0400 Subject: [PATCH 003/158] Refactor file tiles to be compatible with channels --- ui/js/component/channel-indicator.js | 8 ++--- ui/js/component/file-actions.js | 23 +++++++------ ui/js/component/file-tile.js | 50 ++++++++++++++++------------ ui/js/lbry.js | 36 +++++--------------- 4 files changed, 52 insertions(+), 65 deletions(-) diff --git a/ui/js/component/channel-indicator.js b/ui/js/component/channel-indicator.js index 674484200..37897cbcd 100644 --- a/ui/js/component/channel-indicator.js +++ b/ui/js/component/channel-indicator.js @@ -6,11 +6,11 @@ import {Icon} from './common.js'; const ChannelIndicator = React.createClass({ propTypes: { uri: React.PropTypes.string.isRequired, - claimInfo: React.PropTypes.object.isRequired, + hasSignature: React.PropTypes.bool.isRequired, + signatureIsValid: React.PropTypes.bool, }, render: function() { - const {name, has_signature, signature_is_valid} = this.props.claimInfo; - if (!has_signature) { + if (!this.props.hasSignature) { return null; } @@ -24,7 +24,7 @@ const ChannelIndicator = React.createClass({ const channelUri = uri.buildLbryUri(channelUriObj, false); let icon, modifier; - if (!signature_is_valid) { + if (this.props.signatureIsValid) { icon = 'icon-check-circle'; modifier = 'valid'; } else { diff --git a/ui/js/component/file-actions.js b/ui/js/component/file-actions.js index 863c9b9f2..314b2353b 100644 --- a/ui/js/component/file-actions.js +++ b/ui/js/component/file-actions.js @@ -11,11 +11,11 @@ const {shell} = require('electron'); let WatchLink = React.createClass({ propTypes: { - streamName: React.PropTypes.string, + uri: React.PropTypes.string, downloadStarted: React.PropTypes.bool, }, startVideo: function() { - window.location = '?watch=' + this.props.streamName; + window.location = '?watch=' + this.props.uri; }, handleClick: function() { this.setState({ @@ -25,7 +25,7 @@ let WatchLink = React.createClass({ if (this.props.downloadStarted) { this.startVideo(); } else { - lbry.getCostInfoForName(this.props.streamName, ({cost}) => { + lbry.getCostInfo(this.props.uri, ({cost}) => { lbry.getBalance((balance) => { if (cost > balance) { this.setState({ @@ -67,10 +67,10 @@ let FileActionsRow = React.createClass({ _fileInfoSubscribeId: null, propTypes: { - streamName: React.PropTypes.string, + uri: React.PropTypes.string, outpoint: React.PropTypes.string.isRequired, metadata: React.PropTypes.oneOfType([React.PropTypes.object, React.PropTypes.string]), - contentType: React.PropTypes.string, + contentType: React.PropTypes.string.isRequired, }, getInitialState: function() { return { @@ -95,7 +95,7 @@ let FileActionsRow = React.createClass({ attemptingDownload: true, attemptingRemove: false }); - lbry.getCostInfoForName(this.props.streamName, ({cost}) => { + lbry.getCostInfo(this.props.uri, ({cost}) => { lbry.getBalance((balance) => { if (cost > balance) { this.setState({ @@ -103,7 +103,7 @@ let FileActionsRow = React.createClass({ attemptingDownload: false, }); } else { - lbry.getStream(this.props.streamName, (streamInfo) => { + lbry.get({uri: this.props.uri}).then((streamInfo) => { if (streamInfo === null || typeof streamInfo !== 'object') { this.setState({ modal: 'timedOut', @@ -199,7 +199,7 @@ let FileActionsRow = React.createClass({ return (
{this.props.contentType && this.props.contentType.startsWith('video/') - ? + ? : null} {this.state.fileInfo !== null || this.state.fileInfo.isMine ? linkBlock @@ -215,7 +215,7 @@ let FileActionsRow = React.createClass({ - LBRY was unable to download the stream lbry://{this.props.streamName}. + LBRY was unable to download the stream lbry://{this.props.uri}. { + lbry.get_availability({uri: this.props.uri}, (availability) => { if (this._isMounted) { this.setState({ available: availability > 0, @@ -291,7 +292,7 @@ export let FileActions = React.createClass({ return (
{ fileInfo || this.state.available || this.state.forceShowActions - ? :
This file is not currently available.
diff --git a/ui/js/component/file-tile.js b/ui/js/component/file-tile.js index bb8438939..b65434bac 100644 --- a/ui/js/component/file-tile.js +++ b/ui/js/component/file-tile.js @@ -1,5 +1,6 @@ import React from 'react'; 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'; @@ -22,14 +23,15 @@ let FilePrice = React.createClass({ componentDidMount: function() { this._isMounted = true; - lbry.getCostInfoForName(this.props.uri, ({cost, includesData}) => { + 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 }); }, @@ -56,12 +58,14 @@ let FilePrice = React.createClass({ export let FileTileStream = React.createClass({ _fileInfoSubscribeId: null, _isMounted: null, - _metadata: null, propTypes: { uri: React.PropTypes.string, - claimInfo: React.PropTypes.object, + metadata: React.PropTypes.object.isRequired, + contentType: React.PropTypes.string.isRequired, outpoint: React.PropTypes.string, + hasSignature: React.PropTypes.bool, + signatureIsValid: React.PropTypes.bool, hideOnRemove: React.PropTypes.bool, hidePrice: React.PropTypes.bool, obscureNsfw: React.PropTypes.bool @@ -76,14 +80,10 @@ export let FileTileStream = React.createClass({ getDefaultProps: function() { return { obscureNsfw: !lbry.getClientSetting('showNsfw'), - hidePrice: false + hidePrice: false, + hasSignature: false, } }, - componentWillMount: function() { - const {value: {stream: {metadata, source: {contentType}}}} = this.props.claimInfo; - this._metadata = metadata; - this._contentType = contentType; - }, componentDidMount: function() { this._isMounted = true; if (this.props.hideOnRemove) { @@ -103,7 +103,7 @@ export let FileTileStream = React.createClass({ } }, handleMouseOver: function() { - if (this.props.obscureNsfw && this.props.metadata && this._metadata.nsfw) { + if (this.props.obscureNsfw && this.props.metadata && this.props.metadata.nsfw) { this.setState({ showNsfwHelp: true, }); @@ -117,25 +117,30 @@ export let FileTileStream = React.createClass({ } }, render: function() { + console.log('rendering.') if (this.state.isHidden) { + console.log('hidden, so returning null') return null; } - const metadata = this._metadata; + 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 title = isConfirmed ? metadata.title : ('lbry://' + this.props.uri); + const title = isConfirmed ? metadata.title : lbryUri; const obscureNsfw = this.props.obscureNsfw && isConfirmed && metadata.nsfw; return (
- +
{ !this.props.hidePrice ? : null} - +

@@ -143,8 +148,9 @@ export let FileTileStream = React.createClass({

- - + +

{isConfirmed @@ -186,11 +192,9 @@ export let FileTile = React.createClass({ this._isMounted = true; lbry.resolve({uri: this.props.uri}).then(({claim: claimInfo}) => { - const {value: {stream: {metadata}}, txid, nout} = claimInfo; if (this._isMounted && claimInfo.value.stream.metadata) { // In case of a failed lookup, metadata will be null, in which case the component will never display this.setState({ - outpoint: txid + ':' + nout, claimInfo: claimInfo, }); } @@ -200,11 +204,13 @@ export let FileTile = React.createClass({ this._isMounted = false; }, render: function() { - if (!this.state.claimInfo || !this.state.outpoint) { + if (!this.state.claimInfo) { return null; } - return ; + const {txid, nout, has_signature, signature_is_valid, + value: {stream: {metadata, source: {contentType}}}} = this.state.claimInfo; + return ; } }); diff --git a/ui/js/lbry.js b/ui/js/lbry.js index 2105abfb4..029a984d5 100644 --- a/ui/js/lbry.js +++ b/ui/js/lbry.js @@ -1,5 +1,6 @@ import lighthouse from './lighthouse.js'; import jsonrpc from './jsonrpc.js'; +import uri from './uri.js'; import {getLocal, setLocal} from './utils.js'; const {remote} = require('electron'); @@ -219,23 +220,6 @@ lbry.getMyClaim = function(name, callback) { }); } -lbry.getKeyFee = function(name, callback, errorCallback) { - if (!name) { - throw new Error(`Name required.`); - } - lbry.call('stream_cost_estimate', { name: name }, callback, errorCallback); -} - -lbry.getTotalCost = function(name, size, callback, errorCallback) { - if (!name) { - throw new Error(`Name required.`); - } - lbry.call('stream_cost_estimate', { - name: name, - size: size, - }, callback, errorCallback); -} - lbry.getPeersForBlobHash = function(blobHash, callback) { let timedOut = false; const timeout = setTimeout(() => { @@ -251,16 +235,9 @@ lbry.getPeersForBlobHash = function(blobHash, callback) { }); } -lbry.getStreamAvailability = function(name, callback, errorCallback) { - if (!name) { - throw new Error(`Name required.`); - } - lbry.call('get_availability', {name: name}, callback, errorCallback); -} - -lbry.getCostInfoForName = function(name, callback, errorCallback) { +lbry.getCostInfo = function(lbryUri, callback, errorCallback) { /** - * Takes a LBRY name; will first try and calculate a total cost using + * Takes a LBRY URI; will first try and calculate a total cost using * Lighthouse. If Lighthouse can't be reached, it just retrives the * key fee. * @@ -274,7 +251,7 @@ lbry.getCostInfoForName = function(name, callback, errorCallback) { } function getCostWithData(name, size, callback, errorCallback) { - lbry.getTotalCost(name, size, (cost) => { + lbry.stream_cost_estimate({name, size}).then((cost) => { callback({ cost: cost, includesData: true, @@ -283,7 +260,7 @@ lbry.getCostInfoForName = function(name, callback, errorCallback) { } function getCostNoData(name, callback, errorCallback) { - lbry.getKeyFee(name, (cost) => { + lbry.stream_cost_estimate({name}).then((cost) => { callback({ cost: cost, includesData: false, @@ -291,6 +268,9 @@ lbry.getCostInfoForName = function(name, callback, errorCallback) { }, 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); }, () => { -- 2.45.2 From f8e3eff3789180cf8c90cf2fa7012b3d4074a592 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Mon, 10 Apr 2017 21:26:05 -0400 Subject: [PATCH 004/158] Update My Files for channel compatibility --- ui/js/page/file-list.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ui/js/page/file-list.js b/ui/js/page/file-list.js index 8134be11f..561cc6106 100644 --- a/ui/js/page/file-list.js +++ b/ui/js/page/file-list.js @@ -1,5 +1,6 @@ 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 {FileTileStream} from '../component/file-tile.js'; @@ -160,14 +161,14 @@ export let FileList = React.createClass({ seenUris = {}; const fileInfosSorted = this._sortFunctions[this.state.sortBy](this.props.fileInfos); - for (let {name, outpoint, metadata} of fileInfosSorted) { + for (let {name, outpoint, metadata: {stream: {metadata}}, mime_type, claim_id} of fileInfosSorted) { if (!metadata || seenUris[name]) { continue; } seenUris[name] = true; - content.push(); + content.push(); } return ( -- 2.45.2 From a80b2da5c2c1260915a7211185ffb8b67eabdb55 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Mon, 10 Apr 2017 21:29:07 -0400 Subject: [PATCH 005/158] Update Show page for channels --- ui/js/app.js | 4 +-- ui/js/page/show.js | 76 +++++++++++++++++++++------------------------- 2 files changed, 36 insertions(+), 44 deletions(-) diff --git a/ui/js/app.js b/ui/js/app.js index 76ee648bd..735eefea6 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -10,7 +10,7 @@ import StartPage from './page/start.js'; import ClaimCodePage from './page/claim_code.js'; import ReferralPage from './page/referral.js'; import WalletPage from './page/wallet.js'; -import DetailPage from './page/show.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'; @@ -277,7 +277,7 @@ var App = React.createClass({ case 'receive': return ; case 'show': - return ; + return ; case 'publish': return ; case 'developer': diff --git a/ui/js/page/show.js b/ui/js/page/show.js index 8f4d450c9..49aff7569 100644 --- a/ui/js/page/show.js +++ b/ui/js/page/show.js @@ -1,6 +1,7 @@ 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 {FileActions} from '../component/file-actions.js'; import {Link} from '../component/link.js'; @@ -16,22 +17,16 @@ var formatItemImgStyle = { var FormatItem = React.createClass({ propTypes: { - claimInfo: React.PropTypes.object, + metadata: React.PropTypes.object, + contentType: React.PropTypes.string, cost: React.PropTypes.number, - name: React.PropTypes.string, + uri: React.PropTypes.string, outpoint: React.PropTypes.string, costIncludesData: React.PropTypes.bool, }, render: function() { - var claimInfo = this.props.claimInfo; - var thumbnail = claimInfo.thumbnail; - var title = claimInfo.title; - var description = claimInfo.description; - var author = claimInfo.author; - var language = claimInfo.language; - var license = claimInfo.license; - var fileContentType = (claimInfo.content_type || claimInfo['content-type']); - var mediaType = lbry.getMediaType(fileContentType); + 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; @@ -46,7 +41,7 @@ var FormatItem = React.createClass({ - + @@ -63,7 +58,7 @@ var FormatItem = React.createClass({
Content-Type{fileContentType}Content-Type{this.props.contentType}
Cost

- +
@@ -75,17 +70,15 @@ var FormatItem = React.createClass({ var FormatsSection = React.createClass({ propTypes: { - claimInfo: React.PropTypes.object, + uri: React.PropTypes.string, + outpoint: React.PropTypes.string, + metadata: React.PropTypes.object, + contentType: React.PropTypes.string, cost: React.PropTypes.number, - name: React.PropTypes.string, costIncludesData: React.PropTypes.bool, }, render: function() { - var name = this.props.name; - var format = this.props.claimInfo; - var title = format.title; - - if(format == null) + if(this.props.metadata == null) { return (
@@ -95,41 +88,46 @@ var FormatsSection = React.createClass({ return (
-
lbry://{name}
-

{title}

+
{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.claimInfo.formats + // var formats = this.props.metadata.formats // return ({formats.map(function(format,i){ */} - + {/* })}); */}
); } }); -var DetailPage = React.createClass({ +var ShowPage = React.createClass({ + _uri: null, + propTypes: { - name: React.PropTypes.string, + uri: React.PropTypes.string, }, getInitialState: function() { return { metadata: null, + contentType: null, cost: null, costIncludesData: null, - nameLookupComplete: null, + uriLookupComplete: null, }; }, componentWillMount: function() { - document.title = 'lbry://' + this.props.name; + this._uri = uri.normalizeLbryUri(this.props.uri); + document.title = this._uri; - lbry.claim_show({name: this.props.name}, ({name, txid, nout, value}) => { + lbry.resolve({uri: this._uri}).then(({txid, nout, claim: {value: {stream: {metadata, source: {contentType}}}}}) => { this.setState({ outpoint: txid + ':' + nout, - metadata: value, - nameLookupComplete: true, + metadata: metadata, + contentType: contentType, + uriLookupComplete: true, }); }); - lbry.getCostInfoForName(this.props.name, ({cost, includesData}) => { + lbry.getCostInfo(this._uri, ({cost, includesData}) => { this.setState({ cost: cost, costIncludesData: includesData, @@ -141,21 +139,15 @@ var DetailPage = React.createClass({ return null; } - const name = this.props.name; - const costIncludesData = this.state.costIncludesData; - const metadata = this.state.metadata; - const cost = this.state.cost; - const outpoint = this.state.outpoint; - return (
- {this.state.nameLookupComplete ? ( - + {this.state.uriLookupComplete ? ( + ) : (

No content

- There is no content available at the name lbry://{this.props.name}. If you reached this page from a link within the LBRY interface, please . Thanks! + There is no content available at {this._uri}. If you reached this page from a link within the LBRY interface, please . Thanks!
)}
@@ -163,4 +155,4 @@ var DetailPage = React.createClass({ } }); -export default DetailPage; +export default ShowPage; -- 2.45.2 From 562f7dd399cca5b7bb5bdbcd81ecf89c830de1a7 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Mon, 10 Apr 2017 21:30:02 -0400 Subject: [PATCH 006/158] Update Watch page for channels --- ui/js/app.js | 2 +- ui/js/page/watch.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/js/app.js b/ui/js/app.js index 735eefea6..7e8559be7 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -259,7 +259,7 @@ var App = React.createClass({ case 'help': return ; case 'watch': - return ; + return ; case 'report': return ; case 'downloaded': diff --git a/ui/js/page/watch.js b/ui/js/page/watch.js index 9d7bdb75b..ac270d77a 100644 --- a/ui/js/page/watch.js +++ b/ui/js/page/watch.js @@ -15,7 +15,7 @@ var WatchPage = React.createClass({ _outpoint: null, propTypes: { - name: React.PropTypes.string, + uri: React.PropTypes.string, }, getInitialState: function() { return { @@ -27,7 +27,7 @@ var WatchPage = React.createClass({ }; }, componentDidMount: function() { - lbry.get({name: this.props.name}).then((fileInfo) => { + lbry.get({uri: this.props.uri}).then((fileInfo) => { this._outpoint = fileInfo.outpoint; this.updateLoadStatus(); }); -- 2.45.2 From 326493c0b72cd536033953cd6666be08de17893e Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Mon, 10 Apr 2017 21:45:41 -0400 Subject: [PATCH 007/158] Publish: use new discover() RPC method --- ui/js/page/publish.js | 71 +++++++++++++++++++++---------------------- 1 file changed, 34 insertions(+), 37 deletions(-) diff --git a/ui/js/page/publish.js b/ui/js/page/publish.js index 019783587..74bc623b4 100644 --- a/ui/js/page/publish.js +++ b/ui/js/page/publish.js @@ -1,5 +1,6 @@ import React from 'react'; import lbry from '../lbry.js'; +import uri from '../uri.js'; import FormField from '../component/form.js'; import {Link} from '../component/link.js'; import Modal from '../component/modal.js'; @@ -169,49 +170,45 @@ var PublishPage = React.createClass({ rawName: rawName, }); - var name = rawName.toLowerCase(); - - lbry.resolveName(name, (info) => { + const name = rawName.toLowerCase(); + lbry.resolve({uri: name}).then((info) => { if (name != this.refs.name.getValue().toLowerCase()) { // A new name has been typed already, so bail return; } + lbry.getMyClaim(name, (myClaimInfo) => { + lbry.getClaimInfo(name, (claimInfo) => { + if (name != this.refs.name.getValue()) { + return; + } - if (!info) { - this.setState({ - name: name, - nameResolved: false, - myClaimExists: false, + const topClaimIsMine = (myClaimInfo && myClaimInfo.amount >= claimInfo.amount); + const newState = { + name: name, + nameResolved: true, + topClaimValue: parseFloat(claimInfo.amount), + myClaimExists: !!myClaimInfo, + myClaimValue: myClaimInfo ? parseFloat(myClaimInfo.amount) : null, + myClaimMetadata: myClaimInfo ? myClaimInfo.value : null, + topClaimIsMine: topClaimIsMine, + }; + + if (topClaimIsMine) { + newState.bid = myClaimInfo.amount; + } else if (this.state.myClaimMetadata) { + // Just changed away from a name we have a claim on, so clear pre-fill + newState.bid = ''; + } + + this.setState(newState); }); - } else { - lbry.getMyClaim(name, (myClaimInfo) => { - lbry.getClaimInfo(name, (claimInfo) => { - if (name != this.refs.name.getValue()) { - return; - } - - const topClaimIsMine = (myClaimInfo && myClaimInfo.amount >= claimInfo.amount); - const newState = { - name: name, - nameResolved: true, - topClaimValue: parseFloat(claimInfo.amount), - myClaimExists: !!myClaimInfo, - myClaimValue: myClaimInfo ? parseFloat(myClaimInfo.amount) : null, - myClaimMetadata: myClaimInfo ? myClaimInfo.value : null, - topClaimIsMine: topClaimIsMine, - }; - - if (topClaimIsMine) { - newState.bid = myClaimInfo.amount; - } else if (this.state.myClaimMetadata) { - // Just changed away from a name we have a claim on, so clear pre-fill - newState.bid = ''; - } - - this.setState(newState); - }); - }); - } + }); + }, () => { // Assume an error means the name is available + this.setState({ + name: name, + nameResolved: false, + myClaimExists: false, + }); }); }, handleBidChange: function(event) { -- 2.45.2 From 7e8fdf625f6683f82130c575637d89fc09ca6190 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Mon, 10 Apr 2017 21:48:10 -0400 Subject: [PATCH 008/158] Style fixes and cleanup - Remove a couple of unused files from lbry.js - Couple of style fixes --- ui/js/lbry.js | 8 -------- ui/js/page/discover.js | 2 +- ui/js/page/publish.js | 13 ++++++++----- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/ui/js/lbry.js b/ui/js/lbry.js index 029a984d5..f2f201de6 100644 --- a/ui/js/lbry.js +++ b/ui/js/lbry.js @@ -305,14 +305,6 @@ lbry.getMyClaims = function(callback) { lbry.call('get_name_claims', {}, callback); } -lbry.startFile = function(name, callback) { - lbry.call('start_lbry_file', { name: name }, callback); -} - -lbry.stopFile = function(name, callback) { - lbry.call('stop_lbry_file', { name: name }, callback); -} - lbry.removeFile = function(outpoint, deleteTargetFile=true, callback) { this._removedFiles.push(outpoint); this._updateFileInfoSubscribers(outpoint); diff --git a/ui/js/page/discover.js b/ui/js/page/discover.js index 762c55d3c..678310338 100644 --- a/ui/js/page/discover.js +++ b/ui/js/page/discover.js @@ -84,7 +84,7 @@ var FeaturedContent = React.createClass({

Featured Content

- { this.state.featuredNames.map((name) => { return }) } + { this.state.featuredNames.map(name => ) }

diff --git a/ui/js/page/publish.js b/ui/js/page/publish.js index 74bc623b4..8493dae1a 100644 --- a/ui/js/page/publish.js +++ b/ui/js/page/publish.js @@ -340,12 +340,15 @@ var PublishPage = React.createClass({

LBRY Name

- lbry:// + { - (!this.state.name ? '' : - (! 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'}.))) + (!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?
-- 2.45.2 From f64ad6e169f6da5d29f59f45ff4834b6a2763e0e Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Mon, 10 Apr 2017 22:42:20 -0400 Subject: [PATCH 009/158] Fix merging error in FileActions --- ui/js/component/file-actions.js | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/js/component/file-actions.js b/ui/js/component/file-actions.js index 314b2353b..1a535099a 100644 --- a/ui/js/component/file-actions.js +++ b/ui/js/component/file-actions.js @@ -261,7 +261,6 @@ export let FileActions = React.createClass({ componentDidMount: function() { this._isMounted = true; this._fileInfoSubscribeId = lbry.fileInfoSubscribe(this.props.outpoint, this.onFileInfoUpdate); - lbry.getStreamAvailability(this.props.uri, (availability) => { lbry.get_availability({uri: this.props.uri}, (availability) => { if (this._isMounted) { this.setState({ -- 2.45.2 From c49d229eef308efe31506bb75b73a77aca3b0ff8 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Tue, 11 Apr 2017 00:12:34 -0400 Subject: [PATCH 010/158] Publish: name resolution bugfixes --- ui/js/page/publish.js | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/ui/js/page/publish.js b/ui/js/page/publish.js index 8493dae1a..82ef6bdf8 100644 --- a/ui/js/page/publish.js +++ b/ui/js/page/publish.js @@ -171,44 +171,50 @@ var PublishPage = React.createClass({ }); const name = rawName.toLowerCase(); - lbry.resolve({uri: name}).then((info) => { + lbry.getMyClaim(name, (myClaimInfo) => { if (name != this.refs.name.getValue().toLowerCase()) { // A new name has been typed already, so bail return; } - lbry.getMyClaim(name, (myClaimInfo) => { - lbry.getClaimInfo(name, (claimInfo) => { - if (name != this.refs.name.getValue()) { - return; - } + lbry.resolve({uri: name}).then((claimInfo) => { + if (name != this.refs.name.getValue()) { + return; + } - const topClaimIsMine = (myClaimInfo && myClaimInfo.amount >= claimInfo.amount); + 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.amount), + topClaimValue: parseFloat(claimInfo.claim.amount), myClaimExists: !!myClaimInfo, - myClaimValue: myClaimInfo ? parseFloat(myClaimInfo.amount) : null, + myClaimValue: myClaimInfo ? parseFloat(myClaimInfo.claim.amount) : null, myClaimMetadata: myClaimInfo ? myClaimInfo.value : null, topClaimIsMine: topClaimIsMine, }; if (topClaimIsMine) { - newState.bid = myClaimInfo.amount; + newState.bid = myClaimInfo.claim.amount; } else if (this.state.myClaimMetadata) { // Just changed away from a name we have a claim on, so clear pre-fill newState.bid = ''; } this.setState(newState); + } + }, () => { // Assume an error means the name is available + this.setState({ + name: name, + nameResolved: false, + myClaimExists: false, }); }); - }, () => { // Assume an error means the name is available - this.setState({ - name: name, - nameResolved: false, - myClaimExists: false, - }); }); }, handleBidChange: function(event) { -- 2.45.2 From 5577b436d3531fe1ff1ad19468022bff598a2067 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Tue, 11 Apr 2017 00:14:52 -0400 Subject: [PATCH 011/158] Use resolve API method on splash screen --- ui/js/component/splash.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/js/component/splash.js b/ui/js/component/splash.js index bba92e288..8de7e1bbf 100644 --- a/ui/js/component/splash.js +++ b/ui/js/component/splash.js @@ -27,8 +27,8 @@ var SplashScreen = React.createClass({ isLagging: false }); - lbry.resolveName('one', () => { - window.sessionStorage.setItem('loaded', 'y') + lbry.resolve({uri: 'lbry://one'}).then(() => { + window.sessionStorage.setItem('loaded', 'y') this.props.onLoadDone(); }); return; -- 2.45.2 From d7644c03942fbf0457cfa8c06fb879da39453faf Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Tue, 11 Apr 2017 00:15:46 -0400 Subject: [PATCH 012/158] My Files: include channel names in URIs --- ui/js/page/file-list.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/ui/js/page/file-list.js b/ui/js/page/file-list.js index 561cc6106..2d2cfdb18 100644 --- a/ui/js/page/file-list.js +++ b/ui/js/page/file-list.js @@ -161,13 +161,19 @@ export let FileList = React.createClass({ seenUris = {}; const fileInfosSorted = this._sortFunctions[this.state.sortBy](this.props.fileInfos); - for (let {name, outpoint, metadata: {stream: {metadata}}, mime_type, claim_id} of fileInfosSorted) { + for (let {outpoint, name, channel_name, metadata: {stream: {metadata}}, mime_type, claim_id} of fileInfosSorted) { if (!metadata || seenUris[name]) { continue; } + let fileUri; + if (channel_name === undefined) { + fileUri = uri.buildLbryUri({name}); + } else { + fileUri = uri.buildLbryUri({name: channel_name, path: name}); + } seenUris[name] = true; - content.push(); } -- 2.45.2 From a7050303c2ca523930023e397f8a9110e893510a Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Tue, 11 Apr 2017 00:21:02 -0400 Subject: [PATCH 013/158] My Files: pass signing info into file tiles --- ui/js/page/file-list.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ui/js/page/file-list.js b/ui/js/page/file-list.js index 2d2cfdb18..ba91835e7 100644 --- a/ui/js/page/file-list.js +++ b/ui/js/page/file-list.js @@ -161,7 +161,7 @@ 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} of fileInfosSorted) { + for (let {outpoint, name, channel_name, metadata: {stream: {metadata}}, mime_type, claim_id, has_signature, signature_is_valid} of fileInfosSorted) { if (!metadata || seenUris[name]) { continue; } @@ -174,7 +174,8 @@ export let FileList = React.createClass({ } seenUris[name] = true; content.push(); + hidePrice={this.props.hidePrices} metadata={metadata} contentType={mime_type} + hasSignature={has_signature} signatureIsValid={signature_is_valid} />); } return ( -- 2.45.2 From 6af5fde4941032b7d0138a264957d8410b9bcba9 Mon Sep 17 00:00:00 2001 From: Alex Grintsvayg Date: Tue, 11 Apr 2017 10:38:32 -0400 Subject: [PATCH 014/158] external daemon, no more submodules --- .appveyor.yml | 18 +++++++ .gitmodules | 6 --- appveyor.yml | 55 ------------------- build/DAEMON_URL | 1 + build/build.ps1 | 34 ++++++++++++ build/build.sh | 49 ++++++++++------- build/fix_submodule_urls.sh | 13 ----- build/prebuild.sh | 12 ++++- build/release_on_tag.py | 54 ++++++------------- build/reset.sh | 21 -------- build/set_build.py | 28 ---------- build/set_version.py | 11 +--- build/zip_daemon.py | 28 ---------- daemon/build.ps1 | 28 ---------- daemon/cli.onefile.spec | 58 -------------------- daemon/cli.py | 7 --- daemon/daemon.onefile.spec | 77 --------------------------- daemon/daemon.py | 4 -- daemon/gmpy-1.17-cp27-none-win32.whl | Bin 158318 -> 0 bytes daemon/miniupnpc-1.9.tar.gz | Bin 71648 -> 0 bytes lbry | 1 - lbryum | 1 - 22 files changed, 112 insertions(+), 394 deletions(-) create mode 100644 .appveyor.yml delete mode 100644 .gitmodules delete mode 100644 appveyor.yml create mode 100644 build/DAEMON_URL create mode 100644 build/build.ps1 delete mode 100644 build/fix_submodule_urls.sh delete mode 100755 build/reset.sh delete mode 100644 build/set_build.py delete mode 100644 build/zip_daemon.py delete mode 100644 daemon/build.ps1 delete mode 100644 daemon/cli.onefile.spec delete mode 100644 daemon/cli.py delete mode 100644 daemon/daemon.onefile.spec delete mode 100644 daemon/daemon.py delete mode 100644 daemon/gmpy-1.17-cp27-none-win32.whl delete mode 100644 daemon/miniupnpc-1.9.tar.gz delete mode 160000 lbry delete mode 160000 lbryum diff --git a/.appveyor.yml b/.appveyor.yml new file mode 100644 index 000000000..52e3c6879 --- /dev/null +++ b/.appveyor.yml @@ -0,0 +1,18 @@ +# Test against the latest version of this Node.js version +environment: + nodejs_version: "7" + GH_TOKEN: + secure: LiI5jyuHUw6XbH4kC3gP1HX4P/v4rwD/gCNtaFhQu2AvJz1/1wALkp5ECnIxRySN + +skip_branch_with_pr: true + +clone_folder: C:\projects\lbry-app + +build_script: + - ps: build\build.ps1 + +test: off + +artifacts: + - path: dist\*.exe + name: LBRY diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 0b6ffe898..000000000 --- a/.gitmodules +++ /dev/null @@ -1,6 +0,0 @@ -[submodule "lbry"] - path = lbry - url = https://github.com/lbryio/lbry.git -[submodule "lbryum"] - path = lbryum - url = https://github.com/lbryio/lbryum.git diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 299ed1563..000000000 --- a/appveyor.yml +++ /dev/null @@ -1,55 +0,0 @@ -# Test against the latest version of this Node.js version -environment: - nodejs_version: "6" - GH_TOKEN: - secure: LiI5jyuHUw6XbH4kC3gP1HX4P/v4rwD/gCNtaFhQu2AvJz1/1wALkp5ECnIxRySN - -skip_branch_with_pr: true - -clone_folder: C:\projects\lbry-electron - -# Install scripts. (runs after repo cloning) -install: - # needed to deal with submodules - - git submodule update --init --recursive - - python build\set_version.py - - python build\set_build.py - # Get the latest stable version of Node.js or io.js - - ps: Install-Product node $env:nodejs_version - # install modules - - npm install - - cd app - - npm install - - cd .. - # create daemon and cli executable - - cd daemon - - ps: .\build.ps1 - - cd .. - # build ui - - cd ui - - npm install - - node_modules\.bin\node-sass --output dist\css --sourcemap=none scss\ - - node_modules\.bin\webpack - - ps: Copy-Item dist ..\app\ -recurse - - cd .. - # copy executables into ui - - ps: Copy-Item daemon\dist\lbrynet-daemon.exe app\dist - - ps: Copy-Item daemon\dist\lbrynet-cli.exe app\dist - -build_script: - # build electron app - - node_modules\.bin\build -p never - # for debugging, see what was built - - python build\zip_daemon.py - - dir dist - - pip install -r build\requirements.txt - - python build\release_on_tag.py - -test: off - -artifacts: - - path: dist\*.exe - name: LBRY - - - path: dist\*.zip - name: lbrynet-daemon diff --git a/build/DAEMON_URL b/build/DAEMON_URL new file mode 100644 index 000000000..ea9f7e7d9 --- /dev/null +++ b/build/DAEMON_URL @@ -0,0 +1 @@ +https://github.com/lbryio/lbry/releases/download/v0.9.2rc3/lbrynet-daemon-v0.9.2rc3-OSNAME.zip diff --git a/build/build.ps1 b/build/build.ps1 new file mode 100644 index 000000000..3df3d9b7c --- /dev/null +++ b/build/build.ps1 @@ -0,0 +1,34 @@ +pip install -r build\requirements.txt +python build\set_version.py + +# Get the latest stable version of Node.js or io.js +Install-Product node $env:nodejs_version + +# install node modules +npm install +cd app +npm install +cd .. + +# build ui +cd ui +npm install +node_modules\.bin\node-sass --output dist\css --sourcemap=none scss\ +node_modules\.bin\webpack +Copy-Item dist ..\app\ -recurse +cd .. + +# get daemon and cli executable +$daemon_url = (Get-Content build\DAEMON_URL -Raw).replace("OSNAME", "windows") +Invoke-WebRequest -Uri $daemon_url -OutFile daemon.zip +Expand-Archive daemon.zip -DestinationPath app\dist\ +dir app\dist\ # verify that daemon binary is there +rm daemon.zip + +# build electron app +node_modules\.bin\build -p never +$binary_name = Get-ChildItem -Path dist -Filter '*.exe' -Name +$new_name = $binary_name -replace '^LBRY Setup (.*)\.exe$', 'LBRY_$1.exe' +Rename-Item -Path "dist\$binary_name" -NewName $new_name +dir dist # verify that binary was built/named correctly +python build\release_on_tag.py \ No newline at end of file diff --git a/build/build.sh b/build/build.sh index c41d2b67c..60eb60759 100755 --- a/build/build.sh +++ b/build/build.sh @@ -7,7 +7,18 @@ ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )" cd "$ROOT" BUILD_DIR="$ROOT/build" +LINUX=false +OSX=false if [ "$(uname)" == "Darwin" ]; then + OSX=true +elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then + LINUX=true +else + echo "Platform detection failed" + exit 1 +fi + +if $OSX; then ICON="$BUILD_DIR/icon.icns" else ICON="$BUILD_DIR/icons/lbry48.png" @@ -32,7 +43,6 @@ if [ "$FULL_BUILD" == "true" ]; then set -u pip install -r "$BUILD_DIR/requirements.txt" python "$BUILD_DIR/set_version.py" - python "$BUILD_DIR/set_build.py" fi [ -d "$ROOT/dist" ] && rm -rf "$ROOT/dist" @@ -62,24 +72,17 @@ npm install # daemon and cli # #################### -( - cd "$ROOT/daemon" - - # copy requirements from lbry, but remove lbryum (we'll add it back in below) - grep -v lbryum "$ROOT/lbry/requirements.txt" > requirements.txt - # for electron, we install lbryum and lbry using submodules - echo "../lbryum" >> requirements.txt - echo "../lbry" >> requirements.txt - # also add pyinstaller - echo "PyInstaller==3.2.1" >> requirements.txt - - pip install -r requirements.txt - pyinstaller -y daemon.onefile.spec - pyinstaller -y cli.onefile.spec - mv dist/lbrynet-daemon dist/lbrynet-cli "$ROOT/app/dist/" -) -python "$BUILD_DIR/zip_daemon.py" - +if [ "$FULL_BUILD" == "true" ]; then + if $OSX; then + OSNAME="macos" + else + OSNAME="linux" + fi + DAEMON_URL="$(cat "$BUILD_DIR/DAEMON_URL" | sed "s/OSNAME/${OSNAME}/")" + wget --quiet "$DAEMON_URL" -O "$BUILD_DIR/daemon.zip" + unzip "$BUILD_DIR/daemon.zip" -d "$ROOT/app/dist/" + rm "$BUILD_DIR/daemon.zip" +fi ################### # Build the app # @@ -91,12 +94,18 @@ python "$BUILD_DIR/zip_daemon.py" ) if [ "$FULL_BUILD" == "true" ]; then - if [ "$(uname)" == "Darwin" ]; then + if $OSX; then security unlock-keychain -p ${KEYCHAIN_PASSWORD} osx-build.keychain fi node_modules/.bin/build -p never + if $OSX; then + binary_name=$(find "$ROOT/dist" -iname "*dmg") + new_name=$(basename "$binary_name" | sed 's/-/_/') + mv "$binary_name" "$(dirname "$binary_name")/$new_name" + fi + # electron-build has a publish feature, but I had a hard time getting # it to reliably work and it also seemed difficult to configure. Not proud of # this, but it seemed better to write my own. diff --git a/build/fix_submodule_urls.sh b/build/fix_submodule_urls.sh deleted file mode 100644 index 67e024b55..000000000 --- a/build/fix_submodule_urls.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -# https://github.com/lbryio/lbry-app/commit/4386102ba3bf8c731a075797756111d73c31a47a -# https://github.com/lbryio/lbry-app/commit/a3a376922298b94615f7514ca59988b73a522f7f - -# Appveyor and Teamcity struggle with SSH urls in submodules, so we use HTTPS -# But locally, SSH urls are way better since they dont require a password - -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -cd "DIR" - -git config submodule.lbry.url git@github.com:lbryio/lbry.git -git config submodule.lbryum.url git@github.com:lbryio/lbryum.git diff --git a/build/prebuild.sh b/build/prebuild.sh index 93f63bb59..d7d86d4e9 100755 --- a/build/prebuild.sh +++ b/build/prebuild.sh @@ -72,7 +72,6 @@ if ! cmd_exists pip; then fi if $LINUX && [ "$(pip list --format=columns | grep setuptools | wc -l)" -ge 1 ]; then - #$INSTALL python-setuptools $SUDO pip install setuptools fi @@ -88,3 +87,14 @@ if ! cmd_exists node; then brew install node fi fi + +if ! cmd_exists unzip; then + if $LINUX; then + $INSTALL unzip + elif $OSX; then + echo "unzip required" + exit 1 + # not sure this works, but OSX should come with unzip + # brew install unzip + fi +fi diff --git a/build/release_on_tag.py b/build/release_on_tag.py index 255a88364..96dd57057 100644 --- a/build/release_on_tag.py +++ b/build/release_on_tag.py @@ -1,6 +1,5 @@ import glob import json -import logging import os import platform import subprocess @@ -10,15 +9,13 @@ import github import requests import uritemplate -from lbrynet.core import log_support - def main(): try: current_tag = subprocess.check_output( ['git', 'describe', '--exact-match', 'HEAD']).strip() except subprocess.CalledProcessError: - log.info('Stopping as we are not currently on a tag') + print 'Stopping as we are not currently on a tag' return if 'GH_TOKEN' not in os.environ: @@ -27,20 +24,15 @@ def main(): gh_token = os.environ['GH_TOKEN'] auth = github.Github(gh_token) - app_repo = auth.get_repo('lbryio/lbry-app') - daemon_repo = auth.get_repo('lbryio/lbry') + repo = auth.get_repo('lbryio/lbry-app') - if not check_repo_has_tag(app_repo, current_tag): - log.info('Tag %s is not in repo %s', current_tag, app_repo) + if not check_repo_has_tag(repo, current_tag): + print 'Tag {} is not in repo {}'.format(current_tag, repo) # TODO: maybe this should be an error return -# daemon = get_daemon_artifact() -# release = get_release(daemon_repo, current_tag) -# upload_asset(release, daemon, gh_token) - app = get_app_artifact() - release = get_release(app_repo, current_tag) + release = get_release(repo, current_tag) upload_asset(release, app, gh_token) @@ -60,21 +52,18 @@ def get_release(current_repo, current_tag): def get_app_artifact(): + this_dir = os.path.dirname(os.path.realpath(__file__)) system = platform.system() if system == 'Darwin': - return glob.glob('dist/mac/LBRY*.dmg')[0] + return glob.glob(this_dir + '/../dist/mac/LBRY*.dmg')[0] elif system == 'Linux': - return glob.glob('dist/LBRY*.deb')[0] + return glob.glob(this_dir + '/../dist/LBRY*.deb')[0] elif system == 'Windows': - return glob.glob('dist/LBRY*.exe')[0] + return glob.glob(this_dir + '/../dist/LBRY*.exe')[0] else: raise Exception("I don't know about any artifact on {}".format(system)) -def get_daemon_artifact(): - return glob.glob('dist/*.zip')[0] - - def upload_asset(release, asset_to_upload, token): basename = os.path.basename(asset_to_upload) if is_asset_already_uploaded(release, basename): @@ -84,30 +73,26 @@ def upload_asset(release, asset_to_upload, token): try: return _upload_asset(release, asset_to_upload, token, _curl_uploader) except Exception: - log.exception('Failed to upload') + print 'Failed uploading on attempt {}'.format(count + 1) count += 1 def _upload_asset(release, asset_to_upload, token, uploader): basename = os.path.basename(asset_to_upload) - upload_uri = uritemplate.expand( - release.upload_url, - {'name': basename} - ) + upload_uri = uritemplate.expand(release.upload_url, {'name': basename}) output = uploader(upload_uri, asset_to_upload, token) if 'errors' in output: raise Exception(output) else: - log.info('Successfully uploaded to %s', output['browser_download_url']) + print 'Successfully uploaded to {}'.format(output['browser_download_url']) # requests doesn't work on windows / linux / osx. def _requests_uploader(upload_uri, asset_to_upload, token): - log.info('Using requests to upload %s to %s', asset_to_upload, upload_uri) + print 'Using requests to upload {} to {}'.format(asset_to_upload, upload_uri) with open(asset_to_upload, 'rb') as f: response = requests.post(upload_uri, data=f, auth=('', token)) - output = response.json() - return output + return response.json() # curl -H "Content-Type: application/json" -X POST -d '{"username":"xyz","password":"xyz"}' http://localhost:3000/api/login @@ -118,7 +103,7 @@ def _curl_uploader(upload_uri, asset_to_upload, token): # half a day trying to debug before deciding to switch to curl. # # TODO: actually set the content type - log.info('Using curl to upload %s to %s', asset_to_upload, upload_uri) + print 'Using curl to upload {} to {}'.format(asset_to_upload, upload_uri) cmd = [ 'curl', '-sS', @@ -141,21 +126,16 @@ def _curl_uploader(upload_uri, asset_to_upload, token): print stderr print 'stdout from curl:' print stdout - output = json.loads(stdout) - return output + return json.loads(stdout) def is_asset_already_uploaded(release, basename): for asset in release.raw_data['assets']: if asset['name'] == basename: - log.info('File %s has already been uploaded to %s', basename, release.tag_name) + print 'File {} has already been uploaded to {}'.format(basename, release.tag_name) return True return False if __name__ == '__main__': - log = logging.getLogger('release-on-tag') - log_support.configure_console(level='INFO') sys.exit(main()) -else: - log = logging.getLogger(__name__) diff --git a/build/reset.sh b/build/reset.sh deleted file mode 100755 index b8ad8f145..000000000 --- a/build/reset.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash - -set -euxo pipefail - -ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )" -cd "$ROOT" - -( - cd lbry - git tag -d $(git describe) - git reset --hard origin/master -) - -( - cd lbryum - git tag -d $(git describe) - git reset --hard origin/master -) - -git tag -d $(git describe) -git reset --hard HEAD~1 diff --git a/build/set_build.py b/build/set_build.py deleted file mode 100644 index 3467154ab..000000000 --- a/build/set_build.py +++ /dev/null @@ -1,28 +0,0 @@ -"""Set the build version to be 'dev', 'qa', 'rc', 'release'""" - -import os.path -import re -import subprocess -import sys - - -def main(): - build = get_build() - with open(os.path.join('lbry', 'lbrynet', 'build_type.py'), 'w') as f: - f.write('BUILD = "{}"'.format(build)) - - -def get_build(): - try: - tag = subprocess.check_output(['git', 'describe', '--exact-match']).strip() - if re.match('v\d+\.\d+\.\d+rc\d+', tag): - return 'rc' - else: - return 'release' - except subprocess.CalledProcessError: - # if the build doesn't have a tag - return 'qa' - - -if __name__ == '__main__': - sys.exit(main()) diff --git a/build/set_version.py b/build/set_version.py index a47264159..80b777d89 100644 --- a/build/set_version.py +++ b/build/set_version.py @@ -39,20 +39,13 @@ def get_version_from_tag(tag): def set_version(version): - package_file = os.path.join('app', 'package.json') + root_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) + package_file = os.path.join(root_dir, 'app', 'package.json') with open(package_file) as fp: package_data = json.load(fp) package_data['version'] = version with open(package_file, 'w') as fp: json.dump(package_data, fp, indent=2, separators=(',', ': ')) - with open(os.path.join('lbry', 'lbrynet', '__init__.py'), 'w') as fp: - fp.write(LBRYNET_TEMPLATE.format(version=version)) - - -LBRYNET_TEMPLATE = """ -__version__ = "{version}" -version = tuple(__version__.split('.')) -""" if __name__ == '__main__': diff --git a/build/zip_daemon.py b/build/zip_daemon.py deleted file mode 100644 index c8ed738d3..000000000 --- a/build/zip_daemon.py +++ /dev/null @@ -1,28 +0,0 @@ -import os -import platform -import subprocess -import sys -import zipfile - - -def main(): - tag = subprocess.check_output(['git', 'describe']).strip() - zipfilename = 'lbrynet-daemon-{}-{}.zip'.format(tag, get_system_label()) - full_filename = os.path.join('dist', zipfilename) - executables = ['lbrynet-daemon', 'lbrynet-cli'] - ext = '.exe' if platform.system() == 'Windows' else '' - with zipfile.ZipFile(full_filename, 'w') as myzip: - for executable in executables: - myzip.write(os.path.join('app', 'dist', executable + ext), executable + ext) - - -def get_system_label(): - system = platform.system() - if system == 'Darwin': - return 'macos' - else: - return system.lower() - - -if __name__ == '__main__': - sys.exit(main()) diff --git a/daemon/build.ps1 b/daemon/build.ps1 deleted file mode 100644 index 7768aee27..000000000 --- a/daemon/build.ps1 +++ /dev/null @@ -1,28 +0,0 @@ -$env:Path += ";C:\MinGW\bin\" - -$env:Path += ";C:\Program Files (x86)\Windows Kits\10\bin\x86\" -gcc --version -mingw32-make --version - -# build/install miniupnpc manually -tar zxf miniupnpc-1.9.tar.gz -cd miniupnpc-1.9 -mingw32-make.exe -f Makefile.mingw -python.exe setupmingw32.py build --compiler=mingw32 -python.exe setupmingw32.py install -cd ..\ -Remove-Item -Recurse -Force miniupnpc-1.9 - -# copy requirements from lbry, but remove lbryum (we'll add it back in below) and gmpy and miniupnpc (installed manually) -Get-Content ..\lbry\requirements.txt | Select-String -Pattern 'lbryum|gmpy|miniupnpc' -NotMatch | Out-File requirements.txt -# add in gmpy wheel -Add-Content requirements.txt "./gmpy-1.17-cp27-none-win32.whl" -# for electron, we install lbryum and lbry using submodules -Add-Content requirements.txt "../lbryum" -Add-Content requirements.txt "../lbry" - -pip.exe install pyinstaller -pip.exe install -r requirements.txt - -pyinstaller -y daemon.onefile.spec -pyinstaller -y cli.onefile.spec \ No newline at end of file diff --git a/daemon/cli.onefile.spec b/daemon/cli.onefile.spec deleted file mode 100644 index 125e3c5f5..000000000 --- a/daemon/cli.onefile.spec +++ /dev/null @@ -1,58 +0,0 @@ -# -*- mode: python -*- -import platform -import os - - -cwd = os.getcwd() -if os.path.basename(cwd) != 'daemon': - raise Exception('The build needs to be run from the same directory as the spec file') -repo_base = os.path.abspath(os.path.join(cwd, '..')) - - -system = platform.system() -if system == 'Darwin': - icns = os.path.join(repo_base, 'build', 'icon.icns') -elif system == 'Linux': - icns = os.path.join(repo_base, 'build', 'icons', '256x256.png') -elif system == 'Windows': - icns = os.path.join(repo_base, 'build', 'icons', 'lbry256.ico') -else: - print 'Warning: System {} has no icons'.format(system) - icns = None - -block_cipher = None - - -a = Analysis( - ['cli.py'], - pathex=[cwd], - binaries=None, - datas=[], - hiddenimports=[], - hookspath=[], - runtime_hooks=[], - excludes=[], - win_no_prefer_redirects=False, - win_private_assemblies=False, - cipher=block_cipher -) - -pyz = PYZ( - a.pure, - a.zipped_data, - cipher=block_cipher -) - -exe = EXE( - pyz, - a.scripts, - a.binaries, - a.zipfiles, - a.datas, - name='lbrynet-cli', - debug=False, - strip=False, - upx=True, - console=True, - icon=icns -) diff --git a/daemon/cli.py b/daemon/cli.py deleted file mode 100644 index 075c13b7f..000000000 --- a/daemon/cli.py +++ /dev/null @@ -1,7 +0,0 @@ -from lbrynet.lbrynet_daemon import DaemonCLI -import logging - -logging.basicConfig() - -if __name__ == '__main__': - DaemonCLI.main() diff --git a/daemon/daemon.onefile.spec b/daemon/daemon.onefile.spec deleted file mode 100644 index ea42f5289..000000000 --- a/daemon/daemon.onefile.spec +++ /dev/null @@ -1,77 +0,0 @@ -# -*- mode: python -*- -import platform -import os - -import lbryum - - -cwd = os.getcwd() -if os.path.basename(cwd) != 'daemon': - raise Exception('The build needs to be run from the same directory as the spec file') -repo_base = os.path.abspath(os.path.join(cwd, '..')) - - -system = platform.system() -if system == 'Darwin': - icns = os.path.join(repo_base, 'build', 'icon.icns') -elif system == 'Linux': - icns = os.path.join(repo_base, 'build', 'icons', '256x256.png') -elif system == 'Windows': - icns = os.path.join(repo_base, 'build', 'icons', 'lbry256.ico') -else: - print 'Warning: System {} has no icons'.format(system) - icns = None - - -block_cipher = None - - -languages = ( - 'chinese_simplified.txt', 'japanese.txt', 'spanish.txt', - 'english.txt', 'portuguese.txt' -) - - -datas = [ - ( - os.path.join(os.path.dirname(lbryum.__file__), 'wordlist', language), - 'lbryum/wordlist' - ) - for language in languages -] - - -a = Analysis( - ['daemon.py'], - pathex=[cwd], - binaries=None, - datas=datas, - hiddenimports=[], - hookspath=[], - runtime_hooks=[], - excludes=[], - win_no_prefer_redirects=False, - win_private_assemblies=False, - cipher=block_cipher -) - - -pyz = PYZ( - a.pure, a.zipped_data, - cipher=block_cipher -) - - -exe = EXE( - pyz, - a.scripts, - a.binaries, - a.zipfiles, - a.datas, - name='lbrynet-daemon', - debug=False, - strip=False, - upx=True, - console=True, - icon=icns -) diff --git a/daemon/daemon.py b/daemon/daemon.py deleted file mode 100644 index 2ed0360ab..000000000 --- a/daemon/daemon.py +++ /dev/null @@ -1,4 +0,0 @@ -from lbrynet.lbrynet_daemon import DaemonControl - -if __name__ == '__main__': - DaemonControl.start() diff --git a/daemon/gmpy-1.17-cp27-none-win32.whl b/daemon/gmpy-1.17-cp27-none-win32.whl deleted file mode 100644 index 5d15f0efa8b8bd5b29940094a9d35d4709a0dbcc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 158318 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

G|?lH+Za5R zXL#f=lK!cQhDYn!htU<{78LC8l;PcRg_dv2r@*pWPAnfu+y_Xl`4EXeTzFkko+0!+ z+&_7eW$(S;5bGXz-TO?qH-K>81y4?7Lb1vq-v_r9H!IP7Gqk^XjRu=?M-sT$`JZbfrf>i7OB0O{}HV$28g#_fe(TanFaU0<1<^Kk4YWPnZhW@xOVRg!~7qPE>sn%k1|KL|##PivC{CRG@ zN0>jtNNg>3ufs#X+6L4ULuNqVs#OH~nkh&#K?6lBRoM0;&nJ{jvguzaOkJ(zUYXW1b+3@r>$=w9_ zQiwJKo+w6@39xRS9z7EeJ;!~Tf$bVni)xd&TbSmI=nhF90P<}Yvl11p@-$7JJT8Po zSkuF%AM1e7+mNt2!$#Q}0hM>-a|&T~D);n1biv=D|2Vh-+?WSO$Nkf_pYw;{=%jv-X$ig#1V#WK>71&s3*ua;n?OA21NTd0XuZYx; zU13o%uQsOJS1@WX;K$!N=;QE^`7{h#3v1wqH9)`8G*D)hE3YzqRQWNpVj>7o)w1W}jwUeoO zJKgh4V(ENnI#?spDnVL?Yqs=zkOt!{vhYsAbGJ-SEw|!ANVl~!03NZ^SDsP2)^0go z(Vv0vPOrli?gxS#w3*9+Mo0c$96p$W2a0!+9_DV_!rW~qiLF&0cOCTBa?-HjRv;7e zNmi8fnK`feb7!2;}Hw$3q2gn0F27P>3|`oBSdI-E56G}}Ny zW}@Pd=8ODRX@#(8F<3a#VqtNr|IC56+JO~Bc#YnJHk@53?(&in$=z)^=pTS;Rk}x1 z5)r>;-a#8W)yX8i|Hm^dQFwV&7B-WprO7wYV|UTz*a3Gp2BfXVDw-d*!A(EjI+B^T zPIfcU(Nw`sqV(sVD*92j0k4~Xzvr{VF1Im`3ypWMp>a)+O&w4@Pfv9=9%3>E4 zJuTUqpey@eg#nwa5J(mC)ErY>*I-=R^<=e(DhY<{ju3~Q^pgUu=9Qx5gamTN20$5@ z0?>nLvjj|dLKa3?sw&P$2ks&BeqEz%+DNL*wfHPpm2%Pzt*3YX68g0VkWYB9UA7%{ z(9fPqSd(>@KzsQ`Ah@A%oS9B|&I42P)KeTaw_@liT2YR#s{w0dbqS!MS`z3qSA(br zcA$4Z#fV)64}y~1Yu*R# z!BWL!;kWXH-;;w>j~R}B%AO6+fyP|s9=mKB0n0#X{7If+8216XjLAh$l6b9-$**c5 z_zm+nbfVx6P>)mAZ*kDQ{DIK6Y$=C3Pr@z(`UrtlZY2jB~H9ophE#qeUd{}PAFV{-%t zo`ke zZv~mS+OmImXDYY$ke*bl6Qgue|Q6%K!m{x$SYEYaA>6wI0()4&KX)j!9b zv_h!CpMUskJPCCd^T$mW%~e$``F1j0?}P_C_Ok~UWyd=*_?aB^`2Ibn_lYjl#el^lyUSACIBYNAcB zM$c}WJ!%`!vTS;g-3MzFV}2u9mz z#Mx53v&4451v+jEbH|&j?Ck2RKx?0s9z}q=eGF%!O{_P0&17++k_pshV(h7<_2MF zU$X{3No^3&&A4B{Pg7K!$<#T5C6Fq>7nRpgY(j zmKUryR(&Z3zZS!0m|cr4<`;?^!Xg>OAfTS?efUVj7u;{LxADt@(viWM!u5q_Yayr! zc!B=|&=H_UZni|rhdm6cx3ut*UF$`%uklNB)Lb36cEe5K$+?<-_z2nC@CDhYwEI4a z*-J`0&HL6DLMyrxiKlh&6(*s?*1gPRwsk9jy9RnxjeOUov$h78Mqts0^YV7Nst2k5 z4Z__UUIGHA6F9>J_t#>@e9dA>lqYMIUEq4dM>~rvLB<5D;vQ*8isxgcPkF!U|ET(J z{U2Xu`9HS)*Zz;+Ft*`}PBM}jfUi0IA7fMGudR?wW_Y={!Q%yNmdpZ}ETSN_LO;qU z)LIWj4(W`3z2X;!pTos?!t+mqv59bTGhD2=1g+5R34wIvW-gF0v%m=VtcQC@zgboP z%$Fxjl58!_))ono?Wuma~VY3^;`_W%s0Wx+VmjlY~p$?-XClGN{4N>%hRW-vi?ag z%K9c|I<-4#!DEW7Z<74jM}U`V2gk7v(wDuea{zNeTB~~UgEe`U8pESg(D}I6T*n8O z9NA|Ad+_Lh2M%F$i4E@9PBuKtvE%^VM*(xf069KAChIpLiajPz_zoFwO~+$G8&wdh+|wWKsfK$%WJ1s^IHHhqy92_!RnE%6r&dOOWcYJYBYk| zl#W&O13l?+xSZj>&iZaGlpd zuRuTGZXmztVc%dd65{E)(cDqoZ0-Qv(j=RnBc))8d+2O5_3F?DwME*nu$#FCosRS~ zVFx3JeSF$7=QMj7>EWgHOw%ZQm-ONk&==jw2m`FT=}~nKGwxf(?rT+%K>G&ezE)7= zpvQp`93B?_k!{pLpE(T%X@E-B7F`dCDyA^Zo9aop!p&EOzx25c>ZIE4l|m@#Q1T5h)Ns|0Hyv4=FwHuxZ$O<0O>t{;!C&rA8Q! z2D=0UEezf#XaapfrPkzKfo?;Nk$f`G%5v9-8hk_L8CpxdZ!ne>!E_QO&jF!|RZJCG zmoCi2P?_;Jt~oGY=RTp<+R4;tbNRKkR?6gK-w#kahQ|g^kFGGY$Q)&&aYT9)=pSB1 zG&aKxOfLFG+#~)nGwyi&Jb~rIE7hL>;~PF&u%`Qc<}KQV)~j*_V4ArKfIw)s-z1xE zBKl8xyjzR$?i1l_K&m%eJ7xV%Nr&fA{U zT9B`uihQ*h*~SaW-wyBYQ`6^)>Cxk$Ea2_6)>bwlyL~sv!bhp@sjHnyZfkYYKa*tc zm63pb=#F^-_uWO*+$*mgB*WNU%!GJgq6Rs{-!B-~`h@xNJS|YjFi@p?r9jN^u}Mox zsY)M!&c3PC zS&oY=d{FgezU2_mDcJMn=Y_h;`vPvPx~a5!@uFZ|9R4Ln`0;08`Z8?N`0siB4+c-g z{RR*q7OUv%PmJ)@cOu#lo`E}5fK2eAh^5*$ScwM#B%KJXXzB2G&W3h^d*hA&9(B-P zsI>4qcteA~04#9$0a&Ta@Zm0VbyX~HF|@TvvWdE?n=<$%bv#n3m^vgZ=)#617kwZL z8_WB{rIE^z*ifdXbYTQ`q1rE|j5Vz%q*Tn=8J08yBQ&D?P{ua|#|MuOOg;LuFIYQhTqyxN>o* zYY}K1B$qt6<&tO6R zesH|T*TJk9$u2-IqeqH&l2aw3zwhMWg!LNVNiYO%u|xwV*7{b~`sa)LnKoli4$KWm z$&!LV0nAUqL7ua4(0s@@NY-z`Bd}l{s}zKh@;C^##gb_=tf{$Xku(`!>z|!=gMRa@ z{`=+GT1medtfrq%hYs`)3Y)e5QScEctW=y)VhI!gAwB*ndu#M~EnG&kX_dgzyi|Xa zsA$pq1vX)!c=`W8V?)^6K45SAqF>Qw>3;qc8x0tXY(OQS{=fkNQc#9dyfb<{eGcbX zq6V3rx03Q)v|*HKGdySrVM?s*jl)ug-ejw^rF%ZEP&~Op(c{Sc*h{}7wfi_((Z*4S z7J}OZ z`JT9R6(F$_#SJA3wxClD@!cUHoy?R=Cclhn`B~h*4`K=IzW!ZSqt>5K^Kp|{zzB+g zp86fj2pR>Af(q413kAl<@@=gNT^X6jbtRgO#SQY*c+_uHQtHt{5;0SL2q;V6V9VHw zejYnr-WZREe@s@2;UA}ki~3RhY!x8eVh&mnSI-0$4jKsCc8p zMu*FeXN~@yRW&ma%-e+N7#R$7YDkTetyyBzgZ#Hf0QJ&*{wRPA4il_FC5#$s2E#pd zY^Hf>I3VedwTtM21ZKe$Q@=jcx1!R(*ELh(?%W`nJxJAh( zUe*Vm!CS^_`~@39TFydBin3>)tIX*y9h;6nS%T^!0W}b*Ev^R3ms{*g@;IqhVK{QK9^IbSPWW=b zTrK;?$3tDK^-5s5#7!}~s0GIc0!neWR?Qk!C7T{JH|Zk9J0&-8JlN?}7A``E1}HLX z%dAoRqD{jqM_D^1TdS^0SDTu#C(cY}>>ah!`_?3rzgjTGAETe9_nEV@)pt#(%U6hA zSdM2he6hB67iLR>eYb|C?vv6^1?~AM#?T}FVbGx0E(`D5Y2*Q%Dlh=IvU?)z3QGZ8 zqH%cWew<0$0pe~-%j~N&*9Qaj8s-DXh=L&5lktwP9#G;P$?2}1lM(Mow=dibC226v z&qMANa9;q~SG*Udsd%U51X#I>7n}t%PvU}GBCvAgCOufaz{C{=<_L@!qgN4248Fxc zPot#}168wj7Y3nD^3pSM(Y0y2EcEoz=`2=jjj7gd&uVq*L0*ax^6Xx9uD07p|c~H zF(1SnI>{?C-;=dnTCD7(cxAORg-LAwz-%>%PUL}-)QkT!J7azkuUqv5rt?Z^EV)OJ z27$Vk?iR>BqU1n6mhKkGJ$eZ`W7kV}!w3DMRDKV(k513O#5{OlrifSaA7!x}7?(i; zuoUt4rMn-4*{B3z2dp+jkY+&N>JxpdouzbqHP9E`f@zcHsbX;hx)Y`X5{pf>#0*bt zljj!4fkq`;WcH|oy8XI?=^3qbnv(xuM;{Kou|my%;0q)RZvfrPrj59{#~rhr^qVlq zJ-C7ySIp9BLD}by1h>U~MpBodKoMKTJksoLwkamUl3P2OA7LLl7=aKu`9H_VzX=mt zk81d;iJ%==xR3I+`#PYkU2+Px&&-sk8Sx-BIXPg&cyqJ=V$6HA(@`y15$5}pra<)m zLBGX&)AwcFJXg7y>@D6MxQ2K!4_k8A%kDZHpBO!6m;8Gq+W~rty(ldlZB(DN zV>r4Q!_i)`*4o6w(JcXE(BGmZbwFFB7CKK(D)N{GrpZh_rW{NXF;;~k=}z2H;66$Q zkvj8yaSEt2kYgZjBwM@T(NFStO66A6o$gM#{LO9@NeiCpCW9?Q{C6l{<^WEe7yL$; ziD9T_p8yNuLoR0yg4rm*$Um(wYvwLH(MC0!_FW4r`gjxHRN%mGt zgk?jR-Xsx^v+XN^IbHF|a(Jt|(*k{1c0LKjuvN?-IwS=;$!T(oM9Bi^ULZE5WWjTA zXR9GJ5V}7v{4>0{{JCzeA@n`VNbU@lzt`a3vyL)<@L-W-hae8W2aFT5WxNm$1p9EK0hg$Luol6d;~|e)l7$W+s5nA%UOO6u1mGK#_sfDwNgC8j~q1|FYU+K&D; zAUpj-E?IaGx5POeK$q0N1Bn7uX1m?adP%D3{!3wwN~x|XIf#${3IeSuxP7El3S`-K z4Ho}Df__+>d|?y%d*I(IE!!&2CspQcx*Fo|l!bLDy0)3CGFCxP<2T?cNCC>yEt(wk z&ij;w;C-=JTgxxB1Y*8HI4jnpxDC*(9K_VoHWpc_3k0046RGGz7J!3QVkt*QG0qrY ziSWP@jVWXG@*>3JZ*Kd3UQwf;I26hzm z)d|wWAy3#!_R`;mw7|F&(R4@K2iWW&bG5EU?dTBbpn0EscbrTph#r!z!~VhKp5Qws z5f;SC6?sY4q)#+r}~<)Kf+lMbsY>M+9L9_ z_qq{v_rv_>Sxi3Iz#1S3GCP+md2N6&YkYj?7uXDN6R3KO-AAO!px2tXUb|pLRvJtD zXw z+`LQHZ*Y)Z^mm_Yl_GIhEBnYj6~oN?Wnm*-mAEu)F{xe@ZGwyR_Qa3*%%EHi+D&VN$>IM6wy!)D7s!VH+XQiVF8j6h_0%c4)#ru?_gS`KY4GTi}Ik zrXQT7rh_mGlW+UNoRm7!M+f!=%}9-W-;#h!Xj5M9PrxO&5u;!LE|y0zWZcfGtp)Ni zVA<})c#embe~VXQybc?ze34&S`9ya*Zpgr!>Q+gwqW=S;<+Sfdq%Wd?Ir>S8wpDWv zSNX8=DN8(@AxbMU4R*d?A0bh3gU50NwERrbvWu+HOaAWFKf#RPrhtBt1k%DQvS2UY z=enqNhRFN~GX#EO+!1L$Cg7n2?qH_F30c@&o<~j)!(__|_F*p=Fpb4-QP!_7H^G=K zRlWfmk&G@!M{Je9lhiO=(*%tYYd5+`J%Hu6^3!hAgIBs8c2yf}eo?7N<{JQ0#iQp} zYFDJBSLQt52k04dHoVoUd#hLy#emzc)=e&+P2i5)UoY*AbkO-rbW1;5qz68W! zH`p+{F@LC=?6-UyFiEADQwW%|iPT6oBl(uu+M+yNTcQmN09jeRjh*a*)g?+<78xMO zNg}yV#C;iVP)J6Dj1>ngCw!AVPP*U$Ez?vrsQqQ%duqM{I-|vz!LNbrS9b&hdW}Ke z4?i<`vzU3Qa7vsYfWJM)c+La;zMiQ!BW^)&|A;37V!MN|-K+-7-@&G$O^L=Z-M|{d zhhI;5_&j_VmacL7dDEWa{z{g|RCeaHYOHvm<&$vI|(IJ8&@! z&+Y|koALs4bg(Xpx`^5{^r0C+t@sOx(XxNpVDm1#$q?KdSFf|Dg%Nq&s}2YsxAre) zd;66;en|iSP?^>LA=&*`dS5<5?fu&4SuuDNOu~DMb*leW>H9F&cawRSWWdGct|Wd3 zdwTjsdCJpZdQM}#uT1!GGx-bD&@XvxVt)HiTe9^?H*@XU%~$spaVcPbKd7kneK>E#^XV@ub@ilhl2Z3 z`!QJQhb0PR)`$uM4Br^p2CqWTO^j$QR*DvtJAy;K5l#HSbkNmBmeQDSh^%M(R~#;- z@CBpTa385Of?J^gt(e$#r=(Tl>rE{6QIHmjVV~uQ|LRl^IT%G>Ud5es9MH5n=#9Tt z0S&y*4;FFE6TT8ZmMA4X)B-4*uSyHTU#vcdd)vJ(JB|E`n@%k0+fwg8ALek7$LXR` z%rBqTfoWyo0XUg1dh657)6Os(Zp+a1C=+;GDTH!A+ayP9EwYKF(5IB~;KBJz?R4L- zvWleq>6(OVPuWJFdi9}ea^z=VzBmT^01<&uTh21pe0On@b&)0}WtxMr_=@YiTvA*5 zR-mLlu{CwG`)bjJ6guX&%0>#6LZP|EI*hBhyKgc)y^URXx~h1;FF~`5()+t8o4-uy z{KX@aKun7THA&^{Nw5@e;tnteO}Hs&7dj_ zHS?|AHwsJvBsSZ4lZAoSzOPKSk)QIzBm#@sSXkzj2|92ID8!4%kSS!JG$eRRyR1LT zXE55eEShXI*P&^E$xDk__L8D};l1|=Y4xpm|EBz){0m}rJ(Wk#~o;IcBa$DC1qK(5xXkO=2hwo;eN$@`j7j$oBtlPqtOTk zWwf{$V_ZBa2l5(4^7!}oHk3+#0gKp9#$qp_u5!T~WBZJNJr_%PwUY%*CeN$AP zKgGL{nQzV9&vjnAZnuLDz6F!hG19uvH$>MX3m;(qNXGrRKWPsbOYQ@*aE;xyL<0GLk1TA>ZZCW*>*#FedF~|cI=e&H=%ijY804?P*!`?M z)6g8c%n!)=YaGxAr=7ZaAHpWSUzk|;IN(wf=(rbn?qu-%0oe%eS8bF?ui_;icV+`D`pbs$Dq~@QlanqT-{e zGh{{1vjgXO>G93#5Rgs|c$^B60FZdwrwrk!_P!4Y7&dMS7OWU*Fl@Mtod*g1UxVql z`z2c|4&q95wO3r6F^oKnhr0APb5^e40w zFrN@wU0o0iP>o^3V;T(`&-e|!jK|RXcv_e7AdON0vL&?BH-wlT1mk8FywgU5pKE3P z6d-``Frb8)Xvzr>R?!i%X=bgJj$mRfg5eu{fTBM;VpzL4&d)LXP=jVLUz!2QrNP{$ zU2)vo)4FoLy1xkezf$g{|MOq(wg01+DeKY7)&uBNU3TF?JG43N5sfAXnR@BzzwujbtT%%C#!faq(hFO?^7mfZYW2M-MAKG-r&7b?hP4kX17BRK zQRKu0z*{;H6`A!FQr1?6r@2Gce{V+%wQV^9KwtT z!2o)OA0`=IG`up{k{7IuuUw#BdQiDEm2GIgp17Nw*r%HwM{2Hv_9bDcVkRaX{pC}^^Kppm~ZAT@y;LzfdgCQmD2At-SUW)U{8?f~lQ%G3HRSmY6)ci^f}v+t+G zW&-#Kt?nkZv|tJMTJa{V(8TzRCj6r1qKX2ShmLrSW1o45?WIKmz3682i30KMNt)l# z#&(YA?Kdlk{^bOs|M8_6h@Rs$J5x-UD+9;_C9m~w%C=x0?+}yq0m|Lzq0>oLJ-{&+ zedJZ%r5Pewg}%_i$~L=2wNn|KXC6!ycCJ>BF!A+4%iFI*B(<;QmAKKiWPIQvubl>7 zLDv||WxQKt=}#hA$Z-EIX(c<_Y8SL`-VG3@gV(n*^%-T-BE5g3mF(UAZ{*vKCRS_Z zkCX%$Kn06n7NW<=KCp6PmVE#rD)LCxBo+-zDPd4RUmclDu8Y`d(a#f3AWW?5^f=gb z($2mZqKzpYB)CI;{#PT@s^}iQG3i8VN5RCO2fBR|TfjIi4zENTFoL41_$#w>$j_CO zc1vx!#awHbg^xiheBhvWLTmDTG&@5(%ZreQPn!?Q!Ul5>8JE|8>?~>AcqICrfe7(; zhG$}z51H$Al}=ryY!aQiYCARG$g~ZLqN@1HQJ8{u5LHVfHgj+z=KYNu%bGIsroJLk zmrJF<@7JpoC`iIv(^+B=7?EwGG57(1?I1Zp_K_G0lqyy+IkmvBeMDN3;;My-se#+k z;|n9^v-`U~I5*Hq(Gsh;9j2(HBrp`D_M(@$Hxu4;qe$)*`8~L|4&URDO=H;@XDmz| z<3((YIJ{%%kI#!99EHOxMV`$Z00UB-0W^96 z(CFd6@S}HuGZu=#4(KzOA6FJcebG2$=4+VGFH!TT)2qmqB(?6kBzOJeEPe$0(9S}h zz>JvXilrdHbXO7|G0oA2+xH(S;>W>0@;}Iun2K$91dXSXv4J0Z>~t^=12ML!?lHo& zbA)XB6BRr5`G%8&=0?z)&)LyxreAsZW;j+yI#?Yytj7dN^C7swypQqsguMRa=SUN@ zO!xvFGWSmEFu%B%53{5&@MC~|<-1+?PzM>C;7OEhRVR zX$sDuct4Qkbbv%kaREoQ>iz$Bj{P0{AmbXlpC0?U^UPvjQh_RGBslG6vP*QrXwYWh+h z#uu?B4Q5Y@CDWeb*^?k4K{j3lvhkLexNMxlI1h;4-N^|Avrl5MepdiJf41Nyv}HY*y3Hg6eDzWu>T|oEkQQo@7@`BYY+?2-Ph8 zTuAK2q^>Mk@bN{d^)0jcALhZSiU6*UXNpSAPhcI%-H9ssQicB?K>j}s#8kc5Zt%R%6}T5~lp{>r*^-jxG106fM!g+_1H2yE{VWn2a-!Ex@O0bL^Vnchql_0TuMfrj6G=dn77LbBhP6*%BO?L@U~0bi ztWwEaba@=~{p=Qw59KYe1-2aakAW4H<_P2hCkUte{tS~fhh={ zxVOOF3L3nIK5gRBWQx@kTR<=OpQlhM`dUIeeIp#Kn1G#rzK#XXu?&QL^Q7==6jLut9tUlT1p8@gDel|X`v}4qU~;R(uZux9Ssq+ z4__O3MDoK>W8q;S!vpy+qVNFBct#Zx zv+TcDhr2e8mlyg6;5j05pX#>E-O_z;KltLcvl19K>T+NS#vOm;71jbG;R@6w7hQ#} z9Mll3W)0R8>m<`QcQ;bp*%WQkHI<+e4h#T-S@$%uaXa!xgKrob!@F|U7=OJZlt#<^MH^V^3g;By3f@4(dep2yys=|Jg(Up*ivH#%r! z4qL9fDsHo;R3RWC+=9Z6$K#h zSN?&s&s32VvbejK-u5EjrB=tb_+crp>L^yWv3tNO+O&*e7tGfvQo{0Y>h|&oOsd$p z;{t_&&_Dnk6G_dS0KgRJNB=e`^?08VY`~%m6dzXLi_i_H<8hSLWE=45F9j5r?5e{< z8;`zyF*8RkCc$$?t%U^L z0W2yI_%?WjELqyLe(1n`Pc(iQ$lY28KWui8dMBOV#L}ScSfH7X{E2`M7#t474on z%lPg8L)^Q-H&teP!%4GS0^N`R0g6Mrt~G0)`=5*6J@4SJME?fmahH|93M z{439K!RQe|7q}u^E$Bi8;Tl2b_jp1>lu`TB=~%AvNc%vyiCFnXZWGaw{e$Fq5`*(| z0ZCNd+AGW`FU}G)FLw%}?696KSN&G5dx7S;WN9)qLERVfPn3T7e`Lm;3Mj2gW9$ww zkZX6CgM)*wpB6JwPq1n9<(Z2!6{7{+9$fI#4ZGv#|H0j2(Ctx;tH|KWbBihv1lue@ z^}HajI*`!p$v{2^V6S&*@&ccx?GAOC`uqNh`P=M87v(b}uCzeVW=+kb5@oHx1={3? z^+=|NVeDD2J}9E>RiZbo@5Q;AzO`R0QuhW7v5L&PKM z|LS`tZK^fANHe-s0GS&D$5+dTMgA}Z#Ni=~1N+Y42)4*5>ET3I(sOIOY5id|V8g$| z)ql-2?>TBp)?P#KGFg03w#+cSfqnu|y=2Ukmn}7hR=My&(;Mw_A$-x!mKjZNv`VAZ z#>2gWY5gZT7`wa$@jP zZScRNs}hi@+}37312~~H;5quN0h^v0g94FHdFGk^LH`yN#AQZ4xr-A;&_`!8U;7@@xoVG4Qiv&lYw`kos=1sAn*|jo9 z*+CfkMh?+q+8Sle;b@ATY5xLbB@ae33O8EMU@UsDn;kK2K+M-Iq%lR=uIN!S($b1# zOzgK$fK1ddnXpr%(M}`V8SORJ9_4?t_UQQnLp|sTtYh(4YII2ytMS=1f_DuG||7`W>P5_4&eJ$EgfK-=goz&>2 z0EpSZ!9XXfk*7Zi<5a#pkJmCiEIJFuIhR7UdtTQEuYviL%WBTc(`#j)==S-PzqFzN z6^CuSU0fVKi69cM>jTGXS0#~aEI=z9qLcQ+8L9jqUD+b%Fn#5OL2W5c6ZA zx9{U#8tL{Z^DU~i8cjxtwabwOid%4t)qsRKmk^p(pIBbGOa(DzeQl?#33S?i8uN9a zNd5j(1oPyVEpNH$0VY^QnI9@Su^V+D;Qv=Xh3k5qT2cRgtnJ36B_6H>9Fmww@uiSI7pYXDMJ*v<8%s< z_JLBxXV-^GX|!L8I^)gijEho5ToY>?DSOB2Wz5@cMUA6Jo|hweJ9(3q7peb|=cxZ{ zt+1tR=YXf=Z$kJ$11KxOa|B=AnRX;4F^58a=#~SB!12;>6ek2G>@AF?^{asn7{cdQ z{%s^)4TwZdiGTs&Vhxc)fsw?o+Tb|Og6b1U&nkK%JE|T)(Bcwra_w|7lXerFDl^(E zQmd~*$lM;DHF!As35iCwRFTAY%4cJUqBpsEKd7oc*wU>|<=3fndSwm}^ODbSZ?~dP z$B2KV(`~C#pw%GJedu{g9wXXzP=`dcZ$f`Se+VY=>-#@J>j~7S4hZ#LEr=uTQxloW zUQnFQJ&8xmIyq zZb}>j87F$|iv0Rggq2Gdt+T493p6cYG}XV-=@x&0#M!@6FVF5%ZlMHaW=>x`;dJS` zAiCSoRsG0c;Yt{_C((_E6CTYWp>?K8+G^8}LQ&Hb7f>A4Ise@N0_t4AtDxC+sW{%ls@>S9Kml{!r6Axw^ALv_#NA8x-- zsfSk{=-6e@*XXGnd2Tvuj5wT;UuVj~SXiS|In{zaDV5*DhiYeh%6N&QOv8;L_2ML6 z?nAxxEl4L}SiKu)5)4Vu1C^Pi{uO4XK?j^}wFSg>ze}m(2_P#qPPw1ehwHvUS6jR( ztG+`1CAJHz0jlPYc>)(0d4q`Ay@D|=^nKZO!n76mAGADcrQR)Ja;}t;$G`p?rr+bY zPTPpXp0%G<6E^Z)R<%-Il!l2e38tzRs8hw7fDi6s``b@A-HMzbMZHC2ITR^10ned|dCpTtHXEV2;KE0RX zvBYx@QP1Kl^}fLUD9(D6KdnNkGwJht$iTVvK9Hga+(i=J<{<{p*TG9aA~5tQPu0?0 zq_tTVra2{XGf-Q;sZ>ng&K#k@sM z_|thGiROl8ts$&&7uxb&hDN8~V~8u4QDD;HKs}%ui+=O{ zi|i;5=`LwsRa4Q1jBK%h31kqU;1_kaLzqXV!QK%3nToh@ju00J-~ePV<8rxNzpNk7 z0MQIotOYX3BBI-js@)jGU-e{azP)eVwX36e`#LmSFU*G@fdpmmc(oq-ciW`;0TnGD z82=-3uQRgyf-oQKvF|sn--jA?!nA&uj)QxtFkn9v5m6jHx{^{TRedWfV=4K8bmCa} zq#hlSQV(9qgm)4@G0Wo3_7;>C5NAG#DHU?zlZTFx=wM`1p+UKjp)1WpAgg-($}Dsa zG=Y3;XhHI8vbU@$=6d~E)?Ph9mB+;wXG!9Vh*GpB&%UqfR4m7Kh|u_qh6Z%fa~rkQ ztu(Y^9BPkFav_M<5eg8|V7e&X>+)rnw&z#|;y+Pj4Z_!_JdxMT3wnK5J58XhOAES>}*=D@3 z#EXj}(5q(~DX2UXYkGtyWkOIcZI^Go;i6ss^KbNn@!~Fl-v~125r0i_C|9}7K|BBx z@7!<9Ml!rjtq6%$sYrRmVRocooJGT(dU^UNYupf(`~r_ym}?vmcREyAoz$xqWpr*k zK8@=iM_`_-hvTdr@yX&VE@$jJ=urjiqlM(Vi+%emde9|hlUu2s#?faMV>WV(wR0(p zZo)@#e&Yqx@t0}pbo7s6eJXo8)Oev!foFc|P1?>~It6#ap{C$IQ1|Hx%(h+jFk0I_ zJphyRX$MRb?MpQ=cW?;Z+G0M9tkm3;^F~Y@n6M*!^gOf}jv6pax3ox5{!qpx?Q-MA z+Z+4$?|(ws9B-rlH!{TY`!+&&EEoKdx~kzBSd>Uz$U%x(@=;RbpqRZeAhKqkOZlXj z?_qBeUVsY8niB_Ik~gVr+y*={WM)lhx9-$~ybzlMe}jBR}Q6|dj$Z+8?%(v*=Y3+80q_7Ze_&7WNSwgc>4FpxlXrwt6@welSTz+5sWBx@#xhA7X>LfWRqpZ61hXM5FiTZySj;B?|e>1wpg=d`8mO`#vy z&%)jc_8_9y80|vCrz$EGRH)tzI;ER62zN>=k4dy*GobWM1AK&sDo^xi^pWJ->$Uqg zkSIVj-EloP-J?vf66E8TZE9SQ5!uK?Jnq}uEsLb6y0YWGRp-$C*Iz{C{+!hJ=exFC zoPUn!8xArUulWv8xgVF3w;rY$en24Lfd0DvMXd{HV@W}1n8&H)g}6a@&VcFT*BUg5 zum&)Xr?ci!jiengkJ8Pug=W&N&SP8+)xX~uyBSKC48YSIrrdf~pY0I*=5<-FRUS}r zYOd2}lA~i7U7*w5b_0zJCK6a24<`EWXEbu%MKfBN&|iFg!u3&@A01typ@M$xzpHeh zCRZ78D;{kTueK0%#b}QQhWVVzx*|>(Y<0A|L914Jex;TQPJ}+^q30Wl8GiVBd?Vd- zAZ9ud5+wLaSrxxoSM2yBVnn4v`5yk3@i%Bkql(aaWAynIV-f0cM$+Rn3Sc{@JU|5G zx{_cr=#{tPYulks@-Hs8z%P&T`6Ez^-=kOwe8PxrITcK+33FL47p^rNG8~dWyqq_N zWR%YD?y!h~Or_T^0eIYOt&=C62UZbSUC)kYOkiBc@4$nacsJ5c>$9HpP!FPcLm***yl zX95o}Dq8G3h~IUA(N5qyEd(N(QeiGcYjeQll7+{eO50i*w4sBL#dusp==JcY@j z>v*fK9ys3a%l?b;F4n2z4P89m@Vk7xhu$7I-UU}*G~O#u4;pXr0DjC$g^bq{desCw zRgWk~kLta&6tsak7|xDy?jOuRx-P2z3RQd;MGn>6%7SKl0uiTrK4%YGps*n5Mw z4$y3dDKEwJ*)GgOCufC3q5oM&F^(XK?nkJbi1vm6;^8^i!0;bee>F(V>x=u{eS5ew zG{POQVy^X&pDy9-VgNB{KfomKE71{1xwo5q-7{O>jV925wmr*a8K2Wzn=pbeB2m#e4?nfoX; z6z|P78fFUSUIjW>iBlgabopdqixb}8siHlVEPRIsavHHGEL+?jB{E5k7ES3YRbvMs zRedWSpymySSD2+ngfNSq%msygVSmDf1(OQj3`|7cU#%~h5gHB(#)vhUoiWcS6}}a- zVM37VadP0zl$!tM2jrW4Qu9rQg2SNTC#%&PTwr39(I3%F*do>YR%3 z63jFl4r91aZ(839PuH3-q^A*hNZPdVqa2;)UXw{?wJX|{3!JS(p2=O4)7*3Za0t`a z35s5Gz5(rW?Umg5hJWHXj=?wh)sZjtn)3}Tn)6E4`34<%K6Z1r>5bh~4J9G4Fdm8wHGGZ^3m}rp-otL? z<0o}Gzf4Ybhh2ZeOLV2{N$wd{7o5z}nF@YGAF9RXDBHD<0-wrr(vgPXFtnVRHa?E7 z6z!~0nbb!UinFlfz zy2@kQGOR>LYJz|R3FXVabY!1teIw2@cS`y=Lw)K_aOxB8%hAUD6pjYxaivbV{oiot z*{G|(C(&78joGz4tk#0qTriyaaD5ItZ|_@I?pI#WX+xTB+rt_YiO8PdwV7p760fL( z3zoua(7=FRYoVW2xpVD(Yd^>gYs$oo$5emg-*o8*`c)YwW;n<({N7*j4%??XJh!vB>5bFYefH?;;jlW;>n{djD%$G87jjI`)eW#GFT^TWl+HHk zJWA!yHFa)*`lRa#bzn7<*a88)uE{hrsoAC6n z&G)C+1=~5aR!Mih_ilN(A;-Qnwfff-Wo86v+y;~(&0n3U9 zj~UD>A6aoH{G;4O`vF9F*jbn2w;*VOi0f3MPtsK^GE{UzbSiZlkPvjcl>Zn?#498J z59SXbOdt+p0RbFh_I5u;?Cl9&59BN`G~AaqMg9rfK72uMiX1jz2vqio`Slm-(Sj;w z01ZRX{qT7#cpeJ=2@9s7;BT;CQ+C0-w1Q3f4X?xQtTmuP4X=IbY&#-*fZ_p1nAB|8 zf$_t8Lb-{9n2Z4$NC!s(M?(dWVaKHCG#(??+#Z7&tDb;z-h2=oFk)&%CfY=S4s>Ak4Q-08K`llM z<5gsLKwC$X*r~Lii0lC|)3hGhpe)>gLaQnlYo5Q)cj`6IUl3h~T&8;dqJJv$cFMwL z&|(5tKy?J;xt~H7yD^g0Tr!K_)y4Ah&7a4sa@}!V;JYZwqHkdb!r$Wtdd7iG{jJk7B1;GizW=4};<{sR71|c}h=LKazdN zvu2pm_?bSFz8xg`o5p8+_m^VpFbW4L*DuiU-vPMA*wZ;8FvY`*{FVnegU?Xy2O5rkm#6n!IqpB0&SP_ePI)^ z1-N@hrOc!K`8~N*U5L0WNqrzcoJLK`33MD7)H|$|& z)Y!!NXefh)3C?9BVYv#hx?Id-KN<){a6~2%JTTPY&!fzu8Lo$eDhZsy_84?>r)?K5*AR+Gc z9Bp7R5Tu>9-pHP+T*)iOh}eoKG(?ck0W&aT&eZSPJA&7$9)_k5>sd3#)dD?hKfx)Q z4X={0{9Sx{DpeCSOr)9nJ1-N$jlw*2^9OB+PoR4=ScH{ZSW8>4ZMUsuIqaEAYm{F` zFB1%hV)-z?Qia%#KtNhYF{1)BMXLhJ7OS^5sXapoGM{qvQF7qLoPM^?Fy`^iFrU_J z2T5)pQth)an+UI{Uj_$E)t$xgh4k8Mfs2C5;&C-Ox>037*NZhL&_Lep_9=fLP*yXq z)z7MpIGrXHcosX$PRACqahKXlm8^bQ|6bB23-{8Qwv+8K+ZotRu#;>%CY{ODW;;zA zYB}wwO4w1V@9u zx%OK0UXd%3Zf)QdJ<7vfX!Py|;qgs!J~DHYH--(d0^2!U)kU!JP)%J<J!Vr|8P znH5j-74NdORI(;m@#Wfz-;OI@p{;mq4kZT+PQU`g?VkmQR%%xjmD&vlwine0$CLWZ z&bFPjot3;sJc?%+8FOj6D{;!|m#b3-n*=D$ht(WK0yoJK+$5})=1eo>Yw;DLZ=3U< z1~WNAw<-4M8WSrQ!;Pqu#MSW&E6qF(?{$oN9SakJ&dK!1t!$6rC2EgTdE`epX+<0o z{*w1S8hXy#x{HatUBI_hf?-x zV{nRkbLNB&;7BxxL^He1=s^~pQrsg+TDD(NZSwe(>Hk7%j^r@M^3Wq6G1bj-*-Ow) zockQ8{wpZqiFgA(&kXAWggxB~3OC8cRmXs&T0<#WtYsF{Ad5@mggxP7IpO;BLm$%Z z&={?UK#=+`;6PR|qfu}G9v2Pqhk|AwCuAuDTOiVvo4gL4flDz))mVc#wL?JF?Tdby zOOmqL?{c!)&_@)azML2+;L_f1P#au`C-y0YsF~3l86BI>1xCpWK{yzY)Qcdqq&db) zEuIJS;Vw1%5j&7jqYQl3_C_NB5e^~rGraiwX=efdy zO1LrSz^d~Y_>>Ojr^kHUYA}Ng+*Bl&=V*fRJ)dM;hvF{f$cRjYEXc2-+vi06$fLYa z1)@{~Mu|K1PZ~?t?+Er<>mRjW58G8eVZrf!0@Pi3z2QiD%z9`E>X{NWOTlD%%qDCY zdnVtXDc6Q#`1Z+ef*vo$Qo9P8{feb`^i-R0C1fry_rP z^F=j|fEooBjfq%CEN?(5?#Jd5`PI-m(Fa`C??$O&>xX3NLg3OMp^44V2gc(b1zMu7 zMdzJvj9WSPC>AY%seyWj!<;1!fFQS6kmj2E@xR4>Uo5^KdiQBCNV&pG*11dvjuxgSuC5B~xiYsCjJJNML(u@yZOJWziPxmsyX z5Ex~o+=kyfwu3<0i6-K5IIT05SJJvIf@2HksN5_q5(xWt*&S;Cy?2nCluKENKhSVJ zhhDWhm7DJ%ca3G4_)ioDjD8@c8VQVt8;dZ&7S;h;U5R685P{&Zr1BNHJ)s$Uf^X&b zRO*DmuRdiqjnyJo4bl4sla;2-($IR84#a<&l?mi!4^jB@94-8LjoIEOIX+h7m{}7L zl16-@@Spwnt4*QN_CD>AvUxYZU*(Vb?^k==O5xCqX~2<0C+ODxE4`sg_3n(%BdlKB zs5;qwOqTVVL3Pjm*p>MhXS_lEI4DYvVbJNDGB|J0MNOKv664$^*^WO9l z?fTe_^a)m42M6P=M|6}v8tUZ{NWXeSJIk$ZD);nD98p0Fdf%O=lGH64N&S7>Ad;%` z*9sEhR;bt(WxS z=Ct-Fkdm>`NlCB9)3T`cCUJzPV;KQz=8wFbmUmDuI0&|vcjRZ}`)|yIErf1g}D$GPI|?2&Vw(uru5w`juRQrzp@|U0V^K+pALlm%S?W-{e)P=M!Ec z^B#UW?|P5d1RD&Qtf<`n9w()+^6>hP4_$wxQdO9h9);rRwEQU64upI|HZA3-szKtQR z)J$5chv%Y^fOJ+ImmHmNatjdXHrsG&_y;FZ82JD1$as2feWr~sA|_r?OBA*vl#9N0 z#|>@r%&GEhy*&wPU-dY&iD%XH*N=Hk@?5>WwR$|f9oW*#>U-ufnzoi1MXKmv6^Bh*mpq9XrK3l44K0Yo=B%aV^B!-Gk`)S#-Me_Dk6}V`3 zSl9xi8?$1TO(Rd**l6fHQX{vEEwaG#>+pHSVc2^p3YA)un14H3l@Hy;MC;|o*ihy( zI@uhH+eHAP%K53ZRq697APgm}!6a;a7zpO;-L16TLl+EqVXj)9H-;hN=u>0Xgqc7t-F5NG%k?8hp1~Ok~TsDZ_ zz?p=8T<+ucS=d}tvptp@VNab{mVF<7aXutJ>t$!qJohh@z@q8Z$`wccYyZQN=oo2~c2>PcrZ%@+0tyWBO)tEhy^x13EsWbAs0reiUAGx;&u5 zD_gGOl53Vr+=v#vinYfL59dtlkD!fdVE00zvJm^V7a`|HaTCGc`cy+zlC^@?dr`6r z)EvaOUCJ8x&YCbBk%eChw0@!{SN3j)Dtk0<{JVL52yQkYxm){oy@o`K1g1=Dwuc6X z_|tyeQ0Qp}!g=m!%{1l*l_~KJ!A>KXVK@Gq{%RN9`7~IY2%M`QTnf6gazJ+ue{<>X zlnw08d*2M`PLJP3YqU}a?C_ycbSsEpUiV__=xJy4Ltp+E^<^8`DW>zS{Y$Ec96|aQ z=Yi${{)$Wo6c>GA3p}RxU*4jCMzYiN#!ig>G+Wzbae~ipZ(C~&?@C)w8}`ll2(1dy zPBSPK7(R>WqbU|w?QZQS5vh51`?2;;!x`B!fm|X|~w}z8NFb`EPVUQMrmcck+a5<7$ z%>q4G{&Nuo1e@W5P2-V5DWw2+aUKaocEjp?HhRifmN(08So1X-8$D)@o-!NGOFv)+ z+a5z5x<5B1C|bvdDSEs{>v`4|KBkWzGa2gH=(a9H8Kq>YP zGbCBl^2&a^UkjabymmnMR7x%oS4O3?coh=U28`8+xAjqiC*id9h({JTJ6$|tMtO%m z7diILC<3R7dob>&g2ietKBmA<)Her`}Jg@rF+44(XU` zCwgq+P-m*G3&;aSGFGGlV?5YFti!2PPp7(R8&d?QEIi|PVzT$WQ|L+@6L4Ug@a^n! z47E)GH@BY+jD)SRQtxuHX5|ATO&SaK!2ZD)*Cb|JJN{jDD$)7!7VP;_ab=F%?@{!t z@uRtv|s^TMXBty?GsQ$3s$T)hrp?X-%l zsqI2r-(=|KqpI@$Q0}~Nb6+XYm*5?dLp44aZ{$#LGDmD%GlE4GeI1ez5v{E*62tHB zRW$?{w7nycj}&8OuNr`E^non>>Nk8-fQAt7Dk5N^0Xd!i@{v9Z;H;=O!!^u2&zEeu zF>)v{UKS?6Dq5V@R+lWi=v00-m0R6MXFse@W?II4E8k$`KCWi8GnXPL4IOMJ-O6A} z8{@o5df5Wh&gIA5a{c}2dXMqV&XsyH$0-LNpy(F#_ZThv((+=#P1}X#(Z--$Xbz(f zw#pAFZZd8~CT6A#m6W^-^W`=f4uvlig$(7fJWN>@D}vFC@L+s+@iaaJ0$&@Vc73VH z-3#6ste-b>Cq*tFL0W2~vGjDX2XL2UMqTm0D_# z!Vet~Uwv!zs8QeCAl+FkOgrS8cFsSoPIim({Jzn%dq&@p9KAXoN)uWXWMl1+*QOlScE@nQp} z)-`S2r=BM#Ex*}GNs{6&x3U)d$8lKt z>$I*AeFXM{Vy91uT(1T#732g9$!RB!db7)o%F^rPK4a;6j$B^~!pW(ojpw=Z3qpax zUkFxk%eBzsDsOGbyaYy7wJ@s-H+{&D9ZUx1$N5@2c8&rkNNJL+0*NAMzMpQoFj@IA za&;~4x&m7*yD8;dnNGBFOZR`EXWGJ>QaFGmFC-;m!90rH(QR*8bA`Qva&A=Jlmej{ zl!_%eX*BHbpVVsDZ*4(z)w0KvEYzL&($E6mgG7>CY;RdDt{Vy+v6nrST&-VBvxAQN z*d;gR$=We}2~H;Y4Bvsq>cY+MQ(i}_L4^Wa zKoXwn`YRf(a0`$k6jI<#*m`5`pw_Sc z|F-qHHQBAt!yxNy3NuXkVhggyZY`6u|BNbKcLULpt0w5}K{0quHmEL?d3#I0Puy{* zGD#~FS3!IOPo|LW{DiW#nN3?8N!?aB0dgqT-A>X4p2QspYN-bW9-c&7%jW$7WgDW7 z8hUveO8ysc-sxL1S_5?W_ouZzfO1=bTV9|CT5GX3$V+NdZXZfzXRkqGgSrAM+muv& zE|KH4D?{_-IeOfz+l?nLjxvL_K_4b_AR$d8)Qi`4JNXp^y{ z`BGO8SZdT+`;V#OR%+JMy$DnYbwt55-;-tOoWt!O@nt-Gm(J*Nauv<>$nYDbeyZRS<{oo#b75=~3qu1`|%YDbC zm-|=m;&Llm9{G_c)#X;`R4VcsJ>I9Dh!8R28Wx<=Zl4=hyq(fP7sK|FgjZOwTP_zo zPO`)qoyzaO;`duW5KK=W@@bj-SQh~xZ0R}btl?#^>Jwjhq zOU75#e!2!!KG6-#W#8A_|s#y^SjdR=+twie?`FwgPC8T zhU-e5-01@5Bs~Vpv&Q8aybl!X@v_1XAMI7^z*KpY5uwqwqU@6OmFAgF2mruiUQ|5A{m~N>W%0)RHg?94o)nO% zm(tBzyaT}`ESDZIE^ER*=;mGI;JVV79#h4qV$jNE=>x^t`he}6za(f5SXMXrfF-P;rZ(Ny?9AL+2g1{f%91y2^*@=vS5=)}&>GS}vSbZgt zD$+4ujxAK{Q4OH%j8aPux^`a~COI?Vgh|ye4!Yfo7Gi6N8Nc7z<<&$d8 zRuz-J494-Q2> zxDCYS1>yA65W4e&oXLVoC+X127tiZ-u~oHcl387~p-z%n=hcRKv_!%^%42Vkr`IT^ z0Xv*lRPzHu!S8((hcU3$O_VpcI5rOBJ1UIoJ7AhGKFQr{2(4or-fE(U`ZGAe4v_N9 zUDxN)9U!7-+^vuoK1^2ExIB(=a@Lw?65hgzGsH0|xJ5PFmiB+zovX5C*geBi);=k8 z9Jf%NwuL&?E!4^Ju=>gHFy)oM=m+F~Qu-IL2X|vd(kajU0K^9=)Tt~*Kn*|I%2bIA z;YlzdZud~og1;8B$ouTSWGwWGQ+fAGjl5$oEwXT<+uGrj3t#s572`xz`HbrVxss5G z{e;bd)87q}-jLXxvhY^~4@TMZYrX?}vbo5iKWoik?<@M7!8V-M4q12wl*ut)YJO0< zz_2DELE3(kz?Ndb3{cv9Hjd2|TyDM_=T_+`+Y6AFak&5WT{HANq6QYv=vgWFQQmxJ z7#Bh)BBbn{lqS-3bN>Ri@EzE-?|?QKA1Ky=HW(i))-8D_1#%ty7kBhK$$r7k8_yE# z-bs>R7w(%HDkSO7==6DTn4ngT{yCREFp#pg4>xwGL@qZ+I|Xf~-ujc~pMtx2(if`h z6xv@A*9Ypj^>HpQRAg@v^*8L4`~Q^DAh3p`|IBC*AO(7yN?l0DGns(~4@#VJKl#TO z0nZKB^-00xLxQa?mWO*(sKss2l4uJbo!F)z4s$1)pQ1c#Fz@Zh@)4vb@#D8sn96lc z2{CqfJ0Bf1B;y*>Ag=C1%2Qp^~s!F4yB0UfQGG*c>vgmx;crJ9j4t7k))9 zL=RAuO4&}f^f>m!Lc&=^=cLx5-(lirAWq{}6a#-) zF%JVFiBse|Ij8V;PO)rW5SvJ8Q9D_ywap%#P?vTj6CILeGfdh7%;j~QIf^pZM=-_1 z|Fd5Lhf`C1T$8}{7|$D^5R9{6L4HH~F<2xu zBuw%i)(*g%f6#n)QM$_hDEY1E=3<72KoXro4CsHsAfflVKV+yP^x@TG{r8qWmtVOT z{^l7@R_Z*;RA^9wLl3lxs$DGL_M0bgA$GP?dHR?t#P$O_{N&GEh*jnwSTB(894<^d zDRDFKmygLZB%(1#pIz{}KYv?1RJgkkt)xqVpjP5uWzXq4K_-PCkH*oHdTTSKWIc|K z7+$;;k@UC0%$c^<%MS>w6%Ic7`6J(XRu&#-!MJq4XghbCeILOoNfnAj@Q}BgrTdLi zFom&~8^0ha!7OjAeH6tykby7^)$%MziSnJBM9M_ihvd{@UOjHbOStp@h-aV*cm9Tj zYVe?fLeR}-;;ptla@DPPU`j08WKoCa(h7_(G@NJNrvi`IK?<`)wN~ z3%8MjNADE8Ax;_U!aqPL?cfy%2hx@}IH^s(Ege}T1c!wig~g?qeC@QBSK1;0A9M7~ zlh*V|k0hmh$s>eg7LW!h7{VlfKb=kw3d-XC^Pa5P-l)S_&P+Nc8>ldogA*{0CWl2w zn>=N2Glelg!SwzQ%EK3i1;@#A`Vdb~{D__?dU@vJ;OW>Hd3GA3NKNmLFuifX(2H;I z8X8?;Mn%$r+wfL$22@L+QZp%21S%9xWt5R@Ug+^e57>U?d+-(Qr9wk1hB%4*$GnisOhnP#jr%^3Twdb6#@;UofRbU)j1iQsSKJ?2Jgy&Gi+gtM5)4JkFK-aauOKo>KoXiqHv>y zo~^~Wjsk1cPp{TYV~fln*twi;k1|6Aw05LoHtjJo5vOtmf3r+gUR<)zf?1(Or$-6X zeWnGsPPLIO5~NTPmZZqAJ8%cX`_)V4i;Mlh-ps~U=b^wJL-ECf$3)!XQnrua4o>L+ z0gmtf7W`5N*&ZuIYpbD!4`d|snSro-l+|>XW{!<#KEbeGUh;FnP2Gi8`};V;TN0|j z#*YNc<57NuR$|NFWbto?Rvi6Ed+VAJn>7C^WOh6u2BO#_bXm!gFlA}4TxP8w5Il{_vKI+~dN4?M84*P@wB39YND^ZX13+C8Y$LsbD|p)ggww9`?Jijw_-X(Ym*^LN_>;>HhvVVvh9Hxe!iYHv4pigaU?d;^VqP{T5nB2!^iNNWN8W) z6Uaj0Cdw1rZQn;-Xtq{Ztos=BBfP3WZ<4-;juc*4Hfc5@ElR^Kj*rFr&2V?I6euoa zf#RvsoyA4$&f@9PJ;fGwPw@k|&MRc1V`u62&w-X`0h4EqtXX zdla>?IVZ?H|?)^*fIRq-{M zHr2k!Rj;LD3-Mz@}*KA@u-h5T?* zSo4FSI|3qpllWQ^o}M4;VuE1H|jeiMP9BS*=}0D7yJD=d@7jQo2E1}p?Lg$DIqWU zzRooJys5eoRt^h|FNs1yA^6+=x~FP&W5uR>^-L_*cR;O@GO`^?B|bN;i5F3&7u6{J zFs0Qa7H3y8umb2#S@CsQrKxB#++`eE7raeM;>gXv@51{4LBlZDdq#l zvB_26*EQ*JwxJItEvCpmAmCls!{3{r>^0C432T-ESL?7Rk^)7cGy3+#=ZTK-K>Kr{ z2ld#4rjpar_~5~=5R|3!;rDsG&<6|DgZs8{+M)1KL!gl5K}ALKT0!6OH{2d04@yI% zcDY8FA3TnqMZ0nWer%6_sBa)!*~m{=xqyF56ZL=R<=R=fk)GTqWPK#kqvgiTSJHi= zyw)t&nCA!oEOjicT(&&>zQ2HEYV;+tHu^~tz0t{f(b^{q3E;giUPT^2F24ueER>f} zV)UjEKS~xhqj%2ZX1Y!_>|#rHr(|(Ao1X+7M7FGzXTAv1MTNqKp|xtNGz?TiW4K-j z=K#;cON%CEG_*)Y(B+4dJsLT$4&T8G)@Jo4C9}OExn`Wkp9iT&_Fzw7^~xMsIE#S# zr>;cE88meG{VcA;O{9=wsQY&!=YqGfxyEplu~ZrYPu9#-@y(iq@DONEBzU zO~f$WWUqM#6D!vsICV31!01<-(sEqLRTLT{DcO!wo{% z(BJ|(vycnVV$!b&Uc0$LyZyy1^|}B- z@2ZAV8g7sB0ubgHI+1|{rAS3&WFj7jZeeI`$8@Xz0!4vaKxtp34g?5C%I%bezd4oL zi%4_%5xk}*;T@eW z(}~#S=zZl)`cSs)p2#ndEN>C4l4N-Y`VpS_rY>klnE`74pfoeWROo8d6`U^W&|4G9 zMhNBL&&0td;Z3eO^u&trD;FOOqpwm-d>b2RhG&xaW;l(p*r6M^8<-Eh50o4nU$DTB zrTiYBaz-uaNc!FAnRN{Qj{N{{euQ3bWfFa8F}N9{*1QGs^z%L{%j!K&Wimd5$@U`P zA`Ej3{v9j`Z=V?s^`tR;1aqkPlJwU&ln;yTAnAT8pK{z(%I5RmGP|n0{;@%)%L>5-!9Cn(hN;jHN`2I5 z+vUKdpTSemcSmRf-tK@zADN!Q^fC_gH3Izz{)jzL+jkx8`;cvm&+GLjPO_G0*F-}L zP!HiBDJZTeFY38Rq>*jUY^yncfLXq=w(l9e>Py0RY6!*irWg34T~^zt8?MvAI)sKO zZv#n)&*j7O zVXjaW0PLRExRviZKD5v%zA0cXpM8VB(Z0oAjK~c3LT0eGe;brvg03#`-milV0qtFH zuV1r>uK4U7zyqoaWS3y=W#YJ5(y($Q-o5%YL+xj)zFM}tl8$7FLkx_ypAC(Y1vq6) zoFP#<_?SL;cG+?Y(XsaiE&~CtdJNldZIfpS3z%r`|A>E+Z4X&8F#k>gg?9A?Jv1^4 zy3W|6!K<7;Wf5-Uvc42y6a6Y<+j73bhlud%3%=1lF=u~r{VN(Iq1hzgt4BD>9x_52 zeKptd^W*dbrw|Em*kb5oWq(U$SJ3^k=0HnG04uOGP{G5O!Fts;pvEXy{0S61wkw^k zb%^+&_EZl+FBb8(szdUN`7nSj?G-;VCyqr=1SD+WeifiDP^4$_{S)+OPJJD)-fjb9keYkT#AIt;$aPh3Y|B@O4RCrVE z1>I$+l_smcs}2$^IcUMV9q$!D!6FCD{<05iW#<}R4j(fZ>M27(^g_-pR{=e$njP-* zRo%jN-Mu4UwtQr7tC_}^2CD@IS=>| z_EZBmpqITcxQb6-R6Dm=*z&WV{`9AbuD0_kA(Mqe)ZfBJ>f91XvRafa zL!psd@b?T^m~W3mpZSWQGoIh=Q-1lej?jb49v|G`J!*rsq2#-02Hg0x9y9rvo_Yd( zWW)*f)2iN2R4241aZ05-a@mYAvQXN83o%rA;@Rx5=iZ&%NKHoM{^|SC7FE6};O2;& zkGVO5&uDhn+FsD+A}7(MQM)>OoO)7?Jy9p7tYmOczIi5dXVv3Y?%Sz_`y<=WtipqD z4eZH-J9$s|!esYkuG*9TYVc1!lRfxU%OwZz_^`i8(|RK&yjAo&DNLB}Cn3Ap*5|Wn z1JV?fGo#5&u1vi{G;~<|@V>iGT58lT*^j7%-h$?gCf0`*>o`y-%&EDgGWtuyKX|(fvu|xs{@knGCd-94#%|!@5j|=pQw^jUNRDn5$oYC)YK1fI9{S&!KJ_@sCC&W!~ zW?&$t1wn)$3MoSGLL zX8K){++QjiO!rsNkyt1R-^??uX)Q8U3y+!ZTNqBtGathbsM0&b z@j!v;z9=?ywh@Z*FUuZFg-<{oyAlV&(NfcW?a+&`Vl=gXW{MqH{ryd--(%%Q)7gEd z`AsGzlJmMA#mRkbrMiv@>T))0D<3 zgn_%h?yG4zW-q&^xTx)`rRh$b&;PRgRhKH1ol|h8O}MoulT2*e{9@a-`Np>GOl;e> z?TM{7wrxB4v;UKQysNtUuBYlb?XK?C_qtZ@)q>^DQ_37C5K)aTDu-6WiE(@GWL3(V_+&>N@HbWP8gkE>oKi3FT+#c*G8{gF;;fgVZ z(8Jh6Hy6(;0JjZ`v~&lbrBa>~E85z(TNUnlq&4$$8C^o5Q1S~ZpFMQ8{AwevV%*3C zCjpn|f~NQQGVw1}6i^!TDcB1x?F?vDjvZlmn?QkNY8{JR6xw4LJ{S*myoT0-Lvp8d zNkPuqu=xs?_*9>_^7Y3m1|JVa_3r-pXXia`1re;iTnv!W(@eIj479v)vlg_e#aHpq z;B4;x{&m_m?7l;aP_mr@8e3gD;3KLktWjA_=iNZ*NJqH>toPmu(1DdBTVM1g_Veq! zQ?Cm8qg++XmLc|QjWnb*O<+b&ELO`JY8^i(r)tnj*Y-`+=Mv+E$!l{luroF@Xqz>I$>#|PsPP$vMeGt zQ|&}cxBmU`xIPIG!(x=6-L33Fpwo0h-}TUxWW5p5%P(*&KJPTAG|9%Xu-0jCp?52} zXZ{*ezssfks>f*lUj<~tO<6wtJ~a$`6m4#lsaTiJ*}-CC3{Qv%Y<3XAWPpHt|jYl?=xEIhxVx;IwO( z#_vh;?mL;#*_g<;S7(qqdzhb?{SMBccDw<70so&P~??Bc~HZRrGeASyHRW^m8yN66a5v# z_3Su6t7i1GK@BfWTf2S@?;bxzTfH<#66EH0N6{iOBASqH`pc;j(agF9TtK*kpxd52 z#R=S@t&&4G+2LXoCy~Axm0yaj{+2+L1A@zBiW!m5V6EoHT(FBm?r&QS*Fk8O83g+s z_rONiMaLfXQzcG_;WuB#<2q=Z#~~%AUqHb^Jn7D81(f5QG0JhoaPHFVb`-oSi6+#R zHt+ULY%eutd&N99Ti?X24`$3&x?HJlIV^?1eLV=5_E#pnL6kQ5RB<}wePXUlQrcs3 zS%-L{6ZJ_ooZ8isKz{LD?1Y3{pWe3D^ZOI-%_{T^bgPHZ9m7nl%{rD|^!`)l2Zf*> z=i(*89onp*p;DzBU*KBF3T91%I-h?;LHsjge{ypp{)|JJ$0}ns8P5ZWE#8*s>$WzX ze-pB7v8iO+Vt&p5ALy()GR4h?pq3pb%a*?u$_%pJp&{mM)(r>|5`x$DXQB+Zx&?3y zEUUU3ApNM6hPE`H43tFW&HwXIErxCWk-2&ieqc{FdT}tSu7i_ollJGERwpKC)ggE% zKR!)t+?W{B#@YA-+z%)S-YzMmB37y~x+z8}d_^t7ARL-q)of3y6{855f%zbhNo5wm zlZeY9v3}O!omv(Fl{=K0QL5u)Vm0!DGDL}a2j z%*in{utTM7Oeno}+bn;;or=CEv~>ly^Wa8}YeG)*1#+N?U5qbCOouN5ho<{qZPotn zrRWk^YT&IG3z;S3co07lVfZkmDjE(_;O`&nZm^8|fA%DR-yJII(`%9qho=cWtn*US z11JWUsO1+HB;JIy9$E_gn#<8MCxZB+>>4NwYltz}N@jkc{I`mMRL= zFgMSQWpB2G&|#k=xYLgK3;}ijKuAmCBJ6*gHmrv9Barzg*_a*3s{wXkNsga>d_vGJ zQ){Pu@mL9Wcj3%=b31Lnoh)v`AOkmaES9zn z!)4|N@y+$r5FL-D5a?nQv<5wV`1l}QAt19!1Vl?}3=8J!7pM5$2dSZ*b5gFo+Awvj z<|%*A7+WIV&z7saIs(#d?GiGh5BZn0i!0P40KtX9A+uSPj=6NqjjiH9NiJJyZ2dp( z`KQl>)*4SpK!a>t*OUUGx3!@^Zp>su8^~zJDUF>4DSsbF-=?2x;hNXNip$vTZI@(! zi?_I&WEngq`hxl#4ebVbUf~}^jL^N|yoa1Z3FRa~mLXrT6M2cc>`0x+Uw1rE=-iQY zf1v+hRaH=ALd9MoZ87ocg|-#p-RqM=#-E?mVBvJZ#AE*cmO9F6RdyX2sr(f9*DdLh zjen3Y1g3hLY6o#__NAfq;u-;$=$B^t2sBy6FXNYy{^LFrabs#%`8rJPxuQg(>%r-{ zvnUZK7enIvHnZoQ)5VbfWgPlDCg_ z67am^>d=vNokw%UD+7ZNW%kZWW@1+!pkrn={KxEGgPDE3+?!PrjjyuSjC?iOR2Img zJ!scSFVF4SAr*HV>iO`qFQV=W@uX^oy+f{uHxUG|HtbX$GuNhQ7n9elRMw6SA#c2gyY-^o&#IqCJzO!30qU6)kIfZshr)th8mT#2BwKUvRX}SN-4q zdW_&ludjTg(Av|lp98NE-fl?N1#E}HQKtBtFnogTp>8$oUd}&}M-CH*M(Orb-kh0J zZGBX$4oJhX^=>7E4ZbUK+0hJ{QVe%WP1-D`yz1laA(ueT|BjrT@4dWZGQfBEc{2jU z`+_34R#x)ry~X?=|Ev;xHJa}H{+_wu&7FSbZ+zB&?R(0B+SpJ&8?|ks=Fs(B z5=Y3h1ad=4IZcOpF6;;I3^T1>xOPjdc1|6XuTH`~{>cD&d+dltyl9%$7_m1Zl6So5 zh`G6w&^xUwGh?g+GvhgVT+Z2<(Slxi%>Q5YY$v@Rzw4f8Z)8))>r{Ot4fgfr#Tmiyjwb>1@#F^{K~;m);i z!cH=o1%~~`53u3HdmoafPDSist1uXPId6I+sl(38E#cjiZ}f6!o-)sMvZr$ccHP@7 zKs2>USOQ5T1cl|ffz zL>)TOsQ(Pijol<&$yf-DyHtN(+x?}MHIu2VYuz*U-g6#G8{jt}V z+CGf&P&U3b$~ur?t`)j9+*&AX=k;cc#XJa=)H`ViiZ14e092Bwz)0uIz;J{(D?o}| z&tr|h;0PBfmWiEgNR^*}$Z7Hnih`_hnFo|@Noa&oj;>405f-UC2uwuKD>-PWqu9|> zr$kFr`{t-XNb}X}Fmr6H!9>c;TsBENOo;9CW9ey)?k8dG6XqnrFQi>Ax)I65g~YlP z8@d#lF3=s!UABvxuH zhHmNuJr>PoPWl3=$;ZuStVljBspqk=10VrLp6QUgMag53awgl`i$}-?nIxbb+i{9n zoqwv_jENWkwCXZ7a?+ONGXN@ha7cmTosdF}w}W+*DC9s#l^< zL~%cPGCb2Smjfa158Ng3cax|B7`YzE$YYXJM86?egvBNA+u!M5Fz=U=?ewAE!5}Hn z1J*4QQh#6uDIsYR-m~bPAakQ$W@L=pbR2%NgAltpTfMsw3l zk^e-eP&G8_PsDw{;|EfRUe=(*OEU3uy$9e#K@_M7iwDg2{{TaEC<+SSwp9x$nZ!`Y zq1?kosmVa1)(^;a#EBD|Ii;!;8Lu@)%l})1Y6eQp>^G5F1Kg}AnoQSALVO6UsMo$P{YPdcM!oIv(9Ze~AWtCX7@6=k&S9=<|-YvOI+nVx3 zYWQo8&E-8R zLMdGeOhT+`&0uyuV&#__vU46G?y^p~pIwq~;VJKwu;(rJ)-up-TO^gIuGYWWc? z@r61pY{IWg{rK2-vyJE@nEmFX`G;IxpCfQ3KO54QN|e>5m3zv+as{r+QCFR7@E`b;??y zo@9QtuJ31}`St=`ckoeHNBe{yk-B(2`LL&>2WdSYtcmkb0X1-~~9UlM$i6$!pxaHs+u3-pL;uL7Ut0NI>C zoJiLr)rqgMvuw`L@8Qwo<9nT~`58cpfD_w{BWvZHaddi|jg1sxkZFr;w-$K$qq+Dw ze~!?5-AFqi*Jkgx8(+7?lErodXw(%>T79LjY>%3qt#kXKKv~5^hJf1ilZL}?wy;dk z3^l%&0p;Pwl_mKdz=@=S)hl7wpIxNCMILNhs(7VGSq0GQscn7q=@E}rM6`WRrPyu% zkG|*aH%=<^rwlQ@anK7adn)Ex>5W!7s9zyorE4B!8}Ou7wb+a znwuHkf+i$!M+1I5<-;%NX)eU!ysooFJo@(`2R3P>=X9xzb5hR@4Ct3B*`^KGgwhNX zR)6CUo`(+M`gwl>V2pg%Bl=wsqH01#&uChSFDM|cstT2a6a|EmuilQj(yR&^>+tQ$ zXX{^v$WsLuQ`91%Fr@Q#_D=OCcnO_rJ54=%u*+(*5%ii!JJPE ztYFt{OY@)XV%i$E4v*Pn4Nfhq-JU4Q+OxIZyA5D>(KzcY<@T(WHK29AuyLJli(*eI z)BJs9QRVYb^-w#YGSqmUt2#PwQ)^9Zh7-UT8M69t!0lY)nwbJD<4(Oe=c*n#y|Hw{Yf@z2&(2G>wS?jPO5#Ixy+MtG(#!dZ zNQQ|E=>k#Ac#XIX7ZC6+jx5ZXszinBbKGB3%NfE-sx2F@Jj8;Q zf}!3ImgT=vmkYyyWN?0L$t_YW&u9@S2!^B{kP$hEpgYBkt(@bNVJ~YrE>Z~F>F!$Y z9dnu7gv+j2gAI0gf@aljlq~wRrsV|tE}Sq=7*0HGOvs|HJ@$iGODo?p6jPL6xu}{s zFx$#7iW>3EjwbrNqnMNONU5aSxCqf9t|ix#uHp^G>{(Pzm}nkt#6Lc%Z9zE#SC3q3 z@oZYE$R_MaUU21dD=N=)IPoE!3InwZ{3nY=Y@boOu6=E}hX700n7G7pM*C5yPCrJF za&xJbLgl@5j?wEijfi7vf7_X1lF=DPh=238BMle6u1CSeK9c8TX4q@W7l+}1Q&a<4 z<0rJ&K2!gQ$Twi4ruizSV%~{H>>bxSiy7Xz#Ez4LQA=(%ix7_L83p4Fnu~JPG7#I7!eaqs8V(FGeLdh zn>@_CwT36?z;B7B^I=E2j)&8E2Iwf-8(jKVeQGmWD zGITA#>T#|tk{&7KX^+8op4{U5p!jD*q~47!@%2GJa(n$^YiKLXZCfm#1Um;zGhxe% zjMOWKb<|Osjk0wsCuPfRPUT>0#rNTvheROJ{=xW@LV1#cY{z-Vi0`5Nv8%5~c4}U< z@ju{ZtcQ=b>>(WeKXS{T9?#011Rc>foTv9+4Le58y*b5(8^TpIXCYZ{WH?uSGq_e3 zvwXeuRpJCFv3329oQ#L=Shh?&$V7{q&?jJkTc47OI-M7XpmweQII`p~wo^bvTqIyx z65P$J6!`hcq`f-@X(OAm4KLC|!k5*&cw*-<+q8o9yw6j?XT&ReUbX2tcQL{&EfHu%lfsPV|;Gy!k&gcF0$#5^rxw zgiNGwtZEbp{Ax4PnyE;dBnbRq?zjn@1V}UE-`lfUK591SwmYjsIas@0A*sceJTz;S zNFdJ;b2^1ck$T?z%aBQ9)7`zjYr($y{);O z(zF@t4E3-UT6i1gh;#}2?rsmb8b95fr(*1=w&h^Aafh9+WH6sPUC1cZmKLvYx@fPL zlMV4MlPq9h7YsqdxM%c4!LOMU;5JnzGPQjzD1+CynO@)NY)_Q?$!M)! zec@x-B2q0%fs4ufZ*1%bm<#S1AJ5(j6 zQp3P%Lmd1go2bDqtL{ARz?Jn$IB6Rj{G@-kugP}2;+5b|+}&dKjW35Cf!i3yl>b1T z?&WVD9oK)%LZ@zTEoJYPW@R^d_LSg>$102U5_uIWFiZn}CN1e-?TiZ&S6@!GQH>P6 zXHq!IbOeVmD;``kU-Y{gdRz%*$Ukms7UI6E#b~~A3m4uXVj8HG%bY->1O3G&vLk1( z{##?t;44cQb&$L8N#tC?-X2Djx zw#JdH@~endjlEbmikP_06qVbbF^DUJ89Zac!wQ03=bze%(BVPFLlO?qqSzXEuB0lWzQTs>sE;w-htrz?fq#a<8Hid0S z(c1uABcEJ18mp~38!4W!YK_V#6Sr`dE#DmX1Gvjs23WP);H~`@_#$Yovi+gImtqz{ znMQ0`&wc)cMPe_Wqlgns%u*`B7%YKPX_<2AR?n{o3}sjh|Xmd-b?vH z0d*X`swH7yD4!4%b_lOV*KosU_aiHD+_XX6nWBtzuj6njop;jEkf74fhBMee%|5ZM z8kfdbN@gS=`}f@NQ7juOdW20Tg_&q8@v9c|JPQ|It+#%b%7O|gH#amzd-c;9Dm4o7 zRPUFDSOn(TJJ;8`a2Py-deyQ)kTqJlgpz-tzEz|rpuR!A42~sS^hN`**&BaBiN2F` zrsXtO%w9d;_f@$?wiOK{74p-tL$`FsyW<;iTzQ2pd2X?FF*1_%u$*z7Bz5TWbDL^7 zHX+5SHMMCN9=I>X=rHmX=tR7EnnoAR7pVfZ*BYJrbr0-zs;FHs__OzgJdr6ynLv9qc7(OWfmFEW{YdU4mh9&i;G zl?FaZ4#gZaw{V}?gg&jPHKkiqoX1bs6|io*mW{cZx7y6URIk8y+i*W%eDL*5v8`a2 zH!e1rmeL!)`2d3Y#J%rDXgF(O0nC* zAv~H?etFR?&M*0&5LwQ?nsRYu-Z)c{{CJX*#k!=i>iMgz1?}d9x{lVmPh0Mry-3hM zu~B{kyzj|K9&-6Lhdz(P8IS6pj8)9EEXp?{T@zN{?wL{**7>*vxf>%FHP;qc+iARWoxK1R-^aU)9K6T8tX4UOa=88l`#c-rQTFFn z;9qyUm@dJ^ur+$wWv6;YOB|k(*_iM^Rfm#p^DqO56p>c zGQ`s8Emq66Qkg`h+TC-lM#hO(crK>7n%yn&~ku$oD-VZX{%Qj0MpSH1PKhd7{bO}{z(+2L(tL=3QRJ@AxA+#-6*27_o_g82xAX#>!S`!{=YYVIaiEtDpJYyq1EhhHJD% zgNvb?flC&z-=DcYCP7*FrrKRO}C1%_u(a% z@o&p~i^r4x=@!7;h2NsirY3UnFqYeLod*f8I~%UGtP#Q{Q+D7wQ9(``JJ)wOD1!8H zd<}J{;qAG3MDp6cP>8UrNM6=Nc24}=01N@j3=d|^xn^W=W9%b`J6#7$X;l~jM`;xi zvd76NMn&vhXZ3#5#>0d2I9_bxSRbE|EBoelEvF?1$Xis~8V+nZj;E6?YFH~SZc4H6 zn(ZlTlEo4Eq&7ntiwfE(?j-vF&g?r>R$2^o-uBW^Rg(locIVDCxr8(0e3E|2;%&{~ z-=e}0p;}0nS{j6&odVI3VK9z7n;YntID*?=tKMEH)eEsN$Zt0w7d4p0bB)HY1iC1a z(3gqKXSULimJ{E+6B~=)pmo~)G(g%dRG+S;+YJs zai0k02wx_(e65f;8tuH7uQ^%6wMCVTo~BamdW~DLw(>SF}&f| z5JJm3`pZXJU3*J#AuNSt=locQ-s;21sl*H~Jaz)DJ))i?1?ZSNv`+aRdd&ed+TJ|> zVVsNubaP+!z_G5<#F8@EF(aI|tJw0lH*y41Ykis3!}w*lj7`UBB^J$VU6&VHVju6Q z(wUA{3R!+FO6WOPr4is(gl?W=00_pPY{=BAL!wq>97~^UF9NCF4y`IN2=n2%YEOhL z{u0VKtfhnWvWsLipNLMzQZOFLgka$XHJ4R7Uh@{#L|&eB40qAHI{t@roX2p@daa6< zkiG+i49|5G=TG=UdxUbtOBd^N2>Rj+r2HG%15>sj2fRk@J~3#=oh0dFE+@bx`^}S1 zDpjYsvrOS~F|o#BH%joY>F;pUu*LiNHo+g1|H>U*@A2EDT&p+$s=Cp+P_g}qT%{2D z)UJeTXyu(U=JO7C0O+tPrX6y9@}eFg-{9R5z8u||FuGvE{2+kZJPI&JeYsT0uy&4XVm|e%P5ADlj+eX}TYlDv^xkZy z0V~ihs%Kan3{eCJ=-T2;OECGPU~$|Eox7p|2jvh3Voi7a0>cTuE2H&ELxJ=!xky!H zh7`nr>N5b%ehA?s!*bN*Tj)Cob{_`ottjoOPyauRCe0hxpixGpun%0MJD{*qkjzA^ zs>f``zFmFm zuRo4(D!#EOJSwf2-$a{@r@*z#qa{97y!3Sy;I4jPC92&Ag6AhJRE#6?pGmLq2(p|} z;A+Iyt9#6?99=IZ_ED|)U8*bln@m>v6)YoKG(O6Ph}~oIAOSMODr81z<5pzkWYonBml)lUtaki8T))6b zj0EZ^YTDJvic$ns(8bpxnZIop<5B6}@O4vOLY|WLa+{>k#v~S_7eW|guFS$S$7x8+g!EHc#d>y2585JA69ORXBro$>^ zGJ$yqCAv*DHN|`OC80Mwe6v2hKe;|b`S+HLa?*Mk8zbFpz9%@cyu?kHeQBxr#hBsa zF8N}mB!=QyD~=K)K|gusBlhS}d!jd!M3WVg^?UTORnBT0d@C~m36v$|6N8YqiNO|L z+Y=J)TPVyBhD4ATlbaFRpof}ncJ{3H`?@4%Ws7Hi10<%ZTTQQAxt~ z=7I#7?VMl9@Oo8(lU~Vfp||#dV8M^cbvpai)rLjK>7I-2p5BS$;AV7mC2#*sk=_}*du5c~+52x(&-jWjuyf{T4(*&Vx#ElMoH4uV3G1Xc zz3K_-q&Km; z`f7bQ)BXCk_?B{ft|fK*KNjPIPim|dKN-8)@n&|*#77TQP-ts8BiTPl;YwM;528&aiT zTQU&g7tb$F{AL#OJKSVzoYbxg+&Fl-Pp$3Sxh=Bq?$O9m3RD0k9-dR&I@}c3*+;h5 zej^tpi>*QzHsX_319wzK$5l$^a>qp*n=FsQpP`ewc)=}`{0}S z=8qBn&dcwjnu*-BX;Ij@wLB4k*#}Ch<4je^-ZwqD<=OoNs7PU;cs~k2_klvKWk|P_ zD4Du>ow}l+2HBp9ze@Uqi;reJpWK4L6m@3qd|`3R0Yd456P+K0wP^`P*Ww<_2D|lt z`Ih4`D5p})b2F_r6Lx}{2*JSV8|`S=<&UVinSH;n;EP0dbgIR1aN6VDb~#i^xStcp z(Xs15@TE@1Wn>$CoqW{zSli^jXd*>M6vI}Vv(P^`)TIy**3mOaAZiqacP9E z6R9SJ@qiVJQ8EUoIPH6bUX=Vlq(fW)iP5*sKX-k#r3ih#zfj`|$NXo`G0f~;21zjv zc9-4etsdglG(oS8YEs>iKKN{R*DL^`3O3b9TQzFxkq)lSB^gj<+zV%+C>j2#|o*`*hqf_hZKV z*Qvp0SkN63`kWv`MsynBc}HY!hDR0_CaJH%cm?=uUs>1x*9=#WwwZ!RSp zybCqhpehgHKiqz)eHMSs0m!D66Qbo<<(^fPC1YHfr&^maT90i+B8z8O%0PFfVuM68 z2oyHeMDv)h2ko2oXU^x2+e^l9%z=GZi51OiO}CO61!MLir*-SKJs5rRc>DuB*2S+w z!u+)XwaC~F(RV^7!fi%9jS}Q!p=C}#L+a(BMKH??lcOn?RM?Ia7LnTk&s!2g@sm#pt`{K|-wMD6IrGF~(KDenVPQJ3MN`RVCqvKfn1G+o4E^vu~5 zb6++Uml(BQ-mt{6@OWfu=i3z9oS3}?m(5?*k&~CSdyy6TqwfC@#&p&YXeG)NYNjL_=^zG*N{fAQCDAMV>avx_0eh5B{tj_ z>bf95kE*eDpQ}AWZoII2O+tUEdn~|nA)xg7%nAsgaN&#^>f?u=+c$b)R3h?Yi(>!7 zJvLb0Wu=IwAj>@561OxN4)_?y6)4IiYM7ud5rqqL9+5*TwAqVEg5QU6u24)z&T?h9 z)C1!lYV`_nS}Qte3S!JA3Y@1ikIzb3u?}d7bGPRH?f!9M(eFpPs2@79iQ&-UYVVJU zw^*S9nZ?$Z^;DbN;J`d!fq_!-u&rH$p$VI!k*C&To_u+D4h%AuHBLYjGTES~>HRlNvkq6L9S3y<~w1JgZtz6pnQcfRMaioZ%YBBZ7X2Px1N`o;Hinosi3^zZ#VkkECQSGFS?eY0xE0 z7+8vTkS$02nteojV&Hycg0uf)yX4jGCqBds$KKaU`RUyE-jb=g=O85tVM{(YxtT-G zFpQ)VooPJ;wK$x~{+RWbGk7W}ZO|`=m6od^9<1J_1M3AmnvR*{4o0;hIff z$xEGnOH=52D;Sed0%v`guM+-^2e*&*mSydz>+2GdHDB7gVY3iNbB8VKgtsaT+{T=e zZsH+T`<5ezK=dT!#d28~dUP|&?qni;tnY+N$e1KApE!y#me+stjE{PT3wkJMW?B}u zB7(lQ6jMtb!53{rJcjCiiw0rco1Lu;R>^8FH+3V*bU9P=Q?zopnu}ziEsmbYeePT% zHaZ^KgDm+VuPSj!RV6fCMH{E~CCcg$)8VI?kMmpX&o&(LwJ3LUsl z{79>toyI}1WY;Yzn-j7*GJ(u<0PMQbfF<2QgmI0? zW;t&Ct?M=Z*O#W@uYn>U>1^fc%AF991)heV`mm)Fp25VL$d2J z*WSW5FzkuIxuqj!mLhg`T(TBZK8{bm#~$uPlD@Cq2xU$Y`QqY8WZcL<^Jls0q^Y!u zHm@v@)3xfF*JZC=wh-`!?renm258kMT%PxMA9+izuEKz|i>EJ1Fu0PXZ2vvWiA#p; z@~xu}p|zCB{)D=&r&jbKbI7!yts$YNOG>Bqg|8jt%j1Zo+$7yMx!Us)5FU13RPI^1 zf6mHATEPlYPHa#18qjK%mfyyTZ)C<>Nsp<@#(Mc~U{))|ahbsNjT0oIdwJnsA^5e! z1^RohVjE#QH}BJBg;AQPn{-`(yS~iz2|uL`Cj7_wZC9zoN(qwEh(tsy1yD#Pkh>0e zx#ge7xk-Ki|G?12i8b7+0XJPY7+U$0C@<(AT7HZn?C-|`aT8W(Ld9h9Rm61W%M8gwXkdGH%3^`aY=f35M3UD?09qU#k%#70KCwjFyUoS zgf&%yk7DL45wf?l@YZP_;ri`}q!|FrDgEt@!NBoy^7%K}Yr^huh*J<7YQpa#z%nwL z^xIV{wFQ?`4zCj{SG!b(yTlBjl^n`$@65^qjC|7}1 z_J6Ap_cbK>CtzZ%IbUhq4s$@yJ4`G!zA4LZF%b9nnZ@Pg<8Ox#Kh-L5TFO`&Wis_xFw5Q9{2iMf~V)QJu5~-#Bsb7z@<}tAxQa zB30K3dud|-!Er)=&4#R$_cXc7p|uhWTlt~iK+#vBEV0r`Hc_z7URKVxex>JpQ60=v zEe2mIs%xwWw3qb8rDhR>7l|!*G%6n<5Qia@-Tz41>qc~{X0>QK@u#OB8a_+Z>QeC2 z`7Ki}ydHsfrhy3|Kfw0;;X7Nvkei408!CW>{gHt5cRA~`>11DVKPjhV3FU!U=eYfCFuEwP|hPDc`Aq`;TE5?G-{+eN7o*a5dM-UDYsygtmf z9}47H{7W1(?MX6*9FO1gKyv(nNX6DrG&=~e`*VSewUX?N=`CgqZ3}#e4|ifOafwKZ zlfS#}37oReemkr^U;4fImUpl7Z$kL&)%zHp%8mImg&bj|xZzAm;^MP&@KP$29&uJH z=m~Th+y5Z;P({Aju6Sc9c+eA4#j-ds?-4w}q^yA0^G4(2w0QDcFz15F7O&iABJ9@Q zmE@AxTEf_(`e=HBsoM%g_x6jad~vi0inr=zsKkU)LHUFyWo*5U4FfisLCyQMAxp)- zG!?G+H)gq9-9PY;#<=vSE&h1$g^}iT=y-8e+3{=`Nio(sNwAOn7jyGwc;{h@;}>rT ztAEFY$r9>p1Ku6yoFs%)Oi=dZt~B`kaTF-iOQ?CI(fqz3p~e7j-;lH=(l?m+1XChB zPCh=c3HW2ER@K8{)M&kw7^?9Y{_`)(80?Z{GyoFlhThcT2AazUtVKj#3-$_{U+0VKQf(zLi!7|asXVWpjz{!iJs*0iHL>9 zoQD;7kRn(J*lh9sfVeUh=~DjZ>W7&!mCuoJ6I#!6YdPxY#I5b#3jMF(N=xO%#GRv6 zW@ zq1N)O=Hg^sJuX({Eqbs+y~#0%-)vSKUHZ)FF=IuazF`Pm{v?AdN z)2#8<#py$GE;XB&y-T}lDaI|<&N&Z8`8=>X^k(-tU=7KR^6`IrZa7MV{g3)){s&OK zOFL-$9OsT5Nu_3`=>ORf*>eoqncMg}oFqFB?t1L7*9$hvh7zbzzY9aWt*UZtZC9oRCuLc zZK;~vS<>=3kl{OB_YVeMg7j(;+8S>b!cw0O+G|yEoI~;X4?AyOg7~@|F{l5MTt%=~ zs6V})9Z@#%GcVIuGXzTz^F9%5?uqb@aYKUOjh$pya$WXDwD#(oK9gXQ`c^mh(R?SJO3 zcFf4B-Xb#{y(B(c=AFlAhp{*^3nvUt@WP_uqOC~6N2pnKFZDFgQb|`bS|L}gQ1AOu zAggADr_^FXSGe=bNYUaCF3pS7_@8KrA_@Q{ve^JCe^V!`9HY=Er&js#S8edHv9nV7 zgu$~To`uh8{oERvJEpj*4x{eelutH*x~IzscvJPp)yCz<>HWK8R$6feOs!TW5|fXAh)p)>Ob2(c6X&-kip%Im}p>rbrbiH}w5#lO(mX4sz; z(Y~pqlKa$AP|Q4I67{;Sz7df)8amOeM%j-qogFiam{`(&hwE2XFHv)tUTt4=bnX4F z7b0R}0ACD%Q~*EyL)UBiT&&7dC9azMd$*V2Q^PIl-fO5Q(;ty^2{0LLP)je;(Ia7VpcZ1#tNWlvNci=Rk#@Hj7Wj2qJMSm8 z6Z3{e4k#%$tmzunWRa6tC%N!~71LmbLn_j(^?uqZ6lf8m!+Bc0E^6MwwFAD0Al>o$ zCL&R+X9`ww(~X^B#G&-cJmBa=;ufzz-X&G?MOo{}U!O%fpF3|td(lzB?l-3+7}v*b zy{wFR8C@TbU@N)hM!wy3+*Kf~{fBXntja@{pvb4%px=H{z~Ugd;56R~Y>=#rlRNpH zsQXYMpUW)3arwd6XFct2x@m0-)vgtXSWdrIB~x?%?1ngZjgpwN!*oit8tJkukELhg zrrN)bQZfqGK~{0i5EmZka|*AMls>iIzj>X&r2r@wcO^)KK_L+4YIoSPq)a|?3(RWO8 z=f_5FD<)biOTN+aIxwA5xANu_#Y-L2Z!f|>us0Q2Bse;M3|a#8H_|=0aohC#p zhyD6d?=2v1&P3~+k1lM^u2D+ zjjW?E`M8R-f|7O01q}(u&ThQTeOo-w3$L`5faa-hjbkm%`EC?AkF>7{iAG(-y>&op zbB@Nh>8YgN^3p|YOs2+;X3Odxn#2xib0Z_s+7ljn5`L@{Qm2Rs&w%9(T3{Bhj=QFR zpu^=1Wabt1jT<4a$yKGvHZ^yLvjHNJ|BbcnnfV^JPlF@OZt>*eV*Rhy>}8?7RXR7_ z`am3Ikoz2;dBYSmeo4d9(;1o;;h` z#TCR1ap`Y|g1{S8*4>T(E2;`?0Yb*S^vOP%jJ`dkoj%=OaMr6HWq4@TM|5jF0pKXu zb4brOCH+ZotrzLNaM+WcK#U&dvE4og2M-OH(5EvbY$6Dd{H|mn1vMbORn=-DQE+~r zD}fc2Tp2S$5MXiiZ$(?%_0nYZlo5HekSxa+X&2-oL-oo*TStJ_lcU-b#NOe#YK% zXV9L?z0dsigD}lcU9IB-e%=@jehoxiT})pPF7lnyd>?o z1fsJVW{(lpzWqwD4@)j+TtPosmn+=**Ylw`J|4gOiDDQ_W)HR?Fs5;AwMuVVFGVR6 zC7Cu`_V#b|37-ERvEz_|S>nt&j|^(94v$yDeN*;Smwmy8)zhQ*xT_*{cFs)T&cBm* zqP7!bHZZ^E-r$`ngfD8y>CZ>MUw!l+To8X#B|I2GN5p%$abrrv8+KRz3(hClWH1IR zo3Jk_9^$;4JzRZ7fsZ+c`_b#?k4gr{d8+wHawMXk!aZ(IXmMvHl+MKR6$kq`n;J$E zJoOpyOk8s!!5&#dgzG(>rJ_ljzAT`edYxL)caP0%7c$0y z{ze`ag&p4mg$N8e#6TvhzHpk7r{jTw!4e@W`L>)QFB|Opl_B!o=e%QQ#xJh%r~1r=>1mlnB0TgQ6!!}l&Gh6}JC==E zPy>4Y+<+HBG14V$*aSLL{O*jGDd!I-2qE0@ z+@QiHwTOzKoAxdRF(Uf)(Zs*LOk*+vo}>pe-cbafQx4^bmu1ZlTB(ub0*OBs7NO}TLbHzvjbK9a`ZL2wb=c@rGyU5jF(_3NFx*j3 zJv59|_c;;Eb-9dyML;%_YC)vhn$B-+m6g`~0jAZ1Vb$F#Rvu;R0Y}$zN%u7edcJOw zr;~Pr4(P}mvcU8oY{=t%|7JsyiGQ^r?;QDm+K^@I{?&$fp8gLurJ%uu!C|IbC z)uIl~@qnu>a%SxILS-K5#6;1o@o|&4$KK1oHf}~at-9Ln?B<6tdSWOYyVvNfhtaqE zDT8XV8G**%h>CI_!<~lGMxJyetF~x{qRpJ4tr&=j{8*gi-vJh8Z4YrRe;N&K=92LB z2n9|fqEnH9$d_jPP>FnT`GJs_RN@MXQ@O62+}@xd175$6GX z%+3}RhoAAvFaEJZI%qps8tuq-7N1LCJwMB@pCqW}X&{&b8E=eLkwl-#%bcJ@&|7VF^JQz#3G~)8|`|(V4>ZRG&gG(3AC+*zFcmBwl6G ztFIo^e~K@M*h>EP1N4`!#6@>)O_;}dHDl$Kx`A{hI*mY_b9Vx>1CD_8&2Lr<08H0RI__v zRv!0puel!f?UX4}@MqY-<2E{{x?@Z^x2Z!sm3zRpi6qW>T$z>q6>TT_kCg7^+LZlA zwBW!Tq_xk|D+*Jt9cx~T4UV(v*gIWWr4`k7f2i1<$JolD?baIhb?Mg0+B^F`prLJ> z-i-~8j#^*Gq@26=y^qkYcWSrqP+5^a?W}jcLp0Ku zTQV~@1km>%(H9k%TtTsBl&f-J%}#UWfJAJrPQ@BLYGHbP;x9jD@2vrOeNiDCzPd8Y z)w)JmpwC<*&oIn4q3jZmf83D$+G+bWFDG7Js_do5)yNjt9^1Mppd1IHgwK!<{BN(X!OJ59tJ=3R$a&`nLG#@jaS zcP5=XO6y3QR-+xRj_L}=2iK11Sdyd6B)hl7uW+4=ezhbh%!Bj@E?lHWTJ*b8^@HXu zSVlPtu%mdlj6`l?5ju*fcCCp7bZ3*SzPEK0>glA#_3xYwalMm`AzQrSuN8*4-pR6h zr>?A|SM`dk6ur!Dhmg8^+4hUI{mb@V9R~k5iJr|ix;{7#ZutQds`y#5P$h{4 zVbQ=XVjc2n7G=9>q5*HzN(lE=9Nk8O%83)hn*Z57Qhved3De zWXd1wb>XUfmQOgE-B0osdo4OpMyM*Ei^hW9tL|Og{L3#M=li>2+iomvk+UJiBxm~n z=bqE2Z&epiUp{fF@&*toyIcH6C zpBmaR^9ZZD@Tq^dW1p@%eO10wBd_YXUbfbnUezw~#;W0~q6aow)2rGg#$>H(SJmh$ zvs_g^t?Ku`%vqHwXH}OzJG3fO)~dRURgLHSUH@WLQ$|?T4^JAiX=T*vHa8m!^Z^-B z#Gz+p`Yh3piuB_xny1w}OJZ7jQ$Bg-bJ48$2`C(0>~y>;j1m%)$lZ{qmyPoCyt^Z=X z?u?wiUAvMaP`y5b>RlblqI%CJhvwZ^rBvk)&3o?C49m5H7$k&GvTJ}o`B`=5lUz_G zM4!ANX@tVGj#9XQQ%N^F4;cO@k{rIOS@UPBmhxDgQoFtSV|1lCQ9X%noa5Z% zJWLO3`?Sj~>U6_Nd`$KOSi*q}i$~DUxID4eUz_40f5D>Ky{ikf9jd1&K1ORm#b?rA zYL7?85XhIR)0b2>)^3lSFC~9hef2mL?7hM*LxV`>o`L#4T61YbVD$`W2&{9cE@I0e zzc^FYY(cEvCy)bau4dBiEb~?1xn^11e{iXanu}RO5dQsbgQ1Pvd64lNM|!_zv}Wl)s9F}E z)_p2nug9V7=lZnoV2<^*+qoz!SgRYBzmSo+R)}LdUv^+m&&2AJjxY-oq%UV~W{rONIY4WACaRL;JRrHcal@(#1;*d$07P zx$2`uvhjyKd-}IA1{Cra(o^^HVfG1c`d%)4WQztm^ZgLR?AdbqTZ6;jpyy@pFuBmH zmZEl18n?N@CKONZwlV$(=|eVARbJKSn&>Gr3A;4@;w7pHuba6$1yOE+t$65 zCxepnT!{dnwECMhQ;-N=eJ&iv>3*`g{$lvE!(s8qA97hPc!aeM@%#@lk*Vg6YGAEe zw0UXyV!1^g>(a&P{b2WT6fbtL@0xzyk$LfpoMPhK@2BJM^=kWsW2Ni?&E0Y7ma6T0 zsO%qpB_Lb-jH6FN34|FUuJj)&g^I0PUk|GWq<+uTfJU=(U5vie6bAsGgu(pK_iG(`YSX!vZ5DX8OIo+F5&!$~F!uU&z1DQ{9o0{sk24dgfo(Ugw&jhT^ z?ZB8IO2A0fw0Nj!X}bQaB@oi48x3zfg|*v>g&b5X3yFo)W9&k$qjtM(-2_@cj6JSF zo4ymXLDYZ?X{!Tku9VdSUx$VA(Yqy@b5>VV6nqhJR)xB<)_K6s2wu8ErlfMMu-(o* zK3&^U@Yu47K@TH3D>8rt z(2m+e)di?oR`g0aQFinu(#u}TZMVbFxHQuysAei0c-52i%U>Rf7s>4>jj&x>qgN>W zWfYt>zpI`*WN47_$dWeWX_!pI`plM)e&Z`jI1o^y1LE#ghLLwF2}2Vh(3Gt-6KK3> z@jO-BK(`^+IP^uhywSLP8C#=hP)q%Lil&U$XO7oA#XL?CiMoZVEks*Q8&Y=E!mktw z8%8<{Bb6o6*W&9zx*Rflhu)@>>s4){eLas}HA^o~>5c`Gst@?|MGp^n37f?a|HwA+ z5)-~qu`*vTdl05pEw4T~rM_!Ql?gcFE<&*1iHNZ&ePa-hqLJiQ463OH3)KR#3?G_^ zmI>Qt(OU)d6?#bx7K_={cyB5$z`b*g@nCV)>`*u$U;gwD#?ThkrY(Aj-fX5l<#S=< z8Ue-h8Bd`)Q7G#&|1INkG zM>urb&V|jJ^s@T~;LoyYd9u=Zl^Onm0jH-g#NZnh;e;rxDGxCCHG-(zQ$vU$xre`9 zRAoow@=_Wfm2U%LFN?l-o_<8=dHL5eyoqJgtkeR`N`g*Y_5j0!CKfD)U~kjL!n2ns zu>kJ)kR4A1r()3aLjk`iz}nnXZyitnRoO7DFLP+zRLJID;j8A`AuoOPulIBOt%j3K zr{!EAWyQ+_T54E-k^!QVL>`XUOlJJN!bGa3JX6==Ol@ByQ+MnfnQ|@6K8ac8?P@;l zjCs4W9$WD>EHG+KGdCN~sPbT{07o6YGaxNmpYVnK;=fNBS!NCyhhgD92iKjFXcDv# zJn5;g@l!?k?bWS|@#2N7@p?bkUR#lE|8wZG&5y_4H=7&iuqNsU^L1sV){6W<)!hv} znJ6pUqW(Bag+*t5xY>;ir%dSH<9X>a_rceQzkN-H?sc&1Xr;N1xE|opZib)7yjwd_ z|DmE=A2OfPD>SzJw4K*hBG>ls*EJE9Alz~!It}|`X|3E%I&>E< zdr7F-3Ms+e&eh3j6Khk|OV*|?1{iy=vPW2P^T2VSf4nkdU|V9wsk0%09O2C zD3d25uJmlql{L%Q#Hl-W`o&qF$pw|kie&6E(v^K)q24TSI2o#VspbqejfVNf_&DjR z^y}6}pV<0csXGUIb>(a9TLCuA(<>eskn#64cWZj?@%+*{bku|;XB6PoO6W_}PFE7% zfcVz;k*?6KdFo)8rOj1d6@R*d^{*-mTKt87&pg$nyh;yLDPdiCnE*Qb_n5&7Lfhlk^WP&1>Q_O`i-?GI*mFUO?9v#TqY zYMu^ViJ>WzosI|RmIr*|&>hBUu_JB;7*&kIOV$|j_{X|D-H7vLWQu3S`2v};2DyK2 z#>6A!fcEGxv6GTtNWQ+*?s%^MAg|`UQc7?#}vMDg~Ip zpDw}j5N094H#7nsnHW3C(_igw@cN#M7K>3%xmkkzl4(ilA zEvg-pR?g)vKmkazqWSwnFzHU8vQFvm1z=hLn3fR%S14Ls|{d)d=( zoUd-MUYr?S8LwH;Ho@JFAY;!iHP}h)BJ_12J6^9jhnwhhup+4|N4??;C+XlhBn@F+ z0ZJ);anosM?#|QC9M`JSc9gczTGIXF6S98+5m{&sXf}bu{lCYtYlzI{iO^|)@r>Ka zdAS4JESk$BB>$d}D-Fn4Gz6^32duMnhvpZfZ<6;*YizRhx@2%&@iX`ccgZq9BKM_B zzhfBK=!Jy2p4Kx(9!7H;j-!z;kjt+V@d6_W4(7Bdi3$l`8{3j$NDQghDX?Jr#AT(N zhf+oS@0Ac2gyS$^&9eLI|EkOf96T<9n=y3H zEj%FleM$Nn5kSc;oXzv% zW76FqQnI@xlv>tb+`AYfTP1~`=jB?+2$&0rYmONq)Vq-QG}YBoeE~jFJsDjlcYKaL z#QmBymAWrAOr_6fOr?`aF8`c(l0`;*yH#tfJybJZuXrl=JR(zzB(ku%K}*)B^5<%M z)M!%mbjd!fZw(hXyXd}0$eV-1ZwlOOMN>gtS)-TTKm2|xU3PY;l`S-rbIgsIC+6dc zL-a&ti(Y*-={w&rkrYVe@I|90chIVlH24O|Y%~IE1+De}P|QuLX8?UpD}`QRv8usc zMChS{*Ku<`xEQz{k{@~*5e`&!O!!Etg#S{U*~@28ub>ZF7wXDMEjpmzxQEn{R+5-d zA!?86-lA4^N5}fSVLwvw;+8N8TDrS~5OYsl+8Lr9D0!!xsYQD6&Y?0BOh0x!Nkf@uK zIvj_4P_-b^iR7;1QKkPdA^Us-*`vu2ZS{b@`s)0eak}-CPYggj8lyW-`Neyc2F_d2 z*jBgRjZ-*`es#ph()-Qs4#wulRvLS$96MAqfrZX(v@2$=ocBu%eKt6uXPcDBh$^kl z{)nWP>-l&+JS4wleXkSrvXeg+C3K67BC#@lk=8Fw>((kW81iE`r>$n=h)&g}R-5O> zO$#A_!l|`|_z_xKi-+S(WwfZ!%kuI$<%gt=tu$(ZR=KsbE9)3MD z^msY+D1EMpUHI{Qtr=`O!TgC{YSG$QFnq^#G)kmx7rG9jx_5jW92!1vKo9oOEDn7L zsjD3(k@U7k`qDc~n}~u<1G=Z3u)`Y)5OxH(q3v@YvSpS&bQ&_y*W3em7;t_(7_Q!~Rklc1nwnpGC}Po~+tkXw z_?6l*xm~;H0-=vDEN=LAmfd=D?V;5cJzydz#K#fcqb;T>SEBAI^Fq0&Vd40QMMUN} z^2%jBDcQ2USZigRct?wLX}fus4!YW)eiCFyd&40g?JCGcSnNh|gh(GB)#~evgA}eA zM?BUmz6Ak_j`micDF0^G`p?JuD?lYIuzH>*JSCaK9jczY_7uhg-wo~}6ahUu$$S?n zqXUFgpWJ|9CmSc82OHkXIzfXS*DAYcym?vUS!Hp|w@%3Mj1~e<4;#;N+IXtliVd>z z1{tv7V288vwGQXOSlm?Y%v>izMNE`!pw6aw2EWVYpJ`@`_YDs0%$$(mfVoAtJ}B9+ zcEH&%?==2PQtaO9`PvSCo>08(>U=GMx~Hb<1=yG^v4T4DuHU`Z8$|XZzq1DdL`3mUa@cH(RG{{9cQ@tm9kYVpf zUQ0ul-#0*Z@z7Ra5)8Gp2krDRX!En@z}uEzU6PqYr3Kq&Oduxmhk?k9Bpdfjq$|K< z0F&m`CVwcbyDh}KJUCVm$t>{0)GZ~cXEIAY6Ldw>b`ZRW`F~#YW!jopKDQo7FYk8xwEp8M=A*jx3gQeltdv5|W!W`4_XC3Lf$1d*{pN5B_ruz3CG##Y4l9T4CSt7R`+qVoX1CTF9U6~dpR zy2)9jB0lXCFE3^{MtUfcrypaZ-eYvMv_&sj#Ky&jhoTT8VH{6k2)1|KdW20&C|LK~ z|CT1%R+Qu-7I%aeqEjK5MTGQy6`Ahluyvot4zqQ9bL)RvnrFSJdF$;{=7)sXpGLd z@N-LambTluvwD(V_Ozn44VUMUk6ydIx`^Eck6A~~m-Sd#StAg>x`aVU+Fa1iNC8ms zq95lw4ijx|6>O4t`sN#9_Lh3T@tFTc!}TsL^xw{m2LqvjEM8Hugj2+g@pQ*g;7Nmm z@YkTl&k(A&_y|fYlMsX^FJuT+4;X2pb{T9R8~i4mlwo?}!~%Z8KOprT6yJ`W`X=gl zPGBTM59CAu&h9-6Sfa!*NCij)NYWOQcw~oW;=W7{bv(<(z}QZx@P6zSd>H0EMg_{S z_^^PhJ%6q}x(M|=eFnMsD3GBS$`#HQO;QZ&WIAlRb+hhxnb%J8GSQQlQDlzoC5y&e zWuZx3SsS2ls;~eh#Tk{-IeM6AiB0F~?ZiACkL%XQ&8&vi5fR8Qxp5w3l- z*=>=Zrp@8l1>+zOk)ebrz7etQ7BDPyO1G=mDA0k*O+Gq8KVWHuw*Jc!IUX344mjj~ zU#^#kC2(iw8gNNY|fmxW7o4P z$x#Od@*WNY$oX_WHxha@AL1((AI)gIoKcmu8^;EJ5FIbA6<$cK$2@e`CeM=}jiRpk zOwv%t5j=hB2Soa@MpQrxCIbxaKYMt@!q2212M|tZY(^Xp_rs!>IXDy5p^5mRHjLXM zkIeudo0pW+kBzLyJFK6RekX#F%XO;^rrS+8RZkkAbHCQ1D}P1#$*`e9X4Nx){~vkj zYWbu*i@;PP0y%*QSa4XYki#Xx*v!#k!$BCjHq0B;|wey z*o`S69JeY@h7|fm-%741BUAinlVF$(`Kk&KKfQbySa>5=cz8xUyUm|vfyH>vh^r-d zbO9b^5_R>@Bs@m*w#cwqWLAmll{wpD%7Vy=l_PD-v5FyxT=`iJL?*B!rjO7J9<5%{ zQX$>;y{J9I0VTac;bKv}a&NK$5szd8A`1*(xO3RXFc@yD7y`pf%*nD~;EPVo1f&w- z<`28@!i4NVR=&*qH9eoatoba?nNL-I){Lg!HPXhsGk<7C=MM}!E$pr#nEqjY#(<09 z+~e#6PU7ndV`@6m^l#NUk)~F(g$s*S@)K?=!T!xthUi4ZXeDz_6Z|TE!gj|PNshW? zt#<_a-u`W@rXv#)T3KY+Id^63oTVRT+d0$mA4Q0Uft6FYqGWh6@hII|uUF@pbjMmX z0s=yOJVd(U87Maq2#A}4G|6#9pndei&2Bfl3<#fP^r>E@sew_SAb&mt3o6^kLp zZ->ZuM77JBkp&! zs9y1~vLM9Ym@T8xa;3m@edTQVkNagE4YLf0@Zp(PIQKZ;)s=B5qNcpUIjb*m4$Ld6 zyO96x=Kt@KI;6WrYL6UVsw-P$y>GbF>ENF7vp~(!EhI#4f$pe4cNA){UHr|@5s5P* z2zQamVyq%sHRVAwy%H_b9S`a=H>kl@1RxZ++&ki(rky7_J|Qg$eqxFoix;~N zR8K|U6-bN|Auf7~^tsA)+DwajRXSeUs7mNMY}0qK*yter6y*vE%*0(5J+7$vE?Qh{ z9ovND)OVr?NW%8e6Z}G9tXUx)$f8C&xtmcZV(dC%?oK7JOmwU1#;`i(aupW~Mz9|McRGR&z5)k7RUk2#S52WeORD#FMvYvks2H9!{U| za>{^DoWaHQ(a1RVHCB>4SxLTzgla?LBH8*&hHO3aLDbtW(yVK>iL(6nC$e5vru;Wj zwGj5e1sbhjsZz5nf`9TRsR@ovYto>xR?dKsfdb=i%4qIg>O6>+v3r!f>H^M}jF)lN zvg+xe!7t+^g6X>BVPHnZS_RP}g@_g@jM%fHMF>0Nqh->vcA|Fs>Mw9xl!P&s$Iq_rDyfSZyB?)ZHm${XjLt&1$>imD_WQVXi*xVH_npA4r2p~(i`yH zKP72{QyhdUYGLIcZPh%ztnampJP#%_pejFOaXO&S-0USxCjGO5m0@|Xp*5C0u0*F{ z7=$}d0*xuVPm%V&9R0#|IC?(oAPBjGrHT&4$QlUO2Nq8);OIFM5h)OtSJL*D`xh{g zG|g7hMgTnRtwk$Oa+AvRGL3Gqm?Gl(uX3@rm!NG){?bmGgTY^P z#}&F`BOsv$yTnr!jDa|M4OZ0*>m{97$ouR4Gy-y~3caeQS44QX2h_5ctsD?T;zrGv zxX&G}+q@h0;NSpEF8ZPet*8d7%w{_UfPsw60HZ`0CDv%cp)DF} z#laFF1E`G73p1nDKzCdFqx5AfW_;OZf;a?yygrDlRcWnuyWMVI>)CH157i&^F8;O$qTsM-{=4N{D1qaWS-|f_n!N9?m6e4^F46Z#y51j z5=}hUxn#-?V^N4*RoqKi3^MGSV?iU#2U|HXuQYB@qNRR)zSeLNrkA}4TrY-}+BUbi6pqohRgk~56iVJ_kWo>K70~KCX0pF3 zV_A~ML7MqikZl|+(6lZ{g^!Rvr(j*6`M7%`BoyfA3+;;grLltXB%~hok$kUAJ8;>Q zq8$-1V8qQiV#K)rc;CGLz`ghsjEL(iXZUK;7Rhl;fFZW7)(!$egI~&&uWI zM8UX;jC{l@WA$@c)B+9i=eH_LMZaG2g1sSvZNYBb_LqXjvh8+&Wjdy@#M9r6BIzqo zZ;-$J7JW;z2cFpu`^I+rXZaT#b38}g13QEm5h|EO%|U?K;xaF5YDuZ~vJ;A3wI*s! z21$dBJWmXbcyw{W|H`{p*F=Iv3A8UXvHR@p_pp0x%eCx^qL>x^ew-^(oR5C~$B5T= zEoR#b7+HvZK5#9Y$hib5DlceYPBs}_%(9kufMj63=0z=W5f)_KRHNoa8HmX;o?O!nm= zIJSLno)XEmkJ*IzF$_W2EsDi;Q?2_=7Uza%Xw=$eVqJvg&p-K0{xx>u!$%*n^NTpw;$dN9c?bdb{Nc7V}p)`D+C!IocGU`D8v(@}e= z+CzJo?`c2A3MX;Z77|W5Y5~K{ps0mig3FZ!bm>;uKV4hFQ!iQ!6DY|PB>1V*(TUS> zDyt&B9CAg66TIb~56Hf+u$wBlQO=Cv>J72TBIo=!pS#dB05A+IDQ!IncfBJoST@{a za)@4FW^!DrO`Z8cc^?drE;fvIDCt3W0;-&ypG@D*fKPwk|*a@AM zi0k2wZ}|@ zh|~M2ZfOXrGop)TY!i)YNKHnYylP5eGKjNT(w7DBc}PAID>Ah`jHY-NX~|a(kV77J zT=DcWOINoSEFaWf$y@J_@0hn2hxsthayV>niS7(GjDr+}c+Ipe7KllAjKz;t7U;ig zYRnxDQ4f;|o%d9AU(XWJeAAj9v@LiII?< zBs;RoOlRdC^Ke#@-gzfnNIUdMZ+fA8x`zE$sl2W|$a;a(+u2ab$GQ{9t|Z2p-&gVX zP7Xq`Q)#2-b_?&P9+I6Z6`p>Oc|8l;l85K3U#5OfmruM+|KJ2~pv5G^xNjo~YMk;j zWqY{Pyy$hWx}!1dcQWKGjpp+hlKzGCvX**$Cr7RH5?^2pvW5zz!>|H*EW{nQ6*j9} z_f<0^D`K2BqmrLT3d;WZDuT_xwaS$WCi>iRE>0|#MHLrf9uXN#efa0uJjGpaQ=Yg+A!BRww91>RKM!%0AnKgR zP!okYm9ih^AV2Z9D?M*okAyik?vBZ4g-Bt3debm`l(WP~sBF3Za^kJU014gyx{8r_ zS;a_f9vUMNZ!amtNNf(t_iX=fF%nH2BXNwnfEbB3#7L}bR*F~R<&iNGO+#ZO4(pLV zX;=kvQjFFgu+UcV?08A&%F!TQsA92`>O=t)YjgL5dmiP!MzZM=5 zAK@AiAMrZSD<4al{@aIf0Jw_r5l!;4`GdVH#77)1#z*wZar4dih|I`-^`T#V|8soA z;X-`G3!!3s#Ni?F5l@9obE;GMOb?HbpzZwo5%Cc}d|kyyKqmP8@c4+#?Eh7KM5Y)Y zkr^Hz(f4=aBM!4&x)2}n$(~|-#9@|+3h@!&o?VQO=u;p45AhLCU1`Qg^!=~nBQl>6 zA2I(Lc$jfdH9?d29`}b1XnR2TAlp3b600YY2lO%jx!2ush%rfwm;Rta!9FVVmm4`-8NM1`7 zg4U2dIn0{#5iKEDOp+-qp-8V8{t$xyf1t30aNoi45&(|488#^3bnp^_Pb>{dFf1w!qau=_er{u+RPly<{3~40?t| zD?F-FIj8-6kJU3QJYvk{>fPlkLpOce;6!ZS;OFV_YLgjSb%@0TE8XGP*zlqC82n)H>beO0QYRe2bxa1+00>GaqtO&>$gKc>yKlb&0~ za0FEw-Y{yxbN84lth?btMvlp{|C$E@a6%o>ktEu z9d5lxE#<`Pw9r+!!?pLEJ>22cmxqTtTsI`#;TRhO<>Xd%HFS)Hdims+hRprcLb!v{ zFNe>+ubV3T0WGiZlA8B<^tmH+jS5@X9|(pU#>tmk)YQ$C3%;cO`W%$K{Zy9pJ=|aI zhbvfHh*W^EY`ENDVrdIYOMm|2|4;D|JNYOu<0FuW zIS`H-{$J6of38`%3c?xbSH^57w7R`@7kEjQEJPz4!733aPZ(w*T|3Z+)x({mxUOIVGoAo1>xwSq|83x`~DPy8BhU|8 z-$%lHq)Cu8{wwveb>z!m;pMa70mkfVqi*ZS_s`-z5l!c1CiSjn)O~eiiOq&frKa9Z z(o^HEh6`v9E_s0DIYn!oj!$|Grb&<$~jaw0<;Z;EzwK?5)uNkL-)$}WoVPpbh?i$_q5Y37; zAF$@F?Q|qQmU>4sNKvnSDoCQS%)OLXiTyId|CFIM(I)e)8#LOf;b6>&Y*UirxYbkrg=>3Q8B3C28|1m}iLjsxJBY@wVj7S%siCI!M6se+!nMiq|M+1E265hY4;_ zwuAj4>bQKSxe)dS8}NQZdt4bEWWc&RC?|N-+b`3H4d=pgOCo-7$oJ_FE1^Mgs z{MprCFvH%q5h)z@b^-ft*sQzg|7bltIC4E~gUk0eVU|y)Dyijk2ptH>9Vtcq4IFfM7~lvo;>j&RIp;Fe~GhLeenzB*%UKedqeUYGtF-Of?jjCrm{D5ps>J49B2;!&P*L>J+vrscxkkt z{n=ntwjysMZC&3+#srh}_-DkiHsi7HX9eLl6h)q%s5~E!X=efpD~p@)P#~)6$!|qH zzY@NO7~n2P^&F!LK;9Bw-14BC$rxISu43JI#r%RiZFoG9VW`2q#o|Y8{d-( z8Bg`F4}vn+^CS;Cv+*3NGP|;hzu6D-ii_0m8kQ9z-SIJnD5GUI4W3c7nUOXEai@-T z_w!EZ!$PT73uQVj!a&$O9BnkGZ6P`yfBKqQ3)AJr)74)WHjEC21F^J*1Ch~DZ73_h zahW;)h*szMzo@vZc~PG=o^wdJN#bWX@7ocx`hY2`nG5_EkkvNv<%44#(ouV}@h>S^ z4PLT_eSHu}>aJ=Uk6?hPeorM;PS2O$`@H(=E7+sw3I8v6w&{bIlEl^r(^#@>+Rfo7 z%WE4#^066()u7QUUdW6WK&_78<+R(R6RPhMu31jwPZbhDwTwBch^xC{dB9WS8YZ!f(?*?a;%tX9QMBD+K zyVLPY&2`9^sI-iNwER|@bX+DstWJ@DZmyCzBsp>Sh$4A$Eng4N0v#&JuglCz?65q+9Iqw2@3#H?#1-x`Wog z3OnI1&l)d8YE)C*^`ecN?@}V^4Mln)8Q?R+-bg&*5t9yT=i0Q*oZgMCrCeXLk|)y> zjfHu{`iv8a{l!?H9?{ASpc~dp6<=dMFtnRMq#2NOwhq$Aq@NWCGAZ;9r;zEhKQe5b ziuZMARwvDZCNWpwzSA5tqQ|_^@Fwt^0EEf!~G-?1p`9p@O6= z9mH!IF@N1`^T}{_;Mn!yC>>qv==6#FaZs3FN0PiT*LjP#y>1#y^XC_&`7`_>g#X?l z@jok;e@6TdfYDva0n1&p8c)QD2iRJH@D1Do1*#S=#DLQgw*Auq2JUO&SK+FCFIK=` z89rMrq!u|uHu;{V>WynVWxS6bcD<{UL>+f>f|7qJQ3!U(%XlJXsk0%}DN&vtEoy;q zxXMBj-48#Pr)5Gd%vr2be>xs(@?^G6g9Pu(sf;eLy}BBk?wJZlwrDISzcrOU+Q-@! zqmpg4egc?v$2tJt!1Qtn*J?e1dTU{%>nEZcbk$X$$RihhCQZLy zaf>;?!|*!i6!1C^TvM>+UwS59=R9FAHEz!9<@B8i+Id`oc21+WQ$6h{f25#M7W~!H zXGiV?-daR?hC-1OV-QoPU*S#NaWfb5c8g5I_l@f`&ne|E;BpuX^15Xamh>sh9pk;9 z)~8n#tE9Cx$7iqe5+PqPw9TF)!hV%*E405t5#T*lHzj%6(r~V5_v|(nXx9%TvI;36 zhWjJV9kl-+^0~qX6Q`n%t-e!-&$-vBInODv16nu3i|H?sow0-8%b4f+(FC2I9iEK! z#KG^KvYuEwU}pVok@2l1g{;3a8kFy52J&Wh9!?hNcxBn9oY;}-8HL-|8fOon%gaS} zv7YD#0{(3xZl|qe74a;cLMqwC^UAHf@lZp#QS)^1JRBOkU)*`jl%R+IlpEH>4&%0M zMNRzn?Mf3jR!mE!N10TY5!J7e0>s&jndr+E(NHCSI+(iAE;pgnegyDNR5@R5h2Hgv z4kFJk_9WZ#;uKhQw`fnkHH~Mlt4Fk_j*m*V4lL2N2djYmgoIJP%p702V_gphmvp*% zs{BQaG_G8ZA$3L%4yD$V93|}c8QOh1$;+sdz1+Iq1GF(ou2qTGbwIBM!ldgVzFFW7 z(VI9{KY8V?G>I!8q|z&A(_iw(UsK4TK9{$(eqQ{`$=3Ycv)B&oq}8Imr27w3vm{~L zEy4-Irjq1_rMa{*u*81d!acM2dXtHs6305LMI{7?l+W<>SlhbZ*ZrBseU8Sj+Elvn9u$BJeT(4t&O4?`VWKZm^(_cE_7>av0JBZ8Ztj2N8>)6B#ZN$ImPkNP9=RB+FHz)KRY;EHl8hj zR^x6QBfl&0?(xT{d|}!g`g(j#%W0f(PhK?ZS0nLk!F~IlyndB(-!8|Z;+1sa$ulS5 z5FNVLmu-E1b-OiW&xbW27BpUxHrBBj-sLiKm=XejqFCbwe#aFTP!%2;g_GCqW z+UaQ(ne;gNbwGTQF7Ir)OeD?TS9LldWiCgKB&t-}Vu9vt+9ipx4J%GUO?n;-P zre<)BSpPy_*GHla-zQw)LAkPa0mK$uAB&}1=r0$(6T9^Uyz{BBKU>yLjpKd&mOiTO zBlzvQh$+$=+_@N$o)F@Ua>0O{yqF26b;pR>iSlSDZ1{R&0eQi7^d=*Ah@;qZNbpmR z>m?IWIgD)SLHR56JEK4kzC=Ufc=!a)jsZ*DgW)J8O-)uVFkNKrE0&vVOnLeHGUgTe z39BVOhFYq1BBzj3I2-rH;f)(wRhoY7M6)G+vG{?;tHfnazDo>XFkG}sPIY~#U#0&5 zzNaCvN3Q1pAnok=NmPD4Bo_XZP7jY!^UMg;QV!59*awE5cYpw1M5H$!a;$q-2``gq zY7@kbT%tvg?(HOD=%#qoxFJva>O;-EAY7hPhW4ONvf3!*GcFgdG2$xksrWg{rzX*R z{0T4(^AgLkPG`$PVmtq`Wj5XIQ=(&pN8CRfZ@J20u0h*<$js@jvIiQ^GXk%QV1RtWZ%%pOVMB#T6^N+16 zp>MwWw*h!;vY$o0gYOfNQu}kn_3!zsa<&6!nT)C#?3EVB`CK(^*By|W6n-v|P`%01 z+?l0K=d6wyb5)ugOY3|ME2%MGcV1W=5JyFRP@f~&>yujySBrkT4Og_)#_aX_J$C(e zmmYMsj%KOlTHA_N3;tR@bbo`AUUxW#T+XX-M_7)I-JpeEvQp8YSo*5X^M=qKrsuQKPC(2T!6ZLa;LCYd- zh1v-ls!{(;jyHZTIj`g{_iLjHFOu}4|BFTC+uWob5ED?WWXnjI(Erq z<#k=k2P+*8RPF{XL;U_1{;E~<&pm$!1#+rc6!1dctA>%H8i(3&{m|9D7CFn%0*9$rJ8Fv)3vv| zVYI$nqqRouoB{uD;!OYpLgnLf3X*nMvsg;=;z*Nv6fAo${n>|T^dYaZ z<}1!IN$0>OJ`p}H6zLOj8%p%i$8jePgWc44U)TGLy?~RTw~X|Oj7}O^1QvCSASdt{ zGao~P!(g^ZxRI0&A*yQ;D;l?lqoltc!-jS81Ipq#&b3GP+lYa?j9Z`1A#J_w+GN_i*0OnbH*679!~8i^m7{S3WV)O3-ZvXQ zC)%OSc=i_bnYp%-_&Ld|@`}rQ-(2&7aXpx2B~z|#0O3}qx)h<;PH)5>zsTg2G7M$@ z|kEY$dFKrBr#C+=leARM#aPj5w*NRt;O|fv4uP>dlrTG58X}bIOT;1CO?`?#a zxgG{`MA%h-F}V2kT9hMrPdk;R4-43-GI7vhQUM<03^I_xL6qRvDm#S@o|dGNC9wHQz~`Zpn*wH(;|q#Cup0=*Agx(@mM=j!T3m6k|z+fXglNU2we8W-Ky2NmJQU!*#DneW&R>B=cOi@s}1;$WiARA7m{g zQN;w(c&U#%^Z4vWOSSi78haY|R-Y3)ZH;Y3RTCTcOe&@ok_mG37U!n;lgbdLEVe2e z33McO@qn{j0Nypo*C2n$Kn!%YG*ogqTQC+5A)L}hOQnJ3_HvP-rkU>}m8mzLZTNCg zze3YFb%uvSQF&|D+-Z(=A3`GcWs4uL=>odr;!mZ@n)eJWMhuNH-@?MA;cC=W zl^ot__zwpGo}~X(1wtYK814iCyuh;Mkl^hBL^R3YvvsV3x>2RT_*6Fnd{S=BshDLt z9KkqTG(Z{;P^C#45mv~o2+(yHOCIevJ5o2J*m;g;LNRV0sFEThR}~7z-=Y1~-+jx} zrWF^rEFa`w$zD1WDrnuR#EUxpogq3eg2PIRZ5)*&O zm2ofh@`8X;@W8wW_^RiNNTorijPgevZXd10VM5I0%f(vy4-Kh@nx6`7%?-}spHAgX zfk*dE*Zq~8t-($Fx#V(i#`m-E_WL@OJ3hCQe`8?TuHEmRD$|MZ_yG z&%R1(A^o-Zio&Qn9(s_+(0#_>+s-=pD++^O7omQXV~7iFP~DMAmJfW>(TNsMDp<`) z!`%Rhn+5GKDl0z1pi~fpg!86TjUV*Gyu0nm*xCCE>p8){&boY_zh39Wv{ZZdM6JWyk-(nSh8x0@(p)!e$_e4 z*5`1Q((G-8WIH+*3`El!$#FpupBLyclBk$*cH)h!Xpz&&QTT`OLnj*rjspLRuA;m4NgVs|1JJIKOsL9OzvvhL~*9Obj zgw+hoX45@WL^C}g1*TVYf+w-jDRUgg($}E8Aj^B$@k$3Bu%1&W)!x;kw3#>Hdazqe zBE6$sPaGqya8lRXoMxj}OhEeipeEWyB7=t#maAiBZ{X*9dLolvO1s9@(AU^4i7k?sp|JOSWu z1;ebU;^+zKiC$R=D{TgTV<+k+kV;Jx21}cOZXFJksM zalZFReBL1U%q8nz1owO~7azBgHqnjDK4Dw52j$!Y14_)Z>sPNBVoojC$nSX#(9sxx znr*oP|154rY=1kH^}qfZ+aJ~RAJwlKT2UZu_$nCkSUCz5O(YA&3Q!k&*OZDj`1q|o$ro+s zc{2^Af|Ci$OOeYmrzf)MY3ixzlNy4-S3a=6c)Lg{pVNE9vEfS=Q(o~L@g7-oDtUTV z!fnQw(K6t*Y-m&A+`Z=-Dmj_KB_Kh<4c*33-}lYsWTCWEyhrL*0aDZ3-7p^JD0b=E z6bh&9DpwWBK?vWB$u)>pus_32>(5ws!$eJ*{~dfkTY?;8e8KJ6L0<<%YX~BTyajP# zi7e4?;+PoPPfOU2Ww!ti)Jcj`xA#cH62wl;-9l$xEGBlyb!)M*)jza)KlPydG3_Sfs5dWqYL)+k* z14$9Lp8m-ooU6TDXPb`*63?XX8#%%>gCcu*A?FCng2? zas39wSfq4gQR_=IrBGv^>GMaL ziWMP24~%C~j-qMG8J?H*jB3zNZ1PfRsAYoP%;ZFDvKr!Sgc=->kmx4j@mI ze0|Y?DR$q?76+OB&jDp`s9O@_V{%Tye&?+4s?umwzV=mSjwG%B=|2mfO+hW|7ZpAl zz1W+uukNU7T2Z@$_h;eX!S;vcM=xmFv8ueOj;uX}?Px5qC`|r@)6n?2eB=pgeP`Nb z*!g!5&T=x>!7U>{m^6Gn;efn)1u>e1npqprzKRw4dk1S*CB*w~7>g#jZkx$;GJ~JY{DV^ zSpqWZ8&KL)-KDTcwPG3nBUhqTH4V)Nd*x5mLS}N91u1T&`?e*{VOI6S$C;8zB;M?^ z?qf$+`ChxC$KOLovad;wq3H=2ksd}kKsxka{&x5nxrU6<&cQL7_V`)G=s{IEvZTLm z_;%8Wx7D(+Neksw%ydHyozc|@$o20GGH$!P@)hd3zZd9&ogsPCavWyxvF!;GIYzvL z-pPL?-FGoKN5U>L^)Cy6>SA23YggB-EknoU!XKL(+{{mF2bRA4=&b<`YrdTngZOA@ z?KE;N?A3Lx4e$Wpzfk#fG}YOVOTd#Ats`=-s`DcpgM;Vx%C!@!U!xvX$V;Yi&CG#h zH&iv^Q&Bn44BA{#+8g1o^B}Bz%TF);eNoc?elwuLVxWPY!x#I{G>c|xyn;*7N3T+m zm0!65)%8(z|5T&$MZeT|y3Q`z`#Y(*iw}mcBQN+A z{cMF1ciXt=xA9G+FC@Q?KYS7n1^@nUTo2O`lw+2lgk0&p0arN90eRvUzUyL6&F!AM z9_9PY34G#KRMZD;{B^biRExbUd$=k$Ej8F$#eqv`;O^z8A$^RjqvxU}05HE*+A6Ax z#xkbS{=E`|WUEf@oT>Y4*ZhFA?K{qG)$Uie4qJ8M;p#L-Tz=sKri1!_0r&q^T=B%@ zl^i{-B|ik3>-|k*sb#R9v-{71b4KODYW}2kk+X85R%DOneOp-EL@Wt<5qNdVG540K0 z=Qn+O-*12Oo8R$<>!1TY97(f@7~U0URQ}e^N9uV?r(Q+Ic`LsOvTJ* z4&`h0ZrV;~x$iNN1(o}lwOO}#oA0>QbRLU}Ov5xIsCiBn{L$=_=j-DZ;llBx<&Lr5 zPwPKezGBdE>;mtniSyZUtXO`C|Jd>s%75%+!GG)xPp2_oyI$CxL!8XG%&_%sXUmR@ zMD$i)mJKych$_Nx#nN1FNsF?+rD69h>fR}f%>L6Qp9%NbLtL`_t0Ir}_i3-{ZM z#juW%oZ8ws;32_p9_#cu!@btbDgN!QM(VWqvNt4m@RQr{ zq&7>tFS)bQ+P!ujn9FJ;F%1;5<}Og6r`GP|>C*Ux5Z$6twAy}>rJ=b#C$z2Bls82> z6(q+fNjs37jGC<)Ev5Sr{TY$b@PaU|3$9Y;S8E%o%ep^{d;D`Th&_MK=h?TBh6Da@ zS?Gz`eBIb&c0k<`Vlgt&fA}+3;c0`yhbr=M7rj&?-OaZ<_9ak&kgz%&lRx^aIxv8> zM$`Q6M79pIJ!@-8d?Iin#9sMYmFcEcUsWYUa8jdiWZ?v6;B<>O}i{rUT zCOEju68{LL2P~#pvf&%X4S9Ap^6a4s|r&ceWTs9`F70fCaOlsVnL(dy4G@OIVz709yMN!`C6DJ1~ZVz5Yz?4|gRr2j;+ zhAYe9Do9A>^*QUj$y6yd&LMQWg4}TEPrSxyW1(!H9?2-%aYlDNV2xzwd-u?z`j6N_ zgVQJ;R%&C^#Os!+>seIw;$l59J#AC>0|36D-EkiR=L7N;_-pyH)vzL>T>VjWjl;2W zG{3|_Toy6WoZhS@o-sI)rW?^)Fn~mQ5LhfvEM`HhaJn37xOC9JEr%xU_mO~Cv3Nvd zZ__HqsaKE4Fux1xZeK7El4FKaelgs;vI?W4Ptb87ZJ$AOh6?7DYQA!fV^#y2{B|7_BmpVaBU{RU)Z-8_MnI`5+FQbb^^ZP z$37$srizoT+`O(mAAe3HnuZXeD4-T&@~QczOD=-j*sk#)-~3F6$@yM3IRg=AzNc09 zJ9SzwwSZO>V?w~i{RoHG7$UC)ZH^K4&5*6=1N+q;n@+z#C{J`ihWqnKS8RWt6dXQd zEo>gI^mf8{oJ=&8==Q6GLw9Ydv*xv{n0U7({x5u`X13 z^B#$l#=-&OU{(84AC0QoMWXwL7fV#}T9-~svmF7RO%NBBZ2^iZ^+x!6J^mbZ$a6&Z z(Qe5)o@+%~X;S5VJB$8ifS^1KuM1#a0)EpO2q>OAzk}Io`isJroV%q2a9wWXO)0#; zsDoX7!-d}yI@D#MugT(b13cE+ZD_A3BMhCL<;m~ux5OVt?3Lp5a~~*)v$ur(0j?is zBylFs?x0cgs8*WNfTHdK$!269Bu{a zjOn%Gsv3D^#ImGjEKO?gScV5iRY$$1ThgB+hUyux(kyiJzZnY#?;ax`_$x^aV?>*r zr`l$WYT3NdgvN$H(NrIac-+`pDh2whBl>g`c$Ps1Z$&`D`qGm3raq8v#O^B%c# ztjN#rZ1H}&`lQHoy~}RVsdqXY8go!`y-Y@JQsmeu`mGdX3iU!FWQMKsrs+NQp0#8;x3&5xdx zH@l`(WQ`jwtS5K9YuM+R4Be=?Fu$pP!0}uQv38^8D*BP9<+CaI_Oun&=H;pED9F^2 z0&Tw=uJ1XA+B+e)8`|GkSXBJ0kdYv;9zha)oRJ_Kkg3-S#tvFG>Y5}|5Kk^yU%*p> zed7sLQgkEQKH@}V!PA=x$w23t>9Qu3iw1HA_eex&4Q0+ zRMeK6^AKZEq>z}eg)Mkl&&kf?U2mu4D3Z)?gyR|z#-;!=V2MRE(=;uq+SmC}e}C1! zYJ^N9xMWFxzc_+;|FFyPT&J}wR1M^8+=xeBrOve(F1C@fuVBuMX)(02#(hZiDs}CV z1$=$uUcg~{&1UgA5n11Zr+TChFVvaq>&3y_4P>`l@*)X?%Yo zyJ3APHeI#L+Kv0peGTW*eCh57thL?q&DY-D^&hH~O@a>lno|1gN^kQTtFgdB(zo@H zQDdc*d76j`F-Tvx(kRvd;Q95Qae{1F*OPr~?RChdnqNl9f@$?f}Fv9k30idJ$8~h5Y5hku4uX%O1g* z+xhUL=Cwxx@~H)%)wVy2TP<9ARdB9B+7>W>&}sCm2ntrD$J23oyoWWI9z>7ejLU{b zP~4}^#T>hZeDOxU{~A$ou2AufIQVNjSgTnQj>M=^!Cl2T9I#S1fQm7wJpA zt%X>8oV48(AFWx${-S#D8-4Br*2I1i9_#-~l4B%?jp}FA%o-bX2J0?wiq`LZ3mv?0 zp%(Dhgb>Hewlg2J=0kd7zx?L>qKMXur8B)?ukf46yVcUItyas&6xE#dM`LQfD~1ST zxY8sk;`Uq2CF{nLUBv=zc~s?G4#-cdiw$Pb;O7hS0W%sjiRJpiyffRQ#lx%Zm|AT; z3QHy+Ew?bJ6b*yF7czamVHNyrXoE)fdyeY0<;s<_wwy(*Z{V^J*?Meq{PwzYgv(32 z>;}FpAeqohqLzCLq%HU{bX<9G2H3h8Gfbqu}N{nKTcrqE06>TSE#Ujgmj{va;%-JTA8!W^Ibj>*OTjmS2E zyP@f;#s^5i*#h?us+b){oWSRL4gq8lbC& z*i!G80&IIXfOn^h3Ha%A&xvI2IIpmtN9N98dM;do%dcRV6k0ij$YL0?3IwvbLDytO z7t$Np*N~QrsQ3qSBdMFj_v0h!n=V?v(=^lSv#nB|F%{_gA3rQ~J!3ZU`%)fOYC^Ps zXIB}IYQ@M=Jxl)qvP%6&&q)S%dv=QseVo~MZBE2}mTVyvtK1y%7YNHQeu%!h&Az%o zh^N$?0~h9C=_6-!^Na){Y6!=nUq?RResPud5H*V$4_Z`>ZS>6QcN@z#l7QmzRfLv= z`{YeSnf!&O| zyk-XAS!t83F>~||nIJ4p8&3}!^<~aFNu*cR%*(HG2Y}`pCb?E7d(FasRDQEdM#JY^6}2-RxXxXfINmtMlw>!o#r!r!5ltEuHZ9XPC1XDh3h_6zY^! zAF#s*{O`~s7uRlEHBR-Cv{L!;`>F(>rsoAniF}EaNSPi`4w9s+Of2+uv`(t=U|(q! z!x?T)r1$LIb*`E+*j@2HcbBb8PIYz$N5({fbbYk2(1+zE%gCvo0M#On^v@*`=!~xTlU(T?s)JR`~ z8nn~H-rp--qhLHGHb%c!I-SmvsLB~Z&Ht+^LVaHOh5ET=IObwg)K?bze=GB_yJG*? z`KVuM-T7JxApO3Ls+x9|a*C!)=nEoGXd=0@=GXMaK%MOz`iF!oq0&#R%sZ$aYVUGx z&%H8JqDcCO8R~b=s@i$;e1*_SD%LF2)KfPGH8ts$!DDUJ7@!V3h~-8`N9SY93zMRpH^M*<#;%{`K_CiKf!UV+N{6<+=qG38uo<7Z9 z8nghkk*mbjBUibe-#S%&2YFm!+;-yU*SILMp2TP~;>k_NVFG?tW!*X% z5*?yd{^ICBK0pNqI3Gkn_DPgkW7Rgm&jdn|a9wWQYwf^AEs}bUH~q;{TGC>v%f<}* zYlgM~w4LO|&}XrT32+w=QtgXSyTXP#jS+dAV<~&N0U^fWredZ(&f9i}4Ht=}RG;C> zGrXINR6MFWV7T|npV3zqD;C5g!@eyTriwo}LKW4*MQVD?aGl0nT+MZ7&aB(c@t0=( zqSg+sKRae^0nI+9`54jc%Zdp=wkHWO5Ul$awV@y3L!AeKgkw*;_YERxL&I!tF&LC> z@0nvIQtyox_ODn6(5OpyP@}UTCy${9O{;#${N}Ou22{X+83kM+KX*jk=%0~)z>K`( zn~JER^#@{M`6D#x+6shm?FB=7n6oyKQhq>gzlNx7M-ma^k|hQiCq9tw;r+4Xe=c%o&FkE+1 znTGwRN-v4rtQ&ZJr)_9mN70podhr>sUQ7GDnCD@W=%`Ga@W z#QC!p3n$80K@Wgys&WuoeB~B+UsnL)C5|)tR~A6FxRyAcYvGGps$(YVKm{se54~Y1 z1UJHMPm{jHvq*I!Z!_L-ld!<*ADpsgv5Bw?f%f?vSD-nL3^7 z(X2jK=kfWlb^Bo1!UYH1=z@rT+VeJ#c{E1jfAkptO%+(1EXZ(S&V#Km39T$ly8^Vd zb)=whZ6Uo*f13{%P9n}ZC#-jV6AG~F+GJA>Az4@tcXrZQ{b02U+`%w&lrIM|UHi5v zER2r(*i)hvp72=IBt(3~ESrcvbx%fuu1hQF_i_@M&rLd1t zxF;&0A@R{*x-u75udlLd*Boly@zYfDxJ8h>X0Ic?B3&8?2W8@~YKi+Qk)H**VSf0I zazGgtMK!{|!^x*nA*8q5@!Vb{(xd~Q#Ez+jzPZ>$l5B!eI>`A*?nC`k-!XT6g*EHl zTX!)X=Oi?>cv776>=Aku0>H9(Lk15PF11lj?(Q2n{Yd{obt#Bz9U9%ea8s~9*eTg9 z+_rI5DNeh`dsx=OrZwhnlm9_8Xr*e(anXSpvRUE>)P|%{auR_6K6b6u=e{5k6|`ic zJ*H6ZE_R`?p>M%@RL`j-A&Y9UQT538sA>gaK||Y!RTNuAw9vVE{{pa8|-Cz27VOARJqv}*KjNV z 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 I-OL025A=fr8UO$Q diff --git a/daemon/miniupnpc-1.9.tar.gz b/daemon/miniupnpc-1.9.tar.gz deleted file mode 100644 index 85deda499f30985a21ac63e197fa9c5caee821a4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 71648 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 WmY?Nk`B{DvKmQ-4%LQ5hcm)7Bewf+- diff --git a/lbry b/lbry deleted file mode 160000 index 043e2d0ab..000000000 --- a/lbry +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 043e2d0ab96030468d53d02e311fd848f35c2dc1 diff --git a/lbryum b/lbryum deleted file mode 160000 index 121bda396..000000000 --- a/lbryum +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 121bda3963ee94f0c9c027813c55b71b38219739 -- 2.45.2 From 8e36608508c5e158b7f094058a8d23d70d4c78e4 Mon Sep 17 00:00:00 2001 From: Jack Robison Date: Tue, 11 Apr 2017 19:29:42 -0400 Subject: [PATCH 015/158] update submodules --- lbry | 2 +- lbryum | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lbry b/lbry index 043e2d0ab..d1414bc03 160000 --- a/lbry +++ b/lbry @@ -1 +1 @@ -Subproject commit 043e2d0ab96030468d53d02e311fd848f35c2dc1 +Subproject commit d1414bc03af90a6e0f9c02aba32cbcee894a1ce3 diff --git a/lbryum b/lbryum index 121bda396..39ace3737 160000 --- a/lbryum +++ b/lbryum @@ -1 +1 @@ -Subproject commit 121bda3963ee94f0c9c027813c55b71b38219739 +Subproject commit 39ace3737509ff2b09fabaaa64d1525843de1325 -- 2.45.2 From 60e3f3d12b1126a4edc8fde13f5ca5c9fe57f026 Mon Sep 17 00:00:00 2001 From: Jack Robison Date: Tue, 11 Apr 2017 19:30:52 -0400 Subject: [PATCH 016/158] =?UTF-8?q?Bump=20version:=200.9.2rc15=20=E2=86=92?= =?UTF-8?q?=200.9.2rc16?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- app/package.json | 2 +- ui/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 4b1252147..1c8f8625d 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.9.2rc15 +current_version = 0.9.2rc16 commit = True tag = True parse = (?P\d+)\.(?P\d+)\.(?P\d+)((?P[a-z]+)(?P\d+))? diff --git a/app/package.json b/app/package.json index 1f0bcda89..dfdcf2faf 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "LBRY", - "version": "0.9.2rc15", + "version": "0.9.2rc16", "main": "main.js", "description": "LBRY is a fully decentralized, open-source protocol facilitating the discovery, access, and (sometimes) purchase of data.", "author": { diff --git a/ui/package.json b/ui/package.json index 21acf2fb6..232001189 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "lbry-web-ui", - "version": "0.9.2rc15", + "version": "0.9.2rc16", "description": "LBRY UI", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" -- 2.45.2 From 7c2a60798c23bc6e9305f50a1f42cfdfc158fb3f Mon Sep 17 00:00:00 2001 From: Jack Robison Date: Tue, 11 Apr 2017 19:31:01 -0400 Subject: [PATCH 017/158] =?UTF-8?q?Bump=20version:=200.9.2rc16=20=E2=86=92?= =?UTF-8?q?=200.9.2rc17?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- app/package.json | 2 +- ui/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 1c8f8625d..56b738f58 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.9.2rc16 +current_version = 0.9.2rc17 commit = True tag = True parse = (?P\d+)\.(?P\d+)\.(?P\d+)((?P[a-z]+)(?P\d+))? diff --git a/app/package.json b/app/package.json index dfdcf2faf..5dbb5df47 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "LBRY", - "version": "0.9.2rc16", + "version": "0.9.2rc17", "main": "main.js", "description": "LBRY is a fully decentralized, open-source protocol facilitating the discovery, access, and (sometimes) purchase of data.", "author": { diff --git a/ui/package.json b/ui/package.json index 232001189..073ba6a68 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "lbry-web-ui", - "version": "0.9.2rc16", + "version": "0.9.2rc17", "description": "LBRY UI", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" -- 2.45.2 From 8dca65f135670dfd3164c1a4f7b0aaa7661c23fb Mon Sep 17 00:00:00 2001 From: Jack Robison Date: Tue, 11 Apr 2017 19:31:05 -0400 Subject: [PATCH 018/158] =?UTF-8?q?Bump=20version:=200.9.2rc17=20=E2=86=92?= =?UTF-8?q?=200.9.2rc18?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- app/package.json | 2 +- ui/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 56b738f58..c0dbd5bc2 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.9.2rc17 +current_version = 0.9.2rc18 commit = True tag = True parse = (?P\d+)\.(?P\d+)\.(?P\d+)((?P[a-z]+)(?P\d+))? diff --git a/app/package.json b/app/package.json index 5dbb5df47..131c3d68e 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "LBRY", - "version": "0.9.2rc17", + "version": "0.9.2rc18", "main": "main.js", "description": "LBRY is a fully decentralized, open-source protocol facilitating the discovery, access, and (sometimes) purchase of data.", "author": { diff --git a/ui/package.json b/ui/package.json index 073ba6a68..6b789520b 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "lbry-web-ui", - "version": "0.9.2rc17", + "version": "0.9.2rc18", "description": "LBRY UI", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" -- 2.45.2 From 7e1ca070731dcf9619f71eb97c62823b457ae19d Mon Sep 17 00:00:00 2001 From: Alex Grintsvayg Date: Wed, 12 Apr 2017 11:05:11 -0400 Subject: [PATCH 019/158] =?UTF-8?q?Bump=20version:=200.9.2rc18=20=E2=86=92?= =?UTF-8?q?=200.9.2rc19?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- app/package.json | 2 +- lbry | 2 +- ui/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index c0dbd5bc2..09f33159d 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.9.2rc18 +current_version = 0.9.2rc19 commit = True tag = True parse = (?P\d+)\.(?P\d+)\.(?P\d+)((?P[a-z]+)(?P\d+))? diff --git a/app/package.json b/app/package.json index 131c3d68e..5f735413c 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "LBRY", - "version": "0.9.2rc18", + "version": "0.9.2rc19", "main": "main.js", "description": "LBRY is a fully decentralized, open-source protocol facilitating the discovery, access, and (sometimes) purchase of data.", "author": { diff --git a/lbry b/lbry index d1414bc03..2df1092e1 160000 --- a/lbry +++ b/lbry @@ -1 +1 @@ -Subproject commit d1414bc03af90a6e0f9c02aba32cbcee894a1ce3 +Subproject commit 2df1092e1a31dfef78f04b871e74734f62ee7033 diff --git a/ui/package.json b/ui/package.json index 6b789520b..f35c3afc0 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "lbry-web-ui", - "version": "0.9.2rc18", + "version": "0.9.2rc19", "description": "LBRY UI", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" -- 2.45.2 From a8242bf7b2454caed3ddb42fe99484a262728fbc Mon Sep 17 00:00:00 2001 From: Alex Grintsvayg Date: Wed, 12 Apr 2017 11:46:04 -0400 Subject: [PATCH 020/158] =?UTF-8?q?Bump=20version:=200.9.2rc19=20=E2=86=92?= =?UTF-8?q?=200.9.2rc20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- app/package.json | 2 +- lbry | 2 +- ui/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 09f33159d..657bf0d73 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.9.2rc19 +current_version = 0.9.2rc20 commit = True tag = True parse = (?P\d+)\.(?P\d+)\.(?P\d+)((?P[a-z]+)(?P\d+))? diff --git a/app/package.json b/app/package.json index 5f735413c..dc9556c7f 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "LBRY", - "version": "0.9.2rc19", + "version": "0.9.2rc20", "main": "main.js", "description": "LBRY is a fully decentralized, open-source protocol facilitating the discovery, access, and (sometimes) purchase of data.", "author": { diff --git a/lbry b/lbry index 2df1092e1..ddf907231 160000 --- a/lbry +++ b/lbry @@ -1 +1 @@ -Subproject commit 2df1092e1a31dfef78f04b871e74734f62ee7033 +Subproject commit ddf907231d0a27b4aac621e2482c3e0d80b67b06 diff --git a/ui/package.json b/ui/package.json index f35c3afc0..db7480799 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "lbry-web-ui", - "version": "0.9.2rc19", + "version": "0.9.2rc20", "description": "LBRY UI", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" -- 2.45.2 From 64477c3e5e7ea2fb6efdcbcdad9c566670f20701 Mon Sep 17 00:00:00 2001 From: Alex Grintsvayg Date: Wed, 12 Apr 2017 12:25:18 -0400 Subject: [PATCH 021/158] disabled daemon upload prematurely --- build/release_on_tag.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build/release_on_tag.py b/build/release_on_tag.py index 255a88364..a8fa731ad 100644 --- a/build/release_on_tag.py +++ b/build/release_on_tag.py @@ -35,9 +35,9 @@ def main(): # TODO: maybe this should be an error return -# daemon = get_daemon_artifact() -# release = get_release(daemon_repo, current_tag) -# upload_asset(release, daemon, gh_token) + daemon = get_daemon_artifact() + release = get_release(daemon_repo, current_tag) + upload_asset(release, daemon, gh_token) app = get_app_artifact() release = get_release(app_repo, current_tag) -- 2.45.2 From 1d100683938c5520c2567f835ec7e57855a03f6f Mon Sep 17 00:00:00 2001 From: Alex Grintsvayg Date: Wed, 12 Apr 2017 12:25:56 -0400 Subject: [PATCH 022/158] =?UTF-8?q?Bump=20version:=200.9.2rc20=20=E2=86=92?= =?UTF-8?q?=200.9.2rc21?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- app/package.json | 2 +- lbry | 2 +- ui/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 657bf0d73..820c2eb12 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.9.2rc20 +current_version = 0.9.2rc21 commit = True tag = True parse = (?P\d+)\.(?P\d+)\.(?P\d+)((?P[a-z]+)(?P\d+))? diff --git a/app/package.json b/app/package.json index dc9556c7f..9ea402351 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "LBRY", - "version": "0.9.2rc20", + "version": "0.9.2rc21", "main": "main.js", "description": "LBRY is a fully decentralized, open-source protocol facilitating the discovery, access, and (sometimes) purchase of data.", "author": { diff --git a/lbry b/lbry index ddf907231..3820401fc 160000 --- a/lbry +++ b/lbry @@ -1 +1 @@ -Subproject commit ddf907231d0a27b4aac621e2482c3e0d80b67b06 +Subproject commit 3820401fce1b548808bbed02dfe6bd37d49f010a diff --git a/ui/package.json b/ui/package.json index db7480799..69a6a81a4 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "lbry-web-ui", - "version": "0.9.2rc20", + "version": "0.9.2rc21", "description": "LBRY UI", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" -- 2.45.2 From e6b208ebb457de7e342dd9d48f04f3adee6a61a9 Mon Sep 17 00:00:00 2001 From: Alex Grintsvayg Date: Wed, 12 Apr 2017 15:23:09 -0400 Subject: [PATCH 023/158] =?UTF-8?q?Bump=20version:=200.9.2rc21=20=E2=86=92?= =?UTF-8?q?=200.9.2rc22?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- app/package.json | 2 +- lbry | 2 +- ui/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 820c2eb12..f1061fa65 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.9.2rc21 +current_version = 0.9.2rc22 commit = True tag = True parse = (?P\d+)\.(?P\d+)\.(?P\d+)((?P[a-z]+)(?P\d+))? diff --git a/app/package.json b/app/package.json index 9ea402351..431ee6b37 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "LBRY", - "version": "0.9.2rc21", + "version": "0.9.2rc22", "main": "main.js", "description": "LBRY is a fully decentralized, open-source protocol facilitating the discovery, access, and (sometimes) purchase of data.", "author": { diff --git a/lbry b/lbry index 3820401fc..4698910ab 160000 --- a/lbry +++ b/lbry @@ -1 +1 @@ -Subproject commit 3820401fce1b548808bbed02dfe6bd37d49f010a +Subproject commit 4698910ab792e35f2d7ca228a6acd30e79014875 diff --git a/ui/package.json b/ui/package.json index 69a6a81a4..672726b1c 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "lbry-web-ui", - "version": "0.9.2rc21", + "version": "0.9.2rc22", "description": "LBRY UI", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" -- 2.45.2 From 11ca9dbc51a001124cfa2b9b574e64fa82537d05 Mon Sep 17 00:00:00 2001 From: Jack Robison Date: Wed, 12 Apr 2017 18:13:13 -0400 Subject: [PATCH 024/158] update submodule --- lbry | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lbry b/lbry index 4698910ab..c26f83d81 160000 --- a/lbry +++ b/lbry @@ -1 +1 @@ -Subproject commit 4698910ab792e35f2d7ca228a6acd30e79014875 +Subproject commit c26f83d81f1f94bd17aa6a9bd5ed07fffefca924 -- 2.45.2 From 690d3eb487d9b2570c1cbffc8e66dde93d6b454b Mon Sep 17 00:00:00 2001 From: Jack Robison Date: Wed, 12 Apr 2017 18:13:26 -0400 Subject: [PATCH 025/158] =?UTF-8?q?Bump=20version:=200.9.2rc22=20=E2=86=92?= =?UTF-8?q?=200.9.2rc23?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- app/package.json | 2 +- ui/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index f1061fa65..4c3c4e16a 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.9.2rc22 +current_version = 0.9.2rc23 commit = True tag = True parse = (?P\d+)\.(?P\d+)\.(?P\d+)((?P[a-z]+)(?P\d+))? diff --git a/app/package.json b/app/package.json index 431ee6b37..5fee9fb61 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "LBRY", - "version": "0.9.2rc22", + "version": "0.9.2rc23", "main": "main.js", "description": "LBRY is a fully decentralized, open-source protocol facilitating the discovery, access, and (sometimes) purchase of data.", "author": { diff --git a/ui/package.json b/ui/package.json index 672726b1c..20de137c0 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "lbry-web-ui", - "version": "0.9.2rc22", + "version": "0.9.2rc23", "description": "LBRY UI", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" -- 2.45.2 From f5e68da91bfc3abb40c6f9cd439ded8ae2f87277 Mon Sep 17 00:00:00 2001 From: Alex Grintsvayg Date: Wed, 12 Apr 2017 18:46:32 -0400 Subject: [PATCH 026/158] rewrite release.py script --- build/changelog.py | 173 +++++++++++----------- build/release.py | 348 +++++++++++++++++---------------------------- 2 files changed, 208 insertions(+), 313 deletions(-) diff --git a/build/changelog.py b/build/changelog.py index 724324251..eb03b682f 100644 --- a/build/changelog.py +++ b/build/changelog.py @@ -1,8 +1,5 @@ -import argparse import datetime import re -import sys - CHANGELOG_START_RE = re.compile(r'^\#\# \[Unreleased\]') CHANGELOG_END_RE = re.compile(r'^\#\# \[.*\] - \d{4}-\d{2}-\d{2}') @@ -14,118 +11,110 @@ EMPTY_RE = re.compile(r'^\w*\*\w*$') ENTRY_RE = re.compile(r'\* (.*)') VALID_SECTIONS = ['Added', 'Changed', 'Deprecated', 'Removed', 'Fixed', 'Security'] - # allocate some entries to cut-down on merge conflicts TEMPLATE = """### Added * * - * ### Changed * * - * ### Fixed * * - * """ -def main(): - print "i am broken" - return 1 - parser = argparse.ArgumentParser() - parser.add_argument('changelog') - parser.add_argument('version') - args = parser.parse_args() - bump(changelog, version) +class Changelog(object): + def __init__(self, path): + self.path = path + self.start = [] + self.unreleased = [] + self.rest = [] + self._parse() + def _parse(self): + with open(self.path) as fp: + lines = fp.readlines() -def bump(changelog, version): - with open(changelog) as fp: - lines = fp.readlines() + unreleased_start_found = False + unreleased_end_found = False - start = [] - unreleased = [] - rest = [] - unreleased_start_found = False - unreleased_end_found = False - for line in lines: - if not unreleased_start_found: - start.append(line) - if CHANGELOG_START_RE.search(line): - unreleased_start_found = True - continue - if unreleased_end_found: - rest.append(line) - continue - if CHANGELOG_END_RE.search(line): - rest.append(line) - unreleased_end_found = True - continue - if CHANGELOG_ERROR_RE.search(line): - raise Exception( - 'Failed to parse {}: {}'.format(changelog, 'unexpected section header found')) - unreleased.append(line) + for line in lines: + if not unreleased_start_found: + self.start.append(line) + if CHANGELOG_START_RE.search(line): + unreleased_start_found = True + continue + if unreleased_end_found: + self.rest.append(line) + continue + if CHANGELOG_END_RE.search(line): + self.rest.append(line) + unreleased_end_found = True + continue + if CHANGELOG_ERROR_RE.search(line): + raise Exception( + 'Failed to parse {}: {}'.format(self.path, 'unexpected section header found')) + self.unreleased.append(line) - today = datetime.datetime.today() - header = '## [{}] - {}\n'.format(version, today.strftime('%Y-%m-%d')) - released = normalize(unreleased) - if not released: - # If we don't have anything in the Unreleased section, then leave the - # changelog as it is and return None - return + self.unreleased = self._normalize_section(self.unreleased) - changelog_data = ( - ''.join(start) + - TEMPLATE + - header + - '\n'.join(released) + '\n\n' - + ''.join(rest) - ) - with open(changelog, 'w') as fp: - fp.write(changelog_data) - return '\n'.join(released) + '\n\n' + @staticmethod + def _normalize_section(lines): + """Parse a changelog entry and output a normalized form""" + sections = {} + current_section_name = None + current_section_contents = [] + for line in lines: + line = line.strip() + if not line or EMPTY_RE.match(line): + continue + match = SECTION_RE.match(line) + if match: + if current_section_contents: + sections[current_section_name] = current_section_contents + current_section_contents = [] + current_section_name = match.group(1) + if current_section_name not in VALID_SECTIONS: + raise ValueError("Section '{}' is not valid".format(current_section_name)) + continue + match = ENTRY_RE.match(line) + if match: + current_section_contents.append(match.group(1)) + continue + raise Exception('Something is wrong with line: {}'.format(line)) + if current_section_contents: + sections[current_section_name] = current_section_contents + output = [] + for section in VALID_SECTIONS: + if section not in sections: + continue + output.append('### {}'.format(section)) + for entry in sections[section]: + output.append(' * {}'.format(entry)) + return output -def normalize(lines): - """Parse a changelog entry and output a normalized form""" - sections = {} - current_section_name = None - current_section_contents = [] - for line in lines: - line = line.strip() - if not line or EMPTY_RE.match(line): - continue - match = SECTION_RE.match(line) - if match: - if current_section_contents: - sections[current_section_name] = current_section_contents - current_section_contents = [] - current_section_name = match.group(1) - if current_section_name not in VALID_SECTIONS: - raise ValueError("Section '{}' is not valid".format(current_section_name)) - continue - match = ENTRY_RE.match(line) - if match: - current_section_contents.append(match.group(1)) - continue - raise Exception('Something is wrong with line: {}'.format(line)) - if current_section_contents: - sections[current_section_name] = current_section_contents + def get_unreleased(self): + return '\n'.join(self.unreleased) if self.unreleased else None - output = [] - for section in VALID_SECTIONS: - if section not in sections: - continue - output.append('### {}'.format(section)) - for entry in sections[section]: - output.append(' * {}'.format(entry)) - return output + def bump(self, version): + if not self.unreleased: + return + today = datetime.datetime.today() + header = '## [{}] - {}\n'.format(version, today.strftime('%Y-%m-%d')) -if __name__ == '__main__': - sys.exit(main()) + changelog_data = ( + ''.join(self.start) + + TEMPLATE + + header + + '\n'.join(self.unreleased) + '\n\n' + + ''.join(self.rest) + ) + + with open(self.path, 'w') as fp: + fp.write(changelog_data) diff --git a/build/release.py b/build/release.py index 4100cf8cf..619ad7df9 100644 --- a/build/release.py +++ b/build/release.py @@ -1,13 +1,11 @@ -"""Trigger a release. +"""Bump version and create Github release -This script is to be run locally (not on a build server). +This script should be run locally, not on a build server. """ import argparse import contextlib -import logging import os import re -import string import subprocess import sys @@ -16,122 +14,131 @@ import github import changelog -# TODO: ask bumpversion for these -LBRY_PARTS = ('major', 'minor', 'patch', 'release', 'candidate') -LBRYUM_PARTS = ('major', 'minor', 'patch') +ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) def main(): - parser = argparse.ArgumentParser() - parser.add_argument( - "lbry_part", help="part of lbry version to bump", - choices=LBRY_PARTS - ) - parser.add_argument( - "--skip-lbryum", help="skip bumping lbryum, even if there are changes", - action="store_true", - ) - parser.add_argument( - "--lbryum-part", help="part of lbryum version to bump", - choices=LBRYUM_PARTS - ) - parser.add_argument( - "--last-release", - help=("manually set the last release version. The default is to query and parse the" - " value from the release page.") - ) - parser.add_argument( - "--skip-sanity-checks", action="store_true") - parser.add_argument( - "--require-changelog", action="store_true", - help=("Set this flag to raise an exception if a submodules has changes without a" - " corresponding changelog entry. The default is to log a warning") - ) - parser.add_argument( - "--skip-push", action="store_true", - help="Set to not push changes to remote repo" - ) + bumpversion_parts = get_bumpversion_parts() + parser = argparse.ArgumentParser() + parser.add_argument("part", choices=bumpversion_parts, help="part of version to bump") + parser.add_argument("--skip-sanity-checks", action="store_true") + parser.add_argument("--skip-push", action="store_true") + parser.add_argument("--dry-run", action="store_true") + parser.add_argument("--confirm", action="store_true") args = parser.parse_args() - base = git.Repo(os.getcwd()) + if args.dry_run: + print "DRY RUN. Nothing will be committed/pushed." + + repo = Repo('lbry-app', args.part, ROOT) branch = 'master' + print 'Current version: {}'.format(repo.current_version) + print 'New version: {}'.format(repo.new_version) + + if not args.confirm and not confirm(): + print "Aborting" + return 1 + if not args.skip_sanity_checks: - run_sanity_checks(base, branch) + run_sanity_checks(repo, branch) + repo.assert_new_tag_is_absent() - base_repo = Repo('lbry-app', args.lbry_part, os.getcwd()) - base_repo.assert_new_tag_is_absent() + is_rc = re.search('\drc\d+$', repo.new_version) is not None + # only have a release message for real releases, not for RCs + release_msg = '' if is_rc else repo.get_unreleased_changelog() - last_release = args.last_release or base_repo.get_last_tag() - logging.info('Last release: %s', last_release) + if args.dry_run: + print "rc: " + ("yes" if is_rc else "no") + print "release message: \n" + (release_msg or " NO MESSAGE FOR RCs") + return gh_token = get_gh_token() auth = github.Github(gh_token) github_repo = auth.get_repo('lbryio/lbry-app') - names = ['lbryum', 'lbry'] - repos = {name: Repo(name, get_part(args, name)) for name in names} + if not is_rc: + repo.bump_changelog() + repo.bumpversion() - changelogs = {} + new_tag = repo.get_new_tag() + github_repo.create_git_release(new_tag, new_tag, release_msg, draft=True, prerelease=is_rc) - for repo in repos.values(): - logging.info('Processing repo: %s', repo.name) - repo.checkout(branch) - last_submodule_hash = base_repo.get_submodule_hash(last_release, repo.name) - if repo.has_changes_from_revision(last_submodule_hash): - if repo.name == 'lbryum': - if args.skip_lbryum: - continue - if not repo.part: - repo.part = get_lbryum_part() - entry = repo.get_changelog_entry() - if entry: - changelogs[repo.name] = entry.strip() - repo.add_changelog() - else: - msg = 'Changelog entry is missing for {}'.format(repo.name) - if args.require_changelog: - raise Exception(msg) - else: - logging.warning(msg) - else: - logging.warning('Submodule %s has no changes.', repo.name) - if repo.name == 'lbryum': - # The other repos have their version track each other so need to bump - # them even if there aren't any changes, but lbryum should only be - # bumped if it has changes - continue - # bumpversion will fail if there is already the tag we want in the repo - repo.assert_new_tag_is_absent() - repo.bumpversion() - - release_msg = get_release_msg(changelogs, names) - - for name in names: - base.git.add(name) - - base_repo.bumpversion() - current_tag = base.git.describe() - - is_rc = re.match('\drc\d+$', current_tag) is not None - - github_repo.create_git_release(current_tag, current_tag, release_msg, draft=True, - prerelease=is_rc) - no_change_msg = ('No change since the last release. This release is simply a placeholder' - ' so that LBRY and LBRY App track the same version') - lbrynet_daemon_release_msg = changelogs.get('lbry', no_change_msg) - auth.get_repo('lbryio/lbry').create_git_release( - current_tag, current_tag, lbrynet_daemon_release_msg, draft=True) - - if not args.skip_push: - for repo in repos.values(): - repo.git.push(follow_tags=True) - base.git.push(follow_tags=True, recurse_submodules='check') + if args.skip_push: + print ( + 'Skipping push; you will have to reset and delete tags if ' + 'you want to run this script again.' + ) else: - logging.info('Skipping push; you will have to reset and delete tags if ' - 'you want to run this script again. Take a look at reset.sh; ' - 'it probably does what you want.') + repo.git_repo.git.push(follow_tags=True, recurse_submodules='check') + + +class Repo(object): + def __init__(self, name, part, directory): + self.name = name + self.part = part + if not self.part: + raise Exception('Part required') + self.directory = directory + self.git_repo = git.Repo(self.directory) + self._bumped = False + + self.current_version = self._get_current_version() + self.new_version = self._get_new_version() + self._changelog = changelog.Changelog(os.path.join(self.directory, 'CHANGELOG.md')) + + def get_new_tag(self): + return 'v' + self.new_version + + def get_unreleased_changelog(self): + return self._changelog.get_unreleased() + + def bump_changelog(self): + self._changelog.bump(self.new_version) + with pushd(self.directory): + self.git_repo.git.add(os.path.basename(self._changelog.path)) + + def _get_current_version(self): + with pushd(self.directory): + output = subprocess.check_output( + ['bumpversion', '--dry-run', '--list', '--allow-dirty', self.part]) + return re.search('^current_version=(.*)$', output, re.M).group(1) + + def _get_new_version(self): + with pushd(self.directory): + output = subprocess.check_output( + ['bumpversion', '--dry-run', '--list', '--allow-dirty', self.part]) + return re.search('^new_version=(.*)$', output, re.M).group(1) + + def bumpversion(self): + if self._bumped: + raise Exception('Cowardly refusing to bump a repo twice') + with pushd(self.directory): + subprocess.check_call(['bumpversion', '--allow-dirty', self.part]) + self._bumped = True + + def assert_new_tag_is_absent(self): + new_tag = self.get_new_tag() + tags = self.git_repo.git.tag() + if new_tag in tags.split('\n'): + raise Exception('Tag {} is already present in repo {}.'.format(new_tag, self.name)) + + def is_behind(self, branch): + self.git_repo.remotes.origin.fetch() + rev_list = '{branch}...origin/{branch}'.format(branch=branch) + commits_behind = self.git_repo.git.rev_list(rev_list, right_only=True, count=True) + commits_behind = int(commits_behind) + return commits_behind > 0 + + +def get_bumpversion_parts(): + with pushd(ROOT): + output = subprocess.check_output([ + 'bumpversion', '--dry-run', '--list', '--allow-dirty', 'fake-part', + ]) + parse_line = re.search('^parse=(.*)$', output, re.M).group(1) + return tuple(re.findall('<([^>]+)>', parse_line)) def get_gh_token(): @@ -148,131 +155,36 @@ in the future""" return raw_input('token: ').strip() -def get_lbryum_part(): - print """The lbryum repo has changes but you didn't specify how to bump the -version. Please enter one of: {}""".format(', '.join(LBRYUM_PARTS)) - while True: - part = raw_input('part: ').strip() - if part in LBRYUM_PARTS: - return part - print 'Invalid part. Enter one of: {}'.format(', '.join(LBRYUM_PARTS)) +def confirm(): + return raw_input('Is this what you want? [y/N] ').strip().lower() == 'y' -def get_release_msg(changelogs, names): - lines = [] - for name in names: - entry = changelogs.get(name) - if not entry: - continue - lines.append('## {}\n'.format(name)) - lines.append('{}\n'.format(entry)) - return '\n'.join(lines) - - -def run_sanity_checks(base, branch): - if base.is_dirty(): +def run_sanity_checks(repo, branch): + if repo.git_repo.is_dirty(): print 'Cowardly refusing to release a dirty repo' sys.exit(1) - if base.active_branch.name != branch: + if repo.git_repo.active_branch.name != branch: print 'Cowardly refusing to release when not on the {} branch'.format(branch) sys.exit(1) - if is_behind(base, branch): + if repo.is_behind(branch): print 'Cowardly refusing to release when behind origin' sys.exit(1) - check_bumpversion() - - -def is_behind(base, branch): - base.remotes.origin.fetch() - rev_list = '{branch}...origin/{branch}'.format(branch=branch) - commits_behind = base.git.rev_list(rev_list, right_only=True, count=True) - commits_behind = int(commits_behind) - return commits_behind > 0 - - -def check_bumpversion(): - def require_new_version(): - print 'Install bumpversion: pip install -U git+https://github.com/lbryio/bumpversion.git' + if not is_custom_bumpversion_version(): + print ( + 'Install LBRY\'s fork of bumpversion: ' + 'pip install -U git+https://github.com/lbryio/bumpversion.git' + ) sys.exit(1) + +def is_custom_bumpversion_version(): try: - output = subprocess.check_output(['bumpversion', '-v'], stderr=subprocess.STDOUT) - output = output.strip() - if output != 'bumpversion 0.5.4-lbry': - require_new_version() - except (subprocess.CalledProcessError, OSError) as err: - require_new_version() - - -def get_part(args, name): - return getattr(args, name + '_part') or args.lbry_part - - -class Repo(object): - def __init__(self, name, part, directory=None): - self.name = name - self.part = part - self.directory = directory or os.path.join(os.getcwd(), name) - self.git_repo = git.Repo(self.directory) - self.saved_commit = None - self._bumped = False - - def get_last_tag(self): - return string.split(self.git_repo.git.describe(tags=True), '-')[0] - - def get_submodule_hash(self, revision, submodule_path): - line = getattr(self.git_repo.git, 'ls-tree')(revision, submodule_path) - return string.split(line)[2] if line else None - - def has_changes_from_revision(self, revision): - commit = str(self.git_repo.commit()) - logging.info('%s =? %s', commit, revision) - return commit != revision - - def save_commit(self): - self.saved_commit = self.git_repo.commit() - logging.info('Saved ', self.git_repo.commit(), self.saved_commit) - - def checkout(self, branch): - self.git_repo.git.checkout(branch) - self.git_repo.git.pull(rebase=True) - - def get_changelog_entry(self): - filename = os.path.join(self.directory, 'CHANGELOG.md') - return changelog.bump(filename, self.new_version()) - - def add_changelog(self): - with pushd(self.directory): - self.git_repo.git.add('CHANGELOG.md') - - def new_version(self): - if self._bumped: - raise Exception('Cannot calculate a new version on an already bumped repo') - if not self.part: - raise Exception('Cannot calculate a new version without a part') - with pushd(self.directory): - output = subprocess.check_output( - ['bumpversion', '--dry-run', '--list', '--allow-dirty', self.part]) - return re.search('^new_version=(.*)$', output, re.M).group(1) - - def bumpversion(self): - if self._bumped: - raise Exception('Cowardly refusing to bump a repo twice') - if not self.part: - raise Exception('Cannot bump version for {}: no part specified'.format(repo.name)) - with pushd(self.directory): - subprocess.check_call(['bumpversion', '--allow-dirty', self.part]) - self._bumped = True - - def assert_new_tag_is_absent(self): - new_tag = 'v' + self.new_version() - tags = self.git_repo.git.tag() - if new_tag in tags.split('\n'): - raise Exception('Tag {} is already present in repo {}.'.format(new_tag, self.name)) - - @property - def git(self): - return self.git_repo.git + output = subprocess.check_output(['bumpversion', '-v'], stderr=subprocess.STDOUT).strip() + if output == 'bumpversion 0.5.4-lbry': + return True + except (subprocess.CalledProcessError, OSError): + pass + return False @contextlib.contextmanager @@ -284,10 +196,4 @@ def pushd(new_dir): if __name__ == '__main__': - logging.basicConfig( - format="%(asctime)s %(levelname)-8s %(name)s:%(lineno)d: %(message)s", - level='INFO' - ) sys.exit(main()) -else: - log = logging.getLogger('__name__') -- 2.45.2 From 37f66d9144be1d102230d8e66b55efe6947a1ef4 Mon Sep 17 00:00:00 2001 From: Alex Grintsvayg Date: Wed, 12 Apr 2017 18:51:06 -0400 Subject: [PATCH 027/158] =?UTF-8?q?Bump=20version:=200.9.2rc23=20=E2=86=92?= =?UTF-8?q?=200.9.2rc24?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- app/package.json | 2 +- lbry | 2 +- ui/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 4c3c4e16a..a01d01293 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.9.2rc23 +current_version = 0.9.2rc24 commit = True tag = True parse = (?P\d+)\.(?P\d+)\.(?P\d+)((?P[a-z]+)(?P\d+))? diff --git a/app/package.json b/app/package.json index 5fee9fb61..9f78e8852 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "LBRY", - "version": "0.9.2rc23", + "version": "0.9.2rc24", "main": "main.js", "description": "LBRY is a fully decentralized, open-source protocol facilitating the discovery, access, and (sometimes) purchase of data.", "author": { diff --git a/lbry b/lbry index c26f83d81..8b1152186 160000 --- a/lbry +++ b/lbry @@ -1 +1 @@ -Subproject commit c26f83d81f1f94bd17aa6a9bd5ed07fffefca924 +Subproject commit 8b11521868848e73789ace5eaf4e5c63adb88b17 diff --git a/ui/package.json b/ui/package.json index 20de137c0..a6e82d8b1 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "lbry-web-ui", - "version": "0.9.2rc23", + "version": "0.9.2rc24", "description": "LBRY UI", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" -- 2.45.2 From e1ff7c4934251934b714309faa55d43d92517a29 Mon Sep 17 00:00:00 2001 From: Alex Grintsvayg Date: Thu, 13 Apr 2017 13:14:48 -0400 Subject: [PATCH 028/158] update submodules --- lbry | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lbry b/lbry index 8b1152186..342ae231e 160000 --- a/lbry +++ b/lbry @@ -1 +1 @@ -Subproject commit 8b11521868848e73789ace5eaf4e5c63adb88b17 +Subproject commit 342ae231ea038abbdc0a52b3c4152ef2733af750 -- 2.45.2 From 64387a5215f7dc36466df7fb679553a01907a55f Mon Sep 17 00:00:00 2001 From: Alex Grintsvayg Date: Thu, 13 Apr 2017 13:15:26 -0400 Subject: [PATCH 029/158] =?UTF-8?q?Bump=20version:=200.9.2rc24=20=E2=86=92?= =?UTF-8?q?=200.10.0rc1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- app/package.json | 2 +- lbry | 2 +- ui/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index a01d01293..d9e44ac61 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.9.2rc24 +current_version = 0.10.0rc1 commit = True tag = True parse = (?P\d+)\.(?P\d+)\.(?P\d+)((?P[a-z]+)(?P\d+))? diff --git a/app/package.json b/app/package.json index 9f78e8852..d1b821b05 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "LBRY", - "version": "0.9.2rc24", + "version": "0.10.0rc1", "main": "main.js", "description": "LBRY is a fully decentralized, open-source protocol facilitating the discovery, access, and (sometimes) purchase of data.", "author": { diff --git a/lbry b/lbry index 342ae231e..e8bccec71 160000 --- a/lbry +++ b/lbry @@ -1 +1 @@ -Subproject commit 342ae231ea038abbdc0a52b3c4152ef2733af750 +Subproject commit e8bccec71c7424bf06d057904e4722d2d734fa3f diff --git a/ui/package.json b/ui/package.json index a6e82d8b1..cea9371a3 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "lbry-web-ui", - "version": "0.9.2rc24", + "version": "0.10.0rc1", "description": "LBRY UI", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" -- 2.45.2 From 8614276f0c2d1020888b8a6b61eb289d090324ce Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Thu, 13 Apr 2017 17:30:53 -0400 Subject: [PATCH 030/158] Improve shutdown process --- app/main.js | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/app/main.js b/app/main.js index 9c4a72a7e..23239145b 100644 --- a/app/main.js +++ b/app/main.js @@ -17,7 +17,7 @@ let daemonSubprocess; // This is set to true right before we try to shut the daemon subprocess -- // if it dies when we didn't ask it to shut down, we want to alert the user. -let daemonSubprocessKillRequested = false; +let daemonStopRequested = false; // When a quit is attempted, we cancel the quit, do some preparations, then // this is set to true and app.quit() is called again to quit for real. @@ -74,8 +74,8 @@ function createWindow () { function handleDaemonSubprocessExited() { console.log('The daemon has exited.'); daemonSubprocess = null; - if (!daemonSubprocessKillRequested) { - // We didn't stop the daemon subprocess on purpose, so display a + if (!daemonStopRequested) { + // We didn't request to stop the daemon, so display a // warning and schedule a quit. // // TODO: maybe it would be better to restart the daemon? @@ -209,31 +209,27 @@ app.on('activate', () => { // When a quit is attempted, this is called. It attempts to shutdown the daemon, // then calls quitNow() to quit for real. function shutdownDaemonAndQuit(evenIfNotStartedByApp = false) { - if (daemonSubprocess) { - console.log('Killing lbrynet-daemon process'); - daemonSubprocessKillRequested = true; - kill(daemonSubprocess.pid, undefined, (err) => { - console.log('Killed lbrynet-daemon process'); - quitNow(); - }); - } else if (evenIfNotStartedByApp) { - console.log('Stopping lbrynet-daemon, even though app did not start it'); + function doShutdown() { + console.log('Asking daemon to shut down down'); + daemonStopRequested = true; client.request('daemon_stop', [], (err, res) => { if (err) { - // We could get an error because the daemon is already stopped (good) - // or because it's running but not responding properly (bad). - // So try to force kill any daemons that are still running. - - console.log(`received error when stopping lbrynet-daemon. Error message: ${err.message}`); - forceKillAllDaemonsAndQuit(); + console.log(`received error when stopping lbrynet-daemon. Error message: ${err.message}\n`); + console.log('You will need to manually kill the daemon.'); } else { console.log('Successfully stopped daemon via RPC call.') quitNow(); } }); - } else { + } + + if (daemonSubprocess) { + doShutdown(); + } else if (!evenIfNotStartedByApp) { console.log('Not killing lbrynet-daemon because app did not start it'); quitNow(); + } else { + doShutdown(); } // Is it safe to start the installer before the daemon finishes running? -- 2.45.2 From c5d4941535e15edb01ae20dc757cfbae59c3ecfa Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Tue, 28 Mar 2017 05:07:52 -0400 Subject: [PATCH 031/158] Basic views for reward and reward list pages --- ui/js/app.js | 18 ++++++++---- ui/js/page/reward.js | 46 +++++++++++++++++++++++++++++ ui/js/page/rewards.js | 68 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 127 insertions(+), 5 deletions(-) create mode 100644 ui/js/page/reward.js create mode 100644 ui/js/page/rewards.js diff --git a/ui/js/app.js b/ui/js/app.js index 7e8559be7..2677c0b91 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -9,6 +9,8 @@ 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'; @@ -233,12 +235,14 @@ var App = React.createClass({ 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', + '?claim': 'Claim Beta Code', + '?referral': 'Check Referral Credit', + '?rewards': 'Rewards', }; case 'downloaded': case 'published': @@ -272,6 +276,10 @@ var App = React.createClass({ return ; case 'referral': return ; + case 'rewards': + return ; + case 'reward': + return ; case 'wallet': case 'send': case 'receive': diff --git a/ui/js/page/reward.js b/ui/js/page/reward.js new file mode 100644 index 000000000..9c613ef3c --- /dev/null +++ b/ui/js/page/reward.js @@ -0,0 +1,46 @@ +import React from 'react'; + +// Placeholder for something like api.lbry.io/reward_type/get/[name] */ +function apiRewardTypeGet(name) { + return { + name: 'reward1', + title: 'Reward 1', + description: 'Reward 1 description', + value: 50, + claimed: true, + }; +} + +const RewardPage = React.createClass({ + propTypes: { + name: React.PropTypes.string, + }, + getInitialState: function() { + return { + rewardType: null, + }; + }, + componentWillMount: function() { + this.setState({ + rewardType: apiRewardTypeGet(this.props.name), + }); + }, + render: function() { + if (!this.state.rewardType) { + return null; + } + + let {title, description, value} = this.state.rewardType; + return ( +

+
+

{title}

+

{description}

+ {/* Most likely have a component included here for each reward (e.g. WatchVideoReward) */} +
+
+ ); + } +}); + +export default RewardPage; diff --git a/ui/js/page/rewards.js b/ui/js/page/rewards.js new file mode 100644 index 000000000..093821f31 --- /dev/null +++ b/ui/js/page/rewards.js @@ -0,0 +1,68 @@ +import React from 'react'; +import lbry from '../lbry.js'; +import Modal from '../component/modal.js'; +import {Link} from '../component/link.js'; + +// Placeholder for something like api.lbry.io/reward_type/list */ +function apiRewardTypeList() { + return [ + { + name: 'link_github', + title: 'Link your GitHub account', + description: 'Link LBRY to your GitHub account', + value: 50, + claimed: false, + }, + ]; +} + +var RewardTile = React.createClass({ + propTypes: { + name: React.PropTypes.string, + title: React.PropTypes.string, + }, + render: function() { + return ( +
+
+

+
{this.props.description}
+ {this.props.claimed + ? This reward has been claimed. + : } +
+
+ ); + } +}); + +var RewardsPage = React.createClass({ + componentWillMount: function() { + this.setState({ + rewardTypes: apiRewardTypeList(), + }); + }, + getInitialState: function() { + return { + rewardTypes: null, + }; + }, + render: function() { + return ( +
+
+
+

Rewards

+ {!this.state.rewardTypes + ? null + : this.state.rewardTypes.map(({name, title, description, claimed, value}) => { + return ; + })} +
+
+
+ ); + } +}); + +export default RewardsPage; -- 2.45.2 From 6cc2892399959704b2564cd166319f2fb78a9f01 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Wed, 29 Mar 2017 18:44:18 -0400 Subject: [PATCH 032/158] More progress on Rewards * Add wrapper for lbry.io API * View and basic logic for GitHub reward (not working yet) --- ui/js/lbryio.js | 91 ++++++++++++++++++++++++++++++++++ ui/js/page/reward.js | 101 +++++++++++++++++++++++++++++++------- ui/js/page/rewards.js | 33 ++++++------- ui/scss/all.scss | 3 +- ui/scss/page/_reward.scss | 3 ++ 5 files changed, 194 insertions(+), 37 deletions(-) create mode 100644 ui/js/lbryio.js create mode 100644 ui/scss/page/_reward.scss diff --git a/ui/js/lbryio.js b/ui/js/lbryio.js new file mode 100644 index 000000000..c65767aaa --- /dev/null +++ b/ui/js/lbryio.js @@ -0,0 +1,91 @@ +const querystring = require('querystring'); + +const lbryio = {}; + +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, + }; + }, + 'reward_type.list': () => { + return [ + { + name: 'link_github', + title: 'Link your GitHub account', + description: 'Link LBRY to your GitHub account', + value: 50, + claimed: false, + }, + ]; + } +}; + +lbryio.call = function(resource, action, params, method='get') { + console.log('top of lbryio.call') + return new Promise((resolve, reject) => { + console.log('top of promise handler') + /* temp code for mocks */ + if (`${resource}.${action}` in mocks) { + console.log(`found ${resource}.${action} in mocks`) + resolve(mocks[`${resource}.${action}`](params)); + console.log('...resolved.'); + return; + } else { + console.log(`did not find ${resource}.${action} in mocks`); + } + /* end temp */ + + console.log('about to create xhr object'); + const xhr = new XMLHttpRequest; + xhr.addEventListener('error', function (error) { + console.log('received XHR error:', error); + reject(error); + }); + + + console.log('about to add timeout listener'); + xhr.addEventListener('timeout', function() { + console.log('XHR timed out'); + + reject(new Error('XMLHttpRequest connection timed out')); + }); + + console.log('about to create load listener'); + xhr.addEventListener('load', function() { + console.log('loaded'); + const response = JSON.parse(xhr.responseText); + + if (response.error) { + if (reject) { + reject(new Error(response.error)); + } else { + document.dispatchEvent(new CustomEvent('unhandledError', { + detail: { + connectionString: connectionString, + method: method, + params: params, + code: response.error.code, + message: response.error.message, + data: response.error.data, + } + })); + } + } else { + resolve(response.result); + } + }); + + console.log('about to call xhr.open'); + xhr.open(method, CONNECTION_STRING + resource + '/' + action, true); + xhr.send(querystring.stringify(params)); + }); +}; + +export default lbryio; diff --git a/ui/js/page/reward.js b/ui/js/page/reward.js index 9c613ef3c..8c148ad80 100644 --- a/ui/js/page/reward.js +++ b/ui/js/page/reward.js @@ -1,19 +1,80 @@ import React from 'react'; +import lbryio from '../lbryio.js'; +import {Link} from '../component/link.js'; +import {CreditAmount} from '../component/common.js'; -// Placeholder for something like api.lbry.io/reward_type/get/[name] */ -function apiRewardTypeGet(name) { - return { - name: 'reward1', - title: 'Reward 1', - description: 'Reward 1 description', - value: 50, - claimed: true, - }; -} +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)); + }, + handleConfirmClicked: function() { + this.setState({ + confirming: true, + }); + + lbryio.call('reward', 'new', { + reward_type: 'new_developer', + access_token: 'token will go here', + }, '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) => { + console.log('failed with error:', error); + this.setState({ + confirming: false, + }); + }); + }, + getInitialState: function() { + return { + confirming: false, + }; + }, + render: function() { + return ( +
+
+

+

This will open 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.

+
+ + +
+ ); + } +}); const RewardPage = React.createClass({ propTypes: { - name: React.PropTypes.string, + name: React.PropTypes.string.isRequired, + }, + _getRewardType: function() { + lbryio.call('reward_type', 'get', this.props.name).then((rewardType) => { + this.setState({ + rewardType: rewardType, + }); + }); }, getInitialState: function() { return { @@ -21,22 +82,28 @@ const RewardPage = React.createClass({ }; }, componentWillMount: function() { - this.setState({ - rewardType: apiRewardTypeGet(this.props.name), - }); + this._getRewardType(); }, render: function() { if (!this.state.rewardType) { return null; } - let {title, description, value} = this.state.rewardType; + let Reward; + if (this.props.name == 'link_github') { + Reward = LinkGithubReward; + } + + const {title, description, value} = this.state.rewardType; return (

{title}

-

{description}

- {/* Most likely have a component included here for each reward (e.g. WatchVideoReward) */} + +

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

+
); diff --git a/ui/js/page/rewards.js b/ui/js/page/rewards.js index 093821f31..6deefa005 100644 --- a/ui/js/page/rewards.js +++ b/ui/js/page/rewards.js @@ -1,31 +1,24 @@ import React from 'react'; import lbry from '../lbry.js'; +import {CreditAmount} from '../component/common.js'; import Modal from '../component/modal.js'; import {Link} from '../component/link.js'; +import lbryio from '../lbryio.js'; -// Placeholder for something like api.lbry.io/reward_type/list */ -function apiRewardTypeList() { - return [ - { - name: 'link_github', - title: 'Link your GitHub account', - description: 'Link LBRY to your GitHub account', - value: 50, - claimed: false, - }, - ]; -} - -var RewardTile = React.createClass({ +const RewardTile = React.createClass({ propTypes: { - name: React.PropTypes.string, - title: React.PropTypes.string, + name: React.PropTypes.string.isRequired, + title: React.PropTypes.string.isRequired, + description: React.PropTypes.string.isRequired, + claimed: React.PropTypes.bool.isRequired, + value: React.PropTypes.number.isRequired, }, render: function() { return (

+
{this.props.description}
{this.props.claimed ? This reward has been claimed. @@ -38,8 +31,10 @@ var RewardTile = React.createClass({ var RewardsPage = React.createClass({ componentWillMount: function() { - this.setState({ - rewardTypes: apiRewardTypeList(), + lbryio.call('reward_type', 'list', {}).then((rewardTypes) => { + this.setState({ + rewardTypes: rewardTypes, + }); }); }, getInitialState: function() { @@ -56,7 +51,7 @@ var RewardsPage = React.createClass({ {!this.state.rewardTypes ? null : this.state.rewardTypes.map(({name, title, description, claimed, value}) => { - return ; + return ; })}
diff --git a/ui/scss/all.scss b/ui/scss/all.scss index 6012fc3ee..f90d7cd06 100644 --- a/ui/scss/all.scss +++ b/ui/scss/all.scss @@ -12,4 +12,5 @@ @import "component/_load-screen.scss"; @import "component/_channel-indicator.scss"; @import "page/_developer.scss"; -@import "page/_watch.scss"; \ No newline at end of file +@import "page/_watch.scss"; +@import "page/_reward.scss"; diff --git a/ui/scss/page/_reward.scss b/ui/scss/page/_reward.scss new file mode 100644 index 000000000..739e86619 --- /dev/null +++ b/ui/scss/page/_reward.scss @@ -0,0 +1,3 @@ +.reward-page__details { + background-color: rgba(0, 0, 0, 0.01); +} \ No newline at end of file -- 2.45.2 From c374a59af8fa9394b39be2a815c43aa5f4bb5398 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Thu, 30 Mar 2017 00:48:18 -0400 Subject: [PATCH 033/158] Add access token and wallet address to GitHub reward --- ui/js/lbryio.js | 7 +------ ui/js/page/reward.js | 36 ++++++++++++++++++++---------------- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/ui/js/lbryio.js b/ui/js/lbryio.js index c65767aaa..777f70f29 100644 --- a/ui/js/lbryio.js +++ b/ui/js/lbryio.js @@ -28,18 +28,13 @@ const mocks = { }; lbryio.call = function(resource, action, params, method='get') { - console.log('top of lbryio.call') return new Promise((resolve, reject) => { - console.log('top of promise handler') /* temp code for mocks */ if (`${resource}.${action}` in mocks) { - console.log(`found ${resource}.${action} in mocks`) resolve(mocks[`${resource}.${action}`](params)); - console.log('...resolved.'); return; - } else { - console.log(`did not find ${resource}.${action} in mocks`); } + /* end temp */ console.log('about to create xhr object'); diff --git a/ui/js/page/reward.js b/ui/js/page/reward.js index 8c148ad80..55562c8eb 100644 --- a/ui/js/page/reward.js +++ b/ui/js/page/reward.js @@ -13,34 +13,38 @@ const LinkGithubReward = React.createClass({ onClaimed: React.PropTypes.func, }, _launchLinkPage: function() { - const githubAuthParams = { + /* 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://github.com/login/oauth/authorize?' + querystring.stringify(githubAuthParams)); */ + shell.openExternal('https://lbry.io'); }, handleConfirmClicked: function() { this.setState({ confirming: true, }); - lbryio.call('reward', 'new', { - reward_type: 'new_developer', - access_token: 'token will go here', - }, 'post').then((response) => { - console.log('response:', response); + 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.props.onClaimed(); // This will trigger another API call to show that we succeeded - this.setState({ - confirming: false, - }); - }, (error) => { - console.log('failed with error:', error); - this.setState({ - confirming: false, + this.setState({ + confirming: false, + }); + }, (error) => { + console.log('failed with error:', error); + this.setState({ + confirming: false, + }); }); }); }, @@ -54,7 +58,7 @@ const LinkGithubReward = React.createClass({

-

This will open 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.

+

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.

-- 2.45.2 From 8299e229fdcf412b810c69b74a0009223ff4dd62 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Thu, 30 Mar 2017 14:05:37 -0400 Subject: [PATCH 034/158] Improve lbry.io API code - Send as form data - Handle errors better --- ui/js/lbryio.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/ui/js/lbryio.js b/ui/js/lbryio.js index 777f70f29..7271462a0 100644 --- a/ui/js/lbryio.js +++ b/ui/js/lbryio.js @@ -57,18 +57,17 @@ lbryio.call = function(resource, action, params, method='get') { console.log('loaded'); const response = JSON.parse(xhr.responseText); - if (response.error) { + if (!response.success) { if (reject) { reject(new Error(response.error)); } else { document.dispatchEvent(new CustomEvent('unhandledError', { detail: { connectionString: connectionString, - method: method, + method: action, params: params, - code: response.error.code, message: response.error.message, - data: response.error.data, + ... response.error.data ? {data: response.error.data} : {}, } })); } @@ -79,6 +78,11 @@ lbryio.call = function(resource, action, params, method='get') { console.log('about to call xhr.open'); xhr.open(method, CONNECTION_STRING + resource + '/' + action, true); + + if (method == 'post') { + xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); + } + xhr.send(querystring.stringify(params)); }); }; -- 2.45.2 From 3749e0393acc132fbe85e15540982acd49f87211 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Thu, 30 Mar 2017 14:08:27 -0400 Subject: [PATCH 035/158] Reward: add error handling Also adds Notice component --- ui/js/component/notice.js | 25 +++++++++++++++++++++++++ ui/js/page/reward.js | 9 +++++++++ ui/scss/all.scss | 1 + ui/scss/component/_notice.scss | 14 ++++++++++++++ 4 files changed, 49 insertions(+) create mode 100644 ui/js/component/notice.js create mode 100644 ui/scss/component/_notice.scss diff --git a/ui/js/component/notice.js b/ui/js/component/notice.js new file mode 100644 index 000000000..2e5812c21 --- /dev/null +++ b/ui/js/component/notice.js @@ -0,0 +1,25 @@ +import React from 'react'; +import lbry from '../lbry.js'; +import {Link} from '../component/link.js'; +import {FileActions} from '../component/file-actions.js'; +import {Thumbnail, TruncatedText, CreditAmount} from '../component/common.js'; + +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/page/reward.js b/ui/js/page/reward.js index 55562c8eb..a828d6f9f 100644 --- a/ui/js/page/reward.js +++ b/ui/js/page/reward.js @@ -1,6 +1,7 @@ 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'); @@ -39,11 +40,13 @@ const LinkGithubReward = React.createClass({ this.setState({ confirming: false, + error: null, }); }, (error) => { console.log('failed with error:', error); this.setState({ confirming: false, + error: error, }); }); }); @@ -51,6 +54,7 @@ const LinkGithubReward = React.createClass({ getInitialState: function() { return { confirming: false, + error: null, }; }, render: function() { @@ -61,6 +65,11 @@ const LinkGithubReward = React.createClass({

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} diff --git a/ui/scss/all.scss b/ui/scss/all.scss index f90d7cd06..0616bdf4b 100644 --- a/ui/scss/all.scss +++ b/ui/scss/all.scss @@ -14,3 +14,4 @@ @import "page/_developer.scss"; @import "page/_watch.scss"; @import "page/_reward.scss"; +@import "component/_notice.scss"; diff --git a/ui/scss/component/_notice.scss b/ui/scss/component/_notice.scss new file mode 100644 index 000000000..2ae4f403e --- /dev/null +++ b/ui/scss/component/_notice.scss @@ -0,0 +1,14 @@ +@import "../global"; + +.notice { + padding: 10px 20px; + border: 1px solid #000; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + border-radius: 5px; +} + +.notice--error { + color: #b94a48; + background-color: #f2dede; + border-color: #eed3d7; +} -- 2.45.2 From 892607174a0c7aae8d86598e3b93e7cba0ae7dcf Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Thu, 30 Mar 2017 14:09:42 -0400 Subject: [PATCH 036/158] Reward: style and formatting tweaks --- ui/js/page/reward.js | 2 +- ui/scss/page/_reward.scss | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/ui/js/page/reward.js b/ui/js/page/reward.js index a828d6f9f..773e21893 100644 --- a/ui/js/page/reward.js +++ b/ui/js/page/reward.js @@ -60,8 +60,8 @@ const LinkGithubReward = React.createClass({ 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.

diff --git a/ui/scss/page/_reward.scss b/ui/scss/page/_reward.scss index 739e86619..a550c01c3 100644 --- a/ui/scss/page/_reward.scss +++ b/ui/scss/page/_reward.scss @@ -1,3 +1,5 @@ +@import "../global"; + .reward-page__details { - background-color: rgba(0, 0, 0, 0.01); + background-color: lighten($color-canvas, 1.5%); } \ No newline at end of file -- 2.45.2 From b0742427832d30cf82f3a56618d81061c374f64b Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Thu, 30 Mar 2017 15:05:31 -0400 Subject: [PATCH 037/158] Quick cleanup in main.js Friends don't let friends mix tabs and spaces --- ui/js/main.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/ui/js/main.js b/ui/js/main.js index f00c49a69..5d397bb26 100644 --- a/ui/js/main.js +++ b/ui/js/main.js @@ -25,22 +25,23 @@ var init = function() { ReactDOM.render(, canvas) } else { ReactDOM.render( - { if (balance <= 0) { - window.location.href = '?claim'; + window.location.href = '?claim'; } else { - ReactDOM.render(, canvas); + ReactDOM.render(, canvas); } }); - } else { + } else { ReactDOM.render(, canvas); - } - }}/>, - canvas - ); + } + }}/> + ), + canvas); } }; -- 2.45.2 From 98b38855a27bab4a9cbbdc74653433eda9ef1838 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Thu, 30 Mar 2017 19:00:33 -0400 Subject: [PATCH 038/158] Progress toward register page --- ui/js/app.js | 6 +++++- ui/js/lbryio.js | 14 ++++++++------ ui/js/main.js | 36 ++++++++++++++++++++++++++++++------ 3 files changed, 43 insertions(+), 13 deletions(-) diff --git a/ui/js/app.js b/ui/js/app.js index 2677c0b91..d7ca70f5d 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -2,6 +2,7 @@ import React from 'react'; import {Line} from 'rc-progress'; import lbry from './lbry.js'; +import RegisterPage from './page/register.js'; import SettingsPage from './page/settings.js'; import HelpPage from './page/help.js'; import WatchPage from './page/watch.js'; @@ -40,6 +41,7 @@ var App = React.createClass({ message: 'Error message', data: 'Error data', }, + _fullScreenPages: ['register', 'watch'], _upgradeDownloadItem: null, _isMounted: false, @@ -258,6 +260,8 @@ var App = React.createClass({ { switch(this.state.viewingPage) { + case 'register': + return ; case 'settings': return ; case 'help': @@ -301,7 +305,7 @@ var App = React.createClass({ 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/lbryio.js b/ui/js/lbryio.js index 7271462a0..0becbf487 100644 --- a/ui/js/lbryio.js +++ b/ui/js/lbryio.js @@ -2,7 +2,7 @@ const querystring = require('querystring'); const lbryio = {}; -const CONNECTION_STRING = 'https://api.lbry.io/'; +const CONNECTION_STRING = 'https://apidev.lbry.tech/'; const mocks = { 'reward_type.get': (name) => { @@ -72,18 +72,20 @@ lbryio.call = function(resource, action, params, method='get') { })); } } else { - resolve(response.result); + resolve(response.data); } }); console.log('about to call xhr.open'); - xhr.open(method, CONNECTION_STRING + resource + '/' + action, true); - if (method == 'post') { + if (method == 'get') { + xhr.open('get', CONNECTION_STRING + resource + '/' + action + '?' + querystring.stringify(params), 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(params)); } - - xhr.send(querystring.stringify(params)); }); }; diff --git a/ui/js/main.js b/ui/js/main.js index 5d397bb26..9e10b1b41 100644 --- a/ui/js/main.js +++ b/ui/js/main.js @@ -1,6 +1,7 @@ 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'; @@ -16,7 +17,7 @@ window.addEventListener('contextmenu', (event) => { event.preventDefault(); }); -var init = function() { +let init = function() { window.lbry = lbry; window.lighthouse = lighthouse; @@ -27,8 +28,10 @@ var init = function() { ReactDOM.render( ( { if (balance <= 0) { window.location.href = '?claim'; @@ -40,9 +43,30 @@ var init = function() { ReactDOM.render(, canvas); } }}/> - ), - canvas); + ), canvas); } }; -init(); +if (localStorage.getItem('accessToken') || window.location.search == '?register') { + // User is already registered, or on the registration page + init(); +} else { + // Send + lbry.status().then(({installation_id}) => { + installation_id += parseInt(Date.now(), 10); // temp + installation_id += "X".repeat(96 - installation_id.length); // temp + lbryio.call('user_install', 'exists', {app_id: installation_id}).then((userExists) => { + if (userExists) { + /* TODO: somehow user exists with the same installation ID, but we don't have the token recorded. What do we do here? */ + } else { + lbryio.call('user', 'new', { + language: 'en', + app_id: installation_id, + }, 'post').then(({ID}) => { + localStorage.setItem('accessToken', ID); + window.location = '?register'; + }); + } + }); + }); +} -- 2.45.2 From 8964398c2e2b8f5098359033318feafd14e050ed Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Thu, 30 Mar 2017 19:06:06 -0400 Subject: [PATCH 039/158] Register page fixes --- ui/js/page/register.js | 48 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 ui/js/page/register.js diff --git a/ui/js/page/register.js b/ui/js/page/register.js new file mode 100644 index 000000000..929058b3c --- /dev/null +++ b/ui/js/page/register.js @@ -0,0 +1,48 @@ +import React from 'react'; +import lbryio from '../lbryio.js'; +import {getLocal, setLocal} from '../utils.js'; +import FormField from '../component/form.js' +import {Link} from '../component/link.js' + +const RegisterPage = React.createClass({ + _getRewardType: function() { + lbryio.call('reward_type', 'get', this.props.name).then((rewardType) => { + this.setState({ + rewardType: rewardType, + }); + }); + }, + handleSubmit: function(event) { + if (event !== 'undefined') { + event.preventDefault(); + } + + if (!this.state.email) { + this._emailField.warnRequired(); + } + }, + getInitialState: function() { + return { + rewardType: null, + email: null, + }; + }, + componentWillMount: function() { + this._getRewardType(); + }, + render: function() { + return ( +
+
+

Register a LBRY account

+
+
+
+
+
+
+ ); + } +}); + +export default RegisterPage; -- 2.45.2 From 5587d2f0f5eef73a599380cb3b6f5d0c890abc61 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Sat, 1 Apr 2017 02:30:32 -0400 Subject: [PATCH 040/158] Convert Register page to Email page --- ui/js/app.js | 9 +++++---- ui/js/page/{register.js => email.js} | 13 +++---------- 2 files changed, 8 insertions(+), 14 deletions(-) rename ui/js/page/{register.js => email.js} (77%) diff --git a/ui/js/app.js b/ui/js/app.js index d7ca70f5d..517008afa 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -2,7 +2,8 @@ import React from 'react'; import {Line} from 'rc-progress'; import lbry from './lbry.js'; -import RegisterPage from './page/register.js'; +import lbryio from './lbryio.js'; +import EmailPage from './page/email.js'; import SettingsPage from './page/settings.js'; import HelpPage from './page/help.js'; import WatchPage from './page/watch.js'; @@ -41,7 +42,7 @@ var App = React.createClass({ message: 'Error message', data: 'Error data', }, - _fullScreenPages: ['register', 'watch'], + _fullScreenPages: ['watch'], _upgradeDownloadItem: null, _isMounted: false, @@ -260,8 +261,6 @@ var App = React.createClass({ { switch(this.state.viewingPage) { - case 'register': - return ; case 'settings': return ; case 'help': @@ -292,6 +291,8 @@ var App = React.createClass({ return ; case 'publish': return ; + case 'email': + return ; case 'developer': return ; case 'discover': diff --git a/ui/js/page/register.js b/ui/js/page/email.js similarity index 77% rename from ui/js/page/register.js rename to ui/js/page/email.js index 929058b3c..926e253d8 100644 --- a/ui/js/page/register.js +++ b/ui/js/page/email.js @@ -4,16 +4,9 @@ import {getLocal, setLocal} from '../utils.js'; import FormField from '../component/form.js' import {Link} from '../component/link.js' -const RegisterPage = React.createClass({ - _getRewardType: function() { - lbryio.call('reward_type', 'get', this.props.name).then((rewardType) => { - this.setState({ - rewardType: rewardType, - }); - }); - }, +const EmailPage = React.createClass({ handleSubmit: function(event) { - if (event !== 'undefined') { + if (event !== undefined) { event.preventDefault(); } @@ -45,4 +38,4 @@ const RegisterPage = React.createClass({ } }); -export default RegisterPage; +export default EmailPage; -- 2.45.2 From 3727e275c468d73621fd73988ca15589013fe2c8 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Sat, 1 Apr 2017 02:36:45 -0400 Subject: [PATCH 041/158] Update registration logic and move to app.js --- ui/js/app.js | 42 +++++++++++++++++++++++++++++++++++++++++- ui/js/main.js | 24 +----------------------- 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/ui/js/app.js b/ui/js/app.js index 517008afa..353846266 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -88,6 +88,32 @@ var App = React.createClass({ pageArgs: pageArgs === undefined ? null : pageArgs }; }, + updateRegistrationStatus: function() { + if (localStorage.getItem('accessToken')) { + this.setState({ + registrationCheckComplete: true, + }); + } else { + lbry.status().then(({installation_id}) => { + installation_id += parseInt(Date.now(), 10); // temp + installation_id += "X".repeat(96 - installation_id.length); // temp + 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. + lbryio.call('user', 'new', { + language: 'en', + app_id: installation_id, + }, 'post').then(({ID}) => { + localStorage.setItem('accessToken', ID); + this.setState({ + registrationCheckComplete: true, + justRegistered: true, + }); + }); + }) + }); + } + }, getInitialState: function() { var match, param, val, viewingPage, pageArgs, drawerOpenRaw = sessionStorage.getItem('drawerOpen'); @@ -98,9 +124,17 @@ var App = React.createClass({ modal: null, downloadProgress: null, downloadComplete: false, + registrationCheckComplete: null, + justRegistered: false, }); }, componentWillMount: function() { + if (!localStorage.getItem('accessToken') && window.location.search != '?discover') { + // User isn't registered but somehow made it to a page other than Discover, so send them to + // Discover to get them registered and show them the welcome screen. + window.location.search = '?discover'; + } + document.addEventListener('unhandledError', (event) => { this.alertError(event.detail); }); @@ -138,6 +172,8 @@ var App = React.createClass({ }); }); } + + this.updateRegistrationStatus(); }, openDrawer: function() { sessionStorage.setItem('drawerOpen', true); @@ -297,10 +333,14 @@ var App = React.createClass({ return ; case 'discover': default: - return ; + return ; } }, render: function() { + if (!this.state.registrationCheckComplete) { + return null; + } + var mainContent = this.getMainContent(), headerLinks = this.getHeaderLinks(), searchQuery = this.state.viewingPage == 'discover' && this.state.pageArgs ? this.state.pageArgs : ''; diff --git a/ui/js/main.js b/ui/js/main.js index 9e10b1b41..ea8d4d0a5 100644 --- a/ui/js/main.js +++ b/ui/js/main.js @@ -47,26 +47,4 @@ let init = function() { } }; -if (localStorage.getItem('accessToken') || window.location.search == '?register') { - // User is already registered, or on the registration page - init(); -} else { - // Send - lbry.status().then(({installation_id}) => { - installation_id += parseInt(Date.now(), 10); // temp - installation_id += "X".repeat(96 - installation_id.length); // temp - lbryio.call('user_install', 'exists', {app_id: installation_id}).then((userExists) => { - if (userExists) { - /* TODO: somehow user exists with the same installation ID, but we don't have the token recorded. What do we do here? */ - } else { - lbryio.call('user', 'new', { - language: 'en', - app_id: installation_id, - }, 'post').then(({ID}) => { - localStorage.setItem('accessToken', ID); - window.location = '?register'; - }); - } - }); - }); -} +init(); -- 2.45.2 From 7f8bf8a2e2b3424ece6f3263d3211d39514923e6 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Sat, 1 Apr 2017 02:51:15 -0400 Subject: [PATCH 042/158] Add Welcome screen --- ui/js/component/modal-page.js | 18 ++++++++++++++++ ui/js/component/welcome.js | 24 ++++++++++++++++++++++ ui/js/page/discover.js | 19 +++++++++++++++++ ui/scss/all.scss | 1 + ui/scss/component/_modal-page.scss | 33 ++++++++++++++++++++++++++++++ 5 files changed, 95 insertions(+) create mode 100644 ui/js/component/modal-page.js create mode 100644 ui/js/component/welcome.js create mode 100644 ui/scss/component/_modal-page.scss diff --git a/ui/js/component/modal-page.js b/ui/js/component/modal-page.js new file mode 100644 index 000000000..d0def4b82 --- /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/welcome.js b/ui/js/component/welcome.js new file mode 100644 index 000000000..885bfaf3e --- /dev/null +++ b/ui/js/component/welcome.js @@ -0,0 +1,24 @@ +import React from 'react'; +import ModalPage from './modal-page.js'; +import {Link} from '../component/link.js'; + +export const Welcome = React.createClass({ + propTypes: { + onDone: React.PropTypes.func.isRequired, + }, + handleOKClicked: function() { + this.props.onDone(); + }, + render: function() { + return ( + +

Welcome to LBRY

+ Content will go here... +
+ +
+
+ ); + } +}); + diff --git a/ui/js/page/discover.js b/ui/js/page/discover.js index 678310338..a53d55895 100644 --- a/ui/js/page/discover.js +++ b/ui/js/page/discover.js @@ -5,6 +5,7 @@ import {FileTile} from '../component/file-tile.js'; import {Link} from '../component/link.js'; import {ToolTip} from '../component/tooltip.js'; import {BusyMessage} from '../component/common.js'; +import {Welcome} from '../component/welcome.js'; var fetchResultsStyle = { color: '#888', @@ -105,6 +106,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 +117,12 @@ var DiscoverPage = React.createClass({ } }, + getDefaultProps: function() { + return { + showWelcome: false, + } + }, + componentWillReceiveProps: function(nextProps, nextState) { if (nextProps.query != nextState.query) { @@ -128,6 +139,12 @@ 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) { @@ -138,6 +155,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) @@ -161,6 +179,7 @@ var DiscoverPage = React.createClass({ { !this.state.searching && this.props.query && this.state.results.length ? : null } { !this.state.searching && this.props.query && !this.state.results.length ? : null } { !this.props.query && !this.state.searching ? : null } +

); } diff --git a/ui/scss/all.scss b/ui/scss/all.scss index 0616bdf4b..0c58b94df 100644 --- a/ui/scss/all.scss +++ b/ui/scss/all.scss @@ -11,6 +11,7 @@ @import "component/_tooltip.scss"; @import "component/_load-screen.scss"; @import "component/_channel-indicator.scss"; +@import "component/_modal-page.scss"; @import "page/_developer.scss"; @import "page/_watch.scss"; @import "page/_reward.scss"; diff --git a/ui/scss/component/_modal-page.scss b/ui/scss/component/_modal-page.scss new file mode 100644 index 000000000..1acc41790 --- /dev/null +++ b/ui/scss/component/_modal-page.scss @@ -0,0 +1,33 @@ +.modal-page-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-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; +} -- 2.45.2 From e0f05f43a6a95509a6c0c217e4689ed33826edd1 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Sat, 1 Apr 2017 02:52:45 -0400 Subject: [PATCH 043/158] Log requests and responses in lbryio module --- ui/js/lbryio.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ui/js/lbryio.js b/ui/js/lbryio.js index 0becbf487..79df9aa07 100644 --- a/ui/js/lbryio.js +++ b/ui/js/lbryio.js @@ -72,6 +72,7 @@ lbryio.call = function(resource, action, params, method='get') { })); } } else { + console.info(`${resource}.${action} response data:`, response); resolve(response.data); } }); @@ -79,9 +80,11 @@ lbryio.call = function(resource, action, params, method='get') { console.log('about to call xhr.open'); if (method == 'get') { + console.info('GET ', CONNECTION_STRING + resource + '/' + action, ' | params:', params); xhr.open('get', CONNECTION_STRING + resource + '/' + action + '?' + querystring.stringify(params), true); xhr.send(); } else if (method == 'post') { + console.info('POST ', CONNECTION_STRING + resource + '/' + action, '| params: ', params); xhr.open('post', CONNECTION_STRING + resource + '/' + action, true); xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); xhr.send(querystring.stringify(params)); -- 2.45.2 From 1dbbf8fc0126f72c8bfea16f812bdb5b8c915634 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Sat, 1 Apr 2017 02:54:56 -0400 Subject: [PATCH 044/158] Style tweaks --- ui/js/app.js | 1 - ui/js/lbryio.js | 1 + ui/js/page/discover.js | 1 + ui/js/page/rewards.js | 2 +- ui/scss/all.scss | 2 +- 5 files changed, 4 insertions(+), 3 deletions(-) diff --git a/ui/js/app.js b/ui/js/app.js index 353846266..697c62ca8 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -48,7 +48,6 @@ var App = React.createClass({ _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 diff --git a/ui/js/lbryio.js b/ui/js/lbryio.js index 79df9aa07..63bf08888 100644 --- a/ui/js/lbryio.js +++ b/ui/js/lbryio.js @@ -39,6 +39,7 @@ lbryio.call = function(resource, action, params, method='get') { console.log('about to create xhr object'); const xhr = new XMLHttpRequest; + xhr.addEventListener('error', function (error) { console.log('received XHR error:', error); reject(error); diff --git a/ui/js/page/discover.js b/ui/js/page/discover.js index a53d55895..c6705d871 100644 --- a/ui/js/page/discover.js +++ b/ui/js/page/discover.js @@ -147,6 +147,7 @@ var DiscoverPage = React.createClass({ componentWillMount: function() { document.title = "Discover"; + if (this.props.query) { // Rendering with a query already typed this.handleSearchChanged(this.props.query); diff --git a/ui/js/page/rewards.js b/ui/js/page/rewards.js index 6deefa005..52f705035 100644 --- a/ui/js/page/rewards.js +++ b/ui/js/page/rewards.js @@ -1,9 +1,9 @@ import React from 'react'; import lbry from '../lbry.js'; +import lbryio from '../lbryio.js'; import {CreditAmount} from '../component/common.js'; import Modal from '../component/modal.js'; import {Link} from '../component/link.js'; -import lbryio from '../lbryio.js'; const RewardTile = React.createClass({ propTypes: { diff --git a/ui/scss/all.scss b/ui/scss/all.scss index 0c58b94df..126ad742c 100644 --- a/ui/scss/all.scss +++ b/ui/scss/all.scss @@ -11,8 +11,8 @@ @import "component/_tooltip.scss"; @import "component/_load-screen.scss"; @import "component/_channel-indicator.scss"; +@import "component/_notice.scss"; @import "component/_modal-page.scss"; @import "page/_developer.scss"; @import "page/_watch.scss"; @import "page/_reward.scss"; -@import "component/_notice.scss"; -- 2.45.2 From 70d2f7c823d3443832b670d164e72537b5e82865 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Sun, 2 Apr 2017 05:55:24 -0400 Subject: [PATCH 045/158] Add auth to lbryio.js --- ui/js/app.js | 1 + ui/js/lbryio.js | 15 +++++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/ui/js/app.js b/ui/js/app.js index 697c62ca8..5572550ff 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -104,6 +104,7 @@ var App = React.createClass({ app_id: installation_id, }, 'post').then(({ID}) => { localStorage.setItem('accessToken', ID); + localStorage.setItem('appId', installation_id); this.setState({ registrationCheckComplete: true, justRegistered: true, diff --git a/ui/js/lbryio.js b/ui/js/lbryio.js index 63bf08888..7c6771aac 100644 --- a/ui/js/lbryio.js +++ b/ui/js/lbryio.js @@ -80,15 +80,22 @@ lbryio.call = function(resource, action, params, method='get') { console.log('about to call xhr.open'); + // 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: localStorage.getItem('appId'), ...params}; + if (method == 'get') { - console.info('GET ', CONNECTION_STRING + resource + '/' + action, ' | params:', params); - xhr.open('get', CONNECTION_STRING + resource + '/' + action + '?' + querystring.stringify(params), true); + console.info('GET ', CONNECTION_STRING + resource + '/' + action, ' | params:', fullParams); + xhr.open('get', CONNECTION_STRING + resource + '/' + action + '?' + querystring.stringify(fullParams), true); xhr.send(); } else if (method == 'post') { - console.info('POST ', CONNECTION_STRING + resource + '/' + action, '| params: ', params); + console.info('POST ', CONNECTION_STRING + resource + '/' + action, '| params: ', fullParams); xhr.open('post', CONNECTION_STRING + resource + '/' + action, true); xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); - xhr.send(querystring.stringify(params)); + xhr.send(querystring.stringify(fullParams)); } }); }; -- 2.45.2 From b975fab1bb45c23bf15330c2222b344e99004bd1 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Sun, 2 Apr 2017 06:00:40 -0400 Subject: [PATCH 046/158] Add email section to Welcome screen --- ui/js/component/welcome.js | 169 +++++++++++++++++++++++++++++++++++-- 1 file changed, 160 insertions(+), 9 deletions(-) diff --git a/ui/js/component/welcome.js b/ui/js/component/welcome.js index 885bfaf3e..7694997c0 100644 --- a/ui/js/component/welcome.js +++ b/ui/js/component/welcome.js @@ -1,21 +1,172 @@ import React from 'react'; +import lbryio from '../lbryio.js'; + import ModalPage from './modal-page.js'; import {Link} from '../component/link.js'; +import FormField from '../component/form.js'; +import Notice from '../component/notice.js' -export const Welcome = React.createClass({ - propTypes: { - onDone: React.PropTypes.func.isRequired, - }, - handleOKClicked: function() { - this.props.onDone(); +const IntroStage = React.createClass({ + componentWillMount: function() { + this.props.onCompleted(); // Nothing required to move on }, render: function() { return ( - +

Welcome to LBRY

- Content will go here... +

Content will go here...

+
+ ); + } +}); + +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.setState({ + submitting: false, + message: "Your email has been verified.", + success: true, + }); + this.props.onCompleted(); + }, (error) => { + this.setState({ + submitting: false, + message: error.message, + success: false, + }); + }); + }, + render: function() { + return ( +
+

Verify Your Email Address

+ {this.state.message + ? + {this.state.message} + + : null} +

Copy here explaining what we do with your email, and the reward.

+
+
+
+
+
+ ); + } +}); + +/* const ConfirmEmailStage = React.createClass({ + getInitialState: function() { + return { + rewardType: null, + email: '', + submitting: false, + }; + }, + handleEmailChanged: function(event) { + this.setState({ + email: event.target.value, + }); + }, + handleSubmit: function(event) { + event.preventDefault(); + // ... + }, + render: function() { + return ( +
+

Confirm Your Email Address

+ {this.state.message + ? + {this.state.message} + + : null} +

Ask the user to take steps needed to confirm (click link in confirmation email, etc.)

+
+
+
+
+
+ ); + } +}); */ + +const FinalMessageStage = React.createClass({ + componentWillMount: function() { + this.props.onCompleted(); + }, + render: function() { + return ( +
+

Email verified

+

Text here about what happens next

+
+ ); + } +}); + +export const Welcome = React.createClass({ + _stages: [ + IntroStage, + SubmitEmailStage, + //ConfirmEmailStage, + FinalMessageStage, + ], + propTypes: { + onDone: React.PropTypes.func.isRequired, + }, + getInitialState: function() { + return { + stageNum: 0, + }; + }, + handleNextClicked: function() { + if (this.state.stageNum >= this._stages.length - 1) { + this.props.onDone(); + } + + this.setState({ + stageNum: this.state.stageNum + 1, + stageCompleted: false, + }); + }, + handleDoneClicked: function() { + this.props.onDone(); + }, + handleStageComplete: function() { + console.log('inside handleStageComplete') + this.setState({ + stageCompleted: true, + }); + }, + render: function() { + const Content = this._stages[this.state.stageNum]; + const isLastStage = this.state.stageNum >= this._stages.length - 1; + return ( + +
- + {!isLastStage + ? + : }
); -- 2.45.2 From 7b7e361bddd36f6973015e7ab06bb6699a55d3e4 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Sun, 2 Apr 2017 06:02:53 -0400 Subject: [PATCH 047/158] Add notification bar Used for displaying global info (e.g. "you just got a reward.") Can be displayed from anywhere in the app using events. --- ui/js/component/header.js | 2 + ui/js/component/notice.js | 2 +- ui/js/component/notification-bar.js | 53 ++++++++++++++++++++++++ ui/scss/all.scss | 1 + ui/scss/component/_notice.scss | 4 ++ ui/scss/component/_notification-bar.scss | 6 +++ 6 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 ui/js/component/notification-bar.js create mode 100644 ui/scss/component/_notification-bar.scss diff --git a/ui/js/component/header.js b/ui/js/component/header.js index fb64e9ece..6e186cc44 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 NotificationBar from './notification-bar.js'; var Header = React.createClass({ getInitialState: function() { @@ -61,6 +62,7 @@ var Header = React.createClass({ : '' } + ); } diff --git a/ui/js/component/notice.js b/ui/js/component/notice.js index 2e5812c21..a09238294 100644 --- a/ui/js/component/notice.js +++ b/ui/js/component/notice.js @@ -15,7 +15,7 @@ export const Notice = React.createClass({ }, render: function() { return ( -
+
{this.props.children}
); diff --git a/ui/js/component/notification-bar.js b/ui/js/component/notification-bar.js new file mode 100644 index 000000000..f6662b552 --- /dev/null +++ b/ui/js/component/notification-bar.js @@ -0,0 +1,53 @@ +import React from 'react'; +import lbry from '../lbry.js'; +import Notice from '../component/notice.js'; + +export const NotificationBar = React.createClass({ + _displayTime: 8, // in seconds + + _hideTimeout: null, + + getInitialState: function() { + return { + message: null, + isError: null, + } + }, + handleNoticeReceived: function(event) { + if (this._hideTimeout) { + clearTimeout(this._hideTimeout); + } + + const {detail: {message, isError}} = event; + this.setState({ + message: message, + isError: isError, + }); + + this._hideTimeout = setTimeout(() => { + this.setState({ + message: null, + isError: null, + }); + }, this._displayTime * 1000); + }, + componentWillMount: function() { + document.addEventListener('globalNotice', this.handleNoticeReceived); + }, + componentWillUnmount: function() { + document.removeEventListener('globalNotice', this.handleNoticeReceived); + }, + render: function() { + if (!this.state.message) { + return null; + } + + return ( + + {this.state.message} + + ); + }, +}); + +export default NotificationBar; \ No newline at end of file diff --git a/ui/scss/all.scss b/ui/scss/all.scss index 126ad742c..89d84058a 100644 --- a/ui/scss/all.scss +++ b/ui/scss/all.scss @@ -13,6 +13,7 @@ @import "component/_channel-indicator.scss"; @import "component/_notice.scss"; @import "component/_modal-page.scss"; +@import "component/_notification-bar.scss"; @import "page/_developer.scss"; @import "page/_watch.scss"; @import "page/_reward.scss"; diff --git a/ui/scss/component/_notice.scss b/ui/scss/component/_notice.scss index 2ae4f403e..b77ba2a5a 100644 --- a/ui/scss/component/_notice.scss +++ b/ui/scss/component/_notice.scss @@ -5,6 +5,10 @@ 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 { diff --git a/ui/scss/component/_notification-bar.scss b/ui/scss/component/_notification-bar.scss new file mode 100644 index 000000000..2f9959f94 --- /dev/null +++ b/ui/scss/component/_notification-bar.scss @@ -0,0 +1,6 @@ +@import "../global"; + +.notification-bar { + margin-top: 5px; + margin-right: 10px; +} -- 2.45.2 From 647eb80eefbf2ca1b5ee483dd729aedb4cca923a Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Sun, 2 Apr 2017 06:05:34 -0400 Subject: [PATCH 048/158] Add wrapper code for creating rewards Calls the API and displays notices for success and errors --- ui/js/rewards.js | 54 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 ui/js/rewards.js diff --git a/ui/js/rewards.js b/ui/js/rewards.js new file mode 100644 index 000000000..262183f04 --- /dev/null +++ b/ui/js/rewards.js @@ -0,0 +1,54 @@ +import lbry from './lbry.js'; +import lbryio from './lbryio.js'; + +const MESSAGES = { + new_developer: "Your reward has been confirmed for registering as a new developer.", + confirm_email: "Your reward has been confirmed for verifying your email address.", + first_publish: "Your reward has been confirmed for making your first publication.", +}; + +const rewards = {}; + +rewards.claimReward = function(type) { + console.log('top of claimReward') + return new Promise((resolve, reject) => { + console.log('top of promise body') + lbry.get_new_address().then((address) => { + console.log('top of get_new_address') + const params = { + reward_type: type, + wallet_address: address, + }; + lbryio.call('reward', 'new', params, 'post').then(({RewardAmount}) => { + const result = { + type: type, + amount: RewardAmount, + message: MESSAGES[type], + }; + + // Display global notice + document.dispatchEvent(new CustomEvent('globalNotice', { + detail: { + message: MESSAGES[type], + isError: false, + }, + })); + + // Add more events here to display other places + + resolve(result); + }, (error) => { + document.dispatchEvent(new CustomEvent('globalNotice', { + detail: { + message: `Failed to claim reward: ${error.message}`, + isError: true, + }, + })); + document.dispatchEvent(new CustomEvent('rewardFailed', error)); + reject(error); + }); + }); + }); +} + +export default rewards; \ No newline at end of file -- 2.45.2 From e47f86bbfe347ffcd4e11862dc084c2372e4da75 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Sun, 2 Apr 2017 06:07:04 -0400 Subject: [PATCH 049/158] Add mock for reward/new action --- ui/js/lbryio.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ui/js/lbryio.js b/ui/js/lbryio.js index 7c6771aac..a8156ce75 100644 --- a/ui/js/lbryio.js +++ b/ui/js/lbryio.js @@ -24,6 +24,14 @@ const mocks = { claimed: false, }, ]; + }, + 'reward.new': ({reward_type}) => { + return { + UserID: localStorage.getItem('accessToken'), + UserWalletID: 123, + RewardType: reward_type, + RewardAmount: 50, + }; } }; -- 2.45.2 From 26aba5fb04ace3689e2d2669ce4d216e9a8961e5 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Sun, 2 Apr 2017 06:08:25 -0400 Subject: [PATCH 050/158] Random corrections and style fixes --- ui/js/component/notice.js | 4 ---- ui/js/lbryio.js | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/ui/js/component/notice.js b/ui/js/component/notice.js index a09238294..068b545b5 100644 --- a/ui/js/component/notice.js +++ b/ui/js/component/notice.js @@ -1,8 +1,4 @@ import React from 'react'; -import lbry from '../lbry.js'; -import {Link} from '../component/link.js'; -import {FileActions} from '../component/file-actions.js'; -import {Thumbnail, TruncatedText, CreditAmount} from '../component/common.js'; export const Notice = React.createClass({ propTypes: { diff --git a/ui/js/lbryio.js b/ui/js/lbryio.js index a8156ce75..0a1fd4b8f 100644 --- a/ui/js/lbryio.js +++ b/ui/js/lbryio.js @@ -5,7 +5,7 @@ const lbryio = {}; const CONNECTION_STRING = 'https://apidev.lbry.tech/'; const mocks = { - 'reward_type.get': (name) => { + 'reward_type.get': ({name}) => { return { name: 'link_github', title: 'Link your GitHub account', @@ -35,7 +35,7 @@ const mocks = { } }; -lbryio.call = function(resource, action, params, method='get') { +lbryio.call = function(resource, action, params={}, method='get') { return new Promise((resolve, reject) => { /* temp code for mocks */ if (`${resource}.${action}` in mocks) { -- 2.45.2 From 326d06635393e60dcf56deac6c6a8da16d340fca Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Sun, 2 Apr 2017 06:10:12 -0400 Subject: [PATCH 051/158] Fixes to Email page Still not actually working (doing it in the welcome screen right now) --- ui/js/page/email.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/ui/js/page/email.js b/ui/js/page/email.js index 926e253d8..b7f31cd49 100644 --- a/ui/js/page/email.js +++ b/ui/js/page/email.js @@ -3,6 +3,7 @@ import lbryio from '../lbryio.js'; import {getLocal, setLocal} from '../utils.js'; import FormField from '../component/form.js' import {Link} from '../component/link.js' +import rewards from '../rewards.js'; const EmailPage = React.createClass({ handleSubmit: function(event) { @@ -14,12 +15,6 @@ const EmailPage = React.createClass({ this._emailField.warnRequired(); } }, - getInitialState: function() { - return { - rewardType: null, - email: null, - }; - }, componentWillMount: function() { this._getRewardType(); }, @@ -27,7 +22,7 @@ const EmailPage = React.createClass({ return (
-

Register a LBRY account

+

Verify your Email Address

-- 2.45.2 From 498618e39b49844d1aea22370b0a54c496f75be2 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Sun, 2 Apr 2017 22:00:24 -0400 Subject: [PATCH 052/158] More revisions to Welcome screen - Eliminate intro page - Add "verify email" page - Move buttons into individual components - Claim reward (not handling reporting success/failure yet) --- ui/js/component/welcome.js | 113 +++++++++++++++---------------------- ui/js/lbryio.js | 8 --- 2 files changed, 47 insertions(+), 74 deletions(-) diff --git a/ui/js/component/welcome.js b/ui/js/component/welcome.js index 7694997c0..36036abe1 100644 --- a/ui/js/component/welcome.js +++ b/ui/js/component/welcome.js @@ -6,19 +6,6 @@ import {Link} from '../component/link.js'; import FormField from '../component/form.js'; import Notice from '../component/notice.js' -const IntroStage = React.createClass({ - componentWillMount: function() { - this.props.onCompleted(); // Nothing required to move on - }, - render: function() { - return ( -
-

Welcome to LBRY

-

Content will go here...

-
- ); - } -}); const SubmitEmailStage = React.createClass({ getInitialState: function() { @@ -26,6 +13,7 @@ const SubmitEmailStage = React.createClass({ rewardType: null, email: '', submitting: false, + errorMessage: null, }; }, handleEmailChanged: function(event) { @@ -40,84 +28,94 @@ const SubmitEmailStage = React.createClass({ submitting: true, }); lbryio.call('user_email', 'new', {email: this.state.email}, 'post').then(() => { - this.setState({ - submitting: false, - message: "Your email has been verified.", - success: true, - }); - this.props.onCompleted(); + this.props.onDone(); }, (error) => { this.setState({ submitting: false, - message: error.message, - success: false, + errorMessage: error.message, }); }); }, render: function() { return (
-

Verify Your Email Address

- {this.state.message - ? - {this.state.message} +

Welcome to LBRY

+ {this.state.errorMessage + ? + {this.state.errorMessage} : null}

Copy here explaining what we do with your email, and the reward.

-
-
+
Email
+
); } }); -/* const ConfirmEmailStage = React.createClass({ +const ConfirmEmailStage = React.createClass({ getInitialState: function() { return { rewardType: null, - email: '', + code: '', submitting: false, + errorMessage: null, }; }, - handleEmailChanged: function(event) { + handleCodeChanged: function(event) { this.setState({ - email: event.target.value, + code: event.target.value, }); }, handleSubmit: function(event) { event.preventDefault(); - // ... + this.setState({ + submitting: true, + }); + + lbryio.call('user_email', 'confirm', {verification_token: this.state.code}, 'post').then(() => { + rewards.claimReward('confirm_email').then(() => { + console.log('succeeded'); + this.props.onDone(); + }, (err) => { + console.log('failed'); + this.props.onDone(); + }); + }, (error) => { + this.setState({ + submitting: false, + errorMessage: error.message, + }); + }); }, render: function() { return (

Confirm Your Email Address

- {this.state.message - ? - {this.state.message} + {this.state.errorMessage + ? + {this.state.errorMessage} : null} -

Ask the user to take steps needed to confirm (click link in confirmation email, etc.)

+

Please enter your verification code to confirm your email address.

-
-
+
+
); } -}); */ +}); const FinalMessageStage = React.createClass({ - componentWillMount: function() { - this.props.onCompleted(); - }, render: function() { return (

Email verified

Text here about what happens next

+
); } @@ -125,9 +123,8 @@ const FinalMessageStage = React.createClass({ export const Welcome = React.createClass({ _stages: [ - IntroStage, SubmitEmailStage, - //ConfirmEmailStage, + ConfirmEmailStage, FinalMessageStage, ], propTypes: { @@ -138,36 +135,20 @@ export const Welcome = React.createClass({ stageNum: 0, }; }, - handleNextClicked: function() { + handleStageDone: function() { if (this.state.stageNum >= this._stages.length - 1) { this.props.onDone(); + } else { + this.setState({ + stageNum: this.state.stageNum + 1, + }); } - - this.setState({ - stageNum: this.state.stageNum + 1, - stageCompleted: false, - }); - }, - handleDoneClicked: function() { - this.props.onDone(); - }, - handleStageComplete: function() { - console.log('inside handleStageComplete') - this.setState({ - stageCompleted: true, - }); }, render: function() { const Content = this._stages[this.state.stageNum]; - const isLastStage = this.state.stageNum >= this._stages.length - 1; return ( - -
- {!isLastStage - ? - : } -
+
); } diff --git a/ui/js/lbryio.js b/ui/js/lbryio.js index 0a1fd4b8f..e4bb40eb4 100644 --- a/ui/js/lbryio.js +++ b/ui/js/lbryio.js @@ -25,14 +25,6 @@ const mocks = { }, ]; }, - 'reward.new': ({reward_type}) => { - return { - UserID: localStorage.getItem('accessToken'), - UserWalletID: 123, - RewardType: reward_type, - RewardAmount: 50, - }; - } }; lbryio.call = function(resource, action, params={}, method='get') { -- 2.45.2 From 575db85477eadddd472622e759bcfef64c378feb Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Sun, 9 Apr 2017 11:06:23 -0400 Subject: [PATCH 053/158] so far --- .gitignore | 1 + app/main.js | 4 +- doitagain.sh | 4 + ui/js/app.js | 49 +------ ui/js/component/auth.js | 196 ++++++++++++++++++++++++++++ ui/js/component/form.js | 91 +++++++------ ui/js/component/splash.js | 29 ++-- ui/js/lbry.js | 54 ++++---- ui/js/lbryio.js | 91 +++++++++++-- ui/js/main.js | 37 +++--- ui/js/page/discover.js | 4 +- ui/js/page/publish.js | 2 +- ui/js/rewards.js | 5 +- ui/js/utils.js | 2 +- ui/scss/_form.scss | 66 ++++++++++ ui/scss/_global.scss | 8 +- ui/scss/_gui.scss | 175 +------------------------ ui/scss/_reset.scss | 19 +-- ui/scss/all.scss | 3 + ui/scss/component/_button.scss | 78 +++++++++++ ui/scss/component/_form-field.scss | 36 +++++ ui/scss/component/_load-screen.scss | 2 +- ui/scss/component/_modal-page.scss | 28 ++++ 23 files changed, 625 insertions(+), 359 deletions(-) create mode 100755 doitagain.sh create mode 100644 ui/js/component/auth.js create mode 100644 ui/scss/_form.scss create mode 100644 ui/scss/component/_button.scss create mode 100644 ui/scss/component/_form-field.scss 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..e2fad0905 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: 1000 }) //$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/ui/js/app.js b/ui/js/app.js index 5572550ff..03d508f81 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -47,12 +47,6 @@ var App = React.createClass({ _upgradeDownloadItem: null, _isMounted: false, _version: null, - - getDefaultProps: function() { - return { - address: window.location.search - }; - }, getUpdateUrl: function() { switch (process.platform) { case 'darwin': @@ -87,54 +81,19 @@ var App = React.createClass({ pageArgs: pageArgs === undefined ? null : pageArgs }; }, - updateRegistrationStatus: function() { - if (localStorage.getItem('accessToken')) { - this.setState({ - registrationCheckComplete: true, - }); - } else { - lbry.status().then(({installation_id}) => { - installation_id += parseInt(Date.now(), 10); // temp - installation_id += "X".repeat(96 - installation_id.length); // temp - 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. - lbryio.call('user', 'new', { - language: 'en', - app_id: installation_id, - }, 'post').then(({ID}) => { - localStorage.setItem('accessToken', ID); - localStorage.setItem('appId', installation_id); - this.setState({ - registrationCheckComplete: true, - justRegistered: true, - }); - }); - }) - }); - } - }, getInitialState: function() { 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, downloadProgress: null, downloadComplete: false, - registrationCheckComplete: null, - justRegistered: false, }); }, componentWillMount: function() { - if (!localStorage.getItem('accessToken') && window.location.search != '?discover') { - // User isn't registered but somehow made it to a page other than Discover, so send them to - // Discover to get them registered and show them the welcome screen. - window.location.search = '?discover'; - } - document.addEventListener('unhandledError', (event) => { this.alertError(event.detail); }); @@ -172,8 +131,6 @@ var App = React.createClass({ }); }); } - - this.updateRegistrationStatus(); }, openDrawer: function() { sessionStorage.setItem('drawerOpen', true); @@ -337,10 +294,6 @@ var App = React.createClass({ } }, render: function() { - if (!this.state.registrationCheckComplete) { - return null; - } - var mainContent = this.getMainContent(), headerLinks = this.getHeaderLinks(), searchQuery = this.state.viewingPage == 'discover' && this.state.pageArgs ? this.state.pageArgs : ''; diff --git a/ui/js/component/auth.js b/ui/js/component/auth.js new file mode 100644 index 000000000..2b7e6d02b --- /dev/null +++ b/ui/js/component/auth.js @@ -0,0 +1,196 @@ +import React from 'react'; +import lbryio from '../lbryio.js'; + +import ModalPage from './modal-page.js'; +import {Link} from '../component/link.js'; +import FormField from '../component/form.js'; +import Notice from '../component/notice.js' + + +const SubmitEmailStage = React.createClass({ + getInitialState: function() { + return { + rewardType: null, + email: '', + submitting: false + }; + }, + 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._emailField) { + this._emailField.showError(error.message) + } + this.setState({ submitting: false }); + }); + }, + render: function() { + return ( +
+
+ { this._emailField = field }} type="text" label="Email" placeholder="webmaster@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, + }); + + lbryio.call('user_email', 'confirm', {verification_token: this.state.code}, 'post').then(() => { + rewards.claimReward('confirm_email').then(() => { + this.props.onDone(); + }, (err) => {l + this.props.onDone(); + }); + }, (error) => { + if (this._codeField) { + this._codeField.showError(error.message) + this.setState({ submitting: false }) + } + }); + }, + render: function() { + return ( +
+
+ { this._codeField = field }} type="text" + name="code" placeholder="a94bXXXXXXXXXXXXXX" value={this.state.code} onChange={this.handleCodeChanged} + helper="A verification code is required to access this version."/> +
+ +
+ +
+ ); + } +}); + +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, + }, + propTypes: { + // onDone: React.PropTypes.func.isRequired, + }, + getInitialState: function() { + return { + stage: "pending", + stageProps: {} + }; + }, + componentWillMount: function() { + lbryio.authenticate().then(function(user) { + console.log(user); + if (!user.HasVerifiedEmail) { + this.setState({ + stage: "email", + stageProps: { + onEmailSaved: function() { + this.setState({ + stage: "confirm" + }) + }.bind(this) + } + }) + } else { + this.setState({ stage: null }) + } + }.bind(this)).catch((err) => { + this.setState({ + stage: "error", + stageProps: { errorText: err.message } + }) + document.dispatchEvent(new CustomEvent('unhandledError', { + detail: { + message: err.message, + data: err.stack + } + })); + }) + }, + // handleStageDone: function() { + // if (this.state.stageNum >= this._stages.length - 1) { + // this.props.onDone(); + // } else { + // this.setState({ + // stageNum: this.state.stageNum + 1, + // }); + // } + // }, + + // + render: function() { + console.log(lbryio.user); + if (!this.state.stage || lbryio.user && lbryio.user.HasVerifiedEmail) { + return null; + } + const StageContent = this._stages[this.state.stage]; + return ( + +

LBRY Early Access

+ +
+ ); + } +}); \ No newline at end of file diff --git a/ui/js/component/form.js b/ui/js/component/form.js index 33e4aee66..176f3334f 100644 --- a/ui/js/component/form.js +++ b/ui/js/component/form.js @@ -6,6 +6,8 @@ var requiredFieldWarningStyle = { transition: 'opacity 400ms ease-in', }; +var formFieldCounter = 0; + var FormField = React.createClass({ _fieldRequiredText: 'This field is required', _type: null, @@ -13,11 +15,12 @@ var FormField = React.createClass({ propTypes: { type: React.PropTypes.string.isRequired, + row: React.PropTypes.bool, hidden: React.PropTypes.bool, }, getInitialState: function() { return { - adviceState: 'hidden', + errorState: 'hidden', adviceText: null, } }, @@ -33,25 +36,25 @@ var FormField = React.createClass({ this._element = this.props.type; } }, - showAdvice: function(text) { + showError: function(text) { this.setState({ - adviceState: 'shown', + errorState: 'shown', adviceText: text, }); - setTimeout(() => { - this.setState({ - adviceState: 'fading', - }); - setTimeout(() => { - this.setState({ - adviceState: 'hidden', - }); - }, 450); - }, 5000); + // setTimeout(() => { + // this.setState({ + // errorState: 'fading', + // }); + // setTimeout(() => { + // this.setState({ + // errorState: 'hidden', + // }); + // }, 450); + // }, 5000); }, warnRequired: function() { - this.showAdvice(this._fieldRequiredText); + this.showError(this._fieldRequiredText); }, focus: function() { this.refs.field.focus(); @@ -70,43 +73,39 @@ 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), + hasError = this.state.errorState != 'hidden'; delete otherProps.type; delete otherProps.hidden; + delete otherProps.label; + delete otherProps.row; + delete otherProps.helper; - return ( - !this.props.hidden - ?
- - {this.props.children} - - {this.state.adviceText} -
- : null - ); - } -}); + ++formFieldCounter; + const elementId = "form-field-" + formFieldCounter -var FormFieldAdvice = React.createClass({ - propTypes: { - state: React.PropTypes.string.isRequired, - }, - render: function() { + if (this.props.hidden) { + return null; + } + + const field =
+ { this.props.label ? +
+ +
: '' + } + + {this.props.children} + + { !hasError && this.props.helper ?
{this.props.helper}
: '' } + { hasError ?
{this.state.adviceText}
: '' } +
return ( - this.props.state != 'hidden' - ?
-
- -
- - {this.props.children} - -
-
-
- : null + this.props.row ? +
{field}
: + field ); } }); 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..4d227248e 100644 --- a/ui/js/lbry.js +++ b/ui/js/lbry.js @@ -118,28 +118,36 @@ lbry.call = function (method, params, callback, errorCallback, connectFailedCall //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 +155,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); } @@ -430,6 +434,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)); } diff --git a/ui/js/lbryio.js b/ui/js/lbryio.js index e4bb40eb4..582592b71 100644 --- a/ui/js/lbryio.js +++ b/ui/js/lbryio.js @@ -1,8 +1,15 @@ +import {getLocal, setLocal} from './utils.js'; +import lbry from './lbry.js'; + const querystring = require('querystring'); -const lbryio = {}; +const lbryio = { + _accessToken: getLocal('accessToken'), + _authenticationPromise: null, + _user : null +}; -const CONNECTION_STRING = 'https://apidev.lbry.tech/'; +const CONNECTION_STRING = 'http://localhost:8080/'; const mocks = { 'reward_type.get': ({name}) => { @@ -37,25 +44,18 @@ lbryio.call = function(resource, action, params={}, method='get') { /* end temp */ - console.log('about to create xhr object'); const xhr = new XMLHttpRequest; - xhr.addEventListener('error', function (error) { - console.log('received XHR error:', error); - reject(error); + xhr.addEventListener('error', function (event) { + reject(new Error("Something went wrong making an internal API call.")); }); - console.log('about to add timeout listener'); xhr.addEventListener('timeout', function() { - console.log('XHR timed out'); - reject(new Error('XMLHttpRequest connection timed out')); }); - console.log('about to create load listener'); xhr.addEventListener('load', function() { - console.log('loaded'); const response = JSON.parse(xhr.responseText); if (!response.success) { @@ -78,14 +78,12 @@ lbryio.call = function(resource, action, params={}, method='get') { } }); - console.log('about to call xhr.open'); - // 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: localStorage.getItem('appId'), ...params}; + const fullParams = {app_id: lbryio._accessToken, ...params}; if (method == 'get') { console.info('GET ', CONNECTION_STRING + resource + '/' + action, ' | params:', fullParams); @@ -100,4 +98,69 @@ lbryio.call = function(resource, action, params={}, method='get') { }); }; +lbryio.setAccessToken = (token) => { + setLocal('accessToken', token) + lbryio._accessToken = token +} + +lbryio.authenticate = () => { + 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); + 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/main.js b/ui/js/main.js index ea8d4d0a5..12dee5d92 100644 --- a/ui/js/main.js +++ b/ui/js/main.js @@ -5,6 +5,8 @@ import lbryio from './lbryio.js'; import lighthouse from './lighthouse.js'; import App from './app.js'; import SplashScreen from './component/splash.js'; +import {AuthOverlay} from './component/auth.js'; +import {Welcome} from './component/welcome.js'; const {remote} = require('electron'); const contextMenu = remote.require('./menu/context-menu'); @@ -20,30 +22,21 @@ window.addEventListener('contextmenu', (event) => { let init = function() { window.lbry = lbry; window.lighthouse = lighthouse; + let canvas = document.getElementById('canvas'); - var canvas = document.getElementById('canvas'); - if (window.sessionStorage.getItem('loaded') == 'y') { - ReactDOM.render(, canvas) + lbry.connect().then(function(isConnected) { + lbryio.authenticate() //start auth process as soon as soon as we can get an install ID + }) + + async 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(
, canvas) + } + + if (window.sessionStorage.getItem('loaded') == 'y' && false) { + 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/discover.js b/ui/js/page/discover.js index c6705d871..993481d07 100644 --- a/ui/js/page/discover.js +++ b/ui/js/page/discover.js @@ -5,7 +5,6 @@ import {FileTile} from '../component/file-tile.js'; import {Link} from '../component/link.js'; import {ToolTip} from '../component/tooltip.js'; import {BusyMessage} from '../component/common.js'; -import {Welcome} from '../component/welcome.js'; var fetchResultsStyle = { color: '#888', @@ -174,13 +173,12 @@ var DiscoverPage = React.createClass({ }, render: function() { + //{ !this.props.query && !this.state.searching ? : null } return (
{ this.state.searching ? : null } { !this.state.searching && this.props.query && this.state.results.length ? : null } { !this.state.searching && this.props.query && !this.state.results.length ? : null } - { !this.props.query && !this.state.searching ? : null } -
); } diff --git a/ui/js/page/publish.js b/ui/js/page/publish.js index 82ef6bdf8..6e0799263 100644 --- a/ui/js/page/publish.js +++ b/ui/js/page/publish.js @@ -162,7 +162,7 @@ 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; } diff --git a/ui/js/rewards.js b/ui/js/rewards.js index 262183f04..4310b18b8 100644 --- a/ui/js/rewards.js +++ b/ui/js/rewards.js @@ -10,7 +10,6 @@ const MESSAGES = { const rewards = {}; rewards.claimReward = function(type) { - console.log('top of claimReward') return new Promise((resolve, reject) => { console.log('top of promise body') lbry.get_new_address().then((address) => { @@ -30,7 +29,7 @@ rewards.claimReward = function(type) { document.dispatchEvent(new CustomEvent('globalNotice', { detail: { message: MESSAGES[type], - isError: false, + isError: false, }, })); @@ -41,7 +40,7 @@ rewards.claimReward = function(type) { document.dispatchEvent(new CustomEvent('globalNotice', { detail: { message: `Failed to claim reward: ${error.message}`, - isError: true, + isError: true, }, })); document.dispatchEvent(new CustomEvent('rewardFailed', error)); diff --git a/ui/js/utils.js b/ui/js/utils.js index 5b5cf246a..290a0f54e 100644 --- a/ui/js/utils.js +++ b/ui/js/utils.js @@ -12,4 +12,4 @@ export function getLocal(key) { */ export function setLocal(key, value) { localStorage.setItem(key, JSON.stringify(value)); -} +} \ No newline at end of file diff --git a/ui/scss/_form.scss b/ui/scss/_form.scss new file mode 100644 index 000000000..c54343c4a --- /dev/null +++ b/ui/scss/_form.scss @@ -0,0 +1,66 @@ +@import "global"; + +/* this probably shouldn't exist but it does so here we are - Jeremy */ + + +textarea, +select, +input[type="text"], +input[type="password"], +input[type="email"], +input[type="number"], +input[type="search"], +input[type="date"] { + @include placeholder { + color: lighten($color-text-dark, 60%); + } + transition: all $transition-standard; + cursor: pointer; + padding-left: 1px; + padding-right: 1px; + box-sizing: border-box; + -webkit-appearance: none; + &[readonly] { + background-color: #bbb; + } +} + +input[type="text"], +input[type="password"], +input[type="email"], +input[type="number"], +input[type="search"], +input[type="date"] { + border-bottom: 2px solid $color-form-border; + line-height: $spacing-vertical - 4; + height: $spacing-vertical * 1.5; + &.form-field__input--error { + border-color: $color-error; + } +} + +textarea:focus, +input[type="text"]:focus, +input[type="password"]:focus, +input[type="email"]:focus, +input[type="number"]:focus, +input[type="search"]:focus, +input[type="date"]:focus { + border-color: $color-primary; +} + +textarea { + border: 2px solid $color-form-border; +} + +.form-row +{ + + .form-row + { + margin-top: $spacing-vertical; + } + + .form-row-submit + { + margin-top: $spacing-vertical; + } +} \ No newline at end of file diff --git a/ui/scss/_global.scss b/ui/scss/_global.scss index 201409835..7d3249888 100644 --- a/ui/scss/_global.scss +++ b/ui/scss/_global.scss @@ -9,14 +9,15 @@ $color-primary: #155B4A; $color-light-alt: hsl(hue($color-primary), 15, 85); $color-text-dark: #000; $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; @@ -29,6 +30,9 @@ $height-header: $spacing-vertical * 2.5; $height-button: $spacing-vertical * 1.5; $default-box-shadow: 0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12); +//$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..691ccb328 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; @@ -76,11 +76,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 +99,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 +122,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 +144,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; @@ -350,7 +178,6 @@ input[type="text"], input[type="search"] background: rgb(255, 255, 255); overflow: auto; border-radius: 4px; - outline: none; padding: 36px; max-width: 250px; } diff --git a/ui/scss/_reset.scss b/ui/scss/_reset.scss index 66d0b0f1e..8e0db2623 100644 --- a/ui/scss/_reset.scss +++ b/ui/scss/_reset.scss @@ -3,20 +3,20 @@ 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 +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 +25,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 89d84058a..3c28c011c 100644 --- a/ui/scss/all.scss +++ b/ui/scss/all.scss @@ -1,12 +1,15 @@ @import "_reset"; @import "_grid"; @import "_icons"; +@import "_form"; @import "_mediaelement"; @import "_canvas"; @import "_gui"; @import "component/_table"; +@import "component/_button.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"; 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/_form-field.scss b/ui/scss/component/_form-field.scss new file mode 100644 index 000000000..db2d168e0 --- /dev/null +++ b/ui/scss/component/_form-field.scss @@ -0,0 +1,36 @@ +@import "../global"; + +.form-field { + display: inline-block; +} + +.form-field__label { + margin-top: $spacing-vertical * 2/3; + margin-bottom: $spacing-vertical * 1/3; + line-height: 1; +} + +.form-field__label--error { + color: $color-error; +} + +.form-field__input-text { + width: 330px; +} + +.form-field__input-text-number { + width: 50px; +} + +.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 index 1acc41790..5c56e1ad3 100644 --- a/ui/scss/component/_modal-page.scss +++ b/ui/scss/component/_modal-page.scss @@ -12,6 +12,24 @@ z-index: 9999; } +.modal-page { + position: fixed; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + left: 0; + right: 0; + top: 0; + bottom: 0; + + border: 1px solid rgb(204, 204, 204); + background: rgb(255, 255, 255); + overflow: auto; +} + +/* .modal-page { position: fixed; display: flex; @@ -31,3 +49,13 @@ 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 -- 2.45.2 From ecf54f400b454204392fd28dabe147d5890e14cb Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Mon, 10 Apr 2017 08:32:40 -0400 Subject: [PATCH 054/158] mostly garbage --- ui/js/app.js | 16 +- ui/js/component/auth.js | 161 ++++++++------- ui/js/component/drawer.js | 2 +- ui/js/component/file-actions.js | 4 +- ui/js/component/file-tile.js | 141 +++++++++++-- ui/js/component/form.js | 133 ++++++++----- ui/js/component/header.js | 4 +- ui/js/component/link.js | 74 +++++++ ui/js/component/notification-bar.js | 53 ----- ui/js/component/snack-bar.js | 58 ++++++ ui/js/component/welcome.js | 156 --------------- ui/js/lbry.js | 23 --- ui/js/lbryio.js | 24 +-- ui/js/main.js | 8 +- ui/js/page/claim_code.js | 158 --------------- ui/js/page/developer.js | 2 +- ui/js/page/discover.js | 56 +++--- ui/js/page/email.js | 4 +- ui/js/page/file-list.js | 2 +- ui/js/page/publish.js | 24 ++- ui/js/page/referral.js | 130 ------------ ui/js/page/reward.js | 242 +++++++++++------------ ui/js/page/rewards.js | 54 ++--- ui/js/page/settings.js | 149 ++++++++------ ui/js/page/wallet.js | 110 ++++++----- ui/js/rewards.js | 41 ++-- ui/js/utils.js | 16 ++ ui/scss/_canvas.scss | 83 ++------ ui/scss/_form.scss | 66 ------- ui/scss/_global.scss | 4 +- ui/scss/_gui.scss | 6 +- ui/scss/_reset.scss | 4 + ui/scss/all.scss | 4 +- ui/scss/component/_card.scss | 122 ++++++++++++ ui/scss/component/_file-tile.scss | 10 +- ui/scss/component/_form-field.scss | 84 +++++++- ui/scss/component/_modal-page.scss | 10 +- ui/scss/component/_notification-bar.scss | 6 - ui/scss/component/_snack-bar.scss | 42 ++++ 39 files changed, 1120 insertions(+), 1166 deletions(-) delete mode 100644 ui/js/component/notification-bar.js create mode 100644 ui/js/component/snack-bar.js delete mode 100644 ui/js/component/welcome.js delete mode 100644 ui/js/page/claim_code.js delete mode 100644 ui/js/page/referral.js delete mode 100644 ui/scss/_form.scss create mode 100644 ui/scss/component/_card.scss delete mode 100644 ui/scss/component/_notification-bar.scss create mode 100644 ui/scss/component/_snack-bar.scss diff --git a/ui/js/app.js b/ui/js/app.js index 03d508f81..424fb8533 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -2,22 +2,18 @@ import React from 'react'; import {Line} from 'rc-progress'; import lbry from './lbry.js'; -import lbryio from './lbryio.js'; import EmailPage from './page/email.js'; import SettingsPage from './page/settings.js'; 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'; @@ -229,15 +225,11 @@ 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', '?rewards': 'Rewards', }; case 'downloaded': @@ -268,14 +260,8 @@ var App = React.createClass({ return ; case 'start': return ; - case 'claim': - return ; - case 'referral': - return ; case 'rewards': - return ; - case 'reward': - return ; + return ; case 'wallet': case 'send': case 'receive': diff --git a/ui/js/component/auth.js b/ui/js/component/auth.js index 2b7e6d02b..83edd2c9f 100644 --- a/ui/js/component/auth.js +++ b/ui/js/component/auth.js @@ -1,10 +1,11 @@ import React from 'react'; import lbryio from '../lbryio.js'; +import Modal from './modal.js'; import ModalPage from './modal-page.js'; -import {Link} from '../component/link.js'; -import FormField from '../component/form.js'; -import Notice from '../component/notice.js' +import {Link, RewardLink} from '../component/link.js'; +import {FormField, FormRow} from '../component/form.js'; +import rewards from '../rewards.js'; const SubmitEmailStage = React.createClass({ @@ -29,8 +30,8 @@ const SubmitEmailStage = React.createClass({ lbryio.call('user_email', 'new', {email: this.state.email}, 'post').then(() => { this.props.onEmailSaved(); }, (error) => { - if (this._emailField) { - this._emailField.showError(error.message) + if (this._emailRow) { + this._emailRow.showError(error.message) } this.setState({ submitting: false }); }); @@ -39,10 +40,10 @@ const SubmitEmailStage = React.createClass({ return (
- { this._emailField = field }} type="text" label="Email" placeholder="webmaster@toplbryfan.com" + { this._emailRow = ref }} type="text" label="Email" placeholder="webmaster@toplbryfan.com" name="email" value={this.state.email} onChange={this.handleEmailChanged} /> -
+
@@ -71,27 +72,29 @@ const ConfirmEmailStage = React.createClass({ submitting: true, }); - lbryio.call('user_email', 'confirm', {verification_token: this.state.code}, 'post').then(() => { - rewards.claimReward('confirm_email').then(() => { - this.props.onDone(); - }, (err) => {l - this.props.onDone(); - }); - }, (error) => { - if (this._codeField) { - this._codeField.showError(error.message) - this.setState({ submitting: false }) + 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._codeField = field }} type="text" + { 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."/> -
+
@@ -100,9 +103,29 @@ const ConfirmEmailStage = React.createClass({ } }); +const WelcomeStage = React.createClass({ + onRewardClaim: function() { + console.log('omg'); + }, + render: function() { + return ( +
+

Welcome to LBRY.

+

LBRY is the first community controlled content marketplace.

+

Since you're new here, we'll toss you some credits.

+
+ +
+

LBC is blah blah blah.

+

And remember, LBRY is a beta and be safe!

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

An error was encountered that we cannot continue from.

@@ -129,68 +152,66 @@ export const AuthOverlay = React.createClass({ error: ErrorStage, email: SubmitEmailStage, confirm: ConfirmEmailStage, + welcome: WelcomeStage, }, - propTypes: { - // onDone: React.PropTypes.func.isRequired, - }, + getInitialState: function() { return { - stage: "pending", + stage: "welcome", stageProps: {} }; }, - componentWillMount: function() { - lbryio.authenticate().then(function(user) { - console.log(user); - if (!user.HasVerifiedEmail) { - this.setState({ - stage: "email", - stageProps: { - onEmailSaved: function() { - this.setState({ - stage: "confirm" - }) - }.bind(this) - } - }) - } else { - this.setState({ stage: null }) - } - }.bind(this)).catch((err) => { - this.setState({ - stage: "error", - stageProps: { errorText: err.message } - }) - document.dispatchEvent(new CustomEvent('unhandledError', { - detail: { - message: err.message, - data: err.stack - } - })); - }) + 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 { + // this.endAuth() + // } + // }.bind(this)).catch((err) => { + // this.setState({ + // stage: "error", + // stageProps: { errorText: err.message } + // }) + // document.dispatchEvent(new CustomEvent('unhandledError', { + // detail: { + // message: err.message, + // data: err.stack + // } + // })); + // }) }, - // handleStageDone: function() { - // if (this.state.stageNum >= this._stages.length - 1) { - // this.props.onDone(); - // } else { - // this.setState({ - // stageNum: this.state.stageNum + 1, - // }); - // } - // }, - - // render: function() { - console.log(lbryio.user); if (!this.state.stage || lbryio.user && lbryio.user.HasVerifiedEmail) { return null; } const StageContent = this._stages[this.state.stage]; return ( - -

LBRY Early Access

- -
+ this.state.stage != "welcome" ? + +

LBRY Early Access

+ +
: + + + ); } }); \ No newline at end of file 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..715133bd8 100644 --- a/ui/js/component/file-actions.js +++ b/ui/js/component/file-actions.js @@ -3,7 +3,7 @@ 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 {FormField} from './form.js'; import {ToolTip} from '../component/tooltip.js'; import {DropDownMenu, DropDownMenuItem} from './menu.js'; @@ -53,7 +53,7 @@ let WatchLink = React.createClass({ render: function() { return (
- + You don't have enough LBRY credits to pay for this stream. diff --git a/ui/js/component/file-tile.js b/ui/js/component/file-tile.js index b65434bac..bb9483374 100644 --- a/ui/js/component/file-tile.js +++ b/ui/js/component/file-tile.js @@ -47,7 +47,7 @@ let FilePrice = React.createClass({ } return ( - + ); @@ -131,8 +131,8 @@ export let FileTileStream = React.createClass({ const title = isConfirmed ? metadata.title : lbryUri; const obscureNsfw = this.props.obscureNsfw && isConfirmed && metadata.nsfw; return ( -
-
+
+
@@ -140,24 +140,139 @@ export let FileTileStream = React.createClass({ { !this.props.hidePrice ? : null} -
-

+ +
+ +
+
+

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

+
+

+
+ {this.state.showNsfwHelp + ?
+

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

+
+ : null} +
+ ); + } +}); + +export let FileCardStream = React.createClass({ + _fileInfoSubscribeId: null, + _isMounted: null, + + propTypes: { + metadata: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.object]), + outpoint: React.PropTypes.string, + hideOnRemove: React.PropTypes.bool, + hidePrice: React.PropTypes.bool, + obscureNsfw: React.PropTypes.bool + }, + getInitialState: function() { + return { + showNsfwHelp: false, + isHidden: false, + available: null, + } + }, + getDefaultProps: function() { + return { + obscureNsfw: !lbry.getClientSetting('showNsfw'), + hidePrice: false + } + }, + componentDidMount: function() { + this._isMounted = true; + if (this.props.hideOnRemove) { + this._fileInfoSubscribeId = lbry.fileInfoSubscribe(this.props.outpoint, this.onFileInfoUpdate); + } + }, + componentWillUnmount: function() { + if (this._fileInfoSubscribeId) { + lbry.fileInfoUnsubscribe(this.props.outpoint, this._fileInfoSubscribeId); + } + }, + onFileInfoUpdate: function(fileInfo) { + if (!fileInfo && this._isMounted && this.props.hideOnRemove) { + this.setState({ + isHidden: true + }); + } + }, + handleMouseOver: function() { + if (this.props.obscureNsfw && this.props.metadata && this.props.metadata.nsfw) { + this.setState({ + showNsfwHelp: true, + }); + } + }, + handleMouseOut: function() { + if (this.state.showNsfwHelp) { + this.setState({ + showNsfwHelp: false, + }); + } + }, + render: function() { + if (this.state.isHidden) { + return null; + } + + const lbryUri = uri.normalizeLbryUri(this.props.uri); + const metadata = this.props.metadata; + const isConfirmed = typeof metadata == 'object'; + const title = isConfirmed ? metadata.title : lbryUri; + const obscureNsfw = this.props.obscureNsfw && isConfirmed && metadata.nsfw; + return ( +
+
+ - -

- + hasSignature={this.props.hasSignature} signatureIsValid={this.props.signatureIsValid} /> + +

+ +
+ { !this.props.hidePrice + ? + : null} +
+ {isConfirmed - ? metadata.description - : This file is pending confirmation.} + ? metadata.description + : This file is pending confirmation.} -

+
+
+
{this.state.showNsfwHelp diff --git a/ui/js/component/form.js b/ui/js/component/form.js index 176f3334f..b33fcddd5 100644 --- a/ui/js/component/form.js +++ b/ui/js/component/form.js @@ -1,31 +1,30 @@ 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 formFieldCounter = 0; +function formFieldId() { + return "form-field-" + (++formFieldCounter); +} -var FormField = React.createClass({ +export let FormField = React.createClass({ _fieldRequiredText: 'This field is required', _type: null, _element: null, propTypes: { type: React.PropTypes.string.isRequired, - row: React.PropTypes.bool, - hidden: React.PropTypes.bool, + hasError: React.PropTypes.bool }, getInitialState: function() { return { - errorState: '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') { @@ -38,22 +37,11 @@ var FormField = React.createClass({ }, showError: function(text) { this.setState({ - errorState: 'shown', - adviceText: text, + isError: true, + errorMessage: text, }); - - // setTimeout(() => { - // this.setState({ - // errorState: 'fading', - // }); - // setTimeout(() => { - // this.setState({ - // errorState: 'hidden', - // }); - // }, 450); - // }, 5000); }, - warnRequired: function() { + showRequiredError: function() { this.showError(this._fieldRequiredText); }, focus: function() { @@ -74,33 +62,27 @@ var FormField = React.createClass({ render: function() { // Pass all unhandled props to the field element const otherProps = Object.assign({}, this.props), - hasError = this.state.errorState != 'hidden'; + isError = this.state.isError !== null ? this.state.isError : this.props.hasError, + elementId = this.props.id ? this.props.id : formFieldId(), + renderElementInsideLabel = this.props.label && formFieldNestedLabelTypes.includes(this.props.type); + delete otherProps.type; - delete otherProps.hidden; delete otherProps.label; - delete otherProps.row; - delete otherProps.helper; + delete otherProps.hasError; - ++formFieldCounter; - const elementId = "form-field-" + formFieldCounter + const element = + {this.props.children} + ; - if (this.props.hidden) { - return null; - } - - const field =
- { this.props.label ? -
- -
: '' - } - - {this.props.children} - - { !hasError && this.props.helper ?
{this.props.helper}
: '' } - { hasError ?
{this.state.adviceText}
: '' } + return
+ { renderElementInsideLabel ? + : element } + { isError ?
{this.state.errorMessage}
: '' }
return ( this.props.row ? @@ -108,6 +90,57 @@ var FormField = React.createClass({ field ); } -}); +}) -export default FormField; +export let FormRow = React.createClass({ + propTypes: { + label: React.PropTypes.string, + // helper: React.PropTypes.html, + }, + getValue: function() { + if (this.props.type == 'checkbox') { + return this.refs.field.checked; + } else if (this.props.type == 'file') { + return this.refs.field.files[0].path; + } else { + return this.refs.field.value; + } + }, + getInitialState: function() { + return { + isError: false, + errorMessage: null, + } + }, + showError: function(text) { + this.setState({ + isError: true, + errorMessage: text, + }); + }, + getValue: function() { + return this.refs.field.getValue(); + }, + render: function() { + const fieldProps = Object.assign({}, this.props), + elementId = formFieldId(), + renderLabelInFormField = formFieldNestedLabelTypes.includes(this.props.type); + + if (!renderLabelInFormField) { + delete fieldProps.label; + } + delete fieldProps.helper; + + return
+ { 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 6e186cc44..463042cff 100644 --- a/ui/js/component/header.js +++ b/ui/js/component/header.js @@ -1,6 +1,6 @@ import React from 'react'; import {Link} from './link.js'; -import NotificationBar from './notification-bar.js'; +import {Icon} from './common.js'; var Header = React.createClass({ getInitialState: function() { @@ -53,6 +53,7 @@ var Header = React.createClass({

{ this.state.title }

+
@@ -62,7 +63,6 @@ var Header = React.createClass({ : '' } - ); } diff --git a/ui/js/component/link.js b/ui/js/component/link.js index 8a4d76f76..8bcaddd48 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,76 @@ export let Link = React.createClass({ ); } +}); + +export let RewardLink = React.createClass({ + propTypes: { + type: React.PropTypes.string.isRequired, + claimed: React.PropTypes.bool, + onRewardClaim: React.PropTypes.func + }, + refreshClaimable: function() { + switch(this.props.type) { + case 'new_user': + this.setState({ claimable: true }); + return; + + case 'first_publish': + lbry.claim_list_mine().then(function(list) { + this.setState({ + claimable: list.length > 0 + }) + }.bind(this)); + return; + } + }, + componentWillMount: function() { + this.refreshClaimable(); + }, + getInitialState: function() { + return { + claimable: true, + pending: false, + errorMessage: null + } + }, + claimReward: function() { + this.setState({ + pending: true + }) + rewards.claimReward(this.props.type).then(function(reward) { + console.log(reward); + this.setState({ + pending: false, + errorMessage: null + }) + if (this.props.onRewardClaim) { + this.props.onRewardClaim(); + } + }.bind(this)).catch(function(error) { + this.setState({ + errorMessage: error.message, + pending: false + }) + }.bind(this)) + }, + clearError: function() { + this.setState({ + errorMessage: null + }) + }, + render: function() { + return ( +
+ {this.props.claimed + ? Reward claimed. + : } + {this.state.errorMessage ? + + {this.state.errorMessage} + + : ''} +
+ ); + } }); \ No newline at end of file diff --git a/ui/js/component/notification-bar.js b/ui/js/component/notification-bar.js deleted file mode 100644 index f6662b552..000000000 --- a/ui/js/component/notification-bar.js +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react'; -import lbry from '../lbry.js'; -import Notice from '../component/notice.js'; - -export const NotificationBar = React.createClass({ - _displayTime: 8, // in seconds - - _hideTimeout: null, - - getInitialState: function() { - return { - message: null, - isError: null, - } - }, - handleNoticeReceived: function(event) { - if (this._hideTimeout) { - clearTimeout(this._hideTimeout); - } - - const {detail: {message, isError}} = event; - this.setState({ - message: message, - isError: isError, - }); - - this._hideTimeout = setTimeout(() => { - this.setState({ - message: null, - isError: null, - }); - }, this._displayTime * 1000); - }, - componentWillMount: function() { - document.addEventListener('globalNotice', this.handleNoticeReceived); - }, - componentWillUnmount: function() { - document.removeEventListener('globalNotice', this.handleNoticeReceived); - }, - render: function() { - if (!this.state.message) { - return null; - } - - return ( - - {this.state.message} - - ); - }, -}); - -export default NotificationBar; \ 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..e1ddb01b0 --- /dev/null +++ b/ui/js/component/snack-bar.js @@ -0,0 +1,58 @@ +import React from 'react'; +import lbry from '../lbry.js'; + +export const SnackBar = React.createClass({ + + _displayTime: 5, // in seconds + + _hideTimeout: null, + + getInitialState: function() { + return { + snacks: [] + } + }, + handleSnackReceived: function(event) { + // console.log(event); + // if (this._hideTimeout) { + // clearTimeout(this._hideTimeout); + // } + + let snacks = this.state.snacks; + snacks.push(event.detail); + this.setState({ snacks: snacks}); + }, + componentWillMount: function() { + document.addEventListener('globalNotice', this.handleSnackReceived); + }, + componentWillUnmount: function() { + document.removeEventListener('globalNotice', this.handleSnackReceived); + }, + render: function() { + if (!this.state.snacks.length) { + this._hideTimeout = null; //should be unmounting anyway, but be safe? + return null; + } + + let snack = this.state.snacks[0]; + + if (this._hideTimeout === null) { + this._hideTimeout = setTimeout(function() { + this._hideTimeout = null; + let snacks = this.state.snacks; + snacks.shift(); + this.setState({ snacks: snacks }); + }.bind(this), this._displayTime * 1000); + } + + return ( +
+ {snack.message} + {snack.linkText && snack.linkTarget ? + {snack.linkText} : ''} +
+ ); + }, +}); + +export default SnackBar; \ No newline at end of file diff --git a/ui/js/component/welcome.js b/ui/js/component/welcome.js deleted file mode 100644 index 36036abe1..000000000 --- a/ui/js/component/welcome.js +++ /dev/null @@ -1,156 +0,0 @@ -import React from 'react'; -import lbryio from '../lbryio.js'; - -import ModalPage from './modal-page.js'; -import {Link} from '../component/link.js'; -import FormField from '../component/form.js'; -import Notice from '../component/notice.js' - - -const SubmitEmailStage = React.createClass({ - getInitialState: function() { - return { - rewardType: null, - email: '', - submitting: false, - errorMessage: null, - }; - }, - handleEmailChanged: function(event) { - this.setState({ - email: event.target.value, - }); - }, - handleSubmit: function(event) { - event.preventDefault(); - - this.setState({ - submitting: true, - }); - lbryio.call('user_email', 'new', {email: this.state.email}, 'post').then(() => { - this.props.onDone(); - }, (error) => { - this.setState({ - submitting: false, - errorMessage: error.message, - }); - }); - }, - render: function() { - return ( -
-

Welcome to LBRY

- {this.state.errorMessage - ? - {this.state.errorMessage} - - : null} -

Copy here explaining what we do with your email, and the reward.

-
-
Email
-
-
-
- ); - } -}); - -const ConfirmEmailStage = React.createClass({ - getInitialState: function() { - return { - rewardType: null, - code: '', - submitting: false, - errorMessage: null, - }; - }, - handleCodeChanged: function(event) { - this.setState({ - code: event.target.value, - }); - }, - handleSubmit: function(event) { - event.preventDefault(); - this.setState({ - submitting: true, - }); - - lbryio.call('user_email', 'confirm', {verification_token: this.state.code}, 'post').then(() => { - rewards.claimReward('confirm_email').then(() => { - console.log('succeeded'); - this.props.onDone(); - }, (err) => { - console.log('failed'); - this.props.onDone(); - }); - }, (error) => { - this.setState({ - submitting: false, - errorMessage: error.message, - }); - }); - }, - render: function() { - return ( -
-

Confirm Your Email Address

- {this.state.errorMessage - ? - {this.state.errorMessage} - - : null} -

Please enter your verification code to confirm your email address.

-
-
-
-
-
- ); - } -}); - -const FinalMessageStage = React.createClass({ - render: function() { - return ( -
-

Email verified

-

Text here about what happens next

-
-
- ); - } -}); - -export const Welcome = React.createClass({ - _stages: [ - SubmitEmailStage, - ConfirmEmailStage, - FinalMessageStage, - ], - propTypes: { - onDone: React.PropTypes.func.isRequired, - }, - getInitialState: function() { - return { - stageNum: 0, - }; - }, - handleStageDone: function() { - if (this.state.stageNum >= this._stages.length - 1) { - this.props.onDone(); - } else { - this.setState({ - stageNum: this.state.stageNum + 1, - }); - } - }, - render: function() { - const Content = this._stages[this.state.stageNum]; - return ( - - - - ); - } -}); - diff --git a/ui/js/lbry.js b/ui/js/lbry.js index 4d227248e..8fbfdb04d 100644 --- a/ui/js/lbry.js +++ b/ui/js/lbry.js @@ -282,29 +282,6 @@ lbry.getCostInfo = function(lbryUri, callback, errorCallback) { }); } -lbry.getFeaturedDiscoverNames = function(callback) { - return new Promise(function(resolve, reject) { - var xhr = new XMLHttpRequest; - xhr.open('GET', 'https://api.lbry.io/discover/list', true); - xhr.onload = () => { - if (xhr.status === 200) { - var responseData = JSON.parse(xhr.responseText); - if (responseData.data) //new signature, once api.lbry.io is updated - { - resolve(responseData.data); - } - else - { - resolve(responseData); - } - } else { - reject(Error('Failed to fetch featured names.')); - } - }; - xhr.send(); - }); -} - lbry.getMyClaims = function(callback) { lbry.call('get_name_claims', {}, callback); } diff --git a/ui/js/lbryio.js b/ui/js/lbryio.js index 582592b71..9c6d37f03 100644 --- a/ui/js/lbryio.js +++ b/ui/js/lbryio.js @@ -1,4 +1,4 @@ -import {getLocal, setLocal} from './utils.js'; +import {getLocal, getSession, setSession, setLocal} from './utils.js'; import lbry from './lbry.js'; const querystring = require('querystring'); @@ -20,18 +20,7 @@ const mocks = { value: 50, claimed: false, }; - }, - 'reward_type.list': () => { - return [ - { - name: 'link_github', - title: 'Link your GitHub account', - description: 'Link LBRY to your GitHub account', - value: 50, - claimed: false, - }, - ]; - }, + } }; lbryio.call = function(resource, action, params={}, method='get') { @@ -103,7 +92,7 @@ lbryio.setAccessToken = (token) => { lbryio._accessToken = token } -lbryio.authenticate = () => { +lbryio.authenticate = function() { if (lbryio._authenticationPromise === null) { lbryio._authenticationPromise = new Promise((resolve, reject) => { lbry.status().then(({installation_id}) => { @@ -117,7 +106,12 @@ lbryio.authenticate = () => { resolve(data) }).catch(function(err) { lbryio.setAccessToken(null); - reject(err); + if (!getSession('reloadedOnFailedAuth')) { + setSession('reloadedOnFailedAuth', true) + window.location.reload(); + } else { + reject(err); + } }) } diff --git a/ui/js/main.js b/ui/js/main.js index 12dee5d92..544ebb0a2 100644 --- a/ui/js/main.js +++ b/ui/js/main.js @@ -5,8 +5,8 @@ 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'; -import {Welcome} from './component/welcome.js'; const {remote} = require('electron'); const contextMenu = remote.require('./menu/context-menu'); @@ -28,12 +28,12 @@ let init = function() { lbryio.authenticate() //start auth process as soon as soon as we can get an install ID }) - async function onDaemonReady() { + function onDaemonReady() { window.sessionStorage.setItem('loaded', 'y'); //once we've made it here once per session, we don't need to show splash again - ReactDOM.render(
, canvas) + ReactDOM.render(
, canvas) } - if (window.sessionStorage.getItem('loaded') == 'y' && false) { + if (window.sessionStorage.getItem('loaded') == 'y') { onDaemonReady(); } else { 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 993481d07..7751db2db 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,45 +59,44 @@ 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: [], + featuredNames: {}, }; }, componentWillMount: function() { - lbry.getFeaturedDiscoverNames().then((featuredNames) => { + lbryio.call('discover', 'list', { version: "early-access" } ).then((featuredNames) => { this.setState({ featuredNames: featuredNames }); }); }, 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!'); - + console.log(this.state.featuredNames); return ( -
-
-

Featured Content

- { this.state.featuredNames.map(name => ) } -
-
-

- Community Content - -

- - - - - -
+
+ { + Object.keys(this.state.featuredNames).map(function(category) { + return + }.bind(this)) + }
); } @@ -173,12 +173,12 @@ var DiscoverPage = React.createClass({ }, render: function() { - //{ !this.props.query && !this.state.searching ? : null } return (
{ this.state.searching ? : null } { !this.state.searching && this.props.query && this.state.results.length ? : null } { !this.state.searching && this.props.query && !this.state.results.length ? : null } + { !this.props.query && !this.state.searching ? : null }
); } diff --git a/ui/js/page/email.js b/ui/js/page/email.js index b7f31cd49..76b031737 100644 --- a/ui/js/page/email.js +++ b/ui/js/page/email.js @@ -1,7 +1,7 @@ import React from 'react'; import lbryio from '../lbryio.js'; import {getLocal, setLocal} from '../utils.js'; -import FormField from '../component/form.js' +import {FormField} from '../component/form.js' import {Link} from '../component/link.js' import rewards from '../rewards.js'; @@ -12,7 +12,7 @@ const EmailPage = React.createClass({ } if (!this.state.email) { - this._emailField.warnRequired(); + this._emailField.showRequiredError(); } }, componentWillMount: function() { diff --git a/ui/js/page/file-list.js b/ui/js/page/file-list.js index ba91835e7..518bb85d6 100644 --- a/ui/js/page/file-list.js +++ b/ui/js/page/file-list.js @@ -2,7 +2,7 @@ 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 {BusyMessage, Thumbnail} from '../component/common.js'; diff --git a/ui/js/page/publish.js b/ui/js/page/publish.js index 6e0799263..4f28dc567 100644 --- a/ui/js/page/publish.js +++ b/ui/js/page/publish.js @@ -1,7 +1,7 @@ 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 Modal from '../component/modal.js'; @@ -36,7 +36,7 @@ var PublishPage = React.createClass({ for (let fieldName of checkFields) { var field = this.refs[fieldName]; if (field.getValue() === '') { - field.warnRequired(); + field.showRequiredError(); if (!missingFieldFound) { field.focus(); missingFieldFound = true; @@ -84,7 +84,7 @@ var PublishPage = React.createClass({ if (this.refs.file.getValue() !== '') { publishArgs.file_path = this.refs.file.getValue(); } - + lbry.publish(publishArgs, (message) => { this.handlePublishStarted(); }, null, (error) => { @@ -344,9 +344,12 @@ var PublishPage = React.createClass({
-

LBRY Name

-
- +
+

LBRY Name

+
+
+ What LBRY name would you like to claim for this file? .
)} /> { (!this.state.name ? null @@ -356,7 +359,6 @@ var PublishPage = React.createClass({ ? 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?
@@ -381,9 +383,11 @@ var PublishPage = React.createClass({
-

Choose File

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

Choose File

+
+ + { this.state.myClaimExists ?
If you don't choose a file, the file from your existing claim will be used.
: null } +
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 index 773e21893..2fb5b3e64 100644 --- a/ui/js/page/reward.js +++ b/ui/js/page/reward.js @@ -3,124 +3,124 @@ 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; +// +// 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 index 52f705035..62c5d0497 100644 --- a/ui/js/page/rewards.js +++ b/ui/js/page/rewards.js @@ -1,28 +1,34 @@ import React from 'react'; import lbry from '../lbry.js'; import lbryio from '../lbryio.js'; -import {CreditAmount} from '../component/common.js'; +import {CreditAmount, Icon} from '../component/common.js'; +import rewards from '../rewards.js'; import Modal from '../component/modal.js'; -import {Link} from '../component/link.js'; +import {RewardLink} from '../component/link.js'; const RewardTile = React.createClass({ propTypes: { - name: React.PropTypes.string.isRequired, + 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.description}
- {this.props.claimed - ? This reward has been claimed. - : } +
+
+ +

{this.props.title}

+
+
+ {this.props.claimed + ? Reward claimed. + : } +
+
{this.props.description}
); @@ -31,29 +37,29 @@ const RewardTile = React.createClass({ var RewardsPage = React.createClass({ componentWillMount: function() { - lbryio.call('reward_type', 'list', {}).then((rewardTypes) => { - this.setState({ - rewardTypes: rewardTypes, - }); - }); + this.loadRewards() }, getInitialState: function() { return { - rewardTypes: null, + userRewards: null, }; }, + loadRewards: function() { + lbryio.call('reward', 'list', {}).then((userRewards) => { + this.setState({ + userRewards: userRewards, + }); + }); + }, render: function() { return (
-
-

Rewards

- {!this.state.rewardTypes - ? null - : this.state.rewardTypes.map(({name, title, description, claimed, value}) => { - return ; - })} -
+ {!this.state.userRewards + ? null + : this.state.userRewards.map(({RewardType, RewardTitle, RewardDescription, TransactionID, RewardAmount}) => { + return ; + })}
); diff --git a/ui/js/page/settings.js b/ui/js/page/settings.js index 508a8a84d..c278741b4 100644 --- a/ui/js/page/settings.js +++ b/ui/js/page/settings.js @@ -1,21 +1,7 @@ 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({ onRunOnStartChange: function (event) { lbry.setDaemonSetting('run_on_startup', event.target.checked); @@ -81,29 +67,54 @@ var SettingsPage = React.createClass({ return (
-

Run on Startup

- -
-
-

Download Directory

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

Bandwidth Limits

-
-

Max Upload

- - +
+

Run on Startup

-
+
+ +
+
+
+
+

Download Directory

+
+
+ +
+
+
+
+

Bandwidth Limits

+
+
+

Max Upload

+ + + { this.state.isMaxUpload ? + : '' + } +
+

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

Content

+
+
+ +
+
+
-

Search

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

Share Diagnostic Data

- +
+
-
-

Share Diagnostic Data

- -
); } }); +/* + +
+

Search

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

Share Diagnostic Data

+ +
+ */ export default SettingsPage; diff --git a/ui/js/page/wallet.js b/ui/js/page/wallet.js index 2ace64c27..fd68345cb 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((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/rewards.js b/ui/js/rewards.js index 4310b18b8..29f43058c 100644 --- a/ui/js/rewards.js +++ b/ui/js/rewards.js @@ -1,11 +1,14 @@ import lbry from './lbry.js'; import lbryio from './lbryio.js'; -const MESSAGES = { - new_developer: "Your reward has been confirmed for registering as a new developer.", - confirm_email: "Your reward has been confirmed for verifying your email address.", - first_publish: "Your reward has been confirmed for making your first publication.", -}; +function rewardMessage(type, amount) { + return { + new_developer: "Your reward has been confirmed for registering as a new developer.", + new_user: `You received ${amount} LBC new user reward.`, + confirm_email: "Your reward has been confirmed for verifying your email address.", + first_publish: "Your reward has been confirmed for making your first publication.", + }[type]; +} const rewards = {}; @@ -13,22 +16,25 @@ rewards.claimReward = function(type) { return new Promise((resolve, reject) => { console.log('top of promise body') lbry.get_new_address().then((address) => { - console.log('top of get_new_address') const params = { reward_type: type, wallet_address: address, }; lbryio.call('reward', 'new', params, 'post').then(({RewardAmount}) => { - const result = { - type: type, - amount: RewardAmount, - message: MESSAGES[type], - }; + const + message = rewardMessage(type, RewardAmount), + result = { + type: type, + amount: RewardAmount, + message: message + }; // Display global notice document.dispatchEvent(new CustomEvent('globalNotice', { detail: { - message: MESSAGES[type], + message: message, + linkText: "Show All", + linkTarget: "?rewards", isError: false, }, })); @@ -36,16 +42,7 @@ rewards.claimReward = function(type) { // Add more events here to display other places resolve(result); - }, (error) => { - document.dispatchEvent(new CustomEvent('globalNotice', { - detail: { - message: `Failed to claim reward: ${error.message}`, - isError: true, - }, - })); - document.dispatchEvent(new CustomEvent('rewardFailed', error)); - reject(error); - }); + }, reject); }); }); } diff --git a/ui/js/utils.js b/ui/js/utils.js index 290a0f54e..e9472a6a4 100644 --- a/ui/js/utils.js +++ b/ui/js/utils.js @@ -12,4 +12,20 @@ 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) { + const itemRaw = sessionStorage.getItem(key); + return itemRaw === null ? undefined : JSON.parse(itemRaw); +} + +/** + * Thin wrapper around localStorage.setItem(). Converts value to JSON. + */ +export function setSession(key, value) { + sessionStorage.setItem(key, JSON.stringify(value)); } \ No newline at end of file diff --git a/ui/scss/_canvas.scss b/ui/scss/_canvas.scss index a5082d0d9..300ecb75b 100644 --- a/ui/scss/_canvas.scss +++ b/ui/scss/_canvas.scss @@ -60,6 +60,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 +105,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; + } } } @@ -159,26 +180,6 @@ nav.sub-header { padding: $spacing-vertical; } - h2 - { - margin-bottom: $spacing-vertical; - } - h3, h4 - { - margin-bottom: $spacing-vertical / 2; - margin-top: $spacing-vertical; - &:first-child - { - margin-top: 0; - } - } - .meta - { - + h2, + h3, + h4 - { - margin-top: 0; - } - } } $header-icon-size: 1.5em; @@ -197,48 +198,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/_form.scss b/ui/scss/_form.scss deleted file mode 100644 index c54343c4a..000000000 --- a/ui/scss/_form.scss +++ /dev/null @@ -1,66 +0,0 @@ -@import "global"; - -/* this probably shouldn't exist but it does so here we are - Jeremy */ - - -textarea, -select, -input[type="text"], -input[type="password"], -input[type="email"], -input[type="number"], -input[type="search"], -input[type="date"] { - @include placeholder { - color: lighten($color-text-dark, 60%); - } - transition: all $transition-standard; - cursor: pointer; - padding-left: 1px; - padding-right: 1px; - box-sizing: border-box; - -webkit-appearance: none; - &[readonly] { - background-color: #bbb; - } -} - -input[type="text"], -input[type="password"], -input[type="email"], -input[type="number"], -input[type="search"], -input[type="date"] { - border-bottom: 2px solid $color-form-border; - line-height: $spacing-vertical - 4; - height: $spacing-vertical * 1.5; - &.form-field__input--error { - border-color: $color-error; - } -} - -textarea:focus, -input[type="text"]:focus, -input[type="password"]:focus, -input[type="email"]:focus, -input[type="number"]:focus, -input[type="search"]:focus, -input[type="date"]:focus { - border-color: $color-primary; -} - -textarea { - border: 2px solid $color-form-border; -} - -.form-row -{ - + .form-row - { - margin-top: $spacing-vertical; - } - + .form-row-submit - { - margin-top: $spacing-vertical; - } -} \ No newline at end of file diff --git a/ui/scss/_global.scss b/ui/scss/_global.scss index 7d3249888..b203e7d18 100644 --- a/ui/scss/_global.scss +++ b/ui/scss/_global.scss @@ -6,8 +6,10 @@ $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: #8a6d3b; $color-error: #a94442; @@ -30,7 +32,7 @@ $height-header: $spacing-vertical * 2.5; $height-button: $spacing-vertical * 1.5; $default-box-shadow: 0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12); -//$focus-box-shadow: 2px 4px 4px 0 rgba(0,0,0,.14),2px 5px 3px -2px rgba(0,0,0,.2),2px 3px 7px 0 rgba(0,0,0,.12); +$focus-box-shadow: 2px 4px 4px 0 rgba(0,0,0,.14),2px 5px 3px -2px rgba(0,0,0,.2),2px 3px 7px 0 rgba(0,0,0,.12); $transition-standard: .225s ease; diff --git a/ui/scss/_gui.scss b/ui/scss/_gui.scss index 691ccb328..6c97b5fbc 100644 --- a/ui/scss/_gui.scss +++ b/ui/scss/_gui.scss @@ -38,6 +38,7 @@ text-align: center; } +/* section { margin-bottom: $spacing-vertical; @@ -46,10 +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; @@ -178,7 +179,8 @@ p background: rgb(255, 255, 255); overflow: auto; border-radius: 4px; - padding: 36px; + padding: $spacing-vertical; + box-shadow: $default-box-shadow; max-width: 250px; } diff --git a/ui/scss/_reset.scss b/ui/scss/_reset.scss index 8e0db2623..e951875a8 100644 --- a/ui/scss/_reset.scss +++ b/ui/scss/_reset.scss @@ -7,6 +7,10 @@ body, div, dl, dt, dd, ul, ol, li, h1, h2, h3, h4, h5, h6, pre, code, form, fiel { outline: 0; } +input::-webkit-search-cancel-button { + /* Remove default */ + -webkit-appearance: none; +} table { border-collapse: collapse; diff --git a/ui/scss/all.scss b/ui/scss/all.scss index 3c28c011c..18a9ec720 100644 --- a/ui/scss/all.scss +++ b/ui/scss/all.scss @@ -1,12 +1,12 @@ @import "_reset"; @import "_grid"; @import "_icons"; -@import "_form"; @import "_mediaelement"; @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"; @@ -16,7 +16,7 @@ @import "component/_channel-indicator.scss"; @import "component/_notice.scss"; @import "component/_modal-page.scss"; -@import "component/_notification-bar.scss"; +@import "component/_snack-bar.scss"; @import "page/_developer.scss"; @import "page/_watch.scss"; @import "page/_reward.scss"; diff --git a/ui/scss/component/_card.scss b/ui/scss/component/_card.scss new file mode 100644 index 000000000..af42624b2 --- /dev/null +++ b/ui/scss/component/_card.scss @@ -0,0 +1,122 @@ +@import "../global"; + +$padding-card-horizontal: $spacing-vertical * 2/3; + +.card { + margin-left: auto; + margin-right: auto; + max-width: 800px; + background: $color-bg; + box-shadow: $default-box-shadow; + border-radius: 2px; + margin-bottom: $spacing-vertical * 2/3; + overflow: auto; +} +.card--obscured +{ + position: relative; +} +.card--obscured .card__inner { + -webkit-filter: blur($blur-intensity); + -moz-filter: blur($blur-intensity); + -o-filter: blur($blur-intensity); + -ms-filter: blur($blur-intensity); + filter: blur($blur-intensity); +} +.card__title-primary { + padding: 0 $padding-card-horizontal; + margin-top: $spacing-vertical; +} +.card__title-identity { + padding: 0 $padding-card-horizontal; + margin-top: $spacing-vertical * 1/3; + margin-bottom: $spacing-vertical * 1/3; +} +.card__actions { + padding: 0 $padding-card-horizontal; +} +.card__actions { + margin-top: $spacing-vertical * 2/3; +} +.card__actions--bottom { + margin-top: $spacing-vertical * 1/3; + margin-bottom: $spacing-vertical * 1/3; +} +.card__actions--form-submit { + margin-top: $spacing-vertical; + margin-bottom: $spacing-vertical * 2/3; +} +.card__content { + margin-top: $spacing-vertical * 2/3; + margin-bottom: $spacing-vertical * 2/3; + padding: 0 $padding-card-horizontal; +} +.card__subtext { + color: #444; + margin-top: 12px; + font-size: 0.9em; + margin-top: $spacing-vertical * 2/3; + margin-bottom: $spacing-vertical * 2/3; + padding: 0 $padding-card-horizontal; +} +.card__subtext--two-lines { + height: $font-size * 0.9 * $font-line-height * 2; +} +.card-overlay { + position: absolute; + left: 0px; + right: 0px; + top: 0px; + bottom: 0px; + padding: 20px; + background-color: rgba(128, 128, 128, 0.8); + color: #fff; + display: flex; + align-items: center; + font-weight: 600; +} + +.card__media img { + max-width: 100%;; + display: block; + margin-left: auto; + margin-right: auto; +} + +$width-card-small: $spacing-vertical * 12; +.card--small { + width: $width-card-small; +} +.card--small .card__media { + max-height: $width-card-small * 9 / 16; + img { + max-height: $width-card-small * 9 / 16; + } +} + +.card__subtitle { + color: $color-meta-light; + font-size: 0.85em; +} + +.card-series-submit +{ + margin-left: auto; + margin-right: auto; + max-width: 800px; + padding: $spacing-vertical / 2; +} + +.card-row { + > .card { + vertical-align: top; + display: inline-block; + margin-right: $spacing-vertical / 3; + } + + .card-row { + margin-top: $spacing-vertical * 1/3; + } +} +.card-row__header { + margin-bottom: $spacing-vertical / 3; +} \ No newline at end of file diff --git a/ui/scss/component/_file-tile.scss b/ui/scss/component/_file-tile.scss index a5c73a175..1f4b463b8 100644 --- a/ui/scss/component/_file-tile.scss +++ b/ui/scss/component/_file-tile.scss @@ -2,10 +2,10 @@ .file-tile__row { height: $spacing-vertical * 7; -} -.file-tile__row--unavailable { - opacity: 0.5; + .file-price { + float: right; + } } .file-tile__thumbnail { @@ -20,10 +20,6 @@ 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 index db2d168e0..3f22a9af2 100644 --- a/ui/scss/component/_form-field.scss +++ b/ui/scss/component/_form-field.scss @@ -1,14 +1,88 @@ @import "../global"; -.form-field { - display: inline-block; +.form-row-submit +{ + margin-top: $spacing-vertical; } -.form-field__label { +$height-input: $spacing-vertical * 1.5; + +.form-row__label-row { margin-top: $spacing-vertical * 2/3; margin-bottom: $spacing-vertical * 1/3; line-height: 1; } +.form-row__label-row--prefix { + float: left; + margin-right: 5px; + line-height: $height-input; +} + +.form-field { + display: inline-block; + + input[type="checkbox"], + input[type="radio"] { + cursor: pointer; + } + + textarea, + select, + input[type="text"], + input[type="password"], + input[type="email"], + input[type="number"], + input[type="search"], + input[type="date"] { + @include placeholder { + color: lighten($color-text-dark, 60%); + } + transition: all $transition-standard; + cursor: pointer; + padding-left: 1px; + padding-right: 1px; + box-sizing: border-box; + -webkit-appearance: none; + &[readonly] { + background-color: #bbb; + } + } + + input[type="text"], + input[type="password"], + input[type="email"], + input[type="number"], + input[type="search"], + input[type="date"] { + border-bottom: 2px solid $color-form-border; + line-height: $spacing-vertical - 4; + height: $height-input; + &.form-field__input--error { + border-color: $color-error; + } + } + + textarea:focus, + input[type="text"]:focus, + input[type="password"]:focus, + input[type="email"]:focus, + input[type="number"]:focus, + input[type="search"]:focus, + input[type="date"]:focus { + border-color: $color-primary; + } + + textarea { + border: 2px solid $color-form-border; + } +} + +.form-field__label { + &[for] { cursor: pointer; } + > input[type="checkbox"], input[type="radio"] { + margin-right: 6px; + } +} .form-field__label--error { color: $color-error; @@ -18,8 +92,8 @@ width: 330px; } -.form-field__input-text-number { - width: 50px; +.form-field__input-number { + width: 100px; } .form-field__error, .form-field__helper { diff --git a/ui/scss/component/_modal-page.scss b/ui/scss/component/_modal-page.scss index 5c56e1ad3..2b86c5cad 100644 --- a/ui/scss/component/_modal-page.scss +++ b/ui/scss/component/_modal-page.scss @@ -19,14 +19,16 @@ 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; - - border: 1px solid rgb(204, 204, 204); - background: rgb(255, 255, 255); - overflow: auto; } /* diff --git a/ui/scss/component/_notification-bar.scss b/ui/scss/component/_notification-bar.scss deleted file mode 100644 index 2f9959f94..000000000 --- a/ui/scss/component/_notification-bar.scss +++ /dev/null @@ -1,6 +0,0 @@ -@import "../global"; - -.notification-bar { - margin-top: 5px; - margin-right: 10px; -} 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; + } +} -- 2.45.2 From cbb3da2795d3d304398fd0d924417ee212a50459 Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Mon, 10 Apr 2017 10:09:48 -0400 Subject: [PATCH 055/158] rebase fix 1 of n --- ui/js/page/discover.js | 2 +- ui/js/page/wallet.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/js/page/discover.js b/ui/js/page/discover.js index 7751db2db..793362f38 100644 --- a/ui/js/page/discover.js +++ b/ui/js/page/discover.js @@ -72,7 +72,7 @@ var FeaturedCategory = React.createClass({ : '' } : '' } - { this.props.names.map((name) => { return }) } + { this.props.names.map((name) => { return }) }
) } }) diff --git a/ui/js/page/wallet.js b/ui/js/page/wallet.js index fd68345cb..257308c21 100644 --- a/ui/js/page/wallet.js +++ b/ui/js/page/wallet.js @@ -24,12 +24,12 @@ var AddressSection = React.createClass({ event.preventDefault(); } - lbry.wallet_new_address().then((address) => { + lbry.wallet_new_address().then(function(address) { window.localStorage.setItem('wallet_address', address); this.setState({ address: address, }); - }.bind(this)); + }.bind(this)) }, getInitialState: function() { -- 2.45.2 From 029b0b9c3acc5a68c28d5cb36e1de9040a5693f5 Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Mon, 10 Apr 2017 14:12:07 -0400 Subject: [PATCH 056/158] more --- ui/js/component/auth.js | 62 +++---- ui/js/component/file-actions.js | 2 +- ui/js/component/file-tile.js | 51 +++++- ui/js/component/form.js | 5 +- ui/js/lbryio.js | 3 - ui/js/page/publish.js | 283 +++++++++++++++-------------- ui/js/page/settings.js | 19 +- ui/js/page/wallet.js | 2 +- ui/scss/component/_card.scss | 3 +- ui/scss/component/_form-field.scss | 35 +++- 10 files changed, 265 insertions(+), 200 deletions(-) diff --git a/ui/js/component/auth.js b/ui/js/component/auth.js index 83edd2c9f..bf3fb4232 100644 --- a/ui/js/component/auth.js +++ b/ui/js/component/auth.js @@ -157,7 +157,7 @@ export const AuthOverlay = React.createClass({ getInitialState: function() { return { - stage: "welcome", + stage: "pending", stageProps: {} }; }, @@ -167,36 +167,36 @@ export const AuthOverlay = React.createClass({ }); }, 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 { - // this.endAuth() - // } - // }.bind(this)).catch((err) => { - // this.setState({ - // stage: "error", - // stageProps: { errorText: err.message } - // }) - // document.dispatchEvent(new CustomEvent('unhandledError', { - // detail: { - // message: err.message, - // data: err.stack - // } - // })); - // }) + 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 { + this.endAuth() + } + }.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 || lbryio.user && lbryio.user.HasVerifiedEmail) { diff --git a/ui/js/component/file-actions.js b/ui/js/component/file-actions.js index 715133bd8..410d14d66 100644 --- a/ui/js/component/file-actions.js +++ b/ui/js/component/file-actions.js @@ -294,7 +294,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 bb9483374..463269c05 100644 --- a/ui/js/component/file-tile.js +++ b/ui/js/component/file-tile.js @@ -140,6 +140,7 @@ export let FileTileStream = React.createClass({ { !this.props.hidePrice ? : null} +<<<<<<< dd3f3ec4d00066633b136925111bae7193b3c6a8

@@ -159,9 +160,25 @@ export let FileTileStream = React.createClass({ {isConfirmed ? metadata.description : This file is pending confirmation.} +======= + +

+ + + {title} +>>>>>>> more -

-

+ + + + +

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

{this.state.showNsfwHelp @@ -180,9 +197,12 @@ export let FileTileStream = React.createClass({ export let FileCardStream = React.createClass({ _fileInfoSubscribeId: null, _isMounted: null, + _metadata: null, + propTypes: { - metadata: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.object]), + uri: React.PropTypes.string, + claimInfo: React.PropTypes.object, outpoint: React.PropTypes.string, hideOnRemove: React.PropTypes.bool, hidePrice: React.PropTypes.bool, @@ -207,6 +227,11 @@ export let FileCardStream = React.createClass({ this._fileInfoSubscribeId = lbry.fileInfoSubscribe(this.props.outpoint, this.onFileInfoUpdate); } }, + componentWillMount: function() { + const {value: {stream: {metadata, source: {contentType}}}} = this.props.claimInfo; + this._metadata = metadata; + this._contentType = contentType; + }, componentWillUnmount: function() { if (this._fileInfoSubscribeId) { lbry.fileInfoUnsubscribe(this.props.outpoint, this._fileInfoSubscribeId); @@ -220,7 +245,7 @@ export let FileCardStream = React.createClass({ } }, handleMouseOver: function() { - if (this.props.obscureNsfw && this.props.metadata && this.props.metadata.nsfw) { + if (this.props.obscureNsfw && this.props.metadata && this._metadata.nsfw) { this.setState({ showNsfwHelp: true, }); @@ -238,11 +263,16 @@ export let FileCardStream = React.createClass({ return null; } +<<<<<<< dd3f3ec4d00066633b136925111bae7193b3c6a8 const lbryUri = uri.normalizeLbryUri(this.props.uri); const metadata = this.props.metadata; +======= + const metadata = this._metadata; +>>>>>>> more const isConfirmed = typeof metadata == 'object'; const title = isConfirmed ? metadata.title : lbryUri; const obscureNsfw = this.props.obscureNsfw && isConfirmed && metadata.nsfw; + console.log(this.props); return (
@@ -254,10 +284,15 @@ export let FileCardStream = React.createClass({ +<<<<<<< dd3f3ec4d00066633b136925111bae7193b3c6a8
+======= + +
+>>>>>>> more
@@ -325,7 +360,11 @@ export let FileTile = React.createClass({ const {txid, nout, has_signature, signature_is_valid, value: {stream: {metadata, source: {contentType}}}} = this.state.claimInfo; - return ; + return + return this.props.displayStyle == 'card' ? + : + ; } }); diff --git a/ui/js/component/form.js b/ui/js/component/form.js index b33fcddd5..ddc4ee350 100644 --- a/ui/js/component/form.js +++ b/ui/js/component/form.js @@ -51,7 +51,7 @@ export let 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; } else { return this.refs.field.value; } @@ -121,6 +121,9 @@ export let FormRow = React.createClass({ getValue: function() { return this.refs.field.getValue(); }, + getSelectedElement: function() { + return this.refs.field.getSelectedElement(); + }, render: function() { const fieldProps = Object.assign({}, this.props), elementId = formFieldId(), diff --git a/ui/js/lbryio.js b/ui/js/lbryio.js index 9c6d37f03..06dbd46d9 100644 --- a/ui/js/lbryio.js +++ b/ui/js/lbryio.js @@ -62,7 +62,6 @@ lbryio.call = function(resource, action, params={}, method='get') { })); } } else { - console.info(`${resource}.${action} response data:`, response); resolve(response.data); } }); @@ -75,11 +74,9 @@ lbryio.call = function(resource, action, params={}, method='get') { const fullParams = {app_id: lbryio._accessToken, ...params}; if (method == 'get') { - console.info('GET ', CONNECTION_STRING + resource + '/' + action, ' | params:', fullParams); xhr.open('get', CONNECTION_STRING + resource + '/' + action + '?' + querystring.stringify(fullParams), true); xhr.send(); } else if (method == 'post') { - console.info('POST ', CONNECTION_STRING + resource + '/' + action, '| params: ', fullParams); xhr.open('post', CONNECTION_STRING + resource + '/' + action, true); xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); xhr.send(querystring.stringify(fullParams)); diff --git a/ui/js/page/publish.js b/ui/js/page/publish.js index 4f28dc567..d1d52894b 100644 --- a/ui/js/page/publish.js +++ b/ui/js/page/publish.js @@ -6,7 +6,7 @@ import {Link} from '../component/link.js'; import Modal from '../component/modal.js'; var PublishPage = React.createClass({ - _requiredFields: ['name', 'bid', 'meta_title', 'meta_author', 'meta_license', 'meta_description'], + _requiredFields: ['name', 'bid', 'meta_title'], _updateChannelList: function(channel) { // Calls API to update displayed list of channels. If a channel name is provided, will select @@ -85,7 +85,9 @@ var PublishPage = React.createClass({ publishArgs.file_path = this.refs.file.getValue(); } + console.log(publishArgs); lbry.publish(publishArgs, (message) => { + console.log(message); this.handlePublishStarted(); }, null, (error) => { this.handlePublishError(error); @@ -112,6 +114,7 @@ var PublishPage = React.createClass({ rawName: '', name: '', bid: '', + hasFile: false, feeAmount: '', feeCurrency: 'USD', channel: 'anonymous', @@ -237,7 +240,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 +248,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,7 +279,7 @@ 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; } @@ -292,7 +294,7 @@ var PublishPage = React.createClass({ }, 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 +313,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,7 +336,27 @@ 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) { + 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 perminent URL for any amount. + } else { + return ''; + } + }, render: function() { if (this.state.channels === null) { return null; @@ -345,153 +367,136 @@ var PublishPage = React.createClass({
-

LBRY Name

+

Content

+
+ What are you publishing? +
What LBRY name would you like to claim for this file? .
)} /> - { - (!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'}.))) - } + +
+ { !this.state.hasFile ? '' : +
+ + + + + + + + + + + + + + + + + +
} +
+ +
+
+

Access

+
+ How much does this content cost ? +
+
+
+
+ +
+ { this.handleFeePrefChange(false) } } checked={!this.state.isFee} /> + { this.handleFeePrefChange(true) } } checked={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?
+ +
+ {this.state.channel == 'new' ? +
+ { this.refs.newChannelName = newChannelName }} + value={this.state.newChannelName} /> + +
+ +
+
+ : null}
+
-

Choose File

+
+

Address

+
Where should this content permanently reside?
+
- - { 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 - ?
- -
- : null} - {this.state.otherLicenseChosen - ?
- -
- : null} - {this.state.otherLicenseChosen - ?
- -
- : null} - -
- - - - - - - - - -
-
- -
-
- -
-
- - - -
-

Additional Content Information (Optional)

-
- + Select a URL for this publish. .
)} />
+ { this.state.rawName ? +
+ +
: '' }
diff --git a/ui/js/page/settings.js b/ui/js/page/settings.js index c278741b4..d6df42cfb 100644 --- a/ui/js/page/settings.js +++ b/ui/js/page/settings.js @@ -95,12 +95,12 @@ var SettingsPage = React.createClass({

Max Upload

- -

Max Download

- + + { /* + */ }

Content

-
+

Share Diagnostic Data

-
+
Send Credits
- +
diff --git a/ui/scss/component/_card.scss b/ui/scss/component/_card.scss index af42624b2..c2c9c2a9f 100644 --- a/ui/scss/component/_card.scss +++ b/ui/scss/component/_card.scss @@ -95,8 +95,9 @@ $width-card-small: $spacing-vertical * 12; } .card__subtitle { - color: $color-meta-light; + color: $color-help; font-size: 0.85em; + line-height: $font-line-height * 1 / 0.85; } .card-series-submit diff --git a/ui/scss/component/_form-field.scss b/ui/scss/component/_form-field.scss index 3f22a9af2..14744326b 100644 --- a/ui/scss/component/_form-field.scss +++ b/ui/scss/component/_form-field.scss @@ -1,21 +1,21 @@ @import "../global"; +$width-input-border: 2px; + .form-row-submit { margin-top: $spacing-vertical; } -$height-input: $spacing-vertical * 1.5; - .form-row__label-row { margin-top: $spacing-vertical * 2/3; margin-bottom: $spacing-vertical * 1/3; line-height: 1; + font-size: 0.9em; } .form-row__label-row--prefix { float: left; margin-right: 5px; - line-height: $height-input; } .form-field { @@ -26,8 +26,19 @@ $height-input: $spacing-vertical * 1.5; 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, - select, input[type="text"], input[type="password"], input[type="email"], @@ -54,9 +65,10 @@ $height-input: $spacing-vertical * 1.5; input[type="number"], input[type="search"], input[type="date"] { - border-bottom: 2px solid $color-form-border; - line-height: $spacing-vertical - 4; - height: $height-input; + 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; } @@ -73,7 +85,7 @@ $height-input: $spacing-vertical * 1.5; } textarea { - border: 2px solid $color-form-border; + border: $width-input-border solid $color-form-border; } } @@ -93,7 +105,12 @@ $height-input: $spacing-vertical * 1.5; } .form-field__input-number { - width: 100px; + width: 70px; + text-align: right; +} + +.form-field__input-textarea { + width: 330px; } .form-field__error, .form-field__helper { -- 2.45.2 From 0ba69ff32b4f6355c4c036338c27d583a8e1435c Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Tue, 11 Apr 2017 12:03:56 -0400 Subject: [PATCH 057/158] stashed cleanup --- ui/js/page/help.js | 98 ++++++++++++++++++++++++------------------- ui/js/page/publish.js | 5 +-- 2 files changed, 57 insertions(+), 46 deletions(-) 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 d1d52894b..1b41aa3b5 100644 --- a/ui/js/page/publish.js +++ b/ui/js/page/publish.js @@ -61,7 +61,7 @@ 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', 'nsfw']) { var value = this.refs['meta_' + metaField].getValue(); if (value !== '') { metadata[metaField] = value; @@ -113,7 +113,7 @@ var PublishPage = React.createClass({ channels: null, rawName: '', name: '', - bid: '', + bid: 0.01, hasFile: false, feeAmount: '', feeCurrency: 'USD', @@ -491,7 +491,6 @@ var PublishPage = React.createClass({ type="number" step="0.01" label="Deposit" - defaultValue="0.01" onChange={this.handleBidChange} value={this.state.bid} placeholder={this.state.nameResolved ? this.state.topClaimValue + 10 : 100} -- 2.45.2 From 0313ba941addea7eac0aa22246934ad764060330 Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Tue, 11 Apr 2017 22:01:45 -0400 Subject: [PATCH 058/158] approaching acceptable --- ui/js/app.js | 9 +- ui/js/component/channel-indicator.js | 19 ++- ui/js/component/common.js | 13 +- ui/js/component/file-tile.js | 139 +++++++--------------- ui/js/lbry.js | 7 +- ui/js/page/discover.js | 21 ++-- ui/js/page/publish.js | 2 - ui/js/page/show.js | 5 +- ui/scss/_canvas.scss | 21 +++- ui/scss/_gui.scss | 7 -- ui/scss/component/_card.scss | 37 ++++-- ui/scss/component/_channel-indicator.scss | 2 +- ui/scss/component/_file-tile.scss | 2 +- 13 files changed, 126 insertions(+), 158 deletions(-) diff --git a/ui/js/app.js b/ui/js/app.js index 424fb8533..e18fdf25e 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -106,6 +106,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'))); } } @@ -147,6 +149,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); @@ -261,7 +268,7 @@ var App = React.createClass({ case 'start': return ; case 'rewards': - return ; + return ; case 'wallet': case 'send': case 'receive': 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..98dbd45eb 100644 --- a/ui/js/component/common.js +++ b/ui/js/component/common.js @@ -54,15 +54,6 @@ 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; } }); @@ -76,8 +67,8 @@ export let CreditAmount = React.createClass({ var formattedAmount = lbry.formatCredits(this.props.amount, this.props.precision ? this.props.precision : 1); return ( - {formattedAmount} {parseFloat(formattedAmount) == 1.0 ? 'credit' : 'credits'} - { this.props.isEstimate ? (est) : null } + {formattedAmount} {parseFloat(formattedAmount) == 1.0 ? 'credit' : 'credits'} + { this.props.isEstimate ? * : null } ); } diff --git a/ui/js/component/file-tile.js b/ui/js/component/file-tile.js index 463269c05..199f341bd 100644 --- a/ui/js/component/file-tile.js +++ b/ui/js/component/file-tile.js @@ -4,7 +4,7 @@ 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'; +import UriIndicator from '../component/channel-indicator.js'; let FilePrice = React.createClass({ _isMounted: false, @@ -41,15 +41,10 @@ let FilePrice = React.createClass({ }, render: function() { - if (this.state.cost === null) - { - return null; - } - return ( - - - + this.state.cost !== null ? + : + ... ); } }); @@ -117,14 +112,10 @@ 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'; @@ -140,7 +131,6 @@ export let FileTileStream = React.createClass({ { !this.props.hidePrice ? : null} -<<<<<<< dd3f3ec4d00066633b136925111bae7193b3c6a8

@@ -158,27 +148,11 @@ export let FileTileStream = React.createClass({

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

-

- - - {title} ->>>>>>> more + ? metadata.description + : This file is pending confirmation.} - -

- - -

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

+

+
{this.state.showNsfwHelp @@ -218,7 +192,8 @@ export let FileCardStream = React.createClass({ getDefaultProps: function() { return { obscureNsfw: !lbry.getClientSetting('showNsfw'), - hidePrice: false + hidePrice: false, + hasSignature: false, } }, componentDidMount: function() { @@ -227,11 +202,6 @@ export let FileCardStream = React.createClass({ this._fileInfoSubscribeId = lbry.fileInfoSubscribe(this.props.outpoint, this.onFileInfoUpdate); } }, - componentWillMount: function() { - const {value: {stream: {metadata, source: {contentType}}}} = this.props.claimInfo; - this._metadata = metadata; - this._contentType = contentType; - }, componentWillUnmount: function() { if (this._fileInfoSubscribeId) { lbry.fileInfoUnsubscribe(this.props.outpoint, this._fileInfoSubscribeId); @@ -245,79 +215,56 @@ export let FileCardStream = React.createClass({ } }, handleMouseOver: function() { - if (this.props.obscureNsfw && this.props.metadata && this._metadata.nsfw) { - this.setState({ - showNsfwHelp: true, - }); - } + this.setState({ + hovered: true, + }); }, handleMouseOut: function() { - if (this.state.showNsfwHelp) { - this.setState({ - showNsfwHelp: false, - }); - } + this.setState({ + hovered: false, + }); }, render: function() { if (this.state.isHidden) { return null; } -<<<<<<< dd3f3ec4d00066633b136925111bae7193b3c6a8 const lbryUri = uri.normalizeLbryUri(this.props.uri); const metadata = this.props.metadata; -======= - const metadata = this._metadata; ->>>>>>> more const isConfirmed = typeof metadata == 'object'; const title = isConfirmed ? metadata.title : lbryUri; const obscureNsfw = this.props.obscureNsfw && isConfirmed && metadata.nsfw; - console.log(this.props); + const primaryUrl = '?watch=' + lbryUri; return ( -
+
->>>>>>> more -
- -
- { !this.props.hidePrice - ? +
+ + {this.state.showNsfwHelp && this.state.hovered + ?
+

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

+
: null} -
- - {isConfirmed - ? metadata.description - : This file is pending confirmation.} - -
-
- -
- {this.state.showNsfwHelp - ?
-

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

-
- : null}
); } @@ -360,7 +307,7 @@ 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/lbry.js b/ui/js/lbry.js index 8fbfdb04d..51abc311b 100644 --- a/ui/js/lbry.js +++ b/ui/js/lbry.js @@ -250,10 +250,6 @@ lbry.getCostInfo = function(lbryUri, callback, errorCallback) { * - includes_data: Boolean; indicates whether or not the data fee info * from Lighthouse is included. */ - if (!name) { - throw new Error(`Name required.`); - } - function getCostWithData(name, size, callback, errorCallback) { lbry.stream_cost_estimate({name, size}).then((cost) => { callback({ @@ -507,7 +503,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 = []; @@ -519,6 +515,7 @@ lbry._updateClaimOwnershipCache = function(claimId) { return match || claimInfo.claim_id == claimId; }); }); + }; lbry._updateFileInfoSubscribers = function(outpoint) { diff --git a/ui/js/page/discover.js b/ui/js/page/discover.js index 793362f38..b3d427cfe 100644 --- a/ui/js/page/discover.js +++ b/ui/js/page/discover.js @@ -65,7 +65,7 @@ const communityCategoryToolTipText = ('Community Content is a public space where var FeaturedCategory = React.createClass({ render: function() { - return (
+ return (
{ this.props.category ?

{this.props.category} { this.props.category == "community" ? @@ -80,21 +80,28 @@ var FeaturedCategory = React.createClass({ var FeaturedContent = React.createClass({ getInitialState: function() { return { - featuredNames: {}, + featuredUris: {}, }; }, componentWillMount: function() { - lbryio.call('discover', 'list', { version: "early-access" } ).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 }); }); }, render: function() { - console.log(this.state.featuredNames); return (
{ - Object.keys(this.state.featuredNames).map(function(category) { - return + Object.keys(this.state.featuredUris).map(function(category) { + return this.state.featuredUris[category].length ? + : + ''; }.bind(this)) }
diff --git a/ui/js/page/publish.js b/ui/js/page/publish.js index 1b41aa3b5..a220cce5e 100644 --- a/ui/js/page/publish.js +++ b/ui/js/page/publish.js @@ -373,8 +373,6 @@ var PublishPage = React.createClass({

- What LBRY name would you like to claim for this file? .
)} />
diff --git a/ui/js/page/show.js b/ui/js/page/show.js index 49aff7569..7197390d2 100644 --- a/ui/js/page/show.js +++ b/ui/js/page/show.js @@ -88,8 +88,9 @@ var FormatsSection = React.createClass({ return (
+ { this.props.metadata.thumbnail ?
: '' } +

{this.props.metadata.title}

{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){ */} @@ -141,7 +142,6 @@ var ShowPage = React.createClass({ return (
-
{this.state.uriLookupComplete ? ( ) : ( @@ -150,7 +150,6 @@ var ShowPage = React.createClass({ There is no content available at {this._uri}. If you reached this page from a link within the LBRY interface, please . Thanks!
)} - ); } }); diff --git a/ui/scss/_canvas.scss b/ui/scss/_canvas.scss index 300ecb75b..ac5712240 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,19 @@ $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; +} #drawer-handle { padding: $spacing-vertical / 2; diff --git a/ui/scss/_gui.scss b/ui/scss/_gui.scss index 6c97b5fbc..f1cf53c40 100644 --- a/ui/scss/_gui.scss +++ b/ui/scss/_gui.scss @@ -52,13 +52,6 @@ section } */ -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; } diff --git a/ui/scss/component/_card.scss b/ui/scss/component/_card.scss index c2c9c2a9f..608b49007 100644 --- a/ui/scss/component/_card.scss +++ b/ui/scss/component/_card.scss @@ -76,22 +76,36 @@ $padding-card-horizontal: $spacing-vertical * 2/3; font-weight: 600; } -.card__media img { - max-width: 100%;; +$card-link-scaling: 1.1; +.card__link { display: block; - margin-left: auto; - margin-right: auto; +} +.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 { - max-height: $width-card-small * 9 / 16; - img { - max-height: $width-card-small * 9 / 16; - } + height: $width-card-small * 9 / 16; } .card__subtitle { @@ -118,6 +132,13 @@ $width-card-small: $spacing-vertical * 12; 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 1f4b463b8..a3dc30d28 100644 --- a/ui/scss/component/_file-tile.scss +++ b/ui/scss/component/_file-tile.scss @@ -3,7 +3,7 @@ .file-tile__row { height: $spacing-vertical * 7; - .file-price { + .credit-amount { float: right; } } -- 2.45.2 From cb4af24cd748b10843388a5e5127cddebb795ae2 Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Wed, 12 Apr 2017 10:55:19 -0400 Subject: [PATCH 059/158] more clean up and fixes --- ui/js/app.js | 3 - ui/js/component/form.js | 44 ++++++----- ui/js/page/email.js | 36 --------- ui/js/page/publish.js | 79 ++++++++++++++------ ui/js/page/settings.js | 116 ++++++++++++++++------------- ui/js/page/wallet.js | 2 +- ui/scss/component/_card.scss | 2 +- ui/scss/component/_file-tile.scss | 7 +- ui/scss/component/_form-field.scss | 20 ++++- 9 files changed, 169 insertions(+), 140 deletions(-) delete mode 100644 ui/js/page/email.js diff --git a/ui/js/app.js b/ui/js/app.js index e18fdf25e..6126901e7 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -2,7 +2,6 @@ import React from 'react'; import {Line} from 'rc-progress'; import lbry from './lbry.js'; -import EmailPage from './page/email.js'; import SettingsPage from './page/settings.js'; import HelpPage from './page/help.js'; import WatchPage from './page/watch.js'; @@ -277,8 +276,6 @@ var App = React.createClass({ return ; case 'publish': return ; - case 'email': - return ; case 'developer': return ; case 'discover': diff --git a/ui/js/component/form.js b/ui/js/component/form.js index ddc4ee350..c24bd7b92 100644 --- a/ui/js/component/form.js +++ b/ui/js/component/form.js @@ -15,6 +15,8 @@ export let FormField = React.createClass({ propTypes: { type: React.PropTypes.string.isRequired, + prefix: React.PropTypes.string, + postfix: React.PropTypes.string, hasError: React.PropTypes.bool }, getInitialState: function() { @@ -41,9 +43,6 @@ export let FormField = React.createClass({ errorMessage: text, }); }, - showRequiredError: function() { - this.showError(this._fieldRequiredText); - }, focus: function() { this.refs.field.focus(); }, @@ -51,7 +50,7 @@ export let FormField = React.createClass({ if (this.props.type == 'checkbox') { return this.refs.field.checked; } else if (this.props.type == 'file') { - return this.refs.field.files.length && this.refs.field.files[0].path; + return !!(this.refs.field.files.length && this.refs.field.files[0].path); } else { return this.refs.field.value; } @@ -69,6 +68,9 @@ export let FormField = React.createClass({ delete otherProps.type; delete otherProps.label; delete otherProps.hasError; + delete otherProps.className; + delete otherProps.postfix; + delete otherProps.prefix; const element = ; return
+ { this.props.prefix ? {this.props.prefix} : '' } { renderElementInsideLabel ? : element } - { isError ?
{this.state.errorMessage}
: '' } + : + element } + { this.props.postfix ? {this.props.postfix} : '' } + { isError && this.state.errorMessage ?
{this.state.errorMessage}
: '' }
- return ( - this.props.row ? -
{field}
: - field - ); } }) export let FormRow = React.createClass({ + _fieldRequiredText: 'This field is required', propTypes: { label: React.PropTypes.string, // helper: React.PropTypes.html, }, - getValue: function() { - if (this.props.type == 'checkbox') { - return this.refs.field.checked; - } else if (this.props.type == 'file') { - return this.refs.field.files[0].path; - } else { - return this.refs.field.value; - } - }, getInitialState: function() { return { isError: false, @@ -118,12 +110,24 @@ export let FormRow = React.createClass({ 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() { const fieldProps = Object.assign({}, this.props), elementId = formFieldId(), diff --git a/ui/js/page/email.js b/ui/js/page/email.js deleted file mode 100644 index 76b031737..000000000 --- a/ui/js/page/email.js +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; -import lbryio from '../lbryio.js'; -import {getLocal, setLocal} from '../utils.js'; -import {FormField} from '../component/form.js' -import {Link} from '../component/link.js' -import rewards from '../rewards.js'; - -const EmailPage = React.createClass({ - handleSubmit: function(event) { - if (event !== undefined) { - event.preventDefault(); - } - - if (!this.state.email) { - this._emailField.showRequiredError(); - } - }, - componentWillMount: function() { - this._getRewardType(); - }, - render: function() { - return ( -
-
-

Verify your Email Address

- -
-
- -
-
- ); - } -}); - -export default EmailPage; diff --git a/ui/js/page/publish.js b/ui/js/page/publish.js index a220cce5e..7ec3ebd0c 100644 --- a/ui/js/page/publish.js +++ b/ui/js/page/publish.js @@ -6,7 +6,7 @@ import {Link} from '../component/link.js'; import Modal from '../component/modal.js'; var PublishPage = React.createClass({ - _requiredFields: ['name', 'bid', 'meta_title'], + _requiredFields: ['meta_title', 'name', 'bid'], _updateChannelList: function(channel) { // Calls API to update displayed list of channels. If a channel name is provided, will select @@ -27,19 +27,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.showRequiredError(); - 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(); } } } @@ -281,6 +285,8 @@ var PublishPage = React.createClass({ if (newChannelName.length > 1 && !lbry.nameIsValid(newChannelName.substr(1), false)) { this.refs.newChannelName.showError('LBRY channel names must contain only letters, numbers and dashes.'); return; + } else { + this.refs.newChannelName.clearError() } this.setState({ @@ -351,17 +357,24 @@ var PublishPage = React.createClass({ } 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'} + 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 perminent 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 (
@@ -377,10 +390,17 @@ var PublishPage = React.createClass({ helper={this.state.myClaimExists ? "If you don't choose a file, the file from your existing claim will be used." : null}/> { !this.state.hasFile ? '' : +
+
+ +
- +
+
+
+
@@ -390,12 +410,15 @@ var PublishPage = React.createClass({ +
+
-
} +
+ }
@@ -409,27 +432,28 @@ var PublishPage = React.createClass({
- { this.handleFeePrefChange(false) } } checked={!this.state.isFee} /> + { this.handleFeePrefChange(false) } } defaultChecked={!this.state.isFee} /> { this.handleFeePrefChange(true) } } checked={this.state.isFee} /> + onChange={ () => { 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.
: '' } + + - @@ -465,7 +489,13 @@ var PublishPage = React.createClass({
{ this.refs.newChannelName = newChannelName }} value={this.state.newChannelName} /> - +
@@ -477,11 +507,11 @@ var PublishPage = React.createClass({

Address

-
Where should this content permanently reside?
+
Where should this content permanently reside? .
- Select a URL for this publish. .
)} /> +
{ this.state.rawName ?
@@ -489,10 +519,11 @@ var PublishPage = React.createClass({ type="number" step="0.01" label="Deposit" + postfix="LBC" onChange={this.handleBidChange} - value={this.state.bid} + value={this.state.bid ? this.state.bid : '1'} placeholder={this.state.nameResolved ? this.state.topClaimValue + 10 : 100} - helper={this.getNameBidHelpText()} /> + helper={lbcInputHelp} />
: '' }
diff --git a/ui/js/page/settings.js b/ui/js/page/settings.js index d6df42cfb..318711d8b 100644 --- a/ui/js/page/settings.js +++ b/ui/js/page/settings.js @@ -3,36 +3,51 @@ import {FormField, FormRow} from '../component/form.js'; import lbry from '../lbry.js'; 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 { @@ -57,7 +72,7 @@ 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) { @@ -94,38 +109,60 @@ var SettingsPage = React.createClass({

Bandwidth Limits

-

Max Upload

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

Max Download

-
Max Download
+ - { /* - */ } +
+ + { this.state.isMaxDownload ? + + : '' + + } + { this.state.isMaxDownload ? MB/s : '' } +
@@ -160,25 +197,4 @@ var SettingsPage = React.createClass({ } }); -/* - -
-

Search

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

Share Diagnostic Data

- -
- */ - export default SettingsPage; diff --git a/ui/js/page/wallet.js b/ui/js/page/wallet.js index e8cb8abb0..8540e5469 100644 --- a/ui/js/page/wallet.js +++ b/ui/js/page/wallet.js @@ -152,7 +152,7 @@ var SendToAddressSection = React.createClass({

Send Credits

- +
diff --git a/ui/scss/component/_card.scss b/ui/scss/component/_card.scss index 608b49007..163de32d4 100644 --- a/ui/scss/component/_card.scss +++ b/ui/scss/component/_card.scss @@ -137,7 +137,7 @@ $height-card-small: $spacing-vertical * 15; overflow-y: hidden; white-space: nowrap; padding-left: 20px; - margin-left: -20px; /*hacky way to give space for hover */ + margin-left: -20px; /*hacky way to give space for hover */ } .card-row__header { margin-bottom: $spacing-vertical / 3; diff --git a/ui/scss/component/_file-tile.scss b/ui/scss/component/_file-tile.scss index a3dc30d28..9ff9b2c75 100644 --- a/ui/scss/component/_file-tile.scss +++ b/ui/scss/component/_file-tile.scss @@ -1,16 +1,19 @@ @import "../global"; .file-tile__row { - height: $spacing-vertical * 7; - .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; + vertical-align: middle; display: block; margin-left: auto; margin-right: auto; diff --git a/ui/scss/component/_form-field.scss b/ui/scss/component/_form-field.scss index 14744326b..c9f00b141 100644 --- a/ui/scss/component/_form-field.scss +++ b/ui/scss/component/_form-field.scss @@ -8,10 +8,10 @@ $width-input-border: 2px; } .form-row__label-row { - margin-top: $spacing-vertical * 2/3; - margin-bottom: $spacing-vertical * 1/3; + margin-top: $spacing-vertical * 5/6; + margin-bottom: $spacing-vertical * 1/6; line-height: 1; - font-size: 0.9em; + font-size: 0.9 * $font-size; } .form-row__label-row--prefix { float: left; @@ -72,6 +72,13 @@ $width-input-border: 2px; &.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, @@ -104,6 +111,13 @@ $width-input-border: 2px; width: 330px; } +.form-field__prefix { + margin-right: 4px; +} +.form-field__postfix { + margin-left: 4px; +} + .form-field__input-number { width: 70px; text-align: right; -- 2.45.2 From a5d1695084e269e85042ce6867e2891064bdf447 Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Wed, 12 Apr 2017 12:59:43 -0400 Subject: [PATCH 060/158] more tweaks and turn off auth --- ui/js/component/auth.js | 20 +++++++++++--------- ui/js/component/form.js | 3 ++- ui/js/page/publish.js | 16 +++++++--------- ui/scss/_gui.scss | 2 +- 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/ui/js/component/auth.js b/ui/js/component/auth.js index bf3fb4232..aa3937e63 100644 --- a/ui/js/component/auth.js +++ b/ui/js/component/auth.js @@ -104,20 +104,20 @@ const ConfirmEmailStage = React.createClass({ }); const WelcomeStage = React.createClass({ - onRewardClaim: function() { - console.log('omg'); - }, render: function() { + //

Thank you return (

Welcome to LBRY.

-

LBRY is the first community controlled content marketplace.

-

Since you're new here, we'll toss you some credits.

+

LBRY is kind of like 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 like nothing else. Through blockchain and decentralization, LBRY is controlled by it's users -- that is, you.

+

Here is a reward for reading our weird centaur metaphor:

-

LBC is blah blah blah.

-

And remember, LBRY is a beta and be safe!

+

This reward earned you LBC. LBC is used to watch stuff and to have say in how the network works.

+

But no need to understand it all just yet! Try watching something next.

); } @@ -157,7 +157,7 @@ export const AuthOverlay = React.createClass({ getInitialState: function() { return { - stage: "pending", + stage: null, stageProps: {} }; }, @@ -200,7 +200,9 @@ export const AuthOverlay = React.createClass({ }, render: function() { if (!this.state.stage || lbryio.user && lbryio.user.HasVerifiedEmail) { - return null; + if (this.state.stage != "welcome") { + return null; + } } const StageContent = this._stages[this.state.stage]; return ( diff --git a/ui/js/component/form.js b/ui/js/component/form.js index c24bd7b92..ef8a7169b 100644 --- a/ui/js/component/form.js +++ b/ui/js/component/form.js @@ -50,7 +50,8 @@ export let FormField = React.createClass({ if (this.props.type == 'checkbox') { return this.refs.field.checked; } else if (this.props.type == 'file') { - return !!(this.refs.field.files.length && 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; } diff --git a/ui/js/page/publish.js b/ui/js/page/publish.js index 7ec3ebd0c..f00a30370 100644 --- a/ui/js/page/publish.js +++ b/ui/js/page/publish.js @@ -86,12 +86,10 @@ var PublishPage = React.createClass({ }; if (this.refs.file.getValue() !== '') { - publishArgs.file_path = this.refs.file.getValue(); + publishArgs.file_path = this.refs.file.getValue(); } - console.log(publishArgs); lbry.publish(publishArgs, (message) => { - console.log(message); this.handlePublishStarted(); }, null, (error) => { this.handlePublishError(error); @@ -117,13 +115,13 @@ var PublishPage = React.createClass({ channels: null, rawName: '', name: '', - bid: 0.01, + bid: 1, hasFile: false, feeAmount: '', feeCurrency: 'USD', channel: 'anonymous', newChannelName: '@', - newChannelBid: '', + newChannelBid: 10, nameResolved: false, topClaimValue: 0.0, myClaimValue: 0.0, @@ -392,10 +390,10 @@ var PublishPage = React.createClass({ { !this.state.hasFile ? '' :
- +
- +
@@ -495,7 +493,7 @@ var PublishPage = React.createClass({ type="number" helper={lbcInputHelp} onChange={this.handleNewChannelBidChange} - value={this.state.newChannelBid ? this.state.newChannelBid : '10'} /> + value={this.state.newChannelBid} />
@@ -521,7 +519,7 @@ var PublishPage = React.createClass({ label="Deposit" postfix="LBC" onChange={this.handleBidChange} - value={this.state.bid ? this.state.bid : '1'} + value={this.state.bid} placeholder={this.state.nameResolved ? this.state.topClaimValue + 10 : 100} helper={lbcInputHelp} />
: '' } diff --git a/ui/scss/_gui.scss b/ui/scss/_gui.scss index f1cf53c40..15246c7f6 100644 --- a/ui/scss/_gui.scss +++ b/ui/scss/_gui.scss @@ -174,7 +174,7 @@ p border-radius: 4px; padding: $spacing-vertical; box-shadow: $default-box-shadow; - max-width: 250px; + max-width: 400px; } .modal__header { -- 2.45.2 From b38998dc184314aee5ae6abdbb3629f0308ba064 Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Wed, 12 Apr 2017 16:23:20 -0400 Subject: [PATCH 061/158] more fixes --- ui/js/component/auth.js | 73 ++++++++++++++++++--------- ui/js/component/link.js | 9 ++-- ui/js/component/modal-page.js | 2 +- ui/js/component/modal.js | 4 +- ui/js/component/snack-bar.js | 1 - ui/js/lbry.js | 15 +++--- ui/js/page/publish.js | 15 ++++++ ui/js/page/settings.js | 25 ++++----- ui/js/rewards.js | 31 ++++++++++-- ui/scss/_gui.scss | 76 ---------------------------- ui/scss/all.scss | 1 + ui/scss/component/_modal-page.scss | 14 +----- ui/scss/component/_modal.scss | 81 ++++++++++++++++++++++++++++++ 13 files changed, 204 insertions(+), 143 deletions(-) create mode 100644 ui/scss/component/_modal.scss diff --git a/ui/js/component/auth.js b/ui/js/component/auth.js index aa3937e63..a42d3cc06 100644 --- a/ui/js/component/auth.js +++ b/ui/js/component/auth.js @@ -104,21 +104,45 @@ const ConfirmEmailStage = React.createClass({ }); const WelcomeStage = React.createClass({ + propTypes: { + endAuth: React.PropTypes.func, + }, + getInitialState: function() { + return { + hasReward: false, + rewardAmount: null, + } + }, + onRewardClaim: function(reward) { + this.setState({ + hasReward: true, + rewardAmount: reward + }) + }, render: function() { - //

Thank you return ( -

-

Welcome to LBRY.

-

LBRY is kind of like 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 like nothing else. Through blockchain and decentralization, LBRY is controlled by it's users -- that is, you.

-

Here is a reward for reading our weird centaur metaphor:

-
- -
-

This reward earned you LBC. LBC is used to watch stuff and to have say in how the network works.

-

But no need to understand it all just yet! Try watching something next.

-
+ !this.state.hasReward ? + +
+

Welcome to LBRY.

+

LBRY is kind of like 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 like nothing else. Using blockchain and decentralization, LBRY is controlled by its users -- that is, you -- and no one else.

+

Thanks for being a part of it! Here's a nickel, kid.

+
+ +
+
+
: + +
+

About Your Reward

+

You earned a reward of 5 LBRY credits, or LBC.

+

This reward will show in your Wallet momentarily, likely 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.

+
+
); } }); @@ -152,12 +176,11 @@ export const AuthOverlay = React.createClass({ error: ErrorStage, email: SubmitEmailStage, confirm: ConfirmEmailStage, - welcome: WelcomeStage, + welcome: WelcomeStage }, - getInitialState: function() { return { - stage: null, + stage: "pending", stageProps: {} }; }, @@ -183,7 +206,13 @@ export const AuthOverlay = React.createClass({ } }) } else { - this.endAuth() + 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({ @@ -199,21 +228,17 @@ export const AuthOverlay = React.createClass({ }) }, render: function() { - if (!this.state.stage || lbryio.user && lbryio.user.HasVerifiedEmail) { - if (this.state.stage != "welcome") { + 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/link.js b/ui/js/component/link.js index 8bcaddd48..88e60bf7e 100644 --- a/ui/js/component/link.js +++ b/ui/js/component/link.js @@ -60,7 +60,8 @@ export let RewardLink = React.createClass({ propTypes: { type: React.PropTypes.string.isRequired, claimed: React.PropTypes.bool, - onRewardClaim: React.PropTypes.func + onRewardClaim: React.PropTypes.func, + onRewardFailure: React.PropTypes.func }, refreshClaimable: function() { switch(this.props.type) { @@ -92,7 +93,6 @@ export let RewardLink = React.createClass({ pending: true }) rewards.claimReward(this.props.type).then(function(reward) { - console.log(reward); this.setState({ pending: false, errorMessage: null @@ -108,6 +108,9 @@ export let RewardLink = React.createClass({ }.bind(this)) }, clearError: function() { + if (this.props.onRewardFailure) { + this.props.onRewardFailure() + } this.setState({ errorMessage: null }) @@ -117,7 +120,7 @@ export let RewardLink = React.createClass({
{this.props.claimed ? Reward claimed. - : } + : } {this.state.errorMessage ? {this.state.errorMessage} diff --git a/ui/js/component/modal-page.js b/ui/js/component/modal-page.js index d0def4b82..12826a81e 100644 --- a/ui/js/component/modal-page.js +++ b/ui/js/component/modal-page.js @@ -6,7 +6,7 @@ export const ModalPage = React.createClass({ return ( + overlayClassName="modal-overlay">
{this.props.children}
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/snack-bar.js b/ui/js/component/snack-bar.js index e1ddb01b0..a993c3b75 100644 --- a/ui/js/component/snack-bar.js +++ b/ui/js/component/snack-bar.js @@ -13,7 +13,6 @@ export const SnackBar = React.createClass({ } }, handleSnackReceived: function(event) { - // console.log(event); // if (this._hideTimeout) { // clearTimeout(this._hideTimeout); // } diff --git a/ui/js/lbry.js b/ui/js/lbry.js index 51abc311b..0360453b7 100644 --- a/ui/js/lbry.js +++ b/ui/js/lbry.js @@ -204,13 +204,6 @@ lbry.resolveName = function(name, callback) { }); } -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.`); @@ -654,6 +647,14 @@ lbry.claim_list_mine = function(params={}) { }); } +// 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/page/publish.js b/ui/js/page/publish.js index f00a30370..2709f3a8e 100644 --- a/ui/js/page/publish.js +++ b/ui/js/page/publish.js @@ -3,11 +3,25 @@ import lbry from '../lbry.js'; import uri from '../uri.js'; import {FormField, FormRow} from '../component/form.js'; import {Link} from '../component/link.js'; +import rewards from '../rewards.js'; import Modal from '../component/modal.js'; var PublishPage = React.createClass({ _requiredFields: ['meta_title', 'name', 'bid'], + _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) + } + }); + }, _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) @@ -334,6 +348,7 @@ var PublishPage = React.createClass({ }, componentWillMount: function() { this._updateChannelList(); + this._requestPublishReward(); }, componentDidMount: function() { document.title = "Publish"; diff --git a/ui/js/page/settings.js b/ui/js/page/settings.js index 318711d8b..d05be08fe 100644 --- a/ui/js/page/settings.js +++ b/ui/js/page/settings.js @@ -78,20 +78,21 @@ var SettingsPage = React.createClass({ if (!this.state.daemonSettings) { return null; } - +/* +
+
+

Run on Startup

+
+
+ +
+
+ */ return (
-
-
-

Run on Startup

-
-
- -
-

Download Directory

diff --git a/ui/js/rewards.js b/ui/js/rewards.js index 29f43058c..a42ed1637 100644 --- a/ui/js/rewards.js +++ b/ui/js/rewards.js @@ -3,23 +3,44 @@ import lbryio from './lbryio.js'; function rewardMessage(type, amount) { return { - new_developer: "Your reward has been confirmed for registering as a new developer.", + new_developer: "You received ${amount} for registering as a new developer.", new_user: `You received ${amount} LBC new user reward.`, - confirm_email: "Your reward has been confirmed for verifying your email address.", - first_publish: "Your reward has been confirmed for making your first publication.", + confirm_email: "You received ${amount} LBC for verifying your email address.", + first_channel: "You received ${amount} LBC for creating a publisher identity.", + first_purchase: "You received ${amount} LBC for making your first purchase.", + first_publish: "You received ${amount} LBC for making your first publication.", }[type]; } const rewards = {}; -rewards.claimReward = function(type) { +rewards.TYPE_NEW_DEVELOPER = "new_developer", + rewards.TYPE_NEW_USER = "new_user", + rewards.TYPE_CONFIRM_EMAIL = "confirm_email", + rewards.TYPE_FIRST_CHANNEL = "first_channel", + rewards.TYPE_FIRST_PURCHASE = "first_purchase", + rewards.TYPE_FIRST_PUBLISH = "first_publish"; + +rewards.claimReward = function (type) { return new Promise((resolve, reject) => { - console.log('top of promise body') lbry.get_new_address().then((address) => { const params = { reward_type: type, wallet_address: address, }; + switch (type) { + case 'first_channel': + //params.transaction_id = RelevantTransactionID; + break; + + case 'first_purchase': + //params.transaction_id = RelevantTransactionID; + break; + + case 'first_channel': + //params.transaction_id = RelevantTransactionID; + break; + } lbryio.call('reward', 'new', params, 'post').then(({RewardAmount}) => { const message = rewardMessage(type, RewardAmount), diff --git a/ui/scss/_gui.scss b/ui/scss/_gui.scss index 15246c7f6..da32d3ac5 100644 --- a/ui/scss/_gui.scss +++ b/ui/scss/_gui.scss @@ -147,79 +147,3 @@ p 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; - 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; -} diff --git a/ui/scss/all.scss b/ui/scss/all.scss index 18a9ec720..dcb81cb98 100644 --- a/ui/scss/all.scss +++ b/ui/scss/all.scss @@ -15,6 +15,7 @@ @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 "page/_developer.scss"; diff --git a/ui/scss/component/_modal-page.scss b/ui/scss/component/_modal-page.scss index 2b86c5cad..d9bd0d8d5 100644 --- a/ui/scss/component/_modal-page.scss +++ b/ui/scss/component/_modal-page.scss @@ -1,16 +1,4 @@ -.modal-page-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; -} +@import "../global"; .modal-page { position: fixed; 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 -- 2.45.2 From dce87e9079137067ee078bfd7949c38d85be29bf Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Wed, 12 Apr 2017 01:02:29 -0400 Subject: [PATCH 062/158] Switch name check to URI check in lbry.getCostInfo() --- ui/js/lbry.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ui/js/lbry.js b/ui/js/lbry.js index 0360453b7..e87dee4b0 100644 --- a/ui/js/lbry.js +++ b/ui/js/lbry.js @@ -243,6 +243,10 @@ lbry.getCostInfo = function(lbryUri, callback, errorCallback) { * - includes_data: Boolean; indicates whether or not the data fee info * from Lighthouse is included. */ + if (!lbryUri) { + throw new Error(`URI required.`); + } + function getCostWithData(name, size, callback, errorCallback) { lbry.stream_cost_estimate({name, size}).then((cost) => { callback({ -- 2.45.2 From d3258d9de6e846189b21b9b3b0efc0e0bc060208 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Wed, 12 Apr 2017 13:08:23 -0400 Subject: [PATCH 063/158] Convert lbry.getCostInfo() to use URIs --- ui/js/lbry.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/js/lbry.js b/ui/js/lbry.js index e87dee4b0..941c24244 100644 --- a/ui/js/lbry.js +++ b/ui/js/lbry.js @@ -247,8 +247,8 @@ lbry.getCostInfo = function(lbryUri, callback, errorCallback) { throw new Error(`URI required.`); } - function getCostWithData(name, size, callback, errorCallback) { - lbry.stream_cost_estimate({name, size}).then((cost) => { + function getCostWithData(lbryUri, size, callback, errorCallback) { + lbry.stream_cost_estimate({uri: lbryUri, size}).then((cost) => { callback({ cost: cost, includesData: true, @@ -256,8 +256,8 @@ lbry.getCostInfo = function(lbryUri, callback, errorCallback) { }, errorCallback); } - function getCostNoData(name, callback, errorCallback) { - lbry.stream_cost_estimate({name}).then((cost) => { + function getCostNoData(lbryUri, callback, errorCallback) { + lbry.stream_cost_estimate({uri: lbryUri}).then((cost) => { callback({ cost: cost, includesData: false, -- 2.45.2 From b9f0ed2f5b292d9e7d101a9cf2e0aea55a5754ad Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Wed, 12 Apr 2017 14:14:19 -0400 Subject: [PATCH 064/158] Refactor lbry.getCostInfo() --- ui/js/lbry.js | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/ui/js/lbry.js b/ui/js/lbry.js index 941c24244..4c439db79 100644 --- a/ui/js/lbry.js +++ b/ui/js/lbry.js @@ -247,20 +247,11 @@ lbry.getCostInfo = function(lbryUri, callback, errorCallback) { throw new Error(`URI required.`); } - function getCostWithData(lbryUri, size, callback, errorCallback) { - lbry.stream_cost_estimate({uri: lbryUri, size}).then((cost) => { + function getCost(lbryUri, size, callback, errorCallback) { + lbry.stream_cost_estimate({uri: lbryUri, ... size !== null ? {size} : {}}).then((cost) => { callback({ cost: cost, - includesData: true, - }); - }, errorCallback); - } - - function getCostNoData(lbryUri, callback, errorCallback) { - lbry.stream_cost_estimate({uri: lbryUri}).then((cost) => { - callback({ - cost: cost, - includesData: false, + includesData: size !== null, }); }, errorCallback); } @@ -269,9 +260,9 @@ lbry.getCostInfo = function(lbryUri, callback, errorCallback) { const name = uriObj.path || uriObj.name; lighthouse.get_size_for_name(name).then((size) => { - getCostWithData(name, size, callback, errorCallback); + getCost(name, size, callback, errorCallback); }, () => { - getCostNoData(name, callback, errorCallback); + getCost(name, null, callback, errorCallback); }); } -- 2.45.2 From dabedf38a100af2cd906a3d899c2700244a002c6 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Wed, 12 Apr 2017 19:24:04 -0400 Subject: [PATCH 065/158] Update and refactor pending publish caching system for channels --- ui/js/lbry.js | 107 +++++++++++++++------------------------- ui/js/page/file-list.js | 2 +- 2 files changed, 41 insertions(+), 68 deletions(-) diff --git a/ui/js/lbry.js b/ui/js/lbry.js index 4c439db79..6397ca9f5 100644 --- a/ui/js/lbry.js +++ b/ui/js/lbry.js @@ -10,24 +10,32 @@ 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}) { + const lbryUri = uri.buildLbryUri({name, channel_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,59 +44,28 @@ 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 ? {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: {stream: {metadata: 'Attempting publication'}}}; } @@ -330,12 +307,14 @@ lbry.publish = function(params, fileListedCallback, publishedCallback, errorCall returnedPending = true; if (publishedCallback) { - savePendingPublish(params.name); + const {name, channel_name} = params; + savePendingPublish({name, ... channel_name ? {channel_name} : {}}); publishedCallback(true); } if (fileListedCallback) { - savePendingPublish(params.name); + const {name, channel_name} = params; + savePendingPublish({name, ... channel_name ? {channel_name} : {}}); fileListedCallback(true); } }, 2000); @@ -599,14 +578,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)]); @@ -615,14 +594,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); @@ -632,13 +605,13 @@ 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) }); } diff --git a/ui/js/page/file-list.js b/ui/js/page/file-list.js index 518bb85d6..4a3f56f50 100644 --- a/ui/js/page/file-list.js +++ b/ui/js/page/file-list.js @@ -162,7 +162,7 @@ export let FileList = React.createClass({ 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]) { + if (!metadata || seenUris[name] || channel_name === null) { continue; } -- 2.45.2 From c8c97e97ca5825aadec9f83cfbbf7b6a4734be9b Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Thu, 13 Apr 2017 14:52:26 -0400 Subject: [PATCH 066/158] update --- lbry | 2 +- lbryum | 2 +- ui/js/app.js | 2 +- ui/js/component/auth.js | 8 +- ui/js/component/common.js | 74 ++++++++++++++- ui/js/component/file-actions.js | 30 +++++- ui/js/component/file-tile.js | 56 ++--------- ui/js/lbry.js | 71 ++++++++------ ui/js/lighthouse.js | 8 +- ui/js/page/publish.js | 6 +- ui/js/page/show.js | 163 ++++++++++++++------------------ ui/js/page/watch.js | 99 +++++++++++++++++++ ui/js/rewards.js | 71 +++++++++----- ui/js/utils.js | 4 +- ui/scss/_canvas.scss | 10 ++ ui/scss/_global.scss | 2 + ui/scss/all.scss | 2 + ui/scss/component/_card.scss | 7 +- ui/scss/component/_video.scss | 12 +++ ui/scss/page/_show.scss | 6 ++ 20 files changed, 410 insertions(+), 225 deletions(-) create mode 100644 ui/scss/component/_video.scss create mode 100644 ui/scss/page/_show.scss 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 6126901e7..c8f585fb7 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -287,7 +287,7 @@ var App = React.createClass({ var mainContent = this.getMainContent(), headerLinks = this.getHeaderLinks(), searchQuery = this.state.viewingPage == 'discover' && this.state.pageArgs ? this.state.pageArgs : ''; - + return ( this._fullScreenPages.includes(this.state.viewingPage) ? mainContent : diff --git a/ui/js/component/auth.js b/ui/js/component/auth.js index a42d3cc06..5b3d4d46e 100644 --- a/ui/js/component/auth.js +++ b/ui/js/component/auth.js @@ -40,7 +40,7 @@ const SubmitEmailStage = React.createClass({ return (
- { this._emailRow = ref }} type="text" label="Email" placeholder="webmaster@toplbryfan.com" + { this._emailRow = ref }} type="text" label="Email" placeholder="admin@toplbryfan.com" name="email" value={this.state.email} onChange={this.handleEmailChanged} />
@@ -125,9 +125,9 @@ const WelcomeStage = React.createClass({

Welcome to LBRY.

-

LBRY is kind of like a centaur. Totally normal up top, and way different underneath.

+

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 like nothing else. Using blockchain and decentralization, LBRY is controlled by its users -- that is, you -- and no one else.

+

Below, LBRY is like nothing else. Using blockchain and decentralization, LBRY is controlled by its users -- you -- and no one else.

Thanks for being a part of it! Here's a nickel, kid.

@@ -180,7 +180,7 @@ export const AuthOverlay = React.createClass({ }, getInitialState: function() { return { - stage: "pending", + stage: null, stageProps: {} }; }, diff --git a/ui/js/component/common.js b/ui/js/component/common.js index 98dbd45eb..c4e1324f1 100644 --- a/ui/js/component/common.js +++ b/ui/js/component/common.js @@ -60,20 +60,84 @@ export let CurrencySymbol = React.createClass({ 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 ? * : 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', }; diff --git a/ui/js/component/file-actions.js b/ui/js/component/file-actions.js index 410d14d66..9b81fa28c 100644 --- a/ui/js/component/file-actions.js +++ b/ui/js/component/file-actions.js @@ -2,7 +2,7 @@ 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 {Modal} from './modal.js'; import {FormField} from './form.js'; import {ToolTip} from '../component/tooltip.js'; import {DropDownMenu, DropDownMenuItem} from './menu.js'; @@ -25,7 +25,7 @@ let WatchLink = React.createClass({ if (this.props.downloadStarted) { this.startVideo(); } else { - lbry.getCostInfo(this.props.uri, ({cost}) => { + lbry.getCostInfo(this.props.uri).then(({cost}) => { lbry.getBalance((balance) => { if (cost > balance) { this.setState({ @@ -79,7 +79,8 @@ let FileActionsRow = React.createClass({ menuOpen: false, deleteChecked: false, attemptingDownload: false, - attemptingRemove: false + attemptingRemove: false, + affirmedPurchase: false } }, onFileInfoUpdate: function(fileInfo) { @@ -95,14 +96,16 @@ let FileActionsRow = React.createClass({ attemptingDownload: true, attemptingRemove: false }); - lbry.getCostInfo(this.props.uri, ({cost}) => { + lbry.getCostInfo(this.props.uri).then(({cost}) => { + console.log(cost); + console.log(this.props.uri); 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 +114,11 @@ let FileActionsRow = React.createClass({ }); } }); + } else { + this.setState({ + attemptingDownload: false, + modal: 'affirmPurchase' + }) } }); }); @@ -153,6 +161,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, @@ -209,6 +224,10 @@ let FileActionsRow = React.createClass({ : '' } + + Confirm you want to purchase this bro. + You don't have enough LBRY credits to pay for this stream. @@ -261,6 +280,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({ diff --git a/ui/js/component/file-tile.js b/ui/js/component/file-tile.js index 199f341bd..f9522f1c6 100644 --- a/ui/js/component/file-tile.js +++ b/ui/js/component/file-tile.js @@ -3,52 +3,9 @@ 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 {Thumbnail, TruncatedText, FilePrice} from '../component/common.js'; import UriIndicator from '../component/channel-indicator.js'; -let FilePrice = React.createClass({ - _isMounted: false, - - propTypes: { - uri: React.PropTypes.string - }, - - getInitialState: function() { - return { - cost: null, - costIncludesData: null, - } - }, - - componentDidMount: function() { - this._isMounted = true; - - lbry.getCostInfo(this.props.uri, ({cost, includesData}) => { - if (this._isMounted) { - this.setState({ - cost: cost, - costIncludesData: includesData, - }); - } - }, (err) => { - console.log('error from getCostInfo callback:', err) - // If we get an error looking up cost information, do nothing - }); - }, - - componentWillUnmount: function() { - this._isMounted = false; - }, - - render: function() { - return ( - this.state.cost !== null ? - : - ... - ); - } -}); - /*should be merged into FileTile once FileTile is refactored to take a single id*/ export let FileTileStream = React.createClass({ _fileInfoSubscribeId: null, @@ -234,7 +191,7 @@ export let FileCardStream = React.createClass({ const isConfirmed = typeof metadata == 'object'; const title = isConfirmed ? metadata.title : lbryUri; const obscureNsfw = this.props.obscureNsfw && isConfirmed && metadata.nsfw; - const primaryUrl = '?watch=' + lbryUri; + const primaryUrl = '?show=' + lbryUri; return (
@@ -242,7 +199,7 @@ export let FileCardStream = React.createClass({
{title}
- { !this.props.hidePrice ? : null} + { !this.props.hidePrice ? : null}
@@ -288,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, }); } }); diff --git a/ui/js/lbry.js b/ui/js/lbry.js index 6397ca9f5..ef6f6a67c 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'); @@ -93,7 +93,6 @@ lbry.call = function (method, params, callback, errorCallback, connectFailedCall jsonrpc.call(lbry.daemonConnectionString, method, [params], callback, errorCallback, connectFailedCallback); } - //core lbry._connectPromise = null; lbry.connect = function() { @@ -171,16 +170,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.getClaimInfo = function(name, callback) { if (!name) { throw new Error(`Name required.`); @@ -209,7 +198,7 @@ lbry.getPeersForBlobHash = function(blobHash, callback) { }); } -lbry.getCostInfo = function(lbryUri, callback, errorCallback) { +lbry.getCostInfo = function(lbryUri) { /** * Takes a LBRY URI; will first try and calculate a total cost using * Lighthouse. If Lighthouse can't be reached, it just retrives the @@ -223,24 +212,29 @@ lbry.getCostInfo = function(lbryUri, callback, errorCallback) { if (!lbryUri) { throw new Error(`URI required.`); } + return new Promise((resolve, reject) => { + function getCost(lbryUri, size) { + lbry.stream_cost_estimate({uri: lbryUri, ... size !== null ? {size} : {}}).then((cost) => { + callback({ + cost: cost, + includesData: size !== null, + }); + }, reject); + } - function getCost(lbryUri, size, callback, errorCallback) { - lbry.stream_cost_estimate({uri: lbryUri, ... size !== null ? {size} : {}}).then((cost) => { - callback({ - cost: cost, - includesData: size !== null, - }); - }, errorCallback); - } + const uriObj = uri.parseLbryUri(lbryUri); + const name = uriObj.path || uriObj.name; - const uriObj = uri.parseLbryUri(lbryUri); - const name = uriObj.path || uriObj.name; - - lighthouse.get_size_for_name(name).then((size) => { - getCost(name, size, callback, errorCallback); - }, () => { - getCost(name, null, callback, errorCallback); - }); + lighthouse.get_size_for_name(name).then((size) => { + if (size) { + getCost(name, size); + } else { + getCost(name, null); + } + }, () => { + getCost(name, null); + }); + }) } lbry.getMyClaims = function(callback) { @@ -615,6 +609,25 @@ lbry.claim_list_mine = function(params={}) { }); } +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) => { diff --git a/ui/js/lighthouse.js b/ui/js/lighthouse.js index a8b60f0fa..41d2d996c 100644 --- a/ui/js/lighthouse.js +++ b/ui/js/lighthouse.js @@ -21,11 +21,7 @@ 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}`); - } + errorCallback(new Error(`Could not connect to Lighthouse server. Last server attempted: ${server}`)); } /** @@ -48,7 +44,7 @@ function call(method, params, callback, errorCallback) { }, () => { connectTryNum++; call(method, params, callback, errorCallback); - }); + }, queryTimeout); } const lighthouse = new Proxy({}, { diff --git a/ui/js/page/publish.js b/ui/js/page/publish.js index 2709f3a8e..524201b97 100644 --- a/ui/js/page/publish.js +++ b/ui/js/page/publish.js @@ -4,6 +4,7 @@ import uri from '../uri.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({ @@ -26,6 +27,7 @@ var PublishPage = React.createClass({ // 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) this.setState({ channels: channels, ... channel ? {channel} : {} @@ -348,7 +350,7 @@ var PublishPage = React.createClass({ }, componentWillMount: function() { this._updateChannelList(); - this._requestPublishReward(); + // this._requestPublishReward(); }, componentDidMount: function() { document.title = "Publish"; @@ -510,7 +512,7 @@ var PublishPage = React.createClass({ onChange={this.handleNewChannelBidChange} value={this.state.newChannelBid} />
- +
: null} diff --git a/ui/js/page/show.js b/ui/js/page/show.js index 7197390d2..eb9f02e46 100644 --- a/ui/js/page/show.js +++ b/ui/js/page/show.js @@ -2,105 +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.metadata.thumbnail ?
: '' } -

{this.props.metadata.title}

-
{this.props.uri}
- {/* 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: { @@ -110,6 +50,8 @@ var ShowPage = React.createClass({ return { metadata: null, contentType: null, + hasSignature: false, + signatureIsValid: false, cost: null, costIncludesData: null, uriLookupComplete: null, @@ -119,16 +61,19 @@ 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}}}}}) => { + console.log({txid, nout, claim: {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, @@ -140,17 +85,51 @@ var ShowPage = React.createClass({ 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.props.contentType && this.props.contentType.startsWith('video/') ? +
+
+
+
+ +

{title}

+ { this.state.uriLookupComplete ? +
+
+ +
+
+ +
+
: '' }
- )} -
); + { this.state.uriLookupComplete ? +
+
+ {metadata.description} +
+
+ : } +
+
+ +
+
+ +
+
+
+ ); } }); diff --git a/ui/js/page/watch.js b/ui/js/page/watch.js index ac270d77a..45341ded1 100644 --- a/ui/js/page/watch.js +++ b/ui/js/page/watch.js @@ -7,6 +7,105 @@ import LoadScreen from '../component/load_screen.js' const fs = require('fs'); const VideoStream = require('videostream'); +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, + }, + getInitialState: function() { + return { + downloadStarted: false, + readyToPlay: false, + loadStatusMessage: "Requesting stream", + mimeType: null, + controlsShown: false, + }; + }, + componentDidMount: function() { + lbry.get({uri: this.props.uri}).then((fileInfo) => { + this._outpoint = fileInfo.outpoint; + this.updateLoadStatus(); + }); + }, + handleMouseMove: function() { + if (this._controlsTimeout) { + clearTimeout(this._controlsTimeout); + } + + if (!this.state.controlsShown) { + this.setState({ + controlsShown: true, + }); + } + this._controlsTimeout = setTimeout(() => { + if (!this.isMounted) { + return; + } + + this.setState({ + controlsShown: false, + }); + }, this._controlsHideDelay); + }, + handleMouseLeave: function() { + if (this._controlsTimeout) { + clearTimeout(this._controlsTimeout); + } + + if (this.state.controlsShown) { + this.setState({ + controlsShown: false, + }); + } + }, + updateLoadStatus: function() { + lbry.file_list({ + outpoint: this._outpoint, + full_status: true, + }).then(([status]) => { + if (!status || status.written_bytes == 0) { + // Download hasn't started yet, so update status message (if available) then try again + // TODO: Would be nice to check if we have the MOOV before starting playing + if (status) { + this.setState({ + loadStatusMessage: status.message + }); + } + setTimeout(() => { this.updateLoadStatus() }, 250); + } else { + this.setState({ + readyToPlay: true, + mimeType: status.mime_type, + }) + return + const mediaFile = { + createReadStream: function (opts) { + // Return a readable stream that provides the bytes + // between offsets "start" and "end" inclusive + console.log('Stream between ' + opts.start + ' and ' + opts.end + '.'); + return fs.createReadStream(status.download_path, opts) + } + }; + var elem = this.refs.video; + var videostream = VideoStream(mediaFile, elem); + elem.play(); + } + }); + }, + render: function() { + return ( +
{ + !this.state.readyToPlay || true ? + this is the world's world loading message and we shipped our software with it anyway... seriously it is actually loading... it might take a while though : + + }
+ ); + } +}) var WatchPage = React.createClass({ _isMounted: false, diff --git a/ui/js/rewards.js b/ui/js/rewards.js index a42ed1637..ec043de02 100644 --- a/ui/js/rewards.js +++ b/ui/js/rewards.js @@ -22,48 +22,67 @@ rewards.TYPE_NEW_DEVELOPER = "new_developer", rewards.TYPE_FIRST_PUBLISH = "first_publish"; rewards.claimReward = function (type) { + + function requestReward(resolve, reject, params) { + 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 'first_channel': - //params.transaction_id = RelevantTransactionID; + case rewards.TYPE_FIRST_CHANNEL: + lbry.claim_list_mine().then(function(channels) { + if (channels.length) { + params.transaction_id = channels[0].txid; + requestReward(resolve, reject, params) + } else { + reject(new Error("Please create a channel identity first.")) + } + }).catch(reject) break; case 'first_purchase': - //params.transaction_id = RelevantTransactionID; + // lbry.claim_list_mine().then(function(channels) { + // if (channels.length) { + // requestReward(resolve, reject, {transaction_id: channels[0].txid}) + // } + // }).catch(reject) break; case 'first_channel': //params.transaction_id = RelevantTransactionID; break; + + default: + requestReward(resolve, reject, params); } - 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); }); }); } diff --git a/ui/js/utils.js b/ui/js/utils.js index e9472a6a4..b24eb25b6 100644 --- a/ui/js/utils.js +++ b/ui/js/utils.js @@ -18,9 +18,9 @@ export function setLocal(key, value) { * Thin wrapper around localStorage.getItem(). Parses JSON and returns undefined if the value * is not set yet. */ -export function getSession(key) { +export function getSession(key, fallback=undefined) { const itemRaw = sessionStorage.getItem(key); - return itemRaw === null ? undefined : JSON.parse(itemRaw); + return itemRaw === null ? fallback : JSON.parse(itemRaw); } /** diff --git a/ui/scss/_canvas.scss b/ui/scss/_canvas.scss index ac5712240..25eb836fc 100644 --- a/ui/scss/_canvas.scss +++ b/ui/scss/_canvas.scss @@ -62,6 +62,10 @@ $drawer-width: 220px; font-weight: bold; color: $color-money; } +.credit-amount--estimate { + font-style: italic; + color: $color-meta-light; +} #drawer-handle { padding: $spacing-vertical / 2; @@ -188,6 +192,12 @@ nav.sub-header main { padding: $spacing-vertical; + &.constrained-page + { + max-width: $width-page-constrained; + margin-left: auto; + margin-right: auto; + } } } diff --git a/ui/scss/_global.scss b/ui/scss/_global.scss index b203e7d18..68533b206 100644 --- a/ui/scss/_global.scss +++ b/ui/scss/_global.scss @@ -31,6 +31,8 @@ $max-text-width: 660px; $height-header: $spacing-vertical * 2.5; $height-button: $spacing-vertical * 1.5; +$width-page-constrained: 800px; + $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); diff --git a/ui/scss/all.scss b/ui/scss/all.scss index dcb81cb98..b4c6611a6 100644 --- a/ui/scss/all.scss +++ b/ui/scss/all.scss @@ -18,6 +18,8 @@ @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"; @import "page/_reward.scss"; +@import "page/_show.scss"; diff --git a/ui/scss/component/_card.scss b/ui/scss/component/_card.scss index 163de32d4..e019d7342 100644 --- a/ui/scss/component/_card.scss +++ b/ui/scss/component/_card.scss @@ -5,7 +5,7 @@ $padding-card-horizontal: $spacing-vertical * 2/3; .card { margin-left: auto; margin-right: auto; - max-width: 800px; + max-width: $width-page-constrained; background: $color-bg; box-shadow: $default-box-shadow; border-radius: 2px; @@ -59,6 +59,9 @@ $padding-card-horizontal: $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; } @@ -118,7 +121,7 @@ $height-card-small: $spacing-vertical * 15; { margin-left: auto; margin-right: auto; - max-width: 800px; + max-width: $width-page-constrained; padding: $spacing-vertical / 2; } diff --git a/ui/scss/component/_video.scss b/ui/scss/component/_video.scss new file mode 100644 index 000000000..c6800088c --- /dev/null +++ b/ui/scss/component/_video.scss @@ -0,0 +1,12 @@ +video { + border: 1px solid red; + object-fill: contain; +} + +.video-embedded { + max-width: 100%; + height: 0; + padding-bottom: 63%; + video { + } +} \ 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..c0434ee24 --- /dev/null +++ b/ui/scss/page/_show.scss @@ -0,0 +1,6 @@ +@import "../global"; + +.show-page-media { + text-align: center; + margin-bottom: $spacing-vertical; +} \ No newline at end of file -- 2.45.2 From e3222c853aecb41f77e1a5bf4aecef9aa3e044c4 Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Thu, 13 Apr 2017 14:57:12 -0400 Subject: [PATCH 067/158] fix merge --- ui/js/lbry.js | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/ui/js/lbry.js b/ui/js/lbry.js index ef6f6a67c..e7ca23c23 100644 --- a/ui/js/lbry.js +++ b/ui/js/lbry.js @@ -198,24 +198,26 @@ lbry.getPeersForBlobHash = function(blobHash, callback) { }); } +/** + * 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.getCostInfo = function(lbryUri) { - /** - * 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 (!lbryUri) { - throw new Error(`URI required.`); - } return new Promise((resolve, reject) => { + + if (!lbryUri) { + reject(new Error(`URI required.`)); + } + function getCost(lbryUri, size) { lbry.stream_cost_estimate({uri: lbryUri, ... size !== null ? {size} : {}}).then((cost) => { - callback({ + resolve({ cost: cost, includesData: size !== null, }); -- 2.45.2 From 736d769fa69e0d2382f6a975dbe2aa8c4123c583 Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Thu, 13 Apr 2017 15:37:41 -0400 Subject: [PATCH 068/158] minor fixes, cost cache --- ui/js/component/common.js | 2 +- ui/js/lbry.js | 63 +++++++++++++++++++++++---------------- ui/js/lighthouse.js | 7 +++-- ui/js/page/file-list.js | 4 +-- ui/js/page/show.js | 5 ++-- 5 files changed, 48 insertions(+), 33 deletions(-) diff --git a/ui/js/component/common.js b/ui/js/component/common.js index c4e1324f1..78bb6522e 100644 --- a/ui/js/component/common.js +++ b/ui/js/component/common.js @@ -121,7 +121,7 @@ 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 diff --git a/ui/js/lbry.js b/ui/js/lbry.js index e7ca23c23..2b6330656 100644 --- a/ui/js/lbry.js +++ b/ui/js/lbry.js @@ -208,35 +208,48 @@ lbry.getPeersForBlobHash = function(blobHash, callback) { * - includes_data: Boolean; indicates whether or not the data fee info * from Lighthouse is included. */ +lbry.costPromiseCache = {} lbry.getCostInfo = function(lbryUri) { - return new Promise((resolve, reject) => { + 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, {}) - if (!lbryUri) { - reject(new Error(`URI required.`)); - } - - function getCost(lbryUri, size) { - lbry.stream_cost_estimate({uri: lbryUri, ... size !== null ? {size} : {}}).then((cost) => { - resolve({ - cost: cost, - includesData: size !== null, - }); - }, 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); + if (!lbryUri) { + reject(new Error(`URI required.`)); } - }, () => { - getCost(name, null); + + 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) { diff --git a/ui/js/lighthouse.js b/ui/js/lighthouse.js index 41d2d996c..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,8 +20,9 @@ function getServers() { } function call(method, params, callback, errorCallback) { - if (connectTryNum > maxQueryTries) { + if (connectTryNum >= maxQueryTries) { errorCallback(new Error(`Could not connect to Lighthouse server. Last server attempted: ${server}`)); + return; } /** diff --git a/ui/js/page/file-list.js b/ui/js/page/file-list.js index 4a3f56f50..fdb6c7649 100644 --- a/ui/js/page/file-list.js +++ b/ui/js/page/file-list.js @@ -162,12 +162,12 @@ export let FileList = React.createClass({ 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] || channel_name === null) { + if (!metadata || seenUris[name]) { continue; } let fileUri; - if (channel_name === undefined) { + if (!channel_name) { fileUri = uri.buildLbryUri({name}); } else { fileUri = uri.buildLbryUri({name: channel_name, path: name}); diff --git a/ui/js/page/show.js b/ui/js/page/show.js index eb9f02e46..f3b361976 100644 --- a/ui/js/page/show.js +++ b/ui/js/page/show.js @@ -91,6 +91,7 @@ let ShowPage = React.createClass({ metadata = this.state.uriLookupComplete ? this.state.metadata : null, title = this.state.uriLookupComplete ? metadata.title : this._uri; + console.log(metadata); return (
@@ -101,7 +102,7 @@ let ShowPage = React.createClass({
- +

{title}

{ this.state.uriLookupComplete ?
@@ -109,7 +110,7 @@ let ShowPage = React.createClass({
- +
: '' }
-- 2.45.2 From 83f43ab54f00b4bfeea47a72fd7aae80bdcac777 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Thu, 13 Apr 2017 16:35:40 -0400 Subject: [PATCH 069/158] More improvements for better compatibility with new claim format - Update Pending Files cache to match format more closely - Except null instead of string for pending publishes --- ui/js/component/file-tile.js | 8 ++++---- ui/js/lbry.js | 2 +- ui/js/page/file-list.js | 14 +++++++++++--- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/ui/js/component/file-tile.js b/ui/js/component/file-tile.js index f9522f1c6..172dea636 100644 --- a/ui/js/component/file-tile.js +++ b/ui/js/component/file-tile.js @@ -13,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, @@ -75,14 +75,14 @@ export let FileTileStream = React.createClass({ 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 @@ -188,7 +188,7 @@ export let FileCardStream = React.createClass({ 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; const primaryUrl = '?show=' + lbryUri; diff --git a/ui/js/lbry.js b/ui/js/lbry.js index 2b6330656..b09aabe1f 100644 --- a/ui/js/lbry.js +++ b/ui/js/lbry.js @@ -65,7 +65,7 @@ function pendingPublishToDummyClaim({channel_name, name, outpoint, claim_id, txi } function pendingPublishToDummyFileInfo({name, outpoint, claim_id}) { - return {name, outpoint, claim_id, metadata: {stream: {metadata: 'Attempting publication'}}}; + return {name, outpoint, claim_id, null}; } diff --git a/ui/js/page/file-list.js b/ui/js/page/file-list.js index fdb6c7649..3c337565f 100644 --- a/ui/js/page/file-list.js +++ b/ui/js/page/file-list.js @@ -161,11 +161,19 @@ 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) { fileUri = uri.buildLbryUri({name}); @@ -174,7 +182,7 @@ export let FileList = React.createClass({ } seenUris[name] = true; content.push(); } -- 2.45.2 From 62db745c3844d32530264fa047f18ce59c8f8954 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Thu, 13 Apr 2017 16:40:29 -0400 Subject: [PATCH 070/158] Publish: convert nsfw field to submit a boolean --- ui/js/page/publish.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ui/js/page/publish.js b/ui/js/page/publish.js index 524201b97..90dbe4504 100644 --- a/ui/js/page/publish.js +++ b/ui/js/page/publish.js @@ -81,14 +81,16 @@ var PublishPage = React.createClass({ var metadata = {}; } - for (let metaField of ['title', '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; } @@ -428,7 +430,7 @@ var PublishPage = React.createClass({
- + {/* */} -- 2.45.2 From 3b29be467ba42de1e10e739b6fc697802043dc3b Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Thu, 13 Apr 2017 16:50:24 -0400 Subject: [PATCH 071/158] turn off things --- ui/js/main.js | 2 +- ui/js/page/publish.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/js/main.js b/ui/js/main.js index 544ebb0a2..9d11107d0 100644 --- a/ui/js/main.js +++ b/ui/js/main.js @@ -30,7 +30,7 @@ let init = function() { 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(
, canvas) + ReactDOM.render(
, canvas) } if (window.sessionStorage.getItem('loaded') == 'y') { diff --git a/ui/js/page/publish.js b/ui/js/page/publish.js index 90dbe4504..9267550c9 100644 --- a/ui/js/page/publish.js +++ b/ui/js/page/publish.js @@ -19,7 +19,7 @@ var PublishPage = React.createClass({ return; } else { - rewards.claimReward(rewards.TYPE_FIRST_PUBLISH) + // rewards.claimReward(rewards.TYPE_FIRST_PUBLISH) } }); }, @@ -27,7 +27,7 @@ var PublishPage = React.createClass({ // 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) + // rewards.claimReward(rewards.TYPE_FIRST_CHANNEL) this.setState({ channels: channels, ... channel ? {channel} : {} -- 2.45.2 From 280f98902cb4ca2ee8efcd6030562de6f095bd7f Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Thu, 13 Apr 2017 18:32:03 -0400 Subject: [PATCH 072/158] closer to real watch --- ui/js/app.js | 2 - ui/js/component/file-actions.js | 56 ----------- ui/js/main.js | 1 + ui/js/page/publish.js | 4 +- ui/js/page/show.js | 7 +- ui/js/page/watch.js | 162 ++++++++------------------------ ui/scss/component/_video.scss | 53 ++++++++++- ui/scss/page/_show.scss | 3 + ui/scss/page/_watch.scss | 3 - 9 files changed, 93 insertions(+), 198 deletions(-) diff --git a/ui/js/app.js b/ui/js/app.js index c8f585fb7..9cfb43137 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -256,8 +256,6 @@ var App = React.createClass({ return ; case 'help': return ; - case 'watch': - return ; case 'report': return ; case 'downloaded': diff --git a/ui/js/component/file-actions.js b/ui/js/component/file-actions.js index 9b81fa28c..1c4c1650a 100644 --- a/ui/js/component/file-actions.js +++ b/ui/js/component/file-actions.js @@ -9,59 +9,6 @@ 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).then(({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, @@ -213,9 +160,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} diff --git a/ui/js/main.js b/ui/js/main.js index 9d11107d0..d31b4c7d5 100644 --- a/ui/js/main.js +++ b/ui/js/main.js @@ -30,6 +30,7 @@ let init = function() { 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(
, canvas) } diff --git a/ui/js/page/publish.js b/ui/js/page/publish.js index 9267550c9..90dbe4504 100644 --- a/ui/js/page/publish.js +++ b/ui/js/page/publish.js @@ -19,7 +19,7 @@ var PublishPage = React.createClass({ return; } else { - // rewards.claimReward(rewards.TYPE_FIRST_PUBLISH) + rewards.claimReward(rewards.TYPE_FIRST_PUBLISH) } }); }, @@ -27,7 +27,7 @@ var PublishPage = React.createClass({ // 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) + rewards.claimReward(rewards.TYPE_FIRST_CHANNEL) this.setState({ channels: channels, ... channel ? {channel} : {} diff --git a/ui/js/page/show.js b/ui/js/page/show.js index f3b361976..c22dfdeee 100644 --- a/ui/js/page/show.js +++ b/ui/js/page/show.js @@ -62,7 +62,6 @@ 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}}}}}) => { - console.log({txid, nout, claim: {value: {stream: {metadata, source: {contentType}}}}} ); this.setState({ outpoint: txid + ':' + nout, metadata: metadata, @@ -86,17 +85,15 @@ let ShowPage = React.createClass({ } //
- const metadata = this.state.uriLookupComplete ? this.state.metadata : null, title = this.state.uriLookupComplete ? metadata.title : this._uri; - console.log(metadata); return (
- { this.props.contentType && this.props.contentType.startsWith('video/') ? -
diff --git a/ui/js/page/watch.js b/ui/js/page/watch.js index 45341ded1..bd80c30b2 100644 --- a/ui/js/page/watch.js +++ b/ui/js/page/watch.js @@ -1,5 +1,5 @@ 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 LoadScreen from '../component/load_screen.js' @@ -20,16 +20,41 @@ export let Video = 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() { + start: function() { + // lbry.getCostInfo(this.props.uri).then(({cost}) => { + // lbry.getBalance((balance) => { + // if (cost > balance) { + // this.setState({ + // modal: 'notEnoughCredits', + // loading: false, + // }); + // } else { + // this.startVideo(); + // } + // }); + // // }); + // + // You don't have enough LBRY credits to pay for this stream. + // lbry.get({uri: this.props.uri}).then((fileInfo) => { this._outpoint = fileInfo.outpoint; this.updateLoadStatus(); }); + this.setState({ + isPlaying: true + }) + }, + componentDidMount: function() { + if (this.props.autoplay) { + this.start() + } }, handleMouseMove: function() { if (this._controlsTimeout) { @@ -81,7 +106,6 @@ export let Video = React.createClass({ readyToPlay: true, mimeType: status.mime_type, }) - return const mediaFile = { createReadStream: function (opts) { // Return a readable stream that provides the bytes @@ -98,128 +122,16 @@ export let Video = React.createClass({ }, render: function() { return ( -
{ - !this.state.readyToPlay || true ? - this is the world's world loading message and we shipped our software with it anyway... seriously it is actually loading... it might take a while though : - +
{ + 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}
: + : +
+ + +
}
); } }) - -var WatchPage = 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, - }, - getInitialState: function() { - return { - downloadStarted: false, - readyToPlay: false, - loadStatusMessage: "Requesting stream", - mimeType: null, - controlsShown: false, - }; - }, - componentDidMount: function() { - lbry.get({uri: this.props.uri}).then((fileInfo) => { - this._outpoint = fileInfo.outpoint; - this.updateLoadStatus(); - }); - }, - handleBackClicked: function() { - history.back(); - }, - handleMouseMove: function() { - if (this._controlsTimeout) { - clearTimeout(this._controlsTimeout); - } - - if (!this.state.controlsShown) { - this.setState({ - controlsShown: true, - }); - } - this._controlsTimeout = setTimeout(() => { - if (!this.isMounted) { - return; - } - - this.setState({ - controlsShown: false, - }); - }, this._controlsHideDelay); - }, - handleMouseLeave: function() { - if (this._controlsTimeout) { - clearTimeout(this._controlsTimeout); - } - - if (this.state.controlsShown) { - this.setState({ - controlsShown: false, - }); - } - }, - updateLoadStatus: function() { - lbry.file_list({ - outpoint: this._outpoint, - full_status: true, - }).then(([status]) => { - if (!status || status.written_bytes == 0) { - // Download hasn't started yet, so update status message (if available) then try again - // TODO: Would be nice to check if we have the MOOV before starting playing - if (status) { - this.setState({ - loadStatusMessage: status.message - }); - } - setTimeout(() => { this.updateLoadStatus() }, 250); - } else { - this.setState({ - readyToPlay: true, - mimeType: status.mime_type, - }) - const mediaFile = { - createReadStream: function (opts) { - // Return a readable stream that provides the bytes - // between offsets "start" and "end" inclusive - console.log('Stream between ' + opts.start + ' and ' + opts.end + '.'); - return fs.createReadStream(status.download_path, opts) - } - }; - var elem = this.refs.video; - var videostream = VideoStream(mediaFile, elem); - elem.play(); - } - }); - }, - render: function() { - return ( - !this.state.readyToPlay - ? - :
- - {this.state.controlsShown - ?
-
- -
- -
- Back to LBRY -
-
-
-
- : null} -
- ); - } -}); - -export default WatchPage; diff --git a/ui/scss/component/_video.scss b/ui/scss/component/_video.scss index c6800088c..6ee8efe7e 100644 --- a/ui/scss/component/_video.scss +++ b/ui/scss/component/_video.scss @@ -1,12 +1,55 @@ video { - border: 1px solid red; - object-fill: contain; + object-fit: contain; + box-sizing: border-box; + max-height: 100%; + max-width: 100%; } +.video { + background: #000; + color: white; +} + + .video-embedded { - max-width: 100%; - height: 0; - padding-bottom: 63%; + $height-embedded: $width-page-constrained * 9 / 16; + max-width: $width-page-constrained; + max-height: $height-embedded; video { + height: 100%; } + &.video--hidden { + height: $height-embedded; + } + &.video--active { + background: none; + } +} + +.video__cover { + text-align: center; + height: 100%; + width: 100%; + img { + max-width: 100%; + max-height: 100%; + vertical-align: middle; + } + 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/_show.scss b/ui/scss/page/_show.scss index c0434ee24..48b82d065 100644 --- a/ui/scss/page/_show.scss +++ b/ui/scss/page/_show.scss @@ -3,4 +3,7 @@ .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; -- 2.45.2 From 300264e3644d1303e53c2dacbc4d345a8238dbf4 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Thu, 13 Apr 2017 19:39:59 -0400 Subject: [PATCH 073/158] Publish: add Terms of Service agreement field --- ui/js/page/publish.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/ui/js/page/publish.js b/ui/js/page/publish.js index 90dbe4504..0e6c0478c 100644 --- a/ui/js/page/publish.js +++ b/ui/js/page/publish.js @@ -8,7 +8,7 @@ import lbryio from '../lbryio.js'; import Modal from '../component/modal.js'; var PublishPage = React.createClass({ - _requiredFields: ['meta_title', 'name', 'bid'], + _requiredFields: ['meta_title', 'name', 'bid', 'tos_agree'], _requestPublishReward: function() { lbryio.call('reward', 'list', {}).then(function(userRewards) { @@ -314,6 +314,11 @@ 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.showError('LBRY channel names must be at least 4 characters in length.'); @@ -544,6 +549,17 @@ var PublishPage = React.createClass({
: '' }
+
+
+

Terms of Service

+
+
+ I agree to the + } type="checkbox" name="tos_agree" ref={(field) => { this.refs.tos_agree = field }} onChange={this.handleTOSChange} /> +
+
+
-- 2.45.2 From 9b4d8ec2662214d3466c824b0342189c1dbc2f98 Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Thu, 13 Apr 2017 19:43:17 -0400 Subject: [PATCH 074/158] more show/purchase work --- ui/js/component/auth.js | 2 +- ui/js/component/file-actions.js | 1 - ui/js/page/show.js | 17 +++--- ui/js/page/watch.js | 98 ++++++++++++++++++++++++++------- ui/scss/component/_video.scss | 8 +-- 5 files changed, 90 insertions(+), 36 deletions(-) diff --git a/ui/js/component/auth.js b/ui/js/component/auth.js index 5b3d4d46e..ace7c84df 100644 --- a/ui/js/component/auth.js +++ b/ui/js/component/auth.js @@ -137,7 +137,7 @@ const WelcomeStage = React.createClass({

About Your Reward

-

You earned a reward of 5 LBRY credits, or LBC.

+

You earned a reward of %award% LBRY credits, or LBC.

This reward will show in your Wallet momentarily, likely 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.

diff --git a/ui/js/component/file-actions.js b/ui/js/component/file-actions.js index 1c4c1650a..d12e4525f 100644 --- a/ui/js/component/file-actions.js +++ b/ui/js/component/file-actions.js @@ -27,7 +27,6 @@ let FileActionsRow = React.createClass({ deleteChecked: false, attemptingDownload: false, attemptingRemove: false, - affirmedPurchase: false } }, onFileInfoUpdate: function(fileInfo) { diff --git a/ui/js/page/show.js b/ui/js/page/show.js index c22dfdeee..a488bb4a2 100644 --- a/ui/js/page/show.js +++ b/ui/js/page/show.js @@ -80,21 +80,17 @@ let 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; + console.log(metadata); return (
{ this.state.contentType && this.state.contentType.startsWith('video/') ?
@@ -117,11 +113,12 @@ let ShowPage = React.createClass({ {metadata.description}
- : } -
-
- + :
}
+ { metadata ? +
+ +
: '' }
diff --git a/ui/js/page/watch.js b/ui/js/page/watch.js index bd80c30b2..3e3d163dc 100644 --- a/ui/js/page/watch.js +++ b/ui/js/page/watch.js @@ -2,11 +2,87 @@ import React from 'react'; 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 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, + }); + } + }); + 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 @@ -27,22 +103,7 @@ export let Video = React.createClass({ controlsShown: false, }; }, - start: function() { - // lbry.getCostInfo(this.props.uri).then(({cost}) => { - // lbry.getBalance((balance) => { - // if (cost > balance) { - // this.setState({ - // modal: 'notEnoughCredits', - // loading: false, - // }); - // } else { - // this.startVideo(); - // } - // }); - // // }); - // - // You don't have enough LBRY credits to pay for this stream. - // + onGet: function() { lbry.get({uri: this.props.uri}).then((fileInfo) => { this._outpoint = fileInfo.outpoint; this.updateLoadStatus(); @@ -127,9 +188,8 @@ export let Video = React.createClass({ !this.state.readyToPlay ? this is the world's world loading screen and we shipped our software with it anyway...

{this.state.loadStatusMessage}
: : -
- - +
+
}
); diff --git a/ui/scss/component/_video.scss b/ui/scss/component/_video.scss index 6ee8efe7e..09775ca39 100644 --- a/ui/scss/component/_video.scss +++ b/ui/scss/component/_video.scss @@ -30,11 +30,9 @@ video { text-align: center; height: 100%; width: 100%; - img { - max-width: 100%; - max-height: 100%; - vertical-align: middle; - } + background-size: auto 100%; + background-position: center center; + background-repeat: no-repeat; position: relative; &:hover { .video__play-button { @include absolute-center(); } -- 2.45.2 From d926967e49dd76e786755794ed66ad8e04ed11d0 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Thu, 13 Apr 2017 20:56:58 -0400 Subject: [PATCH 075/158] Correct and simplify metadata processing in dummy pending publishes --- ui/js/component/file-actions.js | 2 +- ui/js/lbry.js | 18 +++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/ui/js/component/file-actions.js b/ui/js/component/file-actions.js index d12e4525f..d16e61bab 100644 --- a/ui/js/component/file-actions.js +++ b/ui/js/component/file-actions.js @@ -182,7 +182,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?

diff --git a/ui/js/lbry.js b/ui/js/lbry.js index b09aabe1f..9e8b1565e 100644 --- a/ui/js/lbry.js +++ b/ui/js/lbry.js @@ -11,7 +11,12 @@ const menu = remote.require('./menu/main-menu'); * needed to make a dummy claim or file info object. */ function savePendingPublish({name, channel_name}) { - const lbryUri = uri.buildLbryUri({name, channel_name}, false); + 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 = { name, channel_name, @@ -61,13 +66,13 @@ function getPendingPublish({name, channel_name, outpoint}) { } function pendingPublishToDummyClaim({channel_name, name, outpoint, claim_id, txid, nout}) { - return {name, outpoint, claim_id, txid, nout, ... channel_name ? {channel_name} : {}}; + return {name, outpoint, claim_id, txid, nout, channel_name}; } function pendingPublishToDummyFileInfo({name, outpoint, claim_id}) { - return {name, outpoint, claim_id, null}; + return {name, outpoint, claim_id, metadata: null}; } - +window.pptdfi = pendingPublishToDummyFileInfo; let lbry = { isConnected: false, @@ -316,14 +321,13 @@ lbry.publish = function(params, fileListedCallback, publishedCallback, errorCall returnedPending = true; if (publishedCallback) { - const {name, channel_name} = params; - savePendingPublish({name, ... channel_name ? {channel_name} : {}}); + savePendingPublish({name: params.name, channel_name: params.channel_name}); publishedCallback(true); } if (fileListedCallback) { const {name, channel_name} = params; - savePendingPublish({name, ... channel_name ? {channel_name} : {}}); + savePendingPublish({name: params.name, channel_name: params.channel_name}); fileListedCallback(true); } }, 2000); -- 2.45.2 From 9532f8b29a69dea508685c93fbde0383e94140f6 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Fri, 14 Apr 2017 13:44:06 -0400 Subject: [PATCH 076/158] Publish: improve state management of names Save the new name as soon as it's entered instead of after resolution, so that it's possible to publish before the results from resolve() and claim_list_mine() come back. --- ui/js/page/publish.js | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/ui/js/page/publish.js b/ui/js/page/publish.js index 0e6c0478c..ea3bdb00e 100644 --- a/ui/js/page/publish.js +++ b/ui/js/page/publish.js @@ -140,11 +140,11 @@ var PublishPage = React.createClass({ channel: 'anonymous', newChannelName: '@', newChannelBid: 10, - nameResolved: false, + nameResolved: null, + myClaimExists: null, topClaimValue: 0.0, myClaimValue: 0.0, myClaimMetadata: null, - myClaimExists: null, copyrightNotice: '', otherLicenseDescription: '', otherLicenseUrl: '', @@ -189,31 +189,35 @@ var PublishPage = React.createClass({ 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, @@ -374,13 +378,13 @@ var PublishPage = React.createClass({ getNameBidHelpText: function() { if (!this.state.name) { return "Select a URL for this publish."; - } else if (!this.state.nameResolved) { + } 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 perminent URL for any amount. + is required to win {this.state.name}. However, you can still get a permanent URL for any amount. } else { return ''; } @@ -447,7 +451,7 @@ var PublishPage = React.createClass({

Access

- How much does this content cost ? + How much does this content cost?
-- 2.45.2 From 8d67d36ad9607c3c2997b1f162e434bd38e004c5 Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Fri, 14 Apr 2017 18:23:42 -0400 Subject: [PATCH 077/158] views, rewards, fixes for no api, other fixes --- app/main.js | 2 +- ui/js/component/auth.js | 12 +++++++----- ui/js/component/file-actions.js | 2 -- ui/js/component/form.js | 2 +- ui/js/component/link.js | 11 ++++++----- ui/js/lbryio.js | 15 ++++++++++++++- ui/js/main.js | 3 +-- ui/js/page/discover.js | 25 ++++++++++++++++--------- ui/js/page/publish.js | 2 +- ui/js/page/rewards.js | 5 ++++- ui/js/page/show.js | 3 +-- ui/js/page/watch.js | 7 +++++++ ui/js/rewards.js | 4 ++++ 13 files changed, 63 insertions(+), 30 deletions(-) diff --git a/app/main.js b/app/main.js index e2fad0905..4b9a4664f 100644 --- a/app/main.js +++ b/app/main.js @@ -62,7 +62,7 @@ function getPidsForProcessName(name) { } function createWindow () { - win = new BrowserWindow({backgroundColor: '#155B4A', minWidth: 800, minHeight: 1000 }) //$color-primary + win = new BrowserWindow({backgroundColor: '#155B4A', minWidth: 800, minHeight: 600 }) //$color-primary win.maximize() win.webContents.openDevTools(); win.loadURL(`file://${__dirname}/dist/index.html`) diff --git a/ui/js/component/auth.js b/ui/js/component/auth.js index ace7c84df..197fc0b38 100644 --- a/ui/js/component/auth.js +++ b/ui/js/component/auth.js @@ -5,6 +5,7 @@ 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'; @@ -114,9 +115,10 @@ const WelcomeStage = React.createClass({ } }, onRewardClaim: function(reward) { + console.log(reward); this.setState({ hasReward: true, - rewardAmount: reward + rewardAmount: reward.amount }) }, render: function() { @@ -127,8 +129,8 @@ const WelcomeStage = React.createClass({

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 like nothing else. Using blockchain and decentralization, LBRY is controlled by its users -- you -- and no one else.

-

Thanks for being a part of it! Here's a nickel, kid.

+

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.

@@ -137,8 +139,8 @@ const WelcomeStage = React.createClass({

About Your Reward

-

You earned a reward of %award% LBRY credits, or LBC.

-

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

+

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.

diff --git a/ui/js/component/file-actions.js b/ui/js/component/file-actions.js index d16e61bab..fff8191e2 100644 --- a/ui/js/component/file-actions.js +++ b/ui/js/component/file-actions.js @@ -43,8 +43,6 @@ let FileActionsRow = React.createClass({ attemptingRemove: false }); lbry.getCostInfo(this.props.uri).then(({cost}) => { - console.log(cost); - console.log(this.props.uri); lbry.getBalance((balance) => { if (cost > balance) { this.setState({ diff --git a/ui/js/component/form.js b/ui/js/component/form.js index ef8a7169b..f75310c92 100644 --- a/ui/js/component/form.js +++ b/ui/js/component/form.js @@ -96,7 +96,7 @@ export let FormField = React.createClass({ export let FormRow = React.createClass({ _fieldRequiredText: 'This field is required', propTypes: { - label: React.PropTypes.string, + label: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.element]) // helper: React.PropTypes.html, }, getInitialState: function() { diff --git a/ui/js/component/link.js b/ui/js/component/link.js index 88e60bf7e..55c0060dd 100644 --- a/ui/js/component/link.js +++ b/ui/js/component/link.js @@ -92,20 +92,20 @@ export let RewardLink = React.createClass({ this.setState({ pending: true }) - rewards.claimReward(this.props.type).then(function(reward) { + rewards.claimReward(this.props.type).then((reward) => { this.setState({ pending: false, errorMessage: null }) if (this.props.onRewardClaim) { - this.props.onRewardClaim(); + this.props.onRewardClaim(reward); } - }.bind(this)).catch(function(error) { + }).catch((error) => { this.setState({ errorMessage: error.message, pending: false }) - }.bind(this)) + }) }, clearError: function() { if (this.props.onRewardFailure) { @@ -120,7 +120,8 @@ export let RewardLink = React.createClass({
{this.props.claimed ? Reward claimed. - : } + : } {this.state.errorMessage ? {this.state.errorMessage} diff --git a/ui/js/lbryio.js b/ui/js/lbryio.js index 06dbd46d9..53ba92f8a 100644 --- a/ui/js/lbryio.js +++ b/ui/js/lbryio.js @@ -6,7 +6,8 @@ const querystring = require('querystring'); const lbryio = { _accessToken: getLocal('accessToken'), _authenticationPromise: null, - _user : null + _user : null, + enabled: false }; const CONNECTION_STRING = 'http://localhost:8080/'; @@ -25,6 +26,10 @@ const mocks = { lbryio.call = function(resource, action, params={}, method='get') { return new Promise((resolve, reject) => { + if (!lbryio.enabled) { + reject(new Error("LBRY interal API is disabled")) + return + } /* temp code for mocks */ if (`${resource}.${action}` in mocks) { resolve(mocks[`${resource}.${action}`](params)); @@ -90,6 +95,14 @@ lbryio.setAccessToken = (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}) => { diff --git a/ui/js/main.js b/ui/js/main.js index d31b4c7d5..610ca8594 100644 --- a/ui/js/main.js +++ b/ui/js/main.js @@ -30,8 +30,7 @@ let init = function() { 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(
, canvas) + ReactDOM.render(
{ lbryio.enabled ? : '' }
, canvas) } if (window.sessionStorage.getItem('loaded') == 'y') { diff --git a/ui/js/page/discover.js b/ui/js/page/discover.js index b3d427cfe..8aadd2df4 100644 --- a/ui/js/page/discover.js +++ b/ui/js/page/discover.js @@ -81,6 +81,7 @@ var FeaturedContent = React.createClass({ getInitialState: function() { return { featuredUris: {}, + failed: false }; }, componentWillMount: function() { @@ -92,19 +93,25 @@ var FeaturedContent = React.createClass({ } }) this.setState({ featuredUris: featuredUris }); + }, () => { + this.setState({ + failed: true + }) }); }, render: function() { return ( -
- { - Object.keys(this.state.featuredUris).map(function(category) { - return this.state.featuredUris[category].length ? - : - ''; - }.bind(this)) - } -
+ this.state.failed ? +
Failed to load landing content.
: +
+ { + Object.keys(this.state.featuredUris).map(function(category) { + return this.state.featuredUris[category].length ? + : + ''; + }.bind(this)) + } +
); } }); diff --git a/ui/js/page/publish.js b/ui/js/page/publish.js index ea3bdb00e..3dfc8ac1e 100644 --- a/ui/js/page/publish.js +++ b/ui/js/page/publish.js @@ -27,7 +27,7 @@ var PublishPage = React.createClass({ // 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) + rewards.claimReward(rewards.TYPE_FIRST_CHANNEL).then(() => {}, () => {}) this.setState({ channels: channels, ... channel ? {channel} : {} diff --git a/ui/js/page/rewards.js b/ui/js/page/rewards.js index 62c5d0497..18e936aee 100644 --- a/ui/js/page/rewards.js +++ b/ui/js/page/rewards.js @@ -42,6 +42,7 @@ var RewardsPage = React.createClass({ getInitialState: function() { return { userRewards: null, + failed: null }; }, loadRewards: function() { @@ -49,6 +50,8 @@ var RewardsPage = React.createClass({ this.setState({ userRewards: userRewards, }); + }, () => { + this.setState({failed: true }) }); }, render: function() { @@ -56,7 +59,7 @@ var RewardsPage = React.createClass({
{!this.state.userRewards - ? null + ? (this.state.failed ?
Failed to load rewards.
: '') : this.state.userRewards.map(({RewardType, RewardTitle, RewardDescription, TransactionID, RewardAmount}) => { return ; })} diff --git a/ui/js/page/show.js b/ui/js/page/show.js index a488bb4a2..5ad238ae8 100644 --- a/ui/js/page/show.js +++ b/ui/js/page/show.js @@ -83,8 +83,7 @@ let ShowPage = React.createClass({ const metadata = this.state.uriLookupComplete ? this.state.metadata : null, title = this.state.uriLookupComplete ? metadata.title : this._uri; - - console.log(metadata); + return (
diff --git a/ui/js/page/watch.js b/ui/js/page/watch.js index 3e3d163dc..833cd5775 100644 --- a/ui/js/page/watch.js +++ b/ui/js/page/watch.js @@ -3,6 +3,7 @@ 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 LoadScreen from '../component/load_screen.js' const fs = require('fs'); @@ -25,6 +26,12 @@ export let WatchLink = React.createClass({ attemptingDownload: false, }); } + + lbryio.call('file', 'view', { + uri: this.props.uri, + outpoint: streamInfo.outpoint, + claimId: streamInfo.claim_id + }) }); if (this.props.onGet) { this.props.onGet() diff --git a/ui/js/rewards.js b/ui/js/rewards.js index ec043de02..8b66ce371 100644 --- a/ui/js/rewards.js +++ b/ui/js/rewards.js @@ -24,6 +24,10 @@ rewards.TYPE_NEW_DEVELOPER = "new_developer", 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), -- 2.45.2 From e9c8abd3071f2763d8a627e38ff7fb7a1d54b99b Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Mon, 17 Apr 2017 08:27:39 -0400 Subject: [PATCH 078/158] rewards flow changes --- ui/js/component/common.js | 5 ++- ui/js/component/file-tile.js | 10 ++--- ui/js/lbryio.js | 2 +- ui/js/page/file-list.js | 22 ++++++++++ ui/js/page/publish.js | 16 +------ ui/js/page/rewards.js | 1 + ui/js/page/show.js | 2 +- ui/js/page/watch.js | 4 ++ ui/js/rewards.js | 73 ++++++++++++++++++++++--------- ui/scss/_global.scss | 5 ++- ui/scss/component/_file-tile.scss | 9 +++- ui/scss/component/_video.scss | 8 ++-- 12 files changed, 106 insertions(+), 51 deletions(-) diff --git a/ui/js/component/common.js b/ui/js/component/common.js index 78bb6522e..d8b0fc052 100644 --- a/ui/js/component/common.js +++ b/ui/js/component/common.js @@ -186,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/file-tile.js b/ui/js/component/file-tile.js index 172dea636..b2477a7be 100644 --- a/ui/js/component/file-tile.js +++ b/ui/js/component/file-tile.js @@ -81,14 +81,14 @@ export let FileTileStream = React.createClass({ return (
-
+
- { !this.props.hidePrice - ? - : null}

- + {isConfirmed ? metadata.description : This file is pending confirmation.} diff --git a/ui/js/lbryio.js b/ui/js/lbryio.js index 53ba92f8a..085c3097b 100644 --- a/ui/js/lbryio.js +++ b/ui/js/lbryio.js @@ -7,7 +7,7 @@ const lbryio = { _accessToken: getLocal('accessToken'), _authenticationPromise: null, _user : null, - enabled: false + enabled: true }; const CONNECTION_STRING = 'http://localhost:8080/'; diff --git a/ui/js/page/file-list.js b/ui/js/page/file-list.js index 3c337565f..e746f9d77 100644 --- a/ui/js/page/file-list.js +++ b/ui/js/page/file-list.js @@ -4,6 +4,8 @@ import uri from '../uri.js'; import {Link} from '../component/link.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 ( diff --git a/ui/js/page/publish.js b/ui/js/page/publish.js index 3dfc8ac1e..b424e07ee 100644 --- a/ui/js/page/publish.js +++ b/ui/js/page/publish.js @@ -10,19 +10,6 @@ import Modal from '../component/modal.js'; var PublishPage = React.createClass({ _requiredFields: ['meta_title', 'name', 'bid', 'tos_agree'], - _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) - } - }); - }, _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) @@ -361,7 +348,6 @@ var PublishPage = React.createClass({ }, componentWillMount: function() { this._updateChannelList(); - // this._requestPublishReward(); }, componentDidMount: function() { document.title = "Publish"; @@ -574,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/rewards.js b/ui/js/page/rewards.js index 18e936aee..0e0ac80e4 100644 --- a/ui/js/page/rewards.js +++ b/ui/js/page/rewards.js @@ -55,6 +55,7 @@ var RewardsPage = React.createClass({ }); }, render: function() { + console.log(this.state.userRewards); return (
diff --git a/ui/js/page/show.js b/ui/js/page/show.js index 5ad238ae8..d38e9dc75 100644 --- a/ui/js/page/show.js +++ b/ui/js/page/show.js @@ -83,7 +83,7 @@ let ShowPage = React.createClass({ const metadata = this.state.uriLookupComplete ? this.state.metadata : null, title = this.state.uriLookupComplete ? metadata.title : this._uri; - + return (
diff --git a/ui/js/page/watch.js b/ui/js/page/watch.js index 833cd5775..76e96cada 100644 --- a/ui/js/page/watch.js +++ b/ui/js/page/watch.js @@ -4,6 +4,7 @@ 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'); @@ -182,6 +183,9 @@ export let Video = React.createClass({ return fs.createReadStream(status.download_path, opts) } }; + + rewards.claimNextPurchaseReward() + var elem = this.refs.video; var videostream = VideoStream(mediaFile, elem); elem.play(); diff --git a/ui/js/rewards.js b/ui/js/rewards.js index 8b66ce371..168070627 100644 --- a/ui/js/rewards.js +++ b/ui/js/rewards.js @@ -3,12 +3,13 @@ import lbryio from './lbryio.js'; function rewardMessage(type, amount) { return { - new_developer: "You received ${amount} for registering as a new developer.", - new_user: `You received ${amount} LBC new user reward.`, - confirm_email: "You received ${amount} LBC for verifying your email address.", - first_channel: "You received ${amount} LBC for creating a publisher identity.", - first_purchase: "You received ${amount} LBC for making your first purchase.", - first_publish: "You received ${amount} LBC for making your first publication.", + 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]; } @@ -17,8 +18,9 @@ const rewards = {}; rewards.TYPE_NEW_DEVELOPER = "new_developer", rewards.TYPE_NEW_USER = "new_user", rewards.TYPE_CONFIRM_EMAIL = "confirm_email", - rewards.TYPE_FIRST_CHANNEL = "first_channel", - rewards.TYPE_FIRST_PURCHASE = "first_purchase", + 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) { @@ -62,9 +64,13 @@ rewards.claimReward = function (type) { switch (type) { case rewards.TYPE_FIRST_CHANNEL: - lbry.claim_list_mine().then(function(channels) { - if (channels.length) { - params.transaction_id = channels[0].txid; + 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.")) @@ -72,18 +78,24 @@ rewards.claimReward = function (type) { }).catch(reject) break; - case 'first_purchase': - // lbry.claim_list_mine().then(function(channels) { - // if (channels.length) { - // requestReward(resolve, reject, {transaction_id: channels[0].txid}) - // } - // }).catch(reject) - break; - - case 'first_channel': - //params.transaction_id = RelevantTransactionID; + 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); } @@ -91,4 +103,23 @@ rewards.claimReward = function (type) { }); } +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/scss/_global.scss b/ui/scss/_global.scss index 68533b206..d829d8245 100644 --- a/ui/scss/_global.scss +++ b/ui/scss/_global.scss @@ -28,10 +28,11 @@ $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; - -$width-page-constrained: 800px; +$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); diff --git a/ui/scss/component/_file-tile.scss b/ui/scss/component/_file-tile.scss index 9ff9b2c75..eb768bbf0 100644 --- a/ui/scss/component/_file-tile.scss +++ b/ui/scss/component/_file-tile.scss @@ -1,6 +1,8 @@ @import "../global"; +$height-file-tile: $spacing-vertical * 8; .file-tile__row { + height: $height-file-tile; .credit-amount { float: right; } @@ -12,12 +14,17 @@ .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; diff --git a/ui/scss/component/_video.scss b/ui/scss/component/_video.scss index 09775ca39..c41815dca 100644 --- a/ui/scss/component/_video.scss +++ b/ui/scss/component/_video.scss @@ -12,17 +12,17 @@ video { .video-embedded { - $height-embedded: $width-page-constrained * 9 / 16; max-width: $width-page-constrained; - max-height: $height-embedded; + max-height: $height-video-embedded; + height: $height-video-embedded; video { height: 100%; } &.video--hidden { - height: $height-embedded; + height: $height-video-embedded; } &.video--active { - background: none; + /*background: none;*/ } } -- 2.45.2 From 581be8c429ac5b677d1798689e6124fb79aac4bc Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Mon, 17 Apr 2017 09:06:13 -0400 Subject: [PATCH 079/158] disable lbryio --- ui/js/lbryio.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/js/lbryio.js b/ui/js/lbryio.js index 085c3097b..53ba92f8a 100644 --- a/ui/js/lbryio.js +++ b/ui/js/lbryio.js @@ -7,7 +7,7 @@ const lbryio = { _accessToken: getLocal('accessToken'), _authenticationPromise: null, _user : null, - enabled: true + enabled: false }; const CONNECTION_STRING = 'http://localhost:8080/'; -- 2.45.2 From a937534a8bf9220dabadf155550ccdbb4f344724 Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Mon, 17 Apr 2017 09:46:41 -0400 Subject: [PATCH 080/158] disable lbryio --- ui/js/lbryio.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/js/lbryio.js b/ui/js/lbryio.js index 53ba92f8a..53b49271b 100644 --- a/ui/js/lbryio.js +++ b/ui/js/lbryio.js @@ -8,7 +8,7 @@ const lbryio = { _authenticationPromise: null, _user : null, enabled: false -}; +}; const CONNECTION_STRING = 'http://localhost:8080/'; -- 2.45.2 From 6603bb4cd26faaaeb1d1aae1cf4b67ae6a85b984 Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Mon, 17 Apr 2017 10:01:33 -0400 Subject: [PATCH 081/158] allow discover if lbryio is disabled --- ui/js/lbryio.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/js/lbryio.js b/ui/js/lbryio.js index 53b49271b..d662354e6 100644 --- a/ui/js/lbryio.js +++ b/ui/js/lbryio.js @@ -8,9 +8,9 @@ const lbryio = { _authenticationPromise: null, _user : null, enabled: false -}; +}; -const CONNECTION_STRING = 'http://localhost:8080/'; +const CONNECTION_STRING = 'https://api.lbry.io/'; const mocks = { 'reward_type.get': ({name}) => { @@ -26,7 +26,7 @@ const mocks = { lbryio.call = function(resource, action, params={}, method='get') { return new Promise((resolve, reject) => { - if (!lbryio.enabled) { + if (!lbryio.enabled && (resource != 'discover' || action != 'list')) { reject(new Error("LBRY interal API is disabled")) return } -- 2.45.2 From deb214224e1fd94edc0879947dbb2f372ce8528a Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Mon, 17 Apr 2017 10:05:11 -0400 Subject: [PATCH 082/158] update changelog --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a0ad0c72..947773030 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,11 +16,13 @@ Web UI version numbers should always match the corresponding version of LBRY App * New-style API calls return promises instead of using callbacks * Wherever possible, use outpoints for unique IDs instead of names or SD hashes * 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 ### Changed * Update process now easier and more reliable * Cleaned up shutdown logic - * + * Support lbry v0.10 API signatures ### Fixed * Fix Watch page and progress bars for new API changes -- 2.45.2 From be339d904c3033ebc23163eea824ce01175f4b25 Mon Sep 17 00:00:00 2001 From: Alex Grintsvayg Date: Mon, 17 Apr 2017 10:45:56 -0400 Subject: [PATCH 083/158] update submodules --- lbry | 2 +- lbryum | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lbry b/lbry index 043e2d0ab..41204f7cb 160000 --- a/lbry +++ b/lbry @@ -1 +1 @@ -Subproject commit 043e2d0ab96030468d53d02e311fd848f35c2dc1 +Subproject commit 41204f7cbe65243d08d7c12e5a2a0399cf741625 diff --git a/lbryum b/lbryum index 121bda396..39ace3737 160000 --- a/lbryum +++ b/lbryum @@ -1 +1 @@ -Subproject commit 121bda3963ee94f0c9c027813c55b71b38219739 +Subproject commit 39ace3737509ff2b09fabaaa64d1525843de1325 -- 2.45.2 From 683849046b30c275cd229ede85ab4954030cb10a Mon Sep 17 00:00:00 2001 From: Alex Grintsvayg Date: Mon, 17 Apr 2017 10:46:09 -0400 Subject: [PATCH 084/158] enable early access in build --- build/set_build.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/build/set_build.py b/build/set_build.py index 3467154ab..32e92d0e2 100644 --- a/build/set_build.py +++ b/build/set_build.py @@ -1,15 +1,28 @@ """Set the build version to be 'dev', 'qa', 'rc', 'release'""" +from __future__ import print_function + import os.path import re import subprocess import sys +import fileinput def main(): build = get_build() with open(os.path.join('lbry', 'lbrynet', 'build_type.py'), 'w') as f: f.write('BUILD = "{}"'.format(build)) + set_early_access() + + +def set_early_access(): + filename = os.path.abspath(os.path.join(os.path.abspath(__file__), '..', '..', 'ui', 'js', 'lbryio.js')) + for line in fileinput.input(filename, inplace=True): + if line.startswith(' enabled: false'): + print(' enabled: true') + else: + print(line, end='') def get_build(): -- 2.45.2 From 501f26e210d714c729ac94a85d5712498052f1de Mon Sep 17 00:00:00 2001 From: Alex Grintsvayg Date: Mon, 17 Apr 2017 10:46:37 -0400 Subject: [PATCH 085/158] =?UTF-8?q?Bump=20version:=200.10.0rc1=20=E2=86=92?= =?UTF-8?q?=200.10.0rc2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- app/package.json | 2 +- lbry | 2 +- ui/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index d9e44ac61..7a3d8fc6d 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.10.0rc1 +current_version = 0.10.0rc2 commit = True tag = True parse = (?P\d+)\.(?P\d+)\.(?P\d+)((?P[a-z]+)(?P\d+))? diff --git a/app/package.json b/app/package.json index d1b821b05..b116b0fc3 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "LBRY", - "version": "0.10.0rc1", + "version": "0.10.0rc2", "main": "main.js", "description": "LBRY is a fully decentralized, open-source protocol facilitating the discovery, access, and (sometimes) purchase of data.", "author": { diff --git a/lbry b/lbry index 41204f7cb..6c0f805ec 160000 --- a/lbry +++ b/lbry @@ -1 +1 @@ -Subproject commit 41204f7cbe65243d08d7c12e5a2a0399cf741625 +Subproject commit 6c0f805ec3023ab7c5853ec7d92823355393005c diff --git a/ui/package.json b/ui/package.json index cea9371a3..8e40ea6ca 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "lbry-web-ui", - "version": "0.10.0rc1", + "version": "0.10.0rc2", "description": "LBRY UI", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" -- 2.45.2 From 4e1102cf58371cb91be7667b2ae5b16f50d333af Mon Sep 17 00:00:00 2001 From: Alex Grintsvayg Date: Mon, 17 Apr 2017 12:37:25 -0400 Subject: [PATCH 086/158] new lighthouse server --- ui/js/lighthouse.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ui/js/lighthouse.js b/ui/js/lighthouse.js index faa5b5b67..9aa51747d 100644 --- a/ui/js/lighthouse.js +++ b/ui/js/lighthouse.js @@ -4,9 +4,7 @@ import jsonrpc from './jsonrpc.js'; const queryTimeout = 3000; const maxQueryTries = 2; const defaultServers = [ - 'http://lighthouse4.lbry.io:50005', - 'http://lighthouse5.lbry.io:50005', - 'http://lighthouse6.lbry.io:50005', + 'http://lighthouse7.lbry.io:50005', ]; const path = '/'; -- 2.45.2 From 1aa78c80291dee6e693c901dd6f748961f18fa63 Mon Sep 17 00:00:00 2001 From: Alex Grintsvayg Date: Mon, 17 Apr 2017 12:37:37 -0400 Subject: [PATCH 087/158] =?UTF-8?q?Bump=20version:=200.10.0rc2=20=E2=86=92?= =?UTF-8?q?=200.10.0rc3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- app/package.json | 2 +- lbry | 2 +- ui/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 7a3d8fc6d..3ff1ab12f 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.10.0rc2 +current_version = 0.10.0rc3 commit = True tag = True parse = (?P\d+)\.(?P\d+)\.(?P\d+)((?P[a-z]+)(?P\d+))? diff --git a/app/package.json b/app/package.json index b116b0fc3..13ff19e05 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "LBRY", - "version": "0.10.0rc2", + "version": "0.10.0rc3", "main": "main.js", "description": "LBRY is a fully decentralized, open-source protocol facilitating the discovery, access, and (sometimes) purchase of data.", "author": { diff --git a/lbry b/lbry index 6c0f805ec..af455ff52 160000 --- a/lbry +++ b/lbry @@ -1 +1 @@ -Subproject commit 6c0f805ec3023ab7c5853ec7d92823355393005c +Subproject commit af455ff52ecc81be2229f1061318d607c4cc047d diff --git a/ui/package.json b/ui/package.json index 8e40ea6ca..be3f3ec83 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "lbry-web-ui", - "version": "0.10.0rc2", + "version": "0.10.0rc3", "description": "LBRY UI", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" -- 2.45.2 From 0b17ca60433538ef9b1c125f4c2b4510778ef5d4 Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Mon, 17 Apr 2017 14:38:53 -0400 Subject: [PATCH 088/158] stop opening dev toolbar --- app/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/main.js b/app/main.js index 4b9a4664f..8a66a4403 100644 --- a/app/main.js +++ b/app/main.js @@ -64,7 +64,7 @@ function getPidsForProcessName(name) { function createWindow () { 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 -- 2.45.2 From c252c757b0ce57794ddd6ac948dc33e51f19cf7d Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Mon, 17 Apr 2017 16:45:51 -0400 Subject: [PATCH 089/158] minor changes --- lbry | 2 +- lbryum | 2 +- ui/js/component/auth.js | 10 ++++++---- ui/js/component/file-actions.js | 2 +- ui/js/lbryio.js | 9 ++++----- ui/js/page/watch.js | 4 ++-- ui/scss/component/_video.scss | 9 ++++++--- ui/scss/page/_watch.scss | 2 +- 8 files changed, 22 insertions(+), 18 deletions(-) diff --git a/lbry b/lbry index af455ff52..043e2d0ab 160000 --- a/lbry +++ b/lbry @@ -1 +1 @@ -Subproject commit af455ff52ecc81be2229f1061318d607c4cc047d +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/component/auth.js b/ui/js/component/auth.js index 197fc0b38..6cd547633 100644 --- a/ui/js/component/auth.js +++ b/ui/js/component/auth.js @@ -41,7 +41,7 @@ const SubmitEmailStage = React.createClass({ return (
- { this._emailRow = ref }} type="text" label="Email" placeholder="admin@toplbryfan.com" + { this._emailRow = ref }} type="text" label="Email" placeholder="scrwvwls@lbry.io" name="email" value={this.state.email} onChange={this.handleEmailChanged} />
@@ -128,9 +128,9 @@ const WelcomeStage = React.createClass({

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.

+

Up top, LBRY is similar to popular media sites.

+

Below, LBRY is controlled by users -- you -- via blockchain and decentralization.

+

Thank you for making content freedom possible! Here's a nickel, kid.

@@ -143,6 +143,7 @@ const WelcomeStage = React.createClass({

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.

+

Finally, know that LBRY is a beta and that it earns the name.

); @@ -156,6 +157,7 @@ const ErrorStage = React.createClass({

An error was encountered that we cannot continue from.

At least we're earning the name beta.

+ { this.props.errorText ?

Message: {this.props.errorText}

: '' } { window.location.reload() } } />
); diff --git a/ui/js/component/file-actions.js b/ui/js/component/file-actions.js index fff8191e2..2f2a4074d 100644 --- a/ui/js/component/file-actions.js +++ b/ui/js/component/file-actions.js @@ -167,7 +167,7 @@ let FileActionsRow = React.createClass({ : '' } - Confirm you want to purchase this bro. + Do you want to purchase this? diff --git a/ui/js/lbryio.js b/ui/js/lbryio.js index d662354e6..99fcd0e0d 100644 --- a/ui/js/lbryio.js +++ b/ui/js/lbryio.js @@ -10,7 +10,7 @@ const lbryio = { enabled: false }; -const CONNECTION_STRING = 'https://api.lbry.io/'; +const CONNECTION_STRING = 'http://localhost:8080/'; const mocks = { 'reward_type.get': ({name}) => { @@ -27,7 +27,7 @@ const mocks = { 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")) + reject(new Error("LBRY internal API is disabled")) return } /* temp code for mocks */ @@ -105,10 +105,9 @@ lbryio.authenticate = function() { } if (lbryio._authenticationPromise === null) { lbryio._authenticationPromise = new Promise((resolve, reject) => { - lbry.status().then(({installation_id}) => { + lbry.status().then((response) => { - //temp hack for installation_ids being wrong - installation_id += "Y".repeat(96 - installation_id.length) + let installation_id = response.installation_id; function setCurrentUser() { lbryio.call('user', 'me').then((data) => { diff --git a/ui/js/page/watch.js b/ui/js/page/watch.js index 76e96cada..315e19a9c 100644 --- a/ui/js/page/watch.js +++ b/ui/js/page/watch.js @@ -32,7 +32,7 @@ export let WatchLink = React.createClass({ uri: this.props.uri, outpoint: streamInfo.outpoint, claimId: streamInfo.claim_id - }) + }).catch(() => {}) }); if (this.props.onGet) { this.props.onGet() @@ -84,7 +84,7 @@ export let WatchLink = React.createClass({ - Confirm you want to purchase this bro. + Do you want to purchase this?
); } diff --git a/ui/scss/component/_video.scss b/ui/scss/component/_video.scss index c41815dca..9dd95ebe9 100644 --- a/ui/scss/component/_video.scss +++ b/ui/scss/component/_video.scss @@ -34,9 +34,7 @@ video { background-position: center center; background-repeat: no-repeat; position: relative; - &:hover { - .video__play-button { @include absolute-center(); } - } + .video__play-button { @include absolute-center(); } } .video__play-button { position: absolute; @@ -48,6 +46,11 @@ video { color: white; z-index: 1; background: $color-black-transparent; + opacity: 0.6; left: 0; top: 0; + &:hover { + opacity: 1; + transition: opacity $transition-standard; + } } \ No newline at end of file diff --git a/ui/scss/page/_watch.scss b/ui/scss/page/_watch.scss index 59a614d31..6ed5459ae 100644 --- a/ui/scss/page/_watch.scss +++ b/ui/scss/page/_watch.scss @@ -20,7 +20,7 @@ } .video__back-label { - opacity: 0; + opacity: 0.5; transition: opacity 100ms ease-in; } -- 2.45.2 From 7644566ef5ba9630cc88fd342489bebde36c3d06 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Mon, 17 Apr 2017 03:59:06 -0400 Subject: [PATCH 090/158] 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; -- 2.45.2 From 5fe9f076ebf815d1242dcd93d8ff53b5f5e59771 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Mon, 17 Apr 2017 05:10:45 -0400 Subject: [PATCH 091/158] 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'; -- 2.45.2 From 94b60beebc90c985292a23797075a115dfa4dffe Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Mon, 17 Apr 2017 05:18:41 -0400 Subject: [PATCH 092/158] 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}
: :
- +
}
); -- 2.45.2 From bbab1f064a2a7944c9a787eb894c5a97ae1675f4 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Mon, 17 Apr 2017 06:03:00 -0400 Subject: [PATCH 093/158] 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 ?
-- 2.45.2 From cf1107050dbcd862466b3cacdcc0147e6fdca1de Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Mon, 17 Apr 2017 06:05:36 -0400 Subject: [PATCH 094/158] 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}
: :
- +
}
); -- 2.45.2 From 9b4ebbab0f7d89b274e01ccfaf2a376a6e81b835 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Mon, 17 Apr 2017 12:18:33 -0400 Subject: [PATCH 095/158] 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)); -- 2.45.2 From 757ab11779e7db9c4854c139bd3892c81cd6c1ff Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Mon, 17 Apr 2017 16:22:08 -0400 Subject: [PATCH 096/158] 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")) -- 2.45.2 From b65753d5438b1ef44502d5ff2ea683bbd49d8f68 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Mon, 17 Apr 2017 17:26:36 -0400 Subject: [PATCH 097/158] 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 -- 2.45.2 From 277ba8249d4757c3dcaf1336e599bc1563b370b7 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Mon, 17 Apr 2017 17:28:48 -0400 Subject: [PATCH 098/158] 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 -- 2.45.2 From e01c8268a7124c2ccfbc52ef91760d58b41713bc Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Mon, 17 Apr 2017 17:31:19 -0400 Subject: [PATCH 099/158] =?UTF-8?q?Bump=20version:=200.10.0rc3=20=E2=86=92?= =?UTF-8?q?=200.10.0rc4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- app/package.json | 2 +- lbry | 2 +- ui/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 3ff1ab12f..9fff30b43 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.10.0rc3 +current_version = 0.10.0rc4 commit = True tag = True parse = (?P\d+)\.(?P\d+)\.(?P\d+)((?P[a-z]+)(?P\d+))? diff --git a/app/package.json b/app/package.json index 13ff19e05..906381b3a 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "LBRY", - "version": "0.10.0rc3", + "version": "0.10.0rc4", "main": "main.js", "description": "LBRY is a fully decentralized, open-source protocol facilitating the discovery, access, and (sometimes) purchase of data.", "author": { diff --git a/lbry b/lbry index e8bccec71..27788829b 160000 --- a/lbry +++ b/lbry @@ -1 +1 @@ -Subproject commit e8bccec71c7424bf06d057904e4722d2d734fa3f +Subproject commit 27788829b0d46c102351d04f0b2197f6819dafb5 diff --git a/ui/package.json b/ui/package.json index be3f3ec83..6cf15e0c1 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "lbry-web-ui", - "version": "0.10.0rc3", + "version": "0.10.0rc4", "description": "LBRY UI", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" -- 2.45.2 From b3d14398701564d17d8966efbf9e3c58fed40f9f Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Mon, 17 Apr 2017 17:51:18 -0400 Subject: [PATCH 100/158] render tiles before they load --- lbry | 2 +- lbryum | 2 +- ui/js/component/file-tile.js | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lbry b/lbry index 27788829b..043e2d0ab 160000 --- a/lbry +++ b/lbry @@ -1 +1 @@ -Subproject commit 27788829b0d46c102351d04f0b2197f6819dafb5 +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/component/file-tile.js b/ui/js/component/file-tile.js index 2abaa5d21..a7327272e 100644 --- a/ui/js/component/file-tile.js +++ b/ui/js/component/file-tile.js @@ -257,6 +257,10 @@ export let FileTile = React.createClass({ }, render: function() { if (!this.state.claimInfo) { + if (this.props.displayStyle == 'card') { + return + } return null; } -- 2.45.2 From 3ac31df63fa13b1471a0b0e4ac8cbec8b892a392 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Mon, 17 Apr 2017 18:28:24 -0400 Subject: [PATCH 101/158] Discover: update search to use new format from Lighthouse --- CHANGELOG.md | 1 + ui/js/page/discover.js | 28 ++++++++++++++++++++-------- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb336c848..2002e1aad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Web UI version numbers should always match the corresponding version of LBRY App ### Changed * Update process now easier and more reliable + * Updated search to be compatible with new Lighthouse servers * Cleaned up shutdown logic * Support lbry v0.10 API signatures diff --git a/ui/js/page/discover.js b/ui/js/page/discover.js index 8aadd2df4..fd0f25811 100644 --- a/ui/js/page/discover.js +++ b/ui/js/page/discover.js @@ -1,8 +1,9 @@ import React from 'react'; import lbry from '../lbry.js'; import lbryio from '../lbryio.js'; +import uri from '../uri.js'; import lighthouse from '../lighthouse.js'; -import {FileTile} from '../component/file-tile.js'; +import {FileTile, FileTileStream} from '../component/file-tile.js'; import {Link} from '../component/link.js'; import {ToolTip} from '../component/tooltip.js'; import {BusyMessage} from '../component/common.js'; @@ -45,14 +46,25 @@ var SearchResults = React.createClass({ render: function() { var rows = [], seenNames = {}; //fix this when the search API returns claim IDs - this.props.results.forEach(function({name, value}) { - if (!seenNames[name]) { - seenNames[name] = name; - rows.push( - - ); + for (let {name, claim, claim_id, channel_name, channel_id, txid, nout} of this.props.results) { + let lbryUri; + if (channel_name) { + lbryUri = uri.buildLbryUri({ + name: channel_name, + path: name, + claimId: channel_id, + }); + } else { + lbryUri = uri.buildLbryUri({ + name: name, + claimId: claim_id, + }) } - }); + + rows.push( + + ); + } return (
{rows}
); -- 2.45.2 From 7a4e9ad656424495565eea76e78d693fe63c7b14 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Tue, 18 Apr 2017 15:14:42 -0400 Subject: [PATCH 102/158] Rename uri module and its functions --- ui/js/component/channel-indicator.js | 6 ++-- ui/js/component/file-actions.js | 10 +++---- ui/js/component/file-tile.js | 20 ++++++------- ui/js/lbry.js | 42 ++++++++++++++-------------- ui/js/{uri.js => lbryuri.js} | 14 +++++----- ui/js/page/discover.js | 10 +++---- ui/js/page/file-list.js | 10 +++---- ui/js/page/publish.js | 1 - ui/js/page/show.js | 4 +-- 9 files changed, 58 insertions(+), 59 deletions(-) rename ui/js/{uri.js => lbryuri.js} (92%) diff --git a/ui/js/component/channel-indicator.js b/ui/js/component/channel-indicator.js index 2f30d5755..db2f40995 100644 --- a/ui/js/component/channel-indicator.js +++ b/ui/js/component/channel-indicator.js @@ -1,6 +1,6 @@ import React from 'react'; import lbry from '../lbry.js'; -import uri from '../uri.js'; +import lbryuri from '../lbryuri.js'; import {Icon} from './common.js'; const UriIndicator = React.createClass({ @@ -11,7 +11,7 @@ const UriIndicator = React.createClass({ }, render: function() { - const uriObj = uri.parseLbryUri(this.props.uri); + const uriObj = lbryuri.parse(this.props.uri); if (!this.props.hasSignature || !uriObj.isChannel) { return Anonymous; @@ -19,7 +19,7 @@ const UriIndicator = React.createClass({ const channelUriObj = Object.assign({}, uriObj); delete channelUriObj.path; - const channelUri = uri.buildLbryUri(channelUriObj, false); + const channelUri = lbryuri.build(channelUriObj, false); let icon, modifier; if (this.props.signatureIsValid) { diff --git a/ui/js/component/file-actions.js b/ui/js/component/file-actions.js index 6c105ed75..21d25eb47 100644 --- a/ui/js/component/file-actions.js +++ b/ui/js/component/file-actions.js @@ -1,6 +1,6 @@ import React from 'react'; import lbry from '../lbry.js'; -import uri from '../uri.js'; +import lbryuri from '../lbryuri.js'; import {Link} from '../component/link.js'; import {Icon, FilePrice} from '../component/common.js'; import {Modal} from './modal.js'; @@ -156,8 +156,8 @@ let FileActionsRow = React.createClass({ linkBlock = ; } - const lbryUri = uri.normalizeLbryUri(this.props.uri); - const title = this.props.metadata ? this.props.metadata.title : lbryUri; + const uri = lbryuri.normalize(this.props.uri); + const title = this.props.metadata ? this.props.metadata.title : uri; return (
{this.state.fileInfo !== null || this.state.fileInfo.isMine @@ -170,7 +170,7 @@ let FileActionsRow = React.createClass({ : '' } - Are you sure you'd like to buy {title} for credits? + Are you sure you'd like to buy {title} for credits? @@ -178,7 +178,7 @@ let FileActionsRow = React.createClass({ - LBRY was unable to download the stream {lbryUri}. + LBRY was unable to download the stream {uri}.
- +
{ !this.props.hidePrice ? : null} - +

- + {title} @@ -184,12 +184,12 @@ export let FileCardStream = React.createClass({ return null; } - const lbryUri = uri.normalizeLbryUri(this.props.uri); + const uri = lbryuri.normalize(this.props.uri); const metadata = this.props.metadata; const isConfirmed = !!metadata; - const title = isConfirmed ? metadata.title : lbryUri; + const title = isConfirmed ? metadata.title : uri; const obscureNsfw = this.props.obscureNsfw && isConfirmed && metadata.nsfw; - const primaryUrl = '?show=' + lbryUri; + const primaryUrl = '?show=' + uri; return (
@@ -198,7 +198,7 @@ export let FileCardStream = React.createClass({
{title}
{ !this.props.hidePrice ? : null} -
diff --git a/ui/js/lbry.js b/ui/js/lbry.js index 244b82142..adc8ba4b4 100644 --- a/ui/js/lbry.js +++ b/ui/js/lbry.js @@ -1,7 +1,7 @@ import lbryio from './lbryio.js'; import lighthouse from './lighthouse.js'; import jsonrpc from './jsonrpc.js'; -import uri from './uri.js'; +import lbryuri from './lbryuri.js'; import {getLocal, getSession, setSession, setLocal} from './utils.js'; const {remote} = require('electron'); @@ -12,19 +12,19 @@ const menu = remote.require('./menu/main-menu'); * needed to make a dummy claim or file info object. */ function savePendingPublish({name, channel_name}) { - let lbryUri; + let uri; if (channel_name) { - lbryUri = uri.buildLbryUri({name: channel_name, path: name}, false); + uri = lbryuri.build({name: channel_name, path: name}, false); } else { - lbryUri = uri.buildLbryUri({name: name}, false); + uri = lbryuri.build({name: name}, false); } const pendingPublishes = getLocal('pendingPublishes') || []; const newPendingPublish = { name, channel_name, - claim_id: 'pending_claim_' + lbryUri, - txid: 'pending_' + lbryUri, + claim_id: 'pending_claim_' + uri, + txid: 'pending_' + uri, nout: 0, - outpoint: 'pending_' + lbryUri + ':0', + outpoint: 'pending_' + uri + ':0', time: Date.now(), }; setLocal('pendingPublishes', [...pendingPublishes, newPendingPublish]); @@ -215,35 +215,35 @@ lbry.getPeersForBlobHash = function(blobHash, callback) { * from Lighthouse is included. */ lbry.costPromiseCache = {} -lbry.getCostInfo = function(lbryUri) { - if (lbry.costPromiseCache[lbryUri] === undefined) { - lbry.costPromiseCache[lbryUri] = new Promise((resolve, reject) => { +lbry.getCostInfo = function(uri) { + if (lbry.costPromiseCache[uri] === undefined) { + lbry.costPromiseCache[uri] = 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}; + costInfoCache[uri] = {cost, includesData}; setSession(COST_INFO_CACHE_KEY, costInfoCache); resolve({cost, includesData}); } - if (!lbryUri) { + if (!uri) { return reject(new Error(`URI required.`)); } - if (costInfoCache[lbryUri] && costInfoCache[lbryUri].cost) { - return resolve(costInfoCache[lbryUri]) + if (costInfoCache[uri] && costInfoCache[uri].cost) { + return resolve(costInfoCache[uri]) } - function getCost(lbryUri, size) { - lbry.stream_cost_estimate({uri: lbryUri, ... size !== null ? {size} : {}}).then((cost) => { + function getCost(uri, size) { + lbry.stream_cost_estimate({uri, ... size !== null ? {size} : {}}).then((cost) => { cacheAndResolve(cost, size !== null); }, reject); } - function getCostGenerous(lbryUri) { + function getCostGenerous(uri) { // 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) => { + lbry.resolve({uri: uri}).then((resolutionInfo) => { const fee = resolutionInfo.claim.value.stream.metadata.fee; if (fee === undefined) { cacheAndResolve(0, true); @@ -257,12 +257,12 @@ lbry.getCostInfo = function(lbryUri) { }); } - const uriObj = uri.parseLbryUri(lbryUri); + const uriObj = lbryuri.parse(uri); const name = uriObj.path || uriObj.name; lbry.settings_get({allow_cached: true}).then(({is_generous_host}) => { if (is_generous_host) { - return getCostGenerous(lbryUri); + return getCostGenerous(uri); } lighthouse.get_size_for_name(name).then((size) => { @@ -278,7 +278,7 @@ lbry.getCostInfo = function(lbryUri) { }); }); } - return lbry.costPromiseCache[lbryUri]; + return lbry.costPromiseCache[uri]; } lbry.getMyClaims = function(callback) { diff --git a/ui/js/uri.js b/ui/js/lbryuri.js similarity index 92% rename from ui/js/uri.js rename to ui/js/lbryuri.js index 67615f7b5..2a6009cd7 100644 --- a/ui/js/uri.js +++ b/ui/js/lbryuri.js @@ -1,7 +1,7 @@ const CHANNEL_NAME_MIN_LEN = 4; const CLAIM_ID_MAX_LEN = 40; -const uri = {}; +const lbryuri = {}; /** * Parses a LBRY name into its component parts. Throws errors with user-friendly @@ -16,7 +16,7 @@ const uri = {}; * - claimId (string, if present) * - path (string, if persent) */ -uri.parseLbryUri = function(lbryUri, requireProto=false) { +lbryuri.parse = function(uri, requireProto=false) { // Break into components. Empty sub-matches are converted to null const componentsRegex = new RegExp( '^((?:lbry:\/\/)?)' + // protocol @@ -24,7 +24,7 @@ uri.parseLbryUri = function(lbryUri, requireProto=false) { '([:$#]?)([^/]*)' + // modifier separator, modifier (stops at the first path separator or end) '(/?)(.*)' // path separator, path ); - const [proto, name, modSep, modVal, pathSep, path] = componentsRegex.exec(lbryUri).slice(1).map(match => match || null); + const [proto, name, modSep, modVal, pathSep, path] = componentsRegex.exec(uri).slice(1).map(match => match || null); // Validate protocol if (requireProto && !proto) { @@ -105,7 +105,7 @@ uri.parseLbryUri = function(lbryUri, requireProto=false) { }; } -uri.buildLbryUri = function(uriObj, includeProto=true) { +lbryuri.build = function(uriObj, includeProto=true) { const {name, claimId, claimSequence, bidPosition, path} = uriObj; return (includeProto ? 'lbry://' : '') + name + @@ -117,8 +117,8 @@ uri.buildLbryUri = function(uriObj, includeProto=true) { /* Takes a parseable LBRY URI and converts it to standard, canonical format (currently this just * consists of making sure it has a lbry:// prefix) */ -uri.normalizeLbryUri = function(lbryUri) { - return uri.buildLbryUri(uri.parseLbryUri(lbryUri)); +lbryuri.normalize= function(uri) { + return lbryuri.build(lbryuri.parse(uri)); } -export default uri; +export default lbryuri; diff --git a/ui/js/page/discover.js b/ui/js/page/discover.js index fd0f25811..671f418fe 100644 --- a/ui/js/page/discover.js +++ b/ui/js/page/discover.js @@ -1,7 +1,7 @@ import React from 'react'; import lbry from '../lbry.js'; import lbryio from '../lbryio.js'; -import uri from '../uri.js'; +import lbryuri from '../lbryuri.js'; import lighthouse from '../lighthouse.js'; import {FileTile, FileTileStream} from '../component/file-tile.js'; import {Link} from '../component/link.js'; @@ -47,22 +47,22 @@ var SearchResults = React.createClass({ var rows = [], seenNames = {}; //fix this when the search API returns claim IDs for (let {name, claim, claim_id, channel_name, channel_id, txid, nout} of this.props.results) { - let lbryUri; + let uri; if (channel_name) { - lbryUri = uri.buildLbryUri({ + uri = lbryuri.build({ name: channel_name, path: name, claimId: channel_id, }); } else { - lbryUri = uri.buildLbryUri({ + uri = lbryuri.build({ name: name, claimId: claim_id, }) } rows.push( - + ); } return ( diff --git a/ui/js/page/file-list.js b/ui/js/page/file-list.js index e746f9d77..1fc8ca5b9 100644 --- a/ui/js/page/file-list.js +++ b/ui/js/page/file-list.js @@ -1,6 +1,6 @@ import React from 'react'; import lbry from '../lbry.js'; -import uri from '../uri.js'; +import lbryuri from '../lbryuri.js'; import {Link} from '../component/link.js'; import {FormField} from '../component/form.js'; import {FileTileStream} from '../component/file-tile.js'; @@ -196,14 +196,14 @@ export let FileList = React.createClass({ } - let fileUri; + let uri; if (!channel_name) { - fileUri = uri.buildLbryUri({name}); + uri = lbryuri.build({name}); } else { - fileUri = uri.buildLbryUri({name: channel_name, path: name}); + uri = lbryuri.build({name: channel_name, path: name}); } seenUris[name] = true; - content.push(); } diff --git a/ui/js/page/publish.js b/ui/js/page/publish.js index b424e07ee..13736f0db 100644 --- a/ui/js/page/publish.js +++ b/ui/js/page/publish.js @@ -1,6 +1,5 @@ import React from 'react'; import lbry from '../lbry.js'; -import uri from '../uri.js'; import {FormField, FormRow} from '../component/form.js'; import {Link} from '../component/link.js'; import rewards from '../rewards.js'; diff --git a/ui/js/page/show.js b/ui/js/page/show.js index ea41731af..cc2fb5cfc 100644 --- a/ui/js/page/show.js +++ b/ui/js/page/show.js @@ -1,7 +1,7 @@ import React from 'react'; import lbry from '../lbry.js'; import lighthouse from '../lighthouse.js'; -import uri from '../uri.js'; +import lbryuri from '../lbryuri.js'; import {Video} from '../page/watch.js' import {TruncatedText, Thumbnail, FilePrice, BusyMessage} from '../component/common.js'; import {FileActions} from '../component/file-actions.js'; @@ -59,7 +59,7 @@ let ShowPage = React.createClass({ }; }, componentWillMount: function() { - this._uri = uri.normalizeLbryUri(this.props.uri); + this._uri = lbryuri.normalize(this.props.uri); document.title = this._uri; lbry.resolve({uri: this._uri}).then(({ claim: {txid, nout, has_signature, signature_is_valid, value: {stream: {metadata, source: {contentType}}}}}) => { -- 2.45.2 From b30bfd9f61d3a8ce407420fec692984229e7e81d Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Tue, 18 Apr 2017 15:45:15 -0400 Subject: [PATCH 103/158] auth fix and partial other auth fix --- ui/js/component/auth.js | 17 ++++++++++------- ui/js/lbryio.js | 41 ++++++++++++----------------------------- ui/js/page/rewards.js | 1 - ui/js/rewards.js | 1 - 4 files changed, 22 insertions(+), 38 deletions(-) diff --git a/ui/js/component/auth.js b/ui/js/component/auth.js index 6cd547633..dbb7db239 100644 --- a/ui/js/component/auth.js +++ b/ui/js/component/auth.js @@ -29,9 +29,12 @@ const SubmitEmailStage = React.createClass({ submitting: true, }); lbryio.call('user_email', 'new', {email: this.state.email}, 'post').then(() => { - this.props.onEmailSaved(); + this.props.onEmailSaved(this.state.email); }, (error) => { - if (this._emailRow) { + if (error.xhr && error.xhr.status == 409) { + this.props.onEmailSaved(this.state.email); + return; + } else if (this._emailRow) { this._emailRow.showError(error.message) } this.setState({ submitting: false }); @@ -58,6 +61,7 @@ const ConfirmEmailStage = React.createClass({ return { rewardType: null, code: '', + email: '', submitting: false, errorMessage: null, }; @@ -80,7 +84,7 @@ const ConfirmEmailStage = React.createClass({ this.setState({ submitting: false }); }.bind(this) - lbryio.call('user_email', 'confirm', {verification_token: this.state.code}, 'post').then((userEmail) => { + lbryio.call('user_email', 'confirm', {verification_token: this.state.code, email: this.state.email}, 'post').then((userEmail) => { if (userEmail.IsVerified) { this.props.onEmailConfirmed(); } else { @@ -115,7 +119,6 @@ const WelcomeStage = React.createClass({ } }, onRewardClaim: function(reward) { - console.log(reward); this.setState({ hasReward: true, rewardAmount: reward.amount @@ -184,7 +187,7 @@ export const AuthOverlay = React.createClass({ }, getInitialState: function() { return { - stage: null, + stage: "pending", stageProps: {} }; }, @@ -199,11 +202,11 @@ export const AuthOverlay = React.createClass({ this.setState({ stage: "email", stageProps: { - onEmailSaved: function() { + onEmailSaved: function(email) { this.setState({ stage: "confirm", stageProps: { - onEmailConfirmed: function() { this.setState({ stage: "welcome"}) }.bind(this) + onEmailConfirmed: function() { this.setState({ stage: "welcome", email: email }) }.bind(this) } }) }.bind(this) diff --git a/ui/js/lbryio.js b/ui/js/lbryio.js index 9efeac9b1..b6fa1a5b0 100644 --- a/ui/js/lbryio.js +++ b/ui/js/lbryio.js @@ -7,25 +7,12 @@ const lbryio = { _accessToken: getLocal('accessToken'), _authenticationPromise: null, _user : null, - enabled: false + enabled: true }; -const CONNECTION_STRING = 'https://api.lbry.io/'; +const CONNECTION_STRING = 'http://localhost:8080/'; const EXCHANGE_RATE_TIMEOUT = 20 * 60 * 1000; -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.getExchangeRates = function() { return new Promise((resolve, reject) => { const cached = getSession('exchangeRateCache'); @@ -47,18 +34,10 @@ lbryio.getExchangeRates = function() { 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")) + reject(new Error("LBRY internal 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) { @@ -75,7 +54,9 @@ lbryio.call = function(resource, action, params={}, method='get', evenIfDisabled if (!response.success) { if (reject) { - reject(new Error(response.error)); + let error = new Error(response.error); + error.xhr = xhr; + reject(error); } else { document.dispatchEvent(new CustomEvent('unhandledError', { detail: { @@ -97,7 +78,7 @@ lbryio.call = function(resource, action, params={}, method='get', evenIfDisabled //const fullParams = {...params, ... accessToken ? {access_token: accessToken} : {}}; // Temp app ID based auth: - const fullParams = {app_id: lbryio._accessToken, ...params}; + const fullParams = {app_id: lbryio.getAccessToken(), ...params}; if (method == 'get') { xhr.open('get', CONNECTION_STRING + resource + '/' + action + '?' + querystring.stringify(fullParams), true); @@ -110,9 +91,12 @@ lbryio.call = function(resource, action, params={}, method='get', evenIfDisabled }); }; +lbryio.getAccessToken = () => { + return getLocal('accessToken'); +} + lbryio.setAccessToken = (token) => { setLocal('accessToken', token) - lbryio._accessToken = token } lbryio.authenticate = function() { @@ -145,7 +129,7 @@ lbryio.authenticate = function() { }) } - if (!lbryio._accessToken) { + if (!lbryio.getAccessToken()) { lbryio.call('user', 'new', { language: 'en', app_id: installation_id, @@ -156,7 +140,6 @@ lbryio.authenticate = function() { 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 diff --git a/ui/js/page/rewards.js b/ui/js/page/rewards.js index 0e0ac80e4..18e936aee 100644 --- a/ui/js/page/rewards.js +++ b/ui/js/page/rewards.js @@ -55,7 +55,6 @@ var RewardsPage = React.createClass({ }); }, render: function() { - console.log(this.state.userRewards); return (
diff --git a/ui/js/rewards.js b/ui/js/rewards.js index 168070627..e499085e8 100644 --- a/ui/js/rewards.js +++ b/ui/js/rewards.js @@ -68,7 +68,6 @@ rewards.claimReward = function (type) { 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) -- 2.45.2 From 2e2805fc47933fba6e4e6415af2b2ec2b99275d7 Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Tue, 18 Apr 2017 15:54:16 -0400 Subject: [PATCH 104/158] Fix config values --- ui/js/lbryio.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/js/lbryio.js b/ui/js/lbryio.js index b6fa1a5b0..504b6b3e4 100644 --- a/ui/js/lbryio.js +++ b/ui/js/lbryio.js @@ -7,10 +7,10 @@ const lbryio = { _accessToken: getLocal('accessToken'), _authenticationPromise: null, _user : null, - enabled: true + enabled: false }; -const CONNECTION_STRING = 'http://localhost:8080/'; +const CONNECTION_STRING = 'https://api.lbry.io/'; const EXCHANGE_RATE_TIMEOUT = 20 * 60 * 1000; lbryio.getExchangeRates = function() { -- 2.45.2 From 90e6f16e826323cf40b22e1941781391fc5a63a5 Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Tue, 18 Apr 2017 16:51:00 -0400 Subject: [PATCH 105/158] handle duplicate emails --- ui/js/component/auth.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/js/component/auth.js b/ui/js/component/auth.js index dbb7db239..a36c6d48d 100644 --- a/ui/js/component/auth.js +++ b/ui/js/component/auth.js @@ -61,7 +61,6 @@ const ConfirmEmailStage = React.createClass({ return { rewardType: null, code: '', - email: '', submitting: false, errorMessage: null, }; @@ -84,7 +83,7 @@ const ConfirmEmailStage = React.createClass({ this.setState({ submitting: false }); }.bind(this) - lbryio.call('user_email', 'confirm', {verification_token: this.state.code, email: this.state.email}, 'post').then((userEmail) => { + lbryio.call('user_email', 'confirm', {verification_token: this.state.code, email: this.props.email}, 'post').then((userEmail) => { if (userEmail.IsVerified) { this.props.onEmailConfirmed(); } else { @@ -206,7 +205,8 @@ export const AuthOverlay = React.createClass({ this.setState({ stage: "confirm", stageProps: { - onEmailConfirmed: function() { this.setState({ stage: "welcome", email: email }) }.bind(this) + email: email, + onEmailConfirmed: function() { this.setState({ stage: "welcome"}) }.bind(this) } }) }.bind(this) -- 2.45.2 From 395a48980de29bef6b72e5b8efa54c480ae4233f Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Tue, 18 Apr 2017 17:02:21 -0400 Subject: [PATCH 106/158] world's worst loading screen was truly the worst --- ui/js/page/watch.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/js/page/watch.js b/ui/js/page/watch.js index 164745e10..2f01ae93b 100644 --- a/ui/js/page/watch.js +++ b/ui/js/page/watch.js @@ -204,7 +204,7 @@ export let Video = React.createClass({
{ 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}
: + this is the world's worst loading screen and we shipped our software with it anyway...

{this.state.loadStatusMessage}
: :
-- 2.45.2 From daf621e662f16a003c0ea519f290bc99300ea9e2 Mon Sep 17 00:00:00 2001 From: Alex Grintsvayg Date: Tue, 18 Apr 2017 17:27:33 -0400 Subject: [PATCH 107/158] =?UTF-8?q?Bump=20version:=200.10.0rc4=20=E2=86=92?= =?UTF-8?q?=200.10.0rc5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- app/package.json | 2 +- lbry | 2 +- lbryum | 2 +- ui/package.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 9fff30b43..6039121b2 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.10.0rc4 +current_version = 0.10.0rc5 commit = True tag = True parse = (?P\d+)\.(?P\d+)\.(?P\d+)((?P[a-z]+)(?P\d+))? diff --git a/app/package.json b/app/package.json index 906381b3a..a800dfd2b 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "LBRY", - "version": "0.10.0rc4", + "version": "0.10.0rc5", "main": "main.js", "description": "LBRY is a fully decentralized, open-source protocol facilitating the discovery, access, and (sometimes) purchase of data.", "author": { diff --git a/lbry b/lbry index 043e2d0ab..52a42ce2f 160000 --- a/lbry +++ b/lbry @@ -1 +1 @@ -Subproject commit 043e2d0ab96030468d53d02e311fd848f35c2dc1 +Subproject commit 52a42ce2fcdd1ec6ddca84c4fa754c3d318d2258 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/package.json b/ui/package.json index 6cf15e0c1..decc4bebf 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "lbry-web-ui", - "version": "0.10.0rc4", + "version": "0.10.0rc5", "description": "LBRY UI", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" -- 2.45.2 From ca6d55da210b33c141a793912bcc48a4c1334b75 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Wed, 19 Apr 2017 13:56:17 -0400 Subject: [PATCH 108/158] Add special support for building channel claims in lbryuri module Extends lbryuri.build() and lbryuri.parse() to support special keys, contentName and channelName. These put the right values in the "name" and "path" position for both anonymous claims and channel content claims, which lets us write code that can deal with either type without special logic. --- CHANGELOG.md | 1 + ui/js/component/channel-indicator.js | 1 + ui/js/lbryuri.js | 72 ++++++++++++++++++++++------ ui/js/page/discover.js | 18 ++----- ui/js/page/file-list.js | 7 +-- 5 files changed, 65 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2002e1aad..ced55e42f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Web UI version numbers should always match the corresponding version of LBRY App * New welcome flow for new users * Redesigned UI for Discover * Handle more of price calculations at the daemon layer to improve page load time + * Add special support for building channel claims in lbryuri module ### Changed * Update process now easier and more reliable diff --git a/ui/js/component/channel-indicator.js b/ui/js/component/channel-indicator.js index db2f40995..e19850c28 100644 --- a/ui/js/component/channel-indicator.js +++ b/ui/js/component/channel-indicator.js @@ -19,6 +19,7 @@ const UriIndicator = React.createClass({ const channelUriObj = Object.assign({}, uriObj); delete channelUriObj.path; + delete channelUriObj.contentName; const channelUri = lbryuri.build(channelUriObj, false); let icon, modifier; diff --git a/ui/js/lbryuri.js b/ui/js/lbryuri.js index 2a6009cd7..55a964e66 100644 --- a/ui/js/lbryuri.js +++ b/ui/js/lbryuri.js @@ -7,14 +7,23 @@ const lbryuri = {}; * Parses a LBRY name into its component parts. Throws errors with user-friendly * messages for invalid names. * + * N.B. that "name" indicates the value in the name position of the URI. For + * claims for channel content, this will actually be the channel name, and + * the content name is in the path (e.g. lbry://@channel/content) + * + * In most situations, you'll want to use the contentName and channelName keys + * and ignore the name key. + * * Returns a dictionary with keys: - * - name (string) - * - properName (string; strips off @ for channels) - * - isChannel (boolean) + * - name (string): The value in the "name" position in the URI. Note that this + * could be either content name or channel name; see above. + * - path (string, if persent) * - claimSequence (int, if present) * - bidPosition (int, if present) * - claimId (string, if present) - * - path (string, if persent) + * - isChannel (boolean) + * - contentName (string): For anon claims, the name; for channel claims, the path + * - channelName (string, if present): Channel name without @ */ lbryuri.parse = function(uri, requireProto=false) { // Break into components. Empty sub-matches are converted to null @@ -26,6 +35,8 @@ lbryuri.parse = function(uri, requireProto=false) { ); const [proto, name, modSep, modVal, pathSep, path] = componentsRegex.exec(uri).slice(1).map(match => match || null); + let contentName; + // Validate protocol if (requireProto && !proto) { throw new Error('LBRY URIs must include a protocol prefix (lbry://).'); @@ -36,20 +47,22 @@ lbryuri.parse = function(uri, requireProto=false) { throw new Error('URI does not include name.'); } - const isChannel = name[0] == '@'; - const properName = isChannel ? name.substr(1) : name; + const isChannel = name.startsWith('@'); + const channelName = isChannel ? name.slice(1) : name; if (isChannel) { - if (!properName) { + if (!channelName) { throw new Error('No channel name after @.'); } - if (properName.length < CHANNEL_NAME_MIN_LEN) { + if (channelName.length < CHANNEL_NAME_MIN_LEN) { throw new Error(`Channel names must be at least ${CHANNEL_NAME_MIN_LEN} characters.`); } + + contentName = path; } - const nameBadChars = properName.match(/[^A-Za-z0-9-]/g); + const nameBadChars = (channelName || name).match(/[^A-Za-z0-9-]/g); if (nameBadChars) { throw new Error(`Invalid character${nameBadChars.length == 1 ? '' : 's'} in name: ${nameBadChars.join(', ')}.`); } @@ -82,7 +95,7 @@ lbryuri.parse = function(uri, requireProto=false) { throw new Error('Bid position must be a number.'); } - // Validate path + // Validate and process path if (path) { if (!isChannel) { throw new Error('Only channel URIs may have a path.'); @@ -92,12 +105,16 @@ lbryuri.parse = function(uri, requireProto=false) { if (pathBadChars) { throw new Error(`Invalid character${count == 1 ? '' : 's'} in path: ${nameBadChars.join(', ')}`); } + + contentName = path; } else if (pathSep) { throw new Error('No path provided after /'); } return { - name, properName, isChannel, + name, path, isChannel, + ... contentName ? {contentName} : {}, + ... channelName ? {channelName} : {}, ... claimSequence ? {claimSequence: parseInt(claimSequence)} : {}, ... bidPosition ? {bidPosition: parseInt(bidPosition)} : {}, ... claimId ? {claimId} : {}, @@ -105,20 +122,45 @@ lbryuri.parse = function(uri, requireProto=false) { }; } -lbryuri.build = function(uriObj, includeProto=true) { - const {name, claimId, claimSequence, bidPosition, path} = uriObj; +/** + * Takes an object in the same format returned by lbryuri.parse() and builds a URI. + * + * The channelName key will accept names with or without the @ prefix. + */ +lbryuri.build = function(uriObj, includeProto=true, allowExtraProps=false) { + let {name, claimId, claimSequence, bidPosition, path, contentName, channelName} = uriObj; + + if (channelName) { + const channelNameFormatted = channelName.startsWith('@') ? channelName : '@' + channelName; + if (!name) { + name = channelNameFormatted; + } else if (name !== channelNameFormatted) { + throw new Error('Received a channel content URI, but name and channelName do not match. "name" represents the value in the name position of the URI (lbry://name...), which for channel content will be the channel name. In most cases, to construct a channel URI you should just pass channelName and contentName.'); + } + } + + if (contentName) { + if (!path) { + path = contentName; + } else if (path !== contentName) { + throw new Error('path and contentName do not match. Only one is required; most likely you wanted contentName.'); + } + } return (includeProto ? 'lbry://' : '') + name + (claimId ? `#${claimId}` : '') + (claimSequence ? `:${claimSequence}` : '') + (bidPosition ? `\$${bidPosition}` : '') + (path ? `/${path}` : ''); + } /* Takes a parseable LBRY URI and converts it to standard, canonical format (currently this just - * consists of making sure it has a lbry:// prefix) */ + * consists of adding the lbry:// prefix if needed) */ lbryuri.normalize= function(uri) { - return lbryuri.build(lbryuri.parse(uri)); + const {name, path, bidPosition, claimSequence, claimId} = lbryuri.parse(uri); + return lbryuri.build({name, path, claimSequence, bidPosition, claimId}); } +window.lbryuri = lbryuri; export default lbryuri; diff --git a/ui/js/page/discover.js b/ui/js/page/discover.js index 671f418fe..dc2811cfd 100644 --- a/ui/js/page/discover.js +++ b/ui/js/page/discover.js @@ -47,19 +47,11 @@ var SearchResults = React.createClass({ var rows = [], seenNames = {}; //fix this when the search API returns claim IDs for (let {name, claim, claim_id, channel_name, channel_id, txid, nout} of this.props.results) { - let uri; - if (channel_name) { - uri = lbryuri.build({ - name: channel_name, - path: name, - claimId: channel_id, - }); - } else { - uri = lbryuri.build({ - name: name, - claimId: claim_id, - }) - } + const uri = lbryuri.build({ + channelName: channel_name, + contentName: name, + claimId: channel_id || claim_id, + }); rows.push( diff --git a/ui/js/page/file-list.js b/ui/js/page/file-list.js index 1fc8ca5b9..063730e7f 100644 --- a/ui/js/page/file-list.js +++ b/ui/js/page/file-list.js @@ -196,12 +196,7 @@ export let FileList = React.createClass({ } - let uri; - if (!channel_name) { - uri = lbryuri.build({name}); - } else { - uri = lbryuri.build({name: channel_name, path: name}); - } + const uri = lbryuri.build({contentName: name, channelName: channel_name}); seenUris[name] = true; content.push( Date: Tue, 11 Apr 2017 10:38:32 -0400 Subject: [PATCH 109/158] external daemon, no more submodules --- .appveyor.yml | 18 +++++++ .gitmodules | 6 --- appveyor.yml | 55 ------------------- build/DAEMON_URL | 1 + build/build.ps1 | 34 ++++++++++++ build/build.sh | 49 ++++++++++------- build/fix_submodule_urls.sh | 13 ----- build/prebuild.sh | 12 ++++- build/release_on_tag.py | 54 ++++++------------- build/reset.sh | 21 -------- build/set_version.py | 11 +--- build/zip_daemon.py | 28 ---------- daemon/build.ps1 | 28 ---------- daemon/cli.onefile.spec | 58 -------------------- daemon/cli.py | 7 --- daemon/daemon.onefile.spec | 77 --------------------------- daemon/daemon.py | 4 -- daemon/gmpy-1.17-cp27-none-win32.whl | Bin 158318 -> 0 bytes daemon/miniupnpc-1.9.tar.gz | Bin 71648 -> 0 bytes lbry | 2 +- lbryum | 2 +- 21 files changed, 114 insertions(+), 366 deletions(-) create mode 100644 .appveyor.yml delete mode 100644 .gitmodules delete mode 100644 appveyor.yml create mode 100644 build/DAEMON_URL create mode 100644 build/build.ps1 delete mode 100644 build/fix_submodule_urls.sh delete mode 100755 build/reset.sh delete mode 100644 build/zip_daemon.py delete mode 100644 daemon/build.ps1 delete mode 100644 daemon/cli.onefile.spec delete mode 100644 daemon/cli.py delete mode 100644 daemon/daemon.onefile.spec delete mode 100644 daemon/daemon.py delete mode 100644 daemon/gmpy-1.17-cp27-none-win32.whl delete mode 100644 daemon/miniupnpc-1.9.tar.gz diff --git a/.appveyor.yml b/.appveyor.yml new file mode 100644 index 000000000..52e3c6879 --- /dev/null +++ b/.appveyor.yml @@ -0,0 +1,18 @@ +# Test against the latest version of this Node.js version +environment: + nodejs_version: "7" + GH_TOKEN: + secure: LiI5jyuHUw6XbH4kC3gP1HX4P/v4rwD/gCNtaFhQu2AvJz1/1wALkp5ECnIxRySN + +skip_branch_with_pr: true + +clone_folder: C:\projects\lbry-app + +build_script: + - ps: build\build.ps1 + +test: off + +artifacts: + - path: dist\*.exe + name: LBRY diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 0b6ffe898..000000000 --- a/.gitmodules +++ /dev/null @@ -1,6 +0,0 @@ -[submodule "lbry"] - path = lbry - url = https://github.com/lbryio/lbry.git -[submodule "lbryum"] - path = lbryum - url = https://github.com/lbryio/lbryum.git diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 299ed1563..000000000 --- a/appveyor.yml +++ /dev/null @@ -1,55 +0,0 @@ -# Test against the latest version of this Node.js version -environment: - nodejs_version: "6" - GH_TOKEN: - secure: LiI5jyuHUw6XbH4kC3gP1HX4P/v4rwD/gCNtaFhQu2AvJz1/1wALkp5ECnIxRySN - -skip_branch_with_pr: true - -clone_folder: C:\projects\lbry-electron - -# Install scripts. (runs after repo cloning) -install: - # needed to deal with submodules - - git submodule update --init --recursive - - python build\set_version.py - - python build\set_build.py - # Get the latest stable version of Node.js or io.js - - ps: Install-Product node $env:nodejs_version - # install modules - - npm install - - cd app - - npm install - - cd .. - # create daemon and cli executable - - cd daemon - - ps: .\build.ps1 - - cd .. - # build ui - - cd ui - - npm install - - node_modules\.bin\node-sass --output dist\css --sourcemap=none scss\ - - node_modules\.bin\webpack - - ps: Copy-Item dist ..\app\ -recurse - - cd .. - # copy executables into ui - - ps: Copy-Item daemon\dist\lbrynet-daemon.exe app\dist - - ps: Copy-Item daemon\dist\lbrynet-cli.exe app\dist - -build_script: - # build electron app - - node_modules\.bin\build -p never - # for debugging, see what was built - - python build\zip_daemon.py - - dir dist - - pip install -r build\requirements.txt - - python build\release_on_tag.py - -test: off - -artifacts: - - path: dist\*.exe - name: LBRY - - - path: dist\*.zip - name: lbrynet-daemon diff --git a/build/DAEMON_URL b/build/DAEMON_URL new file mode 100644 index 000000000..ea9f7e7d9 --- /dev/null +++ b/build/DAEMON_URL @@ -0,0 +1 @@ +https://github.com/lbryio/lbry/releases/download/v0.9.2rc3/lbrynet-daemon-v0.9.2rc3-OSNAME.zip diff --git a/build/build.ps1 b/build/build.ps1 new file mode 100644 index 000000000..3df3d9b7c --- /dev/null +++ b/build/build.ps1 @@ -0,0 +1,34 @@ +pip install -r build\requirements.txt +python build\set_version.py + +# Get the latest stable version of Node.js or io.js +Install-Product node $env:nodejs_version + +# install node modules +npm install +cd app +npm install +cd .. + +# build ui +cd ui +npm install +node_modules\.bin\node-sass --output dist\css --sourcemap=none scss\ +node_modules\.bin\webpack +Copy-Item dist ..\app\ -recurse +cd .. + +# get daemon and cli executable +$daemon_url = (Get-Content build\DAEMON_URL -Raw).replace("OSNAME", "windows") +Invoke-WebRequest -Uri $daemon_url -OutFile daemon.zip +Expand-Archive daemon.zip -DestinationPath app\dist\ +dir app\dist\ # verify that daemon binary is there +rm daemon.zip + +# build electron app +node_modules\.bin\build -p never +$binary_name = Get-ChildItem -Path dist -Filter '*.exe' -Name +$new_name = $binary_name -replace '^LBRY Setup (.*)\.exe$', 'LBRY_$1.exe' +Rename-Item -Path "dist\$binary_name" -NewName $new_name +dir dist # verify that binary was built/named correctly +python build\release_on_tag.py \ No newline at end of file diff --git a/build/build.sh b/build/build.sh index c41d2b67c..60eb60759 100755 --- a/build/build.sh +++ b/build/build.sh @@ -7,7 +7,18 @@ ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )" cd "$ROOT" BUILD_DIR="$ROOT/build" +LINUX=false +OSX=false if [ "$(uname)" == "Darwin" ]; then + OSX=true +elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then + LINUX=true +else + echo "Platform detection failed" + exit 1 +fi + +if $OSX; then ICON="$BUILD_DIR/icon.icns" else ICON="$BUILD_DIR/icons/lbry48.png" @@ -32,7 +43,6 @@ if [ "$FULL_BUILD" == "true" ]; then set -u pip install -r "$BUILD_DIR/requirements.txt" python "$BUILD_DIR/set_version.py" - python "$BUILD_DIR/set_build.py" fi [ -d "$ROOT/dist" ] && rm -rf "$ROOT/dist" @@ -62,24 +72,17 @@ npm install # daemon and cli # #################### -( - cd "$ROOT/daemon" - - # copy requirements from lbry, but remove lbryum (we'll add it back in below) - grep -v lbryum "$ROOT/lbry/requirements.txt" > requirements.txt - # for electron, we install lbryum and lbry using submodules - echo "../lbryum" >> requirements.txt - echo "../lbry" >> requirements.txt - # also add pyinstaller - echo "PyInstaller==3.2.1" >> requirements.txt - - pip install -r requirements.txt - pyinstaller -y daemon.onefile.spec - pyinstaller -y cli.onefile.spec - mv dist/lbrynet-daemon dist/lbrynet-cli "$ROOT/app/dist/" -) -python "$BUILD_DIR/zip_daemon.py" - +if [ "$FULL_BUILD" == "true" ]; then + if $OSX; then + OSNAME="macos" + else + OSNAME="linux" + fi + DAEMON_URL="$(cat "$BUILD_DIR/DAEMON_URL" | sed "s/OSNAME/${OSNAME}/")" + wget --quiet "$DAEMON_URL" -O "$BUILD_DIR/daemon.zip" + unzip "$BUILD_DIR/daemon.zip" -d "$ROOT/app/dist/" + rm "$BUILD_DIR/daemon.zip" +fi ################### # Build the app # @@ -91,12 +94,18 @@ python "$BUILD_DIR/zip_daemon.py" ) if [ "$FULL_BUILD" == "true" ]; then - if [ "$(uname)" == "Darwin" ]; then + if $OSX; then security unlock-keychain -p ${KEYCHAIN_PASSWORD} osx-build.keychain fi node_modules/.bin/build -p never + if $OSX; then + binary_name=$(find "$ROOT/dist" -iname "*dmg") + new_name=$(basename "$binary_name" | sed 's/-/_/') + mv "$binary_name" "$(dirname "$binary_name")/$new_name" + fi + # electron-build has a publish feature, but I had a hard time getting # it to reliably work and it also seemed difficult to configure. Not proud of # this, but it seemed better to write my own. diff --git a/build/fix_submodule_urls.sh b/build/fix_submodule_urls.sh deleted file mode 100644 index 67e024b55..000000000 --- a/build/fix_submodule_urls.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -# https://github.com/lbryio/lbry-app/commit/4386102ba3bf8c731a075797756111d73c31a47a -# https://github.com/lbryio/lbry-app/commit/a3a376922298b94615f7514ca59988b73a522f7f - -# Appveyor and Teamcity struggle with SSH urls in submodules, so we use HTTPS -# But locally, SSH urls are way better since they dont require a password - -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -cd "DIR" - -git config submodule.lbry.url git@github.com:lbryio/lbry.git -git config submodule.lbryum.url git@github.com:lbryio/lbryum.git diff --git a/build/prebuild.sh b/build/prebuild.sh index 93f63bb59..d7d86d4e9 100755 --- a/build/prebuild.sh +++ b/build/prebuild.sh @@ -72,7 +72,6 @@ if ! cmd_exists pip; then fi if $LINUX && [ "$(pip list --format=columns | grep setuptools | wc -l)" -ge 1 ]; then - #$INSTALL python-setuptools $SUDO pip install setuptools fi @@ -88,3 +87,14 @@ if ! cmd_exists node; then brew install node fi fi + +if ! cmd_exists unzip; then + if $LINUX; then + $INSTALL unzip + elif $OSX; then + echo "unzip required" + exit 1 + # not sure this works, but OSX should come with unzip + # brew install unzip + fi +fi diff --git a/build/release_on_tag.py b/build/release_on_tag.py index a8fa731ad..96dd57057 100644 --- a/build/release_on_tag.py +++ b/build/release_on_tag.py @@ -1,6 +1,5 @@ import glob import json -import logging import os import platform import subprocess @@ -10,15 +9,13 @@ import github import requests import uritemplate -from lbrynet.core import log_support - def main(): try: current_tag = subprocess.check_output( ['git', 'describe', '--exact-match', 'HEAD']).strip() except subprocess.CalledProcessError: - log.info('Stopping as we are not currently on a tag') + print 'Stopping as we are not currently on a tag' return if 'GH_TOKEN' not in os.environ: @@ -27,20 +24,15 @@ def main(): gh_token = os.environ['GH_TOKEN'] auth = github.Github(gh_token) - app_repo = auth.get_repo('lbryio/lbry-app') - daemon_repo = auth.get_repo('lbryio/lbry') + repo = auth.get_repo('lbryio/lbry-app') - if not check_repo_has_tag(app_repo, current_tag): - log.info('Tag %s is not in repo %s', current_tag, app_repo) + if not check_repo_has_tag(repo, current_tag): + print 'Tag {} is not in repo {}'.format(current_tag, repo) # TODO: maybe this should be an error return - daemon = get_daemon_artifact() - release = get_release(daemon_repo, current_tag) - upload_asset(release, daemon, gh_token) - app = get_app_artifact() - release = get_release(app_repo, current_tag) + release = get_release(repo, current_tag) upload_asset(release, app, gh_token) @@ -60,21 +52,18 @@ def get_release(current_repo, current_tag): def get_app_artifact(): + this_dir = os.path.dirname(os.path.realpath(__file__)) system = platform.system() if system == 'Darwin': - return glob.glob('dist/mac/LBRY*.dmg')[0] + return glob.glob(this_dir + '/../dist/mac/LBRY*.dmg')[0] elif system == 'Linux': - return glob.glob('dist/LBRY*.deb')[0] + return glob.glob(this_dir + '/../dist/LBRY*.deb')[0] elif system == 'Windows': - return glob.glob('dist/LBRY*.exe')[0] + return glob.glob(this_dir + '/../dist/LBRY*.exe')[0] else: raise Exception("I don't know about any artifact on {}".format(system)) -def get_daemon_artifact(): - return glob.glob('dist/*.zip')[0] - - def upload_asset(release, asset_to_upload, token): basename = os.path.basename(asset_to_upload) if is_asset_already_uploaded(release, basename): @@ -84,30 +73,26 @@ def upload_asset(release, asset_to_upload, token): try: return _upload_asset(release, asset_to_upload, token, _curl_uploader) except Exception: - log.exception('Failed to upload') + print 'Failed uploading on attempt {}'.format(count + 1) count += 1 def _upload_asset(release, asset_to_upload, token, uploader): basename = os.path.basename(asset_to_upload) - upload_uri = uritemplate.expand( - release.upload_url, - {'name': basename} - ) + upload_uri = uritemplate.expand(release.upload_url, {'name': basename}) output = uploader(upload_uri, asset_to_upload, token) if 'errors' in output: raise Exception(output) else: - log.info('Successfully uploaded to %s', output['browser_download_url']) + print 'Successfully uploaded to {}'.format(output['browser_download_url']) # requests doesn't work on windows / linux / osx. def _requests_uploader(upload_uri, asset_to_upload, token): - log.info('Using requests to upload %s to %s', asset_to_upload, upload_uri) + print 'Using requests to upload {} to {}'.format(asset_to_upload, upload_uri) with open(asset_to_upload, 'rb') as f: response = requests.post(upload_uri, data=f, auth=('', token)) - output = response.json() - return output + return response.json() # curl -H "Content-Type: application/json" -X POST -d '{"username":"xyz","password":"xyz"}' http://localhost:3000/api/login @@ -118,7 +103,7 @@ def _curl_uploader(upload_uri, asset_to_upload, token): # half a day trying to debug before deciding to switch to curl. # # TODO: actually set the content type - log.info('Using curl to upload %s to %s', asset_to_upload, upload_uri) + print 'Using curl to upload {} to {}'.format(asset_to_upload, upload_uri) cmd = [ 'curl', '-sS', @@ -141,21 +126,16 @@ def _curl_uploader(upload_uri, asset_to_upload, token): print stderr print 'stdout from curl:' print stdout - output = json.loads(stdout) - return output + return json.loads(stdout) def is_asset_already_uploaded(release, basename): for asset in release.raw_data['assets']: if asset['name'] == basename: - log.info('File %s has already been uploaded to %s', basename, release.tag_name) + print 'File {} has already been uploaded to {}'.format(basename, release.tag_name) return True return False if __name__ == '__main__': - log = logging.getLogger('release-on-tag') - log_support.configure_console(level='INFO') sys.exit(main()) -else: - log = logging.getLogger(__name__) diff --git a/build/reset.sh b/build/reset.sh deleted file mode 100755 index b8ad8f145..000000000 --- a/build/reset.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash - -set -euxo pipefail - -ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )" -cd "$ROOT" - -( - cd lbry - git tag -d $(git describe) - git reset --hard origin/master -) - -( - cd lbryum - git tag -d $(git describe) - git reset --hard origin/master -) - -git tag -d $(git describe) -git reset --hard HEAD~1 diff --git a/build/set_version.py b/build/set_version.py index a47264159..80b777d89 100644 --- a/build/set_version.py +++ b/build/set_version.py @@ -39,20 +39,13 @@ def get_version_from_tag(tag): def set_version(version): - package_file = os.path.join('app', 'package.json') + root_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) + package_file = os.path.join(root_dir, 'app', 'package.json') with open(package_file) as fp: package_data = json.load(fp) package_data['version'] = version with open(package_file, 'w') as fp: json.dump(package_data, fp, indent=2, separators=(',', ': ')) - with open(os.path.join('lbry', 'lbrynet', '__init__.py'), 'w') as fp: - fp.write(LBRYNET_TEMPLATE.format(version=version)) - - -LBRYNET_TEMPLATE = """ -__version__ = "{version}" -version = tuple(__version__.split('.')) -""" if __name__ == '__main__': diff --git a/build/zip_daemon.py b/build/zip_daemon.py deleted file mode 100644 index c8ed738d3..000000000 --- a/build/zip_daemon.py +++ /dev/null @@ -1,28 +0,0 @@ -import os -import platform -import subprocess -import sys -import zipfile - - -def main(): - tag = subprocess.check_output(['git', 'describe']).strip() - zipfilename = 'lbrynet-daemon-{}-{}.zip'.format(tag, get_system_label()) - full_filename = os.path.join('dist', zipfilename) - executables = ['lbrynet-daemon', 'lbrynet-cli'] - ext = '.exe' if platform.system() == 'Windows' else '' - with zipfile.ZipFile(full_filename, 'w') as myzip: - for executable in executables: - myzip.write(os.path.join('app', 'dist', executable + ext), executable + ext) - - -def get_system_label(): - system = platform.system() - if system == 'Darwin': - return 'macos' - else: - return system.lower() - - -if __name__ == '__main__': - sys.exit(main()) diff --git a/daemon/build.ps1 b/daemon/build.ps1 deleted file mode 100644 index 7768aee27..000000000 --- a/daemon/build.ps1 +++ /dev/null @@ -1,28 +0,0 @@ -$env:Path += ";C:\MinGW\bin\" - -$env:Path += ";C:\Program Files (x86)\Windows Kits\10\bin\x86\" -gcc --version -mingw32-make --version - -# build/install miniupnpc manually -tar zxf miniupnpc-1.9.tar.gz -cd miniupnpc-1.9 -mingw32-make.exe -f Makefile.mingw -python.exe setupmingw32.py build --compiler=mingw32 -python.exe setupmingw32.py install -cd ..\ -Remove-Item -Recurse -Force miniupnpc-1.9 - -# copy requirements from lbry, but remove lbryum (we'll add it back in below) and gmpy and miniupnpc (installed manually) -Get-Content ..\lbry\requirements.txt | Select-String -Pattern 'lbryum|gmpy|miniupnpc' -NotMatch | Out-File requirements.txt -# add in gmpy wheel -Add-Content requirements.txt "./gmpy-1.17-cp27-none-win32.whl" -# for electron, we install lbryum and lbry using submodules -Add-Content requirements.txt "../lbryum" -Add-Content requirements.txt "../lbry" - -pip.exe install pyinstaller -pip.exe install -r requirements.txt - -pyinstaller -y daemon.onefile.spec -pyinstaller -y cli.onefile.spec \ No newline at end of file diff --git a/daemon/cli.onefile.spec b/daemon/cli.onefile.spec deleted file mode 100644 index 125e3c5f5..000000000 --- a/daemon/cli.onefile.spec +++ /dev/null @@ -1,58 +0,0 @@ -# -*- mode: python -*- -import platform -import os - - -cwd = os.getcwd() -if os.path.basename(cwd) != 'daemon': - raise Exception('The build needs to be run from the same directory as the spec file') -repo_base = os.path.abspath(os.path.join(cwd, '..')) - - -system = platform.system() -if system == 'Darwin': - icns = os.path.join(repo_base, 'build', 'icon.icns') -elif system == 'Linux': - icns = os.path.join(repo_base, 'build', 'icons', '256x256.png') -elif system == 'Windows': - icns = os.path.join(repo_base, 'build', 'icons', 'lbry256.ico') -else: - print 'Warning: System {} has no icons'.format(system) - icns = None - -block_cipher = None - - -a = Analysis( - ['cli.py'], - pathex=[cwd], - binaries=None, - datas=[], - hiddenimports=[], - hookspath=[], - runtime_hooks=[], - excludes=[], - win_no_prefer_redirects=False, - win_private_assemblies=False, - cipher=block_cipher -) - -pyz = PYZ( - a.pure, - a.zipped_data, - cipher=block_cipher -) - -exe = EXE( - pyz, - a.scripts, - a.binaries, - a.zipfiles, - a.datas, - name='lbrynet-cli', - debug=False, - strip=False, - upx=True, - console=True, - icon=icns -) diff --git a/daemon/cli.py b/daemon/cli.py deleted file mode 100644 index 075c13b7f..000000000 --- a/daemon/cli.py +++ /dev/null @@ -1,7 +0,0 @@ -from lbrynet.lbrynet_daemon import DaemonCLI -import logging - -logging.basicConfig() - -if __name__ == '__main__': - DaemonCLI.main() diff --git a/daemon/daemon.onefile.spec b/daemon/daemon.onefile.spec deleted file mode 100644 index ea42f5289..000000000 --- a/daemon/daemon.onefile.spec +++ /dev/null @@ -1,77 +0,0 @@ -# -*- mode: python -*- -import platform -import os - -import lbryum - - -cwd = os.getcwd() -if os.path.basename(cwd) != 'daemon': - raise Exception('The build needs to be run from the same directory as the spec file') -repo_base = os.path.abspath(os.path.join(cwd, '..')) - - -system = platform.system() -if system == 'Darwin': - icns = os.path.join(repo_base, 'build', 'icon.icns') -elif system == 'Linux': - icns = os.path.join(repo_base, 'build', 'icons', '256x256.png') -elif system == 'Windows': - icns = os.path.join(repo_base, 'build', 'icons', 'lbry256.ico') -else: - print 'Warning: System {} has no icons'.format(system) - icns = None - - -block_cipher = None - - -languages = ( - 'chinese_simplified.txt', 'japanese.txt', 'spanish.txt', - 'english.txt', 'portuguese.txt' -) - - -datas = [ - ( - os.path.join(os.path.dirname(lbryum.__file__), 'wordlist', language), - 'lbryum/wordlist' - ) - for language in languages -] - - -a = Analysis( - ['daemon.py'], - pathex=[cwd], - binaries=None, - datas=datas, - hiddenimports=[], - hookspath=[], - runtime_hooks=[], - excludes=[], - win_no_prefer_redirects=False, - win_private_assemblies=False, - cipher=block_cipher -) - - -pyz = PYZ( - a.pure, a.zipped_data, - cipher=block_cipher -) - - -exe = EXE( - pyz, - a.scripts, - a.binaries, - a.zipfiles, - a.datas, - name='lbrynet-daemon', - debug=False, - strip=False, - upx=True, - console=True, - icon=icns -) diff --git a/daemon/daemon.py b/daemon/daemon.py deleted file mode 100644 index 2ed0360ab..000000000 --- a/daemon/daemon.py +++ /dev/null @@ -1,4 +0,0 @@ -from lbrynet.lbrynet_daemon import DaemonControl - -if __name__ == '__main__': - DaemonControl.start() diff --git a/daemon/gmpy-1.17-cp27-none-win32.whl b/daemon/gmpy-1.17-cp27-none-win32.whl deleted file mode 100644 index 5d15f0efa8b8bd5b29940094a9d35d4709a0dbcc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 158318 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

G|?lH+Za5R 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%0AnKgR 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 z zNZDes6kW7kixK#r$gN&Gj33$lK0iYIMeAUfHeq*e9bRu1P3@5!l6`uP5CPn&0VvW1P0;$-|N z)!It4?CS4cfy$$_K`!TOoyJ}DV@2M&YfOrSmxa z1wYg$FW*shf}WP4Ju2_*yhD}8{YdBWAzu{B52GKFjlHT#Ig%v)j~Ts4&0kF4=V<}G znMzNy?kd}3-AAN+IEU6Qqo%U`nri>}uF>&H-czd`-cwT#^&hex^nB_$MXia0sduJz zzF*cwgi>eYfRxqetk7>~&q8y2_-hw^J+v8WH2p5MQ}>09W+k1+%ewxDrp{9T(EU~c zJE<+1FLm5^Zz+5rort};udbAJEI8MD2kx34*SuYKReIm#pu1L2#`7kXT7Ahj6${d5 zUPh*8db>EhYp>^|r%R96soFcn-cks$HA`m;vLSpyGTwpJaqoa*-CyYC+B_~49rv~4 zx~hxYYVl+XH979XJup#YgQn^0OMfZEUeKaT?3Dpr-6W2QNDsW4%<{motp>~YgIV-h zNY+Dxj&(mkV*{!&;8l$1nB%@1(Nh0i6I2Rdi$7=a^&O7@>45RvM7ul+ueErusFmz5 zNZ*uM&{_wzl%buRwM6g{jp6YN=)2VW&eVI(RN9$(*J*vjdL;cr(Bls?XxfTKt*xgJO@7IL7`Rq&n$-8(h<=WVx!2 z$fmj?AX{`E{~B=0Mm$b))>5=fo#WwSKsIV3t2+U+x+@xS16MJ ziyOMOg+z)nfLK@U^S*)kUtt}7U=Fs=feiM-Srn4@wXu&q{7vXc27=HNFjPzmL6o?S zU)lgf2H48P`O2cO^!Z%b(ycHn^dFqMi+JYz<(|~Y^En7RKvP~}Jl^fWb-z97Ic?g5 z&FJ2ObK=nSAQ!4o8${9|Ms$8!apofGNbxPSyNe5Nheuhd`xD`t#pBHp;hSmHXBszY zYy#b`!$nl{_8a!E8cQFbhqFkesxL!mDgt?;3U1e5Ns>7Hwc6DTz_Q_DQ_9r5N&(}W z8EtH;o2PO`!ah{WiLmV|9ZIXVh0m>~x@$P^IP&KN<(h8P)~A=67}n|Alp8A!!f$k& z@J+xOD8zv6h9e9rsH-%qnEAd^7VOV+5<{n_O&?cCOgAN{lEvQ*J^~crUK4EzSt>~e zq+W7fcA9_cB~&1Hl%aOs*o$L%mP*HEXkQI*$bCq*zcHZhlD>*`W(by^9>X=l1_l~SThXsZ5SU9Q@VYv7@F-E8EU0gqBNlw^GsDV}u7;7al zP`vJ(0IiHkO!3UL1C#g50|WV1-%N9`(aBidL>Q{52M;2`64ArN-Xh5VfZTZe=IdwixMwS#ygawl>NsF~9b@ zsUl9NZp27OlsEm^95LZLhR-wg6FCA8xNx*qX3z)eNjzcYY8WP?idrIwdNd1{*1q04 z#0j%I5Xbe&Z=TNQ)8!y9!W$MB(wY7ASsVp<(q@*Jnnd@0)+AEznlY_vLIdGIokPC7 z-()+?apX_m9s1Mce?;g^AUh=U7uL^7p1!btHccLzjw^8!k4QrJr+^ONtq|AY>_s6t zj~$n1ns{TrS%$XFuun7W4?uz=Hr49Om6j+BE8i?cy*B(9BvO~{-qTdL-m3!g0a{M$ zeUE9dE~no#1Rw84;Yx&0khT+)Z|yb*eil&1=kT&{$C&lD>@IvtLqdf8vG{2Wb~gD- z{&W^%szU(Bc$~`<9i*gB4ufS$M+bxo(;t0EnbtSl$X?ffiI{S0LQRDN$2t_U9QYLZ_o*==i|-)0n&2(8>Zan9p+ZH^1O3>*P)wEd)4F}#XPurM;AA`W1P9;@&`y2$ zb{d(+eGPMmkP~5t@w1UJUb@akM##{fL4;5Vgt<5fux6M)QIqoeKbeujcMid~W1|O6 z-ovoSy{N20D?Dej5UBfuA^Ad0eX}bXOH3s#eV^m8CgV0)boLzebu%0!;S^(Vbv3@R zDSkoYE=S{q@Qhrni5oOr$+T$KT~E^EyTG*T=zsI#j_au3jh&9he#jW7E%7g}+Yz@p zUPhQq7eXef;ZRg2IM0@oG2(|dyJ3ZD7O_W{h?wS$Wbe8_eJv7Mh1Uwaj&{^l))7i? zMs9l*W;@JF*DinaH3Z#~%uw*tJ^!&{P_o}1rqhBt|kfBdEbynRFx=O6PgMF0u}iDYAt z8UB%D=r|8^gmZ??N1)Xhk<}A@=`lE+`b5@y8npY4HGFSK?N{A23_SnWj}&W%m)$Xy zn?`&ptJ(Dx0nKz^lg97sBSwY{g~rlxXU!LbCga^C|25|0fLGP;){fs=3ZLh{493EZ zliOlwzZdHr@yas_?e`9W%6}}T;^ou;jDEztS}+!Ag<4j}%w7aIQuv*X#a_hwkm{X| zf#)`|#OZjY<3%3r-CfEObeNL^D-hC@?mHZxMOXoaT?jHp)iG>l-i z{2}IC$6vrS7yLzgq3YiTXLet2F%`vThEDp#`)T4FT#8-F#M#s394dpH0|DSg%JKis zJ6e32Ozba7gjm`?#tGDb>$S0oZ}z}kd#iTQSv6-V>P>k*F4VjC;X=KUOgIFOf-Kb= zkbnGzS#RfU`;*^0Y;iO$W4Wk@_DmEx9YOO&9A(!#G%v>%t0I}k?v7&B=sb?~EJGaM zQm9%)df2q_HI8L;o)xQ$@5#K-sP5KE5?3^ysQV&L*dPgVVy|eGuRf&`Hv^cOWS(xL z=Bg23U$r)GRl~SwSau!x8yCGE;LQ3Rn%uWRB|0gjVwm`_$p{9wpS=03@qpOX(Zc-I zt<|b#uU-2tjn5kTLmasGZHwcN`vAzh_U)saZJRnjtU6Hj*xK2nNn^d{9c=bxYjgFj z*ZdPL?8Kb^_4jq8)m8IH>U**F<*_9tRhH$K@R$5cbd3DC>XBL0Zl|p3&>umGv&(O` za8&#NGLqv}xo?r*?fLfbH3SAVso zj+a%XM912`PAEEs+vXH+P5A$n$M@+a^Ows{fSi_aNBlmTkJhu|1^}jx^E9} z=kfpV?U2l;no73if!%BG@3^=_cWqj6#{Cr=^K2`3QnkTiE}aqa{mk#VNFj@(O}s%A z>cq9@JG(EWCvBdNS3$pUkJHbGfcl{&{;7Bv)UH^rKY)L4;e0QQWy*E6F5*vBl`oc` zilV>p zm)E)&li-j7P;JokM3$KAVq>AjJGx;UsfybVseQplNwL5t8f?f9CqI2*AP=U&TRWv| z&sY!Qq7QW`umWt1%M&h@%*lJ2MDI0Ly_OEhT(x&JF=MX1R@F|&h^qWfIHxAY0MW27 zJxZ_b6MIlAgu-ATTK|voozlE=!?&Vs>&Kp>y3byBr3!E)5;K9Yb$2i-ml1ie!@`D= zrX#p29~B)@5@XAh-#@ZP+c^*q#6L|`SyRU=EeM;_SR^BoNZPX3-&}W|;d(kma>b62 z+kFz<{e{#wn)Ku+u>**0q#s*hxGaAuD`18W-T z*0uD78E{VQ>YrXx*OoOF+~X1B%;t&BqqQBLR^-&|)gyhJPV=RJ;eItNzyJJz@;B>4 zUwgyW&ak}fCDTeZllrJ_;JlKKA2nIql8L?&-Jh$QDy+z4QU=N#z<}Ms&YP2zd#)mo zWNwn;T}IvGBh#s30+WesiJ9AK1&Jmj;)~U#F`PR~U?llxKI@!AlfCP|8@Y1)^|UUx*4uW2v?2<6)y}o4QMM ziBeIU?LTNuS$A0X=%uXM6+FPG+qD_b4yGRw7>|lJZ@c56iCPg8OSnc`Gnm4tM(xBn z2+W1RSi3bvo1x=N++?KBusikfBAy-6EVtQW-HSFINx+nt{&EHXwjl!(44VU!@$?(Z zEy>d~vdj3Y3#273D-5nVv^Rh0jG=XyH5dy^X+I*2+q^$32uG0{21TsCplrFnAYxI6 z*0#Fn$l=i>Zeq-R$ym=*N(?5zKn-^4>#;9#Y)Wx<>TlZi+T1^WuK%sn`=e-FzP%51 zSa-vH0y;eH>|U;M?MIub@3B$%Z!i$TW&frxvINDcq-jg-ZUQZptW5$sw{ch96+tt~ z+;hsPDZ`GT71tYBo8nqxGKLx=nGklQP@fA%@LU%m9vP66?>_?(?1P{hA@6C&hEwR> zj#Q5}%4UHOK5V2s{$V5a)cV1Z>V<8BN0@tGt40AUS*{U$Uh;R@-)JiPbYRZ=8@FS9y6nPJsD#5y~a{+ zJ_xf#RIcP3w+v$s)3^F-Lq{Nt)R2tVGhzhN0dtMO)4Q>Rd)_;==a(-n3eb_>ko z7WXNwg{iwx7IVt`#@7eKV4rn2?bk9uy_tA+o}=EQiH$UlO2V7P6w)bK_YWok!cH3& z?fa#1gT{FQTnxm>c_F;{5bxXR$X7L*lLX=f!q)9ml$7$#baCU}F?QqG_UpgiI4DN4 z{8-MAG*ySHPRyxw#u^rigw2?5!QFKNU95hSsIdwY%wC$8{zCQ8L_>^g08 zOujsc>*k94EdHFTo2y>rt#zaB9Yg&BnZLX6MO*T%X>^LmD~CY0(4ml$qc}*NP6nq! ztl>O@qPCpgRMXne#LRqUq<8gJoe1dhUiqygTcZR+eZmdZHV3Ia9%jf@SHt&>rO#+y zf9{UkNCTL`BXeKWxZYykuRZw^YqBKM1YjTfHODztq$e9;3vf)SbTTHpCi0yG^~*~7 z9c{__`*X>Giq)fG-TAPBdQ|Q8A$3gszjzb5J60F*B3$%1@eS1|H9vhw!_!_e-P*IB zH&UlcL(uU&X6wMm^t7i=mIm?k+?SOl5UM3L{qWGH^VCElxB5r4PK$wPjrW};RbNoGgiJib zLBR=2g!W@`UN|TmSYUY%eSIF<(KeB2ua5@jaUv#qvhGBp&4^PUgB0EYv4dkd?VVXq z2Nf3MVUEsRrVtuN&&TE1&o5H}efB5CdAjRKtYMlF^%-q>f37ndq>rfuB4je+5zNoW zBzFe5hOcuC^j=KP`}^|#OrHAUgltP)0P)0^bjL#Usw5nso&}QEa$02_4}Fs~w1P9b-%hgf z1|=&yJ0-IppdHgxZY<1S@7d;W-eqU>ymxrd`*2f0z4tTm*3OWiK8>cMbM}m(bBFU~ zSg#>PWBPdFf`u8&PaiR3|H2IW&0fbt*W`=c9I_vr>_uqYJ}vd( zC{Bz86b5*-+M~fx4AEzQc$W7&<7rn!4Q(r&@Jn`+G7t`n4%zZNqZSnhFejF6%|TI> zL1XS#Fv*LXV~`*9#X0%nAK2$X4QnBfTeM5YLgBGvn&#y{WK6nCq2!vZ7(7da}gQoNbTJEFn#SSsdW z6eb#ru>9ud84ZePcz+8O-JKWh#QMA5Uvbg07jo(=n-R@dHK_zrjr-~xq%?^B^J zJ)w9{K|cnVoc&ekV2{G^slCd_^=Q%I>8xt5*Yxw!Q$%O~n^9x2C3^Q5dG7D{ig>)o zj7O#~>uUe*S4W@auevYeuaa|L&Bvz~KQfkBs#5i%SLowO(dI7{0M&1vHRCP@^9L|| zNrb(VcJn1z>NwT~eA`bn6PNoseOPq@Om18$d#f4R{kYCo@$MnfvMDI+TZz+5>O|If z8M~ASC==RNOuKz6{OqiXjrp=*NE4_SZryC5p!8g-mOcPv0s2G#=*%lh6xVTWr5Uo~ zc(@&lo$4&(D$8~Dx@R|n3A$5pr{vBW+IU!NHmm@}G?s4FLSoXQJc(xTQ~L46MdI9C zlap#HRj}MyzD^Vqiqw7Y;{PpmU7t6pqjo-nz(q{qQg7T|Aky&=ovZod@a<9K`y2YJ z^L-32=j(ys1GsUbA0rI%PgfSnSMAtCJh%!qc|3Eea$=AH*;%R1FpHvn0%$JRIA4DeZ6$QLHzOOeH=5cOs>K}+z z?W)RCOV!>_iC3)&!Wb9Nn)t?i_bps6RtG}znYNaEpueCBSB>=mvpbVOgY^rf6pL;urW$)S6R z9HR-QZ&7JVs3Ky}LHWZiO27unMI#DYPeKsvPUb6CU#O}^(vKaQQL`~8CVASEsfw<* zEzaae6{;4PG-@7!ljB9SANv+9@n#$k`SWJZ6_g<1Pxk#yr4{P(z-P@y(x|7I=$~r# zi3oZ7E;G)@ev6rQCPE53HBTKou(=Qebjt`3)M9>WK;6B-b1-+SXkp>NaK&gk)^*lR z1O7yq7+y@?dJz(G%P*fYho8YX=~$`5u7?~NO24so0NUi5E3j&3Zy|wqDhZM?6(vlB z-h7hPh86l~oCvL>6+7nR;U&1+c$@FE&+lyZcC0>`JZZnfCeCeQ{3X$uJUM0csFxT? z$@Qa+^sw`|XSX6L+lMZP^w0#O}Fq^|4ZSM?! z!i3Bcqi)U3?B|BSO4O#;bCkn~rK*iOLL%IcswE|DLnV>tf!qLYrBhQ1lizh{55``D`E@6j!3c(u1;&vni^01-Ce6`Y}BpO4IETTqfOPa zF-%H;Ey!g}83j1Di9}6MBY>@$mX`Q|z-NR8Y54@_4jGNYrRO_95HqfnE^=*6V*k^YOhxgfS`#A{R` z4H~b7h$UkNb7%@Z;S94rBTf}+V@9yLZbi`IQmHCF|Lmq$bAse-GXr@wB3v@2Y8kiu;*1{o4M*HDl zGUB`p)(#P@bYwx4ejvm-i^QwtUMtR$=BHv_>1zO*a3adk;azH-%bkq3!lmnJDN(rp zyh>i8&L@uURPyUcLUL^-Z#m51d^yTER7T^hKI$|rIro%6@3s;Ki0>(vq+xhaw4D`l zbMa(V1ij<~QoR{^hoQj0g32v0O{9(lxQbmS8$h-Yk9s%KKD}7 z@$y`bhZY*IqI@}@v;x|UcKtZpbD35XCY)||E-}?B)bPmhGg`*mqmNSWC`BZ&kypbiT8$Z%x%^Pz@KFAzp}lY_l)BRw6}TViI+6 z>N#mhy--6(W#Z$}%z2F!-(LP$BiO{n2?+zb5KpFZQ~@?Bxr9B>3b08}E2PvWDLvlG z`J5DtdxqRQ@#;*g$9O#vd_w=wyjlt zSKcP5FqjdI$T|s_^J+!AANjMDqEaTQH+4JF_TWQ1(0QFC&4~ohDe^%LHPpu=EAR%Q z?Ihuz*ZHp#aZBkJQcHe6||Nai}Rrg{}c@2s7OP`zG!!s!E!9H-lv{qFT4B zU|5rq^mrSNvY04_&GKan(ta*z`4=V0^h#>>AC$yDfoH>-_?Xa~czupP=qn@%Rkdyt&Q2MJ%36z9f!mK8^ULhlS zGxZ&S7?1U{j9|ISO?{6Rl;~CQDkp@`0iHUono@;4W_S+a(t5J{0f$9T3zgCqHbO$5 zc!CcD`?>>W$fdQoai4(MiC*2w)@6y-r>cl5Z8v)OiO)BFN;~2&h6J_3%^@_1TM(!< zWwVIU$Hc5)w2XPJF0qf`15>wLZ3Mhj{!7Nxh3^@_N8fBNVL{qMFKOKv;6}tQYNJ5n zjT?;`H!()*63|=&U?Tvrp$ZY(-&QGW(L=Kcwbzwa#9cYwONtd*XG^k6qgoO) zx($(LY|%eBQN<7BUJKF+Ey=P1a|p<*A31ccCLO-{;x`>`2OZ9S@wvn8*baNW{@mee z(qStiEZU;CA1G0Q*Hgx%!#3pRqZ)GqZa`2JX88X0_>Z=yA_0Od)wwPgKs*RV-Y39P zGnoStiJ?kqiSejW0X_YpHSmS15G#-n0N!9|$; z!1&h@IlzJFm^Z=e27qzJL^c2(F$}X@moRGR4OC=`z!yOenvop8^9K@0F5pGP^|u&A z;q*oh9sCpNpNUxtn{6^KLlLmA=4%A$=#-K1Uo9r12aBu)u}=a%PVaoDZ3}XmLPP`H zseo#b7euLroY^$sIWC@!G431*6C%B(3Ly;kR&|5}GBo*waBRq=<=(+1lCXOUS_0rJ zK%zh=>H&h{Pk4l5ON$O1dYwYA2e5D03in@8e-iGm2Jr!%o6E%*MJd7odz;ask#W)k zi?L4hNwb1MM9~L0H9X|Un7A-Hyw$+3wp5TMOKXmzkD1yC&yfWnBeYcWmZIn*UaB(w zkL53*K}R}dELs}FXlumo&|m3A;r!vS$^?g@$BPE(<=N30Z)eGXPwhiAB?L^Q5M*uc z!-@pls!B;%P$9!TU*5tfuZp$(p3Dt|-E@Q)YzU z&Wc5J;Rs(=f6!Q|X@3EzA}v;%1!0%Tl?TmFGG}1D#Pkh;CBsVMxT?7@)ExTS9;b{q z4k|Q9dKpzP_Az66o*C2ne>$d(cwRGRj0zj z9e*4k*G#S|ZdCcL2X43uO111u~)qOu?pL zal3pF-GF0_HZaj&7MB`;Bh@GJS`d>dFjjA4YEjEJ_w;FD%TbD{2m z<}{;eP43CCiGjqb23yJ71l<@Q{F1lC!*<|=vZN)cTPyYu#S~rexzoov2`=FDmf$MFk*Merg^J%x9*mns8J$!X=p`L<%{E_R3d% zL8~h$`X=xaEEP1BqV~2{l_se)A+KnOnlWl#S;U6xyI(r(( zZ(00YxF225T7{}FWj`=f*JQsmu>Y0zrCyBz<5HS5s?r*m5I)kHB%IFnHLz5{R*gZx z4d*_!rl>i<98WhKoPN37ACKn5#DQ82X}<_}5c+sRjK*f_T>JA1jn}~-;@V}-XkwKI zdvHE4lg4FAE8H}J&(MgM!gkVIK(=Vw>l84L#%jSv-Q`rv$~HJhio^QU-42^(to2?= zZl`*R=sPXs(Hg?~glX+Wc+dsa@NpS1A1^{LPI;CLKTuDl@KF;YGz`{vU>)!vap7Lt zGYPRc#Uo-|iuo{R#NXj`Qi0V!z%TlV!6O24-i51RK*Nt5B?-4o4z^(2i?#FxN*n0q zal)Hej`$qgocM!){WLgC`yIM1R7y0wHulwIUf>zNvOX;YWE87~%V}mqL2n=8V|W4u zd!ItE8~D9Rt1rd%FQ+hb}a@!3XKkDOLepoVuJ zl@J8lmhJGq2%6}rtQRiDp~CYIhTjnp2_;C^gx@chi_E@QLr_HHsGH)p$XpRwshbAr zuKj~~bQ7T{D3qhn_Wwvuoc~}6g7$ln_SdU8!X=Z*8e0#&No%K$Xz-=wciypJ6zuZW zerohbHCE_rrQXo7_mP(LJWxuCUjk1=^8=T-)i^D(v$X~=8@Sal7hnRwJTPIV1k44R z-NVyuT!!%)Hkbqp;o*6}Q)}WeY#1_qxh&7d&H}+93?68u3Xo{0Mlutj$LEP4#)+U2 zFN2}#VZzP=ToH{>+WmY%OV5E&m;-o2+iMw_1McQ}b0D-&sJ4?iS@-qk0FZfBb20~f zaS{ZlG)3bA%`Sa=;4oO6W_|(D@oRMWvV7tGY~sPc;cYe&IEIBM`~%sviYo$%e@|^b zP2iCKf>Wv?eW#ds*Y9 zCd?=~vSmpAFt*Yd^%ISMJLwL?r0Yv2T|Q2_rG`W{^~WvF0V}IcJ&ffmE$MSgREG%G z_A{lo6WSIRU>e>B5aOKL5h-WXh{)gCTj|+*;o6!0$tKN#^SKAQryj`TLDixyEm zl2MXm>nrH;NA@Ao;;idtbBc>z>Xs~OY^@aN9UcE z8r}ie`0p4H2{D)#8pb>jzys3T21TGMq+WZ%fPN88e%W^wVcM+#u0VE|^+zI9#nTjw zO(G#uiCzt9Qbs${BwWNQ;+VTp=)Io>PeF7UP)+5chIbGeG#qD4GvqO0_rQLK5dT+D zdl+%Ys`dQD+9L;&Hqp)Lh)|(A^GBuJJntCDLXD=e4$rX=*jYSUu31ENOvyAdh*yh;B-r$`_l$ym^9o^QAGpj`Hf% z0R`LwTKkV1Qm7Ktafa6M$Zdx@pol!lLPd@1q(pQ$4uZ*bZm~*SEiF#A)c95NKDQ@R zMVxq%vzJ=UP)>u|R25)pA5h{MfZ84bwRA0Ic4Cg2$I%Qnz~d6ZvTUt&dRi3~7?|Rq zE`VYJjmfc-W~jRXam{mCYhCdX&kIA*IY%ySH&$t{E`q$BNR~?1O*64j6Bp};T))DX-GD z>_D@TD%ZKAR72lcg^XT6-{ZS7jhkb z!JKBzX^zB7$B_{Kb9{~0Aw?KtRIj}k%D5f;l0!#S@o5fyJ;m@QmdK&=f6Jl&ruCl| z>$mjAqE)p51jMt4{f97(Yi3}m zc2@}T{z&`(6leq|0~PtafK;u#x^ix2SASP-U*^%R$wK>ngyMnI?=A zgz`u<6Sqs$7kfV~n(?_oLf7Lt*IP4IBtbP5F|#S-o?J$rh(_q?2Qi$kCB~}3maET0 zrxb|kD_0vE8N)U?A2owO$9YFivoj!iEC~UowNht0m?GA(oeHytJ6T1YGCz- zEMYOXCd$+e7)XFrA5f3TK~IsuWb7|)Wc1fzk>U_q74t=vK6vlFI6k z9(zgL>vH4`{tV}*6#Sux&WsN|$H)+&>k-84dHNI_#Zcf9722()pCyd9CjW4J_$1^q z29pwn!r%~9%ZP^bdX8}ZDGD7W!gH6m((|(>sbF4PEhM`+dY@NQM6fX%PoX#leI(W@ zFo1#Dj>x1EKQrkMt=YH%2`2yOvf3-{;ZI-4g}MTk^N6ob+~afBKDFiQz=_*jM$9=eJN{h+V_1dt9*L5V$7Tgqo? z$(TNqwL~`+NK2|PUL;FPdhU2CBt;IvOQr za>@Fge^x7^Y!#OAR+wQ$4iY69SxOsvNuWbX2^yGPgp5IN7sVZVSU1gE74-@9N6zG zjq6IaQ)zudby_tNTT8X;0laj}u|Lnvbty_U(Oo5=ym>9GkNwRsp{f}o;f0*8&)IcQpQ-AO8LdC+<40O2%pix+5d=A~K7Y`N588hsUY<0tD$=x( zA*N@0L2(6+aDWyK#Ptb;zJ?6ap<^h%P*1`xD6~`SY4?j)ieI6h96q23k+P1C;Zb*A z<&}v84WA}z@j4C9l$)@G`kY8bQzB;HU2_20y(U7{X~_ zI@1#QDXUDN9kfuDSGg1kHK)ndNtODVeKEHvENog+C`rqkrH>|iDPo%vZWd)d6i0zqzF?qo125~HVInm0j;ihI$dBNP|LkI%>q@w zgMb48oW4#4oQg?=T>Fa%2QcuH?->HDR+|!Zh}GhG82l73X{u0^vPYwAv_!Gbp)09| zO1hDlaawLRS_A@{17%HvwZA$rP^-pzOwrmjNIRP?U95pwk|tsujiKldL`adoSt8YV ztpS{J)YpoiYXrvJi?G0vIV87I@hdxM1TB9$^(7!kLL&LF zyrBLCOeO<*$Zy#fLpjq>9~l~ZZ>Bz#;0crcl^|`wJAEpFRq8WYgrZ;p%lc)Y4I~XD ziv~RVcD10%g3OPwH-C;S#27#8jXqi2|AH*YnFqNh*N9y-;uT01#6(nkPDCHo0_y%b zSy=s^EUbbMoq)-*8DEeEjb-imdfmE2`$hj;eELHUk_A_;1r7?0P8qUjAeF#EBCoP9 zpet!$E161H0@Z5)fc3y5Z4_Gx?q9)Z^N$Xws@?x7I7NKVa4@Se|7?Ye>{5FrfK9xO`P>TA#s`}2gRk#Kcv0~W;)w3sI7pQnFG zBS_dg>hvk(xAH}Zz$GjFJ#VT|&HJ`NOFPTpEsDs^xH<**oltd6`cw3#{hXuW`73Ki z{Z76S!}f2@+BXX4XV520#KzI8KWpPiyYMAqklN~J+AgX%xSgNA{ZK+FR;4; zS%?qe>WDX+R^)=^P!RB4=qp02%`#^g94mDRWPt4$$xjm4Cs1oR?Gr7T{Y*{^>S5l& z9w0cbHqcn{)AJl%a)D)OUHQeLQaqKRnSwVA&A|KE2(tb2(MN&CmG5#M_GXnc-5sPj zcwM;UF3LB>+ND!J{hn8;rgYmeOrr)x9rmL^TQ8suUV!6LiI>rr|6~ES0yKqc z#B1^P23-^?!Z5Jb1p&Pk^Kq%Es%9cGS>jE4KF#qEi_<{7|Hn=59nrS8WR-jyVAJ+G zpf=!o9Zb*j;-+cq?gP_7csSF_c0n3nKu9AX&9;yA)9@08$D> zTFPXRQS};`g*`AF3)IFMF}g8k4p7=cmp*gIqm&HEg}kQm z3u7e6M9+&)Irf+B5e@Z#WTZNwW&z=Is{2%^;D=B%^Ec?tr#FW`MDJ=M+zALXTdbvd zHDFy4!POdpBh7Unf(f4}X5@&VKxCMQo3gVEaDdYc$Wa>zlpmBTDAeb*;lI*-FTmd4f)V3hR1^DSdvRu6 zDTdEP9EkaVL9*bBK+-^ykm^1`?9^?M!r;-IDT}6tD@08WrybJ#TC`8{T8Hiyp+J`E zKQr1eGC_Ti_AP++^OC=q$v6iN)_f_5(1#It3`YbInlcXrX#@uq^hIEUww;MSS;Ndk z!9jp4746tmLBc_1qQvr=uTRI#vE?*XAxROA96nLcz5s8lC|HXajKI!of!ZSt^weec zzhrj*tNuK3>9x>`p{?UO!s-TGF}z#h1mC)NA$V#*gyt*sowr6TgC0q5D`{0pF^S6y zqpH35f;M%c#gF@;!ex7jQVKs!rV(3fc&k-%0orUdsCgUF?m$3WY;aSF8y^7D_8J|t zP#0Z*y66(={nRUXXcEFKTEXa{)g*{fmsf-mdpgvxu=I?u5-#mUWab7E0>kSJaZgIy zUu>F5+h4#&ypiaS&C+~o#0v1hCc-N9y3X!GjCL3Fxszp3XmNd@Zdaz) z(#8_^!BUOa;?Mdo#GHb!7OzEniZ&WX*PsD4%8ZEn;)RtoKZsKYz}`d|*kC5Hrf8xP zLW^8Uiv%j8)P0Ea2%@BkAy?{=VHTl@1k9|!Fzv`spL;&5uey=BuNTr#pPrzYl;n{^}E$26?i@lg^bb&Hlc3634>qMpA-er+GeQ6FSql(FjL_Hn zknv0FU_b>L2MbaUAd8|?Fv9cSAnpa7WpLwjCN!wnE&_I%P1wGBxSi->g3*UW``1yr z6Es+0gTu60&-GR+J$!yfCW&v29bv}wMutmhz-T^!JXk13slxfHK*4{c z-~36~)TKkFEKf6Me_FCMj^bSuqQ@}!qi_|jbE2tAlj*=ax`O5#L!7uH2Hz4%q^=-7`o$0r(w+nZ8P9;)+NjM`(E7qkd05 zg@KwuHhg>F!-d@JHW$pY!zT1CGH24u7zKsvf+{S|s7;{6HtUjZ1(>qgI zUqgzDvpvTZX*(FA%M+WPdGNah{0;}YfZ}ki4s^-#lnaziJPordc%oTejpq2Bo+jX^ z%|~K2CP2t4^ZF{DNSJXQYL8Ky(R~yC!%j8mI+b2; zkYWy9O2jHFa9%aK;pnK{Cc;~dH^WuLIo14yIHjV|qwyx0o(NDGH-^;H#Wvog3&CeMY1i1E0V1U#8h;(Z^&KT|zPi5h zheoBKv3VAf3tb2y7eY8LS=+GXp7I7IU%}uHXb85U3poO6yeQ7n25?ve^GE$o{VTAa z@K7ax>~aAO7u+NjSbKxCMk(?nq@(1k0v=Z!BbEy(hUuRSl-;-d*pVlaW~X3qk;Wo; zH$Ck!MLXgFh&Gu-+Ifs=2QX_;k z7G=D}s<=vDe7Pe$hnKkR>55Xivza@w^`lZKI%zUd1(c)z11=I-t~yqm9{ZTWp@$*; zgrRLs$d3P5+Y;)|%{Ro(V#r&1?%*U*8fsl}Wl=H?Z?%Q+UU&o`PHK)ihf0IjxfMS` z4*m6tXI}sdeNo2(bzUb5OU_v}juSe>{dFh7)S~+$+ zU%Ld~!(Ow54*A##6AmwxYvkn$jr_3!QSUa5E=!>uLI|gCZX;|WqIRG;L8ZT ze>tpW+CI>nT&S!WX(Yi96sH{wc`(94T}gPq2wZlL^P%x)@FPrJFFl_uR2xIb4H}D3 z#sii0H5c5P&Q)2D<$ySgDvK|fJ6(r$0EQl#oWHrZP1x}x-Q4oj8+)iEk_EGArc zeTZ|C<8qY{HR(E%GLs*pO#JqZ!p2TVRH7L!(-hS1WU-N}-zvIUfh;A7VZWg% zJI*r~b}cMXi<(p*{zkCVQA8?AkF6(jJUHy^;HP~~o`h-A2wRSMMsA8pUC}l%WU2f* z6vJ`A=7k+pHn=EZKM-SosvDbIGT&C@8(IhXB$!WL>msjqsg~9$jn52$k}J(JUMr2! z!JNpfTIGgy*WFaPVFjZiM_iy=HMhzZ7L08r&tYvhzmA{Hi}15;A%40Q;AgKq_#CsO zc{TYme@wpGW|1$$Q{=1Daq`ve2z;$yW~hZPwVc{kM!tF5%Hdlh=WnaR|7VmpG&bfN z+CImPpF?fIhug}aRr!WR@M*%MvhoeDSI}$$L76I}1YX%9CO^y26H;?m+}I@W=6H@a z3`b-&~M_Lh1*}6;f+RJV-CQ@u+){@*w>PX)&aEkS0P}-JM6?&9vuH zUhscsNL?XWL23%=r2~&DgH#CV45R~)zK66D(mY5hkVZoq49Oo-Z%D3?xAT^AgfkNLi5fL0SiCF{Ejb;vfx$DkK%`hgoX_ZAC5WyOunem z(J`@O#>R~sKVf2gLgJ*!Ny#b7DO0CSpP~Q$KgvZ>|7yRSySsNsdpNi5j@_OAM0sDA za7Du){ng{~_w&D>|NZ>$=l@rqqxg*pHzxF?i|6IjkTVCsfo2pnX9GICFr=tR4~l9Y zWk5A7vHGjq{P*+!+t2?DzhXdfDMQMbGNa5X3#u8_l4?s?Q=KR~%AWd)a-*bFFUptd zOZBIQP{C9<6-C8TW2p&LJT;k`O{G&RN=}8tMbZ;TUtwIYLfzPAg6uPA85F$Aja^>BSky3FM66L~x=wV>yW&C1)mQ zHYbCl;jG}S{s28w@lEGzc>YH;6QdH5g|w!C1BiR0o& z<3mzv@`wcbJHk%VtCz&_U*}!_>gR|bZ+~}pA8$_=XK!zRw;tY(9!~y#J^UR#T%El= zJ-r=$J={HeIQzM~_Hc9b^z?A?^XcK}?BeL+0@Z{~`=K*bm#ya`| z#JPt*Ja_WI!kk<^y#0H)xL`(a7e7BYUqAAWn-g@-$&s$Fhrfrjn=knd`Q>gtbUPrw zhl{r#NS~b*M0Zh`VAX5ZQi_P>$Yv*Z{P954?phQwQKjDy?giVKXBmSp+kp{ z96kEev17+iocQ_V$y29KpE;YAm7SfFo11s;-1+?cf(sWeUb<9R_{-(XSBi>?i?3e0 zcK!N|n>TOWDk&|!ef!Rxvb%Th-Me32{@}sGhmRgTe)8n$(`V1L+KT7TD=Vw2Uc7ku z@>O+pP0g>rzJ6U>`{vEtw{>;(_3z%jfB)e_L&L|9pFTDIyYbU^aEK&8(Np3Zm5`8_ zB8i$fF)=zSB~}t28=sgoU1E#7aj#BFJ5!h|zsZ{v`@7tU6JsKoHzWzMQzJjW{r}JT z+JF4lW?Y!>@s=C=vj3NS@w8gTu7hIcyO*0=<~NRyWh>fTzhdJRD*f`Buw2GgXwH16 z2o1-%F25uDHaVx?{LUkD1PpwWX_>!sPKk^Xot^trahrE$4E*TM%RO5a-juCbe@A&~ zt8oic1|AlIe>Y@Y=Kn05ZqIdHdzkpw)W_+Y6wUmu$*7C#r1Q>{H)Y|$F?X{~=3JH8 z$E%m{?hU%iXW-Fad#AhHE|yW$mR3jCZIiL^b}^;(s|FU!+KV7S|xv+&0|7N2)GbXhj%RGQ6}ehpbHd}oBf@USHvA%^ z`V1Vj+ivz=7H&SnecB+KUu10)hEHhT%gc#{_iDLEWq71eHs`^fRpmPikF#*QTR)B{ z9atzEn_hFVBL0Ps5!>?m%(z?6F9CjH%*>i(j%DFVWPC5l0taZCG|imeiiJ-;&|T&$ zy(H_iZ(P&W!P-+S{Gu%Q`gP0$K6=? zy0!RsL1xJOpM`((_2M~Ikr!myUhfATF5b73g?AicHuI(J1zDexW+Ux#?nkrmM@CDo zjIS+_wK}>Y-tnc-o`uu&T%e<87GBkQa7ycy1+oj}FEV%Ls`jw(V8;QiKSmeG`t+W9 z_>;3kJPVI#<+J0MM}e$3?_}`>U`Nr`&|C!vgB(ld}rS-W-XKQ zWz@v$Cf`mwxS54p807GN9GoxvxTn!{yG2|I3-9CB?d!1)`Lgme{sN(5M*s_7m}=eA z&n#c|YQb*vd2U3LK1c%Q^7 z^D;@~=B67y{&I|kN7QUNb!_>0*`r;{#V(%1ma_21(zKRCQ=t5moh_c{rjBFbH4}Qh zIXmpUtds2v%ZV4a_haFAi^dMC>2+TAb-<`LE$#&A+WGV9>o4wgJTF@_ud(fp17XI>Faa3W=IoaBlcRRLoO3d>2kX1$_D-F{!nf^O5Ite{IT_^@-u-OBtSAYU8Qf4bt*P{%ba z{OPNE1k9#j@oy z2ShuXuyFIp8fDuJd9uyJedl`(nEi@{NAB-4=ID|<*@&x?HCKZ|uCVY&Gd8DfOoj3V z?bj?#{r(^e@9^lR`OS%WvJJA(ExnJpu4dsQFDVvN}2PwhN7 zPqwT}+_9*!!LcmdcIC%aJ!McnrN~`6Og)5!%gFwfr`x|+__wDv#eCHvPbL}nzD$2! z@64N9Z6?l>rGG4Ve&B5TmMnb5jhKF2jG%n6|2v%T>^|dLSFV{|pDQ~!--?@>ZPlNJ zKRQ0+ZPUtJnP|5US6X7xlZ8KRx$5HmyScLV4&%AMq?FsU@SIx-F^`LKVSW3C`^;cQ zI~M-_UGJ}~^KxYmMxEx~ZBlH(!rycpv}5TpD8GFLH_oBdkcH1Fl?-*-ohz#u)q;2E ztoa)jer@sjV#7^PetS8uMYB!!S@<)R`FYcCa%IJdO5Xe(L6=x~L6h>wy%$3HLx13{ z>D}Tu3tyDJ>E-5`xw5%8Z}Z0eTC$ymn?|Yke>Dlpr$mOo2-T}tIJefZu_6ZYm--tv zH)uDXgOl)p3CXjMQ$FBZNc_WFg5Hh|CQqecfh^>bq3 z@1}b{nAjTe|IVm;NXKJH&E{Nx(bg6S1GGK**9_+4jSd&YGEN*} z;l1`>e6l<*N0$BUn8_>qX}a+`(J!=k^GPVbvxRBbyZ2YK@ILnzwfW^xj%>xbiKZq` zo-JbGS30HIHs1y17hg2J)@;#C7TyLboVF!L<`isi_U+N*2`oI~#02NJnNWV%QnNd% zq|rM3q)l$Jba{?!%d^*J#;5j$>gf5w>+!NhP`+rSz@===Ko(B(lR2`eJLd)4N>=q@ z;iC%PEw7pi<%hVMZ?AK9XW^x8#o^Bqb7YejeQ#b}EOlVvn>ZO;-i?Lw=eBGT8L+DZ z3m;^y!El-?jzem`A0pP zoy+v!#lri(;anKn0`Tck*vzi3U;_*9Job$F*Fwa9jM#DA*%dl`mR{eH#)t75*eG5& zt4p+h12pywobm#(f^V*YfG+W%e-yFTQ^lz=;&lyv^-Sv3zXlrd7C~?GrP0! zsFmMN4b0D$Sv0%R#%z9wPTS4FV8%(CY$(4b)9U$ZrEYzWPpW)ibTV6Zu=wk?=37fz zu<)vks6BN@p!{5ib_+kv6teIT@BF)O_hrl4Uu$T$(k4PjKg-@E_T=q^@{fq z=~<>5$1J-?IV+(22ZK5mpKD*FgI6cyH(Qb|E57b-<1uQ2j;@-!_gXS}0hB+eQ>XQ> zo1S3d7O}fO-IaKW4X@89NEePcgS^XVYpK3(u1rJw;7`_KaxO_1M#x3>N;vW6J3tVxj!@?d*>n z{h(Wa;*P}%{SWH!e3n0yKd`__f8WoJ-I0>)4fIg- z*14qfOm7{$>xIclUQqtQuCDt005$aJ$(bHNZw=9I9iHBE)|J02tlKhYC_iVbyFQ=r zz2We!d%6KVH+$wW?4($?PiYg|)m^hi{5yH-^AD<&pZSJ1Fkdp~^eo!m#FB-(t@!nb z%o@snc|)qtSM)7(yPe(|=A+1|*V5x>%~<%|__blh&7pjYmA&=(jkw#_1-2G2->G-9 zK?YX3b#I2I*X@M@DF0Qgw>}?o$t8DlwQ;s=%gYjc6Z$~RfE$(0ZLSugq~n}r`b z;lQ0;lO?0Pl>_wo8UKZET-v^qNC@Qnii|hYN7nNy#e}s&XbB_{=~;w zGEPMBkPY1Cy7hDBk58sb%Ck_vG*F-a5#CvpUv>xbM;{Jq8{oNwg>SqRcW?78C_giP zs6Jm*(r3rEiPxY#2fK&X&-X}W;q-Y(mhL=+h1ZUId8_p$=x^z{@Q6%f#GW2=(pPsL^PrgfU>}sfqUTtB ze#~##uj^}fWyw}7^&9unr=JH4pSa`pizYun`MpCY=<{g(< zyr}NKYE0NB#Q%y!eg4ho18;1`cUdxT&z~os+bWUh__3d+7M)lF+^NClTDV_ zev>7$IAb%#S~a3M3%Baj`5U+8P`=ZcY5M$LwB5q|)FoN6gZK8&s1UZ$&6oJ)PHiqO zg7WWh()9Vj$Zt|o+h%0R+V>ne`;}vV-8$dgSX(+}ZkFzRgN0AMGrq&kv@F?!NnPjX zA1Hgp!iRm|UM`&h<`ly1IP zeKXM_7V$4ytew68`%`KhzM)8|V&EPFTO{BUT`Pre&>PHwqT zw{IT*?$qcoC_k#{7JYto!oX=?@rFWwmp$L+J+er*k38J7;@mC;lpkKWL!XbmdCPio zXaL}|_}I=3%1-lG__?9WY>WpW{`cJYHu9<^L34sLxktcFmh!);UWjFSGEGHSzn-b%gQ*<=6E2 zZSC&Vfrlk9-{Z4y=6(Q~~wA4_|O&^UCj9!+LV`_Feg| zF-M+TXJg8xf1(d)2x>lcWZ=;B} z-P8ou!zQb$T&#Gy`Mbqkm9)zY%I8E@>+|!0t22vE@L|1O7q zZ|t%aC-3&XaQMdTx%L4bCpyX#Cx3Uk%g%3(Mn;)+Yq^%&X-MGA4aTbEZ)M(<=5^B_ z8Ghw7BJJ6@V5N2YOD}GP=ROfyNF(z5yM+AP>GA*ibM{_ZUtbsRhVeWc_c*+NZcfp9 zM;?&EAz=`P3P{u8h0*ZB9eAOa5%3_Uz`u!rCvHtqIbA90D>+5gB!X;xl%gy;aVV$T z9LlHBfJ!dqQAbORDceH=>h5AOW!S4d^)TO!dd3T*E)*`I4irD4rf;0Yv70p1U~7|e z+)J}s8O3Yvne-6U3%9Sm+3N20%Xaf*+kGRSB*#CnX?5_g#^JyEyi-h1364Rhzab z_Ye7>$k9>BvC)udTAqXz*g7MRrmxYhOz7&=-K~ckDQx7joHw>3-~G3@_Z~fIgkuEF z>i$9h$41A*PD_cL7#kNKn?yPvrI-J&Zu`Ib+_5WH(z=)pNs3hHbp!T;5L5i@murKJ(0vMXtxjy^U+L$GfH@FM9C=&A?mnc{yn*b3&K(p5%L#8zq%h zKIxx*x=yM^K162_PjPP-NzYnt6Q`2 z%f&lo3zbvr(;F{rwpFyvJ7SS{XJ6_MMM(=xvVZ8e>4_vgvX|MZ1^jJF+v4(fbCmnN z15SL`ajw^l^9iS4rg$ASIMx6ASxH8=iznPZ5In5bx%_lSzmAvXQRdDv!@_=7^X^b< zIu5^)_?_W{V#D56F{!a8Q9q>?xpvP?n0l)?ezNGri`b7FZe70HqW*Es`=3%oQ>{+B zSvu7Py|YwsYwL$zyFA!FdvR*O+MP~)r(Pe@aYl!-`yb!O_j|T_?2zb0LEHB$-(8sW zc97G@G^6bgw*7Fw^wtvBOAc#7+ZT-e@om2w?W;!eKA73O{vq`3y>t9~zF)m7zCGbw zXODF?CgQ%uU#(i7()q-3r?S1{N{WI%1^ryS-DZ@)UbUm7#b|bN>ZNS3ONf#oL zOnbHZ@#*mNpv|_W>&lj&-1ww%=bcF#Qkvde5& zzbYQ`?1RM#w+VcOOZ|p5?Ots-v^cp;Shh97arKlv8+_hK=Xx*cUYSQe z;Wzu;4|hp-^h^Gwvdg_5Kdj9a4R7n=<87Oy9& z|J2a#GnAD{+AIgr3Fp}d!cX5Wny@$H=dnf)U(ZdumD%_Gj8=OpmIiKpB6!f%;R>hM z{%zM{KLVjRuN=eKrmzZJ(d$I*wV~e>O;4MDzWb*eBBPns+m}yx@4bBCxlvhZ-JPvv zK6iSJel^EAyZ_3;lQZ`|{waUVe5bsouRV-qlvjYYb>X`Bl~$3He~9F7O&@N#q4}-i(<2@s~&6Cr%XJ>C$vv^N<&tq_Y#vuBI1Od}uW$BWC~cptA>zCT%!BqQ&Hw z1Dy9c%)V;3Kr(3h!;yX|h0X1zEr`2(J$0YTdCjl)f?w2ScvCjV7Fm>;c>T2N(WwF7 zD95jTH1^Qh8guI5c4@1rt$M$IE8f^L^WlqjMrPLI_DmacyhU(!_RXG+jp@m9Gnj>e zy@i_%l~<45xK%i6(3v8K@?R^4g;bwZ3A&6f-FE1zv+bJWW8V&q4Y_Vzef8ZwS-bZy zALIl%S<9c;t(iY-#!shj&l+`h=;R!y`bRBWM}9IDZ|G~4vhwooEB&TEKDmDOliuq+ z2c;c&c&dHjLH^Wn6T03W*5b~jj{RE2Bm|v4x2Mo%*@8C*+Fo4w{oX-Cd;7^MZhW6N z>85FQ`2w$s*0IY!ZF7Ed_x`Y$9f6b6c1KR_-em12(S=PnR%-^;Tu!c7G&n!2+hhZ` zmbJXC;_&Vt4%LRIH!L*p<(`@o-s$k$>(jjRJFY0LF)sA#krth`r{DHye#V}L9o4Uz zbv^i^`+h~2XWl-C&zJk#Z;dDq4~gE>=lRvF%w3{2F($3-hdNjYco_~w&*FYu#Oa){ z{Mza_Ga^prdhF~v!&y;$U%GG3*pl^;cMgoXs&2a9Z|Z@A(MK-Wtsa(e%KXu|5aFU0 z2^%GX?NuAXW@H+KJ->El*R&ni%(-_bMXdiQSi5w~%2q}xZ_-Q?XRq5osM+J6-%L9C zLs`dPmQ4%jJFPl@OIqEjxEqs~Z<#f^Vd_9?-=%>^$LzWN>nz7TH_9CAPM`Q;&2Ysv z>qQ$1S0CGJaCOV7*%85OvLsbbnKKq09H;1;{kmt}jl-QV%?-mCeejT399 z@Zb0t1_t-GO5xgcPy~M+@T8>mZa*)7`!%i`emt8}9(U_jnc-89`)ztXXrC9TI1^TW zV$hbB$9x6JTh8=YeIow#jib*?zpl8psM&;{uKiS%u0C`6+xtaK2(;KYSKUY#o$BD%GTzsT{FJdtXpx?sp4ZpW~@GV<)zzkQDE0q`>P*r zzh|kT%*UT#Iw|jbwTi)HB3I1K&%zZDv*?M?O=R;o4?V9DJy3FYHpz!Dj zY46ZwO9uyESlcXgWpwz1PEOSaM#LF{K>VelN8cs zUx%;C3!1i~^9WhW)cS23>t_tNmECR`A6Qy(BRjjnc0ttRz9K#^^3LOu7Q7y14_*2% z2@Wv+wj1}r!RH2Z3w@i$Odel(GgE%y({j1*ip^=<-H8F~YbP}san^A5{uwKygsZkB zD$74@a&KNQ?L>|IYRZXU3%x1_EO0 zl*!+OdB=o#&zyPJjCo)9zu*kzzXJd0C>0&3;_rUc|Mx%iu1eX^u2$Xhxqp}k{os;1 znQ!+0X1TN<$y+&GQ6(f6eqAqLCgeH7iAQbMDW zD9Z9PEP$9JQ}Z~A>4Opz#zqdEJ}H)(!;Mr-mnS7f`b5R_Pe_SPN{E{13mIYM`TVAT zQfw@B0&_Be!Ld`Rflv;-5$T0WN6exWh^{Og!TyG zE&qv$QTU!;qH^@aSc;kgHPNpCka_nJz(NwnMJL9@hWG|M!vpVN-~RoXrwc#75j<{2 zEEUMK#Xl)Ao^Fj;R}aG2nlhO-4o(ap7TFXv?{i6*k^bqO5JQTk+_;MAkv__C6Jtmp zsJVZXo0|R^>YJDl9hCy_`^8SA&H)^e9N9lPSUEZc(M`3|wTqyidc)ud|3{-Jo*f?s zvBr?YJ85jBA}T34cBpa^_G2B>k0H^~%A};&7}z&>Fgnn$gji^aGCBoz76y{iKOWwR zkA=aFVV^B$-uImti$ubq+b}#fDba7-lyS-95)P#@uXq zd;%Goq}0_e`b<6jqLSmNk4Qb_Cp#9W5j)r&LQO}Y4TF{OqhpgIy+xT=i{@76(iPI&nnizf}4fxGG``p1_T8Z1r-$)U9!n! zHF|qjxDN$bn=*VJURuseGEzoY8+ZXVwgX; zkQ`};qabCO?tFVXB`D2N)GXR|&d~_W+WJ@q+RMk7>zJ07c)GPE)|rYh%)*+MSR5(@ z$*ygQ=ThMm!|bzNk``jt*T+yHF^6xL%V#6)fQ{^S`E-IH=HTsePK_hBOWMht4R8my z!})oJIUe(;BFPN%Dzw{~&oSI~@C7v%*;pTSkEv<2jo-?5JbL9;tGq+b7qw z05gF;YssYJsA)lqwa_lLM4-$}_kFY_)d~4$)U-tHiv0C}3qT6|OfuV*ez(}n?lmpp zWUj5nF2td~J3=aYa%V2z5{t$G;*$ZlV>6j)kxc&Jcmivh4^JW>`>7VxN>EcQ-I59? z?OaRTu>lP8R$WVH3V@r*Tn4$J2cTQ9`dPItsU?z5 zCetaf5!f~8M{8$W8!&HsIt?8Del4*ySc@G6R)15Fz=SX9h**O!dbhR ziFt(?7te)3flO@&CW=mqw`RlHZnRiA=H|L1I_+%t6wp;Do*=nn)13r!m(&6iPmM=r zk_Qa)SPi7dqUucLX+;x(KOQQ3IwhNq*tr}$?#8lI7^i}M7?T4W+W3oESw9ufA5Vqz zwv(McXOsY${-(MqEBD(|Cdix zo8F3cemY*gd^>?r`Pk2exApQ&p~?fV1Rnn zgysdGr|d2k57&vON{t+q2e$(H3uW@@vbMqZVKa$v1j|AG7-&`Jt#5dA)bJ=C+Ed+L zNB`{=f$#U7pN)b_BWb$1M)?vBmWP1}!BpFm@WXP`H< zT<>SGs%Pj8vcsW4TvkJVqj!*V_y(Fg131-7}l znGFb~Iy$h9X;8-``1Zr!DEPzQjio#>UjxHzO&hc?#ZnfoFU9p{zFa(x^@AKSc${H8 zM`I|>(L5|Vzo?g-waMe`NE{q;CQ84X-SokTMX=h0hjK@f;4_@)yOzql=*hPA+u%Uj z3#Yn)g^5b&0F}@P5|3)HI#1RCs#nG>Y~z&jHmF|4NUz1|Na-!cLXIy(q9LSD@BB=k z`6B2`Rpr}4I9>IKz_30?Ph#uXUQ0OJR^AUtcH;O4X#NyYfvxDbAzUC4C|ppGA>})^ zwtNRIAKTjUMD85N&DW@bHO+TsK}9%X-4>sPUrq+p-DLa$@Zu?zbA&tx^5V9bBXaBz zp#OR!G+v>Ot&`hREZ1+nEsTGv~b8II#$IYIKHu~*jG84}Z91zmP<{?3N%K#~FqaTzMae_xaqpMR6 z3+lxb(QFu3w4yG|z;Ddz`Ub^v{na0L{CGke;*M4|;C7L`Y$8?pN{v$e2EGV57i=~%zoyN#uENLrEv=ygz7xU>R+96G|#m+)KCj>hrSUq|y(j1gjE#E&6flZDtS z^NoDbICLoQN9&MEvrfZ}-x6gki&6p^fLta2IP4m)PBM^62$&llbNYn^b-;-qP?QXJ z(*je-_`L^u=Vv)1XP6>UWex&xc&Lb#9JWqW&WR7k&c(IcYm`!|HoaZ3>BRA2=K=N!Fj*b!_ z9VI{pQg5M|#+c=9fXfFi{~%u>!NV*9KzNB3@}Nc|qt(PmMQRoxly7%Mf;hrtED|Y| z!Ofz?4<-_rImH8lkt6en(1#MRX;3d1IHQ~qm|5%{sl34vhsI|2JZJ~{!hak1g_4-2 zurk8Cb^-*f4Rur`ZD%92HQ;CH?LcT*l#MtY0dn*diMzf>I(KXfI(K}3g#LE$R}s2n zbA)D4cYLq(ifs#e#T@Ws+X{ZaV}vk&iaH^i!sFC|c9!j{v!2a#7R!+ilm1eAQ9I^4 z0Y|n@EbDb;pNl=iM8O&D@_-8s?m8J0^9cxCK=6Z;8w+_FOB480ma0X6tfXaH@|aAIY= z=;SS&;Q#mcYYRHX_R^=GKG_3$SQ50Yz{B^Nmm&5VJg5FxEn%nH^6ki^{ZYk(T)*X1 z!BT|ZOT!!JGv?As8`sk05br$rrdi^Irr4B?6XGDrIub0CP1D6j9uh;Ox-qP7g8;fX zaP5a|*%D#725h*2D>SgUG=dI#`jO9*VVs7gL&QZ;Xt$~ADPgA%mi_-5KWuLysnDUG zzJ#QR&uRuGxUJ;R|F*naGY63<${~czEfll9En+dsSc1p(UyHRAwzisbNH*mk4m95; zV~tWJn(JMbg+ANb4vK;A&<@UygI~Fg4crz1{4ESz#{di*#Gvt#-N1!!oG)c^RRg|2 z(584QY8QvDV(~0A#5Lh$x$8NG^N3w=*PPKH=t!y@pma7$rz#z!k*J)&%}yjE9jN;} zq)wqCXtnx0h-MvC>h;|(b~@Yf#BWNb152;Kms~E6(PV8Buq#8meUhezpHW}+y6{J`ap{|?xV-v;akl@5g8xJ?M)=J3gHkzD@YMbwrG zCc)hn%E5ns{>{rCwSSp(CW5Pi`=3#DsE*^>NOknh-BEQi_749yxO8hM8g2BBh&K?< zlM!-1+r;;_IK|cr|Mup$SiiflR+6IRA%5HUR39eNUD#h|TCNNsEUxfJfSoz1LZt|M z`W9i1;@PdW;NMpD>H`g%iU@RIsN&?5qf$x?{GXnWJ}D|_qMr-R&}VpFK{gUJ@&2Ng zE>2mYW~Re;x@TZ&*d0G~TB#)CwrdB8>W(Cq=;`TOASf(V=)yRNnt=+P^xu}Z^tjRx z$BTM1=g`|f%j7EtbKL>a;^2onPK)-9QUuE4P%3>pYk;O$XJ`2-sfU&rB3I;x?*|Tj z%hjk0bM@FB8QMF+-+P4b|F``8@gC(nJY%s!%Y<2>$)-CT**tEwV}0HJBzR7_PMn=b z14uSkU|jCYq;t7=E4W?xY$tBjw4;q|stFhZt97)0(%&EX(TC*G#FB9|ED?9cN?2dP zG`mNPNI|=V_~<$I$xfa=RU7G5rB?fI@7HGRV{geI4ObbTa=!S!->0fxS>laa%`d5<2lk{ zhRzIG-vnjdg%QqRWcf%_JVtW>@z4XrVc%4@$H^b2aZ7ksIG(^cwZ`JOQfd6T_|mDi z(2(;KSK8>#=y9$yL83Hg01O&)R*pCeT;c`zLG~Bp&$10iJb%7mC2A=nK*#%6&9Qpfmv1@Bl`D6QbpG zcJxcCw=(iOVYc&-Ops*5T`pIk^M#mon9tppP+DUIY7DUvt)i8~%HgtEVW3RW{bOS8 zxGbgImP(Z0vqiJT{}aFeiQoUkFHhz~@I&Vg=&xcN0C1O&ouJ@x#MF=E$$CZ-QMopf zyaQ(?jtLx_!qSDUE522}H?-H^_`}w|i*ExwbZk>ZY<@Knu1%qEgVSPX%K;}3h_s{I z2$?Z2%~j|(+g5;pz7g(^D{s4O!<^eX{m`6+O=R722N|4^ z;@xqrZBiY1+~FybgLsT|t~5%HP9dw6y4h}&naN}xaTXY>@1*|8&;Zo}1KXkGNc?~@ zBi!yLMr@aCU?en)2_3!@>}>YLs&TZ2Ry&t(AS+EvN|#QG=F=Y3K%iZ+c7nRfbTb(I zi(4?sw5Xq~pemZXM%}5=H=r>g%ifW} z0i;F`wwm)h4qSh&ldS{A#p=!)7H~Myv0|J3`C@d|hSU#uYRADXVEi;x>a1jUD{!}B z;Hn(5v7*&k-U0MNY4DL*1b1?VtgOMB8n}jH6=12+DnQ$b?ndJFb=ipoe!F`aOTY+n zfUK*hYN}X8acA!-)hiy^suhpuR-ns*DgZ6FTSL)*Ar-lso87HV?t`QaosXinETK6^ z*Je)X#*!^am`2B+0jI}P+5_C^uI9jz0-&Q%HMUsHr?D05XUY~Em(@kk>GD-kA*F0P z+TR)#XOBS%Q^jooewLth6YF!>=JK{(EABRrGDLZ-ti2LGGrpDk=DX6xqI6`VST+|<+LX*rXh1hIh1rHt zWvkPgi^coq{}SJK^jhg#lCDkhKP}%;#qY(!eer!qKYZWOC*OArkncM-#rLH)%6X{+ zc&RsEV}k?cY%#z#SHLhZ-X8dV=DyD{pzmV6_&>HeyOApHC`y$m?1UMV!>AAB`0}m` zSE{*@Xy35AlB!roX*ZY;Ll~SmhddcOMb=BDDe?gP&McV(Kv4wnaqfeT4`955llU|y zC*Tt{2V}-$aX?DdjyNAzY&vO&(NRd_2Cw1@uOdADV%3eL&79L_KhhlA2GZ94lfLqPfiUF1~a>U(s9nS7WXXLwLHI)D24zI+W`N?oA_QSzTzbsa) z#9_5&PnnL2^{Jy`l{yL+8}_NAVnj!kPu-U3DDu2a55=nHUMlL?w0)0N+jqV^LZLRk zmnyB+gR!l%@3Aem@3C^793wiJAjupabq|)z z=hcX*uCEM170>%Z64Sdc)p)9?qZ(NZvrX`I7%HqxqPstQ^?L8%tJiY}BNfm4z*oil zGJI7$-%5N{)bSnR>)=EhisG;a7}Y)yhJJq3IR_Ul4Z1)~pXFRh9B3IwQ~KS}Zco4E z23B8h;#9sbm-gy+eG_PN^g+LM^KkVh+(77t`nPXf^!Eyp>}ww9LF z@c0ftoU8?M>)n<#b?e^J?*YndML+tZxm)L!#4Yrkz<3JJ!L?f;N-Awht-P+F+IQcA zSfH|vTxaVh+SpqExw4I9XJQj={5yTl{+?ID8^f9*TmL}+C+O!&n6q|r6FAPsaUv|& z9*^beo^N+u9(lw@=-KbR^QAk1h1zU9RH-6of93mo-d{aceW369?*@~wV@2@#YpXoZ zzZd?*X5p{QpGkMY!`5^gR7*yA$VDiSqZ!^f14S-C8FGh%`>qTgci7RH)6Ck5Xgn5c zve@Anrt+CP*U2#b<~^k)+?p#pQ<|j7`_ft1Z58h!UG~1C>U~GA_m%Z`RMj7=dLQfc zzOw#URsE+RPThVw^Sa*#pQJw4*BNhQomNWqIhvb(_kRp2q!FAgYe;Vq6cR^d*}mMzpEhc*?9f&PlL;|&{l zGloXQ$ro-cJIA<>_pRmcomYL@y1bptkgU#lCK=~M!$RHTB=Krh8I;~ffE%Uo7%9Diz6xKp>#EvH_PC}xSDDDA5WPly}hugc{g$b6)7NAZn zS=gGQRjZ6*n_uUIi!CUvu~n^1&M{q*qe3&W$y?eXgLKIG&>FV1W5!l>tkf^ks#dNK zoM*+(6`IMSkxlfiP};fxZFGgR(HzTpd}OLQjEmtNdkSm62!8wBYJ8xT%K%hvT2pH<|EQ>s+ti5ICt(}h0r6pDGa z)K}P6KF9afZd)Yk3;SEE)_}E<8KNBK!h4OX*B-j7Bf?~Qy4B-s|8|+AY_UxRr<5~_ zJ7|RM_u42`vD`HGZ_Sg4B>TXJvt!o5Mv}%y_BKWZX?q~>{oC?T`q+hh6-vUaSiMuL z&7bq<8%tZY$h>ZwX}bJH(9_16CL11+V@I;BBk~@fgS4%|mIK%3y70Z_^BqnH3UFKj z5kQtf71zU-ZE0&AQ83N0>ntTp{U$f6SqpMB|@G_pu=K#5W zz${sDzcu)sd;tCj`h?u~gTp3qa*JUs9O<=F9iD+=z-<&L5V~`u(qzcT;;CO{+e|&1 z-SS*nSJg%{d>?P^y;C0d{T&={1@|cui94CU1sKTprRw^IBNMLxBNKNbkv>k4riUk% zR#%XImi865##4a)Sg&3+zza~ifC+pZi$~(XSq(+I-cazQfh=sQ0Xs(b_iUn1ZY#ad z1K5V`^l?N$_32DytFDT^g5%q!lf6mXP+EvYZ~h`)a;^a%G!*bC=u}(I??F;g(aXE^Xu0CXBlf_HEt*1ZVR%c_KSv()l-&eD!FR|IaR7Rn>ukN^` zf@MB>HHZ+cH`~o*^;ZJWh;B%CnfU;hy?aD=b3a(1@bp5%chT&oHY9mAk3EW={&pNO z+vI)!bjjMu$^(Xb?L2Nr{mKBc2Y(I4PXs8#5815mi;Wn8=L8^zx`)S1)Un9DetKR&!CA$a z>UKO8Hs+y?MV&2)xN@>Deck8|AUGaBsH=Rp$31n+sf(VBYn9{+|X8r8%E1!dn64*TY{4>kW zL7-luCeQMczJPRnB z?j%Q2x&!1RC#GV>i_%I+?}{f9Y7SSsylsitj&P zCHh;EnPfQM-a_{ikV7Lhw%tzP-rnL4l^EI3g!xEkW8VqAng&8IGUb7^@`Q6+O{>@d zarXV^tE5%_JHb2IELVZ|&7i%oHE7Qdue@eIe%Xv%x#f_+BzmXkf0wws$kEQSHZlomM220n$m5cMHj<54K|*RX!VsAUPRML8uCK_{lh`pE4eJA&nZDSfIxgK@fuZAX_83R?x}AV(}3^VZ?|6 zd6iF-N~lvpFKu`C(v^0j6$=9)(IqNjFG;s%ovowOuZ@kmJzQz}{5!lGM# z22>f8{f^9St2_iJWEtM(P(m`51{N@MBtr$BZaS`%viS0&Kb>Pab<$P8i$kZS{s$b3 zc{YdUZwpTUBeMMl-CG3zrUq4AUd({;w0$X4kjpwN6g%4zv9oxWTcW##C0YF?On9Jh zx;5Nde8>9egLopI@5b>hwu$9O3G`%Cu2?6oRU=UlIjD!6#E!Rp)?YQ-=PvuBg`Ht5i%k2KE(%7H z;4vL|o<*1~GDR*QkKnlnc+G%w_o72+st?HNN%Wo#!WUc57B;AeUVMQEA(Z#5Kj|;a z(`&NxzqK41 z+1-t-uba)ar#ll-rz#Jbg{k6YdOnX+Yws|9AoU_;JiY4Gw`Qcnfn&3ybKh6KLUY1y zvr|!UMyfG2$$7EWG{8@>_TH5EHuZ1r5{*8`=8LclV65~%DQ_B4g!Su}@-x&8+BfKeyj9QhkcPvhhbp=(Hz-w!9Rui#UYOOaxH`Cr0N)PLE zZ(Z_K!sln(!IPVXUM8uEsL+1EErWOS2>(yWdl0=g5*}f9iWse=3F)76cM1k@{a-=U z=PJ4ay@Q05?Em_pzKgmCVOK8p8Q3~@%CC?myEQ<2T1ddqg%Jgg#DPV&qb;n@MFO$D zv_fEm_IMlOv3QGgvDGc_a6j!1q}i?8^Fi^Hb0~yM>T-5|V9?^xqR?84D>^OY_%sWb z60jq{CwJWy;zfhxY0K?Y~sP-br}%$`#{mg^F@4N^xowxkd3DR4`DnM z%R{Ph2M$QEhY5P0HS2bxkq&3KlJRX^LqRy%6YC_0t7UD{wzH0=zaoADn**5(0lkx} z?2ZMK-YZ12l`lvvl#RQ)_qox-ve+?^d}laOhD>akEW4;!r4BwBQX9g$88QbplWMmx>*}vG!Din{LrSr-*PDNKuE&H7*=x%b- zA3b-^Wj(U~yoz}#6ng3KaZeccx-}KYdJym|o;Qp4)^&8!t?%L1oUhQ--8jnUBR?BQ z;#YTL<0zrzKe)ZJc6^(AN=r=UYS5jmrf9M7R)3&>A8V9 z)a=MYUCxpFWX*Q2y))LJc^WW!Yry-7<9N#fp62HC5zk#9yGETGjtQi$YHnytr=z4< znmc9ZauD{BOSv(d!>l}ELS6_KkA7xF!8u*zY(~m5@4(k2Wv>9^iBX$DLA0cVeL4{)lIGvba4>H zrE^Fadj9~lo3rg`1F=@4@btxe1(q7{DndtJWZN)zc$N*h+p6->Tt37S+iol(5Uh_=M~?1J-z^sb2HEWsfjv)J4$j2I#Efb$02CR$xDNC(Z! z8D_!Yotwb0YCOafAdGb;2+=F*Co)Jf#dBm>9hY?Z$yiutHj7h-4&_*va@aoQIJa#~ z#i&7W_!UPK2(zb&pXjhJorH5(xRGsuW*hWkX+&JAiR`L(t_7{UOOhN<=t>4K7~k9s z+z8K|^ zZ0BT002wW}+YUv9^R8M*8N5?`W-8r9Bo&Ql!|+@KQ@mLK-mm_E@oxB(@p4}>-gUdy zc$W>S@iGV0c&Df}-gWJaciBwFI|IHqq#5s=45Vi<-YI!V!*4Ffc)eN1OBCJ+c;3i( z*^%(98{WYma{}a_4&_gxdDvFQyWqeYZzcwHMV)faXGUF(cje$3@9LIPeFKGaMx61k z1UT2hH=C)fxB9aT>RkqK&KSAreEoe~ILn`vpWa*kd;U}P-epJ9cBH54$@g0LUgtb} z$9dM*XH)(4LSI+IU*=@M`DXe%8u~i}`a60X_7@jy0y8l!ky*KTl$>4K;yz1e;@M&X zK8bk5PUUQ-+)u;jc*#rDZtZMyegn?X)niJRztHEgNE$Cy!MLO)5{|UnPJZ_{E+(L7 z1I2K!oU>n!>}@1_;jpb(ID-4Qm`t47Wzv2}wtNkFX|9PW&4DmXX)GCkm&Wqkaih}M z1HLcDuMF8g6V7(yS~5IEnB4E-{v`MBCG1pNzP)%SsB?FudzWA7E?BJBSrb@Xmxj-r zHB#>S8+T0zy%(Et?&xLc9cm0y+|NVyA7PqC3htUx256bibTjn+7J@SeK^@KmB8!Z} z>-gO1V29JImJV8hn;-ADpzRR9lyS}scJG&R;sM5uuZwXfmO-LpT`{Mwn9xDa6CiNm z?s3N1ht4p)*SwTHql-f-*LpBa#oCFAbpz$^5d8AFWtaC?Jp(dpGTs{ay;iIDu7UJu zCoSpq-nEdP8^Tj6w{|T^<7r)ZAW;2M$^7U01KoSoa*%Nm-XH}q&J*K)9shnduKy> z@fkE7yaHf>wD(M!z7Nu8BYaMJ^p*AAd5~WBd#8U-K;4jDb`DLyb8WqM5#nPmO=qry zx-tJ>!69rqp}) zf;4le^Zo=V3+XwNXnM(sKwn6&JIujP3%~+t?hu+T9AED>u)g^;?Ojsu9S7+R7tr*S zTcB-7ue`|V->pDzZ0};H{8FGVq(?7w@OvB36VeL~cIZ15`UdG46KHzzvGv{pmY?GE zzZGCbcz*7bM|zc|2VD(yVgJu{^8X6xh40UI`1vNF7o;;|oHX_s`+I}~-%RKmw*S0? zpKE}AnEy?uz6A6Q(%!cm`XuYUPar;=^kPV_hV0A<-=^tlDWETw{|ime_ygeLIjG~04u4Oq_YQ*ej6XT_YXjIIJ?GC({f`5^A)Q&{ z!1FNF1!=~4zvPj6uOHIfA4~8^SijR=0r=m*`k!>(gS?K$@~a*E|DxVI;W^|Xhu#+h z?9X8yCw&RfAJU6|Nz>8a)O!<~vOm)+p-(;YxRZ0}a5 z|994Vmq6OPl%^-#RqtJn{axnZYkA3^ar^V%XlJ*T4RviGem1^P(qkFWV@yv;j|~Dn zHmIkh$Hsvk+pVXh$EJZEV|z+^Z1ugsH$7!~?05CvU{6VpjRrk7x~HVaR{kFBKu<}J zZ2&zst*4~Nyr9Qs^py12v_n8I^py12%1NNtdrEq20_d?hJtaLh5A@i)o{}CL4SH-r zPf3q)pvM;Vl=RrfY?|0N_r>=dg#-hk{;NY?1Qfz8dv*8 zE)P`mrOOMeUo7dH(V%a7UM%UejgVgVVo9Inq1|_0tkhQYA5Ic4jfy znYC4TTmMo?@A}d}hnGrvcRA?Y4KJ1S?%AMsH@;NTyE887jdzSw#vgt(OU}AvrxZuE z={=prD;|pZ$>(Lz1J2K7`}U6C=ID%f(Nm?i=5*6MGxW;5!J1m8{^e4>m3n17$NaVb zICN`17?1VU*W!7u&XPuq*~h2Aj=DgxUID+}`MsczelCW7a?g}tTjun0j?ufHYhT#} zUDs|~x{g0=%5g2@4m$AY@$|kDmT%@2wuv2220s%N2c-#{a5kQAPujS!2L~zXz?|lx z1L3*ib_6n}M=#{T0aY9rhunuvnRq0dh__~O(`G*H4(kcc(nzw>ffbwiky${NqQ6l z?5Vh0DL2Zw?ap7;bw3VxcExkfZM3;`(xyX_9y$ful(1*n2^Ql_8}B`g;7K&anE;(k z+LPtXc|Z~wigrdpJ&AbA4riMH%(iT&O|KPphC`hh1!qv2&;r{g!<+yhpbe)2@D5#i z^dA{CtfWT~Y5OoX$E?$QQt^47t7 z9EW?G^64hx1=14-+MR2i-4g_GCKHE^Ne42v9qsnGfMOeDi4>k3My{VBb5w4h{iSn) z@G@WLcpbloTE3>T^FW|aZIP&-O+=D@_h1TgpCJJNy+6o-TWJX5=*BN{(OC+5mc~gn zJQR~$v{&SKy3jLjQ+y%kbbF#{77t@aE_c^~0-DLjC0AWE5p_|Vb+c2sPI|LW-b1HH zamk!b*2(p{HLHQ`N(0U+C%efVP2^6hOgc}wB+=c}-q~i8nXCdji|(MoPZLw0JyOQCIY zV_2>WmdjMaxYS2qlZ?}QCUSt?diio!Elr4WBA2@+gdv{>-h6XA_d93SOfqxMZ}!Yy`@hav*;xxN4ZM9hWXUui)+c2zZ69o} zZqH#)Yj0$)V{d6MZ!bANnrN#gBwQ-OE|LYBerC%qqAoNdWIb&EJv--F+OCzh!_@03 zyD0~LJII*X_mI29-_x4+6QaQ3hJ z*1I(#cC|oUPvMe5-#T3Lq-85Um&#eYhH$B6vCKuvMZ5M%@7A^$?ZS1*)=94A;TaUlJo6#Yt)nRd}wzuda?YovseRXf+#Zqg)qMYuP@u5hoz(Swd zmD=HtRu(wq<<<1}+^)kt|Jonzb&c+}1!ffsyMFd7sIJoe!CU;Igm&D6Lfcd0>}NEFD?ao*L<2976VT#(Or=E>{}e zn>^P}+ag$~@F-dym;AQ@;r%Q_P?p;)p{ayJ1X-`gS<_2M^+o8Fat~=3<#ke2zduU) zx_0)@+VKW(q*E|-BSd^V>ShT7qcSHRMNwg>tRotXd$hY0`c(V0+Hgd09B{Yr_({!} zP=<&$SV6=U_B8$~X$T!k9bt);a`Mowa$O(yv%xTT9G5!xdq+_0UIdT)v%X%_3k&EY32(IXy3t2XEyU4E3{+-K`W0@UQ zd&4(HWL#SN25!4g_oZzP4uXG%zusqe$vKbTyr;>=C(2Jv@>uEH%)v}+ah`H}%n=Dz zsBvs3S>LdCocD-VfRBey6e<)|e9d_)1EC0e*q_m#5cU?s)1Mrchl!O}l6=_^Bju%j zJhJ|Jm&PgPEdGpRSAUnsDJP`belkq56}mOQ|!75?L z5oSfwo(p5o01R^suz5p{beF}!f@+!D`6{7;e0w` zuiG@3s!OLSxhWY~HQmzm5^D$2**X|B8Z>L_(CZQUI@J1*{3P~*n2YV9D7z?|uj|ey zU0TlpP%*gJGL|MTD-J>?87h0`zOzp*l55S3*W*4yvURa_u@UP-FOe+$Ho<$zQ>$am z^N;7WR~VjI*kI_uA^?gC*B?ICM{l6{Zy!J&V~8;hxA$*-AP&|of+mutlA4l+9SX*r zW?ah;Wmsjndx(6|3c8=tKZy%H_J6w_PaA*lbHL}6&jWLHbE|1O&RNu44+AfTn?pi^=`H3$`90Wr6OB3y)X@*UJ`&C?WFA7?WvoUp9^z5AkFCi+VRcU zd70!a?3=MikHbQqbgsNGiBAofV5WDdXQpSNXT0Y}&)W6y^_xY&9P6t?g*A5RJKIV9F-h&lJG8>{J+l@)pOMLy_E|Xw{ePK|XIj+r<3~+KPnJ&()tF zuX`Umc_oT!N?CM0H^-tg`OsGlsl3%+F=D7oTChC~tTl95YWfa_ol7`n+!ZDH%G zOIhT=7Oz|KSbV?RKhH96v@fuio=95yLvlrO2^G^w@CzZkLA3!@D*!)2-VmSKHqYnV z6gohhNL%S^Ut~EaHL@GaH`ld5>{ec{*!4Pv0`hM%#W-7B9M^Q!cbaCziW!vJL)M|;OYM~_2N$&TM22O$3(lbwWZ zV+O(qZ!pCT-Bv_=N_nZ&UD1|=?L+|tmPF?z>dViUbr{*6uB)!wg(n%V(|gsTx=?)` zQNJ=TGDx#X-L6j` z&iM%Ul#lf|i{e$5)f=r3TWlgM9Gp*E5?ZTUEu8`!1)Mx0-EWCI0>f5-G?Tb{OvTVD z3}XjMuXmUnX7OBo&MS1vn&8o2uT@O+NPng+AGcrdR%m@ z>?3~)XeRvihrov(`P=oBH+9(>UJGY}hr+6V#Q75*Vl9tPSdWH#$@V|QJiL90=vPN` zptn)IF~yOOc4qd`gO3OK20fzVqI*B52Hrg}$S|F)nw;RBD50N%J45=f1(A~+Fo|Hi zSbSbYz9x)s8uykm@QH3|(W*YR))Ngvp2Mf~J!NE>VVss+%29HNbekv+yh8+N06dTo z*-uzlR=#MmH@(2Kun*e@6F<5H`{^8VuuhXj$XzwJ(fH=#^~&MJ?6_6+yTnmI$%(ggWu;_6&~b6Ygk?7NPNS>Ff??U|tX7Jm6qt6(x)VNIavG ziupEZG6;=!h+(#lM-d_wNk%R1z3VwGv(!jXxSSC=9_=0t#@hN6PD4i}DS-^MlCWz# z5l2K4y0ctcdtwmrtSrR%P`m=nt86WWj(%vE{sl^Ya(07o`K4Z)Juu6hZLE=5yp$Y|bNTpHiBK0n}|m@J)DxLVE9N5C2?0 zb*`Ace)qdx?JH>M^rHNt#Gd6|;tLOAVT076(r|1j$TD(Ow5AcxMq_$R60aZcP!33W z4#aSdxB3wB51CJ{;M1r6bU?a=yp`9NF){;~x{B>IG9Tj{GaLiww8KZ@eVZ28%vt#| z>Ttn9YL^LTO)cp?{mYy_Cy&sa=s+;PV_op3`X^_US?|vh`+}~EhV== za|sH}O^Ij=l0Wlue7NrOc*r(h_FIZ{YTg9XWGnVdoo)Vg2w~xD((=$U1A+<-qa`{m zxGwl)c`I#+(`Flw)Gs!Xv>=F5-KAy*x?Mc^nr=GFL$vOAEa+7g7#Y|V*caFs_{4Lx zLLqKoE~SiZ&Qmira;|vpg`O$5#x`YNAE;%ycP?e~=9iGS@lmQydKmxo`}dOcKjW}O zJdF_xut6AAgaFP6=jnhjF(FOly(pvbfKwTimm?ebrh}7 z2d@Gz9>>TLSZtW{^_NO9hG(n9xojeK{yFb+jBwykaZ5oSx`EXRw{#Gh-rcepQa~M%L|NrJ>x9% zxffu8(XiMU0ZiG1q{+v~r}w8?5{UmIZP;cBr@g!%#yM~e8hnC?v0yV3Smx&i63N_y z>}anSuJ>7K*}sveswc0%??H)qBzG^(lN(h?3n=`Z0pSV7e=*2>(r4977Oi2O74jxT zR)t9C%k&q4aY)104eU52#vdR_yw~6U5cL19KByPPZpSdLFYISn@{n)kh5*)QIihS9Z5H3d==-aaXCTFVb9IZtmmxQl@gU3D6!CHfiU#f! zkPd^*1mTwLseOEEpJ*@5J#V=mT?`>t(CG5$DKc~VF3Ja`!OA+UtMC_{Yf$O5JRDPB ztN=X6(twT^;U7MRPzRpMK=7ss@^#6xh%54#`dMy+F#FNDf z-2F6MSNW9c1D9UAZ|<4TN9|R`RTDWwIUw(>+eyKR$EM%OtCLX$faGtKf)*?Os>Jsq z0qiwgNRdiL&0H-n?lYO91A;7dl4N|cO{E=-*Z@P64lb1N_A#pAvaCxjl%mC>K%6*4 z14|~23YL)Q(vQn>Vt?p=2tHIj6hjP$vi30(_Wg=LXsMk=uM*0QfiFoeq305;Rks=} z_M)J)Vr9{9?qzIm`@S2$UUskXcTKlr#ud7sdAl@OP_t4STJzKDopy{QI%HojKEtE+ zf`o)jEap=xNb0d~=nj0QY38@m1U|t#ENGOZKs#{6`yos&hIQ^b`ijG7L}1Hf%VQfz zXkkqjabFEPpqBiWw}(CC;Q`ypYidzU%sSt?Bp1J38UHS1*LpW)E;K!`oUBYTls7tazh{gjNM(megej~IE z{QMGwI2|YCt34{$Dy1W@lkpD<$U3fCFY^7%A?c93J~NCNhW+0(fb8Il{5#^9Ve7SC&Al)D{dOQ^9o^U`$K7QvFNMl+3 zL=DZ*$;ArvMF!mE6ex^|+w7&0JN|enN4Osh-|+09X<)fMpb!C+Nh%hPH$|Y>m&NS` zop`2iS`1|QnX_Ht%@z^HZseafs9?>iInv{j%z+MlUT?f&j$Le$j@cZW>k|tmDahYI ztzsv0whp({r&fp?;UddG z?Xt%wwb$RS(c$mo*x;3T$uC&PyUdJvmwBJjK3pGLKU`l~CpzXjRzBvK!%DLx0%+Sm zHGz%H&(ZJRkAE!pt}jg+uo5Qhc&+|R9@|1LgF|82^O==9q6GFK!Ug9yP!11W9aq^- zQnJdw$(-7!o_U1GM=T?*26Un^ zy_>$249F#I@_UAFgVl!e>*9R42R}~eS-=Jh!s8lt7Y>#f?fC_1Bl_Dv^~{IBuA{y= zdS$)-P-9XAor=&PkzK=4C@)K8dK&9(W%xT5b?|ORpN*P3@rAnggNn%h0QLa>ovH*@ z`tIB?+Q2QJhJvV=Q!MF#DB5dojRi6VeG;ma2bY(wusN#rSL;UWw+O9PO)e*p<33Qa zi~n6c;Ek!O=vnXybA)xoqn&Mi9C@-D;=8|F_$7K%qHei14eScN8=JH|ojrpDi752t z0tvs4ctpzU2Iz5o?AH}AsyFm_(Z1vl_g}h*BG{#dO@fp8FBLE<70e^%2p1V!8N$`! zNAac(IS!G%SGDN3Xm(Uwn%LCSR|RnFeQsp%k}0O4ujK+DGeNupoV~nfOPnxHdf6LH z34QR{94PneaJkux><>3>_|6$CM0kz(#emGu2S0`5BQ2;_DQ>wKA8AvhP&mYU%?K70 zHM8KGPe!y`X;O`5k1^?~$l+XKa(KgA60vyvoUBw4lQ1BR0Yh}H`;Dq*L2}b<(|gl& zlVlQBtvFEKFdQ5CiHL62Itk zBGiy*aToNBD zR6_tI=F znJm8lm;>tWIc@3dz?o8Xh>s(l@m{M&nBc+?q8hJ7g|A-((b@es*e}q zC)+F_QW@tzGd&35Pa#MIhS|6yln}O1;kT$ou1)8L@&KUxJpGyS*~=T=oBBtuoy6(V z*|d44>_hlhd8ZBO7dq)5Z7m1}4;bY&^k(5=_?=pE%pZ}Kf>Z9}%<-Z_Z+u+CghIvN z=kcsCx(aqROxcM^x=t)lOm6=iXVhhs%zzFEBp*hJ zJc0Vi^=i_dE1z$igNl~CFI6wkFDdW(WkO&}K@E{V@~*P4@)Fy#&JGp4(okh62O+@h zxed9=8lZ34{JU>~wF{vfDPbaD^a3X#!Q1Q$?$3StyJXVKV^iduEcwO)egN?i;qfNv zUY&D7SG%|pw2b9r+&T2M45hPTNJ`yf$w0hoku&zK6WX^r3Faqe=RWB z5xT>c`ZeZK?DFt3D(EhU6Y}*yNFqE5Lx@sPf93G16syof=~;OeuV_%}ntoY`lC?3` z#WvHqjbHX^jfcmC*qRz0jjqhWSTAcYt4IAihXj|L0i<6T(jwDDEo?2}x+(4U_8FS@ zQ%!k)H}#p}?r&Oxo1Nd)hG$%R1eg%^K5|qrf*+abfODP_Nr&Wd3pSr2VcO=~BXH%G zrxz3~Fk3`|*FsL;d2LG5}QS(SdIbI|;Ech62*BDIvwM{$T`3P$d=g-8)QluAi zp@~8m2J5On`_>{H$yX}q26!q%0>`-Q&&`M-{MP7AL8kXF8C2d_#%5>wO{lp>{dg}O z-mDNj8C@Anqr!97HjS$GoYk?qb=ipZu>0g8y75}&R^?Xo_Q5UDZO|4tQT)VuxO1tb%c*fl&ag}M5+0MrNF^29sZeS%c7`WS^%j$?f zf=7nOl)Ilm|G|@;xWY?j!9NwJuDY-h~Bi-~NtIR%3fxW-OS5~bc&hF8Y z)vNTjByED<>G6el?h0T^x|CqPKo*onO62>Nw~4=N?k^?$j{i-y9LfdPiYgnJbDy)ABX`RFg`=bY0L2Kr z9eNbGxQhU{X%TXfp$hm4_$Jf0sAMcv)oQbzei_iQWRvBVWvj)WZc#VosRjN%3*FUs zDn0vPAV8J|{L(!ZJf{51x``~%AT6qiEhU{}H2v4d6H#y%W9E+t=@S@BbA z$pb_>L%4nxcKtF(eJV>P6p(C!BhyUNvBiWAMeE07_hszkL}5qa++}IP!?y3CaP(N| z1X<~!=`e16U928dd@`j9|dlQV`Dq9^rWG%-+3W8j{DP^@%o@!KBbxvO7amx4Kg#pUaBe7}!f#3kAH_nsoZ8hxy0 zdu>r&r@tg^6Kz?Q__Ce^UjQQE7C)@Sc(^3_e2zFCf5a)hh3mV4TbH@dMgXX0Clx+aH#t`CCdLK;;5YyP+rPlv?0l_lt$nTeoqasqll06}z6+9H^Hj^2 zFN*){sOCOipbA(|#a8jWNv)z1zDVnMdNAsbAYbnfSPFm2Qh90rd!okSuE<-Wqaa8@ zPyCP{4bY5IxQ{_&Kps!U9A6m|{0`t(gojzg+xt7JQN!b_7VC2vn_EC7->-~(44 zMC1t~T?^FH87;^f1NeHDT%K!sUj3qwF#eJCeDFd14494>K@}1`XtJnzkwek&fa(#8 z{Xoh&Bj%>%K*tpWLjktQs;Mm(YomteO(GCehGA$WrvA(`6&-<0?jFRO* zHBpSc>cG+t1~KZDDmdlTWnPHEFE4@eUr@qMK|7A|ua{#Ptry1wCSR&QJSR*jAp0sP zL*dB@2{PM^a+rlrLz|BBiK>X^$-bP88?adCx!|60R0@yd3RcQvUp|r6>M>yOM|C z2Y29|3*p__zqyjHho_~hoxhzczi*(g)-+$`ufH8q#W1VY?vC&u1KrvG@@^vyRn=$r zrZt{V&XQ>HaZgQ5H|q&5h#)q-btbj=p7M?fH0fzP9pmMHBLL*mn-QE55uO#9xoeL- zJ}oeN&I>2u+182@E#&s7Z+RTK>rGxY}Ce+UxyG9cxV9oF%^Z45*^EvyW4R*g0bF5Ns2 zMcM#b!jGPq-$D%D*?UD;gM8Uk`_$#q@bFv(5eM(`jo<_wQ*z~+Z*p;4f+WkvxV$sX zSIRYIos*7Oiz;9KK{r(@mm6?UKNQ7MaH-)WdSe- zx||d;@{XkrF;{v0-~kWzzY?fEzv2oQld{>Pe=dEZ^k7$F{$jk}>GhDFAm_YNC^EwL z8LzJUp8W9iJ@Thg`-l80+nPDUa#vxc+*voV`9^b63sUTbfBHIj4+Aq@%`tbSu8C0A zK<49pQXI^9KCqJbUBV-Qc|sy8O+B4~dcDI`^|mG^(xZ{X{%QI=XygyW zf4jD(3;<(&H}N!gN=g4O6R)SLtfQymH`DDMsQ`%iDJKG^dc;4tN5e%>!x~`SnLVs% z{9=fYH~!>I(}r6gn&UBbt-831z9{rL$RMH&N%0k#1?nw6??4+%OGY#Y+UdO(bZgUy zS(%Il&783oD7mvKu)eJq_*^f9BV&Am&N*-P33XjHVC1|Ve#o%&QyMNGXv8*zP~b_3 zJ1GMunC&IM&v&~rCg1yzT+kX8NsBjE@)LIb#LebGweUL@_NfvfZ-g^>GdrxpOazA~ z{eU1z4@AErYVi||WTGd&&zN?Lel;sMIWM;piQ+`*GF_(8S*{|bnf-&O8?S> zs=d_Wc561=?q3|(|3ap#g@sKG_$L}B0EnJ&THnpfCGK5({O4$k_|ND6s0Y6%*!J(c z{*Iyl|Gu^e?K{^0(wx5{>fgY>{Qq1FygP627Nn`; I-OL025A=fr8UO$Q diff --git a/daemon/miniupnpc-1.9.tar.gz b/daemon/miniupnpc-1.9.tar.gz deleted file mode 100644 index 85deda499f30985a21ac63e197fa9c5caee821a4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 71648 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 WmY?Nk`B{DvKmQ-4%LQ5hcm)7Bewf+- diff --git a/lbry b/lbry index 52a42ce2f..2303f88bd 160000 --- a/lbry +++ b/lbry @@ -1 +1 @@ -Subproject commit 52a42ce2fcdd1ec6ddca84c4fa754c3d318d2258 +Subproject commit 2303f88bd4482e1f0b2f2357516d2c547ccd64ee diff --git a/lbryum b/lbryum index 39ace3737..71cf689c4 160000 --- a/lbryum +++ b/lbryum @@ -1 +1 @@ -Subproject commit 39ace3737509ff2b09fabaaa64d1525843de1325 +Subproject commit 71cf689c450bbe696fb1fd51d562b195b2f6854a -- 2.45.2 From bfbc443e5411df9fb2130ab307c60e0441a0b544 Mon Sep 17 00:00:00 2001 From: Alex Grintsvayg Date: Wed, 19 Apr 2017 15:09:44 -0400 Subject: [PATCH 110/158] delete submodules --- lbry | 1 - lbryum | 1 - 2 files changed, 2 deletions(-) delete mode 160000 lbry delete mode 160000 lbryum diff --git a/lbry b/lbry deleted file mode 160000 index 2303f88bd..000000000 --- a/lbry +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 2303f88bd4482e1f0b2f2357516d2c547ccd64ee diff --git a/lbryum b/lbryum deleted file mode 160000 index 71cf689c4..000000000 --- a/lbryum +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 71cf689c450bbe696fb1fd51d562b195b2f6854a -- 2.45.2 From 1853c688da0a4802ca144480b02b066b270505eb Mon Sep 17 00:00:00 2001 From: Alex Grintsvayg Date: Wed, 19 Apr 2017 15:13:14 -0400 Subject: [PATCH 111/158] update to a more recent daemon --- build/DAEMON_URL | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/DAEMON_URL b/build/DAEMON_URL index ea9f7e7d9..b79fb8bde 100644 --- a/build/DAEMON_URL +++ b/build/DAEMON_URL @@ -1 +1 @@ -https://github.com/lbryio/lbry/releases/download/v0.9.2rc3/lbrynet-daemon-v0.9.2rc3-OSNAME.zip +https://github.com/lbryio/lbry/releases/download/v0.10.0rc5/lbrynet-daemon-v0.10.0rc5-OSNAME.zip -- 2.45.2 From 2e9e0f129b560242957020a2e351d8abeb49626a Mon Sep 17 00:00:00 2001 From: Alex Grintsvayg Date: Wed, 19 Apr 2017 15:16:29 -0400 Subject: [PATCH 112/158] run daemon install even in local build --- build/build.sh | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/build/build.sh b/build/build.sh index 60eb60759..1b3340054 100755 --- a/build/build.sh +++ b/build/build.sh @@ -72,17 +72,15 @@ npm install # daemon and cli # #################### -if [ "$FULL_BUILD" == "true" ]; then - if $OSX; then - OSNAME="macos" - else - OSNAME="linux" - fi - DAEMON_URL="$(cat "$BUILD_DIR/DAEMON_URL" | sed "s/OSNAME/${OSNAME}/")" - wget --quiet "$DAEMON_URL" -O "$BUILD_DIR/daemon.zip" - unzip "$BUILD_DIR/daemon.zip" -d "$ROOT/app/dist/" - rm "$BUILD_DIR/daemon.zip" +if $OSX; then + OSNAME="macos" +else + OSNAME="linux" fi +DAEMON_URL="$(cat "$BUILD_DIR/DAEMON_URL" | sed "s/OSNAME/${OSNAME}/")" +wget --quiet "$DAEMON_URL" -O "$BUILD_DIR/daemon.zip" +unzip "$BUILD_DIR/daemon.zip" -d "$ROOT/app/dist/" +rm "$BUILD_DIR/daemon.zip" ################### # Build the app # @@ -116,4 +114,4 @@ if [ "$FULL_BUILD" == "true" ]; then echo 'Build and packaging complete.' else echo 'Build complete. Run `./node_modules/.bin/electron app` to launch the app' -fi \ No newline at end of file +fi -- 2.45.2 From 38fdf56583049a931880c1a592b19ca3f1813592 Mon Sep 17 00:00:00 2001 From: Alex Grintsvayg Date: Wed, 12 Apr 2017 18:46:32 -0400 Subject: [PATCH 113/158] rewrite release.py script --- build/changelog.py | 173 +++++++++++----------- build/release.py | 348 +++++++++++++++++---------------------------- 2 files changed, 208 insertions(+), 313 deletions(-) diff --git a/build/changelog.py b/build/changelog.py index 724324251..eb03b682f 100644 --- a/build/changelog.py +++ b/build/changelog.py @@ -1,8 +1,5 @@ -import argparse import datetime import re -import sys - CHANGELOG_START_RE = re.compile(r'^\#\# \[Unreleased\]') CHANGELOG_END_RE = re.compile(r'^\#\# \[.*\] - \d{4}-\d{2}-\d{2}') @@ -14,118 +11,110 @@ EMPTY_RE = re.compile(r'^\w*\*\w*$') ENTRY_RE = re.compile(r'\* (.*)') VALID_SECTIONS = ['Added', 'Changed', 'Deprecated', 'Removed', 'Fixed', 'Security'] - # allocate some entries to cut-down on merge conflicts TEMPLATE = """### Added * * - * ### Changed * * - * ### Fixed * * - * """ -def main(): - print "i am broken" - return 1 - parser = argparse.ArgumentParser() - parser.add_argument('changelog') - parser.add_argument('version') - args = parser.parse_args() - bump(changelog, version) +class Changelog(object): + def __init__(self, path): + self.path = path + self.start = [] + self.unreleased = [] + self.rest = [] + self._parse() + def _parse(self): + with open(self.path) as fp: + lines = fp.readlines() -def bump(changelog, version): - with open(changelog) as fp: - lines = fp.readlines() + unreleased_start_found = False + unreleased_end_found = False - start = [] - unreleased = [] - rest = [] - unreleased_start_found = False - unreleased_end_found = False - for line in lines: - if not unreleased_start_found: - start.append(line) - if CHANGELOG_START_RE.search(line): - unreleased_start_found = True - continue - if unreleased_end_found: - rest.append(line) - continue - if CHANGELOG_END_RE.search(line): - rest.append(line) - unreleased_end_found = True - continue - if CHANGELOG_ERROR_RE.search(line): - raise Exception( - 'Failed to parse {}: {}'.format(changelog, 'unexpected section header found')) - unreleased.append(line) + for line in lines: + if not unreleased_start_found: + self.start.append(line) + if CHANGELOG_START_RE.search(line): + unreleased_start_found = True + continue + if unreleased_end_found: + self.rest.append(line) + continue + if CHANGELOG_END_RE.search(line): + self.rest.append(line) + unreleased_end_found = True + continue + if CHANGELOG_ERROR_RE.search(line): + raise Exception( + 'Failed to parse {}: {}'.format(self.path, 'unexpected section header found')) + self.unreleased.append(line) - today = datetime.datetime.today() - header = '## [{}] - {}\n'.format(version, today.strftime('%Y-%m-%d')) - released = normalize(unreleased) - if not released: - # If we don't have anything in the Unreleased section, then leave the - # changelog as it is and return None - return + self.unreleased = self._normalize_section(self.unreleased) - changelog_data = ( - ''.join(start) + - TEMPLATE + - header + - '\n'.join(released) + '\n\n' - + ''.join(rest) - ) - with open(changelog, 'w') as fp: - fp.write(changelog_data) - return '\n'.join(released) + '\n\n' + @staticmethod + def _normalize_section(lines): + """Parse a changelog entry and output a normalized form""" + sections = {} + current_section_name = None + current_section_contents = [] + for line in lines: + line = line.strip() + if not line or EMPTY_RE.match(line): + continue + match = SECTION_RE.match(line) + if match: + if current_section_contents: + sections[current_section_name] = current_section_contents + current_section_contents = [] + current_section_name = match.group(1) + if current_section_name not in VALID_SECTIONS: + raise ValueError("Section '{}' is not valid".format(current_section_name)) + continue + match = ENTRY_RE.match(line) + if match: + current_section_contents.append(match.group(1)) + continue + raise Exception('Something is wrong with line: {}'.format(line)) + if current_section_contents: + sections[current_section_name] = current_section_contents + output = [] + for section in VALID_SECTIONS: + if section not in sections: + continue + output.append('### {}'.format(section)) + for entry in sections[section]: + output.append(' * {}'.format(entry)) + return output -def normalize(lines): - """Parse a changelog entry and output a normalized form""" - sections = {} - current_section_name = None - current_section_contents = [] - for line in lines: - line = line.strip() - if not line or EMPTY_RE.match(line): - continue - match = SECTION_RE.match(line) - if match: - if current_section_contents: - sections[current_section_name] = current_section_contents - current_section_contents = [] - current_section_name = match.group(1) - if current_section_name not in VALID_SECTIONS: - raise ValueError("Section '{}' is not valid".format(current_section_name)) - continue - match = ENTRY_RE.match(line) - if match: - current_section_contents.append(match.group(1)) - continue - raise Exception('Something is wrong with line: {}'.format(line)) - if current_section_contents: - sections[current_section_name] = current_section_contents + def get_unreleased(self): + return '\n'.join(self.unreleased) if self.unreleased else None - output = [] - for section in VALID_SECTIONS: - if section not in sections: - continue - output.append('### {}'.format(section)) - for entry in sections[section]: - output.append(' * {}'.format(entry)) - return output + def bump(self, version): + if not self.unreleased: + return + today = datetime.datetime.today() + header = '## [{}] - {}\n'.format(version, today.strftime('%Y-%m-%d')) -if __name__ == '__main__': - sys.exit(main()) + changelog_data = ( + ''.join(self.start) + + TEMPLATE + + header + + '\n'.join(self.unreleased) + '\n\n' + + ''.join(self.rest) + ) + + with open(self.path, 'w') as fp: + fp.write(changelog_data) diff --git a/build/release.py b/build/release.py index 4100cf8cf..619ad7df9 100644 --- a/build/release.py +++ b/build/release.py @@ -1,13 +1,11 @@ -"""Trigger a release. +"""Bump version and create Github release -This script is to be run locally (not on a build server). +This script should be run locally, not on a build server. """ import argparse import contextlib -import logging import os import re -import string import subprocess import sys @@ -16,122 +14,131 @@ import github import changelog -# TODO: ask bumpversion for these -LBRY_PARTS = ('major', 'minor', 'patch', 'release', 'candidate') -LBRYUM_PARTS = ('major', 'minor', 'patch') +ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) def main(): - parser = argparse.ArgumentParser() - parser.add_argument( - "lbry_part", help="part of lbry version to bump", - choices=LBRY_PARTS - ) - parser.add_argument( - "--skip-lbryum", help="skip bumping lbryum, even if there are changes", - action="store_true", - ) - parser.add_argument( - "--lbryum-part", help="part of lbryum version to bump", - choices=LBRYUM_PARTS - ) - parser.add_argument( - "--last-release", - help=("manually set the last release version. The default is to query and parse the" - " value from the release page.") - ) - parser.add_argument( - "--skip-sanity-checks", action="store_true") - parser.add_argument( - "--require-changelog", action="store_true", - help=("Set this flag to raise an exception if a submodules has changes without a" - " corresponding changelog entry. The default is to log a warning") - ) - parser.add_argument( - "--skip-push", action="store_true", - help="Set to not push changes to remote repo" - ) + bumpversion_parts = get_bumpversion_parts() + parser = argparse.ArgumentParser() + parser.add_argument("part", choices=bumpversion_parts, help="part of version to bump") + parser.add_argument("--skip-sanity-checks", action="store_true") + parser.add_argument("--skip-push", action="store_true") + parser.add_argument("--dry-run", action="store_true") + parser.add_argument("--confirm", action="store_true") args = parser.parse_args() - base = git.Repo(os.getcwd()) + if args.dry_run: + print "DRY RUN. Nothing will be committed/pushed." + + repo = Repo('lbry-app', args.part, ROOT) branch = 'master' + print 'Current version: {}'.format(repo.current_version) + print 'New version: {}'.format(repo.new_version) + + if not args.confirm and not confirm(): + print "Aborting" + return 1 + if not args.skip_sanity_checks: - run_sanity_checks(base, branch) + run_sanity_checks(repo, branch) + repo.assert_new_tag_is_absent() - base_repo = Repo('lbry-app', args.lbry_part, os.getcwd()) - base_repo.assert_new_tag_is_absent() + is_rc = re.search('\drc\d+$', repo.new_version) is not None + # only have a release message for real releases, not for RCs + release_msg = '' if is_rc else repo.get_unreleased_changelog() - last_release = args.last_release or base_repo.get_last_tag() - logging.info('Last release: %s', last_release) + if args.dry_run: + print "rc: " + ("yes" if is_rc else "no") + print "release message: \n" + (release_msg or " NO MESSAGE FOR RCs") + return gh_token = get_gh_token() auth = github.Github(gh_token) github_repo = auth.get_repo('lbryio/lbry-app') - names = ['lbryum', 'lbry'] - repos = {name: Repo(name, get_part(args, name)) for name in names} + if not is_rc: + repo.bump_changelog() + repo.bumpversion() - changelogs = {} + new_tag = repo.get_new_tag() + github_repo.create_git_release(new_tag, new_tag, release_msg, draft=True, prerelease=is_rc) - for repo in repos.values(): - logging.info('Processing repo: %s', repo.name) - repo.checkout(branch) - last_submodule_hash = base_repo.get_submodule_hash(last_release, repo.name) - if repo.has_changes_from_revision(last_submodule_hash): - if repo.name == 'lbryum': - if args.skip_lbryum: - continue - if not repo.part: - repo.part = get_lbryum_part() - entry = repo.get_changelog_entry() - if entry: - changelogs[repo.name] = entry.strip() - repo.add_changelog() - else: - msg = 'Changelog entry is missing for {}'.format(repo.name) - if args.require_changelog: - raise Exception(msg) - else: - logging.warning(msg) - else: - logging.warning('Submodule %s has no changes.', repo.name) - if repo.name == 'lbryum': - # The other repos have their version track each other so need to bump - # them even if there aren't any changes, but lbryum should only be - # bumped if it has changes - continue - # bumpversion will fail if there is already the tag we want in the repo - repo.assert_new_tag_is_absent() - repo.bumpversion() - - release_msg = get_release_msg(changelogs, names) - - for name in names: - base.git.add(name) - - base_repo.bumpversion() - current_tag = base.git.describe() - - is_rc = re.match('\drc\d+$', current_tag) is not None - - github_repo.create_git_release(current_tag, current_tag, release_msg, draft=True, - prerelease=is_rc) - no_change_msg = ('No change since the last release. This release is simply a placeholder' - ' so that LBRY and LBRY App track the same version') - lbrynet_daemon_release_msg = changelogs.get('lbry', no_change_msg) - auth.get_repo('lbryio/lbry').create_git_release( - current_tag, current_tag, lbrynet_daemon_release_msg, draft=True) - - if not args.skip_push: - for repo in repos.values(): - repo.git.push(follow_tags=True) - base.git.push(follow_tags=True, recurse_submodules='check') + if args.skip_push: + print ( + 'Skipping push; you will have to reset and delete tags if ' + 'you want to run this script again.' + ) else: - logging.info('Skipping push; you will have to reset and delete tags if ' - 'you want to run this script again. Take a look at reset.sh; ' - 'it probably does what you want.') + repo.git_repo.git.push(follow_tags=True, recurse_submodules='check') + + +class Repo(object): + def __init__(self, name, part, directory): + self.name = name + self.part = part + if not self.part: + raise Exception('Part required') + self.directory = directory + self.git_repo = git.Repo(self.directory) + self._bumped = False + + self.current_version = self._get_current_version() + self.new_version = self._get_new_version() + self._changelog = changelog.Changelog(os.path.join(self.directory, 'CHANGELOG.md')) + + def get_new_tag(self): + return 'v' + self.new_version + + def get_unreleased_changelog(self): + return self._changelog.get_unreleased() + + def bump_changelog(self): + self._changelog.bump(self.new_version) + with pushd(self.directory): + self.git_repo.git.add(os.path.basename(self._changelog.path)) + + def _get_current_version(self): + with pushd(self.directory): + output = subprocess.check_output( + ['bumpversion', '--dry-run', '--list', '--allow-dirty', self.part]) + return re.search('^current_version=(.*)$', output, re.M).group(1) + + def _get_new_version(self): + with pushd(self.directory): + output = subprocess.check_output( + ['bumpversion', '--dry-run', '--list', '--allow-dirty', self.part]) + return re.search('^new_version=(.*)$', output, re.M).group(1) + + def bumpversion(self): + if self._bumped: + raise Exception('Cowardly refusing to bump a repo twice') + with pushd(self.directory): + subprocess.check_call(['bumpversion', '--allow-dirty', self.part]) + self._bumped = True + + def assert_new_tag_is_absent(self): + new_tag = self.get_new_tag() + tags = self.git_repo.git.tag() + if new_tag in tags.split('\n'): + raise Exception('Tag {} is already present in repo {}.'.format(new_tag, self.name)) + + def is_behind(self, branch): + self.git_repo.remotes.origin.fetch() + rev_list = '{branch}...origin/{branch}'.format(branch=branch) + commits_behind = self.git_repo.git.rev_list(rev_list, right_only=True, count=True) + commits_behind = int(commits_behind) + return commits_behind > 0 + + +def get_bumpversion_parts(): + with pushd(ROOT): + output = subprocess.check_output([ + 'bumpversion', '--dry-run', '--list', '--allow-dirty', 'fake-part', + ]) + parse_line = re.search('^parse=(.*)$', output, re.M).group(1) + return tuple(re.findall('<([^>]+)>', parse_line)) def get_gh_token(): @@ -148,131 +155,36 @@ in the future""" return raw_input('token: ').strip() -def get_lbryum_part(): - print """The lbryum repo has changes but you didn't specify how to bump the -version. Please enter one of: {}""".format(', '.join(LBRYUM_PARTS)) - while True: - part = raw_input('part: ').strip() - if part in LBRYUM_PARTS: - return part - print 'Invalid part. Enter one of: {}'.format(', '.join(LBRYUM_PARTS)) +def confirm(): + return raw_input('Is this what you want? [y/N] ').strip().lower() == 'y' -def get_release_msg(changelogs, names): - lines = [] - for name in names: - entry = changelogs.get(name) - if not entry: - continue - lines.append('## {}\n'.format(name)) - lines.append('{}\n'.format(entry)) - return '\n'.join(lines) - - -def run_sanity_checks(base, branch): - if base.is_dirty(): +def run_sanity_checks(repo, branch): + if repo.git_repo.is_dirty(): print 'Cowardly refusing to release a dirty repo' sys.exit(1) - if base.active_branch.name != branch: + if repo.git_repo.active_branch.name != branch: print 'Cowardly refusing to release when not on the {} branch'.format(branch) sys.exit(1) - if is_behind(base, branch): + if repo.is_behind(branch): print 'Cowardly refusing to release when behind origin' sys.exit(1) - check_bumpversion() - - -def is_behind(base, branch): - base.remotes.origin.fetch() - rev_list = '{branch}...origin/{branch}'.format(branch=branch) - commits_behind = base.git.rev_list(rev_list, right_only=True, count=True) - commits_behind = int(commits_behind) - return commits_behind > 0 - - -def check_bumpversion(): - def require_new_version(): - print 'Install bumpversion: pip install -U git+https://github.com/lbryio/bumpversion.git' + if not is_custom_bumpversion_version(): + print ( + 'Install LBRY\'s fork of bumpversion: ' + 'pip install -U git+https://github.com/lbryio/bumpversion.git' + ) sys.exit(1) + +def is_custom_bumpversion_version(): try: - output = subprocess.check_output(['bumpversion', '-v'], stderr=subprocess.STDOUT) - output = output.strip() - if output != 'bumpversion 0.5.4-lbry': - require_new_version() - except (subprocess.CalledProcessError, OSError) as err: - require_new_version() - - -def get_part(args, name): - return getattr(args, name + '_part') or args.lbry_part - - -class Repo(object): - def __init__(self, name, part, directory=None): - self.name = name - self.part = part - self.directory = directory or os.path.join(os.getcwd(), name) - self.git_repo = git.Repo(self.directory) - self.saved_commit = None - self._bumped = False - - def get_last_tag(self): - return string.split(self.git_repo.git.describe(tags=True), '-')[0] - - def get_submodule_hash(self, revision, submodule_path): - line = getattr(self.git_repo.git, 'ls-tree')(revision, submodule_path) - return string.split(line)[2] if line else None - - def has_changes_from_revision(self, revision): - commit = str(self.git_repo.commit()) - logging.info('%s =? %s', commit, revision) - return commit != revision - - def save_commit(self): - self.saved_commit = self.git_repo.commit() - logging.info('Saved ', self.git_repo.commit(), self.saved_commit) - - def checkout(self, branch): - self.git_repo.git.checkout(branch) - self.git_repo.git.pull(rebase=True) - - def get_changelog_entry(self): - filename = os.path.join(self.directory, 'CHANGELOG.md') - return changelog.bump(filename, self.new_version()) - - def add_changelog(self): - with pushd(self.directory): - self.git_repo.git.add('CHANGELOG.md') - - def new_version(self): - if self._bumped: - raise Exception('Cannot calculate a new version on an already bumped repo') - if not self.part: - raise Exception('Cannot calculate a new version without a part') - with pushd(self.directory): - output = subprocess.check_output( - ['bumpversion', '--dry-run', '--list', '--allow-dirty', self.part]) - return re.search('^new_version=(.*)$', output, re.M).group(1) - - def bumpversion(self): - if self._bumped: - raise Exception('Cowardly refusing to bump a repo twice') - if not self.part: - raise Exception('Cannot bump version for {}: no part specified'.format(repo.name)) - with pushd(self.directory): - subprocess.check_call(['bumpversion', '--allow-dirty', self.part]) - self._bumped = True - - def assert_new_tag_is_absent(self): - new_tag = 'v' + self.new_version() - tags = self.git_repo.git.tag() - if new_tag in tags.split('\n'): - raise Exception('Tag {} is already present in repo {}.'.format(new_tag, self.name)) - - @property - def git(self): - return self.git_repo.git + output = subprocess.check_output(['bumpversion', '-v'], stderr=subprocess.STDOUT).strip() + if output == 'bumpversion 0.5.4-lbry': + return True + except (subprocess.CalledProcessError, OSError): + pass + return False @contextlib.contextmanager @@ -284,10 +196,4 @@ def pushd(new_dir): if __name__ == '__main__': - logging.basicConfig( - format="%(asctime)s %(levelname)-8s %(name)s:%(lineno)d: %(message)s", - level='INFO' - ) sys.exit(main()) -else: - log = logging.getLogger('__name__') -- 2.45.2 From e73f36704532180ab1c488fb512acc3eb4854a65 Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Wed, 19 Apr 2017 15:54:57 -0400 Subject: [PATCH 114/158] always show title on mouseover --- ui/js/component/file-tile.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/js/component/file-tile.js b/ui/js/component/file-tile.js index 17ef2bae1..51413bd02 100644 --- a/ui/js/component/file-tile.js +++ b/ui/js/component/file-tile.js @@ -90,7 +90,7 @@ export let FileTileStream = React.createClass({ : null}

- + {title} @@ -195,7 +195,7 @@ export let FileCardStream = React.createClass({
-
{title}
+
{title}
{ !this.props.hidePrice ? : null} Date: Thu, 20 Apr 2017 10:45:39 -0400 Subject: [PATCH 115/158] allow anyone with lbc into early access --- app/main.js | 1 - doitagain.sh | 4 - ui/js/component/auth.js | 136 ++++++++++++++++++++--------- ui/js/component/common.js | 4 +- ui/js/lbryio.js | 4 +- ui/scss/_gui.scss | 18 +--- ui/scss/component/_form-field.scss | 23 ++++- ui/scss/component/_modal-page.scss | 3 + ui/webpack.config.js | 2 +- ui/webpack.dev.config.js | 2 +- 10 files changed, 127 insertions(+), 70 deletions(-) delete mode 100755 doitagain.sh diff --git a/app/main.js b/app/main.js index 8a66a4403..07aed14d0 100644 --- a/app/main.js +++ b/app/main.js @@ -107,7 +107,6 @@ function launchDaemon() { daemonSubprocess.stdout.on('data', (buf) => {console.log(String(buf).trim());}); daemonSubprocess.stderr.on('data', (buf) => {console.log(String(buf).trim());}); daemonSubprocess.on('exit', handleDaemonSubprocessExited); - console.log('lbrynet daemon has launched') } /* diff --git a/doitagain.sh b/doitagain.sh deleted file mode 100755 index 37564e1dd..000000000 --- a/doitagain.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -rm -rf ~/.lbrynet/ -rm -rf ~/.lbryum/ -./node_modules/.bin/electron app diff --git a/ui/js/component/auth.js b/ui/js/component/auth.js index a36c6d48d..f0e1f1074 100644 --- a/ui/js/component/auth.js +++ b/ui/js/component/auth.js @@ -1,12 +1,11 @@ -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'; +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 {FormRow} from "../component/form.js"; +import {CreditAmount, Address} from "../component/common.js"; +import {getLocal, getSession, setSession, setLocal} from '../utils.js'; const SubmitEmailStage = React.createClass({ @@ -22,6 +21,9 @@ const SubmitEmailStage = React.createClass({ email: event.target.value, }); }, + onEmailSaved: function(email) { + this.props.setStage("confirm", { email: email }) + }, handleSubmit: function(event) { event.preventDefault(); @@ -29,10 +31,10 @@ const SubmitEmailStage = React.createClass({ submitting: true, }); lbryio.call('user_email', 'new', {email: this.state.email}, 'post').then(() => { - this.props.onEmailSaved(this.state.email); + this.onEmailSaved(this.state.email); }, (error) => { if (error.xhr && error.xhr.status == 409) { - this.props.onEmailSaved(this.state.email); + this.onEmailSaved(this.state.email); return; } else if (this._emailRow) { this._emailRow.showError(error.message) @@ -85,7 +87,7 @@ const ConfirmEmailStage = React.createClass({ lbryio.call('user_email', 'confirm', {verification_token: this.state.code, email: this.props.email}, 'post').then((userEmail) => { if (userEmail.IsVerified) { - this.props.onEmailConfirmed(); + this.props.setStage("welcome") } else { onSubmitError(new Error("Your email is still not verified.")) //shouldn't happen? } @@ -98,9 +100,12 @@ const ConfirmEmailStage = React.createClass({ { 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."/> -
+
+
+ No code? { this.props.setStage("nocode")}} label="Click here" />. +

); @@ -176,10 +181,66 @@ const PendingStage = React.createClass({ } }); + +const CodeRequiredStage = React.createClass({ + _balanceSubscribeId: null, + getInitialState: function() { + return { + balance: 0, + address: getLocal('wallet_address') + } + }, + + componentWillMount: function() { + this._balanceSubscribeId = lbry.balanceSubscribe((balance) => { + this.setState({ + balance: balance + }); + }) + + if (!this.state.address) { + lbry.call('wallet_new_address', {}, (address) => { + setLocal('wallet_address', address); + this.setState({ address: address }); + }); + } + }, + componentWillUnmount: function() { + if (this._balanceSubscribeId) { + lbry.balanceUnsubscribe(this._balanceSubscribeId) + } + }, + render: function() { + const disabled = this.state.balance < 1; + return ( +
+
+

Access to LBRY is restricted as we build and scale the network.

+

There are two ways in:

+

Own LBRY Credits

+

If you own at least 1 LBC, you can get in right now.

+

{ setLocal('auth_bypassed', true); this.props.setStage(null); }} + disabled={disabled} label="Let Me In" button={ disabled ? "alt" : "primary" } />

+

Your balance is . To increase your balance, send credits to this address:

+

+

If you don't understand how to send credits, then...

+
+
+

Wait For A Code

+

If you provide your email, you'll automatically receive a notification when the system is open.

+

{ this.props.setStage("email"); }} label="Return" />

+
+
+ ); + } +}); + + export const AuthOverlay = React.createClass({ _stages: { pending: PendingStage, error: ErrorStage, + nocode: CodeRequiredStage, email: SubmitEmailStage, confirm: ConfirmEmailStage, welcome: WelcomeStage @@ -190,42 +251,31 @@ export const AuthOverlay = React.createClass({ stageProps: {} }; }, - endAuth: function() { + setStage: function(stage, stageProps = {}) { this.setState({ - stage: null - }); + stage: stage, + stageProps: stageProps + }) }, componentWillMount: function() { - lbryio.authenticate().then(function(user) { - if (!user.HasVerifiedEmail) { //oops I fucked this up - this.setState({ - stage: "email", - stageProps: { - onEmailSaved: function(email) { - this.setState({ - stage: "confirm", - stageProps: { - email: email, - onEmailConfirmed: function() { this.setState({ stage: "welcome"}) }.bind(this) - } - }) - }.bind(this) - } - }) + lbryio.authenticate().then((user) => { + if (!user.HasVerifiedEmail) { + if (getLocal('auth_bypassed')) { + this.setStage(null) + } else { + this.setStage("email", {}) + } } else { - lbryio.call('reward', 'list', {}).then(function(userRewards) { + lbryio.call('reward', 'list', {}).then((userRewards) => { userRewards.filter(function(reward) { return reward.RewardType == "new_user" && reward.TransactionID; }).length ? - this.endAuth() : - this.setState({ stage: "welcome" }) - }.bind(this)); + this.setStage(null) : + this.setStage("welcome") + }); } - }.bind(this)).catch((err) => { - this.setState({ - stage: "error", - stageProps: { errorText: err.message } - }) + }).catch((err) => { + this.setStage("error", { errorText: err.message }) document.dispatchEvent(new CustomEvent('unhandledError', { detail: { message: err.message, @@ -243,9 +293,9 @@ export const AuthOverlay = React.createClass({ this.state.stage != "welcome" ?

LBRY Early Access

- +
: - + ); } }); \ No newline at end of file diff --git a/ui/js/component/common.js b/ui/js/component/common.js index 0c8d66ca0..7139e5dfe 100644 --- a/ui/js/component/common.js +++ b/ui/js/component/common.js @@ -149,12 +149,14 @@ var addressStyle = { fontFamily: '"Consolas", "Lucida Console", "Adobe Source Code Pro", monospace', }; export let Address = React.createClass({ + _inputElem: null, propTypes: { address: React.PropTypes.string, }, render: function() { return ( - {this.props.address} + { this._inputElem = input; }} + onFocus={() => { this._inputElem.select(); }} style={addressStyle} readOnly="readonly" value={this.props.address}> ); } }); diff --git a/ui/js/lbryio.js b/ui/js/lbryio.js index 504b6b3e4..bbe6b9ccd 100644 --- a/ui/js/lbryio.js +++ b/ui/js/lbryio.js @@ -7,10 +7,10 @@ const lbryio = { _accessToken: getLocal('accessToken'), _authenticationPromise: null, _user : null, - enabled: false + enabled: true }; -const CONNECTION_STRING = 'https://api.lbry.io/'; +const CONNECTION_STRING = process.env.LBRY_APP_API_URL ? process.env.LBRY_APP_API_URL : 'https://api.lbry.io/'; const EXCHANGE_RATE_TIMEOUT = 20 * 60 * 1000; lbryio.getExchangeRates = function() { diff --git a/ui/scss/_gui.scss b/ui/scss/_gui.scss index da32d3ac5..f875a6940 100644 --- a/ui/scss/_gui.scss +++ b/ui/scss/_gui.scss @@ -38,20 +38,6 @@ text-align: center; } -/* -section -{ - margin-bottom: $spacing-vertical; - &:last-child - { - margin-bottom: 0; - } - &:only-child { - margin-bottom: $spacing-vertical; - } -} -*/ - h2 { font-size: 1.75em; } @@ -147,3 +133,7 @@ p font-size: 0.85em; color: $color-help; } + +section.section-spaced { + margin-bottom: $spacing-vertical; +} diff --git a/ui/scss/component/_form-field.scss b/ui/scss/component/_form-field.scss index c9f00b141..fa2b36e27 100644 --- a/ui/scss/component/_form-field.scss +++ b/ui/scss/component/_form-field.scss @@ -1,11 +1,16 @@ @import "../global"; $width-input-border: 2px; +$width-input-text: 330px; .form-row-submit { margin-top: $spacing-vertical; } +.form-row-submit--with-footer +{ + margin-bottom: $spacing-vertical; +} .form-row__label-row { margin-top: $spacing-vertical * 5/6; @@ -18,6 +23,18 @@ $width-input-border: 2px; margin-right: 5px; } +input[type="text"].input-copyable { + border: 1px solid $color-form-border; + line-height: 1; + padding-top: $spacing-vertical * 1/3; + padding-bottom: $spacing-vertical * 1/3; + width: $width-input-text; + padding-left: 5px; + padding-right: 5px; + width: 100%; + &:focus { border-color: black; } +} + .form-field { display: inline-block; @@ -66,7 +83,7 @@ $width-input-border: 2px; input[type="search"], input[type="date"] { border-bottom: $width-input-border solid $color-form-border; - line-height: 1px; + line-height: 1; padding-top: $spacing-vertical * 1/3; padding-bottom: $spacing-vertical * 1/3; &.form-field__input--error { @@ -108,7 +125,7 @@ $width-input-border: 2px; } .form-field__input-text { - width: 330px; + width: $width-input-text; } .form-field__prefix { @@ -124,7 +141,7 @@ $width-input-border: 2px; } .form-field__input-textarea { - width: 330px; + width: $width-input-text; } .form-field__error, .form-field__helper { diff --git a/ui/scss/component/_modal-page.scss b/ui/scss/component/_modal-page.scss index d9bd0d8d5..ada366f61 100644 --- a/ui/scss/component/_modal-page.scss +++ b/ui/scss/component/_modal-page.scss @@ -17,6 +17,9 @@ right: 0; top: 0; bottom: 0; + .modal-page__content { + max-width: 500px; + } } /* diff --git a/ui/webpack.config.js b/ui/webpack.config.js index fdafd70b2..be349baa9 100644 --- a/ui/webpack.config.js +++ b/ui/webpack.config.js @@ -35,4 +35,4 @@ module.exports = { ] }, target: 'electron-main', -}; +}; \ No newline at end of file diff --git a/ui/webpack.dev.config.js b/ui/webpack.dev.config.js index b84f2f94a..358d6c04d 100644 --- a/ui/webpack.dev.config.js +++ b/ui/webpack.dev.config.js @@ -38,4 +38,4 @@ module.exports = { ] }, target: 'electron-main', -}; +}; \ No newline at end of file -- 2.45.2 From f2d6bc3dcff09b67d7a192d0cda8f1d2069afdba Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Thu, 20 Apr 2017 19:26:29 -0400 Subject: [PATCH 116/158] fix search result uri building and empty array reduce warning --- ui/js/lbry.js | 2 +- ui/js/lbryuri.js | 7 +++++-- ui/js/page/discover.js | 1 + 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/ui/js/lbry.js b/ui/js/lbry.js index adc8ba4b4..e265f43d7 100644 --- a/ui/js/lbry.js +++ b/ui/js/lbry.js @@ -517,7 +517,7 @@ lbry._updateClaimOwnershipCache = function(claimId) { lbry.getMyClaims((claimInfos) => { lbry._claimIdOwnershipCache[claimId] = !!claimInfos.reduce(function(match, claimInfo) { return match || claimInfo.claim_id == claimId; - }); + }, false); }); }; diff --git a/ui/js/lbryuri.js b/ui/js/lbryuri.js index 55a964e66..2712b812a 100644 --- a/ui/js/lbryuri.js +++ b/ui/js/lbryuri.js @@ -140,9 +140,12 @@ lbryuri.build = function(uriObj, includeProto=true, allowExtraProps=false) { } if (contentName) { - if (!path) { + if (!name) { + name = contentName; + } else if (!path) { path = contentName; - } else if (path !== contentName) { + } + if (path && path !== contentName) { throw new Error('path and contentName do not match. Only one is required; most likely you wanted contentName.'); } } diff --git a/ui/js/page/discover.js b/ui/js/page/discover.js index dc2811cfd..b6afa426f 100644 --- a/ui/js/page/discover.js +++ b/ui/js/page/discover.js @@ -46,6 +46,7 @@ var SearchResults = React.createClass({ render: function() { var rows = [], seenNames = {}; //fix this when the search API returns claim IDs + for (let {name, claim, claim_id, channel_name, channel_id, txid, nout} of this.props.results) { const uri = lbryuri.build({ channelName: channel_name, -- 2.45.2 From b2dd790d4c462631a31e5cd379757c4aeecefbed Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Thu, 20 Apr 2017 20:31:52 -0400 Subject: [PATCH 117/158] fix overly aggressive balance checking --- ui/js/lbry.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/ui/js/lbry.js b/ui/js/lbry.js index e265f43d7..2471b38b5 100644 --- a/ui/js/lbry.js +++ b/ui/js/lbry.js @@ -517,7 +517,7 @@ lbry._updateClaimOwnershipCache = function(claimId) { lbry.getMyClaims((claimInfos) => { lbry._claimIdOwnershipCache[claimId] = !!claimInfos.reduce(function(match, claimInfo) { return match || claimInfo.claim_id == claimId; - }, false); + }); }); }; @@ -570,6 +570,7 @@ lbry.fileInfoUnsubscribe = function(outpoint, subscribeId) { delete lbry._fileInfoSubscribeCallbacks[outpoint][subscribeId]; } +lbry._balanceUpdateInterval = null; lbry._updateBalanceSubscribers = function() { lbry.get_balance().then(function(balance) { for (let callback of Object.values(lbry._balanceSubscribeCallbacks)) { @@ -577,8 +578,8 @@ lbry._updateBalanceSubscribers = function() { } }); - if (Object.keys(lbry._balanceSubscribeCallbacks).length) { - setTimeout(() => { + if (!lbry._balanceUpdateInterval && Object.keys(lbry._balanceSubscribeCallbacks).length) { + lbry._balanceUpdateInterval = setInterval(() => { lbry._updateBalanceSubscribers(); }, lbry._balanceSubscribeInterval); } @@ -593,6 +594,9 @@ lbry.balanceSubscribe = function(callback) { lbry.balanceUnsubscribe = function(subscribeId) { delete lbry._balanceSubscribeCallbacks[subscribeId]; + if (lbry._balanceUpdateInterval && !Object.keys(lbry._balanceSubscribeCallbacks).length) { + clearInterval(lbry._balanceUpdateInterval) + } } lbry.showMenuIfNeeded = function() { -- 2.45.2 From 10580729f899174a93bb05ef579a6cfbc1622fb7 Mon Sep 17 00:00:00 2001 From: Alex Grintsvayg Date: Fri, 21 Apr 2017 13:09:02 -0400 Subject: [PATCH 118/158] stop the stupid version rewriting --- build/set_build.py | 41 ----------------------------------- build/set_version.py | 51 +++++++++----------------------------------- 2 files changed, 10 insertions(+), 82 deletions(-) delete mode 100644 build/set_build.py diff --git a/build/set_build.py b/build/set_build.py deleted file mode 100644 index 32e92d0e2..000000000 --- a/build/set_build.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Set the build version to be 'dev', 'qa', 'rc', 'release'""" - -from __future__ import print_function - -import os.path -import re -import subprocess -import sys -import fileinput - - -def main(): - build = get_build() - with open(os.path.join('lbry', 'lbrynet', 'build_type.py'), 'w') as f: - f.write('BUILD = "{}"'.format(build)) - set_early_access() - - -def set_early_access(): - filename = os.path.abspath(os.path.join(os.path.abspath(__file__), '..', '..', 'ui', 'js', 'lbryio.js')) - for line in fileinput.input(filename, inplace=True): - if line.startswith(' enabled: false'): - print(' enabled: true') - else: - print(line, end='') - - -def get_build(): - try: - tag = subprocess.check_output(['git', 'describe', '--exact-match']).strip() - if re.match('v\d+\.\d+\.\d+rc\d+', tag): - return 'rc' - else: - return 'release' - except subprocess.CalledProcessError: - # if the build doesn't have a tag - return 'qa' - - -if __name__ == '__main__': - sys.exit(main()) diff --git a/build/set_version.py b/build/set_version.py index 80b777d89..313cf6c93 100644 --- a/build/set_version.py +++ b/build/set_version.py @@ -1,51 +1,20 @@ """Set the package version to the output of `git describe`""" -import argparse -import json +from __future__ import print_function + import os.path -import re -import subprocess import sys +import fileinput def main(): - parser = argparse.ArgumentParser() - parser.add_argument('--version', help="defaults to the output of `git describe`") - args = parser.parse_args() - if args.version: - version = args.version - else: - tag = subprocess.check_output(['git', 'describe']).strip() - try: - version = get_version_from_tag(tag) - except InvalidVersionTag: - # this should be an error but its easier to handle here - # than in the calling scripts. - print 'Tag cannot be converted to a version, Exitting' - return - set_version(version) - - -class InvalidVersionTag(Exception): - pass - - -def get_version_from_tag(tag): - match = re.match('v([\d.]+)', tag) - if match: - return match.group(1) - else: - raise InvalidVersionTag('Failed to parse version from tag {}'.format(tag)) - - -def set_version(version): - root_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) - package_file = os.path.join(root_dir, 'app', 'package.json') - with open(package_file) as fp: - package_data = json.load(fp) - package_data['version'] = version - with open(package_file, 'w') as fp: - json.dump(package_data, fp, indent=2, separators=(',', ': ')) + filename = os.path.abspath( + os.path.join(os.path.abspath(__file__), '..', '..', 'ui', 'js', 'lbryio.js')) + for line in fileinput.input(filename, inplace=True): + if line.startswith(' enabled: false'): + print(' enabled: true') + else: + print(line, end='') if __name__ == '__main__': -- 2.45.2 From 1573662a06093b777b6dc61ce6c7a932feb5c3de Mon Sep 17 00:00:00 2001 From: Alex Grintsvayg Date: Fri, 21 Apr 2017 19:00:13 -0400 Subject: [PATCH 119/158] check that daemon_url works before releasing --- build/DAEMON_URL | 2 +- build/release.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/build/DAEMON_URL b/build/DAEMON_URL index b79fb8bde..ea9a259eb 100644 --- a/build/DAEMON_URL +++ b/build/DAEMON_URL @@ -1 +1 @@ -https://github.com/lbryio/lbry/releases/download/v0.10.0rc5/lbrynet-daemon-v0.10.0rc5-OSNAME.zip +https://github.com/lbryio/lbry/releases/download/v0.10.0rc13/lbrynet-daemon-v0.10.0rc13-OSNAME.zip diff --git a/build/release.py b/build/release.py index 619ad7df9..7ecfa32cd 100644 --- a/build/release.py +++ b/build/release.py @@ -6,6 +6,7 @@ import argparse import contextlib import os import re +import requests import subprocess import sys @@ -175,6 +176,40 @@ def run_sanity_checks(repo, branch): 'pip install -U git+https://github.com/lbryio/bumpversion.git' ) sys.exit(1) + if not check_daemon_urls(): + sys.exit(1) + + +def check_daemon_urls(): + daemon_url_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'DAEMON_URL') + success = True + with open(daemon_url_file, 'r') as f: + daemon_url_template = f.read().strip() + if "OSNAME" not in daemon_url_template: + print "Daemon URL must include the string 'OSNAME'" + return False + for osname in ('linux', 'macos', 'windows'): + if not check_url(daemon_url_template.replace('OSNAME', osname)): + success = False + print "Daemon URL for " + osname + " does not work" + return success + + +def check_url(url): + url = url.strip() + r = requests.head(url) + if r.status_code >= 400: + return False + elif r.status_code >= 300: + new_location = r.headers.get('Location').strip() + if new_location == url: + # self-loop + return False + if "github-cloud.s3.amazonaws.com/releases" in new_location: + # HEAD doesnt work on s3 links, so assume its good + return True + return check_url(new_location) + return True def is_custom_bumpversion_version(): -- 2.45.2 From 8b2a3882522e06a1f9a65f322fde47be4b389319 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Breda?= Date: Sat, 22 Apr 2017 15:21:04 +0100 Subject: [PATCH 120/158] Fix .appveyor.yml link in README.md Someone seems to have forgotten a dot in the URL. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e93d9f630..7403db3bf 100644 --- a/README.md +++ b/README.md @@ -45,4 +45,4 @@ to create distributable packages, which is run by calling: ### Development on Windows This project has currently only been worked on in Linux and macOS. If you are on Windows, you can -checkout out the build steps in [appveyor.yml](https://github.com/lbryio/lbry-app/blob/master/appveyor.yml) and probably figure out something from there. +checkout out the build steps in [appveyor.yml](https://github.com/lbryio/lbry-app/blob/master/.appveyor.yml) and probably figure out something from there. -- 2.45.2 From e005289967051ec7f7f98664abe55c2ebf1bb49c Mon Sep 17 00:00:00 2001 From: Alex Grintsvayg Date: Mon, 24 Apr 2017 15:53:39 -0400 Subject: [PATCH 121/158] more lighthouses --- ui/js/lighthouse.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/js/lighthouse.js b/ui/js/lighthouse.js index 9aa51747d..5ca4ef038 100644 --- a/ui/js/lighthouse.js +++ b/ui/js/lighthouse.js @@ -5,6 +5,8 @@ const queryTimeout = 3000; const maxQueryTries = 2; const defaultServers = [ 'http://lighthouse7.lbry.io:50005', + 'http://lighthouse8.lbry.io:50005', + 'http://lighthouse9.lbry.io:50005', ]; const path = '/'; -- 2.45.2 From a1198059ac76522c0658a5e374b2a145187900ec Mon Sep 17 00:00:00 2001 From: Alex Grintsvayg Date: Tue, 25 Apr 2017 14:41:26 -0400 Subject: [PATCH 122/158] enable windows code signing --- .appveyor.yml | 8 +++++++- CHANGELOG.md | 1 + build/build.ps1 | 6 ++++++ build/lbry2.pfx.enc | Bin 0 -> 5968 bytes 4 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 build/lbry2.pfx.enc diff --git a/.appveyor.yml b/.appveyor.yml index 52e3c6879..21e80e683 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -1,8 +1,14 @@ # Test against the latest version of this Node.js version environment: - nodejs_version: "7" + nodejs_version: 7 GH_TOKEN: secure: LiI5jyuHUw6XbH4kC3gP1HX4P/v4rwD/gCNtaFhQu2AvJz1/1wALkp5ECnIxRySN + key_pass: + secure: u6DydPcdrUJlxGL9uc7yQRYG8+5rY6aAEE9nfCSzFyNzZlX9NniOp8Uh5ZKQqX7bGEngLI6ipbLfiJvn0XFnhbn2iTkOuMqOXVJVOehvwlQ= + pfx_key: + secure: 1mwqyRy7hDqDjDK+TIAoaXyXzpNgwruFNA6TPkinUcVM7A+NLD33RQLnfnwVy+R5ovD2pUfhQ6+N0Fqebv6tZh436LIEsock+6IOdpgFwrg= + # find with: Get-Childitem –Path "C:\Program Files (x86)\Microsoft SDKs\Windows\" -Include *signtool* -File -Recurse -ErrorAction SilentlyContinue + SIGNTOOL_PATH: C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Bin\signtool.exe skip_branch_with_pr: true diff --git a/CHANGELOG.md b/CHANGELOG.md index ced55e42f..ead250225 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Web UI version numbers should always match the corresponding version of LBRY App * Redesigned UI for Discover * Handle more of price calculations at the daemon layer to improve page load time * Add special support for building channel claims in lbryuri module + * Enable windows code signing of binary ### Changed * Update process now easier and more reliable diff --git a/build/build.ps1 b/build/build.ps1 index 3df3d9b7c..431b418c4 100644 --- a/build/build.ps1 +++ b/build/build.ps1 @@ -31,4 +31,10 @@ $binary_name = Get-ChildItem -Path dist -Filter '*.exe' -Name $new_name = $binary_name -replace '^LBRY Setup (.*)\.exe$', 'LBRY_$1.exe' Rename-Item -Path "dist\$binary_name" -NewName $new_name dir dist # verify that binary was built/named correctly + +# sign binary +nuget install secure-file -ExcludeVersion +secure-file\tools\secure-file -decrypt build\lbry2.pfx.enc -secret "$env:pfx_key" +& ${env:SIGNTOOL_PATH} sign /f build\lbry2.pfx /p "$env:key_pass" /tr http://tsa.starfieldtech.com /td SHA256 /fd SHA256 dist\*.exe + python build\release_on_tag.py \ No newline at end of file diff --git a/build/lbry2.pfx.enc b/build/lbry2.pfx.enc new file mode 100644 index 0000000000000000000000000000000000000000..46e52260aab51183e669e77f96edbe1a463bf391 GIT binary patch literal 5968 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≫?re#eh04 zGzpYx)jg6$F+Y)1wEAH(kYwS`$^Bn(F*>M02fv6maS}<5k+i9fxE+~YnokO`|DrI2r%87G;Nq!Qb+%TX z-85F2Q{3$k4Gku-biuI}C& Date: Tue, 25 Apr 2017 15:48:45 -0400 Subject: [PATCH 123/158] latest daemon version --- build/DAEMON_URL | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/DAEMON_URL b/build/DAEMON_URL index ea9a259eb..19a28c9a8 100644 --- a/build/DAEMON_URL +++ b/build/DAEMON_URL @@ -1 +1 @@ -https://github.com/lbryio/lbry/releases/download/v0.10.0rc13/lbrynet-daemon-v0.10.0rc13-OSNAME.zip +https://github.com/lbryio/lbry/releases/download/v0.10.0rc15/lbrynet-daemon-v0.10.0rc15-OSNAME.zip -- 2.45.2 From 066f96cf8a355f1375cf85fcf04063e41cb7c375 Mon Sep 17 00:00:00 2001 From: Alex Grintsvayg Date: Tue, 25 Apr 2017 15:57:31 -0400 Subject: [PATCH 124/158] show daemon version before releasing --- build/release.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/build/release.py b/build/release.py index 7ecfa32cd..1a8392160 100644 --- a/build/release.py +++ b/build/release.py @@ -16,6 +16,7 @@ import github import changelog ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) +DAEMON_URL_FILE = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'DAEMON_URL') def main(): @@ -37,6 +38,11 @@ def main(): print 'Current version: {}'.format(repo.current_version) print 'New version: {}'.format(repo.new_version) + with open(DAEMON_URL_FILE, 'r') as f: + daemon_url_template = f.read().strip() + daemon_version = re.search('/(?Pv[^/]+)', daemon_url_template) + print 'Daemon version: {} ({})'.format( + daemon_version.group('version'), daemon_url_template) if not args.confirm and not confirm(): print "Aborting" @@ -157,7 +163,10 @@ in the future""" def confirm(): - return raw_input('Is this what you want? [y/N] ').strip().lower() == 'y' + try: + return raw_input('Is this what you want? [y/N] ').strip().lower() == 'y' + except KeyboardInterrupt: + return False def run_sanity_checks(repo, branch): @@ -181,9 +190,8 @@ def run_sanity_checks(repo, branch): def check_daemon_urls(): - daemon_url_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'DAEMON_URL') success = True - with open(daemon_url_file, 'r') as f: + with open(DAEMON_URL_FILE, 'r') as f: daemon_url_template = f.read().strip() if "OSNAME" not in daemon_url_template: print "Daemon URL must include the string 'OSNAME'" -- 2.45.2 From 092a018d6390630a07baeec0463db118d75faa41 Mon Sep 17 00:00:00 2001 From: Alex Grintsvayg Date: Tue, 25 Apr 2017 15:57:40 -0400 Subject: [PATCH 125/158] =?UTF-8?q?Bump=20version:=200.10.0rc5=20=E2=86=92?= =?UTF-8?q?=200.10.0rc6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- app/package.json | 2 +- ui/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 6039121b2..aa6e0b625 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.10.0rc5 +current_version = 0.10.0rc6 commit = True tag = True parse = (?P\d+)\.(?P\d+)\.(?P\d+)((?P[a-z]+)(?P\d+))? diff --git a/app/package.json b/app/package.json index a800dfd2b..a2b1a644d 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "LBRY", - "version": "0.10.0rc5", + "version": "0.10.0rc6", "main": "main.js", "description": "LBRY is a fully decentralized, open-source protocol facilitating the discovery, access, and (sometimes) purchase of data.", "author": { diff --git a/ui/package.json b/ui/package.json index decc4bebf..a0580e550 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "lbry-web-ui", - "version": "0.10.0rc5", + "version": "0.10.0rc6", "description": "LBRY UI", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" -- 2.45.2 From adb42b2c75c715b490102f5309b6e35e555b3a6a Mon Sep 17 00:00:00 2001 From: Alex Grintsvayg Date: Tue, 25 Apr 2017 16:57:01 -0400 Subject: [PATCH 126/158] more changelog sections --- build/changelog.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/build/changelog.py b/build/changelog.py index eb03b682f..322fc30a7 100644 --- a/build/changelog.py +++ b/build/changelog.py @@ -24,6 +24,14 @@ TEMPLATE = """### Added * * +### Deprecated + * + * + +### Removed + * + * + """ @@ -96,6 +104,7 @@ class Changelog(object): output.append('### {}'.format(section)) for entry in sections[section]: output.append(' * {}'.format(entry)) + output.append("\n") return output def get_unreleased(self): @@ -106,7 +115,7 @@ class Changelog(object): return today = datetime.datetime.today() - header = '## [{}] - {}\n'.format(version, today.strftime('%Y-%m-%d')) + header = "## [{}] - {}\n\n".format(version, today.strftime('%Y-%m-%d')) changelog_data = ( ''.join(self.start) + -- 2.45.2 From 88720903f43882f0fbfdd32515b2b961916dd269 Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Tue, 25 Apr 2017 21:22:44 -0400 Subject: [PATCH 127/158] add missing initial value for array reduce call --- ui/js/lbry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/js/lbry.js b/ui/js/lbry.js index 2471b38b5..999f192c2 100644 --- a/ui/js/lbry.js +++ b/ui/js/lbry.js @@ -517,7 +517,7 @@ lbry._updateClaimOwnershipCache = function(claimId) { lbry.getMyClaims((claimInfos) => { lbry._claimIdOwnershipCache[claimId] = !!claimInfos.reduce(function(match, claimInfo) { return match || claimInfo.claim_id == claimId; - }); + }, false); }); }; -- 2.45.2 From 9348d4d9121c01aeb627124bb9934f3315d6943c Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Thu, 27 Apr 2017 02:52:14 -0400 Subject: [PATCH 128/158] Move version checking into Electron --- app/main.js | 47 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 3 +++ ui/js/app.js | 12 ++++-------- ui/js/lbry.js | 48 +++++++++--------------------------------------- 4 files changed, 63 insertions(+), 47 deletions(-) diff --git a/app/main.js b/app/main.js index 07aed14d0..eb867a2d7 100644 --- a/app/main.js +++ b/app/main.js @@ -1,11 +1,18 @@ const {app, BrowserWindow, ipcMain} = require('electron'); +const url = require('url'); const path = require('path'); const jayson = require('jayson'); +const semver = require('semver'); +const https = require('https'); // tree-kill has better cross-platform handling of // killing a process. child-process.kill was unreliable const kill = require('tree-kill'); const child_process = require('child_process'); const assert = require('assert'); +const {version: localVersion} = require(app.getAppPath() + '/package.json'); + +const VERSION_CHECK_INTERVAL = 30 * 60 * 1000; +const LATEST_RELEASE_API_URL = 'https://api.github.com/repos/lbryio/lbry-app/releases/latest'; let client = jayson.client.http('http://localhost:5279/lbryapi'); @@ -23,6 +30,46 @@ let daemonStopRequested = false; // this is set to true and app.quit() is called again to quit for real. let readyToQuit = false; +function checkForNewVersion(callback) { + function formatRc(ver) { + // Adds dash if needed to make RC suffix semver friendly + return ver.replace(/([^-])rc/, '$1-rc'); + } + + let result = ''; + const opts = { + headers: { + 'User-Agent': `LBRY/${localVersion}`, + } + }; + const req = https.get(Object.assign(opts, url.parse(LATEST_RELEASE_API_URL)), (res) => { + res.on('data', (data) => { + result += data; + }); + res.on('end', () => { + console.log('Local version:', localVersion); + const tagName = JSON.parse(result).tag_name; + const [_, remoteVersion] = tagName.match(/^v([\d.]+(?:-?rc\d+)?)$/); + if (!remoteVersion) { + console.log('Malformed remote version string:', tagName); + win.webContents.send('version-info-received', null); + } else { + console.log('Remote version:', remoteVersion); + const upgradeAvailable = semver.gt(formatRc(remoteVersion), formatRc(localVersion)); + console.log(upgradeAvailable ? 'Upgrade available' : 'No upgrade available'); + win.webContents.send('version-info-received', {remoteVersion, localVersion, upgradeAvailable}); + } + }) + }); + + req.on('error', (err) => { + console.log('Failed to get current version from GitHub. Error:', err); + win.webContents.send('version-info-received', null); + }); +} + +ipcMain.on('version-info-requested', checkForNewVersion); + /* * Replacement for Electron's shell.openItem. The Electron version doesn't * reliably work from the main process, and we need to be able to run it diff --git a/package.json b/package.json index f342a1e52..7c7099f6a 100644 --- a/package.json +++ b/package.json @@ -42,5 +42,8 @@ "devDependencies": { "electron": "^1.4.15", "electron-builder": "^11.7.0" + }, + "dependencies": { + "semver": "^5.3.0" } } diff --git a/ui/js/app.js b/ui/js/app.js index 9cfb43137..1f686d07d 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -115,17 +115,13 @@ var App = React.createClass({ }); if (!sessionStorage.getItem('upgradeSkipped')) { - lbry.checkNewVersionAvailable(({isAvailable}) => { - if (!isAvailable) { - return; - } - - lbry.getVersionInfo((versionInfo) => { - this._version = versionInfo.lbrynet_version; + lbry.getVersionInfo().then(({remoteVersion, upgradeAvailable}) => { + if (upgradeAvailable) { + this._version = remoteVersion; this.setState({ modal: 'upgrade', }); - }); + } }); } }, diff --git a/ui/js/lbry.js b/ui/js/lbry.js index 999f192c2..e2807090a 100644 --- a/ui/js/lbry.js +++ b/ui/js/lbry.js @@ -4,7 +4,7 @@ import jsonrpc from './jsonrpc.js'; import lbryuri from './lbryuri.js'; import {getLocal, getSession, setSession, setLocal} from './utils.js'; -const {remote} = require('electron'); +const {remote, ipcRenderer} = require('electron'); const menu = remote.require('./menu/main-menu'); /** @@ -361,44 +361,6 @@ lbry.publish = function(params, fileListedCallback, publishedCallback, errorCall //}); } -lbry.getVersionInfo = function(callback) { - lbry.call('version', {}, callback); -}; - -lbry.checkNewVersionAvailable = function(callback) { - lbry.call('version', {}, function(versionInfo) { - var ver = versionInfo.lbrynet_version.split('.'); - - var maj = parseInt(ver[0]), - min = parseInt(ver[1]), - patch = parseInt(ver[2]); - - var remoteVer = versionInfo.remote_lbrynet.split('.'); - var remoteMaj = parseInt(remoteVer[0]), - remoteMin = parseInt(remoteVer[1]), - remotePatch = parseInt(remoteVer[2]); - - if (maj < remoteMaj) { - var newVersionAvailable = true; - } else if (maj == remoteMaj) { - if (min < remoteMin) { - var newVersionAvailable = true; - } else if (min == remoteMin) { - var newVersionAvailable = (patch < remotePatch); - } else { - var newVersionAvailable = false; - } - } else { - var newVersionAvailable = false; - } - callback(newVersionAvailable); - }, function(err) { - if (err.fault == 'NoSuchFunction') { - // Really old daemon that can't report a version - callback(true); - } - }); -} lbry.getClientSettings = function() { var outSettings = {}; @@ -608,6 +570,14 @@ lbry.showMenuIfNeeded = function() { sessionStorage.setItem('menuShown', chosenMenu); }; +lbry.getVersionInfo = function() { + return new Promise((resolve, reject) => { + ipcRenderer.once('version-info-received', (event, versionInfo) => { resolve(versionInfo) }); + ipcRenderer.send('version-info-requested'); + }); +} + + /** * Wrappers for API methods to simulate missing or future behavior. Unlike the old-style stubs, * these are designed to be transparent wrappers around the corresponding API methods. -- 2.45.2 From 63da853ba7e73053812a0d9924f53e4edca446ce Mon Sep 17 00:00:00 2001 From: Alex Grintsvayg Date: Thu, 27 Apr 2017 08:40:29 -0400 Subject: [PATCH 129/158] rename share_usage_stats setting --- build/DAEMON_URL | 2 +- ui/js/page/settings.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build/DAEMON_URL b/build/DAEMON_URL index 19a28c9a8..a30ad8609 100644 --- a/build/DAEMON_URL +++ b/build/DAEMON_URL @@ -1 +1 @@ -https://github.com/lbryio/lbry/releases/download/v0.10.0rc15/lbrynet-daemon-v0.10.0rc15-OSNAME.zip +https://github.com/lbryio/lbry/releases/download/v0.10.1rc1/lbrynet-daemon-v0.10.1rc1-OSNAME.zip diff --git a/ui/js/page/settings.js b/ui/js/page/settings.js index d05be08fe..01c1cca70 100644 --- a/ui/js/page/settings.js +++ b/ui/js/page/settings.js @@ -22,7 +22,7 @@ var SettingsPage = React.createClass({ this.setDaemonSetting('run_on_startup', event.target.checked); }, onShareDataChange: function (event) { - this.setDaemonSetting('share_debug_info', event.target.checked); + this.setDaemonSetting('share_usage_stats', event.target.checked); }, onDownloadDirChange: function(event) { this.setDaemonSetting('download_directory', event.target.value); @@ -189,7 +189,7 @@ var SettingsPage = React.createClass({

-- 2.45.2 From e06c2320e19af320b193cb78139a042d56ac378d Mon Sep 17 00:00:00 2001 From: Alex Grintsvayg Date: Thu, 27 Apr 2017 14:18:59 -0400 Subject: [PATCH 130/158] prefer wallet_unused_address over wallet_new_address --- ui/js/component/auth.js | 2 +- ui/js/page/wallet.js | 6 +++--- ui/js/rewards.js | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ui/js/component/auth.js b/ui/js/component/auth.js index f0e1f1074..4f9caa755 100644 --- a/ui/js/component/auth.js +++ b/ui/js/component/auth.js @@ -199,7 +199,7 @@ const CodeRequiredStage = React.createClass({ }) if (!this.state.address) { - lbry.call('wallet_new_address', {}, (address) => { + lbry.getUnusedAddress((address) => { setLocal('wallet_address', address); this.setState({ address: address }); }); diff --git a/ui/js/page/wallet.js b/ui/js/page/wallet.js index 8540e5469..42fa92907 100644 --- a/ui/js/page/wallet.js +++ b/ui/js/page/wallet.js @@ -16,7 +16,7 @@ var AddressSection = React.createClass({ this.setState({ address: address, }); - }); + }.bind(this)); }, _getNewAddress: function(event) { @@ -24,12 +24,12 @@ var AddressSection = React.createClass({ event.preventDefault(); } - lbry.wallet_new_address().then(function(address) { + lbry.getNewAddress((address) => { window.localStorage.setItem('wallet_address', address); this.setState({ address: address, }); - }.bind(this)) + }.bind(this)); }, getInitialState: function() { diff --git a/ui/js/rewards.js b/ui/js/rewards.js index e499085e8..399965db2 100644 --- a/ui/js/rewards.js +++ b/ui/js/rewards.js @@ -56,7 +56,7 @@ rewards.claimReward = function (type) { } return new Promise((resolve, reject) => { - lbry.get_new_address().then((address) => { + lbry.wallet_new_address().then((address) => { const params = { reward_type: type, wallet_address: address, -- 2.45.2 From 3648554f48867852138315e52055f0bc2ffaf3fd Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Fri, 28 Apr 2017 03:13:02 -0400 Subject: [PATCH 131/158] Move semver dependency into app directory --- app/package.json | 1 + package.json | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/package.json b/app/package.json index a2b1a644d..0e7151bce 100644 --- a/app/package.json +++ b/app/package.json @@ -12,6 +12,7 @@ "install": "^0.8.7", "jayson": "^2.0.2", "npm": "^4.2.0", + "semver": "^5.3.0", "tree-kill": "^1.1.0" } } diff --git a/package.json b/package.json index 7c7099f6a..45142bbaa 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,5 @@ "electron": "^1.4.15", "electron-builder": "^11.7.0" }, - "dependencies": { - "semver": "^5.3.0" - } + "dependencies": {} } -- 2.45.2 From 2bd12a9ecd3662740eecec90250cf65b1bccc3cc Mon Sep 17 00:00:00 2001 From: Alex Grintsvayg Date: Fri, 28 Apr 2017 10:29:05 -0400 Subject: [PATCH 132/158] =?UTF-8?q?Bump=20version:=200.10.0rc6=20=E2=86=92?= =?UTF-8?q?=200.10.0rc7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .bumpversion.cfg | 2 +- app/package.json | 2 +- ui/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index aa6e0b625..153146421 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.10.0rc6 +current_version = 0.10.0rc7 commit = True tag = True parse = (?P\d+)\.(?P\d+)\.(?P\d+)((?P[a-z]+)(?P\d+))? diff --git a/app/package.json b/app/package.json index 0e7151bce..cb66f9e1c 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "LBRY", - "version": "0.10.0rc6", + "version": "0.10.0rc7", "main": "main.js", "description": "LBRY is a fully decentralized, open-source protocol facilitating the discovery, access, and (sometimes) purchase of data.", "author": { diff --git a/ui/package.json b/ui/package.json index a0580e550..cb9fe5663 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "lbry-web-ui", - "version": "0.10.0rc6", + "version": "0.10.0rc7", "description": "LBRY UI", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" -- 2.45.2 From 95cb2baab24a4cbab3ef08c8cbf3b12ab187c59f Mon Sep 17 00:00:00 2001 From: MaxiBoether Date: Sat, 29 Apr 2017 18:28:33 +0200 Subject: [PATCH 133/158] Removed .bind() from lambda wallet.js did not compile with .bind after the lambda function, and after talking in #dev in Gitter we came to the conclusion to just remove the bind as it's not necessary --- ui/js/page/wallet.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/js/page/wallet.js b/ui/js/page/wallet.js index 42fa92907..5f9e4e53a 100644 --- a/ui/js/page/wallet.js +++ b/ui/js/page/wallet.js @@ -16,7 +16,7 @@ var AddressSection = React.createClass({ this.setState({ address: address, }); - }.bind(this)); + }); }, _getNewAddress: function(event) { @@ -29,7 +29,7 @@ var AddressSection = React.createClass({ this.setState({ address: address, }); - }.bind(this)); + }); }, getInitialState: function() { -- 2.45.2 From 0ffb0fd4d7db3c4a19573c7d2db37b8aa3a8ce08 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Sat, 29 Apr 2017 21:56:55 -0400 Subject: [PATCH 134/158] Replace all uses of bind() with arrow functions --- ui/js/app.js | 4 ++-- ui/js/component/auth.js | 4 ++-- ui/js/component/drawer.js | 4 ++-- ui/js/component/header.js | 4 ++-- ui/js/component/link.js | 4 ++-- ui/js/component/snack-bar.js | 4 ++-- ui/js/page/discover.js | 4 ++-- ui/js/page/settings.js | 12 ++++++------ ui/js/page/wallet.js | 4 ++-- 9 files changed, 22 insertions(+), 22 deletions(-) diff --git a/ui/js/app.js b/ui/js/app.js index 1f686d07d..b7a552e5e 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -145,9 +145,9 @@ var App = React.createClass({ this._isMounted = false; }, registerHistoryPop: function() { - window.addEventListener("popstate", function() { + window.addEventListener("popstate", () => { 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 diff --git a/ui/js/component/auth.js b/ui/js/component/auth.js index 4f9caa755..303c3a964 100644 --- a/ui/js/component/auth.js +++ b/ui/js/component/auth.js @@ -78,12 +78,12 @@ const ConfirmEmailStage = React.createClass({ submitting: true, }); - const onSubmitError = function(error) { + const onSubmitError = (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, email: this.props.email}, 'post').then((userEmail) => { if (userEmail.IsVerified) { diff --git a/ui/js/component/drawer.js b/ui/js/component/drawer.js index e719af073..b19e901f5 100644 --- a/ui/js/component/drawer.js +++ b/ui/js/component/drawer.js @@ -34,11 +34,11 @@ var Drawer = React.createClass({ }; }, componentDidMount: function() { - this._balanceSubscribeId = lbry.balanceSubscribe(function(balance) { + this._balanceSubscribeId = lbry.balanceSubscribe((balance) => { this.setState({ balance: balance }); - }.bind(this)); + }); }, componentWillUnmount: function() { if (this._balanceSubscribeId) { diff --git a/ui/js/component/header.js b/ui/js/component/header.js index 463042cff..32315ef2f 100644 --- a/ui/js/component/header.js +++ b/ui/js/component/header.js @@ -10,9 +10,9 @@ var Header = React.createClass({ }; }, componentWillMount: function() { - new MutationObserver(function(mutations) { + new MutationObserver((mutations) => { this.setState({ title: mutations[0].target.textContent }); - }.bind(this)).observe( + }).observe( document.querySelector('title'), { subtree: true, characterData: true, childList: true } ); diff --git a/ui/js/component/link.js b/ui/js/component/link.js index 55c0060dd..466fcb45c 100644 --- a/ui/js/component/link.js +++ b/ui/js/component/link.js @@ -70,11 +70,11 @@ export let RewardLink = React.createClass({ return; case 'first_publish': - lbry.claim_list_mine().then(function(list) { + lbry.claim_list_mine().then((list) => { this.setState({ claimable: list.length > 0 }) - }.bind(this)); + }); return; } }, diff --git a/ui/js/component/snack-bar.js b/ui/js/component/snack-bar.js index a993c3b75..6d6f17915 100644 --- a/ui/js/component/snack-bar.js +++ b/ui/js/component/snack-bar.js @@ -36,12 +36,12 @@ export const SnackBar = React.createClass({ let snack = this.state.snacks[0]; if (this._hideTimeout === null) { - this._hideTimeout = setTimeout(function() { + this._hideTimeout = setTimeout(() => { this._hideTimeout = null; let snacks = this.state.snacks; snacks.shift(); this.setState({ snacks: snacks }); - }.bind(this), this._displayTime * 1000); + }, this._displayTime * 1000); } return ( diff --git a/ui/js/page/discover.js b/ui/js/page/discover.js index b6afa426f..42ed3999d 100644 --- a/ui/js/page/discover.js +++ b/ui/js/page/discover.js @@ -110,11 +110,11 @@ var FeaturedContent = React.createClass({
Failed to load landing content.
:
{ - Object.keys(this.state.featuredUris).map(function(category) { + Object.keys(this.state.featuredUris).map((category) => { return this.state.featuredUris[category].length ? : ''; - }.bind(this)) + }) }
); diff --git a/ui/js/page/settings.js b/ui/js/page/settings.js index 01c1cca70..b0f4ff9d9 100644 --- a/ui/js/page/settings.js +++ b/ui/js/page/settings.js @@ -60,13 +60,13 @@ var SettingsPage = React.createClass({ document.title = "Settings"; }, componentWillMount: function() { - lbry.getDaemonSettings(function(settings) { + lbry.getDaemonSettings((settings) => { this.setState({ daemonSettings: settings, isMaxUpload: settings.max_upload != 0, isMaxDownload: settings.max_download != 0 }); - }.bind(this)); + }); }, onShowNsfwChange: function(event) { lbry.setClientSetting('showNsfw', event.target.checked); @@ -113,13 +113,13 @@ var SettingsPage = React.createClass({
Max Upload
{ this.onMaxUploadPrefChange(false) }} defaultChecked={!this.state.isMaxUpload} label="Unlimited" />
{ this.onMaxUploadPrefChange(true) }} defaultChecked={this.state.isMaxUpload} label={ this.state.isMaxUpload ? 'Up to' : 'Choose limit...' } /> { this.state.isMaxUpload ? @@ -142,12 +142,12 @@ var SettingsPage = React.createClass({ { this.onMaxDownloadPrefChange(false) }} defaultChecked={!this.state.isMaxDownload} />
{ this.onMaxDownloadPrefChange(true) }} defaultChecked={this.state.isMaxDownload} label={ this.state.isMaxDownload ? 'Up to' : 'Choose limit...' } /> { this.state.isMaxDownload ? diff --git a/ui/js/page/wallet.js b/ui/js/page/wallet.js index 42fa92907..5f9e4e53a 100644 --- a/ui/js/page/wallet.js +++ b/ui/js/page/wallet.js @@ -16,7 +16,7 @@ var AddressSection = React.createClass({ this.setState({ address: address, }); - }.bind(this)); + }); }, _getNewAddress: function(event) { @@ -29,7 +29,7 @@ var AddressSection = React.createClass({ this.setState({ address: address, }); - }.bind(this)); + }); }, getInitialState: function() { -- 2.45.2 From ac8a64f5e56da4ca9d82d9c665e02a84a19da1f1 Mon Sep 17 00:00:00 2001 From: Josh Finer Date: Sun, 30 Apr 2017 14:39:29 -0400 Subject: [PATCH 135/158] Update publish.js Raise default minimum bid --- ui/js/page/publish.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/js/page/publish.js b/ui/js/page/publish.js index 13736f0db..36bcae479 100644 --- a/ui/js/page/publish.js +++ b/ui/js/page/publish.js @@ -119,7 +119,7 @@ var PublishPage = React.createClass({ channels: null, rawName: '', name: '', - bid: 1, + bid: 10, hasFile: false, feeAmount: '', feeCurrency: 'USD', -- 2.45.2 From 778289a2b1226d0747595a973ef9955b66c428b2 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Tue, 2 May 2017 03:26:23 -0400 Subject: [PATCH 136/158] Make final dialog of auth process close properly --- ui/js/component/auth.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/js/component/auth.js b/ui/js/component/auth.js index 303c3a964..dc36367be 100644 --- a/ui/js/component/auth.js +++ b/ui/js/component/auth.js @@ -139,11 +139,11 @@ const WelcomeStage = React.createClass({

Below, LBRY is controlled by users -- you -- via blockchain and decentralization.

Thank you for making content freedom possible! Here's a nickel, kid.

- + this.props.setStage(null)} onConfirmed={() => { this.props.setStage(null) }} />
: - + { this.props.setStage(null) }}>

About Your Reward

You earned a reward of LBRY credits, or LBC.

-- 2.45.2 From 9a26078e347c72ed173859c744da7e86dc49da3a Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Tue, 2 May 2017 03:47:41 -0400 Subject: [PATCH 137/158] Add Select All option to Edit menu --- app/menu/main-menu.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/menu/main-menu.js b/app/menu/main-menu.js index 585435730..32fc5168f 100644 --- a/app/menu/main-menu.js +++ b/app/menu/main-menu.js @@ -24,6 +24,9 @@ const baseTemplate = [ { role: 'paste', }, + { + role: 'selectall', + }, ] }, { -- 2.45.2 From 6fed6304b3d4f417a40435ef8c8644f43441dcf9 Mon Sep 17 00:00:00 2001 From: Alex Grintsvayg Date: Tue, 2 May 2017 10:06:53 -0400 Subject: [PATCH 138/158] bump daemon version --- build/DAEMON_URL | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/DAEMON_URL b/build/DAEMON_URL index a30ad8609..03324eba1 100644 --- a/build/DAEMON_URL +++ b/build/DAEMON_URL @@ -1 +1 @@ -https://github.com/lbryio/lbry/releases/download/v0.10.1rc1/lbrynet-daemon-v0.10.1rc1-OSNAME.zip +https://github.com/lbryio/lbry/releases/download/v0.10.1rc2/lbrynet-daemon-v0.10.1rc2-OSNAME.zip -- 2.45.2 From 71771aeb6f224efcaf0d9a28f4f56da954d1db90 Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Wed, 26 Apr 2017 23:54:53 -0400 Subject: [PATCH 139/158] it's not the worst thing I've ever done --- ui/js/app.js | 39 ++++--- ui/js/component/header.js | 159 ++++++++++++++++++++--------- ui/js/component/link.js | 2 +- ui/js/lbry.js | 8 +- ui/js/lbryio.js | 5 +- ui/js/page/discover.js | 2 +- ui/js/page/file-list.js | 1 - ui/js/page/publish.js | 3 - ui/js/page/settings.js | 2 +- ui/js/page/show.js | 4 +- ui/js/page/start.js | 3 - ui/js/page/wallet.js | 3 - ui/js/utils.js | 4 +- ui/scss/_canvas.scss | 173 +------------------------------- ui/scss/_global.scss | 4 +- ui/scss/_icons.scss | 6 -- ui/scss/all.scss | 1 + ui/scss/component/_button.scss | 15 ++- ui/scss/component/_card.scss | 4 +- ui/scss/component/_header.scss | 103 +++++++++++++++++++ ui/scss/component/_menu.scss | 2 +- ui/scss/component/_modal.scss | 2 +- ui/scss/component/_tooltip.scss | 2 +- 23 files changed, 272 insertions(+), 275 deletions(-) create mode 100644 ui/scss/component/_header.scss diff --git a/ui/js/app.js b/ui/js/app.js index b7a552e5e..97ad1bfab 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -244,51 +244,50 @@ var App = React.createClass({ return null; } }, - getMainContent: function() + getContentAndAddress: function() { switch(this.state.viewingPage) { case 'settings': - return ; + return ["Settings", "icon-gear", ]; case 'help': - return ; + return ["Help", "icon-question", ]; case 'report': - return ; + return ['Report', 'icon-file', ]; case 'downloaded': - return ; + return ["Downloads & Purchases", "icon-folder", ]; case 'published': - return ; + return ["Publishes", "icon-folder", ]; case 'start': - return ; + return ["Start", "icon-file", ]; case 'rewards': - return ; + return ["Rewards", "icon-bank", ]; case 'wallet': case 'send': case 'receive': - return ; + return [this.state.viewingPage.charAt(0).toUpperCase() + this.state.viewingPage.slice(1), "icon-bank", ] case 'show': - return ; + return [this.state.pageArgs, "icon-file", ]; case 'publish': - return ; + return ["Publish", "icon-upload", ]; case 'developer': - return ; + return ["Developer", "icon-file", ]; case 'discover': default: - return ; + return ["Home", "icon-home", ]; } }, render: function() { - var mainContent = this.getMainContent(), - headerLinks = this.getHeaderLinks(), - searchQuery = this.state.viewingPage == 'discover' && this.state.pageArgs ? this.state.pageArgs : ''; - + let [address, wunderBarIcon, mainContent] = this.getContentAndAddress(), + headerLinks = this.getHeaderLinks(); + return ( this._fullScreenPages.includes(this.state.viewingPage) ? mainContent : -
- +
+
-
{mainContent}
{ - this.setState({ title: mutations[0].target.textContent }); - }).observe( - document.querySelector('title'), - { subtree: true, characterData: true, childList: true } - ); - }, componentDidMount: function() { - document.addEventListener('scroll', this.handleScroll); - }, - componentWillUnmount: function() { - document.removeEventListener('scroll', this.handleScroll); - if (this.userTypingTimer) - { - clearTimeout(this.userTypingTimer); - } - }, - handleScroll: function() { - this.setState({ - isScrolled: document.body.scrollTop > 0 + this._balanceSubscribeId = lbry.balanceSubscribe((balance) => { + this.setState({ balance: balance }); }); }, - onQueryChange: function(event) { - - if (this.userTypingTimer) - { - clearTimeout(this.userTypingTimer); + componentWillUnmount: function() { + if (this._balanceSubscribeId) { + lbry.balanceUnsubscribe(this._balanceSubscribeId) } + }, + render: function() { + return
+ + {this.props.links ? + : + ''} +
+ } +}); + +let WunderBar = React.createClass({ + _userTypingTimer: null, + _input: null, + _stateBeforeSearch: null, + + getInitialState: function() { + return { + address: this.props.address, + icon: this.props.icon + }; + }, + componentWillUnmount: function() { + if (this.userTypingTimer) { + clearTimeout(this._userTypingTimer); + } + }, + onChange: function(event) { + + if (this._userTypingTimer) + { + clearTimeout(this._userTypingTimer); + } + + this.setState({ address: event.target.value }) //@TODO: Switch to React.js timing var searchTerm = event.target.value; - this.userTypingTimer = setTimeout(() => { + + this._userTypingTimer = setTimeout(() => { this.props.onSearch(searchTerm); }, 800); // 800ms delay, tweak for faster/slower }, + componentWillReceiveProps(nextProps) { + if (nextProps.address !== this.state.address || nextProps.icon !== this.state.icon) { + this.setState({ address: nextProps.address, icon: nextProps.icon }); + } + }, + onFocus: function() { + this._stateBeforeSearch = this.state; + let newState = { + icon: "icon-search" + } + // this._input.value = ""; //trigger placeholder + this._focusPending = true; + if (!this.state.address.match(/^lbry:\/\//)) //onFocus, if they are not on an exact URL, clear the bar + { + newState.address = ""; + } + this.setState(newState); + }, + onBlur: function() { + this.setState(this._stateBeforeSearch); + this._input.value = this.state.address; + }, + componentDidUpdate: function() { + this._input.value = this.state.address; + if (this._input && this._focusPending) { + this._input.select(); + this._focusPending = false; + } + }, + onReceiveRef: function(ref) { + this._input = ref; + }, render: function() { - return ( - - ); + return
+ {this.state.icon ? : '' } + +
} -}); +}) var SubHeader = React.createClass({ render: function() { diff --git a/ui/js/component/link.js b/ui/js/component/link.js index 466fcb45c..d46fcda25 100644 --- a/ui/js/component/link.js +++ b/ui/js/component/link.js @@ -41,7 +41,7 @@ export let Link = React.createClass({ content = ( {'icon' in this.props ? : null} - {{this.props.label}} + {this.props.label ? {this.props.label} : null} {'badge' in this.props ? {this.props.badge} : null} ); diff --git a/ui/js/lbry.js b/ui/js/lbry.js index e2807090a..c13635a09 100644 --- a/ui/js/lbry.js +++ b/ui/js/lbry.js @@ -627,18 +627,18 @@ lbry.claim_list_mine = function(params={}) { } lbry.resolve = function(params={}) { - const claimCacheKey = 'resolve_claim_cache', - claimCache = getSession(claimCacheKey, {}) + const claimCacheKey = 'resolve_claim_cache3', + claimCache = getLocal(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]) { + if (params.uri && claimCache[params.uri] !== undefined) { resolve(claimCache[params.uri]); } else { lbry.call('resolve', params, function(data) { claimCache[params.uri] = data; - setSession(claimCacheKey, claimCache) + setLocal(claimCacheKey, claimCache) resolve(data) }, reject) } diff --git a/ui/js/lbryio.js b/ui/js/lbryio.js index bbe6b9ccd..7a1ab58c2 100644 --- a/ui/js/lbryio.js +++ b/ui/js/lbryio.js @@ -7,10 +7,11 @@ const lbryio = { _accessToken: getLocal('accessToken'), _authenticationPromise: null, _user : null, - enabled: true + enabled: false }; -const CONNECTION_STRING = process.env.LBRY_APP_API_URL ? process.env.LBRY_APP_API_URL : 'https://api.lbry.io/'; +// const CONNECTION_STRING = process.env.LBRY_APP_API_URL ? process.env.LBRY_APP_API_URL : 'https://api.lbry.io/'; +const CONNECTION_STRING = 'https://api.lbry.io/'; const EXCHANGE_RATE_TIMEOUT = 20 * 60 * 1000; lbryio.getExchangeRates = function() { diff --git a/ui/js/page/discover.js b/ui/js/page/discover.js index 42ed3999d..3c807e831 100644 --- a/ui/js/page/discover.js +++ b/ui/js/page/discover.js @@ -164,7 +164,7 @@ var DiscoverPage = React.createClass({ }, componentWillMount: function() { - document.title = "Discover"; + document.title = "Home"; if (this.props.query) { // Rendering with a query already typed diff --git a/ui/js/page/file-list.js b/ui/js/page/file-list.js index 063730e7f..3189d61fd 100644 --- a/ui/js/page/file-list.js +++ b/ui/js/page/file-list.js @@ -19,7 +19,6 @@ export let FileListDownloaded = React.createClass({ }, componentDidMount: function() { this._isMounted = true; - document.title = "Downloaded Files"; lbry.claim_list_mine().then((myClaimInfos) => { if (!this._isMounted) { return; } diff --git a/ui/js/page/publish.js b/ui/js/page/publish.js index 36bcae479..dc34ea500 100644 --- a/ui/js/page/publish.js +++ b/ui/js/page/publish.js @@ -348,9 +348,6 @@ var PublishPage = React.createClass({ componentWillMount: function() { this._updateChannelList(); }, - componentDidMount: function() { - document.title = "Publish"; - }, componentDidUpdate: function() { }, onFileChange: function() { diff --git a/ui/js/page/settings.js b/ui/js/page/settings.js index b0f4ff9d9..6cb062099 100644 --- a/ui/js/page/settings.js +++ b/ui/js/page/settings.js @@ -17,7 +17,7 @@ var SettingsPage = React.createClass({ setClientSetting: function(name, value) { lbry.setClientSetting(name, value) this._onSettingSaveSuccess() - }, + }, onRunOnStartChange: function (event) { this.setDaemonSetting('run_on_startup', event.target.checked); }, diff --git a/ui/js/page/show.js b/ui/js/page/show.js index cc2fb5cfc..f41039868 100644 --- a/ui/js/page/show.js +++ b/ui/js/page/show.js @@ -60,7 +60,6 @@ let ShowPage = React.createClass({ }, componentWillMount: function() { this._uri = lbryuri.normalize(this.props.uri); - 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; @@ -71,6 +70,8 @@ let ShowPage = React.createClass({ }); }); + document.title = metadata.title ? metadata.title : this._uri; + this.setState({ outpoint: outpoint, metadata: metadata, @@ -91,6 +92,7 @@ let ShowPage = React.createClass({ render: function() { const metadata = this.state.metadata; const title = metadata ? this.state.metadata.title : this._uri; + return (
diff --git a/ui/js/page/start.js b/ui/js/page/start.js index 9f918db27..972df3141 100644 --- a/ui/js/page/start.js +++ b/ui/js/page/start.js @@ -5,9 +5,6 @@ var StartPage = React.createClass({ componentWillMount: function() { lbry.stop(); }, - componentDidMount: function() { - document.title = "LBRY is Closed"; - }, render: function() { return (
diff --git a/ui/js/page/wallet.js b/ui/js/page/wallet.js index 5f9e4e53a..4c3a52d0e 100644 --- a/ui/js/page/wallet.js +++ b/ui/js/page/wallet.js @@ -270,9 +270,6 @@ var WalletPage = React.createClass({ propTypes: { viewingPage: React.PropTypes.string, }, - componentDidMount: function() { - document.title = "My Wallet"; - }, /* Below should be refactored so that balance is shared all of wallet page. Or even broader? What is the proper React pattern for sharing a global state like balance? diff --git a/ui/js/utils.js b/ui/js/utils.js index b24eb25b6..61bf53188 100644 --- a/ui/js/utils.js +++ b/ui/js/utils.js @@ -2,9 +2,9 @@ * Thin wrapper around localStorage.getItem(). Parses JSON and returns undefined if the value * is not set yet. */ -export function getLocal(key) { +export function getLocal(key, fallback=undefined) { const itemRaw = localStorage.getItem(key); - return itemRaw === null ? undefined : JSON.parse(itemRaw); + return itemRaw === null ? fallback : JSON.parse(itemRaw); } /** diff --git a/ui/scss/_canvas.scss b/ui/scss/_canvas.scss index 8aa4227e3..ce8f8e01c 100644 --- a/ui/scss/_canvas.scss +++ b/ui/scss/_canvas.scss @@ -11,44 +11,12 @@ body line-height: $font-line-height; } -$drawer-width: 220px; - -#drawer +#window { - width: $drawer-width; - position: fixed; min-height: 100vh; - left: 0; - top: 0; - background: $color-bg; - z-index: 3; - .drawer-item - { - display: block; - padding: $spacing-vertical / 2; - font-size: 1.2em; - height: $spacing-vertical * 1.5; - .icon - { - margin-right: 6px; - } - .link-label - { - line-height: $spacing-vertical * 1.5; - } - .badge - { - float: right; - margin-top: $spacing-vertical * 0.25 - 2; - background: $color-money; - } - } - .drawer-item-selected - { - background: $color-canvas; - color: $color-primary; - } + background: $color-canvas; } + .badge { background: $color-money; @@ -62,119 +30,9 @@ $drawer-width: 220px; font-weight: bold; color: $color-money; } -#drawer-handle -{ - padding: $spacing-vertical / 2; - max-height: $height-header - $spacing-vertical; - 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 } -} -#window.drawer-open -{ - #main-content { margin-left: $drawer-width; } - .open-drawer-link { display: none } - #header { padding-left: $drawer-width + $spacing-vertical / 2; } -} - -#header -{ - background: $color-primary; - color: white; - &.header-no-subnav { - height: $height-header; - } - &.header-with-subnav { - height: $height-header * 2; - } - position: fixed; - top: 0; - left: 0; - width: 100%; - z-index: 2; - box-sizing: border-box; - h1 { font-size: 1.8em; line-height: $height-header - $spacing-vertical; display: inline-block; float: left; } - &.header-scrolled - { - box-shadow: $default-box-shadow; - } -} -.header-top-bar -{ - padding: $spacing-vertical / 2; -} -.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; - } - } -} - -nav.sub-header -{ - background: $color-primary; - text-transform: uppercase; - padding: $spacing-vertical / 2; - > a - { - $sub-header-selected-underline-height: 2px; - display: inline-block; - margin: 0 15px; - padding: 0 5px; - line-height: $height-header - $spacing-vertical - $sub-header-selected-underline-height; - color: #e8e8e8; - &:first-child - { - margin-left: 0; - } - &:last-child - { - margin-right: 0; - } - &.sub-header-selected - { - border-bottom: $sub-header-selected-underline-height solid #fff; - color: #fff; - } - &:hover - { - color: #fff; - } - } -} #main-content { - background: $color-canvas; &.no-sub-nav { min-height: calc(100vh - 60px); //should be -$height-header, but I'm dumb I guess? It wouldn't work @@ -182,8 +40,7 @@ nav.sub-header } &.with-sub-nav { - min-height: calc(100vh - 120px); //should be -$height-header, but I'm dumb I guess? It wouldn't work - main { margin-top: $height-header * 2; } + min-height: calc(100vh - 60px); //should be -$height-header, but I'm dumb I guess? It wouldn't work } main { @@ -195,26 +52,4 @@ nav.sub-header margin-right: auto; } } -} - -$header-icon-size: 1.5em; - -.open-drawer-link, .close-drawer-link -{ - display: inline-block; - font-size: $header-icon-size; - padding: 2px 6px 0 6px; - float: left; -} -.close-lbry-link -{ - font-size: $header-icon-size; - float: right; - padding: 0 6px 0 18px; -} - -.full-screen -{ - width: 100%; - height: 100%; } \ No newline at end of file diff --git a/ui/scss/_global.scss b/ui/scss/_global.scss index d829d8245..c2ea35878 100644 --- a/ui/scss/_global.scss +++ b/ui/scss/_global.scss @@ -34,8 +34,8 @@ $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); +$box-shadow-layer: 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); +$box-shadow-focus: 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; diff --git a/ui/scss/_icons.scss b/ui/scss/_icons.scss index 91b8255bb..441113e39 100644 --- a/ui/scss/_icons.scss +++ b/ui/scss/_icons.scss @@ -25,12 +25,6 @@ transform: translate(0, 0); } -.icon-mega -{ - font-size: 200px; - line-height: 1; -} - /* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen readers do not read off random characters that represent icons */ .icon-glass:before { diff --git a/ui/scss/all.scss b/ui/scss/all.scss index b4c6611a6..413a7edaa 100644 --- a/ui/scss/all.scss +++ b/ui/scss/all.scss @@ -10,6 +10,7 @@ @import "component/_file-actions.scss"; @import "component/_file-tile.scss"; @import "component/_form-field.scss"; +@import "component/_header.scss"; @import "component/_menu.scss"; @import "component/_tooltip.scss"; @import "component/_load-screen.scss"; diff --git a/ui/scss/component/_button.scss b/ui/scss/component/_button.scss index e3c5fe8e8..5c6fed22f 100644 --- a/ui/scss/component/_button.scss +++ b/ui/scss/component/_button.scss @@ -34,6 +34,11 @@ $button-focus-shift: 12%; { padding-left: 5px; } + .icon:only-child + { + padding-left: 0; + padding-right: 0; + } } .button-block { @@ -49,17 +54,17 @@ $button-focus-shift: 12%; $color-button-text: white; color: darken($color-button-text, $button-focus-shift * 0.5); background-color: $color-primary; - box-shadow: $default-box-shadow; + box-shadow: $box-shadow-layer; &:focus { color: $color-button-text; - //box-shadow: $focus-box-shadow; + //box-shadow: $box-shadow-focus; background-color: mix(black, $color-primary, $button-focus-shift) } } .button-alt { background-color: $color-bg-alt; - box-shadow: $default-box-shadow; + box-shadow: $box-shadow-layer; } .button-text @@ -76,3 +81,7 @@ $button-focus-shift: 12%; @include text-link(#aaa); font-size: 0.8em; } +.button--flat +{ + box-shadow: none !important; +} \ No newline at end of file diff --git a/ui/scss/component/_card.scss b/ui/scss/component/_card.scss index e019d7342..e10d02fb3 100644 --- a/ui/scss/component/_card.scss +++ b/ui/scss/component/_card.scss @@ -7,7 +7,7 @@ $padding-card-horizontal: $spacing-vertical * 2/3; margin-right: auto; max-width: $width-page-constrained; background: $color-bg; - box-shadow: $default-box-shadow; + box-shadow: $box-shadow-layer; border-radius: 2px; margin-bottom: $spacing-vertical * 2/3; overflow: auto; @@ -86,7 +86,7 @@ $card-link-scaling: 1.1; .card--link:hover { position: relative; z-index: 1; - box-shadow: $focus-box-shadow; + box-shadow: $box-shadow-focus; transform: scale($card-link-scaling); transform-origin: 50% 50%; overflow-x: visible; diff --git a/ui/scss/component/_header.scss b/ui/scss/component/_header.scss new file mode 100644 index 000000000..5a550e771 --- /dev/null +++ b/ui/scss/component/_header.scss @@ -0,0 +1,103 @@ +@import "../global"; + +$color-header: #666; + + +$header-icon-size: 1.5em; + +.open-drawer-link, .close-drawer-link +{ + display: inline-block; + font-size: $header-icon-size; + padding: 2px 6px 0 6px; + float: left; +} + +#header +{ + color: $color-header; + background: #fff; + display: flex; + /* + &.header-no-subnav { + height: $height-header; + } + &.header-with-subnav { + height: $height-header * 2; + }*/ + position: fixed; + box-shadow: $box-shadow-layer; + top: 0; + left: 0; + width: 100%; + z-index: 2; + padding: $spacing-vertical / 2; + box-sizing: border-box; +} +.header__item { + flex: 0 0 content; + padding-left: $spacing-vertical / 4; + padding-right: $spacing-vertical / 4; +} +.header__item--wunderbar { + flex-grow: 1; +} + +.wunderbar +{ + position: relative; + .icon { + position: absolute; + left: 10px; + top: $spacing-vertical / 2 - 4px; //hacked + } +} +.wunderbar__input { + background: rgba(255, 255, 255, 0.7); + width: 100%; + color: $color-header; + height: $spacing-vertical * 1.5; + line-height: $spacing-vertical * 1.5; + padding-left: 38px; + padding-right: 5px; + border: 1px solid $color-text-dark; + @include border-radius(2px); + border: 1px solid #ccc; + &:focus { + box-shadow: $box-shadow-focus; + border-color: $color-header; + } +} + +nav.sub-header +{ + text-transform: uppercase; + padding: $spacing-vertical / 2; + > a + { + $sub-header-selected-underline-height: 2px; + display: inline-block; + margin: 0 15px; + padding: 0 5px; + line-height: $height-header - $spacing-vertical - $sub-header-selected-underline-height; + color: $color-header; + &:first-child + { + margin-left: 0; + } + &:last-child + { + margin-right: 0; + } + $color-selected-subheader: darken($color-header, 20%); + &.sub-header-selected + { + border-bottom: $sub-header-selected-underline-height solid $color-selected-subheader; + color: $color-selected-subheader; + } + &:hover + { + color: $color-selected-subheader; + } + } +} \ No newline at end of file diff --git a/ui/scss/component/_menu.scss b/ui/scss/component/_menu.scss index e3b0566c4..d8e79be28 100644 --- a/ui/scss/component/_menu.scss +++ b/ui/scss/component/_menu.scss @@ -10,7 +10,7 @@ $border-radius-menu: 2px; position: absolute; white-space: nowrap; background-color: white; - box-shadow: $default-box-shadow; + box-shadow: $box-shadow-layer; border-radius: $border-radius-menu; padding-top: ($spacing-vertical / 5) 0px; z-index: 1; diff --git a/ui/scss/component/_modal.scss b/ui/scss/component/_modal.scss index 13284c7ff..05d5e8de1 100644 --- a/ui/scss/component/_modal.scss +++ b/ui/scss/component/_modal.scss @@ -29,7 +29,7 @@ overflow: auto; border-radius: 4px; padding: $spacing-vertical; - box-shadow: $default-box-shadow; + box-shadow: $box-shadow-layer; max-width: 400px; } diff --git a/ui/scss/component/_tooltip.scss b/ui/scss/component/_tooltip.scss index 9a6ccd7da..0d909d9fb 100644 --- a/ui/scss/component/_tooltip.scss +++ b/ui/scss/component/_tooltip.scss @@ -24,7 +24,7 @@ background-color: $color-bg; font-size: $font-size * 7/8; line-height: $font-line-height; - box-shadow: $default-box-shadow; + box-shadow: $box-shadow-layer; } .tooltip--header .tooltip__link { -- 2.45.2 From 543b912ddb75ad593d7c8eb46482231e39cb19d3 Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Thu, 27 Apr 2017 09:17:18 -0400 Subject: [PATCH 140/158] more cleanup / restoration --- ui/js/app.js | 31 ++---------------- ui/js/component/header.js | 27 +++++++-------- ui/js/page/file-list.js | 60 +++++++++++++++++----------------- ui/js/page/help.js | 4 ++- ui/js/page/rewards.js | 8 +++-- ui/js/page/settings.js | 13 +++++++- ui/js/page/wallet.js | 12 +++++++ ui/scss/_canvas.scss | 23 ++++--------- ui/scss/_gui.scss | 3 +- ui/scss/component/_header.scss | 22 +++++++++---- 10 files changed, 102 insertions(+), 101 deletions(-) diff --git a/ui/js/app.js b/ui/js/app.js index 97ad1bfab..bf0bd4723 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -220,30 +220,6 @@ var App = React.createClass({ errorInfo:
    {errorInfoList}
, }); }, - getHeaderLinks: function() - { - switch(this.state.viewingPage) - { - case 'wallet': - case 'send': - case 'receive': - case 'rewards': - return { - '?wallet': 'Overview', - '?send': 'Send', - '?receive': 'Receive', - '?rewards': 'Rewards', - }; - case 'downloaded': - case 'published': - return { - '?downloaded': 'Downloaded', - '?published': 'Published', - }; - default: - return null; - } - }, getContentAndAddress: function() { switch(this.state.viewingPage) @@ -278,16 +254,15 @@ var App = React.createClass({ } }, render: function() { - let [address, wunderBarIcon, mainContent] = this.getContentAndAddress(), - headerLinks = this.getHeaderLinks(); + let [address, wunderBarIcon, mainContent] = this.getContentAndAddress(); return ( this._fullScreenPages.includes(this.state.viewingPage) ? mainContent :
-
+ onSearch={this.onSearch} viewingPage={this.state.viewingPage} /> +
{mainContent}
{ - this.setState({ balance: balance }); + if (this._isMounted) { + this.setState({balance: balance}); + } }); }, componentWillUnmount: function() { + this._isMounted = false; if (this._balanceSubscribeId) { lbry.balanceUnsubscribe(this._balanceSubscribeId) } }, render: function() { - return
-
- {this.props.links ? - : - ''} -
} }); @@ -93,7 +93,8 @@ let WunderBar = React.createClass({ onFocus: function() { this._stateBeforeSearch = this.state; let newState = { - icon: "icon-search" + icon: "icon-search", + isActive: true } // this._input.value = ""; //trigger placeholder this._focusPending = true; @@ -104,7 +105,7 @@ let WunderBar = React.createClass({ this.setState(newState); }, onBlur: function() { - this.setState(this._stateBeforeSearch); + this.setState(Object.assign({}, this._stateBeforeSearch, { isActive: false })); this._input.value = this.state.address; }, componentDidUpdate: function() { @@ -118,7 +119,7 @@ let WunderBar = React.createClass({ this._input = ref; }, render: function() { - return
+ return
{this.state.icon ? : '' } + ); diff --git a/ui/js/page/file-list.js b/ui/js/page/file-list.js index 3189d61fd..41a2254b5 100644 --- a/ui/js/page/file-list.js +++ b/ui/js/page/file-list.js @@ -3,12 +3,22 @@ import lbry from '../lbry.js'; import lbryuri from '../lbryuri.js'; import {Link} from '../component/link.js'; import {FormField} from '../component/form.js'; +import {SubHeader} from '../component/header.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'; +export let FileListNav = React.createClass({ + render: function() { + return ; + } +}); + export let FileListDownloaded = React.createClass({ _isMounted: false, @@ -37,25 +47,20 @@ export let FileListDownloaded = React.createClass({ this._isMounted = false; }, render: function() { + let content = ""; if (this.state.fileInfos === null) { - return ( -
- -
- ); + content = ; } else if (!this.state.fileInfos.length) { - return ( -
- You haven't downloaded anything from LBRY yet. Go ! -
- ); + content = You haven't downloaded anything from LBRY yet. Go !; } else { - return ( -
- -
- ); + content = ; } + return ( +
+ + {content} +
+ ); } }); @@ -102,27 +107,22 @@ export let FileListPublished = React.createClass({ this._isMounted = false; }, render: function () { + let content = null; if (this.state.fileInfos === null) { - return ( -
- -
- ); + content = ; } else if (!this.state.fileInfos.length) { - return ( -
- You haven't published anything to LBRY yet. Try ! -
- ); + content = You haven't published anything to LBRY yet. Try !; } else { - return ( -
- -
- ); + content = ; } + return ( +
+ + {content} +
+ ); } }); diff --git a/ui/js/page/help.js b/ui/js/page/help.js index 99e6ad0d7..42e65df14 100644 --- a/ui/js/page/help.js +++ b/ui/js/page/help.js @@ -3,6 +3,7 @@ import React from 'react'; import lbry from '../lbry.js'; import {Link} from '../component/link.js'; +import {SettingsNav} from './settings.js'; import {version as uiVersion} from 'json!../../package.json'; var HelpPage = React.createClass({ @@ -49,7 +50,8 @@ var HelpPage = React.createClass({ } return ( -
+
+

Read the FAQ

diff --git a/ui/js/page/rewards.js b/ui/js/page/rewards.js index 18e936aee..c4261804f 100644 --- a/ui/js/page/rewards.js +++ b/ui/js/page/rewards.js @@ -4,6 +4,7 @@ import lbryio from '../lbryio.js'; import {CreditAmount, Icon} from '../component/common.js'; import rewards from '../rewards.js'; import Modal from '../component/modal.js'; +import {WalletNav} from './wallet.js'; import {RewardLink} from '../component/link.js'; const RewardTile = React.createClass({ @@ -56,14 +57,15 @@ var RewardsPage = React.createClass({ }, render: function() { return ( -
-
+
+ +
{!this.state.userRewards ? (this.state.failed ?
Failed to load rewards.
: '') : this.state.userRewards.map(({RewardType, RewardTitle, RewardDescription, TransactionID, RewardAmount}) => { return ; })} - +
); } diff --git a/ui/js/page/settings.js b/ui/js/page/settings.js index 6cb062099..abc65271d 100644 --- a/ui/js/page/settings.js +++ b/ui/js/page/settings.js @@ -1,7 +1,17 @@ import React from 'react'; import {FormField, FormRow} from '../component/form.js'; +import {SubHeader} from '../component/header.js'; import lbry from '../lbry.js'; +export let SettingsNav = React.createClass({ + render: function() { + return ; + } +}); + var SettingsPage = React.createClass({ _onSettingSaveSuccess: function() { // This is bad. @@ -92,7 +102,8 @@ var SettingsPage = React.createClass({
*/ return ( -
+
+

Download Directory

diff --git a/ui/js/page/wallet.js b/ui/js/page/wallet.js index 4c3a52d0e..b3c1b458b 100644 --- a/ui/js/page/wallet.js +++ b/ui/js/page/wallet.js @@ -2,6 +2,7 @@ import React from 'react'; import lbry from '../lbry.js'; import {Link} from '../component/link.js'; import Modal from '../component/modal.js'; +import {SubHeader} from '../component/header.js'; import {FormField, FormRow} from '../component/form.js'; import {Address, BusyMessage, CreditAmount} from '../component/common.js'; @@ -263,6 +264,16 @@ var TransactionList = React.createClass({ } }); +export let WalletNav = React.createClass({ + render: function() { + return ; + } +}); var WalletPage = React.createClass({ _balanceSubscribeId: null, @@ -294,6 +305,7 @@ var WalletPage = React.createClass({ render: function() { return (
+

Balance

diff --git a/ui/scss/_canvas.scss b/ui/scss/_canvas.scss index ce8f8e01c..21237c939 100644 --- a/ui/scss/_canvas.scss +++ b/ui/scss/_canvas.scss @@ -33,23 +33,12 @@ body #main-content { - &.no-sub-nav + padding: $spacing-vertical; + margin-top: $height-header; + main.constrained-page { - min-height: calc(100vh - 60px); //should be -$height-header, but I'm dumb I guess? It wouldn't work - main { margin-top: $height-header; } - } - &.with-sub-nav - { - min-height: calc(100vh - 60px); //should be -$height-header, but I'm dumb I guess? It wouldn't work - } - main - { - padding: $spacing-vertical; - &.constrained-page - { - max-width: $width-page-constrained; - margin-left: auto; - margin-right: auto; - } + max-width: $width-page-constrained; + margin-left: auto; + margin-right: auto; } } \ No newline at end of file diff --git a/ui/scss/_gui.scss b/ui/scss/_gui.scss index f875a6940..c514f0712 100644 --- a/ui/scss/_gui.scss +++ b/ui/scss/_gui.scss @@ -126,10 +126,11 @@ p .sort-section { display: block; - margin-bottom: 5px; + margin-bottom: $spacing-vertical * 2/3; text-align: right; + line-height: 1; font-size: 0.85em; color: $color-help; } diff --git a/ui/scss/component/_header.scss b/ui/scss/component/_header.scss index 5a550e771..15e15d1ee 100644 --- a/ui/scss/component/_header.scss +++ b/ui/scss/component/_header.scss @@ -1,7 +1,7 @@ @import "../global"; $color-header: #666; - +$color-header-active: darken($color-header, 20%); $header-icon-size: 1.5em; @@ -52,6 +52,9 @@ $header-icon-size: 1.5em; top: $spacing-vertical / 2 - 4px; //hacked } } + +.wunderbar--active .icon-search { color: $color-primary; } + .wunderbar__input { background: rgba(255, 255, 255, 0.7); width: 100%; @@ -64,15 +67,21 @@ $header-icon-size: 1.5em; @include border-radius(2px); border: 1px solid #ccc; &:focus { + color: $color-header-active; box-shadow: $box-shadow-focus; - border-color: $color-header; + border-color: $color-primary; } } nav.sub-header { text-transform: uppercase; - padding: $spacing-vertical / 2; + padding: 0 0 $spacing-vertical; + &.sub-header--constrained { + max-width: $width-page-constrained; + margin-left: auto; + margin-right: auto; + } > a { $sub-header-selected-underline-height: 2px; @@ -89,15 +98,14 @@ nav.sub-header { margin-right: 0; } - $color-selected-subheader: darken($color-header, 20%); &.sub-header-selected { - border-bottom: $sub-header-selected-underline-height solid $color-selected-subheader; - color: $color-selected-subheader; + border-bottom: $sub-header-selected-underline-height solid $color-header-active; + color: $color-header-active; } &:hover { - color: $color-selected-subheader; + color: $color-header-active; } } } \ No newline at end of file -- 2.45.2 From e42252507bb2307f8211fb5d4ce993a2da1a277f Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Sun, 30 Apr 2017 04:06:50 -0400 Subject: [PATCH 141/158] Minor style fixes --- ui/js/component/header.js | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/ui/js/component/header.js b/ui/js/component/header.js index aadcf7cd2..b05246036 100644 --- a/ui/js/component/header.js +++ b/ui/js/component/header.js @@ -98,9 +98,9 @@ let WunderBar = React.createClass({ } // this._input.value = ""; //trigger placeholder this._focusPending = true; - if (!this.state.address.match(/^lbry:\/\//)) //onFocus, if they are not on an exact URL, clear the bar + if (!this.state.address.startsWith('lbry://')) //onFocus, if they are not on an exact URL, clear the bar { - newState.address = ""; + newState.address = ''; } this.setState(newState); }, @@ -119,16 +119,18 @@ let WunderBar = React.createClass({ this._input = ref; }, render: function() { - return
- {this.state.icon ? : '' } - + return ( +
+ {this.state.icon ? : '' } +
+ ); } }) -- 2.45.2 From a82ee8a2d584a5773ef936e6c9f204eca99ead51 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Sun, 30 Apr 2017 04:07:35 -0400 Subject: [PATCH 142/158] Fix refreshing behavior on new input in WunderBar Before, would compare the address inside it to the page address passed in by
to determine if there was a page change. This would cause user input to be replaced with "Home" when typing a search, since during a search the page address is always "Home," not the query. Now it simply checks if the page being viewed has changed. --- ui/js/component/header.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/js/component/header.js b/ui/js/component/header.js index b05246036..464b11f9a 100644 --- a/ui/js/component/header.js +++ b/ui/js/component/header.js @@ -34,7 +34,7 @@ var Header = React.createClass({
- +
@@ -86,7 +86,7 @@ let WunderBar = React.createClass({ }, componentWillReceiveProps(nextProps) { - if (nextProps.address !== this.state.address || nextProps.icon !== this.state.icon) { + if (nextProps.viewingPage !== this.props.viewingPage) { this.setState({ address: nextProps.address, icon: nextProps.icon }); } }, -- 2.45.2 From 0b8d9e3d826f4e02d4201954a6532c1ed7e09f12 Mon Sep 17 00:00:00 2001 From: Alex Liebowitz Date: Sun, 30 Apr 2017 23:33:53 -0400 Subject: [PATCH 143/158] Simplify state management of viewing page Explicitly set the page being viewed to "discover" instead of just rendering the discover page if none is set. Eliminates several real or possible bugs and edge cases. --- ui/js/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/js/app.js b/ui/js/app.js index bf0bd4723..4a11a858b 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -81,6 +81,7 @@ var App = React.createClass({ drawerOpenRaw = sessionStorage.getItem('drawerOpen'); return Object.assign(this.getViewingPageAndArgs(window.location.search), { + viewingPage: 'discover', drawerOpen: drawerOpenRaw !== null ? JSON.parse(drawerOpenRaw) : true, errorInfo: null, modal: null, @@ -249,7 +250,6 @@ var App = React.createClass({ case 'developer': return ["Developer", "icon-file", ]; case 'discover': - default: return ["Home", "icon-home", ]; } }, -- 2.45.2 From da538a7a239c63cfb5b2237549db2b660902e46b Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Sun, 30 Apr 2017 20:15:21 -0400 Subject: [PATCH 144/158] many bug fixes, working back button, progress towards working search --- ui/js/app.js | 64 +++++++------ ui/js/component/common.js | 19 +--- ui/js/component/drawer.js | 67 ------------- ui/js/component/header.js | 16 ++-- ui/js/component/load_screen.js | 6 -- ui/js/lbry.js | 19 +++- ui/js/lbryio.js | 14 --- ui/js/page/discover.js | 165 +++----------------------------- ui/js/page/file-list.js | 7 +- ui/js/page/help.js | 5 +- ui/js/page/publish.js | 6 +- ui/js/page/report.js | 5 +- ui/js/page/rewards.js | 2 +- ui/js/page/search.js | 139 +++++++++++++++++++++++++++ ui/js/page/settings.js | 5 +- ui/js/page/show.js | 4 +- ui/js/page/start.js | 2 +- ui/js/page/wallet.js | 2 +- ui/package.json | 1 - ui/scss/_canvas.scss | 13 ++- ui/scss/_gui.scss | 5 +- ui/scss/component/_card.scss | 6 +- ui/scss/component/_header.scss | 17 ---- ui/scss/component/_tooltip.scss | 1 + 24 files changed, 247 insertions(+), 343 deletions(-) delete mode 100644 ui/js/component/drawer.js create mode 100644 ui/js/page/search.js diff --git a/ui/js/app.js b/ui/js/app.js index 4a11a858b..a416a6e80 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -12,10 +12,10 @@ 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 SearchPage from './page/search.js'; import DiscoverPage from './page/discover.js'; import DeveloperPage from './page/developer.js'; import {FileListDownloaded, FileListPublished} from './page/file-list.js'; -import Drawer from './component/drawer.js'; import Header from './component/header.js'; import {Modal, ExpandableModal} from './component/modal.js'; import {Link} from './component/link.js'; @@ -38,6 +38,7 @@ var App = React.createClass({ data: 'Error data', }, _fullScreenPages: ['watch'], + _storeHistoryOfNextRender: false, _upgradeDownloadItem: null, _isMounted: false, @@ -71,18 +72,17 @@ var App = React.createClass({ getViewingPageAndArgs: function(address) { // For now, routes are in format ?page or ?page=args let [isMatch, viewingPage, pageArgs] = address.match(/\??([^=]*)(?:=(.*))?/); + console.log(pageArgs); + console.log(decodeURIComponent(pageArgs)); return { viewingPage: viewingPage, - pageArgs: pageArgs === undefined ? null : pageArgs + pageArgs: pageArgs === undefined ? null : decodeURIComponent(pageArgs) }; }, getInitialState: function() { - var match, param, val, viewingPage, pageArgs, - drawerOpenRaw = sessionStorage.getItem('drawerOpen'); - return Object.assign(this.getViewingPageAndArgs(window.location.search), { viewingPage: 'discover', - drawerOpen: drawerOpenRaw !== null ? JSON.parse(drawerOpenRaw) : true, + appUrl: null, errorInfo: null, modal: null, downloadProgress: null, @@ -90,6 +90,8 @@ var App = React.createClass({ }); }, componentWillMount: function() { + window.addEventListener("popstate", this.onHistoryPop); + document.addEventListener('unhandledError', (event) => { this.alertError(event.detail); }); @@ -106,9 +108,9 @@ 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'))); + let appUrl = target.getAttribute('href'); + this._storeHistoryOfNextRender = true; + this.setState(Object.assign({}, this.getViewingPageAndArgs(appUrl), { appUrl: appUrl })); } } target = target.parentNode; @@ -126,14 +128,6 @@ var App = React.createClass({ }); } }, - openDrawer: function() { - sessionStorage.setItem('drawerOpen', true); - this.setState({ drawerOpen: true }); - }, - closeDrawer: function() { - sessionStorage.setItem('drawerOpen', false); - this.setState({ drawerOpen: false }); - }, closeModal: function() { this.setState({ modal: null, @@ -144,10 +138,17 @@ var App = React.createClass({ }, componentWillUnmount: function() { this._isMounted = false; + window.removeEventListener("popstate", this.onHistoryPop); }, - registerHistoryPop: function() { - window.addEventListener("popstate", () => { - this.setState(this.getViewingPageAndArgs(location.pathname)); + onHistoryPop: function() { + this.setState(this.getViewingPageAndArgs(location.search)); + }, + onSearch: function(term) { + this._storeHistoryOfNextRender = true; + this.setState({ + viewingPage: "search", + appUrl: "?search=" + encodeURIComponent(term), + pageArgs: term }); }, handleUpgradeClicked: function() { @@ -202,12 +203,6 @@ var App = React.createClass({ modal: null, }); }, - onSearch: function(term) { - this.setState({ - viewingPage: 'discover', - pageArgs: term - }); - }, alertError: function(error) { var errorInfoList = []; for (let key of Object.keys(error)) { @@ -225,12 +220,14 @@ var App = React.createClass({ { switch(this.state.viewingPage) { + case 'search': + return [this.state.pageArgs ? this.state.pageArgs : "Search", 'icon-search', ]; case 'settings': return ["Settings", "icon-gear", ]; case 'help': return ["Help", "icon-question", ]; case 'report': - return ['Report', 'icon-file', ]; + return ['Report an Issue', 'icon-file', ]; case 'downloaded': return ["Downloads & Purchases", "icon-folder", ]; case 'published': @@ -250,18 +247,25 @@ var App = React.createClass({ case 'developer': return ["Developer", "icon-file", ]; case 'discover': - return ["Home", "icon-home", ]; + default: + return ["Home", "icon-home", ]; } }, render: function() { let [address, wunderBarIcon, mainContent] = this.getContentAndAddress(); + lbry.setTitle(address); + + if (this._storeHistoryOfNextRender) { + this._storeHistoryOfNextRender = false; + history.pushState({}, document.title, this.state.appUrl); + } + return ( this._fullScreenPages.includes(this.state.viewingPage) ? mainContent :
-
+
{mainContent}
diff --git a/ui/js/component/common.js b/ui/js/component/common.js index 7139e5dfe..8da20ca8e 100644 --- a/ui/js/component/common.js +++ b/ui/js/component/common.js @@ -1,6 +1,5 @@ import React from 'react'; import lbry from '../lbry.js'; -import $clamp from 'clamp-js-main'; //component/icon.js export let Icon = React.createClass({ @@ -19,29 +18,15 @@ export let Icon = React.createClass({ export let TruncatedText = React.createClass({ propTypes: { - lines: React.PropTypes.number, - height: React.PropTypes.string, - auto: React.PropTypes.bool, + lines: React.PropTypes.number }, getDefaultProps: function() { return { lines: null, - height: null, - auto: true, } }, - componentDidMount: function() { - // Manually round up the line height, because clamp.js doesn't like fractional-pixel line heights. - - // Need to work directly on the style object because setting the style prop doesn't update internal styles right away. - this.refs.span.style.lineHeight = Math.ceil(parseFloat(getComputedStyle(this.refs.span).lineHeight)) + 'px'; - - $clamp(this.refs.span, { - clamp: this.props.lines || this.props.height || 'auto', - }); - }, render: function() { - return {this.props.children}; + return {this.props.children}; } }); diff --git a/ui/js/component/drawer.js b/ui/js/component/drawer.js deleted file mode 100644 index b19e901f5..000000000 --- a/ui/js/component/drawer.js +++ /dev/null @@ -1,67 +0,0 @@ -import lbry from '../lbry.js'; -import React from 'react'; -import {Link} from './link.js'; - -var DrawerItem = React.createClass({ - getDefaultProps: function() { - return { - subPages: [], - }; - }, - render: function() { - var isSelected = (this.props.viewingPage == this.props.href.substr(1) || - this.props.subPages.indexOf(this.props.viewingPage) != -1); - return - } -}); - -var drawerImageStyle = { //@TODO: remove this, img should be properly scaled once size is settled - height: '36px' -}; - -var Drawer = React.createClass({ - _balanceSubscribeId: null, - - handleLogoClicked: function(event) { - if ((event.ctrlKey || event.metaKey) && event.shiftKey) { - window.location.href = '?developer' - event.preventDefault(); - } - }, - getInitialState: function() { - return { - balance: 0, - }; - }, - componentDidMount: function() { - this._balanceSubscribeId = lbry.balanceSubscribe((balance) => { - this.setState({ - balance: balance - }); - }); - }, - componentWillUnmount: function() { - if (this._balanceSubscribeId) { - lbry.balanceUnsubscribe(this._balanceSubscribeId) - } - }, - render: function() { - return ( - - ); - } -}); - - -export default Drawer; diff --git a/ui/js/component/header.js b/ui/js/component/header.js index 464b11f9a..308cd633b 100644 --- a/ui/js/component/header.js +++ b/ui/js/component/header.js @@ -28,13 +28,14 @@ var Header = React.createClass({ render: function() { return
); diff --git a/ui/js/lbry.js b/ui/js/lbry.js index c13635a09..356887483 100644 --- a/ui/js/lbry.js +++ b/ui/js/lbry.js @@ -31,7 +31,6 @@ function savePendingPublish({name, channel_name}) { return newPendingPublish; } - /** * If there is a pending publish with the given name or outpoint, remove it. * A channel name may also be provided along with name. @@ -132,6 +131,24 @@ lbry.connect = function() { return lbry._connectPromise; } + +//kill this but still better than document.title =, which this replaced +lbry.setTitle = function(title) { + document.title = title + " - LBRY"; +} + +//kill this with proper routing +lbry.back = function() { + console.log(window.history); + if (window.history.length > 1) { + console.log('history exists, go back'); + window.history.back(); + } else { + console.log('no history, reload'); + window.location.href = "?discover"; + } +} + lbry.isDaemonAcceptingConnections = function (callback) { // Returns true/false whether the daemon is at a point it will start returning status lbry.call('status', {}, () => callback(true), null, () => callback(false)) diff --git a/ui/js/lbryio.js b/ui/js/lbryio.js index 7a1ab58c2..f37cccf62 100644 --- a/ui/js/lbryio.js +++ b/ui/js/lbryio.js @@ -151,20 +151,6 @@ lbryio.authenticate = function() { } 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); }); } diff --git a/ui/js/page/discover.js b/ui/js/page/discover.js index 3c807e831..d522a99f8 100644 --- a/ui/js/page/discover.js +++ b/ui/js/page/discover.js @@ -1,79 +1,18 @@ import React from 'react'; -import lbry from '../lbry.js'; import lbryio from '../lbryio.js'; -import lbryuri from '../lbryuri.js'; -import lighthouse from '../lighthouse.js'; import {FileTile, FileTileStream} from '../component/file-tile.js'; -import {Link} from '../component/link.js'; import {ToolTip} from '../component/tooltip.js'; -import {BusyMessage} from '../component/common.js'; - -var fetchResultsStyle = { - color: '#888', - textAlign: 'center', - fontSize: '1.2em' - }; - -var SearchActive = React.createClass({ - render: function() { - return ( -
- -
- ); - } -}); - -var searchNoResultsStyle = { - textAlign: 'center' -}, searchNoResultsMessageStyle = { - fontStyle: 'italic', - marginRight: '5px' -}; - -var SearchNoResults = React.createClass({ - render: function() { - return ( -
- No one has checked anything in for {this.props.query} yet. - -
- ); - } -}); - -var SearchResults = React.createClass({ - render: function() { - var rows = [], - seenNames = {}; //fix this when the search API returns claim IDs - - for (let {name, claim, claim_id, channel_name, channel_id, txid, nout} of this.props.results) { - const uri = lbryuri.build({ - channelName: channel_name, - contentName: name, - claimId: channel_id || claim_id, - }); - - rows.push( - - ); - } - return ( -
{rows}
- ); - } -}); 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 ' + + 'rest of the LBRY community. Bid on the names "one," "two," "three," "four" and ' + '"five" to put your content here!'); -var FeaturedCategory = React.createClass({ +let FeaturedCategory = React.createClass({ render: function() { return (
{ this.props.category ?

{this.props.category} - { this.props.category == "community" ? + { this.props.category.match(/^community/i) ? : '' }

: '' } @@ -82,7 +21,7 @@ var FeaturedCategory = React.createClass({ } }) -var FeaturedContent = React.createClass({ +let DiscoverPage = React.createClass({ getInitialState: function() { return { featuredUris: {}, @@ -105,101 +44,19 @@ var FeaturedContent = React.createClass({ }); }, render: function() { - return ( + return
{ this.state.failed ?
Failed to load landing content.
:
{ - Object.keys(this.state.featuredUris).map((category) => { - return this.state.featuredUris[category].length ? - : - ''; - }) + Object.keys(this.state.featuredUris).map((category) => { + return this.state.featuredUris[category].length ? + : + ''; + }) }
- ); - } -}); - -var DiscoverPage = React.createClass({ - userTypingTimer: null, - - propTypes: { - showWelcome: React.PropTypes.bool.isRequired, - }, - - componentDidUpdate: function() { - if (this.props.query != this.state.query) - { - this.handleSearchChanged(this.props.query); - } - }, - - getDefaultProps: function() { - return { - showWelcome: false, - } - }, - - componentWillReceiveProps: function(nextProps, nextState) { - if (nextProps.query != nextState.query) - { - this.handleSearchChanged(nextProps.query); - } - }, - - handleSearchChanged: function(query) { - this.setState({ - searching: true, - query: query, - }); - - lighthouse.search(query).then(this.searchCallback); - }, - - handleWelcomeDone: function() { - this.setState({ - welcomeComplete: true, - }); - }, - - componentWillMount: function() { - document.title = "Home"; - - if (this.props.query) { - // Rendering with a query already typed - this.handleSearchChanged(this.props.query); - } - }, - - getInitialState: function() { - return { - welcomeComplete: false, - results: [], - query: this.props.query, - searching: ('query' in this.props) && (this.props.query.length > 0) - }; - }, - - searchCallback: function(results) { - if (this.state.searching) //could have canceled while results were pending, in which case nothing to do - { - this.setState({ - results: results, - searching: false //multiple searches can be out, we're only done if we receive one we actually care about - }); - } - }, - - render: function() { - return ( -
- { this.state.searching ? : null } - { !this.state.searching && this.props.query && this.state.results.length ? : null } - { !this.state.searching && this.props.query && !this.state.results.length ? : null } - { !this.props.query && !this.state.searching ? : null } -
- ); + }
; } }); diff --git a/ui/js/page/file-list.js b/ui/js/page/file-list.js index 41a2254b5..71f8e2fc2 100644 --- a/ui/js/page/file-list.js +++ b/ui/js/page/file-list.js @@ -56,7 +56,7 @@ export let FileListDownloaded = React.createClass({ content = ; } return ( -
+
{content}
@@ -83,12 +83,11 @@ export let FileListPublished = React.createClass({ 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) => { if (!this._isMounted) { return; } @@ -118,7 +117,7 @@ export let FileListPublished = React.createClass({ content = ; } return ( -
+
{content}
diff --git a/ui/js/page/help.js b/ui/js/page/help.js index 42e65df14..d6a28ae99 100644 --- a/ui/js/page/help.js +++ b/ui/js/page/help.js @@ -25,9 +25,6 @@ var HelpPage = React.createClass({ }); }); }, - componentDidMount: function() { - document.title = "Help"; - }, render: function() { let ver, osName, platform, newVerLink; if (this.state.versionInfo) { @@ -50,7 +47,7 @@ var HelpPage = React.createClass({ } return ( -
+
diff --git a/ui/js/page/publish.js b/ui/js/page/publish.js index dc34ea500..03583136b 100644 --- a/ui/js/page/publish.js +++ b/ui/js/page/publish.js @@ -148,7 +148,7 @@ var PublishPage = React.createClass({ }); }, handlePublishStartedConfirmed: function() { - window.location = "?published"; + window.location.href = "?published"; }, handlePublishError: function(error) { this.setState({ @@ -384,7 +384,7 @@ var PublishPage = React.createClass({ const lbcInputHelp = "This LBC remains yours and the deposit can be undone at any time." return ( -
+
@@ -548,7 +548,7 @@ var PublishPage = React.createClass({
- +
diff --git a/ui/js/page/report.js b/ui/js/page/report.js index 47a4d2a7a..e76905d4b 100644 --- a/ui/js/page/report.js +++ b/ui/js/page/report.js @@ -18,9 +18,6 @@ var ReportPage = React.createClass({ this._messageArea.value = ''; } }, - componentDidMount: function() { - document.title = "Report an Issue"; - }, closeModal: function() { this.setState({ modal: null, @@ -34,7 +31,7 @@ var ReportPage = React.createClass({ }, render: function() { return ( -
+

Report an Issue

Please describe the problem you experienced and any information you think might be useful to us. Links to screenshots are great!

diff --git a/ui/js/page/rewards.js b/ui/js/page/rewards.js index c4261804f..3462517c9 100644 --- a/ui/js/page/rewards.js +++ b/ui/js/page/rewards.js @@ -57,7 +57,7 @@ var RewardsPage = React.createClass({ }, render: function() { return ( -
+
{!this.state.userRewards diff --git a/ui/js/page/search.js b/ui/js/page/search.js new file mode 100644 index 000000000..3df3dd07d --- /dev/null +++ b/ui/js/page/search.js @@ -0,0 +1,139 @@ +import React from 'react'; +import lbry from '../lbry.js'; +import lbryio from '../lbryio.js'; +import lbryuri from '../lbryuri.js'; +import lighthouse from '../lighthouse.js'; +import {FileTile, FileTileStream} from '../component/file-tile.js'; +import {Link} from '../component/link.js'; +import {ToolTip} from '../component/tooltip.js'; +import {BusyMessage} from '../component/common.js'; + +var SearchNoResults = React.createClass({ + render: function() { + return
+ + No one has checked anything in for {this.props.query} yet. + + +
; + } +}); + +var SearchResultList = React.createClass({ + render: function() { + var rows = [], + seenNames = {}; //fix this when the search API returns claim IDs + + for (let {name, claim, claim_id, channel_name, channel_id, txid, nout} of this.props.results) { + const uri = lbryuri.build({ + channelName: channel_name, + contentName: name, + claimId: channel_id || claim_id, + }); + + rows.push( + + ); + } + return ( +
{rows}
+ ); + } +}); + +let SearchResults = React.createClass({ + propTypes: { + query: React.PropTypes.string.isRequired + }, + + _isMounted: false, + + componentWillMount: function () { + this._isMounted = true; + lighthouse.search(this.props.query).then(this.searchCallback); + }, + + componentWillUnmount: function () { + this._isMounted = false; + }, + + getInitialState: function () { + return { + results: [], + searching: true + }; + }, + + searchCallback: function (results) { + if (this._isMounted) //could have canceled while results were pending, in which case nothing to do + { + this.setState({ + results: results, + searching: false //multiple searches can be out, we're only done if we receive one we actually care about + }); + } + }, + + render: function () { + return this.state.searching ? + : + (this.state.results.length ? + : + ); + } +}); + +let SearchPage = React.createClass({ + + _isMounted: false, + + propTypes: { + query: React.PropTypes.string.isRequired + }, + + isValidUri: function(query) { + return true; + }, + + componentWillMount: function() { + this._isMounted = true; + lighthouse.search(this.props.query).then(this.searchCallback); + }, + + componentWillUnmount: function() { + this._isMounted = false; + }, + + getInitialState: function() { + return { + results: [], + searching: true + }; + }, + + searchCallback: function(results) { + if (this._isMounted) //could have canceled while results were pending, in which case nothing to do + { + this.setState({ + results: results, + searching: false //multiple searches can be out, we're only done if we receive one we actually care about + }); + } + }, + + render: function() { + return ( +
+ { this.isValidUri(this.props.query) ? +
+

lbry://{this.props.query}

+
+
: '' } +

Search

+ +
+ ); + } +}); + +export default SearchPage; diff --git a/ui/js/page/settings.js b/ui/js/page/settings.js index abc65271d..cbc0765a8 100644 --- a/ui/js/page/settings.js +++ b/ui/js/page/settings.js @@ -66,9 +66,6 @@ var SettingsPage = React.createClass({ showUnavailable: lbry.getClientSetting('showUnavailable'), } }, - componentDidMount: function() { - document.title = "Settings"; - }, componentWillMount: function() { lbry.getDaemonSettings((settings) => { this.setState({ @@ -102,7 +99,7 @@ var SettingsPage = React.createClass({
*/ return ( -
+
diff --git a/ui/js/page/show.js b/ui/js/page/show.js index f41039868..8622a814d 100644 --- a/ui/js/page/show.js +++ b/ui/js/page/show.js @@ -70,7 +70,7 @@ let ShowPage = React.createClass({ }); }); - document.title = metadata.title ? metadata.title : this._uri; + lbry.setTitle(metadata.title ? metadata.title : this._uri) this.setState({ outpoint: outpoint, @@ -94,7 +94,7 @@ let ShowPage = React.createClass({ const title = metadata ? this.state.metadata.title : this._uri; return ( -
+
{ this.state.contentType && this.state.contentType.startsWith('video/') ?