Make the app pretty #1173

Merged
neb-b merged 1 commit from redesign into master 2018-03-26 23:34:17 +02:00
249 changed files with 11090 additions and 10904 deletions

View file

@ -33,6 +33,7 @@
"printWidth": 100, "printWidth": 100,
"singleQuote": true "singleQuote": true
}], }],
"func-names": ["warn", "as-needed"] "func-names": ["warn", "as-needed"],
"arrow-body-style": "off"
} }
} }

View file

@ -17,5 +17,8 @@ module.name_mapper='^types\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/types\1'
module.name_mapper='^component\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/component\1' module.name_mapper='^component\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/component\1'
module.name_mapper='^page\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/page\1' module.name_mapper='^page\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/page\1'
module.name_mapper='^lbry\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/lbry\1' module.name_mapper='^lbry\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/lbry\1'
module.name_mapper='^rewards\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/rewards\1'
module.name_mapper='^modal\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/modal\1'
module.name_mapper='^app\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/app\1'
[strict] [strict]

1
.gitignore vendored
View file

@ -4,3 +4,4 @@
/static/daemon/lbrynet* /static/daemon/lbrynet*
/static/locales /static/locales
yarn-error.log yarn-error.log
npm-debug.log*

3
flow-typed/react-feather.js vendored Normal file
View file

@ -0,0 +1,3 @@
declare module 'react-feather' {
declare module.exports: any;
}

3
flow-typed/react-markdown.js vendored Normal file
View file

@ -0,0 +1,3 @@
declare module 'react-markdown' {
declare module.exports: any;
}

3
flow-typed/react-modal.js vendored Normal file
View file

@ -0,0 +1,3 @@
declare module 'react-modal' {
declare module.exports: any;
}

3
flow-typed/react-paginate.js vendored Normal file
View file

@ -0,0 +1,3 @@
declare module 'react-paginate' {
declare module.exports: any;
}

7
flow-typed/react-simplemde-editor.js vendored Normal file
View file

@ -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;
}

3
flow-typed/react-transition-group.js vendored Normal file
View file

@ -0,0 +1,3 @@
declare module 'react-transition-group' {
declare module.exports: any;
}

3
flow-typed/render-media.js vendored Normal file
View file

@ -0,0 +1,3 @@
declare module 'render-media' {
declare module.exports: any;
}

View file

@ -38,6 +38,7 @@
"bluebird": "^3.5.1", "bluebird": "^3.5.1",
"classnames": "^2.2.5", "classnames": "^2.2.5",
"country-data": "^0.0.31", "country-data": "^0.0.31",
"dom-scroll-into-view": "^1.2.1",
"electron-dl": "^1.11.0", "electron-dl": "^1.11.0",
"electron-is-dev": "^0.3.0", "electron-is-dev": "^0.3.0",
"electron-log": "^2.2.12", "electron-log": "^2.2.12",
@ -46,7 +47,10 @@
"electron-window-state": "^4.1.1", "electron-window-state": "^4.1.1",
"find-process": "^1.1.0", "find-process": "^1.1.0",
"formik": "^0.10.4", "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", "localforage": "^1.5.0",
"mixpanel-browser": "^2.17.1", "mixpanel-browser": "^2.17.1",
"moment": "^2.20.1", "moment": "^2.20.1",
@ -54,11 +58,13 @@
"rc-progress": "^2.0.6", "rc-progress": "^2.0.6",
"react": "^16.2.0", "react": "^16.2.0",
"react-dom": "^16.2.0", "react-dom": "^16.2.0",
"react-feather": "^1.0.8",
"react-markdown": "^2.5.0", "react-markdown": "^2.5.0",
"react-modal": "^3.1.7", "react-modal": "^3.1.7",
"react-paginate": "^5.2.1", "react-paginate": "^5.2.1",
"react-redux": "^5.0.3", "react-redux": "^5.0.3",
"react-simplemde-editor": "^3.6.11", "react-simplemde-editor": "^3.6.11",
"react-transition-group": "1.x",
"redux": "^3.6.0", "redux": "^3.6.0",
"redux-logger": "^3.0.1", "redux-logger": "^3.0.1",
"redux-persist": "^4.8.0", "redux-persist": "^4.8.0",

View file

@ -16,8 +16,8 @@ export default appState => {
}); });
let windowConfiguration = { let windowConfiguration = {
backgroundColor: '#155B4A', backgroundColor: '#44b098',
minWidth: 800, minWidth: 950,
minHeight: 600, minHeight: 600,
autoHideMenuBar: true, autoHideMenuBar: true,
show: false, show: false,

View file

@ -1,52 +1,52 @@
import React from 'react'; // @flow
import PropTypes from 'prop-types'; import * as React from 'react';
import { clipboard } from 'electron'; import { clipboard } from 'electron';
import Link from 'component/link'; import { FormRow } from 'component/common/form';
import classnames from 'classnames'; import Button from 'component/button';
import * as icons from 'constants/icons';
export default class Address extends React.PureComponent { type Props = {
static propTypes = { address: string,
address: PropTypes.string, doShowSnackBar: ({ message: string }) => void,
}; };
constructor(props) { export default class Address extends React.PureComponent<Props> {
super(props); constructor() {
super();
this._inputElem = null; this.input = null;
} }
input: ?HTMLInputElement;
render() { render() {
const { address, showCopyButton, doShowSnackBar } = this.props; const { address, doShowSnackBar } = this.props;
return ( return (
<div className="form-field form-field--address"> <FormRow verticallyCentered padded>
<input <input
className={classnames('input-copyable', { className="input-copyable form-field__input"
'input-copyable--with-copy-btn': showCopyButton, readOnly
})} value={address || ''}
type="text"
ref={input => { ref={input => {
this._inputElem = input; this.input = input;
}} }}
onFocus={() => { onFocus={() => {
this._inputElem.select(); if (this.input) {
this.input.select();
}
}} }}
readOnly="readonly"
value={address || ''}
/> />
{showCopyButton && ( <Button
<span className="header__item"> noPadding
<Link button="secondary"
button="alt button--flat" icon={icons.CLIPBOARD}
icon="clipboard"
onClick={() => { onClick={() => {
clipboard.writeText(address); clipboard.writeText(address);
doShowSnackBar({ message: __('Address copied') }); doShowSnackBar({ message: __('Address copied') });
}} }}
/> />
</span> </FormRow>
)}
</div>
); );
} }
} }

View file

@ -1,4 +1,3 @@
import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
selectPageTitle, selectPageTitle,
@ -10,7 +9,7 @@ import { doAlertError } from 'redux/actions/app';
import { doRecordScroll } from 'redux/actions/navigation'; import { doRecordScroll } from 'redux/actions/navigation';
import App from './view'; import App from './view';
const select = (state, props) => ({ const select = state => ({
pageTitle: selectPageTitle(state), pageTitle: selectPageTitle(state),
user: selectUser(state), user: selectUser(state),
currentStackIndex: selectHistoryIndex(state), currentStackIndex: selectHistoryIndex(state),

View file

@ -1,68 +1,95 @@
// @flow
import React from 'react'; import React from 'react';
import Router from 'component/router/index'; import Router from 'component/router/index';
import Header from 'component/header';
import Theme from 'component/theme'; import Theme from 'component/theme';
import ModalRouter from 'modal/modalRouter'; import ModalRouter from 'modal/modalRouter';
import ReactModal from 'react-modal'; import ReactModal from 'react-modal';
import throttle from 'util/throttle'; import throttle from 'util/throttle';
import SideBar from 'component/sideBar';
import Header from 'component/header';
class App extends React.PureComponent { type Props = {
alertError: (string | {}) => void,
recordScroll: number => void,
currentStackIndex: number,
currentPageAttributes: { path: string, scrollY: number },
pageTitle: ?string,
};
class App extends React.PureComponent<Props> {
constructor() { constructor() {
super(); super();
this.mainContent = undefined; this.mainContent = undefined;
(this: any).scrollListener = this.scrollListener.bind(this);
} }
componentWillMount() { componentWillMount() {
const { alertError } = this.props; const { alertError } = this.props;
document.addEventListener('unhandledError', event => { // TODO: create type for this object
// it lives in jsonrpc.js
document.addEventListener('unhandledError', (event: any) => {
alertError(event.detail); alertError(event.detail);
}); });
} }
componentDidMount() { componentDidMount() {
const { recordScroll } = this.props; const mainContent = document.getElementById('content');
const mainContent = document.getElementById('main-content');
this.mainContent = mainContent; this.mainContent = mainContent;
const scrollListener = () => recordScroll(this.mainContent.scrollTop); if (this.mainContent) {
this.mainContent.addEventListener('scroll', throttle(this.scrollListener, 750));
this.mainContent.addEventListener('scroll', throttle(scrollListener, 750)); }
ReactModal.setAppElement('#window'); // fuck this ReactModal.setAppElement('#window'); // fuck this
} }
componentWillUnmount() { componentWillReceiveProps(props: Props) {
this.mainContent.removeEventListener('scroll', this.scrollListener); const { pageTitle } = props;
this.setTitleFromProps(pageTitle);
} }
componentWillReceiveProps(props) { componentDidUpdate(prevProps: Props) {
this.setTitleFromProps(props);
}
componentDidUpdate(prevProps) {
const { currentStackIndex: prevStackIndex } = prevProps; const { currentStackIndex: prevStackIndex } = prevProps;
const { currentStackIndex, currentPageAttributes } = this.props; const { currentStackIndex, currentPageAttributes } = this.props;
if (currentStackIndex !== prevStackIndex) { if (this.mainContent && currentStackIndex !== prevStackIndex && currentPageAttributes) {
this.mainContent.scrollTop = currentPageAttributes.scrollY || 0; this.mainContent.scrollTop = currentPageAttributes.scrollY || 0;
} }
} }
setTitleFromProps(props) { componentWillUnmount() {
window.document.title = props.pageTitle || 'LBRY'; if (this.mainContent) {
this.mainContent.removeEventListener('scroll', this.scrollListener);
} }
}
setTitleFromProps = (title: ?string) => {
window.document.title = title || 'LBRY';
};
scrollListener() {
const { recordScroll } = this.props;
if (this.mainContent) {
recordScroll(this.mainContent.scrollTop);
}
}
mainContent: ?HTMLElement;
render() { render() {
return ( return (
<div id="window"> <div id="window">
<Theme /> <Theme />
<main className="page">
<SideBar />
<Header /> <Header />
<div id="main-content"> <div className="content" id="content">
<Router /> <Router />
</div>
<ModalRouter /> <ModalRouter />
</div> </div>
</main>
</div>
); );
} }
} }

View file

@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doNavigate } from 'redux/actions/navigation'; import { doNavigate } from 'redux/actions/navigation';
import Link from './view'; import Button from './view';
const perform = dispatch => ({ const perform = dispatch => ({
doNavigate: (path, params) => dispatch(doNavigate(path, params)), doNavigate: (path, params) => dispatch(doNavigate(path, params)),
}); });
export default connect(null, perform)(Link); export default connect(null, perform)(Button);

View file

@ -0,0 +1,111 @@
// @flow
import * as React from 'react';
import Icon from 'component/common/icon';
import classnames from 'classnames';
type Props = {
onClick: ?(any) => any,
href: ?string,
title: ?string,
label: ?string,
icon: ?string,
iconRight: ?string,
disabled: ?boolean,
children: ?React.Node,
navigate: ?string,
// TODO: these (nav) should be a reusable type
doNavigate: (string, ?any) => void,
navigateParams: any,
className: ?string,
description: ?string,
type: string,
button: ?string, // primary, secondary, alt, link
noPadding: ?boolean, // to remove padding and allow circular buttons
uppercase: ?boolean,
};
class Button extends React.PureComponent<Props> {
static defaultProps = {
type: 'button',
};
render() {
const {
onClick,
href,
title,
label,
icon,
iconRight,
disabled,
children,
navigate,
navigateParams,
doNavigate,
className,
description,
button,
type,
noPadding,
uppercase,
...otherProps
} = this.props;
const combinedClassName = classnames(
'btn',
{
'btn--no-padding': noPadding,
},
button
? {
'btn--primary': button === 'primary',
'btn--secondary': button === 'secondary',
'btn--alt': button === 'alt',
'btn--danger': button === 'danger',
'btn--inverse': button === 'inverse',
'btn--disabled': disabled,
'btn--link': button === 'link',
'btn--external-link': button === 'link' && href,
'btn--uppercase': uppercase,
}
: 'btn--no-style',
className
);
const extendedOnClick =
!onClick && navigate
? event => {
event.stopPropagation();
doNavigate(navigate, navigateParams || {});
}
: onClick;
const content = (
<span className="btn__content">
{icon && <Icon icon={icon} />}
{label && <span className="btn__label">{label}</span>}
{children && children}
{iconRight && <Icon icon={iconRight} />}
</span>
);
return href ? (
<a className={combinedClassName} href={href} title={title}>
{content}
</a>
) : (
<button
aria-label={description || label || title}
className={combinedClassName}
onClick={extendedOnClick}
disabled={disabled}
type={type}
{...otherProps}
>
{content}
</button>
);
}
}
export default Button;

View file

@ -1,7 +1,13 @@
// @flow
import React from 'react'; import React from 'react';
import classnames from 'classnames';
class CardMedia extends React.PureComponent { type Props = {
static AUTO_THUMB_CLASSES = [ thumbnail: ?string, // externally sourced image
nsfw: ?boolean,
};
const autoThumbColors = [
'purple', 'purple',
'red', 'red',
'pink', 'pink',
@ -13,34 +19,33 @@ class CardMedia extends React.PureComponent {
'green', 'green',
'yellow', 'yellow',
'orange', 'orange',
]; ];
componentWillMount() { class CardMedia extends React.PureComponent<Props> {
this.setState({ getAutoThumbClass = () => {
autoThumbClass: return autoThumbColors[Math.floor(Math.random() * autoThumbColors.length)];
CardMedia.AUTO_THUMB_CLASSES[ };
Math.floor(Math.random() * CardMedia.AUTO_THUMB_CLASSES.length)
],
});
}
render() { render() {
const { title, thumbnail } = this.props; const { thumbnail, nsfw } = this.props;
const atClass = this.state.autoThumbClass;
if (thumbnail) { const generateAutothumb = !thumbnail && !nsfw;
return <div className="card__media" style={{ backgroundImage: `url('${thumbnail}')` }} />; let autoThumbClass;
if (generateAutothumb) {
autoThumbClass = `card__media--autothumb.${this.getAutoThumbClass()}`;
} }
return ( return (
<div className={`card__media card__media--autothumb ${atClass}`}> <div
<div className="card__autothumb__text"> style={thumbnail && !nsfw ? { backgroundImage: `url('${thumbnail}')` } : {}}
{title && className={classnames('card__media', autoThumbClass, {
title 'card__media--no-img': !thumbnail || nsfw,
.replace(/\s+/g, '') 'card__media--nsfw': nsfw,
.substring(0, Math.min(title.replace(' ', '').length, 5)) })}
.toUpperCase()} >
</div> {(!thumbnail || nsfw) && (
<span className="card__media-text">{nsfw ? __('NSFW') : 'LBRY'}</span>
)}
</div> </div>
); );
} }

View file

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; 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 scriptLoading = false;
let scriptLoaded = false; let scriptLoaded = false;
@ -156,10 +157,10 @@ class CardVerify extends React.Component {
render() { render() {
return ( return (
<Link <Button
button="alt" button="alt"
label={this.props.label} label={this.props.label}
icon="icon-lock" icon={icons.LOCK}
disabled={this.props.disabled || this.state.open || this.hasPendingClick} disabled={this.props.disabled || this.state.open || this.hasPendingClick}
onClick={this.onClick.bind(this)} onClick={this.onClick.bind(this)}
/> />

View file

@ -1,18 +1,36 @@
import React from 'react'; // @flow
import * as React from 'react';
import CardMedia from 'component/cardMedia'; 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<Props> {
componentDidMount() { componentDidMount() {
const { uri, resolveUri } = this.props; const { uri, resolveUri } = this.props;
resolveUri(uri); resolveUri(uri);
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps: Props) {
const { uri, resolveUri } = this.props; const { uri, resolveUri } = this.props;
if (nextProps.uri != uri) { if (nextProps.uri !== uri) {
resolveUri(uri); resolveUri(uri);
} }
} }
@ -29,29 +47,25 @@ class ChannelTile extends React.PureComponent {
const onClick = () => navigate('/show', { uri }); const onClick = () => navigate('/show', { uri });
return ( return (
<section className="file-tile card"> <section className="file-tile card--link" onClick={onClick} role="button">
<div onClick={onClick} className="card__link"> <CardMedia title={channelName} thumbnail={null} />
<div className="card__inner file-tile__row"> <div className="file-tile__info">
{channelName && <CardMedia title={channelName} thumbnail={null} />} {isResolvingUri && <div className="card__title--small">{__('Loading...')}</div>}
<div className="file-tile__content"> {!isResolvingUri && (
<div className="card__title-primary"> <React.Fragment>
<h3> <div className="card__title--small card__title--file">
<TruncatedText lines={1}>{channelName || uri}</TruncatedText> <TruncatedText lines={1}>{channelName || uri}</TruncatedText>
</h3>
</div> </div>
<div className="card__content card__subtext"> <div className="card__subtitle">
{isResolvingUri && <BusyMessage message={__('Resolving channel')} />}
{totalItems > 0 && ( {totalItems > 0 && (
<span> <span>
This is a channel with {totalItems} {totalItems === 1 ? ' item' : ' items'}{' '} {totalItems} {totalItems === 1 ? 'file' : 'files'}
inside of it.
</span> </span>
)} )}
{!isResolvingUri && {!isResolvingUri && !totalItems && <span>This is an empty channel.</span>}
!totalItems && <span className="empty">This is an empty channel.</span>}
</div>
</div>
</div> </div>
</React.Fragment>
)}
</div> </div>
</section> </section>
); );

View file

@ -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 (
<span className="truncated-text" style={{ WebkitLineClamp: this.props.lines }}>
{this.props.children}
</span>
);
}
}
export class BusyMessage extends React.PureComponent {
static propTypes = {
message: PropTypes.string,
};
render() {
return (
<span>
{this.props.message} <span className="busy-indicator" />
</span>
);
}
}
export class CurrencySymbol extends React.PureComponent {
render() {
return <span>LBC</span>;
}
}
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 (
<span className={`credit-amount credit-amount--${this.props.look}`} title={fullPrice}>
<span>{amountText}</span>
{this.props.isEstimate ? (
<span
className="credit-amount__estimate"
title={__('This is an estimate and does not include data fees')}
>
*
</span>
) : null}
</span>
);
}
}
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 (
<img
ref="img"
onError={() => {
this.handleError();
}}
{...otherProps}
className={className}
src={this.state.imageUri}
/>
);
}
}

View file

@ -0,0 +1,16 @@
// @flow
import React from 'react';
type Props = {
message: ?string,
};
const BusyIndicator = (props: Props) => {
return (
<span className="busy-indicator">
{props.message} <span className="busy-indicator__loader" />
</span>
);
};
export default BusyIndicator;

View file

@ -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<string>,
categoryLink?: string,
};
type State = {
canScrollNext: boolean,
canScrollPrevious: boolean,
};
class CategoryList extends React.PureComponent<Props, State> {
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 <button> instead of <a>
// We are using buttons, needs more exploration
return (
<div className="card-row">
<div className="card-row__header">
<div className="card-row__title">
{categoryLink ? (
<Button label={category} navigate="/show" navigateParams={{ uri: categoryLink }} />
) : (
category
)}
{category &&
category.match(/^community/i) && (
<ToolTip
label={__("What's this?")}
body={__(
'Community Content is a public space where anyone can share content with the rest of the LBRY community. Bid on the names "one," "two," "three," "four" and "five" to put your content here!'
)}
/>
)}
</div>
<div className="card-row__scroll-btns">
<Button
className="btn--arrow"
disabled={!canScrollPrevious}
onClick={this.handleScrollPrevious}
icon={icons.ARROW_LEFT}
/>
<Button
className="btn--arrow"
disabled={!canScrollNext}
onClick={this.handleScrollNext}
icon={icons.ARROW_RIGHT}
/>
</div>
</div>
<div
ref={ref => {
this.rowItems = ref;
}}
className="card-row__scrollhouse"
>
{names &&
names.map(name => <FileCard key={name} displayStyle="card" uri={normalizeURI(name)} />)}
</div>
</div>
);
}
}
export default CategoryList;

View file

@ -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<Props> {
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 (
<span
title={fullPrice}
className={classnames('credit-amount', {
'credit-amount--free': !large && isFree,
'credit-amount--cost': !large && !isFree,
'credit-amount--large': large,
'credit-amount--plain': plain,
'credit-amount--no-style': noStyle,
})}
>
{amountText}
{isEstimate ? (
<span
className="credit-amount__estimate"
title={__('This is an estimate and does not include data fees')}
>
*
</span>
) : null}
</span>
);
}
}
export default CreditAmount;

View file

@ -1,31 +1,35 @@
// @flow
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Link from 'component/link'; import Button from 'component/button';
import parseData from 'util/parseData'; import parseData from 'util/parseData';
import * as icons from 'constants/icons'; import * as icons from 'constants/icons';
const { remote } = require('electron'); import { remote } from 'electron';
class FileExporter extends React.PureComponent { type Props = {
static propTypes = { data: Array<any>,
data: PropTypes.array, title: string,
title: PropTypes.string, label: string,
label: PropTypes.string, defaultPath?: string,
filters: PropTypes.arrayOf(PropTypes.string), filters: Array<string>,
defaultPath: PropTypes.string, onFileCreated?: string => void,
onFileCreated: PropTypes.func, };
};
class FileExporter extends React.PureComponent<Props> {
static defaultProps = { static defaultProps = {
filters: [], filters: [],
}; };
constructor(props) { constructor() {
super(props); super();
this.handleButtonClick = this.handleButtonClick.bind(this);
} }
handleFileCreation(filename, data) { handleButtonClick: () => void;
handleFileCreation(filename: string, data: any) {
const { onFileCreated } = this.props; const { onFileCreated } = this.props;
fs.writeFile(filename, data, err => { fs.writeFile(filename, data, err => {
if (err) throw err; if (err) throw err;
@ -67,12 +71,11 @@ class FileExporter extends React.PureComponent {
render() { render() {
const { title, label } = this.props; const { title, label } = this.props;
return ( return (
<Link <Button
button="primary" button="primary"
icon={icons.DOWNLOAD} icon={icons.DOWNLOAD}
title={title || __('Export')}
label={label || __('Export')} label={label || __('Export')}
onClick={() => this.handleButtonClick()} onClick={this.handleButtonClick}
/> />
); );
} }

View file

@ -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<Props> {
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 (
<FormRow verticallyCentered padded>
<Button
button="primary"
onClick={() => this.handleButtonClick()}
label={type === 'file' ? __('Choose File') : __('Choose Directory')}
/>
<input
webkitdirectory="true"
className="input-copyable"
type="text"
ref={input => {
if (this.input) this.input = input;
}}
onFocus={() => {
if (this.input) this.input.select();
}}
readOnly="readonly"
value={currentPath || __('No File Chosen')}
/>
</FormRow>
);
}
}
export default FileSelector;

View file

@ -0,0 +1,77 @@
// @flow
import * as React from 'react';
import type { Price } from 'page/settings';
import { FormField } from './form-field';
import { FormRow } from './form-row';
type Props = {
price: Price,
onChange: Price => void,
placeholder: number,
min: number,
disabled: boolean,
name: string,
label: string,
step: ?number,
};
export class FormFieldPrice extends React.PureComponent<Props> {
constructor(props: Props) {
super(props);
(this: any).handleAmountChange = this.handleAmountChange.bind(this);
(this: any).handleCurrencyChange = this.handleCurrencyChange.bind(this);
}
handleAmountChange(event: SyntheticInputEvent<*>) {
const { price, onChange } = this.props;
onChange({
currency: price.currency,
amount: parseFloat(event.target.value),
});
}
handleCurrencyChange(event: SyntheticInputEvent<*>) {
const { price, onChange } = this.props;
onChange({
currency: event.target.value,
amount: price.amount,
});
}
render() {
const { price, placeholder, min, disabled, name, label, step } = this.props;
return (
<FormRow padded>
<FormField
name={`${name}_amount`}
label={label}
type="number"
className="form-field input--price-amount"
min={min}
value={price.amount || ''}
onChange={this.handleAmountChange}
placeholder={placeholder || 5}
disabled={disabled}
step={step || 'any'}
/>
<FormField
name={`${name}_currency`}
type="select"
id={`${name}_currency`}
className="form-field"
disabled={disabled}
onChange={this.handleCurrencyChange}
value={price.currency}
>
<option value="LBC">{__('LBRY Credits (LBC)')}</option>
<option value="USD">{__('US Dollars')}</option>
</FormField>
</FormRow>
);
}
}
export default FormFieldPrice;

View file

@ -0,0 +1,100 @@
// @flow
import * as React from 'react';
import classnames from 'classnames';
import SimpleMDE from 'react-simplemde-editor';
import style from 'react-simplemde-editor/dist/simplemde.min.css'; // eslint-disable-line no-unused-vars
type Props = {
name: string,
label?: string,
render?: () => React.Node,
prefix?: string,
postfix?: string,
error?: string | boolean,
helper?: string | React.Node,
type?: string,
onChange?: any => any,
defaultValue?: string | number,
placeholder?: string | number,
children?: React.Node,
stretch?: boolean,
};
export class FormField extends React.PureComponent<Props> {
render() {
const {
render,
label,
prefix,
postfix,
error,
helper,
name,
type,
children,
stretch,
...inputProps
} = this.props;
let input;
if (type) {
if (type === 'select') {
input = (
<select id={name} {...inputProps}>
{children}
</select>
);
} else if (type === 'markdown') {
input = (
<div className="form-field--SimpleMDE">
<SimpleMDE
{...inputProps}
type="textarea"
options={{ hideIcons: ['heading', 'image', 'fullscreen', 'side-by-side'] }}
/>
</div>
);
} else {
input = <input type={type} id={name} {...inputProps} />;
}
}
return (
<div
className={classnames('form-field', {
'form-field--stretch': stretch || type === 'markdown',
})}
>
{(label || error) && (
<label
className={classnames('form-field__label', { 'form-field__error': error })}
htmlFor={name}
>
{!error && label}
{error}
</label>
)}
<div className="form-field__input">
{prefix && (
<label htmlFor={name} className="form-field__prefix">
{prefix}
</label>
)}
{input}
{postfix && (
<label htmlFor={name} className="form-field__postfix">
{postfix}
</label>
)}
</div>
{helper && (
<label htmlFor={name} className="form-field__help">
{helper}
</label>
)}
</div>
);
}
}
export default FormField;

View file

@ -0,0 +1,32 @@
// @flow
// Used as a wrapper for FormField to produce inline form elements
import * as React from 'react';
import classnames from 'classnames';
type Props = {
children: React.Node,
padded?: boolean,
verticallyCentered?: boolean,
};
export class FormRow extends React.PureComponent<Props> {
static defaultProps = {
padded: false,
};
render() {
const { children, padded, verticallyCentered } = this.props;
return (
<div
className={classnames('form-row', {
'form-row--padded': padded,
'form-row--centered': verticallyCentered,
})}
>
{children}
</div>
);
}
}
export default FormRow;

View file

@ -0,0 +1,27 @@
// @flow
import * as React from 'react';
type Props = {
children: React.Node,
onSubmit: any => any,
};
export class Form extends React.PureComponent<Props> {
render() {
const { children, onSubmit, ...otherProps } = this.props;
return (
<form
className="form"
onSubmit={event => {
event.preventDefault();
onSubmit(event);
}}
{...otherProps}
>
{children}
</form>
);
}
}
export default Form;

View file

@ -0,0 +1,23 @@
// @flow
import * as React from 'react';
import Button from 'component/button';
type Props = {
label: string,
disabled: boolean,
};
export class Submit extends React.PureComponent<Props> {
static defaultProps = {
label: 'Submit',
};
render() {
const { label, disabled, ...otherProps } = this.props;
return (
<Button button="primary" type="submit" label={label} disabled={disabled} {...otherProps} />
);
}
}
export default Submit;

View file

@ -0,0 +1,5 @@
export { Form } from './form-components/form';
export { FormRow } from './form-components/form-row';
export { FormField } from './form-components/form-field';
export { FormFieldPrice } from './form-components/form-field-price';
export { Submit } from './form-components/submit';

View file

@ -0,0 +1,36 @@
// @flow
import React from 'react';
// import * as icons from 'constants/icons';
import * as FeatherIcons from 'react-feather';
import * as icons from 'constants/icons';
const RED_COLOR = '#e2495e';
type Props = {
icon: string,
size?: number,
};
class IconComponent extends React.PureComponent<Props> {
// TODO: Move all icons to constants and add titles for all
// Add some some sort of hover flyout with the title?
render() {
const { icon } = this.props;
const Icon = FeatherIcons[icon];
let color;
if (icon === icons.HEART) {
color = RED_COLOR;
}
let size = 14;
if (icon === icons.ARROW_LEFT || icon === icons.ARROW_RIGHT) {
size = 18;
}
return Icon ? <Icon size={size} className="icon" color={color} /> : null;
}
}
export default IconComponent;

View file

@ -0,0 +1,6 @@
// @flow
import React from 'react';
const LbcSymbol = () => <span>LBC</span>;
export default LbcSymbol;

View file

@ -0,0 +1,18 @@
// @flow
import React from 'react';
import QRCodeElement from 'qrcode.react';
type Props = {
value: string,
};
const QRCode = (props: Props) => {
const { value } = props;
return (
<div className="qr-code">
<QRCodeElement value={value} />
</div>
);
};
export default QRCode;

View file

@ -1,14 +1,28 @@
import React from 'react'; // @flow
import * as React from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
export default ({ dark, className }) => ( type Props = {
<div dark?: boolean,
className={classnames( };
'spinner',
{ class Spinner extends React.Component<Props> {
'spinner--dark': dark, static defaultProps = {
}, dark: false,
className };
)}
/> render() {
); const { dark } = this.props;
return (
<div className={classnames('spinner', { 'spinner--dark': dark })}>
<div className="rect rect1" />
<div className="rect rect2" />
<div className="rect rect3" />
<div className="rect rect4" />
<div className="rect rect5" />
</div>
);
}
}
export default Spinner;

View file

@ -0,0 +1,28 @@
// @flow
import React from 'react';
import classnames from 'classnames';
type Props = {
src: string,
shouldObscure: boolean,
className?: string,
};
const Thumbnail = (props: Props) => {
const { className, src, shouldObscure } = props;
return (
<img
alt={__('Image thumbnail')}
className={classnames(
'card__media',
{
'card__media--nsfw': shouldObscure,
},
className
)}
src={src}
/>
);
};
export default Thumbnail;

View file

@ -0,0 +1,58 @@
// @flow
import React from 'react';
import classnames from 'classnames';
import Icon from 'component/common/icon';
import Button from 'component/button';
import * as icons from 'constants/icons';
type Props = {
body: string,
label: string,
};
type State = {
showTooltip: boolean,
};
class ToolTip extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
showTooltip: false,
};
(this: any).handleClick = this.handleClick.bind(this);
}
handleClick() {
const { showTooltip } = this.state;
if (!showTooltip) {
document.addEventListener('click', this.handleClick);
} else {
document.removeEventListener('click', this.handleClick);
}
this.setState({
showTooltip: !showTooltip,
});
}
render() {
const { label, body } = this.props;
const { showTooltip } = this.state;
return (
<span className="tooltip">
<Button button="link" className="help tooltip__link" onClick={this.handleClick}>
{label}
{showTooltip && <Icon icon={icons.CLOSE} />}
</Button>
<div className={classnames('tooltip__body', { hidden: !showTooltip })}>{body}</div>
</span>
);
}
}
export default ToolTip;

View file

@ -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 <Button button="link" href={href} label={label} />;
};
export default TransactionLink;

View file

@ -0,0 +1,17 @@
// @flow
import * as React from 'react';
type Props = {
lines: ?number,
children: React.Node,
};
const TruncatedText = (props: Props) => {
return (
<span className="truncated-text" style={{ WebkitLineClamp: props.lines }}>
{props.children}
</span>
);
};
export default TruncatedText;

View file

@ -1,83 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
const { remote } = require('electron');
class FileSelector extends React.PureComponent {
static propTypes = {
type: PropTypes.oneOf(['file', 'directory']),
initPath: PropTypes.string,
onFileChosen: PropTypes.func,
};
static defaultProps = {
type: 'file',
};
constructor(props) {
super(props);
this._inputElem = null;
}
componentWillMount() {
this.setState({
path: this.props.initPath || null,
});
}
handleButtonClick() {
remote.dialog.showOpenDialog(
{
properties: this.props.type == 'file' ? ['openFile'] : ['openDirectory', 'createDirectory'],
},
paths => {
if (!paths) {
// User hit cancel, so do nothing
return;
}
const path = paths[0];
this.setState({
path,
});
if (this.props.onFileChosen) {
this.props.onFileChosen(path);
}
}
);
}
render() {
return (
<div className="file-selector">
<button
type="button"
className="button-block button-alt file-selector__choose-button"
onClick={() => this.handleButtonClick()}
>
<span className="button__content">
<span className="button-label">
{this.props.type == 'file' ? __('Choose File') : __('Choose Directory')}
</span>
</span>
</button>{' '}
<span className="file-selector__path">
<input
className="input-copyable"
type="text"
ref={input => {
this._inputElem = input;
}}
onFocus={() => {
this._inputElem.select();
}}
readOnly="readonly"
value={this.state.path || __('No File Chosen')}
/>
</span>
</div>
);
}
}
export default FileSelector;

View file

@ -1,52 +1,47 @@
// @flow
import React from 'react'; import React from 'react';
import Link from 'component/link'; import Button from 'component/button';
import FileDownloadLink from 'component/fileDownloadLink'; import FileDownloadLink from 'component/fileDownloadLink';
import * as modals from 'constants/modal_types'; 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<Props> {
render() { render() {
const { fileInfo, uri, openModal, claimIsMine } = this.props; const { fileInfo, uri, openModal, claimIsMine, vertical } = this.props;
const claimId = fileInfo ? fileInfo.claim_id : null, const claimId = fileInfo ? fileInfo.claim_id : '';
showDelete = fileInfo && Object.keys(fileInfo).length > 0; const showDelete = fileInfo && Object.keys(fileInfo).length > 0;
return ( return (
<section className="card__actions"> <section className={classnames('card__actions', { 'card__actions--vertical': vertical })}>
<FileDownloadLink uri={uri} /> <FileDownloadLink uri={uri} />
{showDelete && ( {showDelete && (
<Link <Button
button="text" className="btn--file-actions"
icon="icon-trash" icon={icons.TRASH}
label={__('Remove')} description={__('Delete')}
className="no-underline"
onClick={() => openModal(modals.CONFIRM_FILE_REMOVE, { uri })} onClick={() => openModal(modals.CONFIRM_FILE_REMOVE, { uri })}
/> />
)} )}
{!claimIsMine && ( {!claimIsMine && (
<Link <Button
button="text" className="btn--file-actions"
icon="icon-flag" icon={icons.REPORT}
href={`https://lbry.io/dmca?claim_id=${claimId}`} href={`https://lbry.io/dmca?claim_id=${claimId}`}
className="no-underline" description={__('Report content')}
label={__('report')}
/>
)}
<Link
button="primary"
icon="icon-gift"
label={__('Support')}
navigate="/show"
className="card__action--right"
navigateParams={{ uri, tab: 'tip' }}
/>
{claimIsMine && (
<Link
button="alt"
icon="icon-edit"
label={__('Edit')}
navigate="/publish"
className="card__action--right"
navigateParams={{ id: claimId }}
/> />
)} )}
</section> </section>

View file

@ -6,16 +6,31 @@ import { selectShowNsfw } from 'redux/selectors/settings';
import { makeSelectClaimForUri, makeSelectMetadataForUri } from 'redux/selectors/claims'; import { makeSelectClaimForUri, makeSelectMetadataForUri } from 'redux/selectors/claims';
import { makeSelectFileInfoForUri } from 'redux/selectors/file_info'; import { makeSelectFileInfoForUri } from 'redux/selectors/file_info';
import { makeSelectIsUriResolving, selectRewardContentClaimIds } from 'redux/selectors/content'; import { makeSelectIsUriResolving, selectRewardContentClaimIds } from 'redux/selectors/content';
import { selectPendingPublish } from 'redux/selectors/publish';
import FileCard from './view'; import FileCard from './view';
const select = (state, props) => ({ 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), claim: makeSelectClaimForUri(props.uri)(state),
fileInfo: makeSelectFileInfoForUri(props.uri)(state), fileInfo: makeSelectFileInfoForUri(props.uri)(state),
obscureNsfw: !selectShowNsfw(state),
metadata: makeSelectMetadataForUri(props.uri)(state), metadata: makeSelectMetadataForUri(props.uri)(state),
rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
isResolvingUri: makeSelectIsUriResolving(props.uri)(state), isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
}); };
return {
obscureNsfw: !selectShowNsfw(state),
rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
...fileCardInfo,
pending: !!pendingPublish,
};
};
const perform = dispatch => ({ const perform = dispatch => ({
navigate: (path, params) => dispatch(doNavigate(path, params)), navigate: (path, params) => dispatch(doNavigate(path, params)),

View file

@ -1,111 +1,102 @@
import React from 'react'; // @flow
import * as React from 'react';
import { normalizeURI } from 'lbryURI'; import { normalizeURI } from 'lbryURI';
import CardMedia from 'component/cardMedia'; import CardMedia from 'component/cardMedia';
import Link from 'component/link'; import TruncatedText from 'component/common/truncated-text';
import { TruncatedText } from 'component/common'; import Icon from 'component/common/icon';
import Icon from 'component/icon';
import FilePrice from 'component/filePrice'; import FilePrice from 'component/filePrice';
import UriIndicator from 'component/uriIndicator'; import UriIndicator from 'component/uriIndicator';
import NsfwOverlay from 'component/nsfwOverlay'; import NsfwOverlay from 'component/nsfwOverlay';
import TruncatedMarkdown from 'component/truncatedMarkdown';
import * as icons from 'constants/icons'; import * as icons from 'constants/icons';
import classnames from 'classnames';
class FileCard extends React.PureComponent { // TODO: iron these out
constructor(props) { type Props = {
super(props); isResolvingUri: boolean,
resolveUri: string => void,
uri: string,
claim: ?{ claim_id: string },
fileInfo: ?{},
metadata: ?{ nsfw: boolean, thumbnail: ?string },
navigate: (string, ?{}) => void,
rewardedContentClaimIds: Array<string>,
obscureNsfw: boolean,
showPrice: boolean,
pending?: boolean,
};
this.state = { class FileCard extends React.PureComponent<Props> {
hovered: false, static defaultProps = {
showPrice: true,
}; };
}
componentWillMount() { componentWillMount() {
this.resolve(this.props); this.resolve(this.props);
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps: Props) {
this.resolve(nextProps); this.resolve(nextProps);
} }
resolve(props) { resolve = (props: Props) => {
const { isResolvingUri, resolveUri, claim, uri } = props; const { isResolvingUri, resolveUri, claim, uri } = props;
if (!isResolvingUri && claim === undefined && uri) { if (!isResolvingUri && claim === undefined && uri) {
resolveUri(uri); resolveUri(uri);
} }
} };
handleMouseOver() {
this.setState({
hovered: true,
});
}
handleMouseOut() {
this.setState({
hovered: false,
});
}
render() { render() {
const { const {
claim, claim,
fileInfo, fileInfo,
metadata, metadata,
isResolvingUri,
navigate, navigate,
rewardedContentClaimIds, rewardedContentClaimIds,
obscureNsfw,
showPrice,
pending,
} = this.props; } = this.props;
const uri = !pending ? normalizeURI(this.props.uri) : this.props.uri;
const uri = normalizeURI(this.props.uri);
const title = metadata && metadata.title ? metadata.title : uri; const title = metadata && metadata.title ? metadata.title : uri;
const thumbnail = metadata && metadata.thumbnail ? metadata.thumbnail : null; 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); const isRewardContent = claim && rewardedContentClaimIds.includes(claim.claim_id);
let description = ''; // We should be able to tab through cards
if (isResolvingUri && !claim) { /* eslint-disable jsx-a11y/click-events-have-key-events */
description = __('Loading...');
} else if (metadata && metadata.description) {
description = metadata.description;
} else if (claim === null) {
description = __('This address contains no content.');
}
return ( return (
<section <section
className={`card card--small card--link ${obscureNsfw ? 'card--obscured ' : ''}`} tabIndex="0"
onMouseEnter={this.handleMouseOver.bind(this)} role="button"
onMouseLeave={this.handleMouseOut.bind(this)} onClick={!pending ? () => navigate('/show', { uri }) : () => {}}
className={classnames('card card--small', {
'card--link': !pending,
'card--pending': pending,
})}
> >
<div className="card__inner"> <CardMedia nsfw={shouldObscureNsfw} thumbnail={thumbnail} />
<Link onClick={() => navigate('/show', { uri })} className="card__link"> <div className="card-media__internal-links">{showPrice && <FilePrice uri={uri} />}</div>
<CardMedia title={title} thumbnail={thumbnail} />
<div className="card__title-identity"> <div className="card__title-identity">
<div className="card__title" title={title}> <div className="card__title--small">
<TruncatedText lines={1}>{title}</TruncatedText> <TruncatedText lines={3}>{title}</TruncatedText>
</div> </div>
<div className="card__subtitle"> <div className="card__subtitle card__subtitle--file-info">
<span className="card__indicators card--file-subtitle"> {pending ? (
<FilePrice uri={uri} />{' '} <div>Pending...</div>
{isRewardContent && <Icon icon={icons.FEATURED} leftPad />}{' '} ) : (
{fileInfo && <Icon icon={icons.LOCAL} leftPad />} <React.Fragment>
</span> <UriIndicator uri={uri} link />
<span className="card--file-subtitle"> {isRewardContent && <Icon icon={icons.FEATURED} />}
<UriIndicator uri={uri} link span smallCard /> {fileInfo && <Icon icon={icons.LOCAL} />}
</span> </React.Fragment>
)}
</div> </div>
</div> </div>
</Link>
{/* Test for nizuka's design: should we remove description?
<div className="card__content card__subtext card__subtext--two-lines">
<TruncatedMarkdown lines={2}>{description}</TruncatedMarkdown>
</div>
*/}
</div>
{obscureNsfw && this.state.hovered && <NsfwOverlay />}
</section> </section>
); );
/* eslint-enable jsx-a11y/click-events-have-key-events */
} }
} }

View file

@ -1,15 +1,26 @@
import React from 'react'; // @flow
import * as React from 'react';
import ReactMarkdown from 'react-markdown'; import ReactMarkdown from 'react-markdown';
import lbry from 'lbry.js'; import lbry from 'lbry';
import FileActions from 'component/fileActions'; import Button from 'component/button';
import Link from 'component/link'; import path from 'path';
import DateTime from 'component/dateTime';
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 { const FileDetails = (props: Props) => {
render() { const { claim, contentType, fileInfo, metadata, openFolder } = props;
const { claim, contentType, fileInfo, metadata, openFolder, uri } = this.props;
if (!claim || !metadata) { if (!claim || !metadata) {
return ( return (
@ -25,46 +36,46 @@ class FileDetails extends React.PureComponent {
const downloadPath = fileInfo ? path.normalize(fileInfo.download_path) : null; const downloadPath = fileInfo ? path.normalize(fileInfo.download_path) : null;
return ( return (
<div> <React.Fragment>
<div className="divider__horizontal" /> {description && (
<FileActions uri={uri} /> <React.Fragment>
<div className="divider__horizontal" /> <div className="card__subtext-title">About</div>
<div className="card__content card__subtext card__subtext--allow-newlines"> <div className="card__subtext">
<ReactMarkdown <ReactMarkdown
source={description || ''} source={description || ''}
escapeHtml escapeHtml
disallowedTypes={['Heading', 'HtmlInline', 'HtmlBlock']} disallowedTypes={['Heading', 'HtmlInline', 'HtmlBlock']}
/> />
</div> </div>
<div className="card__content"> </React.Fragment>
<table className="table-standard table-stretch">
<tbody>
<tr>
<td>{__('Content-Type')}</td>
<td>{mediaType}</td>
</tr>
<tr>
<td>{__('Language')}</td>
<td>{language}</td>
</tr>
<tr>
<td>{__('License')}</td>
<td>{license}</td>
</tr>
{downloadPath && (
<tr>
<td>{__('Downloaded to')}</td>
<td>
<Link onClick={() => openFolder(downloadPath)}>{downloadPath}</Link>
</td>
</tr>
)} )}
</tbody> <div className="card__subtext-title">Info</div>
</table> <div className="card__subtext">
<div>
{__('Content-Type')}
{': '}
{mediaType}
</div> </div>
<div>
{__('Language')}
{': '}
{language}
</div> </div>
<div>
{__('License')}
{': '}
{license}
</div>
{downloadPath && (
<div>
{__('Downloaded to')}
{': '}
<Button button="link" onClick={() => openFolder(downloadPath)} label={downloadPath} />
</div>
)}
</div>
</React.Fragment>
); );
} };
}
export default FileDetails; export default FileDetails;

View file

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { BusyMessage } from 'component/common'; import Button from 'component/button';
import Icon from 'component/icon'; import classnames from 'classnames';
import Link from 'component/link'; import * as icons from 'constants/icons';
class FileDownloadLink extends React.PureComponent { class FileDownloadLink extends React.PureComponent {
componentWillMount() { componentWillMount() {
@ -55,36 +55,32 @@ class FileDownloadLink extends React.PureComponent {
const progress = const progress =
fileInfo && fileInfo.written_bytes fileInfo && fileInfo.written_bytes
? fileInfo.written_bytes / fileInfo.total_bytes * 100 ? fileInfo.written_bytes / fileInfo.total_bytes * 100
: 0, : 0;
label = fileInfo ? progress.toFixed(0) + __('% complete') : __('Connecting...'), const label = fileInfo ? progress.toFixed(0) + __('% complete') : __('Connecting...');
labelWithIcon = (
<span className="button__content">
<Icon icon="icon-download" />
<span>{label}</span>
</span>
);
return ( return (
<div className="faux-button-block file-download button-set-item"> <div className="file-download btn__content">
<div <div
className="faux-button-block file-download__overlay" className={classnames('file-download__overlay', {
btn__content: !!progress,
})}
style={{ width: `${progress}%` }} style={{ width: `${progress}%` }}
> >
{labelWithIcon} {label}
</div> </div>
{labelWithIcon} {label}
</div> </div>
); );
} else if (fileInfo === null && !downloading) { } else if (fileInfo === null && !downloading) {
if (!costInfo) { if (!costInfo) {
return <BusyMessage message={__('Fetching cost info')} />; return null;
} }
return ( return (
<Link <Button
button="text" className="btn--file-actions"
label={__('Download')} description={__('Download')}
icon="icon-download" icon={icons.DOWNLOAD}
className="no-underline"
onClick={() => { onClick={() => {
purchaseUri(uri); purchaseUri(uri);
}} }}
@ -92,11 +88,10 @@ class FileDownloadLink extends React.PureComponent {
); );
} else if (fileInfo && fileInfo.download_path) { } else if (fileInfo && fileInfo.download_path) {
return ( return (
<Link <Button
label={__('Open')} className="btn--file-actions"
button="text" description={__('Open')}
icon="icon-external-link-square" icon={icons.OPEN}
className="no-underline"
onClick={() => openFile()} onClick={() => openFile()}
/> />
); );

View file

@ -1,21 +1,54 @@
import React from 'react'; // @flow
import * as React from 'react';
import { buildURI } from 'lbryURI'; import { buildURI } from 'lbryURI';
import FormField from 'component/formField'; import { FormField } from 'component/common/form';
import FileTile from 'component/fileTile'; import FileCard from 'component/fileCard';
import { BusyMessage } from 'component/common.js';
class FileList extends React.PureComponent { type FileInfo = {
constructor(props) { name: string,
channelName: ?string,
pending?: boolean,
value?: {
publisherSignature: {
certificateId: string,
},
},
metadata: {
publisherSignature: {
certificateId: string,
},
},
};
type Props = {
hideFilter: boolean,
fileInfos: Array<FileInfo>,
};
type State = {
sortBy: string,
};
class FileList extends React.PureComponent<Props, State> {
static defaultProps = {
hideFilter: false,
};
constructor(props: Props) {
super(props); super(props);
this.state = { this.state = {
sortBy: 'dateNew', sortBy: 'dateNew',
}; };
this._sortFunctions = { this.sortFunctions = {
dateNew: fileInfos => dateNew: fileInfos =>
this.props.sortByHeight this.props.sortByHeight
? fileInfos.slice().sort((fileInfo1, fileInfo2) => { ? fileInfos.slice().sort((fileInfo1, fileInfo2) => {
if (fileInfo1.pending) {
return -1;
}
const height1 = this.props.claimsById[fileInfo1.claim_id] const height1 = this.props.claimsById[fileInfo1.claim_id]
? this.props.claimsById[fileInfo1.claim_id].height ? this.props.claimsById[fileInfo1.claim_id].height
: 0; : 0;
@ -76,60 +109,62 @@ class FileList extends React.PureComponent {
}; };
} }
getChannelSignature(fileInfo) { getChannelSignature = (fileInfo: FileInfo) => {
if (fileInfo.pending) {
return undefined;
}
if (fileInfo.value) { if (fileInfo.value) {
return fileInfo.value.publisherSignature.certificateId; return fileInfo.value.publisherSignature.certificateId;
} }
return fileInfo.channel_claim_id; return fileInfo.channel_claim_id;
} };
handleSortChanged(event) { handleSortChanged(event: SyntheticInputEvent<*>) {
this.setState({ this.setState({
sortBy: event.target.value, sortBy: event.target.value,
}); });
} }
render() { render() {
const { handleSortChanged, fetching, fileInfos } = this.props; const { fileInfos, hideFilter } = this.props;
const { sortBy } = this.state; const { sortBy } = this.state;
const content = []; 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 = {}; const uriParams = {};
if (fileInfo.channel_name) { if (channelName) {
uriParams.channelName = fileInfo.channel_name; uriParams.channelName = channelName;
uriParams.contentName = fileInfo.claim_name || fileInfo.name; uriParams.contentName = claimName;
uriParams.claimId = this.getChannelSignature(fileInfo); uriParams.claimId = this.getChannelSignature(fileInfo);
} else { } else {
uriParams.claimId = fileInfo.claim_id; uriParams.claimId = claimId;
uriParams.claimName = fileInfo.claim_name || fileInfo.name; uriParams.claimName = claimName;
} }
const uri = buildURI(uriParams); const uri = buildURI(uriParams);
content.push( content.push(<FileCard key={claimName} uri={uri} showPrice={false} />);
<FileTile
key={fileInfo.outpoint || fileInfo.claim_id}
uri={uri}
showPrice={false}
showLocal={false}
showActions
showEmpty={this.props.fileTileShowEmpty}
/>
);
}); });
return ( return (
<section className="file-list__header"> <section>
{fetching && <BusyMessage />} <div className="file-list__sort">
<span className="sort-section"> {!hideFilter && (
{__('Sort by')}{' '} <FormField
<FormField type="select" onChange={this.handleSortChanged.bind(this)}> prefix={__('Sort by')}
<option value="dateNew">{__('Newest First')}</option> type="select"
<option value="dateOld">{__('Oldest First')}</option> value={sortBy}
onChange={this.handleSortChanged}
>
<option value="date">{__('Date')}</option>
<option value="title">{__('Title')}</option> <option value="title">{__('Title')}</option>
</FormField> </FormField>
</span> )}
{content} </div>
<div className="card__list">{content}</div>
</section> </section>
); );
} }

View file

@ -1,12 +1,13 @@
import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doSearch } from 'redux/actions/search'; 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'; import FileListSearch from './view';
const select = (state, props) => ({ const select = (state, props) => ({
isSearching: selectIsSearching(state),
uris: makeSelectSearchUris(props.query)(state), uris: makeSelectSearchUris(props.query)(state),
downloadUris: selectSearchDownloadUris(props.query)(state),
isSearching: selectIsSearching(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({

View file

@ -1,58 +1,95 @@
// @flow
import React from 'react'; import React from 'react';
import FileTile from 'component/fileTile'; import FileTile from 'component/fileTile';
import ChannelTile from 'component/channelTile'; import ChannelTile from 'component/channelTile';
import Link from 'component/link';
import { BusyMessage } from 'component/common.js';
import { parseURI } from 'lbryURI'; import { parseURI } from 'lbryURI';
import debounce from 'util/debounce';
const SearchNoResults = props => { const SEARCH_DEBOUNCE_TIME = 800;
const { query } = props;
return ( const NoResults = () => {
<section> return <div className="file-tile">{__('No results')}</div>;
<span className="empty">
{(__('No one has checked anything in for %s yet.'), query)}{' '}
<Link label={__('Be the first')} navigate="/publish" />
</span>
</section>
);
}; };
class FileListSearch extends React.PureComponent { type Props = {
componentWillMount() { search: string => void,
this.doSearch(this.props); query: string,
isSearching: boolean,
uris: ?Array<string>,
downloadUris: ?Array<string>,
};
class FileListSearch extends React.PureComponent<Props> {
constructor(props: Props) {
super(props);
this.debouncedSearch = debounce(this.props.search, SEARCH_DEBOUNCE_TIME);
} }
componentWillReceiveProps(props) { componentDidMount() {
if (props.query != this.props.query) { const { search, query } = this.props;
this.doSearch(props); search(query);
}
componentWillReceiveProps(nextProps: Props) {
const { query: nextQuery } = nextProps;
const { query: currentQuerry } = this.props;
if (nextQuery !== currentQuerry) {
this.debouncedSearch(nextQuery);
} }
} }
doSearch(props) { debouncedSearch: string => void;
this.props.search(props.query);
}
render() { 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 ( return (
<div> query && (
{isSearching && !uris && <BusyMessage message={__('Looking up the Dewey Decimals')} />} <div className="search__results">
<div className="search-result__column">
{isSearching && uris && <BusyMessage message={__('Refreshing the Dewey Decimals')} />} <div className="file-list__header">{__('Files')}</div>
{!isSearching &&
{uris && uris.length (fileResults.length ? (
? uris.map( fileResults.map(uri => <FileTile key={uri} uri={uri} />)
uri =>
parseURI(uri).claimName[0] === '@' ? (
<ChannelTile key={uri} uri={uri} />
) : ( ) : (
<FileTile key={uri} uri={uri} /> <NoResults />
) ))}
)
: !isSearching && <SearchNoResults query={query} />}
</div> </div>
<div className="search-result__column">
<div className="file-list__header">{__('Channels')}</div>
{!isSearching &&
(channelResults.length ? (
channelResults.map(uri => <ChannelTile key={uri} uri={uri} />)
) : (
<NoResults />
))}
</div>
<div className="search-result__column">
<div className="file-list__header">{__('Your downloads')}</div>
{downloadUris && downloadUris.length ? (
downloadUris.map(uri => <FileTile test key={uri} uri={uri} />)
) : (
<NoResults />
)}
</div>
</div>
)
); );
} }
} }

View file

@ -1,35 +1,48 @@
// @flow
import React from 'react'; 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<Props> {
static defaultProps = {
showFullPrice: false,
};
class FilePrice extends React.PureComponent {
componentWillMount() { componentWillMount() {
this.fetchCost(this.props); this.fetchCost(this.props);
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps: Props) {
this.fetchCost(nextProps); this.fetchCost(nextProps);
} }
fetchCost(props) { fetchCost = (props: Props) => {
const { costInfo, fetchCostInfo, uri, fetching, claim } = props; const { costInfo, fetchCostInfo, uri, fetching, claim } = props;
if (costInfo === undefined && !fetching && claim) { if (costInfo === undefined && !fetching && claim) {
fetchCostInfo(uri); fetchCostInfo(uri);
} }
} };
render() { 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) { if (!costInfo) {
return <span className={`credit-amount credit-amount--${look}`}>???</span>; return <span className="credit-amount">PRICE</span>;
} }
return ( return (
<CreditAmount <CreditAmount
label={false}
amount={costInfo.cost} amount={costInfo.cost}
isEstimate={isEstimate} isEstimate={isEstimate}
showFree showFree

View file

@ -10,8 +10,7 @@ import FileTile from './view';
const select = (state, props) => ({ const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state), claim: makeSelectClaimForUri(props.uri)(state),
fileInfo: makeSelectFileInfoForUri(props.uri)(state), isDownloaded: !!makeSelectFileInfoForUri(props.uri)(state),
obscureNsfw: !selectShowNsfw(state),
metadata: makeSelectMetadataForUri(props.uri)(state), metadata: makeSelectMetadataForUri(props.uri)(state),
isResolvingUri: makeSelectIsUriResolving(props.uri)(state), isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
rewardedContentClaimIds: selectRewardContentClaimIds(state, props), rewardedContentClaimIds: selectRewardContentClaimIds(state, props),

View file

@ -1,132 +1,115 @@
import React from 'react'; // @flow
import * as React from 'react';
import * as icons from 'constants/icons'; import * as icons from 'constants/icons';
import { normalizeURI, isURIClaimable, parseURI } from 'lbryURI'; import { normalizeURI, isURIClaimable, parseURI } from 'lbryURI';
import CardMedia from 'component/cardMedia'; import CardMedia from 'component/cardMedia';
import { TruncatedText } from 'component/common.js'; import TruncatedText from 'component/common/truncated-text';
import FilePrice from 'component/filePrice'; import FilePrice from 'component/filePrice';
import NsfwOverlay from 'component/nsfwOverlay'; import Icon from 'component/common/icon';
import Icon from 'component/icon'; import Button from 'component/button';
import classnames from 'classnames';
class FileTile extends React.PureComponent { type Props = {
static SHOW_EMPTY_PUBLISH = 'publish'; fullWidth: boolean, // removes the max-width css
static SHOW_EMPTY_PENDING = 'pending'; showUri: boolean,
showLocal: boolean,
isDownloaded: boolean,
uri: string,
isResolvingUri: boolean,
rewardedContentClaimIds: Array<string>,
claim: ?{
name: string,
channel_name: string,
claim_id: string,
},
metadata: {},
resolveUri: string => void,
navigate: (string, ?{}) => void,
};
class FileTile extends React.PureComponent<Props> {
static defaultProps = { static defaultProps = {
showPrice: true, showUri: false,
showLocal: true, showLocal: false,
fullWidth: false,
}; };
constructor(props) {
super(props);
this.state = {
showNsfwHelp: false,
};
}
componentDidMount() { componentDidMount() {
const { isResolvingUri, claim, uri, resolveUri } = this.props; const { isResolvingUri, claim, uri, resolveUri } = this.props;
if (!isResolvingUri && !claim && uri) resolveUri(uri); if (!isResolvingUri && !claim && uri) resolveUri(uri);
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps: Props) {
const { isResolvingUri, claim, uri, resolveUri } = this.props; const { isResolvingUri, claim, uri, resolveUri } = nextProps;
if (!isResolvingUri && claim === undefined && uri) resolveUri(uri); 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() { render() {
const { const {
claim, claim,
showActions,
metadata, metadata,
isResolvingUri, isResolvingUri,
showEmpty,
navigate, navigate,
showPrice,
showLocal,
rewardedContentClaimIds, rewardedContentClaimIds,
fileInfo, showUri,
fullWidth,
showLocal,
isDownloaded,
} = this.props; } = this.props;
const uri = normalizeURI(this.props.uri); const uri = normalizeURI(this.props.uri);
const isClaimed = !!claim; const isClaimed = !!claim;
const isClaimable = isURIClaimable(uri);
const title = const title =
isClaimed && metadata && metadata.title ? metadata.title : parseURI(uri).contentName; isClaimed && metadata && metadata.title ? metadata.title : parseURI(uri).contentName;
const thumbnail = metadata && metadata.thumbnail ? metadata.thumbnail : null; const thumbnail = metadata && metadata.thumbnail ? metadata.thumbnail : null;
const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw;
const isRewardContent = claim && rewardedContentClaimIds.includes(claim.claim_id); 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) { if (claim) {
name = claim.name; name = claim.name;
} channel = claim.channel_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 = (
<span className="empty">
{__('This location is unused.')}{' '}
{isClaimable && <span className="button-text">{__('Put something here!')}</span>}
</span>
);
} else if (showEmpty === FileTile.SHOW_EMPTY_PENDING) {
description = <span className="empty">{__('This file is pending confirmation.')}</span>;
} }
return ( return (
<section <section
className={`file-tile card ${obscureNsfw ? 'card--obscured ' : ''}`} className={classnames('file-tile card--link', {
onMouseEnter={this.handleMouseOver.bind(this)} 'file-tile--fullwidth': fullWidth,
onMouseLeave={this.handleMouseOut.bind(this)} })}
onClick={onClick}
> >
<div onClick={onClick} className="card__link">
<div className="card__inner file-tile__row">
<CardMedia title={title || name} thumbnail={thumbnail} /> <CardMedia title={title || name} thumbnail={thumbnail} />
<div className="file-tile__content"> <div className="file-tile__info">
<div className="card__title-primary"> {isResolvingUri && <div className="card__title--small">{__('Loading...')}</div>}
<span className="card__indicators"> {!isResolvingUri && (
{showPrice && <FilePrice uri={this.props.uri} />}{' '} <React.Fragment>
{isRewardContent && <Icon icon={icons.FEATURED} />}{' '} <div className="card__title--small card__title--file">
{showLocal && fileInfo && <Icon icon={icons.LOCAL} />} <TruncatedText lines={2}>{title || name}</TruncatedText>
</span>
<h3>
<TruncatedText lines={1}>{title || name}</TruncatedText>
</h3>
</div> </div>
{description && ( <div className="card__subtitle">
<div className="card__content card__subtext"> {showUri ? uri : channel || __('Anonymous')}
<TruncatedText lines={!showActions ? 3 : 2}>{description}</TruncatedText> {isRewardContent && <Icon icon={icons.FEATURED} />}
{showLocal && isDownloaded && <Icon icon={icons.LOCAL} />}
</div> </div>
{!name && (
<React.Fragment>
{__('This location is unused.')}{' '}
<Button
button="link"
label={__('Put something here!')}
onClick={e => {
// avoid navigating to /show from clicking on the section
e.preventDefault();
navigate('/publish');
}}
/>
</React.Fragment>
)}
</React.Fragment>
)} )}
</div> </div>
</div>
</div>
{this.state.showNsfwHelp && <NsfwOverlay />}
</section> </section>
); );
} }

View file

@ -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 <form onSubmit={event => this.handleSubmit(event)}>{this.props.children}</form>;
}
}
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 (
<div className={`form-row${this.state.isFocus ? ' form-row--focus' : ''}`}>
{this.props.label && !renderLabelInFormField ? (
<div
className={`form-row__label-row ${
this.props.labelPrefix ? 'form-row__label-row--prefix' : ''
}`}
>
<label
htmlFor={elementId}
className={`form-field__label ${
this.state.isError ? 'form-field__label--error' : ' '
}`}
>
{this.props.label}
</label>
</div>
) : (
''
)}
<FormField
ref={ref => {
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 ? (
<div className="form-field__helper">{this.props.helper}</div>
) : (
''
)}
{this.state.isError ? (
<div className="form-field__error">{this.state.errorMessage}</div>
) : (
''
)}
</div>
);
}
}
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 = (
<span className="button__content">
{'icon' in props ? <Icon icon={icon} fixed /> : null}
{label ? <span className="button-label">{label}</span> : null}
</span>
);
return (
<button type="submit" className={className} title={title}>
{content}
</button>
);
};

View file

@ -1,8 +1,10 @@
// This file is going to die
/* eslint-disable */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; 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 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'; import style from 'react-simplemde-editor/dist/simplemde.min.css';
const formFieldFileSelectorTypes = ['file', 'directory']; const formFieldFileSelectorTypes = ['file', 'directory'];
@ -195,3 +197,4 @@ class FormField extends React.PureComponent {
} }
export default FormField; export default FormField;
/* eslint-enable */

View file

@ -1,63 +1,5 @@
import React from 'react'; // This just exists so the app builds. It will be removed
import FormField from 'component/formField';
class FormFieldPrice extends React.PureComponent { const FormFieldPrice = () => null;
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 (
<span className="form-field">
<FormField
type="number"
name="amount"
min={min}
placeholder={placeholder || null}
step="any" // Unfortunately, you cannot set a step without triggering validation that enforces a multiple of the step
onChange={event => this.handleFeeAmountChange(event)}
defaultValue={defaultValue && defaultValue.amount ? defaultValue.amount : ''}
className="form-field__input--inline"
/>
<FormField
type="select"
name="currency"
onChange={event => this.handleFeeCurrencyChange(event)}
defaultValue={defaultValue && defaultValue.currency ? defaultValue.currency : ''}
className="form-field__input--inline"
>
<option value="LBC">{__('LBRY Credits (LBC)')}</option>
<option value="USD">{__('US Dollars')}</option>
</FormField>
</span>
);
}
}
export default FormFieldPrice; export default FormFieldPrice;

View file

@ -1,16 +1,12 @@
import React from 'react';
import { formatCredits } from 'util/formatCredits';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { selectIsBackDisabled, selectIsForwardDisabled } from 'redux/selectors/navigation'; import { doNavigate } from 'redux/actions/navigation';
import { selectBalance } from 'redux/selectors/wallet';
import { doNavigate, doHistoryBack, doHistoryForward } from 'redux/actions/navigation';
import Header from './view';
import { selectIsUpgradeAvailable, selectAutoUpdateDownloaded } from 'redux/selectors/app'; 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'; import { doDownloadUpgradeRequested } from 'redux/actions/app';
const select = state => ({ const select = state => ({
isBackDisabled: selectIsBackDisabled(state),
isForwardDisabled: selectIsForwardDisabled(state),
isUpgradeAvailable: selectIsUpgradeAvailable(state), isUpgradeAvailable: selectIsUpgradeAvailable(state),
autoUpdateDownloaded: selectAutoUpdateDownloaded(state), autoUpdateDownloaded: selectAutoUpdateDownloaded(state),
balance: formatCredits(selectBalance(state) || 0, 2), balance: formatCredits(selectBalance(state) || 0, 2),

View file

@ -1,100 +1,68 @@
import React from 'react'; // @flow
import Link from 'component/link'; import * as React from 'react';
import Button from 'component/button';
import WunderBar from 'component/wunderbar'; 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 { const {
balance, balance,
back,
forward,
isBackDisabled,
isForwardDisabled,
isUpgradeAvailable, isUpgradeAvailable,
autoUpdateDownloaded,
navigate, navigate,
downloadUpgradeRequested, downloadUpgradeRequested,
autoUpdateDownloaded,
} = props; } = props;
const showUpgradeButton =
autoUpdateDownloaded || (process.platform === 'linux' && isUpgradeAvailable);
return ( return (
<header id="header"> <header className="header">
<div className="header__item">
<Link
onClick={back}
disabled={isBackDisabled}
button="alt button--flat"
icon="icon-arrow-left"
title={__('Back')}
/>
</div>
<div className="header__item">
<Link
onClick={forward}
disabled={isForwardDisabled}
button="alt button--flat"
icon="icon-arrow-right"
title={__('Forward')}
/>
</div>
<div className="header__item">
<Link
onClick={() => navigate('/discover')}
button="alt button--flat"
icon="icon-home"
title={__('Discover Content')}
/>
</div>
<div className="header__item">
<Link
onClick={() => navigate('/subscriptions')}
button="alt button--flat"
icon="icon-at"
title={__('My Subscriptions')}
/>
</div>
<div className="header__item header__item--wunderbar">
<WunderBar /> <WunderBar />
</div> <div className="header__actions-right">
<div className="header__item"> <Button
<Link button="inverse"
className="btn--header-balance"
onClick={() => navigate('/wallet')} onClick={() => navigate('/wallet')}
button="text" label={
className="no-underline" isUpgradeAvailable ? (
icon="icon-bank" `${balance}`
label={balance} ) : (
title={__('Wallet')} <React.Fragment>
<span className="btn__label--balance">You have</span> <span>{balance} LBC</span>
</React.Fragment>
)
}
iconRight="LBC"
description={__('Your wallet')}
/> />
</div>
<div className="header__item"> <Button
<Link uppercase
button="primary"
onClick={() => navigate('/publish')} onClick={() => navigate('/publish')}
button="primary button--flat" icon={icons.UPLOAD}
icon="icon-upload" label={isUpgradeAvailable ? '' : __('Publish')}
label={__('Publish')} description={__('Publish content')}
/> />
</div>
<div className="header__item"> {showUpgradeButton && (
<Link <Button
onClick={() => navigate('/downloaded')} button="primary"
button="alt button--flat" onClick={downloadUpgradeRequested}
icon="icon-folder" icon={icons.DOWNLOAD}
title={__('Downloads and Publishes')}
/>
</div>
<div className="header__item">
<Link
onClick={() => navigate('/settings')}
button="alt button--flat"
icon="icon-gear"
title={__('Settings')}
/>
</div>
{(autoUpdateDownloaded || (process.platform === 'linux' && isUpgradeAvailable)) && (
<Link
onClick={() => downloadUpgradeRequested()}
button="primary button--flat"
icon="icon-arrow-up"
label={__('Upgrade App')} label={__('Upgrade App')}
/> />
)} )}
</div>
</header> </header>
); );
}; };

View file

@ -1,5 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import Icon from './view';
export default connect(null, null)(Icon);

View file

@ -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 <span className={spanClassName} title={title} />;
}
}

View file

@ -1,7 +1,8 @@
import React from 'react'; import React from 'react';
import Icon from 'component/icon'; import Icon from 'component/common/icon';
import RewardLink from 'component/rewardLink'; import RewardLink from 'component/rewardLink';
import rewards from 'rewards.js'; import rewards from 'rewards.js';
import * as icons from 'constants/icons';
class InviteList extends React.PureComponent { class InviteList extends React.PureComponent {
render() { render() {
@ -12,8 +13,8 @@ class InviteList extends React.PureComponent {
} }
return ( return (
<section className="card"> <section className="card card--section">
<div className="card__title-primary"> <div className="card__title">
<h3>{__('Invite History')}</h3> <h3>{__('Invite History')}</h3>
</div> </div>
<div className="card__content"> <div className="card__content">
@ -21,7 +22,7 @@ class InviteList extends React.PureComponent {
<span className="empty">{__("You haven't invited anyone.")} </span> <span className="empty">{__("You haven't invited anyone.")} </span>
)} )}
{invitees.length > 0 && ( {invitees.length > 0 && (
<table className="table-standard table-stretch"> <table className="table table--stretch">
<thead> <thead>
<tr> <tr>
<th>{__('Invitee Email')}</th> <th>{__('Invitee Email')}</th>
@ -35,14 +36,14 @@ class InviteList extends React.PureComponent {
<td>{invitee.email}</td> <td>{invitee.email}</td>
<td className="text-center"> <td className="text-center">
{invitee.invite_accepted ? ( {invitee.invite_accepted ? (
<Icon icon="icon-check" /> <Icon icon={icons.CHECK} />
) : ( ) : (
<span className="empty">{__('unused')}</span> <span className="empty">{__('unused')}</span>
)} )}
</td> </td>
<td className="text-center"> <td className="text-center">
{invitee.invite_reward_claimed ? ( {invitee.invite_reward_claimed ? (
<Icon icon="icon-check" /> <Icon icon={icons.CHECK} />
) : invitee.invite_reward_claimable ? ( ) : invitee.invite_reward_claimable ? (
<RewardLink label={__('claim')} reward_type={rewards.TYPE_REFERRAL} /> <RewardLink label={__('claim')} reward_type={rewards.TYPE_REFERRAL} />
) : ( ) : (

View file

@ -1,14 +1,19 @@
// I'll come back to this
/* eslint-disable */
import React from 'react'; import React from 'react';
import { BusyMessage, CreditAmount } from 'component/common'; import BusyIndicator from 'component/common/busy-indicator';
import { Form, FormRow, Submit } from 'component/form.js'; import CreditAmount from 'component/common/credit-amount';
import { Form, FormRow, FormField, Submit } from 'component/common/form';
class FormInviteNew extends React.PureComponent { class FormInviteNew extends React.PureComponent {
constructor(props) { constructor() {
super(props); super();
this.state = { this.state = {
email: '', email: '',
}; };
this.handleSubmit = this.handleSubmit.bind(this);
} }
handleEmailChanged(event) { handleEmailChanged(event) {
@ -23,23 +28,27 @@ class FormInviteNew extends React.PureComponent {
} }
render() { render() {
const { errorMessage, isPending } = this.props; const { errorMessage, isPending, rewardAmount } = this.props;
const label = `${__('Get')} ${rewardAmount} LBC`;
return ( return (
<Form onSubmit={this.handleSubmit.bind(this)}> <Form onSubmit={this.handleSubmit}>
<FormRow <FormRow stretch>
<FormField
stretch
type="text" type="text"
label="Email" label="Email"
placeholder="youremail@example.org" placeholder="youremail@example.org"
name="email" name="email"
value={this.state.email} value={this.state.email}
errorMessage={errorMessage} error={errorMessage}
onChange={event => { onChange={event => {
this.handleEmailChanged(event); this.handleEmailChanged(event);
}} }}
/> />
<div className="form-row-submit"> </FormRow>
<Submit label={__('Send Invite')} disabled={isPending} /> <div className="card__actions">
<Submit label={label} disabled={isPending} />
</div> </div>
</Form> </Form>
); );
@ -58,10 +67,10 @@ class InviteNew extends React.PureComponent {
} = this.props; } = this.props;
return ( return (
<section className="card"> <section className="card card--section">
<div className="card__title-primary"> <div className="card__title">{__('Invite a Friend')}</div>
<CreditAmount amount={rewardAmount} /> <div className="card__subtitle">
<h3>{__('Invite a Friend')}</h3> {__("Or an enemy. Or your cousin Jerry, who you're kind of unsure about.")}
</div> </div>
{/* {/*
<div className="card__content"> <div className="card__content">
@ -71,8 +80,12 @@ class InviteNew extends React.PureComponent {
<p className="empty">{__("You have no invites.")}</p>} <p className="empty">{__("You have no invites.")}</p>}
</div> */} </div> */}
<div className="card__content"> <div className="card__content">
<p>{__("Or an enemy. Or your cousin Jerry, who you're kind of unsure about.")}</p> <FormInviteNew
<FormInviteNew errorMessage={errorMessage} inviteNew={inviteNew} isPending={isPending} /> errorMessage={errorMessage}
inviteNew={inviteNew}
isPending={isPending}
rewardAmount={rewardAmount}
/>
</div> </div>
</section> </section>
); );
@ -80,3 +93,4 @@ class InviteNew extends React.PureComponent {
} }
export default InviteNew; export default InviteNew;
/* eslint-enable */

View file

@ -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 = (
<span {...('button' in props ? { className: 'button__content' } : {})}>
{icon ? <Icon icon={icon} fixed /> : null}
{label ? <span className="link-label">{label}</span> : null}
{iconRight ? <Icon icon={iconRight} fixed /> : null}
</span>
);
}
const linkProps = {
className: combinedClassName,
href: href || 'javascript:;',
title,
onClick,
style,
};
return span ? <span {...linkProps}>{content}</span> : <a {...linkProps}>{content}</a>;
};
export default Link;

View file

@ -1,5 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import LinkTransaction from './view';
export default connect(null, null)(LinkTransaction);

View file

@ -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 <Link {...linkProps} />;
};
export default LinkTransaction;

View file

@ -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 (
<div className="load-screen">
<img src={imgSrc} alt="LBRY" />
<div className="load-screen__message">
<h3>
{!this.props.isWarning ? (
<BusyMessage message={this.props.message} />
) : (
<span>
<Icon icon="icon-warning" />
{` ${this.props.message}`}
</span>
)}
</h3>
<span
className={`load-screen__details ${
this.props.isWarning ? 'load-screen__details--warning' : ''
}`}
>
{this.props.details}
</span>
</div>
</div>
);
}
}
export default LoadScreen;

View file

@ -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 ? <Icon icon={this.props.icon} fixed /> : null;
return (
<a
className="menu__menu-item"
onClick={this.props.onClick}
href={this.props.href || 'javascript:'}
label={this.props.label}
>
{this.props.iconPosition == 'left' ? icon : null}
{this.props.label}
{this.props.iconPosition == 'left' ? null : icon}
</a>
);
}
}
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 (
<div className="menu-container">
<Link
ref={span => (this._menuButton = span)}
button="text"
icon="icon-ellipsis-v"
onClick={event => {
this.handleMenuIconClick(event);
}}
/>
{this.state.menuOpen ? (
<div
ref={div => (this._menuDiv = div)}
className="menu"
onClick={event => {
this.handleMenuClick(event);
}}
>
{this.props.children}
</div>
) : null}
</div>
);
}
}

View file

@ -1,15 +1,11 @@
import React from 'react'; import React from 'react';
import Link from 'component/link'; import Button from 'component/button';
const NsfwOverlay = props => ( const NsfwOverlay = () => (
<div className="card-overlay"> <div className="card-overlay">
<p> <p>
{__('This content is Not Safe For Work. To view adult content, please change your')}{' '} {__('This content is Not Safe For Work. To view adult content, please change your')}{' '}
<Link <Button button="link" navigate="/settings" label={__('settings')} />.
className="button-text"
onClick={() => props.navigateSettings()}
label={__('Settings')}
/>.
</p> </p>
</div> </div>
); );

View file

@ -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);

View file

@ -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 (
<main
className={classnames('main', {
'main--contained': !notContained && !noPadding && !extraPadding,
'main--no-padding': noPadding,
'main--extra-padding': extraPadding,
})}
>
{pageTitle && (
<div className="page__header">
{pageTitle && <h1 className="page__title">{pageTitle}</h1>}
</div>
)}
{children}
</main>
);
};
export default Page;

View file

@ -1,10 +1,5 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PublishForm from './view'; import PublishForm from './view';
import { selectBalance } from 'redux/selectors/wallet';
const select = state => ({ export default connect(null, null)(PublishForm);
balance: selectBalance(state),
});
export default connect(select, null)(PublishForm);

View file

@ -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<Props> {
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 (
<React.Fragment>
{__('You already have a claim at')}
{` ${uri} `}
<Button button="link" label="Edit it" onClick={onEditMyClaim} />
<br />
{__('Publishing will update your existing claim.')}
</React.Fragment>
);
}
return winningBidForClaimUri ? (
<React.Fragment>
{__('A deposit greater than')} {winningBidForClaimUri} {__('is needed to win')}
{` ${uri}. `}
{__('However, you can still get this URL for any amount')}
</React.Fragment>
) : (
__('Any amount will give you the winning bid')
);
}
}
export default BidHelpText;

View file

@ -1,181 +0,0 @@
import React from 'react';
import { isNameValid } from 'lbryURI';
import { FormRow } from 'component/form.js';
import { BusyMessage } from 'component/common';
import Link from 'component/link';
class ChannelSection extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
newChannelName: '@',
newChannelBid: 10,
addingChannel: false,
};
}
handleChannelChange(event) {
const channel = event.target.value;
if (channel === 'new') this.setState({ addingChannel: true });
else {
this.setState({ addingChannel: false });
this.props.handleChannelChange(event.target.value);
}
}
handleNewChannelNameChange(event) {
const newChannelName = event.target.value.startsWith('@')
? event.target.value
: `@${event.target.value}`;
if (newChannelName.length > 1 && !isNameValid(newChannelName.substr(1), false)) {
this.refs.newChannelName.showError(
__('LBRY channel names must contain only letters, numbers and dashes.')
);
return;
}
this.refs.newChannelName.clearError();
this.setState({
newChannelName,
});
}
handleNewChannelBidChange(event) {
this.setState({
newChannelBid: parseFloat(event.target.value),
});
}
handleCreateChannelClick(event) {
const { balance } = this.props;
const { newChannelBid } = this.state;
if (newChannelBid > balance) {
this.refs.newChannelName.showError(__('Unable to create channel due to insufficient funds.'));
return;
}
if (newChannelBid === 0) {
this.refs.newChannelName.showError(__('Bid value must be greater than 0.'));
return;
}
if (newChannelBid === balance) {
this.refs.newChannelName.showError(
__('Please decrease your bid to account for transaction fees.')
);
return;
}
this.setState({
creatingChannel: true,
});
const newChannelName = this.state.newChannelName;
const amount = parseFloat(this.state.newChannelBid);
this.setState({
creatingChannel: true,
});
const success = () => {
this.setState({
creatingChannel: false,
addingChannel: false,
channel: newChannelName,
});
this.props.handleChannelChange(newChannelName);
};
const failure = err => {
this.setState({
creatingChannel: false,
});
this.refs.newChannelName.showError(__('Unable to create channel due to an internal error.'));
};
this.props.createChannel(newChannelName, amount).then(success, failure);
}
render() {
const lbcInputHelp = __(
'This LBC remains yours. It is a deposit to reserve the name and can be undone at any time.'
);
const channel = this.state.addingChannel ? 'new' : this.props.channel;
const { fetchingChannels, channels = [] } = this.props;
const channelSelector = (
<FormRow
key="channel"
type="select"
tabIndex="1"
onChange={this.handleChannelChange.bind(this)}
value={channel}
>
<option key="anonymous" value="anonymous">
{__('Anonymous')}
</option>
{channels.map(({ name }) => (
<option key={name} value={name}>
{name}
</option>
))}
<option key="new" value="new">
{__('New channel...')}
</option>
</FormRow>
);
return (
<section className="card">
<div className="card__title-primary">
<h4>{__('Channel Name')}</h4>
<div className="card__subtitle">
{__('This is a username or handle that your content can be found under.')}{' '}
{__('Ex. @Marvel, @TheBeatles, @BooksByJoe')}
</div>
</div>
<div className="card__content">
{fetchingChannels ? (
<BusyMessage message="Updating channels" key="loading" />
) : (
channelSelector
)}
</div>
{this.state.addingChannel && (
<div className="card__content">
<FormRow
label={__('Name')}
type="text"
onChange={this.handleNewChannelNameChange.bind(this)}
value={this.state.newChannelName}
/>
<FormRow
label={__('Deposit')}
postfix="LBC"
step="any"
min="0"
type="number"
helper={lbcInputHelp}
ref="newChannelName"
onChange={this.handleNewChannelBidChange.bind(this)}
value={this.state.newChannelBid}
/>
<div className="form-row-submit">
<Link
button="primary"
label={
!this.state.creatingChannel ? __('Create channel') : __('Creating channel...')
}
onClick={this.handleCreateChannelClick.bind(this)}
disabled={this.state.creatingChannel}
/>
</div>
</div>
)}
</section>
);
}
}
export default ChannelSection;

View file

@ -0,0 +1,108 @@
// @flow
import * as React from 'react';
import { FormRow, FormField } from 'component/common/form';
import { CC_LICENSES, COPYRIGHT, OTHER, PUBLIC_DOMAIN, NONE } from 'constants/licenses';
type Props = {
licenseType: string,
copyrightNotice: ?string,
licenseUrl: ?string,
otherLicenseDescription: ?string,
handleLicenseChange: (string, string) => void,
handleLicenseDescriptionChange: (SyntheticInputEvent<*>) => void,
handleLicenseUrlChange: (SyntheticInputEvent<*>) => void,
handleCopyrightNoticeChange: (SyntheticInputEvent<*>) => void,
};
class LicenseType extends React.PureComponent<Props> {
constructor() {
super();
(this: any).handleLicenseOnChange = this.handleLicenseOnChange.bind(this);
}
handleLicenseOnChange(event: SyntheticInputEvent<*>) {
const { handleLicenseChange } = this.props;
// $FlowFixMe
const { options, selectedIndex } = event.target;
const selectedOption = options[selectedIndex];
const licenseType = selectedOption.value;
const licenseUrl = selectedOption.getAttribute('data-url');
handleLicenseChange(licenseType, licenseUrl);
}
render() {
const {
licenseType,
otherLicenseDescription,
licenseUrl,
copyrightNotice,
handleLicenseChange,
handleLicenseDescriptionChange,
handleLicenseUrlChange,
handleCopyrightNoticeChange,
} = this.props;
return (
<div className="card__content">
<FormField
label={__('License (Optional)')}
type="select"
value={licenseType}
onChange={this.handleLicenseOnChange}
>
<option value={NONE}>{__('None')}</option>
<option value={PUBLIC_DOMAIN}>{__('Public Domain')}</option>
{CC_LICENSES.map(({ value, url }) => (
<option key={value} value={value} data-url={url}>
{value}
</option>
))}
<option value={COPYRIGHT}>{__('Copyrighted...')}</option>
<option value={OTHER}>{__('Other...')}</option>
</FormField>
{licenseType === COPYRIGHT && (
<FormRow padded>
<FormField
stretch
label={__('Copyright notice')}
type="text"
name="copyright-notice"
value={copyrightNotice}
onChange={handleCopyrightNoticeChange}
/>
</FormRow>
)}
{licenseType === OTHER && (
<React.Fragment>
<FormRow padded>
<FormField
label={__('License description')}
type="text"
name="other-license-description"
value={otherLicenseDescription}
onChange={handleLicenseDescriptionChange}
/>
</FormRow>
<FormRow padded>
<FormField
label={__('License URL')}
type="text"
name="other-license-url"
value={licenseUrl}
onChange={handleLicenseUrlChange}
/>
</FormRow>
</React.Fragment>
)}
</div>
);
}
}
export default LicenseType;

File diff suppressed because it is too large Load diff

View file

@ -1,16 +1,16 @@
import React from 'react'; import React from 'react';
import Modal from 'modal/modal'; import Modal from 'modal/modal';
import Link from 'component/link'; import Button from 'component/button';
const RewardLink = props => { const RewardLink = props => {
const { reward, button, claimReward, clearError, errorMessage, label, isPending } = props; const { reward, button, claimReward, clearError, errorMessage, label, isPending } = props;
return ( return !reward ? null : (
<div className="reward-link"> <div className="reward-link">
<Link <Button
button={button} button="primary"
disabled={isPending} disabled={isPending}
label={isPending ? __('Claiming...') : label || __('Claim Reward')} label={isPending ? __('Claiming...') : label || `${__('Get')} ${reward.reward_amount} LBC`}
onClick={() => { onClick={() => {
claimReward(reward); claimReward(reward);
}} }}

View file

@ -1,7 +1,20 @@
// @flow
import React from 'react'; 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<Reward>,
};
const RewardListClaimed = (props: Props) => {
const { rewards } = props; const { rewards } = props;
if (!rewards || !rewards.length) { if (!rewards || !rewards.length) {
@ -9,12 +22,10 @@ const RewardListClaimed = props => {
} }
return ( return (
<section className="card"> <section className="card card--section">
<div className="card__title-identity"> <div className="card__title">Claimed Rewards</div>
<h3>Claimed Rewards</h3>
</div> <table className="card__content table table--stretch">
<div className="card__content">
<table className="table-standard table-stretch">
<thead> <thead>
<tr> <tr>
<th>{__('Title')}</th> <th>{__('Title')}</th>
@ -29,14 +40,13 @@ const RewardListClaimed = props => {
<td>{reward.reward_title}</td> <td>{reward.reward_title}</td>
<td>{reward.reward_amount}</td> <td>{reward.reward_amount}</td>
<td> <td>
<LinkTransaction id={reward.transaction_id} /> <ButtonTransaction id={reward.transaction_id} />
</td> </td>
<td>{reward.created_at.replace('Z', ' ').replace('T', ' ')}</td> <td>{reward.created_at.replace('Z', ' ').replace('T', ' ')}</td>
</tr> </tr>
))} ))}
</tbody> </tbody>
</table> </table>
</div>
</section> </section>
); );
}; };

View file

@ -1,7 +1,7 @@
// @flow // @flow
import React from 'react'; import * as React from 'react';
import Link from 'component/link'; import Button from 'component/button';
import { CreditAmount } from 'component/common'; import CreditAmount from 'component/common/credit-amount';
type Props = { type Props = {
unclaimedRewardAmount: number, unclaimedRewardAmount: number,
@ -9,29 +9,38 @@ type Props = {
const RewardSummary = (props: Props) => { const RewardSummary = (props: Props) => {
const { unclaimedRewardAmount } = props; const { unclaimedRewardAmount } = props;
const hasRewards = unclaimedRewardAmount > 0;
return ( return (
<section className="card"> <section className="card card--section">
<div className="card__title-primary"> <div className="card__title">{__('Rewards')}</div>
<h3>{__('Rewards')}</h3> <p className="card__subtitle">
<p className="help"> {hasRewards ? (
{__('Read our')} <Link href="https://lbry.io/faq/rewards">{__('FAQ')}</Link>{' '} <React.Fragment>
{__('You have')}
&nbsp;
<CreditAmount noStyle amount={unclaimedRewardAmount} precision={8} />
&nbsp;
{__('in unclaimed rewards')}.
</React.Fragment>
) : (
<React.Fragment>
{__('There are no rewards available at this time, please check back later')}.
</React.Fragment>
)}
</p>
<div className="card__actions">
<Button
button="primary"
navigate="/rewards"
label={hasRewards ? __('Claim Rewards') : __('View Rewards')}
/>
</div>
<p className="help help--padded">
{__('Read our')}{' '}
<Button button="link" label={__('FAQ')} href="https://lbry.io/faq/rewards" />{' '}
{__('to learn more about LBRY Rewards')}. {__('to learn more about LBRY Rewards')}.
</p> </p>
</div>
<div className="card__content">
{unclaimedRewardAmount > 0 ? (
<p>
{__('You have')} <CreditAmount amount={unclaimedRewardAmount} precision={8} />{' '}
{__('in unclaimed rewards')}.
</p>
) : (
<p>{__('There are no rewards available at this time, please check back later')}.</p>
)}
</div>
<div className="card__actions">
<Link button="primary" navigate="/rewards" label={__('Claim Rewards')} />
</div>
</section> </section>
); );
}; };

View file

@ -1,36 +1,44 @@
// @flow
import React from 'react'; import React from 'react';
import { CreditAmount, Icon } from 'component/common'; import Icon from 'component/common/icon';
import RewardLink from 'component/rewardLink'; import RewardLink from 'component/rewardLink';
import Link from 'component/link'; import Button from 'component/button';
import rewards from 'rewards'; 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 { reward } = props;
const claimed = !!reward.transaction_id; const claimed = !!reward.transaction_id;
return ( return (
<section className="card"> <section className="card card--section">
<div className="card__inner"> <div className="card__title">{reward.reward_title}</div>
<div className="card__title-primary"> <div className="card__subtitle">{reward.reward_description}</div>
<CreditAmount amount={reward.reward_amount} /> <div className="card__actions">
<h3>{reward.reward_title}</h3> {reward.reward_type === rewards.TYPE_REFERRAL && (
</div> <Button button="primary" navigate="/invite" label={__('Go To Invites')} />
<div className="card__content">{reward.reward_description}</div>
<div className="card__actions ">
{reward.reward_type == rewards.TYPE_REFERRAL && (
<Link button="alt" navigate="/invite" label={__('Go To Invites')} />
)} )}
{reward.reward_type !== rewards.TYPE_REFERRAL && {reward.reward_type !== rewards.TYPE_REFERRAL &&
(claimed ? ( (claimed ? (
<span> <span>
<Icon icon="icon-check" /> {__('Reward claimed.')} <Icon icon={icons.CHECK} /> {__('Reward claimed.')}
</span> </span>
) : ( ) : (
<RewardLink button="alt" reward_type={reward.reward_type} /> <RewardLink reward_type={reward.reward_type} />
))} ))}
</div> </div>
</div>
</section> </section>
); );
}; };

View file

@ -13,7 +13,6 @@ import FileListDownloaded from 'page/fileListDownloaded';
import FileListPublished from 'page/fileListPublished'; import FileListPublished from 'page/fileListPublished';
import TransactionHistoryPage from 'page/transactionHistory'; import TransactionHistoryPage from 'page/transactionHistory';
import ChannelPage from 'page/channel'; import ChannelPage from 'page/channel';
import SearchPage from 'page/search';
import AuthPage from 'page/auth'; import AuthPage from 'page/auth';
import InvitePage from 'page/invite'; import InvitePage from 'page/invite';
import BackupPage from 'page/backup'; import BackupPage from 'page/backup';
@ -22,7 +21,7 @@ import SubscriptionsPage from 'page/subscriptions';
const route = (page, routesMap) => { const route = (page, routesMap) => {
const component = routesMap[page]; const component = routesMap[page];
return component; return component || DiscoverPage;
}; };
const Router = props => { const Router = props => {
@ -42,7 +41,6 @@ const Router = props => {
getcredits: <GetCreditsPage params={params} />, getcredits: <GetCreditsPage params={params} />,
report: <ReportPage params={params} />, report: <ReportPage params={params} />,
rewards: <RewardsPage params={params} />, rewards: <RewardsPage params={params} />,
search: <SearchPage params={params} />,
send: <SendReceivePage params={params} />, send: <SendReceivePage params={params} />,
settings: <SettingsPage params={params} />, settings: <SettingsPage params={params} />,
show: <ShowPage {...params} />, show: <ShowPage {...params} />,

View file

@ -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);

View file

@ -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<any>,
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<Props, State> {
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 (
<div className="card__content">
{createChannelError && <div className="form-field__error">{createChannelError}</div>}
{fetchingChannels ? (
<BusyIndicator message="Updating channels" />
) : (
<FormField
key="channel"
type="select"
tabIndex="1"
onChange={this.handleChannelChange}
value={channel}
>
<option value={CHANNEL_ANONYMOUS}>{__('Anonymous')}</option>
{channels.map(({ name }) => (
<option key={name} value={name}>
{name}
</option>
))}
<option value={CHANNEL_NEW}>{__('New channel...')}</option>
</FormField>
)}
{addingChannel && (
<div className="card__content">
<FormRow padded>
<FormField
label={__('Name')}
type="text"
prefix="@"
error={newChannelNameError}
value={newChannelName}
onChange={this.handleNewChannelNameChange}
/>
</FormRow>
<FormRow padded>
<FormField
label={__('Deposit')}
postfix="LBC"
step="any"
min="0"
type="number"
helper={__(
'This LBC remains yours. It is a deposit to reserve the name and can be undone at any time.'
)}
error={newChannelBidError}
value={newChannelBid}
onChange={this.handleNewChannelBidChange}
/>
</FormRow>
<div className="card__actions">
<Button
button="primary"
label={!creatingChannel ? __('Create channel') : __('Creating channel...')}
onClick={this.handleCreateChannelClick}
disabled={
!newChannelName ||
!newChannelBid ||
creatingChannel ||
newChannelNameError ||
newChannelBidError
}
/>
</div>
</div>
)}
</div>
);
}
}
export default ChannelSection;

View file

@ -1,9 +1,10 @@
// @flow // @flow
import * as React from 'react'; 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 * as statuses from 'constants/shape_shift';
import Address from 'component/address'; import Address from 'component/address';
import Link from 'component/link'; import Button from 'component/button';
import type { Dispatch } from 'redux/actions/shape_shift'; import type { Dispatch } from 'redux/actions/shape_shift';
import ShiftMarketInfo from './market_info'; import ShiftMarketInfo from './market_info';
@ -92,12 +93,12 @@ class ActiveShapeShift extends React.PureComponent<Props> {
originCoinDepositMax={originCoinDepositMax} originCoinDepositMax={originCoinDepositMax}
/> />
<div className="shapeshift__deposit-address-wrapper"> {shiftDepositAddress && (
<FormRow verticallyCentered padded>
<Address address={shiftDepositAddress} showCopyButton /> <Address address={shiftDepositAddress} showCopyButton />
<div className="shapeshift__qrcode">
<QRCode value={shiftDepositAddress} /> <QRCode value={shiftDepositAddress} />
</div> </FormRow>
</div> )}
</div> </div>
)} )}
@ -115,9 +116,9 @@ class ActiveShapeShift extends React.PureComponent<Props> {
<p>{__('Transaction complete! You should see the new LBC in your wallet.')}</p> <p>{__('Transaction complete! You should see the new LBC in your wallet.')}</p>
</div> </div>
)} )}
<div className="card__actions card__actions--only-vertical"> <div className="card__actions">
<Link <Button
button={shiftState === statuses.COMPLETE ? 'primary' : 'alt'} button="primary"
onClick={clearShapeShift} onClick={clearShapeShift}
label={ label={
shiftState === statuses.COMPLETE || shiftState === statuses.RECEIVED shiftState === statuses.COMPLETE || shiftState === statuses.RECEIVED
@ -126,13 +127,11 @@ class ActiveShapeShift extends React.PureComponent<Props> {
} }
/> />
{shiftOrderId && ( {shiftOrderId && (
<span className="shapeshift__link"> <Button
<Link button="inverse"
button="text"
label={__('View the status on Shapeshift.io')} label={__('View the status on Shapeshift.io')}
href={`https://shapeshift.io/#/status/${shiftOrderId}`} href={`https://shapeshift.io/#/status/${shiftOrderId}`}
/> />
</span>
)} )}
{shiftState === statuses.NO_DEPOSITS && {shiftState === statuses.NO_DEPOSITS &&
shiftReturnAddress && ( shiftReturnAddress && (

View file

@ -1,7 +1,7 @@
// @flow
import React from 'react'; import React from 'react';
import Link from 'component/link';
import { getExampleAddress } from 'util/shape_shift'; import { getExampleAddress } from 'util/shape_shift';
import { Submit, FormRow } from 'component/form'; import { FormField, FormRow, Submit } from 'component/common/form';
import type { ShapeShiftFormValues, Dispatch } from 'redux/actions/shape_shift'; import type { ShapeShiftFormValues, Dispatch } from 'redux/actions/shape_shift';
import ShiftMarketInfo from './market_info'; import ShiftMarketInfo from './market_info';
@ -12,7 +12,7 @@ type ShapeShiftFormErrors = {
type Props = { type Props = {
values: ShapeShiftFormValues, values: ShapeShiftFormValues,
errors: ShapeShiftFormErrors, errors: ShapeShiftFormErrors,
touched: boolean, touched: { returnAddress: boolean },
handleChange: Event => any, handleChange: Event => any,
handleBlur: Event => any, handleBlur: Event => any,
handleSubmit: Event => any, handleSubmit: Event => any,
@ -21,7 +21,6 @@ type Props = {
originCoin: string, originCoin: string,
updating: boolean, updating: boolean,
getCoinStats: Dispatch, getCoinStats: Dispatch,
receiveAddress: string,
originCoinDepositFee: number, originCoinDepositFee: number,
originCoinDepositMin: string, originCoinDepositMin: string,
originCoinDepositMax: number, originCoinDepositMax: number,
@ -41,7 +40,6 @@ export default (props: Props) => {
originCoin, originCoin,
updating, updating,
getCoinStats, getCoinStats,
receiveAddress,
originCoinDepositMax, originCoinDepositMax,
originCoinDepositMin, originCoinDepositMin,
originCoinDepositFee, originCoinDepositFee,
@ -49,11 +47,11 @@ export default (props: Props) => {
} = props; } = props;
return ( return (
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="form-field"> <FormField
<span>{__('Exchange')} </span> prefix={__('Exchange')}
<select postfix={__('for LBC')}
className="form-field__input form-field__input-select" type="select"
name="originCoin" name="origin_coin"
onChange={e => { onChange={e => {
getCoinStats(e.target.value); getCoinStats(e.target.value);
handleChange(e); handleChange(e);
@ -64,11 +62,7 @@ export default (props: Props) => {
{coin} {coin}
</option> </option>
))} ))}
</select> </FormField>
<span> {__('for LBC')}</span>
<div className="shapeshift__tx-info">
{!updating &&
originCoinDepositMax && (
<ShiftMarketInfo <ShiftMarketInfo
originCoin={originCoin} originCoin={originCoin}
shapeShiftRate={shapeShiftRate} shapeShiftRate={shapeShiftRate}
@ -76,29 +70,30 @@ export default (props: Props) => {
originCoinDepositMin={originCoinDepositMin} originCoinDepositMin={originCoinDepositMin}
originCoinDepositMax={originCoinDepositMax} originCoinDepositMax={originCoinDepositMax}
/> />
)}
</div>
</div>
<FormRow <FormRow padded>
type="text" <FormField
name="returnAddress"
placeholder={getExampleAddress(originCoin)}
label={__('Return address')} label={__('Return address')}
error={touched.returnAddress && !!errors.returnAddress && errors.returnAddress}
type="text"
name="return_address"
className="input--address"
placeholder={getExampleAddress(originCoin)}
onChange={handleChange} onChange={handleChange}
onBlur={handleBlur} onBlur={handleBlur}
value={values.returnAddress} value={values.returnAddress}
errorMessage={errors.returnAddress}
hasError={touched.returnAddress && !!errors.returnAddress}
/> />
</FormRow>
<span className="help"> <span className="help">
<span> <span>
({__('optional but recommended')}) {__('We will return your')} {originCoin}{' '} ({__('optional but recommended')})<br />
{__('We will return your')} {originCoin}{' '}
{__("to this address if the transaction doesn't go through.")} {__("to this address if the transaction doesn't go through.")}
</span> </span>
</span> </span>
<div className="card__actions card__actions--only-vertical"> <div className="card__actions">
<Submit <Submit
button="primary"
label={__('Begin Conversion')} label={__('Begin Conversion')}
disabled={isSubmitting || !!Object.keys(errors).length} disabled={isSubmitting || !!Object.keys(errors).length}
/> />

View file

@ -1,18 +1,12 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import { shell } from 'electron';
import { Formik } from 'formik'; import { Formik } from 'formik';
import classnames from 'classnames';
import * as statuses from 'constants/shape_shift';
import { validateShapeShiftForm } from 'util/shape_shift'; import { validateShapeShiftForm } from 'util/shape_shift';
import Link from 'component/link'; import Button from 'component/button';
import Spinner from 'component/common/spinner';
import { BusyMessage } from 'component/common';
import ShapeShiftForm from './internal/form';
import ActiveShapeShift from './internal/active-shift';
import type { ShapeShiftState } from 'redux/reducers/shape_shift'; import type { ShapeShiftState } from 'redux/reducers/shape_shift';
import type { Dispatch, ShapeShiftFormValues } from 'redux/actions/shape_shift'; import type { Dispatch, ShapeShiftFormValues } from 'redux/actions/shape_shift';
import ShapeShiftForm from './internal/form';
import ActiveShapeShift from './internal/active-shift';
type Props = { type Props = {
shapeShift: ShapeShiftState, shapeShift: ShapeShiftState,
@ -72,31 +66,18 @@ class ShapeShift extends React.PureComponent<Props> {
}; };
return ( return (
// add the "shapeshift__intital-wrapper class so we can avoid content jumping once everything loads" <section className="card card--section">
// it just gives the section a min-height equal to the height of the content when the form is rendered <div className="card__title">{__('Convert Crypto to LBC')}</div>
// if the markup below changes for the initial render (form.jsx) there will be content jumping <p className="card__subtitle">
// the styling in shapeshift.scss will need to be updated to the correct min-height
<section
className={classnames('card shapeshift__wrapper', {
'shapeshift__initial-wrapper': loading,
})}
>
<div className="card__title-primary">
<h3>{__('Convert Crypto to LBC')}</h3>
<p className="help">
{__('Powered by ShapeShift. Read our FAQ')}{' '} {__('Powered by ShapeShift. Read our FAQ')}{' '}
<Link href="https://lbry.io/faq/shapeshift">{__('here')}</Link>. <Button button="link" label={__('here')} href="https://lbry.io/faq/shapeshift" />.
{hasActiveShift && {hasActiveShift &&
shiftState !== 'complete' && <span>{__('This will update automatically.')}</span>} shiftState !== 'complete' && <span>{__('This will update automatically.')}</span>}
</p> </p>
</div>
<div className="card__content shapeshift__content"> <div className="card__content">
{error && <div className="form-field__error">{error}</div>} {error && <div className="form-field__error">{error}</div>}
{loading && <Spinner dark />} {!hasActiveShift && (
{!loading &&
!hasActiveShift &&
!!shiftSupportedCoins.length && (
<Formik <Formik
onSubmit={createShapeShift} onSubmit={createShapeShift}
validate={validateShapeShiftForm} validate={validateShapeShiftForm}
@ -113,7 +94,6 @@ class ShapeShift extends React.PureComponent<Props> {
originCoinDepositMin={originCoinDepositMin} originCoinDepositMin={originCoinDepositMin}
originCoinDepositFee={originCoinDepositFee} originCoinDepositFee={originCoinDepositFee}
shapeShiftRate={shapeShiftRate} shapeShiftRate={shapeShiftRate}
updating={updating}
/> />
)} )}
/> />
@ -124,7 +104,6 @@ class ShapeShift extends React.PureComponent<Props> {
shiftCoinType={shiftCoinType} shiftCoinType={shiftCoinType}
shiftReturnAddress={shiftReturnAddress} shiftReturnAddress={shiftReturnAddress}
shiftDepositAddress={shiftDepositAddress} shiftDepositAddress={shiftDepositAddress}
originCoinDepositMax={originCoinDepositMax}
shiftOrderId={shiftOrderId} shiftOrderId={shiftOrderId}
shiftState={shiftState} shiftState={shiftState}
clearShapeShift={clearShapeShift} clearShapeShift={clearShapeShift}

View file

@ -0,0 +1,24 @@
import { connect } from 'react-redux';
import { doNavigate, doHistoryBack, doHistoryForward } from 'redux/actions/navigation';
import {
selectIsBackDisabled,
selectIsForwardDisabled,
selectNavLinks,
} from 'redux/selectors/navigation';
import { selectNotifications } from 'redux/selectors/subscriptions';
import SideBar from './view';
const select = state => ({
navLinks: selectNavLinks(state),
isBackDisabled: selectIsBackDisabled(state),
isForwardDisabled: selectIsForwardDisabled(state),
notifications: selectNotifications(state),
});
const perform = dispatch => ({
navigate: path => dispatch(doNavigate(path)),
back: () => dispatch(doHistoryBack()),
forward: () => dispatch(doHistoryForward()),
});
export default connect(select, perform)(SideBar);

View file

@ -0,0 +1,139 @@
// @flow
import * as React from 'react';
import Button from 'component/button';
import classnames from 'classnames';
import { CSSTransitionGroup } from 'react-transition-group';
import * as icons from 'constants/icons';
import * as NOTIFICATION_TYPES from 'constants/notification_types';
type SideBarLink = {
label: string,
path: string,
active: boolean,
icon: ?string,
subLinks: Array<SideBarLink>,
};
type Props = {
navigate: any => void,
back: any => void,
forward: any => void,
isBackDisabled: boolean,
isForwardDisabled: boolean,
isHome: boolean,
navLinks: {
primary: Array<SideBarLink>,
secondary: Array<SideBarLink>,
},
};
const SideBar = (props: Props) => {
const {
navigate,
back,
forward,
isBackDisabled,
isForwardDisabled,
navLinks,
notifications,
} = props;
const badges = Object.keys(notifications).reduce(
(acc, cur) => (notifications[cur].type === NOTIFICATION_TYPES.DOWNLOADING ? acc : acc + 1),
0
);
return (
<nav className="nav">
<div className="nav__actions-top">
<Button
noPadding
button="alt"
icon={icons.HOME}
className="btn--home-nav"
description={__('Home')}
onClick={() => navigate('/discover')}
/>
<div className="nav__actions-history">
<Button
className="btn--arrow"
icon={icons.ARROW_LEFT}
description={__('Navigate back')}
onClick={back}
disabled={isBackDisabled}
/>
<Button
className="btn--arrow"
icon={icons.ARROW_RIGHT}
description={__('Navigate forward')}
onClick={forward}
disabled={isForwardDisabled}
/>
</div>
</div>
<div className="nav__links">
<ul className="nav__primary">
{navLinks.primary.map(({ label, path, active, icon }) => (
<li
key={path}
className={classnames('nav__link', {
'nav__link--active': active,
})}
>
<Button
navigate={path}
label={path === '/subscriptions' && badges ? `${label} (${badges})` : label}
icon={icon}
/>
</li>
))}
</ul>
<hr />
<ul>
{navLinks.secondary.map(({ label, path, active, icon, subLinks = [] }) => (
<li
key={label}
className={classnames('nav__link', {
'nav__link--active': active,
})}
>
<Button navigate={path} label={label} icon={icon} />
{!!subLinks.length &&
active && (
<CSSTransitionGroup
transitionAppear
transitionLeave
transitionAppearTimeout={300}
transitionEnterTimeout={300}
transitionLeaveTimeout={300}
transitionName="nav__sub"
>
<ul key="0" className="nav__sub-links">
{subLinks.map(({ label: subLabel, path: subPath, active: subLinkActive }) => (
<li
key={subPath}
className={classnames('nav__link nav__link--sub', {
'nav__link--active': subLinkActive,
})}
>
{subPath ? (
<Button navigate={subPath} label={subLabel} />
) : (
<span>{subLabel}</span>
)}
</li>
))}
</ul>
</CSSTransitionGroup>
)}
</li>
))}
</ul>
</div>
</nav>
);
};
export default SideBar;

View file

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import Link from 'component/link'; import Button from 'component/button';
class SnackBar extends React.PureComponent { class SnackBar extends React.PureComponent {
constructor(props) { constructor(props) {
@ -32,7 +32,7 @@ class SnackBar extends React.PureComponent {
{message} {message}
{linkText && {linkText &&
linkTarget && ( linkTarget && (
<Link navigate={linkTarget} className="snack-bar__action" label={linkText} /> <Button navigate={linkTarget} className="snack-bar__action" label={linkText} />
)} )}
</div> </div>
); );

View file

@ -1,6 +1,4 @@
import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { selectCurrentModal, selectDaemonVersionMatched } from 'redux/selectors/app'; import { selectCurrentModal, selectDaemonVersionMatched } from 'redux/selectors/app';
import { doCheckDaemonVersion } from 'redux/actions/app'; import { doCheckDaemonVersion } from 'redux/actions/app';
import SplashScreen from './view'; import SplashScreen from './view';

View file

@ -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<Props> {
static defaultProps = {
isWarning: false,
};
render() {
const { details, message, isWarning } = this.props;
return (
<div className="load-screen">
<h1 className="load-screen__title">{__('LBRY')}</h1>
{isWarning ? (
<span className="load-screen__message">
<Icon size={20} icon={icons.ALERT} />
{` ${message}`}
</span>
) : (
<div className="load-screen__message">{message}</div>
)}
{details && <div className="load-screen__details">{details}</div>}
</div>
);
}
}
export default LoadScreen;

View file

@ -1,19 +1,25 @@
import React from 'react'; import * as React from 'react';
import PropTypes from 'prop-types'; import lbry from 'lbry';
import lbry from 'lbry.js'; import LoadScreen from './internal/load-screen';
import LoadScreen from '../load_screen.js';
import ModalIncompatibleDaemon from 'modal/modalIncompatibleDaemon'; import ModalIncompatibleDaemon from 'modal/modalIncompatibleDaemon';
import ModalUpgrade from 'modal/modalUpgrade'; import ModalUpgrade from 'modal/modalUpgrade';
import ModalDownloading from 'modal/modalDownloading'; import ModalDownloading from 'modal/modalDownloading';
import * as modals from 'constants/modal_types'; import * as modals from 'constants/modal_types';
export class SplashScreen extends React.PureComponent { type Props = {
static propTypes = { checkDaemonVersion: () => Promise<any>,
message: PropTypes.string, modal: string,
onLoadDone: PropTypes.func, };
};
constructor(props) { type State = {
details: string,
message: string,
isRunning: boolean,
isLagging: boolean,
};
export class SplashScreen extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props); super(props);
this.state = { this.state = {
@ -75,9 +81,11 @@ export class SplashScreen extends React.PureComponent {
} }
componentDidMount() { componentDidMount() {
const { checkDaemonVersion } = this.props;
lbry lbry
.connect() .connect()
.then(this.props.checkDaemonVersion) .then(checkDaemonVersion)
.then(() => { .then(() => {
this.updateStatus(); this.updateStatus();
}) })
@ -97,15 +105,19 @@ export class SplashScreen extends React.PureComponent {
const { message, details, isLagging, isRunning } = this.state; const { message, details, isLagging, isRunning } = this.state;
return ( return (
<div> <React.Fragment>
<LoadScreen message={message} details={details} isWarning={isLagging} /> <LoadScreen message={message} details={details} isWarning={isLagging} />
{/* Temp hack: don't show any modals on splash screen daemon is running; {/* 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 daemon doesn't let you quit during startup, so the "Quit" buttons
in the modals won't work. */} in the modals won't work. */}
{modal == 'incompatibleDaemon' && isRunning && <ModalIncompatibleDaemon />} {isRunning && (
{modal == modals.UPGRADE && isRunning && <ModalUpgrade />} <React.Fragment>
{modal == modals.DOWNLOADING && isRunning && <ModalDownloading />} {modal === modals.INCOMPATIBLE_DAEMON && <ModalIncompatibleDaemon />}
</div> {modal === modals.UPGRADE && <ModalUpgrade />}
{modal === modals.DOWNLOADING && <ModalDownloading />}
</React.Fragment>
)}
</React.Fragment>
); );
} }
} }

View file

@ -2,7 +2,6 @@ import { connect } from 'react-redux';
import { doChannelSubscribe, doChannelUnsubscribe } from 'redux/actions/subscriptions'; import { doChannelSubscribe, doChannelUnsubscribe } from 'redux/actions/subscriptions';
import { doOpenModal } from 'redux/actions/app'; import { doOpenModal } from 'redux/actions/app';
import { selectSubscriptions } from 'redux/selectors/subscriptions'; import { selectSubscriptions } from 'redux/selectors/subscriptions';
import SubscribeButton from './view'; import SubscribeButton from './view';
const select = (state, props) => ({ const select = (state, props) => ({

View file

@ -1,27 +1,44 @@
// @flow
import React from 'react'; import React from 'react';
import Link from 'component/link';
import * as modals from 'constants/modal_types'; 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';
export default ({ type SubscribtionArgs = {
channelName: string,
uri: string,
};
type Props = {
channelName: ?string,
uri: ?string,
subscriptions: Array<Subscription>,
doChannelSubscribe: ({ channelName: string, uri: string }) => void,
doChannelUnsubscribe: SubscribtionArgs => void,
doOpenModal: string => void,
};
export default (props: Props) => {
const {
channelName, channelName,
uri, uri,
subscriptions, subscriptions,
doChannelSubscribe, doChannelSubscribe,
doChannelUnsubscribe, doChannelUnsubscribe,
doOpenModal, doOpenModal,
}) => { } = props;
const isSubscribed = const isSubscribed =
subscriptions.map(subscription => subscription.channelName).indexOf(channelName) !== -1; subscriptions.map(subscription => subscription.channelName).indexOf(channelName) !== -1;
const subscriptionHandler = isSubscribed ? doChannelUnsubscribe : doChannelSubscribe; const subscriptionHandler = isSubscribed ? doChannelUnsubscribe : doChannelSubscribe;
const subscriptionLabel = isSubscribed ? __('Unsubscribe') : __('Subscribe'); const subscriptionLabel = isSubscribed ? __('Unsubscribe') : __('Subscribe');
return channelName && uri ? ( return channelName && uri ? (
<div className="card__actions"> <Button
<Link icon={isSubscribed ? undefined : icons.HEART}
iconRight={isSubscribed ? '' : 'at'} button={isSubscribed ? 'danger' : 'alt'}
button={isSubscribed ? 'alt' : 'primary'}
label={subscriptionLabel} label={subscriptionLabel}
onClick={() => { onClick={() => {
if (!subscriptions.length) { if (!subscriptions.length) {
@ -33,6 +50,5 @@ export default ({
}); });
}} }}
/> />
</div>
) : null; ) : null;
}; };

View file

@ -1,54 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
export class ToolTip extends React.PureComponent {
static propTypes = {
body: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
};
constructor(props) {
super(props);
this.state = {
showTooltip: false,
};
}
handleClick() {
this.setState({
showTooltip: !this.state.showTooltip,
});
}
handleTooltipMouseOut() {
this.setState({
showTooltip: false,
});
}
render() {
return (
<span className={`tooltip ${this.props.className || ''}`}>
<a
className="tooltip__link"
onClick={() => {
this.handleClick();
}}
>
{this.props.label}
</a>
<div
className={`tooltip__body ${this.state.showTooltip ? '' : ' hidden'}`}
onMouseOut={() => {
this.handleTooltipMouseOut();
}}
>
{this.props.body}
</div>
</span>
);
}
}
export default ToolTip;

View file

@ -1,91 +0,0 @@
import React from 'react';
import LinkTransaction from 'component/linkTransaction';
import { CreditAmount } from 'component/common';
import DateTime from 'component/dateTime';
import Link from 'component/link';
import { buildURI } from 'lbryURI';
import * as txnTypes from 'constants/transaction_types';
class TransactionListItem extends React.PureComponent {
abandonClaim() {
const { txid, nout } = this.props.transaction;
this.props.revokeClaim(txid, nout);
}
getLink(type) {
if (type == txnTypes.TIP) {
return (
<Link onClick={this.abandonClaim.bind(this)} icon="icon-unlock-alt" title={__('Unlock')} />
);
}
return <Link onClick={this.abandonClaim.bind(this)} icon="icon-trash" title={__('Revoke')} />;
}
capitalize(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
render() {
const { reward, transaction, isRevokeable } = this.props;
const {
amount,
claim_id: claimId,
claim_name: name,
date,
fee,
txid,
type,
nout,
} = transaction;
const dateFormat = {
month: 'short',
day: 'numeric',
year: 'numeric',
};
return (
<tr>
<td>
{date ? (
<div>
<DateTime date={date} show={DateTime.SHOW_DATE} formatOptions={dateFormat} />
<div className="meta">
<DateTime date={date} show={DateTime.SHOW_TIME} />
</div>
</div>
) : (
<span className="empty">{__('Pending')}</span>
)}
</td>
<td>
<CreditAmount amount={amount} look="plain" label={false} showPlus precision={8} />
<br />
{fee != 0 && <CreditAmount amount={fee} look="fee" label={false} precision={8} />}
</td>
<td>
{this.capitalize(type)} {isRevokeable && this.getLink(type)}
</td>
<td>
{reward && <Link navigate="/rewards">{__('Reward: %s', reward.reward_title)}</Link>}
{name &&
claimId && (
<Link
className="button-text"
navigate="/show"
navigateParams={{ uri: buildURI({ claimName: name, claimId }) }}
>
{name}
</Link>
)}
</td>
<td>
<LinkTransaction id={txid} />
</td>
</tr>
);
}
}
export default TransactionListItem;

View file

@ -0,0 +1,103 @@
// @flow
import React from 'react';
import ButtonTransaction from 'component/common/transaction-link';
import CreditAmount from 'component/common/credit-amount';
import DateTime from 'component/dateTime';
import Button from 'component/button';
import { buildURI } from 'lbryURI';
import * as txnTypes from 'constants/transaction_types';
import type { Transaction } from '../view';
type Props = {
transaction: Transaction,
revokeClaim: (string, number) => void,
isRevokeable: boolean,
reward: ?{
reward_title: string,
},
};
class TransactionListItem extends React.PureComponent<Props> {
constructor() {
super();
(this: any).abandonClaim = this.abandonClaim.bind(this);
}
abandonClaim() {
const { txid, nout } = this.props.transaction;
this.props.revokeClaim(txid, nout);
}
getLink(type: string) {
if (type === txnTypes.TIP) {
return <Button button="link" onClick={this.abandonClaim} label={__('Unlock Tip')} />;
}
return <Button button="link" onClick={this.abandonClaim} label={__('Abandon Claim')} />;
}
capitalize(string: string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
render() {
const { reward, transaction, isRevokeable } = this.props;
const { amount, claim_id: claimId, claim_name: name, date, fee, txid, type } = transaction;
const dateFormat = {
month: 'short',
day: 'numeric',
year: 'numeric',
};
return (
<tr>
<td>
<CreditAmount amount={amount} plain noStyle showPlus precision={8} />
<br />
{fee !== 0 && (
<span className="table__item-label">
<CreditAmount plain noStyle fee amount={fee} precision={8} />
</span>
)}
</td>
<td className="table__item--actionable">
<span>{this.capitalize(type)}</span> {isRevokeable && this.getLink(type)}
</td>
<td className="table__item--actionable">
{reward && <span>{reward.reward_title}</span>}
{name &&
claimId && (
<Button
button="link"
navigate="/show"
navigateParams={{ uri: buildURI({ claimName: name, claimId }) }}
>
{name}
</Button>
)}
</td>
<td>
<ButtonTransaction id={txid} />
</td>
<td>
{date ? (
<div>
<DateTime date={date} show={DateTime.SHOW_DATE} formatOptions={dateFormat} />
<div className="table__item-label">
<DateTime date={date} show={DateTime.SHOW_TIME} />
</div>
</div>
) : (
<span className="empty">{__('Pending')}</span>
)}
</td>
</tr>
);
}
}
export default TransactionListItem;

View file

@ -1,50 +1,86 @@
import React from 'react'; // @flow
import TransactionListItem from './internal/TransactionListItem'; import * as React from 'react';
import FormField from 'component/formField'; import { FormField } from 'component/common/form';
import Link from 'component/link'; import Button from 'component/button';
import FileExporter from 'component/file-exporter.js'; import FileExporter from 'component/common/file-exporter';
import * as icons from 'constants/icons'; import * as icons from 'constants/icons';
import * as modals from 'constants/modal_types'; import * as modals from 'constants/modal_types';
import TransactionListItem from './internal/transaction-list-item';
class TransactionList extends React.PureComponent { export type Transaction = {
constructor(props) { 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<Transaction>,
rewards: {},
openModal: (string, any) => void,
myClaims: any,
};
type State = {
filter: string,
};
class TransactionList extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props); super(props);
this.state = { 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({ this.setState({
filter: event.target.value, filter: event.target.value,
}); });
} }
filterTransaction(transaction) { filterTransaction(transaction: Transaction) {
const { filter } = this.state; 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 // a claim/support/update is revokable if it
// is in my claim list(claim_list_mine) // 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 }); this.props.openModal(modals.CONFIRM_CLAIM_REVOKE, { txid, nout });
} }
render() { render() {
const { emptyMessage, rewards, transactions } = this.props; const { emptyMessage, rewards, transactions, slim } = this.props;
const { filter } = this.state;
const transactionList = transactions.filter(this.filterTransaction.bind(this)); const transactionList = transactions.filter(this.filterTransaction);
return ( return (
<div> <React.Fragment>
{Boolean(transactionList.length) && ( {!transactionList.length && (
<p className="card__content">{emptyMessage || __('No transactions to list.')}</p>
)}
{!slim &&
!!transactionList.length && (
<div className="card__actions">
<FileExporter <FileExporter
data={transactionList} data={transactionList}
label={__('Export')} label={__('Export')}
@ -52,12 +88,24 @@ class TransactionList extends React.PureComponent {
filters={['nout']} filters={['nout']}
defaultPath={__('lbry-transactions-history')} defaultPath={__('lbry-transactions-history')}
/> />
</div>
)} )}
{(transactionList.length || this.state.filter) && ( {!slim && (
<span className="sort-section"> <div className="card__actions-top-corner">
{__('Filter')}{' '} <FormField
<FormField type="select" onChange={this.handleFilterChanged.bind(this)}> type="select"
<option value="">{__('All')}</option> value={filter}
onChange={this.handleFilterChanged}
prefix={__('Show')}
postfix={
<Button
button="link"
href="https://lbry.io/faq/transaction-types"
label={__('Help')}
/>
}
>
<option value="all">{__('All')}</option>
<option value="spend">{__('Spends')}</option> <option value="spend">{__('Spends')}</option>
<option value="receive">{__('Receives')}</option> <option value="receive">{__('Receives')}</option>
<option value="publish">{__('Publishes')}</option> <option value="publish">{__('Publishes')}</option>
@ -65,22 +113,18 @@ class TransactionList extends React.PureComponent {
<option value="tip">{__('Tips')}</option> <option value="tip">{__('Tips')}</option>
<option value="support">{__('Supports')}</option> <option value="support">{__('Supports')}</option>
<option value="update">{__('Updates')}</option> <option value="update">{__('Updates')}</option>
</FormField>{' '} </FormField>
<Link href="https://lbry.io/faq/transaction-types" icon={icons.HELP_CIRCLE} /> </div>
</span>
)} )}
{!transactionList.length && ( {!!transactionList.length && (
<div className="empty">{emptyMessage || __('No transactions to list.')}</div> <table className="card__content table table--transactions table--stretch">
)}
{Boolean(transactionList.length) && (
<table className="table-standard table-transactions table-stretch">
<thead> <thead>
<tr> <tr>
<th>{__('Date')}</th> <th>{__('Amount')}</th>
<th>{__('Amount (Fee)')}</th>
<th>{__('Type')} </th> <th>{__('Type')} </th>
<th>{__('Details')} </th> <th>{__('Details')} </th>
<th>{__('Transaction')}</th> <th>{__('Transaction')}</th>
<th>{__('Date')}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -90,13 +134,13 @@ class TransactionList extends React.PureComponent {
transaction={t} transaction={t}
reward={rewards && rewards[t.txid]} reward={rewards && rewards[t.txid]}
isRevokeable={this.isRevokeable(t.txid, t.nout)} isRevokeable={this.isRevokeable(t.txid, t.nout)}
revokeClaim={this.revokeClaim.bind(this)} revokeClaim={this.revokeClaim}
/> />
))} ))}
</tbody> </tbody>
</table> </table>
)} )}
</div> </React.Fragment>
); );
} }
} }

View file

@ -1,11 +1,20 @@
// @flow
import React from 'react'; import React from 'react';
import { BusyMessage } from 'component/common'; import BusyIndicator from 'component/common/busy-indicator';
import Link from 'component/link'; import Button from 'component/button';
import TransactionList from 'component/transactionList'; import TransactionList from 'component/transactionList';
import * as icons from 'constants/icons'; import * as icons from 'constants/icons';
import type { Transaction } from 'component/transactionList/view';
class TransactionListRecent extends React.PureComponent { type Props = {
componentWillMount() { fetchTransactions: () => void,
fetchingTransactions: boolean,
hasTransactions: boolean,
transactions: Array<Transaction>,
};
class TransactionListRecent extends React.PureComponent<Props> {
componentDidMount() {
this.props.fetchTransactions(); this.props.fetchTransactions();
} }
@ -13,27 +22,27 @@ class TransactionListRecent extends React.PureComponent {
const { fetchingTransactions, hasTransactions, transactions } = this.props; const { fetchingTransactions, hasTransactions, transactions } = this.props;
return ( return (
<section className="card"> <section className="card card--section">
<div className="card__title-primary"> <div className="card__title">{__('Recent Transactions')}</div>
<h3>{__('Recent Transactions')}</h3> {fetchingTransactions && (
</div>
<div className="card__content"> <div className="card__content">
{fetchingTransactions && <BusyMessage message={__('Loading transactions')} />} <BusyIndicator message={__('Loading transactions')} />
</div>
)}
{!fetchingTransactions && ( {!fetchingTransactions && (
<TransactionList <TransactionList
slim
transactions={transactions} transactions={transactions}
emptyMessage={__('You have no recent transactions.')} emptyMessage={__("Looks like you don't have any recent transactions.")}
/> />
)} )}
</div>
{hasTransactions && ( {hasTransactions && (
<div className="card__actions card__actions--bottom"> <div className="card__actions">
<Link <Button
button="primary"
navigate="/history" navigate="/history"
label={__('Full History')} label={__('Full History')}
icon={icons.HISTORY} icon={icons.CLOCK}
className="no-underline"
button="text"
/> />
</div> </div>
)} )}

View file

@ -1,35 +1,46 @@
// @flow
import React from 'react'; import React from 'react';
import Icon from 'component/icon'; import Button from 'component/button';
import Link from 'component/link';
import { buildURI } from 'lbryURI'; import { buildURI } from 'lbryURI';
import classnames from 'classnames'; 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<Props> {
componentWillMount() { componentWillMount() {
this.resolve(this.props); this.resolve(this.props);
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps: Props) {
this.resolve(nextProps); this.resolve(nextProps);
} }
resolve(props) { resolve = (props: Props) => {
const { isResolvingUri, resolveUri, claim, uri } = props; const { isResolvingUri, resolveUri, claim, uri } = props;
if (!isResolvingUri && claim === undefined && uri) { if (!isResolvingUri && claim === undefined && uri) {
resolveUri(uri); resolveUri(uri);
} }
} };
render() { render() {
const { claim, link, uri, isResolvingUri, smallCard, span } = this.props; const { claim, link, isResolvingUri } = this.props;
if (isResolvingUri && !claim) {
return <span className="empty">Validating...</span>;
}
if (!claim) { if (!claim) {
return <span className="empty">Unused</span>; return <span className="empty">{isResolvingUri ? 'Validating...' : 'Unused'}</span>;
} }
const { const {
@ -38,41 +49,28 @@ class UriIndicator extends React.PureComponent {
signature_is_valid: signatureIsValid, signature_is_valid: signatureIsValid,
value, value,
} = claim; } = claim;
const channelClaimId = const channelClaimId =
value && value.publisherSignature && value.publisherSignature.certificateId; value && value.publisherSignature && value.publisherSignature.certificateId;
if (!hasSignature || !channelName) { if (!hasSignature || !channelName) {
return <span className="empty">Anonymous</span>; return <span className="channel-name">Anonymous</span>;
} }
let icon, channelLink, modifier; let channelLink;
if (signatureIsValid) { if (signatureIsValid) {
modifier = 'valid';
channelLink = link ? buildURI({ channelName, claimId: channelClaimId }, false) : false; channelLink = link ? buildURI({ channelName, claimId: channelClaimId }, false) : false;
} else {
icon = 'icon-times-circle';
modifier = 'invalid';
} }
const inner = ( const inner = (
<span> <span>
<span <span
className={classnames('channel-name', { className={classnames('channel-name', {
'channel-name--small': smallCard,
'button-text no-underline': link, 'button-text no-underline': link,
})} })}
> >
{channelName} {channelName}
</span>{' '} </span>{' '}
{!signatureIsValid ? (
<Icon
icon={icon}
className={`channel-indicator__icon channel-indicator__icon--${modifier}`}
/>
) : (
''
)}
</span> </span>
); );
@ -81,14 +79,14 @@ class UriIndicator extends React.PureComponent {
} }
return ( return (
<Link <Button
noPadding
className="btn--uri-indicator"
navigate="/show" navigate="/show"
navigateParams={{ uri: channelLink }} navigateParams={{ uri: channelLink }}
className="no-underline"
span={span}
> >
{inner} {inner}
</Link> </Button>
); );
} }
} }

View file

@ -1,5 +1,7 @@
// I'll come back to this
/* eslint-disable */
import React from 'react'; 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 { class UserEmailNew extends React.PureComponent {
constructor(props) { constructor(props) {
@ -53,3 +55,4 @@ class UserEmailNew extends React.PureComponent {
} }
export default UserEmailNew; export default UserEmailNew;
/* eslint-enable */

Some files were not shown because too many files have changed in this diff Show more