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,
|
||||
"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='^page\(.*\)$' -> '<PROJECT_ROOT>/src/renderer/page\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]
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -4,3 +4,4 @@
|
|||
/static/daemon/lbrynet*
|
||||
/static/locales
|
||||
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",
|
||||
"classnames": "^2.2.5",
|
||||
"country-data": "^0.0.31",
|
||||
"dom-scroll-into-view": "^1.2.1",
|
||||
"electron-dl": "^1.11.0",
|
||||
"electron-is-dev": "^0.3.0",
|
||||
"electron-log": "^2.2.12",
|
||||
|
@ -46,7 +47,10 @@
|
|||
"electron-window-state": "^4.1.1",
|
||||
"find-process": "^1.1.0",
|
||||
"formik": "^0.10.4",
|
||||
"keytar-prebuild": "4.0.4",
|
||||
"from2": "^2.3.0",
|
||||
"install": "^0.10.2",
|
||||
"jshashes": "^1.0.7",
|
||||
"keytar-prebuild": "4.1.1",
|
||||
"localforage": "^1.5.0",
|
||||
"mixpanel-browser": "^2.17.1",
|
||||
"moment": "^2.20.1",
|
||||
|
@ -54,11 +58,13 @@
|
|||
"rc-progress": "^2.0.6",
|
||||
"react": "^16.2.0",
|
||||
"react-dom": "^16.2.0",
|
||||
"react-feather": "^1.0.8",
|
||||
"react-markdown": "^2.5.0",
|
||||
"react-modal": "^3.1.7",
|
||||
"react-paginate": "^5.2.1",
|
||||
"react-redux": "^5.0.3",
|
||||
"react-simplemde-editor": "^3.6.11",
|
||||
"react-transition-group": "1.x",
|
||||
"redux": "^3.6.0",
|
||||
"redux-logger": "^3.0.1",
|
||||
"redux-persist": "^4.8.0",
|
||||
|
|
|
@ -16,8 +16,8 @@ export default appState => {
|
|||
});
|
||||
|
||||
let windowConfiguration = {
|
||||
backgroundColor: '#155B4A',
|
||||
minWidth: 800,
|
||||
backgroundColor: '#44b098',
|
||||
minWidth: 950,
|
||||
minHeight: 600,
|
||||
autoHideMenuBar: true,
|
||||
show: false,
|
||||
|
|
|
@ -1,52 +1,52 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { clipboard } from 'electron';
|
||||
import Link from 'component/link';
|
||||
import classnames from 'classnames';
|
||||
import { FormRow } from 'component/common/form';
|
||||
import Button from 'component/button';
|
||||
import * as icons from 'constants/icons';
|
||||
|
||||
export default class Address extends React.PureComponent {
|
||||
static propTypes = {
|
||||
address: PropTypes.string,
|
||||
};
|
||||
type Props = {
|
||||
address: string,
|
||||
doShowSnackBar: ({ message: string }) => void,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
export default class Address extends React.PureComponent<Props> {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this._inputElem = null;
|
||||
this.input = null;
|
||||
}
|
||||
|
||||
input: ?HTMLInputElement;
|
||||
|
||||
render() {
|
||||
const { address, showCopyButton, doShowSnackBar } = this.props;
|
||||
const { address, doShowSnackBar } = this.props;
|
||||
|
||||
return (
|
||||
<div className="form-field form-field--address">
|
||||
<FormRow verticallyCentered padded>
|
||||
<input
|
||||
className={classnames('input-copyable', {
|
||||
'input-copyable--with-copy-btn': showCopyButton,
|
||||
})}
|
||||
type="text"
|
||||
className="input-copyable form-field__input"
|
||||
readOnly
|
||||
value={address || ''}
|
||||
ref={input => {
|
||||
this._inputElem = input;
|
||||
this.input = input;
|
||||
}}
|
||||
onFocus={() => {
|
||||
this._inputElem.select();
|
||||
if (this.input) {
|
||||
this.input.select();
|
||||
}
|
||||
}}
|
||||
readOnly="readonly"
|
||||
value={address || ''}
|
||||
/>
|
||||
{showCopyButton && (
|
||||
<span className="header__item">
|
||||
<Link
|
||||
button="alt button--flat"
|
||||
icon="clipboard"
|
||||
onClick={() => {
|
||||
clipboard.writeText(address);
|
||||
doShowSnackBar({ message: __('Address copied') });
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
noPadding
|
||||
button="secondary"
|
||||
icon={icons.CLIPBOARD}
|
||||
onClick={() => {
|
||||
clipboard.writeText(address);
|
||||
doShowSnackBar({ message: __('Address copied') });
|
||||
}}
|
||||
/>
|
||||
</FormRow>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
selectPageTitle,
|
||||
|
@ -10,7 +9,7 @@ import { doAlertError } from 'redux/actions/app';
|
|||
import { doRecordScroll } from 'redux/actions/navigation';
|
||||
import App from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
const select = state => ({
|
||||
pageTitle: selectPageTitle(state),
|
||||
user: selectUser(state),
|
||||
currentStackIndex: selectHistoryIndex(state),
|
||||
|
|
|
@ -1,67 +1,94 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import Router from 'component/router/index';
|
||||
import Header from 'component/header';
|
||||
import Theme from 'component/theme';
|
||||
import ModalRouter from 'modal/modalRouter';
|
||||
import ReactModal from 'react-modal';
|
||||
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() {
|
||||
super();
|
||||
this.mainContent = undefined;
|
||||
(this: any).scrollListener = this.scrollListener.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { recordScroll } = this.props;
|
||||
const mainContent = document.getElementById('main-content');
|
||||
const mainContent = document.getElementById('content');
|
||||
this.mainContent = mainContent;
|
||||
|
||||
const scrollListener = () => recordScroll(this.mainContent.scrollTop);
|
||||
|
||||
this.mainContent.addEventListener('scroll', throttle(scrollListener, 750));
|
||||
if (this.mainContent) {
|
||||
this.mainContent.addEventListener('scroll', throttle(this.scrollListener, 750));
|
||||
}
|
||||
|
||||
ReactModal.setAppElement('#window'); // fuck this
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.mainContent.removeEventListener('scroll', this.scrollListener);
|
||||
componentWillReceiveProps(props: Props) {
|
||||
const { pageTitle } = props;
|
||||
this.setTitleFromProps(pageTitle);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(props) {
|
||||
this.setTitleFromProps(props);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
const { currentStackIndex: prevStackIndex } = prevProps;
|
||||
const { currentStackIndex, currentPageAttributes } = this.props;
|
||||
|
||||
if (currentStackIndex !== prevStackIndex) {
|
||||
if (this.mainContent && currentStackIndex !== prevStackIndex && currentPageAttributes) {
|
||||
this.mainContent.scrollTop = currentPageAttributes.scrollY || 0;
|
||||
}
|
||||
}
|
||||
|
||||
setTitleFromProps(props) {
|
||||
window.document.title = props.pageTitle || 'LBRY';
|
||||
componentWillUnmount() {
|
||||
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() {
|
||||
return (
|
||||
<div id="window">
|
||||
<Theme />
|
||||
<Header />
|
||||
<div id="main-content">
|
||||
<Router />
|
||||
</div>
|
||||
<ModalRouter />
|
||||
<main className="page">
|
||||
<SideBar />
|
||||
<Header />
|
||||
<div className="content" id="content">
|
||||
<Router />
|
||||
<ModalRouter />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { doNavigate } from 'redux/actions/navigation';
|
||||
import Link from './view';
|
||||
import Button from './view';
|
||||
|
||||
const perform = dispatch => ({
|
||||
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 classnames from 'classnames';
|
||||
|
||||
class CardMedia extends React.PureComponent {
|
||||
static AUTO_THUMB_CLASSES = [
|
||||
'purple',
|
||||
'red',
|
||||
'pink',
|
||||
'indigo',
|
||||
'blue',
|
||||
'light-blue',
|
||||
'cyan',
|
||||
'teal',
|
||||
'green',
|
||||
'yellow',
|
||||
'orange',
|
||||
];
|
||||
type Props = {
|
||||
thumbnail: ?string, // externally sourced image
|
||||
nsfw: ?boolean,
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
this.setState({
|
||||
autoThumbClass:
|
||||
CardMedia.AUTO_THUMB_CLASSES[
|
||||
Math.floor(Math.random() * CardMedia.AUTO_THUMB_CLASSES.length)
|
||||
],
|
||||
});
|
||||
}
|
||||
const autoThumbColors = [
|
||||
'purple',
|
||||
'red',
|
||||
'pink',
|
||||
'indigo',
|
||||
'blue',
|
||||
'light-blue',
|
||||
'cyan',
|
||||
'teal',
|
||||
'green',
|
||||
'yellow',
|
||||
'orange',
|
||||
];
|
||||
|
||||
class CardMedia extends React.PureComponent<Props> {
|
||||
getAutoThumbClass = () => {
|
||||
return autoThumbColors[Math.floor(Math.random() * autoThumbColors.length)];
|
||||
};
|
||||
|
||||
render() {
|
||||
const { title, thumbnail } = this.props;
|
||||
const atClass = this.state.autoThumbClass;
|
||||
const { thumbnail, nsfw } = this.props;
|
||||
|
||||
if (thumbnail) {
|
||||
return <div className="card__media" style={{ backgroundImage: `url('${thumbnail}')` }} />;
|
||||
const generateAutothumb = !thumbnail && !nsfw;
|
||||
let autoThumbClass;
|
||||
if (generateAutothumb) {
|
||||
autoThumbClass = `card__media--autothumb.${this.getAutoThumbClass()}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`card__media card__media--autothumb ${atClass}`}>
|
||||
<div className="card__autothumb__text">
|
||||
{title &&
|
||||
title
|
||||
.replace(/\s+/g, '')
|
||||
.substring(0, Math.min(title.replace(' ', '').length, 5))
|
||||
.toUpperCase()}
|
||||
</div>
|
||||
<div
|
||||
style={thumbnail && !nsfw ? { backgroundImage: `url('${thumbnail}')` } : {}}
|
||||
className={classnames('card__media', autoThumbClass, {
|
||||
'card__media--no-img': !thumbnail || nsfw,
|
||||
'card__media--nsfw': nsfw,
|
||||
})}
|
||||
>
|
||||
{(!thumbnail || nsfw) && (
|
||||
<span className="card__media-text">{nsfw ? __('NSFW') : 'LBRY'}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Link from 'component/link';
|
||||
import Button from 'component/button';
|
||||
import * as icons from 'constants/icons';
|
||||
|
||||
let scriptLoading = false;
|
||||
let scriptLoaded = false;
|
||||
|
@ -156,10 +157,10 @@ class CardVerify extends React.Component {
|
|||
|
||||
render() {
|
||||
return (
|
||||
<Link
|
||||
<Button
|
||||
button="alt"
|
||||
label={this.props.label}
|
||||
icon="icon-lock"
|
||||
icon={icons.LOCK}
|
||||
disabled={this.props.disabled || this.state.open || this.hasPendingClick}
|
||||
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 { 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() {
|
||||
const { uri, resolveUri } = this.props;
|
||||
|
||||
resolveUri(uri);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
componentWillReceiveProps(nextProps: Props) {
|
||||
const { uri, resolveUri } = this.props;
|
||||
|
||||
if (nextProps.uri != uri) {
|
||||
if (nextProps.uri !== uri) {
|
||||
resolveUri(uri);
|
||||
}
|
||||
}
|
||||
|
@ -29,29 +47,25 @@ class ChannelTile extends React.PureComponent {
|
|||
const onClick = () => navigate('/show', { uri });
|
||||
|
||||
return (
|
||||
<section className="file-tile card">
|
||||
<div onClick={onClick} className="card__link">
|
||||
<div className="card__inner file-tile__row">
|
||||
{channelName && <CardMedia title={channelName} thumbnail={null} />}
|
||||
<div className="file-tile__content">
|
||||
<div className="card__title-primary">
|
||||
<h3>
|
||||
<TruncatedText lines={1}>{channelName || uri}</TruncatedText>
|
||||
</h3>
|
||||
<section className="file-tile card--link" onClick={onClick} role="button">
|
||||
<CardMedia title={channelName} thumbnail={null} />
|
||||
<div className="file-tile__info">
|
||||
{isResolvingUri && <div className="card__title--small">{__('Loading...')}</div>}
|
||||
{!isResolvingUri && (
|
||||
<React.Fragment>
|
||||
<div className="card__title--small card__title--file">
|
||||
<TruncatedText lines={1}>{channelName || uri}</TruncatedText>
|
||||
</div>
|
||||
<div className="card__content card__subtext">
|
||||
{isResolvingUri && <BusyMessage message={__('Resolving channel')} />}
|
||||
<div className="card__subtitle">
|
||||
{totalItems > 0 && (
|
||||
<span>
|
||||
This is a channel with {totalItems} {totalItems === 1 ? ' item' : ' items'}{' '}
|
||||
inside of it.
|
||||
{totalItems} {totalItems === 1 ? 'file' : 'files'}
|
||||
</span>
|
||||
)}
|
||||
{!isResolvingUri &&
|
||||
!totalItems && <span className="empty">This is an empty channel.</span>}
|
||||
{!isResolvingUri && !totalItems && <span>This is an empty channel.</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
</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 path from 'path';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Link from 'component/link';
|
||||
import Button from 'component/button';
|
||||
import parseData from 'util/parseData';
|
||||
import * as icons from 'constants/icons';
|
||||
const { remote } = require('electron');
|
||||
import { remote } from 'electron';
|
||||
|
||||
class FileExporter extends React.PureComponent {
|
||||
static propTypes = {
|
||||
data: PropTypes.array,
|
||||
title: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
filters: PropTypes.arrayOf(PropTypes.string),
|
||||
defaultPath: PropTypes.string,
|
||||
onFileCreated: PropTypes.func,
|
||||
};
|
||||
type Props = {
|
||||
data: Array<any>,
|
||||
title: string,
|
||||
label: string,
|
||||
defaultPath?: string,
|
||||
filters: Array<string>,
|
||||
onFileCreated?: string => void,
|
||||
};
|
||||
|
||||
class FileExporter extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
filters: [],
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
constructor() {
|
||||
super();
|
||||
this.handleButtonClick = this.handleButtonClick.bind(this);
|
||||
}
|
||||
|
||||
handleFileCreation(filename, data) {
|
||||
handleButtonClick: () => void;
|
||||
|
||||
handleFileCreation(filename: string, data: any) {
|
||||
const { onFileCreated } = this.props;
|
||||
fs.writeFile(filename, data, err => {
|
||||
if (err) throw err;
|
||||
|
@ -67,12 +71,11 @@ class FileExporter extends React.PureComponent {
|
|||
render() {
|
||||
const { title, label } = this.props;
|
||||
return (
|
||||
<Link
|
||||
<Button
|
||||
button="primary"
|
||||
icon={icons.DOWNLOAD}
|
||||
title={title || __('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';
|
||||
|
||||
export default ({ dark, className }) => (
|
||||
<div
|
||||
className={classnames(
|
||||
'spinner',
|
||||
{
|
||||
'spinner--dark': dark,
|
||||
},
|
||||
className
|
||||
)}
|
||||
/>
|
||||
);
|
||||
type Props = {
|
||||
dark?: boolean,
|
||||
};
|
||||
|
||||
class Spinner extends React.Component<Props> {
|
||||
static defaultProps = {
|
||||
dark: false,
|
||||
};
|
||||
|
||||
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 Link from 'component/link';
|
||||
import Button from 'component/button';
|
||||
import FileDownloadLink from 'component/fileDownloadLink';
|
||||
import * as modals from 'constants/modal_types';
|
||||
import classnames from 'classnames';
|
||||
import * as icons from 'constants/icons';
|
||||
|
||||
class FileActions extends React.PureComponent {
|
||||
type FileInfo = {
|
||||
claim_id: string,
|
||||
};
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
openModal: (string, any) => void,
|
||||
claimIsMine: boolean,
|
||||
fileInfo: FileInfo,
|
||||
vertical?: boolean, // should the buttons be stacked vertically?
|
||||
};
|
||||
|
||||
class FileActions extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const { fileInfo, uri, openModal, claimIsMine } = this.props;
|
||||
const { fileInfo, uri, openModal, claimIsMine, vertical } = this.props;
|
||||
|
||||
const claimId = fileInfo ? fileInfo.claim_id : null,
|
||||
showDelete = fileInfo && Object.keys(fileInfo).length > 0;
|
||||
const claimId = fileInfo ? fileInfo.claim_id : '';
|
||||
const showDelete = fileInfo && Object.keys(fileInfo).length > 0;
|
||||
|
||||
return (
|
||||
<section className="card__actions">
|
||||
<section className={classnames('card__actions', { 'card__actions--vertical': vertical })}>
|
||||
<FileDownloadLink uri={uri} />
|
||||
{showDelete && (
|
||||
<Link
|
||||
button="text"
|
||||
icon="icon-trash"
|
||||
label={__('Remove')}
|
||||
className="no-underline"
|
||||
<Button
|
||||
className="btn--file-actions"
|
||||
icon={icons.TRASH}
|
||||
description={__('Delete')}
|
||||
onClick={() => openModal(modals.CONFIRM_FILE_REMOVE, { uri })}
|
||||
/>
|
||||
)}
|
||||
{!claimIsMine && (
|
||||
<Link
|
||||
button="text"
|
||||
icon="icon-flag"
|
||||
<Button
|
||||
className="btn--file-actions"
|
||||
icon={icons.REPORT}
|
||||
href={`https://lbry.io/dmca?claim_id=${claimId}`}
|
||||
className="no-underline"
|
||||
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 }}
|
||||
description={__('Report content')}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
|
|
|
@ -6,16 +6,31 @@ import { selectShowNsfw } from 'redux/selectors/settings';
|
|||
import { makeSelectClaimForUri, makeSelectMetadataForUri } from 'redux/selectors/claims';
|
||||
import { makeSelectFileInfoForUri } from 'redux/selectors/file_info';
|
||||
import { makeSelectIsUriResolving, selectRewardContentClaimIds } from 'redux/selectors/content';
|
||||
import { selectPendingPublish } from 'redux/selectors/publish';
|
||||
import FileCard from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||
obscureNsfw: !selectShowNsfw(state),
|
||||
metadata: makeSelectMetadataForUri(props.uri)(state),
|
||||
rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
|
||||
isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
|
||||
});
|
||||
const select = (state, props) => {
|
||||
let claim;
|
||||
let fileInfo;
|
||||
let metadata;
|
||||
let isResolvingUri;
|
||||
|
||||
const pendingPublish = selectPendingPublish(props.uri)(state);
|
||||
|
||||
const fileCardInfo = pendingPublish || {
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||
metadata: makeSelectMetadataForUri(props.uri)(state),
|
||||
isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
|
||||
};
|
||||
|
||||
return {
|
||||
obscureNsfw: !selectShowNsfw(state),
|
||||
rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
|
||||
...fileCardInfo,
|
||||
pending: !!pendingPublish,
|
||||
};
|
||||
};
|
||||
|
||||
const perform = dispatch => ({
|
||||
navigate: (path, params) => dispatch(doNavigate(path, params)),
|
||||
|
|
|
@ -1,111 +1,102 @@
|
|||
import React from 'react';
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { normalizeURI } from 'lbryURI';
|
||||
import CardMedia from 'component/cardMedia';
|
||||
import Link from 'component/link';
|
||||
import { TruncatedText } from 'component/common';
|
||||
import Icon from 'component/icon';
|
||||
import TruncatedText from 'component/common/truncated-text';
|
||||
import Icon from 'component/common/icon';
|
||||
import FilePrice from 'component/filePrice';
|
||||
import UriIndicator from 'component/uriIndicator';
|
||||
import NsfwOverlay from 'component/nsfwOverlay';
|
||||
import TruncatedMarkdown from 'component/truncatedMarkdown';
|
||||
import * as icons from 'constants/icons';
|
||||
import classnames from 'classnames';
|
||||
|
||||
class FileCard extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
// TODO: iron these out
|
||||
type Props = {
|
||||
isResolvingUri: boolean,
|
||||
resolveUri: string => void,
|
||||
uri: string,
|
||||
claim: ?{ claim_id: string },
|
||||
fileInfo: ?{},
|
||||
metadata: ?{ nsfw: boolean, thumbnail: ?string },
|
||||
navigate: (string, ?{}) => void,
|
||||
rewardedContentClaimIds: Array<string>,
|
||||
obscureNsfw: boolean,
|
||||
showPrice: boolean,
|
||||
pending?: boolean,
|
||||
};
|
||||
|
||||
this.state = {
|
||||
hovered: false,
|
||||
};
|
||||
}
|
||||
class FileCard extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
showPrice: true,
|
||||
};
|
||||
|
||||
componentWillMount() {
|
||||
this.resolve(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
componentWillReceiveProps(nextProps: Props) {
|
||||
this.resolve(nextProps);
|
||||
}
|
||||
|
||||
resolve(props) {
|
||||
resolve = (props: Props) => {
|
||||
const { isResolvingUri, resolveUri, claim, uri } = props;
|
||||
|
||||
if (!isResolvingUri && claim === undefined && uri) {
|
||||
resolveUri(uri);
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseOver() {
|
||||
this.setState({
|
||||
hovered: true,
|
||||
});
|
||||
}
|
||||
|
||||
handleMouseOut() {
|
||||
this.setState({
|
||||
hovered: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
claim,
|
||||
fileInfo,
|
||||
metadata,
|
||||
isResolvingUri,
|
||||
navigate,
|
||||
rewardedContentClaimIds,
|
||||
obscureNsfw,
|
||||
showPrice,
|
||||
pending,
|
||||
} = this.props;
|
||||
|
||||
const uri = normalizeURI(this.props.uri);
|
||||
const uri = !pending ? normalizeURI(this.props.uri) : this.props.uri;
|
||||
const title = metadata && metadata.title ? metadata.title : uri;
|
||||
const thumbnail = metadata && metadata.thumbnail ? metadata.thumbnail : null;
|
||||
const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw;
|
||||
const shouldObscureNsfw = obscureNsfw && metadata && metadata.nsfw;
|
||||
const isRewardContent = claim && rewardedContentClaimIds.includes(claim.claim_id);
|
||||
|
||||
let description = '';
|
||||
if (isResolvingUri && !claim) {
|
||||
description = __('Loading...');
|
||||
} else if (metadata && metadata.description) {
|
||||
description = metadata.description;
|
||||
} else if (claim === null) {
|
||||
description = __('This address contains no content.');
|
||||
}
|
||||
|
||||
// We should be able to tab through cards
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
return (
|
||||
<section
|
||||
className={`card card--small card--link ${obscureNsfw ? 'card--obscured ' : ''}`}
|
||||
onMouseEnter={this.handleMouseOver.bind(this)}
|
||||
onMouseLeave={this.handleMouseOut.bind(this)}
|
||||
tabIndex="0"
|
||||
role="button"
|
||||
onClick={!pending ? () => navigate('/show', { uri }) : () => {}}
|
||||
className={classnames('card card--small', {
|
||||
'card--link': !pending,
|
||||
'card--pending': pending,
|
||||
})}
|
||||
>
|
||||
<div className="card__inner">
|
||||
<Link onClick={() => navigate('/show', { uri })} className="card__link">
|
||||
<CardMedia title={title} thumbnail={thumbnail} />
|
||||
<div className="card__title-identity">
|
||||
<div className="card__title" title={title}>
|
||||
<TruncatedText lines={1}>{title}</TruncatedText>
|
||||
</div>
|
||||
<div className="card__subtitle">
|
||||
<span className="card__indicators card--file-subtitle">
|
||||
<FilePrice uri={uri} />{' '}
|
||||
{isRewardContent && <Icon icon={icons.FEATURED} leftPad />}{' '}
|
||||
{fileInfo && <Icon icon={icons.LOCAL} leftPad />}
|
||||
</span>
|
||||
<span className="card--file-subtitle">
|
||||
<UriIndicator uri={uri} link span smallCard />
|
||||
</span>
|
||||
</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>
|
||||
*/}
|
||||
<CardMedia nsfw={shouldObscureNsfw} thumbnail={thumbnail} />
|
||||
<div className="card-media__internal-links">{showPrice && <FilePrice uri={uri} />}</div>
|
||||
|
||||
<div className="card__title-identity">
|
||||
<div className="card__title--small">
|
||||
<TruncatedText lines={3}>{title}</TruncatedText>
|
||||
</div>
|
||||
<div className="card__subtitle card__subtitle--file-info">
|
||||
{pending ? (
|
||||
<div>Pending...</div>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<UriIndicator uri={uri} link />
|
||||
{isRewardContent && <Icon icon={icons.FEATURED} />}
|
||||
{fileInfo && <Icon icon={icons.LOCAL} />}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{obscureNsfw && this.state.hovered && <NsfwOverlay />}
|
||||
</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 lbry from 'lbry.js';
|
||||
import FileActions from 'component/fileActions';
|
||||
import Link from 'component/link';
|
||||
import DateTime from 'component/dateTime';
|
||||
import lbry from 'lbry';
|
||||
import Button from 'component/button';
|
||||
import path from 'path';
|
||||
|
||||
const path = require('path');
|
||||
type Props = {
|
||||
claim: {},
|
||||
fileInfo: {
|
||||
download_path: string,
|
||||
},
|
||||
metadata: {
|
||||
description: string,
|
||||
language: string,
|
||||
license: string,
|
||||
},
|
||||
openFolder: string => void,
|
||||
contentType: string,
|
||||
};
|
||||
|
||||
class FileDetails extends React.PureComponent {
|
||||
render() {
|
||||
const { claim, contentType, fileInfo, metadata, openFolder, uri } = this.props;
|
||||
|
||||
if (!claim || !metadata) {
|
||||
return (
|
||||
<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;
|
||||
const FileDetails = (props: Props) => {
|
||||
const { claim, contentType, fileInfo, metadata, openFolder } = props;
|
||||
|
||||
if (!claim || !metadata) {
|
||||
return (
|
||||
<div>
|
||||
<div className="divider__horizontal" />
|
||||
<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 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;
|
||||
|
||||
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;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { BusyMessage } from 'component/common';
|
||||
import Icon from 'component/icon';
|
||||
import Link from 'component/link';
|
||||
import Button from 'component/button';
|
||||
import classnames from 'classnames';
|
||||
import * as icons from 'constants/icons';
|
||||
|
||||
class FileDownloadLink extends React.PureComponent {
|
||||
componentWillMount() {
|
||||
|
@ -53,38 +53,34 @@ class FileDownloadLink extends React.PureComponent {
|
|||
|
||||
if (loading || downloading) {
|
||||
const progress =
|
||||
fileInfo && fileInfo.written_bytes
|
||||
? fileInfo.written_bytes / fileInfo.total_bytes * 100
|
||||
: 0,
|
||||
label = fileInfo ? progress.toFixed(0) + __('% complete') : __('Connecting...'),
|
||||
labelWithIcon = (
|
||||
<span className="button__content">
|
||||
<Icon icon="icon-download" />
|
||||
<span>{label}</span>
|
||||
</span>
|
||||
);
|
||||
fileInfo && fileInfo.written_bytes
|
||||
? fileInfo.written_bytes / fileInfo.total_bytes * 100
|
||||
: 0;
|
||||
const label = fileInfo ? progress.toFixed(0) + __('% complete') : __('Connecting...');
|
||||
|
||||
return (
|
||||
<div className="faux-button-block file-download button-set-item">
|
||||
<div className="file-download btn__content">
|
||||
<div
|
||||
className="faux-button-block file-download__overlay"
|
||||
className={classnames('file-download__overlay', {
|
||||
btn__content: !!progress,
|
||||
})}
|
||||
style={{ width: `${progress}%` }}
|
||||
>
|
||||
{labelWithIcon}
|
||||
{label}
|
||||
</div>
|
||||
{labelWithIcon}
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
} else if (fileInfo === null && !downloading) {
|
||||
if (!costInfo) {
|
||||
return <BusyMessage message={__('Fetching cost info')} />;
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
button="text"
|
||||
label={__('Download')}
|
||||
icon="icon-download"
|
||||
className="no-underline"
|
||||
<Button
|
||||
className="btn--file-actions"
|
||||
description={__('Download')}
|
||||
icon={icons.DOWNLOAD}
|
||||
onClick={() => {
|
||||
purchaseUri(uri);
|
||||
}}
|
||||
|
@ -92,11 +88,10 @@ class FileDownloadLink extends React.PureComponent {
|
|||
);
|
||||
} else if (fileInfo && fileInfo.download_path) {
|
||||
return (
|
||||
<Link
|
||||
label={__('Open')}
|
||||
button="text"
|
||||
icon="icon-external-link-square"
|
||||
className="no-underline"
|
||||
<Button
|
||||
className="btn--file-actions"
|
||||
description={__('Open')}
|
||||
icon={icons.OPEN}
|
||||
onClick={() => openFile()}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -1,21 +1,54 @@
|
|||
import React from 'react';
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { buildURI } from 'lbryURI';
|
||||
import FormField from 'component/formField';
|
||||
import FileTile from 'component/fileTile';
|
||||
import { BusyMessage } from 'component/common.js';
|
||||
import { FormField } from 'component/common/form';
|
||||
import FileCard from 'component/fileCard';
|
||||
|
||||
class FileList extends React.PureComponent {
|
||||
constructor(props) {
|
||||
type FileInfo = {
|
||||
name: string,
|
||||
channelName: ?string,
|
||||
pending?: boolean,
|
||||
value?: {
|
||||
publisherSignature: {
|
||||
certificateId: string,
|
||||
},
|
||||
},
|
||||
metadata: {
|
||||
publisherSignature: {
|
||||
certificateId: string,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
type Props = {
|
||||
hideFilter: boolean,
|
||||
fileInfos: Array<FileInfo>,
|
||||
};
|
||||
|
||||
type State = {
|
||||
sortBy: string,
|
||||
};
|
||||
|
||||
class FileList extends React.PureComponent<Props, State> {
|
||||
static defaultProps = {
|
||||
hideFilter: false,
|
||||
};
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
sortBy: 'dateNew',
|
||||
};
|
||||
|
||||
this._sortFunctions = {
|
||||
this.sortFunctions = {
|
||||
dateNew: fileInfos =>
|
||||
this.props.sortByHeight
|
||||
? fileInfos.slice().sort((fileInfo1, fileInfo2) => {
|
||||
if (fileInfo1.pending) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const height1 = this.props.claimsById[fileInfo1.claim_id]
|
||||
? this.props.claimsById[fileInfo1.claim_id].height
|
||||
: 0;
|
||||
|
@ -76,60 +109,62 @@ class FileList extends React.PureComponent {
|
|||
};
|
||||
}
|
||||
|
||||
getChannelSignature(fileInfo) {
|
||||
getChannelSignature = (fileInfo: FileInfo) => {
|
||||
if (fileInfo.pending) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (fileInfo.value) {
|
||||
return fileInfo.value.publisherSignature.certificateId;
|
||||
}
|
||||
return fileInfo.channel_claim_id;
|
||||
}
|
||||
};
|
||||
|
||||
handleSortChanged(event) {
|
||||
handleSortChanged(event: SyntheticInputEvent<*>) {
|
||||
this.setState({
|
||||
sortBy: event.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { handleSortChanged, fetching, fileInfos } = this.props;
|
||||
const { fileInfos, hideFilter } = this.props;
|
||||
const { sortBy } = this.state;
|
||||
const content = [];
|
||||
|
||||
this._sortFunctions[sortBy](fileInfos).forEach(fileInfo => {
|
||||
this.sortFunctions[sortBy](fileInfos).forEach(fileInfo => {
|
||||
const { channel_name: channelName, name: claimName, claim_id: claimId } = fileInfo;
|
||||
const uriParams = {};
|
||||
|
||||
if (fileInfo.channel_name) {
|
||||
uriParams.channelName = fileInfo.channel_name;
|
||||
uriParams.contentName = fileInfo.claim_name || fileInfo.name;
|
||||
if (channelName) {
|
||||
uriParams.channelName = channelName;
|
||||
uriParams.contentName = claimName;
|
||||
uriParams.claimId = this.getChannelSignature(fileInfo);
|
||||
} else {
|
||||
uriParams.claimId = fileInfo.claim_id;
|
||||
uriParams.claimName = fileInfo.claim_name || fileInfo.name;
|
||||
uriParams.claimId = claimId;
|
||||
uriParams.claimName = claimName;
|
||||
}
|
||||
|
||||
const uri = buildURI(uriParams);
|
||||
|
||||
content.push(
|
||||
<FileTile
|
||||
key={fileInfo.outpoint || fileInfo.claim_id}
|
||||
uri={uri}
|
||||
showPrice={false}
|
||||
showLocal={false}
|
||||
showActions
|
||||
showEmpty={this.props.fileTileShowEmpty}
|
||||
/>
|
||||
);
|
||||
content.push(<FileCard key={claimName} uri={uri} showPrice={false} />);
|
||||
});
|
||||
|
||||
return (
|
||||
<section className="file-list__header">
|
||||
{fetching && <BusyMessage />}
|
||||
<span className="sort-section">
|
||||
{__('Sort by')}{' '}
|
||||
<FormField type="select" onChange={this.handleSortChanged.bind(this)}>
|
||||
<option value="dateNew">{__('Newest First')}</option>
|
||||
<option value="dateOld">{__('Oldest First')}</option>
|
||||
<option value="title">{__('Title')}</option>
|
||||
</FormField>
|
||||
</span>
|
||||
{content}
|
||||
<section>
|
||||
<div className="file-list__sort">
|
||||
{!hideFilter && (
|
||||
<FormField
|
||||
prefix={__('Sort by')}
|
||||
type="select"
|
||||
value={sortBy}
|
||||
onChange={this.handleSortChanged}
|
||||
>
|
||||
<option value="date">{__('Date')}</option>
|
||||
<option value="title">{__('Title')}</option>
|
||||
</FormField>
|
||||
)}
|
||||
</div>
|
||||
<div className="card__list">{content}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { doSearch } from 'redux/actions/search';
|
||||
import { selectIsSearching, makeSelectSearchUris } from 'redux/selectors/search';
|
||||
import { makeSelectSearchUris, selectIsSearching } from 'redux/selectors/search';
|
||||
import { selectSearchDownloadUris } from 'redux/selectors/file_info';
|
||||
import FileListSearch from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
isSearching: selectIsSearching(state),
|
||||
uris: makeSelectSearchUris(props.query)(state),
|
||||
downloadUris: selectSearchDownloadUris(props.query)(state),
|
||||
isSearching: selectIsSearching(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
|
|
|
@ -1,58 +1,95 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import FileTile from 'component/fileTile';
|
||||
import ChannelTile from 'component/channelTile';
|
||||
import Link from 'component/link';
|
||||
import { BusyMessage } from 'component/common.js';
|
||||
import { parseURI } from 'lbryURI';
|
||||
import debounce from 'util/debounce';
|
||||
|
||||
const SearchNoResults = props => {
|
||||
const { query } = props;
|
||||
const SEARCH_DEBOUNCE_TIME = 800;
|
||||
|
||||
return (
|
||||
<section>
|
||||
<span className="empty">
|
||||
{(__('No one has checked anything in for %s yet.'), query)}{' '}
|
||||
<Link label={__('Be the first')} navigate="/publish" />
|
||||
</span>
|
||||
</section>
|
||||
);
|
||||
const NoResults = () => {
|
||||
return <div className="file-tile">{__('No results')}</div>;
|
||||
};
|
||||
|
||||
class FileListSearch extends React.PureComponent {
|
||||
componentWillMount() {
|
||||
this.doSearch(this.props);
|
||||
type Props = {
|
||||
search: string => void,
|
||||
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) {
|
||||
if (props.query != this.props.query) {
|
||||
this.doSearch(props);
|
||||
componentDidMount() {
|
||||
const { search, query } = this.props;
|
||||
search(query);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps: Props) {
|
||||
const { query: nextQuery } = nextProps;
|
||||
const { query: currentQuerry } = this.props;
|
||||
|
||||
if (nextQuery !== currentQuerry) {
|
||||
this.debouncedSearch(nextQuery);
|
||||
}
|
||||
}
|
||||
|
||||
doSearch(props) {
|
||||
this.props.search(props.query);
|
||||
}
|
||||
debouncedSearch: string => void;
|
||||
|
||||
render() {
|
||||
const { isSearching, uris, query } = this.props;
|
||||
const { uris, query, downloadUris, isSearching } = this.props;
|
||||
|
||||
const fileResults = [];
|
||||
const channelResults = [];
|
||||
if (uris && uris.length) {
|
||||
uris.forEach(uri => {
|
||||
const isChannel = parseURI(uri).claimName[0] === '@';
|
||||
if (isChannel) {
|
||||
channelResults.push(uri);
|
||||
} else {
|
||||
fileResults.push(uri);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isSearching && !uris && <BusyMessage message={__('Looking up the Dewey Decimals')} />}
|
||||
query && (
|
||||
<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
|
||||
? uris.map(
|
||||
uri =>
|
||||
parseURI(uri).claimName[0] === '@' ? (
|
||||
<ChannelTile key={uri} uri={uri} />
|
||||
) : (
|
||||
<FileTile key={uri} uri={uri} />
|
||||
)
|
||||
)
|
||||
: !isSearching && <SearchNoResults query={query} />}
|
||||
</div>
|
||||
<div className="search-result__column">
|
||||
<div className="file-list__header">{__('Your downloads')}</div>
|
||||
{downloadUris && downloadUris.length ? (
|
||||
downloadUris.map(uri => <FileTile test key={uri} uri={uri} />)
|
||||
) : (
|
||||
<NoResults />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,35 +1,48 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { CreditAmount } from 'component/common';
|
||||
import CreditAmount from 'component/common/credit-amount';
|
||||
|
||||
type Props = {
|
||||
showFullPrice: boolean,
|
||||
costInfo: ?{ includesData: boolean, cost: number },
|
||||
fetchCostInfo: string => void,
|
||||
uri: string,
|
||||
fetching: boolean,
|
||||
claim: ?{},
|
||||
};
|
||||
|
||||
class FilePrice extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
showFullPrice: false,
|
||||
};
|
||||
|
||||
class FilePrice extends React.PureComponent {
|
||||
componentWillMount() {
|
||||
this.fetchCost(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
componentWillReceiveProps(nextProps: Props) {
|
||||
this.fetchCost(nextProps);
|
||||
}
|
||||
|
||||
fetchCost(props) {
|
||||
fetchCost = (props: Props) => {
|
||||
const { costInfo, fetchCostInfo, uri, fetching, claim } = props;
|
||||
|
||||
if (costInfo === undefined && !fetching && claim) {
|
||||
fetchCostInfo(uri);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { costInfo, look = 'indicator', showFullPrice = false } = this.props;
|
||||
const { costInfo, showFullPrice } = this.props;
|
||||
|
||||
const isEstimate = costInfo ? !costInfo.includesData : null;
|
||||
const isEstimate = costInfo ? !costInfo.includesData : false;
|
||||
|
||||
if (!costInfo) {
|
||||
return <span className={`credit-amount credit-amount--${look}`}>???</span>;
|
||||
return <span className="credit-amount">PRICE</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<CreditAmount
|
||||
label={false}
|
||||
amount={costInfo.cost}
|
||||
isEstimate={isEstimate}
|
||||
showFree
|
||||
|
|
|
@ -10,8 +10,7 @@ import FileTile from './view';
|
|||
|
||||
const select = (state, props) => ({
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
|
||||
obscureNsfw: !selectShowNsfw(state),
|
||||
isDownloaded: !!makeSelectFileInfoForUri(props.uri)(state),
|
||||
metadata: makeSelectMetadataForUri(props.uri)(state),
|
||||
isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
|
||||
rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
|
||||
|
|
|
@ -1,132 +1,115 @@
|
|||
import React from 'react';
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import * as icons from 'constants/icons';
|
||||
import { normalizeURI, isURIClaimable, parseURI } from 'lbryURI';
|
||||
import CardMedia from 'component/cardMedia';
|
||||
import { TruncatedText } from 'component/common.js';
|
||||
import TruncatedText from 'component/common/truncated-text';
|
||||
import FilePrice from 'component/filePrice';
|
||||
import NsfwOverlay from 'component/nsfwOverlay';
|
||||
import Icon from 'component/icon';
|
||||
import Icon from 'component/common/icon';
|
||||
import Button from 'component/button';
|
||||
import classnames from 'classnames';
|
||||
|
||||
class FileTile extends React.PureComponent {
|
||||
static SHOW_EMPTY_PUBLISH = 'publish';
|
||||
static SHOW_EMPTY_PENDING = 'pending';
|
||||
type Props = {
|
||||
fullWidth: boolean, // removes the max-width css
|
||||
showUri: boolean,
|
||||
showLocal: boolean,
|
||||
isDownloaded: boolean,
|
||||
uri: string,
|
||||
isResolvingUri: boolean,
|
||||
rewardedContentClaimIds: Array<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 = {
|
||||
showPrice: true,
|
||||
showLocal: true,
|
||||
showUri: false,
|
||||
showLocal: false,
|
||||
fullWidth: false,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
showNsfwHelp: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { isResolvingUri, claim, uri, resolveUri } = this.props;
|
||||
|
||||
if (!isResolvingUri && !claim && uri) resolveUri(uri);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const { isResolvingUri, claim, uri, resolveUri } = this.props;
|
||||
|
||||
componentWillReceiveProps(nextProps: Props) {
|
||||
const { isResolvingUri, claim, uri, resolveUri } = nextProps;
|
||||
if (!isResolvingUri && claim === undefined && uri) resolveUri(uri);
|
||||
}
|
||||
|
||||
handleMouseOver() {
|
||||
if (this.props.obscureNsfw && this.props.metadata && this.props.metadata.nsfw) {
|
||||
this.setState({
|
||||
showNsfwHelp: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseOut() {
|
||||
if (this.state.showNsfwHelp) {
|
||||
this.setState({
|
||||
showNsfwHelp: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
claim,
|
||||
showActions,
|
||||
metadata,
|
||||
isResolvingUri,
|
||||
showEmpty,
|
||||
navigate,
|
||||
showPrice,
|
||||
showLocal,
|
||||
rewardedContentClaimIds,
|
||||
fileInfo,
|
||||
showUri,
|
||||
fullWidth,
|
||||
showLocal,
|
||||
isDownloaded,
|
||||
} = this.props;
|
||||
|
||||
const uri = normalizeURI(this.props.uri);
|
||||
const isClaimed = !!claim;
|
||||
const isClaimable = isURIClaimable(uri);
|
||||
const title =
|
||||
isClaimed && metadata && metadata.title ? metadata.title : parseURI(uri).contentName;
|
||||
const thumbnail = metadata && metadata.thumbnail ? metadata.thumbnail : null;
|
||||
const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw;
|
||||
const isRewardContent = claim && rewardedContentClaimIds.includes(claim.claim_id);
|
||||
|
||||
let onClick = () => navigate('/show', { uri });
|
||||
const onClick = () => navigate('/show', { uri });
|
||||
|
||||
let name = '';
|
||||
let name;
|
||||
let channel;
|
||||
if (claim) {
|
||||
name = claim.name;
|
||||
}
|
||||
|
||||
let description = '';
|
||||
if (isClaimed) {
|
||||
description = metadata && metadata.description;
|
||||
} else if (isResolvingUri) {
|
||||
description = __('Loading...');
|
||||
} else if (showEmpty === FileTile.SHOW_EMPTY_PUBLISH) {
|
||||
onClick = () => navigate('/publish', {});
|
||||
description = (
|
||||
<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>;
|
||||
channel = claim.channel_name;
|
||||
}
|
||||
|
||||
return (
|
||||
<section
|
||||
className={`file-tile card ${obscureNsfw ? 'card--obscured ' : ''}`}
|
||||
onMouseEnter={this.handleMouseOver.bind(this)}
|
||||
onMouseLeave={this.handleMouseOut.bind(this)}
|
||||
className={classnames('file-tile card--link', {
|
||||
'file-tile--fullwidth': fullWidth,
|
||||
})}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div onClick={onClick} className="card__link">
|
||||
<div className="card__inner file-tile__row">
|
||||
<CardMedia title={title || name} thumbnail={thumbnail} />
|
||||
<div className="file-tile__content">
|
||||
<div className="card__title-primary">
|
||||
<span className="card__indicators">
|
||||
{showPrice && <FilePrice uri={this.props.uri} />}{' '}
|
||||
{isRewardContent && <Icon icon={icons.FEATURED} />}{' '}
|
||||
{showLocal && fileInfo && <Icon icon={icons.LOCAL} />}
|
||||
</span>
|
||||
<h3>
|
||||
<TruncatedText lines={1}>{title || name}</TruncatedText>
|
||||
</h3>
|
||||
<CardMedia title={title || name} thumbnail={thumbnail} />
|
||||
<div className="file-tile__info">
|
||||
{isResolvingUri && <div className="card__title--small">{__('Loading...')}</div>}
|
||||
{!isResolvingUri && (
|
||||
<React.Fragment>
|
||||
<div className="card__title--small card__title--file">
|
||||
<TruncatedText lines={2}>{title || name}</TruncatedText>
|
||||
</div>
|
||||
{description && (
|
||||
<div className="card__content card__subtext">
|
||||
<TruncatedText lines={!showActions ? 3 : 2}>{description}</TruncatedText>
|
||||
</div>
|
||||
<div className="card__subtitle">
|
||||
{showUri ? uri : channel || __('Anonymous')}
|
||||
{isRewardContent && <Icon icon={icons.FEATURED} />}
|
||||
{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>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
{this.state.showNsfwHelp && <NsfwOverlay />}
|
||||
</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 PropTypes from 'prop-types';
|
||||
import FileSelector from 'component/file-selector.js';
|
||||
import FileSelector from 'component/common/file-selector';
|
||||
import SimpleMDE from 'react-simplemde-editor';
|
||||
import { formFieldNestedLabelTypes, formFieldId } from '../form';
|
||||
import { formFieldNestedLabelTypes, formFieldId } from 'component/common/form';
|
||||
import style from 'react-simplemde-editor/dist/simplemde.min.css';
|
||||
|
||||
const formFieldFileSelectorTypes = ['file', 'directory'];
|
||||
|
@ -195,3 +197,4 @@ class FormField extends React.PureComponent {
|
|||
}
|
||||
|
||||
export default FormField;
|
||||
/* eslint-enable */
|
||||
|
|
|
@ -1,63 +1,5 @@
|
|||
import React from 'react';
|
||||
import FormField from 'component/formField';
|
||||
// This just exists so the app builds. It will be removed
|
||||
|
||||
class FormFieldPrice extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
amount: props.defaultValue && props.defaultValue.amount ? props.defaultValue.amount : '',
|
||||
currency:
|
||||
props.defaultValue && props.defaultValue.currency ? props.defaultValue.currency : 'LBC',
|
||||
};
|
||||
}
|
||||
|
||||
handleChange(newValues) {
|
||||
const newState = Object.assign({}, this.state, newValues);
|
||||
this.setState(newState);
|
||||
this.props.onChange({
|
||||
amount: newState.amount,
|
||||
currency: newState.currency,
|
||||
});
|
||||
}
|
||||
|
||||
handleFeeAmountChange(event) {
|
||||
this.handleChange({
|
||||
amount: event.target.value ? Number(event.target.value) : null,
|
||||
});
|
||||
}
|
||||
|
||||
handleFeeCurrencyChange(event) {
|
||||
this.handleChange({ currency: event.target.value });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { defaultValue, placeholder, min } = this.props;
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
}
|
||||
const FormFieldPrice = () => null;
|
||||
|
||||
export default FormFieldPrice;
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
import React from 'react';
|
||||
import { formatCredits } from 'util/formatCredits';
|
||||
import { connect } from 'react-redux';
|
||||
import { selectIsBackDisabled, selectIsForwardDisabled } from 'redux/selectors/navigation';
|
||||
import { selectBalance } from 'redux/selectors/wallet';
|
||||
import { doNavigate, doHistoryBack, doHistoryForward } from 'redux/actions/navigation';
|
||||
import Header from './view';
|
||||
import { doNavigate } from 'redux/actions/navigation';
|
||||
import { selectIsUpgradeAvailable, selectAutoUpdateDownloaded } from 'redux/selectors/app';
|
||||
import { formatCredits } from 'util/formatCredits';
|
||||
import { selectBalance } from 'redux/selectors/wallet';
|
||||
import Header from './view';
|
||||
import { doDownloadUpgradeRequested } from 'redux/actions/app';
|
||||
|
||||
const select = state => ({
|
||||
isBackDisabled: selectIsBackDisabled(state),
|
||||
isForwardDisabled: selectIsForwardDisabled(state),
|
||||
isUpgradeAvailable: selectIsUpgradeAvailable(state),
|
||||
autoUpdateDownloaded: selectAutoUpdateDownloaded(state),
|
||||
balance: formatCredits(selectBalance(state) || 0, 2),
|
||||
|
|
|
@ -1,100 +1,68 @@
|
|||
import React from 'react';
|
||||
import Link from 'component/link';
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import Button from 'component/button';
|
||||
import WunderBar from 'component/wunderbar';
|
||||
import * as icons from 'constants/icons';
|
||||
|
||||
export const Header = props => {
|
||||
type Props = {
|
||||
balance: string,
|
||||
navigate: any => void,
|
||||
downloadUpgradeRequested: any => void,
|
||||
isUpgradeAvailable: boolean,
|
||||
autoUpdateDownloaded: boolean,
|
||||
};
|
||||
|
||||
const Header = (props: Props) => {
|
||||
const {
|
||||
balance,
|
||||
back,
|
||||
forward,
|
||||
isBackDisabled,
|
||||
isForwardDisabled,
|
||||
isUpgradeAvailable,
|
||||
autoUpdateDownloaded,
|
||||
navigate,
|
||||
downloadUpgradeRequested,
|
||||
autoUpdateDownloaded,
|
||||
} = props;
|
||||
|
||||
const showUpgradeButton =
|
||||
autoUpdateDownloaded || (process.platform === 'linux' && isUpgradeAvailable);
|
||||
|
||||
return (
|
||||
<header id="header">
|
||||
<div className="header__item">
|
||||
<Link
|
||||
onClick={back}
|
||||
disabled={isBackDisabled}
|
||||
button="alt button--flat"
|
||||
icon="icon-arrow-left"
|
||||
title={__('Back')}
|
||||
/>
|
||||
</div>
|
||||
<div className="header__item">
|
||||
<Link
|
||||
onClick={forward}
|
||||
disabled={isForwardDisabled}
|
||||
button="alt button--flat"
|
||||
icon="icon-arrow-right"
|
||||
title={__('Forward')}
|
||||
/>
|
||||
</div>
|
||||
<div className="header__item">
|
||||
<Link
|
||||
onClick={() => navigate('/discover')}
|
||||
button="alt button--flat"
|
||||
icon="icon-home"
|
||||
title={__('Discover Content')}
|
||||
/>
|
||||
</div>
|
||||
<div className="header__item">
|
||||
<Link
|
||||
onClick={() => navigate('/subscriptions')}
|
||||
button="alt button--flat"
|
||||
icon="icon-at"
|
||||
title={__('My Subscriptions')}
|
||||
/>
|
||||
</div>
|
||||
<div className="header__item header__item--wunderbar">
|
||||
<WunderBar />
|
||||
</div>
|
||||
<div className="header__item">
|
||||
<Link
|
||||
<header className="header">
|
||||
<WunderBar />
|
||||
<div className="header__actions-right">
|
||||
<Button
|
||||
button="inverse"
|
||||
className="btn--header-balance"
|
||||
onClick={() => navigate('/wallet')}
|
||||
button="text"
|
||||
className="no-underline"
|
||||
icon="icon-bank"
|
||||
label={balance}
|
||||
title={__('Wallet')}
|
||||
label={
|
||||
isUpgradeAvailable ? (
|
||||
`${balance}`
|
||||
) : (
|
||||
<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">
|
||||
<Link
|
||||
|
||||
<Button
|
||||
uppercase
|
||||
button="primary"
|
||||
onClick={() => navigate('/publish')}
|
||||
button="primary button--flat"
|
||||
icon="icon-upload"
|
||||
label={__('Publish')}
|
||||
icon={icons.UPLOAD}
|
||||
label={isUpgradeAvailable ? '' : __('Publish')}
|
||||
description={__('Publish content')}
|
||||
/>
|
||||
|
||||
{showUpgradeButton && (
|
||||
<Button
|
||||
button="primary"
|
||||
onClick={downloadUpgradeRequested}
|
||||
icon={icons.DOWNLOAD}
|
||||
label={__('Upgrade App')}
|
||||
/>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 Icon from 'component/icon';
|
||||
import Icon from 'component/common/icon';
|
||||
import RewardLink from 'component/rewardLink';
|
||||
import rewards from 'rewards.js';
|
||||
import * as icons from 'constants/icons';
|
||||
|
||||
class InviteList extends React.PureComponent {
|
||||
render() {
|
||||
|
@ -12,8 +13,8 @@ class InviteList extends React.PureComponent {
|
|||
}
|
||||
|
||||
return (
|
||||
<section className="card">
|
||||
<div className="card__title-primary">
|
||||
<section className="card card--section">
|
||||
<div className="card__title">
|
||||
<h3>{__('Invite History')}</h3>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
|
@ -21,7 +22,7 @@ class InviteList extends React.PureComponent {
|
|||
<span className="empty">{__("You haven't invited anyone.")} </span>
|
||||
)}
|
||||
{invitees.length > 0 && (
|
||||
<table className="table-standard table-stretch">
|
||||
<table className="table table--stretch">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{__('Invitee Email')}</th>
|
||||
|
@ -35,14 +36,14 @@ class InviteList extends React.PureComponent {
|
|||
<td>{invitee.email}</td>
|
||||
<td className="text-center">
|
||||
{invitee.invite_accepted ? (
|
||||
<Icon icon="icon-check" />
|
||||
<Icon icon={icons.CHECK} />
|
||||
) : (
|
||||
<span className="empty">{__('unused')}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="text-center">
|
||||
{invitee.invite_reward_claimed ? (
|
||||
<Icon icon="icon-check" />
|
||||
<Icon icon={icons.CHECK} />
|
||||
) : invitee.invite_reward_claimable ? (
|
||||
<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 { BusyMessage, CreditAmount } from 'component/common';
|
||||
import { Form, FormRow, Submit } from 'component/form.js';
|
||||
import BusyIndicator from 'component/common/busy-indicator';
|
||||
import CreditAmount from 'component/common/credit-amount';
|
||||
import { Form, FormRow, FormField, Submit } from 'component/common/form';
|
||||
|
||||
class FormInviteNew extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
email: '',
|
||||
};
|
||||
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
}
|
||||
|
||||
handleEmailChanged(event) {
|
||||
|
@ -23,23 +28,27 @@ class FormInviteNew extends React.PureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { errorMessage, isPending } = this.props;
|
||||
const { errorMessage, isPending, rewardAmount } = this.props;
|
||||
const label = `${__('Get')} ${rewardAmount} LBC`;
|
||||
|
||||
return (
|
||||
<Form onSubmit={this.handleSubmit.bind(this)}>
|
||||
<FormRow
|
||||
type="text"
|
||||
label="Email"
|
||||
placeholder="youremail@example.org"
|
||||
name="email"
|
||||
value={this.state.email}
|
||||
errorMessage={errorMessage}
|
||||
onChange={event => {
|
||||
this.handleEmailChanged(event);
|
||||
}}
|
||||
/>
|
||||
<div className="form-row-submit">
|
||||
<Submit label={__('Send Invite')} disabled={isPending} />
|
||||
<Form onSubmit={this.handleSubmit}>
|
||||
<FormRow stretch>
|
||||
<FormField
|
||||
stretch
|
||||
type="text"
|
||||
label="Email"
|
||||
placeholder="youremail@example.org"
|
||||
name="email"
|
||||
value={this.state.email}
|
||||
error={errorMessage}
|
||||
onChange={event => {
|
||||
this.handleEmailChanged(event);
|
||||
}}
|
||||
/>
|
||||
</FormRow>
|
||||
<div className="card__actions">
|
||||
<Submit label={label} disabled={isPending} />
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
|
@ -58,10 +67,10 @@ class InviteNew extends React.PureComponent {
|
|||
} = this.props;
|
||||
|
||||
return (
|
||||
<section className="card">
|
||||
<div className="card__title-primary">
|
||||
<CreditAmount amount={rewardAmount} />
|
||||
<h3>{__('Invite a Friend')}</h3>
|
||||
<section className="card card--section">
|
||||
<div className="card__title">{__('Invite a Friend')}</div>
|
||||
<div className="card__subtitle">
|
||||
{__("Or an enemy. Or your cousin Jerry, who you're kind of unsure about.")}
|
||||
</div>
|
||||
{/*
|
||||
<div className="card__content">
|
||||
|
@ -71,8 +80,12 @@ class InviteNew extends React.PureComponent {
|
|||
<p className="empty">{__("You have no invites.")}</p>}
|
||||
</div> */}
|
||||
<div className="card__content">
|
||||
<p>{__("Or an enemy. Or your cousin Jerry, who you're kind of unsure about.")}</p>
|
||||
<FormInviteNew errorMessage={errorMessage} inviteNew={inviteNew} isPending={isPending} />
|
||||
<FormInviteNew
|
||||
errorMessage={errorMessage}
|
||||
inviteNew={inviteNew}
|
||||
isPending={isPending}
|
||||
rewardAmount={rewardAmount}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
@ -80,3 +93,4 @@ class InviteNew extends React.PureComponent {
|
|||
}
|
||||
|
||||
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 Link from 'component/link';
|
||||
import Button from 'component/button';
|
||||
|
||||
const NsfwOverlay = props => (
|
||||
const NsfwOverlay = () => (
|
||||
<div className="card-overlay">
|
||||
<p>
|
||||
{__('This content is Not Safe For Work. To view adult content, please change your')}{' '}
|
||||
<Link
|
||||
className="button-text"
|
||||
onClick={() => props.navigateSettings()}
|
||||
label={__('Settings')}
|
||||
/>.
|
||||
<Button button="link" navigate="/settings" label={__('settings')} />.
|
||||
</p>
|
||||
</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 { connect } from 'react-redux';
|
||||
import PublishForm from './view';
|
||||
import { selectBalance } from 'redux/selectors/wallet';
|
||||
|
||||
const select = state => ({
|
||||
balance: selectBalance(state),
|
||||
});
|
||||
|
||||
export default connect(select, null)(PublishForm);
|
||||
export default connect(null, null)(PublishForm);
|
||||
|
|
|
@ -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 Modal from 'modal/modal';
|
||||
import Link from 'component/link';
|
||||
import Button from 'component/button';
|
||||
|
||||
const RewardLink = props => {
|
||||
const { reward, button, claimReward, clearError, errorMessage, label, isPending } = props;
|
||||
|
||||
return (
|
||||
return !reward ? null : (
|
||||
<div className="reward-link">
|
||||
<Link
|
||||
button={button}
|
||||
<Button
|
||||
button="primary"
|
||||
disabled={isPending}
|
||||
label={isPending ? __('Claiming...') : label || __('Claim Reward')}
|
||||
label={isPending ? __('Claiming...') : label || `${__('Get')} ${reward.reward_amount} LBC`}
|
||||
onClick={() => {
|
||||
claimReward(reward);
|
||||
}}
|
||||
|
|
|
@ -1,7 +1,20 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import LinkTransaction from 'component/linkTransaction';
|
||||
import ButtonTransaction from 'component/common/transaction-link';
|
||||
|
||||
const RewardListClaimed = props => {
|
||||
type Reward = {
|
||||
id: string,
|
||||
reward_title: string,
|
||||
reward_amount: number,
|
||||
transaction_id: string,
|
||||
created_at: string,
|
||||
};
|
||||
|
||||
type Props = {
|
||||
rewards: Array<Reward>,
|
||||
};
|
||||
|
||||
const RewardListClaimed = (props: Props) => {
|
||||
const { rewards } = props;
|
||||
|
||||
if (!rewards || !rewards.length) {
|
||||
|
@ -9,34 +22,31 @@ const RewardListClaimed = props => {
|
|||
}
|
||||
|
||||
return (
|
||||
<section className="card">
|
||||
<div className="card__title-identity">
|
||||
<h3>Claimed Rewards</h3>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<table className="table-standard table-stretch">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{__('Title')}</th>
|
||||
<th>{__('Amount')}</th>
|
||||
<th>{__('Transaction')}</th>
|
||||
<th>{__('Date')}</th>
|
||||
<section className="card card--section">
|
||||
<div className="card__title">Claimed Rewards</div>
|
||||
|
||||
<table className="card__content table table--stretch">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{__('Title')}</th>
|
||||
<th>{__('Amount')}</th>
|
||||
<th>{__('Transaction')}</th>
|
||||
<th>{__('Date')}</th>
|
||||
</tr>
|
||||
</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>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rewards.map(reward => (
|
||||
<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>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import Link from 'component/link';
|
||||
import { CreditAmount } from 'component/common';
|
||||
import * as React from 'react';
|
||||
import Button from 'component/button';
|
||||
import CreditAmount from 'component/common/credit-amount';
|
||||
|
||||
type Props = {
|
||||
unclaimedRewardAmount: number,
|
||||
|
@ -9,29 +9,38 @@ type Props = {
|
|||
|
||||
const RewardSummary = (props: Props) => {
|
||||
const { unclaimedRewardAmount } = props;
|
||||
const hasRewards = unclaimedRewardAmount > 0;
|
||||
|
||||
return (
|
||||
<section className="card">
|
||||
<div className="card__title-primary">
|
||||
<h3>{__('Rewards')}</h3>
|
||||
<p className="help">
|
||||
{__('Read our')} <Link href="https://lbry.io/faq/rewards">{__('FAQ')}</Link>{' '}
|
||||
{__('to learn more about LBRY Rewards')}.
|
||||
</p>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
{unclaimedRewardAmount > 0 ? (
|
||||
<p>
|
||||
{__('You have')} <CreditAmount amount={unclaimedRewardAmount} precision={8} />{' '}
|
||||
<section className="card card--section">
|
||||
<div className="card__title">{__('Rewards')}</div>
|
||||
<p className="card__subtitle">
|
||||
{hasRewards ? (
|
||||
<React.Fragment>
|
||||
{__('You have')}
|
||||
|
||||
<CreditAmount noStyle amount={unclaimedRewardAmount} precision={8} />
|
||||
|
||||
{__('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">
|
||||
<Link button="primary" navigate="/rewards" label={__('Claim Rewards')} />
|
||||
<Button
|
||||
button="primary"
|
||||
navigate="/rewards"
|
||||
label={hasRewards ? __('Claim Rewards') : __('View Rewards')}
|
||||
/>
|
||||
</div>
|
||||
<p className="help help--padded">
|
||||
{__('Read our')}{' '}
|
||||
<Button button="link" label={__('FAQ')} href="https://lbry.io/faq/rewards" />{' '}
|
||||
{__('to learn more about LBRY Rewards')}.
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,35 +1,43 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { CreditAmount, Icon } from 'component/common';
|
||||
import Icon from 'component/common/icon';
|
||||
import RewardLink from 'component/rewardLink';
|
||||
import Link from 'component/link';
|
||||
import Button from 'component/button';
|
||||
import rewards from 'rewards';
|
||||
import * as icons from 'constants/icons';
|
||||
|
||||
const RewardTile = props => {
|
||||
type Props = {
|
||||
reward: {
|
||||
id: string,
|
||||
reward_title: string,
|
||||
reward_amount: number,
|
||||
transaction_id: string,
|
||||
created_at: string,
|
||||
reward_description: string,
|
||||
reward_type: string,
|
||||
},
|
||||
};
|
||||
|
||||
const RewardTile = (props: Props) => {
|
||||
const { reward } = props;
|
||||
|
||||
const claimed = !!reward.transaction_id;
|
||||
|
||||
return (
|
||||
<section className="card">
|
||||
<div className="card__inner">
|
||||
<div className="card__title-primary">
|
||||
<CreditAmount amount={reward.reward_amount} />
|
||||
<h3>{reward.reward_title}</h3>
|
||||
</div>
|
||||
<div className="card__content">{reward.reward_description}</div>
|
||||
<div className="card__actions ">
|
||||
{reward.reward_type == rewards.TYPE_REFERRAL && (
|
||||
<Link button="alt" navigate="/invite" label={__('Go To Invites')} />
|
||||
)}
|
||||
{reward.reward_type !== rewards.TYPE_REFERRAL &&
|
||||
(claimed ? (
|
||||
<span>
|
||||
<Icon icon="icon-check" /> {__('Reward claimed.')}
|
||||
</span>
|
||||
) : (
|
||||
<RewardLink button="alt" reward_type={reward.reward_type} />
|
||||
))}
|
||||
</div>
|
||||
<section className="card card--section">
|
||||
<div className="card__title">{reward.reward_title}</div>
|
||||
<div className="card__subtitle">{reward.reward_description}</div>
|
||||
<div className="card__actions">
|
||||
{reward.reward_type === rewards.TYPE_REFERRAL && (
|
||||
<Button button="primary" navigate="/invite" label={__('Go To Invites')} />
|
||||
)}
|
||||
{reward.reward_type !== rewards.TYPE_REFERRAL &&
|
||||
(claimed ? (
|
||||
<span>
|
||||
<Icon icon={icons.CHECK} /> {__('Reward claimed.')}
|
||||
</span>
|
||||
) : (
|
||||
<RewardLink reward_type={reward.reward_type} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
|
|
@ -13,7 +13,6 @@ import FileListDownloaded from 'page/fileListDownloaded';
|
|||
import FileListPublished from 'page/fileListPublished';
|
||||
import TransactionHistoryPage from 'page/transactionHistory';
|
||||
import ChannelPage from 'page/channel';
|
||||
import SearchPage from 'page/search';
|
||||
import AuthPage from 'page/auth';
|
||||
import InvitePage from 'page/invite';
|
||||
import BackupPage from 'page/backup';
|
||||
|
@ -22,7 +21,7 @@ import SubscriptionsPage from 'page/subscriptions';
|
|||
const route = (page, routesMap) => {
|
||||
const component = routesMap[page];
|
||||
|
||||
return component;
|
||||
return component || DiscoverPage;
|
||||
};
|
||||
|
||||
const Router = props => {
|
||||
|
@ -42,7 +41,6 @@ const Router = props => {
|
|||
getcredits: <GetCreditsPage params={params} />,
|
||||
report: <ReportPage params={params} />,
|
||||
rewards: <RewardsPage params={params} />,
|
||||
search: <SearchPage params={params} />,
|
||||
send: <SendReceivePage params={params} />,
|
||||
settings: <SettingsPage params={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
|
||||
import * as React from 'react';
|
||||
import QRCode from 'qrcode.react';
|
||||
import QRCode from 'component/common/qr-code';
|
||||
import { FormRow } from 'component/common/form';
|
||||
import * as statuses from 'constants/shape_shift';
|
||||
import Address from 'component/address';
|
||||
import Link from 'component/link';
|
||||
import Button from 'component/button';
|
||||
import type { Dispatch } from 'redux/actions/shape_shift';
|
||||
import ShiftMarketInfo from './market_info';
|
||||
|
||||
|
@ -92,12 +93,12 @@ class ActiveShapeShift extends React.PureComponent<Props> {
|
|||
originCoinDepositMax={originCoinDepositMax}
|
||||
/>
|
||||
|
||||
<div className="shapeshift__deposit-address-wrapper">
|
||||
<Address address={shiftDepositAddress} showCopyButton />
|
||||
<div className="shapeshift__qrcode">
|
||||
{shiftDepositAddress && (
|
||||
<FormRow verticallyCentered padded>
|
||||
<Address address={shiftDepositAddress} showCopyButton />
|
||||
<QRCode value={shiftDepositAddress} />
|
||||
</div>
|
||||
</div>
|
||||
</FormRow>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
@ -115,9 +116,9 @@ class ActiveShapeShift extends React.PureComponent<Props> {
|
|||
<p>{__('Transaction complete! You should see the new LBC in your wallet.')}</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="card__actions card__actions--only-vertical">
|
||||
<Link
|
||||
button={shiftState === statuses.COMPLETE ? 'primary' : 'alt'}
|
||||
<div className="card__actions">
|
||||
<Button
|
||||
button="primary"
|
||||
onClick={clearShapeShift}
|
||||
label={
|
||||
shiftState === statuses.COMPLETE || shiftState === statuses.RECEIVED
|
||||
|
@ -126,13 +127,11 @@ class ActiveShapeShift extends React.PureComponent<Props> {
|
|||
}
|
||||
/>
|
||||
{shiftOrderId && (
|
||||
<span className="shapeshift__link">
|
||||
<Link
|
||||
button="text"
|
||||
label={__('View the status on Shapeshift.io')}
|
||||
href={`https://shapeshift.io/#/status/${shiftOrderId}`}
|
||||
/>
|
||||
</span>
|
||||
<Button
|
||||
button="inverse"
|
||||
label={__('View the status on Shapeshift.io')}
|
||||
href={`https://shapeshift.io/#/status/${shiftOrderId}`}
|
||||
/>
|
||||
)}
|
||||
{shiftState === statuses.NO_DEPOSITS &&
|
||||
shiftReturnAddress && (
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import Link from 'component/link';
|
||||
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 ShiftMarketInfo from './market_info';
|
||||
|
||||
|
@ -12,7 +12,7 @@ type ShapeShiftFormErrors = {
|
|||
type Props = {
|
||||
values: ShapeShiftFormValues,
|
||||
errors: ShapeShiftFormErrors,
|
||||
touched: boolean,
|
||||
touched: { returnAddress: boolean },
|
||||
handleChange: Event => any,
|
||||
handleBlur: Event => any,
|
||||
handleSubmit: Event => any,
|
||||
|
@ -21,7 +21,6 @@ type Props = {
|
|||
originCoin: string,
|
||||
updating: boolean,
|
||||
getCoinStats: Dispatch,
|
||||
receiveAddress: string,
|
||||
originCoinDepositFee: number,
|
||||
originCoinDepositMin: string,
|
||||
originCoinDepositMax: number,
|
||||
|
@ -41,7 +40,6 @@ export default (props: Props) => {
|
|||
originCoin,
|
||||
updating,
|
||||
getCoinStats,
|
||||
receiveAddress,
|
||||
originCoinDepositMax,
|
||||
originCoinDepositMin,
|
||||
originCoinDepositFee,
|
||||
|
@ -49,56 +47,53 @@ export default (props: Props) => {
|
|||
} = props;
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="form-field">
|
||||
<span>{__('Exchange')} </span>
|
||||
<select
|
||||
className="form-field__input form-field__input-select"
|
||||
name="originCoin"
|
||||
onChange={e => {
|
||||
getCoinStats(e.target.value);
|
||||
handleChange(e);
|
||||
}}
|
||||
>
|
||||
{shiftSupportedCoins.map(coin => (
|
||||
<option key={coin} value={coin}>
|
||||
{coin}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<span> {__('for LBC')}</span>
|
||||
<div className="shapeshift__tx-info">
|
||||
{!updating &&
|
||||
originCoinDepositMax && (
|
||||
<ShiftMarketInfo
|
||||
originCoin={originCoin}
|
||||
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}
|
||||
<FormField
|
||||
prefix={__('Exchange')}
|
||||
postfix={__('for LBC')}
|
||||
type="select"
|
||||
name="origin_coin"
|
||||
onChange={e => {
|
||||
getCoinStats(e.target.value);
|
||||
handleChange(e);
|
||||
}}
|
||||
>
|
||||
{shiftSupportedCoins.map(coin => (
|
||||
<option key={coin} value={coin}>
|
||||
{coin}
|
||||
</option>
|
||||
))}
|
||||
</FormField>
|
||||
<ShiftMarketInfo
|
||||
originCoin={originCoin}
|
||||
shapeShiftRate={shapeShiftRate}
|
||||
originCoinDepositFee={originCoinDepositFee}
|
||||
originCoinDepositMin={originCoinDepositMin}
|
||||
originCoinDepositMax={originCoinDepositMax}
|
||||
/>
|
||||
|
||||
<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>
|
||||
({__('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.")}
|
||||
</span>
|
||||
</span>
|
||||
<div className="card__actions card__actions--only-vertical">
|
||||
<div className="card__actions">
|
||||
<Submit
|
||||
button="primary"
|
||||
label={__('Begin Conversion')}
|
||||
disabled={isSubmitting || !!Object.keys(errors).length}
|
||||
/>
|
||||
|
|
|
@ -1,18 +1,12 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import { shell } from 'electron';
|
||||
import { Formik } from 'formik';
|
||||
import classnames from 'classnames';
|
||||
import * as statuses from 'constants/shape_shift';
|
||||
import { validateShapeShiftForm } from 'util/shape_shift';
|
||||
import Link from 'component/link';
|
||||
import Spinner from 'component/common/spinner';
|
||||
import { BusyMessage } from 'component/common';
|
||||
import ShapeShiftForm from './internal/form';
|
||||
import ActiveShapeShift from './internal/active-shift';
|
||||
|
||||
import Button from 'component/button';
|
||||
import type { ShapeShiftState } from 'redux/reducers/shape_shift';
|
||||
import type { Dispatch, ShapeShiftFormValues } from 'redux/actions/shape_shift';
|
||||
import ShapeShiftForm from './internal/form';
|
||||
import ActiveShapeShift from './internal/active-shift';
|
||||
|
||||
type Props = {
|
||||
shapeShift: ShapeShiftState,
|
||||
|
@ -72,59 +66,44 @@ class ShapeShift extends React.PureComponent<Props> {
|
|||
};
|
||||
|
||||
return (
|
||||
// add the "shapeshift__intital-wrapper class so we can avoid content jumping once everything loads"
|
||||
// it just gives the section a min-height equal to the height of the content when the form is rendered
|
||||
// if the markup below changes for the initial render (form.jsx) there will be content jumping
|
||||
// the styling in shapeshift.scss will need to be updated to the correct min-height
|
||||
<section
|
||||
className={classnames('card shapeshift__wrapper', {
|
||||
'shapeshift__initial-wrapper': loading,
|
||||
})}
|
||||
>
|
||||
<div className="card__title-primary">
|
||||
<h3>{__('Convert Crypto to LBC')}</h3>
|
||||
<p className="help">
|
||||
{__('Powered by ShapeShift. Read our FAQ')}{' '}
|
||||
<Link href="https://lbry.io/faq/shapeshift">{__('here')}</Link>.
|
||||
{hasActiveShift &&
|
||||
shiftState !== 'complete' && <span>{__('This will update automatically.')}</span>}
|
||||
</p>
|
||||
</div>
|
||||
<section className="card card--section">
|
||||
<div className="card__title">{__('Convert Crypto to LBC')}</div>
|
||||
<p className="card__subtitle">
|
||||
{__('Powered by ShapeShift. Read our FAQ')}{' '}
|
||||
<Button button="link" label={__('here')} href="https://lbry.io/faq/shapeshift" />.
|
||||
{hasActiveShift &&
|
||||
shiftState !== 'complete' && <span>{__('This will update automatically.')}</span>}
|
||||
</p>
|
||||
|
||||
<div className="card__content shapeshift__content">
|
||||
<div className="card__content">
|
||||
{error && <div className="form-field__error">{error}</div>}
|
||||
{loading && <Spinner dark />}
|
||||
{!loading &&
|
||||
!hasActiveShift &&
|
||||
!!shiftSupportedCoins.length && (
|
||||
<Formik
|
||||
onSubmit={createShapeShift}
|
||||
validate={validateShapeShiftForm}
|
||||
initialValues={initialFormValues}
|
||||
render={formProps => (
|
||||
<ShapeShiftForm
|
||||
{...formProps}
|
||||
updating={updating}
|
||||
originCoin={originCoin}
|
||||
shiftSupportedCoins={shiftSupportedCoins}
|
||||
getCoinStats={getCoinStats}
|
||||
receiveAddress={receiveAddress}
|
||||
originCoinDepositMax={originCoinDepositMax}
|
||||
originCoinDepositMin={originCoinDepositMin}
|
||||
originCoinDepositFee={originCoinDepositFee}
|
||||
shapeShiftRate={shapeShiftRate}
|
||||
updating={updating}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{!hasActiveShift && (
|
||||
<Formik
|
||||
onSubmit={createShapeShift}
|
||||
validate={validateShapeShiftForm}
|
||||
initialValues={initialFormValues}
|
||||
render={formProps => (
|
||||
<ShapeShiftForm
|
||||
{...formProps}
|
||||
updating={updating}
|
||||
originCoin={originCoin}
|
||||
shiftSupportedCoins={shiftSupportedCoins}
|
||||
getCoinStats={getCoinStats}
|
||||
receiveAddress={receiveAddress}
|
||||
originCoinDepositMax={originCoinDepositMax}
|
||||
originCoinDepositMin={originCoinDepositMin}
|
||||
originCoinDepositFee={originCoinDepositFee}
|
||||
shapeShiftRate={shapeShiftRate}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{hasActiveShift && (
|
||||
<ActiveShapeShift
|
||||
getActiveShift={getActiveShift}
|
||||
shiftCoinType={shiftCoinType}
|
||||
shiftReturnAddress={shiftReturnAddress}
|
||||
shiftDepositAddress={shiftDepositAddress}
|
||||
originCoinDepositMax={originCoinDepositMax}
|
||||
shiftOrderId={shiftOrderId}
|
||||
shiftState={shiftState}
|
||||
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 Link from 'component/link';
|
||||
import Button from 'component/button';
|
||||
|
||||
class SnackBar extends React.PureComponent {
|
||||
constructor(props) {
|
||||
|
@ -32,7 +32,7 @@ class SnackBar extends React.PureComponent {
|
|||
{message}
|
||||
{linkText &&
|
||||
linkTarget && (
|
||||
<Link navigate={linkTarget} className="snack-bar__action" label={linkText} />
|
||||
<Button navigate={linkTarget} className="snack-bar__action" label={linkText} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { selectCurrentModal, selectDaemonVersionMatched } from 'redux/selectors/app';
|
||||
import { doCheckDaemonVersion } from 'redux/actions/app';
|
||||
import SplashScreen from './view';
|
||||
|
|
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 PropTypes from 'prop-types';
|
||||
import lbry from 'lbry.js';
|
||||
import LoadScreen from '../load_screen.js';
|
||||
import * as React from 'react';
|
||||
import lbry from 'lbry';
|
||||
import LoadScreen from './internal/load-screen';
|
||||
import ModalIncompatibleDaemon from 'modal/modalIncompatibleDaemon';
|
||||
import ModalUpgrade from 'modal/modalUpgrade';
|
||||
import ModalDownloading from 'modal/modalDownloading';
|
||||
import * as modals from 'constants/modal_types';
|
||||
|
||||
export class SplashScreen extends React.PureComponent {
|
||||
static propTypes = {
|
||||
message: PropTypes.string,
|
||||
onLoadDone: PropTypes.func,
|
||||
};
|
||||
type Props = {
|
||||
checkDaemonVersion: () => Promise<any>,
|
||||
modal: string,
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
this.state = {
|
||||
|
@ -75,9 +81,11 @@ export class SplashScreen extends React.PureComponent {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { checkDaemonVersion } = this.props;
|
||||
|
||||
lbry
|
||||
.connect()
|
||||
.then(this.props.checkDaemonVersion)
|
||||
.then(checkDaemonVersion)
|
||||
.then(() => {
|
||||
this.updateStatus();
|
||||
})
|
||||
|
@ -97,15 +105,19 @@ export class SplashScreen extends React.PureComponent {
|
|||
const { message, details, isLagging, isRunning } = this.state;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<React.Fragment>
|
||||
<LoadScreen message={message} details={details} isWarning={isLagging} />
|
||||
{/* Temp hack: don't show any modals on splash screen daemon is running;
|
||||
daemon doesn't let you quit during startup, so the "Quit" buttons
|
||||
in the modals won't work. */}
|
||||
{modal == 'incompatibleDaemon' && isRunning && <ModalIncompatibleDaemon />}
|
||||
{modal == modals.UPGRADE && isRunning && <ModalUpgrade />}
|
||||
{modal == modals.DOWNLOADING && isRunning && <ModalDownloading />}
|
||||
</div>
|
||||
{isRunning && (
|
||||
<React.Fragment>
|
||||
{modal === modals.INCOMPATIBLE_DAEMON && <ModalIncompatibleDaemon />}
|
||||
{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 { doOpenModal } from 'redux/actions/app';
|
||||
import { selectSubscriptions } from 'redux/selectors/subscriptions';
|
||||
|
||||
import SubscribeButton from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
|
|
|
@ -1,38 +1,54 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import Link from 'component/link';
|
||||
import * as modals from 'constants/modal_types';
|
||||
import * as icons from 'constants/icons';
|
||||
import Button from 'component/button';
|
||||
import type { Subscription } from 'redux/reducers/subscriptions';
|
||||
|
||||
type SubscribtionArgs = {
|
||||
channelName: string,
|
||||
uri: string,
|
||||
};
|
||||
|
||||
type Props = {
|
||||
channelName: ?string,
|
||||
uri: ?string,
|
||||
subscriptions: Array<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 =
|
||||
subscriptions.map(subscription => subscription.channelName).indexOf(channelName) !== -1;
|
||||
|
||||
const subscriptionHandler = isSubscribed ? doChannelUnsubscribe : doChannelSubscribe;
|
||||
|
||||
const subscriptionLabel = isSubscribed ? __('Unsubscribe') : __('Subscribe');
|
||||
|
||||
return channelName && uri ? (
|
||||
<div className="card__actions">
|
||||
<Link
|
||||
iconRight={isSubscribed ? '' : 'at'}
|
||||
button={isSubscribed ? 'alt' : 'primary'}
|
||||
label={subscriptionLabel}
|
||||
onClick={() => {
|
||||
if (!subscriptions.length) {
|
||||
doOpenModal(modals.FIRST_SUBSCRIPTION);
|
||||
}
|
||||
subscriptionHandler({
|
||||
channelName,
|
||||
uri,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
icon={isSubscribed ? undefined : icons.HEART}
|
||||
button={isSubscribed ? 'danger' : 'alt'}
|
||||
label={subscriptionLabel}
|
||||
onClick={() => {
|
||||
if (!subscriptions.length) {
|
||||
doOpenModal(modals.FIRST_SUBSCRIPTION);
|
||||
}
|
||||
subscriptionHandler({
|
||||
channelName,
|
||||
uri,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
) : 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';
|
||||
import TransactionListItem from './internal/TransactionListItem';
|
||||
import FormField from 'component/formField';
|
||||
import Link from 'component/link';
|
||||
import FileExporter from 'component/file-exporter.js';
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { FormField } from 'component/common/form';
|
||||
import Button from 'component/button';
|
||||
import FileExporter from 'component/common/file-exporter';
|
||||
import * as icons from 'constants/icons';
|
||||
import * as modals from 'constants/modal_types';
|
||||
import TransactionListItem from './internal/transaction-list-item';
|
||||
|
||||
class TransactionList extends React.PureComponent {
|
||||
constructor(props) {
|
||||
export type Transaction = {
|
||||
amount: number,
|
||||
claim_id: string,
|
||||
claim_name: string,
|
||||
fee: number,
|
||||
nout: number,
|
||||
txid: string,
|
||||
type: string,
|
||||
date: Date,
|
||||
};
|
||||
|
||||
type Props = {
|
||||
emptyMessage: ?string,
|
||||
slim?: boolean,
|
||||
transactions: Array<Transaction>,
|
||||
rewards: {},
|
||||
openModal: (string, any) => void,
|
||||
myClaims: any,
|
||||
};
|
||||
|
||||
type State = {
|
||||
filter: string,
|
||||
};
|
||||
|
||||
class TransactionList extends React.PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
filter: null,
|
||||
filter: 'all',
|
||||
};
|
||||
|
||||
(this: any).handleFilterChanged = this.handleFilterChanged.bind(this);
|
||||
(this: any).filterTransaction = this.filterTransaction.bind(this);
|
||||
(this: any).revokeClaim = this.revokeClaim.bind(this);
|
||||
(this: any).isRevokeable = this.isRevokeable.bind(this);
|
||||
}
|
||||
|
||||
handleFilterChanged(event) {
|
||||
handleFilterChanged(event: SyntheticInputEvent<*>) {
|
||||
this.setState({
|
||||
filter: event.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
filterTransaction(transaction) {
|
||||
filterTransaction(transaction: Transaction) {
|
||||
const { filter } = this.state;
|
||||
|
||||
return !filter || filter == transaction.type;
|
||||
return filter === 'all' || filter === transaction.type;
|
||||
}
|
||||
|
||||
isRevokeable(txid, nout) {
|
||||
isRevokeable(txid: string, nout: number) {
|
||||
const { myClaims } = this.props;
|
||||
// a claim/support/update is revokable if it
|
||||
// is in my claim list(claim_list_mine)
|
||||
return this.props.myClaims.has(`${txid}:${nout}`);
|
||||
return myClaims.has(`${txid}:${nout}`);
|
||||
}
|
||||
|
||||
revokeClaim(txid, nout) {
|
||||
revokeClaim(txid: string, nout: number) {
|
||||
this.props.openModal(modals.CONFIRM_CLAIM_REVOKE, { txid, nout });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { emptyMessage, rewards, transactions } = this.props;
|
||||
|
||||
const transactionList = transactions.filter(this.filterTransaction.bind(this));
|
||||
const { emptyMessage, rewards, transactions, slim } = this.props;
|
||||
const { filter } = this.state;
|
||||
const transactionList = transactions.filter(this.filterTransaction);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{Boolean(transactionList.length) && (
|
||||
<FileExporter
|
||||
data={transactionList}
|
||||
label={__('Export')}
|
||||
title={__('Export Transactions')}
|
||||
filters={['nout']}
|
||||
defaultPath={__('lbry-transactions-history')}
|
||||
/>
|
||||
<React.Fragment>
|
||||
{!transactionList.length && (
|
||||
<p className="card__content">{emptyMessage || __('No transactions to list.')}</p>
|
||||
)}
|
||||
{(transactionList.length || this.state.filter) && (
|
||||
<span className="sort-section">
|
||||
{__('Filter')}{' '}
|
||||
<FormField type="select" onChange={this.handleFilterChanged.bind(this)}>
|
||||
<option value="">{__('All')}</option>
|
||||
{!slim &&
|
||||
!!transactionList.length && (
|
||||
<div className="card__actions">
|
||||
<FileExporter
|
||||
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="receive">{__('Receives')}</option>
|
||||
<option value="publish">{__('Publishes')}</option>
|
||||
|
@ -65,22 +113,18 @@ class TransactionList extends React.PureComponent {
|
|||
<option value="tip">{__('Tips')}</option>
|
||||
<option value="support">{__('Supports')}</option>
|
||||
<option value="update">{__('Updates')}</option>
|
||||
</FormField>{' '}
|
||||
<Link href="https://lbry.io/faq/transaction-types" icon={icons.HELP_CIRCLE} />
|
||||
</span>
|
||||
</FormField>
|
||||
</div>
|
||||
)}
|
||||
{!transactionList.length && (
|
||||
<div className="empty">{emptyMessage || __('No transactions to list.')}</div>
|
||||
)}
|
||||
{Boolean(transactionList.length) && (
|
||||
<table className="table-standard table-transactions table-stretch">
|
||||
{!!transactionList.length && (
|
||||
<table className="card__content table table--transactions table--stretch">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{__('Date')}</th>
|
||||
<th>{__('Amount (Fee)')}</th>
|
||||
<th>{__('Amount')}</th>
|
||||
<th>{__('Type')} </th>
|
||||
<th>{__('Details')} </th>
|
||||
<th>{__('Transaction')}</th>
|
||||
<th>{__('Date')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -90,13 +134,13 @@ class TransactionList extends React.PureComponent {
|
|||
transaction={t}
|
||||
reward={rewards && rewards[t.txid]}
|
||||
isRevokeable={this.isRevokeable(t.txid, t.nout)}
|
||||
revokeClaim={this.revokeClaim.bind(this)}
|
||||
revokeClaim={this.revokeClaim}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,20 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { BusyMessage } from 'component/common';
|
||||
import Link from 'component/link';
|
||||
import BusyIndicator from 'component/common/busy-indicator';
|
||||
import Button from 'component/button';
|
||||
import TransactionList from 'component/transactionList';
|
||||
import * as icons from 'constants/icons';
|
||||
import type { Transaction } from 'component/transactionList/view';
|
||||
|
||||
class TransactionListRecent extends React.PureComponent {
|
||||
componentWillMount() {
|
||||
type Props = {
|
||||
fetchTransactions: () => void,
|
||||
fetchingTransactions: boolean,
|
||||
hasTransactions: boolean,
|
||||
transactions: Array<Transaction>,
|
||||
};
|
||||
|
||||
class TransactionListRecent extends React.PureComponent<Props> {
|
||||
componentDidMount() {
|
||||
this.props.fetchTransactions();
|
||||
}
|
||||
|
||||
|
@ -13,27 +22,27 @@ class TransactionListRecent extends React.PureComponent {
|
|||
const { fetchingTransactions, hasTransactions, transactions } = this.props;
|
||||
|
||||
return (
|
||||
<section className="card">
|
||||
<div className="card__title-primary">
|
||||
<h3>{__('Recent Transactions')}</h3>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
{fetchingTransactions && <BusyMessage message={__('Loading transactions')} />}
|
||||
{!fetchingTransactions && (
|
||||
<TransactionList
|
||||
transactions={transactions}
|
||||
emptyMessage={__('You have no recent transactions.')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<section className="card card--section">
|
||||
<div className="card__title">{__('Recent Transactions')}</div>
|
||||
{fetchingTransactions && (
|
||||
<div className="card__content">
|
||||
<BusyIndicator message={__('Loading transactions')} />
|
||||
</div>
|
||||
)}
|
||||
{!fetchingTransactions && (
|
||||
<TransactionList
|
||||
slim
|
||||
transactions={transactions}
|
||||
emptyMessage={__("Looks like you don't have any recent transactions.")}
|
||||
/>
|
||||
)}
|
||||
{hasTransactions && (
|
||||
<div className="card__actions card__actions--bottom">
|
||||
<Link
|
||||
<div className="card__actions">
|
||||
<Button
|
||||
button="primary"
|
||||
navigate="/history"
|
||||
label={__('Full History')}
|
||||
icon={icons.HISTORY}
|
||||
className="no-underline"
|
||||
button="text"
|
||||
icon={icons.CLOCK}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -1,35 +1,46 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import Icon from 'component/icon';
|
||||
import Link from 'component/link';
|
||||
import Button from 'component/button';
|
||||
import { buildURI } from 'lbryURI';
|
||||
import classnames from 'classnames';
|
||||
// import Icon from 'component/common/icon';
|
||||
|
||||
class UriIndicator extends React.PureComponent {
|
||||
type Props = {
|
||||
isResolvingUri: boolean,
|
||||
resolveUri: string => void,
|
||||
claim: {
|
||||
channel_name: string,
|
||||
has_signature: boolean,
|
||||
signature_is_valid: boolean,
|
||||
value: {
|
||||
publisherSignature: { certificateId: string },
|
||||
},
|
||||
},
|
||||
uri: string,
|
||||
link: ?boolean,
|
||||
};
|
||||
|
||||
class UriIndicator extends React.PureComponent<Props> {
|
||||
componentWillMount() {
|
||||
this.resolve(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
componentWillReceiveProps(nextProps: Props) {
|
||||
this.resolve(nextProps);
|
||||
}
|
||||
|
||||
resolve(props) {
|
||||
resolve = (props: Props) => {
|
||||
const { isResolvingUri, resolveUri, claim, uri } = props;
|
||||
|
||||
if (!isResolvingUri && claim === undefined && uri) {
|
||||
resolveUri(uri);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { claim, link, uri, isResolvingUri, smallCard, span } = this.props;
|
||||
|
||||
if (isResolvingUri && !claim) {
|
||||
return <span className="empty">Validating...</span>;
|
||||
}
|
||||
|
||||
const { claim, link, isResolvingUri } = this.props;
|
||||
if (!claim) {
|
||||
return <span className="empty">Unused</span>;
|
||||
return <span className="empty">{isResolvingUri ? 'Validating...' : 'Unused'}</span>;
|
||||
}
|
||||
|
||||
const {
|
||||
|
@ -38,41 +49,28 @@ class UriIndicator extends React.PureComponent {
|
|||
signature_is_valid: signatureIsValid,
|
||||
value,
|
||||
} = claim;
|
||||
|
||||
const channelClaimId =
|
||||
value && value.publisherSignature && value.publisherSignature.certificateId;
|
||||
|
||||
if (!hasSignature || !channelName) {
|
||||
return <span className="empty">Anonymous</span>;
|
||||
return <span className="channel-name">Anonymous</span>;
|
||||
}
|
||||
|
||||
let icon, channelLink, modifier;
|
||||
|
||||
let channelLink;
|
||||
if (signatureIsValid) {
|
||||
modifier = 'valid';
|
||||
channelLink = link ? buildURI({ channelName, claimId: channelClaimId }, false) : false;
|
||||
} else {
|
||||
icon = 'icon-times-circle';
|
||||
modifier = 'invalid';
|
||||
}
|
||||
|
||||
const inner = (
|
||||
<span>
|
||||
<span
|
||||
className={classnames('channel-name', {
|
||||
'channel-name--small': smallCard,
|
||||
'button-text no-underline': link,
|
||||
})}
|
||||
>
|
||||
{channelName}
|
||||
</span>{' '}
|
||||
{!signatureIsValid ? (
|
||||
<Icon
|
||||
icon={icon}
|
||||
className={`channel-indicator__icon channel-indicator__icon--${modifier}`}
|
||||
/>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
|
||||
|
@ -81,14 +79,14 @@ class UriIndicator extends React.PureComponent {
|
|||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
<Button
|
||||
noPadding
|
||||
className="btn--uri-indicator"
|
||||
navigate="/show"
|
||||
navigateParams={{ uri: channelLink }}
|
||||
className="no-underline"
|
||||
span={span}
|
||||
>
|
||||
{inner}
|
||||
</Link>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
// I'll come back to this
|
||||
/* eslint-disable */
|
||||
import React from 'react';
|
||||
import { Form, FormRow, Submit } from 'component/form.js';
|
||||
import { Form, FormRow, Submit } from 'component/common/form';
|
||||
|
||||
class UserEmailNew extends React.PureComponent {
|
||||
constructor(props) {
|
||||
|
@ -53,3 +55,4 @@ class UserEmailNew extends React.PureComponent {
|
|||
}
|
||||
|
||||
export default UserEmailNew;
|
||||
/* eslint-enable */
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue