make page changes fast #25

Merged
kauffj merged 3 commits from fast_pages into master 2017-03-27 15:46:54 +02:00
12 changed files with 111 additions and 41 deletions

View file

@ -8,6 +8,10 @@ Web UI version numbers should always match the corresponding version of LBRY App
## [Unreleased] ## [Unreleased]
### Added ### Added
* The app is much more responsive switching pages. It no longer reloads the entire page and all assets on each page change.
* lbry.js now offers a subscription model for wallet balance similar to file info.
* Fixed file info subscribes not being unsubscribed in unmount.
* Fixed drawer not highlighting selected page.
* You can now make API calls directly on the lbry module, e.g. lbry.peer_list() * You can now make API calls directly on the lbry module, e.g. lbry.peer_list()
* New-style API calls return promises instead of using callbacks * New-style API calls return promises instead of using callbacks
* Wherever possible, use outpoints for unique IDs instead of names or SD hashes * Wherever possible, use outpoints for unique IDs instead of names or SD hashes

View file

@ -61,7 +61,7 @@ function getPidsForProcessName(name) {
} }
function createWindow () { function createWindow () {
win = new BrowserWindow({backgroundColor: '#155b4a'}) win = new BrowserWindow({backgroundColor: '#155B4A'}) //$color-primary
win.maximize() win.maximize()
//win.webContents.openDevTools() //win.webContents.openDevTools()
win.loadURL(`file://${__dirname}/dist/index.html`) win.loadURL(`file://${__dirname}/dist/index.html`)

View file

@ -40,8 +40,15 @@ var App = React.createClass({
}, },
_upgradeDownloadItem: null, _upgradeDownloadItem: null,
_isMounted: false,
_version: null, _version: null,
// Temporary workaround since electron-dl throws errors when you try to get the filename
getDefaultProps: function() {
return {
address: window.location.search
};
},
getUpdateUrl: function() { getUpdateUrl: function() {
switch (process.platform) { switch (process.platform) {
case 'darwin': case 'darwin':
@ -68,31 +75,33 @@ var App = React.createClass({
throw 'Unknown platform'; throw 'Unknown platform';
} }
}, },
getInitialState: function() { getViewingPageAndArgs: function(address) {
// For now, routes are in format ?page or ?page=args // For now, routes are in format ?page or ?page=args
var match, param, val, viewingPage, let [isMatch, viewingPage, pageArgs] = address.match(/\??([^=]*)(?:=(.*))?/);
drawerOpenRaw = sessionStorage.getItem('drawerOpen');
[match, viewingPage, val] = window.location.search.match(/\??([^=]*)(?:=(.*))?/);
return { return {
viewingPage: viewingPage, viewingPage: viewingPage,
pageArgs: pageArgs === undefined ? null : pageArgs
};
},
getInitialState: function() {
var match, param, val, viewingPage, pageArgs,
drawerOpenRaw = sessionStorage.getItem('drawerOpen');
return Object.assign(this.getViewingPageAndArgs(this.props.address), {
drawerOpen: drawerOpenRaw !== null ? JSON.parse(drawerOpenRaw) : true, drawerOpen: drawerOpenRaw !== null ? JSON.parse(drawerOpenRaw) : true,
pageArgs: typeof val !== 'undefined' ? val : null,
errorInfo: null, errorInfo: null,
modal: null, modal: null,
downloadProgress: null, downloadProgress: null,
downloadComplete: false, downloadComplete: false,
}; });
}, },
componentWillMount: function() { componentWillMount: function() {
document.addEventListener('unhandledError', (event) => { document.addEventListener('unhandledError', (event) => {
this.alertError(event.detail); this.alertError(event.detail);
}); });
//open links in external browser //open links in external browser and skip full redraw on changing page
document.addEventListener('click', function(event) { document.addEventListener('click', (event) => {
var target = event.target; var target = event.target;
while (target && target !== document) { while (target && target !== document) {
if (target.matches('a[href^="http"]')) { if (target.matches('a[href^="http"]')) {
@ -100,6 +109,12 @@ var App = React.createClass({
shell.openExternal(target.href); shell.openExternal(target.href);
return; return;
} }
if (target.matches('a[href^="?"]')) {
event.preventDefault();
if (this._isMounted) {
this.setState(this.getViewingPageAndArgs(target.getAttribute('href')));
}
}
target = target.parentNode; target = target.parentNode;
} }
}); });
@ -132,6 +147,12 @@ var App = React.createClass({
modal: null, modal: null,
}); });
}, },
componentDidMount: function() {
this._isMounted = true;
},
componentWillUnmount: function() {
this._isMounted = false;
},
handleUpgradeClicked: function() { handleUpgradeClicked: function() {
// Make a new directory within temp directory so the filename is guaranteed to be available // Make a new directory within temp directory so the filename is guaranteed to be available
const dir = fs.mkdtempSync(app.getPath('temp') + require('path').sep); const dir = fs.mkdtempSync(app.getPath('temp') + require('path').sep);

View file

@ -9,7 +9,7 @@ var DrawerItem = React.createClass({
}; };
}, },
render: function() { render: function() {
var isSelected = (this.props.viewingPage == this.props.href.substr(2) || var isSelected = (this.props.viewingPage == this.props.href.substr(1) ||
this.props.subPages.indexOf(this.props.viewingPage) != -1); this.props.subPages.indexOf(this.props.viewingPage) != -1);
return <Link {...this.props} className={ 'drawer-item ' + (isSelected ? 'drawer-item-selected' : '') } /> return <Link {...this.props} className={ 'drawer-item ' + (isSelected ? 'drawer-item-selected' : '') } />
} }
@ -20,9 +20,11 @@ var drawerImageStyle = { //@TODO: remove this, img should be properly scaled onc
}; };
var Drawer = React.createClass({ var Drawer = React.createClass({
_balanceSubscribeId: null,
handleLogoClicked: function(event) { handleLogoClicked: function(event) {
if ((event.ctrlKey || event.metaKey) && event.shiftKey) { if ((event.ctrlKey || event.metaKey) && event.shiftKey) {
window.location.href = 'index.html?developer' window.location.href = '?developer'
event.preventDefault(); event.preventDefault();
} }
}, },
@ -32,25 +34,30 @@ var Drawer = React.createClass({
}; };
}, },
componentDidMount: function() { componentDidMount: function() {
lbry.getBalance(function(balance) { this._balanceSubscribeId = lbry.balanceSubscribe(function(balance) {
this.setState({ this.setState({
balance: balance balance: balance
}); });
}.bind(this)); }.bind(this));
}, },
componentWillUnmount: function() {
if (this._balanceSubscribeId) {
lbry.balanceUnsubscribe(this._balanceSubscribeId)
}
},
render: function() { render: function() {
return ( return (
<nav id="drawer"> <nav id="drawer">
<div id="drawer-handle"> <div id="drawer-handle">
<Link title="Close" onClick={this.props.onCloseDrawer} icon="icon-bars" className="close-drawer-link"/> <Link title="Close" onClick={this.props.onCloseDrawer} icon="icon-bars" className="close-drawer-link"/>
<a href="index.html?discover" onMouseUp={this.handleLogoClicked}><img src={lbry.imagePath("lbry-dark-1600x528.png")} style={drawerImageStyle}/></a> <a href="?discover" onMouseUp={this.handleLogoClicked}><img src={lbry.imagePath("lbry-dark-1600x528.png")} style={drawerImageStyle}/></a>
</div> </div>
<DrawerItem href='index.html?discover' viewingPage={this.props.viewingPage} label="Discover" icon="icon-search" /> <DrawerItem href='?discover' viewingPage={this.props.viewingPage} label="Discover" icon="icon-search" />
<DrawerItem href='index.html?publish' viewingPage={this.props.viewingPage} label="Publish" icon="icon-upload" /> <DrawerItem href='?publish' viewingPage={this.props.viewingPage} label="Publish" icon="icon-upload" />
<DrawerItem href='index.html?downloaded' subPages={['published']} viewingPage={this.props.viewingPage} label="My Files" icon='icon-cloud-download' /> <DrawerItem href='?downloaded' subPages={['published']} viewingPage={this.props.viewingPage} label="My Files" icon='icon-cloud-download' />
<DrawerItem href="index.html?wallet" subPages={['send', 'receive', 'claim', 'referral']} viewingPage={this.props.viewingPage} label="My Wallet" badge={lbry.formatCredits(this.state.balance) } icon="icon-bank" /> <DrawerItem href="?wallet" subPages={['send', 'receive', 'claim', 'referral']} viewingPage={this.props.viewingPage} label="My Wallet" badge={lbry.formatCredits(this.state.balance) } icon="icon-bank" />
<DrawerItem href='index.html?settings' viewingPage={this.props.viewingPage} label="Settings" icon='icon-gear' /> <DrawerItem href='?settings' viewingPage={this.props.viewingPage} label="Settings" icon='icon-gear' />
<DrawerItem href='index.html?help' viewingPage={this.props.viewingPage} label="Help" icon='icon-question-circle' /> <DrawerItem href='?help' viewingPage={this.props.viewingPage} label="Help" icon='icon-question-circle' />
</nav> </nav>
); );
} }

View file

@ -253,7 +253,7 @@ export let FileActions = React.createClass({
if (this.isMounted) { if (this.isMounted) {
this.setState({ this.setState({
fileInfo: fileInfo, fileInfo: fileInfo,
}); });
} }
}, },
componentDidMount: function() { componentDidMount: function() {
@ -276,6 +276,9 @@ export let FileActions = React.createClass({
}, },
componentWillUnmount: function() { componentWillUnmount: function() {
this._isMounted = false; this._isMounted = false;
if (this._fileInfoSubscribeId) {
lbry.fileInfoUnsubscribe(this.props.outpoint, this._fileInfoSubscribeId);
}
}, },
render: function() { render: function() {
const fileInfo = this.state.fileInfo; const fileInfo = this.state.fileInfo;

View file

@ -79,7 +79,7 @@ export let FileTileStream = React.createClass({
componentDidMount: function() { componentDidMount: function() {
this._isMounted = true; this._isMounted = true;
if (this.props.hideOnRemove) { if (this.props.hideOnRemove) {
lbry.fileInfoSubscribe(this.props.outpoint, this.onFileInfoUpdate); this._fileInfoSubscribeId = lbry.fileInfoSubscribe(this.props.outpoint, this.onFileInfoUpdate);
} }
}, },
componentWillUnmount: function() { componentWillUnmount: function() {
@ -121,15 +121,15 @@ export let FileTileStream = React.createClass({
<section className={ 'file-tile card ' + (obscureNsfw ? 'card-obscured ' : '') } onMouseEnter={this.handleMouseOver} onMouseLeave={this.handleMouseOut}> <section className={ 'file-tile card ' + (obscureNsfw ? 'card-obscured ' : '') } onMouseEnter={this.handleMouseOver} onMouseLeave={this.handleMouseOut}>
<div className={"row-fluid card-content file-tile__row"}> <div className={"row-fluid card-content file-tile__row"}>
<div className="span3"> <div className="span3">
<a href={'/?show=' + this.props.name}><Thumbnail className="file-tile__thumbnail" src={metadata.thumbnail} alt={'Photo for ' + (title || this.props.name)} /></a> <a href={'?show=' + this.props.name}><Thumbnail className="file-tile__thumbnail" src={metadata.thumbnail} alt={'Photo for ' + (title || this.props.name)} /></a>
</div> </div>
<div className="span9"> <div className="span9">
{ !this.props.hidePrice { !this.props.hidePrice
? <FilePrice name={this.props.name} /> ? <FilePrice name={this.props.name} />
: null} : null}
<div className="meta"><a href={'index.html?show=' + this.props.name}>{'lbry://' + this.props.name}</a></div> <div className="meta"><a href={'?show=' + this.props.name}>{'lbry://' + this.props.name}</a></div>
<h3 className="file-tile__title"> <h3 className="file-tile__title">
<a href={'index.html?show=' + this.props.name}> <a href={'?show=' + this.props.name}>
<TruncatedText lines={1}> <TruncatedText lines={1}>
{title} {title}
</TruncatedText> </TruncatedText>

View file

@ -223,7 +223,7 @@ lbry.stopFile = function(name, callback) {
lbry.removeFile = function(outpoint, deleteTargetFile=true, callback) { lbry.removeFile = function(outpoint, deleteTargetFile=true, callback) {
this._removedFiles.push(outpoint); this._removedFiles.push(outpoint);
this._updateSubscribedFileInfo(outpoint); this._updateFileInfoSubscribers(outpoint);
lbry.file_delete({ lbry.file_delete({
outpoint: outpoint, outpoint: outpoint,
@ -405,9 +405,11 @@ lbry.stop = function(callback) {
}; };
lbry.fileInfo = {}; lbry.fileInfo = {};
lbry._fileInfoSubscribeIdCounter = 0; lbry._subscribeIdCount = 0;
lbry._fileInfoSubscribeCallbacks = {}; lbry._fileInfoSubscribeCallbacks = {};
lbry._fileInfoSubscribeInterval = 5000; lbry._fileInfoSubscribeInterval = 5000;
lbry._balanceSubscribeCallbacks = {};
lbry._balanceSubscribeInterval = 5000;
lbry._removedFiles = []; lbry._removedFiles = [];
lbry._claimIdOwnershipCache = {}; lbry._claimIdOwnershipCache = {};
@ -419,9 +421,9 @@ lbry._updateClaimOwnershipCache = function(claimId) {
}); });
}; };
lbry._updateSubscribedFileInfo = function(outpoint) { lbry._updateFileInfoSubscribers = function(outpoint) {
const callSubscribedCallbacks = (outpoint, fileInfo) => { const callSubscribedCallbacks = (outpoint, fileInfo) => {
for (let [subscribeId, callback] of Object.entries(this._fileInfoSubscribeCallbacks[outpoint])) { for (let callback of Object.values(this._fileInfoSubscribeCallbacks[outpoint])) {
callback(fileInfo); callback(fileInfo);
} }
} }
@ -446,7 +448,7 @@ lbry._updateSubscribedFileInfo = function(outpoint) {
if (Object.keys(this._fileInfoSubscribeCallbacks[outpoint]).length) { if (Object.keys(this._fileInfoSubscribeCallbacks[outpoint]).length) {
setTimeout(() => { setTimeout(() => {
this._updateSubscribedFileInfo(outpoint); this._updateFileInfoSubscribers(outpoint);
}, lbry._fileInfoSubscribeInterval); }, lbry._fileInfoSubscribeInterval);
} }
} }
@ -457,14 +459,39 @@ lbry.fileInfoSubscribe = function(outpoint, callback) {
lbry._fileInfoSubscribeCallbacks[outpoint] = {}; lbry._fileInfoSubscribeCallbacks[outpoint] = {};
} }
const subscribeId = ++lbry._fileInfoSubscribeIdCounter; const subscribeId = ++lbry._subscribeIdCount;
lbry._fileInfoSubscribeCallbacks[outpoint][subscribeId] = callback; lbry._fileInfoSubscribeCallbacks[outpoint][subscribeId] = callback;
lbry._updateSubscribedFileInfo(outpoint); lbry._updateFileInfoSubscribers(outpoint);
return subscribeId; return subscribeId;
} }
lbry.fileInfoUnsubscribe = function(name, subscribeId) { lbry.fileInfoUnsubscribe = function(outpoint, subscribeId) {
delete lbry._fileInfoSubscribeCallbacks[name][subscribeId]; delete lbry._fileInfoSubscribeCallbacks[outpoint][subscribeId];
}
lbry._updateBalanceSubscribers = function() {
lbry.get_balance().then(function(balance) {
for (let callback of Object.values(lbry._balanceSubscribeCallbacks)) {
callback(balance);
}
});
if (Object.keys(lbry._balanceSubscribeCallbacks).length) {
setTimeout(() => {
lbry._updateBalanceSubscribers();
}, lbry._balanceSubscribeInterval);
}
}
lbry.balanceSubscribe = function(callback) {
const subscribeId = ++lbry._subscribeIdCount;
lbry._balanceSubscribeCallbacks[subscribeId] = callback;
lbry._updateBalanceSubscribers();
return subscribeId;
}
lbry.balanceUnsubscribe = function(subscribeId) {
delete lbry._balanceSubscribeCallbacks[subscribeId];
} }
lbry.showMenuIfNeeded = function() { lbry.showMenuIfNeeded = function() {

View file

@ -41,7 +41,7 @@ export let FileListDownloaded = React.createClass({
} else if (!this.state.fileInfos.length) { } else if (!this.state.fileInfos.length) {
return ( return (
<main className="page"> <main className="page">
<span>You haven't downloaded anything from LBRY yet. Go <Link href="/index.html?discover" label="search for your first download" />!</span> <span>You haven't downloaded anything from LBRY yet. Go <Link href="?discover" label="search for your first download" />!</span>
</main> </main>
); );
} else { } else {
@ -90,7 +90,7 @@ export let FileListPublished = React.createClass({
else if (!this.state.fileInfos.length) { else if (!this.state.fileInfos.length) {
return ( return (
<main className="page"> <main className="page">
<span>You haven't published anything to LBRY yet.</span> Try <Link href="index.html?publish" label="publishing" />! <span>You haven't published anything to LBRY yet.</span> Try <Link href="?publish" label="publishing" />!
</main> </main>
); );
} }

View file

@ -67,7 +67,7 @@ var HelpPage = React.createClass({
<section className="card"> <section className="card">
<h3>Report a Bug</h3> <h3>Report a Bug</h3>
<p>Did you find something wrong?</p> <p>Did you find something wrong?</p>
<p><Link href="index.html?report" label="Submit a Bug Report" icon="icon-bug" button="alt" /></p> <p><Link href="?report" label="Submit a Bug Report" icon="icon-bug" button="alt" /></p>
<div className="meta">Thanks! LBRY is made by its users.</div> <div className="meta">Thanks! LBRY is made by its users.</div>
</section> </section>
{!ver ? null : {!ver ? null :

View file

@ -155,7 +155,7 @@ var DetailPage = React.createClass({
) : ( ) : (
<div> <div>
<h2>No content</h2> <h2>No content</h2>
There is no content available at the name <strong>lbry://{this.props.name}</strong>. If you reached this page from a link within the LBRY interface, please <Link href="index.html?report" label="report a bug" />. Thanks! There is no content available at the name <strong>lbry://{this.props.name}</strong>. If you reached this page from a link within the LBRY interface, please <Link href="?report" label="report a bug" />. Thanks!
</div> </div>
)} )}
</section> </section>

View file

@ -243,6 +243,8 @@ var TransactionList = React.createClass({
var WalletPage = React.createClass({ var WalletPage = React.createClass({
_balanceSubscribeId: null,
propTypes: { propTypes: {
viewingPage: React.PropTypes.string, viewingPage: React.PropTypes.string,
}, },
@ -259,12 +261,17 @@ var WalletPage = React.createClass({
} }
}, },
componentWillMount: function() { componentWillMount: function() {
lbry.getBalance((results) => { this._balanceSubscribeId = lbry.balanceSubscribe((results) => {
this.setState({ this.setState({
balance: results, balance: results,
}) })
}); });
}, },
componentWillUnmount: function() {
if (this._balanceSubscribeId) {
lbry.balanceUnsubscribe(this._balanceSubscribeId);
}
},
render: function() { render: function() {
return ( return (
<main className="page"> <main className="page">

View file

@ -2,6 +2,7 @@
.load-screen { .load-screen {
color: white; color: white;
background: $color-primary;
background-size: cover; background-size: cover;
min-height: 100vh; min-height: 100vh;
min-width: 100vw; min-width: 100vw;