diff --git a/.bumpversion.cfg b/.bumpversion.cfg index d477ef385..785910217 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.9.2rc2 +current_version = 0.9.2rc1 commit = True tag = True parse = (?P\d+)\.(?P\d+)\.(?P\d+)((?P[a-z]+)(?P\d+))? diff --git a/ui/CHANGELOG.md b/CHANGELOG.md similarity index 79% rename from ui/CHANGELOG.md rename to CHANGELOG.md index b8c82b321..2fe463b57 100644 --- a/ui/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,9 +8,14 @@ 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 + * New publishes now display immediately in My Files, even before they hit the lbrynet file manager. ### Changed * Update process now easier and more reliable diff --git a/app/main.js b/app/main.js index d10d250ef..9ea2b52e8 100644 --- a/app/main.js +++ b/app/main.js @@ -1,67 +1,23 @@ -const {app, BrowserWindow, ipcMain} = require('electron'); +const {app, BrowserWindow, ipcMain, shell} = require('electron'); const path = require('path'); const jayson = require('jayson'); // 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'); let client = jayson.client.http('http://localhost:5279/lbryapi'); // Keep a global reference of the window object, if you don't, the window will // be closed automatically when the JavaScript object is garbage collected. -let win; +let win // Also keep the daemon subprocess alive -let daemonSubprocess; +let subpy +// set to true when the quitting sequence has started +let quitting = false; -// 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; - -// 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. -let readyToQuit = false; - -/* - * 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 - * when no windows are open. - */ -function openItem(fullPath) { - const subprocOptions = { - detached: true, - stdio: 'ignore', - }; - - let child; - if (process.platform == 'darwin') { - child = child_process.spawn('open', [fullPath], subprocOptions); - } else if (process.platform == 'linux') { - child = child_process.spawn('xdg-open', [fullPath], subprocOptions); - } else if (process.platform == 'win32') { - child = child_process.execSync('start', [fullPath], subprocOptions); - } - - // Causes child process reference to be garbage collected, allowing main process to exit - child.unref(); -} - -function getPidsForProcessName(name) { - if (process.platform == 'windows') { - const tasklistOut = child_process.execSync('tasklist', - ['/fi', `Imagename eq ${name}.exe`, '/nh'], - {encoding: 'utf8'} - ).stdout; - return tasklistOut.match(/[^\n]+/g).map((line) => line.split(/\s+/)[1]); // Second column of every non-empty line - } else { - const pgrepOut = child_process.spawnSync('pgrep', ['-x', name], {encoding: 'utf8'}).stdout; - return pgrepOut.match(/\d+/g); - } -} 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`) @@ -70,61 +26,49 @@ 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 - // warning and schedule a quit. - // - // TODO: maybe it would be better to restart the daemon? - if (win) { - console.log('Did not request daemon stop, so quitting in 5 seconds.'); - win.loadURL(`file://${__dirname}/dist/warning.html`); - setTimeout(quitNow, 5000); - } else { - console.log('Did not request daemon stop, so quitting.'); - quitNow(); - } - } -} - function launchDaemon() { - assert(!daemonSubprocess, 'Tried to launch daemon twice'); - + if (subpy) { + return; + } if (process.env.LBRY_DAEMON) { executable = process.env.LBRY_DAEMON; } else { executable = path.join(__dirname, 'dist', 'lbrynet-daemon'); } - console.log('Launching daemon:', executable) - daemonSubprocess = child_process.spawn(executable) + console.log('Launching daemon: ' + executable) + subpy = require('child_process').spawn(executable) // Need to handle the data event instead of attaching to // process.stdout because the latter doesn't work. I believe on // windows it buffers stdout and we don't get any meaningful output - daemonSubprocess.stdout.on('data', (buf) => {console.log(String(buf).trim());}); - daemonSubprocess.stderr.on('data', (buf) => {console.log(String(buf).trim());}); - daemonSubprocess.on('exit', handleDaemonSubprocessExited); + subpy.stdout.on('data', (buf) => {console.log(String(buf).trim());}); + subpy.stderr.on('data', (buf) => {console.log(String(buf).trim());}); + subpy.on('exit', () => { + console.log('The daemon has exited. Quitting the app'); + subpy = null; + if (quitting) { + // If quitting is True it means that we were expecting the daemon + // to be shutdown so we can quit right away + app.quit(); + } else { + // Otherwise, this shutdown was a surprise so display a warning + // and schedule a quit + // + // TODO: maybe it would be better to restart the daemon? + win.loadURL(`file://${__dirname}/dist/warning.html`); + setTimeout(app.quit, 5000) + } + }); console.log('lbrynet daemon has launched') } -/* - * Quits without any preparation. When a quit is requested (either through the - * interface or through app.quit()), we abort the quit, try to shut down the daemon, - * and then call this to quit for real. - */ -function quitNow() { - readyToQuit = true; - app.quit(); -} - app.on('ready', function(){ launchDaemonIfNotRunning(); createWindow(); }); + function launchDaemonIfNotRunning() { // Check if the daemon is already running. If we get // an error its because its not running @@ -142,36 +86,6 @@ function launchDaemonIfNotRunning() { ); } -/* - * Last resort for killing unresponsive daemon instances. - * Looks for any processes called "lbrynet-daemon" and - * tries to force kill them. - */ -function forceKillAllDaemonsAndQuit() { - console.log('Attempting to force kill any running lbrynet-daemon instances...'); - - const daemonPids = getPidsForProcessName('lbrynet-daemon'); - if (!daemonPids) { - console.log('No lbrynet-daemon found running.'); - quitNow(); - } else { - console.log(`Found ${daemonPids.length} running daemon instances. Attempting to force kill...`); - - for (const pid of daemonPids) { - const daemonKillAttemptsComplete = 0; - kill(pid, 'SIGKILL', (err) => { - daemonKillAttemptsComplete++; - if (err) { - console.log(`Failed to force kill running daemon with pid ${pid}. Error message: ${err.message}`); - } - if (daemonKillAttemptsComplete >= daemonPids.length) { - quitNow(); - } - }); - } - } -} - // Quit when all windows are closed. app.on('window-all-closed', () => { @@ -184,15 +98,12 @@ app.on('window-all-closed', () => { app.on('before-quit', (event) => { - if (!readyToQuit) { - // We need to shutdown the daemon before we're ready to actually quit. This - // event will be triggered re-entrantly once preparation is done. - event.preventDefault(); - shutdownDaemonAndQuit(); - } else { - console.log('Quitting.') + if (subpy == null) { + return } -}); + event.preventDefault(); + shutdownDaemon(); +}) app.on('activate', () => { @@ -201,60 +112,52 @@ app.on('activate', () => { if (win === null) { createWindow() } -}); +}) -// 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) { + +function shutdownDaemon(evenIfNotStartedByApp = false) { + if (subpy) { console.log('Killing lbrynet-daemon process'); - kill(daemonSubprocess.pid, undefined, (err) => { + kill(subpy.pid, undefined, (err) => { console.log('Killed lbrynet-daemon process'); - requestedDaemonSubprocessKilled = true; - quitNow(); }); } else if (evenIfNotStartedByApp) { - console.log('Stopping lbrynet-daemon, even though app did not start it'); - 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(); - } else { - console.log('Successfully stopped daemon via RPC call.') - quitNow(); - } - }); + console.log('Killing lbrynet-daemon, even though app did not start it'); + client.request('daemon_stop', []); + // TODO: If the daemon errors or times out when we make this request, find + // the process and force quit it. } else { - console.log('Not killing lbrynet-daemon because app did not start it'); - quitNow(); + console.log('Not killing lbrynet-daemon because app did not start it') } // Is it safe to start the installer before the daemon finishes running? // If not, we should wait until the daemon is closed before we start the install. } +function shutdown() { + if (win) { + win.loadURL(`file://${__dirname}/dist/quit.html`); + } + quitting = true; + shutdownDaemon(); +} + function upgrade(event, installerPath) { app.on('quit', () => { - console.log('Launching upgrade installer at', installerPath); - // This gets triggered called after *all* other quit-related events, so - // we'll only get here if we're fully prepared and quitting for real. - openItem(installerPath); + shell.openItem(installerPath); }); - if (win) { win.loadURL(`file://${__dirname}/dist/upgrade.html`); } - - app.quit(); + quitting = true; + shutdownDaemon(true); // wait for daemon to shut down before upgrading // what to do if no shutdown in a long time? - console.log('Update downloaded to', installerPath); + console.log('Update downloaded to ', installerPath); console.log('The app will close, and you will be prompted to install the latest version of LBRY.'); console.log('After the install is complete, please reopen the app.'); } ipcMain.on('upgrade', upgrade); + +ipcMain.on('shutdown', shutdown); \ No newline at end of file diff --git a/app/package.json b/app/package.json index dc2762462..bb3ccbbc4 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "LBRY", - "version": "0.9.2rc2", + "version": "0.9.2rc1", "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 b04455f9c..e8e2a0d35 160000 --- a/lbry +++ b/lbry @@ -1 +1 @@ -Subproject commit b04455f9c733575b5191f1711d6d065ea5195ebb +Subproject commit e8e2a0d353928f01746a275c38faf3cd8f072d28 diff --git a/ui/js/app.js b/ui/js/app.js index 3a639c945..b2f58cd14 100644 --- a/ui/js/app.js +++ b/ui/js/app.js @@ -24,9 +24,9 @@ import {Link} from './component/link.js'; const {remote, ipcRenderer, shell} = require('electron'); const {download} = remote.require('electron-dl'); +const os = require('os'); const path = require('path'); const app = require('electron').remote.app; -const fs = remote.require('fs'); var App = React.createClass({ @@ -40,59 +40,53 @@ var App = React.createClass({ }, _upgradeDownloadItem: null, + _isMounted: false, _version: null, - getUpdateUrl: function() { - switch (process.platform) { - case 'darwin': - return 'https://lbry.io/get/lbry.dmg'; - case 'linux': - return 'https://lbry.io/get/lbry.deb'; - case 'win32': - return 'https://lbry.io/get/lbry.exe'; - default: - throw 'Unknown platform'; - } + // Temporary workaround since electron-dl throws errors when you try to get the filename + getDefaultProps: function() { + return { + address: window.location.search + }; }, - // Hard code the filenames as a temporary workaround, because - // electron-dl throws errors when you try to get the filename getUpgradeFilename: function() { - switch (process.platform) { - case 'darwin': - return `LBRY-${this._version}.dmg`; - case 'linux': - return `LBRY_${this._version}_amd64.deb`; - case 'windows': - return `LBRY.Setup.${this._version}.exe`; - default: - throw 'Unknown platform'; + if (os.platform() == 'darwin') { + return `LBRY-${this._version}.dmg`; + } else if (os.platform() == 'linux') { + return `LBRY_${this._version}_amd64.deb`; + } else { + return `LBRY.Setup.${this._version}.exe`; } }, - 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, + updateUrl: null, + isOldOSX: 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,24 +94,48 @@ 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; } }); - if (!sessionStorage.getItem('upgradeSkipped')) { - lbry.checkNewVersionAvailable(({isAvailable}) => { - if (!isAvailable) { - return; + lbry.checkNewVersionAvailable((isAvailable) => { + if (!isAvailable || sessionStorage.getItem('upgradeSkipped')) { + return; + } + + lbry.getVersionInfo((versionInfo) => { + this._version = versionInfo.lbrynet_version; // temp for building upgrade filename + + var isOldOSX = false; + if (versionInfo.os_system == 'Darwin') { + var updateUrl = 'https://lbry.io/get/lbry.dmg'; + + var maj, min, patch; + [maj, min, patch] = versionInfo.lbrynet_version.split('.'); + if (maj == 0 && min <= 2 && patch <= 2) { + isOldOSX = true; + } + } else if (versionInfo.os_system == 'Linux') { + var updateUrl = 'https://lbry.io/get/lbry.deb'; + } else if (versionInfo.os_system == 'Windows') { + var updateUrl = 'https://lbry.io/get/lbry.exe'; + } else { + var updateUrl = 'https://lbry.io/get'; } - lbry.getVersionInfo((versionInfo) => { - this._version = versionInfo.lbrynet_version; - this.setState({ - modal: 'upgrade', - }); - }); + this.setState({ + modal: 'upgrade', + isOldOSX: isOldOSX, + updateUrl: updateUrl, + }) }); - } + }); }, openDrawer: function() { sessionStorage.setItem('drawerOpen', true); @@ -132,15 +150,23 @@ 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); - + // TODO: create a callback for onProgress and have the UI + // show download progress + // TODO: calling lbry.stop() ends up displaying the "daemon + // unexpectedly stopped" page. Have a better way of shutting down + let dir = app.getPath('temp'); let options = { onProgress: (p) => this.setState({downloadProgress: Math.round(p * 100)}), directory: dir, }; - download(remote.getCurrentWindow(), this.getUpdateUrl(), options) + download(remote.getCurrentWindow(), this.state.updateUrl, options) .then(downloadItem => { /** * TODO: get the download path directly from the download object. It should just be @@ -283,7 +309,11 @@ var App = React.createClass({ - Your version of LBRY is out of date and may be unreliable or insecure. +

Your version of LBRY is out of date and may be unreliable or insecure.

+ {this.state.isOldOSX + ?

Before installing the new version, make sure to exit LBRY. If you started the app, click the LBRY icon in your status bar and choose "Quit."

+ : null} +
Downloading Update{this.state.downloadProgress ? `: ${this.state.downloadProgress}%` : null} diff --git a/ui/js/component/drawer.js b/ui/js/component/drawer.js index bc8f6d666..eaf11506b 100644 --- a/ui/js/component/drawer.js +++ b/ui/js/component/drawer.js @@ -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 } @@ -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 ( ); } diff --git a/ui/js/component/file-actions.js b/ui/js/component/file-actions.js index 9a1d42af3..c8331e261 100644 --- a/ui/js/component/file-actions.js +++ b/ui/js/component/file-actions.js @@ -179,7 +179,7 @@ let FileActionsRow = React.createClass({ let linkBlock; if (this.state.fileInfo === false && !this.state.attemptingDownload) { linkBlock = ; - } else if (this.state.attemptingDownload || !this.state.fileInfo.completed) { + } else if (this.state.attemptingDownload || (!this.state.fileInfo.completed && !this.state.fileInfo.isMine)) { const progress = this.state.fileInfo ? this.state.fileInfo.written_bytes / this.state.fileInfo.total_bytes * 100 : 0, label = this.state.fileInfo ? progress.toFixed(0) + '% complete' : 'Connecting...', @@ -253,7 +253,7 @@ export let FileActions = React.createClass({ if (this.isMounted) { this.setState({ fileInfo: fileInfo, - }); + }); } }, componentDidMount: function() { @@ -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; diff --git a/ui/js/component/file-tile.js b/ui/js/component/file-tile.js index 4cc3166ae..5c9ce2890 100644 --- a/ui/js/component/file-tile.js +++ b/ui/js/component/file-tile.js @@ -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({
- +
{ !this.props.hidePrice ? : null} - +

- + {title} diff --git a/ui/js/lbry.js b/ui/js/lbry.js index 5d41abc42..aeb91e12f 100644 --- a/ui/js/lbry.js +++ b/ui/js/lbry.js @@ -1,15 +1,103 @@ import lighthouse from './lighthouse.js'; import jsonrpc from './jsonrpc.js'; +import {getLocal, setLocal} from './utils.js'; const {remote} = require('electron'); 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) { + const pendingPublishes = getLocal('pendingPublishes') || []; + const newPendingPublish = { + claim_id: 'pending_claim_' + name, + txid: 'pending_' + name, + nout: 0, + outpoint: 'pending_' + name + ':0', + name: name, + time: Date.now(), + }; + setLocal('pendingPublishes', [...pendingPublishes, newPendingPublish]); + return newPendingPublish; +} + +function removePendingPublish({name, outpoint}) { + setLocal('pendingPublishes', getPendingPublishes().filter( + (pub) => pub.name != name && pub.outpoint != outpoint + )); +} + +/** + * Gets the current list of pending publish attempts. Filters out any that have timed out and + * removes them from the list. + */ +function getPendingPublishes() { + const pendingPublishes = getLocal('pendingPublishes') || []; + + const newPendingPublishes = []; + for (let pendingPublish of pendingPublishes) { + if (Date.now() - pendingPublish.time <= lbry.pendingPublishTimeout) { + newPendingPublishes.push(pendingPublish); + } + } + setLocal('pendingPublishes', 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. + */ +function getPendingPublish({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; + } +} + +function pendingPublishToDummyClaim({name, outpoint, claim_id, txid, nout}) { + return { + name: name, + outpoint: outpoint, + claim_id: claim_id, + txid: txid, + nout: nout, + }; +} + +function pendingPublishToDummyFileInfo({name, outpoint, claim_id}) { + return { + name: name, + outpoint: outpoint, + claim_id: claim_id, + metadata: "Attempting publication", + }; +} + + let lbry = { isConnected: false, rootPath: '.', daemonConnectionString: 'http://localhost:5279/lbryapi', webUiUri: 'http://localhost:5279', peerListTimeout: 6000, + pendingPublishTimeout: 20 * 60 * 1000, colors: { primary: '#155B4A' }, @@ -223,7 +311,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, @@ -251,18 +339,49 @@ lbry.getFileInfoWhenListed = function(name, callback, timeoutCallback, tryNum=0) }, () => scheduleNextCheckOrTimeout()); } +/** + * Publishes a file. The optional fileListedCallback is called when the file becomes available in + * lbry.file_list() during the publish process. + * + * This currently includes a work-around to cache the file in local storage so that the pending + * publish can appear in the UI immediately. + */ lbry.publish = function(params, fileListedCallback, publishedCallback, errorCallback) { - // Publishes a file. - // The optional fileListedCallback is called when the file becomes available in - // lbry.getFilesInfo() during the publish process. + lbry.call('publish', params, (result) => { + if (returnedPending) { + return; + } - // Use ES6 named arguments instead of directly passing param dict? - lbry.call('publish', params, publishedCallback, errorCallback); - if (fileListedCallback) { - lbry.getFileInfoWhenListed(params.name, function(fileInfo) { - fileListedCallback(fileInfo); - }); - } + clearTimeout(returnPendingTimeout); + publishedCallback(result); + }, (err) => { + if (returnedPending) { + return; + } + + clearTimeout(returnPendingTimeout); + errorCallback(err); + }); + + let returnedPending = false; + // Give a short grace period in case publish() returns right away or (more likely) gives an error + const returnPendingTimeout = setTimeout(() => { + returnedPending = true; + + if (publishedCallback) { + savePendingPublish(params.name); + publishedCallback(true); + } + + if (fileListedCallback) { + savePendingPublish(params.name); + fileListedCallback(true); + } + }, 2000); + + //lbry.getFileInfoWhenListed(params.name, function(fileInfo) { + // fileListedCallback(fileInfo); + //}); } lbry.getVersionInfo = function(callback) { @@ -405,9 +524,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 +540,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 +567,7 @@ lbry._updateSubscribedFileInfo = function(outpoint) { if (Object.keys(this._fileInfoSubscribeCallbacks[outpoint]).length) { setTimeout(() => { - this._updateSubscribedFileInfo(outpoint); + this._updateFileInfoSubscribers(outpoint); }, lbry._fileInfoSubscribeInterval); } } @@ -457,14 +578,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() { @@ -476,6 +622,60 @@ lbry.showMenuIfNeeded = function() { sessionStorage.setItem('menuShown', chosenMenu); }; +/** + * 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. + */ + +/** + * Returns results from the file_list API method, plus dummy entries for pending publishes. + * (If a real publish with the same name is found, the pending publish will be ignored and removed.) + */ +lbry.file_list = function(params={}) { + return new Promise((resolve, reject) => { + const {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) { + const pendingPublish = getPendingPublish({outpoint}); + if (pendingPublish) { + resolve([pendingPublishToDummyFileInfo(pendingPublish)]); + return; + } + } + + lbry.call('file_list', params, (fileInfos) => { + // Remove any pending publications that are now listed in the file manager + + 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); + }); +} + +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); + + resolve([...claims, ...dummyClaims]); + }, reject, reject); + }); +} + lbry = new Proxy(lbry, { get: function(target, name) { if (name in target) { diff --git a/ui/js/page/file-list.js b/ui/js/page/file-list.js index 734cb75eb..8134be11f 100644 --- a/ui/js/page/file-list.js +++ b/ui/js/page/file-list.js @@ -41,7 +41,7 @@ export let FileListDownloaded = React.createClass({ } else if (!this.state.fileInfos.length) { return (
- You haven't downloaded anything from LBRY yet. Go ! + You haven't downloaded anything from LBRY yet. Go !
); } else { @@ -90,7 +90,7 @@ export let FileListPublished = React.createClass({ else if (!this.state.fileInfos.length) { return (
- You haven't published anything to LBRY yet. Try ! + You haven't published anything to LBRY yet. Try !
); } diff --git a/ui/js/page/help.js b/ui/js/page/help.js index 6bf964205..632c3abd0 100644 --- a/ui/js/page/help.js +++ b/ui/js/page/help.js @@ -67,7 +67,7 @@ var HelpPage = React.createClass({

Report a Bug

Did you find something wrong?

-

+

Thanks! LBRY is made by its users.
{!ver ? null : diff --git a/ui/js/page/show.js b/ui/js/page/show.js index a2bb1b5f1..8f4d450c9 100644 --- a/ui/js/page/show.js +++ b/ui/js/page/show.js @@ -155,7 +155,7 @@ var DetailPage = React.createClass({ ) : (

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 the name lbry://{this.props.name}. If you reached this page from a link within the LBRY interface, please . Thanks!
)}

diff --git a/ui/js/page/wallet.js b/ui/js/page/wallet.js index bdd6eff0e..6e6714b0a 100644 --- a/ui/js/page/wallet.js +++ b/ui/js/page/wallet.js @@ -91,7 +91,7 @@ var SendToAddressSection = React.createClass({ } }, (error) => { this.setState({ - results: "Something went wrong: " + error.message + results: "Something went wrong: " + error.faultString + " " + error.faultCode }) }); }, @@ -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 (
diff --git a/ui/js/utils.js b/ui/js/utils.js new file mode 100644 index 000000000..5b5cf246a --- /dev/null +++ b/ui/js/utils.js @@ -0,0 +1,15 @@ +/** + * Thin wrapper around localStorage.getItem(). Parses JSON and returns undefined if the value + * is not set yet. + */ +export function getLocal(key) { + const itemRaw = localStorage.getItem(key); + return itemRaw === null ? undefined : JSON.parse(itemRaw); +} + +/** + * Thin wrapper around localStorage.setItem(). Converts value to JSON. + */ +export function setLocal(key, value) { + localStorage.setItem(key, JSON.stringify(value)); +} diff --git a/ui/package.json b/ui/package.json index f4caf9b3e..e01103e7a 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,6 +1,6 @@ { "name": "lbry-web-ui", - "version": "0.9.2rc2", + "version": "0.9.2rc1", "description": "LBRY UI", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" diff --git a/ui/scss/component/_load-screen.scss b/ui/scss/component/_load-screen.scss index 06540324f..e56eb12c0 100644 --- a/ui/scss/component/_load-screen.scss +++ b/ui/scss/component/_load-screen.scss @@ -2,6 +2,7 @@ .load-screen { color: white; + background: $color-primary; background-size: cover; min-height: 100vh; min-width: 100vw;