diff --git a/.gitignore b/.gitignore index 2d10cf601..2a5b30f19 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,5 @@ build/daemon.zip .vimrc package-lock.json + +.DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md index cae3c17e6..1273376a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ Web UI version numbers should always match the corresponding version of LBRY App * Fixed scriolling restore/reset/set (#729) * Fixed sorting by title for published files (#614) * App now uses the new balance_delta field in the txn list. + * Abandoning from the claim page now works. * ### Deprecated diff --git a/src/renderer/.flowconfig b/src/renderer/.flowconfig new file mode 100644 index 000000000..18e154135 --- /dev/null +++ b/src/renderer/.flowconfig @@ -0,0 +1,17 @@ +[ignore] +.*/node_modules/** + +[include] + +[libs] +flow-typed + +[lints] + +[options] +suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe +suppress_comment=\\(.\\|\n\\)*\\$FlowIssue +module.name_mapper='^constants\(.*\)$' -> '/js/constants\1' +module.name_mapper='^redux\(.*\)$' -> '/js/redux\1' + +[strict] diff --git a/src/renderer/component/common.js b/src/renderer/component/common.js index 010bd8f31..c5352637b 100644 --- a/src/renderer/component/common.js +++ b/src/renderer/component/common.js @@ -1,13 +1,14 @@ import React from "react"; +import PropTypes from "prop-types"; import { formatCredits, formatFullPrice } from "util/formatCredits"; import lbry from "../lbry.js"; //component/icon.js export class Icon extends React.PureComponent { static propTypes = { - icon: React.PropTypes.string.isRequired, - className: React.PropTypes.string, - fixed: React.PropTypes.bool, + icon: PropTypes.string.isRequired, + className: PropTypes.string, + fixed: PropTypes.bool, }; render() { @@ -24,7 +25,7 @@ export class Icon extends React.PureComponent { export class TruncatedText extends React.PureComponent { static propTypes = { - lines: React.PropTypes.number, + lines: PropTypes.number, }; static defaultProps = { @@ -45,7 +46,7 @@ export class TruncatedText extends React.PureComponent { export class BusyMessage extends React.PureComponent { static propTypes = { - message: React.PropTypes.string, + message: PropTypes.string, }; render() { @@ -65,14 +66,14 @@ export class CurrencySymbol extends React.PureComponent { export class CreditAmount extends React.PureComponent { static propTypes = { - amount: React.PropTypes.number.isRequired, - precision: React.PropTypes.number, - isEstimate: React.PropTypes.bool, - label: React.PropTypes.bool, - showFree: React.PropTypes.bool, - showFullPrice: React.PropTypes.bool, - showPlus: React.PropTypes.bool, - look: React.PropTypes.oneOf(["indicator", "plain", "fee"]), + amount: PropTypes.number.isRequired, + precision: PropTypes.number, + isEstimate: PropTypes.bool, + label: PropTypes.bool, + showFree: PropTypes.bool, + showFullPrice: PropTypes.bool, + showPlus: PropTypes.bool, + look: PropTypes.oneOf(["indicator", "plain", "fee"]), }; static defaultProps = { @@ -142,7 +143,7 @@ let addressStyle = { }; export class Address extends React.PureComponent { static propTypes = { - address: React.PropTypes.string, + address: PropTypes.string, }; constructor(props) { @@ -174,7 +175,7 @@ export class Address extends React.PureComponent { export class Thumbnail extends React.PureComponent { static propTypes = { - src: React.PropTypes.string, + src: PropTypes.string, }; handleError() { diff --git a/src/renderer/component/file-selector.js b/src/renderer/component/file-selector.js index 2c3189c2f..adc481782 100644 --- a/src/renderer/component/file-selector.js +++ b/src/renderer/component/file-selector.js @@ -1,11 +1,12 @@ import React from "react"; +import PropTypes from "prop-types"; const { remote } = require("electron"); class FileSelector extends React.PureComponent { static propTypes = { - type: React.PropTypes.oneOf(["file", "directory"]), - initPath: React.PropTypes.string, - onFileChosen: React.PropTypes.func, + type: PropTypes.oneOf(["file", "directory"]), + initPath: PropTypes.string, + onFileChosen: PropTypes.func, }; static defaultProps = { diff --git a/src/renderer/component/form.js b/src/renderer/component/form.js index 03bf1185e..820ae251c 100644 --- a/src/renderer/component/form.js +++ b/src/renderer/component/form.js @@ -1,4 +1,5 @@ import React from "react"; +import PropTypes from "prop-types"; import FormField from "component/formField"; import { Icon } from "component/common.js"; @@ -12,7 +13,7 @@ export function formFieldId() { export class Form extends React.PureComponent { static propTypes = { - onSubmit: React.PropTypes.func.isRequired, + onSubmit: PropTypes.func.isRequired, }; constructor(props) { @@ -35,15 +36,9 @@ export class Form extends React.PureComponent { export class FormRow extends React.PureComponent { static propTypes = { - label: React.PropTypes.oneOfType([ - React.PropTypes.string, - React.PropTypes.element, - ]), - errorMessage: React.PropTypes.oneOfType([ - React.PropTypes.string, - React.PropTypes.object, - ]), - // helper: React.PropTypes.html, + label: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), + errorMessage: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + // helper: PropTypes.html, }; static defaultProps = { diff --git a/src/renderer/component/formField/view.jsx b/src/renderer/component/formField/view.jsx index e93559eb0..bb8cc5024 100644 --- a/src/renderer/component/formField/view.jsx +++ b/src/renderer/component/formField/view.jsx @@ -1,4 +1,5 @@ import React from "react"; +import PropTypes from "prop-types"; import FileSelector from "component/file-selector.js"; import SimpleMDE from "react-simplemde-editor"; import { formFieldNestedLabelTypes, formFieldId } from "../form"; @@ -8,14 +9,14 @@ const formFieldFileSelectorTypes = ["file", "directory"]; class FormField extends React.PureComponent { static propTypes = { - type: React.PropTypes.string.isRequired, - prefix: React.PropTypes.string, - postfix: React.PropTypes.string, - hasError: React.PropTypes.bool, - trim: React.PropTypes.bool, - regexp: React.PropTypes.oneOfType([ - React.PropTypes.instanceOf(RegExp), - React.PropTypes.string, + type: PropTypes.string.isRequired, + prefix: PropTypes.string, + postfix: PropTypes.string, + hasError: PropTypes.bool, + trim: PropTypes.bool, + regexp: PropTypes.oneOfType([ + PropTypes.instanceOf(RegExp), + PropTypes.string, ]), }; diff --git a/src/renderer/component/icon/view.jsx b/src/renderer/component/icon/view.jsx index 5c23af877..ff772c467 100644 --- a/src/renderer/component/icon/view.jsx +++ b/src/renderer/component/icon/view.jsx @@ -1,10 +1,11 @@ import React from "react"; +import PropTypes from "prop-types"; import * as icons from "constants/icons"; export default class Icon extends React.PureComponent { static propTypes = { - icon: React.PropTypes.string.isRequired, - fixed: React.PropTypes.bool, + icon: PropTypes.string.isRequired, + fixed: PropTypes.bool, }; static defaultProps = { diff --git a/src/renderer/component/load_screen.js b/src/renderer/component/load_screen.js index 8304b2a5f..788e42cf1 100644 --- a/src/renderer/component/load_screen.js +++ b/src/renderer/component/load_screen.js @@ -1,13 +1,14 @@ import React from "react"; +import PropTypes from "prop-types"; import lbry from "../lbry.js"; import { BusyMessage, Icon } from "./common.js"; import Link from "component/link"; class LoadScreen extends React.PureComponent { static propTypes = { - message: React.PropTypes.string.isRequired, - details: React.PropTypes.string, - isWarning: React.PropTypes.bool, + message: PropTypes.string.isRequired, + details: PropTypes.string, + isWarning: PropTypes.bool, }; constructor(props) { diff --git a/src/renderer/component/menu.js b/src/renderer/component/menu.js index 6010c0b8f..d1b533077 100644 --- a/src/renderer/component/menu.js +++ b/src/renderer/component/menu.js @@ -1,13 +1,14 @@ import React from "react"; +import PropTypes from "prop-types"; import { Icon } from "./common.js"; import Link from "component/link"; export class DropDownMenuItem extends React.PureComponent { static propTypes = { - href: React.PropTypes.string, - label: React.PropTypes.string, - icon: React.PropTypes.string, - onClick: React.PropTypes.func, + href: PropTypes.string, + label: PropTypes.string, + icon: PropTypes.string, + onClick: PropTypes.func, }; static defaultProps = { diff --git a/src/renderer/component/publishForm/index.js b/src/renderer/component/publishForm/index.js index a55cf9187..df41581d9 100644 --- a/src/renderer/component/publishForm/index.js +++ b/src/renderer/component/publishForm/index.js @@ -1,5 +1,10 @@ import React from "react"; import { connect } from "react-redux"; import PublishForm from "./view"; +import { selectBalance } from "redux/selectors/wallet"; -export default connect(null, null)(PublishForm); +const select = state => ({ + balance: selectBalance(state), +}); + +export default connect(select, null)(PublishForm); diff --git a/src/renderer/component/publishForm/internal/channelSection.jsx b/src/renderer/component/publishForm/internal/channelSection.jsx index 203e90e74..49c560f65 100644 --- a/src/renderer/component/publishForm/internal/channelSection.jsx +++ b/src/renderer/component/publishForm/internal/channelSection.jsx @@ -48,11 +48,22 @@ class ChannelSection extends React.PureComponent { handleNewChannelBidChange(event) { this.setState({ - newChannelBid: event.target.value, + newChannelBid: parseFloat(event.target.value), }); } handleCreateChannelClick(event) { + const { balance } = this.props; + const { newChannelBid } = this.state; + + if (newChannelBid > balance) { + this.refs.newChannelName.showError( + __("Unable to create channel due to insufficient funds.") + ); + + return; + } + this.setState({ creatingChannel: true, }); diff --git a/src/renderer/component/publishForm/view.jsx b/src/renderer/component/publishForm/view.jsx index 75ac22d2b..deb16e248 100644 --- a/src/renderer/component/publishForm/view.jsx +++ b/src/renderer/component/publishForm/view.jsx @@ -61,6 +61,15 @@ class PublishForm extends React.PureComponent { } handleSubmit() { + const { balance } = this.props; + const { bid } = this.state; + + if (bid > balance) { + this.handlePublishError({ message: "insufficient funds" }); + + return; + } + this.setState({ submitting: true, }); diff --git a/src/renderer/component/splash/view.jsx b/src/renderer/component/splash/view.jsx index 6d4ed62a4..eabe723c5 100644 --- a/src/renderer/component/splash/view.jsx +++ b/src/renderer/component/splash/view.jsx @@ -1,4 +1,5 @@ import React from "react"; +import PropTypes from "prop-types"; import lbry from "lbry.js"; import LoadScreen from "../load_screen.js"; import ModalIncompatibleDaemon from "modal/modalIncompatibleDaemon"; @@ -8,8 +9,8 @@ import * as modals from "constants/modal_types"; export class SplashScreen extends React.PureComponent { static propTypes = { - message: React.PropTypes.string, - onLoadDone: React.PropTypes.func, + message: PropTypes.string, + onLoadDone: PropTypes.func, }; constructor(props) { diff --git a/src/renderer/component/tooltip.js b/src/renderer/component/tooltip.js index 7f694c7dc..6f1437405 100644 --- a/src/renderer/component/tooltip.js +++ b/src/renderer/component/tooltip.js @@ -1,9 +1,10 @@ import React from "react"; +import PropTypes from "prop-types"; export class ToolTip extends React.PureComponent { static propTypes = { - body: React.PropTypes.string.isRequired, - label: React.PropTypes.string.isRequired, + body: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, }; constructor(props) { diff --git a/src/renderer/component/truncatedMarkdown/view.jsx b/src/renderer/component/truncatedMarkdown/view.jsx index 9146855f1..ba060016b 100644 --- a/src/renderer/component/truncatedMarkdown/view.jsx +++ b/src/renderer/component/truncatedMarkdown/view.jsx @@ -1,10 +1,11 @@ import React from "react"; +import PropTypes from "prop-types"; import ReactMarkdown from "react-markdown"; import ReactDOMServer from "react-dom/server"; class TruncatedMarkdown extends React.PureComponent { static propTypes = { - lines: React.PropTypes.number, + lines: PropTypes.number, }; static defaultProps = { diff --git a/src/renderer/component/wunderbar/view.jsx b/src/renderer/component/wunderbar/view.jsx index 04ba1a030..b23dc2218 100644 --- a/src/renderer/component/wunderbar/view.jsx +++ b/src/renderer/component/wunderbar/view.jsx @@ -1,4 +1,5 @@ import React from "react"; +import PropTypes from "prop-types"; import lbryuri from "lbryuri.js"; import { Icon } from "component/common.js"; import { parseQueryParams } from "util/query_params"; @@ -7,8 +8,8 @@ class WunderBar extends React.PureComponent { static TYPING_TIMEOUT = 800; static propTypes = { - onSearch: React.PropTypes.func.isRequired, - onSubmit: React.PropTypes.func.isRequired, + onSearch: PropTypes.func.isRequired, + onSubmit: PropTypes.func.isRequired, }; constructor(props) { diff --git a/src/renderer/flow-typed/electron.js b/src/renderer/flow-typed/electron.js new file mode 100644 index 000000000..4c575c618 --- /dev/null +++ b/src/renderer/flow-typed/electron.js @@ -0,0 +1,3 @@ +declare module 'electron' { + declare module.exports: any; +} diff --git a/src/renderer/flowtype-plugin.js b/src/renderer/flowtype-plugin.js new file mode 100644 index 000000000..8a33c707f --- /dev/null +++ b/src/renderer/flowtype-plugin.js @@ -0,0 +1,116 @@ +var spawnSync = require('child_process').spawnSync; +var flow = require('flow-bin'); +var merge = require('lodash.merge'); + +var store = { + error: null, + flowOptions: [ + 'status', + '--color=always', + ], + options: { + warn: false, + + formatter: function (errorCode, errorDetails) { + return 'Flow: ' + errorCode + '\n\n' + errorDetails; + }, + }, +}; + + +function flowErrorCode(status) { + var error; + switch (status) { + /* + case 0: + error = null; + break; + */ + case 1: + error = 'Server Initializing'; + break; + case 2: + error = 'Type Error'; + break; + case 3: + error = 'Out of Time'; + break; + case 4: + error = 'Kill Error'; + break; + case 6: + error = 'No Server Running'; + break; + case 7: + error = 'Out of Retries'; + break; + case 8: + error = 'Invalid Flowconfig'; + break; + case 9: + error = 'Build Id Mismatch'; + break; + case 10: + error = 'Input Error'; + break; + case 11: + error = 'Lock Stolen'; + break; + case 12: + error = 'Could Not Find Flowconfig'; + break; + case 13: + error = 'Server Out of Date'; + break; + case 14: + error = 'Server Client Directory Mismatch'; + break; + case 15: + error = 'Out of Shared Memory'; + break; + } + + return error; +} + + +function checkFlowStatus(compiler, next) { + var res = spawnSync(flow, store.flowOptions); + var status = res.status; + + if (status !== 0) { + var errorCode = flowErrorCode(status); + var errorDetails = res.stdout.toString() + res.stderr.toString(); + + store.error = new Error(store.options.formatter(errorCode, errorDetails)); + } + + next(); +} + + +function pushError(compilation) { + if (store.error) { + if (store.options.warn) { + compilation.warnings.push(store.error); + } else { + compilation.errors.push(store.error); + } + + store.error = null; + } +} + + +function FlowFlowPlugin(options) { + store.options = merge(store.options, options); +} + +FlowFlowPlugin.prototype.apply = function(compiler) { + compiler.plugin('run', checkFlowStatus); + compiler.plugin('watch-run', checkFlowStatus); + + compiler.plugin('compilation', pushError); +}; + +module.exports = FlowFlowPlugin; diff --git a/src/renderer/modal/modal.js b/src/renderer/modal/modal.js index 39b32c067..a139698e1 100644 --- a/src/renderer/modal/modal.js +++ b/src/renderer/modal/modal.js @@ -1,18 +1,19 @@ import React from "react"; +import PropTypes from "prop-types"; import ReactModal from "react-modal"; import Link from "component/link/index"; import app from "app.js"; export class Modal extends React.PureComponent { static propTypes = { - type: React.PropTypes.oneOf(["alert", "confirm", "custom"]), - overlay: React.PropTypes.bool, - onConfirmed: React.PropTypes.func, - onAborted: React.PropTypes.func, - confirmButtonLabel: React.PropTypes.string, - abortButtonLabel: React.PropTypes.string, - confirmButtonDisabled: React.PropTypes.bool, - abortButtonDisabled: React.PropTypes.bool, + type: PropTypes.oneOf(["alert", "confirm", "custom"]), + overlay: PropTypes.bool, + onConfirmed: PropTypes.func, + onAborted: PropTypes.func, + confirmButtonLabel: PropTypes.string, + abortButtonLabel: PropTypes.string, + confirmButtonDisabled: PropTypes.bool, + abortButtonDisabled: PropTypes.bool, }; static defaultProps = { @@ -64,8 +65,8 @@ export class Modal extends React.PureComponent { export class ExpandableModal extends React.PureComponent { static propTypes = { - expandButtonLabel: React.PropTypes.string, - extraContent: React.PropTypes.element, + expandButtonLabel: PropTypes.string, + extraContent: PropTypes.element, }; static defaultProps = { diff --git a/src/renderer/redux/actions/file_info.js b/src/renderer/redux/actions/file_info.js index 84062ee23..89e50386f 100644 --- a/src/renderer/redux/actions/file_info.js +++ b/src/renderer/redux/actions/file_info.js @@ -102,8 +102,8 @@ export function doDeleteFile(outpoint, deleteFromComputer, abandonClaim) { const fileInfo = byOutpoint[outpoint]; if (fileInfo) { - txid = fileInfo.outpoint.slice(0, -2); - nout = fileInfo.outpoint.slice(-1); + const txid = fileInfo.outpoint.slice(0, -2); + const nout = fileInfo.outpoint.slice(-1); dispatch(doAbandonClaim(txid, nout)); } diff --git a/src/renderer/redux/reducers/app.js b/src/renderer/redux/reducers/app.js index b44e84ccb..b18062e0c 100644 --- a/src/renderer/redux/reducers/app.js +++ b/src/renderer/redux/reducers/app.js @@ -1,22 +1,60 @@ +// @flow + import * as types from "constants/action_types"; import * as modalTypes from "constants/modal_types"; const { remote } = require("electron"); + const application = remote.app; const win = remote.BrowserWindow.getFocusedWindow(); const reducers = {}; -const defaultState = { + +export type SnackBar = { + message: string, + linkText: string, + linkTarget: string, + isError: boolean, +}; +export type AppState = { + isLoaded: boolean, + modal: ?string, + modalProps: mixed, + platform: string, + upgradeSkipped: boolean, + daemonVersionMatched: ?boolean, + daemonReady: boolean, + hasSignature: boolean, + badgeNumber: number, + volume: number, + downloadProgress: ?number, + upgradeDownloading: ?boolean, + upgradeDownloadComplete: ?boolean, + checkUpgradeTimer: ?number, + isUpgradeAvailable: ?boolean, + isUpgradeSkipped: ?boolean, + snackBar: ?SnackBar, +}; + +const defaultState: AppState = { isLoaded: false, modal: null, modalProps: {}, platform: process.platform, - upgradeSkipped: sessionStorage.getItem("upgradeSkipped"), + upgradeSkipped: sessionStorage.getItem("upgradeSkipped") === "true", daemonVersionMatched: null, daemonReady: false, hasSignature: false, badgeNumber: 0, - volume: sessionStorage.getItem("volume") || 1, + volume: Number(sessionStorage.getItem("volume")) || 1, + + downloadProgress: undefined, + upgradeDownloading: undefined, + upgradeDownloadComplete: undefined, + checkUpgradeTimer: undefined, + isUpgradeAvailable: undefined, + isUpgradeSkipped: undefined, + snackBar: undefined, }; reducers[types.DAEMON_READY] = function(state, action) { @@ -61,7 +99,7 @@ reducers[types.UPGRADE_DOWNLOAD_STARTED] = function(state, action) { }; reducers[types.SKIP_UPGRADE] = function(state, action) { - sessionStorage.setItem("upgradeSkipped", true); + sessionStorage.setItem("upgradeSkipped", "true"); return Object.assign({}, state, { isUpgradeSkipped: true, @@ -164,7 +202,7 @@ reducers[types.VOLUME_CHANGED] = function(state, action) { }); }; -export default function reducer(state = defaultState, action) { +export default function reducer(state: AppState = defaultState, action: any) { const handler = reducers[action.type]; if (handler) return handler(state, action); return state; diff --git a/src/renderer/rewards.js b/src/renderer/rewards.js index 2a30be2d3..1482fc1ca 100644 --- a/src/renderer/rewards.js +++ b/src/renderer/rewards.js @@ -22,7 +22,7 @@ function rewardMessage(type, amount) { amount ), many_downloads: __( - "You earned %s LBC for downloading some of the things.", + "You earned %s LBC for downloading a bunch of things.", amount ), first_publish: __( @@ -33,6 +33,10 @@ function rewardMessage(type, amount) { "You earned %s LBC for watching a featured download.", amount ), + referral: __( + "You earned %s LBC for referring someone.", + amount + ), }[type]; }