implement app redesign
This commit is contained in:
parent
ddc15a37e3
commit
3edd660f77
249 changed files with 11090 additions and 10904 deletions
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
1
.gitignore
vendored
|
@ -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
3
flow-typed/react-feather.js
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
declare module 'react-feather' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
3
flow-typed/react-markdown.js
vendored
Normal file
3
flow-typed/react-markdown.js
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
declare module 'react-markdown' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
3
flow-typed/react-modal.js
vendored
Normal file
3
flow-typed/react-modal.js
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
declare module 'react-modal' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
3
flow-typed/react-paginate.js
vendored
Normal file
3
flow-typed/react-paginate.js
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
declare module 'react-paginate' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
7
flow-typed/react-simplemde-editor.js
vendored
Normal file
7
flow-typed/react-simplemde-editor.js
vendored
Normal 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
3
flow-typed/react-transition-group.js
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
declare module 'react-transition-group' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
3
flow-typed/render-media.js
vendored
Normal file
3
flow-typed/render-media.js
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
declare module 'render-media' {
|
||||||
|
declare module.exports: any;
|
||||||
|
}
|
|
@ -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",
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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') });
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
</FormRow>
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -1,67 +1,94 @@
|
||||||
|
// @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 />
|
||||||
<Header />
|
<main className="page">
|
||||||
<div id="main-content">
|
<SideBar />
|
||||||
<Router />
|
<Header />
|
||||||
</div>
|
<div className="content" id="content">
|
||||||
<ModalRouter />
|
<Router />
|
||||||
|
<ModalRouter />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
111
src/renderer/component/button/view.jsx
Normal file
111
src/renderer/component/button/view.jsx
Normal 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;
|
|
@ -1,46 +1,51 @@
|
||||||
|
// @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
|
||||||
'purple',
|
nsfw: ?boolean,
|
||||||
'red',
|
};
|
||||||
'pink',
|
|
||||||
'indigo',
|
|
||||||
'blue',
|
|
||||||
'light-blue',
|
|
||||||
'cyan',
|
|
||||||
'teal',
|
|
||||||
'green',
|
|
||||||
'yellow',
|
|
||||||
'orange',
|
|
||||||
];
|
|
||||||
|
|
||||||
componentWillMount() {
|
const autoThumbColors = [
|
||||||
this.setState({
|
'purple',
|
||||||
autoThumbClass:
|
'red',
|
||||||
CardMedia.AUTO_THUMB_CLASSES[
|
'pink',
|
||||||
Math.floor(Math.random() * CardMedia.AUTO_THUMB_CLASSES.length)
|
'indigo',
|
||||||
],
|
'blue',
|
||||||
});
|
'light-blue',
|
||||||
}
|
'cyan',
|
||||||
|
'teal',
|
||||||
|
'green',
|
||||||
|
'yellow',
|
||||||
|
'orange',
|
||||||
|
];
|
||||||
|
|
||||||
|
class CardMedia extends React.PureComponent<Props> {
|
||||||
|
getAutoThumbClass = () => {
|
||||||
|
return autoThumbColors[Math.floor(Math.random() * autoThumbColors.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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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>
|
</React.Fragment>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
16
src/renderer/component/common/busy-indicator.jsx
Normal file
16
src/renderer/component/common/busy-indicator.jsx
Normal 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;
|
245
src/renderer/component/common/category-list.jsx
Normal file
245
src/renderer/component/common/category-list.jsx
Normal 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;
|
100
src/renderer/component/common/credit-amount.jsx
Normal file
100
src/renderer/component/common/credit-amount.jsx
Normal 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;
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
77
src/renderer/component/common/file-selector.jsx
Normal file
77
src/renderer/component/common/file-selector.jsx
Normal 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;
|
|
@ -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;
|
100
src/renderer/component/common/form-components/form-field.jsx
Normal file
100
src/renderer/component/common/form-components/form-field.jsx
Normal 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;
|
32
src/renderer/component/common/form-components/form-row.jsx
Normal file
32
src/renderer/component/common/form-components/form-row.jsx
Normal 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;
|
27
src/renderer/component/common/form-components/form.jsx
Normal file
27
src/renderer/component/common/form-components/form.jsx
Normal 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;
|
23
src/renderer/component/common/form-components/submit.jsx
Normal file
23
src/renderer/component/common/form-components/submit.jsx
Normal 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;
|
5
src/renderer/component/common/form.jsx
Normal file
5
src/renderer/component/common/form.jsx
Normal 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';
|
36
src/renderer/component/common/icon.jsx
Normal file
36
src/renderer/component/common/icon.jsx
Normal 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;
|
6
src/renderer/component/common/lbc-symbol.jsx
Normal file
6
src/renderer/component/common/lbc-symbol.jsx
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const LbcSymbol = () => <span>LBC</span>;
|
||||||
|
|
||||||
|
export default LbcSymbol;
|
18
src/renderer/component/common/qr-code.jsx
Normal file
18
src/renderer/component/common/qr-code.jsx
Normal 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;
|
|
@ -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;
|
||||||
|
|
28
src/renderer/component/common/thumbnail.jsx
Normal file
28
src/renderer/component/common/thumbnail.jsx
Normal 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;
|
58
src/renderer/component/common/tooltip.jsx
Normal file
58
src/renderer/component/common/tooltip.jsx
Normal 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;
|
18
src/renderer/component/common/transaction-link.jsx
Normal file
18
src/renderer/component/common/transaction-link.jsx
Normal 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;
|
17
src/renderer/component/common/truncated-text.jsx
Normal file
17
src/renderer/component/common/truncated-text.jsx
Normal 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;
|
|
@ -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;
|
|
|
@ -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>
|
||||||
|
|
|
@ -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) => {
|
||||||
claim: makeSelectClaimForUri(props.uri)(state),
|
let claim;
|
||||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
let fileInfo;
|
||||||
obscureNsfw: !selectShowNsfw(state),
|
let metadata;
|
||||||
metadata: makeSelectMetadataForUri(props.uri)(state),
|
let isResolvingUri;
|
||||||
rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
|
|
||||||
isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
|
const pendingPublish = selectPendingPublish(props.uri)(state);
|
||||||
});
|
|
||||||
|
const fileCardInfo = pendingPublish || {
|
||||||
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
|
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||||
|
metadata: makeSelectMetadataForUri(props.uri)(state),
|
||||||
|
isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
obscureNsfw: !selectShowNsfw(state),
|
||||||
|
rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
|
||||||
|
...fileCardInfo,
|
||||||
|
pending: !!pendingPublish,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = dispatch => ({
|
||||||
navigate: (path, params) => dispatch(doNavigate(path, params)),
|
navigate: (path, params) => dispatch(doNavigate(path, params)),
|
||||||
|
|
|
@ -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>
|
||||||
</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>
|
</div>
|
||||||
{obscureNsfw && this.state.hovered && <NsfwOverlay />}
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
/* eslint-enable jsx-a11y/click-events-have-key-events */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,70 +1,81 @@
|
||||||
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) {
|
|
||||||
return (
|
|
||||||
<div className="card__content">
|
|
||||||
<span className="empty">{__('Empty claim or metadata info.')}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { description, language, license } = metadata;
|
|
||||||
const mediaType = lbry.getMediaType(contentType);
|
|
||||||
|
|
||||||
const downloadPath = fileInfo ? path.normalize(fileInfo.download_path) : null;
|
|
||||||
|
|
||||||
|
if (!claim || !metadata) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="card__content">
|
||||||
<div className="divider__horizontal" />
|
<span className="empty">{__('Empty claim or metadata info.')}</span>
|
||||||
<FileActions uri={uri} />
|
|
||||||
<div className="divider__horizontal" />
|
|
||||||
<div className="card__content card__subtext card__subtext--allow-newlines">
|
|
||||||
<ReactMarkdown
|
|
||||||
source={description || ''}
|
|
||||||
escapeHtml
|
|
||||||
disallowedTypes={['Heading', 'HtmlInline', 'HtmlBlock']}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="card__content">
|
|
||||||
<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>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
const { description, language, license } = metadata;
|
||||||
|
const mediaType = lbry.getMediaType(contentType);
|
||||||
|
|
||||||
|
const downloadPath = fileInfo ? path.normalize(fileInfo.download_path) : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{description && (
|
||||||
|
<React.Fragment>
|
||||||
|
<div className="card__subtext-title">About</div>
|
||||||
|
<div className="card__subtext">
|
||||||
|
<ReactMarkdown
|
||||||
|
source={description || ''}
|
||||||
|
escapeHtml
|
||||||
|
disallowedTypes={['Heading', 'HtmlInline', 'HtmlBlock']}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
)}
|
||||||
|
<div className="card__subtext-title">Info</div>
|
||||||
|
<div className="card__subtext">
|
||||||
|
<div>
|
||||||
|
{__('Content-Type')}
|
||||||
|
{': '}
|
||||||
|
{mediaType}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{__('Language')}
|
||||||
|
{': '}
|
||||||
|
{language}
|
||||||
|
</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;
|
||||||
|
|
|
@ -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() {
|
||||||
|
@ -53,38 +53,34 @@ class FileDownloadLink extends React.PureComponent {
|
||||||
|
|
||||||
if (loading || downloading) {
|
if (loading || downloading) {
|
||||||
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()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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}
|
||||||
<option value="title">{__('Title')}</option>
|
onChange={this.handleSortChanged}
|
||||||
</FormField>
|
>
|
||||||
</span>
|
<option value="date">{__('Date')}</option>
|
||||||
{content}
|
<option value="title">{__('Title')}</option>
|
||||||
|
</FormField>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="card__list">{content}</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 => ({
|
||||||
|
|
|
@ -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">
|
||||||
|
<div className="file-list__header">{__('Files')}</div>
|
||||||
|
{!isSearching &&
|
||||||
|
(fileResults.length ? (
|
||||||
|
fileResults.map(uri => <FileTile key={uri} uri={uri} />)
|
||||||
|
) : (
|
||||||
|
<NoResults />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
{isSearching && uris && <BusyMessage message={__('Refreshing the Dewey Decimals')} />}
|
<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>
|
||||||
|
|
||||||
{uris && uris.length
|
<div className="search-result__column">
|
||||||
? uris.map(
|
<div className="file-list__header">{__('Your downloads')}</div>
|
||||||
uri =>
|
{downloadUris && downloadUris.length ? (
|
||||||
parseURI(uri).claimName[0] === '@' ? (
|
downloadUris.map(uri => <FileTile test key={uri} uri={uri} />)
|
||||||
<ChannelTile key={uri} uri={uri} />
|
) : (
|
||||||
) : (
|
<NoResults />
|
||||||
<FileTile key={uri} uri={uri} />
|
)}
|
||||||
)
|
</div>
|
||||||
)
|
</div>
|
||||||
: !isSearching && <SearchNoResults query={query} />}
|
)
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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">
|
<CardMedia title={title || name} thumbnail={thumbnail} />
|
||||||
<div className="card__inner file-tile__row">
|
<div className="file-tile__info">
|
||||||
<CardMedia title={title || name} thumbnail={thumbnail} />
|
{isResolvingUri && <div className="card__title--small">{__('Loading...')}</div>}
|
||||||
<div className="file-tile__content">
|
{!isResolvingUri && (
|
||||||
<div className="card__title-primary">
|
<React.Fragment>
|
||||||
<span className="card__indicators">
|
<div className="card__title--small card__title--file">
|
||||||
{showPrice && <FilePrice uri={this.props.uri} />}{' '}
|
<TruncatedText lines={2}>{title || name}</TruncatedText>
|
||||||
{isRewardContent && <Icon icon={icons.FEATURED} />}{' '}
|
|
||||||
{showLocal && fileInfo && <Icon icon={icons.LOCAL} />}
|
|
||||||
</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} />}
|
||||||
</div>
|
{showLocal && isDownloaded && <Icon icon={icons.LOCAL} />}
|
||||||
|
</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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</React.Fragment>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
{this.state.showNsfwHelp && <NsfwOverlay />}
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -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 */
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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">
|
<WunderBar />
|
||||||
<Link
|
<div className="header__actions-right">
|
||||||
onClick={back}
|
<Button
|
||||||
disabled={isBackDisabled}
|
button="inverse"
|
||||||
button="alt button--flat"
|
className="btn--header-balance"
|
||||||
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 />
|
|
||||||
</div>
|
|
||||||
<div className="header__item">
|
|
||||||
<Link
|
|
||||||
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')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{showUpgradeButton && (
|
||||||
|
<Button
|
||||||
|
button="primary"
|
||||||
|
onClick={downloadUpgradeRequested}
|
||||||
|
icon={icons.DOWNLOAD}
|
||||||
|
label={__('Upgrade App')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="header__item">
|
|
||||||
<Link
|
|
||||||
onClick={() => navigate('/downloaded')}
|
|
||||||
button="alt button--flat"
|
|
||||||
icon="icon-folder"
|
|
||||||
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')}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import Icon from './view';
|
|
||||||
|
|
||||||
export default connect(null, null)(Icon);
|
|
|
@ -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} />;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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} />
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -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>
|
||||||
type="text"
|
<FormField
|
||||||
label="Email"
|
stretch
|
||||||
placeholder="youremail@example.org"
|
type="text"
|
||||||
name="email"
|
label="Email"
|
||||||
value={this.state.email}
|
placeholder="youremail@example.org"
|
||||||
errorMessage={errorMessage}
|
name="email"
|
||||||
onChange={event => {
|
value={this.state.email}
|
||||||
this.handleEmailChanged(event);
|
error={errorMessage}
|
||||||
}}
|
onChange={event => {
|
||||||
/>
|
this.handleEmailChanged(event);
|
||||||
<div className="form-row-submit">
|
}}
|
||||||
<Submit label={__('Send Invite')} disabled={isPending} />
|
/>
|
||||||
|
</FormRow>
|
||||||
|
<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 */
|
||||||
|
|
|
@ -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;
|
|
|
@ -1,5 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import LinkTransaction from './view';
|
|
||||||
|
|
||||||
export default connect(null, null)(LinkTransaction);
|
|
|
@ -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;
|
|
|
@ -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;
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
31
src/renderer/component/page/index.js
Normal file
31
src/renderer/component/page/index.js
Normal 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);
|
33
src/renderer/component/page/view.jsx
Normal file
33
src/renderer/component/page/view.jsx
Normal 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;
|
|
@ -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);
|
|
||||||
|
|
|
@ -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;
|
|
@ -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;
|
|
108
src/renderer/component/publishForm/internal/license-type.jsx
Normal file
108
src/renderer/component/publishForm/internal/license-type.jsx
Normal 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
|
@ -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);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -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,34 +22,31 @@ 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">
|
<thead>
|
||||||
<table className="table-standard table-stretch">
|
<tr>
|
||||||
<thead>
|
<th>{__('Title')}</th>
|
||||||
<tr>
|
<th>{__('Amount')}</th>
|
||||||
<th>{__('Title')}</th>
|
<th>{__('Transaction')}</th>
|
||||||
<th>{__('Amount')}</th>
|
<th>{__('Date')}</th>
|
||||||
<th>{__('Transaction')}</th>
|
</tr>
|
||||||
<th>{__('Date')}</th>
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{rewards.map(reward => (
|
||||||
|
<tr key={reward.id}>
|
||||||
|
<td>{reward.reward_title}</td>
|
||||||
|
<td>{reward.reward_amount}</td>
|
||||||
|
<td>
|
||||||
|
<ButtonTransaction id={reward.transaction_id} />
|
||||||
|
</td>
|
||||||
|
<td>{reward.created_at.replace('Z', ' ').replace('T', ' ')}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
))}
|
||||||
<tbody>
|
</tbody>
|
||||||
{rewards.map(reward => (
|
</table>
|
||||||
<tr key={reward.id}>
|
|
||||||
<td>{reward.reward_title}</td>
|
|
||||||
<td>{reward.reward_amount}</td>
|
|
||||||
<td>
|
|
||||||
<LinkTransaction id={reward.transaction_id} />
|
|
||||||
</td>
|
|
||||||
<td>{reward.created_at.replace('Z', ' ').replace('T', ' ')}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
{__('to learn more about LBRY Rewards')}.
|
{__('You have')}
|
||||||
</p>
|
|
||||||
</div>
|
<CreditAmount noStyle amount={unclaimedRewardAmount} precision={8} />
|
||||||
<div className="card__content">
|
|
||||||
{unclaimedRewardAmount > 0 ? (
|
|
||||||
<p>
|
|
||||||
{__('You have')} <CreditAmount amount={unclaimedRewardAmount} precision={8} />{' '}
|
|
||||||
{__('in unclaimed rewards')}.
|
{__('in unclaimed rewards')}.
|
||||||
</p>
|
</React.Fragment>
|
||||||
) : (
|
) : (
|
||||||
<p>{__('There are no rewards available at this time, please check back later')}.</p>
|
<React.Fragment>
|
||||||
|
{__('There are no rewards available at this time, please check back later')}.
|
||||||
|
</React.Fragment>
|
||||||
)}
|
)}
|
||||||
</div>
|
</p>
|
||||||
<div className="card__actions">
|
<div className="card__actions">
|
||||||
<Link button="primary" navigate="/rewards" label={__('Claim Rewards')} />
|
<Button
|
||||||
|
button="primary"
|
||||||
|
navigate="/rewards"
|
||||||
|
label={hasRewards ? __('Claim Rewards') : __('View Rewards')}
|
||||||
|
/>
|
||||||
</div>
|
</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')}.
|
||||||
|
</p>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,35 +1,43 @@
|
||||||
|
// @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 &&
|
||||||
{reward.reward_type == rewards.TYPE_REFERRAL && (
|
(claimed ? (
|
||||||
<Link button="alt" navigate="/invite" label={__('Go To Invites')} />
|
<span>
|
||||||
)}
|
<Icon icon={icons.CHECK} /> {__('Reward claimed.')}
|
||||||
{reward.reward_type !== rewards.TYPE_REFERRAL &&
|
</span>
|
||||||
(claimed ? (
|
) : (
|
||||||
<span>
|
<RewardLink reward_type={reward.reward_type} />
|
||||||
<Icon icon="icon-check" /> {__('Reward claimed.')}
|
))}
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<RewardLink button="alt" reward_type={reward.reward_type} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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} />,
|
||||||
|
|
18
src/renderer/component/selectChannel/index.js
Normal file
18
src/renderer/component/selectChannel/index.js
Normal 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);
|
220
src/renderer/component/selectChannel/view.jsx
Normal file
220
src/renderer/component/selectChannel/view.jsx
Normal 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;
|
|
@ -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 && (
|
||||||
<Address address={shiftDepositAddress} showCopyButton />
|
<FormRow verticallyCentered padded>
|
||||||
<div className="shapeshift__qrcode">
|
<Address address={shiftDepositAddress} showCopyButton />
|
||||||
<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 && (
|
||||||
|
|
|
@ -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,56 +47,53 @@ 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);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{shiftSupportedCoins.map(coin => (
|
{shiftSupportedCoins.map(coin => (
|
||||||
<option key={coin} value={coin}>
|
<option key={coin} value={coin}>
|
||||||
{coin}
|
{coin}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</FormField>
|
||||||
<span> {__('for LBC')}</span>
|
<ShiftMarketInfo
|
||||||
<div className="shapeshift__tx-info">
|
originCoin={originCoin}
|
||||||
{!updating &&
|
shapeShiftRate={shapeShiftRate}
|
||||||
originCoinDepositMax && (
|
originCoinDepositFee={originCoinDepositFee}
|
||||||
<ShiftMarketInfo
|
originCoinDepositMin={originCoinDepositMin}
|
||||||
originCoin={originCoin}
|
originCoinDepositMax={originCoinDepositMax}
|
||||||
shapeShiftRate={shapeShiftRate}
|
|
||||||
originCoinDepositFee={originCoinDepositFee}
|
|
||||||
originCoinDepositMin={originCoinDepositMin}
|
|
||||||
originCoinDepositMax={originCoinDepositMax}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormRow
|
|
||||||
type="text"
|
|
||||||
name="returnAddress"
|
|
||||||
placeholder={getExampleAddress(originCoin)}
|
|
||||||
label={__('Return address')}
|
|
||||||
onChange={handleChange}
|
|
||||||
onBlur={handleBlur}
|
|
||||||
value={values.returnAddress}
|
|
||||||
errorMessage={errors.returnAddress}
|
|
||||||
hasError={touched.returnAddress && !!errors.returnAddress}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormRow padded>
|
||||||
|
<FormField
|
||||||
|
label={__('Return address')}
|
||||||
|
error={touched.returnAddress && !!errors.returnAddress && errors.returnAddress}
|
||||||
|
type="text"
|
||||||
|
name="return_address"
|
||||||
|
className="input--address"
|
||||||
|
placeholder={getExampleAddress(originCoin)}
|
||||||
|
onChange={handleChange}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
value={values.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}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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,59 +66,44 @@ 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
|
{__('Powered by ShapeShift. Read our FAQ')}{' '}
|
||||||
<section
|
<Button button="link" label={__('here')} href="https://lbry.io/faq/shapeshift" />.
|
||||||
className={classnames('card shapeshift__wrapper', {
|
{hasActiveShift &&
|
||||||
'shapeshift__initial-wrapper': loading,
|
shiftState !== 'complete' && <span>{__('This will update automatically.')}</span>}
|
||||||
})}
|
</p>
|
||||||
>
|
|
||||||
<div className="card__title-primary">
|
|
||||||
<h3>{__('Convert Crypto to LBC')}</h3>
|
|
||||||
<p className="help">
|
|
||||||
{__('Powered by ShapeShift. Read our FAQ')}{' '}
|
|
||||||
<Link href="https://lbry.io/faq/shapeshift">{__('here')}</Link>.
|
|
||||||
{hasActiveShift &&
|
|
||||||
shiftState !== 'complete' && <span>{__('This will update automatically.')}</span>}
|
|
||||||
</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 &&
|
<Formik
|
||||||
!hasActiveShift &&
|
onSubmit={createShapeShift}
|
||||||
!!shiftSupportedCoins.length && (
|
validate={validateShapeShiftForm}
|
||||||
<Formik
|
initialValues={initialFormValues}
|
||||||
onSubmit={createShapeShift}
|
render={formProps => (
|
||||||
validate={validateShapeShiftForm}
|
<ShapeShiftForm
|
||||||
initialValues={initialFormValues}
|
{...formProps}
|
||||||
render={formProps => (
|
updating={updating}
|
||||||
<ShapeShiftForm
|
originCoin={originCoin}
|
||||||
{...formProps}
|
shiftSupportedCoins={shiftSupportedCoins}
|
||||||
updating={updating}
|
getCoinStats={getCoinStats}
|
||||||
originCoin={originCoin}
|
receiveAddress={receiveAddress}
|
||||||
shiftSupportedCoins={shiftSupportedCoins}
|
originCoinDepositMax={originCoinDepositMax}
|
||||||
getCoinStats={getCoinStats}
|
originCoinDepositMin={originCoinDepositMin}
|
||||||
receiveAddress={receiveAddress}
|
originCoinDepositFee={originCoinDepositFee}
|
||||||
originCoinDepositMax={originCoinDepositMax}
|
shapeShiftRate={shapeShiftRate}
|
||||||
originCoinDepositMin={originCoinDepositMin}
|
/>
|
||||||
originCoinDepositFee={originCoinDepositFee}
|
)}
|
||||||
shapeShiftRate={shapeShiftRate}
|
/>
|
||||||
updating={updating}
|
)}
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{hasActiveShift && (
|
{hasActiveShift && (
|
||||||
<ActiveShapeShift
|
<ActiveShapeShift
|
||||||
getActiveShift={getActiveShift}
|
getActiveShift={getActiveShift}
|
||||||
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}
|
||||||
|
|
24
src/renderer/component/sideBar/index.js
Normal file
24
src/renderer/component/sideBar/index.js
Normal 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);
|
139
src/renderer/component/sideBar/view.jsx
Normal file
139
src/renderer/component/sideBar/view.jsx
Normal 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;
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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';
|
||||||
|
|
38
src/renderer/component/splash/internal/load-screen.jsx
Normal file
38
src/renderer/component/splash/internal/load-screen.jsx
Normal 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;
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) => ({
|
||||||
|
|
|
@ -1,38 +1,54 @@
|
||||||
|
// @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';
|
||||||
|
|
||||||
|
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,
|
||||||
|
uri,
|
||||||
|
subscriptions,
|
||||||
|
doChannelSubscribe,
|
||||||
|
doChannelUnsubscribe,
|
||||||
|
doOpenModal,
|
||||||
|
} = props;
|
||||||
|
|
||||||
export default ({
|
|
||||||
channelName,
|
|
||||||
uri,
|
|
||||||
subscriptions,
|
|
||||||
doChannelSubscribe,
|
|
||||||
doChannelUnsubscribe,
|
|
||||||
doOpenModal,
|
|
||||||
}) => {
|
|
||||||
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) {
|
doOpenModal(modals.FIRST_SUBSCRIPTION);
|
||||||
doOpenModal(modals.FIRST_SUBSCRIPTION);
|
}
|
||||||
}
|
subscriptionHandler({
|
||||||
subscriptionHandler({
|
channelName,
|
||||||
channelName,
|
uri,
|
||||||
uri,
|
});
|
||||||
});
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : null;
|
) : null;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
|
|
@ -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;
|
|
|
@ -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;
|
|
@ -1,63 +1,111 @@
|
||||||
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 && (
|
||||||
<FileExporter
|
<p className="card__content">{emptyMessage || __('No transactions to list.')}</p>
|
||||||
data={transactionList}
|
|
||||||
label={__('Export')}
|
|
||||||
title={__('Export Transactions')}
|
|
||||||
filters={['nout']}
|
|
||||||
defaultPath={__('lbry-transactions-history')}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{(transactionList.length || this.state.filter) && (
|
{!slim &&
|
||||||
<span className="sort-section">
|
!!transactionList.length && (
|
||||||
{__('Filter')}{' '}
|
<div className="card__actions">
|
||||||
<FormField type="select" onChange={this.handleFilterChanged.bind(this)}>
|
<FileExporter
|
||||||
<option value="">{__('All')}</option>
|
data={transactionList}
|
||||||
|
label={__('Export')}
|
||||||
|
title={__('Export Transactions')}
|
||||||
|
filters={['nout']}
|
||||||
|
defaultPath={__('lbry-transactions-history')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!slim && (
|
||||||
|
<div className="card__actions-top-corner">
|
||||||
|
<FormField
|
||||||
|
type="select"
|
||||||
|
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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">
|
<BusyIndicator message={__('Loading transactions')} />
|
||||||
{fetchingTransactions && <BusyMessage message={__('Loading transactions')} />}
|
</div>
|
||||||
{!fetchingTransactions && (
|
)}
|
||||||
<TransactionList
|
{!fetchingTransactions && (
|
||||||
transactions={transactions}
|
<TransactionList
|
||||||
emptyMessage={__('You have no recent transactions.')}
|
slim
|
||||||
/>
|
transactions={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>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
Loading…
Reference in a new issue