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]
### 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()
* New-style API calls return promises instead of using callbacks
* Wherever possible, use outpoints for unique IDs instead of names or SD hashes

View file

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

View file

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

View file

@ -9,7 +9,7 @@ var DrawerItem = React.createClass({
};
},
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);
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({
_balanceSubscribeId: null,
handleLogoClicked: function(event) {
if ((event.ctrlKey || event.metaKey) && event.shiftKey) {
window.location.href = 'index.html?developer'
window.location.href = '?developer'
event.preventDefault();
}
},
@ -32,25 +34,30 @@ var Drawer = React.createClass({
};
},
componentDidMount: function() {
lbry.getBalance(function(balance) {
this._balanceSubscribeId = lbry.balanceSubscribe(function(balance) {
this.setState({
balance: balance
});
}.bind(this));
},
componentWillUnmount: function() {
if (this._balanceSubscribeId) {
lbry.balanceUnsubscribe(this._balanceSubscribeId)
}
},
render: function() {
return (
<nav id="drawer">
<div id="drawer-handle">
<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>
<DrawerItem href='index.html?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='index.html?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='index.html?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='?discover' viewingPage={this.props.viewingPage} label="Discover" icon="icon-search" />
<DrawerItem href='?publish' viewingPage={this.props.viewingPage} label="Publish" icon="icon-upload" />
<DrawerItem href='?downloaded' subPages={['published']} viewingPage={this.props.viewingPage} label="My Files" icon='icon-cloud-download' />
<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='?settings' viewingPage={this.props.viewingPage} label="Settings" icon='icon-gear' />
<DrawerItem href='?help' viewingPage={this.props.viewingPage} label="Help" icon='icon-question-circle' />
</nav>
);
}

View file

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

View file

@ -79,7 +79,7 @@ export let FileTileStream = React.createClass({
componentDidMount: function() {
this._isMounted = true;
if (this.props.hideOnRemove) {
lbry.fileInfoSubscribe(this.props.outpoint, this.onFileInfoUpdate);
this._fileInfoSubscribeId = lbry.fileInfoSubscribe(this.props.outpoint, this.onFileInfoUpdate);
}
},
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}>
<div className={"row-fluid card-content file-tile__row"}>
<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 className="span9">
{ !this.props.hidePrice
? <FilePrice name={this.props.name} />
: 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">
<a href={'index.html?show=' + this.props.name}>
<a href={'?show=' + this.props.name}>
<TruncatedText lines={1}>
{title}
</TruncatedText>

View file

@ -223,7 +223,7 @@ lbry.stopFile = function(name, callback) {
lbry.removeFile = function(outpoint, deleteTargetFile=true, callback) {
this._removedFiles.push(outpoint);
this._updateSubscribedFileInfo(outpoint);
this._updateFileInfoSubscribers(outpoint);
lbry.file_delete({
outpoint: outpoint,
@ -405,9 +405,11 @@ lbry.stop = function(callback) {
};
lbry.fileInfo = {};
lbry._fileInfoSubscribeIdCounter = 0;
lbry._subscribeIdCount = 0;
lbry._fileInfoSubscribeCallbacks = {};
lbry._fileInfoSubscribeInterval = 5000;
lbry._balanceSubscribeCallbacks = {};
lbry._balanceSubscribeInterval = 5000;
lbry._removedFiles = [];
lbry._claimIdOwnershipCache = {};
@ -419,9 +421,9 @@ lbry._updateClaimOwnershipCache = function(claimId) {
});
};
lbry._updateSubscribedFileInfo = function(outpoint) {
lbry._updateFileInfoSubscribers = function(outpoint) {
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);
}
}
@ -446,7 +448,7 @@ lbry._updateSubscribedFileInfo = function(outpoint) {
if (Object.keys(this._fileInfoSubscribeCallbacks[outpoint]).length) {
setTimeout(() => {
this._updateSubscribedFileInfo(outpoint);
this._updateFileInfoSubscribers(outpoint);
}, lbry._fileInfoSubscribeInterval);
}
}
@ -457,14 +459,39 @@ lbry.fileInfoSubscribe = function(outpoint, callback) {
lbry._fileInfoSubscribeCallbacks[outpoint] = {};
}
const subscribeId = ++lbry._fileInfoSubscribeIdCounter;
const subscribeId = ++lbry._subscribeIdCount;
lbry._fileInfoSubscribeCallbacks[outpoint][subscribeId] = callback;
lbry._updateSubscribedFileInfo(outpoint);
lbry._updateFileInfoSubscribers(outpoint);
return subscribeId;
}
lbry.fileInfoUnsubscribe = function(name, subscribeId) {
delete lbry._fileInfoSubscribeCallbacks[name][subscribeId];
lbry.fileInfoUnsubscribe = function(outpoint, 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() {

View file

@ -41,7 +41,7 @@ export let FileListDownloaded = React.createClass({
} else if (!this.state.fileInfos.length) {
return (
<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>
);
} else {
@ -90,7 +90,7 @@ export let FileListPublished = React.createClass({
else if (!this.state.fileInfos.length) {
return (
<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>
);
}

View file

@ -67,7 +67,7 @@ var HelpPage = React.createClass({
<section className="card">
<h3>Report a Bug</h3>
<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>
</section>
{!ver ? null :

View file

@ -155,7 +155,7 @@ var DetailPage = React.createClass({
) : (
<div>
<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>
)}
</section>

View file

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

View file

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