diff --git a/.eslintrc.json b/.eslintrc.json index 69a674b7a..009c389de 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -33,6 +33,7 @@ "printWidth": 100, "singleQuote": true }], - "func-names": ["warn", "as-needed"] + "func-names": ["warn", "as-needed"], + "arrow-body-style": "off" } } diff --git a/.flowconfig b/.flowconfig index f98db8793..664b2d8de 100644 --- a/.flowconfig +++ b/.flowconfig @@ -17,5 +17,8 @@ module.name_mapper='^types\(.*\)$' -> '/src/renderer/types\1' module.name_mapper='^component\(.*\)$' -> '/src/renderer/component\1' module.name_mapper='^page\(.*\)$' -> '/src/renderer/page\1' module.name_mapper='^lbry\(.*\)$' -> '/src/renderer/lbry\1' +module.name_mapper='^rewards\(.*\)$' -> '/src/renderer/rewards\1' +module.name_mapper='^modal\(.*\)$' -> '/src/renderer/modal\1' +module.name_mapper='^app\(.*\)$' -> '/src/renderer/app\1' [strict] diff --git a/.gitignore b/.gitignore index e8d5eb911..ce340404f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ /static/daemon/lbrynet* /static/locales yarn-error.log +npm-debug.log* diff --git a/flow-typed/react-feather.js b/flow-typed/react-feather.js new file mode 100644 index 000000000..c91a71f66 --- /dev/null +++ b/flow-typed/react-feather.js @@ -0,0 +1,3 @@ +declare module 'react-feather' { + declare module.exports: any; +} diff --git a/flow-typed/react-markdown.js b/flow-typed/react-markdown.js new file mode 100644 index 000000000..63b7f2aad --- /dev/null +++ b/flow-typed/react-markdown.js @@ -0,0 +1,3 @@ +declare module 'react-markdown' { + declare module.exports: any; +} diff --git a/flow-typed/react-modal.js b/flow-typed/react-modal.js new file mode 100644 index 000000000..17766e9f0 --- /dev/null +++ b/flow-typed/react-modal.js @@ -0,0 +1,3 @@ +declare module 'react-modal' { + declare module.exports: any; +} diff --git a/flow-typed/react-paginate.js b/flow-typed/react-paginate.js new file mode 100644 index 000000000..f161d1a42 --- /dev/null +++ b/flow-typed/react-paginate.js @@ -0,0 +1,3 @@ +declare module 'react-paginate' { + declare module.exports: any; +} diff --git a/flow-typed/react-simplemde-editor.js b/flow-typed/react-simplemde-editor.js new file mode 100644 index 000000000..7d23780f1 --- /dev/null +++ b/flow-typed/react-simplemde-editor.js @@ -0,0 +1,7 @@ +declare module 'react-simplemde-editor' { + declare module.exports: any; +} + +declare module 'react-simplemde-editor/dist/simplemde.min.css' { + declare module.exports: any; +} diff --git a/flow-typed/react-transition-group.js b/flow-typed/react-transition-group.js new file mode 100644 index 000000000..77bc87c09 --- /dev/null +++ b/flow-typed/react-transition-group.js @@ -0,0 +1,3 @@ +declare module 'react-transition-group' { + declare module.exports: any; +} diff --git a/flow-typed/render-media.js b/flow-typed/render-media.js new file mode 100644 index 000000000..acb885cc8 --- /dev/null +++ b/flow-typed/render-media.js @@ -0,0 +1,3 @@ +declare module 'render-media' { + declare module.exports: any; +} diff --git a/src/renderer/flow-typed/reselect.js b/flow-typed/reselect.js similarity index 100% rename from src/renderer/flow-typed/reselect.js rename to flow-typed/reselect.js diff --git a/package.json b/package.json index 702894dd5..992a96788 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "bluebird": "^3.5.1", "classnames": "^2.2.5", "country-data": "^0.0.31", + "dom-scroll-into-view": "^1.2.1", "electron-dl": "^1.11.0", "electron-is-dev": "^0.3.0", "electron-log": "^2.2.12", @@ -46,7 +47,10 @@ "electron-window-state": "^4.1.1", "find-process": "^1.1.0", "formik": "^0.10.4", - "keytar-prebuild": "4.0.4", + "from2": "^2.3.0", + "install": "^0.10.2", + "jshashes": "^1.0.7", + "keytar-prebuild": "4.1.1", "localforage": "^1.5.0", "mixpanel-browser": "^2.17.1", "moment": "^2.20.1", @@ -54,11 +58,13 @@ "rc-progress": "^2.0.6", "react": "^16.2.0", "react-dom": "^16.2.0", + "react-feather": "^1.0.8", "react-markdown": "^2.5.0", "react-modal": "^3.1.7", "react-paginate": "^5.2.1", "react-redux": "^5.0.3", "react-simplemde-editor": "^3.6.11", + "react-transition-group": "1.x", "redux": "^3.6.0", "redux-logger": "^3.0.1", "redux-persist": "^4.8.0", diff --git a/src/main/createWindow.js b/src/main/createWindow.js index afdfe5d65..d83f9b1c3 100644 --- a/src/main/createWindow.js +++ b/src/main/createWindow.js @@ -16,8 +16,8 @@ export default appState => { }); let windowConfiguration = { - backgroundColor: '#155B4A', - minWidth: 800, + backgroundColor: '#44b098', + minWidth: 950, minHeight: 600, autoHideMenuBar: true, show: false, diff --git a/src/renderer/component/address/view.jsx b/src/renderer/component/address/view.jsx index 65ea563c0..eff0f6697 100644 --- a/src/renderer/component/address/view.jsx +++ b/src/renderer/component/address/view.jsx @@ -1,52 +1,52 @@ -import React from 'react'; -import PropTypes from 'prop-types'; +// @flow +import * as React from 'react'; import { clipboard } from 'electron'; -import Link from 'component/link'; -import classnames from 'classnames'; +import { FormRow } from 'component/common/form'; +import Button from 'component/button'; +import * as icons from 'constants/icons'; -export default class Address extends React.PureComponent { - static propTypes = { - address: PropTypes.string, - }; +type Props = { + address: string, + doShowSnackBar: ({ message: string }) => void, +}; - constructor(props) { - super(props); +export default class Address extends React.PureComponent { + constructor() { + super(); - this._inputElem = null; + this.input = null; } + input: ?HTMLInputElement; + render() { - const { address, showCopyButton, doShowSnackBar } = this.props; + const { address, doShowSnackBar } = this.props; return ( -
+ { - this._inputElem = input; + this.input = input; }} onFocus={() => { - this._inputElem.select(); + if (this.input) { + this.input.select(); + } }} - readOnly="readonly" - value={address || ''} /> - {showCopyButton && ( - - { - clipboard.writeText(address); - doShowSnackBar({ message: __('Address copied') }); - }} - /> - - )} -
+ + ); + } +} + +export default Button; diff --git a/src/renderer/component/cardMedia/view.jsx b/src/renderer/component/cardMedia/view.jsx index 06f80f1d7..a0d4b6d4e 100644 --- a/src/renderer/component/cardMedia/view.jsx +++ b/src/renderer/component/cardMedia/view.jsx @@ -1,46 +1,51 @@ +// @flow import React from 'react'; +import classnames from 'classnames'; -class CardMedia extends React.PureComponent { - static AUTO_THUMB_CLASSES = [ - 'purple', - 'red', - 'pink', - 'indigo', - 'blue', - 'light-blue', - 'cyan', - 'teal', - 'green', - 'yellow', - 'orange', - ]; +type Props = { + thumbnail: ?string, // externally sourced image + nsfw: ?boolean, +}; - componentWillMount() { - this.setState({ - autoThumbClass: - CardMedia.AUTO_THUMB_CLASSES[ - Math.floor(Math.random() * CardMedia.AUTO_THUMB_CLASSES.length) - ], - }); - } +const autoThumbColors = [ + 'purple', + 'red', + 'pink', + 'indigo', + 'blue', + 'light-blue', + 'cyan', + 'teal', + 'green', + 'yellow', + 'orange', +]; + +class CardMedia extends React.PureComponent { + getAutoThumbClass = () => { + return autoThumbColors[Math.floor(Math.random() * autoThumbColors.length)]; + }; render() { - const { title, thumbnail } = this.props; - const atClass = this.state.autoThumbClass; + const { thumbnail, nsfw } = this.props; - if (thumbnail) { - return
; + const generateAutothumb = !thumbnail && !nsfw; + let autoThumbClass; + if (generateAutothumb) { + autoThumbClass = `card__media--autothumb.${this.getAutoThumbClass()}`; } return ( -
-
- {title && - title - .replace(/\s+/g, '') - .substring(0, Math.min(title.replace(' ', '').length, 5)) - .toUpperCase()} -
+
+ {(!thumbnail || nsfw) && ( + {nsfw ? __('NSFW') : 'LBRY'} + )}
); } diff --git a/src/renderer/component/cardVerify/view.jsx b/src/renderer/component/cardVerify/view.jsx index 4690b61d2..2df72f076 100644 --- a/src/renderer/component/cardVerify/view.jsx +++ b/src/renderer/component/cardVerify/view.jsx @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import Link from 'component/link'; +import Button from 'component/button'; +import * as icons from 'constants/icons'; let scriptLoading = false; let scriptLoaded = false; @@ -156,10 +157,10 @@ class CardVerify extends React.Component { render() { return ( - diff --git a/src/renderer/component/channelTile/view.jsx b/src/renderer/component/channelTile/view.jsx index 73efed63a..531b9fccd 100644 --- a/src/renderer/component/channelTile/view.jsx +++ b/src/renderer/component/channelTile/view.jsx @@ -1,18 +1,36 @@ -import React from 'react'; +// @flow +import * as React from 'react'; import CardMedia from 'component/cardMedia'; -import { TruncatedText, BusyMessage } from 'component/common.js'; +import TruncatedText from 'component/common/truncated-text'; -class ChannelTile extends React.PureComponent { +/* + This component can probably be combined with FileTile + Currently the only difference is showing the number of files/empty channel +*/ + +type Props = { + uri: string, + isResolvingUri: boolean, + totalItems: number, + claim: ?{ + claim_id: string, + name: string, + }, + resolveUri: string => void, + navigate: (string, ?{}) => void, +}; + +class ChannelTile extends React.PureComponent { componentDidMount() { const { uri, resolveUri } = this.props; resolveUri(uri); } - componentWillReceiveProps(nextProps) { + componentWillReceiveProps(nextProps: Props) { const { uri, resolveUri } = this.props; - if (nextProps.uri != uri) { + if (nextProps.uri !== uri) { resolveUri(uri); } } @@ -29,29 +47,25 @@ class ChannelTile extends React.PureComponent { const onClick = () => navigate('/show', { uri }); return ( -
-
-
- {channelName && } -
-
-

- {channelName || uri} -

+
+ +
+ {isResolvingUri &&
{__('Loading...')}
} + {!isResolvingUri && ( + +
+ {channelName || uri}
-
- {isResolvingUri && } +
{totalItems > 0 && ( - This is a channel with {totalItems} {totalItems === 1 ? ' item' : ' items'}{' '} - inside of it. + {totalItems} {totalItems === 1 ? 'file' : 'files'} )} - {!isResolvingUri && - !totalItems && This is an empty channel.} + {!isResolvingUri && !totalItems && This is an empty channel.}
-
-
+ + )}
); diff --git a/src/renderer/component/common.js b/src/renderer/component/common.js deleted file mode 100644 index 629d09f57..000000000 --- a/src/renderer/component/common.js +++ /dev/null @@ -1,172 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { formatCredits, formatFullPrice } from 'util/formatCredits'; -import lbry from '../lbry.js'; - -export class TruncatedText extends React.PureComponent { - static propTypes = { - lines: PropTypes.number, - }; - - static defaultProps = { - lines: null, - }; - - render() { - return ( - - {this.props.children} - - ); - } -} - -export class BusyMessage extends React.PureComponent { - static propTypes = { - message: PropTypes.string, - }; - - render() { - return ( - - {this.props.message} - - ); - } -} - -export class CurrencySymbol extends React.PureComponent { - render() { - return LBC; - } -} - -export class CreditAmount extends React.PureComponent { - static propTypes = { - amount: PropTypes.number.isRequired, - precision: PropTypes.number, - isEstimate: PropTypes.bool, - label: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), - showFree: PropTypes.bool, - showFullPrice: PropTypes.bool, - showPlus: PropTypes.bool, - look: PropTypes.oneOf(['indicator', 'plain', 'fee']), - }; - - static defaultProps = { - precision: 2, - label: true, - showFree: false, - look: 'indicator', - showFullPrice: false, - showPlus: false, - }; - - render() { - const minimumRenderableAmount = Math.pow(10, -1 * this.props.precision); - const { amount, precision, showFullPrice } = this.props; - - let formattedAmount; - const fullPrice = formatFullPrice(amount, 2); - - if (showFullPrice) { - formattedAmount = fullPrice; - } else { - formattedAmount = - amount > 0 && amount < minimumRenderableAmount - ? `<${minimumRenderableAmount}` - : formatCredits(amount, precision); - } - - let amountText; - if (this.props.showFree && parseFloat(this.props.amount) === 0) { - amountText = __('free'); - } else { - if (this.props.label) { - const label = - typeof this.props.label === 'string' - ? this.props.label - : parseFloat(amount) == 1 ? __('credit') : __('credits'); - - amountText = `${formattedAmount} ${label}`; - } else { - amountText = formattedAmount; - } - if (this.props.showPlus && amount > 0) { - amountText = `+${amountText}`; - } - } - - return ( - - {amountText} - {this.props.isEstimate ? ( - - * - - ) : null} - - ); - } -} - -export class Thumbnail extends React.PureComponent { - static propTypes = { - src: PropTypes.string, - }; - - handleError() { - if (this.state.imageUrl != this._defaultImageUri) { - this.setState({ - imageUri: this._defaultImageUri, - }); - } - } - - constructor(props) { - super(props); - - this._defaultImageUri = lbry.imagePath('default-thumb.svg'); - this._maxLoadTime = 10000; - this._isMounted = false; - - this.state = { - imageUri: this.props.src || this._defaultImageUri, - }; - } - - componentDidMount() { - this._isMounted = true; - setTimeout(() => { - if (this._isMounted && !this.refs.img.complete) { - this.setState({ - imageUri: this._defaultImageUri, - }); - } - }, this._maxLoadTime); - } - - componentWillUnmount() { - this._isMounted = false; - } - - render() { - const className = this.props.className ? this.props.className : '', - otherProps = Object.assign({}, this.props); - delete otherProps.className; - return ( - { - this.handleError(); - }} - {...otherProps} - className={className} - src={this.state.imageUri} - /> - ); - } -} diff --git a/src/renderer/component/common/busy-indicator.jsx b/src/renderer/component/common/busy-indicator.jsx new file mode 100644 index 000000000..ee95a00ca --- /dev/null +++ b/src/renderer/component/common/busy-indicator.jsx @@ -0,0 +1,16 @@ +// @flow +import React from 'react'; + +type Props = { + message: ?string, +}; + +const BusyIndicator = (props: Props) => { + return ( + + {props.message} + + ); +}; + +export default BusyIndicator; diff --git a/src/renderer/component/common/category-list.jsx b/src/renderer/component/common/category-list.jsx new file mode 100644 index 000000000..13aacab76 --- /dev/null +++ b/src/renderer/component/common/category-list.jsx @@ -0,0 +1,245 @@ +// @flow +import React from 'react'; +import { normalizeURI } from 'lbryURI'; +import ToolTip from 'component/common/tooltip'; +import FileCard from 'component/fileCard'; +import Button from 'component/button'; +import * as icons from 'constants/icons'; + +type Props = { + category: string, + names: Array, + categoryLink?: string, +}; + +type State = { + canScrollNext: boolean, + canScrollPrevious: boolean, +}; + +class CategoryList extends React.PureComponent { + constructor() { + super(); + + this.state = { + canScrollPrevious: false, + canScrollNext: false, + }; + + (this: any).handleScrollNext = this.handleScrollNext.bind(this); + (this: any).handleScrollPrevious = this.handleScrollPrevious.bind(this); + this.rowItems = undefined; + } + + componentDidMount() { + const cardRow = this.rowItems; + if (cardRow) { + const cards = cardRow.getElementsByTagName('section'); + const lastCard = cards[cards.length - 1]; + const isCompletelyVisible = this.isCardVisible(lastCard); + + if (!isCompletelyVisible) { + // not sure how we can avoid doing this + /* eslint-disable react/no-did-mount-set-state */ + this.setState({ + canScrollNext: true, + }); + /* eslint-enable react/no-did-mount-set-state */ + } + } + } + + rowItems: ?HTMLDivElement; + + handleScroll(cardRow: HTMLDivElement, scrollTarget: number) { + const cards = cardRow.getElementsByTagName('section'); + const animationCallback = () => { + const firstCard = cards[0]; + const lastCard = cards[cards.length - 1]; + const firstCardVisible = this.isCardVisible(firstCard); + const lastCardVisible = this.isCardVisible(lastCard); + this.setState({ + canScrollNext: !lastCardVisible, + canScrollPrevious: !firstCardVisible, + }); + }; + + const currentScrollLeft = cardRow.scrollLeft; + const direction = currentScrollLeft > scrollTarget ? 'left' : 'right'; + this.scrollCardsAnimated(cardRow, scrollTarget, direction, animationCallback); + } + + scrollCardsAnimated = ( + cardRow: HTMLDivElement, + scrollTarget: number, + direction: string, + callback: () => any + ) => { + let start; + const step = timestamp => { + if (!start) start = timestamp; + + const currentLeftVal = cardRow.scrollLeft; + + let newTarget; + let shouldContinue; + let progress = currentLeftVal; + + if (direction === 'right') { + progress += timestamp - start; + newTarget = Math.min(progress, scrollTarget); + shouldContinue = newTarget < scrollTarget; + } else { + progress -= timestamp - start; + newTarget = Math.max(progress, scrollTarget); + shouldContinue = newTarget > scrollTarget; + } + + cardRow.scrollLeft = newTarget; // eslint-disable-line no-param-reassign + + if (shouldContinue) { + window.requestAnimationFrame(step); + } else { + callback(); + } + }; + + window.requestAnimationFrame(step); + }; + + // check if a card is fully visible horizontally + isCardVisible = (section: HTMLElement) => { + const rect = section.getBoundingClientRect(); + const isVisible = rect.left >= 0 && rect.right <= window.innerWidth; + return isVisible; + }; + + handleScrollNext() { + const cardRow = this.rowItems; + if (cardRow) { + const cards = cardRow.getElementsByTagName('section'); + + // loop over items until we find one that is on the screen + // continue searching until a card isn't fully visible, this is the new target + let firstFullVisibleCard; + let firstSemiVisibleCard; + + for (let i = 0; i < cards.length; i += 1) { + const currentCardVisible = this.isCardVisible(cards[i]); + + if (firstFullVisibleCard && !currentCardVisible) { + firstSemiVisibleCard = cards[i]; + break; + } else if (currentCardVisible) { + [firstFullVisibleCard] = cards; + } + } + + if (firstFullVisibleCard && firstSemiVisibleCard) { + const scrollTarget = firstSemiVisibleCard.offsetLeft - firstFullVisibleCard.offsetLeft; + this.handleScroll(cardRow, scrollTarget); + } + } + } + + handleScrollPrevious() { + const cardRow = this.rowItems; + if (cardRow) { + const cards = cardRow.getElementsByTagName('section'); + + let hasFoundCard; + let numberOfCardsThatCanFit = 0; + + // loop starting at the end until we find a visible card + // then count to find how many cards can fit on the screen + for (let i = cards.length - 1; i >= 0; i -= 1) { + const currentCard = cards[i]; + const isCurrentCardVisible = this.isCardVisible(currentCard); + + if (isCurrentCardVisible) { + if (!hasFoundCard) { + hasFoundCard = true; + } + + numberOfCardsThatCanFit += 1; + } else if (hasFoundCard) { + // this card is off the screen to the left + // we know how many cards can fit on a screen + // find the new target and scroll + const firstCardOffsetLeft = cards[0].offsetLeft; + const cardIndexToScrollTo = i + 1 - numberOfCardsThatCanFit; + const newFirstCard = cards[cardIndexToScrollTo]; + + let scrollTarget; + if (newFirstCard) { + scrollTarget = newFirstCard.offsetLeft; + } else { + // more cards can fit on the screen than are currently hidden + // just scroll to the first card + scrollTarget = cards[0].offsetLeft; + } + + scrollTarget -= firstCardOffsetLeft; // to play nice with the margins + + this.handleScroll(cardRow, scrollTarget); + break; + } + } + } + } + + render() { + const { category, names, categoryLink } = this.props; + const { canScrollNext, canScrollPrevious } = this.state; + + // The lint was throwing an error saying we should use
+
+
+
+
{ + this.rowItems = ref; + }} + className="card-row__scrollhouse" + > + {names && + names.map(name => )} +
+ + ); + } +} + +export default CategoryList; diff --git a/src/renderer/component/common/credit-amount.jsx b/src/renderer/component/common/credit-amount.jsx new file mode 100644 index 000000000..0147c0154 --- /dev/null +++ b/src/renderer/component/common/credit-amount.jsx @@ -0,0 +1,100 @@ +// @flow +import React from 'react'; +import classnames from 'classnames'; +import { formatCredits, formatFullPrice } from 'util/formatCredits'; + +type Props = { + amount: number, + precision: number, + showFree: boolean, + showFullPrice: boolean, + showPlus: boolean, + isEstimate?: boolean, + large?: boolean, + plain?: boolean, + fee?: boolean, + noStyle?: boolean, +}; + +class CreditAmount extends React.PureComponent { + static defaultProps = { + precision: 2, + showFree: false, + showFullPrice: false, + showPlus: false, + }; + + render() { + const { + amount, + precision, + showFullPrice, + showFree, + showPlus, + large, + isEstimate, + plain, + noStyle, + fee, + } = this.props; + + const minimumRenderableAmount = 10 ** (-1 * precision); + const fullPrice = formatFullPrice(amount, 2); + const isFree = parseFloat(amount) === 0; + + let formattedAmount; + if (showFullPrice) { + formattedAmount = fullPrice; + } else { + formattedAmount = + amount > 0 && amount < minimumRenderableAmount + ? `<${minimumRenderableAmount}` + : formatCredits(amount, precision); + } + + let amountText; + if (showFree && isFree) { + amountText = __('FREE'); + } else { + amountText = formattedAmount; + + if (showPlus && amount > 0) { + amountText = `+${amountText}`; + } + + if (!plain) { + amountText = `${amountText} ${__('LBC')}`; + } + + if (fee) { + amountText = `${amountText} ${__('fee')}`; + } + } + + return ( + + {amountText} + + {isEstimate ? ( + + * + + ) : null} + + ); + } +} + +export default CreditAmount; diff --git a/src/renderer/component/file-exporter.js b/src/renderer/component/common/file-exporter.jsx similarity index 72% rename from src/renderer/component/file-exporter.js rename to src/renderer/component/common/file-exporter.jsx index 7c9fed011..8ab48e05c 100644 --- a/src/renderer/component/file-exporter.js +++ b/src/renderer/component/common/file-exporter.jsx @@ -1,31 +1,35 @@ +// @flow import fs from 'fs'; import path from 'path'; import React from 'react'; import PropTypes from 'prop-types'; -import Link from 'component/link'; +import Button from 'component/button'; import parseData from 'util/parseData'; import * as icons from 'constants/icons'; -const { remote } = require('electron'); +import { remote } from 'electron'; -class FileExporter extends React.PureComponent { - static propTypes = { - data: PropTypes.array, - title: PropTypes.string, - label: PropTypes.string, - filters: PropTypes.arrayOf(PropTypes.string), - defaultPath: PropTypes.string, - onFileCreated: PropTypes.func, - }; +type Props = { + data: Array, + title: string, + label: string, + defaultPath?: string, + filters: Array, + onFileCreated?: string => void, +}; +class FileExporter extends React.PureComponent { static defaultProps = { filters: [], }; - constructor(props) { - super(props); + constructor() { + super(); + this.handleButtonClick = this.handleButtonClick.bind(this); } - handleFileCreation(filename, data) { + handleButtonClick: () => void; + + handleFileCreation(filename: string, data: any) { const { onFileCreated } = this.props; fs.writeFile(filename, data, err => { if (err) throw err; @@ -67,12 +71,11 @@ class FileExporter extends React.PureComponent { render() { const { title, label } = this.props; return ( - this.handleButtonClick()} + onClick={this.handleButtonClick} /> ); } diff --git a/src/renderer/component/common/file-selector.jsx b/src/renderer/component/common/file-selector.jsx new file mode 100644 index 000000000..d5d12cfde --- /dev/null +++ b/src/renderer/component/common/file-selector.jsx @@ -0,0 +1,77 @@ +// @flow +import React from 'react'; +import { remote } from 'electron'; +import Button from 'component/button'; +import { FormRow } from 'component/common/form'; +import path from 'path'; + +type Props = { + type: string, + currentPath: ?string, + onFileChosen: (string, string) => void, +}; + +class FileSelector extends React.PureComponent { + static defaultProps = { + type: 'file', + }; + + constructor() { + super(); + this.input = null; + } + + handleButtonClick() { + remote.dialog.showOpenDialog( + { + properties: + this.props.type === 'file' ? ['openFile'] : ['openDirectory', 'createDirectory'], + }, + paths => { + if (!paths) { + // User hit cancel, so do nothing + return; + } + + const filePath = paths[0]; + const extension = path.extname(filePath); + const fileName = path.basename(filePath, extension); + + if (this.props.onFileChosen) { + this.props.onFileChosen(filePath, fileName); + } + } + ); + } + + input: ?HTMLInputElement; + + render() { + const { type, currentPath } = this.props; + + return ( + + +
{body}
+ + ); + } +} + +export default ToolTip; diff --git a/src/renderer/component/common/transaction-link.jsx b/src/renderer/component/common/transaction-link.jsx new file mode 100644 index 000000000..2d48feb96 --- /dev/null +++ b/src/renderer/component/common/transaction-link.jsx @@ -0,0 +1,18 @@ +// @flow +import React from 'react'; +import Button from 'component/button'; + +type Props = { + id: string, +}; + +const TransactionLink = (props: Props) => { + const { id } = props; + + const href = `https://explorer.lbry.io/#!/transaction/${id}`; + const label = id.substr(0, 7); + + return {' '} - - { - this._inputElem = input; - }} - onFocus={() => { - this._inputElem.select(); - }} - readOnly="readonly" - value={this.state.path || __('No File Chosen')} - /> - - - ); - } -} - -export default FileSelector; diff --git a/src/renderer/component/fileActions/view.jsx b/src/renderer/component/fileActions/view.jsx index b2d83f3b9..0ffb21468 100644 --- a/src/renderer/component/fileActions/view.jsx +++ b/src/renderer/component/fileActions/view.jsx @@ -1,52 +1,47 @@ +// @flow import React from 'react'; -import Link from 'component/link'; +import Button from 'component/button'; import FileDownloadLink from 'component/fileDownloadLink'; import * as modals from 'constants/modal_types'; +import classnames from 'classnames'; +import * as icons from 'constants/icons'; -class FileActions extends React.PureComponent { +type FileInfo = { + claim_id: string, +}; + +type Props = { + uri: string, + openModal: (string, any) => void, + claimIsMine: boolean, + fileInfo: FileInfo, + vertical?: boolean, // should the buttons be stacked vertically? +}; + +class FileActions extends React.PureComponent { render() { - const { fileInfo, uri, openModal, claimIsMine } = this.props; + const { fileInfo, uri, openModal, claimIsMine, vertical } = this.props; - const claimId = fileInfo ? fileInfo.claim_id : null, - showDelete = fileInfo && Object.keys(fileInfo).length > 0; + const claimId = fileInfo ? fileInfo.claim_id : ''; + const showDelete = fileInfo && Object.keys(fileInfo).length > 0; return ( -
+
{showDelete && ( - openModal(modals.CONFIRM_FILE_REMOVE, { uri })} /> )} {!claimIsMine && ( - - )} - - {claimIsMine && ( - )}
diff --git a/src/renderer/component/fileCard/index.js b/src/renderer/component/fileCard/index.js index f324e93a3..12bc9e649 100644 --- a/src/renderer/component/fileCard/index.js +++ b/src/renderer/component/fileCard/index.js @@ -6,16 +6,31 @@ import { selectShowNsfw } from 'redux/selectors/settings'; import { makeSelectClaimForUri, makeSelectMetadataForUri } from 'redux/selectors/claims'; import { makeSelectFileInfoForUri } from 'redux/selectors/file_info'; import { makeSelectIsUriResolving, selectRewardContentClaimIds } from 'redux/selectors/content'; +import { selectPendingPublish } from 'redux/selectors/publish'; import FileCard from './view'; -const select = (state, props) => ({ - claim: makeSelectClaimForUri(props.uri)(state), - fileInfo: makeSelectFileInfoForUri(props.uri)(state), - obscureNsfw: !selectShowNsfw(state), - metadata: makeSelectMetadataForUri(props.uri)(state), - rewardedContentClaimIds: selectRewardContentClaimIds(state, props), - isResolvingUri: makeSelectIsUriResolving(props.uri)(state), -}); +const select = (state, props) => { + let claim; + let fileInfo; + let metadata; + let isResolvingUri; + + const pendingPublish = selectPendingPublish(props.uri)(state); + + const fileCardInfo = pendingPublish || { + claim: makeSelectClaimForUri(props.uri)(state), + fileInfo: makeSelectFileInfoForUri(props.uri)(state), + metadata: makeSelectMetadataForUri(props.uri)(state), + isResolvingUri: makeSelectIsUriResolving(props.uri)(state), + }; + + return { + obscureNsfw: !selectShowNsfw(state), + rewardedContentClaimIds: selectRewardContentClaimIds(state, props), + ...fileCardInfo, + pending: !!pendingPublish, + }; +}; const perform = dispatch => ({ navigate: (path, params) => dispatch(doNavigate(path, params)), diff --git a/src/renderer/component/fileCard/view.jsx b/src/renderer/component/fileCard/view.jsx index 19b1370da..1e23ad092 100644 --- a/src/renderer/component/fileCard/view.jsx +++ b/src/renderer/component/fileCard/view.jsx @@ -1,111 +1,102 @@ -import React from 'react'; +// @flow +import * as React from 'react'; import { normalizeURI } from 'lbryURI'; import CardMedia from 'component/cardMedia'; -import Link from 'component/link'; -import { TruncatedText } from 'component/common'; -import Icon from 'component/icon'; +import TruncatedText from 'component/common/truncated-text'; +import Icon from 'component/common/icon'; import FilePrice from 'component/filePrice'; import UriIndicator from 'component/uriIndicator'; import NsfwOverlay from 'component/nsfwOverlay'; -import TruncatedMarkdown from 'component/truncatedMarkdown'; import * as icons from 'constants/icons'; +import classnames from 'classnames'; -class FileCard extends React.PureComponent { - constructor(props) { - super(props); +// TODO: iron these out +type Props = { + isResolvingUri: boolean, + resolveUri: string => void, + uri: string, + claim: ?{ claim_id: string }, + fileInfo: ?{}, + metadata: ?{ nsfw: boolean, thumbnail: ?string }, + navigate: (string, ?{}) => void, + rewardedContentClaimIds: Array, + obscureNsfw: boolean, + showPrice: boolean, + pending?: boolean, +}; - this.state = { - hovered: false, - }; - } +class FileCard extends React.PureComponent { + static defaultProps = { + showPrice: true, + }; componentWillMount() { this.resolve(this.props); } - componentWillReceiveProps(nextProps) { + componentWillReceiveProps(nextProps: Props) { this.resolve(nextProps); } - resolve(props) { + resolve = (props: Props) => { const { isResolvingUri, resolveUri, claim, uri } = props; if (!isResolvingUri && claim === undefined && uri) { resolveUri(uri); } - } - - handleMouseOver() { - this.setState({ - hovered: true, - }); - } - - handleMouseOut() { - this.setState({ - hovered: false, - }); - } + }; render() { const { claim, fileInfo, metadata, - isResolvingUri, navigate, rewardedContentClaimIds, + obscureNsfw, + showPrice, + pending, } = this.props; - - const uri = normalizeURI(this.props.uri); + const uri = !pending ? normalizeURI(this.props.uri) : this.props.uri; const title = metadata && metadata.title ? metadata.title : uri; const thumbnail = metadata && metadata.thumbnail ? metadata.thumbnail : null; - const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw; + const shouldObscureNsfw = obscureNsfw && metadata && metadata.nsfw; const isRewardContent = claim && rewardedContentClaimIds.includes(claim.claim_id); - let description = ''; - if (isResolvingUri && !claim) { - description = __('Loading...'); - } else if (metadata && metadata.description) { - description = metadata.description; - } else if (claim === null) { - description = __('This address contains no content.'); - } - + // We should be able to tab through cards + /* eslint-disable jsx-a11y/click-events-have-key-events */ return (
navigate('/show', { uri }) : () => {}} + className={classnames('card card--small', { + 'card--link': !pending, + 'card--pending': pending, + })} > -
- navigate('/show', { uri })} className="card__link"> - -
-
- {title} -
-
- - {' '} - {isRewardContent && }{' '} - {fileInfo && } - - - - -
-
- - {/* Test for nizuka's design: should we remove description? -
- {description} -
- */} + +
{showPrice && }
+ +
+
+ {title} +
+
+ {pending ? ( +
Pending...
+ ) : ( + + + {isRewardContent && } + {fileInfo && } + + )} +
- {obscureNsfw && this.state.hovered && }
); + /* eslint-enable jsx-a11y/click-events-have-key-events */ } } diff --git a/src/renderer/component/fileDetails/view.jsx b/src/renderer/component/fileDetails/view.jsx index 9b6753484..3912729ad 100644 --- a/src/renderer/component/fileDetails/view.jsx +++ b/src/renderer/component/fileDetails/view.jsx @@ -1,70 +1,81 @@ -import React from 'react'; +// @flow +import * as React from 'react'; import ReactMarkdown from 'react-markdown'; -import lbry from 'lbry.js'; -import FileActions from 'component/fileActions'; -import Link from 'component/link'; -import DateTime from 'component/dateTime'; +import lbry from 'lbry'; +import Button from 'component/button'; +import path from 'path'; -const path = require('path'); +type Props = { + claim: {}, + fileInfo: { + download_path: string, + }, + metadata: { + description: string, + language: string, + license: string, + }, + openFolder: string => void, + contentType: string, +}; -class FileDetails extends React.PureComponent { - render() { - const { claim, contentType, fileInfo, metadata, openFolder, uri } = this.props; - - if (!claim || !metadata) { - return ( -
- {__('Empty claim or metadata info.')} -
- ); - } - - const { description, language, license } = metadata; - const mediaType = lbry.getMediaType(contentType); - - const downloadPath = fileInfo ? path.normalize(fileInfo.download_path) : null; +const FileDetails = (props: Props) => { + const { claim, contentType, fileInfo, metadata, openFolder } = props; + if (!claim || !metadata) { return ( -
-
- -
-
- -
-
- - - - - - - - - - - - - - - {downloadPath && ( - - - - - )} - -
{__('Content-Type')}{mediaType}
{__('Language')}{language}
{__('License')}{license}
{__('Downloaded to')} - openFolder(downloadPath)}>{downloadPath} -
-
+
+ {__('Empty claim or metadata info.')}
); } -} + + const { description, language, license } = metadata; + const mediaType = lbry.getMediaType(contentType); + + const downloadPath = fileInfo ? path.normalize(fileInfo.download_path) : null; + + return ( + + {description && ( + +
About
+
+ +
+
+ )} +
Info
+
+
+ {__('Content-Type')} + {': '} + {mediaType} +
+
+ {__('Language')} + {': '} + {language} +
+
+ {__('License')} + {': '} + {license} +
+ {downloadPath && ( +
+ {__('Downloaded to')} + {': '} +
+ )} +
+
+ ); +}; export default FileDetails; diff --git a/src/renderer/component/fileDownloadLink/view.jsx b/src/renderer/component/fileDownloadLink/view.jsx index 57818da51..c2c413bd6 100644 --- a/src/renderer/component/fileDownloadLink/view.jsx +++ b/src/renderer/component/fileDownloadLink/view.jsx @@ -1,7 +1,7 @@ import React from 'react'; -import { BusyMessage } from 'component/common'; -import Icon from 'component/icon'; -import Link from 'component/link'; +import Button from 'component/button'; +import classnames from 'classnames'; +import * as icons from 'constants/icons'; class FileDownloadLink extends React.PureComponent { componentWillMount() { @@ -53,38 +53,34 @@ class FileDownloadLink extends React.PureComponent { if (loading || downloading) { const progress = - fileInfo && fileInfo.written_bytes - ? fileInfo.written_bytes / fileInfo.total_bytes * 100 - : 0, - label = fileInfo ? progress.toFixed(0) + __('% complete') : __('Connecting...'), - labelWithIcon = ( - - - {label} - - ); + fileInfo && fileInfo.written_bytes + ? fileInfo.written_bytes / fileInfo.total_bytes * 100 + : 0; + const label = fileInfo ? progress.toFixed(0) + __('% complete') : __('Connecting...'); return ( -
+
- {labelWithIcon} + {label}
- {labelWithIcon} + {label}
); } else if (fileInfo === null && !downloading) { if (!costInfo) { - return ; + return null; } + return ( - { purchaseUri(uri); }} @@ -92,11 +88,10 @@ class FileDownloadLink extends React.PureComponent { ); } else if (fileInfo && fileInfo.download_path) { return ( - openFile()} /> ); diff --git a/src/renderer/component/fileList/view.jsx b/src/renderer/component/fileList/view.jsx index 9c926e825..0e3b730f9 100644 --- a/src/renderer/component/fileList/view.jsx +++ b/src/renderer/component/fileList/view.jsx @@ -1,21 +1,54 @@ -import React from 'react'; +// @flow +import * as React from 'react'; import { buildURI } from 'lbryURI'; -import FormField from 'component/formField'; -import FileTile from 'component/fileTile'; -import { BusyMessage } from 'component/common.js'; +import { FormField } from 'component/common/form'; +import FileCard from 'component/fileCard'; -class FileList extends React.PureComponent { - constructor(props) { +type FileInfo = { + name: string, + channelName: ?string, + pending?: boolean, + value?: { + publisherSignature: { + certificateId: string, + }, + }, + metadata: { + publisherSignature: { + certificateId: string, + }, + }, +}; + +type Props = { + hideFilter: boolean, + fileInfos: Array, +}; + +type State = { + sortBy: string, +}; + +class FileList extends React.PureComponent { + static defaultProps = { + hideFilter: false, + }; + + constructor(props: Props) { super(props); this.state = { sortBy: 'dateNew', }; - this._sortFunctions = { + this.sortFunctions = { dateNew: fileInfos => this.props.sortByHeight ? fileInfos.slice().sort((fileInfo1, fileInfo2) => { + if (fileInfo1.pending) { + return -1; + } + const height1 = this.props.claimsById[fileInfo1.claim_id] ? this.props.claimsById[fileInfo1.claim_id].height : 0; @@ -76,60 +109,62 @@ class FileList extends React.PureComponent { }; } - getChannelSignature(fileInfo) { + getChannelSignature = (fileInfo: FileInfo) => { + if (fileInfo.pending) { + return undefined; + } + if (fileInfo.value) { return fileInfo.value.publisherSignature.certificateId; } return fileInfo.channel_claim_id; - } + }; - handleSortChanged(event) { + handleSortChanged(event: SyntheticInputEvent<*>) { this.setState({ sortBy: event.target.value, }); } render() { - const { handleSortChanged, fetching, fileInfos } = this.props; + const { fileInfos, hideFilter } = this.props; const { sortBy } = this.state; const content = []; - this._sortFunctions[sortBy](fileInfos).forEach(fileInfo => { + this.sortFunctions[sortBy](fileInfos).forEach(fileInfo => { + const { channel_name: channelName, name: claimName, claim_id: claimId } = fileInfo; const uriParams = {}; - if (fileInfo.channel_name) { - uriParams.channelName = fileInfo.channel_name; - uriParams.contentName = fileInfo.claim_name || fileInfo.name; + if (channelName) { + uriParams.channelName = channelName; + uriParams.contentName = claimName; uriParams.claimId = this.getChannelSignature(fileInfo); } else { - uriParams.claimId = fileInfo.claim_id; - uriParams.claimName = fileInfo.claim_name || fileInfo.name; + uriParams.claimId = claimId; + uriParams.claimName = claimName; } + const uri = buildURI(uriParams); - content.push( - - ); + content.push(); }); + return ( -
- {fetching && } - - {__('Sort by')}{' '} - - - - - - - {content} +
+
+ {!hideFilter && ( + + + + + )} +
+
{content}
); } diff --git a/src/renderer/component/fileListSearch/index.js b/src/renderer/component/fileListSearch/index.js index ec06231af..328295768 100644 --- a/src/renderer/component/fileListSearch/index.js +++ b/src/renderer/component/fileListSearch/index.js @@ -1,12 +1,13 @@ -import React from 'react'; import { connect } from 'react-redux'; import { doSearch } from 'redux/actions/search'; -import { selectIsSearching, makeSelectSearchUris } from 'redux/selectors/search'; +import { makeSelectSearchUris, selectIsSearching } from 'redux/selectors/search'; +import { selectSearchDownloadUris } from 'redux/selectors/file_info'; import FileListSearch from './view'; const select = (state, props) => ({ - isSearching: selectIsSearching(state), uris: makeSelectSearchUris(props.query)(state), + downloadUris: selectSearchDownloadUris(props.query)(state), + isSearching: selectIsSearching(state), }); const perform = dispatch => ({ diff --git a/src/renderer/component/fileListSearch/view.jsx b/src/renderer/component/fileListSearch/view.jsx index 89e99ca65..53add5ab7 100644 --- a/src/renderer/component/fileListSearch/view.jsx +++ b/src/renderer/component/fileListSearch/view.jsx @@ -1,58 +1,95 @@ +// @flow import React from 'react'; import FileTile from 'component/fileTile'; import ChannelTile from 'component/channelTile'; -import Link from 'component/link'; -import { BusyMessage } from 'component/common.js'; import { parseURI } from 'lbryURI'; +import debounce from 'util/debounce'; -const SearchNoResults = props => { - const { query } = props; +const SEARCH_DEBOUNCE_TIME = 800; - return ( -
- - {(__('No one has checked anything in for %s yet.'), query)}{' '} - - -
- ); +const NoResults = () => { + return
{__('No results')}
; }; -class FileListSearch extends React.PureComponent { - componentWillMount() { - this.doSearch(this.props); +type Props = { + search: string => void, + query: string, + isSearching: boolean, + uris: ?Array, + downloadUris: ?Array, +}; + +class FileListSearch extends React.PureComponent { + constructor(props: Props) { + super(props); + this.debouncedSearch = debounce(this.props.search, SEARCH_DEBOUNCE_TIME); } - componentWillReceiveProps(props) { - if (props.query != this.props.query) { - this.doSearch(props); + componentDidMount() { + const { search, query } = this.props; + search(query); + } + + componentWillReceiveProps(nextProps: Props) { + const { query: nextQuery } = nextProps; + const { query: currentQuerry } = this.props; + + if (nextQuery !== currentQuerry) { + this.debouncedSearch(nextQuery); } } - doSearch(props) { - this.props.search(props.query); - } + debouncedSearch: string => void; render() { - const { isSearching, uris, query } = this.props; + const { uris, query, downloadUris, isSearching } = this.props; + + const fileResults = []; + const channelResults = []; + if (uris && uris.length) { + uris.forEach(uri => { + const isChannel = parseURI(uri).claimName[0] === '@'; + if (isChannel) { + channelResults.push(uri); + } else { + fileResults.push(uri); + } + }); + } return ( -
- {isSearching && !uris && } + query && ( +
+
+
{__('Files')}
+ {!isSearching && + (fileResults.length ? ( + fileResults.map(uri => ) + ) : ( + + ))} +
- {isSearching && uris && } +
+
{__('Channels')}
+ {!isSearching && + (channelResults.length ? ( + channelResults.map(uri => ) + ) : ( + + ))} +
- {uris && uris.length - ? uris.map( - uri => - parseURI(uri).claimName[0] === '@' ? ( - - ) : ( - - ) - ) - : !isSearching && } -
+
+
{__('Your downloads')}
+ {downloadUris && downloadUris.length ? ( + downloadUris.map(uri => ) + ) : ( + + )} +
+
+ ) ); } } diff --git a/src/renderer/component/filePrice/view.jsx b/src/renderer/component/filePrice/view.jsx index 5a5947f14..090c93126 100644 --- a/src/renderer/component/filePrice/view.jsx +++ b/src/renderer/component/filePrice/view.jsx @@ -1,35 +1,48 @@ +// @flow import React from 'react'; -import { CreditAmount } from 'component/common'; +import CreditAmount from 'component/common/credit-amount'; + +type Props = { + showFullPrice: boolean, + costInfo: ?{ includesData: boolean, cost: number }, + fetchCostInfo: string => void, + uri: string, + fetching: boolean, + claim: ?{}, +}; + +class FilePrice extends React.PureComponent { + static defaultProps = { + showFullPrice: false, + }; -class FilePrice extends React.PureComponent { componentWillMount() { this.fetchCost(this.props); } - componentWillReceiveProps(nextProps) { + componentWillReceiveProps(nextProps: Props) { this.fetchCost(nextProps); } - fetchCost(props) { + fetchCost = (props: Props) => { const { costInfo, fetchCostInfo, uri, fetching, claim } = props; if (costInfo === undefined && !fetching && claim) { fetchCostInfo(uri); } - } + }; render() { - const { costInfo, look = 'indicator', showFullPrice = false } = this.props; + const { costInfo, showFullPrice } = this.props; - const isEstimate = costInfo ? !costInfo.includesData : null; + const isEstimate = costInfo ? !costInfo.includesData : false; if (!costInfo) { - return ???; + return PRICE; } return ( ({ claim: makeSelectClaimForUri(props.uri)(state), - fileInfo: makeSelectFileInfoForUri(props.uri)(state), - obscureNsfw: !selectShowNsfw(state), + isDownloaded: !!makeSelectFileInfoForUri(props.uri)(state), metadata: makeSelectMetadataForUri(props.uri)(state), isResolvingUri: makeSelectIsUriResolving(props.uri)(state), rewardedContentClaimIds: selectRewardContentClaimIds(state, props), diff --git a/src/renderer/component/fileTile/view.jsx b/src/renderer/component/fileTile/view.jsx index 6bbc3b4c6..7761b9f4e 100644 --- a/src/renderer/component/fileTile/view.jsx +++ b/src/renderer/component/fileTile/view.jsx @@ -1,132 +1,115 @@ -import React from 'react'; +// @flow +import * as React from 'react'; import * as icons from 'constants/icons'; import { normalizeURI, isURIClaimable, parseURI } from 'lbryURI'; import CardMedia from 'component/cardMedia'; -import { TruncatedText } from 'component/common.js'; +import TruncatedText from 'component/common/truncated-text'; import FilePrice from 'component/filePrice'; -import NsfwOverlay from 'component/nsfwOverlay'; -import Icon from 'component/icon'; +import Icon from 'component/common/icon'; +import Button from 'component/button'; +import classnames from 'classnames'; -class FileTile extends React.PureComponent { - static SHOW_EMPTY_PUBLISH = 'publish'; - static SHOW_EMPTY_PENDING = 'pending'; +type Props = { + fullWidth: boolean, // removes the max-width css + showUri: boolean, + showLocal: boolean, + isDownloaded: boolean, + uri: string, + isResolvingUri: boolean, + rewardedContentClaimIds: Array, + claim: ?{ + name: string, + channel_name: string, + claim_id: string, + }, + metadata: {}, + resolveUri: string => void, + navigate: (string, ?{}) => void, +}; +class FileTile extends React.PureComponent { static defaultProps = { - showPrice: true, - showLocal: true, + showUri: false, + showLocal: false, + fullWidth: false, }; - constructor(props) { - super(props); - this.state = { - showNsfwHelp: false, - }; - } - componentDidMount() { const { isResolvingUri, claim, uri, resolveUri } = this.props; - if (!isResolvingUri && !claim && uri) resolveUri(uri); } - componentWillReceiveProps(nextProps) { - const { isResolvingUri, claim, uri, resolveUri } = this.props; - + componentWillReceiveProps(nextProps: Props) { + const { isResolvingUri, claim, uri, resolveUri } = nextProps; if (!isResolvingUri && claim === undefined && uri) resolveUri(uri); } - handleMouseOver() { - if (this.props.obscureNsfw && this.props.metadata && this.props.metadata.nsfw) { - this.setState({ - showNsfwHelp: true, - }); - } - } - - handleMouseOut() { - if (this.state.showNsfwHelp) { - this.setState({ - showNsfwHelp: false, - }); - } - } - render() { const { claim, - showActions, metadata, isResolvingUri, - showEmpty, navigate, - showPrice, - showLocal, rewardedContentClaimIds, - fileInfo, + showUri, + fullWidth, + showLocal, + isDownloaded, } = this.props; const uri = normalizeURI(this.props.uri); const isClaimed = !!claim; - const isClaimable = isURIClaimable(uri); const title = isClaimed && metadata && metadata.title ? metadata.title : parseURI(uri).contentName; const thumbnail = metadata && metadata.thumbnail ? metadata.thumbnail : null; - const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw; const isRewardContent = claim && rewardedContentClaimIds.includes(claim.claim_id); - let onClick = () => navigate('/show', { uri }); + const onClick = () => navigate('/show', { uri }); - let name = ''; + let name; + let channel; if (claim) { name = claim.name; - } - - let description = ''; - if (isClaimed) { - description = metadata && metadata.description; - } else if (isResolvingUri) { - description = __('Loading...'); - } else if (showEmpty === FileTile.SHOW_EMPTY_PUBLISH) { - onClick = () => navigate('/publish', {}); - description = ( - - {__('This location is unused.')}{' '} - {isClaimable && {__('Put something here!')}} - - ); - } else if (showEmpty === FileTile.SHOW_EMPTY_PENDING) { - description = {__('This file is pending confirmation.')}; + channel = claim.channel_name; } return (
-
-
- -
-
- - {showPrice && }{' '} - {isRewardContent && }{' '} - {showLocal && fileInfo && } - -

- {title || name} -

+ +
+ {isResolvingUri &&
{__('Loading...')}
} + {!isResolvingUri && ( + +
+ {title || name}
- {description && ( -
- {description} -
+
+ {showUri ? uri : channel || __('Anonymous')} + {isRewardContent && } + {showLocal && isDownloaded && } +
+ {!name && ( + + {__('This location is unused.')}{' '} +
-
+ + )}
- {this.state.showNsfwHelp && }
); } diff --git a/src/renderer/component/form.js b/src/renderer/component/form.js deleted file mode 100644 index c13619b1a..000000000 --- a/src/renderer/component/form.js +++ /dev/null @@ -1,186 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import FormField from 'component/formField'; -import Icon from 'component/icon'; - -let formFieldCounter = 0; - -export const formFieldNestedLabelTypes = ['radio', 'checkbox']; - -export function formFieldId() { - return `form-field-${++formFieldCounter}`; -} - -export class Form extends React.PureComponent { - static propTypes = { - onSubmit: PropTypes.func.isRequired, - }; - - constructor(props) { - super(props); - } - - handleSubmit(event) { - event.preventDefault(); - this.props.onSubmit(); - } - - render() { - return
this.handleSubmit(event)}>{this.props.children}
; - } -} - -export class FormRow extends React.PureComponent { - static propTypes = { - label: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), - errorMessage: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), - // helper: PropTypes.html, - }; - - static defaultProps = { - isFocus: false, - }; - - constructor(props) { - super(props); - - this._field = null; - - this._fieldRequiredText = __('This field is required'); - - this.state = this.getStateFromProps(props); - } - - componentWillReceiveProps(nextProps) { - this.setState(this.getStateFromProps(nextProps)); - } - - getStateFromProps(props) { - return { - isError: !!props.errorMessage, - errorMessage: - typeof props.errorMessage === 'string' - ? props.errorMessage - : props.errorMessage instanceof Error ? props.errorMessage.toString() : '', - }; - } - - showError(text) { - this.setState({ - isError: true, - errorMessage: text, - }); - } - - showRequiredError() { - this.showError(this._fieldRequiredText); - } - - clearError(text) { - this.setState({ - isError: false, - errorMessage: '', - }); - } - - getValue() { - return this._field.getValue(); - } - - getSelectedElement() { - return this._field.getSelectedElement(); - } - - getOptions() { - return this._field.getOptions(); - } - - focus() { - this._field.focus(); - } - - onFocus() { - this.setState({ isFocus: true }); - } - - onBlur() { - this.setState({ isFocus: false }); - } - - render() { - const fieldProps = Object.assign({}, this.props), - elementId = formFieldId(), - renderLabelInFormField = formFieldNestedLabelTypes.includes(this.props.type); - - if (!renderLabelInFormField) { - delete fieldProps.label; - } - delete fieldProps.helper; - delete fieldProps.errorMessage; - delete fieldProps.isFocus; - - return ( -
- {this.props.label && !renderLabelInFormField ? ( -
- -
- ) : ( - '' - )} - { - this._field = ref ? ref.getWrappedInstance() : null; - }} - hasError={this.state.isError} - onFocus={this.onFocus.bind(this)} - onBlur={this.onBlur.bind(this)} - {...fieldProps} - /> - {!this.state.isError && this.props.helper ? ( -
{this.props.helper}
- ) : ( - '' - )} - {this.state.isError ? ( -
{this.state.errorMessage}
- ) : ( - '' - )} -
- ); - } -} - -export const Submit = props => { - const { title, label, icon, disabled } = props; - - const className = `${'button-block' + - ' button-primary' + - ' button-set-item' + - ' button--submit'}${disabled ? ' disabled' : ''}`; - - const content = ( - - {'icon' in props ? : null} - {label ? {label} : null} - - ); - - return ( - - ); -}; diff --git a/src/renderer/component/formField/view.jsx b/src/renderer/component/formField/view.jsx index 960b08c17..9d8dca9ec 100644 --- a/src/renderer/component/formField/view.jsx +++ b/src/renderer/component/formField/view.jsx @@ -1,8 +1,10 @@ +// This file is going to die +/* eslint-disable */ import React from 'react'; import PropTypes from 'prop-types'; -import FileSelector from 'component/file-selector.js'; +import FileSelector from 'component/common/file-selector'; import SimpleMDE from 'react-simplemde-editor'; -import { formFieldNestedLabelTypes, formFieldId } from '../form'; +import { formFieldNestedLabelTypes, formFieldId } from 'component/common/form'; import style from 'react-simplemde-editor/dist/simplemde.min.css'; const formFieldFileSelectorTypes = ['file', 'directory']; @@ -195,3 +197,4 @@ class FormField extends React.PureComponent { } export default FormField; +/* eslint-enable */ diff --git a/src/renderer/component/formFieldPrice/view.jsx b/src/renderer/component/formFieldPrice/view.jsx index 71382bb47..405e9b8ce 100644 --- a/src/renderer/component/formFieldPrice/view.jsx +++ b/src/renderer/component/formFieldPrice/view.jsx @@ -1,63 +1,5 @@ -import React from 'react'; -import FormField from 'component/formField'; +// This just exists so the app builds. It will be removed -class FormFieldPrice extends React.PureComponent { - constructor(props) { - super(props); - this.state = { - amount: props.defaultValue && props.defaultValue.amount ? props.defaultValue.amount : '', - currency: - props.defaultValue && props.defaultValue.currency ? props.defaultValue.currency : 'LBC', - }; - } - - handleChange(newValues) { - const newState = Object.assign({}, this.state, newValues); - this.setState(newState); - this.props.onChange({ - amount: newState.amount, - currency: newState.currency, - }); - } - - handleFeeAmountChange(event) { - this.handleChange({ - amount: event.target.value ? Number(event.target.value) : null, - }); - } - - handleFeeCurrencyChange(event) { - this.handleChange({ currency: event.target.value }); - } - - render() { - const { defaultValue, placeholder, min } = this.props; - - return ( - - this.handleFeeAmountChange(event)} - defaultValue={defaultValue && defaultValue.amount ? defaultValue.amount : ''} - className="form-field__input--inline" - /> - this.handleFeeCurrencyChange(event)} - defaultValue={defaultValue && defaultValue.currency ? defaultValue.currency : ''} - className="form-field__input--inline" - > - - - - - ); - } -} +const FormFieldPrice = () => null; export default FormFieldPrice; diff --git a/src/renderer/component/header/index.js b/src/renderer/component/header/index.js index 59c98d0ff..073d0d2eb 100644 --- a/src/renderer/component/header/index.js +++ b/src/renderer/component/header/index.js @@ -1,16 +1,12 @@ -import React from 'react'; -import { formatCredits } from 'util/formatCredits'; import { connect } from 'react-redux'; -import { selectIsBackDisabled, selectIsForwardDisabled } from 'redux/selectors/navigation'; -import { selectBalance } from 'redux/selectors/wallet'; -import { doNavigate, doHistoryBack, doHistoryForward } from 'redux/actions/navigation'; -import Header from './view'; +import { doNavigate } from 'redux/actions/navigation'; import { selectIsUpgradeAvailable, selectAutoUpdateDownloaded } from 'redux/selectors/app'; +import { formatCredits } from 'util/formatCredits'; +import { selectBalance } from 'redux/selectors/wallet'; +import Header from './view'; import { doDownloadUpgradeRequested } from 'redux/actions/app'; const select = state => ({ - isBackDisabled: selectIsBackDisabled(state), - isForwardDisabled: selectIsForwardDisabled(state), isUpgradeAvailable: selectIsUpgradeAvailable(state), autoUpdateDownloaded: selectAutoUpdateDownloaded(state), balance: formatCredits(selectBalance(state) || 0, 2), diff --git a/src/renderer/component/header/view.jsx b/src/renderer/component/header/view.jsx index 7109d7b8b..e081e2139 100644 --- a/src/renderer/component/header/view.jsx +++ b/src/renderer/component/header/view.jsx @@ -1,100 +1,68 @@ -import React from 'react'; -import Link from 'component/link'; +// @flow +import * as React from 'react'; +import Button from 'component/button'; import WunderBar from 'component/wunderbar'; +import * as icons from 'constants/icons'; -export const Header = props => { +type Props = { + balance: string, + navigate: any => void, + downloadUpgradeRequested: any => void, + isUpgradeAvailable: boolean, + autoUpdateDownloaded: boolean, +}; + +const Header = (props: Props) => { const { balance, - back, - forward, - isBackDisabled, - isForwardDisabled, isUpgradeAvailable, - autoUpdateDownloaded, navigate, downloadUpgradeRequested, + autoUpdateDownloaded, } = props; + + const showUpgradeButton = + autoUpdateDownloaded || (process.platform === 'linux' && isUpgradeAvailable); + return ( - ); }; diff --git a/src/renderer/component/icon/index.js b/src/renderer/component/icon/index.js deleted file mode 100644 index 81d61e58b..000000000 --- a/src/renderer/component/icon/index.js +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import Icon from './view'; - -export default connect(null, null)(Icon); diff --git a/src/renderer/component/icon/view.jsx b/src/renderer/component/icon/view.jsx deleted file mode 100644 index 795a1b241..000000000 --- a/src/renderer/component/icon/view.jsx +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import * as icons from 'constants/icons'; -import classnames from 'classnames'; - -export default class Icon extends React.PureComponent { - static propTypes = { - icon: PropTypes.string.isRequired, - fixed: PropTypes.bool, - }; - - static defaultProps = { - fixed: false, - }; - - getIconClass() { - const { icon } = this.props; - - return icon.startsWith('icon-') ? icon : `icon-${icon}`; - } - - getIconTitle() { - switch (this.props.icon) { - case icons.FEATURED: - return __('Watch this and earn rewards.'); - case icons.LOCAL: - return __('You have a copy of this file.'); - default: - return ''; - } - } - - render() { - const { icon, fixed, className, leftPad } = this.props; - const iconClass = this.getIconClass(); - const title = this.getIconTitle(); - - const spanClassName = classnames( - 'icon', - iconClass, - { - 'icon-fixed-width': fixed, - 'icon--left-pad': leftPad, - }, - className - ); - - return ; - } -} diff --git a/src/renderer/component/inviteList/view.jsx b/src/renderer/component/inviteList/view.jsx index c339cfd1b..7874e9251 100644 --- a/src/renderer/component/inviteList/view.jsx +++ b/src/renderer/component/inviteList/view.jsx @@ -1,7 +1,8 @@ import React from 'react'; -import Icon from 'component/icon'; +import Icon from 'component/common/icon'; import RewardLink from 'component/rewardLink'; import rewards from 'rewards.js'; +import * as icons from 'constants/icons'; class InviteList extends React.PureComponent { render() { @@ -12,8 +13,8 @@ class InviteList extends React.PureComponent { } return ( -
-
+
+

{__('Invite History')}

@@ -21,7 +22,7 @@ class InviteList extends React.PureComponent { {__("You haven't invited anyone.")} )} {invitees.length > 0 && ( - +
@@ -35,14 +36,14 @@ class InviteList extends React.PureComponent {
{__('Invitee Email')}{invitee.email} {invitee.invite_accepted ? ( - + ) : ( {__('unused')} )} {invitee.invite_reward_claimed ? ( - + ) : invitee.invite_reward_claimable ? ( ) : ( diff --git a/src/renderer/component/inviteNew/view.jsx b/src/renderer/component/inviteNew/view.jsx index b7a76709f..6e174bb73 100644 --- a/src/renderer/component/inviteNew/view.jsx +++ b/src/renderer/component/inviteNew/view.jsx @@ -1,14 +1,19 @@ +// I'll come back to this +/* eslint-disable */ import React from 'react'; -import { BusyMessage, CreditAmount } from 'component/common'; -import { Form, FormRow, Submit } from 'component/form.js'; +import BusyIndicator from 'component/common/busy-indicator'; +import CreditAmount from 'component/common/credit-amount'; +import { Form, FormRow, FormField, Submit } from 'component/common/form'; class FormInviteNew extends React.PureComponent { - constructor(props) { - super(props); + constructor() { + super(); this.state = { email: '', }; + + this.handleSubmit = this.handleSubmit.bind(this); } handleEmailChanged(event) { @@ -23,23 +28,27 @@ class FormInviteNew extends React.PureComponent { } render() { - const { errorMessage, isPending } = this.props; + const { errorMessage, isPending, rewardAmount } = this.props; + const label = `${__('Get')} ${rewardAmount} LBC`; return ( -
- { - this.handleEmailChanged(event); - }} - /> -
- + + + { + this.handleEmailChanged(event); + }} + /> + +
+
); @@ -58,10 +67,10 @@ class InviteNew extends React.PureComponent { } = this.props; return ( -
-
- -

{__('Invite a Friend')}

+
+
{__('Invite a Friend')}
+
+ {__("Or an enemy. Or your cousin Jerry, who you're kind of unsure about.")}
{/*
@@ -71,8 +80,12 @@ class InviteNew extends React.PureComponent {

{__("You have no invites.")}

}
*/}
-

{__("Or an enemy. Or your cousin Jerry, who you're kind of unsure about.")}

- +
); @@ -80,3 +93,4 @@ class InviteNew extends React.PureComponent { } export default InviteNew; +/* eslint-enable */ diff --git a/src/renderer/component/link/view.jsx b/src/renderer/component/link/view.jsx deleted file mode 100644 index 92451daff..000000000 --- a/src/renderer/component/link/view.jsx +++ /dev/null @@ -1,60 +0,0 @@ -import React from 'react'; -import Icon from 'component/icon'; - -const Link = props => { - const { - href, - title, - style, - label, - icon, - iconRight, - button, - disabled, - children, - navigate, - navigateParams, - doNavigate, - className, - span, - } = props; - - const combinedClassName = - (className || '') + - (!className && !button ? 'button-text' : '') + // Non-button links get the same look as text buttons - (button ? ` button-block button-${button} button-set-item` : '') + - (disabled ? ' disabled' : ''); - - const onClick = - !props.onClick && navigate - ? event => { - event.stopPropagation(); - doNavigate(navigate, navigateParams || {}); - } - : props.onClick; - - let content; - if (children) { - content = children; - } else { - content = ( - - {icon ? : null} - {label ? {label} : null} - {iconRight ? : null} - - ); - } - - const linkProps = { - className: combinedClassName, - href: href || 'javascript:;', - title, - onClick, - style, - }; - - return span ? {content} : {content}; -}; - -export default Link; diff --git a/src/renderer/component/linkTransaction/index.js b/src/renderer/component/linkTransaction/index.js deleted file mode 100644 index 4f4aa23b7..000000000 --- a/src/renderer/component/linkTransaction/index.js +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import LinkTransaction from './view'; - -export default connect(null, null)(LinkTransaction); diff --git a/src/renderer/component/linkTransaction/view.jsx b/src/renderer/component/linkTransaction/view.jsx deleted file mode 100644 index 44e81c321..000000000 --- a/src/renderer/component/linkTransaction/view.jsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import Link from 'component/link'; - -const LinkTransaction = props => { - const { id } = props; - const linkProps = Object.assign({}, props); - - linkProps.href = `https://explorer.lbry.io/#!/transaction/${id}`; - linkProps.label = id.substr(0, 7); - - return ; -}; - -export default LinkTransaction; diff --git a/src/renderer/component/load_screen.js b/src/renderer/component/load_screen.js deleted file mode 100644 index 5c990d37d..000000000 --- a/src/renderer/component/load_screen.js +++ /dev/null @@ -1,57 +0,0 @@ -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: PropTypes.string.isRequired, - details: PropTypes.string, - isWarning: PropTypes.bool, - }; - - constructor(props) { - super(props); - - this.state = { - message: null, - details: null, - isLagging: false, - }; - } - - static defaultProps = { - isWarning: false, - }; - - render() { - const imgSrc = lbry.imagePath('lbry-white-485x160.png'); - return ( -
- LBRY -
-

- {!this.props.isWarning ? ( - - ) : ( - - - {` ${this.props.message}`} - - )} -

- - {this.props.details} - -
-
- ); - } -} - -export default LoadScreen; diff --git a/src/renderer/component/menu.js b/src/renderer/component/menu.js deleted file mode 100644 index e070f8711..000000000 --- a/src/renderer/component/menu.js +++ /dev/null @@ -1,111 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import Icon from 'component/icon'; -import Link from 'component/link'; - -export class DropDownMenuItem extends React.PureComponent { - static propTypes = { - href: PropTypes.string, - label: PropTypes.string, - icon: PropTypes.string, - onClick: PropTypes.func, - }; - - static defaultProps = { - iconPosition: 'left', - }; - - render() { - const icon = this.props.icon ? : null; - - return ( - - {this.props.iconPosition == 'left' ? icon : null} - {this.props.label} - {this.props.iconPosition == 'left' ? null : icon} - - ); - } -} - -export class DropDownMenu extends React.PureComponent { - constructor(props) { - super(props); - - this._isWindowClickBound = false; - this._menuDiv = null; - - this.state = { - menuOpen: false, - }; - } - - componentWillUnmount() { - if (this._isWindowClickBound) { - window.removeEventListener('click', this.handleWindowClick, false); - } - } - - handleMenuIconClick(e) { - this.setState({ - menuOpen: !this.state.menuOpen, - }); - if (!this.state.menuOpen && !this._isWindowClickBound) { - this._isWindowClickBound = true; - window.addEventListener('click', this.handleWindowClick, false); - e.stopPropagation(); - } - return false; - } - - handleMenuClick(e) { - // Event bubbles up to the menu after a link is clicked - this.setState({ - menuOpen: false, - }); - } - - /* this will force "this" to always be the class, even when passed to an event listener */ - handleWindowClick = e => { - if (this.state.menuOpen && (!this._menuDiv || !this._menuDiv.contains(e.target))) { - this.setState({ - menuOpen: false, - }); - } - }; - - render() { - if (!this.state.menuOpen && this._isWindowClickBound) { - this._isWindowClickBound = false; - window.removeEventListener('click', this.handleWindowClick, false); - } - return ( -
- (this._menuButton = span)} - button="text" - icon="icon-ellipsis-v" - onClick={event => { - this.handleMenuIconClick(event); - }} - /> - {this.state.menuOpen ? ( -
(this._menuDiv = div)} - className="menu" - onClick={event => { - this.handleMenuClick(event); - }} - > - {this.props.children} -
- ) : null} -
- ); - } -} diff --git a/src/renderer/component/nsfwOverlay/view.jsx b/src/renderer/component/nsfwOverlay/view.jsx index c89fe6da9..3c38cdbf8 100644 --- a/src/renderer/component/nsfwOverlay/view.jsx +++ b/src/renderer/component/nsfwOverlay/view.jsx @@ -1,15 +1,11 @@ import React from 'react'; -import Link from 'component/link'; +import Button from 'component/button'; -const NsfwOverlay = props => ( +const NsfwOverlay = () => (

{__('This content is Not Safe For Work. To view adult content, please change your')}{' '} - props.navigateSettings()} - label={__('Settings')} - />. +

); diff --git a/src/renderer/component/page/index.js b/src/renderer/component/page/index.js new file mode 100644 index 000000000..0bf9f77fe --- /dev/null +++ b/src/renderer/component/page/index.js @@ -0,0 +1,31 @@ +import { connect } from 'react-redux'; +import { + selectPageTitle, + selectIsBackDisabled, + selectIsForwardDisabled, + selectNavLinks, +} from 'redux/selectors/navigation'; +import { doNavigate, doHistoryBack, doHistoryForward } from 'redux/actions/navigation'; +import { doDownloadUpgrade } from 'redux/actions/app'; +import { selectIsUpgradeAvailable } from 'redux/selectors/app'; +import { formatCredits } from 'util/formatCredits'; +import { selectBalance } from 'redux/selectors/wallet'; +import Page from './view'; + +const select = state => ({ + pageTitle: selectPageTitle(state), + navLinks: selectNavLinks(state), + isBackDisabled: selectIsBackDisabled(state), + isForwardDisabled: selectIsForwardDisabled(state), + isUpgradeAvailable: selectIsUpgradeAvailable(state), + balance: formatCredits(selectBalance(state) || 0, 2), +}); + +const perform = dispatch => ({ + navigate: path => dispatch(doNavigate(path)), + back: () => dispatch(doHistoryBack()), + forward: () => dispatch(doHistoryForward()), + downloadUpgrade: () => dispatch(doDownloadUpgrade()), +}); + +export default connect(select, perform)(Page); diff --git a/src/renderer/component/page/view.jsx b/src/renderer/component/page/view.jsx new file mode 100644 index 000000000..40ec8564b --- /dev/null +++ b/src/renderer/component/page/view.jsx @@ -0,0 +1,33 @@ +// @flow +import * as React from 'react'; +import classnames from 'classnames'; + +type Props = { + children: React.Node, + pageTitle: ?string, + noPadding: ?boolean, + extraPadding: ?boolean, + notContained: ?boolean, // No max-width, but keep the padding +}; + +const Page = (props: Props) => { + const { pageTitle, children, noPadding, extraPadding, notContained } = props; + return ( +
+ {pageTitle && ( +
+ {pageTitle &&

{pageTitle}

} +
+ )} + {children} +
+ ); +}; + +export default Page; diff --git a/src/renderer/component/publishForm/index.js b/src/renderer/component/publishForm/index.js index de3ba7663..3eb2a7bc4 100644 --- a/src/renderer/component/publishForm/index.js +++ b/src/renderer/component/publishForm/index.js @@ -1,10 +1,5 @@ import React from 'react'; import { connect } from 'react-redux'; import PublishForm from './view'; -import { selectBalance } from 'redux/selectors/wallet'; -const select = state => ({ - balance: selectBalance(state), -}); - -export default connect(select, null)(PublishForm); +export default connect(null, null)(PublishForm); diff --git a/src/renderer/component/publishForm/internal/bid-help-text.jsx b/src/renderer/component/publishForm/internal/bid-help-text.jsx new file mode 100644 index 000000000..8c99118d2 --- /dev/null +++ b/src/renderer/component/publishForm/internal/bid-help-text.jsx @@ -0,0 +1,61 @@ +// @flow +import * as React from 'react'; +import Button from 'component/button'; + +type Props = { + uri: ?string, + editingURI: ?string, + isResolvingUri: boolean, + winningBidForClaimUri: ?number, + claimIsMine: ?boolean, + onEditMyClaim: any => void, +}; + +class BidHelpText extends React.PureComponent { + render() { + const { + uri, + editingURI, + isResolvingUri, + winningBidForClaimUri, + claimIsMine, + onEditMyClaim, + } = this.props; + + if (!uri) { + return __('Create a URL for this content'); + } + + if (uri === editingURI) { + return __('You are currently editing this claim'); + } + + if (isResolvingUri) { + return __('Checking the winning claim amount...'); + } + + if (claimIsMine) { + return ( + + {__('You already have a claim at')} + {` ${uri} `} +
- {!this.state.hasFile && !this.myClaimExists() ? null : ( -
-
- { - this.handleMetadataChange(event); - }} - /> -
-
- { - this.handleMetadataChange(event); - }} - /> -
-
- { - this.handleDescriptionChanged(text); - }} - /> -
-
- { - this.handleMetadataChange(event); - }} - > - - - - - - - - -
-
- { - this.handleMetadataChange(event); - }} - > - {/* */} - - - -
-
- )} + )} + + {!!editingURI && ( +

+ {__("If you don't choose a file, the file from your existing claim")} + {` "${name}" `} + {__('will be used.')} +

+ )} +
+
+
+ + updatePublishForm({ title: e.target.value })} + /> + + + updatePublishForm({ thumbnail: e.target.value })} + /> + + + updatePublishForm({ description: text })} + /> +
-
-
-

{__('Price')}

-
{__('How much does this content cost?')}
-
+
+
{__('Price')}
+
{__('How much will this content cost?')}
- this.handleFeePrefChange(false)} - checked={!this.state.isFee} + name="content_free" + postfix={__('Free')} + checked={contentIsFree} + disabled={formDisabled} + onChange={() => updatePublishForm({ contentIsFree: true })} /> { - this.handleFeePrefChange(true); - }} - checked={this.state.isFee} + name="content_cost" + postfix={__('Choose price')} + checked={!contentIsFree} + disabled={formDisabled} + onChange={() => updatePublishForm({ contentIsFree: false })} /> - - this.handleFeeChange(val)} - /> - - {this.state.isFee && this.state.feeCurrency.toUpperCase() != 'LBC' ? ( -
+ + updatePublishForm({ price: newPrice })} + disabled={formDisabled || contentIsFree} + /> + {price.currency !== 'LBC' && ( +

{__( 'All content fees are charged in LBC. For non-LBC payment methods, the number of credits charged will be adjusted based on the value of LBRY credits at the time of purchase.' )} -

- ) : null} +

+ )}
-
-
-

{__('License')}

-
+ +
+
{__('Anonymous or under a channel?')}
+

+ {__('This is a username or handle that your content can be found under.')}{' '} + {__('Ex. @Marvel, @TheBeatles, @BooksByJoe')} +

+ +
+ +
+
{__('Where can people find this content?')}
+

+ {__( + 'The LBRY URL is the exact address where people find your content (ex. lbry://myvideo).' + )}{' '} +

- - - -
-
-

{__('Content URL')}

-
- {__( - 'This is the exact address where people find your content (ex. lbry://myvideo).' - )}{' '} - . -
-
-
- { - this.handleNameChange(event); - }} - helper={this.getNameBidHelpText()} +
+ this.handleBidChange(parseFloat(event.target.value))} + helper={__('This LBC remains yours and the deposit can be undone at any time.')} + placeholder={winningBidForClaimUri ? winningBidForClaimUri + 0.1 : 0.1} />
- {this.state.rawName ? ( -
- { - this.handleBidChange(event); - }} - value={this.state.bid} - placeholder={this.claim() ? this.topClaimValue() + 10 : 100} - helper={lbcInputHelp} - min="0" - /> -
- ) : ( - '' - )}
-
-
-

{__('Terms of Service')}

-
+
+ + updatePublishForm({ nsfw: event.target.checked })} + /> + + + + updatePublishForm({ language: event.target.value })} + > + + + + + + + + + + + + updatePublishForm({ + licenseType: newLicenseType, + licenseUrl: newLicenseUrl, + }) + } + handleLicenseDescriptionChange={event => + updatePublishForm({ + otherLicenseDescription: event.target.value, + }) + } + handleLicenseUrlChange={event => + updatePublishForm({ licenseUrl: event.target.value }) + } + handleCopyrightNoticeChange={event => + updatePublishForm({ copyrightNotice: event.target.value }) + } + /> +
+ +
+
{__('Terms of Service')}
- {__('I agree to the')}{' '} - } - type="checkbox" - checked={this.state.tosAgree} - onChange={event => { - this.handleTOSChange(event); - }} + onChange={event => updatePublishForm({ tosAccepted: event.target.checked })} />
-
- - +
+ +
- - - { - this.handlePublishStartedConfirmed(event); - }} - > -

- {__('Your file has been published to LBRY at the address')}{' '} - {this.state.uri}! -

-

- {__( - '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.' - )} -

-
- { - this.closeModal(event); - }} - > - {__('The following error occurred when attempting to publish your file')}:{' '} - {this.state.errorMessage} - - + {!formDisabled && !formValid && this.renderFormErrors()} +
+ ); } } diff --git a/src/renderer/component/rewardLink/view.jsx b/src/renderer/component/rewardLink/view.jsx index f15bb9c99..edb43aefd 100644 --- a/src/renderer/component/rewardLink/view.jsx +++ b/src/renderer/component/rewardLink/view.jsx @@ -1,16 +1,16 @@ import React from 'react'; import Modal from 'modal/modal'; -import Link from 'component/link'; +import Button from 'component/button'; const RewardLink = props => { const { reward, button, claimReward, clearError, errorMessage, label, isPending } = props; - return ( + return !reward ? null : (
- { claimReward(reward); }} diff --git a/src/renderer/component/rewardListClaimed/view.jsx b/src/renderer/component/rewardListClaimed/view.jsx index 63f422fa1..75268f250 100644 --- a/src/renderer/component/rewardListClaimed/view.jsx +++ b/src/renderer/component/rewardListClaimed/view.jsx @@ -1,7 +1,20 @@ +// @flow import React from 'react'; -import LinkTransaction from 'component/linkTransaction'; +import ButtonTransaction from 'component/common/transaction-link'; -const RewardListClaimed = props => { +type Reward = { + id: string, + reward_title: string, + reward_amount: number, + transaction_id: string, + created_at: string, +}; + +type Props = { + rewards: Array, +}; + +const RewardListClaimed = (props: Props) => { const { rewards } = props; if (!rewards || !rewards.length) { @@ -9,34 +22,31 @@ const RewardListClaimed = props => { } return ( -
-
-

Claimed Rewards

-
-
- - - - - - - +
+
Claimed Rewards
+ +
{__('Title')}{__('Amount')}{__('Transaction')}{__('Date')}
+ + + + + + + + + + {rewards.map(reward => ( + + + + + - - - {rewards.map(reward => ( - - - - - - - ))} - -
{__('Title')}{__('Amount')}{__('Transaction')}{__('Date')}
{reward.reward_title}{reward.reward_amount} + + {reward.created_at.replace('Z', ' ').replace('T', ' ')}
{reward.reward_title}{reward.reward_amount} - - {reward.created_at.replace('Z', ' ').replace('T', ' ')}
-
+ ))} + +
); }; diff --git a/src/renderer/component/rewardSummary/view.jsx b/src/renderer/component/rewardSummary/view.jsx index 9fa611902..5436897d6 100644 --- a/src/renderer/component/rewardSummary/view.jsx +++ b/src/renderer/component/rewardSummary/view.jsx @@ -1,7 +1,7 @@ // @flow -import React from 'react'; -import Link from 'component/link'; -import { CreditAmount } from 'component/common'; +import * as React from 'react'; +import Button from 'component/button'; +import CreditAmount from 'component/common/credit-amount'; type Props = { unclaimedRewardAmount: number, @@ -9,29 +9,38 @@ type Props = { const RewardSummary = (props: Props) => { const { unclaimedRewardAmount } = props; + const hasRewards = unclaimedRewardAmount > 0; return ( -
-
-

{__('Rewards')}

-

- {__('Read our')} {__('FAQ')}{' '} - {__('to learn more about LBRY Rewards')}. -

-
-
- {unclaimedRewardAmount > 0 ? ( -

- {__('You have')} {' '} +

+
{__('Rewards')}
+

+ {hasRewards ? ( + + {__('You have')} +   + +   {__('in unclaimed rewards')}. -

+ ) : ( -

{__('There are no rewards available at this time, please check back later')}.

+ + {__('There are no rewards available at this time, please check back later')}. + )} -
+

- +
+

+ {__('Read our')}{' '} +

); }; diff --git a/src/renderer/component/rewardTile/view.jsx b/src/renderer/component/rewardTile/view.jsx index 6cbfcc4cd..b5a9d00a3 100644 --- a/src/renderer/component/rewardTile/view.jsx +++ b/src/renderer/component/rewardTile/view.jsx @@ -1,35 +1,43 @@ +// @flow import React from 'react'; -import { CreditAmount, Icon } from 'component/common'; +import Icon from 'component/common/icon'; import RewardLink from 'component/rewardLink'; -import Link from 'component/link'; +import Button from 'component/button'; import rewards from 'rewards'; +import * as icons from 'constants/icons'; -const RewardTile = props => { +type Props = { + reward: { + id: string, + reward_title: string, + reward_amount: number, + transaction_id: string, + created_at: string, + reward_description: string, + reward_type: string, + }, +}; + +const RewardTile = (props: Props) => { const { reward } = props; - const claimed = !!reward.transaction_id; return ( -
-
-
- -

{reward.reward_title}

-
-
{reward.reward_description}
-
- {reward.reward_type == rewards.TYPE_REFERRAL && ( - - )} - {reward.reward_type !== rewards.TYPE_REFERRAL && - (claimed ? ( - - {__('Reward claimed.')} - - ) : ( - - ))} -
+
+
{reward.reward_title}
+
{reward.reward_description}
+
+ {reward.reward_type === rewards.TYPE_REFERRAL && ( +
); diff --git a/src/renderer/component/router/view.jsx b/src/renderer/component/router/view.jsx index b65e830fe..4b1cf2acc 100644 --- a/src/renderer/component/router/view.jsx +++ b/src/renderer/component/router/view.jsx @@ -13,7 +13,6 @@ import FileListDownloaded from 'page/fileListDownloaded'; import FileListPublished from 'page/fileListPublished'; import TransactionHistoryPage from 'page/transactionHistory'; import ChannelPage from 'page/channel'; -import SearchPage from 'page/search'; import AuthPage from 'page/auth'; import InvitePage from 'page/invite'; import BackupPage from 'page/backup'; @@ -22,7 +21,7 @@ import SubscriptionsPage from 'page/subscriptions'; const route = (page, routesMap) => { const component = routesMap[page]; - return component; + return component || DiscoverPage; }; const Router = props => { @@ -42,7 +41,6 @@ const Router = props => { getcredits: , report: , rewards: , - search: , send: , settings: , show: , diff --git a/src/renderer/component/selectChannel/index.js b/src/renderer/component/selectChannel/index.js new file mode 100644 index 000000000..66d208875 --- /dev/null +++ b/src/renderer/component/selectChannel/index.js @@ -0,0 +1,18 @@ +import { connect } from 'react-redux'; +import SelectChannel from './view'; +import { selectMyChannelClaims, selectFetchingMyChannels } from 'redux/selectors/claims'; +import { doFetchChannelListMine, doCreateChannel } from 'redux/actions/content'; +import { selectBalance } from 'redux/selectors/wallet'; + +const select = state => ({ + channels: selectMyChannelClaims(state), + fetchingChannels: selectFetchingMyChannels(state), + balance: selectBalance(state), +}); + +const perform = dispatch => ({ + createChannel: (name, amount) => dispatch(doCreateChannel(name, amount)), + fetchChannelListMine: () => dispatch(doFetchChannelListMine()), +}); + +export default connect(select, perform)(SelectChannel); diff --git a/src/renderer/component/selectChannel/view.jsx b/src/renderer/component/selectChannel/view.jsx new file mode 100644 index 000000000..88182c49c --- /dev/null +++ b/src/renderer/component/selectChannel/view.jsx @@ -0,0 +1,220 @@ +// @flow +import React from 'react'; +import { isNameValid } from 'lbryURI'; +import { FormRow, FormField } from 'component/common/form'; +import BusyIndicator from 'component/common/busy-indicator'; +import Button from 'component/button'; +import { CHANNEL_NEW, CHANNEL_ANONYMOUS } from 'constants/claim'; + +type Props = { + channel: string, // currently selected channel + channels: Array<{ name: string }>, + balance: number, + onChannelChange: string => void, + createChannel: (string, number) => Promise, + fetchChannelListMine: () => void, + fetchingChannels: boolean, +}; + +type State = { + newChannelName: string, + newChannelBid: number, + addingChannel: boolean, + creatingChannel: boolean, + newChannelNameError: string, + newChannelBidError: string, + createChannelError: ?string, +}; + +class ChannelSection extends React.PureComponent { + constructor(props: Props) { + super(props); + + this.state = { + newChannelName: '', + newChannelBid: 0.1, + addingChannel: false, + creatingChannel: false, + newChannelNameError: '', + newChannelBidError: '', + createChannelError: undefined, + }; + + (this: any).handleChannelChange = this.handleChannelChange.bind(this); + (this: any).handleNewChannelNameChange = this.handleNewChannelNameChange.bind(this); + (this: any).handleNewChannelBidChange = this.handleNewChannelBidChange.bind(this); + (this: any).handleCreateChannelClick = this.handleCreateChannelClick.bind(this); + } + + componentDidMount() { + const { channels, fetchChannelListMine, fetchingChannels } = this.props; + if (!channels.length && !fetchingChannels) { + fetchChannelListMine(); + } + } + + handleChannelChange(event: SyntheticInputEvent<*>) { + const { onChannelChange } = this.props; + const channel = event.target.value; + + if (channel === CHANNEL_NEW) { + this.setState({ addingChannel: true }); + onChannelChange(channel); + } else { + this.setState({ addingChannel: false }); + onChannelChange(channel); + } + } + + handleNewChannelNameChange(event: SyntheticInputEvent<*>) { + let newChannelName = event.target.value; + + if (newChannelName.startsWith('@')) { + newChannelName = newChannelName.slice(1); + } + + let newChannelNameError; + if (newChannelName.length > 1 && !isNameValid(newChannelName.substr(1), false)) { + newChannelNameError = __('LBRY channel names must contain only letters, numbers and dashes.'); + } + + this.setState({ + newChannelNameError, + newChannelName, + }); + } + + handleNewChannelBidChange(event: SyntheticInputEvent<*>) { + const { balance } = this.props; + const newChannelBid = parseFloat(event.target.value); + let newChannelBidError; + if (newChannelBid === balance) { + newChannelBidError = __('Please decrease your bid to account for transaction fees'); + } else if (newChannelBid > balance) { + newChannelBidError = __('Not enough credits'); + } + + this.setState({ + newChannelBid, + newChannelBidError, + }); + } + + handleCreateChannelClick() { + const { balance, createChannel, onChannelChange } = this.props; + const { newChannelBid, newChannelName } = this.state; + + const channelName = `@${newChannelName}`; + + if (newChannelBid > balance) { + return; + } + + this.setState({ + creatingChannel: true, + createChannelError: undefined, + }); + + const success = () => { + this.setState({ + creatingChannel: false, + addingChannel: false, + }); + + onChannelChange(channelName); + }; + + const failure = () => { + this.setState({ + creatingChannel: false, + createChannelError: __('Unable to create channel due to an internal error.'), + }); + }; + + createChannel(channelName, newChannelBid).then(success, failure); + } + + render() { + const channel = this.state.addingChannel ? 'new' : this.props.channel; + const { fetchingChannels, channels = [] } = this.props; + const { + newChannelName, + newChannelNameError, + newChannelBid, + newChannelBidError, + creatingChannel, + createChannelError, + addingChannel, + } = this.state; + + return ( +
+ {createChannelError &&
{createChannelError}
} + {fetchingChannels ? ( + + ) : ( + + + {channels.map(({ name }) => ( + + ))} + + + )} + {addingChannel && ( +
+ + + + + + +
+
+
+ )} +
+ ); + } +} + +export default ChannelSection; diff --git a/src/renderer/component/shapeShift/internal/active-shift.jsx b/src/renderer/component/shapeShift/internal/active-shift.jsx index ae2e83348..bb4524bb7 100644 --- a/src/renderer/component/shapeShift/internal/active-shift.jsx +++ b/src/renderer/component/shapeShift/internal/active-shift.jsx @@ -1,9 +1,10 @@ // @flow import * as React from 'react'; -import QRCode from 'qrcode.react'; +import QRCode from 'component/common/qr-code'; +import { FormRow } from 'component/common/form'; import * as statuses from 'constants/shape_shift'; import Address from 'component/address'; -import Link from 'component/link'; +import Button from 'component/button'; import type { Dispatch } from 'redux/actions/shape_shift'; import ShiftMarketInfo from './market_info'; @@ -92,12 +93,12 @@ class ActiveShapeShift extends React.PureComponent { originCoinDepositMax={originCoinDepositMax} /> -
-
-
+ {shiftDepositAddress && ( + +
-
-
+ + )}
)} @@ -115,9 +116,9 @@ class ActiveShapeShift extends React.PureComponent {

{__('Transaction complete! You should see the new LBC in your wallet.')}

)} -
- +
+
+ +
+
    + {navLinks.primary.map(({ label, path, active, icon }) => ( +
  • +
  • + ))} +
+
+
    + {navLinks.secondary.map(({ label, path, active, icon, subLinks = [] }) => ( +
  • +
  • + ))} +
+ + )} + + ))} + +
+ + ); +}; + +export default SideBar; diff --git a/src/renderer/component/snackBar/view.jsx b/src/renderer/component/snackBar/view.jsx index 2205cab3d..38867c811 100644 --- a/src/renderer/component/snackBar/view.jsx +++ b/src/renderer/component/snackBar/view.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import Link from 'component/link'; +import Button from 'component/button'; class SnackBar extends React.PureComponent { constructor(props) { @@ -32,7 +32,7 @@ class SnackBar extends React.PureComponent { {message} {linkText && linkTarget && ( - +
); diff --git a/src/renderer/component/splash/index.js b/src/renderer/component/splash/index.js index d57d014aa..04cc84e15 100644 --- a/src/renderer/component/splash/index.js +++ b/src/renderer/component/splash/index.js @@ -1,6 +1,4 @@ -import React from 'react'; import { connect } from 'react-redux'; - import { selectCurrentModal, selectDaemonVersionMatched } from 'redux/selectors/app'; import { doCheckDaemonVersion } from 'redux/actions/app'; import SplashScreen from './view'; diff --git a/src/renderer/component/splash/internal/load-screen.jsx b/src/renderer/component/splash/internal/load-screen.jsx new file mode 100644 index 000000000..ee1d120e8 --- /dev/null +++ b/src/renderer/component/splash/internal/load-screen.jsx @@ -0,0 +1,38 @@ +// @flow +import * as React from 'react'; +import Icon from 'component/common/icon'; +import * as icons from 'constants/icons'; + +type Props = { + message: string, + details: ?string, + isWarning: boolean, +}; + +class LoadScreen extends React.PureComponent { + static defaultProps = { + isWarning: false, + }; + + render() { + const { details, message, isWarning } = this.props; + + return ( +
+

{__('LBRY')}

+ {isWarning ? ( + + + {` ${message}`} + + ) : ( +
{message}
+ )} + + {details &&
{details}
} +
+ ); + } +} + +export default LoadScreen; diff --git a/src/renderer/component/splash/view.jsx b/src/renderer/component/splash/view.jsx index 02bb7c237..89c5c82f4 100644 --- a/src/renderer/component/splash/view.jsx +++ b/src/renderer/component/splash/view.jsx @@ -1,19 +1,25 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import lbry from 'lbry.js'; -import LoadScreen from '../load_screen.js'; +import * as React from 'react'; +import lbry from 'lbry'; +import LoadScreen from './internal/load-screen'; import ModalIncompatibleDaemon from 'modal/modalIncompatibleDaemon'; import ModalUpgrade from 'modal/modalUpgrade'; import ModalDownloading from 'modal/modalDownloading'; import * as modals from 'constants/modal_types'; -export class SplashScreen extends React.PureComponent { - static propTypes = { - message: PropTypes.string, - onLoadDone: PropTypes.func, - }; +type Props = { + checkDaemonVersion: () => Promise, + modal: string, +}; - constructor(props) { +type State = { + details: string, + message: string, + isRunning: boolean, + isLagging: boolean, +}; + +export class SplashScreen extends React.PureComponent { + constructor(props: Props) { super(props); this.state = { @@ -75,9 +81,11 @@ export class SplashScreen extends React.PureComponent { } componentDidMount() { + const { checkDaemonVersion } = this.props; + lbry .connect() - .then(this.props.checkDaemonVersion) + .then(checkDaemonVersion) .then(() => { this.updateStatus(); }) @@ -97,15 +105,19 @@ export class SplashScreen extends React.PureComponent { const { message, details, isLagging, isRunning } = this.state; return ( -
+ {/* Temp hack: don't show any modals on splash screen daemon is running; daemon doesn't let you quit during startup, so the "Quit" buttons in the modals won't work. */} - {modal == 'incompatibleDaemon' && isRunning && } - {modal == modals.UPGRADE && isRunning && } - {modal == modals.DOWNLOADING && isRunning && } -
+ {isRunning && ( + + {modal === modals.INCOMPATIBLE_DAEMON && } + {modal === modals.UPGRADE && } + {modal === modals.DOWNLOADING && } + + )} + ); } } diff --git a/src/renderer/component/subscribeButton/index.js b/src/renderer/component/subscribeButton/index.js index 6c93bbb65..933153af8 100644 --- a/src/renderer/component/subscribeButton/index.js +++ b/src/renderer/component/subscribeButton/index.js @@ -2,7 +2,6 @@ import { connect } from 'react-redux'; import { doChannelSubscribe, doChannelUnsubscribe } from 'redux/actions/subscriptions'; import { doOpenModal } from 'redux/actions/app'; import { selectSubscriptions } from 'redux/selectors/subscriptions'; - import SubscribeButton from './view'; const select = (state, props) => ({ diff --git a/src/renderer/component/subscribeButton/view.jsx b/src/renderer/component/subscribeButton/view.jsx index bb0d0c146..f362310c4 100644 --- a/src/renderer/component/subscribeButton/view.jsx +++ b/src/renderer/component/subscribeButton/view.jsx @@ -1,38 +1,54 @@ +// @flow import React from 'react'; -import Link from 'component/link'; import * as modals from 'constants/modal_types'; +import * as icons from 'constants/icons'; +import Button from 'component/button'; +import type { Subscription } from 'redux/reducers/subscriptions'; + +type SubscribtionArgs = { + channelName: string, + uri: string, +}; + +type Props = { + channelName: ?string, + uri: ?string, + subscriptions: Array, + doChannelSubscribe: ({ channelName: string, uri: string }) => void, + doChannelUnsubscribe: SubscribtionArgs => void, + doOpenModal: string => void, +}; + +export default (props: Props) => { + const { + channelName, + uri, + subscriptions, + doChannelSubscribe, + doChannelUnsubscribe, + doOpenModal, + } = props; -export default ({ - channelName, - uri, - subscriptions, - doChannelSubscribe, - doChannelUnsubscribe, - doOpenModal, -}) => { const isSubscribed = subscriptions.map(subscription => subscription.channelName).indexOf(channelName) !== -1; const subscriptionHandler = isSubscribed ? doChannelUnsubscribe : doChannelSubscribe; - const subscriptionLabel = isSubscribed ? __('Unsubscribe') : __('Subscribe'); return channelName && uri ? ( -
- { - if (!subscriptions.length) { - doOpenModal(modals.FIRST_SUBSCRIPTION); - } - subscriptionHandler({ - channelName, - uri, - }); - }} - /> -
+
+ )} + + + + + + + {date ? ( +
+ +
+ +
+
+ ) : ( + {__('Pending')} + )} + + + ); + } +} + +export default TransactionListItem; diff --git a/src/renderer/component/transactionList/view.jsx b/src/renderer/component/transactionList/view.jsx index 335e0d525..6f8521b88 100644 --- a/src/renderer/component/transactionList/view.jsx +++ b/src/renderer/component/transactionList/view.jsx @@ -1,63 +1,111 @@ -import React from 'react'; -import TransactionListItem from './internal/TransactionListItem'; -import FormField from 'component/formField'; -import Link from 'component/link'; -import FileExporter from 'component/file-exporter.js'; +// @flow +import * as React from 'react'; +import { FormField } from 'component/common/form'; +import Button from 'component/button'; +import FileExporter from 'component/common/file-exporter'; import * as icons from 'constants/icons'; import * as modals from 'constants/modal_types'; +import TransactionListItem from './internal/transaction-list-item'; -class TransactionList extends React.PureComponent { - constructor(props) { +export type Transaction = { + amount: number, + claim_id: string, + claim_name: string, + fee: number, + nout: number, + txid: string, + type: string, + date: Date, +}; + +type Props = { + emptyMessage: ?string, + slim?: boolean, + transactions: Array, + rewards: {}, + openModal: (string, any) => void, + myClaims: any, +}; + +type State = { + filter: string, +}; + +class TransactionList extends React.PureComponent { + constructor(props: Props) { super(props); this.state = { - filter: null, + filter: 'all', }; + + (this: any).handleFilterChanged = this.handleFilterChanged.bind(this); + (this: any).filterTransaction = this.filterTransaction.bind(this); + (this: any).revokeClaim = this.revokeClaim.bind(this); + (this: any).isRevokeable = this.isRevokeable.bind(this); } - handleFilterChanged(event) { + handleFilterChanged(event: SyntheticInputEvent<*>) { this.setState({ filter: event.target.value, }); } - filterTransaction(transaction) { + filterTransaction(transaction: Transaction) { const { filter } = this.state; - return !filter || filter == transaction.type; + return filter === 'all' || filter === transaction.type; } - isRevokeable(txid, nout) { + isRevokeable(txid: string, nout: number) { + const { myClaims } = this.props; // a claim/support/update is revokable if it // is in my claim list(claim_list_mine) - return this.props.myClaims.has(`${txid}:${nout}`); + return myClaims.has(`${txid}:${nout}`); } - revokeClaim(txid, nout) { + revokeClaim(txid: string, nout: number) { this.props.openModal(modals.CONFIRM_CLAIM_REVOKE, { txid, nout }); } render() { - const { emptyMessage, rewards, transactions } = this.props; - - const transactionList = transactions.filter(this.filterTransaction.bind(this)); + const { emptyMessage, rewards, transactions, slim } = this.props; + const { filter } = this.state; + const transactionList = transactions.filter(this.filterTransaction); return ( -
- {Boolean(transactionList.length) && ( - + + {!transactionList.length && ( +

{emptyMessage || __('No transactions to list.')}

)} - {(transactionList.length || this.state.filter) && ( - - {__('Filter')}{' '} - - + {!slim && + !!transactionList.length && ( +
+ +
+ )} + {!slim && ( +
+ + } + > + @@ -65,22 +113,18 @@ class TransactionList extends React.PureComponent { - {' '} - - + +
)} - {!transactionList.length && ( -
{emptyMessage || __('No transactions to list.')}
- )} - {Boolean(transactionList.length) && ( - + {!!transactionList.length && ( +
- - + + @@ -90,13 +134,13 @@ class TransactionList extends React.PureComponent { transaction={t} reward={rewards && rewards[t.txid]} isRevokeable={this.isRevokeable(t.txid, t.nout)} - revokeClaim={this.revokeClaim.bind(this)} + revokeClaim={this.revokeClaim} /> ))}
{__('Date')}{__('Amount (Fee)')}{__('Amount')} {__('Type')} {__('Details')} {__('Transaction')}{__('Date')}
)} -
+ ); } } diff --git a/src/renderer/component/transactionListRecent/view.jsx b/src/renderer/component/transactionListRecent/view.jsx index 3bc5b0159..325bbc1cb 100644 --- a/src/renderer/component/transactionListRecent/view.jsx +++ b/src/renderer/component/transactionListRecent/view.jsx @@ -1,11 +1,20 @@ +// @flow import React from 'react'; -import { BusyMessage } from 'component/common'; -import Link from 'component/link'; +import BusyIndicator from 'component/common/busy-indicator'; +import Button from 'component/button'; import TransactionList from 'component/transactionList'; import * as icons from 'constants/icons'; +import type { Transaction } from 'component/transactionList/view'; -class TransactionListRecent extends React.PureComponent { - componentWillMount() { +type Props = { + fetchTransactions: () => void, + fetchingTransactions: boolean, + hasTransactions: boolean, + transactions: Array, +}; + +class TransactionListRecent extends React.PureComponent { + componentDidMount() { this.props.fetchTransactions(); } @@ -13,27 +22,27 @@ class TransactionListRecent extends React.PureComponent { const { fetchingTransactions, hasTransactions, transactions } = this.props; return ( -
-
-

{__('Recent Transactions')}

-
-
- {fetchingTransactions && } - {!fetchingTransactions && ( - - )} -
+
+
{__('Recent Transactions')}
+ {fetchingTransactions && ( +
+ +
+ )} + {!fetchingTransactions && ( + + )} {hasTransactions && ( -
- +
)} diff --git a/src/renderer/component/uriIndicator/view.jsx b/src/renderer/component/uriIndicator/view.jsx index 932ff7e5c..b4e3b3707 100644 --- a/src/renderer/component/uriIndicator/view.jsx +++ b/src/renderer/component/uriIndicator/view.jsx @@ -1,35 +1,46 @@ +// @flow import React from 'react'; -import Icon from 'component/icon'; -import Link from 'component/link'; +import Button from 'component/button'; import { buildURI } from 'lbryURI'; import classnames from 'classnames'; +// import Icon from 'component/common/icon'; -class UriIndicator extends React.PureComponent { +type Props = { + isResolvingUri: boolean, + resolveUri: string => void, + claim: { + channel_name: string, + has_signature: boolean, + signature_is_valid: boolean, + value: { + publisherSignature: { certificateId: string }, + }, + }, + uri: string, + link: ?boolean, +}; + +class UriIndicator extends React.PureComponent { componentWillMount() { this.resolve(this.props); } - componentWillReceiveProps(nextProps) { + componentWillReceiveProps(nextProps: Props) { this.resolve(nextProps); } - resolve(props) { + resolve = (props: Props) => { const { isResolvingUri, resolveUri, claim, uri } = props; if (!isResolvingUri && claim === undefined && uri) { resolveUri(uri); } - } + }; render() { - const { claim, link, uri, isResolvingUri, smallCard, span } = this.props; - - if (isResolvingUri && !claim) { - return Validating...; - } - + const { claim, link, isResolvingUri } = this.props; if (!claim) { - return Unused; + return {isResolvingUri ? 'Validating...' : 'Unused'}; } const { @@ -38,41 +49,28 @@ class UriIndicator extends React.PureComponent { signature_is_valid: signatureIsValid, value, } = claim; + const channelClaimId = value && value.publisherSignature && value.publisherSignature.certificateId; if (!hasSignature || !channelName) { - return Anonymous; + return Anonymous; } - let icon, channelLink, modifier; - + let channelLink; if (signatureIsValid) { - modifier = 'valid'; channelLink = link ? buildURI({ channelName, claimId: channelClaimId }, false) : false; - } else { - icon = 'icon-times-circle'; - modifier = 'invalid'; } const inner = ( {channelName} {' '} - {!signatureIsValid ? ( - - ) : ( - '' - )} ); @@ -81,14 +79,14 @@ class UriIndicator extends React.PureComponent { } return ( - {inner} - + ); } } diff --git a/src/renderer/component/userEmailNew/view.jsx b/src/renderer/component/userEmailNew/view.jsx index 661adb611..16760dc6d 100644 --- a/src/renderer/component/userEmailNew/view.jsx +++ b/src/renderer/component/userEmailNew/view.jsx @@ -1,5 +1,7 @@ +// I'll come back to this +/* eslint-disable */ import React from 'react'; -import { Form, FormRow, Submit } from 'component/form.js'; +import { Form, FormRow, Submit } from 'component/common/form'; class UserEmailNew extends React.PureComponent { constructor(props) { @@ -53,3 +55,4 @@ class UserEmailNew extends React.PureComponent { } export default UserEmailNew; +/* eslint-enable */ diff --git a/src/renderer/component/userEmailVerify/view.jsx b/src/renderer/component/userEmailVerify/view.jsx index 3304730b9..5d6a367fa 100644 --- a/src/renderer/component/userEmailVerify/view.jsx +++ b/src/renderer/component/userEmailVerify/view.jsx @@ -1,6 +1,8 @@ +// I'll come back to this +/* eslint-disable */ import React from 'react'; -import Link from 'component/link'; -import { Form, FormRow, Submit } from 'component/form.js'; +import Button from 'component/button'; +import { Form, FormField, Submit } from 'component/common/form'; class UserEmailVerify extends React.PureComponent { constructor(props) { @@ -29,24 +31,27 @@ class UserEmailVerify extends React.PureComponent { render() { const { cancelButton, errorMessage, email, isPending } = this.props; + // ( + // { + // this.handleCodeChanged(event); + // }} + // /> + // )} + // /> return (

Please enter the verification code emailed to {email}.

- { - this.handleCodeChanged(event); - }} - errorMessage={errorMessage} - /> {/* render help separately so it always shows */}

- {__('Email')} or join our{' '} - {' '} + {__('Email')}

@@ -60,3 +65,4 @@ class UserEmailVerify extends React.PureComponent { } export default UserEmailVerify; +/* eslint-enable */ diff --git a/src/renderer/component/userPhoneNew/view.jsx b/src/renderer/component/userPhoneNew/view.jsx index b9999c746..fefd640de 100644 --- a/src/renderer/component/userPhoneNew/view.jsx +++ b/src/renderer/component/userPhoneNew/view.jsx @@ -1,6 +1,7 @@ +// I'll come back to this +/* eslint-disable */ import React from 'react'; -import { Form, FormRow, Submit } from 'component/form.js'; -import FormField from 'component/formField'; +import { Form, FormRow, FormField } from 'component/common/form'; const os = require('os').type(); const countryCodes = require('country-data') @@ -77,29 +78,36 @@ class UserPhoneNew extends React.PureComponent {

- - {countryCodes.map((country, index) => ( - - ))} - - ( + + )} + /> + { this.handleChanged(event); }} + render={() => ( + + )} />
-
- - {cancelButton} -
+ + {cancelButton}
); @@ -107,3 +115,4 @@ class UserPhoneNew extends React.PureComponent { } export default UserPhoneNew; +/* eslint-enable */ diff --git a/src/renderer/component/userPhoneVerify/view.jsx b/src/renderer/component/userPhoneVerify/view.jsx index 225fad5d1..7e038da06 100644 --- a/src/renderer/component/userPhoneVerify/view.jsx +++ b/src/renderer/component/userPhoneVerify/view.jsx @@ -1,6 +1,8 @@ +// I'll come back to this +/* eslint-disable */ import React from 'react'; -import Link from 'component/link'; -import { Form, FormRow, Submit } from 'component/form.js'; +import Button from 'component/button'; +import { Form, FormElement, Submit } from 'component/common/form'; class UserPhoneVerify extends React.PureComponent { constructor(props) { @@ -35,23 +37,27 @@ class UserPhoneVerify extends React.PureComponent { {__( `Please enter the verification code sent to +${countryCode}${phone}. Didn't receive it? ` )} - +
@@ -65,3 +71,4 @@ class UserPhoneVerify extends React.PureComponent { } export default UserPhoneVerify; +/* eslint-enable */ diff --git a/src/renderer/component/userVerify/view.jsx b/src/renderer/component/userVerify/view.jsx index bf7060e6a..4e730a986 100644 --- a/src/renderer/component/userVerify/view.jsx +++ b/src/renderer/component/userVerify/view.jsx @@ -1,7 +1,9 @@ +/* eslint-disable */ import React from 'react'; -import Link from 'component/link'; +import Button from 'component/button'; import CardVerify from 'component/cardVerify'; import lbryio from 'lbryio.js'; +import * as icons from 'constants/icons'; class UserVerify extends React.PureComponent { constructor(props) { @@ -25,9 +27,9 @@ class UserVerify extends React.PureComponent { render() { const { errorMessage, isPending, navigate, verifyPhone, modal } = this.props; return ( -
-
-
+ +
+

{__('Final Human Proof')}

@@ -36,8 +38,8 @@ class UserVerify extends React.PureComponent {

-
-
+
+

{__('1) Proof via Credit')}

@@ -57,15 +59,15 @@ class UserVerify extends React.PureComponent {
{__('A $1 authorization may temporarily appear with your provider.')}{' '} -
-
-
+
+

{__('2) Proof via Phone')}

@@ -74,24 +76,24 @@ class UserVerify extends React.PureComponent { )}`}
- { verifyPhone(); }} button="alt" - icon="icon-phone" + icon={icons.PHONE} label={__('Submit Phone Number')} />
{__('Standard messaging rates apply. Having trouble?')}{' '} - +
-
+

{__('3) Proof via Chat')}

@@ -107,16 +109,16 @@ class UserVerify extends React.PureComponent {

-
-
-
+
+
{__('Or, Skip It Entirely')}
@@ -127,12 +129,13 @@ class UserVerify extends React.PureComponent {

- navigate('/discover')} button="alt" label={__('Skip Rewards')} /> +
-
+ ); } } export default UserVerify; +/* eslint-enable */ diff --git a/src/renderer/component/video/internal/loading-screen.jsx b/src/renderer/component/video/internal/loading-screen.jsx index 958619b79..ccada9d41 100644 --- a/src/renderer/component/video/internal/loading-screen.jsx +++ b/src/renderer/component/video/internal/loading-screen.jsx @@ -1,14 +1,27 @@ +// @flow import React from 'react'; import Spinner from 'component/common/spinner'; -const LoadingScreen = ({ status, spinner = true }) => ( -
-
- {spinner && } +type Props = { + spinner: boolean, + status: string, +}; -
{status}
-
-
-); +class LoadingScreen extends React.PureComponent { + static defaultProps = { + spinner: true, + }; + + render() { + const { status, spinner } = this.props; + return ( +
+ {spinner && } + + {status} +
+ ); + } +} export default LoadingScreen; diff --git a/src/renderer/component/video/internal/play-button.jsx b/src/renderer/component/video/internal/play-button.jsx index 35f15f636..74e16cf7a 100644 --- a/src/renderer/component/video/internal/play-button.jsx +++ b/src/renderer/component/video/internal/play-button.jsx @@ -1,21 +1,21 @@ +// @flow import React from 'react'; -import Link from 'component/link'; +import Button from 'component/button'; -class VideoPlayButton extends React.PureComponent { - componentDidMount() { - this.keyDownListener = this.onKeyDown.bind(this); - document.addEventListener('keydown', this.keyDownListener); - } +type Props = { + play: string => void, + isLoading: boolean, + uri: string, + mediaType: string, + fileInfo: ?{}, +}; - componentWillUnmount() { - document.removeEventListener('keydown', this.keyDownListener); - } +class VideoPlayButton extends React.PureComponent { + watch: () => void; - onKeyDown(event) { - if (event.target.tagName.toLowerCase() !== 'input' && event.code === 'Space') { - event.preventDefault(); - this.watch(); - } + constructor() { + super(); + this.watch = this.watch.bind(this); } watch() { @@ -23,26 +23,19 @@ class VideoPlayButton extends React.PureComponent { } render() { - const { button, label, fileInfo, mediaType } = this.props; - - /* - title={ - isLoading ? "Video is Loading" : - !costInfo ? "Waiting on cost info..." : - fileInfo === undefined ? "Waiting on file info..." : "" - } - */ - - const icon = ['audio', 'video'].indexOf(mediaType) !== -1 ? 'icon-play' : 'icon-folder-o'; + const { fileInfo, mediaType, isLoading } = this.props; + const disabled = isLoading || fileInfo === undefined; + const doesPlayback = ['audio', 'video'].indexOf(mediaType) !== -1; + const icon = doesPlayback ? 'Play' : 'Folder'; + const label = doesPlayback ? 'Play' : 'View'; return ( - this.watch()} + onClick={this.watch} /> ); } diff --git a/src/renderer/component/video/internal/player.jsx b/src/renderer/component/video/internal/player.jsx index 8bbf2692f..547be16c4 100644 --- a/src/renderer/component/video/internal/player.jsx +++ b/src/renderer/component/video/internal/player.jsx @@ -1,6 +1,7 @@ -import { remote } from 'electron'; +/* eslint-disable */ import React from 'react'; -import { Thumbnail } from 'component/common'; +import { remote } from 'electron'; +import Thumbnail from 'component/common/thumbnail'; import player from 'render-media'; import fs from 'fs'; import LoadingScreen from './loading-screen'; @@ -20,6 +21,11 @@ class VideoPlayer extends React.PureComponent { this.togglePlayListener = this.togglePlay.bind(this); } + componentWillReceiveProps(nextProps) { + const el = this.refs.media.children[0]; + if (!this.props.paused && nextProps.paused && !el.paused) el.pause(); + } + componentDidMount() { const container = this.media; const { contentType, changeVolume, volume, position, claim } = this.props; @@ -161,10 +167,10 @@ class VideoPlayer extends React.PureComponent { const unplayableMessage = "Sorry, looks like we can't play this file."; return ( -
+ {['audio', 'application'].indexOf(mediaType) !== -1 && (!this.playableType() || hasMetadata) && - !unplayable && } + !unplayable && } {this.playableType() && !hasMetadata && !unplayable && } @@ -173,11 +179,11 @@ class VideoPlayer extends React.PureComponent { ref={container => { this.media = container; }} - className="media" /> -
+ ); } } export default VideoPlayer; +/* eslint-disable */ diff --git a/src/renderer/component/video/view.jsx b/src/renderer/component/video/view.jsx index 9a1afe7e4..965bedd03 100644 --- a/src/renderer/component/video/view.jsx +++ b/src/renderer/component/video/view.jsx @@ -1,23 +1,48 @@ +// @flow import React from 'react'; import lbry from 'lbry'; +import classnames from 'classnames'; import VideoPlayer from './internal/player'; import VideoPlayButton from './internal/play-button'; import LoadingScreen from './internal/loading-screen'; -import NsfwOverlay from 'component/nsfwOverlay'; -class Video extends React.PureComponent { - constructor(props) { - super(props); - this.state = { - showNsfwHelp: false, - }; - } +type Props = { + cancelPlay: () => void, + fileInfo: { + outpoint: string, + file_name: string, + written_bytes: number, + download_path: string, + completed: boolean, + }, + metadata: ?{ + nsfw: boolean, + thumbnail: string, + }, + isLoading: boolean, + isDownloading: boolean, + playingUri: ?string, + contentType: string, + changeVolume: number => void, + volume: number, + claim: {}, + uri: string, + doPlay: () => void, + doPause: () => void, + savePosition: (string, number) => void, + mediaPaused: boolean, + mediaPosition: ?number, + className: ?string, + obscureNsfw: boolean, + play: string => void, +}; +class Video extends React.PureComponent { componentWillUnmount() { this.props.cancelPlay(); } - isMediaSame(nextProps) { + isMediaSame(nextProps: Props) { return ( this.props.fileInfo && nextProps.fileInfo && @@ -25,22 +50,6 @@ class Video extends React.PureComponent { ); } - handleMouseOver() { - if (this.props.obscureNsfw && this.props.metadata && this.props.metadata.nsfw) { - this.setState({ - showNsfwHelp: true, - }); - } - } - - handleMouseOut() { - if (this.state.showNsfwHelp) { - this.setState({ - showNsfwHelp: false, - }); - } - } - render() { const { metadata, @@ -58,11 +67,14 @@ class Video extends React.PureComponent { savePosition, mediaPaused, mediaPosition, + className, + obscureNsfw, + play, } = this.props; const isPlaying = playingUri === uri; const isReadyToPlay = fileInfo && fileInfo.written_bytes > 0; - const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw; + const shouldObscureNsfw = obscureNsfw && metadata && metadata.nsfw; const mediaType = lbry.getMediaType(contentType, fileInfo && fileInfo.file_name); let loadStatusMessage = ''; @@ -77,51 +89,49 @@ class Video extends React.PureComponent { loadStatusMessage = __('Downloading stream... not long left now!'); } - const klasses = []; - klasses.push(obscureNsfw ? 'video--obscured ' : ''); - if (isLoading || isDownloading) klasses.push('video-embedded', 'video'); - if (mediaType === 'video') { - klasses.push('video-embedded', 'video'); - klasses.push(isPlaying ? 'video--active' : 'video--hidden'); - } else if (mediaType === 'application') { - klasses.push('video-embedded'); - } else if (!isPlaying) klasses.push('video-embedded'); - const poster = metadata.thumbnail; + const poster = metadata && metadata.thumbnail; return ( -
- {isPlaying && - (!isReadyToPlay ? ( - - ) : ( - - ))} - {!isPlaying && ( -
- +
+ {isPlaying && ( +
+ {!isReadyToPlay ? ( + + ) : ( + + )} +
+ )} + {!isPlaying && ( +
+
)} - {this.state.showNsfwHelp && }
); } diff --git a/src/renderer/component/walletAddress/view.jsx b/src/renderer/component/walletAddress/view.jsx index a062f0bb5..1955f5aaf 100644 --- a/src/renderer/component/walletAddress/view.jsx +++ b/src/renderer/component/walletAddress/view.jsx @@ -1,8 +1,17 @@ +// @flow import React from 'react'; -import Link from 'component/link'; +import Button from 'component/button'; import Address from 'component/address'; +import * as icons from 'constants/icons'; -class WalletAddress extends React.PureComponent { +type Props = { + checkAddressIsMine: string => void, + receiveAddress: string, + getNewAddress: () => void, + gettingNewAddress: boolean, +}; + +class WalletAddress extends React.PureComponent { componentWillMount() { this.props.checkAddressIsMine(this.props.receiveAddress); } @@ -11,21 +20,21 @@ class WalletAddress extends React.PureComponent { const { receiveAddress, getNewAddress, gettingNewAddress } = this.props; return ( -
-
-

{__('Receive Credits')}

-
+
+
{__('Receive Credits')}
+

+ {__('Use this wallet address to receive credits sent by another user (or yourself).')} +

+
-

- {__('Use this wallet address to receive credits sent by another user (or yourself).')} -

+
- diff --git a/src/renderer/component/walletBalance/view.jsx b/src/renderer/component/walletBalance/view.jsx index 9c1b1f6cc..112fc8f9f 100644 --- a/src/renderer/component/walletBalance/view.jsx +++ b/src/renderer/component/walletBalance/view.jsx @@ -1,33 +1,21 @@ +// @flow import React from 'react'; -import Link from 'component/link'; -import { CreditAmount } from 'component/common'; +import CreditAmount from 'component/common/credit-amount'; -const WalletBalance = props => { - const { balance, navigate } = props; - /* -
- navigate("/backup")} - label={__("Backup Your Wallet")} - /> -
- */ +type Props = { + balance: number, +}; + +const WalletBalance = (props: Props) => { + const { balance } = props; return ( -
-
-

{__('Balance')}

+
+
+
{__('Balance')}
+ {__('You currently have')}
- {(balance || balance === 0) && } -
-
- - + {(balance || balance === 0) && }
); diff --git a/src/renderer/component/walletSend/index.js b/src/renderer/component/walletSend/index.js index fa005c0c4..2643c5b57 100644 --- a/src/renderer/component/walletSend/index.js +++ b/src/renderer/component/walletSend/index.js @@ -1,28 +1,14 @@ -import React from 'react'; import { connect } from 'react-redux'; -import { - doSendDraftTransaction, - doSetDraftTransactionAmount, - doSetDraftTransactionAddress, -} from 'redux/actions/wallet'; -import { - selectDraftTransactionAmount, - selectDraftTransactionAddress, - selectDraftTransactionError, -} from 'redux/selectors/wallet'; - +import { doSendDraftTransaction } from 'redux/actions/wallet'; +import { selectBalance } from 'redux/selectors/wallet'; import WalletSend from './view'; -const select = state => ({ - address: selectDraftTransactionAddress(state), - amount: selectDraftTransactionAmount(state), - error: selectDraftTransactionError(state), +const perform = dispatch => ({ + sendToAddress: values => dispatch(doSendDraftTransaction(values)), }); -const perform = dispatch => ({ - sendToAddress: () => dispatch(doSendDraftTransaction()), - setAmount: event => dispatch(doSetDraftTransactionAmount(event.target.value)), - setAddress: event => dispatch(doSetDraftTransactionAddress(event.target.value)), +const select = state => ({ + balance: selectBalance(state), }); export default connect(select, perform)(WalletSend); diff --git a/src/renderer/component/walletSend/view.jsx b/src/renderer/component/walletSend/view.jsx index 7c51953b1..4a124527a 100644 --- a/src/renderer/component/walletSend/view.jsx +++ b/src/renderer/component/walletSend/view.jsx @@ -1,55 +1,93 @@ +// @flow import React from 'react'; -import { Form, FormRow, Submit } from 'component/form'; -import { regexAddress } from 'lbryURI'; +import Button from 'component/button'; +import { Form, FormRow, FormField } from 'component/common/form'; +import { Formik } from 'formik'; +import { validateSendTx } from 'util/form-validation'; -class WalletSend extends React.PureComponent { - handleSubmit() { - const { amount, address, sendToAddress } = this.props; - const validSubmit = parseFloat(amount) > 0.0 && address; +type DraftTransaction = { + address: string, + amount: number | string, // So we can use a placeholder in the input +}; - if (validSubmit) { - sendToAddress(); - } +type Props = { + sendToAddress: DraftTransaction => void, + balance: number, +}; + +class WalletSend extends React.PureComponent { + constructor() { + super(); + + (this: any).handleSubmit = this.handleSubmit.bind(this); + } + + handleSubmit(values: DraftTransaction) { + const { sendToAddress } = this.props; + sendToAddress(values); } render() { - const { closeModal, modal, setAmount, setAddress, amount, address, error } = this.props; + const { balance } = this.props; return ( -
-
-
-

{__('Send Credits')}

-
-
- -
-
- -
- 0.0) || !address} /> -
-
-
+
+
{__('Send Credits')}
+
+ ( +
+ + balance && __('Not enough')) + } + /> + + + +
+
+
+ )} + /> +
); } diff --git a/src/renderer/component/walletSendTip/view.jsx b/src/renderer/component/walletSendTip/view.jsx index d3e106c19..6433d0cc1 100644 --- a/src/renderer/component/walletSendTip/view.jsx +++ b/src/renderer/component/walletSendTip/view.jsx @@ -1,64 +1,89 @@ +// @flow import React from 'react'; -import Link from 'component/link'; -import { FormRow } from 'component/form'; +import Button from 'component/button'; +import { FormField } from 'component/common/form'; import UriIndicator from 'component/uriIndicator'; -class WalletSendTip extends React.PureComponent { - constructor(props) { +type Props = { + claim_id: string, + uri: string, + title: string, + errorMessage: string, + isPending: boolean, + sendSupport: (number, string, string) => void, + onCancel: () => void, + sendTipCallback?: () => void, +}; + +type State = { + amount: number, +}; + +class WalletSendTip extends React.PureComponent { + constructor(props: Props) { super(props); this.state = { - amount: 0.0, + amount: 0, }; + + (this: any).handleSendButtonClicked = this.handleSendButtonClicked.bind(this); } handleSendButtonClicked() { - const { claim_id, uri } = this.props; - const amount = this.state.amount; - this.props.sendSupport(amount, claim_id, uri); + const { claim_id: claimId, uri, sendSupport, sendTipCallback } = this.props; + const { amount } = this.state; + + sendSupport(amount, claimId, uri); + + // ex: close modal + if (sendTipCallback) { + sendTipCallback(); + } } - handleSupportPriceChange(event) { + handleSupportPriceChange(event: SyntheticInputEvent<*>) { this.setState({ amount: Number(event.target.value), }); } render() { - const { errorMessage, isPending, title, uri } = this.props; + const { errorMessage, isPending, title, uri, onCancel } = this.props; return (
-
+

- {__('Support')} + {__('Send a tip to')}

- - {`${__('This will appear as a tip for "%s" located at %s.', title, uri)} `} - - - } placeholder="1.00" onChange={event => this.handleSupportPriceChange(event)} + helper={ + + {__(`This will appear as a tip for ${title} located at ${uri}.`)}{' '} +
diff --git a/src/renderer/component/wunderbar/index.js b/src/renderer/component/wunderbar/index.js index c446a885e..9491c46f3 100644 --- a/src/renderer/component/wunderbar/index.js +++ b/src/renderer/component/wunderbar/index.js @@ -1,19 +1,33 @@ -import React from 'react'; +import * as MODALS from 'constants/modal_types'; import { connect } from 'react-redux'; -import { normalizeURI } from 'lbryURI.js'; -import { selectWunderBarAddress, selectWunderBarIcon } from 'redux/selectors/search'; +import { normalizeURI } from 'lbryURI'; +import { selectState as selectSearch, selectWunderBarAddress } from 'redux/selectors/search'; +import { doUpdateSearchQuery } from 'redux/actions/search'; import { doNavigate } from 'redux/actions/navigation'; +import { doOpenModal } from 'redux/actions/app'; import Wunderbar from './view'; -const select = state => ({ - address: selectWunderBarAddress(state), - icon: selectWunderBarIcon(state), -}); +const select = state => { + const { isActive, searchQuery, ...searchState } = selectSearch(state); + const address = selectWunderBarAddress(state); + + // if we are on the file/channel page + // use the address in the history stack + const wunderbarValue = isActive ? searchQuery : searchQuery || address; + + return { + ...searchState, + wunderbarValue, + }; +}; const perform = dispatch => ({ - onSearch: query => dispatch(doNavigate('/search', { query })), - onSubmit: (query, extraParams) => - dispatch(doNavigate('/show', { uri: normalizeURI(query), ...extraParams })), + onSearch: query => { + dispatch(doUpdateSearchQuery(query)); + dispatch(doOpenModal(MODALS.SEARCH)); + }, + onSubmit: (uri, extraParams) => dispatch(doNavigate('/show', { uri, ...extraParams })), + updateSearchQuery: query => dispatch(doUpdateSearchQuery(query)), }); export default connect(select, perform)(Wunderbar); diff --git a/src/renderer/component/wunderbar/internal/autocomplete.jsx b/src/renderer/component/wunderbar/internal/autocomplete.jsx new file mode 100644 index 000000000..78b3216a9 --- /dev/null +++ b/src/renderer/component/wunderbar/internal/autocomplete.jsx @@ -0,0 +1,604 @@ +/* +This is taken from https://github.com/reactjs/react-autocomplete + +We aren't using that component because (for now) there is no way to autohightlight +the first item if it isn't an exact match from what is in the search bar. + +Our use case is: +value in search bar: "hello" +first suggestion: "lbry://hello" + +I changed the function maybeAutoCompleteText to check if the suggestion contains +the search query anywhere, instead of the suggestion starting with it + +https://github.com/reactjs/react-autocomplete/issues/239 +*/ +/* eslint-disable */ + +const React = require('react'); +const PropTypes = require('prop-types'); +const { findDOMNode } = require('react-dom'); +const scrollIntoView = require('dom-scroll-into-view'); + +const IMPERATIVE_API = [ + 'blur', + 'checkValidity', + 'click', + 'focus', + 'select', + 'setCustomValidity', + 'setSelectionRange', + 'setRangeText', +]; + +function getScrollOffset() { + return { + x: + window.pageXOffset !== undefined + ? window.pageXOffset + : (document.documentElement || document.body.parentNode || document.body).scrollLeft, + y: + window.pageYOffset !== undefined + ? window.pageYOffset + : (document.documentElement || document.body.parentNode || document.body).scrollTop, + }; +} + +export default class Autocomplete extends React.Component { + static propTypes = { + /** + * The items to display in the dropdown menu + */ + items: PropTypes.array.isRequired, + /** + * The value to display in the input field + */ + value: PropTypes.any, + /** + * Arguments: `event: Event, value: String` + * + * Invoked every time the user changes the input's value. + */ + onChange: PropTypes.func, + /** + * Arguments: `value: String, item: Any` + * + * Invoked when the user selects an item from the dropdown menu. + */ + onSelect: PropTypes.func, + /** + * Arguments: `item: Any, value: String` + * + * Invoked for each entry in `items` and its return value is used to + * determine whether or not it should be displayed in the dropdown menu. + * By default all items are always rendered. + */ + shouldItemRender: PropTypes.func, + /** + * Arguments: `itemA: Any, itemB: Any, value: String` + * + * The function which is used to sort `items` before display. + */ + sortItems: PropTypes.func, + /** + * Arguments: `item: Any` + * + * Used to read the display value from each entry in `items`. + */ + getItemValue: PropTypes.func.isRequired, + /** + * Arguments: `item: Any, isHighlighted: Boolean, styles: Object` + * + * Invoked for each entry in `items` that also passes `shouldItemRender` to + * generate the render tree for each item in the dropdown menu. `styles` is + * an optional set of styles that can be applied to improve the look/feel + * of the items in the dropdown menu. + */ + renderItem: PropTypes.func.isRequired, + /** + * Arguments: `items: Array, value: String, styles: Object` + * + * Invoked to generate the render tree for the dropdown menu. Ensure the + * returned tree includes every entry in `items` or else the highlight order + * and keyboard navigation logic will break. `styles` will contain + * { top, left, minWidth } which are the coordinates of the top-left corner + * and the width of the dropdown menu. + */ + renderMenu: PropTypes.func, + /** + * Styles that are applied to the dropdown menu in the default `renderMenu` + * implementation. If you override `renderMenu` and you want to use + * `menuStyle` you must manually apply them (`this.props.menuStyle`). + */ + menuStyle: PropTypes.object, + /** + * Arguments: `props: Object` + * + * Invoked to generate the input element. The `props` argument is the result + * of merging `props.inputProps` with a selection of props that are required + * both for functionality and accessibility. At the very least you need to + * apply `props.ref` and all `props.on` event handlers. Failing to do + * this will cause `Autocomplete` to behave unexpectedly. + */ + renderInput: PropTypes.func, + /** + * Props passed to `props.renderInput`. By default these props will be + * applied to the `` element rendered by `Autocomplete`, unless you + * have specified a custom value for `props.renderInput`. Any properties + * supported by `HTMLInputElement` can be specified, apart from the + * following which are set by `Autocomplete`: value, autoComplete, role, + * aria-autocomplete. `inputProps` is commonly used for (but not limited to) + * placeholder, event handlers (onFocus, onBlur, etc.), autoFocus, etc.. + */ + inputProps: PropTypes.object, + /** + * Props that are applied to the element which wraps the `` and + * dropdown menu elements rendered by `Autocomplete`. + */ + wrapperProps: PropTypes.object, + /** + * This is a shorthand for `wrapperProps={{ style: }}`. + * Note that `wrapperStyle` is applied before `wrapperProps`, so the latter + * will win if it contains a `style` entry. + */ + wrapperStyle: PropTypes.object, + /** + * Whether or not to automatically highlight the top match in the dropdown + * menu. + */ + autoHighlight: PropTypes.bool, + /** + * Whether or not to automatically select the highlighted item when the + * `` loses focus. + */ + selectOnBlur: PropTypes.bool, + /** + * Arguments: `isOpen: Boolean` + * + * Invoked every time the dropdown menu's visibility changes (i.e. every + * time it is displayed/hidden). + */ + onMenuVisibilityChange: PropTypes.func, + /** + * Used to override the internal logic which displays/hides the dropdown + * menu. This is useful if you want to force a certain state based on your + * UX/business logic. Use it together with `onMenuVisibilityChange` for + * fine-grained control over the dropdown menu dynamics. + */ + open: PropTypes.bool, + debug: PropTypes.bool, + }; + + static defaultProps = { + value: '', + wrapperProps: {}, + wrapperStyle: { + display: 'inline-block', + }, + inputProps: {}, + renderInput(props) { + return ; + }, + onChange() {}, + onSelect() {}, + renderMenu(items, value, style) { + return
; + }, + menuStyle: { + borderRadius: '3px', + boxShadow: '0 2px 12px rgba(0, 0, 0, 0.1)', + background: 'rgba(255, 255, 255, 0.9)', + padding: '2px 0', + fontSize: '90%', + position: 'fixed', + overflow: 'hidden', + maxHeight: '50%', // TODO: don't cheat, let it flow to the bottom, + }, + autoHighlight: true, + selectOnBlur: false, + onMenuVisibilityChange() {}, + }; + + constructor(props) { + super(props); + this.state = { + isOpen: false, + highlightedIndex: null, + }; + this._debugStates = []; + this.ensureHighlightedIndex = this.ensureHighlightedIndex.bind(this); + this.exposeAPI = this.exposeAPI.bind(this); + this.handleInputFocus = this.handleInputFocus.bind(this); + this.handleInputBlur = this.handleInputBlur.bind(this); + this.handleChange = this.handleChange.bind(this); + this.handleKeyDown = this.handleKeyDown.bind(this); + this.handleInputClick = this.handleInputClick.bind(this); + this.maybeAutoCompleteText = this.maybeAutoCompleteText.bind(this); + } + + componentWillMount() { + // this.refs is frozen, so we need to assign a new object to it + this.refs = {}; + this._ignoreBlur = false; + this._ignoreFocus = false; + this._scrollOffset = null; + this._scrollTimer = null; + } + + componentWillUnmount() { + clearTimeout(this._scrollTimer); + this._scrollTimer = null; + } + + componentWillReceiveProps(nextProps) { + if (this.state.highlightedIndex !== null) { + this.setState(this.ensureHighlightedIndex); + } + if ( + nextProps.autoHighlight && + (this.props.value !== nextProps.value || this.state.highlightedIndex === null) + ) { + this.setState(this.maybeAutoCompleteText); + } + } + + componentDidMount() { + if (this.isOpen()) { + this.setMenuPositions(); + } + } + + componentDidUpdate(prevProps, prevState) { + if ( + (this.state.isOpen && !prevState.isOpen) || + ('open' in this.props && this.props.open && !prevProps.open) + ) + this.setMenuPositions(); + + this.maybeScrollItemIntoView(); + if (prevState.isOpen !== this.state.isOpen) { + this.props.onMenuVisibilityChange(this.state.isOpen); + } + } + + exposeAPI(el) { + this.refs.input = el; + IMPERATIVE_API.forEach(ev => (this[ev] = el && el[ev] && el[ev].bind(el))); + } + + maybeScrollItemIntoView() { + if (this.isOpen() && this.state.highlightedIndex !== null) { + const itemNode = this.refs[`item-${this.state.highlightedIndex}`]; + const menuNode = this.refs.menu; + scrollIntoView(findDOMNode(itemNode), findDOMNode(menuNode), { + onlyScrollIfNeeded: true, + }); + } + } + + handleKeyDown(event) { + if (Autocomplete.keyDownHandlers[event.key]) + Autocomplete.keyDownHandlers[event.key].call(this, event); + else if (!this.isOpen()) { + this.setState({ + isOpen: true, + }); + } + } + + handleChange(event) { + this.props.onChange(event, event.target.value); + } + + static keyDownHandlers = { + ArrowDown(event) { + event.preventDefault(); + const itemsLength = this.getFilteredItems(this.props).length; + if (!itemsLength) return; + const { highlightedIndex } = this.state; + const index = + highlightedIndex === null || highlightedIndex === itemsLength - 1 + ? 0 + : highlightedIndex + 1; + this.setState({ + highlightedIndex: index, + isOpen: true, + }); + }, + + ArrowUp(event) { + event.preventDefault(); + const itemsLength = this.getFilteredItems(this.props).length; + if (!itemsLength) return; + const { highlightedIndex } = this.state; + const index = + highlightedIndex === 0 || highlightedIndex === null + ? itemsLength - 1 + : highlightedIndex - 1; + this.setState({ + highlightedIndex: index, + isOpen: true, + }); + }, + + Enter(event) { + // Key code 229 is used for selecting items from character selectors (Pinyin, Kana, etc) + if (event.keyCode !== 13) return; + + const inputValue = this.refs.input.value; + if (!inputValue) return; + + if (!this.isOpen() || this.state.highlightedIndex == null) { + // User pressed enter before any search suggestions were populated + this.setState({ isOpen: false }, () => { + this.props.onSelect(inputValue); + this.refs.input.blur(); + }); + } else { + // text entered + menu item has been highlighted + enter is hit -> update value to that of selected menu item, close the menu + event.preventDefault(); + const item = this.getFilteredItems(this.props)[this.state.highlightedIndex]; + const value = this.props.getItemValue(item); + this.setState( + { + isOpen: false, + highlightedIndex: null, + }, + () => { + this.props.onSelect(value, item); + this.refs.input.blur(); + } + ); + } + }, + + Escape() { + // In case the user is currently hovering over the menu + this.setIgnoreBlur(false); + this.setState({ + highlightedIndex: null, + isOpen: false, + }); + }, + + Tab() { + // In case the user is currently hovering over the menu + this.setIgnoreBlur(false); + }, + }; + + getFilteredItems(props) { + let items = props.items; + + if (props.shouldItemRender) { + items = items.filter(item => props.shouldItemRender(item, props.value)); + } + + if (props.sortItems) { + items.sort((a, b) => props.sortItems(a, b, props.value)); + } + + return items; + } + + maybeAutoCompleteText(state, props) { + const { highlightedIndex } = state; + const { value, getItemValue } = props; + const index = highlightedIndex === null ? 0 : highlightedIndex; + const matchedItem = this.getFilteredItems(props)[index]; + if (value !== '' && matchedItem) { + const itemValue = getItemValue(matchedItem); + const itemValueDoesMatch = + itemValue.toLowerCase().indexOf( + value.toLowerCase() + // below line is the the only thing that is changed from the real component + ) !== -1; + if (itemValueDoesMatch) { + return { highlightedIndex: index }; + } + } + return { highlightedIndex: null }; + } + + ensureHighlightedIndex(state, props) { + if (state.highlightedIndex >= this.getFilteredItems(props).length) { + return { highlightedIndex: null }; + } + } + + setMenuPositions() { + const node = this.refs.input; + const rect = node.getBoundingClientRect(); + const computedStyle = global.window.getComputedStyle(node); + const marginBottom = parseInt(computedStyle.marginBottom, 10) || 0; + const marginLeft = parseInt(computedStyle.marginLeft, 10) || 0; + const marginRight = parseInt(computedStyle.marginRight, 10) || 0; + this.setState({ + menuTop: rect.bottom + marginBottom, + menuLeft: rect.left + marginLeft, + menuWidth: rect.width + marginLeft + marginRight, + }); + } + + highlightItemFromMouse(index) { + this.setState({ highlightedIndex: index }); + } + + selectItemFromMouse(item) { + const value = this.props.getItemValue(item); + // The menu will de-render before a mouseLeave event + // happens. Clear the flag to release control over focus + this.setIgnoreBlur(false); + this.setState( + { + isOpen: false, + highlightedIndex: null, + }, + () => { + this.props.onSelect(value, item); + } + ); + } + + setIgnoreBlur(ignore) { + this._ignoreBlur = ignore; + } + + renderMenu() { + const items = this.getFilteredItems(this.props).map((item, index) => { + const element = this.props.renderItem(item, this.state.highlightedIndex === index, { + cursor: 'default', + }); + return React.cloneElement(element, { + onMouseEnter: () => this.highlightItemFromMouse(index), + onClick: () => this.selectItemFromMouse(item), + ref: e => (this.refs[`item-${index}`] = e), + }); + }); + const style = { + left: this.state.menuLeft, + top: this.state.menuTop, + minWidth: this.state.menuWidth, + }; + const menu = this.props.renderMenu(items, this.props.value, style); + return React.cloneElement(menu, { + ref: e => (this.refs.menu = e), + className: 'wunderbar__menu', + // Ignore blur to prevent menu from de-rendering before we can process click + onMouseEnter: () => this.setIgnoreBlur(true), + onMouseLeave: () => this.setIgnoreBlur(false), + }); + } + + handleInputBlur(event) { + if (this._ignoreBlur) { + this._ignoreFocus = true; + this._scrollOffset = getScrollOffset(); + this.refs.input.focus(); + return; + } + let setStateCallback; + const { highlightedIndex } = this.state; + if (this.props.selectOnBlur && highlightedIndex !== null) { + const items = this.getFilteredItems(this.props); + const item = items[highlightedIndex]; + const value = this.props.getItemValue(item); + setStateCallback = () => this.props.onSelect(value, item); + } + this.setState( + { + isOpen: false, + highlightedIndex: null, + }, + setStateCallback + ); + const { onBlur } = this.props.inputProps; + if (onBlur) { + onBlur(event); + } + } + + handleInputFocus(event) { + if (this._ignoreFocus) { + this._ignoreFocus = false; + const { x, y } = this._scrollOffset; + this._scrollOffset = null; + // Focus will cause the browser to scroll the into view. + // This can cause the mouse coords to change, which in turn + // could cause a new highlight to happen, cancelling the click + // event (when selecting with the mouse) + window.scrollTo(x, y); + // Some browsers wait until all focus event handlers have been + // processed before scrolling the into view, so let's + // scroll again on the next tick to ensure we're back to where + // the user was before focus was lost. We could do the deferred + // scroll only, but that causes a jarring split second jump in + // some browsers that scroll before the focus event handlers + // are triggered. + clearTimeout(this._scrollTimer); + this._scrollTimer = setTimeout(() => { + this._scrollTimer = null; + window.scrollTo(x, y); + }, 0); + return; + } + + // Highlight + this.refs.input.select(); + + this.setState({ isOpen: true }); + + const { onFocus } = this.props.inputProps; + if (onFocus) { + onFocus(event); + } + } + + isInputFocused() { + const el = this.refs.input; + return el.ownerDocument && el === el.ownerDocument.activeElement; + } + + handleInputClick() { + // Input will not be focused if it's disabled + if (this.isInputFocused() && !this.isOpen()) this.setState({ isOpen: true }); + } + + composeEventHandlers(internal, external) { + return external + ? e => { + internal(e); + external(e); + } + : internal; + } + + isOpen() { + return 'open' in this.props ? this.props.open : this.state.isOpen; + } + + render() { + if (this.props.debug) { + // you don't like it, you love it + this._debugStates.push({ + id: this._debugStates.length, + state: this.state, + }); + } + + const { inputProps, items } = this.props; + + const open = this.isOpen(); + return ( +
+ {this.props.renderInput({ + ...inputProps, + role: 'combobox', + 'aria-autocomplete': 'list', + 'aria-expanded': open, + autoComplete: 'off', + ref: this.exposeAPI, + onFocus: this.handleInputFocus, + onBlur: this.handleInputBlur, + onChange: this.handleChange, + onKeyDown: this.composeEventHandlers(this.handleKeyDown, inputProps.onKeyDown), + onClick: this.composeEventHandlers(this.handleInputClick, inputProps.onClick), + value: this.props.value, + })} + {open && !!items.length && this.renderMenu()} + {this.props.debug && ( +
+            {JSON.stringify(
+              this._debugStates.slice(
+                Math.max(0, this._debugStates.length - 5),
+                this._debugStates.length
+              ),
+              null,
+              2
+            )}
+          
+ )} +
+ ); + } +} diff --git a/src/renderer/component/wunderbar/view.jsx b/src/renderer/component/wunderbar/view.jsx index 29cbc7418..31b7fdee8 100644 --- a/src/renderer/component/wunderbar/view.jsx +++ b/src/renderer/component/wunderbar/view.jsx @@ -1,166 +1,124 @@ +// @flow import React from 'react'; -import PropTypes from 'prop-types'; -import { normalizeURI } from 'lbryURI'; -import Icon from 'component/icon'; +import classnames from 'classnames'; +import Icon from 'component/common/icon'; +import Autocomplete from './internal/autocomplete'; import { parseQueryParams } from 'util/query_params'; +import * as icons from 'constants/icons'; -class WunderBar extends React.PureComponent { - static TYPING_TIMEOUT = 800; +type Props = { + updateSearchQuery: string => void, + onSearch: string => void, + onSubmit: (string, {}) => void, + wunderbarValue: ?string, + suggestions: Array, +}; - static propTypes = { - onSearch: PropTypes.func.isRequired, - onSubmit: PropTypes.func.isRequired, +class WunderBar extends React.PureComponent { + constructor(props: Props) { + super(props); + + (this: any).handleSubmit = this.handleSubmit.bind(this); + (this: any).handleChange = this.handleChange.bind(this); + this.input = undefined; + } + + getSuggestionIcon = (type: string) => { + switch (type) { + case 'file': + return icons.COMPASS; + case 'channel': + return icons.AT_SIGN; + default: + return icons.SEARCH; + } }; - constructor(props) { - super(props); - this._userTypingTimer = null; - this._isSearchDispatchPending = false; - this._input = null; - this._stateBeforeSearch = null; - this._resetOnNextBlur = true; - this.onChange = this.onChange.bind(this); - this.onFocus = this.onFocus.bind(this); - this.onBlur = this.onBlur.bind(this); - this.onKeyPress = this.onKeyPress.bind(this); - this.onReceiveRef = this.onReceiveRef.bind(this); - this.state = { - address: this.props.address, - icon: this.props.icon, - }; + handleChange(e: SyntheticInputEvent<*>) { + const { updateSearchQuery } = this.props; + const { value } = e.target; + + updateSearchQuery(value); } - componentWillUnmount() { - if (this.userTypingTimer) { - clearTimeout(this._userTypingTimer); - } - } + handleSubmit(value: string, suggestion?: { value: string, type: string }) { + const { onSubmit, onSearch } = this.props; + const query = value.trim(); + const getParams = () => { + const parts = query.split('?'); - onChange(event) { - if (this._userTypingTimer) { - clearTimeout(this._userTypingTimer); - } - - this.setState({ address: event.target.value }); - - this._isSearchDispatchPending = true; - - const searchQuery = event.target.value; - - this._userTypingTimer = setTimeout(() => { - const hasQuery = searchQuery.length === 0; - this._resetOnNextBlur = hasQuery; - this._isSearchDispatchPending = false; - if (searchQuery) { - this.props.onSearch(searchQuery.trim()); + let extraParams = {}; + if (parts.length > 0) { + extraParams = parseQueryParams(parts.join('')); } - }, WunderBar.TYPING_TIMEOUT); // 800ms delay, tweak for faster/slower - } - componentWillReceiveProps(nextProps) { - if ( - nextProps.viewingPage !== this.props.viewingPage || - nextProps.address != this.props.address - ) { - this.setState({ address: nextProps.address, icon: nextProps.icon }); - } - } - - onFocus() { - this._stateBeforeSearch = this.state; - const newState = { - icon: 'icon-search', - isActive: true, + return extraParams; }; - this._focusPending = true; - // below is hacking, improved when we have proper routing - if (!this.state.address.startsWith('lbry://') && this.state.icon !== 'icon-search') { - // onFocus, if they are not on an exact URL or a search page, clear the bar - newState.address = ''; - } - this.setState(newState); - } - - onBlur() { - if (this._isSearchDispatchPending) { - setTimeout(() => { - this.onBlur(); - }, WunderBar.TYPING_TIMEOUT + 1); - } else { - const commonState = { isActive: false }; - if (this._resetOnNextBlur) { - this.setState(Object.assign({}, this._stateBeforeSearch, commonState)); - this._input.value = this.state.address; + // User selected a suggestion + if (suggestion) { + if (suggestion.type === 'search') { + onSearch(query); } else { - this._resetOnNextBlur = true; - this._stateBeforeSearch = this.state; - this.setState(commonState); + const params = getParams(); + const uri = normalizeURI(query); + onSubmit(uri, params); } + + return; + } + + // Currently no suggestion is highlighted. The user may have started + // typing, then lost focus and came back later on the same page + try { + const uri = normalizeURI(query); + const params = getParams(); + onSubmit(uri, params); + } catch (e) { + onSearch(query); } } - componentDidUpdate() { - if (this._input) { - const start = this._input.selectionStart, - end = this._input.selectionEnd; - - this._input.value = this.state.address; // this causes cursor to go to end of input - - this._input.setSelectionRange(start, end); - - if (this._focusPending) { - this._input.select(); - this._focusPending = false; - } - } - } - - onKeyPress(event) { - if (event.charCode == 13 && this._input.value) { - let uri = null, - method = 'onSubmit', - extraParams = {}; - - this._resetOnNextBlur = false; - clearTimeout(this._userTypingTimer); - - const parts = this._input.value.trim().split('?'); - const value = parts.shift(); - if (parts.length > 0) extraParams = parseQueryParams(parts.join('')); - - try { - uri = normalizeURI(value); - this.setState({ value: uri }); - } catch (error) { - // then it's not a valid URL, so let's search - uri = value; - method = 'onSearch'; - } - - this.props[method](uri, extraParams); - this._input.blur(); - } - } - - onReceiveRef(ref) { - this._input = ref; - } + input: ?HTMLInputElement; render() { + const { wunderbarValue, suggestions } = this.props; + return ( -
- {this.state.icon ? : ''} - + + item.value} + onChange={this.handleChange} + onSelect={this.handleSubmit} + renderInput={props => ( + + )} + renderItem={({ value, type, shorthand }, isHighlighted) => ( +
+ + {shorthand || value} + {(true || isHighlighted) && ( + + {'- '} + {type === 'search' ? 'Search' : value} + + )} +
+ )} />
); diff --git a/src/renderer/constants/action_types.js b/src/renderer/constants/action_types.js index 43594a8ce..1c718580c 100644 --- a/src/renderer/constants/action_types.js +++ b/src/renderer/constants/action_types.js @@ -39,8 +39,6 @@ export const FETCH_TRANSACTIONS_COMPLETED = 'FETCH_TRANSACTIONS_COMPLETED'; export const UPDATE_BALANCE = 'UPDATE_BALANCE'; export const CHECK_ADDRESS_IS_MINE_STARTED = 'CHECK_ADDRESS_IS_MINE_STARTED'; export const CHECK_ADDRESS_IS_MINE_COMPLETED = 'CHECK_ADDRESS_IS_MINE_COMPLETED'; -export const SET_DRAFT_TRANSACTION_AMOUNT = 'SET_DRAFT_TRANSACTION_AMOUNT'; -export const SET_DRAFT_TRANSACTION_ADDRESS = 'SET_DRAFT_TRANSACTION_ADDRESS'; export const SEND_TRANSACTION_STARTED = 'SEND_TRANSACTION_STARTED'; export const SEND_TRANSACTION_COMPLETED = 'SEND_TRANSACTION_COMPLETED'; export const SEND_TRANSACTION_FAILED = 'SEND_TRANSACTION_FAILED'; @@ -90,9 +88,11 @@ export const FETCH_AVAILABILITY_COMPLETED = 'FETCH_AVAILABILITY_COMPLETED'; export const FILE_DELETE = 'FILE_DELETE'; // Search -export const SEARCH_STARTED = 'SEARCH_STARTED'; -export const SEARCH_COMPLETED = 'SEARCH_COMPLETED'; -export const SEARCH_CANCELLED = 'SEARCH_CANCELLED'; +export const SEARCH_START = 'SEARCH_START'; +export const SEARCH_SUCCESS = 'SEARCH_SUCCESS'; +export const SEARCH_FAIL = 'SEARCH_FAIL'; +export const UPDATE_SEARCH_QUERY = 'UPDATE_SEARCH_QUERY'; +export const UPDATE_SEARCH_SUGGESTIONS = 'UPDATE_SEARCH_SUGGESTIONS'; // Settings export const DAEMON_SETTINGS_RECEIVED = 'DAEMON_SETTINGS_RECEIVED'; @@ -178,3 +178,13 @@ export const SET_VIDEO_PAUSE = 'SET_VIDEO_PAUSE'; export const MEDIA_PLAY = 'MEDIA_PLAY'; export const MEDIA_PAUSE = 'MEDIA_PAUSE'; export const MEDIA_POSITION = 'MEDIA_POSITION'; + +// Publishing +export const CLEAR_PUBLISH = 'CLEAR_PUBLISH'; +export const UPDATE_PUBLISH_FORM = 'UPDATE_PUBLISH_FORM'; +export const PUBLISH_START = 'PUBLISH_START'; +export const PUBLISH_SUCCESS = 'PUBLISH_SUCCESS'; +export const PUBLISH_FAIL = 'PUBLISH_FAIL'; +export const CLEAR_PUBLISH_ERROR = 'CLEAR_PUBLISH_ERROR'; +export const REMOVE_PENDING_PUBLISH = 'REMOVE_PENDING_PUBLISH'; +export const DO_PREPARE_EDIT = 'DO_PREPARE_EDIT'; diff --git a/src/renderer/constants/claim.js b/src/renderer/constants/claim.js new file mode 100644 index 000000000..3f5db67bf --- /dev/null +++ b/src/renderer/constants/claim.js @@ -0,0 +1,4 @@ +export const MINIMUM_PUBLISH_BID = 0.00000001; + +export const CHANNEL_ANONYMOUS = 'anonymous'; +export const CHANNEL_NEW = 'new'; diff --git a/src/renderer/constants/icons.js b/src/renderer/constants/icons.js index 87b9942ce..5b7b4a8a8 100644 --- a/src/renderer/constants/icons.js +++ b/src/renderer/constants/icons.js @@ -1,6 +1,25 @@ -export const FEATURED = 'rocket'; -export const LOCAL = 'folder'; -export const FILE = 'file'; -export const HISTORY = 'history'; -export const HELP_CIRCLE = 'question-circle'; -export const DOWNLOAD = 'download'; +export const FEATURED = 'Award'; +export const LOCAL = 'Folder'; +export const ALERT = 'AlertCircle'; +export const CLIPBOARD = 'Clipboard'; +export const ARROW_LEFT = 'ChevronLeft'; +export const ARROW_RIGHT = 'ChevronRight'; +export const DOWNLOAD = 'Download'; +export const UPLOAD = 'UploadCloud'; +export const CLOSE = 'X'; +export const EDIT = 'Edit3'; +export const TRASH = 'Trash'; +export const REPORT = 'Flag'; +export const OPEN = 'BookOpen'; +export const HELP = 'HelpCircle'; +export const MESSAGE = 'MessageCircle'; +export const SEND = 'Send'; +export const SEARCH = 'Search'; +export const COMPASS = 'Compass'; +export const AT_SIGN = 'AtSign'; +export const REFRESH = 'RefreshCw'; +export const CLOCK = 'Clock'; +export const HOME = 'Home'; +export const PHONE = 'Phone'; +export const CHECK = 'CheckCircle'; +export const HEART = 'Heart'; diff --git a/src/renderer/constants/licenses.js b/src/renderer/constants/licenses.js new file mode 100644 index 000000000..fbcc7e6a5 --- /dev/null +++ b/src/renderer/constants/licenses.js @@ -0,0 +1,31 @@ +export const CC_LICENSES = [ + { + value: 'Creative Commons Attribution 4.0 International', + url: 'https://creativecommons.org/licenses/by/4.0/legalcode', + }, + { + value: 'Creative Commons Attribution-ShareAlike 4.0 International', + url: 'https://creativecommons.org/licenses/by-sa/4.0/legalcode', + }, + { + value: 'Creative Commons Attribution-NoDerivatives 4.0 International', + url: 'https://creativecommons.org/licenses/by-nd/4.0/legalcode', + }, + { + value: 'Creative Commons Attribution-NonCommercial 4.0 International', + url: 'https://creativecommons.org/licenses/by-nc/4.0/legalcode', + }, + { + value: 'Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International', + url: 'https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode', + }, + { + value: 'Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International', + url: 'https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode', + }, +]; + +export const NONE = 'None'; +export const PUBLIC_DOMAIN = 'Public Domain'; +export const OTHER = 'other'; +export const COPYRIGHT = 'copyright'; diff --git a/src/renderer/constants/modal_types.js b/src/renderer/constants/modal_types.js index 0c2e03da1..3d3a725a2 100644 --- a/src/renderer/constants/modal_types.js +++ b/src/renderer/constants/modal_types.js @@ -1,5 +1,5 @@ -export const CONFIRM_FILE_REMOVE = 'confirmFileRemove'; -export const INCOMPATIBLE_DAEMON = 'incompatibleDaemon'; +export const CONFIRM_FILE_REMOVE = 'confirm_file_remove'; +export const INCOMPATIBLE_DAEMON = 'incompatible_daemon'; export const FILE_TIMEOUT = 'file_timeout'; export const DOWNLOADING = 'downloading'; export const AUTO_UPDATE_DOWNLOADED = 'auto_update_downloaded'; @@ -15,5 +15,8 @@ export const AUTHENTICATION_FAILURE = 'auth_failure'; export const TRANSACTION_FAILED = 'transaction_failed'; export const REWARD_APPROVAL_REQUIRED = 'reward_approval_required'; export const AFFIRM_PURCHASE = 'affirm_purchase'; -export const CONFIRM_CLAIM_REVOKE = 'confirmClaimRevoke'; +export const CONFIRM_CLAIM_REVOKE = 'confirm_claim_revoke'; export const FIRST_SUBSCRIPTION = 'firstSubscription'; +export const SEND_TIP = 'send_tip'; +export const PUBLISH = 'publish'; +export const SEARCH = 'search'; diff --git a/src/renderer/constants/search.js b/src/renderer/constants/search.js new file mode 100644 index 000000000..5bdbcd142 --- /dev/null +++ b/src/renderer/constants/search.js @@ -0,0 +1,3 @@ +export const FILE = 'file'; +export const CHANNEL = 'channel'; +export const SEARCH = 'search'; diff --git a/src/renderer/lbry.js b/src/renderer/lbry.js index 79855b4ce..c3fab9c7d 100644 --- a/src/renderer/lbry.js +++ b/src/renderer/lbry.js @@ -35,78 +35,6 @@ function setLocal(key, value) { localStorage.setItem(key, JSON.stringify(value)); } -/** - * 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. - */ -let pendingId = 0; -function savePendingPublish({ name, channelName }) { - pendingId += 1; - const pendingPublishes = getLocal('pendingPublishes') || []; - const newPendingPublish = { - name, - channelName, - claim_id: `pending-${pendingId}`, - txid: `pending-${pendingId}`, - nout: 0, - outpoint: `pending-${pendingId}:0`, - time: Date.now(), - }; - setLocal('pendingPublishes', [...pendingPublishes, newPendingPublish]); - 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. - */ -function removePendingPublishIfNeeded({ name, channelName, outpoint }) { - function pubMatches(pub) { - return ( - pub.outpoint === outpoint || - (pub.name === name && (!channelName || pub.channel_name === channelName)) - ); - } - - setLocal('pendingPublishes', Lbry.getPendingPublishes().filter(pub => !pubMatches(pub))); -} - -/** - * Gets the current list of pending publish attempts. Filters out any that have timed out and - * removes them from the list. - */ -Lbry.getPendingPublishes = () => { - const pendingPublishes = getLocal('pendingPublishes') || []; - const newPendingPublishes = pendingPublishes.filter( - pub => Date.now() - pub.time <= Lbry.pendingPublishTimeout - ); - setLocal('pendingPublishes', newPendingPublishes); - return newPendingPublishes; -}; - -/** - * 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, channelName, outpoint }) { - const pendingPublishes = Lbry.getPendingPublishes(); - return ( - pendingPublishes.find( - pub => - pub.outpoint === outpoint || - (pub.name === name && (!channelName || pub.channel_name === channelName)) - ) || null - ); -} - -function pendingPublishToDummyClaim({ channelName, name, outpoint, claimId, txid, nout }) { - return { name, outpoint, claimId, txid, nout, channelName }; -} - -function pendingPublishToDummyFileInfo({ name, outpoint, claimId }) { - return { name, outpoint, claimId, metadata: null }; -} - // core Lbry.connectPromise = null; Lbry.connect = () => { @@ -136,42 +64,6 @@ Lbry.connect = () => { return Lbry.connectPromise; }; -/** - * 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.publishDeprecated = (params, fileListedCallback, publishedCallback, errorCallback) => { - // Give a short grace period in case publish() returns right away or (more likely) gives an error - const returnPendingTimeout = setTimeout( - () => { - const { name, channel_name: channelName } = params; - if (publishedCallback || fileListedCallback) { - savePendingPublish({ - name, - channelName, - }); - publishedCallback(true); - } - }, - 2000, - { once: true } - ); - - lbryProxy.publish(params).then( - result => { - if (returnPendingTimeout) clearTimeout(returnPendingTimeout); - publishedCallback(result); - }, - err => { - if (returnPendingTimeout) clearTimeout(returnPendingTimeout); - errorCallback(err); - } - ); -}; - Lbry.imagePath = file => `${staticResourcesPath}/img/${file}`; Lbry.getMediaType = (contentType, fileName) => { @@ -217,33 +109,11 @@ Lbry.file_list = (params = {}) => new Promise((resolve, reject) => { const { claim_name: claimName, channel_name: channelName, 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) { - const pendingPublish = getPendingPublish({ outpoint }); - if (pendingPublish) { - resolve([pendingPublishToDummyFileInfo(pendingPublish)]); - return; - } - } - apiCall( 'file_list', params, fileInfos => { - removePendingPublishIfNeeded({ name: claimName, channelName, outpoint }); - - // if a naked file_list call, append the pending file infos - if (!claimName && !channelName && !outpoint) { - const dummyFileInfos = Lbry.getPendingPublishes().map(pendingPublishToDummyFileInfo); - - resolve([...fileInfos, ...dummyFileInfos]); - } else { - resolve(fileInfos); - } + resolve(fileInfos); }, reject ); @@ -255,16 +125,7 @@ Lbry.claim_list_mine = (params = {}) => 'claim_list_mine', params, claims => { - claims.forEach(({ name, channel_name: channelName, txid, nout }) => { - removePendingPublishIfNeeded({ - name, - channelName, - outpoint: `${txid}:${nout}`, - }); - }); - - const dummyClaims = Lbry.getPendingPublishes().map(pendingPublishToDummyClaim); - resolve([...claims, ...dummyClaims]); + resolve(claims); }, reject ); diff --git a/src/renderer/modal/modal.js b/src/renderer/modal/modal.js deleted file mode 100644 index 2bae02a6d..000000000 --- a/src/renderer/modal/modal.js +++ /dev/null @@ -1,118 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ReactModal from 'react-modal'; -import Link from 'component/link/index'; -import app from 'app'; - -export class Modal extends React.PureComponent { - static propTypes = { - 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 = { - type: 'alert', - overlay: true, - confirmButtonLabel: app.i18n.__('OK'), - abortButtonLabel: app.i18n.__('Cancel'), - confirmButtonDisabled: false, - abortButtonDisabled: false, - }; - - render() { - return ( - -
{this.props.children}
- {this.props.type == 'custom' ? null : ( // custom modals define their own buttons -
- - {this.props.type == 'confirm' ? ( - - ) : null} -
- )} -
- ); - } -} - -export class ExpandableModal extends React.PureComponent { - static propTypes = { - expandButtonLabel: PropTypes.string, - extraContent: PropTypes.element, - }; - - static defaultProps = { - confirmButtonLabel: app.i18n.__('OK'), - expandButtonLabel: app.i18n.__('Show More...'), - hideButtonLabel: app.i18n.__('Show Less'), - }; - - constructor(props) { - super(props); - - this.state = { - expanded: false, - }; - } - - toggleExpanded() { - this.setState({ - expanded: !this.state.expanded, - }); - } - - render() { - return ( - - {this.props.children} - {this.state.expanded ? this.props.extraContent : null} -
- - { - this.toggleExpanded(); - }} - /> -
-
- ); - } -} - -export default Modal; diff --git a/src/renderer/modal/modal.jsx b/src/renderer/modal/modal.jsx new file mode 100644 index 000000000..eeed95ef0 --- /dev/null +++ b/src/renderer/modal/modal.jsx @@ -0,0 +1,146 @@ +// @flow +/* eslint-disable react/no-multi-comp */ +// These should probably just be combined into one modal component +import * as React from 'react'; +import ReactModal from 'react-modal'; +import Button from 'component/button'; +import app from 'app'; +import classnames from 'classnames'; + +type ModalProps = { + type: string, + overlay: boolean, + confirmButtonLabel: string, + abortButtonLabel: string, + confirmButtonDisabled: boolean, + abortButtonDisabled: boolean, + onConfirmed?: any => any, + onAborted?: any => any, + className?: string, + overlayClassName?: string, + children?: React.Node, + extraContent?: React.Node, + expandButtonLabel?: string, + hideButtonLabel?: string, + fullScreen: boolean, +}; + +export class Modal extends React.PureComponent { + static defaultProps = { + type: 'alert', + overlay: true, + /* eslint-disable no-underscore-dangle */ + confirmButtonLabel: app.i18n.__('OK'), + abortButtonLabel: app.i18n.__('Cancel'), + /* eslint-enable no-underscore-dangle */ + confirmButtonDisabled: false, + abortButtonDisabled: false, + fullScreen: false, + }; + + render() { + const { + children, + type, + confirmButtonLabel, + confirmButtonDisabled, + onConfirmed, + abortButtonLabel, + abortButtonDisabled, + onAborted, + fullScreen, + className, + overlayClassName, + ...modalProps + } = this.props; + return ( + +
{children}
+ {type === 'custom' ? null : ( // custom modals define their own buttons +
+
+ )} +
+ ); + } +} + +type State = { + expanded: boolean, +}; + +export class ExpandableModal extends React.PureComponent { + static defaultProps = { + /* eslint-disable no-underscore-dangle */ + confirmButtonLabel: app.i18n.__('OK'), + expandButtonLabel: app.i18n.__('Show More...'), + hideButtonLabel: app.i18n.__('Show Less'), + /* eslint-enable no-underscore-dangle */ + }; + + constructor(props: ModalProps) { + super(props); + + this.state = { + expanded: false, + }; + } + + toggleExpanded() { + this.setState({ + expanded: !this.state.expanded, + }); + } + + render() { + return ( + + {this.props.children} + {this.state.expanded ? this.props.extraContent : null} +
+
+
+ ); + } +} + +export default Modal; +/* eslint-enable react/no-multi-comp */ diff --git a/src/renderer/modal/modalAutoUpdateConfirm/view.jsx b/src/renderer/modal/modalAutoUpdateConfirm/view.jsx index f37d59a79..ac2e8d420 100644 --- a/src/renderer/modal/modalAutoUpdateConfirm/view.jsx +++ b/src/renderer/modal/modalAutoUpdateConfirm/view.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { Modal } from 'modal/modal'; import { Line } from 'rc-progress'; -import Link from 'component/link/index'; +import Button from 'component/button'; const { ipcRenderer } = require('electron'); @@ -11,7 +11,7 @@ class ModalAutoUpdateConfirm extends React.PureComponent { return ( {__('Your LBRY update is ready. Restart LBRY now to use it!')}

{__('Want to know what has changed?')} See the{' '} - . +

diff --git a/src/renderer/modal/modalAutoUpdateDownloaded/view.jsx b/src/renderer/modal/modalAutoUpdateDownloaded/view.jsx index c067c24ba..0627d7b51 100644 --- a/src/renderer/modal/modalAutoUpdateDownloaded/view.jsx +++ b/src/renderer/modal/modalAutoUpdateDownloaded/view.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { Modal } from 'modal/modal'; import { Line } from 'rc-progress'; -import Link from 'component/link/index'; +import Button from 'component/button'; const { ipcRenderer } = require('electron'); @@ -11,7 +11,7 @@ class ModalAutoUpdateDownloaded extends React.PureComponent { return ( { const { closeModal, totalRewardValue, currentBalance, addBalance } = props; @@ -33,8 +36,8 @@ const ModalCreditIntro = props => {

- - +
diff --git a/src/renderer/modal/modalIncompatibleDaemon/view.jsx b/src/renderer/modal/modalIncompatibleDaemon/view.jsx index 3526ab4e9..7b056bcbe 100644 --- a/src/renderer/modal/modalIncompatibleDaemon/view.jsx +++ b/src/renderer/modal/modalIncompatibleDaemon/view.jsx @@ -1,6 +1,6 @@ import React from 'react'; import { Modal } from 'modal/modal'; -import Link from 'component/link/index'; +import Button from 'component/button'; class ModalIncompatibleDaemon extends React.PureComponent { render() { @@ -19,7 +19,7 @@ class ModalIncompatibleDaemon extends React.PureComponent { {__( 'This browser is running with an incompatible version of the LBRY protocol and your install must be repaired. ' )} - +
diff --git a/src/renderer/page/auth/view.jsx b/src/renderer/page/auth/view.jsx index 6a7052343..b3379af54 100644 --- a/src/renderer/page/auth/view.jsx +++ b/src/renderer/page/auth/view.jsx @@ -1,9 +1,10 @@ import React from 'react'; -import { BusyMessage } from 'component/common'; -import Link from 'component/link'; +import BusyIndicator from 'component/common/busy-indicator'; +import Button from 'component/button'; import UserEmailNew from 'component/userEmailNew'; import UserEmailVerify from 'component/userEmailVerify'; import UserVerify from 'component/userVerify'; +import Page from 'component/page'; export class AuthPage extends React.PureComponent { componentWillMount() { @@ -43,7 +44,7 @@ export class AuthPage extends React.PureComponent { const { email, isPending, isVerificationCandidate, user } = this.props; if (isPending) { - return [, true]; + return [, true]; } else if (user && !user.has_verified_email && !email) { return [, true]; } else if (user && !user.has_verified_email) { @@ -58,25 +59,27 @@ export class AuthPage extends React.PureComponent { const { email, user, isPending, navigate } = this.props; const [innerContent, useTemplate] = this.renderMain(); - return useTemplate ? ( -
-
-
-

{this.getTitle()}

-
-
{innerContent}
-
-
- {`${__( - 'This information is disclosed only to LBRY, Inc. and not to the LBRY network. It is only required to earn LBRY rewards and may be used to sync usage data across devices.' - )} `} - navigate('/discover')} label={__('Return home')} />. + return ( + + {useTemplate ? ( +
+
+

{this.getTitle()}

-
-
-
- ) : ( - innerContent +
{innerContent}
+
+
+ {`${__( + 'This information is disclosed only to LBRY, Inc. and not to the LBRY network. It is only required to earn LBRY rewards and may be used to sync usage data across devices.' + )} `} +
+
+
+ ) : ( + innerContent + )} + ); } } diff --git a/src/renderer/page/backup/view.jsx b/src/renderer/page/backup/view.jsx index bcb0a0667..3163c0d29 100644 --- a/src/renderer/page/backup/view.jsx +++ b/src/renderer/page/backup/view.jsx @@ -1,63 +1,72 @@ -import React from 'react'; -import SubHeader from 'component/subHeader'; -import Link from 'component/link'; +// @flow +import * as React from 'react'; +import Button from 'component/button'; +import Page from 'component/page'; -class BackupPage extends React.PureComponent { +type Props = { + daemonSettings: { + lbryum_wallet_dir: ?string, + }, +}; + +class BackupPage extends React.PureComponent { render() { const { daemonSettings } = this.props; + const { lbryum_wallet_dir } = daemonSettings; - if (!daemonSettings || Object.keys(daemonSettings).length === 0) { - return ( -
- - {__('Failed to load settings.')} -
- ); - } + const noDaemonSettings = Object.keys(daemonSettings).length === 0; return ( -
- -
-
-

{__('Backup Your LBRY Credits')}

-
-
-

- {__( - 'Your LBRY credits are controllable by you and only you, via wallet file(s) stored locally on your computer.' - )} -

-

- {__( - 'Currently, there is no automatic wallet backup. If you lose access to these files, you will lose your credits permanently.' - )} -

-

- {__( - 'However, it is fairly easy to back up manually. To backup your wallet, make a copy of the folder listed below:' - )} -

-

- {__(`${daemonSettings.lbryum_wallet_dir}`)} -

-

- + +

+ {noDaemonSettings ? ( +
{__('Failed to load settings.')}
+ ) : ( + +
{__('Backup Your LBRY Credits')}
+

{__( - 'Access to these files are equivalent to having access to your credits. Keep any copies you make of your wallet in a secure place.' + 'Your LBRY credits are controllable by you and only you, via wallet file(s) stored locally on your computer.' )} - -

-

- For more details on backing up and best practices,{' '} - . -

-
+

+
+

+ {__( + 'Currently, there is no automatic wallet backup. If you lose access to these files, you will lose your credits permanently.' + )} +

+
+
+

+ {__( + 'However, it is fairly easy to back up manually. To backup your wallet, make a copy of the folder listed below:' + )} +

+
+ {lbryum_wallet_dir} +
+
+
+

+ + {__( + 'Access to these files are equivalent to having access to your credits. Keep any copies you make of your wallet in a secure place.' + )} + +

+

+ For more details on backing up and best practices,{' '} +

+ + )}
-
+ ); } } diff --git a/src/renderer/page/channel/index.js b/src/renderer/page/channel/index.js index c06f42e49..38fabedfe 100644 --- a/src/renderer/page/channel/index.js +++ b/src/renderer/page/channel/index.js @@ -9,6 +9,7 @@ import { import { makeSelectCurrentParam, selectCurrentParams } from 'redux/selectors/navigation'; import { doNavigate } from 'redux/actions/navigation'; import { makeSelectTotalPagesForChannel } from 'redux/selectors/content'; +import { doOpenModal } from 'redux/actions/app'; import ChannelPage from './view'; const select = (state, props) => ({ @@ -24,6 +25,7 @@ const perform = dispatch => ({ fetchClaims: (uri, page) => dispatch(doFetchClaimsByChannel(uri, page)), fetchClaimCount: uri => dispatch(doFetchClaimCountByChannel(uri)), navigate: (path, params) => dispatch(doNavigate(path, params)), + openModal: (modal, props) => dispatch(doOpenModal(modal, props)), }); export default connect(select, perform)(ChannelPage); diff --git a/src/renderer/page/channel/view.jsx b/src/renderer/page/channel/view.jsx index 4614bb5c7..d0ba96394 100644 --- a/src/renderer/page/channel/view.jsx +++ b/src/renderer/page/channel/view.jsx @@ -1,12 +1,35 @@ +// @flow import React from 'react'; import { buildURI } from 'lbryURI'; -import { BusyMessage } from 'component/common'; +import BusyIndicator from 'component/common/busy-indicator'; import FileTile from 'component/fileTile'; import ReactPaginate from 'react-paginate'; -import Link from 'component/link'; +import Button from 'component/button'; import SubscribeButton from 'component/subscribeButton'; +import Page from 'component/page'; +import FileList from 'component/fileList'; +import * as modals from 'constants/modal_types'; -class ChannelPage extends React.PureComponent { +type Props = { + uri: string, + page: number, + totalPages: number, + fetching: boolean, + params: { page: number }, + claim: { + name: string, + claim_id: string, + }, + claimsInChannel: Array<{}>, + fetchClaims: (string, number) => void, + fetchClaimCount: string => void, + navigate: (string, {}) => void, + doChannelSubscribe: string => void, + doChannelUnsubscribe: string => void, + openModal: (string, {}) => void, +}; + +class ChannelPage extends React.PureComponent { componentDidMount() { const { uri, page, fetchClaims, fetchClaimCount } = this.props; @@ -14,7 +37,7 @@ class ChannelPage extends React.PureComponent { fetchClaimCount(uri); } - componentWillReceiveProps(nextProps) { + componentWillReceiveProps(nextProps: Props) { const { page, uri, fetching, fetchClaims, fetchClaimCount } = this.props; if (nextProps.page && page !== nextProps.page) { @@ -25,7 +48,7 @@ class ChannelPage extends React.PureComponent { } } - changePage(pageNumber) { + changePage(pageNumber: number) { const { params } = this.props; const newParams = Object.assign({}, params, { page: pageNumber }); @@ -33,62 +56,37 @@ class ChannelPage extends React.PureComponent { } render() { - const { - fetching, - claimsInChannel, - claim, - uri, - page, - totalPages, - doChannelSubscribe, - doChannelUnsubscribe, - subscriptions, - } = this.props; - + const { fetching, claimsInChannel, claim, uri, page, totalPages, openModal } = this.props; const { name, claim_id: claimId } = claim; const subscriptionUri = buildURI({ channelName: name, claimId }, false); let contentList; if (fetching) { - contentList = ; + contentList = ; } else { contentList = claimsInChannel && claimsInChannel.length ? ( - claimsInChannel.map(claim => ( - - )) + ) : ( {__('No content found.')} ); } return ( -
-
-
-
-

{uri}

-
+ +
+

{name}

+
+
-
-

- {__( - 'Channel pages are empty for all publishers currently, but will be coming in a future update.' - )} -

-
-

{__('Published Content')}

- {contentList} -
+
{contentList}
{(!fetching || (claimsInChannel && claimsInChannel.length)) && totalPages > 1 && ( )} -
+
); } } diff --git a/src/renderer/page/discover/view.jsx b/src/renderer/page/discover/view.jsx index 3280da858..abe0c0f56 100644 --- a/src/renderer/page/discover/view.jsx +++ b/src/renderer/page/discover/view.jsx @@ -1,259 +1,37 @@ +// @flow import React from 'react'; -import ReactDOM from 'react-dom'; -import { normalizeURI } from 'lbryURI'; -import FileCard from 'component/fileCard'; -import { BusyMessage } from 'component/common.js'; -import Icon from 'component/icon'; -import ToolTip from 'component/tooltip.js'; -import SubHeader from 'component/subHeader'; -import classnames from 'classnames'; -import Link from 'component/link'; +import Page from 'component/page'; +import CategoryList from 'component/common/category-list'; -// This should be in a separate file -export class FeaturedCategory extends React.PureComponent { - constructor() { - super(); +type Props = { + fetchFeaturedUris: () => void, + fetchingFeaturedUris: boolean, + featuredUris: {}, +}; - this.state = { - numItems: undefined, - canScrollPrevious: false, - canScrollNext: false, - }; - } - - componentWillMount() { - this.setState({ - numItems: this.props.names.length, - }); - } - - componentDidMount() { - const cardRow = ReactDOM.findDOMNode(this.refs.rowitems); - const cards = cardRow.getElementsByTagName('section'); - - // check if the last card is visible - const lastCard = cards[cards.length - 1]; - const isCompletelyVisible = this.isCardVisible(lastCard, cardRow, false); - - if (!isCompletelyVisible) { - this.setState({ - canScrollNext: true, - }); - } - } - - handleScrollPrevious() { - const cardRow = ReactDOM.findDOMNode(this.refs.rowitems); - if (cardRow.scrollLeft > 0) { - // check the visible cards - const cards = cardRow.getElementsByTagName('section'); - let firstVisibleCard = null; - let firstVisibleIdx = -1; - for (let i = 0; i < cards.length; i++) { - if (this.isCardVisible(cards[i], cardRow, false)) { - firstVisibleCard = cards[i]; - firstVisibleIdx = i; - break; - } - } - - const numDisplayed = this.numDisplayedCards(cardRow); - const scrollToIdx = firstVisibleIdx - numDisplayed; - const animationCallback = () => { - this.setState({ - canScrollPrevious: cardRow.scrollLeft !== 0, - canScrollNext: true, - }); - }; - this.scrollCardItemsLeftAnimated( - cardRow, - scrollToIdx < 0 ? 0 : cards[scrollToIdx].offsetLeft, - 100, - animationCallback - ); - } - } - - handleScrollNext() { - const cardRow = ReactDOM.findDOMNode(this.refs.rowitems); - - // check the visible cards - const cards = cardRow.getElementsByTagName('section'); - let lastVisibleCard = null; - let lastVisibleIdx = -1; - for (let i = 0; i < cards.length; i++) { - if (this.isCardVisible(cards[i], cardRow, true)) { - lastVisibleCard = cards[i]; - lastVisibleIdx = i; - } - } - - if (lastVisibleCard) { - const numDisplayed = this.numDisplayedCards(cardRow); - const animationCallback = () => { - // update last visible index after scroll - for (let i = 0; i < cards.length; i++) { - if (this.isCardVisible(cards[i], cardRow, true)) { - lastVisibleIdx = i; - } - } - - this.setState({ canScrollPrevious: true }); - if (lastVisibleIdx === cards.length - 1) { - this.setState({ canScrollNext: false }); - } - }; - - this.scrollCardItemsLeftAnimated( - cardRow, - Math.min(lastVisibleCard.offsetLeft, cardRow.scrollWidth - cardRow.clientWidth), - 100, - animationCallback - ); - } - } - - scrollCardItemsLeftAnimated(cardRow, target, duration, callback) { - if (!duration || duration <= diff) { - cardRow.scrollLeft = target; - if (callback) { - callback(); - } - return; - } - - const component = this; - const diff = target - cardRow.scrollLeft; - const tick = diff / duration * 10; - setTimeout(() => { - cardRow.scrollLeft += tick; - if (cardRow.scrollLeft === target) { - if (callback) { - callback(); - } - return; - } - component.scrollCardItemsLeftAnimated(cardRow, target, duration - 10, callback); - }, 10); - } - - isCardVisible(section, cardRow, partialVisibility) { - // check if a card is fully or partialy visible in its parent - const cardRowWidth = cardRow.offsetWidth; - const cardRowLeft = cardRow.scrollLeft; - const cardRowEnd = cardRowLeft + cardRow.offsetWidth; - const sectionLeft = section.offsetLeft - cardRowLeft; - const sectionEnd = sectionLeft + section.offsetWidth; - - return ( - (sectionLeft >= 0 && sectionEnd <= cardRowWidth) || - (((sectionLeft < 0 && sectionEnd > 0) || (sectionLeft > 0 && sectionLeft <= cardRowWidth)) && - partialVisibility) - ); - } - - numDisplayedCards(cardRow) { - const cards = cardRow.getElementsByTagName('section'); - const cardRowWidth = cardRow.offsetWidth; - // get the width of the first card and then calculate - const cardWidth = cards.length > 0 ? cards[0].offsetWidth : 0; - - if (cardWidth > 0) { - return Math.ceil(cardRowWidth / cardWidth); - } - - // return a default value of 1 card displayed if the card width couldn't be determined - return 1; - } - - render() { - const { category, names, categoryLink } = this.props; - - return ( -
-

- {categoryLink ? ( - - ) : ( - category - )} - - {category && - category.match(/^community/i) && ( - - )} -

-
- {this.state.canScrollPrevious && ( -
- - - -
- )} - {this.state.canScrollNext && ( -
- - - -
- )} -
- {names && - names.map(name => ( - - ))} -
-
-
- ); - } -} - -class DiscoverPage extends React.PureComponent { +class DiscoverPage extends React.PureComponent { componentWillMount() { this.props.fetchFeaturedUris(); } render() { const { featuredUris, fetchingFeaturedUris } = this.props; - const hasContent = typeof featuredUris === 'object' && Object.keys(featuredUris).length, - failedToLoad = !fetchingFeaturedUris && !hasContent; - + const hasContent = typeof featuredUris === 'object' && Object.keys(featuredUris).length; + const failedToLoad = !fetchingFeaturedUris && !hasContent; + // lbry://fortnite-top-stream-moments-nickatnydte#27395875d68e9d3e53be46edf36d622aa8284441 return ( -
- - {!hasContent && fetchingFeaturedUris && } + {hasContent && Object.keys(featuredUris).map( category => featuredUris[category].length ? ( - + ) : ( '' ) )} {failedToLoad &&
{__('Failed to load landing content.')}
} -
+ ); } } diff --git a/src/renderer/page/file/index.js b/src/renderer/page/file/index.js index f8e515222..08e25e314 100644 --- a/src/renderer/page/file/index.js +++ b/src/renderer/page/file/index.js @@ -2,19 +2,23 @@ import { connect } from 'react-redux'; import { doNavigate } from 'redux/actions/navigation'; import { doFetchFileInfo } from 'redux/actions/file_info'; import { makeSelectFileInfoForUri } from 'redux/selectors/file_info'; -import { selectRewardContentClaimIds } from 'redux/selectors/content'; +import { selectRewardContentClaimIds, selectPlayingUri } from 'redux/selectors/content'; import { doFetchCostInfoForUri } from 'redux/actions/cost_info'; import { doCheckSubscription } from 'redux/actions/subscriptions'; import { makeSelectClaimForUri, makeSelectContentTypeForUri, makeSelectMetadataForUri, + makeSelectClaimIsMine, } from 'redux/selectors/claims'; import { makeSelectCostInfoForUri } from 'redux/selectors/cost_info'; import { selectShowNsfw } from 'redux/selectors/settings'; +import { selectMediaPaused } from 'redux/selectors/media'; +import { doOpenModal } from 'redux/actions/app'; import FilePage from './view'; import { makeSelectCurrentParam } from 'redux/selectors/navigation'; import { selectSubscriptions } from 'redux/selectors/subscriptions'; +import { doPrepareEdit } from 'redux/actions/publish'; const select = (state, props) => ({ claim: makeSelectClaimForUri(props.uri)(state), @@ -22,10 +26,12 @@ const select = (state, props) => ({ costInfo: makeSelectCostInfoForUri(props.uri)(state), metadata: makeSelectMetadataForUri(props.uri)(state), obscureNsfw: !selectShowNsfw(state), - tab: makeSelectCurrentParam('tab')(state), fileInfo: makeSelectFileInfoForUri(props.uri)(state), rewardedContentClaimIds: selectRewardContentClaimIds(state, props), subscriptions: selectSubscriptions(state), + playingUri: selectPlayingUri(state), + isPaused: selectMediaPaused(state), + claimIsMine: makeSelectClaimIsMine(props.uri)(state), }); const perform = dispatch => ({ @@ -33,6 +39,8 @@ const perform = dispatch => ({ fetchFileInfo: uri => dispatch(doFetchFileInfo(uri)), fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)), checkSubscription: subscription => dispatch(doCheckSubscription(subscription)), + openModal: (modal, props) => dispatch(doOpenModal(modal, props)), + prepareEdit: publishData => dispatch(doPrepareEdit(publishData)), }); export default connect(select, perform)(FilePage); diff --git a/src/renderer/page/file/view.jsx b/src/renderer/page/file/view.jsx index c7c29c194..b017ea745 100644 --- a/src/renderer/page/file/view.jsx +++ b/src/renderer/page/file/view.jsx @@ -1,38 +1,74 @@ -import React from 'react'; +// @flow +import * as React from 'react'; import lbry from 'lbry'; import { buildURI, normalizeURI } from 'lbryURI'; import Video from 'component/video'; -import { Thumbnail } from 'component/common'; +import Thumbnail from 'component/common/thumbnail'; import FilePrice from 'component/filePrice'; import FileDetails from 'component/fileDetails'; +import FileActions from 'component/fileActions'; import UriIndicator from 'component/uriIndicator'; -import Icon from 'component/icon'; +import Icon from 'component/common/icon'; import WalletSendTip from 'component/walletSendTip'; import DateTime from 'component/dateTime'; import * as icons from 'constants/icons'; -import Link from 'component/link'; +import Button from 'component/button'; import SubscribeButton from 'component/subscribeButton'; +import Page from 'component/page'; +import player from 'render-media'; +import * as modals from 'constants/modal_types'; -class FilePage extends React.PureComponent { +type Props = { + claim: { + claim_id: string, + height: number, + channel_name: string, + value: { + publisherSignature: ?{ + certificateId: ?string, + }, + }, + }, + fileInfo: {}, + metadata: { + title: string, + thumbnail: string, + nsfw: boolean, + }, + contentType: string, + uri: string, + rewardedContentClaimIds: Array, + obscureNsfw: boolean, + playingUri: ?string, + isPaused: boolean, + claimIsMine: boolean, + costInfo: ?{}, + navigate: (string, ?{}) => void, + openModal: (string, any) => void, + fetchFileInfo: string => void, + fetchCostInfo: string => void, + prepareEdit: ({}) => void, +}; + +class FilePage extends React.Component { componentDidMount() { - this.fetchFileInfo(this.props); - this.fetchCostInfo(this.props); + const { uri, fileInfo, fetchFileInfo, costInfo, fetchCostInfo } = this.props; + + if (fileInfo === undefined) { + fetchFileInfo(uri); + } + + if (costInfo === undefined) { + fetchCostInfo(uri); + } + this.checkSubscription(this.props); } - componentWillReceiveProps(nextProps) { - this.fetchFileInfo(nextProps); - } - - fetchFileInfo(props) { - if (props.fileInfo === undefined) { - props.fetchFileInfo(props.uri); - } - } - - fetchCostInfo(props) { - if (props.costInfo === undefined) { - props.fetchCostInfo(props.uri); + componentWillReceiveProps(nextProps: Props) { + const { fetchFileInfo, uri } = this.props; + if (nextProps.fileInfo === undefined) { + fetchFileInfo(uri); } } @@ -61,75 +97,99 @@ class FilePage extends React.PureComponent { fileInfo, metadata, contentType, - tab, uri, rewardedContentClaimIds, + obscureNsfw, + playingUri, + isPaused, + openModal, + claimIsMine, + prepareEdit, + navigate, } = this.props; - const showTipBox = tab == 'tip'; - - if (!claim || !metadata) { - return {__('Empty claim or metadata info.')}; - } - + // File info const title = metadata.title; const isRewardContent = rewardedContentClaimIds.includes(claim.claim_id); + const shouldObscureThumbnail = obscureNsfw && metadata.nsfw; + const thumbnail = metadata.thumbnail; + const { height, channel_name: channelName, value } = claim; const mediaType = lbry.getMediaType(contentType); - const player = require('render-media'); - const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw; const isPlayable = Object.values(player.mime).indexOf(contentType) !== -1 || mediaType === 'audio'; - const { height, channel_name: channelName, value } = claim; const channelClaimId = value && value.publisherSignature && value.publisherSignature.certificateId; - let subscriptionUri; if (channelName && channelClaimId) { subscriptionUri = buildURI({ channelName, claimId: channelClaimId }, false); } + const isPlaying = playingUri === uri && !isPaused; return ( -
-
- {isPlayable ? ( -
-
- {(!tab || tab === 'details') && ( -
- {' '} -
- {!fileInfo || fileInfo.written_bytes <= 0 ? ( - - - {isRewardContent && ( - - {' '} - - - )} - - ) : null} -

{title}

-
- - - Published on - + + {!claim || !metadata ? ( +
+ {__('Empty claim or metadata info.')} +
+ ) : ( +
+ {isPlayable ? ( +
-
+
+ )} + ); } } diff --git a/src/renderer/page/fileListDownloaded/index.js b/src/renderer/page/fileListDownloaded/index.js index 8361a560f..9fc0fb685 100644 --- a/src/renderer/page/fileListDownloaded/index.js +++ b/src/renderer/page/fileListDownloaded/index.js @@ -1,29 +1,16 @@ -import React from 'react'; import { connect } from 'react-redux'; -import { doFetchFileInfosAndPublishedClaims } from 'redux/actions/file_info'; -import { - selectFileInfosDownloaded, - selectIsFetchingFileListDownloadedOrPublished, -} from 'redux/selectors/file_info'; -import { - selectMyClaimsWithoutChannels, - selectIsFetchingClaimListMine, -} from 'redux/selectors/claims'; -import { doFetchClaimListMine } from 'redux/actions/content'; +import { selectFileInfosDownloaded } from 'redux/selectors/file_info'; +import { selectMyClaimsWithoutChannels } from 'redux/selectors/claims'; import { doNavigate } from 'redux/actions/navigation'; import FileListDownloaded from './view'; const select = state => ({ fileInfos: selectFileInfosDownloaded(state), - isFetching: selectIsFetchingFileListDownloadedOrPublished(state), claims: selectMyClaimsWithoutChannels(state), - isFetchingClaims: selectIsFetchingClaimListMine(state), }); const perform = dispatch => ({ navigate: path => dispatch(doNavigate(path)), - fetchFileInfosDownloaded: () => dispatch(doFetchFileInfosAndPublishedClaims()), - fetchClaims: () => dispatch(doFetchClaimListMine()), }); export default connect(select, perform)(FileListDownloaded); diff --git a/src/renderer/page/fileListDownloaded/view.jsx b/src/renderer/page/fileListDownloaded/view.jsx index f6be896ef..f6b977468 100644 --- a/src/renderer/page/fileListDownloaded/view.jsx +++ b/src/renderer/page/fileListDownloaded/view.jsx @@ -1,41 +1,31 @@ import React from 'react'; -import Link from 'component/link'; +import Button from 'component/button'; import { FileTile } from 'component/fileTile'; -import { BusyMessage, Thumbnail } from 'component/common.js'; import FileList from 'component/fileList'; -import SubHeader from 'component/subHeader'; +import Page from 'component/page'; class FileListDownloaded extends React.PureComponent { - componentWillMount() { - if (!this.props.isFetchingClaims) this.props.fetchClaims(); - if (!this.props.isFetching) this.props.fetchFileInfosDownloaded(); - } - render() { - const { fileInfos, isFetching, navigate } = this.props; - - let content; - if (fileInfos && fileInfos.length > 0) { - content = ; - } else if (isFetching) { - content = ; - } else { - content = ( - - {__("You haven't downloaded anything from LBRY yet. Go")}{' '} - navigate('/discover')} - label={__('search for your first download')} - />! - - ); - } + const { fileInfos, navigate } = this.props; + const hasDownloads = fileInfos && fileInfos.length > 0; return ( -
- - {content} -
+ + {hasDownloads ? ( + + ) : ( +
+ {__("You haven't downloaded anything from LBRY yet.")} +
+
+
+ )} +
); } } diff --git a/src/renderer/page/fileListPublished/index.js b/src/renderer/page/fileListPublished/index.js index a6eb4be3b..a053baa3a 100644 --- a/src/renderer/page/fileListPublished/index.js +++ b/src/renderer/page/fileListPublished/index.js @@ -1,24 +1,18 @@ -import React from 'react'; -import rewards from 'rewards'; import { connect } from 'react-redux'; -import { doFetchClaimListMine } from 'redux/actions/content'; -import { - selectMyClaimsWithoutChannels, - selectIsFetchingClaimListMine, -} from 'redux/selectors/claims'; -import { doClaimRewardType } from 'redux/actions/rewards'; +import { selectMyClaimsWithoutChannels } from 'redux/selectors/claims'; +import { selectPendingPublishes } from 'redux/selectors/publish'; import { doNavigate } from 'redux/actions/navigation'; +import { doCheckPendingPublishes } from 'redux/actions/publish'; import FileListPublished from './view'; const select = state => ({ claims: selectMyClaimsWithoutChannels(state), - isFetching: selectIsFetchingClaimListMine(state), + pendingPublishes: selectPendingPublishes(state), }); const perform = dispatch => ({ navigate: path => dispatch(doNavigate(path)), - fetchClaims: () => dispatch(doFetchClaimListMine()), - claimFirstPublishReward: () => dispatch(doClaimRewardType(rewards.TYPE_FIRST_PUBLISH)), + checkIfPublishesConfirmed: publishes => dispatch(doCheckPendingPublishes(publishes)), }); export default connect(select, perform)(FileListPublished); diff --git a/src/renderer/page/fileListPublished/view.jsx b/src/renderer/page/fileListPublished/view.jsx index e7917f67d..24b0520d0 100644 --- a/src/renderer/page/fileListPublished/view.jsx +++ b/src/renderer/page/fileListPublished/view.jsx @@ -1,52 +1,37 @@ import React from 'react'; -import Link from 'component/link'; -import FileTile from 'component/fileTile'; -import { BusyMessage, Thumbnail } from 'component/common.js'; +import Button from 'component/button'; import FileList from 'component/fileList'; -import SubHeader from 'component/subHeader'; +import Page from 'component/page'; class FileListPublished extends React.PureComponent { - componentWillMount() { - if (!this.props.isFetching) this.props.fetchClaims(); - } - - componentDidUpdate() { - // if (this.props.claims.length > 0) this.props.fetchClaims(); + componentDidMount() { + const { pendingPublishes, checkIfPublishesConfirmed } = this.props; + if (pendingPublishes.length) { + checkIfPublishesConfirmed(pendingPublishes); + } } render() { - const { claims, isFetching, navigate } = this.props; - - let content; - - if (claims && claims.length > 0) { - content = ( - - ); - } else if (isFetching) { - content = ; - } else { - content = ( - - {__("It looks like you haven't published anything to LBRY yet. Go")}{' '} - navigate('/publish')} - label={__('share your beautiful cats with the world')} - />! - - ); - } + const { claims, pendingPublishes, navigate } = this.props; + const fileInfos = [...pendingPublishes, ...claims]; return ( -
- - {content} -
+ + {fileInfos.length ? ( + + ) : ( +
+ {__("It looks like you haven't published anything to LBRY yet.")} +
+
+
+ )} +
); } } diff --git a/src/renderer/page/getCredits/view.jsx b/src/renderer/page/getCredits/view.jsx index b8e670443..e08e77d6b 100644 --- a/src/renderer/page/getCredits/view.jsx +++ b/src/renderer/page/getCredits/view.jsx @@ -1,26 +1,22 @@ import React from 'react'; -import SubHeader from 'component/subHeader'; -import Link from 'component/link'; +import Button from 'component/button'; import RewardSummary from 'component/rewardSummary'; import ShapeShift from 'component/shapeShift'; +import Page from 'component/page'; +import * as icons from 'constants/icons'; const GetCreditsPage = props => ( -
- + -
-
-

{__('From External Wallet')}

-
+
+
{__('From External Wallet')}
- +
-
-
-

{__('More ways to get LBRY Credits')}

-
+
+
{__('More ways to get LBRY Credits')}

{ @@ -29,10 +25,10 @@ const GetCreditsPage = props => (

- +
-
+ ); export default GetCreditsPage; diff --git a/src/renderer/page/help/view.jsx b/src/renderer/page/help/view.jsx index bb22e6fe5..27cd7faec 100644 --- a/src/renderer/page/help/view.jsx +++ b/src/renderer/page/help/view.jsx @@ -1,10 +1,11 @@ // @TODO: Customize advice based on OS import React from 'react'; import lbry from 'lbry.js'; -import Link from 'component/link'; -import SubHeader from 'component/subHeader'; -import { BusyMessage } from 'component/common'; -import Icon from 'component/icon'; +import Button from 'component/button'; +import BusyIndicator from 'component/common/busy-indicator'; +import Icon from 'component/common/icon'; +import Page from 'component/page'; +import * as icons from 'constants/icons'; class HelpPage extends React.PureComponent { constructor(props) { @@ -17,9 +18,11 @@ class HelpPage extends React.PureComponent { upgradeAvailable: null, accessTokenHidden: true, }; + + this.showAccessToken = this.showAccessToken.bind(this); } - componentWillMount() { + componentDidMount() { lbry.getAppVersionInfo().then(({ remoteVersion, localVersion, upgradeAvailable }) => { this.setState({ uiVersion: localVersion, @@ -70,147 +73,127 @@ class HelpPage extends React.PureComponent { } return ( -
- -
-
-

{__('Read the FAQ')}

-
-
-

{__('Our FAQ answers many common questions.')}

-

- -

-
-
-
-
-

{__('Get Live Help')}

-
-
-

- {__('Live help is available most hours in the')} #help{' '} - {__('channel of our Discord chat room.')} -

-

- -

-
-
-
-
-

{__('Report a Bug')}

-
-
-

{__('Did you find something wrong?')}

-

- -

-
{__('Thanks! LBRY is made by its users.')}
+ +
+
{__('Read the FAQ')}
+

{__('Our FAQ answers many common questions.')}

+ +
+
-
-
-

{__('About')}

-
-
- {this.state.upgradeAvailable === null ? ( - '' - ) : this.state.upgradeAvailable ? ( -

- {__('A newer version of LBRY is available.')}{' '} - -

- ) : ( -

{__('Your copy of LBRY is up to date.')}

- )} - {this.state.uiVersion && ver ? ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{__('App')}{this.state.uiVersion}
{__('Daemon (lbrynet)')}{ver.lbrynet_version}
{__('Wallet (lbryum)')}{ver.lbryum_version}
{__('Connected Email')} - {user && user.primary_email ? ( - user.primary_email - ) : ( - - {__('none')} - ( doAuth()} label={__('set email')} />) - - )} -
{__('Reward Eligible')} - {user && user.is_reward_approved ? ( - - ) : ( - - )} -
{__('Platform')}{platform}
{__('Installation ID')}{this.state.lbryId}
{__('Access Token')} - {this.state.accessTokenHidden && ( - - )} - {!this.state.accessTokenHidden && - accessToken && ( -
-

{accessToken}

-
- {__('This is equivalent to a password. Do not post or share this.')} -
-
- )} -
- ) : ( - - )} +
+
{__('Get Live Help')}
+

+ {__('Live help is available most hours in the')} #help{' '} + {__('channel of our Discord chat room.')} +

+
+
-
+ +
+
{__('Report a Bug')}
+

{__('Did you find something wrong?')}

+ +
+
+
{__('Thanks! LBRY is made by its users.')}
+
+ +
+
{__('About')}
+ {this.state.upgradeAvailable !== null && this.state.upgradeAvailable ? ( +
+ {__('A newer version of LBRY is available.')}{' '} +
+ ) : ( +
{__('Your LBRY app is up to date.')}
+ )} + + {this.state.uiVersion && ver ? ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{__('App')}{this.state.uiVersion}
{__('Daemon (lbrynet)')}{ver.lbrynet_version}
{__('Wallet (lbryum)')}{ver.lbryum_version}
{__('Connected Email')} + {user && user.primary_email ? ( + user.primary_email + ) : ( + + {__('none')} + (
{__('Reward Eligible')}{user && user.is_reward_approved ? __('Yes') : __('No')}
{__('Platform')}{platform}
{__('Installation ID')}{this.state.lbryId}
{__('Access Token')} + {this.state.accessTokenHidden && ( +
+ ) : ( + + )} +
+ ); } } diff --git a/src/renderer/page/invite/view.jsx b/src/renderer/page/invite/view.jsx index 4199066a5..04aadbe46 100644 --- a/src/renderer/page/invite/view.jsx +++ b/src/renderer/page/invite/view.jsx @@ -1,8 +1,8 @@ import React from 'react'; -import { BusyMessage } from 'component/common'; -import SubHeader from 'component/subHeader'; +import BusyIndicator from 'component/common/busy-indicator'; import InviteNew from 'component/inviteNew'; import InviteList from 'component/inviteList'; +import Page from 'component/page'; class InvitePage extends React.PureComponent { componentWillMount() { @@ -13,14 +13,18 @@ class InvitePage extends React.PureComponent { const { isPending, isFailed } = this.props; return ( -
- - {isPending && } + + {isPending && } {!isPending && isFailed && {__('Failed to retrieve invite status.')}} - {!isPending && !isFailed && } - {!isPending && !isFailed && } -
+ {!isPending && + !isFailed && ( + + + + + )} + ); } } diff --git a/src/renderer/page/publish/index.js b/src/renderer/page/publish/index.js index 01f579e5a..c73a3d492 100644 --- a/src/renderer/page/publish/index.js +++ b/src/renderer/page/publish/index.js @@ -1,43 +1,56 @@ import React from 'react'; import { connect } from 'react-redux'; -import { doNavigate, doHistoryBack } from 'redux/actions/navigation'; +import { doNavigate } from 'redux/actions/navigation'; import { doClaimRewardType } from 'redux/actions/rewards'; -import { - selectMyClaims, - selectFetchingMyChannels, - selectMyChannelClaims, - selectClaimsByUri, -} from 'redux/selectors/claims'; +import { selectMyClaims, selectClaimsByUri } from 'redux/selectors/claims'; import { selectResolvingUris } from 'redux/selectors/content'; -import { - doFetchClaimListMine, - doFetchChannelListMine, - doResolveUri, - doCreateChannel, - doPublish, -} from 'redux/actions/content'; +import { selectPublishFormValues } from 'redux/selectors/publish'; +import { doResolveUri } from 'redux/actions/content'; import { selectBalance } from 'redux/selectors/wallet'; -import rewards from 'rewards'; +import { doClearPublish, doUpdatePublishForm, doPublish } from 'redux/actions/publish'; +import { makeSelectCostInfoForUri } from 'redux/selectors/cost_info'; +import { doPrepareEdit } from 'redux/actions/publish'; import PublishPage from './view'; -const select = state => ({ - balance: selectBalance(state), - myClaims: selectMyClaims(state), - fetchingChannels: selectFetchingMyChannels(state), - channels: selectMyChannelClaims(state), - claimsByUri: selectClaimsByUri(state), - resolvingUris: selectResolvingUris(state), -}); +const select = (state, props) => { + const publishState = selectPublishFormValues(state); + const { uri, name } = publishState; + + const resolvingUris = selectResolvingUris(state); + let isResolvingUri = false; + if (uri) { + isResolvingUri = resolvingUris.includes(uri); + } + + const claimsByUri = selectClaimsByUri(state); + const myClaims = selectMyClaims(state); + + const claimForUri = claimsByUri[uri]; + let winningBidForClaimUri; + let myClaimForUri; + if (claimForUri) { + winningBidForClaimUri = claimForUri.effective_amount; + myClaimForUri = myClaims.find(claim => claim.name === name); + } + + return { + ...publishState, + isResolvingUri, + claimForUri, + winningBidForClaimUri, + myClaimForUri, + costInfo: makeSelectCostInfoForUri(props.uri)(state), + balance: selectBalance(state), + }; +}; const perform = dispatch => ({ - back: () => dispatch(doHistoryBack()), - navigate: path => dispatch(doNavigate(path)), - fetchClaimListMine: () => dispatch(doFetchClaimListMine()), - claimFirstChannelReward: () => dispatch(doClaimRewardType(rewards.TYPE_FIRST_CHANNEL)), - fetchChannelListMine: () => dispatch(doFetchChannelListMine()), + updatePublishForm: value => dispatch(doUpdatePublishForm(value)), + clearPublish: () => dispatch(doClearPublish()), resolveUri: uri => dispatch(doResolveUri(uri)), - createChannel: (name, amount) => dispatch(doCreateChannel(name, amount)), publish: params => dispatch(doPublish(params)), + navigate: path => dispatch(doNavigate(path)), + prepareEdit: claim => dispatch(doPrepareEdit(claim)), }); export default connect(select, perform)(PublishPage); diff --git a/src/renderer/page/publish/view.jsx b/src/renderer/page/publish/view.jsx index c0cc68aab..0745e9430 100644 --- a/src/renderer/page/publish/view.jsx +++ b/src/renderer/page/publish/view.jsx @@ -1,6 +1,23 @@ import React from 'react'; import PublishForm from 'component/publishForm'; +import Page from 'component/page'; -const PublishPage = props => ; +class PublishPage extends React.PureComponent { + scrollToTop = () => { + // #content wraps every + const mainContent = document.getElementById('content'); + if (mainContent) { + mainContent.scrollTop = 0; // It would be nice to animate this + } + }; + + render() { + return ( + + + + ); + } +} export default PublishPage; diff --git a/src/renderer/page/report.js b/src/renderer/page/report.js index 617ca6d3d..1c8eea4b2 100644 --- a/src/renderer/page/report.js +++ b/src/renderer/page/report.js @@ -1,6 +1,6 @@ import React from 'react'; -import Link from 'component/link'; -import { FormRow } from 'component/form'; +import Button from 'component/button'; +import { FormRow } from 'component/common/form'; import { doShowSnackBar } from 'redux/actions/app'; import lbry from '../lbry.js'; @@ -82,7 +82,7 @@ class ReportPage extends React.Component {

{__('Developer?')}

{__('You can also')}{' '} - . diff --git a/src/renderer/page/rewards/view.jsx b/src/renderer/page/rewards/view.jsx index 99375faee..a25b06ec4 100644 --- a/src/renderer/page/rewards/view.jsx +++ b/src/renderer/page/rewards/view.jsx @@ -1,9 +1,10 @@ import React from 'react'; -import { BusyMessage } from 'component/common'; +import BusyIndicator from 'component/common/busy-indicator'; import RewardListClaimed from 'component/rewardListClaimed'; import RewardTile from 'component/rewardTile'; -import SubHeader from 'component/subHeader'; -import Link from 'component/link'; +import Button from 'component/button'; +import Page from 'component/page'; +import classnames from 'classnames'; class RewardsPage extends React.PureComponent { /* @@ -34,18 +35,16 @@ class RewardsPage extends React.PureComponent { if (user && !user.is_reward_approved && daemonSettings.share_usage_data) { if (!user.primary_email || !user.has_verified_email || !user.is_identity_verified) { return ( -
-
-

{__('Humans Only')}

-
-
+
+
{__('Humans Only')}
+

{__('Rewards are for human beings only.')}{' '} {__("You'll have to prove you're one of us before you can claim any rewards.")}

- +
); @@ -70,7 +69,7 @@ class RewardsPage extends React.PureComponent { )}`}

- navigate('/discover')} button="primary" label="Return Home" /> +

); @@ -87,7 +86,7 @@ class RewardsPage extends React.PureComponent { {__( 'Rewards are currently disabled for your account. Turn on diagnostic data sharing, in' )}{' '} - navigate('/settings')} label="Settings" /> +
@@ -95,12 +94,12 @@ class RewardsPage extends React.PureComponent { } else if (fetching) { return (
- +
); } else if (user === null) { return ( -
+

{__('This application is unable to earn rewards due to an authentication failure.')}

@@ -108,13 +107,20 @@ class RewardsPage extends React.PureComponent { ); } else if (!rewards || rewards.length <= 0) { return ( -
+
{__('There are no rewards available at this time, please check back later.')}
); } + + const isNotEligible = + !user.primary_email || !user.has_verified_email || !user.is_identity_verified; return ( -
+
{rewards.map(reward => )}
); @@ -122,12 +128,11 @@ class RewardsPage extends React.PureComponent { render() { return ( -
- + {this.renderPageHeader()} {this.renderUnclaimedRewards()} {} -
+ ); } } diff --git a/src/renderer/page/search/index.js b/src/renderer/page/search/index.js index 202844e07..7946f5378 100644 --- a/src/renderer/page/search/index.js +++ b/src/renderer/page/search/index.js @@ -1,16 +1,18 @@ import React from 'react'; import { connect } from 'react-redux'; -import { selectIsSearching, selectSearchQuery } from 'redux/selectors/search'; +import { selectIsSearching, selectSearchValue } from 'redux/selectors/search'; import { doNavigate } from 'redux/actions/navigation'; +import { doUpdateSearchQuery } from 'redux/actions/search'; import SearchPage from './view'; const select = state => ({ isSearching: selectIsSearching(state), - query: selectSearchQuery(state), + query: selectSearchValue(state), }); const perform = dispatch => ({ navigate: path => dispatch(doNavigate(path)), + updateSearchQuery: query => dispatch(doUpdateSearchQuery(query)), }); export default connect(select, perform)(SearchPage); diff --git a/src/renderer/page/search/view.jsx b/src/renderer/page/search/view.jsx index 4f9cae040..48273e7a4 100644 --- a/src/renderer/page/search/view.jsx +++ b/src/renderer/page/search/view.jsx @@ -1,43 +1,68 @@ -import React from 'react'; +// @flow +import * as React from 'react'; import { isURIValid, normalizeURI } from 'lbryURI'; import FileTile from 'component/fileTile'; import FileListSearch from 'component/fileListSearch'; -import { ToolTip } from 'component/tooltip.js'; +import ToolTip from 'component/common/tooltip'; +import Page from 'component/page'; + +const MODAL_ANIMATION_TIME = 250; + +type Props = { + query: ?string, + updateSearchQuery: string => void, +}; + +class SearchPage extends React.PureComponent { + constructor() { + super(); + + this.input = null; + } + + componentDidMount() { + // Wait for the modal to animate down before focusing + // without this there is an issue with scroll the page down + setTimeout(() => { + if (this.input) { + this.input.focus(); + } + }, MODAL_ANIMATION_TIME); + } + + input: ?HTMLInputElement; -class SearchPage extends React.PureComponent { render() { - const { query } = this.props; - + const { query, updateSearchQuery } = this.props; return ( -
- {isURIValid(query) ? ( -
-

- {__('Exact URL')}{' '} - -

- -
- ) : ( - '' - )} -
-

- {__('Search Results for')} {query}{' '} - -

+ +
+ (this.input = input)} + className="search__input" + value={query} + placeholder={__('Search for anything...')} + onChange={event => updateSearchQuery(event.target.value)} + /> + + {isURIValid(query) && ( + +
+ {__('Exact URL')} + +
+ +
+ )} -
-
+
+ ); } } + export default SearchPage; diff --git a/src/renderer/page/sendCredits/view.jsx b/src/renderer/page/sendCredits/view.jsx index 07df1fd6f..82b038068 100644 --- a/src/renderer/page/sendCredits/view.jsx +++ b/src/renderer/page/sendCredits/view.jsx @@ -1,14 +1,13 @@ import React from 'react'; -import SubHeader from 'component/subHeader'; import WalletSend from 'component/walletSend'; import WalletAddress from 'component/walletAddress'; +import Page from 'component/page'; const SendReceivePage = props => ( -
- + -
+ ); export default SendReceivePage; diff --git a/src/renderer/page/settings/index.js b/src/renderer/page/settings/index.js index ca277df82..e4f4ff930 100644 --- a/src/renderer/page/settings/index.js +++ b/src/renderer/page/settings/index.js @@ -1,4 +1,3 @@ -import React from 'react'; import { connect } from 'react-redux'; import * as settings from 'constants/settings'; import { doClearCache } from 'redux/actions/app'; @@ -6,7 +5,6 @@ import { doSetDaemonSetting, doSetClientSetting, doGetThemes, - doSetTheme, doChangeLanguage, } from 'redux/actions/settings'; import { @@ -23,8 +21,7 @@ const select = state => ({ showUnavailable: makeSelectClientSetting(settings.SHOW_UNAVAILABLE)(state), instantPurchaseEnabled: makeSelectClientSetting(settings.INSTANT_PURCHASE_ENABLED)(state), instantPurchaseMax: makeSelectClientSetting(settings.INSTANT_PURCHASE_MAX)(state), - showUnavailable: makeSelectClientSetting(settings.SHOW_UNAVAILABLE)(state), - theme: makeSelectClientSetting(settings.THEME)(state), + currentTheme: makeSelectClientSetting(settings.THEME)(state), themes: makeSelectClientSetting(settings.THEMES)(state), language: selectCurrentLanguage(state), languages: selectLanguages(state), diff --git a/src/renderer/page/settings/view.jsx b/src/renderer/page/settings/view.jsx index f83fc9bc9..bde2f935e 100644 --- a/src/renderer/page/settings/view.jsx +++ b/src/renderer/page/settings/view.jsx @@ -1,21 +1,118 @@ -import React from 'react'; -import FormField from 'component/formField'; -import { FormRow } from 'component/form.js'; -import SubHeader from 'component/subHeader'; +// @flow +import * as React from 'react'; +import { FormField, FormFieldPrice } from 'component/common/form'; import * as settings from 'constants/settings'; -import lbry from 'lbry.js'; -import Link from 'component/link'; -import FormFieldPrice from 'component/formFieldPrice'; +import * as icons from 'constants/icons'; +import Button from 'component/button'; +import Page from 'component/page'; +import FileSelector from 'component/common/file-selector'; -class SettingsPage extends React.PureComponent { - constructor(props) { +export type Price = { + currency: string, + amount: number, +}; + +type DaemonSettings = { + download_directory: string, + disable_max_key_fee: boolean, + share_usage_data: boolean, +}; + +type Props = { + setDaemonSetting: (string, boolean | string | Price) => void, + setClientSetting: (string, boolean | string | Price) => void, + clearCache: () => Promise, + getThemes: () => void, + daemonSettings: DaemonSettings, + showNsfw: boolean, + instantPurchaseEnabled: boolean, + instantPurchaseMax: Price, + showUnavailable: boolean, + currentTheme: string, + themes: Array, + automaticDarkModeEnabled: boolean, +}; + +type State = { + clearingCache: boolean, +}; + +class SettingsPage extends React.PureComponent { + constructor(props: Props) { super(props); this.state = { clearingCache: false, }; - this.onAutomaticDarkModeChange = this.onAutomaticDarkModeChange.bind(this); + (this: any).onDownloadDirChange = this.onDownloadDirChange.bind(this); + (this: any).onKeyFeeChange = this.onKeyFeeChange.bind(this); + (this: any).onInstantPurchaseMaxChange = this.onInstantPurchaseMaxChange.bind(this); + (this: any).onShowNsfwChange = this.onShowNsfwChange.bind(this); + (this: any).onShowUnavailableChange = this.onShowUnavailableChange.bind(this); + (this: any).onShareDataChange = this.onShareDataChange.bind(this); + (this: any).onThemeChange = this.onThemeChange.bind(this); + (this: any).onAutomaticDarkModeChange = this.onAutomaticDarkModeChange.bind(this); + (this: any).clearCache = this.clearCache.bind(this); + // (this: any).onLanguageChange = this.onLanguageChange.bind(this) + } + + componentDidMount() { + this.props.getThemes(); + } + + onRunOnStartChange(event: SyntheticInputEvent<*>) { + this.setDaemonSetting('run_on_startup', event.target.checked); + } + + onShareDataChange(event: SyntheticInputEvent<*>) { + this.setDaemonSetting('share_usage_data', event.target.checked); + } + + onDownloadDirChange(newDirectory: string) { + this.setDaemonSetting('download_directory', newDirectory); + } + + onKeyFeeChange(newValue: Price) { + this.setDaemonSetting('max_key_fee', newValue); + } + + onKeyFeeDisableChange(isDisabled: boolean) { + this.setDaemonSetting('disable_max_key_fee', isDisabled); + } + + onThemeChange(event: SyntheticInputEvent<*>) { + const { value } = event.target; + + if (value === 'dark') { + this.onAutomaticDarkModeChange(false); + } + + this.props.setClientSetting(settings.THEME, value); + } + + onAutomaticDarkModeChange(value: boolean) { + this.props.setClientSetting(settings.AUTOMATIC_DARK_MODE_ENABLED, value); + } + + onInstantPurchaseEnabledChange(enabled: boolean) { + this.props.setClientSetting(settings.INSTANT_PURCHASE_ENABLED, enabled); + } + + onInstantPurchaseMaxChange(newValue: Price) { + this.props.setClientSetting(settings.INSTANT_PURCHASE_MAX, newValue); + } + + onShowNsfwChange(event: SyntheticInputEvent<*>) { + this.props.setClientSetting(settings.SHOW_NSFW, event.target.checked); + } + + onShowUnavailableChange(event: SyntheticInputEvent<*>) { + this.props.setClientSetting(settings.SHOW_UNAVAILABLE, event.target.checked); + } + + setDaemonSetting(name: string, value: boolean | string | Price) { + this.props.setDaemonSetting(name, value); } clearCache() { @@ -26,343 +123,205 @@ class SettingsPage extends React.PureComponent { this.setState({ clearingCache: false }); window.location.href = 'index.html'; }; - const clear = () => this.props.clearCache().then(success.bind(this)); + const clear = () => this.props.clearCache().then(success); setTimeout(clear, 1000, { once: true }); } - setDaemonSetting(name, value) { - this.props.setDaemonSetting(name, value); - } - - onRunOnStartChange(event) { - this.setDaemonSetting('run_on_startup', event.target.checked); - } - - onShareDataChange(event) { - this.setDaemonSetting('share_usage_data', event.target.checked); - } - - onDownloadDirChange(event) { - this.setDaemonSetting('download_directory', event.target.value); - } - - onKeyFeeChange(newValue) { - const setting = newValue; - - // this is stupid and should be fixed... somewhere - if (setting && (setting.amount === undefined || setting.amount === null)) { - setting.amount = 0; - } - - this.setDaemonSetting('max_key_fee', setting); - } - - onKeyFeeDisableChange(isDisabled) { - this.setDaemonSetting('disable_max_key_fee', isDisabled); - } - - onThemeChange(event) { - const { value } = event.target; - - if (value === 'dark') { - this.onAutomaticDarkModeChange(false); - } - - this.props.setClientSetting(settings.THEME, value); - } - - onAutomaticDarkModeChange(value) { - this.props.setClientSetting(settings.AUTOMATIC_DARK_MODE_ENABLED, value); - } - - onInstantPurchaseEnabledChange(enabled) { - this.props.setClientSetting(settings.INSTANT_PURCHASE_ENABLED, enabled); - } - - onInstantPurchaseMaxChange(newValue) { - this.props.setClientSetting(settings.INSTANT_PURCHASE_MAX, newValue); - } - - // onMaxUploadPrefChange(isLimited) { - // if (!isLimited) { - // this.setDaemonSetting("max_upload", 0.0); - // } - // this.setState({ - // isMaxUpload: isLimited, - // }); - // } - // - // onMaxUploadFieldChange(event) { - // this.setDaemonSetting("max_upload", Number(event.target.value)); - // } - // - // onMaxDownloadPrefChange(isLimited) { - // if (!isLimited) { - // this.setDaemonSetting("max_download", 0.0); - // } - // this.setState({ - // isMaxDownload: isLimited, - // }); - // } - // - // onMaxDownloadFieldChange(event) { - // this.setDaemonSetting("max_download", Number(event.target.value)); - // } - - onShowNsfwChange(event) { - this.props.setClientSetting(settings.SHOW_NSFW, event.target.checked); - } - - onLanguageChange(e) { - this.props.changeLanguage(e.target.value); - this.forceUpdate(); - } - - onShowUnavailableChange(event) { - this.props.setClientSetting(settings.SHOW_UNAVAILABLE, event.target.checked); - } - - componentWillMount() { - this.props.getThemes(); - } - - componentDidMount() {} - render() { const { daemonSettings, - language, - languages, showNsfw, instantPurchaseEnabled, instantPurchaseMax, showUnavailable, - theme, + currentTheme, themes, automaticDarkModeEnabled, } = this.props; - if (!daemonSettings || Object.keys(daemonSettings).length === 0) { - return ( -
- {__('Failed to load settings.')} -
- ); - } + const noDaemonSettings = !daemonSettings || Object.keys(daemonSettings).length === 0; return ( -
- - {/* -
-
-

{__("Language")}

-
-
-
- - - {Object.keys(languages).map(dLang => - - )} - -
-
-
*/} -
-
-

{__('Download Directory')}

-
-
- -
-
-
-
-

{__('Max Purchase Price')}

-
-
- { - this.onKeyFeeDisableChange(true); - }} - defaultChecked={daemonSettings.disable_max_key_fee} - label={__('No Limit')} - /> -
- { - this.onKeyFeeDisableChange(false); - }} - defaultChecked={!daemonSettings.disable_max_key_fee} - label={daemonSettings.disable_max_key_fee ? __('Choose limit') : __('Limit to')} + + {noDaemonSettings ? ( +
+
{__('Failed to load settings.')}
+
+ ) : ( + +
+
{__('Download Directory')}
+ {__('LBRY downloads will be saved here.')} + - {!daemonSettings.disable_max_key_fee && ( +
+
+
{__('Max Purchase Price')}
+ + {__( + 'This will prevent you from purchasing any content over a certain cost, as a safety measure.' + )} + +
+ { + this.onKeyFeeDisableChange(true); + }} + /> + { + this.onKeyFeeDisableChange(false); + }} + checked={!daemonSettings.disable_max_key_fee} + postfix={__('Choose limit')} + /> - )} -
-
- {__( - 'This will prevent you from purchasing any content over this cost, as a safety measure.' - )} -
-
-
+
+
-
-
-

{__('Purchase Confirmations')}

-
-
- { - this.onInstantPurchaseEnabledChange(false); - }} - /> -
- { - this.onInstantPurchaseEnabledChange(true); - }} - /> - {instantPurchaseEnabled && ( - this.onInstantPurchaseMaxChange(val)} - defaultValue={instantPurchaseMax} +
+
{__('Purchase Confirmations')}
+
+ {__( + "When this option is chosen, LBRY won't ask you to confirm downloads below your chosen price." + )} +
+
+ { + this.onInstantPurchaseEnabledChange(false); + }} /> - )} -
-
- When this option is chosen, LBRY won't ask you to confirm downloads below the given - price. -
-
-
-
-
-

{__('Content')}

-
-
- - -
-
+ { + this.onInstantPurchaseEnabledChange(true); + }} + /> + +
+
-
-
-

{__('Share Diagnostic Data')}

-
-
- -
-
- -
-
-

{__('Theme')}

-
-
- - {themes.map((theme, index) => ( - - ))} - - - this.onAutomaticDarkModeChange(e.target.checked)} - checked={automaticDarkModeEnabled} - label={__('Automatic dark mode (9pm to 8am)')} - /> -
-
- -
-
-

{__('Application Cache')}

-
-
-

- +

{__('Content Settings')}
+ -

-
-
- + +
+ +
+
{__('Share Diagnostic Data')}
+
+ +
+
+ + { + // Hiding this for now until we update the dark mode styles + //
+ //
{__('Theme')}
+ // + // {themes.map(theme => ( + // + // ))} + // + // + // this.onAutomaticDarkModeChange(e.target.checked)} + // checked={automaticDarkModeEnabled} + // postfix={__('Automatic dark mode (9pm to 8am)')} + // /> + //
+ } + +
+
{__('Application Cache')}
+ + {__("This will delete your subscriptions, and clar the app's cache")} + +
+
+
+
+ )} + ); } } diff --git a/src/renderer/page/show/view.jsx b/src/renderer/page/show/view.jsx index 65799a7ee..acfae0bed 100644 --- a/src/renderer/page/show/view.jsx +++ b/src/renderer/page/show/view.jsx @@ -1,16 +1,25 @@ +// @flow import React from 'react'; -import { BusyMessage } from 'component/common'; +import BusyIndicator from 'component/common/busy-indicator'; import ChannelPage from 'page/channel'; import FilePage from 'page/file'; +import Page from 'component/page'; -class ShowPage extends React.PureComponent { - componentWillMount() { +type Props = { + isResolvingUri: boolean, + resolveUri: string => void, + uri: string, + claim: { name: string }, +}; + +class ShowPage extends React.PureComponent { + componentDidMount() { const { isResolvingUri, resolveUri, uri } = this.props; if (!isResolvingUri) resolveUri(uri); } - componentWillReceiveProps(nextProps) { + componentWillReceiveProps(nextProps: Props) { const { isResolvingUri, resolveUri, claim, uri } = nextProps; if (!isResolvingUri && claim === undefined && uri) { @@ -25,20 +34,18 @@ class ShowPage extends React.PureComponent { if ((isResolvingUri && !claim) || !claim) { innerContent = ( -
-
-
-

{uri}

+ +
+

{uri}

+
+ {isResolvingUri && } + {claim === null && + !isResolvingUri && ( + {__("There's nothing at this location.")} + )}
-
-
- {isResolvingUri && } - {claim === null && - !isResolvingUri && ( - {__("There's nothing at this location.")} - )} -
-
+
+ ); } else if (claim && claim.name.length && claim.name[0] === '@') { innerContent = ; @@ -46,7 +53,7 @@ class ShowPage extends React.PureComponent { innerContent = ; } - return
{innerContent}
; + return innerContent; } } diff --git a/src/renderer/page/subscriptions/view.jsx b/src/renderer/page/subscriptions/view.jsx index 8f0e56840..27f3fac45 100644 --- a/src/renderer/page/subscriptions/view.jsx +++ b/src/renderer/page/subscriptions/view.jsx @@ -1,10 +1,10 @@ // @flow import React from 'react'; -import SubHeader from 'component/subHeader'; -import { BusyMessage } from 'component/common.js'; -import { FeaturedCategory } from 'page/discover/view'; +import Page from 'component/page'; +import CategoryList from 'component/common/category-list'; import type { Subscription } from 'redux/reducers/subscriptions'; import * as NOTIFICATION_TYPES from 'constants/notification_types'; +import Button from 'component/button'; type SavedSubscriptions = Array; @@ -75,14 +75,13 @@ export default class extends React.PureComponent { (subscriptions.length !== savedSubscriptions.length || someClaimsNotLoaded); return ( -
- + {!savedSubscriptions.length && ( - {__("You haven't subscribed to any channels yet")} - )} - {fetchingSubscriptions && ( -
- +
+ {__("It looks like you aren't subscribed to any channels yet.")} +
+
)} {!!savedSubscriptions.length && ( @@ -97,7 +96,7 @@ export default class extends React.PureComponent { } return ( - { })}
)} -
+ ); } } diff --git a/src/renderer/page/transactionHistory/view.jsx b/src/renderer/page/transactionHistory/view.jsx index 562da6aa5..62d9b394f 100644 --- a/src/renderer/page/transactionHistory/view.jsx +++ b/src/renderer/page/transactionHistory/view.jsx @@ -1,7 +1,7 @@ import React from 'react'; -import { BusyMessage } from 'component/common'; -import SubHeader from 'component/subHeader'; +import BusyIndicator from 'component/common/busy-indicator'; import TransactionList from 'component/transactionList'; +import Page from 'component/page'; class TransactionHistoryPage extends React.PureComponent { componentWillMount() { @@ -12,30 +12,29 @@ class TransactionHistoryPage extends React.PureComponent { const { fetchingTransactions, transactions } = this.props; return ( -
- -
+ +

{__('Transaction History')}

-
- {fetchingTransactions && !transactions.length ? ( - - ) : ( - '' - )} - {transactions && transactions.length ? ( - - ) : ( - '' - )} -
+ {fetchingTransactions && !transactions.length ? ( +
+ +
+ ) : ( + '' + )} + {transactions && transactions.length ? ( + + ) : ( + '' + )}
-
+ ); } } diff --git a/src/renderer/page/wallet/view.jsx b/src/renderer/page/wallet/view.jsx index b98bb80f9..683ba03e7 100644 --- a/src/renderer/page/wallet/view.jsx +++ b/src/renderer/page/wallet/view.jsx @@ -1,18 +1,19 @@ import React from 'react'; -import SubHeader from 'component/subHeader'; import WalletBalance from 'component/walletBalance'; import RewardSummary from 'component/rewardSummary'; import TransactionListRecent from 'component/transactionListRecent'; +import WalletAddress from 'component/walletAddress'; +import Page from 'component/page'; -const WalletPage = props => ( -
- -
+const WalletPage = () => ( + +
+ -
+ ); export default WalletPage; diff --git a/src/renderer/redux/actions/content.js b/src/renderer/redux/actions/content.js index 2fdffc677..afa1ad2ef 100644 --- a/src/renderer/redux/actions/content.js +++ b/src/renderer/redux/actions/content.js @@ -526,24 +526,6 @@ export function doCreateChannel(name, amount) { }; } -export function doPublish(params) { - return dispatch => - new Promise((resolve, reject) => { - const success = claim => { - resolve(claim); - - if (claim === true) dispatch(doFetchClaimListMine()); - else - setTimeout(() => dispatch(doFetchClaimListMine()), 20000, { - once: true, - }); - }; - const failure = err => reject(err); - - Lbry.publishDeprecated(params, null, success, failure); - }); -} - export function doAbandonClaim(txid, nout) { return (dispatch, getState) => { const state = getState(); diff --git a/src/renderer/redux/actions/publish.js b/src/renderer/redux/actions/publish.js new file mode 100644 index 000000000..ba93acb2b --- /dev/null +++ b/src/renderer/redux/actions/publish.js @@ -0,0 +1,181 @@ +// @flow +import Lbry from 'lbry'; +import * as ACTIONS from 'constants/action_types'; +import * as MODALS from 'constants/modal_types'; +import { selectMyClaimsWithoutChannels } from 'redux/selectors/claims'; +import { selectPendingPublishes } from 'redux/selectors/publish'; +import { doOpenModal } from 'redux/actions/app'; +import type { + UpdatePublishFormData, + UpdatePublishFormAction, + PublishParams, +} from 'redux/reducers/publish'; +import { CHANNEL_NEW, CHANNEL_ANONYMOUS } from 'constants/claim'; + +type Action = UpdatePublishFormAction | { type: ACTIONS.CLEAR_PUBLISH }; +type PromiseAction = Promise; +type Dispatch = (action: Action | PromiseAction | Array) => any; +type ThunkAction = (dispatch: Dispatch) => any; +type GetState = () => {}; + +export const doClearPublish = () => (dispatch: Dispatch): Action => + dispatch({ type: ACTIONS.CLEAR_PUBLISH }); + +export const doUpdatePublishForm = (publishFormValue: UpdatePublishFormData) => ( + dispatch: Dispatch +): UpdatePublishFormAction => + dispatch({ + type: ACTIONS.UPDATE_PUBLISH_FORM, + data: { ...publishFormValue }, + }); + +export const doPrepareEdit = (claim: any) => (dispatch: Dispatch) => { + const { name, amount, channel_name: channelName, value: { stream: { metadata } } } = claim; + const { + author, + description, + fee, + language, + license, + licenseUrl, + nsfw, + thumbnail, + title, + } = metadata; + + const publishData = { + name, + channel: channelName, + bid: amount, + price: { amount: fee.amount, currency: fee.currency }, + contentIsFree: !fee.amount, + author, + description, + fee, + language, + license, + licenseUrl, + nsfw, + thumbnail, + title, + }; + + dispatch({ type: ACTIONS.DO_PREPARE_EDIT, data: publishData }); +}; + +export const doPublish = (params: PublishParams): ThunkAction => { + const { + name, + bid, + filePath: file_path, + description, + language, + license, + licenseUrl, + thumbnail, + nsfw, + channel, + title, + contentIsFree, + price, + uri, + } = params; + + const channelName = channel === CHANNEL_ANONYMOUS || channel === CHANNEL_NEW ? '' : channel; + const fee = contentIsFree || !price.amount ? undefined : { ...price }; + + const metadata = { + title, + nsfw, + license, + licenseUrl, + language, + thumbnail, + }; + + if (fee) { + metadata.fee = fee; + } + + if (description) { + metadata.description = description; + } + + const publishPayload = { + file_path, + name, + channel_name: channelName, + bid, + metadata, + }; + + return (dispatch: Dispatch) => { + dispatch({ type: ACTIONS.PUBLISH_START }); + + const success = () => { + dispatch({ + type: ACTIONS.PUBLISH_SUCCESS, + data: { pendingPublish: publishPayload }, + }); + dispatch(doOpenModal(MODALS.PUBLISH, { uri })); + }; + + const failure = error => { + dispatch({ type: ACTIONS.PUBLISH_FAIL }); + dispatch(doOpenModal(MODALS.ERROR, { error: error.message })); + }; + + return Lbry.publish(publishPayload).then(success, failure); + }; +}; + +// Calls claim_list_mine until any pending publishes are confirmed +export const doCheckPendingPublishes = () => { + return (dispatch: Dispatch, getState: GetState) => { + const state = getState(); + const pendingPublishes = selectPendingPublishes(state); + const myClaims = selectMyClaimsWithoutChannels(state); + + let publishCheckInterval; + + const checkFileList = () => { + Lbry.claim_list_mine().then(claims => { + const claimsWithoutChannels = claims.filter(claim => !claim.name.match(/^@/)); + if (myClaims.length !== claimsWithoutChannels.length) { + const pendingPublishMap = {}; + pendingPublishes.forEach(({ name }) => { + pendingPublishMap[name] = name; + }); + + claims.forEach(claim => { + if (pendingPublishMap[claim.name]) { + dispatch({ + type: ACTIONS.REMOVE_PENDING_PUBLISH, + data: { + name: claim.name, + }, + }); + dispatch({ + type: ACTIONS.FETCH_CLAIM_LIST_MINE_COMPLETED, + data: { + claims, + }, + }); + + delete pendingPublishMap[claim.name]; + } + }); + + clearInterval(publishCheckInterval); + } + }); + }; + + if (pendingPublishes.length) { + checkFileList(); + publishCheckInterval = setInterval(() => { + checkFileList(); + }, 10000); + } + }; +}; diff --git a/src/renderer/redux/actions/search.js b/src/renderer/redux/actions/search.js index b4b12cd57..3aac1bf09 100644 --- a/src/renderer/redux/actions/search.js +++ b/src/renderer/redux/actions/search.js @@ -1,67 +1,176 @@ import * as ACTIONS from 'constants/action_types'; -import { buildURI } from 'lbryURI'; +import * as SEARCH_TYPES from 'constants/search'; +import { normalizeURI, buildURI, parseURI } from 'lbryURI'; import { doResolveUri } from 'redux/actions/content'; import { doNavigate } from 'redux/actions/navigation'; import { selectCurrentPage } from 'redux/selectors/navigation'; +import { makeSelectSearchUris } from 'redux/selectors/search'; import batchActions from 'util/batchActions'; +import handleFetchResponse from 'util/handle-fetch'; -// eslint-disable-next-line import/prefer-default-export -export function doSearch(rawQuery) { - return (dispatch, getState) => { - const state = getState(); - const page = selectCurrentPage(state); - - const query = rawQuery.replace(/^lbry:\/\//i, ''); - - if (!query) { - dispatch({ - type: ACTIONS.SEARCH_CANCELLED, - }); - return; - } +export const doSearch = rawQuery => (dispatch, getState) => { + const state = getState(); + const query = rawQuery.replace(/^lbry:\/\//i, ''); + if (!query) { dispatch({ - type: ACTIONS.SEARCH_STARTED, - data: { query }, + type: ACTIONS.SEARCH_FAIL, }); + return; + } - if (page !== 'search') { - dispatch(doNavigate('search', { query })); - } else { - fetch(`https://lighthouse.lbry.io/search?s=${query}`) - .then( - response => - response.status === 200 - ? Promise.resolve(response.json()) - : Promise.reject(new Error(response.statusText)) - ) - .then(data => { - const uris = []; - const actions = []; + // If we have already searched for something, we don't need to do anything + const urisForQuery = makeSelectSearchUris(query)(state); + if (urisForQuery && !!urisForQuery.length) { + return; + } - data.forEach(result => { - const uri = buildURI({ - claimName: result.name, - claimId: result.claimId, - }); - actions.push(doResolveUri(uri)); - uris.push(uri); - }); + dispatch({ + type: ACTIONS.SEARCH_START, + }); - actions.push({ - type: ACTIONS.SEARCH_COMPLETED, - data: { - query, - uris, - }, - }); - dispatch(batchActions(...actions)); - }) - .catch(() => { - dispatch({ - type: ACTIONS.SEARCH_CANCELLED, - }); + // If the user is on the file page with a pre-populated uri and they select + // the search option without typing anything, searchQuery will be empty + // We need to populate it so the input is filled on the search page + if (!state.search.searchQuery) { + dispatch({ + type: ACTIONS.UPDATE_SEARCH_QUERY, + data: { searchQuery: query }, + }); + } + + fetch(`https://lighthouse.lbry.io/search?s=${query}`) + .then(handleFetchResponse) + .then(data => { + const uris = []; + const actions = []; + + data.forEach(result => { + const uri = buildURI({ + claimName: result.name, + claimId: result.claimId, }); - } + actions.push(doResolveUri(uri)); + uris.push(uri); + }); + + actions.push({ + type: ACTIONS.SEARCH_SUCCESS, + data: { + query, + uris, + }, + }); + dispatch(batchActions(...actions)); + }) + .catch(() => { + dispatch({ + type: ACTIONS.SEARCH_FAIL, + }); + }); +}; + +export const doUpdateSearchQuery = (query: string, shouldSkipSuggestions: ?boolean) => dispatch => { + dispatch({ + type: ACTIONS.UPDATE_SEARCH_QUERY, + data: { query }, + }); + + // Don't fetch new suggestions if the user just added a space + if (!query.endsWith(' ') || !shouldSkipSuggestions) { + dispatch(getSearchSuggestions(query)); + } +}; + +export const getSearchSuggestions = (value: string) => dispatch => { + const query = value.trim(); + + const isPrefix = () => { + return query === '@' || query === 'lbry:' || query === 'lbry:/' || query === 'lbry://'; }; -} + + if (!query || isPrefix()) { + dispatch({ + type: ACTIONS.UPDATE_SEARCH_SUGGESTIONS, + data: { suggestions: [] }, + }); + return; + } + + let suggestions = []; + try { + // If the user is about to manually add the claim id ignore it until they + // actually add one. This would hardly ever happen, but then the search + // suggestions won't change just from adding a '#' after a uri + let uriQuery = query; + if (uriQuery.endsWith('#')) { + uriQuery = uriQuery.slice(0, -1); + } + + const uri = normalizeURI(uriQuery); + const { claimName, isChannel } = parseURI(uri); + + suggestions.push( + { + value: uri, + shorthand: isChannel ? claimName.slice(1) : claimName, + type: isChannel ? SEARCH_TYPES.CHANNEL : SEARCH_TYPES.FILE, + }, + { + value: claimName, + type: SEARCH_TYPES.SEARCH, + } + ); + + // If it's a valid url, don't fetch any extra search results + return dispatch({ + type: ACTIONS.UPDATE_SEARCH_SUGGESTIONS, + data: { suggestions }, + }); + } catch (e) { + suggestions.push({ + value: query, + type: SEARCH_TYPES.SEARCH, + }); + } + + // Populate the current search query suggestion before fetching results + dispatch({ + type: ACTIONS.UPDATE_SEARCH_SUGGESTIONS, + data: { suggestions }, + }); + + // strip out any basic stuff for more accurate search results + let searchValue = value.replace(/lbry:\/\//g, '').replace(/-/g, ' '); + if (searchValue.includes('#')) { + // This should probably be more robust, but I think it's fine for now + // Remove everything after # to get rid of the claim id + searchValue = searchValue.substring(0, searchValue.indexOf('#')); + } + + return fetch(`https://lighthouse.lbry.io/autocomplete?s=${searchValue}`) + .then(handleFetchResponse) + .then(apiSuggestions => { + const formattedSuggestions = apiSuggestions.slice(0, 6).map(suggestion => { + // This will need to be more robust when the api starts returning lbry uris + const isChannel = suggestion.startsWith('@'); + const suggestionObj = { + value: isChannel ? `lbry://${suggestion}` : suggestion, + shorthand: isChannel ? suggestion.slice(1) : '', + type: isChannel ? 'channel' : 'search', + }; + + return suggestionObj; + }); + + suggestions = suggestions.concat(formattedSuggestions); + dispatch({ + type: ACTIONS.UPDATE_SEARCH_SUGGESTIONS, + data: { suggestions }, + }); + }) + .catch(() => { + // If the fetch fails, do nothing + // Basic search suggestions are already populated at this point + }); +}; diff --git a/src/renderer/redux/actions/video.js b/src/renderer/redux/actions/video.js deleted file mode 100644 index 4ede45bd0..000000000 --- a/src/renderer/redux/actions/video.js +++ /dev/null @@ -1,10 +0,0 @@ -// @flow -import * as actions from 'constants/action_types'; -import type { Dispatch } from 'redux/reducers/video'; - -// eslint-disable-next-line import/prefer-default-export -export const setVideoPause = (data: boolean) => (dispatch: Dispatch) => - dispatch({ - type: actions.SET_VIDEO_PAUSE, - data, - }); diff --git a/src/renderer/redux/actions/wallet.js b/src/renderer/redux/actions/wallet.js index 6422b6315..a89ca9273 100644 --- a/src/renderer/redux/actions/wallet.js +++ b/src/renderer/redux/actions/wallet.js @@ -3,22 +3,21 @@ import * as MODALS from 'constants/modal_types'; import Lbry from 'lbry'; import { doOpenModal, doShowSnackBar } from 'redux/actions/app'; import { doNavigate } from 'redux/actions/navigation'; -import { - selectBalance, - selectDraftTransaction, - selectDraftTransactionAmount, -} from 'redux/selectors/wallet'; +import { selectBalance } from 'redux/selectors/wallet'; export function doUpdateBalance() { - return dispatch => { - Lbry.wallet_balance().then(balance => - dispatch({ - type: ACTIONS.UPDATE_BALANCE, - data: { - balance, - }, - }) - ); + return (dispatch, getState) => { + const { wallet: { balance: balanceInStore } } = getState(); + Lbry.wallet_balance().then(balance => { + if (balanceInStore !== balance) { + return dispatch({ + type: ACTIONS.UPDATE_BALANCE, + data: { + balance, + }, + }); + } + }); }; } @@ -89,12 +88,10 @@ export function doCheckAddressIsMine(address) { }; } -export function doSendDraftTransaction() { +export function doSendDraftTransaction({ amount, address }) { return (dispatch, getState) => { const state = getState(); - const draftTx = selectDraftTransaction(state); const balance = selectBalance(state); - const amount = selectDraftTransactionAmount(state); if (balance - amount <= 0) { dispatch(doOpenModal(MODALS.INSUFFICIENT_CREDITS)); @@ -135,26 +132,12 @@ export function doSendDraftTransaction() { }; Lbry.wallet_send({ - amount: draftTx.amount, - address: draftTx.address, + amount, + address, }).then(successCallback, errorCallback); }; } -export function doSetDraftTransactionAmount(amount) { - return { - type: ACTIONS.SET_DRAFT_TRANSACTION_AMOUNT, - data: { amount }, - }; -} - -export function doSetDraftTransactionAddress(address) { - return { - type: ACTIONS.SET_DRAFT_TRANSACTION_ADDRESS, - data: { address }, - }; -} - export function doSendSupport(amount, claimId, uri) { return (dispatch, getState) => { const state = getState(); diff --git a/src/renderer/redux/reducers/app.js b/src/renderer/redux/reducers/app.js index fa13a325f..ac8b8f5fb 100644 --- a/src/renderer/redux/reducers/app.js +++ b/src/renderer/redux/reducers/app.js @@ -138,9 +138,8 @@ reducers[ACTIONS.SET_PLAYING_URI] = (state, action) => { return Object.assign({}, state, { modalsAllowed: true, }); - } else { - return state; } + return state; }; reducers[ACTIONS.UPDATE_VERSION] = (state, action) => @@ -162,12 +161,11 @@ reducers[ACTIONS.CHECK_UPGRADE_SUBSCRIBE] = (state, action) => reducers[ACTIONS.OPEN_MODAL] = (state, action) => { if (!state.modalsAllowed) { return state; - } else { - return Object.assign({}, state, { - modal: action.data.modal, - modalProps: action.data.modalProps || {}, - }); } + return Object.assign({}, state, { + modal: action.data.modal, + modalProps: action.data.modalProps || {}, + }); }; reducers[ACTIONS.CLOSE_MODAL] = state => Object.assign({}, state, { @@ -234,6 +232,12 @@ reducers[ACTIONS.VOLUME_CHANGED] = (state, action) => volume: action.data.volume, }); +reducers[ACTIONS.HISTORY_NAVIGATE] = state => + Object.assign({}, state, { + modal: undefined, + modalProps: {}, + }); + export default function reducer(state: AppState = defaultState, action: any) { const handler = reducers[action.type]; if (handler) return handler(state, action); diff --git a/src/renderer/redux/reducers/content.js b/src/renderer/redux/reducers/content.js index e5242b599..e9cbdff99 100644 --- a/src/renderer/redux/reducers/content.js +++ b/src/renderer/redux/reducers/content.js @@ -3,6 +3,7 @@ import * as ACTIONS from 'constants/action_types'; const reducers = {}; const defaultState = { playingUri: null, + currentlyIsPlaying: false, rewardedContentClaimIds: [], channelClaimCounts: {}, }; diff --git a/src/renderer/redux/reducers/publish.js b/src/renderer/redux/reducers/publish.js new file mode 100644 index 000000000..195e8c6cf --- /dev/null +++ b/src/renderer/redux/reducers/publish.js @@ -0,0 +1,167 @@ +// @flow +import { handleActions } from 'util/redux-utils'; +import { buildURI } from 'lbryURI'; +import * as ACTIONS from 'constants/action_types'; +import { CHANNEL_ANONYMOUS } from 'constants/claim'; + +type PublishState = { + editingURI: ?string, + filePath: ?string, + contentIsFree: boolean, + price: { + amount: number, + currency: string, + }, + title: string, + thumbnail: string, + description: string, + language: string, + tosAccepted: boolean, + channel: string, + name: string, + nameError: ?string, + bid: number, + bidError: ?string, + otherLicenseDescription: string, + licenseUrl: string, + copyrightNotice: string, + pendingPublishes: Array, +}; + +export type UpdatePublishFormData = { + filePath?: string, + contentIsFree?: boolean, + price?: { + amount: number, + currency: string, + }, + title?: string, + thumbnail?: string, + description?: string, + language?: string, + tosAccepted?: boolean, + channel?: string, + name?: string, + nameError?: string, + bid?: number, + bidError?: string, + otherLicenseDescription?: string, + licenseUrl?: string, + copyrightNotice?: string, +}; + +export type UpdatePublishFormAction = { + type: ACTIONS.UPDATE_PUBLISH_FORM | ACTIONS.DO_PREPARE_EDIT, + data: UpdatePublishFormData, +}; + +export type PublishParams = { + name: string, + bid: number, + filePath: string, + description: ?string, + language: string, + publishingLicense: string, + publishingLicenseUrl: string, + thumbnail: ?string, + nsfw: boolean, + channel: string, + title: string, + contentIsFree: boolean, + uri: string, + license: ?string, + licenseUrl: ?string, + price: { + currency: string, + amount: number, + }, +}; + +const defaultState: PublishState = { + editingURI: undefined, + filePath: undefined, + contentIsFree: true, + price: { + amount: 1, + currency: 'LBC', + }, + title: '', + thumbnail: '', + description: '', + language: 'en', + nsfw: false, + channel: CHANNEL_ANONYMOUS, + tosAccepted: false, + name: '', + nameError: undefined, + bid: 0.1, + bidError: undefined, + licenseType: 'None', + otherLicenseDescription: '', + licenseUrl: '', + copyrightNotice: 'All rights reserved', + publishing: false, + publishSuccess: false, + publishError: undefined, + pendingPublishes: [], +}; + +export default handleActions( + { + [ACTIONS.UPDATE_PUBLISH_FORM]: (state, action): PublishState => { + const { data } = action; + return { + ...state, + ...data, + }; + }, + [ACTIONS.CLEAR_PUBLISH]: (state: PublishState): PublishState => { + const { pendingPublishes } = state; + return { ...defaultState, pendingPublishes }; + }, + [ACTIONS.PUBLISH_START]: (state: PublishState): PublishState => ({ + ...state, + publishing: true, + }), + [ACTIONS.PUBLISH_FAIL]: (state: PublishState): PublishState => ({ + ...state, + publishing: false, + }), + [ACTIONS.PUBLISH_SUCCESS]: (state: PublishState, action): PublishState => { + const { pendingPublish } = action.data; + + const newPendingPublishes = state.pendingPublishes.slice(); + newPendingPublishes.push(pendingPublish); + + return { + ...state, + publishing: false, + pendingPublishes: newPendingPublishes, + }; + }, + [ACTIONS.REMOVE_PENDING_PUBLISH]: (state: PublishState, action) => { + const { name } = action.data; + const pendingPublishes = state.pendingPublishes.filter(publish => publish.name !== name); + return { + ...state, + pendingPublishes, + }; + }, + [ACTIONS.DO_PREPARE_EDIT]: (state: PublishState, action) => { + const { ...publishData } = action.data; + const { channel, name } = publishData; + + const uri = buildURI({ + channelName: channel, + contentName: name, + }); + + return { + ...defaultState, + editingURI: uri, + ...publishData, + }; + }, + }, + defaultState +); diff --git a/src/renderer/redux/reducers/search.js b/src/renderer/redux/reducers/search.js index b3a0754e4..7a2a92ca1 100644 --- a/src/renderer/redux/reducers/search.js +++ b/src/renderer/redux/reducers/search.js @@ -1,32 +1,103 @@ +// @flow import * as ACTIONS from 'constants/action_types'; +import { handleActions } from 'util/redux-utils'; -const reducers = {}; -const defaultState = { +type SearchSuccess = { + type: ACTIONS.SEARCH_SUCCESS, + data: { + query: string, + uris: Array, + }, +}; + +type UpdateSearchQuery = { + type: ACTIONS.UPDATE_SEARCH_QUERY, + data: { + query: string, + }, +}; + +type SearchSuggestion = { + value: string, + shorthand: string, + type: string, +}; + +type UpdateSearchSuggestions = { + type: ACTIONS.UPDATE_SEARCH_SUGGESTIONS, + data: { + suggestions: Array, + }, +}; + +type SearchState = { + isActive: boolean, + searchQuery: string, + suggestions: Array, urisByQuery: {}, - searching: false, }; -reducers[ACTIONS.SEARCH_STARTED] = state => - Object.assign({}, state, { - searching: true, - }); - -reducers[ACTIONS.SEARCH_COMPLETED] = (state, action) => { - const { query, uris } = action.data; - - return Object.assign({}, state, { - searching: false, - urisByQuery: Object.assign({}, state.urisByQuery, { [query]: uris }), - }); +const defaultState = { + isActive: false, + searchQuery: '', // needs to be an empty string for input focusing + suggestions: [], + urisByQuery: {}, }; -reducers[ACTIONS.SEARCH_CANCELLED] = state => - Object.assign({}, state, { - searching: false, - }); +export default handleActions( + { + [ACTIONS.SEARCH_START]: (state: SearchState): SearchState => ({ + ...state, + searching: true, + }), + [ACTIONS.SEARCH_SUCCESS]: (state: SearchState, action: SearchSuccess): SearchState => { + const { query, uris } = action.data; -export default function reducer(state = defaultState, action) { - const handler = reducers[action.type]; - if (handler) return handler(state, action); - return state; -} + return { + ...state, + searching: false, + urisByQuery: Object.assign({}, state.urisByQuery, { [query]: uris }), + }; + }, + + [ACTIONS.SEARCH_FAIL]: (state: SearchState): SearchState => ({ + ...state, + searching: false, + }), + + [ACTIONS.UPDATE_SEARCH_QUERY]: ( + state: SearchState, + action: UpdateSearchQuery + ): SearchState => ({ + ...state, + searchQuery: action.data.query, + isActive: true, + }), + + [ACTIONS.UPDATE_SEARCH_SUGGESTIONS]: ( + state: SearchState, + action: UpdateSearchSuggestions + ): SearchState => ({ + ...state, + suggestions: action.data.suggestions, + }), + + // clear the searchQuery on back/forward + // it may be populated by the page title for search/file pages + // if going home, it should be blank + [ACTIONS.HISTORY_NAVIGATE]: (state: SearchState): SearchState => ({ + ...state, + searchQuery: '', + suggestions: [], + isActive: false, + }), + // sets isActive to false so the uri will be populated correctly if the + // user is on a file page. The search query will still be present on any + // other page + [ACTIONS.CLOSE_MODAL]: (state: SearchState): SearchState => ({ + ...state, + isActive: false, + }), + }, + defaultState +); diff --git a/src/renderer/redux/reducers/settings.js b/src/renderer/redux/reducers/settings.js index 73cf69209..eecba6083 100644 --- a/src/renderer/redux/reducers/settings.js +++ b/src/renderer/redux/reducers/settings.js @@ -27,6 +27,7 @@ const defaultState = { }, isNight: false, languages: {}, + daemonSettings: {}, }; reducers[ACTIONS.DAEMON_SETTINGS_RECEIVED] = (state, action) => diff --git a/src/renderer/redux/reducers/video.js b/src/renderer/redux/reducers/video.js deleted file mode 100644 index 4025c0d77..000000000 --- a/src/renderer/redux/reducers/video.js +++ /dev/null @@ -1,25 +0,0 @@ -// @flow -import * as ACTIONS from 'constants/action_types'; -import { handleActions } from 'util/redux-utils'; - -export type VideoState = { videoPause: boolean }; - -type setVideoPause = { - type: ACTIONS.SET_VIDEO_PAUSE, - data: boolean, -}; - -export type Action = setVideoPause; -export type Dispatch = (action: Action) => any; - -const defaultState = { videoPause: false }; - -export default handleActions( - { - [ACTIONS.SET_VIDEO_PAUSE]: (state: VideoState, action: setVideoPause): VideoState => ({ - ...state, - videoPause: action.data, - }), - }, - defaultState -); diff --git a/src/renderer/redux/reducers/wallet.js b/src/renderer/redux/reducers/wallet.js index bc5761c6c..590404ce8 100644 --- a/src/renderer/redux/reducers/wallet.js +++ b/src/renderer/redux/reducers/wallet.js @@ -2,10 +2,6 @@ import * as ACTIONS from 'constants/action_types'; const reducers = {}; const receiveAddress = localStorage.getItem('receiveAddress'); -const buildDraftTransaction = () => ({ - amount: undefined, - address: undefined, -}); const defaultState = { balance: undefined, @@ -14,8 +10,8 @@ const defaultState = { fetchingTransactions: false, receiveAddress, gettingNewAddress: false, - draftTransaction: buildDraftTransaction(), sendingSupport: false, + sendingTx: false, }; reducers[ACTIONS.FETCH_TRANSACTIONS_STARTED] = state => @@ -68,51 +64,21 @@ reducers[ACTIONS.CHECK_ADDRESS_IS_MINE_COMPLETED] = state => checkingAddressOwnership: false, }); -reducers[ACTIONS.SET_DRAFT_TRANSACTION_AMOUNT] = (state, action) => { - const oldDraft = state.draftTransaction; - const newDraft = Object.assign({}, oldDraft, { - amount: parseFloat(action.data.amount), - }); - - return Object.assign({}, state, { - draftTransaction: newDraft, - }); -}; - -reducers[ACTIONS.SET_DRAFT_TRANSACTION_ADDRESS] = (state, action) => { - const oldDraft = state.draftTransaction; - const newDraft = Object.assign({}, oldDraft, { - address: action.data.address, - }); - - return Object.assign({}, state, { - draftTransaction: newDraft, - }); -}; - reducers[ACTIONS.SEND_TRANSACTION_STARTED] = state => { - const newDraftTransaction = Object.assign({}, state.draftTransaction, { - sending: true, - }); - return Object.assign({}, state, { - draftTransaction: newDraftTransaction, + sendingTx: true, }); }; reducers[ACTIONS.SEND_TRANSACTION_COMPLETED] = state => Object.assign({}, state, { - draftTransaction: buildDraftTransaction(), + sendingTx: false, }); reducers[ACTIONS.SEND_TRANSACTION_FAILED] = (state, action) => { - const newDraftTransaction = Object.assign({}, state.draftTransaction, { - sending: false, - error: action.data.error, - }); - return Object.assign({}, state, { - draftTransaction: newDraftTransaction, + sendingTx: false, + error: action.data.error, }); }; diff --git a/src/renderer/redux/selectors/claims.js b/src/renderer/redux/selectors/claims.js index 9da8a6d51..bb3c7ae04 100644 --- a/src/renderer/redux/selectors/claims.js +++ b/src/renderer/redux/selectors/claims.js @@ -6,6 +6,12 @@ const selectState = state => state.claims || {}; export const selectClaimsById = createSelector(selectState, state => state.byId || {}); +export const selectClaimById = id => + createSelector(selectClaimsById, claims => { + const claimById = claims[id]; + return claimById; + }); + export const selectClaimsByUri = createSelector(selectState, selectClaimsById, (state, byId) => { const byUri = state.claimsByUri || {}; const claims = {}; diff --git a/src/renderer/redux/selectors/file_info.js b/src/renderer/redux/selectors/file_info.js index f760bb3be..f13c0c9ae 100644 --- a/src/renderer/redux/selectors/file_info.js +++ b/src/renderer/redux/selectors/file_info.js @@ -4,6 +4,7 @@ import { selectMyClaims, } from 'redux/selectors/claims'; import { createSelector } from 'reselect'; +import { buildURI } from 'lbryURI'; export const selectState = state => state.fileInfo || {}; @@ -27,7 +28,6 @@ export const makeSelectFileInfoForUri = uri => createSelector(selectClaimsByUri, selectFileInfosByOutpoint, (claims, byOutpoint) => { const claim = claims[uri]; const outpoint = claim ? `${claim.txid}:${claim.nout}` : undefined; - return outpoint ? byOutpoint[outpoint] : undefined; }); @@ -104,3 +104,92 @@ export const selectTotalDownloadProgress = createSelector(selectDownloadingFileI if (fileInfos.length > 0) return totalProgress / fileInfos.length / 100.0; return -1; }); + +export const selectSearchDownloadUris = query => + createSelector(selectFileInfosDownloaded, fileInfos => { + if (!query || !fileInfos.length) { + return null; + } + + const queryParts = query.toLowerCase().split(' '); + const searchQueryDictionary = {}; + queryParts.forEach(subQuery => { + searchQueryDictionary[subQuery] = subQuery; + }); + + const arrayContainsQueryPart = array => { + for (let i = 0; i < array.length; i += 1) { + const subQuery = array[i]; + if (searchQueryDictionary[subQuery]) { + return true; + } + } + return false; + }; + + const downloadResultsFromQuery = []; + fileInfos.forEach(fileInfo => { + const { channel_name, claim_name, metadata } = fileInfo; + const { author, description, title } = metadata; + + if (channel_name) { + const channelName = channel_name.toLowerCase(); + const strippedOutChannelName = channelName.slice(1); // trim off the @ + if (searchQueryDictionary[channel_name] || searchQueryDictionary[strippedOutChannelName]) { + downloadResultsFromQuery.push(fileInfo); + return; + } + } + + const nameParts = claim_name.toLowerCase().split('-'); + if (arrayContainsQueryPart(nameParts)) { + downloadResultsFromQuery.push(fileInfo); + return; + } + + const titleParts = title.toLowerCase().split(' '); + if (arrayContainsQueryPart(titleParts)) { + downloadResultsFromQuery.push(fileInfo); + return; + } + + if (author) { + const authorParts = author.toLowerCase().split(' '); + if (arrayContainsQueryPart(authorParts)) { + downloadResultsFromQuery.push(fileInfo); + return; + } + } + + if (description) { + const descriptionParts = description.toLowerCase().split(' '); + if (arrayContainsQueryPart(descriptionParts)) { + downloadResultsFromQuery.push(fileInfo); + } + } + }); + + return downloadResultsFromQuery.length + ? downloadResultsFromQuery.map(fileInfo => { + const { + channel_name: channelName, + claim_id: claimId, + claim_name: claimName, + value, + metadata, + } = fileInfo; + const uriParams = {}; + + if (channelName) { + uriParams.channelName = channelName; + } + + uriParams.claimId = claimId; + uriParams.claimId = claimId; + uriParams.contentName = claimName; + + const uri = buildURI(uriParams); + return uri; + }) + : null; + }); diff --git a/src/renderer/redux/selectors/navigation.js b/src/renderer/redux/selectors/navigation.js index fdee0783d..f4acc3034 100644 --- a/src/renderer/redux/selectors/navigation.js +++ b/src/renderer/redux/selectors/navigation.js @@ -1,6 +1,5 @@ import { createSelector } from 'reselect'; -import { parseQueryParams, toQueryString } from 'util/query_params'; -import { normalizeURI } from 'lbryURI'; +import { parseQueryParams } from 'util/query_params'; export const selectState = state => state.navigation || {}; @@ -22,103 +21,6 @@ export const selectCurrentParams = createSelector(selectCurrentPath, path => { export const makeSelectCurrentParam = param => createSelector(selectCurrentParams, params => (params ? params[param] : undefined)); -export const selectHeaderLinks = createSelector(selectCurrentPage, page => { - // This contains intentional fall throughs - switch (page) { - case 'wallet': - case 'history': - case 'send': - case 'getcredits': - case 'invite': - case 'rewards': - case 'backup': - return { - wallet: __('Overview'), - getcredits: __('Get Credits'), - send: __('Send / Receive'), - rewards: __('Rewards'), - invite: __('Invites'), - history: __('History'), - }; - case 'downloaded': - case 'published': - return { - downloaded: __('Downloaded'), - published: __('Published'), - }; - case 'settings': - case 'help': - return { - settings: __('Settings'), - help: __('Help'), - }; - case 'discover': - case 'subscriptions': - return { - discover: __('Discover'), - subscriptions: __('Subscriptions'), - }; - default: - return null; - } -}); - -export const selectPageTitle = createSelector( - selectCurrentPage, - selectCurrentParams, - (page, params) => { - switch (page) { - case 'settings': - return __('Settings'); - case 'report': - return __('Report'); - case 'wallet': - return __('Wallet'); - case 'send': - return __('Send or Receive LBRY Credits'); - case 'getcredits': - return __('Get LBRY Credits'); - case 'backup': - return __('Backup Your Wallet'); - case 'rewards': - return __('Rewards'); - case 'invite': - return __('Invites'); - case 'start': - return __('Start'); - case 'publish': - return params.id ? __('Edit') : __('Publish'); - case 'help': - return __('Help'); - case 'developer': - return __('Developer'); - case 'show': { - const parts = [normalizeURI(params.uri)]; - // If the params has any keys other than "uri" - if (Object.keys(params).length > 1) { - parts.push(toQueryString(Object.assign({}, params, { uri: null }))); - } - return parts.join('?'); - } - case 'downloaded': - return __('Downloads & Purchases'); - case 'published': - return __('Publications'); - case 'search': - return params.query ? __('Search results for %s', params.query) : __('Search'); - case 'subscriptions': - return __('Your Subscriptions'); - case 'discover': - case false: - case null: - case '': - return ''; - default: - return page[0].toUpperCase() + (page.length > 0 ? page.substr(1) : ''); - } - } -); - export const selectPathAfterAuth = createSelector(selectState, state => state.pathAfterAuth); export const selectIsBackDisabled = createSelector(selectState, state => state.index === 0); @@ -128,6 +30,8 @@ export const selectIsForwardDisabled = createSelector( state => state.index === state.stack.length - 1 ); +export const selectIsHome = createSelector(selectCurrentPage, page => page === 'discover'); + export const selectHistoryIndex = createSelector(selectState, state => state.index); export const selectHistoryStack = createSelector(selectState, state => state.stack); @@ -137,3 +41,162 @@ export const selectActiveHistoryEntry = createSelector( selectState, state => state.stack[state.index] ); + +export const selectPageTitle = createSelector(selectCurrentPage, page => { + switch (page) { + default: + return ''; + } +}); + +export const selectNavLinks = createSelector( + selectCurrentPage, + selectHistoryStack, + (currentPage, historyStack) => { + const isWalletPage = page => + page === 'wallet' || + page === 'send' || + page === 'getcredits' || + page === 'rewards' || + page === 'history' || + page === 'invite'; + + const isMyLbryPage = page => + page === 'downloaded' || page === 'published' || page === 'settings'; + + const previousStack = historyStack.slice().reverse(); + + const getPreviousSubLinkPath = checkIfValidPage => { + for (let i = 0; i < previousStack.length; i += 1) { + const currentStackItem = previousStack[i]; + + // Trim off the "/" from the path + const pageInStack = currentStackItem.path.slice(1); + if (checkIfValidPage(pageInStack)) { + return currentStackItem.path; + } + } + + return undefined; + }; + + // Gets the last active sublink in a section + const getActiveSublink = category => { + if (category === 'wallet') { + const previousPath = getPreviousSubLinkPath(isWalletPage); + return previousPath || '/wallet'; + } else if (category === 'myLbry') { + const previousPath = getPreviousSubLinkPath(isMyLbryPage); + return previousPath || '/downloaded'; + } + + return undefined; + }; + + const isCurrentlyWalletPage = isWalletPage(currentPage); + const isCurrentlyMyLbryPage = isMyLbryPage(currentPage); + + const walletSubLinks = [ + { + label: 'Overview', + path: '/wallet', + active: currentPage === 'wallet', + }, + { + label: 'Send & Recieve', + path: '/send', + active: currentPage === 'send', + }, + { + label: 'Get Credits', + path: '/getcredits', + active: currentPage === 'getcredits', + }, + { + label: 'Rewards', + path: '/rewards', + active: currentPage === 'rewards', + }, + { + label: 'Invites', + path: '/invite', + active: currentPage === 'invite', + }, + { + label: 'Transactions', + path: '/history', + active: currentPage === 'history', + }, + ]; + + const myLbrySubLinks = [ + { + label: 'Downloads', + path: '/downloaded', + active: currentPage === 'downloaded', + }, + { + label: 'Publishes', + path: '/published', + active: currentPage === 'published', + }, + { + label: 'Settings', + path: '/settings', + active: currentPage === 'settings', + }, + { + label: 'Backup', + path: '/backup', + active: currentPage === 'backup', + }, + ]; + + const navLinks = { + primary: [ + { + label: 'Explore', + path: '/discover', + active: currentPage === 'discover', + icon: 'Compass', + }, + { + label: 'Subscriptions', + path: '/subscriptions', + active: currentPage === 'subscriptions', + icon: 'AtSign', + }, + ], + secondary: [ + { + label: 'Wallet', + icon: 'CreditCard', + subLinks: walletSubLinks, + path: isCurrentlyWalletPage ? '/wallet' : getActiveSublink('wallet'), + active: isWalletPage(currentPage), + }, + { + label: 'My LBRY', + icon: 'Settings', + subLinks: myLbrySubLinks, + path: isCurrentlyMyLbryPage ? '/downloaded' : getActiveSublink('myLbry'), + active: isMyLbryPage(currentPage), + }, + { + label: 'Publish', + icon: 'UploadCloud', + path: '/publish', + active: currentPage === 'publish', + }, + { + label: 'Help', + path: '/help', + active: currentPage === 'help', + icon: 'HelpCircle', + }, + ], + }; + + return navLinks; + } +); diff --git a/src/renderer/redux/selectors/publish.js b/src/renderer/redux/selectors/publish.js new file mode 100644 index 000000000..38d7c67cc --- /dev/null +++ b/src/renderer/redux/selectors/publish.js @@ -0,0 +1,26 @@ +import { createSelector } from 'reselect'; +import { parseURI } from 'lbryURI'; + +const selectState = state => state.publish || {}; + +export const selectPendingPublishes = createSelector(selectState, state => { + return state.pendingPublishes.map(pendingClaim => ({ ...pendingClaim, pending: true })) || []; +}); + +export const selectPublishFormValues = createSelector(selectState, state => { + const { pendingPublish, ...formValues } = state; + return formValues; +}); + +export const selectPendingPublish = uri => + createSelector(selectPendingPublishes, pendingPublishes => { + const { claimName, contentName } = parseURI(uri); + + if (!pendingPublishes.length) { + return null; + } + + return pendingPublishes.filter( + publish => publish.name === claimName || publish.name === contentName + )[0]; + }); diff --git a/src/renderer/redux/selectors/search.js b/src/renderer/redux/selectors/search.js index 4e49f47d8..c7e983ced 100644 --- a/src/renderer/redux/selectors/search.js +++ b/src/renderer/redux/selectors/search.js @@ -1,12 +1,12 @@ -import { - selectCurrentPage, - selectCurrentParams, - selectPageTitle, -} from 'redux/selectors/navigation'; +import { selectCurrentPage, selectCurrentParams } from 'redux/selectors/navigation'; import { createSelector } from 'reselect'; export const selectState = state => state.search || {}; +export const selectSearchValue = createSelector(selectState, state => { + return state.searchQuery; +}); + export const selectSearchQuery = createSelector( selectCurrentPage, selectCurrentParams, @@ -26,54 +26,14 @@ export const makeSelectSearchUris = query => export const selectWunderBarAddress = createSelector( selectCurrentPage, - selectPageTitle, selectSearchQuery, - (page, title, query) => (page !== 'search' ? title : query || title) -); - -export const selectWunderBarIcon = createSelector( - selectCurrentPage, selectCurrentParams, - (page, params) => { - switch (page) { - case 'auth': - return 'icon-user'; - case 'settings': - return 'icon-gear'; - case 'help': - return 'icon-question'; - case 'report': - return 'icon-file'; - case 'downloaded': - return 'icon-folder'; - case 'published': - return 'icon-folder'; - case 'history': - return 'icon-history'; - case 'send': - return 'icon-send'; - case 'rewards': - return 'icon-rocket'; - case 'invite': - return 'icon-envelope-open'; - case 'getcredits': - return 'icon-shopping-cart'; - case 'wallet': - case 'backup': - return 'icon-bank'; - case 'show': - return 'icon-file'; - case 'publish': - return params.id ? __('icon-pencil') : __('icon-upload'); - case 'developer': - return 'icon-code'; - case 'discover': - case 'search': - return 'icon-search'; - case 'subscriptions': - return 'icon-th-list'; - default: - return 'icon-file'; + (page, query, params) => { + // only populate the wunderbar address if we are on the file/channel pages + // or show the search query + if (page === 'show') { + return params.uri; } + return query; } ); diff --git a/src/renderer/redux/selectors/video.js b/src/renderer/redux/selectors/video.js deleted file mode 100644 index 62534be6b..000000000 --- a/src/renderer/redux/selectors/video.js +++ /dev/null @@ -1,6 +0,0 @@ -import { createSelector } from 'reselect'; - -const selectState = state => state.video || {}; - -// eslint-disable-next-line import/prefer-default-export -export const selectVideoPause = createSelector(selectState, state => state.videoPause); diff --git a/src/renderer/redux/selectors/wallet.js b/src/renderer/redux/selectors/wallet.js index bddedb8f4..d5fd9326d 100644 --- a/src/renderer/redux/selectors/wallet.js +++ b/src/renderer/redux/selectors/wallet.js @@ -96,26 +96,6 @@ export const selectGettingNewAddress = createSelector( state => state.gettingNewAddress ); -export const selectDraftTransaction = createSelector( - selectState, - state => state.draftTransaction || {} -); - -export const selectDraftTransactionAmount = createSelector( - selectDraftTransaction, - draft => draft.amount -); - -export const selectDraftTransactionAddress = createSelector( - selectDraftTransaction, - draft => draft.address -); - -export const selectDraftTransactionError = createSelector( - selectDraftTransaction, - draft => draft.error -); - export const selectBlocks = createSelector(selectState, state => state.blocks); export const makeSelectBlockDate = block => diff --git a/src/renderer/scss/_gui.scss b/src/renderer/scss/_gui.scss index 92b82d146..3a6bdae70 100644 --- a/src/renderer/scss/_gui.scss +++ b/src/renderer/scss/_gui.scss @@ -1,14 +1,235 @@ -@import url(https://fonts.googleapis.com/css?family=Roboto:400,400i,500,500i,700); +// Generic html styles used accross the App +// component specific styling should go in the component scss file + +@font-face { + font-family: 'metropolis-bold'; + src: url('../../../static/font/metropolis/bold.eot'); + src: url('../../../static/font/metropolis/bold.eot?#iefix') format('embedded-opentype'), + url('../../../static/font/metropolis/bold.woff') format('woff'), + url('../../../static/font/metropolis/bold.ttf') format('truetype'), + url('../../../static/font/metropolis/bold.svg#metropolis-bold') format('svg'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'metropolis-semibold'; + src: url('../../../static/font/metropolis/semibold.eot'); + src: url('../../../static/font/metropolis/semibold.eot?#iefix') format('embedded-opentype'), + url('../../../static/font/metropolis/semibold.woff') format('woff'), + url('../../../static/font/metropolis/semibold.ttf') format('truetype'), + url('../../../static/font/metropolis/semibold.svg#metropolis-semibold') format('svg'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'metropolis-medium'; + src: url('../../../static/font/metropolis/medium.eot'); + src: url('../../../static/font/metropolis/medium.eot?#iefix') format('embedded-opentype'), + url('../../../static/font/metropolis/medium.woff') format('woff'), + url('../../../static/font/metropolis/medium.ttf') format('truetype'), + url('../../../static/font/metropolis/medium.svg#metropolis-medium') format('svg'); + font-weight: normal; + font-style: normal; +} html { height: 100%; - font-size: var(--font-size); } body { + font-family: 'metropolis-semibold'; + font-weight: 400; + font-size: 16px; + line-height: 1.5; color: var(--text-color); - font-family: 'Roboto', sans-serif; - line-height: var(--font-line-height); + height: 100%; + overflow: hidden; +} + +* { + box-sizing: border-box; +} + +code { + font: 1.5em Consolas, 'Lucida Console', 'Source Sans', monospace; + background-color: var(--color-bg-alt); +} + +// Without this buttons don't have the Metropolis font +button { + font-weight: inherit; + font-family: inherit; +} + +ul { + list-style-type: none; + padding: 0; +} + +input { + border-bottom: var(--input-border-size) dotted var(--input-border-color); + color: var(--input-color); + line-height: 1; + cursor: text; + background-color: transparent; + font-family: 'metropolis-medium'; + + &[type='radio'], + &[type='checkbox'], + &[type='file'], + &[type='select'] { + cursor: pointer; + } + + &[type='file'] { + border-bottom: none; + } + + &.input-copyable { + flex: 1; + background: var(--input-copyable-bg); + color: var(--input-copyable-color); + padding: 10px 16px; + border: 1px dashed var(--input-copyable-border); + } + + &:not(.input-copyable):not(.wunderbar__input):not(:placeholder-shown) { + border-bottom: var(--input-border-size) solid var(--input-border-color); + } + + &:disabled { + color: var(--input-disabled-color); + border-bottom: var(--input-border-size) solid var(--input-disabled-color); + cursor: default; + } +} + +input::placeholder { + opacity: 0.5; +} + +button + input { + margin-left: $spacing-vertical * 2/3; +} + +dl { + width: 100%; + overflow: hidden; + padding: 0; + margin: 0; + overflow-x: scroll; +} + +dt { + float: left; + width: 20%; + padding: 0; + margin: 0; +} + +dd { + float: left; + width: 80%; + padding: 0; + margin: 0; +} + +p { + font-family: 'metropolis-medium'; +} + +.page { + display: grid; + grid-template-rows: var(--header-height) calc(100vh - var(--header-height)); + grid-template-columns: var(--side-nav-width) auto; + grid-template-areas: + 'nav header' + 'nav content'; + background-color: var(--color-bg); + + @media only screen and (min-width: $medium-breakpoint) { + grid-template-columns: var(--side-nav-width-m) auto; + } + + @media only screen and (min-width: $large-breakpoint) { + grid-template-columns: var(--side-nav-width-l) auto; + } +} + +/* + Page content +*/ +.content { + grid-area: content; + overflow: auto; +} + +.main { + padding: $spacing-width $spacing-width; + margin: auto; +} + +.main--contained { + max-width: 1000px; + margin: 0 80px - $spacing-width; + + @media only screen and (min-width: $medium-breakpoint) { + margin: 0 120px - $spacing-width; + } + + @media only screen and (min-width: $large-breakpoint) { + margin: 0 200px - $spacing-width; + } +} + +.main--no-padding { + padding-left: 0; + padding-right: 0; + margin: 0; +} + +.main--extra-padding { + padding-left: 100px; + padding-right: 100px; +} + +.page__header { + padding: $spacing-vertical * 2/3; + padding-bottom: 0; +} + +.page__title { + // not currently using page titles on any page +} + +.page__empty { + margin-top: 200px; + text-align: center; + font-family: 'metropolis-medium'; +} + +.columns { + display: flex; + justify-content: space-between; + + > * { + flex-grow: 1; + flex-basis: 0; + + &:not(:first-of-type):not(:last-of-type) { + margin: 0 $spacing-vertical / 3; + } + + &:first-of-type { + margin-right: $spacing-vertical / 3; + } + + &:last-of-type { + margin-left: $spacing-vertical / 3; + } + } } /* Custom text selection */ @@ -17,111 +238,47 @@ body { color: var(--text-selection-color); } -#window { - min-height: 100vh; - background: var(--window-bg); +.credit-amount { + padding: 5px; + border-radius: 5px; + font-family: 'metropolis-bold'; + font-size: 8px; } -.credit-amount--indicator { - font-weight: 500; - color: var(--color-money); -} -.credit-amount--fee { - font-size: 0.9em; - color: var(--color-meta-light); +.credit-amount--large { + font-family: 'metropolis-semibold'; + font-size: 36px; } -.credit-amount--bold { - font-weight: 700; +.credit-amount--free { + color: var(--color-dark-blue); + background-color: var(--color-secondary); } -#main-content { - margin: auto; - display: flex; - flex-direction: column; - overflow: overlay; - padding: $spacing-vertical; - position: absolute; - top: var(--header-height); - bottom: 0; - left: 4px; - right: 4px; - main { - margin-left: auto; - margin-right: auto; - max-width: 100%; - } - main.main--single-column { - width: $width-page-constrained; - } - - main.main--no-margin { - margin-left: 0; - margin-right: 0; - } +.credit-amount--cost { + color: var(--color-black); + background-color: var(--color-yellow); } -.reloading { - &:before { - $width: 30px; - position: absolute; - background: url('../../../static/img/busy.gif') no-repeat center center; - width: $width; - height: $spacing-vertical; - content: ''; - left: 50%; - margin-left: -1 / 2 * $width; - display: inline-block; - } +.credit-amount--plain { + background-color: inherit; + color: inherit; + font-weight: inherit; + font-size: inherit; } -.icon-fixed-width { - /* This borrowed is from a component of Font Awesome we're not using, maybe add it? */ - width: (18em / 14); - text-align: center; +.credit-amount.credit-amount--no-style { + padding: 0; + font-size: inherit; + font-weight: inherit; + color: inherit; + background-color: transparent; + font-family: 'metropolis-medium'; } -.icon--left-pad { - padding-left: 3px; -} - -h2 { - font-size: 1.75em; -} - -h3 { - font-size: 1.4em; -} - -h4 { - font-size: 1.2em; -} - -h5 { - font-size: 1.1em; -} -sup, -sub { - vertical-align: baseline; - position: relative; -} -sup { - top: -0.4em; -} -sub { - top: 0.4em; -} - -code { - font: 0.8em Consolas, 'Lucida Console', 'Source Sans', monospace; - background-color: var(--color-bg-alt); -} - -p { - margin-bottom: 0.8em; - &:last-child { - margin-bottom: 0; - } +.divider__horizontal { + border-top: var(--color-divider); + margin: 16px 0; } .hidden { @@ -141,6 +298,10 @@ p { } .busy-indicator { + font-family: 'metropolis-medium'; +} + +.busy-indicator__loader { background: url('../../../static/img/busy.gif') no-repeat center center; display: inline-block; margin: -1em 0; @@ -157,12 +318,18 @@ p { } .help { - font-size: 0.85em; + font-size: 12px; + font-family: 'metropolis-medium'; color: var(--color-help); } +.help--padded { + padding-top: $spacing-vertical * 2/3; +} + .meta { - font-size: 0.9em; + font-family: 'metropolis-medium'; + font-size: 14px; color: var(--color-meta-light); } @@ -170,29 +337,3 @@ p { color: var(--color-meta-light); font-style: italic; } - -/*should be redone/moved*/ -.file-list__header { - .busy-indicator { - float: left; - margin-top: 12px; - } -} - -.sort-section { - display: block; - margin-bottom: $spacing-vertical * 2/3; - - text-align: right; - line-height: 1; - font-size: 0.85em; - color: var(--color-help); -} - -section.section-spaced { - margin-bottom: $spacing-vertical; -} - -.text-center { - text-align: center; -} diff --git a/src/renderer/scss/_icons.scss b/src/renderer/scss/_icons.scss deleted file mode 100644 index c589a1ac5..000000000 --- a/src/renderer/scss/_icons.scss +++ /dev/null @@ -1,1678 +0,0 @@ -@font-face { - font-family: 'FontAwesome'; - src: url('../../../static/font/fontawesome-webfont.eot?v=4.7.0'); - src: url('../../../static/font/fontawesome-webfont.eot?#iefix&v=4.7.0') - format('embedded-opentype'), - url('../../../static/font/fontawesome-webfont.woff2?v=4.7.0') format('woff2'), - url('../../../static/font/fontawesome-webfont.woff?v=4.7.0') format('woff'), - url('../../../static/font/fontawesome-webfont.ttf?v=4.7.0') format('truetype'), - url('../../../static/font/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg'); - font-weight: normal; - font-style: normal; -} - -[class*='icon-'] { - display: inline-block; - text-align: center; - font-family: 'FontAwesome'; - font-style: normal; - font-weight: normal; - font-variant: normal; - text-transform: none; - speak: none; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - font-size: inherit; - text-rendering: auto; - transform: translate(0, 0); -} - -/* Adjustments for icon size and alignment */ -.icon-rocket { - color: orangered; - font-size: 0.95em; - position: relative; - top: -0.04em; - margin-left: 0.025em; - margin-right: 0.025em; -} - -/* 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 { - content: '\f000'; -} -.icon-music:before { - content: '\f001'; -} -.icon-search:before { - content: '\f002'; -} -.icon-envelope-o:before { - content: '\f003'; -} -.icon-heart:before { - content: '\f004'; -} -.icon-star:before { - content: '\f005'; -} -.icon-star-o:before { - content: '\f006'; -} -.icon-user:before { - content: '\f007'; -} -.icon-film:before { - content: '\f008'; -} -.icon-th-large:before { - content: '\f009'; -} -.icon-th:before { - content: '\f00a'; -} -.icon-th-list:before { - content: '\f00b'; -} -.icon-check:before { - content: '\f00c'; -} -.icon-remove:before, -.icon-close:before, -.icon-times:before { - content: '\f00d'; -} -.icon-search-plus:before { - content: '\f00e'; -} -.icon-search-minus:before { - content: '\f010'; -} -.icon-power-off:before { - content: '\f011'; -} -.icon-signal:before { - content: '\f012'; -} -.icon-gear:before, -.icon-cog:before { - content: '\f013'; -} -.icon-trash-o:before { - content: '\f014'; -} -.icon-home:before { - content: '\f015'; -} -.icon-file-o:before { - content: '\f016'; -} -.icon-clock-o:before { - content: '\f017'; -} -.icon-road:before { - content: '\f018'; -} -.icon-download:before { - content: '\f019'; -} -.icon-arrow-circle-o-down:before { - content: '\f01a'; -} -.icon-arrow-circle-o-up:before { - content: '\f01b'; -} -.icon-inbox:before { - content: '\f01c'; -} -.icon-play-circle-o:before { - content: '\f01d'; -} -.icon-rotate-right:before, -.icon-repeat:before { - content: '\f01e'; -} -.icon-refresh:before { - content: '\f021'; -} -.icon-list-alt:before { - content: '\f022'; -} -.icon-lock:before { - content: '\f023'; -} -.icon-flag:before { - content: '\f024'; -} -.icon-headphones:before { - content: '\f025'; -} -.icon-volume-off:before { - content: '\f026'; -} -.icon-volume-down:before { - content: '\f027'; -} -.icon-volume-up:before { - content: '\f028'; -} -.icon-qrcode:before { - content: '\f029'; -} -.icon-barcode:before { - content: '\f02a'; -} -.icon-tag:before { - content: '\f02b'; -} -.icon-tags:before { - content: '\f02c'; -} -.icon-book:before { - content: '\f02d'; -} -.icon-bookmark:before { - content: '\f02e'; -} -.icon-print:before { - content: '\f02f'; -} -.icon-camera:before { - content: '\f030'; -} -.icon-font:before { - content: '\f031'; -} -.icon-bold:before { - content: '\f032'; -} -.icon-italic:before { - content: '\f033'; -} -.icon-text-height:before { - content: '\f034'; -} -.icon-text-width:before { - content: '\f035'; -} -.icon-align-left:before { - content: '\f036'; -} -.icon-align-center:before { - content: '\f037'; -} -.icon-align-right:before { - content: '\f038'; -} -.icon-align-justify:before { - content: '\f039'; -} -.icon-list:before { - content: '\f03a'; -} -.icon-dedent:before, -.icon-outdent:before { - content: '\f03b'; -} -.icon-indent:before { - content: '\f03c'; -} -.icon-video-camera:before { - content: '\f03d'; -} -.icon-photo:before, -.icon-image:before, -.icon-picture-o:before { - content: '\f03e'; -} -.icon-pencil:before { - content: '\f040'; -} -.icon-map-marker:before { - content: '\f041'; -} -.icon-adjust:before { - content: '\f042'; -} -.icon-tint:before { - content: '\f043'; -} -.icon-edit:before, -.icon-pencil-square-o:before { - content: '\f044'; -} -.icon-share-square-o:before { - content: '\f045'; -} -.icon-check-square-o:before { - content: '\f046'; -} -.icon-arrows:before { - content: '\f047'; -} -.icon-step-backward:before { - content: '\f048'; -} -.icon-fast-backward:before { - content: '\f049'; -} -.icon-backward:before { - content: '\f04a'; -} -.icon-play:before { - content: '\f04b'; -} -.icon-pause:before { - content: '\f04c'; -} -.icon-stop:before { - content: '\f04d'; -} -.icon-forward:before { - content: '\f04e'; -} -.icon-fast-forward:before { - content: '\f050'; -} -.icon-step-forward:before { - content: '\f051'; -} -.icon-eject:before { - content: '\f052'; -} -.icon-chevron-left:before { - content: '\f053'; -} -.icon-chevron-right:before { - content: '\f054'; -} -.icon-plus-circle:before { - content: '\f055'; -} -.icon-minus-circle:before { - content: '\f056'; -} -.icon-times-circle:before { - content: '\f057'; -} -.icon-check-circle:before { - content: '\f058'; -} -.icon-question-circle:before { - content: '\f059'; -} -.icon-info-circle:before { - content: '\f05a'; -} -.icon-crosshairs:before { - content: '\f05b'; -} -.icon-times-circle-o:before { - content: '\f05c'; -} -.icon-check-circle-o:before { - content: '\f05d'; -} -.icon-ban:before { - content: '\f05e'; -} -.icon-arrow-left:before { - content: '\f060'; -} -.icon-arrow-right:before { - content: '\f061'; -} -.icon-arrow-up:before { - content: '\f062'; -} -.icon-arrow-down:before { - content: '\f063'; -} -.icon-mail-forward:before, -.icon-share:before { - content: '\f064'; -} -.icon-expand:before { - content: '\f065'; -} -.icon-compress:before { - content: '\f066'; -} -.icon-plus:before { - content: '\f067'; -} -.icon-minus:before { - content: '\f068'; -} -.icon-asterisk:before { - content: '\f069'; -} -.icon-exclamation-circle:before { - content: '\f06a'; -} -.icon-gift:before { - content: '\f06b'; -} -.icon-leaf:before { - content: '\f06c'; -} -.icon-fire:before { - content: '\f06d'; -} -.icon-eye:before { - content: '\f06e'; -} -.icon-eye-slash:before { - content: '\f070'; -} -.icon-warning:before, -.icon-exclamation-triangle:before { - content: '\f071'; -} -.icon-plane:before { - content: '\f072'; -} -.icon-calendar:before { - content: '\f073'; -} -.icon-random:before { - content: '\f074'; -} -.icon-comment:before { - content: '\f075'; -} -.icon-magnet:before { - content: '\f076'; -} -.icon-chevron-up:before { - content: '\f077'; -} -.icon-chevron-down:before { - content: '\f078'; -} -.icon-retweet:before { - content: '\f079'; -} -.icon-shopping-cart:before { - content: '\f07a'; -} -.icon-folder:before { - content: '\f07b'; -} -.icon-folder-open:before { - content: '\f07c'; -} -.icon-arrows-v:before { - content: '\f07d'; -} -.icon-arrows-h:before { - content: '\f07e'; -} -.icon-bar-chart-o:before, -.icon-bar-chart:before { - content: '\f080'; -} -.icon-twitter-square:before { - content: '\f081'; -} -.icon-facebook-square:before { - content: '\f082'; -} -.icon-camera-retro:before { - content: '\f083'; -} -.icon-key:before { - content: '\f084'; -} -.icon-gears:before, -.icon-cogs:before { - content: '\f085'; -} -.icon-comments:before { - content: '\f086'; -} -.icon-thumbs-o-up:before { - content: '\f087'; -} -.icon-thumbs-o-down:before { - content: '\f088'; -} -.icon-star-half:before { - content: '\f089'; -} -.icon-heart-o:before { - content: '\f08a'; -} -.icon-sign-out:before { - content: '\f08b'; -} -.icon-linkedin-square:before { - content: '\f08c'; -} -.icon-thumb-tack:before { - content: '\f08d'; -} -.icon-external-link:before { - content: '\f08e'; -} -.icon-sign-in:before { - content: '\f090'; -} -.icon-trophy:before { - content: '\f091'; -} -.icon-github-square:before { - content: '\f092'; -} -.icon-upload:before { - content: '\f093'; -} -.icon-lemon-o:before { - content: '\f094'; -} -.icon-phone:before { - content: '\f095'; -} -.icon-square-o:before { - content: '\f096'; -} -.icon-bookmark-o:before { - content: '\f097'; -} -.icon-phone-square:before { - content: '\f098'; -} -.icon-twitter:before { - content: '\f099'; -} -.icon-facebook-f:before, -.icon-facebook:before { - content: '\f09a'; -} -.icon-github:before { - content: '\f09b'; -} -.icon-unlock:before { - content: '\f09c'; -} -.icon-credit-card:before { - content: '\f09d'; -} -.icon-rss:before { - content: '\f09e'; -} -.icon-hdd-o:before { - content: '\f0a0'; -} -.icon-bullhorn:before { - content: '\f0a1'; -} -.icon-bell:before { - content: '\f0f3'; -} -.icon-certificate:before { - content: '\f0a3'; -} -.icon-hand-o-right:before { - content: '\f0a4'; -} -.icon-hand-o-left:before { - content: '\f0a5'; -} -.icon-hand-o-up:before { - content: '\f0a6'; -} -.icon-hand-o-down:before { - content: '\f0a7'; -} -.icon-arrow-circle-left:before { - content: '\f0a8'; -} -.icon-arrow-circle-right:before { - content: '\f0a9'; -} -.icon-arrow-circle-up:before { - content: '\f0aa'; -} -.icon-arrow-circle-down:before { - content: '\f0ab'; -} -.icon-globe:before { - content: '\f0ac'; -} -.icon-wrench:before { - content: '\f0ad'; -} -.icon-tasks:before { - content: '\f0ae'; -} -.icon-filter:before { - content: '\f0b0'; -} -.icon-briefcase:before { - content: '\f0b1'; -} -.icon-arrows-alt:before { - content: '\f0b2'; -} -.icon-group:before, -.icon-users:before { - content: '\f0c0'; -} -.icon-chain:before, -.icon-link:before { - content: '\f0c1'; -} -.icon-cloud:before { - content: '\f0c2'; -} -.icon-flask:before { - content: '\f0c3'; -} -.icon-cut:before, -.icon-scissors:before { - content: '\f0c4'; -} -.icon-copy:before, -.icon-files-o:before { - content: '\f0c5'; -} -.icon-paperclip:before { - content: '\f0c6'; -} -.icon-save:before, -.icon-floppy-o:before { - content: '\f0c7'; -} -.icon-square:before { - content: '\f0c8'; -} -.icon-navicon:before, -.icon-reorder:before, -.icon-bars:before { - content: '\f0c9'; -} -.icon-list-ul:before { - content: '\f0ca'; -} -.icon-list-ol:before { - content: '\f0cb'; -} -.icon-strikethrough:before { - content: '\f0cc'; -} -.icon-underline:before { - content: '\f0cd'; -} -.icon-table:before { - content: '\f0ce'; -} -.icon-magic:before { - content: '\f0d0'; -} -.icon-truck:before { - content: '\f0d1'; -} -.icon-pinterest:before { - content: '\f0d2'; -} -.icon-pinterest-square:before { - content: '\f0d3'; -} -.icon-google-plus-square:before { - content: '\f0d4'; -} -.icon-google-plus:before { - content: '\f0d5'; -} -.icon-money:before { - content: '\f0d6'; -} -.icon-caret-down:before { - content: '\f0d7'; -} -.icon-caret-up:before { - content: '\f0d8'; -} -.icon-caret-left:before { - content: '\f0d9'; -} -.icon-caret-right:before { - content: '\f0da'; -} -.icon-columns:before { - content: '\f0db'; -} -.icon-unsorted:before, -.icon-sort:before { - content: '\f0dc'; -} -.icon-sort-down:before, -.icon-sort-desc:before { - content: '\f0dd'; -} -.icon-sort-up:before, -.icon-sort-asc:before { - content: '\f0de'; -} -.icon-envelope:before { - content: '\f0e0'; -} -.icon-linkedin:before { - content: '\f0e1'; -} -.icon-rotate-left:before, -.icon-undo:before { - content: '\f0e2'; -} -.icon-legal:before, -.icon-gavel:before { - content: '\f0e3'; -} -.icon-dashboard:before, -.icon-tachometer:before { - content: '\f0e4'; -} -.icon-comment-o:before { - content: '\f0e5'; -} -.icon-comments-o:before { - content: '\f0e6'; -} -.icon-flash:before, -.icon-bolt:before { - content: '\f0e7'; -} -.icon-sitemap:before { - content: '\f0e8'; -} -.icon-umbrella:before { - content: '\f0e9'; -} -.icon-paste:before, -.icon-clipboard:before { - content: '\f0ea'; -} -.icon-lightbulb-o:before { - content: '\f0eb'; -} -.icon-exchange:before { - content: '\f0ec'; -} -.icon-cloud-download:before { - content: '\f0ed'; -} -.icon-cloud-upload:before { - content: '\f0ee'; -} -.icon-user-md:before { - content: '\f0f0'; -} -.icon-stethoscope:before { - content: '\f0f1'; -} -.icon-suitcase:before { - content: '\f0f2'; -} -.icon-bell-o:before { - content: '\f0a2'; -} -.icon-coffee:before { - content: '\f0f4'; -} -.icon-cutlery:before { - content: '\f0f5'; -} -.icon-file-text-o:before { - content: '\f0f6'; -} -.icon-building-o:before { - content: '\f0f7'; -} -.icon-hospital-o:before { - content: '\f0f8'; -} -.icon-ambulance:before { - content: '\f0f9'; -} -.icon-medkit:before { - content: '\f0fa'; -} -.icon-fighter-jet:before { - content: '\f0fb'; -} -.icon-beer:before { - content: '\f0fc'; -} -.icon-h-square:before { - content: '\f0fd'; -} -.icon-plus-square:before { - content: '\f0fe'; -} -.icon-angle-double-left:before { - content: '\f100'; -} -.icon-angle-double-right:before { - content: '\f101'; -} -.icon-angle-double-up:before { - content: '\f102'; -} -.icon-angle-double-down:before { - content: '\f103'; -} -.icon-angle-left:before { - content: '\f104'; -} -.icon-angle-right:before { - content: '\f105'; -} -.icon-angle-up:before { - content: '\f106'; -} -.icon-angle-down:before { - content: '\f107'; -} -.icon-desktop:before { - content: '\f108'; -} -.icon-laptop:before { - content: '\f109'; -} -.icon-tablet:before { - content: '\f10a'; -} -.icon-mobile-phone:before, -.icon-mobile:before { - content: '\f10b'; -} -.icon-circle-o:before { - content: '\f10c'; -} -.icon-quote-left:before { - content: '\f10d'; -} -.icon-quote-right:before { - content: '\f10e'; -} -.icon-spinner:before { - content: '\f110'; -} -.icon-circle:before { - content: '\f111'; -} -.icon-mail-reply:before, -.icon-reply:before { - content: '\f112'; -} -.icon-github-alt:before { - content: '\f113'; -} -.icon-folder-o:before { - content: '\f114'; -} -.icon-folder-open-o:before { - content: '\f115'; -} -.icon-smile-o:before { - content: '\f118'; -} -.icon-frown-o:before { - content: '\f119'; -} -.icon-meh-o:before { - content: '\f11a'; -} -.icon-gamepad:before { - content: '\f11b'; -} -.icon-keyboard-o:before { - content: '\f11c'; -} -.icon-flag-o:before { - content: '\f11d'; -} -.icon-flag-checkered:before { - content: '\f11e'; -} -.icon-terminal:before { - content: '\f120'; -} -.icon-code:before { - content: '\f121'; -} -.icon-mail-reply-all:before, -.icon-reply-all:before { - content: '\f122'; -} -.icon-star-half-empty:before, -.icon-star-half-full:before, -.icon-star-half-o:before { - content: '\f123'; -} -.icon-location-arrow:before { - content: '\f124'; -} -.icon-crop:before { - content: '\f125'; -} -.icon-code-fork:before { - content: '\f126'; -} -.icon-unlink:before, -.icon-chain-broken:before { - content: '\f127'; -} -.icon-question:before { - content: '\f128'; -} -.icon-info:before { - content: '\f129'; -} -.icon-exclamation:before { - content: '\f12a'; -} -.icon-superscript:before { - content: '\f12b'; -} -.icon-subscript:before { - content: '\f12c'; -} -.icon-eraser:before { - content: '\f12d'; -} -.icon-puzzle-piece:before { - content: '\f12e'; -} -.icon-microphone:before { - content: '\f130'; -} -.icon-microphone-slash:before { - content: '\f131'; -} -.icon-shield:before { - content: '\f132'; -} -.icon-calendar-o:before { - content: '\f133'; -} -.icon-fire-extinguisher:before { - content: '\f134'; -} -.icon-rocket:before { - content: '\f135'; -} -.icon-maxcdn:before { - content: '\f136'; -} -.icon-chevron-circle-left:before { - content: '\f137'; -} -.icon-chevron-circle-right:before { - content: '\f138'; -} -.icon-chevron-circle-up:before { - content: '\f139'; -} -.icon-chevron-circle-down:before { - content: '\f13a'; -} -.icon-html5:before { - content: '\f13b'; -} -.icon-css3:before { - content: '\f13c'; -} -.icon-anchor:before { - content: '\f13d'; -} -.icon-unlock-alt:before { - content: '\f13e'; -} -.icon-bullseye:before { - content: '\f140'; -} -.icon-ellipsis-h:before { - content: '\f141'; -} -.icon-ellipsis-v:before { - content: '\f142'; -} -.icon-rss-square:before { - content: '\f143'; -} -.icon-play-circle:before { - content: '\f144'; -} -.icon-ticket:before { - content: '\f145'; -} -.icon-minus-square:before { - content: '\f146'; -} -.icon-minus-square-o:before { - content: '\f147'; -} -.icon-level-up:before { - content: '\f148'; -} -.icon-level-down:before { - content: '\f149'; -} -.icon-check-square:before { - content: '\f14a'; -} -.icon-pencil-square:before { - content: '\f14b'; -} -.icon-external-link-square:before { - content: '\f14c'; -} -.icon-share-square:before { - content: '\f14d'; -} -.icon-compass:before { - content: '\f14e'; -} -.icon-toggle-down:before, -.icon-caret-square-o-down:before { - content: '\f150'; -} -.icon-toggle-up:before, -.icon-caret-square-o-up:before { - content: '\f151'; -} -.icon-toggle-right:before, -.icon-caret-square-o-right:before { - content: '\f152'; -} -.icon-euro:before, -.icon-eur:before { - content: '\f153'; -} -.icon-gbp:before { - content: '\f154'; -} -.icon-dollar:before, -.icon-usd:before { - content: '\f155'; -} -.icon-rupee:before, -.icon-inr:before { - content: '\f156'; -} -.icon-cny:before, -.icon-rmb:before, -.icon-yen:before, -.icon-jpy:before { - content: '\f157'; -} -.icon-ruble:before, -.icon-rouble:before, -.icon-rub:before { - content: '\f158'; -} -.icon-won:before, -.icon-krw:before { - content: '\f159'; -} -.icon-bitcoin:before, -.icon-btc:before { - content: '\f15a'; -} -.icon-file:before { - content: '\f15b'; -} -.icon-file-text:before { - content: '\f15c'; -} -.icon-sort-alpha-asc:before { - content: '\f15d'; -} -.icon-sort-alpha-desc:before { - content: '\f15e'; -} -.icon-sort-amount-asc:before { - content: '\f160'; -} -.icon-sort-amount-desc:before { - content: '\f161'; -} -.icon-sort-numeric-asc:before { - content: '\f162'; -} -.icon-sort-numeric-desc:before { - content: '\f163'; -} -.icon-thumbs-up:before { - content: '\f164'; -} -.icon-thumbs-down:before { - content: '\f165'; -} -.icon-youtube-square:before { - content: '\f166'; -} -.icon-youtube:before { - content: '\f167'; -} -.icon-xing:before { - content: '\f168'; -} -.icon-xing-square:before { - content: '\f169'; -} -.icon-youtube-play:before { - content: '\f16a'; -} -.icon-dropbox:before { - content: '\f16b'; -} -.icon-stack-overflow:before { - content: '\f16c'; -} -.icon-instagram:before { - content: '\f16d'; -} -.icon-flickr:before { - content: '\f16e'; -} -.icon-adn:before { - content: '\f170'; -} -.icon-bitbucket:before { - content: '\f171'; -} -.icon-bitbucket-square:before { - content: '\f172'; -} -.icon-tumblr:before { - content: '\f173'; -} -.icon-tumblr-square:before { - content: '\f174'; -} -.icon-long-arrow-down:before { - content: '\f175'; -} -.icon-long-arrow-up:before { - content: '\f176'; -} -.icon-long-arrow-left:before { - content: '\f177'; -} -.icon-long-arrow-right:before { - content: '\f178'; -} -.icon-apple:before { - content: '\f179'; -} -.icon-windows:before { - content: '\f17a'; -} -.icon-android:before { - content: '\f17b'; -} -.icon-linux:before { - content: '\f17c'; -} -.icon-dribbble:before { - content: '\f17d'; -} -.icon-skype:before { - content: '\f17e'; -} -.icon-foursquare:before { - content: '\f180'; -} -.icon-trello:before { - content: '\f181'; -} -.icon-female:before { - content: '\f182'; -} -.icon-male:before { - content: '\f183'; -} -.icon-gittip:before, -.icon-gratipay:before { - content: '\f184'; -} -.icon-sun-o:before { - content: '\f185'; -} -.icon-moon-o:before { - content: '\f186'; -} -.icon-archive:before { - content: '\f187'; -} -.icon-bug:before { - content: '\f188'; -} -.icon-vk:before { - content: '\f189'; -} -.icon-weibo:before { - content: '\f18a'; -} -.icon-renren:before { - content: '\f18b'; -} -.icon-pagelines:before { - content: '\f18c'; -} -.icon-stack-exchange:before { - content: '\f18d'; -} -.icon-arrow-circle-o-right:before { - content: '\f18e'; -} -.icon-arrow-circle-o-left:before { - content: '\f190'; -} -.icon-toggle-left:before, -.icon-caret-square-o-left:before { - content: '\f191'; -} -.icon-dot-circle-o:before { - content: '\f192'; -} -.icon-wheelchair:before { - content: '\f193'; -} -.icon-vimeo-square:before { - content: '\f194'; -} -.icon-turkish-lira:before, -.icon-try:before { - content: '\f195'; -} -.icon-plus-square-o:before { - content: '\f196'; -} -.icon-space-shuttle:before { - content: '\f197'; -} -.icon-slack:before { - content: '\f198'; -} -.icon-envelope-square:before { - content: '\f199'; -} -.icon-wordpress:before { - content: '\f19a'; -} -.icon-openid:before { - content: '\f19b'; -} -.icon-institution:before, -.icon-bank:before, -.icon-university:before { - content: '\f19c'; -} -.icon-mortar-board:before, -.icon-graduation-cap:before { - content: '\f19d'; -} -.icon-yahoo:before { - content: '\f19e'; -} -.icon-google:before { - content: '\f1a0'; -} -.icon-reddit:before { - content: '\f1a1'; -} -.icon-reddit-square:before { - content: '\f1a2'; -} -.icon-stumbleupon-circle:before { - content: '\f1a3'; -} -.icon-stumbleupon:before { - content: '\f1a4'; -} -.icon-delicious:before { - content: '\f1a5'; -} -.icon-digg:before { - content: '\f1a6'; -} -.icon-pied-piper:before { - content: '\f1a7'; -} -.icon-pied-piper-alt:before { - content: '\f1a8'; -} -.icon-drupal:before { - content: '\f1a9'; -} -.icon-joomla:before { - content: '\f1aa'; -} -.icon-language:before { - content: '\f1ab'; -} -.icon-fax:before { - content: '\f1ac'; -} -.icon-building:before { - content: '\f1ad'; -} -.icon-child:before { - content: '\f1ae'; -} -.icon-paw:before { - content: '\f1b0'; -} -.icon-spoon:before { - content: '\f1b1'; -} -.icon-cube:before { - content: '\f1b2'; -} -.icon-cubes:before { - content: '\f1b3'; -} -.icon-behance:before { - content: '\f1b4'; -} -.icon-behance-square:before { - content: '\f1b5'; -} -.icon-steam:before { - content: '\f1b6'; -} -.icon-steam-square:before { - content: '\f1b7'; -} -.icon-recycle:before { - content: '\f1b8'; -} -.icon-automobile:before, -.icon-car:before { - content: '\f1b9'; -} -.icon-cab:before, -.icon-taxi:before { - content: '\f1ba'; -} -.icon-tree:before { - content: '\f1bb'; -} -.icon-spotify:before { - content: '\f1bc'; -} -.icon-deviantart:before { - content: '\f1bd'; -} -.icon-soundcloud:before { - content: '\f1be'; -} -.icon-database:before { - content: '\f1c0'; -} -.icon-file-pdf-o:before { - content: '\f1c1'; -} -.icon-file-word-o:before { - content: '\f1c2'; -} -.icon-file-excel-o:before { - content: '\f1c3'; -} -.icon-file-powerpoint-o:before { - content: '\f1c4'; -} -.icon-file-photo-o:before, -.icon-file-picture-o:before, -.icon-file-image-o:before { - content: '\f1c5'; -} -.icon-file-zip-o:before, -.icon-file-archive-o:before { - content: '\f1c6'; -} -.icon-file-sound-o:before, -.icon-file-audio-o:before { - content: '\f1c7'; -} -.icon-file-movie-o:before, -.icon-file-video-o:before { - content: '\f1c8'; -} -.icon-file-code-o:before { - content: '\f1c9'; -} -.icon-vine:before { - content: '\f1ca'; -} -.icon-codepen:before { - content: '\f1cb'; -} -.icon-jsfiddle:before { - content: '\f1cc'; -} -.icon-life-bouy:before, -.icon-life-buoy:before, -.icon-life-saver:before, -.icon-support:before, -.icon-life-ring:before { - content: '\f1cd'; -} -.icon-circle-o-notch:before { - content: '\f1ce'; -} -.icon-ra:before, -.icon-rebel:before { - content: '\f1d0'; -} -.icon-ge:before, -.icon-empire:before { - content: '\f1d1'; -} -.icon-git-square:before { - content: '\f1d2'; -} -.icon-git:before { - content: '\f1d3'; -} -.icon-hacker-news:before { - content: '\f1d4'; -} -.icon-tencent-weibo:before { - content: '\f1d5'; -} -.icon-qq:before { - content: '\f1d6'; -} -.icon-wechat:before, -.icon-weixin:before { - content: '\f1d7'; -} -.icon-send:before, -.icon-paper-plane:before { - content: '\f1d8'; -} -.icon-send-o:before, -.icon-paper-plane-o:before { - content: '\f1d9'; -} -.icon-history:before { - content: '\f1da'; -} -.icon-genderless:before, -.icon-circle-thin:before { - content: '\f1db'; -} -.icon-header:before { - content: '\f1dc'; -} -.icon-paragraph:before { - content: '\f1dd'; -} -.icon-sliders:before { - content: '\f1de'; -} -.icon-share-alt:before { - content: '\f1e0'; -} -.icon-share-alt-square:before { - content: '\f1e1'; -} -.icon-bomb:before { - content: '\f1e2'; -} -.icon-soccer-ball-o:before, -.icon-futbol-o:before { - content: '\f1e3'; -} -.icon-tty:before { - content: '\f1e4'; -} -.icon-binoculars:before { - content: '\f1e5'; -} -.icon-plug:before { - content: '\f1e6'; -} -.icon-slideshare:before { - content: '\f1e7'; -} -.icon-twitch:before { - content: '\f1e8'; -} -.icon-yelp:before { - content: '\f1e9'; -} -.icon-newspaper-o:before { - content: '\f1ea'; -} -.icon-wifi:before { - content: '\f1eb'; -} -.icon-calculator:before { - content: '\f1ec'; -} -.icon-paypal:before { - content: '\f1ed'; -} -.icon-google-wallet:before { - content: '\f1ee'; -} -.icon-cc-visa:before { - content: '\f1f0'; -} -.icon-cc-mastercard:before { - content: '\f1f1'; -} -.icon-cc-discover:before { - content: '\f1f2'; -} -.icon-cc-amex:before { - content: '\f1f3'; -} -.icon-cc-paypal:before { - content: '\f1f4'; -} -.icon-cc-stripe:before { - content: '\f1f5'; -} -.icon-bell-slash:before { - content: '\f1f6'; -} -.icon-bell-slash-o:before { - content: '\f1f7'; -} -.icon-trash:before { - content: '\f1f8'; -} -.icon-copyright:before { - content: '\f1f9'; -} -.icon-at:before { - content: '\f1fa'; -} -.icon-eyedropper:before { - content: '\f1fb'; -} -.icon-paint-brush:before { - content: '\f1fc'; -} -.icon-birthday-cake:before { - content: '\f1fd'; -} -.icon-area-chart:before { - content: '\f1fe'; -} -.icon-pie-chart:before { - content: '\f200'; -} -.icon-line-chart:before { - content: '\f201'; -} -.icon-lastfm:before { - content: '\f202'; -} -.icon-lastfm-square:before { - content: '\f203'; -} -.icon-toggle-off:before { - content: '\f204'; -} -.icon-toggle-on:before { - content: '\f205'; -} -.icon-bicycle:before { - content: '\f206'; -} -.icon-bus:before { - content: '\f207'; -} -.icon-ioxhost:before { - content: '\f208'; -} -.icon-angellist:before { - content: '\f209'; -} -.icon-cc:before { - content: '\f20a'; -} -.icon-shekel:before, -.icon-sheqel:before, -.icon-ils:before { - content: '\f20b'; -} -.icon-meanpath:before { - content: '\f20c'; -} -.icon-buysellads:before { - content: '\f20d'; -} -.icon-connectdevelop:before { - content: '\f20e'; -} -.icon-dashcube:before { - content: '\f210'; -} -.icon-forumbee:before { - content: '\f211'; -} -.icon-leanpub:before { - content: '\f212'; -} -.icon-sellsy:before { - content: '\f213'; -} -.icon-shirtsinbulk:before { - content: '\f214'; -} -.icon-simplybuilt:before { - content: '\f215'; -} -.icon-skyatlas:before { - content: '\f216'; -} -.icon-cart-plus:before { - content: '\f217'; -} -.icon-cart-arrow-down:before { - content: '\f218'; -} -.icon-diamond:before { - content: '\f219'; -} -.icon-ship:before { - content: '\f21a'; -} -.icon-user-secret:before { - content: '\f21b'; -} -.icon-motorcycle:before { - content: '\f21c'; -} -.icon-street-view:before { - content: '\f21d'; -} -.icon-heartbeat:before { - content: '\f21e'; -} -.icon-venus:before { - content: '\f221'; -} -.icon-mars:before { - content: '\f222'; -} -.icon-mercury:before { - content: '\f223'; -} -.icon-transgender:before { - content: '\f224'; -} -.icon-transgender-alt:before { - content: '\f225'; -} -.icon-venus-double:before { - content: '\f226'; -} -.icon-mars-double:before { - content: '\f227'; -} -.icon-venus-mars:before { - content: '\f228'; -} -.icon-mars-stroke:before { - content: '\f229'; -} -.icon-mars-stroke-v:before { - content: '\f22a'; -} -.icon-mars-stroke-h:before { - content: '\f22b'; -} -.icon-neuter:before { - content: '\f22c'; -} -.icon-facebook-official:before { - content: '\f230'; -} -.icon-pinterest-p:before { - content: '\f231'; -} -.icon-whatsapp:before { - content: '\f232'; -} -.icon-server:before { - content: '\f233'; -} -.icon-user-plus:before { - content: '\f234'; -} -.icon-user-times:before { - content: '\f235'; -} -.icon-hotel:before, -.icon-bed:before { - content: '\f236'; -} -.icon-viacoin:before { - content: '\f237'; -} -.icon-train:before { - content: '\f238'; -} -.icon-subway:before { - content: '\f239'; -} -.icon-medium:before { - content: '\f23a'; -} -.icon-address-book:before { - content: '\f2b9'; -} -.icon-envelope-open:before { - content: '\f2b6'; -} diff --git a/src/renderer/scss/_reset.scss b/src/renderer/scss/_reset.scss index b93394c96..b9e3eca1c 100644 --- a/src/renderer/scss/_reset.scss +++ b/src/renderer/scss/_reset.scss @@ -68,7 +68,7 @@ select { border: 0 none; } img { - width: auto\9; + width: auto; height: auto; vertical-align: middle; -ms-interpolation-mode: bicubic; diff --git a/src/renderer/scss/_vars.scss b/src/renderer/scss/_vars.scss index f05049912..0f0b4e797 100644 --- a/src/renderer/scss/_vars.scss +++ b/src/renderer/scss/_vars.scss @@ -2,69 +2,74 @@ Both of these should probably die and become variables as well */ $spacing-vertical: 24px; -$width-page-constrained: 800px; -$text-color: #000; +$spacing-width: 36px; + +$medium-breakpoint: 1280px; +$large-breakpoint: 1760px; :root { + /* Widths & spacings */ + --side-nav-width: 220px; + --side-nav-width-m: 240px; + --side-nav-width-l: 320px; + + --video-aspect-ratio: 56.25%; // 9 x 16 + --snack-bar-width: 756px; + /* Colors */ - --color-brand: #155b4a; - --color-primary: #155b4a; - --color-primary-light: saturate(lighten(#155b4a, 50%), 20%); - --color-light-alt: hsl(hue(#155b4a), 15, 85); + --color-white: #fff; + --color-black: #000; + --color-grey: #d6d6d6; + --color-grey-light: #f6f6f6; + --color-grey-dark: #888; + --color-primary: #44b098; + --color-primary-dark: #2c6e60; + --color-primary-light: #64c9b2; + --color-secondary: #6afbda; + --color-teal: #19a6a3; + --color-dark-blue: #2f6f61; + --color-light-blue: #49b2e2; + --color-red: #e2495e; + --color-yellow: #fbd55e; + --color-divider: #e3e3e3; + + --text-color: var(--color-black); + --text-color-inverse: var(--color-white); + --color-dark-overlay: rgba(32, 32, 32, 0.9); --color-help: rgba(0, 0, 0, 0.54); - --color-notice: #8a6d3b; --color-error: #a94442; - --color-load-screen-text: #c3c3c3; - --color-meta-light: #505050; - --color-money: #216c2a; - --color-download: rgba(0, 0, 0, 0.75); - --color-canvas: #f5f5f5; - --color-bg: #ffffff; - --color-bg-alt: #d9d9d9; - - /* Misc */ - --content-max-width: 1000px; - --nsfw-blur-intensity: 20px; - --height-video-embedded: $width-page-constrained * 9 / 16; - - /* Font */ - --font-size: 16px; - --font-line-height: 1.3333; - --font-size-subtext-multiple: 0.82; + --color-download: var(--color-white); + --color-download-overlay: var(--color-black); + --color-bg: #fafafa; + --color-bg-alt: var(--color-grey-light); + --color-placeholder: #ececec; + --color-nav-bg: var(--color-grey-light); /* Shadows */ - --box-shadow-layer: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); - --box-shadow-focus: 2px 4px 4px 0 rgba(0, 0, 0, 0.14), 2px 5px 3px -2px rgba(0, 0, 0, 0.2), - 2px 3px 7px 0 rgba(0, 0, 0, 0.12); - - /* Transition */ - --transition-duration: 0.225s; - --transition-type: ease; + --box-shadow-layer: 0 4px 9px -2px var(--color-grey); + --box-shadow-button: 0 10px 20px rgba(0, 0, 0, 0.1); + --box-shadow-wunderbar: 0px 10px 20px rgba(0, 0, 0, 0.03); /* Text */ - --text-color: $text-color; --text-help-color: #eee; --text-max-width: 660px; --text-link-padding: 4px; --text-selection-bg: rgba(saturate(lighten(#155b4a, 20%), 20%), 1); // temp color --text-selection-color: #fff; - /* Window */ - --window-bg: var(--color-canvas); - /* Form */ --form-label-color: rgba(0, 0, 0, 0.54); /* Input */ --input-bg: transparent; - --input-width: 330px; + --input-label-color: var(--color-grey-dark); --input-color: var(--text-color); - --input-border-size: 2px; - --input-border-color: rgba(0, 0, 0, 0.54); - - /* input:active */ - --input-active-bg: transparent; + --input-border-size: 1px; + --input-border-color: var(--color-grey-dark); + --input-copyable-bg: var(--color-grey-light); + --input-copyable-color: var(--color-grey-dark); + --input-copyable-border: var(--color-grey); /* input:disabled */ --input-disabled-border-color: rgba(0, 0, 0, 0.42); @@ -82,36 +87,32 @@ $text-color: #000; --select-height: 30px; /* Button */ - --button-bg: var(--color-bg-alt); - --button-color: #fff; - --button-primary-bg: var(--color-primary); - --button-primary-color: #fff; - --button-padding: $spacing-vertical * 2/3; - --button-height: $spacing-vertical * 1.5; - --button-intra-margin: $spacing-vertical; - --button-radius: 3px; + --btn-bg-primary: var(--color-primary); + --btn-color-primary: var(--color-white); + --btn-bg-primary-hover: var(--color-primary-light); + --btn-bg-alt: var(--color-white); + --btn-color-alt: var(--text-color); + --btn-color-inverse: var(--color-primary); + --btn-external-color: var(--color-light-blue); + --btn-bg-secondary: var(--color-teal); + --btn-bg-danger: var(--color-red); + --btn-radius: 20px; + --btn-height: 36px; /* Header */ - --header-bg: var(--color-bg); - --header-color: #666; + --header-bg: var(--color-white); + --header-color: var(--color-text); --header-active-color: rgba(0, 0, 0, 0.85); - --header-height: $spacing-vertical * 2.5; - --header-button-bg: transparent; //var(--button-bg); + --header-height: $spacing-width * 3; + --header-button-bg: transparent; --header-button-hover-bg: rgba(100, 100, 100, 0.15); /* Header -> search */ - --search-bg: rgba(255, 255, 255, 0.7); - --search-border: 1px solid #ccc; --search-color: #666; + --search-bg-color: #fff; --search-active-color: var(--header-active-color); - --search-active-shadow: 0 0 3px 0px var(--text-selection-bg); - - /* Tabs */ - --tab-bg: transparent; - --tab-color: rgba(0, 0, 0, 0.5); - --tab-active-color: var(--color-primary); - --tab-border-size: 2px; - --tab-border: var(--tab-border-size) solid var(--tab-active-color); + --search-active-shadow: 0 6px 9px -2px var(--color-grey--dark); + --search-modal-input-height: 70px; /* Table */ --table-border: 1px solid #e2e2e2; @@ -119,14 +120,12 @@ $text-color: #000; --table-item-odd: #f4f4f4; /* Card */ - --card-bg: var(--color-bg); - --card-hover-translate: 10px; - --card-margin: $spacing-vertical * 2/3; - --card-max-width: $width-page-constrained; - --card-padding: $spacing-vertical * 2/3; --card-radius: 2px; - --card-link-scaling: 1.1; - --card-small-width: $spacing-vertical * 10; + --card-margin: $spacing-vertical * 2/3; + --card-wallet-color: var(--text-color-inverse); + + /* File Tile Card */ + --file-tile--media-size: 60px; /* Modal */ --modal-width: 440px; @@ -134,16 +133,10 @@ $text-color: #000; --modal-overlay-bg: rgba(#f5f5f5, 0.75); // --color-canvas: #F5F5F5 --modal-border: 1px solid rgb(204, 204, 204); - /* Menu */ - --menu-bg: var(--color-bg); - --menu-radius: 2px; - --menu-item-hover-bg: var(--color-bg-alt); - - /* Tooltip */ + // /* Tooltip */ --tooltip-width: 300px; --tooltip-bg: var(--color-bg); --tooltip-color: var(--text-color); - --tooltip-border: 1px solid #aaa; /* Scrollbar */ --scrollbar-radius: 10px; @@ -152,10 +145,7 @@ $text-color: #000; --scrollbar-thumb-active-bg: var(--color-primary); --scrollbar-track-bg: transparent; - /* Divider */ - --divider: 1px solid rgba(0, 0, 0, 0.12); - - /* Animation :) */ + // /* Animation :) */ --animation-duration: 0.3s; --animation-style: cubic-bezier(0.55, 0, 0.1, 1); } diff --git a/src/renderer/scss/all.scss b/src/renderer/scss/all.scss index 7d39cb43a..db8defe00 100644 --- a/src/renderer/scss/all.scss +++ b/src/renderer/scss/all.scss @@ -1,14 +1,11 @@ @charset "UTF-8"; @import '_reset'; @import '_vars'; -@import '_icons'; @import '_gui'; @import 'component/_table'; @import 'component/_button.scss'; @import 'component/_card.scss'; @import 'component/_file-download.scss'; -@import 'component/_file-selector.scss'; -@import 'component/_file-tile.scss'; @import 'component/_form-field.scss'; @import 'component/_header.scss'; @import 'component/_menu.scss'; @@ -18,14 +15,11 @@ @import 'component/_notice.scss'; @import 'component/_modal.scss'; @import 'component/_snack-bar.scss'; -@import 'component/_video.scss'; +@import 'component/_content.scss'; @import 'component/_pagination.scss'; @import 'component/_markdown-editor.scss'; @import 'component/_scrollbar.scss'; -@import 'component/_tabs.scss'; -@import 'component/_divider.scss'; -@import 'component/_checkbox.scss'; -@import 'component/_radio.scss'; -@import 'component/_shapeshift.scss'; @import 'component/_spinner.scss'; -@import 'page/_show.scss'; +@import 'component/_nav.scss'; +@import 'component/_file-list.scss'; +@import 'component/_search.scss'; diff --git a/src/renderer/scss/component/__divider.scss b/src/renderer/scss/component/__divider.scss deleted file mode 100644 index 00fbb74eb..000000000 --- a/src/renderer/scss/component/__divider.scss +++ /dev/null @@ -1,8 +0,0 @@ -.divider__horizontal { - border-top: var(--divider); - margin: 16px 0; -} - -.divider__vertical { - margin: 10px; -} diff --git a/src/renderer/scss/component/_button.scss b/src/renderer/scss/component/_button.scss index 576ca4c33..8b19a4be9 100644 --- a/src/renderer/scss/component/_button.scss +++ b/src/renderer/scss/component/_button.scss @@ -1,89 +1,193 @@ -@import '../mixin/link.scss'; - -$button-focus-shift: 12%; - -.button-set-item { - position: relative; - display: inline-block; - - + .button-set-item { - margin-left: var(--button-intra-margin); - } +button:disabled { + cursor: default; } -.button-block, -.faux-button-block { - display: inline-block; - height: var(--button-height); - line-height: var(--button-height); +.btn { + border: none; text-decoration: none; - border: 0 none; - text-align: center; - border-radius: var(--button-radius); - text-transform: uppercase; - .icon { - top: 0em; + cursor: pointer; + position: relative; + padding: 10px; + height: var(--btn-height); + min-width: var(--btn-height); + border-radius: var(--btn-radius); + background-color: var(--btn-bg-primary); + color: var(--btn-color-primary); + display: flex; + align-items: center; + justify-content: center; + fill: currentColor; // for proper icon color + font-size: 12px; + transition: all var(--animation-duration) var(--animation-style); + font-family: 'metropolis-medium'; + + &:not(:disabled) { + box-shadow: var(--box-shadow-button); } - .icon:first-child { - padding-right: 5px; + + &:hover { + box-shadow: none; + background-color: var(--btn-bg-primary-hover); } - .icon:last-child { + + .icon + .btn__label { padding-left: 5px; } - .icon:only-child { - padding-left: 0; - padding-right: 0; - } -} -.button-block { - cursor: pointer; - font-weight: 500; - font-size: 14px; - user-select: none; - transition: background var(--animation-duration) var(--animation-style); } -.button__content { - margin: 0 var(--button-padding); - display: flex; - .link-label { - text-decoration: none !important; +.btn.btn--alt { + color: var(--btn-color-alt); + background-color: var(--btn-bg-alt); + + &:disabled { + color: var(--color-help); + background-color: transparent; } } -.button-primary { - color: var(--button-primary-color); - background-color: var(--button-primary-bg); - box-shadow: var(--box-shadow-layer); - - &:focus { - //color: var(--button-primary-active-color); - //background-color:color: var(--button-primary-active-bg); - //box-shadow: $box-shadow-focus; - } -} -.button-alt { - background-color: var(--button-bg); - box-shadow: var(--box-shadow-layer); +.btn.btn--danger { + background-color: var(--btn-bg-danger); } -.button-text { - @include text-link(); +.btn.btn--inverse { + background-color: transparent; + box-shadow: none; + color: var(--btn-color-inverse); +} + +.btn.btn--link { + padding: 0; + margin: 0; + background-color: inherit; + font-size: 1em; + color: var(--btn-color-inverse); + border-radius: 0; display: inline-block; + min-width: 0; + box-shadow: none; + text-align: left; +} - .button__content { - margin: 0 var(--text-link-padding); +.btn.btn--external-link { + color: var(--btn-external-color); +} + +.btn.btn--secondary { + background-color: var(--btn-bg-secondary); +} + +.btn.btn--no-style { + font-size: inherit; + font-weight: inherit; + color: inherit; + background-color: inherit; + border-radius: 0; + padding: 0; + margin: 0; + box-shadow: none; + min-width: 0; +} + +.btn--link, +.btn--no-style { + height: auto; + + .btn__label, + .btn__content { + padding: 0; } } -.button-text-help { - @include text-link(var(--text-help-color)); - font-size: 0.8em; -} -.button--flat { - box-shadow: none !important; + +.btn.btn--disabled:disabled { + cursor: default; + + &.btn--primary { + background-color: rgba(0, 0, 0, 0.5); + } + + &:hover { + box-shadow: none; + } } -.button--submit { - font-family: inherit; - line-height: 0; +.btn.btn--uppercase { + text-transform: uppercase; +} + +.btn:not(.btn--no-padding):not(.btn--link) { + .btn__content { + padding: 0 8px; + display: flex; + align-items: center; + } +} + +.icon + .btn__label, +.btn__label + .icon { + margin-left: 5px; +} + +/* + Everything below this is variations of the default button classes + You must pass in a className, props will not set these classes, + if you use these in several different places they should probably + be applied via props +*/ + +.btn.btn--home-nav { + box-shadow: none; + + .btn__content { + padding: 0; + } +} + +.btn.btn--arrow { + width: var(--btn-arrow-width); + + &:disabled { + opacity: 0.3; + } +} + +.btn--uri-indicator { + transition: color var(--animation-duration) var(--animation-duration); + + &:hover { + color: var(--color-light-blue); + } +} + +.btn.btn--header-balance { + font-family: 'metropolis-medium'; + font-size: 13px; + + @media only screen and (min-width: $medium-breakpoint) { + font-size: 18px; + } + + @media only screen and (min-width: $large-breakpoint) { + font-size: 21px; + } + + .btn__label--balance { + color: var(--color-grey-dark); + } + + &:hover { + background-color: transparent; + + .btn__label--balance { + color: var(--btn-primary-bg); + } + } +} + +.btn.btn--file-actions { + background-color: var(--color-black); + color: var(--color-white); + opacity: 0.8; + border-radius: var(--btn-radius); + height: var(--btn-height); + padding: 0 3px; } diff --git a/src/renderer/scss/component/_card.scss b/src/renderer/scss/component/_card.scss index cef50a1fb..5cb1baae9 100644 --- a/src/renderer/scss/component/_card.scss +++ b/src/renderer/scss/component/_card.scss @@ -1,322 +1,400 @@ .card { margin-left: auto; margin-right: auto; - max-width: var(--card-max-width); - background: var(--card-bg); - box-shadow: var(--box-shadow-layer); border-radius: var(--card-radius); - margin-bottom: var(--card-margin); overflow: auto; user-select: text; - - //below added to prevent scrollbar on long titles when show page loads, would prefer a cleaner CSS solution - overflow-x: hidden; -} -.card--obscured { - position: relative; -} -.card--obscured .card__inner { - filter: blur(var(--nsfw-blur-intensity)); -} -.card__title-primary, -.card__title-identity, -.card__content, -.card__subtext, -.card__actions { - padding: 0 var(--card-padding); -} - -.card--small { - .card__title-primary, - .card__title-identity, - .card__actions, - .card__content, - .card__subtext { - padding: 0 calc(var(--card-padding) / 2); - } -} -.card__title-primary { - margin-top: var(--card-margin); - margin-bottom: var(--card-margin); -} -.card__title-primary .meta { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} -.card__title-identity { - margin: 16px 0; -} -.card__actions { - margin-top: var(--card-margin); - margin-bottom: var(--card-margin); - user-select: none; -} -.card__actions--bottom { - margin-top: $spacing-vertical * 1/3; - margin-bottom: $spacing-vertical * 1/3; - border-top: var(--divider); -} -.card__actions--form-submit { - margin-top: $spacing-vertical; - margin-bottom: var(--card-margin); -} -.card__action--right { - float: right; -} -.card__content { - margin-top: var(--card-margin); - margin-bottom: var(--card-margin); - table:not(:last-child) { - margin-bottom: var(--card-margin); - } -} - -.card__actions--only-vertical { - margin-left: 0; - margin-right: 0; - padding-left: 0; - padding-right: 0; -} - -.card__content--extra-vertical-space { - margin: $spacing-vertical 0; -} - -$font-size-subtext-multiple: 0.82; -.card__subtext { - color: var(--color-meta-light); - font-size: calc(var(--font-size-subtext-multiple) * 1em); - margin-top: $spacing-vertical * 1/3; - margin-bottom: $spacing-vertical * 1/3; -} -.card__subtext--allow-newlines { - white-space: pre-wrap; -} -.card__subtext--two-lines { - height: calc( - var(--font-size) * var(--font-size-subtext-multiple) * var(--font-line-height) * 2 - ); /*this is so one line text still has the proper height*/ -} -.card-overlay { - position: absolute; - left: 0px; - right: 0px; - top: 0px; - bottom: 0px; - padding: 20px; - background-color: var(--color-dark-overlay); - color: #fff; display: flex; - align-items: center; - font-weight: 600; -} - -.card__link { - display: block; - cursor: pointer; -} -.card--link { - transition: transform 0.2s var(--animation-style); -} -.card--link:hover { position: relative; - z-index: 1; - box-shadow: var(--box-shadow-focus); - transform: scale(var(--card-link-scaling)) translateX(var(--card-hover-translate)); - transform-origin: 50% 50%; - overflow-x: visible; - overflow-y: visible; -} -.card--link:hover ~ .card--link { - transform: translateX(calc(var(--card-hover-translate) * 2)); + flex-direction: column; } -.card__media { - background-size: cover; - background-repeat: no-repeat; - background-position: 50% 50%; -} - -.card__media--autothumb { - position: relative; -} -.card__media--autothumb.purple { - background-color: #9c27b0; -} -.card__media--autothumb.red { - background-color: #e53935; -} -.card__media--autothumb.pink { - background-color: #e91e63; -} -.card__media--autothumb.indigo { - background-color: #3f51b5; -} -.card__media--autothumb.blue { - background-color: #2196f3; -} -.card__media--autothumb.light-blue { - background-color: #039be5; -} -.card__media--autothumb.cyan { - background-color: #00acc1; -} -.card__media--autothumb.teal { - background-color: #009688; -} -.card__media--autothumb.green { - background-color: #43a047; -} -.card__media--autothumb.yellow { - background-color: #ffeb3b; -} -.card__media--autothumb.orange { - background-color: #ffa726; -} - -.card__media--autothumb .card__autothumb__text { - font-size: 2em; - width: 100%; - color: #ffffff; - text-align: center; - position: absolute; - top: 36%; -} - -.card__indicators { - float: right; +.card--section { + background-color: var(--color-white); + padding: $spacing-vertical; + margin-top: $spacing-vertical * 2/3; } .card--small { width: var(--card-small-width); overflow-x: hidden; white-space: normal; -} -.card--small .card__media { - height: calc(var(--card-small-width) * 9 / 16); + + .card__media { + padding-top: var(--video-aspect-ratio); + } + + .card__media-text { + // for the weird padding required for dynamic height + // this lets the text sit in the middle instead of the bottom + margin-top: calc(var(--video-aspect-ratio) * -1); + } + + .channel-name { + font-size: 12px; + } } -.card--form { - width: calc(var(--input-width) + var(--card-padding) * 2); +.card--link { + cursor: pointer; +} + +.card--pending { + opacity: 0.5; +} + +.card--wallet-balance { + background: url('../../../static/img/stripe-background.png') no-repeat; + background-size: cover; + color: var(--card-wallet-color); + justify-content: space-between; + + .card__subtitle { + color: var(--card-wallet-color); + } +} + +.card--disabled { + opacity: 0.3; +} + +.card__media { + background-size: cover; + background-repeat: no-repeat; + background-position: 50% 50%; + background-color: var(--color-placeholder); +} + +.card__media--no-img { + display: flex; + justify-content: center; + align-items: center; + position: relative; +} + +.card__media--nsfw { + background-color: var(--color-error); +} + +.card__title-identity--file { + display: flex; + align-items: center; + + .credit-amount, + .icon { + margin-top: $spacing-vertical * 1/3; + margin-left: $spacing-vertical * 2/3; + } +} + +.card__title-identity-icons { + display: flex; + align-items: center; + align-self: flex-start; +} + +.card__title { + font-size: 18px; +} + +.card__title--file { + font-family: 'metropolis-bold'; + font-size: 28px; + line-height: 36px; + padding-top: 20px; +} + +.card__title--small { + font-size: 14px; + line-height: 18px; + padding-top: 20px; +} + +.card__title--file { + padding-top: 0; + padding-bottom: 5px; } .card__subtitle { color: var(--color-help); - font-size: 0.85em; - line-height: calc(var(--font-line-height) * 1 / 0.85); + font-size: 14px; + font-family: 'metropolis-medium'; + + .icon { + margin-left: $spacing-vertical * 1/3; + } } -.card--file-subtitle { +.card__subtitle--file-info { display: flex; + align-items: center; } -// this is too specific -// it should be a helper class -// ex. ".m-padding-left" -// will come back to this during the redesign - sean -.card__publish-date { - padding-left: 20px; +.card__subtitle--block { + display: block; } -.card-series-submit { - margin-left: auto; - margin-right: auto; - max-width: var(--card-max-width); - padding: $spacing-vertical / 2; +.card__meta { + color: var(--color-help); + font-size: 14px; + font-family: 'metropolis-medium'; + padding-top: $spacing-vertical * 2/3; } -.card-row { - + .card-row { +// .card-media__internal__links should always be inside a card +.card { + .card-media__internal-links { + position: absolute; + top: $spacing-vertical * 2/3; + right: $spacing-vertical * 2/3; + } +} + +.card--small { + .card-media__internal-links { + top: $spacing-vertical * 1/3; + right: $spacing-vertical * 1/3; + } +} + +// Channel info with buttons on the right side +.card__channel-info { + display: flex; + justify-content: space-between; + align-items: center; + padding: $spacing-width 0; +} + +.card__channel-info--large { + padding-top: 0; + padding-bottom: $spacing-width; +} + +.card__content { + margin-top: var(--card-margin); +} + +.card__subtext-title { + color: var(--color-black); + font-size: calc(var(--font-size-subtext-multiple) * 1.5em); + + &:not(:first-of-type) { + margin-top: $spacing-vertical * 3/2; + } +} + +.card__subtext { + color: var(--color-grey-dark); + font-size: calc(var(--font-size-subtext-multiple) * 1em); + padding-top: $spacing-vertical * 1/3; + word-break: break-word; + font-family: 'metropolis-medium'; + font-size: 13px; +} + +.card__actions { + margin-top: var(--card-margin); + display: flex; + + &:not(.card__actions--vertical) .btn:nth-child(n + 2) { + margin-left: $spacing-vertical / 3; + } +} + +.card__actions--no-margin { + margin-top: 0; +} + +.card__actions--vertical { + flex-direction: column; + margin-top: 0; + align-items: flex-end; + + .btn:not(:first-child) { margin-top: $spacing-vertical * 1/3; } } -.card-row__placeholder { - padding-bottom: $spacing-vertical; -} +.card__actions--center { + align-items: center; + justify-content: center; -$padding-top-card-hover-hack: 20px; -$padding-right-card-hover-hack: 30px; - -.card-row__items { - width: 100%; - overflow: hidden; - - /*hacky way to give space for hover */ - padding-top: $padding-top-card-hover-hack; - margin-top: -1 * $padding-top-card-hover-hack; - padding-right: $padding-right-card-hover-hack; - margin-right: -1 * $padding-right-card-hover-hack; - > .card { - vertical-align: top; - display: inline-block; - } - > .card + .card { - margin-left: 16px; + .btn { + margin: 0 $spacing-vertical / 3; } } -.card-row--small { +.card__actions-top-corner { + position: absolute; + top: $spacing-vertical; + right: $spacing-vertical; +} + +/* + .card-row is used on the discover/subscriptions page + It is a list of cards that extend past the right edge of the screen + There are left/right arrows to scroll the cards and view hidden content +*/ +.card-row { overflow: hidden; white-space: nowrap; width: 100%; min-width: var(--card-small-width); - margin-right: $spacing-vertical; + padding-top: $spacing-vertical; + + &:first-of-type { + padding-top: 0; + } + + &:last-of-type { + padding-bottom: $spacing-vertical * 2/3; + } } + .card-row__header { - margin-bottom: 16px; + display: flex; + flex-direction: row; + justify-content: space-between; + // specific padding-left styling is needed here + // this needs to be used on a page with noPadding + // doing so allows the content to scroll to the edge of the screen + padding-left: $spacing-width; +} + +.card-row__title { + display: flex; + align-items: center; + font-size: 18px; + line-height: 24px; +} + +.card-row__scroll-btns { + display: flex; + padding-right: $spacing-width - 10px; // page padding - 1/2 width of arrow button } .card-row__scrollhouse { - position: relative; - /*hacky way to give space for hover */ - padding-right: $padding-right-card-hover-hack; -} + padding-top: $spacing-vertical * 2/3; + overflow: hidden; -.card-row__nav { - position: absolute; - padding: 0 var(--card-margin); - height: 100%; - top: calc($padding-top-card-hover-hack - var(--card-margin)); -} -.card-row__nav .card-row__scroll-button { - background: var(--card-bg); - color: var(--color-help); - box-shadow: var(--box-shadow-layer); - padding: $spacing-vertical $spacing-vertical / 2; - position: absolute; - cursor: pointer; - left: 0; - top: 36%; - z-index: 2; - opacity: 0.8; - transition: transform 0.2s var(--animation-style); - - &:hover { - opacity: 1; - transform: scale(calc(var(--card-link-scaling) * 1.1)); + .card:first-of-type { + margin-left: $spacing-width; + } + + .card:last-of-type { + margin-right: $spacing-vertical * 2/3; } -} -.card-row__nav--left { - left: 0; -} -.card-row__nav--right { - right: 0; } /* -if we keep doing things like this, we should add a real grid system, but I'm going to be a selective dick about it - Jeremy - */ -.card-grid { - $margin-card-grid: $spacing-vertical * 2/3; - display: flex; - flex-wrap: wrap; - > .card { - width: $width-page-constrained / 2 - $margin-card-grid / 2; - flex-grow: 1; + How cards are displayed in lists +*/ +.card__list { + .card { + display: inline-block; + vertical-align: top; + margin-bottom: 60px; + + @media only screen and (max-width: $medium-breakpoint) { + width: calc((100% / 3) - (40px / 3)); + + &:not(:nth-child(3n + 1)) { + margin-left: 20px; + } + } } - > .card:nth-of-type(2n - 1):not(:last-child) { - margin-right: $margin-card-grid; + + @media only screen and (min-width: $medium-breakpoint) { + .card { + width: calc((100% / 4) - (60px / 4)); + + &:not(:nth-child(4n + 1)) { + margin-left: 20px; + } + } + } +} + +.card__list--rewards { + .card { + display: inline-block; + width: calc(50% - 10px); + margin-bottom: 20px; + vertical-align: top; + + &:not(:nth-child(2n + 1)) { + margin-left: 20px; + } + } +} + +.card-row__scrollhouse { + padding-top: $spacing-vertical * 2/3; + overflow: hidden; + + .card { + display: inline-block; + vertical-align: top; + overflow: visible; + // -- three cards on a screen + // -- minus 12px for 1/3 of the page padding (36px) + // -- minus 20px for the card's margin + // Ideally we should be able to use $spacing-width / 3, but I'm not sure + // how inside the calc function + width: calc((100% / 3) - 12px - 20px); + } + + .card:not(:first-of-type) { + margin-left: 20px; + } + + .card:last-of-type { + margin-right: 20px; + } + + @media only screen and (min-width: $medium-breakpoint) { + .card { + width: calc((100% / 4) - 12px - 20px); + } + } +} + +.card__media--autothumb { + color: red !important; +} + +.card__media { + &.card__media--autothumb.purple { + background-color: #9c27b0; + } + &.card__media--autothumb.red { + background-color: #e53935; + } + &.card__media--autothumb.pink { + background-color: #e91e63; + } + &.card__media--autothumb.indigo { + background-color: #3f51b5; + } + &.card__media--autothumb.blue { + background-color: #2196f3; + } + &.card__media--autothumb.light-blue { + background-color: #039be5; + } + &.card__media--autothumb.cyan { + background-color: #00acc1; + } + &.card__media--autothumb.teal { + background-color: #009688; + } + &.card__media--autothumb.green { + background-color: #43a047; + } + &.card__media--autothumb.yellow { + background-color: #ffeb3b; + } + &.card__media--autothumb.orange { + background-color: #ffa726; } } diff --git a/src/renderer/scss/component/_channel-indicator.scss b/src/renderer/scss/component/_channel-indicator.scss index 7c437780c..2291cc933 100644 --- a/src/renderer/scss/component/_channel-indicator.scss +++ b/src/renderer/scss/component/_channel-indicator.scss @@ -5,12 +5,6 @@ text-overflow: ellipsis; } -// this shouldn't know about the card width -// will come back to this for the redesign - sean -.channel-name--small { - width: calc(var(--card-small-width) * 2 / 3); -} - .channel-indicator__icon--invalid { color: var(--color-error); } diff --git a/src/renderer/scss/component/_checkbox.scss b/src/renderer/scss/component/_checkbox.scss deleted file mode 100644 index d4796afe6..000000000 --- a/src/renderer/scss/component/_checkbox.scss +++ /dev/null @@ -1,69 +0,0 @@ -*, -*:before, -*:after { - box-sizing: border-box; -} - -$md-checkbox-checked-color: var(--color-primary); -$md-checkbox-border-color: var(--input-border-color); -$md-checkbox-size: 20px; -$md-checkbox-padding: 4px; -$md-checkmark-width: 2px; -$md-checkmark-color: #fff; - -.form-field--checkbox { - position: relative; - - label { - cursor: pointer; - &:before, - &:after { - content: ''; - position: absolute; - left: 0; - top: 0; - } - - &:before { - // box - width: $md-checkbox-size; - height: $md-checkbox-size; - background: transparent; - border: 2px solid $md-checkbox-border-color; - border-radius: 2px; - cursor: pointer; - transition: background 0.3s; - } - - &:after { - // checkmark - } - } - - input[type='checkbox'] { - outline: 0; - visibility: hidden; - margin-right: 16px; - - &:checked { - + label:before { - background: $md-checkbox-checked-color; - border: none; - } - + label:after { - $md-checkmark-size: $md-checkbox-size - 2 * $md-checkbox-padding; - - transform: rotate(-45deg); - - top: ($md-checkbox-size / 2) - ($md-checkmark-size / 4) - $md-checkbox-size/10; - left: $md-checkbox-padding; - width: $md-checkmark-size; - height: $md-checkmark-size / 2; - - border: $md-checkmark-width solid $md-checkmark-color; - border-top-style: none; - border-right-style: none; - } - } - } -} diff --git a/src/renderer/scss/component/_content.scss b/src/renderer/scss/component/_content.scss new file mode 100644 index 000000000..11a16cbdf --- /dev/null +++ b/src/renderer/scss/component/_content.scss @@ -0,0 +1,56 @@ +.content__embedded { + background-color: var(--color-black); + width: 100%; + padding-top: var(--video-aspect-ratio); + position: relative; + display: flex; + align-items: center; + justify-content: center; + + video { + height: 100%; + width: 100%; + position: absolute; + top: 0; + left: 0; + } +} + +// Video thumbnail with play/download button +.content__cover { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-size: cover; + background-repeat: no-repeat; + background-position: 50% 50%; + display: flex; + align-items: center; + justify-content: center; + + &:not(.card__media--nsfw) { + background-color: var(--color-black); + } +} + +.content__view { + margin-top: -56.25%; +} + +.content__loading { + display: flex; + flex-direction: column; + align-items: center; + padding: 0 20px; +} + +.content__loading-text { + color: var(--color-white); +} + +img { + max-height: 100%; + max-width: 100%; +} diff --git a/src/renderer/scss/component/_file-download.scss b/src/renderer/scss/component/_file-download.scss index 04eb6fc90..642d6be7e 100644 --- a/src/renderer/scss/component/_file-download.scss +++ b/src/renderer/scss/component/_file-download.scss @@ -1,17 +1,19 @@ .file-download, .file-download__overlay { - .button__content { - margin: 0 var(--text-link-padding); - } + padding: 5px; } .file-download { position: relative; color: var(--color-download); + font-size: 12px; + font-family: 'metropolis-medium'; } + .file-download__overlay { background: var(--color-download); - color: var(--color-bg); + color: var(--color-download-overlay); + border-radius: var(--btn-radius); position: absolute; white-space: nowrap; overflow: hidden; diff --git a/src/renderer/scss/component/_file-list.scss b/src/renderer/scss/component/_file-list.scss new file mode 100644 index 000000000..a35ac115b --- /dev/null +++ b/src/renderer/scss/component/_file-list.scss @@ -0,0 +1,44 @@ +.file-list { + display: grid; + grid-gap: $spacing-vertical * 2/3; + grid-template-columns: repeat(auto-fill, var(--card-small-width)); + margin-top: $spacing-vertical * 2/3; +} + +.file-list__sort { + display: flex; + justify-content: flex-end; + padding-bottom: 20px; +} + +.file-list__header { + margin-top: $spacing-vertical * 4/3; + font-size: 18px; +} + +.file-tile { + display: flex; + margin-top: $spacing-vertical; + max-width: 260px; + height: var(--file-tile--media-size); + + .card__media { + flex: 0 0 var(--file-tile--media-size); + } + + .card__subtitle { + line-height: 1; + } +} + +.file-tile--fullwidth { + max-width: none; +} + +.file-tile__info { + margin-left: $spacing-vertical * 1/3; +} + +.file-tile__uri { + color: var(--color-grey-dark); +} diff --git a/src/renderer/scss/component/_file-selector.scss b/src/renderer/scss/component/_file-selector.scss deleted file mode 100644 index 8172da203..000000000 --- a/src/renderer/scss/component/_file-selector.scss +++ /dev/null @@ -1,23 +0,0 @@ -.form-field--file, -.form-field--directory { - width: 100%; -} - -.file-selector { - display: flex; -} - -.file-selector__choose-button { - font-family: inherit; - line-height: 0; - color: inherit; - margin-right: 16px; -} - -.file-selector__path { - font-size: 14px; - flex-grow: 2; - .input-copyable { - width: 100%; - } -} diff --git a/src/renderer/scss/component/_file-tile.scss b/src/renderer/scss/component/_file-tile.scss deleted file mode 100644 index eccb74446..000000000 --- a/src/renderer/scss/component/_file-tile.scss +++ /dev/null @@ -1,22 +0,0 @@ -$height-file-tile: $spacing-vertical * 6; -.file-tile__row { - overflow: hidden; - height: $height-file-tile; - //also a hack - .card__media { - height: $height-file-tile; - max-width: $height-file-tile; - width: $height-file-tile; - margin-right: $spacing-vertical / 2; - float: left; - } - //basically everything here is a hack now - .file-tile__content { - padding-top: $spacing-vertical * 1/3; - margin-left: $height-file-tile + $spacing-vertical / 2; - } - - .card__title-primary { - margin-top: 0; - } -} diff --git a/src/renderer/scss/component/_form-field.scss b/src/renderer/scss/component/_form-field.scss index a63352589..e4c9d4287 100644 --- a/src/renderer/scss/component/_form-field.scss +++ b/src/renderer/scss/component/_form-field.scss @@ -1,195 +1,89 @@ -.form-row-submit { - margin-top: $spacing-vertical; -} -.form-row-submit--with-footer { - margin-bottom: $spacing-vertical; -} - -.form-row-phone { +.form-row { display: flex; + flex-direction: row; + align-items: flex-end; - .form-field__input-text { - margin-left: 5px; - width: calc(0.85 * var(--input-width)); - } -} - -.form-row__label-row { - margin-top: $spacing-vertical * 5/6; - margin-bottom: 0px; - line-height: 1; - font-size: calc(0.9 * var(--font-size)); -} -.form-row__label-row--prefix { - float: left; - margin-right: 5px; -} - -.form-row--focus { - .form-field__label, - .form-field__prefix { - color: var(--color-primary) !important; - } -} - -.form-field { - display: inline-block; - margin: 8px 0; - - select { - transition: outline var(--transition-duration) var(--transition-type); - box-sizing: border-box; - padding-left: 5px; - padding-right: 5px; - height: var(--select-height); - background: var(--select-bg); - color: var(--select-color); - &:focus { - outline: var(--input-border-size) solid var(--color-primary); - } + .form-field:not(:first-of-type) { + padding-left: $spacing-vertical; } - input[type='radio'], - input[type='checkbox'] { - &:checked + .form-field__label { - color: var(--text-color); - } + &.form-row--padded { + padding-top: $spacing-vertical * 2/3; } - input[type='text'].input-copyable { - background: var(--input-bg); - color: var(--input-disabled-color); - line-height: 1; - padding-top: $spacing-vertical * 1/3; - padding-bottom: $spacing-vertical * 1/3; - padding-left: 5px; - padding-right: 5px; + &.form-row--centered { + align-items: center; + } + + .form-field.form-field--stretch { width: 100%; - font-family: 'Consolas', 'Lucida Console', 'Adobe Source Code Pro', monospace; - &.input-copyable--with-copy-btn { - width: 85%; + input { + width: 100%; + max-width: 400px; } } - input[readonly] { - color: var(--input-disabled-color) !important; - border-bottom: 1px dashed var(--input-disabled-border-color) !important; + input + .btn { + margin-left: $spacing-vertical * 1/3; } - - input[readonly]:focus { - background: var(--input-bg) !important; - border-bottom: 1px dashed var(--input-disabled-border-color) !important; - } - - textarea, - input[type='text'], - input[type='password'], - input[type='email'], - input[type='number'], - input[type='search'], - input[type='date'] { - background: var(--input-bg); - border-bottom: var(--input-border-size) solid var(--input-border-color); - caret-color: var(--color-primary); - color: var(--input-color); - cursor: pointer; - line-height: 1; - padding: 0 1px 8px 1px; - box-sizing: border-box; - -webkit-appearance: none; - transition: all var(--transition-duration) var(--transition-type); - - &::-webkit-input-placeholder { - color: var(--input-placeholder-color); - opacity: var(--input-placeholder-opacity) !important; - } - - &:focus { - border-color: var(--color-primary); - background: var(--input-active-bg); - } - - &:hover:not(:focus) { - border-color: var(--input-hover-border-color); - } - - &.form-field__input--error { - border-color: var(--color-error); - } - - &.form-field__input--inline { - padding-top: 0; - padding-bottom: 0; - border-bottom-width: var(--input-border-size); - margin-left: 8px; - margin-right: 8px; - } - } - - textarea { - padding: 2px; - border: var(--input-border-size) solid var(--input-border-color); - } -} -.form-field--address { - width: 100%; } .form-field--SimpleMDE { display: block; + width: 100%; } -.form-field__label, -.form-row__label { - color: var(--form-label-color); - &[for] { - cursor: pointer; +.form-field__input { + display: flex; + padding-top: $spacing-vertical / 3; + height: 36px; + + input[type='checkbox'], + input[type='radio'] { + margin-top: 5px; } } -.form-row__label-row .form-field__label--error { - /*the row restriction is to prevent coloring checkboxes and radio labels*/ - color: var(--color-error); +.form-field__help, +.form-field__label, +.form-field__error { + font-size: 12px; + font-family: 'metropolis-medium'; } -.form-field__input-text { - width: var(--input-width); +.form-field__label { + color: var(--color-black); } -.form-field__prefix { - margin-right: 4px; -} -.form-field__postfix { - margin-left: 4px; -} - -.form-field__input-number { - width: 70px; - text-align: right; -} - -.form-field--textarea { - width: 100%; -} -.form-field__input-textarea { - width: 100%; -} - -.form-field__error, -.form-field__helper { - margin-top: $spacing-vertical * 1/3; - font-size: 0.8em; - transition: opacity var(--transition-duration) var(--transition-type); +.form-field__help { + color: var(--color-grey-dark); + padding-top: $spacing-vertical * 1/3; } .form-field__error { color: var(--color-error); } -.form-field__helper { - color: var(--color-help); + +.form-field__prefix, +.form-field__postfix { + font-family: 'metropolis-medium'; } -.form-field__input.form-field__input-SimpleMDE .CodeMirror-scroll { - height: auto; +.form-field__prefix { + padding-right: $spacing-vertical * 1/3; +} + +.form-field__postfix { + padding-left: $spacing-vertical * 1/3; +} + +// Not sure if I like these +// Maybe this should be in gui.scss? +.input--price-amount { + width: 60px; +} + +.input--address { + width: 370px; } diff --git a/src/renderer/scss/component/_header.scss b/src/renderer/scss/component/_header.scss index a8967d0eb..6df42f34e 100644 --- a/src/renderer/scss/component/_header.scss +++ b/src/renderer/scss/component/_header.scss @@ -1,64 +1,19 @@ -#header { - color: var(--header-color); - background: var(--header-bg); +.header { + grid-area: header; display: flex; - align-items: center; - justify-content: space-around; - position: fixed; - box-shadow: var(--box-shadow-layer); - top: 0; - left: 0; - width: 100%; - z-index: 3; - padding: $spacing-vertical / 2; - box-sizing: border-box; -} -.header__item { - padding-left: $spacing-vertical / 4; - padding-right: $spacing-vertical / 4; - .button-alt { - background: var(--header-button-bg) !important; - font-size: 1em; - } - .button-alt:hover { - background: var(--header-button-hover-bg) !important; - } + z-index: 1; + justify-content: space-between; + padding: $spacing-width $spacing-width 0 $spacing-width; + background-color: var(--color-bg); + // height: 100px; } -.header__item--wunderbar { - flex-grow: 1; -} +.header__actions-right { + margin-left: auto; + padding-left: $spacing-vertical / 2; + display: flex; -.wunderbar { - position: relative; - .icon { - position: absolute; - left: 10px; - top: $spacing-vertical / 2 - 4px; //hacked - } -} - -.wunderbar--active .icon-search { - color: var(--color-primary); -} - -// below styles should be inside the common input styling -// will come back to this with the redesign - sean -.wunderbar__input { - background: var(--search-bg); - width: 100%; - color: var(--search-color); - height: $spacing-vertical * 1.5; - line-height: $spacing-vertical * 1.5; - padding-left: 38px; - padding-right: 5px; - border-radius: 2px; - border: var(--search-border); - transition: box-shadow var(--transition-duration) var(--transition-type); - &:focus { - background: var(--search-active-bg); - color: var(--search-active-color); - box-shadow: var(--search-active-shadow); - border-color: var(--color-primary); + .btn { + margin-left: $spacing-vertical * 1/3; } } diff --git a/src/renderer/scss/component/_load-screen.scss b/src/renderer/scss/component/_load-screen.scss index 05cce635e..a34dd2d89 100644 --- a/src/renderer/scss/component/_load-screen.scss +++ b/src/renderer/scss/component/_load-screen.scss @@ -1,6 +1,6 @@ .load-screen { color: white; - background: var(--color-brand); + background: var(--color-primary); background-size: cover; min-height: 100vh; min-width: 100vw; @@ -10,20 +10,24 @@ justify-content: center; } +.load-screen__title { + font-family: 'metropolis-bold'; + font-size: 60px; + line-height: 100px; +} + .load-screen__message { - margin-top: 24px; - width: 325px; + font-family: 'metropolis-semibold'; + font-size: 16px; + line-height: 20px; + margin-top: $spacing-vertical * 2/3; text-align: center; } .load-screen__details { - color: var(--color-load-screen-text); -} - -.load-screen__details--warning { - color: white; -} - -.load-screen__cancel-link { - color: white; + font-family: 12px 'metropolis-medium'; + font-size: 12px; + line-height: 1; + padding-top: $spacing-vertical * 2/3; + max-width: 400px; } diff --git a/src/renderer/scss/component/_modal.scss b/src/renderer/scss/component/_modal.scss index d5a561334..5d58e46d7 100644 --- a/src/renderer/scss/component/_modal.scss +++ b/src/renderer/scss/component/_modal.scss @@ -30,10 +30,45 @@ padding: $spacing-vertical; box-shadow: var(--box-shadow-layer); max-width: var(--modal-width); - word-break: break-word; } +.modal--fullscreen { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + padding: $spacing-vertical; + background: var(--modal-bg); + overflow-y: scroll; + + .main { + // I will come back to these when I do media queries + // They should be variables + padding: 130px 80px 0 80px; + } +} + +// For slide down animation on the search modal +// Slide down isn't possible without doing a lot of re-work to the modal component +.ReactModal__Overlay { + .modal--fullscreen { + transition: height var(--animation-style) var(--animation-style); + height: 0; + } + + &--after-open { + .modal--fullscreen { + height: 100vh; + } + } + + &--before-close { + height: 0; + } +} + .modal__header { margin-bottom: $spacing-vertical * 2/3; text-align: center; @@ -72,6 +107,7 @@ max-width: none; width: var(--modal-width); } + .error-modal__error-list { /*shitty hack/temp fix for long errors making modal unusable*/ border: 1px solid var(--input-border-color); @@ -81,3 +117,7 @@ max-width: var(--modal-width); overflow-y: hidden; } + +.error-modal__content p { + padding: 0 0 $spacing-vertical $spacing-vertical; +} diff --git a/src/renderer/scss/component/_nav.scss b/src/renderer/scss/component/_nav.scss new file mode 100644 index 000000000..54c432142 --- /dev/null +++ b/src/renderer/scss/component/_nav.scss @@ -0,0 +1,98 @@ +.nav { + grid-area: nav; + background-color: var(--color-nav-bg); + padding: $spacing-width; + + hr { + width: 24px; + margin: 36px 0; + // width: 40px; + border: solid 1px var(--color-grey); + margin: $spacing-vertical $spacing-vertical * 2/3; + } +} + +.nav__actions-top { + display: flex; + justify-content: space-between; +} + +.nav__actions-history { + display: flex; +} + +// Sidebar links +.nav__primary { + padding-top: 80px; +} + +.nav__link { + // padding-top: $spacing-vertical / 3; + color: var(--color-grey-dark); + white-space: nowrap; + + .btn__label { + margin-left: $spacing-vertical * 1/3; + } + + .btn { + font: normal 400 16px/36px 'metropolis-semibold'; + + @media only screen and (min-width: $medium-breakpoint) { + font: normal 400 18px/40px 'metropolis-semibold'; + } + + @media only screen and (min-width: $large-breakpoint) { + font: normal 400 21px/50px 'metropolis-semibold'; + } + } + + .btn:hover { + color: var(--color-black); + } +} + +.nav__link--sub { + margin-left: 5px; + padding-left: $spacing-vertical * 2/3; + + .btn { + font: normal 400 14px/30px 'metropolis-medium'; + + @media only screen and (min-width: $medium-breakpoint) { + font: normal 400 15px/30px 'metropolis-semibold'; + } + + @media only screen and (min-width: $large-breakpoint) { + font: normal 400 18px/40px 'metropolis-medium'; + } + } +} + +.nav__link--active { + color: var(--color-black); +} + +.nav__sub-links { + padding-bottom: $spacing-vertical * 1/3; +} + +// Sub links animations +// The -appear, -leave classes are added by 'react-transition-group' +.nav__sub-appear, +.nav__sub-leave { + max-height: 0; + opacity: 0; +} + +.nav__sub-appear.nav__sub-appear-active { + // using max-height is a hack to animate to height "auto" + // Needs to be some arbitrarily large height + max-height: 500px; + opacity: 1; + transition: all var(--animation-duration) var(--animation-style); +} + +.nav__sub { + padding-top: 5px; +} diff --git a/src/renderer/scss/component/_radio.scss b/src/renderer/scss/component/_radio.scss deleted file mode 100644 index 4d511816f..000000000 --- a/src/renderer/scss/component/_radio.scss +++ /dev/null @@ -1,54 +0,0 @@ -$md-radio-checked-color: var(--color-primary); -$md-radio-border-color: var(--input-border-color); -$md-radio-size: 20px; -$md-radio-checked-size: 10px; -$md-radio-ripple-size: 15px; - -.form-field--radio { - position: relative; - - label { - cursor: pointer; - - &:before, - &:after { - content: ''; - position: absolute; - left: 0; - top: 0; - border-radius: 50%; - transition: all 0.3s ease; - transition-property: transform, border-color; - } - - &:before { - width: $md-radio-size; - height: $md-radio-size; - background: transparent; - border: 2px solid $md-radio-border-color; - cursor: pointer; - } - - &:after { - top: $md-radio-size / 2 - $md-radio-checked-size / 2; - left: $md-radio-size / 2 - $md-radio-checked-size / 2; - width: $md-radio-checked-size; - height: $md-radio-checked-size; - transform: scale(0); - background: $md-radio-checked-color; - } - } - - input[type='radio'] { - visibility: hidden; - margin-right: 16px; - - &:checked + label:before { - border-color: $md-radio-checked-color; - } - - &:checked + label:after { - transform: scale(1); - } - } -} diff --git a/src/renderer/scss/component/_search.scss b/src/renderer/scss/component/_search.scss new file mode 100644 index 000000000..b172f67a4 --- /dev/null +++ b/src/renderer/scss/component/_search.scss @@ -0,0 +1,97 @@ +.wunderbar { + z-index: 1; + flex: 1; + display: flex; + min-width: 175px; + cursor: text; + position: relative; + + > .icon { + position: absolute; + left: 10px; + top: 10px; + } +} + +.wunderbar__input { + height: var(--btn-height); + border-radius: var(--btn-radius); + width: 100%; + max-width: 700px; + color: var(--search-color); + background-color: var(--search-bg-color); + box-shadow: var(--box-shadow-wunderbar); + padding: 10px; + padding-left: 30px; + font-size: 13px; + font-family: 'metropolis-medium'; + display: flex; + align-items: center; + justify-content: center; + border-bottom: none; + + &:focus { + background-color: var(--color-bg); + border-radius: 0; + border-bottom: 1px solid var(--color-grey); + box-shadow: var(--box-shadow-button); + } +} + +.wunderbar__menu { + max-width: 100px; + overflow-x: hidden; +} + +.wunderbar__suggestion { + padding: 5px 10px; + background-color: var(--header-bg); + cursor: pointer; + display: flex; + align-items: center; + text-overflow: ellipsis; + font-family: 'metropolis-medium'; + + &:not(:first-of-type) { + border-top: 1px solid var(--color-divider); + } +} + +.wunderbar__suggestion-label { + padding-left: $spacing-vertical; +} + +.wunderbar__suggestion-label--action { + margin-left: $spacing-vertical * 1/3; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.wunderbar__active-suggestion { + background-color: var(--color-secondary); +} + +// Search modal +// Input field used inside search modal +.search__wrapper { + height: var(--search-modal-input-height); +} + +.search__input { + font-family: 'metropolis-bold'; + background: var(--color-black); + font-size: var(--search-modal-input-height); + color: var(--color-black); + background: var(--modal-bg); + width: 100%; +} + +.search__results { + display: flex; + padding-bottom: $spacing-vertical; + + .search-result__column { + flex: 0 0 270px; + } +} diff --git a/src/renderer/scss/component/_shapeshift.scss b/src/renderer/scss/component/_shapeshift.scss deleted file mode 100644 index cbe040180..000000000 --- a/src/renderer/scss/component/_shapeshift.scss +++ /dev/null @@ -1,39 +0,0 @@ -// Can't think of a better way to do this -// The initial shapeshift form is 311px tall -// the .shapeshift__initial-wrapper class is only added when the form is being loaded -// Once the form is rendered, there is a very smooth transition because the height doesn't change -.shapeshift__wrapper.shapeshift__initial-wrapper { - min-height: 346px; -} - -.shapeshift__content { - .spinner { - margin-top: $spacing-vertical * 3; - } -} - -.shapeshift__tx-info { - min-height: 63px; -} - -.shapeshift__deposit-address-wrapper { - display: flex; - flex-direction: row; - - * { - align-self: center; - } -} - -// this should be pulled out into it's own styling when we add more qr codes -.shapeshift__qrcode { - // don't use a variable here. adds a white border for easier reading in dark mode - // needs to stay the same no matter what theme is present - background-color: #fff; - padding: 2px; - margin-left: 40px; -} - -.shapeshift__link { - padding-left: 10px; -} diff --git a/src/renderer/scss/component/_snack-bar.scss b/src/renderer/scss/component/_snack-bar.scss index ccc81e0b9..3ded1d68a 100644 --- a/src/renderer/scss/component/_snack-bar.scss +++ b/src/renderer/scss/component/_snack-bar.scss @@ -1,11 +1,5 @@ -$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; + padding: $spacing-vertical; position: fixed; top: $spacing-vertical; left: 0; @@ -13,18 +7,14 @@ $padding-snack-horizontal: $spacing-vertical; margin-left: auto; margin-right: auto; min-width: 300px; - max-width: $width-page-constrained - $spacing-vertical * 2; + max-width: var(--snack-bar-width); background: var(--color-dark-overlay); color: #f0f0f0; - display: flex; justify-content: space-between; align-items: center; - border-radius: 2px; - transition: all var(--transition-duration) var(--transition-type); - z-index: 10000; /*hack to get it over react modal */ } @@ -32,7 +22,7 @@ $padding-snack-horizontal: $spacing-vertical; display: inline-block; text-transform: uppercase; color: var(--color-primary-light); - margin: 0px 0px 0px $padding-snack-horizontal; + margin: 0px 0px 0px $spacing-vertical; min-width: min-content; &:hover { text-decoration: underline; diff --git a/src/renderer/scss/component/_spinner.scss b/src/renderer/scss/component/_spinner.scss index f3e0485f7..09433cbb1 100644 --- a/src/renderer/scss/component/_spinner.scss +++ b/src/renderer/scss/component/_spinner.scss @@ -1,58 +1,49 @@ .spinner { - position: relative; - width: 11em; - height: 11em; - margin: 20px auto; - font-size: 3px; - border-radius: 50%; + margin: $spacing-vertical * 1/3; + width: 50px; + height: 40px; + text-align: center; + font-size: 10px; - background: linear-gradient(to right, #fff 10%, rgba(255, 255, 255, 0) 50%); - animation: spin 1.4s infinite linear; - transform: translateZ(0); + .rect { + display: inline-block; + height: 100%; + width: 6px; + margin: 0 2px; + background-color: var(--color-white); + animation: sk-stretchdelay 1.2s infinite ease-in-out; - @keyframes spin { - from { - transform: rotate(0deg); + &.rect2 { + animation-delay: -1.1s; } - to { - transform: rotate(360deg); + + &.rect3 { + animation-delay: -1s; } - } - &:before, - &:after { - content: ''; - position: absolute; - top: 0; - left: 0; - } + &.rect4 { + animation-delay: -0.9s; + } - &:before { - width: 50%; - height: 50%; - background: #fff; - border-radius: 100% 0 0 0; - } - - &:after { - height: 75%; - width: 75%; - margin: auto; - bottom: 0; - right: 0; - background: #000; - border-radius: 50%; + &.rect5 { + animation-delay: -0.8s; + } } } -.spinner.spinner--dark { - background: linear-gradient(to right, var(--button-primary-bg) 10%, var(--color-bg) 50%); - - &:before { - background: var(--button-primary-bg); - } - - &:after { - background: var(--color-bg); +.spinner--dark { + .rect { + background-color: var(--color-black); + } +} + +@keyframes sk-stretchdelay { + 0%, + 40%, + 100% { + transform: scaleY(0.4); + } + 20% { + transform: scaleY(1); } } diff --git a/src/renderer/scss/component/_table.scss b/src/renderer/scss/component/_table.scss index 428c5d152..c5df4f48e 100644 --- a/src/renderer/scss/component/_table.scss +++ b/src/renderer/scss/component/_table.scss @@ -1,40 +1,51 @@ -table.table-standard { +table.table { word-wrap: break-word; max-width: 100%; + text-align: left; + margin-top: $spacing-width; + + tr td:first-of-type, + tr th:first-of-type { + padding-left: $spacing-vertical * 2/3; + } + + tr td:last-of-type, + tr th:last-of-type { + padding-right: $spacing-vertical * 2/3; + } + + thead { + border-bottom: 1px solid var(--color-grey); + } th, td { - padding: $spacing-vertical/2 8px; + font-size: 13px; } + th { - font-weight: 500; - font-size: 0.9em; + font-family: 'metropolis-semibold'; } + td { - vertical-align: top; - } - thead th, - > tr:first-child th { - vertical-align: bottom; - font-weight: 500; - font-size: 0.9em; - padding: $spacing-vertical/4 + 1 8px $spacing-vertical/4-2; - text-align: left; - border-bottom: var(--table-border); - img { - vertical-align: text-bottom; - } - &.text-center { - text-align: center; + font-family: 'metropolis-medium'; + color: var(--color-grey-dark); + padding: $spacing-vertical * 1/6 0; + + .btn:not(.btn--link) { + display: inline; + margin-left: $spacing-vertical * 1/3; } } - tr.thead:not(:first-child) th { - border-top: var(--table-border); + + .table__item-label { + font-size: 12px; } - tfoot td { - padding: $spacing-vertical / 2 8px; - font-size: 0.85em; + + .table__item--actionable span + .btn { + padding-left: $spacing-vertical * 1/3; } + tbody { tr { &:nth-child(even):not(.odd) { @@ -52,28 +63,31 @@ table.table-standard { } } } -.table-standard--definition-list { - th { - text-align: right; - } -} -table.table-stretch { +table.table--stretch { width: 100%; } -table.table-transactions { +table.table--help { td:nth-of-type(1) { - width: 15%; + color: var(--color-black); + font-family: 'metropolis-semibold'; + min-width: 130px; + } +} + +table.table--transactions { + td:nth-of-type(1) { + width: 17.5%; } td:nth-of-type(2) { - width: 15%; + width: 27.5%; } td:nth-of-type(3) { - width: 15%; + width: 22.5%; } td:nth-of-type(4) { - width: 40%; + width: 17.5%; } td:nth-of-type(5) { width: 15%; diff --git a/src/renderer/scss/component/_tabs.scss b/src/renderer/scss/component/_tabs.scss deleted file mode 100644 index 69f6bc7a3..000000000 --- a/src/renderer/scss/component/_tabs.scss +++ /dev/null @@ -1,64 +0,0 @@ -/* Tabs */ - -nav.sub-header { - text-transform: uppercase; - max-width: $width-page-constrained; - margin-bottom: 40px; - border-bottom: var(--divider); - user-select: none; - > a { - height: 38px; - line-height: 38px; - text-align: center; - font-weight: 500; - text-transform: uppercase; - display: inline-block; - vertical-align: baseline; - margin: 0 12px; - padding: 0 8px; - color: var(--tab-color); - position: relative; - - &:first-child { - margin-left: 0; - } - &:last-child { - margin-right: 0; - } - &.sub-header-selected { - color: var(--tab-active-color); - &:before { - width: 100%; - height: var(--tab-border-size); - background: var(--tab-active-color); - position: absolute; - bottom: 0; - left: 0; - content: ''; - animation-name: activeTab; - animation-duration: var(--animation-duration); - animation-timing-function: var(--animation-style); - } - } - &:hover { - color: var(--tab-active-color); - } - } - - &.sub-header--full-width { - max-width: 100%; - } - - &.sub-header--small-margin { - margin-bottom: $spacing-vertical; - } -} - -@keyframes activeTab { - from { - width: 0; - } - to { - width: 100%; - } -} diff --git a/src/renderer/scss/component/_tooltip.scss b/src/renderer/scss/component/_tooltip.scss index 91fbe3f2f..504880b09 100644 --- a/src/renderer/scss/component/_tooltip.scss +++ b/src/renderer/scss/component/_tooltip.scss @@ -2,10 +2,8 @@ .tooltip { position: relative; -} - -.tooltip__link { - @include text-link(); + padding: 0 $spacing-vertical / 3; + font-size: 12px; } .tooltip__body { @@ -17,16 +15,15 @@ box-sizing: border-box; padding: $spacing-vertical / 2; width: var(--tooltip-width); - border: var(--tooltip-border); color: var(--tooltip-color); background-color: var(--tooltip-bg); font-size: calc(var(--font-size) * 7 / 8); line-height: var(--font-line-height); box-shadow: var(--box-shadow-layer); + border-radius: var(--card-radius); } -.tooltip--header .tooltip__link { - @include text-link(#aaa); +.tooltip__link { font-size: calc(var(--font-size) * 3 / 4); margin-left: var(--button-padding); vertical-align: middle; diff --git a/src/renderer/scss/component/_video.scss b/src/renderer/scss/component/_video.scss deleted file mode 100644 index aa449c48e..000000000 --- a/src/renderer/scss/component/_video.scss +++ /dev/null @@ -1,83 +0,0 @@ -$height-video-embedded: $width-page-constrained * 9 / 16; - -video { - object-fit: contain; - box-sizing: border-box; - max-height: 100%; - max-width: 100%; - background-size: contain; - background-position: center center; - background-repeat: no-repeat; -} - -.video { - background: #000; - color: white; -} - -.video-embedded { - max-width: $width-page-constrained; - max-height: $height-video-embedded; - height: $height-video-embedded; - position: relative; - video { - height: 100%; - width: 100%; - position: absolute; - top: 0; - left: 0; - } - &.video--hidden { - height: $height-video-embedded; - } -} -.video--obscured .video__cover { - position: relative; - filter: blur(var(--nsfw-blur-intensity)); -} - -.video__loading-screen { - height: 100%; - display: flex; - justify-content: center; - align-items: center; -} - -.video__loading-status { - padding-top: 20px; - color: white; -} - -.video__cover { - text-align: center; - height: 100%; - width: 100%; - background-size: auto 100%; - background-position: center center; - background-repeat: no-repeat; - position: relative; - .video__play-button { - display: flex; - align-items: center; - justify-content: 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: var(--color-dark-overlay); - opacity: 0.6; - left: 0; - top: 0; - &:hover { - opacity: 1; - transition: opacity var(--transition-duration) var(--transition-type); - } -} diff --git a/src/renderer/scss/page/_show.scss b/src/renderer/scss/page/_show.scss deleted file mode 100644 index 88ec2b7dd..000000000 --- a/src/renderer/scss/page/_show.scss +++ /dev/null @@ -1,13 +0,0 @@ -.show-page-media { - text-align: center; - margin-bottom: 16px; - overflow: auto; - img { - max-width: 100%; - } - - iframe { - width: 100%; - min-height: 500px; - } -} diff --git a/src/renderer/store.js b/src/renderer/store.js index ca6db3afd..b7ab9c01d 100644 --- a/src/renderer/store.js +++ b/src/renderer/store.js @@ -14,6 +14,7 @@ import walletReducer from 'redux/reducers/wallet'; import shapeShiftReducer from 'redux/reducers/shape_shift'; import subscriptionsReducer from 'redux/reducers/subscriptions'; import mediaReducer from 'redux/reducers/media'; +import publishReducer from 'redux/reducers/publish'; import { persistStore, autoRehydrate } from 'redux-persist'; import createCompressor from 'redux-persist-transform-compress'; import createFilter from 'redux-persist-transform-filter'; @@ -65,6 +66,7 @@ const reducers = combineReducers({ shapeShift: shapeShiftReducer, subscriptions: subscriptionsReducer, media: mediaReducer, + publish: publishReducer, }); const bulkThunk = createBulkThunkMiddleware(); @@ -96,7 +98,7 @@ const saveClaimsFilter = createFilter('claims', ['byId', 'claimsByUri']); const subscriptionsFilter = createFilter('subscriptions', ['subscriptions']); const persistOptions = { - whitelist: ['claims', 'subscriptions'], + whitelist: ['claims', 'subscriptions', 'navigation', 'publish'], // Order is important. Needs to be compressed last or other transforms can't // read the data transforms: [saveClaimsFilter, subscriptionsFilter, compressor], @@ -106,7 +108,7 @@ const persistOptions = { window.cacheStore = persistStore(store, persistOptions, err => { if (err) { - console.error('Unable to load saved SETTINGS'); + console.error('Unable to load saved settings'); // eslint-disable-line no-console } }); diff --git a/src/renderer/util/debounce.js b/src/renderer/util/debounce.js new file mode 100644 index 000000000..f702f7b0b --- /dev/null +++ b/src/renderer/util/debounce.js @@ -0,0 +1,21 @@ +// Returns a function, that, as long as it continues to be invoked, will not +// be triggered. The function will be called after it stops being called for +// N milliseconds. If `immediate` is passed, trigger the function on the +// leading edge, instead of the trailing. +export default function debouce(func, wait, immediate) { + let timeout; + + return function() { + const context = this; + const args = arguments; + const later = () => { + timeout = null; + if (!immediate) func.apply(context, args); + }; + + const callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) func.apply(context, args); + }; +} diff --git a/src/renderer/util/form-validation.js b/src/renderer/util/form-validation.js new file mode 100644 index 000000000..161e628d6 --- /dev/null +++ b/src/renderer/util/form-validation.js @@ -0,0 +1,23 @@ +// @flow +/* eslint-disable prefer-default-export */ +import { regexAddress } from 'lbryURI'; + +type DraftTxValues = { + address: string, + // amount: number +}; + +export const validateSendTx = (formValues: DraftTxValues) => { + const { address } = formValues; + const errors = {}; + + // All we need to check is if the address is valid + // If values are missing, users wont' be able to submit the form + if (address && !regexAddress.test(address)) { + errors.address = __('Not a valid LBRY address'); + } + + return errors; +}; + +/* eslint-enable prefer-default-export */ diff --git a/src/renderer/util/handle-fetch.js b/src/renderer/util/handle-fetch.js new file mode 100644 index 000000000..96ce44acb --- /dev/null +++ b/src/renderer/util/handle-fetch.js @@ -0,0 +1,5 @@ +export default function handleFetchResponse(response) { + return response.status === 200 + ? Promise.resolve(response.json()) + : Promise.reject(new Error(response.statusText)); +} diff --git a/static/font/.DS_Store b/static/font/.DS_Store new file mode 100644 index 000000000..c4dae0a21 Binary files /dev/null and b/static/font/.DS_Store differ diff --git a/static/font/FontAwesome.otf b/static/font/FontAwesome.otf deleted file mode 100644 index 401ec0f36..000000000 Binary files a/static/font/FontAwesome.otf and /dev/null differ diff --git a/static/font/fontawesome-webfont.eot b/static/font/fontawesome-webfont.eot deleted file mode 100644 index e9f60ca95..000000000 Binary files a/static/font/fontawesome-webfont.eot and /dev/null differ diff --git a/static/font/fontawesome-webfont.svg b/static/font/fontawesome-webfont.svg deleted file mode 100644 index 855c845e5..000000000 --- a/static/font/fontawesome-webfont.svg +++ /dev/null @@ -1,2671 +0,0 @@ - - - - -Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 - By ,,, -Copyright Dave Gandy 2016. All rights reserved. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/static/font/fontawesome-webfont.ttf b/static/font/fontawesome-webfont.ttf deleted file mode 100644 index 35acda2fa..000000000 Binary files a/static/font/fontawesome-webfont.ttf and /dev/null differ diff --git a/static/font/fontawesome-webfont.woff b/static/font/fontawesome-webfont.woff deleted file mode 100644 index 400014a4b..000000000 Binary files a/static/font/fontawesome-webfont.woff and /dev/null differ diff --git a/static/font/fontawesome-webfont.woff2 b/static/font/fontawesome-webfont.woff2 deleted file mode 100644 index 4d13fc604..000000000 Binary files a/static/font/fontawesome-webfont.woff2 and /dev/null differ diff --git a/static/font/metropolis/bold.eot b/static/font/metropolis/bold.eot new file mode 100644 index 000000000..48ded273f Binary files /dev/null and b/static/font/metropolis/bold.eot differ diff --git a/static/font/metropolis/bold.otf b/static/font/metropolis/bold.otf new file mode 100644 index 000000000..40a8a1682 Binary files /dev/null and b/static/font/metropolis/bold.otf differ diff --git a/static/font/metropolis/bold.svg b/static/font/metropolis/bold.svg new file mode 100644 index 000000000..92af03602 --- /dev/null +++ b/static/font/metropolis/bold.svg @@ -0,0 +1,875 @@ + + + + +Created by FontForge 20090622 at Fri Dec 8 09:51:27 2017 + By deploy user +Copyright © 2016 by Chris Simpson. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/font/metropolis/bold.ttf b/static/font/metropolis/bold.ttf new file mode 100644 index 000000000..f67993325 Binary files /dev/null and b/static/font/metropolis/bold.ttf differ diff --git a/static/font/metropolis/bold.woff b/static/font/metropolis/bold.woff new file mode 100644 index 000000000..f5508b97c Binary files /dev/null and b/static/font/metropolis/bold.woff differ diff --git a/static/font/metropolis/medium.eot b/static/font/metropolis/medium.eot new file mode 100644 index 000000000..b8d6eb90e Binary files /dev/null and b/static/font/metropolis/medium.eot differ diff --git a/static/font/metropolis/medium.otf b/static/font/metropolis/medium.otf new file mode 100644 index 000000000..aee4d98c4 Binary files /dev/null and b/static/font/metropolis/medium.otf differ diff --git a/static/font/metropolis/medium.svg b/static/font/metropolis/medium.svg new file mode 100644 index 000000000..42ae97670 --- /dev/null +++ b/static/font/metropolis/medium.svg @@ -0,0 +1,876 @@ + + + + +Created by FontForge 20090622 at Fri Dec 8 10:34:17 2017 + By deploy user +Copyright © 2016 by Chris Simpson. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/font/metropolis/medium.ttf b/static/font/metropolis/medium.ttf new file mode 100644 index 000000000..1bb9493cd Binary files /dev/null and b/static/font/metropolis/medium.ttf differ diff --git a/static/font/metropolis/medium.woff b/static/font/metropolis/medium.woff new file mode 100644 index 000000000..6e8b41eed Binary files /dev/null and b/static/font/metropolis/medium.woff differ diff --git a/static/font/metropolis/semibold.eot b/static/font/metropolis/semibold.eot new file mode 100644 index 000000000..8bb29729f Binary files /dev/null and b/static/font/metropolis/semibold.eot differ diff --git a/static/font/metropolis/semibold.otf b/static/font/metropolis/semibold.otf new file mode 100644 index 000000000..2fb1be69d Binary files /dev/null and b/static/font/metropolis/semibold.otf differ diff --git a/static/font/metropolis/semibold.svg b/static/font/metropolis/semibold.svg new file mode 100644 index 000000000..5181438f1 --- /dev/null +++ b/static/font/metropolis/semibold.svg @@ -0,0 +1,875 @@ + + + + +Created by FontForge 20090622 at Fri Dec 8 09:52:08 2017 + By deploy user +Copyright © 2016 by Chris Simpson. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/font/metropolis/semibold.ttf b/static/font/metropolis/semibold.ttf new file mode 100644 index 000000000..febf889f3 Binary files /dev/null and b/static/font/metropolis/semibold.ttf differ diff --git a/static/font/metropolis/semibold.woff b/static/font/metropolis/semibold.woff new file mode 100644 index 000000000..2f50d16ad Binary files /dev/null and b/static/font/metropolis/semibold.woff differ diff --git a/static/img/stripe-background.png b/static/img/stripe-background.png new file mode 100644 index 000000000..211c394a8 Binary files /dev/null and b/static/img/stripe-background.png differ diff --git a/static/themes/light.css b/static/themes/light.css index 4409f8f54..052bf632f 100644 --- a/static/themes/light.css +++ b/static/themes/light.css @@ -1,8 +1 @@ -:root { - /* Colors */ - --color-primary: #155B4A; - - /* search */ - --search-bg: var(--color-canvas); - --search-border: 1px solid rgba(0,0,0,0.15); -} +:root {} diff --git a/yarn.lock b/yarn.lock index a3fc62ffd..51eca2179 100644 --- a/yarn.lock +++ b/yarn.lock @@ -118,8 +118,8 @@ to-fast-properties "^2.0.0" "@types/node@^7.0.18": - version "7.0.56" - resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.56.tgz#b6b659049191822be43c14610c1785d4b9cddecf" + version "7.0.57" + resolved "https://registry.yarnpkg.com/@types/node/-/node-7.0.57.tgz#eed149b2c75cdbd7b9823c3fd64ecddbdc68ed9c" "@types/webpack-env@^1.13.5": version "1.13.5" @@ -492,8 +492,8 @@ autoprefixer@^6.3.1: postcss-value-parser "^3.2.3" aws-sdk@^2.212.1: - version "2.212.1" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.212.1.tgz#833dbe8d837d46058a3772ac9bb73b69113136e4" + version "2.213.1" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.213.1.tgz#bcbfdf1fd7439a948bf53c46bd90c97b4a64925b" dependencies: buffer "4.9.1" events "1.1.1" @@ -1473,10 +1473,25 @@ browserslist@^2.1.2: caniuse-lite "^1.0.30000792" electron-to-chromium "^1.3.30" +buffer-alloc-unsafe@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-0.1.1.tgz#ffe1f67551dd055737de253337bfe853dfab1a6a" + +buffer-alloc@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.1.0.tgz#05514d33bf1656d3540c684f65b1202e90eca303" + dependencies: + buffer-alloc-unsafe "^0.1.0" + buffer-fill "^0.1.0" + buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" +buffer-fill@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-0.1.0.tgz#ca9470e8d4d1b977fd7543f4e2ab6a7dc95101a8" + buffer-from@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.0.0.tgz#4cb8832d23612589b0406e9e2956c17f06fdf531" @@ -1697,6 +1712,10 @@ center-align@^0.1.1: align-text "^0.1.3" lazy-cache "^1.0.3" +chain-function@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/chain-function/-/chain-function-1.0.0.tgz#0d4ab37e7e18ead0bdc47b920764118ce58733dc" + chainsaw@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/chainsaw/-/chainsaw-0.1.0.tgz#5eab50b28afe58074d0d58291388828b5e5fbc98" @@ -1730,8 +1749,8 @@ charenc@~0.0.1: resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" chokidar@^2.0.0, chokidar@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.2.tgz#4dc65139eeb2714977735b6a35d06e97b494dfd7" + version "2.0.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.3.tgz#dcbd4f6cbb2a55b4799ba8a840ac527e5f4b1176" dependencies: anymatch "^2.0.0" async-each "^1.0.0" @@ -1745,7 +1764,7 @@ chokidar@^2.0.0, chokidar@^2.0.2: readdirp "^2.0.0" upath "^1.0.0" optionalDependencies: - fsevents "^1.0.0" + fsevents "^1.1.2" chownr@^1.0.1: version "1.0.1" @@ -2646,6 +2665,14 @@ dom-converter@~0.1: dependencies: utila "~0.3" +dom-helpers@^3.2.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.3.1.tgz#fc1a4e15ffdf60ddde03a480a9c0fece821dd4a6" + +dom-scroll-into-view@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/dom-scroll-into-view/-/dom-scroll-into-view-1.2.1.tgz#e8f36732dd089b0201a88d7815dc3f88e6d66c7e" + dom-serializer@0: version "0.1.0" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" @@ -3926,7 +3953,7 @@ fresh@0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" -from2@^2.1.0: +from2@^2.1.0, from2@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" dependencies: @@ -3979,7 +4006,7 @@ fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" -fsevents@^1.0.0: +fsevents@^1.1.2: version "1.1.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.3.tgz#11f82318f5fe7bb2cd22965a108e9306208216d8" dependencies: @@ -4672,6 +4699,10 @@ inquirer@^3.0.6: strip-ansi "^4.0.0" through "^2.3.6" +install@^0.10.2: + version "0.10.4" + resolved "https://registry.yarnpkg.com/install/-/install-0.10.4.tgz#9cb09115768b93a582d1450a6ba3f275975b49aa" + internal-ip@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-1.2.0.tgz#ae9fbf93b984878785d50a8de1b356956058cf5c" @@ -4933,8 +4964,8 @@ is-path-cwd@^1.0.0: resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" is-path-in-cwd@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz#6477582b8214d602346094567003be8a9eac04dc" + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz#5ac48b345ef675339bd6c7a48a912110b241cf52" dependencies: is-path-inside "^1.0.0" @@ -5285,6 +5316,10 @@ jsesc@~0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" +jshashes@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/jshashes/-/jshashes-1.0.7.tgz#bed8c97a0e9632fd0513916f55f76dd5486be59f" + json-loader@^0.5.4: version "0.5.7" resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.7.tgz#dca14a70235ff82f0ac9a3abeb60d337a365185d" @@ -5368,12 +5403,12 @@ jsx-ast-utils@^2.0.0, jsx-ast-utils@^2.0.1: dependencies: array-includes "^3.0.3" -keytar-prebuild@4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/keytar-prebuild/-/keytar-prebuild-4.0.4.tgz#eb6354c68f2b3609dc325ef8709844632652d602" +keytar-prebuild@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/keytar-prebuild/-/keytar-prebuild-4.1.1.tgz#f31cd3b2e5de743303f8c2f607f29f0117981295" dependencies: - nan "2.7.0" - prebuild-install "^2.2.2" + nan "2.8.0" + prebuild-install "^2.5.0" killable@^1.0.0: version "1.0.0" @@ -5765,8 +5800,8 @@ map-visit@^1.0.0: object-visit "^1.0.0" marked@*: - version "0.3.17" - resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.17.tgz#607f06668b3c6b1246b28f13da76116ac1aa2d2b" + version "0.3.18" + resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.18.tgz#3ef058cd926101849b92a7a7c15db18c7fc76b2f" math-expression-evaluator@^1.2.14: version "1.2.17" @@ -5862,8 +5897,8 @@ micromatch@^2.3.11: regex-cache "^0.4.2" micromatch@^3.1.4, micromatch@^3.1.8: - version "3.1.9" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.9.tgz#15dc93175ae39e52e93087847096effc73efcf89" + version "3.1.10" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" dependencies: arr-diff "^4.0.0" array-unique "^0.3.2" @@ -5877,7 +5912,7 @@ micromatch@^3.1.4, micromatch@^3.1.8: object.pick "^1.3.0" regex-not "^1.0.0" snapdragon "^0.8.1" - to-regex "^3.0.1" + to-regex "^3.0.2" miller-rabin@^4.0.0: version "4.0.1" @@ -6009,15 +6044,18 @@ move-concurrently@^1.0.1: run-queue "^1.0.3" mp4-box-encoding@^1.1.0, mp4-box-encoding@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/mp4-box-encoding/-/mp4-box-encoding-1.1.2.tgz#39850ee05ba5370460070b3a2acbd07616e2d831" + version "1.1.3" + resolved "https://registry.yarnpkg.com/mp4-box-encoding/-/mp4-box-encoding-1.1.3.tgz#1e30f37ba0907e153ef3fb0ac34ca47391940e40" dependencies: + buffer-alloc "^1.1.0" + buffer-from "^1.0.0" uint64be "^1.0.1" mp4-stream@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/mp4-stream/-/mp4-stream-2.0.2.tgz#34161ba2d9b608733b4b2247edf3780ba2c47ec5" + version "2.0.3" + resolved "https://registry.yarnpkg.com/mp4-stream/-/mp4-stream-2.0.3.tgz#30acee07709d323f8dcd87a07b3ce9c3c4bfb364" dependencies: + buffer-alloc "^1.1.0" inherits "^2.0.1" mp4-box-encoding "^1.1.0" next-event "^1.0.0" @@ -6049,9 +6087,9 @@ mute-stream@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" -nan@2.7.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.7.0.tgz#d95bf721ec877e08db276ed3fc6eb78f9083ad46" +nan@2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a" nan@^2.3.0, nan@^2.3.2: version "2.10.0" @@ -6979,14 +7017,14 @@ postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0 supports-color "^3.2.3" postcss@^6.0.1: - version "6.0.20" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.20.tgz#686107e743a12d5530cb68438c590d5b2bf72c3c" + version "6.0.21" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.21.tgz#8265662694eddf9e9a5960db6da33c39e4cd069d" dependencies: chalk "^2.3.2" source-map "^0.6.1" supports-color "^5.3.0" -prebuild-install@^2.2.2: +prebuild-install@^2.5.0: version "2.5.1" resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-2.5.1.tgz#0f234140a73760813657c413cdccdda58296b1da" dependencies: @@ -7084,7 +7122,7 @@ promise@^7.0.3, promise@^7.1.1: dependencies: asap "~2.0.3" -prop-types@^15.5.1, prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1: +prop-types@^15.5.1, prop-types@^15.5.10, prop-types@^15.5.6, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1: version "15.6.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.1.tgz#36644453564255ddda391191fb3a125cbdf654ca" dependencies: @@ -7270,6 +7308,10 @@ react-dom@^16.2.0: object-assign "^4.1.1" prop-types "^15.6.0" +react-feather@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/react-feather/-/react-feather-1.0.8.tgz#69b13d5c729949f194d33201dee91bab67fa31a2" + react-markdown@^2.5.0: version "2.5.1" resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-2.5.1.tgz#f7a6c26a3a5faf5d4c2098155d9775e826fd56ee" @@ -7310,6 +7352,16 @@ react-simplemde-editor@^3.6.11: react "^0.14.2" simplemde "^1.11.2" +react-transition-group@1.x: + version "1.2.1" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-1.2.1.tgz#e11f72b257f921b213229a774df46612346c7ca6" + dependencies: + chain-function "^1.0.0" + dom-helpers "^3.2.0" + loose-envify "^1.3.1" + prop-types "^15.5.6" + warning "^3.0.0" + react@^0.14.2: version "0.14.9" resolved "https://registry.yarnpkg.com/react/-/react-0.14.9.tgz#9110a6497c49d44ba1c0edd317aec29c2e0d91d1" @@ -8678,7 +8730,7 @@ to-regex-range@^2.1.0: is-number "^3.0.0" repeat-string "^1.6.1" -to-regex@^3.0.1: +to-regex@^3.0.1, to-regex@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" dependencies: