Discovery fixes (#2576)
* point change email button to faq arcticle until we add that functionality * hide claims if they are blocked * fix: sorting on 'library' page * add @reach/tooltip * cleanup and add tags to publish page * fix: button color * fix: PublishPrice props * fix: claim list sorting * update lbry-redux * respond to PR comments
This commit is contained in:
parent
d7b4e91544
commit
2a21b6936b
77 changed files with 1575 additions and 5715 deletions
|
@ -39,6 +39,7 @@
|
||||||
"space-before-function-paren": ["error", "never"],
|
"space-before-function-paren": ["error", "never"],
|
||||||
"standard/object-curly-even-spacing": 0,
|
"standard/object-curly-even-spacing": 0,
|
||||||
"standard/no-callback-literal": 0,
|
"standard/no-callback-literal": 0,
|
||||||
|
"react/display-name": 0,
|
||||||
"semi": [
|
"semi": [
|
||||||
"error",
|
"error",
|
||||||
"always",
|
"always",
|
||||||
|
|
3847
flow-typed/npm/react-feather_vx.x.x.js
vendored
3847
flow-typed/npm/react-feather_vx.x.x.js
vendored
File diff suppressed because it is too large
Load diff
1
flow-typed/publish.js
vendored
1
flow-typed/publish.js
vendored
|
@ -48,4 +48,5 @@ declare type PublishParams = {
|
||||||
},
|
},
|
||||||
claim: StreamClaim,
|
claim: StreamClaim,
|
||||||
nsfw: boolean,
|
nsfw: boolean,
|
||||||
|
tags: Array<Tag>,
|
||||||
};
|
};
|
||||||
|
|
3
flow-typed/react-feather.js
vendored
3
flow-typed/react-feather.js
vendored
|
@ -1,3 +0,0 @@
|
||||||
declare module 'react-feather' {
|
|
||||||
declare module.exports: any;
|
|
||||||
}
|
|
|
@ -123,7 +123,7 @@
|
||||||
"jsmediatags": "^3.8.1",
|
"jsmediatags": "^3.8.1",
|
||||||
"json-loader": "^0.5.4",
|
"json-loader": "^0.5.4",
|
||||||
"lbry-format": "https://github.com/lbryio/lbry-format.git",
|
"lbry-format": "https://github.com/lbryio/lbry-format.git",
|
||||||
"lbry-redux": "lbryio/lbry-redux#141593500693a93db74c62ef5a9fe67b43896603",
|
"lbry-redux": "lbryio/lbry-redux#b3bf3f6d53410ff1c5415b51ca425341e364959f",
|
||||||
"lbryinc": "lbryio/lbryinc#43d382d9b74d396a581a74d87e4c53105e04f845",
|
"lbryinc": "lbryio/lbryinc#43d382d9b74d396a581a74d87e4c53105e04f845",
|
||||||
"lint-staged": "^7.0.2",
|
"lint-staged": "^7.0.2",
|
||||||
"localforage": "^1.7.1",
|
"localforage": "^1.7.1",
|
||||||
|
@ -147,7 +147,6 @@
|
||||||
"rc-progress": "^2.0.6",
|
"rc-progress": "^2.0.6",
|
||||||
"react": "^16.8.2",
|
"react": "^16.8.2",
|
||||||
"react-dom": "^16.8.2",
|
"react-dom": "^16.8.2",
|
||||||
"react-feather": "^1.0.8",
|
|
||||||
"react-ga": "^2.5.7",
|
"react-ga": "^2.5.7",
|
||||||
"react-hot-loader": "^4.11.1",
|
"react-hot-loader": "^4.11.1",
|
||||||
"react-modal": "^3.1.7",
|
"react-modal": "^3.1.7",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as React from 'react';
|
import type { Node } from 'react';
|
||||||
|
import React, { forwardRef } from 'react';
|
||||||
import Icon from 'component/common/icon';
|
import Icon from 'component/common/icon';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
|
@ -14,14 +15,14 @@ type Props = {
|
||||||
icon: ?string,
|
icon: ?string,
|
||||||
iconRight: ?string,
|
iconRight: ?string,
|
||||||
disabled: ?boolean,
|
disabled: ?boolean,
|
||||||
children: ?React.Node,
|
children: ?Node,
|
||||||
navigate: ?string,
|
navigate: ?string,
|
||||||
className: ?string,
|
className: ?string,
|
||||||
description: ?string,
|
description: ?string,
|
||||||
type: string,
|
type: string,
|
||||||
button: ?string, // primary, secondary, alt, link
|
button: ?string, // primary, secondary, alt, link
|
||||||
iconColor?: string,
|
|
||||||
iconSize?: number,
|
iconSize?: number,
|
||||||
|
iconColor?: string,
|
||||||
constrict: ?boolean, // to shorten the button and ellipsis, only use for links
|
constrict: ?boolean, // to shorten the button and ellipsis, only use for links
|
||||||
activeClass?: string,
|
activeClass?: string,
|
||||||
innerRef: ?any,
|
innerRef: ?any,
|
||||||
|
@ -31,120 +32,109 @@ type Props = {
|
||||||
onMouseLeave: ?(any) => any,
|
onMouseLeave: ?(any) => any,
|
||||||
};
|
};
|
||||||
|
|
||||||
class Button extends React.PureComponent<Props> {
|
// use forwardRef to allow consumers to pass refs to the button content if they want to
|
||||||
static defaultProps = {
|
// flow requires forwardRef have default type arguments passed to it
|
||||||
type: 'button',
|
const Button = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
};
|
const {
|
||||||
|
type = 'button',
|
||||||
|
onClick,
|
||||||
|
href,
|
||||||
|
title,
|
||||||
|
label,
|
||||||
|
icon,
|
||||||
|
// This should rarely be used. Regular buttons should just use `icon`
|
||||||
|
// `iconRight` is used for the header (home) button with the LBRY icon and external links that are displayed inline
|
||||||
|
iconRight,
|
||||||
|
disabled,
|
||||||
|
children,
|
||||||
|
navigate,
|
||||||
|
className,
|
||||||
|
description,
|
||||||
|
button,
|
||||||
|
iconSize,
|
||||||
|
iconColor,
|
||||||
|
constrict,
|
||||||
|
activeClass,
|
||||||
|
...otherProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
render() {
|
const combinedClassName = classnames(
|
||||||
const {
|
'button',
|
||||||
id,
|
button
|
||||||
onClick,
|
? {
|
||||||
onMouseEnter,
|
'button--primary': button === 'primary',
|
||||||
onMouseLeave,
|
'button--secondary': button === 'secondary',
|
||||||
innerRef,
|
'button--alt': button === 'alt',
|
||||||
href,
|
'button--inverse': button === 'inverse',
|
||||||
title,
|
'button--close': button === 'close',
|
||||||
label,
|
'button--disabled': disabled,
|
||||||
icon,
|
'button--link': button === 'link',
|
||||||
// This should rarely be used. Regular buttons should just use `icon`
|
'button--constrict': constrict,
|
||||||
// `iconRight` is used for the header (home) button with the LBRY icon and external links that are displayed inline
|
}
|
||||||
iconRight,
|
: 'button--no-style',
|
||||||
disabled,
|
className
|
||||||
children,
|
);
|
||||||
navigate,
|
|
||||||
className,
|
|
||||||
description,
|
|
||||||
button,
|
|
||||||
type,
|
|
||||||
iconColor,
|
|
||||||
iconSize,
|
|
||||||
constrict,
|
|
||||||
activeClass,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const combinedClassName = classnames(
|
const content = (
|
||||||
'button',
|
<span className="button__content">
|
||||||
button
|
{icon && <Icon icon={icon} iconColor={iconColor} size={iconSize} />}
|
||||||
? {
|
{label && <span className="button__label">{label}</span>}
|
||||||
'button--primary': button === 'primary',
|
{children && children}
|
||||||
'button--secondary': button === 'secondary',
|
{iconRight && <Icon icon={iconRight} iconColor={iconColor} size={iconSize} />}
|
||||||
'button--alt': button === 'alt',
|
</span>
|
||||||
'button--inverse': button === 'inverse',
|
);
|
||||||
'button--close': button === 'close',
|
|
||||||
'button--disabled': disabled,
|
|
||||||
'button--link': button === 'link',
|
|
||||||
'button--constrict': constrict,
|
|
||||||
}
|
|
||||||
: 'button--no-style',
|
|
||||||
className
|
|
||||||
);
|
|
||||||
|
|
||||||
const content = (
|
if (href) {
|
||||||
<span className="button__content">
|
return (
|
||||||
{icon && <Icon icon={icon} iconColor={iconColor} size={iconSize} />}
|
<OutboundLink eventLabel="outboundClick" to={href} target="_blank" className={combinedClassName}>
|
||||||
{label && <span className="button__label">{label}</span>}
|
|
||||||
{children && children}
|
|
||||||
{iconRight && <Icon icon={iconRight} iconColor={iconColor} size={iconSize} />}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (href) {
|
|
||||||
return (
|
|
||||||
<OutboundLink eventLabel="outboundClick" to={href} target="_blank" className={combinedClassName}>
|
|
||||||
{content}
|
|
||||||
</OutboundLink>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle lbry:// uris passed in, or already formatted web urls
|
|
||||||
let path = navigate;
|
|
||||||
if (path) {
|
|
||||||
if (path.startsWith('lbry://')) {
|
|
||||||
path = formatLbryUriForWeb(path);
|
|
||||||
} else if (!path.startsWith('/')) {
|
|
||||||
// Force a leading slash so new paths aren't appended on to the current path
|
|
||||||
path = `/${path}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return path ? (
|
|
||||||
<NavLink
|
|
||||||
id={id}
|
|
||||||
exact
|
|
||||||
to={path}
|
|
||||||
title={title}
|
|
||||||
disabled={disabled}
|
|
||||||
onClick={e => {
|
|
||||||
e.stopPropagation();
|
|
||||||
if (onClick) {
|
|
||||||
onClick();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onMouseEnter={onMouseEnter}
|
|
||||||
onMouseLeave={onMouseLeave}
|
|
||||||
className={combinedClassName}
|
|
||||||
activeClassName={activeClass}
|
|
||||||
innerRef={innerRef}
|
|
||||||
>
|
|
||||||
{content}
|
{content}
|
||||||
</NavLink>
|
</OutboundLink>
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
id={id}
|
|
||||||
title={title}
|
|
||||||
aria-label={description || label || title}
|
|
||||||
className={combinedClassName}
|
|
||||||
onClick={onClick}
|
|
||||||
disabled={disabled}
|
|
||||||
type={type}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
{content}
|
|
||||||
</button>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Handle lbry:// uris passed in, or already formatted web urls
|
||||||
|
let path = navigate;
|
||||||
|
if (path) {
|
||||||
|
if (path.startsWith('lbry://')) {
|
||||||
|
path = formatLbryUriForWeb(path);
|
||||||
|
} else if (!path.startsWith('/')) {
|
||||||
|
// Force a leading slash so new paths aren't appended on to the current path
|
||||||
|
path = `/${path}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return path ? (
|
||||||
|
<NavLink
|
||||||
|
ref={ref}
|
||||||
|
exact
|
||||||
|
to={path}
|
||||||
|
title={title}
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (onClick) {
|
||||||
|
onClick();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={combinedClassName}
|
||||||
|
activeClassName={activeClass}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</NavLink>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
ref={ref}
|
||||||
|
title={title}
|
||||||
|
aria-label={description || label || title}
|
||||||
|
className={combinedClassName}
|
||||||
|
onClick={onClick}
|
||||||
|
disabled={disabled}
|
||||||
|
type={type}
|
||||||
|
{...otherProps}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export default Button;
|
export default Button;
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import type { Node } from 'react';
|
import type { Node } from 'react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import ClaimListItem from 'component/claimListItem';
|
import ClaimPreview from 'component/claimPreview';
|
||||||
import Spinner from 'component/spinner';
|
import Spinner from 'component/spinner';
|
||||||
import { FormField } from 'component/common/form';
|
import { FormField } from 'component/common/form';
|
||||||
import usePersistedState from 'util/use-persisted-state';
|
import usePersistedState from 'util/use-persisted-state';
|
||||||
|
@ -26,8 +26,8 @@ type Props = {
|
||||||
export default function ClaimList(props: Props) {
|
export default function ClaimList(props: Props) {
|
||||||
const { uris, headerAltControls, injectedItem, loading, persistedStorageKey, empty, meta, type, header } = props;
|
const { uris, headerAltControls, injectedItem, loading, persistedStorageKey, empty, meta, type, header } = props;
|
||||||
const [currentSort, setCurrentSort] = usePersistedState(persistedStorageKey, SORT_NEW);
|
const [currentSort, setCurrentSort] = usePersistedState(persistedStorageKey, SORT_NEW);
|
||||||
const sortedUris = uris && currentSort === SORT_OLD ? uris.reverse() : uris;
|
|
||||||
const hasUris = uris && !!uris.length;
|
const hasUris = uris && !!uris.length;
|
||||||
|
const sortedUris = (hasUris && (currentSort === SORT_NEW ? uris : uris.slice().reverse())) || [];
|
||||||
|
|
||||||
function handleSortChange() {
|
function handleSortChange() {
|
||||||
setCurrentSort(currentSort === SORT_NEW ? SORT_OLD : SORT_NEW);
|
setCurrentSort(currentSort === SORT_NEW ? SORT_OLD : SORT_NEW);
|
||||||
|
@ -36,10 +36,10 @@ export default function ClaimList(props: Props) {
|
||||||
return (
|
return (
|
||||||
<section className={classnames('file-list')}>
|
<section className={classnames('file-list')}>
|
||||||
{header !== false && (
|
{header !== false && (
|
||||||
<div className={classnames('file-list__header', { 'file-list__header--small': type === 'small' })}>
|
<div className={classnames('claim-list__header', { 'claim-list__header--small': type === 'small' })}>
|
||||||
{header || (
|
{header || (
|
||||||
<FormField
|
<FormField
|
||||||
className="file-list__dropdown"
|
className="claim-list__dropdown"
|
||||||
type="select"
|
type="select"
|
||||||
name="file_sort"
|
name="file_sort"
|
||||||
value={currentSort}
|
value={currentSort}
|
||||||
|
@ -50,16 +50,16 @@ export default function ClaimList(props: Props) {
|
||||||
</FormField>
|
</FormField>
|
||||||
)}
|
)}
|
||||||
{loading && <Spinner light type="small" />}
|
{loading && <Spinner light type="small" />}
|
||||||
<div className="file-list__alt-controls">{headerAltControls}</div>
|
<div className="claim-list__alt-controls">{headerAltControls}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{meta && <div className="file-list__meta">{meta}</div>}
|
{meta && <div className="claim-list__meta">{meta}</div>}
|
||||||
{hasUris && (
|
{hasUris && (
|
||||||
<ul>
|
<ul>
|
||||||
{sortedUris.map((uri, index) => (
|
{sortedUris.map((uri, index) => (
|
||||||
<React.Fragment key={uri}>
|
<React.Fragment key={uri}>
|
||||||
<ClaimListItem uri={uri} type={type} />
|
<ClaimPreview uri={uri} type={type} />
|
||||||
{index === 4 && injectedItem && <li className="file-list__item--injected">{injectedItem}</li>}
|
{index === 4 && injectedItem && <li className="claim-list__item--injected">{injectedItem}</li>}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -70,7 +70,7 @@ function ClaimListDiscover(props: Props) {
|
||||||
const header = (
|
const header = (
|
||||||
<h1 className="card__title--flex">
|
<h1 className="card__title--flex">
|
||||||
<FormField
|
<FormField
|
||||||
className="file-list__dropdown"
|
className="claim-list__dropdown"
|
||||||
type="select"
|
type="select"
|
||||||
name="trending_sort"
|
name="trending_sort"
|
||||||
value={typeSort}
|
value={typeSort}
|
||||||
|
@ -89,7 +89,7 @@ function ClaimListDiscover(props: Props) {
|
||||||
<FormField
|
<FormField
|
||||||
type="select"
|
type="select"
|
||||||
name="trending_overview"
|
name="trending_overview"
|
||||||
className="file-list__dropdown"
|
className="claim-list__dropdown"
|
||||||
value={personalSort}
|
value={personalSort}
|
||||||
onChange={e => setPersonalSort(e.target.value)}
|
onChange={e => setPersonalSort(e.target.value)}
|
||||||
>
|
>
|
||||||
|
@ -107,7 +107,7 @@ function ClaimListDiscover(props: Props) {
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{typeSort === 'top' && (
|
{typeSort === 'top' && (
|
||||||
<FormField
|
<FormField
|
||||||
className="file-list__dropdown"
|
className="claim-list__dropdown"
|
||||||
type="select"
|
type="select"
|
||||||
name="trending_time"
|
name="trending_time"
|
||||||
value={timeSort}
|
value={timeSort}
|
||||||
|
|
|
@ -9,8 +9,9 @@ import {
|
||||||
makeSelectTitleForUri,
|
makeSelectTitleForUri,
|
||||||
makeSelectClaimIsNsfw,
|
makeSelectClaimIsNsfw,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
|
import { selectBlackListedOutpoints } from 'lbryinc';
|
||||||
import { selectShowNsfw } from 'redux/selectors/settings';
|
import { selectShowNsfw } from 'redux/selectors/settings';
|
||||||
import ClaimListItem from './view';
|
import ClaimPreview from './view';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => ({
|
||||||
pending: makeSelectClaimIsPending(props.uri)(state),
|
pending: makeSelectClaimIsPending(props.uri)(state),
|
||||||
|
@ -21,6 +22,7 @@ const select = (state, props) => ({
|
||||||
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
|
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
|
||||||
title: makeSelectTitleForUri(props.uri)(state),
|
title: makeSelectTitleForUri(props.uri)(state),
|
||||||
nsfw: makeSelectClaimIsNsfw(props.uri)(state),
|
nsfw: makeSelectClaimIsNsfw(props.uri)(state),
|
||||||
|
blackListedOutpoints: selectBlackListedOutpoints(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = dispatch => ({
|
||||||
|
@ -30,4 +32,4 @@ const perform = dispatch => ({
|
||||||
export default connect(
|
export default connect(
|
||||||
select,
|
select,
|
||||||
perform
|
perform
|
||||||
)(ClaimListItem);
|
)(ClaimPreview);
|
||||||
|
|
35
src/ui/component/claimPreview/index.js
Normal file
35
src/ui/component/claimPreview/index.js
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import {
|
||||||
|
doResolveUri,
|
||||||
|
makeSelectClaimForUri,
|
||||||
|
makeSelectIsUriResolving,
|
||||||
|
makeSelectClaimIsMine,
|
||||||
|
makeSelectClaimIsPending,
|
||||||
|
makeSelectThumbnailForUri,
|
||||||
|
makeSelectTitleForUri,
|
||||||
|
makeSelectClaimIsNsfw,
|
||||||
|
} from 'lbry-redux';
|
||||||
|
import { selectBlackListedOutpoints } from 'lbryinc';
|
||||||
|
import { selectShowNsfw } from 'redux/selectors/settings';
|
||||||
|
import ClaimPreview from './view';
|
||||||
|
|
||||||
|
const select = (state, props) => ({
|
||||||
|
pending: makeSelectClaimIsPending(props.uri)(state),
|
||||||
|
claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
|
obscureNsfw: !selectShowNsfw(state),
|
||||||
|
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||||
|
isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
|
||||||
|
thumbnail: makeSelectThumbnailForUri(props.uri)(state),
|
||||||
|
title: makeSelectTitleForUri(props.uri)(state),
|
||||||
|
nsfw: makeSelectClaimIsNsfw(props.uri)(state),
|
||||||
|
blackListedOutpoints: selectBlackListedOutpoints(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
const perform = dispatch => ({
|
||||||
|
resolveUri: uri => dispatch(doResolveUri(uri)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
select,
|
||||||
|
perform
|
||||||
|
)(ClaimPreview);
|
|
@ -29,9 +29,13 @@ type Props = {
|
||||||
nsfw: boolean,
|
nsfw: boolean,
|
||||||
placeholder: boolean,
|
placeholder: boolean,
|
||||||
type: string,
|
type: string,
|
||||||
|
blackListedOutpoints: Array<{
|
||||||
|
txid: string,
|
||||||
|
nout: number,
|
||||||
|
}>,
|
||||||
};
|
};
|
||||||
|
|
||||||
function ClaimListItem(props: Props) {
|
function ClaimPreview(props: Props) {
|
||||||
const {
|
const {
|
||||||
obscureNsfw,
|
obscureNsfw,
|
||||||
claimIsMine,
|
claimIsMine,
|
||||||
|
@ -46,13 +50,23 @@ function ClaimListItem(props: Props) {
|
||||||
claim,
|
claim,
|
||||||
placeholder,
|
placeholder,
|
||||||
type,
|
type,
|
||||||
|
blackListedOutpoints,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const haventFetched = claim === undefined;
|
const haventFetched = claim === undefined;
|
||||||
const abandoned = !isResolvingUri && !claim;
|
const abandoned = !isResolvingUri && !claim;
|
||||||
const shouldHide = abandoned || (!claimIsMine && obscureNsfw && nsfw);
|
|
||||||
const isChannel = claim && claim.value_type === 'channel';
|
const isChannel = claim && claim.value_type === 'channel';
|
||||||
const claimsInChannel = (claim && claim.meta.claims_in_channel) || 0;
|
const claimsInChannel = (claim && claim.meta.claims_in_channel) || 0;
|
||||||
|
let shouldHide = abandoned || (!claimIsMine && obscureNsfw && nsfw);
|
||||||
|
|
||||||
|
// This will be replaced once blocking is done at the wallet server level
|
||||||
|
if (claim && !shouldHide) {
|
||||||
|
for (let i = 0; i < blackListedOutpoints.length; i += 1) {
|
||||||
|
const outpoint = blackListedOutpoints[i];
|
||||||
|
if (outpoint.txid === claim.txid && outpoint.nout === claim.nout) {
|
||||||
|
shouldHide = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleContextMenu(e) {
|
function handleContextMenu(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
@ -80,10 +94,10 @@ function ClaimListItem(props: Props) {
|
||||||
|
|
||||||
if (placeholder && !claim) {
|
if (placeholder && !claim) {
|
||||||
return (
|
return (
|
||||||
<li className="file-list__item" disabled>
|
<li className="claim-list__item" disabled>
|
||||||
<div className="placeholder media__thumb" />
|
<div className="placeholder media__thumb" />
|
||||||
<div className="placeholder__wrapper">
|
<div className="placeholder__wrapper">
|
||||||
<div className="placeholder file-list__item-title" />
|
<div className="placeholder claim-list__item-title" />
|
||||||
<div className="placeholder media__subtitle" />
|
<div className="placeholder media__subtitle" />
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
@ -93,16 +107,17 @@ function ClaimListItem(props: Props) {
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
role="link"
|
role="link"
|
||||||
onClick={onClick}
|
onClick={pending ? undefined : onClick}
|
||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
className={classnames('file-list__item', {
|
className={classnames('claim-list__item', {
|
||||||
'file-list__item--large': type === 'large',
|
'claim-list__item--large': type === 'large',
|
||||||
|
'claim-list__pending': pending,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{isChannel ? <ChannelThumbnail uri={uri} /> : <CardMedia thumbnail={thumbnail} />}
|
{isChannel ? <ChannelThumbnail uri={uri} /> : <CardMedia thumbnail={thumbnail} />}
|
||||||
<div className="file-list__item-metadata">
|
<div className="claim-list__item-metadata">
|
||||||
<div className="file-list__item-info">
|
<div className="claim-list__item-info">
|
||||||
<div className="file-list__item-title">
|
<div className="claim-list__item-title">
|
||||||
<TruncatedText text={title || (claim && claim.name)} lines={1} />
|
<TruncatedText text={title || (claim && claim.name)} lines={1} />
|
||||||
</div>
|
</div>
|
||||||
{type !== 'small' && (
|
{type !== 'small' && (
|
||||||
|
@ -113,7 +128,7 @@ function ClaimListItem(props: Props) {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="file-list__item-properties">
|
<div className="claim-list__item-properties">
|
||||||
<div className="media__subtitle">
|
<div className="media__subtitle">
|
||||||
<UriIndicator uri={uri} link />
|
<UriIndicator uri={uri} link />
|
||||||
{pending && <div>Pending...</div>}
|
{pending && <div>Pending...</div>}
|
||||||
|
@ -127,4 +142,4 @@ function ClaimListItem(props: Props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withRouter(ClaimListItem);
|
export default withRouter(ClaimPreview);
|
|
@ -1,13 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
// @if TARGET='app'
|
|
||||||
// $FlowFixMe
|
|
||||||
import { remote } from 'electron';
|
import { remote } from 'electron';
|
||||||
// @endif
|
|
||||||
// @if TARGET='web'
|
|
||||||
// $FlowFixMe
|
|
||||||
import { remote } from 'web/stubs';
|
|
||||||
// @endif
|
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import { FormField } from 'component/common/form';
|
import { FormField } from 'component/common/form';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
@ -21,6 +14,8 @@ type Props = {
|
||||||
type: string,
|
type: string,
|
||||||
currentPath: ?string,
|
currentPath: ?string,
|
||||||
onFileChosen: (string, string) => void,
|
onFileChosen: (string, string) => void,
|
||||||
|
label?: string,
|
||||||
|
placeholder?: string,
|
||||||
fileLabel?: string,
|
fileLabel?: string,
|
||||||
directoryLabel?: string,
|
directoryLabel?: string,
|
||||||
filters?: FileFilters[],
|
filters?: FileFilters[],
|
||||||
|
@ -83,13 +78,14 @@ class FileSelector extends React.PureComponent<Props> {
|
||||||
input: ?HTMLInputElement;
|
input: ?HTMLInputElement;
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { type, currentPath, fileLabel, directoryLabel } = this.props;
|
const { type, currentPath, label, fileLabel, directoryLabel, placeholder } = this.props;
|
||||||
|
|
||||||
const label = type === 'file' ? fileLabel || __('Choose File') : directoryLabel || __('Choose Directory');
|
const buttonLabel = type === 'file' ? fileLabel || __('Choose File') : directoryLabel || __('Choose Directory');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<FormField
|
<FormField
|
||||||
|
label={label}
|
||||||
webkitdirectory="true"
|
webkitdirectory="true"
|
||||||
className="form-field--copyable"
|
className="form-field--copyable"
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -98,8 +94,8 @@ class FileSelector extends React.PureComponent<Props> {
|
||||||
if (this.fileInput) this.fileInput.current.select();
|
if (this.fileInput) this.fileInput.current.select();
|
||||||
}}
|
}}
|
||||||
readOnly="readonly"
|
readOnly="readonly"
|
||||||
value={currentPath || __('No File Chosen')}
|
value={currentPath || placeholder || __('Choose a file')}
|
||||||
inputButton={<Button button="primary" label={label} onClick={() => this.handleButtonClick()} />}
|
inputButton={<Button button="primary" label={buttonLabel} onClick={() => this.handleButtonClick()} />}
|
||||||
/>
|
/>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
// A housing for all of our icons. Mostly taken from https://github.com/feathericons/react-feather
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import React from 'react';
|
import React, { forwardRef } from 'react';
|
||||||
|
|
||||||
type IconProps = {
|
type IconProps = {
|
||||||
size: number,
|
size: number,
|
||||||
|
@ -8,28 +9,37 @@ type IconProps = {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Returns a react component
|
// Returns a react component
|
||||||
const buildIcon = (iconStrokes: React$Node, options?: {} = {}) => (props: IconProps) => {
|
// Icons with tooltips need to use this function so the ref can be properly forwarded
|
||||||
const { size = 24, color = 'currentColor', ...otherProps } = props;
|
const buildIcon = (iconStrokes: React$Node) =>
|
||||||
return (
|
forwardRef((props: IconProps, ref) => {
|
||||||
<svg
|
const { size = 24, color = 'currentColor', ...otherProps } = props;
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
return (
|
||||||
viewBox="0 0 24 24"
|
<svg
|
||||||
width={size}
|
ref={ref}
|
||||||
height={size}
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
viewBox="0 0 24 24"
|
||||||
stroke={color}
|
width={size}
|
||||||
strokeWidth="2"
|
height={size}
|
||||||
strokeLinecap="round"
|
fill="none"
|
||||||
strokeLinejoin="round"
|
stroke={color}
|
||||||
{...options}
|
strokeWidth="2"
|
||||||
{...otherProps}
|
strokeLinecap="round"
|
||||||
>
|
strokeLinejoin="round"
|
||||||
{iconStrokes}
|
{...otherProps}
|
||||||
</svg>
|
>
|
||||||
);
|
{iconStrokes}
|
||||||
};
|
</svg>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export const customIcons = {
|
export const icons = {
|
||||||
|
// The LBRY icon is different from the base icon set so don't use buildIcon()
|
||||||
|
[ICONS.LBRY]: () => (
|
||||||
|
<svg stroke="currentColor" fill="currentColor" x="0px" y="0px" viewBox="0 0 322 254" className="icon lbry-icon">
|
||||||
|
<path d="M296,85.9V100l-138.8,85.3L52.6,134l0.2-7.9l104,51.2L289,96.1v-5.8L164.2,30.1L25,116.2v38.5l131.8,65.2 l137.6-84.4l3.9,6l-141.1,86.4L18.1,159.1v-46.8l145.8-90.2C163.9,22.1,296,85.9,296,85.9z" />
|
||||||
|
<path d="M294.3,150.9l2-12.6l-12.2-2.1l0.8-4.9l17.1,2.9l-2.8,17.5L294.3,150.9L294.3,150.9z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
[ICONS.ARROW_LEFT]: buildIcon(
|
[ICONS.ARROW_LEFT]: buildIcon(
|
||||||
<g fill="none" fillRule="evenodd" strokeLinecap="round">
|
<g fill="none" fillRule="evenodd" strokeLinecap="round">
|
||||||
<path d="M4, 12 L21, 12" />
|
<path d="M4, 12 L21, 12" />
|
||||||
|
@ -46,48 +56,12 @@ export const customIcons = {
|
||||||
<polyline strokeLinejoin="round" points="13 4 21 12 13 20" />
|
<polyline strokeLinejoin="round" points="13 4 21 12 13 20" />
|
||||||
</g>
|
</g>
|
||||||
),
|
),
|
||||||
[ICONS.VIEW]: buildIcon(
|
|
||||||
<g fill="none" fillRule="evenodd">
|
|
||||||
<path
|
|
||||||
d="M2, 12 C2, 12 5, 5 12, 5 C19, 5 22, 12 22, 12 C22, 12 19, 19 12, 19 C5, 19 2, 12 2, 12 Z"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
<circle cx="12" cy="12" r="3" />
|
|
||||||
<path d="M12, 5 L12, 3" strokeLinecap="round" />
|
|
||||||
<path d="M18, 6.5 L19, 5" strokeLinecap="round" />
|
|
||||||
<path d="M21, 10 L22.5, 9" strokeLinecap="round" />
|
|
||||||
<path
|
|
||||||
d="M1.5, 10 L3, 9"
|
|
||||||
strokeLinecap="round"
|
|
||||||
transform="translate(2.250000, 9.500000) scale(1, -1) translate(-2.250000, -9.500000)"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M5, 6.5 L6, 5"
|
|
||||||
strokeLinecap="round"
|
|
||||||
transform="translate(5.500000, 5.750000) scale(-1, 1) translate(-5.500000, -5.750000)"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
),
|
|
||||||
|
|
||||||
[ICONS.HOME]: buildIcon(
|
[ICONS.HOME]: buildIcon(
|
||||||
<g strokeWidth="2" fill="none" fillRule="evenodd" strokeLinecap="round" strokeLinejoin="round">
|
<g strokeWidth="2" fill="none" fillRule="evenodd" strokeLinecap="round" strokeLinejoin="round">
|
||||||
<path d="M1, 11 L12, 2 C12, 2 22.9999989, 11.0000005 23, 11" />
|
<path d="M1, 11 L12, 2 C12, 2 22.9999989, 11.0000005 23, 11" />
|
||||||
<path d="M3, 10 C3, 10 3, 10.4453982 3, 10.9968336 L3, 20.0170446 C3, 20.5675806 3.43788135, 21.0138782 4.00292933, 21.0138781 L8.99707067, 21.0138779 C9.55097324, 21.0138779 10, 20.5751284 10, 20.0089602 L10, 15.0049177 C10, 14.449917 10.4433532, 14 11.0093689, 14 L12.9906311, 14 C13.5480902, 14 14, 14.4387495 14, 15.0049177 L14, 20.0089602 C14, 20.5639609 14.4378817, 21.0138779 15.0029302, 21.0138779 L19.9970758, 21.0138781 C20.5509789, 21.0138782 21.000006, 20.56848 21.000006, 20.0170446 L21.0000057, 10" />
|
<path d="M3, 10 C3, 10 3, 10.4453982 3, 10.9968336 L3, 20.0170446 C3, 20.5675806 3.43788135, 21.0138782 4.00292933, 21.0138781 L8.99707067, 21.0138779 C9.55097324, 21.0138779 10, 20.5751284 10, 20.0089602 L10, 15.0049177 C10, 14.449917 10.4433532, 14 11.0093689, 14 L12.9906311, 14 C13.5480902, 14 14, 14.4387495 14, 15.0049177 L14, 20.0089602 C14, 20.5639609 14.4378817, 21.0138779 15.0029302, 21.0138779 L19.9970758, 21.0138781 C20.5509789, 21.0138782 21.000006, 20.56848 21.000006, 20.0170446 L21.0000057, 10" />
|
||||||
</g>
|
</g>
|
||||||
),
|
),
|
||||||
[ICONS.MENU]: buildIcon(
|
|
||||||
<path
|
|
||||||
d="M3.5, 7 C3.5, 7.27910535 3.72002141, 7.5 3.99339768, 7.5 L20.0066023, 7.5 C20.2782464, 7.5 20.5, 7.27680164 20.5, 7 C20.5, 6.72089465 20.2799786, 6.5 20.0066023, 6.5 L3.99339768, 6.5 C3.72175357, 6.5 3.5, 6.72319836 3.5, 7 Z M3.5, 12 C3.5, 12.2791054 3.72002141, 12.5 3.99339768, 12.5 L20.0066023, 12.5 C20.2782464, 12.5 20.5, 12.2768016 20.5, 12 C20.5, 11.7208946 20.2799786, 11.5 20.0066023, 11.5 L3.99339768, 11.5 C3.72175357, 11.5 3.5, 11.7231984 3.5, 12 Z M3.5, 17 C3.5, 17.2791054 3.72002141, 17.5 3.99339768, 17.5 L20.0066023, 17.5 C20.2782464, 17.5 20.5, 17.2768016 20.5, 17 C20.5, 16.7208946 20.2799786, 16.5 20.0066023, 16.5 L3.99339768, 16.5 C3.72175357, 16.5 3.5, 16.7231984 3.5, 17 Z"
|
|
||||||
fill="none"
|
|
||||||
fillRule="evenodd"
|
|
||||||
strokeWidth="1"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
[ICONS.PLAY]: buildIcon(
|
|
||||||
<g fill="white" fillRule="evenodd" strokeLinejoin="round">
|
|
||||||
<polygon points="5 21 5 3 21 12" />
|
|
||||||
</g>
|
|
||||||
),
|
|
||||||
[ICONS.UPLOAD]: buildIcon(
|
[ICONS.UPLOAD]: buildIcon(
|
||||||
<g fill="none" fillRule="evenodd" strokeLinecap="round">
|
<g fill="none" fillRule="evenodd" strokeLinecap="round">
|
||||||
<path
|
<path
|
||||||
|
@ -111,15 +85,129 @@ export const customIcons = {
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
),
|
),
|
||||||
// Extended from the feather-icons Heart
|
[ICONS.SUBSCRIPTION]: buildIcon(
|
||||||
[ICONS.UNSUBSCRIBE]: buildIcon(
|
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
|
||||||
<path d="M 12,5.67 10.94,4.61 C 5.7533356,-0.57666427 -2.0266644,7.2033357 3.16,12.39 l 1.06,1.06 7.78,7.78 7.78,-7.78 1.06,-1.06 c 2.149101,-2.148092 2.149101,-5.6319078 0,-7.78 -2.148092,-2.1491008 -5.631908,-2.1491008 -7.78,0 L 9.4481298,8.2303201 15.320603,9.2419066 11.772427,13.723825" />
|
|
||||||
),
|
),
|
||||||
// The LBRY icon is different from the base icon set so don't use buildIcon()
|
[ICONS.SETTINGS]: buildIcon(
|
||||||
[ICONS.LBRY]: props => (
|
<g>
|
||||||
<svg stroke="currentColor" fill="currentColor" x="0px" y="0px" viewBox="0 0 322 254" className="icon lbry-icon">
|
<circle cx="12" cy="12" r="3" />
|
||||||
<path d="M296,85.9V100l-138.8,85.3L52.6,134l0.2-7.9l104,51.2L289,96.1v-5.8L164.2,30.1L25,116.2v38.5l131.8,65.2 l137.6-84.4l3.9,6l-141.1,86.4L18.1,159.1v-46.8l145.8-90.2C163.9,22.1,296,85.9,296,85.9z" />
|
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" />
|
||||||
<path d="M294.3,150.9l2-12.6l-12.2-2.1l0.8-4.9l17.1,2.9l-2.8,17.5L294.3,150.9L294.3,150.9z" />
|
</g>
|
||||||
</svg>
|
),
|
||||||
|
[ICONS.ACCOUNT]: buildIcon(
|
||||||
|
<g>
|
||||||
|
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
|
||||||
|
<circle cx="12" cy="7" r="4" />
|
||||||
|
</g>
|
||||||
|
),
|
||||||
|
[ICONS.OVERVIEW]: buildIcon(<polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />),
|
||||||
|
[ICONS.WALLET]: buildIcon(
|
||||||
|
<g>
|
||||||
|
<line x1="8" y1="6" x2="21" y2="6" />
|
||||||
|
<line x1="8" y1="12" x2="21" y2="12" />
|
||||||
|
<line x1="8" y1="18" x2="21" y2="18" />
|
||||||
|
<line x1="3" y1="6" x2="3" y2="6" />
|
||||||
|
<line x1="3" y1="12" x2="3" y2="12" />
|
||||||
|
<line x1="3" y1="18" x2="3" y2="18" />
|
||||||
|
</g>
|
||||||
|
),
|
||||||
|
[ICONS.LIBRARY]: buildIcon(<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />),
|
||||||
|
[ICONS.EDIT]: buildIcon(
|
||||||
|
<g>
|
||||||
|
<path d="M20 14.66V20a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h5.34" />
|
||||||
|
<polygon points="18 2 22 6 12 16 8 16 8 12 18 2" />
|
||||||
|
</g>
|
||||||
|
),
|
||||||
|
[ICONS.DOWNLOAD]: buildIcon(
|
||||||
|
<g>
|
||||||
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||||
|
<polyline points="7 10 12 15 17 10" />
|
||||||
|
<line x1="12" y1="15" x2="12" y2="3" />
|
||||||
|
</g>
|
||||||
|
),
|
||||||
|
[ICONS.HELP]: buildIcon(
|
||||||
|
<g>
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
|
||||||
|
<line x1="12" y1="17" x2="12" y2="17" />
|
||||||
|
</g>
|
||||||
|
),
|
||||||
|
[ICONS.LIGHT]: buildIcon(
|
||||||
|
<g>
|
||||||
|
<circle cx="12" cy="12" r="5" />
|
||||||
|
<line x1="12" y1="1" x2="12" y2="3" />
|
||||||
|
<line x1="12" y1="21" x2="12" y2="23" />
|
||||||
|
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
|
||||||
|
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
|
||||||
|
<line x1="1" y1="12" x2="3" y2="12" />
|
||||||
|
<line x1="21" y1="12" x2="23" y2="12" />
|
||||||
|
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
|
||||||
|
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
|
||||||
|
</g>
|
||||||
|
),
|
||||||
|
[ICONS.DARK]: buildIcon(<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />),
|
||||||
|
[ICONS.SEARCH]: buildIcon(
|
||||||
|
<g>
|
||||||
|
<circle cx="11" cy="11" r="8" />
|
||||||
|
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
||||||
|
</g>
|
||||||
|
),
|
||||||
|
[ICONS.TIP]: buildIcon(
|
||||||
|
<g>
|
||||||
|
<polyline points="20 12 20 22 4 22 4 12" />
|
||||||
|
<rect x="2" y="7" width="20" height="5" />
|
||||||
|
<line x1="12" y1="22" x2="12" y2="7" />
|
||||||
|
<path d="M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7z" />
|
||||||
|
<path d="M12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z" />
|
||||||
|
</g>
|
||||||
|
),
|
||||||
|
[ICONS.SHARE]: buildIcon(
|
||||||
|
<g>
|
||||||
|
<circle cx="18" cy="5" r="3" />
|
||||||
|
<circle cx="6" cy="12" r="3" />
|
||||||
|
<circle cx="18" cy="19" r="3" />
|
||||||
|
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49" />
|
||||||
|
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49" />
|
||||||
|
</g>
|
||||||
|
),
|
||||||
|
[ICONS.REPORT]: buildIcon(
|
||||||
|
<g>
|
||||||
|
<path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z" />
|
||||||
|
<line x1="4" y1="22" x2="4" y2="15" />
|
||||||
|
</g>
|
||||||
|
),
|
||||||
|
[ICONS.EXTERNAL]: buildIcon(
|
||||||
|
<g>
|
||||||
|
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
|
||||||
|
<polyline points="15 3 21 3 21 9" />
|
||||||
|
<line x1="10" y1="14" x2="21" y2="3" />
|
||||||
|
</g>
|
||||||
|
),
|
||||||
|
[ICONS.DELETE]: buildIcon(
|
||||||
|
<g>
|
||||||
|
<polyline points="3 6 5 6 21 6" />
|
||||||
|
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
||||||
|
</g>
|
||||||
|
),
|
||||||
|
[ICONS.COPY]: buildIcon(
|
||||||
|
<g>
|
||||||
|
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2" />
|
||||||
|
<rect x="8" y="2" width="8" height="4" rx="1" ry="1" />
|
||||||
|
</g>
|
||||||
|
),
|
||||||
|
[ICONS.REMOVE]: buildIcon(
|
||||||
|
<g>
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18" />
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18" />
|
||||||
|
</g>
|
||||||
|
),
|
||||||
|
[ICONS.ADD]: buildIcon(
|
||||||
|
<g>
|
||||||
|
<line x1="12" y1="5" x2="12" y2="19" />
|
||||||
|
<line x1="5" y1="12" x2="19" y2="12" />
|
||||||
|
</g>
|
||||||
|
),
|
||||||
|
[ICONS.CHAT]: buildIcon(
|
||||||
|
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import * as FEATHER_ICONS from 'react-feather';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Tooltip from 'component/common/tooltip';
|
import Tooltip from 'component/common/tooltip';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { customIcons } from './icon-custom';
|
import { icons } from './icon-custom';
|
||||||
|
|
||||||
// It would be nice to standardize this somehow
|
// It would be nice to standardize this somehow
|
||||||
// These are copied from `scss/vars`, can they both come from the same source?
|
// These are copied from `scss/vars`, can they both come from the same source?
|
||||||
|
@ -14,7 +13,7 @@ const BLUE_COLOR = '#49b2e2';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
icon: string,
|
icon: string,
|
||||||
tooltip?: string, // tooltip direction
|
tooltip?: boolean,
|
||||||
iconColor?: string,
|
iconColor?: string,
|
||||||
size?: number,
|
size?: number,
|
||||||
className?: string,
|
className?: string,
|
||||||
|
@ -27,6 +26,10 @@ class IconComponent extends React.PureComponent<Props> {
|
||||||
return __('Featured content. Earn rewards for watching.');
|
return __('Featured content. Earn rewards for watching.');
|
||||||
case ICONS.DOWNLOAD:
|
case ICONS.DOWNLOAD:
|
||||||
return __('This file is downloaded.');
|
return __('This file is downloaded.');
|
||||||
|
case ICONS.SUBSCRIPTION:
|
||||||
|
return __('You are subscribed to this channel.');
|
||||||
|
case ICONS.SETTINGS:
|
||||||
|
return __('Your settings.');
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -47,9 +50,10 @@ class IconComponent extends React.PureComponent<Props> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { icon, tooltip, iconColor, size, className } = this.props;
|
const { icon, tooltip, iconColor, size, className } = this.props;
|
||||||
const Icon = customIcons[this.props.icon] || FEATHER_ICONS[this.props.icon];
|
const Icon = icons[this.props.icon];
|
||||||
|
|
||||||
if (!Icon) {
|
if (!Icon) {
|
||||||
|
console.error('no icon found for ', icon);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,13 +71,7 @@ class IconComponent extends React.PureComponent<Props> {
|
||||||
|
|
||||||
const inner = <Icon size={iconSize} className={classnames(`icon icon--${icon}`, className)} color={color} />;
|
const inner = <Icon size={iconSize} className={classnames(`icon icon--${icon}`, className)} color={color} />;
|
||||||
|
|
||||||
return tooltipText ? (
|
return tooltipText ? <Tooltip label={tooltipText}>{inner}</Tooltip> : inner;
|
||||||
<Tooltip icon body={tooltipText} direction={tooltip}>
|
|
||||||
{inner}
|
|
||||||
</Tooltip>
|
|
||||||
) : (
|
|
||||||
inner
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,71 +1,18 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as React from 'react';
|
import type { Node } from 'react';
|
||||||
import classnames from 'classnames';
|
import React from 'react';
|
||||||
|
import ReachTooltip from '@reach/tooltip';
|
||||||
|
import '@reach/tooltip/styles.css';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
body: string,
|
label: string | Node,
|
||||||
label?: string,
|
children?: Node,
|
||||||
children?: React.Node,
|
|
||||||
icon?: boolean,
|
|
||||||
direction: string,
|
|
||||||
onComponent?: boolean, // extra padding to account for button/form field size
|
|
||||||
alwaysVisible?: boolean, // should tooltip stay open, guide callbacks will close it manually
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
function Tooltip(props: Props) {
|
||||||
direction: string,
|
const { children, label } = props;
|
||||||
};
|
|
||||||
|
|
||||||
class ToolTip extends React.PureComponent<Props, State> {
|
return <ReachTooltip label={label}>{children}</ReachTooltip>;
|
||||||
static defaultProps = {
|
|
||||||
direction: 'bottom',
|
|
||||||
alwaysVisible: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props: Props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
direction: this.props.direction,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
tooltip: ?HTMLSpanElement;
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { direction } = this.state;
|
|
||||||
const { children, label, body, icon, onComponent, alwaysVisible } = this.props;
|
|
||||||
|
|
||||||
const tooltipContent = children || label;
|
|
||||||
const bodyLength = body.length;
|
|
||||||
const isShortDescription = bodyLength < 30;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
className={classnames('tooltip', {
|
|
||||||
'tooltip--label': label && !icon,
|
|
||||||
'tooltip--icon': icon,
|
|
||||||
'tooltip--top': direction === 'top',
|
|
||||||
'tooltip--right': direction === 'right',
|
|
||||||
'tooltip--bottom': direction === 'bottom',
|
|
||||||
'tooltip--left': direction === 'left',
|
|
||||||
'tooltip--on-component': onComponent,
|
|
||||||
'tooltip--always-visible': alwaysVisible,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{tooltipContent}
|
|
||||||
<span
|
|
||||||
ref={ref => {
|
|
||||||
this.tooltip = ref;
|
|
||||||
}}
|
|
||||||
className={classnames('tooltip__body', {
|
|
||||||
'tooltip__body--short': isShortDescription,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{body}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ToolTip;
|
export default Tooltip;
|
||||||
|
|
|
@ -3,10 +3,12 @@ import Button from 'component/button';
|
||||||
|
|
||||||
export default function UnsupportedOnWeb() {
|
export default function UnsupportedOnWeb() {
|
||||||
return (
|
return (
|
||||||
<div className="card__content help help--warning">
|
IS_WEB && (
|
||||||
This page is not currently supported on the web.{' '}
|
<div className="card__content help help--warning">
|
||||||
<Button button="link" label={__('Download the desktop app')} href="https://lbry.com/get" /> for full feature
|
This page is not currently supported on the web.{' '}
|
||||||
support.
|
<Button button="link" label={__('Download the desktop app')} href="https://lbry.com/get" /> for full feature
|
||||||
</div>
|
support.
|
||||||
|
</div>
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,7 @@ export default class CopyableText extends React.PureComponent<Props> {
|
||||||
inputButton={
|
inputButton={
|
||||||
<Button
|
<Button
|
||||||
button="inverse"
|
button="inverse"
|
||||||
icon={ICONS.CLIPBOARD}
|
icon={ICONS.COPY}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
clipboard.writeText(copyable);
|
clipboard.writeText(copyable);
|
||||||
doToast({
|
doToast({
|
||||||
|
|
|
@ -68,7 +68,7 @@ class ErrorBoundary extends React.Component<Props, State> {
|
||||||
render() {
|
render() {
|
||||||
if (this.state.hasError) {
|
if (this.state.hasError) {
|
||||||
return (
|
return (
|
||||||
<div className="load-screen">
|
<div className="main main--empty">
|
||||||
<Yrbl
|
<Yrbl
|
||||||
type="sad"
|
type="sad"
|
||||||
title={__('Aw shucks!')}
|
title={__('Aw shucks!')}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import type { Node } from 'react';
|
||||||
import * as MODALS from 'constants/modal_types';
|
import * as MODALS from 'constants/modal_types';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import * as React from 'react';
|
import React from 'react';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import Tooltip from 'component/common/tooltip';
|
import Tooltip from 'component/common/tooltip';
|
||||||
import { requestFullscreen, fullscreenElement } from 'util/full-screen';
|
import { requestFullscreen, fullscreenElement } from 'util/full-screen';
|
||||||
|
@ -16,7 +17,7 @@ type Props = {
|
||||||
openModal: (id: string, { uri: string }) => void,
|
openModal: (id: string, { uri: string }) => void,
|
||||||
claimIsMine: boolean,
|
claimIsMine: boolean,
|
||||||
fileInfo: FileInfo,
|
fileInfo: FileInfo,
|
||||||
viewerContainer: React.Ref,
|
viewerContainer: ?{ current: Node },
|
||||||
showFullscreen: boolean,
|
showFullscreen: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -38,14 +39,19 @@ class FileActions extends React.PureComponent<Props> {
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{showFullscreen && (
|
{showFullscreen && (
|
||||||
<Tooltip onComponent body={__('Full screen (f)')}>
|
<Tooltip label={__('Full screen (f)')}>
|
||||||
<Button button="alt" description={__('Fullscreen')} icon={ICONS.FULLSCREEN} onClick={this.maximizeViewer} />
|
<Button
|
||||||
|
button="link"
|
||||||
|
description={__('Fullscreen')}
|
||||||
|
icon={ICONS.FULLSCREEN}
|
||||||
|
onClick={this.maximizeViewer}
|
||||||
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{showDelete && (
|
{showDelete && (
|
||||||
<Tooltip onComponent body={__('Delete this file')}>
|
<Tooltip label={__('Remove from your library')}>
|
||||||
<Button
|
<Button
|
||||||
button="alt"
|
button="link"
|
||||||
icon={ICONS.DELETE}
|
icon={ICONS.DELETE}
|
||||||
description={__('Delete')}
|
description={__('Delete')}
|
||||||
onClick={() => openModal(MODALS.CONFIRM_FILE_REMOVE, { uri })}
|
onClick={() => openModal(MODALS.CONFIRM_FILE_REMOVE, { uri })}
|
||||||
|
@ -53,8 +59,8 @@ class FileActions extends React.PureComponent<Props> {
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{!claimIsMine && (
|
{!claimIsMine && (
|
||||||
<Tooltip onComponent body={__('Report content')}>
|
<Tooltip label={__('Report content')}>
|
||||||
<Button icon={ICONS.REPORT} href={`https://lbry.com/dmca?claim_id=${claimId}`} />
|
<Button button="link" icon={ICONS.REPORT} href={`https://lbry.com/dmca?claim_id=${claimId}`} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
|
|
@ -57,11 +57,10 @@ class FileDownloadLink extends React.PureComponent<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToolTip onComponent body={__('Download')}>
|
<ToolTip label={__('Add to your library')}>
|
||||||
<Button
|
<Button
|
||||||
button="alt"
|
button="link"
|
||||||
icon={ICONS.DOWNLOAD}
|
icon={ICONS.DOWNLOAD}
|
||||||
iconColor="green"
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
purchaseUri(uri);
|
purchaseUri(uri);
|
||||||
|
|
||||||
|
@ -76,10 +75,9 @@ class FileDownloadLink extends React.PureComponent<Props> {
|
||||||
);
|
);
|
||||||
} else if (fileInfo && fileInfo.download_path) {
|
} else if (fileInfo && fileInfo.download_path) {
|
||||||
return (
|
return (
|
||||||
<ToolTip onComponent body={__('Open file')}>
|
<ToolTip label={__('Open file')}>
|
||||||
<Button
|
<Button
|
||||||
button="alt"
|
button="link"
|
||||||
iconColor="green"
|
|
||||||
icon={ICONS.EXTERNAL}
|
icon={ICONS.EXTERNAL}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
pause();
|
pause();
|
||||||
|
|
|
@ -21,9 +21,9 @@ export default function FileProperties(props: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="file-properties">
|
<div className="file-properties">
|
||||||
{isSubscribed && <Icon icon={icons.SUBSCRIPTION} />}
|
{isSubscribed && <Icon tooltip icon={icons.SUBSCRIPTION} />}
|
||||||
{!claimIsMine && downloaded && <Icon icon={icons.DOWNLOAD} />}
|
{!claimIsMine && downloaded && <Icon tooltip icon={icons.DOWNLOAD} />}
|
||||||
{isRewardContent && <Icon iconColor="red" icon={icons.FEATURED} />}
|
{isRewardContent && <Icon tooltip icon={icons.FEATURED} />}
|
||||||
<FilePrice hideFree uri={uri} />
|
<FilePrice hideFree uri={uri} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,12 +4,12 @@ import React from 'react';
|
||||||
import LoadingScreen from 'component/common/loading-screen';
|
import LoadingScreen from 'component/common/loading-screen';
|
||||||
import VideoViewer from 'component/viewers/videoViewer';
|
import VideoViewer from 'component/viewers/videoViewer';
|
||||||
|
|
||||||
const AudioViewer = React.lazy<*>(() =>
|
// Audio player on hold until the current player is dropped
|
||||||
import(
|
// This component is half working
|
||||||
/* webpackChunkName: "audioViewer" */
|
// const AudioViewer = React.lazy<*>(() =>
|
||||||
'component/viewers/audioViewer'
|
// import(/* webpackChunkName: "audioViewer" */
|
||||||
)
|
// 'component/viewers/audioViewer')
|
||||||
);
|
// );
|
||||||
|
|
||||||
const DocumentViewer = React.lazy<*>(() =>
|
const DocumentViewer = React.lazy<*>(() =>
|
||||||
import(
|
import(
|
||||||
|
@ -166,7 +166,7 @@ class FileRender extends React.PureComponent<Props> {
|
||||||
video: (
|
video: (
|
||||||
<VideoViewer claim={claim} source={{ downloadPath, fileName }} contentType={contentType} poster={poster} />
|
<VideoViewer claim={claim} source={{ downloadPath, fileName }} contentType={contentType} poster={poster} />
|
||||||
),
|
),
|
||||||
audio: <AudioViewer claim={claim} source={{ downloadPath, fileName }} contentType={contentType} />,
|
// audio: <AudioViewer claim={claim} source={{ downloadPath, fileName }} contentType={contentType} />,
|
||||||
// Add routes to viewer...
|
// Add routes to viewer...
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -208,7 +208,7 @@ class FileRender extends React.PureComponent<Props> {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="file-render">
|
<div className="file-render">
|
||||||
<React.Suspense fallback={<div />}>{this.renderViewer()}</React.Suspense>
|
<Suspense fallback={<div />}>{this.renderViewer()}</Suspense>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,7 @@ const Header = (props: Props) => {
|
||||||
description={__('Navigate back')}
|
description={__('Navigate back')}
|
||||||
onClick={() => window.history.back()}
|
onClick={() => window.history.back()}
|
||||||
icon={ICONS.ARROW_LEFT}
|
icon={ICONS.ARROW_LEFT}
|
||||||
iconSize={15}
|
iconSize={18}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
@ -61,7 +61,7 @@ const Header = (props: Props) => {
|
||||||
description={__('Navigate forward')}
|
description={__('Navigate forward')}
|
||||||
onClick={() => window.history.forward()}
|
onClick={() => window.history.forward()}
|
||||||
icon={ICONS.ARROW_RIGHT}
|
icon={ICONS.ARROW_RIGHT}
|
||||||
iconSize={15}
|
iconSize={18}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* @endif */}
|
{/* @endif */}
|
||||||
|
@ -98,11 +98,11 @@ const Header = (props: Props) => {
|
||||||
</Menu>
|
</Menu>
|
||||||
<Menu>
|
<Menu>
|
||||||
<MenuButton className="header__navigation-item menu__title">
|
<MenuButton className="header__navigation-item menu__title">
|
||||||
<Icon icon={ICONS.SETTINGS} />
|
<Icon size={18} icon={ICONS.SETTINGS} />
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
<MenuList>
|
<MenuList>
|
||||||
<MenuItem className="menu__link" onSelect={() => history.push(`/$/settings`)}>
|
<MenuItem className="menu__link" onSelect={() => history.push(`/$/settings`)}>
|
||||||
<Icon aria-hidden icon={ICONS.SETTINGS} />
|
<Icon aria-hidden tootlip icon={ICONS.SETTINGS} />
|
||||||
{__('Settings')}
|
{__('Settings')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem className="menu__link" onSelect={() => history.push(`/$/help`)}>
|
<MenuItem className="menu__link" onSelect={() => history.push(`/$/help`)}>
|
||||||
|
|
46
src/ui/component/publishAdditionalOptions/index.js
Normal file
46
src/ui/component/publishAdditionalOptions/index.js
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { doResolveUri, selectBalance } from 'lbry-redux';
|
||||||
|
import {
|
||||||
|
selectPublishFormValues,
|
||||||
|
selectIsStillEditing,
|
||||||
|
selectMyClaimForUri,
|
||||||
|
selectIsResolvingPublishUris,
|
||||||
|
selectTakeOverAmount,
|
||||||
|
} from 'redux/selectors/publish';
|
||||||
|
import {
|
||||||
|
doResetThumbnailStatus,
|
||||||
|
doClearPublish,
|
||||||
|
doUpdatePublishForm,
|
||||||
|
doPublish,
|
||||||
|
doPrepareEdit,
|
||||||
|
} from 'redux/actions/publish';
|
||||||
|
import { selectUnclaimedRewardValue } from 'lbryinc';
|
||||||
|
import PublishPage from './view';
|
||||||
|
|
||||||
|
const select = state => ({
|
||||||
|
...selectPublishFormValues(state),
|
||||||
|
// The winning claim for a short lbry uri
|
||||||
|
amountNeededForTakeover: selectTakeOverAmount(state),
|
||||||
|
// My previously published claims under this short lbry uri
|
||||||
|
myClaimForUri: selectMyClaimForUri(state),
|
||||||
|
// If I clicked the "edit" button, have I changed the uri?
|
||||||
|
// Need this to make it easier to find the source on previously published content
|
||||||
|
isStillEditing: selectIsStillEditing(state),
|
||||||
|
isResolvingUri: selectIsResolvingPublishUris(state),
|
||||||
|
totalRewardValue: selectUnclaimedRewardValue(state),
|
||||||
|
balance: selectBalance(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
const perform = dispatch => ({
|
||||||
|
updatePublishForm: value => dispatch(doUpdatePublishForm(value)),
|
||||||
|
clearPublish: () => dispatch(doClearPublish()),
|
||||||
|
resolveUri: uri => dispatch(doResolveUri(uri)),
|
||||||
|
publish: params => dispatch(doPublish(params)),
|
||||||
|
prepareEdit: (claim, uri) => dispatch(doPrepareEdit(claim, uri)),
|
||||||
|
resetThumbnailStatus: () => dispatch(doResetThumbnailStatus()),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
select,
|
||||||
|
perform
|
||||||
|
)(PublishPage);
|
|
@ -4,7 +4,7 @@ import { FormField } from 'component/common/form';
|
||||||
import { CC_LICENSES, COPYRIGHT, OTHER, PUBLIC_DOMAIN, NONE } from 'constants/licenses';
|
import { CC_LICENSES, COPYRIGHT, OTHER, PUBLIC_DOMAIN, NONE } from 'constants/licenses';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
licenseType: string,
|
licenseType: ?string,
|
||||||
licenseUrl: ?string,
|
licenseUrl: ?string,
|
||||||
otherLicenseDescription: ?string,
|
otherLicenseDescription: ?string,
|
||||||
handleLicenseChange: (string, string) => void,
|
handleLicenseChange: (string, string) => void,
|
80
src/ui/component/publishAdditionalOptions/view.jsx
Normal file
80
src/ui/component/publishAdditionalOptions/view.jsx
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import usePersistedState from 'util/use-persisted-state';
|
||||||
|
import { FormField } from 'component/common/form';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import LicenseType from './license-type';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
language: ?string,
|
||||||
|
name: ?string,
|
||||||
|
licenseType: ?string,
|
||||||
|
otherLicenseDescription: ?string,
|
||||||
|
licenseUrl: ?string,
|
||||||
|
disabled: boolean,
|
||||||
|
updatePublishForm: ({}) => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
function PublishAdvanced(props: Props) {
|
||||||
|
const { language, name, licenseType, otherLicenseDescription, licenseUrl, updatePublishForm } = props;
|
||||||
|
const [hideSection, setHideSection] = usePersistedState('publish-advanced-options', true);
|
||||||
|
|
||||||
|
function toggleHideSection() {
|
||||||
|
setHideSection(!hideSection);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="card card--section">
|
||||||
|
{!hideSection && (
|
||||||
|
<div className={classnames('card__content', { 'card--disabled': !name })}>
|
||||||
|
<FormField
|
||||||
|
label={__('Language')}
|
||||||
|
type="select"
|
||||||
|
name="content_language"
|
||||||
|
value={language}
|
||||||
|
onChange={event => updatePublishForm({ language: event.target.value })}
|
||||||
|
>
|
||||||
|
<option value="en">{__('English')}</option>
|
||||||
|
<option value="zh">{__('Chinese')}</option>
|
||||||
|
<option value="fr">{__('French')}</option>
|
||||||
|
<option value="de">{__('German')}</option>
|
||||||
|
<option value="jp">{__('Japanese')}</option>
|
||||||
|
<option value="ru">{__('Russian')}</option>
|
||||||
|
<option value="es">{__('Spanish')}</option>
|
||||||
|
<option value="id">{__('Indonesian')}</option>
|
||||||
|
<option value="it">{__('Italian')}</option>
|
||||||
|
<option value="nl">{__('Dutch')}</option>
|
||||||
|
<option value="tr">{__('Turkish')}</option>
|
||||||
|
<option value="pl">{__('Polish')}</option>
|
||||||
|
<option value="ms">{__('Malay')}</option>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<LicenseType
|
||||||
|
licenseType={licenseType}
|
||||||
|
otherLicenseDescription={otherLicenseDescription}
|
||||||
|
licenseUrl={licenseUrl}
|
||||||
|
handleLicenseChange={(newLicenseType, newLicenseUrl) =>
|
||||||
|
updatePublishForm({
|
||||||
|
licenseType: newLicenseType,
|
||||||
|
licenseUrl: newLicenseUrl,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
handleLicenseDescriptionChange={event =>
|
||||||
|
updatePublishForm({
|
||||||
|
otherLicenseDescription: event.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
handleLicenseUrlChange={event => updatePublishForm({ licenseUrl: event.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="card__actions">
|
||||||
|
<Button label={hideSection ? __('Additional Options') : __('Hide')} button="link" onClick={toggleHideSection} />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PublishAdvanced;
|
21
src/ui/component/publishFile/index.js
Normal file
21
src/ui/component/publishFile/index.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { selectBalance } from 'lbry-redux';
|
||||||
|
import { selectIsStillEditing, makeSelectPublishFormValue } from 'redux/selectors/publish';
|
||||||
|
import { doUpdatePublishForm } from 'redux/actions/publish';
|
||||||
|
import PublishPage from './view';
|
||||||
|
|
||||||
|
const select = state => ({
|
||||||
|
name: makeSelectPublishFormValue('name')(state),
|
||||||
|
filePath: makeSelectPublishFormValue('filePath')(state),
|
||||||
|
isStillEditing: selectIsStillEditing(state),
|
||||||
|
balance: selectBalance(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
const perform = dispatch => ({
|
||||||
|
updatePublishForm: value => dispatch(doUpdatePublishForm(value)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
select,
|
||||||
|
perform
|
||||||
|
)(PublishPage);
|
55
src/ui/component/publishFile/view.jsx
Normal file
55
src/ui/component/publishFile/view.jsx
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import { regexInvalidURI } from 'lbry-redux';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import FileSelector from 'component/common/file-selector';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
name: ?string,
|
||||||
|
filePath: ?string,
|
||||||
|
isStillEditing: boolean,
|
||||||
|
balance: number,
|
||||||
|
updatePublishForm: ({}) => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
function PublishFile(props: Props) {
|
||||||
|
const { name, balance, filePath, isStillEditing, updatePublishForm } = props;
|
||||||
|
|
||||||
|
function handleFileChange(filePath: string, fileName: string) {
|
||||||
|
const publishFormParams: { filePath: string, name?: string } = { filePath };
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
const parsedFileName = fileName.replace(regexInvalidURI, '');
|
||||||
|
publishFormParams.name = parsedFileName.replace(' ', '-');
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePublishForm(publishFormParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className={classnames('card card--section', {
|
||||||
|
'card--disabled': balance === 0,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<header className="card__header">
|
||||||
|
<h2 className="card__title card__title--flex-between">{isStillEditing ? __('Edit') : __('Publish')}</h2>
|
||||||
|
{isStillEditing && <p className="card__subtitle">{__('You are currently editing a claim.')}</p>}
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="card__content">
|
||||||
|
<FileSelector currentPath={filePath} onFileChosen={handleFileChange} />
|
||||||
|
{!!isStillEditing && name && (
|
||||||
|
<p className="help">
|
||||||
|
{/* @i18nfixme */}
|
||||||
|
{__("If you don't choose a file, the file from your existing claim")}
|
||||||
|
{` "${name}" `}
|
||||||
|
{__('will be used.')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PublishFile;
|
|
@ -1,3 +1,45 @@
|
||||||
import PublishForm from './view';
|
import { connect } from 'react-redux';
|
||||||
|
import { doResolveUri } from 'lbry-redux';
|
||||||
|
import {
|
||||||
|
selectPublishFormValues,
|
||||||
|
selectIsStillEditing,
|
||||||
|
selectMyClaimForUri,
|
||||||
|
selectIsResolvingPublishUris,
|
||||||
|
selectTakeOverAmount,
|
||||||
|
} from 'redux/selectors/publish';
|
||||||
|
import {
|
||||||
|
doResetThumbnailStatus,
|
||||||
|
doClearPublish,
|
||||||
|
doUpdatePublishForm,
|
||||||
|
doPublish,
|
||||||
|
doPrepareEdit,
|
||||||
|
} from 'redux/actions/publish';
|
||||||
|
import { selectUnclaimedRewardValue } from 'lbryinc';
|
||||||
|
import PublishPage from './view';
|
||||||
|
|
||||||
export default PublishForm;
|
const select = state => ({
|
||||||
|
...selectPublishFormValues(state),
|
||||||
|
// The winning claim for a short lbry uri
|
||||||
|
amountNeededForTakeover: selectTakeOverAmount(state),
|
||||||
|
// My previously published claims under this short lbry uri
|
||||||
|
myClaimForUri: selectMyClaimForUri(state),
|
||||||
|
// If I clicked the "edit" button, have I changed the uri?
|
||||||
|
// Need this to make it easier to find the source on previously published content
|
||||||
|
isStillEditing: selectIsStillEditing(state),
|
||||||
|
isResolvingUri: selectIsResolvingPublishUris(state),
|
||||||
|
totalRewardValue: selectUnclaimedRewardValue(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
const perform = dispatch => ({
|
||||||
|
updatePublishForm: value => dispatch(doUpdatePublishForm(value)),
|
||||||
|
clearPublish: () => dispatch(doClearPublish()),
|
||||||
|
resolveUri: uri => dispatch(doResolveUri(uri)),
|
||||||
|
publish: params => dispatch(doPublish(params)),
|
||||||
|
prepareEdit: (claim, uri) => dispatch(doPrepareEdit(claim, uri)),
|
||||||
|
resetThumbnailStatus: () => dispatch(doResetThumbnailStatus()),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
select,
|
||||||
|
perform
|
||||||
|
)(PublishPage);
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
// @flow
|
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
uri: ?string,
|
|
||||||
isResolvingUri: boolean,
|
|
||||||
amountNeededForTakeover: ?number,
|
|
||||||
};
|
|
||||||
|
|
||||||
class BidHelpText extends React.PureComponent<Props> {
|
|
||||||
render() {
|
|
||||||
const { uri, isResolvingUri, amountNeededForTakeover } = this.props;
|
|
||||||
let bidHelpText;
|
|
||||||
|
|
||||||
if (uri) {
|
|
||||||
if (isResolvingUri) {
|
|
||||||
bidHelpText = __('Checking the winning claim amount...');
|
|
||||||
} else if (!amountNeededForTakeover) {
|
|
||||||
bidHelpText = __('Any amount will give you the winning bid.');
|
|
||||||
} else {
|
|
||||||
bidHelpText = `${__('If you bid more than')} ${amountNeededForTakeover} LBC, ${__(
|
|
||||||
'when someone navigates to'
|
|
||||||
)} ${uri} ${__('it will load your published content')}. ${__(
|
|
||||||
'However, you can get a longer version of this URL for any bid'
|
|
||||||
)}.`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
{__('This LBC remains yours and the deposit can be undone at any time.')}
|
|
||||||
<div>{bidHelpText}</div>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default BidHelpText;
|
|
|
@ -1,50 +0,0 @@
|
||||||
// @flow
|
|
||||||
import * as React from 'react';
|
|
||||||
import Button from 'component/button';
|
|
||||||
import { buildURI } from 'lbry-redux';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
uri: ?string,
|
|
||||||
myClaimForUri: ?StreamClaim,
|
|
||||||
isStillEditing: boolean,
|
|
||||||
onEditMyClaim: (any, string) => void,
|
|
||||||
};
|
|
||||||
|
|
||||||
class NameHelpText extends React.PureComponent<Props> {
|
|
||||||
render() {
|
|
||||||
const { uri, myClaimForUri, onEditMyClaim, isStillEditing } = this.props;
|
|
||||||
|
|
||||||
let nameHelpText;
|
|
||||||
|
|
||||||
if (isStillEditing) {
|
|
||||||
nameHelpText = __(
|
|
||||||
'You are currently editing this claim. If you change the URL, you will need to reselect a file.'
|
|
||||||
);
|
|
||||||
} else if (uri && myClaimForUri) {
|
|
||||||
const editUri = buildURI({
|
|
||||||
contentName: myClaimForUri.name,
|
|
||||||
claimId: myClaimForUri.claim_id,
|
|
||||||
});
|
|
||||||
|
|
||||||
nameHelpText = (
|
|
||||||
<React.Fragment>
|
|
||||||
{__('You already have a claim at')}
|
|
||||||
{` ${uri} `}
|
|
||||||
<Button button="link" label="Edit it" onClick={() => onEditMyClaim(myClaimForUri, editUri)} />
|
|
||||||
<br />
|
|
||||||
{__('Publishing will update your existing claim.')}
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
{nameHelpText || (
|
|
||||||
<span>{__('Create a URL for this content. Simpler names are easier to find and remember.')}</span>
|
|
||||||
)}
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default NameHelpText;
|
|
|
@ -1,21 +1,22 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { COPYRIGHT, OTHER } from 'constants/licenses';
|
import React, { useEffect, Fragment } from 'react';
|
||||||
import { CHANNEL_NEW, CHANNEL_ANONYMOUS, MINIMUM_PUBLISH_BID } from 'constants/claim';
|
import { CHANNEL_NEW, CHANNEL_ANONYMOUS } from 'constants/claim';
|
||||||
import * as ICONS from 'constants/icons';
|
import { buildURI, THUMBNAIL_STATUSES } from 'lbry-redux';
|
||||||
import * as React from 'react';
|
|
||||||
import { isNameValid, buildURI, regexInvalidURI, THUMBNAIL_STATUSES } from 'lbry-redux';
|
|
||||||
import { Form, FormField, FormFieldPrice, Submit } from 'component/common/form';
|
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import ChannelSection from 'component/selectChannel';
|
import ChannelSection from 'component/selectChannel';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import FileSelector from 'component/common/file-selector';
|
|
||||||
import SelectThumbnail from 'component/selectThumbnail';
|
|
||||||
import UnsupportedOnWeb from 'component/common/unsupported-on-web';
|
import UnsupportedOnWeb from 'component/common/unsupported-on-web';
|
||||||
import BidHelpText from './internal/bid-help-text';
|
import TagSelect from 'component/tagsSelect';
|
||||||
import NameHelpText from './internal/name-help-text';
|
import PublishText from 'component/publishText';
|
||||||
import LicenseType from './internal/license-type';
|
import PublishPrice from 'component/publishPrice';
|
||||||
|
import PublishFile from 'component/publishFile';
|
||||||
|
import PublishName from 'component/publishName';
|
||||||
|
import PublishAdditionalOptions from 'component/publishAdditionalOptions';
|
||||||
|
import PublishFormErrors from 'component/publishFormErrors';
|
||||||
|
import SelectThumbnail from 'component/selectThumbnail';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
tags: Array<Tag>,
|
||||||
publish: PublishParams => void,
|
publish: PublishParams => void,
|
||||||
filePath: ?string,
|
filePath: ?string,
|
||||||
bid: ?number,
|
bid: ?number,
|
||||||
|
@ -34,7 +35,6 @@ type Props = {
|
||||||
},
|
},
|
||||||
channel: string,
|
channel: string,
|
||||||
name: ?string,
|
name: ?string,
|
||||||
updatePublishForm: UpdatePublishFormData => void,
|
|
||||||
nameError: ?string,
|
nameError: ?string,
|
||||||
isResolvingUri: boolean,
|
isResolvingUri: boolean,
|
||||||
winningBidForClaimUri: number,
|
winningBidForClaimUri: number,
|
||||||
|
@ -43,7 +43,6 @@ type Props = {
|
||||||
otherLicenseDescription: ?string,
|
otherLicenseDescription: ?string,
|
||||||
licenseUrl: ?string,
|
licenseUrl: ?string,
|
||||||
uri: ?string,
|
uri: ?string,
|
||||||
bidError: ?string,
|
|
||||||
publishing: boolean,
|
publishing: boolean,
|
||||||
balance: number,
|
balance: number,
|
||||||
isStillEditing: boolean,
|
isStillEditing: boolean,
|
||||||
|
@ -53,35 +52,49 @@ type Props = {
|
||||||
prepareEdit: (claim: any, uri: string) => void,
|
prepareEdit: (claim: any, uri: string) => void,
|
||||||
resetThumbnailStatus: () => void,
|
resetThumbnailStatus: () => void,
|
||||||
amountNeededForTakeover: ?number,
|
amountNeededForTakeover: ?number,
|
||||||
|
// Add back type
|
||||||
|
updatePublishForm: any => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
class PublishForm extends React.PureComponent<Props> {
|
function PublishForm(props: Props) {
|
||||||
constructor(props: Props) {
|
const {
|
||||||
super(props);
|
thumbnail,
|
||||||
|
name,
|
||||||
|
channel,
|
||||||
|
editingURI,
|
||||||
|
resolveUri,
|
||||||
|
title,
|
||||||
|
bid,
|
||||||
|
uploadThumbnailStatus,
|
||||||
|
resetThumbnailStatus,
|
||||||
|
updatePublishForm,
|
||||||
|
filePath,
|
||||||
|
publishing,
|
||||||
|
clearPublish,
|
||||||
|
isStillEditing,
|
||||||
|
tags,
|
||||||
|
publish,
|
||||||
|
} = props;
|
||||||
|
const formDisabled = (!filePath && !editingURI) || publishing;
|
||||||
|
// If they are editing, they don't need a new file chosen
|
||||||
|
const formValidLessFile = name && title && bid && !(uploadThumbnailStatus === THUMBNAIL_STATUSES.IN_PROGRESS);
|
||||||
|
const formValid = editingURI && !filePath ? isStillEditing && formValidLessFile : formValidLessFile;
|
||||||
|
|
||||||
(this: any).handleFileChange = this.handleFileChange.bind(this);
|
let submitLabel;
|
||||||
(this: any).checkIsFormValid = this.checkIsFormValid.bind(this);
|
if (isStillEditing) {
|
||||||
(this: any).renderFormErrors = this.renderFormErrors.bind(this);
|
submitLabel = !publishing ? __('Edit') : __('Editing...');
|
||||||
(this: any).handlePublish = this.handlePublish.bind(this);
|
} else {
|
||||||
(this: any).handleCancelPublish = this.handleCancelPublish.bind(this);
|
submitLabel = !publishing ? __('Publish') : __('Publishing...');
|
||||||
(this: any).handleNameChange = this.handleNameChange.bind(this);
|
|
||||||
(this: any).handleChannelChange = this.handleChannelChange.bind(this);
|
|
||||||
(this: any).editExistingClaim = this.editExistingClaim.bind(this);
|
|
||||||
(this: any).getNewUri = this.getNewUri.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
useEffect(() => {
|
||||||
const { thumbnail, name, channel, editingURI } = this.props;
|
|
||||||
if (!thumbnail) {
|
if (!thumbnail) {
|
||||||
this.props.resetThumbnailStatus();
|
resetThumbnailStatus();
|
||||||
}
|
}
|
||||||
if (editingURI) {
|
}, [thumbnail, resetThumbnailStatus]);
|
||||||
this.getNewUri(name, channel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getNewUri(name: string, channel: string) {
|
// Every time the channel or name changes, resolve the uris to find winning bid amounts
|
||||||
const { resolveUri } = this.props;
|
useEffect(() => {
|
||||||
// If they are midway through a channel creation, treat it as anonymous until it completes
|
// If they are midway through a channel creation, treat it as anonymous until it completes
|
||||||
const channelName = channel === CHANNEL_ANONYMOUS || channel === CHANNEL_NEW ? '' : channel;
|
const channelName = channel === CHANNEL_ANONYMOUS || channel === CHANNEL_NEW ? '' : channel;
|
||||||
|
|
||||||
|
@ -89,523 +102,78 @@ class PublishForm extends React.PureComponent<Props> {
|
||||||
let uri;
|
let uri;
|
||||||
try {
|
try {
|
||||||
uri = buildURI({ contentName: name, channelName });
|
uri = buildURI({ contentName: name, channelName });
|
||||||
} catch (e) {
|
} catch (e) {}
|
||||||
// something wrong with channel or name
|
|
||||||
|
if (channelName) {
|
||||||
|
// resolve without the channel name so we know the winning bid for it
|
||||||
|
const uriLessChannel = buildURI({ contentName: name });
|
||||||
|
resolveUri(uriLessChannel);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uri) {
|
if (uri) {
|
||||||
if (channelName) {
|
|
||||||
// resolve without the channel name so we know the winning bid for it
|
|
||||||
const uriLessChannel = buildURI({ contentName: name });
|
|
||||||
resolveUri(uriLessChannel);
|
|
||||||
}
|
|
||||||
resolveUri(uri);
|
resolveUri(uri);
|
||||||
return uri;
|
updatePublishForm({ uri });
|
||||||
}
|
}
|
||||||
|
}, [name, channel, resolveUri, updatePublishForm]);
|
||||||
|
|
||||||
return '';
|
return (
|
||||||
}
|
<Fragment>
|
||||||
|
<UnsupportedOnWeb />
|
||||||
|
|
||||||
handleFileChange(filePath: string, fileName: string) {
|
<PublishFile />
|
||||||
const { updatePublishForm, channel, name } = this.props;
|
<div className={classnames({ 'card--disabled': formDisabled })}>
|
||||||
const newFileParams: UpdatePublishFormData = { filePath };
|
<PublishText disabled={formDisabled} />
|
||||||
|
<div className="card card--section">
|
||||||
if (!name) {
|
{/* This should probably be PublishThumbnail */}
|
||||||
const parsedFileName = fileName.replace(regexInvalidURI, '');
|
<SelectThumbnail />
|
||||||
const uri = this.getNewUri(parsedFileName, channel);
|
|
||||||
newFileParams.name = parsedFileName;
|
|
||||||
newFileParams.uri = uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePublishForm(newFileParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleNameChange(name: ?string) {
|
|
||||||
const { channel, updatePublishForm } = this.props;
|
|
||||||
|
|
||||||
if (!name) {
|
|
||||||
updatePublishForm({ name: '', nameError: __('A name is required.') });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isNameValid(name, false)) {
|
|
||||||
updatePublishForm({
|
|
||||||
name,
|
|
||||||
nameError: __('LBRY names must contain only letters, numbers and dashes.'),
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const uri = this.getNewUri(name, channel);
|
|
||||||
updatePublishForm({
|
|
||||||
name,
|
|
||||||
uri,
|
|
||||||
nameError: undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
handleChannelChange(channelName: string) {
|
|
||||||
const { name, updatePublishForm } = this.props;
|
|
||||||
const form: UpdatePublishFormData = { channel: channelName };
|
|
||||||
|
|
||||||
if (name) {
|
|
||||||
form.uri = this.getNewUri(name, channelName);
|
|
||||||
}
|
|
||||||
updatePublishForm(form);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleBidChange(bid: number) {
|
|
||||||
const { balance, updatePublishForm, myClaimForUri } = this.props;
|
|
||||||
|
|
||||||
let previousBidAmount = 0;
|
|
||||||
if (myClaimForUri) {
|
|
||||||
previousBidAmount = Number(myClaimForUri.amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
const totalAvailableBidAmount = previousBidAmount + balance;
|
|
||||||
|
|
||||||
let bidError;
|
|
||||||
if (bid === 0) {
|
|
||||||
bidError = __('Deposit cannot be 0');
|
|
||||||
} else if (totalAvailableBidAmount === bid) {
|
|
||||||
bidError = __('Please decrease your deposit to account for transaction fees');
|
|
||||||
} else if (totalAvailableBidAmount < bid) {
|
|
||||||
bidError = __('Deposit cannot be higher than your balance');
|
|
||||||
} else if (bid <= MINIMUM_PUBLISH_BID) {
|
|
||||||
bidError = __('Your deposit must be higher');
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePublishForm({ bid, bidError });
|
|
||||||
}
|
|
||||||
|
|
||||||
editExistingClaim(myClaimForUri: ?{}, uri: string) {
|
|
||||||
const { prepareEdit, scrollToTop } = this.props;
|
|
||||||
if (myClaimForUri) {
|
|
||||||
prepareEdit(myClaimForUri, uri);
|
|
||||||
scrollToTop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCancelPublish() {
|
|
||||||
const { clearPublish, scrollToTop } = this.props;
|
|
||||||
scrollToTop();
|
|
||||||
clearPublish();
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePublish() {
|
|
||||||
const { filePath, licenseType, licenseUrl, otherLicenseDescription, publish } = this.props;
|
|
||||||
|
|
||||||
let publishingLicense;
|
|
||||||
switch (licenseType) {
|
|
||||||
case COPYRIGHT:
|
|
||||||
case OTHER:
|
|
||||||
publishingLicense = otherLicenseDescription;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
publishingLicense = licenseType;
|
|
||||||
}
|
|
||||||
|
|
||||||
const publishingLicenseUrl = licenseType === COPYRIGHT ? '' : licenseUrl;
|
|
||||||
|
|
||||||
const publishParams: PublishParams = {
|
|
||||||
filePath: filePath || undefined,
|
|
||||||
bid: this.props.bid || undefined,
|
|
||||||
title: this.props.title || '',
|
|
||||||
thumbnail: this.props.thumbnail,
|
|
||||||
description: this.props.description,
|
|
||||||
language: this.props.language,
|
|
||||||
nsfw: this.props.nsfw,
|
|
||||||
license: publishingLicense,
|
|
||||||
licenseUrl: publishingLicenseUrl,
|
|
||||||
otherLicenseDescription,
|
|
||||||
name: this.props.name || undefined,
|
|
||||||
contentIsFree: this.props.contentIsFree,
|
|
||||||
fee: this.props.fee,
|
|
||||||
uri: this.props.uri || undefined,
|
|
||||||
channel: this.props.channel,
|
|
||||||
isStillEditing: this.props.isStillEditing,
|
|
||||||
claim: this.props.myClaimForUri,
|
|
||||||
};
|
|
||||||
|
|
||||||
publish(publishParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
checkIsFormValid() {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
nameError,
|
|
||||||
title,
|
|
||||||
bid,
|
|
||||||
bidError,
|
|
||||||
editingURI,
|
|
||||||
isStillEditing,
|
|
||||||
filePath,
|
|
||||||
uploadThumbnailStatus,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
// If they are editing, they don't need a new file chosen
|
|
||||||
const formValidLessFile =
|
|
||||||
name && !nameError && title && bid && !bidError && !(uploadThumbnailStatus === THUMBNAIL_STATUSES.IN_PROGRESS);
|
|
||||||
return editingURI && !filePath ? isStillEditing && formValidLessFile : formValidLessFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
renderFormErrors() {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
nameError,
|
|
||||||
title,
|
|
||||||
bid,
|
|
||||||
bidError,
|
|
||||||
editingURI,
|
|
||||||
filePath,
|
|
||||||
isStillEditing,
|
|
||||||
uploadThumbnailStatus,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const isFormValid = this.checkIsFormValid();
|
|
||||||
|
|
||||||
// These are extra help
|
|
||||||
// If there is an error it will be presented as an inline error as well
|
|
||||||
return (
|
|
||||||
!isFormValid && (
|
|
||||||
<div className="card__content error-text">
|
|
||||||
{!title && <div>{__('A title is required')}</div>}
|
|
||||||
{!name && <div>{__('A URL is required')}</div>}
|
|
||||||
{name && nameError && <div>{__('The URL you created is not valid')}</div>}
|
|
||||||
{!bid && <div>{__('A deposit amount is required')}</div>}
|
|
||||||
{!!bid && bidError && <div>{bidError}</div>}
|
|
||||||
{uploadThumbnailStatus === THUMBNAIL_STATUSES.IN_PROGRESS && (
|
|
||||||
<div>{__('Please wait for thumbnail to finish uploading')}</div>
|
|
||||||
)}
|
|
||||||
{!!editingURI && !isStillEditing && !filePath && (
|
|
||||||
<div>{__('You need to reselect a file after changing the LBRY URL')}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
<div className="card">
|
||||||
);
|
<TagSelect
|
||||||
}
|
title={false}
|
||||||
|
help={__('The better your tags are, the easier it will be for people to discover your content.')}
|
||||||
render() {
|
empty={__('No tags added')}
|
||||||
const {
|
onSelect={tag => updatePublishForm({ tags: [...tags, tag] })}
|
||||||
filePath,
|
onRemove={clickedTag => {
|
||||||
editingURI,
|
const newTags = tags.slice().filter(tag => tag.name !== clickedTag.name);
|
||||||
title,
|
updatePublishForm({ tags: newTags });
|
||||||
thumbnail,
|
}}
|
||||||
uploadThumbnailStatus,
|
tagsChosen={tags}
|
||||||
description,
|
/>
|
||||||
language,
|
</div>
|
||||||
nsfw,
|
<section className="card card--section">
|
||||||
contentIsFree,
|
<div className="card__content">
|
||||||
fee,
|
<ChannelSection channel={channel} onChannelChange={channel => updatePublishForm({ channel })} />
|
||||||
channel,
|
<p className="help">
|
||||||
name,
|
{__('This is a username or handle that your content can be found under.')}{' '}
|
||||||
updatePublishForm,
|
{__('Ex. @Marvel, @TheBeatles, @BooksByJoe')}
|
||||||
bid,
|
</p>
|
||||||
nameError,
|
|
||||||
isResolvingUri,
|
|
||||||
winningBidForClaimUri,
|
|
||||||
myClaimForUri,
|
|
||||||
licenseType,
|
|
||||||
otherLicenseDescription,
|
|
||||||
licenseUrl,
|
|
||||||
uri,
|
|
||||||
bidError,
|
|
||||||
publishing,
|
|
||||||
clearPublish,
|
|
||||||
thumbnailPath,
|
|
||||||
resetThumbnailStatus,
|
|
||||||
isStillEditing,
|
|
||||||
amountNeededForTakeover,
|
|
||||||
balance,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const formDisabled = (!filePath && !editingURI) || publishing;
|
|
||||||
const formValid = this.checkIsFormValid();
|
|
||||||
|
|
||||||
let submitLabel;
|
|
||||||
if (isStillEditing) {
|
|
||||||
submitLabel = !publishing ? __('Edit') : __('Editing...');
|
|
||||||
} else {
|
|
||||||
submitLabel = !publishing ? __('Publish') : __('Publishing...');
|
|
||||||
}
|
|
||||||
|
|
||||||
const shortUri = buildURI({ contentName: name });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<React.Fragment>
|
|
||||||
{IS_WEB && <UnsupportedOnWeb />}
|
|
||||||
<Form onSubmit={this.handlePublish}>
|
|
||||||
<section
|
|
||||||
className={classnames('card card--section', {
|
|
||||||
'card--disabled': IS_WEB || publishing || balance === 0,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<header className="card__header">
|
|
||||||
<h2 className="card__title card__title--flex-between">
|
|
||||||
{__('Content')}
|
|
||||||
{(filePath || !!editingURI) && (
|
|
||||||
<Button button="inverse" icon={ICONS.CLOSE} label={__('Clear')} onClick={clearPublish} />
|
|
||||||
)}
|
|
||||||
</h2>
|
|
||||||
<p className="card__subtitle">
|
|
||||||
{isStillEditing ? __('You are currently editing a claim.') : __('What are you publishing?')}{' '}
|
|
||||||
{__('Read our')} <Button button="link" label={__('FAQ')} href="https://lbry.com/faq/how-to-publish" />{' '}
|
|
||||||
{__('to learn more.')}
|
|
||||||
</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div className="card__content">
|
|
||||||
<FileSelector currentPath={filePath} onFileChosen={this.handleFileChange} />
|
|
||||||
{!!isStillEditing && name && (
|
|
||||||
<p className="help">
|
|
||||||
{__("If you don't choose a file, the file from your existing claim")}
|
|
||||||
{` "${name}" `}
|
|
||||||
{__('will be used.')}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<div className={classnames({ 'card--disabled': formDisabled })}>
|
|
||||||
<section className="card card--section">
|
|
||||||
<div className="card__content">
|
|
||||||
<FormField
|
|
||||||
type="text"
|
|
||||||
name="content_title"
|
|
||||||
label={__('Title')}
|
|
||||||
placeholder={__('Titular Title')}
|
|
||||||
disabled={formDisabled}
|
|
||||||
value={title}
|
|
||||||
onChange={e => updatePublishForm({ title: e.target.value })}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
type="markdown"
|
|
||||||
name="content_description"
|
|
||||||
label={__('Description')}
|
|
||||||
placeholder={__('Description of your content')}
|
|
||||||
value={description}
|
|
||||||
disabled={formDisabled}
|
|
||||||
onChange={text => updatePublishForm({ description: text })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="card card--section">
|
|
||||||
<header className="card__header">
|
|
||||||
<h2 className="card__title">{__('Thumbnail')}</h2>
|
|
||||||
<p className="card__subtitle">
|
|
||||||
{(uploadThumbnailStatus === undefined && __('You should reselect your file to choose a thumbnail')) ||
|
|
||||||
(uploadThumbnailStatus === THUMBNAIL_STATUSES.API_DOWN ? (
|
|
||||||
__('Enter a URL for your thumbnail.')
|
|
||||||
) : (
|
|
||||||
<React.Fragment>
|
|
||||||
{__('Upload your thumbnail (.png/.jpg/.jpeg/.gif) to')}{' '}
|
|
||||||
<Button button="link" label={__('spee.ch')} href="https://spee.ch/about" />.{' '}
|
|
||||||
{__('Recommended size: 800x450 (16:9)')}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<SelectThumbnail
|
|
||||||
filePath={filePath}
|
|
||||||
thumbnailPath={thumbnailPath}
|
|
||||||
thumbnail={thumbnail}
|
|
||||||
uploadThumbnailStatus={uploadThumbnailStatus}
|
|
||||||
updatePublishForm={updatePublishForm}
|
|
||||||
formDisabled={formDisabled}
|
|
||||||
resetThumbnailStatus={resetThumbnailStatus}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="card card--section">
|
|
||||||
<header className="card__header">
|
|
||||||
<h2 className="card__title">{__('Price')}</h2>
|
|
||||||
<p className="card__subtitle">{__('How much will this content cost?')}</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div className="card__content">
|
|
||||||
<FormField
|
|
||||||
type="radio"
|
|
||||||
name="content_free"
|
|
||||||
label={__('Free')}
|
|
||||||
checked={contentIsFree}
|
|
||||||
disabled={formDisabled}
|
|
||||||
onChange={() => updatePublishForm({ contentIsFree: true })}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
type="radio"
|
|
||||||
name="content_cost"
|
|
||||||
label={__('Choose price')}
|
|
||||||
checked={!contentIsFree}
|
|
||||||
disabled={formDisabled}
|
|
||||||
onChange={() => updatePublishForm({ contentIsFree: false })}
|
|
||||||
/>
|
|
||||||
{!contentIsFree && (
|
|
||||||
<FormFieldPrice
|
|
||||||
name="content_cost_amount"
|
|
||||||
min="0"
|
|
||||||
price={fee}
|
|
||||||
onChange={newFee => updatePublishForm({ fee: newFee })}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{fee && fee.currency !== 'LBC' && (
|
|
||||||
<p className="form-field__help">
|
|
||||||
{__(
|
|
||||||
'All content fees are charged in LBC. For non-LBC payment methods, the number of credits charged will be adjusted based on the value of LBRY credits at the time of purchase.'
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="card card--section">
|
|
||||||
<header className="card__header">
|
|
||||||
<h2 className="card__title">{__('Anonymous or under a channel?')}</h2>
|
|
||||||
<p className="card__subtitle">
|
|
||||||
{__('This is a username or handle that your content can be found under.')}{' '}
|
|
||||||
{__('Ex. @Marvel, @TheBeatles, @BooksByJoe')}
|
|
||||||
</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div className="card__content">
|
|
||||||
<ChannelSection channel={channel} onChannelChange={this.handleChannelChange} />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="card card--section">
|
|
||||||
<header className="card__header">
|
|
||||||
<h2 className="card__title">{__('Where can people find this content?')}</h2>
|
|
||||||
<p className="card__subtitle">
|
|
||||||
{__('The LBRY URL is the exact address where people find your content (ex. lbry://myvideo).')}{' '}
|
|
||||||
<Button button="link" label={__('Learn more')} href="https://lbry.com/faq/naming" />
|
|
||||||
</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div className="card__content">
|
|
||||||
<fieldset-group class="fieldset-group--smushed fieldset-group--disabled-prefix">
|
|
||||||
<fieldset-section>
|
|
||||||
<label>{__('Name')}</label>
|
|
||||||
<span className="form-field__prefix">{`lbry://${
|
|
||||||
!channel || channel === CHANNEL_ANONYMOUS || channel === CHANNEL_NEW ? '' : `${channel}/`
|
|
||||||
}`}</span>
|
|
||||||
</fieldset-section>
|
|
||||||
<FormField
|
|
||||||
type="text"
|
|
||||||
name="content_name"
|
|
||||||
value={name}
|
|
||||||
onChange={event => this.handleNameChange(event.target.value)}
|
|
||||||
error={nameError}
|
|
||||||
/>
|
|
||||||
</fieldset-group>
|
|
||||||
<div className="form-field__help">
|
|
||||||
<NameHelpText
|
|
||||||
isStillEditing={isStillEditing}
|
|
||||||
uri={uri}
|
|
||||||
myClaimForUri={myClaimForUri}
|
|
||||||
onEditMyClaim={this.editExistingClaim}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={classnames('card__content', { 'card--disabled': !name })}>
|
|
||||||
<FormField
|
|
||||||
className="form-field--price-amount"
|
|
||||||
type="number"
|
|
||||||
name="content_bid"
|
|
||||||
step="any"
|
|
||||||
label={__('Deposit (LBC)')}
|
|
||||||
postfix="LBC"
|
|
||||||
value={bid}
|
|
||||||
error={bidError}
|
|
||||||
min="0"
|
|
||||||
disabled={!name}
|
|
||||||
onChange={event => this.handleBidChange(parseFloat(event.target.value))}
|
|
||||||
placeholder={winningBidForClaimUri ? winningBidForClaimUri + 0.1 : 0.1}
|
|
||||||
helper={
|
|
||||||
<BidHelpText
|
|
||||||
uri={shortUri}
|
|
||||||
isResolvingUri={isResolvingUri}
|
|
||||||
amountNeededForTakeover={amountNeededForTakeover}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="card card--section">
|
|
||||||
<div className="card__content">
|
|
||||||
<FormField
|
|
||||||
type="checkbox"
|
|
||||||
name="content_is_mature"
|
|
||||||
label={__('Mature audiences only')}
|
|
||||||
checked={nsfw}
|
|
||||||
onChange={() => updatePublishForm({ nsfw: !nsfw })}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
label={__('Language')}
|
|
||||||
type="select"
|
|
||||||
name="content_language"
|
|
||||||
value={language}
|
|
||||||
onChange={event => updatePublishForm({ language: event.target.value })}
|
|
||||||
>
|
|
||||||
<option value="en">{__('English')}</option>
|
|
||||||
<option value="zh">{__('Chinese')}</option>
|
|
||||||
<option value="fr">{__('French')}</option>
|
|
||||||
<option value="de">{__('German')}</option>
|
|
||||||
<option value="jp">{__('Japanese')}</option>
|
|
||||||
<option value="ru">{__('Russian')}</option>
|
|
||||||
<option value="es">{__('Spanish')}</option>
|
|
||||||
<option value="id">{__('Indonesian')}</option>
|
|
||||||
<option value="it">{__('Italian')}</option>
|
|
||||||
<option value="nl">{__('Dutch')}</option>
|
|
||||||
<option value="tr">{__('Turkish')}</option>
|
|
||||||
<option value="pl">{__('Polish')}</option>
|
|
||||||
<option value="ms">{__('Malay')}</option>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
<LicenseType
|
|
||||||
licenseType={licenseType}
|
|
||||||
otherLicenseDescription={otherLicenseDescription}
|
|
||||||
licenseUrl={licenseUrl}
|
|
||||||
handleLicenseChange={(newLicenseType, newLicenseUrl) =>
|
|
||||||
updatePublishForm({
|
|
||||||
licenseType: newLicenseType,
|
|
||||||
licenseUrl: newLicenseUrl,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
handleLicenseDescriptionChange={event =>
|
|
||||||
updatePublishForm({
|
|
||||||
otherLicenseDescription: event.target.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
handleLicenseUrlChange={event => updatePublishForm({ licenseUrl: event.target.value })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="card card--section">
|
|
||||||
<div className="card__actions">
|
|
||||||
<Submit
|
|
||||||
label={submitLabel}
|
|
||||||
disabled={formDisabled || !formValid || uploadThumbnailStatus === THUMBNAIL_STATUSES.IN_PROGRESS}
|
|
||||||
/>
|
|
||||||
<Button button="link" onClick={this.handleCancelPublish} label={__('Cancel')} />
|
|
||||||
</div>
|
|
||||||
<p className="help">
|
|
||||||
{__('By continuing, you accept the')}{' '}
|
|
||||||
<Button button="link" href="https://www.lbry.com/termsofservice" label={__('LBRY Terms of Service')} />.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
{!formDisabled && !formValid && this.renderFormErrors()}
|
<PublishName disabled={formDisabled} />
|
||||||
</Form>
|
<PublishPrice disabled={formDisabled} />
|
||||||
</React.Fragment>
|
<PublishAdditionalOptions disabled={formDisabled} />
|
||||||
);
|
|
||||||
}
|
<section className="card card--section">
|
||||||
|
{!formDisabled && !formValid && <PublishFormErrors />}
|
||||||
|
|
||||||
|
<div className="card__actions">
|
||||||
|
<Button
|
||||||
|
button="primary"
|
||||||
|
onClick={publish}
|
||||||
|
label={submitLabel}
|
||||||
|
disabled={formDisabled || !formValid || uploadThumbnailStatus === THUMBNAIL_STATUSES.IN_PROGRESS}
|
||||||
|
/>
|
||||||
|
<Button button="link" onClick={clearPublish} label={__('Cancel')} />
|
||||||
|
</div>
|
||||||
|
<p className="help">
|
||||||
|
{__('By continuing, you accept the')}{' '}
|
||||||
|
<Button button="link" href="https://www.lbry.com/termsofservice" label={__('LBRY Terms of Service')} />.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PublishForm;
|
export default PublishForm;
|
||||||
|
|
17
src/ui/component/publishFormErrors/index.js
Normal file
17
src/ui/component/publishFormErrors/index.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { makeSelectPublishFormValue, selectIsStillEditing } from 'redux/selectors/publish';
|
||||||
|
import PublishPage from './view';
|
||||||
|
|
||||||
|
const select = state => ({
|
||||||
|
name: makeSelectPublishFormValue('name')(state),
|
||||||
|
title: makeSelectPublishFormValue('title')(state),
|
||||||
|
bid: makeSelectPublishFormValue('bid')(state),
|
||||||
|
editingUri: makeSelectPublishFormValue('editingUri')(state),
|
||||||
|
uploadThumbnailStatus: makeSelectPublishFormValue('uploadThumbnailStatus')(state),
|
||||||
|
isStillEditing: selectIsStillEditing(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
select,
|
||||||
|
null
|
||||||
|
)(PublishPage);
|
35
src/ui/component/publishFormErrors/view.jsx
Normal file
35
src/ui/component/publishFormErrors/view.jsx
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import { THUMBNAIL_STATUSES } from 'lbry-redux';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title: ?string,
|
||||||
|
name: ?string,
|
||||||
|
bid: ?string,
|
||||||
|
editingURI: ?string,
|
||||||
|
filePath: ?string,
|
||||||
|
isStillEditing: boolean,
|
||||||
|
uploadThumbnailStatus: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
function PublishFormErrors(props: Props) {
|
||||||
|
const { name, title, bid, editingURI, filePath, isStillEditing, uploadThumbnailStatus } = props;
|
||||||
|
|
||||||
|
// These are extra help
|
||||||
|
// If there is an error it will be presented as an inline error as well
|
||||||
|
return (
|
||||||
|
<div className="card__content error-text">
|
||||||
|
{!title && <div>{__('A title is required')}</div>}
|
||||||
|
{!name && <div>{__('A URL is required')}</div>}
|
||||||
|
{!bid && <div>{__('A deposit amount is required')}</div>}
|
||||||
|
{uploadThumbnailStatus === THUMBNAIL_STATUSES.IN_PROGRESS && (
|
||||||
|
<div>{__('Please wait for thumbnail to finish uploading')}</div>
|
||||||
|
)}
|
||||||
|
{!!editingURI && !isStillEditing && !filePath && (
|
||||||
|
<div>{__('Please reselect a file after changing the LBRY URL')}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PublishFormErrors;
|
33
src/ui/component/publishName/bid-help-text.jsx
Normal file
33
src/ui/component/publishName/bid-help-text.jsx
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
// @flow
|
||||||
|
type Props = {
|
||||||
|
uri: ?string,
|
||||||
|
isResolvingUri: boolean,
|
||||||
|
amountNeededForTakeover: number,
|
||||||
|
};
|
||||||
|
|
||||||
|
function BidHelpText(props: Props) {
|
||||||
|
const { uri, isResolvingUri, amountNeededForTakeover } = props;
|
||||||
|
|
||||||
|
let bidHelpText;
|
||||||
|
|
||||||
|
if (uri) {
|
||||||
|
if (isResolvingUri) {
|
||||||
|
bidHelpText = __('Checking the winning claim amount...');
|
||||||
|
} else if (!amountNeededForTakeover) {
|
||||||
|
bidHelpText = __('Any amount will give you the winning bid.');
|
||||||
|
} else {
|
||||||
|
// @i18nfixme
|
||||||
|
bidHelpText = `${__('If you bid more than')} ${amountNeededForTakeover} LBC, ${__(
|
||||||
|
'when someone navigates to'
|
||||||
|
)} ${uri} ${__('it will load your published content')}. ${__(
|
||||||
|
'However, you can get a longer version of this URL for any bid'
|
||||||
|
)}.`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bidHelpText = __('This LBC remains yours and the deposit can be undone at any time.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return bidHelpText;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BidHelpText;
|
32
src/ui/component/publishName/index.js
Normal file
32
src/ui/component/publishName/index.js
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import {
|
||||||
|
makeSelectPublishFormValue,
|
||||||
|
selectIsStillEditing,
|
||||||
|
selectMyClaimForUri,
|
||||||
|
selectIsResolvingPublishUris,
|
||||||
|
selectTakeOverAmount,
|
||||||
|
} from 'redux/selectors/publish';
|
||||||
|
import { doUpdatePublishForm, doPrepareEdit } from 'redux/actions/publish';
|
||||||
|
import { selectBalance } from 'lbry-redux';
|
||||||
|
import PublishPage from './view';
|
||||||
|
|
||||||
|
const select = state => ({
|
||||||
|
name: makeSelectPublishFormValue('name')(state),
|
||||||
|
channel: makeSelectPublishFormValue('channel')(state),
|
||||||
|
uri: makeSelectPublishFormValue('uri')(state),
|
||||||
|
isStillEditing: selectIsStillEditing(state),
|
||||||
|
isResolvingUri: selectIsResolvingPublishUris(state),
|
||||||
|
amountNeededForTakeover: selectTakeOverAmount(state),
|
||||||
|
balance: selectBalance(state),
|
||||||
|
myClaimForUri: selectMyClaimForUri(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
const perform = dispatch => ({
|
||||||
|
updatePublishForm: value => dispatch(doUpdatePublishForm(value)),
|
||||||
|
prepareEdit: (claim, uri) => dispatch(doPrepareEdit(claim, uri)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
select,
|
||||||
|
perform
|
||||||
|
)(PublishPage);
|
46
src/ui/component/publishName/name-help-text.jsx
Normal file
46
src/ui/component/publishName/name-help-text.jsx
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
// @flow
|
||||||
|
import * as React from 'react';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import { buildURI } from 'lbry-redux';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
uri: ?string,
|
||||||
|
myClaimForUri: ?StreamClaim,
|
||||||
|
isStillEditing: boolean,
|
||||||
|
onEditMyClaim: (any, string) => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
function NameHelpText(props: Props) {
|
||||||
|
const { uri, myClaimForUri, onEditMyClaim, isStillEditing } = props;
|
||||||
|
|
||||||
|
let nameHelpText;
|
||||||
|
|
||||||
|
if (isStillEditing) {
|
||||||
|
nameHelpText = __('You are currently editing this claim. If you change the URL, you will need to reselect a file.');
|
||||||
|
} else if (uri && myClaimForUri) {
|
||||||
|
const editUri = buildURI({
|
||||||
|
contentName: myClaimForUri.name,
|
||||||
|
claimId: myClaimForUri.claim_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
nameHelpText = (
|
||||||
|
<React.Fragment>
|
||||||
|
{__('You already have a claim at')}
|
||||||
|
{` ${uri} `}
|
||||||
|
<Button button="link" label="Edit it" onClick={() => onEditMyClaim(myClaimForUri, editUri)} />
|
||||||
|
<br />
|
||||||
|
{__('Publishing will update your existing claim.')}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{nameHelpText || (
|
||||||
|
<span>{__('Create a URL for this content. Simpler names are easier to find and remember.')}</span>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NameHelpText;
|
124
src/ui/component/publishName/view.jsx
Normal file
124
src/ui/component/publishName/view.jsx
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
// @flow
|
||||||
|
import { CHANNEL_NEW, CHANNEL_ANONYMOUS, MINIMUM_PUBLISH_BID } from 'constants/claim';
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { isNameValid } from 'lbry-redux';
|
||||||
|
import { FormField } from 'component/common/form';
|
||||||
|
import NameHelpText from './name-help-text';
|
||||||
|
import BidHelpText from './bid-help-text';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
name: string,
|
||||||
|
channel: string,
|
||||||
|
uri: string,
|
||||||
|
bid: string,
|
||||||
|
balance: number,
|
||||||
|
isStillEditing: boolean,
|
||||||
|
myClaimForUri: ?StreamClaim,
|
||||||
|
isResolvingUri: boolean,
|
||||||
|
amountNeededForTakeover: number,
|
||||||
|
prepareEdit: ({}, string) => void,
|
||||||
|
updatePublishForm: ({}) => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
function PublishText(props: Props) {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
channel,
|
||||||
|
uri,
|
||||||
|
isStillEditing,
|
||||||
|
myClaimForUri,
|
||||||
|
bid: bidString,
|
||||||
|
isResolvingUri,
|
||||||
|
amountNeededForTakeover,
|
||||||
|
prepareEdit,
|
||||||
|
updatePublishForm,
|
||||||
|
balance,
|
||||||
|
} = props;
|
||||||
|
const [nameError, setNameError] = useState(undefined);
|
||||||
|
const [bidError, setBidError] = useState(undefined);
|
||||||
|
const previousBidAmount = myClaimForUri && Number(myClaimForUri.amount);
|
||||||
|
const bid = Number(bidString);
|
||||||
|
|
||||||
|
function editExistingClaim() {
|
||||||
|
if (myClaimForUri) {
|
||||||
|
prepareEdit(myClaimForUri, uri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let nameError;
|
||||||
|
if (!name) {
|
||||||
|
nameError = __('A name is required');
|
||||||
|
} else if (!isNameValid(name, false)) {
|
||||||
|
nameError = __('LBRY names cannot contain that symbol ($, #, @)');
|
||||||
|
}
|
||||||
|
|
||||||
|
setNameError(nameError);
|
||||||
|
}, [name]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const totalAvailableBidAmount = previousBidAmount + balance;
|
||||||
|
|
||||||
|
let bidError;
|
||||||
|
if (bid === 0) {
|
||||||
|
bidError = __('Deposit cannot be 0');
|
||||||
|
} else if (totalAvailableBidAmount === bid) {
|
||||||
|
bidError = __('Please decrease your deposit to account for transaction fees');
|
||||||
|
} else if (totalAvailableBidAmount < bid) {
|
||||||
|
bidError = __('Deposit cannot be higher than your balance');
|
||||||
|
} else if (bid <= MINIMUM_PUBLISH_BID) {
|
||||||
|
bidError = __('Your deposit must be higher');
|
||||||
|
}
|
||||||
|
|
||||||
|
setBidError(bidError);
|
||||||
|
}, [bid, previousBidAmount, balance]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="card card--section">
|
||||||
|
<div className="card__content">
|
||||||
|
<fieldset-group class="fieldset-group--smushed fieldset-group--disabled-prefix">
|
||||||
|
<fieldset-section>
|
||||||
|
<label>{__('Name')}</label>
|
||||||
|
<span className="form-field__prefix">{`lbry://${
|
||||||
|
!channel || channel === CHANNEL_ANONYMOUS || channel === CHANNEL_NEW ? '' : `${channel}/`
|
||||||
|
}`}</span>
|
||||||
|
</fieldset-section>
|
||||||
|
<FormField
|
||||||
|
type="text"
|
||||||
|
name="content_name"
|
||||||
|
value={name}
|
||||||
|
error={nameError}
|
||||||
|
onChange={event => updatePublishForm({ name: event.target.value })}
|
||||||
|
/>
|
||||||
|
</fieldset-group>
|
||||||
|
<div className="form-field__help">
|
||||||
|
<NameHelpText
|
||||||
|
uri={uri}
|
||||||
|
isStillEditing={isStillEditing}
|
||||||
|
myClaimForUri={myClaimForUri}
|
||||||
|
onEditMyClaim={editExistingClaim}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<FormField
|
||||||
|
type="number"
|
||||||
|
name="content_bid"
|
||||||
|
min="0"
|
||||||
|
step="any"
|
||||||
|
placeholder="0.123"
|
||||||
|
className="form-field--price-amount"
|
||||||
|
label={__('Deposit (LBC)')}
|
||||||
|
postfix="LBC"
|
||||||
|
value={bid}
|
||||||
|
error={bidError}
|
||||||
|
disabled={!name}
|
||||||
|
onChange={event => updatePublishForm({ bid: parseFloat(event.target.value) })}
|
||||||
|
helper={
|
||||||
|
<BidHelpText uri={uri} amountNeededForTakeover={amountNeededForTakeover} isResolvingUri={isResolvingUri} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PublishText;
|
13
src/ui/component/publishPrice/index.js
Normal file
13
src/ui/component/publishPrice/index.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { makeSelectPublishFormValue } from 'redux/selectors/publish';
|
||||||
|
import PublishPage from './view';
|
||||||
|
|
||||||
|
const select = state => ({
|
||||||
|
contentIsFree: makeSelectPublishFormValue('contentIsFree')(state),
|
||||||
|
fee: makeSelectPublishFormValue('fee')(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
select,
|
||||||
|
null
|
||||||
|
)(PublishPage);
|
55
src/ui/component/publishPrice/view.jsx
Normal file
55
src/ui/component/publishPrice/view.jsx
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import { FormField, FormFieldPrice } from 'component/common/form';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
contentIsFree: boolean,
|
||||||
|
fee: Fee,
|
||||||
|
disabled: boolean,
|
||||||
|
updatePublishForm: ({}) => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
function PublishText(props: Props) {
|
||||||
|
const { contentIsFree, fee, updatePublishForm, disabled } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="card card--section">
|
||||||
|
<div className="card__content">
|
||||||
|
<FormField
|
||||||
|
type="radio"
|
||||||
|
name="content_free"
|
||||||
|
label={__('Free')}
|
||||||
|
checked={contentIsFree}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={() => updatePublishForm({ contentIsFree: true })}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
type="radio"
|
||||||
|
name="content_cost"
|
||||||
|
label={__('Add a price to this file')}
|
||||||
|
checked={!contentIsFree}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={() => updatePublishForm({ contentIsFree: false })}
|
||||||
|
/>
|
||||||
|
{!contentIsFree && (
|
||||||
|
<FormFieldPrice
|
||||||
|
name="content_cost_amount"
|
||||||
|
min="0"
|
||||||
|
price={fee}
|
||||||
|
onChange={newFee => updatePublishForm({ fee: newFee })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{fee && fee.currency !== 'LBC' && (
|
||||||
|
<p className="form-field__help">
|
||||||
|
{__(
|
||||||
|
'All content fees are charged in LBC. For non-LBC payment methods, the number of credits charged will be adjusted based on the value of LBRY credits at the time of purchase.'
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PublishText;
|
18
src/ui/component/publishText/index.js
Normal file
18
src/ui/component/publishText/index.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { makeSelectPublishFormValue } from 'redux/selectors/publish';
|
||||||
|
import { doUpdatePublishForm } from 'redux/actions/publish';
|
||||||
|
import PublishPage from './view';
|
||||||
|
|
||||||
|
const select = state => ({
|
||||||
|
title: makeSelectPublishFormValue('title')(state),
|
||||||
|
description: makeSelectPublishFormValue('description')(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
const perform = dispatch => ({
|
||||||
|
updatePublishForm: value => dispatch(doUpdatePublishForm(value)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
select,
|
||||||
|
perform
|
||||||
|
)(PublishPage);
|
51
src/ui/component/publishText/view.jsx
Normal file
51
src/ui/component/publishText/view.jsx
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import { FormField } from 'component/common/form';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import usePersistedState from 'util/use-persisted-state';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title: ?string,
|
||||||
|
description: ?string,
|
||||||
|
disabled: boolean,
|
||||||
|
updatePublishForm: ({}) => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
function PublishText(props: Props) {
|
||||||
|
const { title, description, updatePublishForm, disabled } = props;
|
||||||
|
const [advancedEditor, setAdvancedEditor] = usePersistedState('publish-form-description-mode', false);
|
||||||
|
function toggleMarkdown() {
|
||||||
|
setAdvancedEditor(!advancedEditor);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="card card--section">
|
||||||
|
<div className="card__content">
|
||||||
|
<FormField
|
||||||
|
type="text"
|
||||||
|
name="content_title"
|
||||||
|
label={__('Title')}
|
||||||
|
placeholder={__('Titular Title')}
|
||||||
|
disabled={disabled}
|
||||||
|
value={title}
|
||||||
|
onChange={e => updatePublishForm({ title: e.target.value })}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
type={advancedEditor ? 'markdown' : 'textarea'}
|
||||||
|
name="content_description"
|
||||||
|
label={__('Description')}
|
||||||
|
placeholder={__('My description for this and that')}
|
||||||
|
value={description}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={value => updatePublishForm({ description: advancedEditor ? value : value.target.text })}
|
||||||
|
/>
|
||||||
|
<div className="card__actions">
|
||||||
|
<Button button="link" onClick={toggleMarkdown} label={advancedEditor ? 'Simple Editor' : 'Advanced Editor'} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PublishText;
|
|
@ -20,7 +20,7 @@ import UserHistoryPage from 'page/userHistory';
|
||||||
import WalletPage from 'page/wallet';
|
import WalletPage from 'page/wallet';
|
||||||
import NavigationHistory from 'page/navigationHistory';
|
import NavigationHistory from 'page/navigationHistory';
|
||||||
import TagsPage from 'page/tags';
|
import TagsPage from 'page/tags';
|
||||||
import TagsEditPage from 'page/tagsEdit';
|
import FollowingPage from 'page/following';
|
||||||
|
|
||||||
const Scroll = withRouter(function ScrollWrapper(props) {
|
const Scroll = withRouter(function ScrollWrapper(props) {
|
||||||
const { pathname } = props.location;
|
const { pathname } = props.location;
|
||||||
|
@ -50,17 +50,17 @@ export default function AppRouter() {
|
||||||
<Route path={`/$/${PAGES.REWARDS}`} exact component={RewardsPage} />
|
<Route path={`/$/${PAGES.REWARDS}`} exact component={RewardsPage} />
|
||||||
<Route path={`/$/${PAGES.SEARCH}`} exact component={SearchPage} />
|
<Route path={`/$/${PAGES.SEARCH}`} exact component={SearchPage} />
|
||||||
<Route path={`/$/${PAGES.SETTINGS}`} exact component={SettingsPage} />
|
<Route path={`/$/${PAGES.SETTINGS}`} exact component={SettingsPage} />
|
||||||
<Route path={`/$/${PAGES.SUBSCRIPTIONS}`} exact component={SubscriptionsPage} />
|
|
||||||
<Route path={`/$/${PAGES.TRANSACTIONS}`} exact component={TransactionHistoryPage} />
|
<Route path={`/$/${PAGES.TRANSACTIONS}`} exact component={TransactionHistoryPage} />
|
||||||
<Route path={`/$/${PAGES.LIBRARY}`} exact component={UserHistoryPage} />
|
<Route path={`/$/${PAGES.LIBRARY}`} exact component={UserHistoryPage} />
|
||||||
<Route path={`/$/${PAGES.ACCOUNT}`} exact component={AccountPage} />
|
<Route path={`/$/${PAGES.ACCOUNT}`} exact component={AccountPage} />
|
||||||
<Route path={`/$/${PAGES.LIBRARY}/all`} exact component={NavigationHistory} />
|
<Route path={`/$/${PAGES.LIBRARY}/all`} exact component={NavigationHistory} />
|
||||||
<Route path={`/$/${PAGES.TAGS}`} exact component={TagsPage} />
|
<Route path={`/$/${PAGES.TAGS}`} exact component={TagsPage} />
|
||||||
<Route path={`/$/${PAGES.TAGS}/edit`} exact component={TagsEditPage} />
|
<Route path={`/$/${PAGES.FOLLOWING}`} exact component={SubscriptionsPage} />
|
||||||
|
<Route path={`/$/${PAGES.FOLLOWING}/edit`} exact component={FollowingPage} />
|
||||||
<Route path={`/$/${PAGES.WALLET}`} exact component={WalletPage} />
|
<Route path={`/$/${PAGES.WALLET}`} exact component={WalletPage} />
|
||||||
{/* Below need to go at the end to make sure we don't match any of our pages first */}
|
{/* Below need to go at the end to make sure we don't match any of our pages first */}
|
||||||
<Route path="/:claimName" exact component={ShowPage} />
|
<Route path="/:claimName" exact component={ShowPage} />
|
||||||
<Route path="/:claimName/:claimId" exact component={ShowPage} />
|
<Route path="/:claimName/:contentName" exact component={ShowPage} />
|
||||||
|
|
||||||
{/* Route not found. Mostly for people typing crazy urls into the url */}
|
{/* Route not found. Mostly for people typing crazy urls into the url */}
|
||||||
<Route render={() => <Redirect to="/" />} />
|
<Route render={() => <Redirect to="/" />} />
|
||||||
|
|
|
@ -1,12 +1,47 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import { doResolveUri } from 'lbry-redux';
|
||||||
|
import {
|
||||||
|
selectPublishFormValues,
|
||||||
|
selectIsStillEditing,
|
||||||
|
selectMyClaimForUri,
|
||||||
|
selectIsResolvingPublishUris,
|
||||||
|
selectTakeOverAmount,
|
||||||
|
} from 'redux/selectors/publish';
|
||||||
|
import {
|
||||||
|
doResetThumbnailStatus,
|
||||||
|
doClearPublish,
|
||||||
|
doUpdatePublishForm,
|
||||||
|
doPublish,
|
||||||
|
doPrepareEdit,
|
||||||
|
} from 'redux/actions/publish';
|
||||||
import { doOpenModal } from 'redux/actions/app';
|
import { doOpenModal } from 'redux/actions/app';
|
||||||
import SelectThumbnail from './view';
|
import { selectUnclaimedRewardValue } from 'lbryinc';
|
||||||
|
import PublishPage from './view';
|
||||||
|
|
||||||
|
const select = state => ({
|
||||||
|
...selectPublishFormValues(state),
|
||||||
|
// The winning claim for a short lbry uri
|
||||||
|
amountNeededForTakeover: selectTakeOverAmount(state),
|
||||||
|
// My previously published claims under this short lbry uri
|
||||||
|
myClaimForUri: selectMyClaimForUri(state),
|
||||||
|
// If I clicked the "edit" button, have I changed the uri?
|
||||||
|
// Need this to make it easier to find the source on previously published content
|
||||||
|
isStillEditing: selectIsStillEditing(state),
|
||||||
|
isResolvingUri: selectIsResolvingPublishUris(state),
|
||||||
|
totalRewardValue: selectUnclaimedRewardValue(state),
|
||||||
|
});
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = dispatch => ({
|
||||||
|
updatePublishForm: value => dispatch(doUpdatePublishForm(value)),
|
||||||
|
clearPublish: () => dispatch(doClearPublish()),
|
||||||
|
resolveUri: uri => dispatch(doResolveUri(uri)),
|
||||||
|
publish: params => dispatch(doPublish(params)),
|
||||||
|
prepareEdit: (claim, uri) => dispatch(doPrepareEdit(claim, uri)),
|
||||||
|
resetThumbnailStatus: () => dispatch(doResetThumbnailStatus()),
|
||||||
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
null,
|
select,
|
||||||
perform
|
perform
|
||||||
)(SelectThumbnail);
|
)(PublishPage);
|
||||||
|
|
|
@ -122,7 +122,8 @@ class SelectThumbnail extends React.PureComponent<Props, State> {
|
||||||
{status === THUMBNAIL_STATUSES.READY && (
|
{status === THUMBNAIL_STATUSES.READY && (
|
||||||
<FileSelector
|
<FileSelector
|
||||||
currentPath={thumbnailPath}
|
currentPath={thumbnailPath}
|
||||||
fileLabel={__('Choose Thumbnail')}
|
label={__('Thumbnail')}
|
||||||
|
placeholder={__('Choose a thumbnail')}
|
||||||
filters={filters}
|
filters={filters}
|
||||||
onFileChosen={path => openModal(MODALS.CONFIRM_THUMBNAIL_UPLOAD, { path })}
|
onFileChosen={path => openModal(MODALS.CONFIRM_THUMBNAIL_UPLOAD, { path })}
|
||||||
/>
|
/>
|
||||||
|
@ -160,6 +161,18 @@ class SelectThumbnail extends React.PureComponent<Props, State> {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{status === THUMBNAIL_STATUSES.IN_PROGRESS && <p>{__('Uploading thumbnail')}...</p>}
|
{status === THUMBNAIL_STATUSES.IN_PROGRESS && <p>{__('Uploading thumbnail')}...</p>}
|
||||||
|
<p className="help">
|
||||||
|
{(status === undefined && __('You should reselect your file to choose a thumbnail')) ||
|
||||||
|
(status === THUMBNAIL_STATUSES.API_DOWN ? (
|
||||||
|
__('Enter a URL for your thumbnail.')
|
||||||
|
) : (
|
||||||
|
<React.Fragment>
|
||||||
|
{__('Upload your thumbnail to')}{' '}
|
||||||
|
<Button button="link" label={__('spee.ch')} href="https://spee.ch/about" />.{' '}
|
||||||
|
{__('Recommended size: 800x450 (16:9)')}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,23 +34,23 @@ function SideBar(props: Props) {
|
||||||
...buildLink(null, __('Home'), ICONS.HOME),
|
...buildLink(null, __('Home'), ICONS.HOME),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...buildLink(PAGES.SUBSCRIPTIONS, __('Subscriptions'), ICONS.SUBSCRIPTION),
|
...buildLink(PAGES.FOLLOWING, __('Following'), ICONS.SUBSCRIPTION),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...buildLink(PAGES.LIBRARY, __('Library'), ICONS.LIBRARY),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...buildLink(PAGES.PUBLISHED, __('Publishes'), ICONS.PUBLISHED),
|
...buildLink(PAGES.PUBLISHED, __('Publishes'), ICONS.PUBLISHED),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
...buildLink(PAGES.LIBRARY, __('Library'), ICONS.DOWNLOAD),
|
|
||||||
},
|
|
||||||
].map(renderLink)}
|
].map(renderLink)}
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<Button
|
<Button
|
||||||
navigate="/$/tags/edit"
|
navigate="/$/following/edit"
|
||||||
icon={ICONS.EDIT}
|
icon={ICONS.EDIT}
|
||||||
className="navigation__link"
|
className="navigation__link"
|
||||||
activeClass="navigation__link--active"
|
activeClass="navigation__link--active"
|
||||||
label={__('Following')}
|
label={__('Edit')}
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -3,7 +3,7 @@ import * as ICONS from 'constants/icons';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import CopyableText from 'component/copyableText';
|
import CopyableText from 'component/copyableText';
|
||||||
import ToolTip from 'component/common/tooltip';
|
import Tooltip from 'component/common/tooltip';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
claim: StreamClaim,
|
claim: StreamClaim,
|
||||||
|
@ -73,7 +73,7 @@ class SocialShare extends React.PureComponent<Props> {
|
||||||
<label className="help">{__('Web link')}</label>
|
<label className="help">{__('Web link')}</label>
|
||||||
<CopyableText copyable={speechURL} />
|
<CopyableText copyable={speechURL} />
|
||||||
<div className="card__actions card__actions--center">
|
<div className="card__actions card__actions--center">
|
||||||
<ToolTip onComponent body={__('Facebook')}>
|
<Tooltip label={__('Facebook')}>
|
||||||
<Button
|
<Button
|
||||||
iconColor="blue"
|
iconColor="blue"
|
||||||
icon={ICONS.FACEBOOK}
|
icon={ICONS.FACEBOOK}
|
||||||
|
@ -81,8 +81,8 @@ class SocialShare extends React.PureComponent<Props> {
|
||||||
label={__('')}
|
label={__('')}
|
||||||
href={`https://facebook.com/sharer/sharer.php?u=${encodedSpeechURL}`}
|
href={`https://facebook.com/sharer/sharer.php?u=${encodedSpeechURL}`}
|
||||||
/>
|
/>
|
||||||
</ToolTip>
|
</Tooltip>
|
||||||
<ToolTip onComponent body={__('Twitter')}>
|
<Tooltip label={__('Twitter')}>
|
||||||
<Button
|
<Button
|
||||||
iconColor="blue"
|
iconColor="blue"
|
||||||
icon={ICONS.TWITTER}
|
icon={ICONS.TWITTER}
|
||||||
|
@ -90,10 +90,10 @@ class SocialShare extends React.PureComponent<Props> {
|
||||||
label={__('')}
|
label={__('')}
|
||||||
href={`https://twitter.com/home?status=${encodedSpeechURL}`}
|
href={`https://twitter.com/home?status=${encodedSpeechURL}`}
|
||||||
/>
|
/>
|
||||||
</ToolTip>
|
</Tooltip>
|
||||||
<ToolTip onComponent body={__('View on Spee.ch')}>
|
<Tooltip label={__('View on Spee.ch')}>
|
||||||
<Button icon={ICONS.WEB} iconColor="blue" button="alt" label={__('')} href={`${speechURL}`} />
|
<Button icon={ICONS.WEB} iconColor="blue" button="alt" label={__('')} href={`${speechURL}`} />
|
||||||
</ToolTip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -101,7 +101,7 @@ class SocialShare extends React.PureComponent<Props> {
|
||||||
<label className="help">{__('LBRY App link')}</label>
|
<label className="help">{__('LBRY App link')}</label>
|
||||||
<CopyableText copyable={lbryURL} noSnackbar />
|
<CopyableText copyable={lbryURL} noSnackbar />
|
||||||
<div className="card__actions card__actions--center">
|
<div className="card__actions card__actions--center">
|
||||||
<ToolTip onComponent body={__('Facebook')}>
|
<Tooltip label={__('Facebook')}>
|
||||||
<Button
|
<Button
|
||||||
iconColor="blue"
|
iconColor="blue"
|
||||||
icon={ICONS.FACEBOOK}
|
icon={ICONS.FACEBOOK}
|
||||||
|
@ -109,8 +109,8 @@ class SocialShare extends React.PureComponent<Props> {
|
||||||
label={__('')}
|
label={__('')}
|
||||||
href={`https://facebook.com/sharer/sharer.php?u=${encodedLbryURL}`}
|
href={`https://facebook.com/sharer/sharer.php?u=${encodedLbryURL}`}
|
||||||
/>
|
/>
|
||||||
</ToolTip>
|
</Tooltip>
|
||||||
<ToolTip onComponent body={__('Twitter')}>
|
<Tooltip label={__('Twitter')}>
|
||||||
<Button
|
<Button
|
||||||
iconColor="blue"
|
iconColor="blue"
|
||||||
icon={ICONS.TWITTER}
|
icon={ICONS.TWITTER}
|
||||||
|
@ -118,7 +118,7 @@ class SocialShare extends React.PureComponent<Props> {
|
||||||
label={__('')}
|
label={__('')}
|
||||||
href={`https://twitter.com/home?status=${encodedLbryURL}`}
|
href={`https://twitter.com/home?status=${encodedLbryURL}`}
|
||||||
/>
|
/>
|
||||||
</ToolTip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="card__actions">
|
<div className="card__actions">
|
||||||
|
|
|
@ -34,7 +34,7 @@ export default function SubscribeButton(props: Props) {
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const subscriptionHandler = isSubscribed ? doChannelUnsubscribe : doChannelSubscribe;
|
const subscriptionHandler = isSubscribed ? doChannelUnsubscribe : doChannelSubscribe;
|
||||||
const subscriptionLabel = isSubscribed ? __('Subscribed') : __('Subscribe');
|
const subscriptionLabel = isSubscribed ? __('Following') : __('Follow');
|
||||||
|
|
||||||
const { claimName } = parseURI(uri);
|
const { claimName } = parseURI(uri);
|
||||||
|
|
||||||
|
@ -57,7 +57,7 @@ export default function SubscribeButton(props: Props) {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (showSnackBarOnSubscribe) {
|
if (showSnackBarOnSubscribe) {
|
||||||
doToast({ message: `${__('Successfully subscribed to')} ${claimName}!` });
|
doToast({ message: `${__('Now following ')} ${claimName}!` });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -26,7 +26,7 @@ export default function Tag(props: Props) {
|
||||||
})}
|
})}
|
||||||
label={name}
|
label={name}
|
||||||
iconSize={12}
|
iconSize={12}
|
||||||
iconRight={type !== 'link' && (type === 'remove' ? ICONS.CLOSE : ICONS.ADD)}
|
iconRight={type !== 'link' && (type === 'remove' ? ICONS.REMOVE : ICONS.ADD)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,33 +15,48 @@ type Props = {
|
||||||
followedTags: Array<Tag>,
|
followedTags: Array<Tag>,
|
||||||
doToggleTagFollow: string => void,
|
doToggleTagFollow: string => void,
|
||||||
doAddTag: string => void,
|
doAddTag: string => void,
|
||||||
|
onSelect?: Tag => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function TagSelect(props: Props) {
|
export default function TagSelect(props: Props) {
|
||||||
const { unfollowedTags, followedTags, doToggleTagFollow, doAddTag } = props;
|
const { unfollowedTags, followedTags, doToggleTagFollow, doAddTag, onSelect } = props;
|
||||||
const [newTag, setNewTag] = useState('');
|
const [newTag, setNewTag] = useState('');
|
||||||
|
|
||||||
let tags = unfollowedTags.slice();
|
let tags = unfollowedTags.slice();
|
||||||
if (newTag) {
|
if (newTag) {
|
||||||
tags = [{ name: newTag }, ...tags];
|
tags.unshift({ name: newTag });
|
||||||
}
|
}
|
||||||
const suggestedTags = tags
|
|
||||||
.filter(({ name }) => (newTag ? name.toLowerCase().includes(newTag.toLowerCase()) : true))
|
const doesTagMatch = ({ name }) => (newTag ? name.toLowerCase().includes(newTag.toLowerCase()) : true);
|
||||||
.slice(0, 5);
|
const suggestedTags = tags.filter(doesTagMatch).slice(0, 5);
|
||||||
const suggestedTransitions = useTransition(suggestedTags, tag => tag.name, unfollowedTagsAnimation);
|
const suggestedTransitions = useTransition(suggestedTags, tag => tag.name, unfollowedTagsAnimation);
|
||||||
|
|
||||||
function onChange(e) {
|
function onChange(e) {
|
||||||
setNewTag(e.target.value);
|
setNewTag(e.target.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSubmit() {
|
function handleSubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
setNewTag('');
|
setNewTag('');
|
||||||
|
|
||||||
if (!unfollowedTags.includes(newTag)) {
|
if (onSelect) {
|
||||||
doAddTag(newTag);
|
onSelect({ name: newTag });
|
||||||
}
|
} else {
|
||||||
|
if (!unfollowedTags.includes(newTag)) {
|
||||||
|
doAddTag(newTag);
|
||||||
|
}
|
||||||
|
|
||||||
if (!followedTags.includes(newTag)) {
|
if (!followedTags.includes(newTag)) {
|
||||||
doToggleTagFollow(newTag);
|
doToggleTagFollow(newTag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTagClick(tag) {
|
||||||
|
if (onSelect) {
|
||||||
|
onSelect(tag);
|
||||||
|
} else {
|
||||||
|
doToggleTagFollow(tag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,7 +74,7 @@ export default function TagSelect(props: Props) {
|
||||||
<ul className="tags">
|
<ul className="tags">
|
||||||
{suggestedTransitions.map(({ item, key, props }) => (
|
{suggestedTransitions.map(({ item, key, props }) => (
|
||||||
<animated.li key={key} style={props}>
|
<animated.li key={key} style={props}>
|
||||||
<Tag name={item.name} type="add" onClick={() => doToggleTagFollow(item.name)} />
|
<Tag name={item.name} type="add" onClick={() => handleTagClick(item)} />
|
||||||
</animated.li>
|
</animated.li>
|
||||||
))}
|
))}
|
||||||
{!suggestedTransitions.length && <p className="empty tags__empty-message">No suggested tags</p>}
|
{!suggestedTransitions.length && <p className="empty tags__empty-message">No suggested tags</p>}
|
||||||
|
|
|
@ -8,10 +8,18 @@ import usePersistedState from 'util/use-persisted-state';
|
||||||
import { useTransition, animated } from 'react-spring';
|
import { useTransition, animated } from 'react-spring';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
followedTags: Array<Tag>,
|
|
||||||
showClose: boolean,
|
showClose: boolean,
|
||||||
title: string,
|
followedTags: Array<Tag>,
|
||||||
doDeleteTag: string => void,
|
doToggleTagFollow: string => void,
|
||||||
|
|
||||||
|
// Ovverides
|
||||||
|
// The default component is for following tags
|
||||||
|
title?: string,
|
||||||
|
help?: string,
|
||||||
|
empty?: string,
|
||||||
|
tagsChosen?: Array<Tag>,
|
||||||
|
onSelect?: Tag => void,
|
||||||
|
onRemove?: Tag => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
const tagsAnimation = {
|
const tagsAnimation = {
|
||||||
|
@ -21,23 +29,32 @@ const tagsAnimation = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function TagSelect(props: Props) {
|
export default function TagSelect(props: Props) {
|
||||||
const { title, followedTags, showClose, doDeleteTag } = props;
|
const { showClose, followedTags, doToggleTagFollow, title, help, empty, tagsChosen, onSelect, onRemove } = props;
|
||||||
const [hasClosed, setHasClosed] = usePersistedState('tag-select:has-closed', false);
|
const [hasClosed, setHasClosed] = usePersistedState('tag-select:has-closed', false);
|
||||||
|
const tagsToDisplay = tagsChosen || followedTags;
|
||||||
|
const transitions = useTransition(tagsToDisplay, tag => tag.name, tagsAnimation);
|
||||||
|
|
||||||
function handleClose() {
|
function handleClose() {
|
||||||
setHasClosed(true);
|
setHasClosed(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const transitions = useTransition(followedTags.map(tag => tag), tag => tag.name, tagsAnimation);
|
function handleTagClick(tag) {
|
||||||
|
if (onRemove) {
|
||||||
|
onRemove(tag);
|
||||||
|
} else {
|
||||||
|
doToggleTagFollow(tag.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
((showClose && !hasClosed) || !showClose) && (
|
((showClose && !hasClosed) || !showClose) && (
|
||||||
<div className="card--section">
|
<div className="card--section">
|
||||||
<h2 className="card__title card__title--flex-between">
|
{title !== false && (
|
||||||
{title}
|
<h2 className="card__title card__title--flex-between">
|
||||||
{showClose && !hasClosed && <Button button="close" icon={ICONS.CLOSE} onClick={handleClose} />}
|
{title}
|
||||||
</h2>
|
{showClose && !hasClosed && <Button button="close" icon={ICONS.REMOVE} onClick={handleClose} />}
|
||||||
<p className="help">{__("The tags you follow will change what's trending for you.")}</p>
|
</h2>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="card__content">
|
<div className="card__content">
|
||||||
<ul className="tags--remove">
|
<ul className="tags--remove">
|
||||||
|
@ -47,16 +64,19 @@ export default function TagSelect(props: Props) {
|
||||||
name={item.name}
|
name={item.name}
|
||||||
type="remove"
|
type="remove"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
doDeleteTag(item.name);
|
handleTagClick(item);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</animated.li>
|
</animated.li>
|
||||||
))}
|
))}
|
||||||
{!transitions.length && (
|
{!transitions.length && (
|
||||||
<div className="card__subtitle">{__("You aren't following any tags, try searching for one.")}</div>
|
<div className="empty">{empty || __("You aren't following any tags, try searching for one.")}</div>
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
<TagsSearch />
|
<TagsSearch onSelect={onSelect} />
|
||||||
|
{help !== false && (
|
||||||
|
<p className="help">{help || __("The tags you follow will change what's trending for you.")}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import { buildURI } from 'lbry-redux';
|
import { buildURI } from 'lbry-redux';
|
||||||
|
import Tooltip from 'component/common/tooltip';
|
||||||
|
import ClaimPreview from 'component/claimPreview';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isResolvingUri: boolean,
|
isResolvingUri: boolean,
|
||||||
|
@ -61,7 +63,7 @@ class UriIndicator extends React.PureComponent<Props> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button className="button--uri-indicator" navigate={channelLink}>
|
<Button className="button--uri-indicator" navigate={channelLink}>
|
||||||
{inner}
|
<Tooltip label={<ClaimPreview uri={channelLink} type="small" />}>{inner}</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -43,7 +43,9 @@ function UserEmail(props: Props) {
|
||||||
readOnly
|
readOnly
|
||||||
label={__('Your Email')}
|
label={__('Your Email')}
|
||||||
value={email}
|
value={email}
|
||||||
inputButton={<Button button="inverse" label={__('Change')} />}
|
inputButton={
|
||||||
|
<Button button="inverse" label={__('Change')} href="https://lbry.com/faq/how-to-change-email" />
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<p className="help">
|
<p className="help">
|
||||||
|
|
|
@ -1,296 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import * as ICONS from 'constants/icons';
|
|
||||||
import Button from 'component/button';
|
|
||||||
import Tooltip from 'component/common/tooltip';
|
|
||||||
import { stopContextMenu } from 'util/context-menu';
|
|
||||||
import butterchurn from 'butterchurn';
|
|
||||||
import detectButterchurnSupport from 'butterchurn/lib/isSupported.min';
|
|
||||||
import butterchurnPresets from 'butterchurn-presets';
|
|
||||||
import jsmediatags from 'jsmediatags/dist/jsmediatags';
|
|
||||||
import WaveSurfer from 'wavesurfer.js';
|
|
||||||
|
|
||||||
import styles from './audioViewer.module.scss';
|
|
||||||
|
|
||||||
const isButterchurnSupported = detectButterchurnSupport();
|
|
||||||
|
|
||||||
const EQ_BANDS_SIMPLE = [55, 150, 250, 400, 500, 1000, 2000, 4000, 8000, 16000];
|
|
||||||
/*
|
|
||||||
const EQ_LOWSHELF = EQ_BANDS_SIMPLE.shift();
|
|
||||||
const EQ_HIGHSHELF = EQ_BANDS_SIMPLE.pop();
|
|
||||||
|
|
||||||
const eqFilters = EQ.map(function(band) {
|
|
||||||
var filter = wavesurfer.backend.ac.createBiquadFilter();
|
|
||||||
filter.type = 'peaking';
|
|
||||||
filter.gain.value = 0;
|
|
||||||
filter.Q.value = 1;
|
|
||||||
filter.frequency.value = band.f;
|
|
||||||
return filter;
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
|
|
||||||
// type Props = {
|
|
||||||
// source: {
|
|
||||||
// downloadPath: string,
|
|
||||||
// fileName: string,
|
|
||||||
// },
|
|
||||||
// contentType: string,
|
|
||||||
// poster?: string,
|
|
||||||
// claim: StreamClaim,
|
|
||||||
// };
|
|
||||||
|
|
||||||
const presets = [
|
|
||||||
require('butterchurn-presets/presets/converted/Flexi - when monopolies were the future [simple warp + non-reactive moebius].json'),
|
|
||||||
require('butterchurn-presets/presets/converted/Rovastar & Loadus - FractalDrop (Active Sparks Mix).json'),
|
|
||||||
require('butterchurn-presets/presets/converted/shifter - tumbling cubes (ripples).json'),
|
|
||||||
require('butterchurn-presets/presets/converted/ORB - Blue Emotion.json'),
|
|
||||||
require('butterchurn-presets/presets/converted/shifter - urchin mod.json'),
|
|
||||||
require('butterchurn-presets/presets/converted/Stahlregen & fishbrain + flexi + geiss - The Machine that conquered the Aether.json'),
|
|
||||||
require('butterchurn-presets/presets/converted/Zylot - Crosshair Dimension (Light of Ages).json'),
|
|
||||||
];
|
|
||||||
|
|
||||||
class AudioVideoViewer extends React.PureComponent {
|
|
||||||
// audioNode: ?HTMLAudioElement;
|
|
||||||
// player: ?{ dispose: () => void };
|
|
||||||
|
|
||||||
state = {
|
|
||||||
playing: false,
|
|
||||||
enableMilkdrop: isButterchurnSupported,
|
|
||||||
showEqualizer: false,
|
|
||||||
showSongDetails: true,
|
|
||||||
enableArt: true,
|
|
||||||
artLoaded: false,
|
|
||||||
artist: null,
|
|
||||||
title: null,
|
|
||||||
album: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const me = this;
|
|
||||||
const { contentType, poster, claim } = me.props;
|
|
||||||
|
|
||||||
const path = `https://api.lbry.tv/content/claims/${claim.name}/${claim.claim_id}/stream.mp4`;
|
|
||||||
const sources = [
|
|
||||||
{
|
|
||||||
src: path,
|
|
||||||
type: contentType,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const audioNode = this.audioNode;
|
|
||||||
|
|
||||||
audioNode.crossOrigin = 'anonymous';
|
|
||||||
|
|
||||||
const canvasHeight = me.canvasNode.offsetHeight;
|
|
||||||
const canvasWidth = me.canvasNode.offsetWidth;
|
|
||||||
|
|
||||||
// Required for canvas, nuance of rendering
|
|
||||||
me.canvasNode.height = canvasHeight;
|
|
||||||
me.canvasNode.width = canvasWidth;
|
|
||||||
|
|
||||||
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
|
||||||
const audioContext = new AudioContext();
|
|
||||||
|
|
||||||
const audioSource = audioContext.createMediaElementSource(audioNode);
|
|
||||||
audioSource.connect(audioContext.destination);
|
|
||||||
|
|
||||||
if (isButterchurnSupported) {
|
|
||||||
const visualizer = (me.visualizer = butterchurn.createVisualizer(audioContext, me.canvasNode, {
|
|
||||||
height: canvasHeight,
|
|
||||||
width: canvasWidth,
|
|
||||||
pixelRatio: window.devicePixelRatio || 1,
|
|
||||||
textureRatio: 1,
|
|
||||||
}));
|
|
||||||
|
|
||||||
visualizer.connectAudio(audioSource);
|
|
||||||
visualizer.loadPreset(presets[Math.floor(Math.random() * presets.length)], 2.0);
|
|
||||||
|
|
||||||
me._frameCycle = () => {
|
|
||||||
requestAnimationFrame(me._frameCycle);
|
|
||||||
|
|
||||||
if (me.state.enableMilkdrop === true) {
|
|
||||||
visualizer.render();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
me._frameCycle();
|
|
||||||
}
|
|
||||||
|
|
||||||
const wavesurfer = WaveSurfer.create({
|
|
||||||
barWidth: 3,
|
|
||||||
container: this.waveNode,
|
|
||||||
waveColor: '#000',
|
|
||||||
progressColor: '#fff',
|
|
||||||
mediaControls: true,
|
|
||||||
responsive: true,
|
|
||||||
normalize: true,
|
|
||||||
backend: 'MediaElement',
|
|
||||||
minPxPerSec: 100,
|
|
||||||
height: this.waveNode.offsetHeight,
|
|
||||||
});
|
|
||||||
|
|
||||||
wavesurfer.load(audioNode);
|
|
||||||
|
|
||||||
jsmediatags.Config.setDisallowedXhrHeaders(['If-Modified-Since', 'Range']);
|
|
||||||
jsmediatags.read(path, {
|
|
||||||
onSuccess: function(result) {
|
|
||||||
const { album, artist, title, picture } = result.tags;
|
|
||||||
|
|
||||||
if (picture) {
|
|
||||||
const byteArray = new Uint8Array(picture.data);
|
|
||||||
const blob = new Blob([byteArray], { type: picture.type });
|
|
||||||
const albumArtUrl = URL.createObjectURL(blob);
|
|
||||||
me.artNode.src = albumArtUrl;
|
|
||||||
|
|
||||||
me.setState({ artLoaded: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
me.setState({
|
|
||||||
album,
|
|
||||||
artist,
|
|
||||||
title,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onError: function(error) {
|
|
||||||
console.log(':(', error.type, error.info);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (this.player) {
|
|
||||||
this.player.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Kill the render loop
|
|
||||||
this._frameCycle = () => {};
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const me = this;
|
|
||||||
const { contentType, poster, claim } = me.props;
|
|
||||||
const {
|
|
||||||
album,
|
|
||||||
artist,
|
|
||||||
title,
|
|
||||||
enableMilkdrop,
|
|
||||||
showEqualizer,
|
|
||||||
showSongDetails,
|
|
||||||
enableArt,
|
|
||||||
artLoaded,
|
|
||||||
playing,
|
|
||||||
userActive,
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const renderArt = enableArt && artLoaded;
|
|
||||||
|
|
||||||
const path = `https://api.lbry.tv/content/claims/${claim.name}/${claim.claim_id}/stream.mp4`;
|
|
||||||
|
|
||||||
const playButton = (
|
|
||||||
<div
|
|
||||||
onClick={() => {
|
|
||||||
const audioNode = this.audioNode;
|
|
||||||
if (audioNode.paused) {
|
|
||||||
audioNode.play();
|
|
||||||
} else {
|
|
||||||
audioNode.pause();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className={playing ? styles.playButtonPause : styles.playButtonPlay}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={userActive ? styles.userActive : styles.wrapper}
|
|
||||||
onMouseEnter={() => me.setState({ userActive: true })}
|
|
||||||
onMouseLeave={() => me.setState({ userActive: false })}
|
|
||||||
onContextMenu={stopContextMenu}
|
|
||||||
>
|
|
||||||
<div className={enableMilkdrop ? styles.containerWithMilkdrop : styles.container}>
|
|
||||||
<div style={{ position: 'absolute', top: 0, right: 0 }}>
|
|
||||||
<Tooltip onComponent body={__('Toggle Visualizer')}>
|
|
||||||
<Button
|
|
||||||
icon={enableMilkdrop ? ICONS.VISUALIZER_ON : ICONS.VISUALIZER_OFF}
|
|
||||||
onClick={() => {
|
|
||||||
if (!isButterchurnSupported) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get new preset
|
|
||||||
this.visualizer.loadPreset(presets[Math.floor(Math.random() * presets.length)], 2.0);
|
|
||||||
|
|
||||||
this.setState({ enableMilkdrop: !enableMilkdrop });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip onComponent body={__('Toggle Album Art')}>
|
|
||||||
<Button
|
|
||||||
icon={enableArt ? ICONS.MUSIC_ART_ON : ICONS.MUSIC_ART_OFF}
|
|
||||||
onClick={() => this.setState({ enableArt: !enableArt })}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip onComponent body={__('Toggle Details')}>
|
|
||||||
<Button
|
|
||||||
icon={showSongDetails ? ICONS.MUSIC_DETAILS_ON : ICONS.MUSIC_DETAILS_OFF}
|
|
||||||
onClick={() => this.setState({ showSongDetails: !showSongDetails })}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip onComponent body={__('Equalizer')}>
|
|
||||||
<Button icon={ICONS.MUSIC_EQUALIZER} onClick={() => this.setState({ showEqualizer: !showEqualizer })} />
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
<div ref={node => (this.waveNode = node)} className={styles.wave} />
|
|
||||||
<div className={styles.infoContainer}>
|
|
||||||
<div className={renderArt ? styles.infoArtContainer : styles.infoArtContainerHidden}>
|
|
||||||
<img className={styles.infoArtImage} ref={node => (this.artNode = node)} />
|
|
||||||
{renderArt && playButton}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
showSongDetails
|
|
||||||
? renderArt
|
|
||||||
? styles.songDetailsContainer
|
|
||||||
: styles.songDetailsContainerNoArt
|
|
||||||
: styles.songDetailsContainerHidden
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className={renderArt ? styles.songDetails : styles.songDetailsNoArt}>
|
|
||||||
{artist && (
|
|
||||||
<div className={styles.detailsLineArtist}>
|
|
||||||
<Button icon={ICONS.MUSIC_ARTIST} className={styles.detailsIconArtist} />
|
|
||||||
{artist}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{title && (
|
|
||||||
<div className={styles.detailsLineSong}>
|
|
||||||
<Button icon={ICONS.MUSIC_SONG} className={styles.detailsIconSong} />
|
|
||||||
{title}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{album && (
|
|
||||||
<div className={styles.detailsLineAlbum}>
|
|
||||||
<Button icon={ICONS.MUSIC_ALBUM} className={styles.detailsIconAlbum} />
|
|
||||||
{album}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{!renderArt && <div className={styles.playButtonDetachedContainer}>{playButton}</div>}
|
|
||||||
</div>
|
|
||||||
<canvas
|
|
||||||
ref={node => (this.canvasNode = node)}
|
|
||||||
className={enableMilkdrop ? styles.milkdrop : styles.milkdropDisabled}
|
|
||||||
/>
|
|
||||||
<audio
|
|
||||||
ref={node => (this.audioNode = node)}
|
|
||||||
src={path}
|
|
||||||
style={{ position: 'absolute', top: '-100px' }}
|
|
||||||
onPlay={() => this.setState({ playing: true })}
|
|
||||||
onPause={() => this.setState({ playing: false })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AudioVideoViewer;
|
|
|
@ -1,193 +0,0 @@
|
||||||
.wrapper {
|
|
||||||
composes: 'file-render__viewer' from global;
|
|
||||||
}
|
|
||||||
|
|
||||||
.userActive {
|
|
||||||
composes: wrapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
background: #212529;
|
|
||||||
position: absolute;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.containerWithMilkdrop {
|
|
||||||
composes: container;
|
|
||||||
|
|
||||||
background: rgba(50, 50, 55, .7);
|
|
||||||
}
|
|
||||||
|
|
||||||
.wave {
|
|
||||||
position: absolute;
|
|
||||||
bottom: -20%;
|
|
||||||
height: 40%;
|
|
||||||
opacity: 0.5;
|
|
||||||
overflow: hidden;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.infoContainer {
|
|
||||||
padding: 0 20%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
min-height: 42%;
|
|
||||||
align-self: center;
|
|
||||||
width: 100%;
|
|
||||||
margin-top: -10%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.infoArtContainer {
|
|
||||||
align-self: flex-start;
|
|
||||||
width: 40%;
|
|
||||||
float: left;
|
|
||||||
position: relative;
|
|
||||||
background: rgba(0, 0, 0 , 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.infoArtContainerHidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.infoArtImage {
|
|
||||||
display: block;
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.7s;
|
|
||||||
|
|
||||||
.userActive & {
|
|
||||||
opacity: 0.2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.songDetailsContainer {
|
|
||||||
text-align: left;
|
|
||||||
padding: 3%;
|
|
||||||
width: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.songDetailsContainerHidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.songDetailsContainerNoArt {
|
|
||||||
composes: songDetailsContainer;
|
|
||||||
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.songDetails {
|
|
||||||
width: 150%;
|
|
||||||
text-shadow: 2px 2px 3px #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.songDetailsNoArt {
|
|
||||||
composes: songDetails;
|
|
||||||
|
|
||||||
width: 200%;
|
|
||||||
margin-left: -50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detailsIcon {
|
|
||||||
color: rgba(255, 255, 255, .5);
|
|
||||||
top: -3px;
|
|
||||||
padding-right: 10px;
|
|
||||||
width: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detailsIconArtist {
|
|
||||||
composes: detailsIcon;
|
|
||||||
|
|
||||||
top: -3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detailsIconSong {
|
|
||||||
composes: detailsIcon;
|
|
||||||
|
|
||||||
top: -5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detailsIconAlbum {
|
|
||||||
composes: detailsIcon;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detailsLineArtist {
|
|
||||||
font-size: 26px;
|
|
||||||
padding-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detailsLineSong {
|
|
||||||
font-size: 34px;
|
|
||||||
line-height: 36px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detailsLineAlbum {
|
|
||||||
font-size: 20px;
|
|
||||||
padding-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.playButton {
|
|
||||||
position: absolute;
|
|
||||||
border: 5px solid #fff;
|
|
||||||
border-radius: 45px;
|
|
||||||
color: #fff;
|
|
||||||
font-family: arial;
|
|
||||||
font-size: 60px;
|
|
||||||
left: 50%;
|
|
||||||
line-height: 80px;
|
|
||||||
margin-left: -45px;
|
|
||||||
padding-left: 20px;
|
|
||||||
bottom: 50%;
|
|
||||||
margin-bottom: -45px;
|
|
||||||
height: 90px;
|
|
||||||
width: 90px;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity .7s;
|
|
||||||
|
|
||||||
.userActive & {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.playButtonPlay {
|
|
||||||
composes: playButton;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
display: block;
|
|
||||||
content: "▶";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.playButtonPause {
|
|
||||||
composes: playButton;
|
|
||||||
|
|
||||||
font-size: 50px;
|
|
||||||
line-height: 75px;
|
|
||||||
padding-left: 20px;
|
|
||||||
letter-spacing: -24px;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
display: block;
|
|
||||||
content: "▎▎";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.playButtonDetachedContainer {
|
|
||||||
bottom: 35%;
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.milkdrop {
|
|
||||||
top: 0;
|
|
||||||
z-index: 100;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.milkdropDisabled {
|
|
||||||
display: none;
|
|
||||||
}
|
|
|
@ -6,15 +6,15 @@
|
||||||
export const FEATURED = 'Award';
|
export const FEATURED = 'Award';
|
||||||
export const LOCAL = 'Folder';
|
export const LOCAL = 'Folder';
|
||||||
export const ALERT = 'AlertCircle';
|
export const ALERT = 'AlertCircle';
|
||||||
export const CLIPBOARD = 'Clipboard';
|
export const COPY = 'Clipboard';
|
||||||
export const ARROW_LEFT = 'ChevronLeft';
|
export const ARROW_LEFT = 'ChevronLeft';
|
||||||
export const ARROW_RIGHT = 'ChevronRight';
|
export const ARROW_RIGHT = 'ChevronRight';
|
||||||
export const DOWNLOAD = 'Download';
|
export const DOWNLOAD = 'Download';
|
||||||
export const UPLOAD = 'UploadCloud';
|
export const UPLOAD = 'UploadCloud';
|
||||||
export const PUBLISHED = 'Cloud';
|
export const PUBLISHED = 'Cloud';
|
||||||
export const CLOSE = 'X';
|
export const REMOVE = 'X';
|
||||||
export const ADD = 'Plus';
|
export const ADD = 'Plus';
|
||||||
export const EDIT = 'Edit3';
|
export const EDIT = 'Edit';
|
||||||
export const DELETE = 'Trash';
|
export const DELETE = 'Trash';
|
||||||
export const REPORT = 'Flag';
|
export const REPORT = 'Flag';
|
||||||
export const HELP = 'HelpCircle';
|
export const HELP = 'HelpCircle';
|
||||||
|
@ -69,4 +69,4 @@ export const MUSIC_SONG = 'Music';
|
||||||
export const MUSIC_EQUALIZER = 'Sliders';
|
export const MUSIC_EQUALIZER = 'Sliders';
|
||||||
export const LIGHT = 'Sun';
|
export const LIGHT = 'Sun';
|
||||||
export const DARK = 'Moon';
|
export const DARK = 'Moon';
|
||||||
export const AUTO = 'Clock';
|
export const LIBRARY = 'Folder';
|
||||||
|
|
|
@ -20,3 +20,4 @@ export const SEARCH = 'search';
|
||||||
export const TRANSACTIONS = 'transactions';
|
export const TRANSACTIONS = 'transactions';
|
||||||
export const TAGS = 'tags';
|
export const TAGS = 'tags';
|
||||||
export const WALLET = 'wallet';
|
export const WALLET = 'wallet';
|
||||||
|
export const FOLLOWING = 'following';
|
||||||
|
|
|
@ -232,9 +232,9 @@ class FilePage extends React.Component<Props> {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{claimIsMine && (
|
{claimIsMine && (
|
||||||
<div className="media__subtext--large">
|
<p>
|
||||||
{viewCount} {viewCount !== 1 ? __('Views') : __('View')}
|
{viewCount} {viewCount !== 1 ? __('Views') : __('View')}
|
||||||
</div>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -307,17 +307,7 @@ class FilePage extends React.Component<Props> {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="file-properties">
|
<div className="file-properties">
|
||||||
{isRewardContent && (
|
{isRewardContent && <Icon size={20} iconColor="red" icon={icons.FEATURED} />}
|
||||||
<Icon
|
|
||||||
size={20}
|
|
||||||
iconColor="red"
|
|
||||||
icon={icons.FEATURED}
|
|
||||||
// Figure out how to get the tooltip to overlap the navbar on the file page and I will love you
|
|
||||||
// https://stackoverflow.com/questions/6421966/css-overflow-x-visible-and-overflow-y-hidden-causing-scrollbar-issue
|
|
||||||
// https://spee.ch/4/overflow-issue
|
|
||||||
// tooltip="bottom"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{nsfw && <div className="badge badge--nsfw">MATURE</div>}
|
{nsfw && <div className="badge badge--nsfw">MATURE</div>}
|
||||||
<FilePrice badge uri={normalizeURI(uri)} />
|
<FilePrice badge uri={normalizeURI(uri)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { selectFollowedTags } from 'lbry-redux';
|
import { selectFollowedTags } from 'lbry-redux';
|
||||||
|
import { selectSubscriptions } from 'redux/selectors/subscriptions';
|
||||||
import TagsEdit from './view';
|
import TagsEdit from './view';
|
||||||
|
|
||||||
const select = state => ({
|
const select = state => ({
|
||||||
followedTags: selectFollowedTags(state),
|
followedTags: selectFollowedTags(state),
|
||||||
|
subscribedChannels: selectSubscriptions(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = {};
|
const perform = {};
|
30
src/ui/page/following/view.jsx
Normal file
30
src/ui/page/following/view.jsx
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import Page from 'component/page';
|
||||||
|
import TagsSelect from 'component/tagsSelect';
|
||||||
|
import ClaimList from 'component/claimList';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
subscribedChannels: Array<{ uri: string }>,
|
||||||
|
};
|
||||||
|
|
||||||
|
function DiscoverPage(props: Props) {
|
||||||
|
const { subscribedChannels } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<div className="card">
|
||||||
|
<TagsSelect showClose={false} title={__('Find New Tags To Follow')} />
|
||||||
|
</div>
|
||||||
|
<div className="card">
|
||||||
|
<ClaimList
|
||||||
|
header={<h1>{__('Channels You Are Following')}</h1>}
|
||||||
|
empty={__("You aren't following any channels.")}
|
||||||
|
uris={subscribedChannels.map(({ uri }) => uri)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DiscoverPage;
|
|
@ -1,46 +1,14 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { doResolveUri, selectBalance } from 'lbry-redux';
|
import { selectBalance } from 'lbry-redux';
|
||||||
import {
|
|
||||||
selectPublishFormValues,
|
|
||||||
selectIsStillEditing,
|
|
||||||
selectMyClaimForUri,
|
|
||||||
selectIsResolvingPublishUris,
|
|
||||||
selectTakeOverAmount,
|
|
||||||
} from 'redux/selectors/publish';
|
|
||||||
import {
|
|
||||||
doResetThumbnailStatus,
|
|
||||||
doClearPublish,
|
|
||||||
doUpdatePublishForm,
|
|
||||||
doPublish,
|
|
||||||
doPrepareEdit,
|
|
||||||
} from 'redux/actions/publish';
|
|
||||||
import { selectUnclaimedRewardValue } from 'lbryinc';
|
import { selectUnclaimedRewardValue } from 'lbryinc';
|
||||||
import PublishPage from './view';
|
import PublishPage from './view';
|
||||||
|
|
||||||
const select = state => ({
|
const select = state => ({
|
||||||
...selectPublishFormValues(state),
|
|
||||||
// The winning claim for a short lbry uri
|
|
||||||
amountNeededForTakeover: selectTakeOverAmount(state),
|
|
||||||
// My previously published claims under this short lbry uri
|
|
||||||
myClaimForUri: selectMyClaimForUri(state),
|
|
||||||
// If I clicked the "edit" button, have I changed the uri?
|
|
||||||
// Need this to make it easier to find the source on previously published content
|
|
||||||
isStillEditing: selectIsStillEditing(state),
|
|
||||||
balance: selectBalance(state),
|
balance: selectBalance(state),
|
||||||
isResolvingUri: selectIsResolvingPublishUris(state),
|
|
||||||
totalRewardValue: selectUnclaimedRewardValue(state),
|
totalRewardValue: selectUnclaimedRewardValue(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = dispatch => ({
|
|
||||||
updatePublishForm: value => dispatch(doUpdatePublishForm(value)),
|
|
||||||
clearPublish: () => dispatch(doClearPublish()),
|
|
||||||
resolveUri: uri => dispatch(doResolveUri(uri)),
|
|
||||||
publish: params => dispatch(doPublish(params)),
|
|
||||||
prepareEdit: (claim, uri) => dispatch(doPrepareEdit(claim, uri)),
|
|
||||||
resetThumbnailStatus: () => dispatch(doResetThumbnailStatus()),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
select,
|
select,
|
||||||
perform
|
null
|
||||||
)(PublishPage);
|
)(PublishPage);
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
// @flow
|
||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import PublishForm from 'component/publishForm';
|
import PublishForm from 'component/publishForm';
|
||||||
import Page from 'component/page';
|
import Page from 'component/page';
|
||||||
|
@ -6,64 +7,67 @@ import LbcSymbol from 'component/common/lbc-symbol';
|
||||||
import CreditAmount from 'component/common/credit-amount';
|
import CreditAmount from 'component/common/credit-amount';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
|
|
||||||
class PublishPage extends React.PureComponent {
|
type Props = {
|
||||||
scrollToTop = () => {
|
balance: number,
|
||||||
|
totalRewardValue: number,
|
||||||
|
};
|
||||||
|
|
||||||
|
function PublishPage(props: Props) {
|
||||||
|
const { balance, totalRewardValue } = props;
|
||||||
|
const totalRewardRounded = Math.floor(totalRewardValue / 10) * 10;
|
||||||
|
|
||||||
|
function scrollToTop() {
|
||||||
const mainContent = document.querySelector('main');
|
const mainContent = document.querySelector('main');
|
||||||
if (mainContent) {
|
if (mainContent) {
|
||||||
mainContent.scrollTop = 0; // It would be nice to animate this
|
mainContent.scrollTop = 0; // It would be nice to animate this
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { balance, totalRewardValue } = this.props;
|
|
||||||
const totalRewardRounded = Math.floor(totalRewardValue / 10) * 10;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
{balance === 0 && (
|
|
||||||
<Fragment>
|
|
||||||
<Yrbl
|
|
||||||
title={__("You can't publish things quite yet")}
|
|
||||||
subtitle={
|
|
||||||
<Fragment>
|
|
||||||
<p>
|
|
||||||
{__(
|
|
||||||
'LBRY uses a blockchain, which is a fancy way of saying that users (you) are in control of your data.'
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
{__('Because of the blockchain, some actions require LBRY credits')} (
|
|
||||||
<LbcSymbol />
|
|
||||||
).
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<LbcSymbol />{' '}
|
|
||||||
{__(
|
|
||||||
'allows you to do some neat things, like paying your favorite creators for their content. And no company can stop you.'
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</Fragment>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<section className="card card--section">
|
|
||||||
<header className="card__header">
|
|
||||||
<h1 className="card__title">{__('LBRY Credits Required')}</h1>
|
|
||||||
</header>
|
|
||||||
<p className="card__subtitle">
|
|
||||||
{__(' There are a variety of ways to get credits, including more than')}{' '}
|
|
||||||
<CreditAmount inheritStyle amount={totalRewardRounded} />{' '}
|
|
||||||
{__('in free rewards for participating in the LBRY beta.')}
|
|
||||||
</p>
|
|
||||||
<div className="card__actions">
|
|
||||||
<Button button="link" navigate="/$/rewards" label={__('Checkout the rewards')} />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</Fragment>
|
|
||||||
)}
|
|
||||||
<PublishForm {...this.props} scrollToTop={this.scrollToTop} />
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
{balance === 0 && (
|
||||||
|
<Fragment>
|
||||||
|
<Yrbl
|
||||||
|
title={__("You can't publish things quite yet")}
|
||||||
|
subtitle={
|
||||||
|
<Fragment>
|
||||||
|
<p>
|
||||||
|
{__(
|
||||||
|
'LBRY uses a blockchain, which is a fancy way of saying that users (you) are in control of your data.'
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{__('Because of the blockchain, some actions require LBRY credits')} (
|
||||||
|
<LbcSymbol />
|
||||||
|
).
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<LbcSymbol />{' '}
|
||||||
|
{__(
|
||||||
|
'allows you to do some neat things, like paying your favorite creators for their content. And no company can stop you.'
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</Fragment>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<section className="card card--section">
|
||||||
|
<header className="card__header">
|
||||||
|
<h1 className="card__title">{__('LBRY Credits Required')}</h1>
|
||||||
|
</header>
|
||||||
|
<p className="card__subtitle">
|
||||||
|
{__(' There are a variety of ways to get credits, including more than')}{' '}
|
||||||
|
<CreditAmount inheritStyle amount={totalRewardRounded} />{' '}
|
||||||
|
{__('in free rewards for participating in the LBRY beta.')}
|
||||||
|
</p>
|
||||||
|
<div className="card__actions">
|
||||||
|
<Button button="link" navigate="/$/rewards" label={__('Checkout the rewards')} />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</Fragment>
|
||||||
|
)}
|
||||||
|
<PublishForm scrollToTop={scrollToTop} />
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PublishPage;
|
export default PublishPage;
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import React, { useEffect, Fragment } from 'react';
|
import React, { useEffect, Fragment } from 'react';
|
||||||
import { isURIValid, normalizeURI } from 'lbry-redux';
|
import { isURIValid, normalizeURI } from 'lbry-redux';
|
||||||
import ClaimListItem from 'component/claimListItem';
|
import ClaimPreview from 'component/claimPreview';
|
||||||
import ClaimList from 'component/claimList';
|
import ClaimList from 'component/claimList';
|
||||||
import Page from 'component/page';
|
import Page from 'component/page';
|
||||||
import SearchOptions from 'component/searchOptions';
|
import SearchOptions from 'component/searchOptions';
|
||||||
|
@ -29,7 +29,6 @@ export default function SearchPage(props: Props) {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (urlQuery) {
|
if (urlQuery) {
|
||||||
console.log('search', urlQuery);
|
|
||||||
search(urlQuery);
|
search(urlQuery);
|
||||||
}
|
}
|
||||||
}, [search, urlQuery]);
|
}, [search, urlQuery]);
|
||||||
|
@ -44,7 +43,7 @@ export default function SearchPage(props: Props) {
|
||||||
<Button button="alt" navigate={uri} className="media__uri">
|
<Button button="alt" navigate={uri} className="media__uri">
|
||||||
{uri}
|
{uri}
|
||||||
</Button>
|
</Button>
|
||||||
<ClaimListItem uri={uri} type="large" />
|
<ClaimPreview uri={uri} type="large" />
|
||||||
</header>
|
</header>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ export default function SubscriptionsPage(props: Props) {
|
||||||
const viewingSuggestedSubs = urlParams.get('view');
|
const viewingSuggestedSubs = urlParams.get('view');
|
||||||
|
|
||||||
function onClick() {
|
function onClick() {
|
||||||
let url = `/$/${PAGES.SUBSCRIPTIONS}`;
|
let url = `/$/${PAGES.FOLLOWING}`;
|
||||||
if (!viewingSuggestedSubs) {
|
if (!viewingSuggestedSubs) {
|
||||||
url += '?view=discover';
|
url += '?view=discover';
|
||||||
}
|
}
|
||||||
|
@ -54,6 +54,7 @@ export default function SubscriptionsPage(props: Props) {
|
||||||
const ids = idString.split(',');
|
const ids = idString.split(',');
|
||||||
const options = {
|
const options = {
|
||||||
channel_ids: ids,
|
channel_ids: ids,
|
||||||
|
order_by: ['release_time'],
|
||||||
};
|
};
|
||||||
|
|
||||||
doClaimSearch(20, options);
|
doClaimSearch(20, options);
|
||||||
|
@ -68,7 +69,7 @@ export default function SubscriptionsPage(props: Props) {
|
||||||
headerAltControls={
|
headerAltControls={
|
||||||
<Button
|
<Button
|
||||||
button="link"
|
button="link"
|
||||||
label={viewingSuggestedSubs ? hasSubscriptions && __('View Your Subscriptions') : __('Find New Channels')}
|
label={viewingSuggestedSubs ? hasSubscriptions && __('Following') : __('Find New Channels')}
|
||||||
onClick={() => onClick()}
|
onClick={() => onClick()}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
// @flow
|
|
||||||
import React from 'react';
|
|
||||||
import Page from 'component/page';
|
|
||||||
import TagsSelect from 'component/tagsSelect';
|
|
||||||
|
|
||||||
type Props = {};
|
|
||||||
|
|
||||||
function DiscoverPage(props: Props) {
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<div className="card">
|
|
||||||
<TagsSelect showClose={false} title={__('Find New Tags To Follow')} />
|
|
||||||
</div>
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DiscoverPage;
|
|
|
@ -15,6 +15,7 @@ import {
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { doOpenModal } from 'redux/actions/app';
|
import { doOpenModal } from 'redux/actions/app';
|
||||||
import { selectosNotificationsEnabled } from 'redux/selectors/settings';
|
import { selectosNotificationsEnabled } from 'redux/selectors/settings';
|
||||||
|
import { selectMyClaimForUri, selectPublishFormValues } from 'redux/selectors/publish';
|
||||||
import { push } from 'connected-react-router';
|
import { push } from 'connected-react-router';
|
||||||
import analytics from 'analytics';
|
import analytics from 'analytics';
|
||||||
import { formatLbryUriForWeb } from 'util/uri';
|
import { formatLbryUriForWeb } from 'util/uri';
|
||||||
|
@ -146,7 +147,7 @@ export const doPrepareEdit = (claim: StreamClaim, uri: string, fileInfo: FileLis
|
||||||
// use same values as default state
|
// use same values as default state
|
||||||
// fee will be undefined for free content
|
// fee will be undefined for free content
|
||||||
fee = {
|
fee = {
|
||||||
amount: 0,
|
amount: '0',
|
||||||
currency: 'LBC',
|
currency: 'LBC',
|
||||||
},
|
},
|
||||||
languages,
|
languages,
|
||||||
|
@ -159,11 +160,11 @@ export const doPrepareEdit = (claim: StreamClaim, uri: string, fileInfo: FileLis
|
||||||
const publishData: UpdatePublishFormData = {
|
const publishData: UpdatePublishFormData = {
|
||||||
name,
|
name,
|
||||||
channel: channelName,
|
channel: channelName,
|
||||||
bid: amount,
|
bid: Number(amount),
|
||||||
contentIsFree: !fee.amount,
|
contentIsFree: !fee.amount,
|
||||||
author,
|
author,
|
||||||
description,
|
description,
|
||||||
fee: { amount: fee.amount, currency: fee.currency },
|
fee,
|
||||||
languages,
|
languages,
|
||||||
thumbnail: thumbnail ? thumbnail.url : null,
|
thumbnail: thumbnail ? thumbnail.url : null,
|
||||||
title,
|
title,
|
||||||
|
@ -201,10 +202,13 @@ export const doPrepareEdit = (claim: StreamClaim, uri: string, fileInfo: FileLis
|
||||||
dispatch({ type: ACTIONS.DO_PREPARE_EDIT, data: publishData });
|
dispatch({ type: ACTIONS.DO_PREPARE_EDIT, data: publishData });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const doPublish = (params: PublishParams) => (dispatch: Dispatch, getState: () => {}) => {
|
export const doPublish = () => (dispatch: Dispatch, getState: () => {}) => {
|
||||||
dispatch({ type: ACTIONS.PUBLISH_START });
|
dispatch({ type: ACTIONS.PUBLISH_START });
|
||||||
|
|
||||||
const state = getState();
|
const state = getState();
|
||||||
|
const publishData = selectPublishFormValues(state);
|
||||||
|
const myClaimForUri = selectMyClaimForUri(state);
|
||||||
|
|
||||||
const myChannels = selectMyChannelClaims(state);
|
const myChannels = selectMyChannelClaims(state);
|
||||||
const myClaims = selectMyClaimsWithoutChannels(state);
|
const myClaims = selectMyClaimsWithoutChannels(state);
|
||||||
|
|
||||||
|
@ -214,8 +218,9 @@ export const doPublish = (params: PublishParams) => (dispatch: Dispatch, getStat
|
||||||
filePath,
|
filePath,
|
||||||
description,
|
description,
|
||||||
language,
|
language,
|
||||||
license,
|
|
||||||
licenseUrl,
|
licenseUrl,
|
||||||
|
licenseType,
|
||||||
|
otherLicenseDescription,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
channel,
|
channel,
|
||||||
title,
|
title,
|
||||||
|
@ -223,8 +228,19 @@ export const doPublish = (params: PublishParams) => (dispatch: Dispatch, getStat
|
||||||
fee,
|
fee,
|
||||||
uri,
|
uri,
|
||||||
nsfw,
|
nsfw,
|
||||||
claim,
|
tags,
|
||||||
} = params;
|
locations,
|
||||||
|
} = publishData;
|
||||||
|
|
||||||
|
let publishingLicense;
|
||||||
|
switch (licenseType) {
|
||||||
|
case COPYRIGHT:
|
||||||
|
case OTHER:
|
||||||
|
publishingLicense = otherLicenseDescription;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
publishingLicense = licenseType;
|
||||||
|
}
|
||||||
|
|
||||||
// get the claim id from the channel name, we will use that instead
|
// get the claim id from the channel name, we will use that instead
|
||||||
const namedChannelClaim = myChannels.find(myChannel => myChannel.name === channel);
|
const namedChannelClaim = myChannels.find(myChannel => myChannel.name === channel);
|
||||||
|
@ -244,29 +260,19 @@ export const doPublish = (params: PublishParams) => (dispatch: Dispatch, getStat
|
||||||
fee_amount?: string,
|
fee_amount?: string,
|
||||||
} = {
|
} = {
|
||||||
name,
|
name,
|
||||||
bid: creditsToString(bid),
|
|
||||||
title,
|
title,
|
||||||
license,
|
|
||||||
languages: [language],
|
|
||||||
description,
|
description,
|
||||||
tags: (claim && claim.value.tags) || [],
|
locations,
|
||||||
locations: claim && claim.value.locations,
|
bid: creditsToString(bid),
|
||||||
|
license: publishingLicense,
|
||||||
|
languages: [language],
|
||||||
|
tags: tags && tags.map(tag => tag.name),
|
||||||
|
license_url: licenseType === COPYRIGHT ? '' : licenseUrl,
|
||||||
|
thumbnail_url: thumbnail,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Temporary solution to keep the same publish flow with the new tags api
|
if (myClaimForUri && myClaimForUri.value.release_time) {
|
||||||
// Eventually we will allow users to enter their own tags on publish
|
publishPayload.release_time = Number(myClaimForUri.value.release_time);
|
||||||
// `nsfw` will probably be removed
|
|
||||||
|
|
||||||
if (licenseUrl) {
|
|
||||||
publishPayload.license_url = licenseUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (thumbnail) {
|
|
||||||
publishPayload.thumbnail_url = thumbnail;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (claim && claim.value.release_time) {
|
|
||||||
publishPayload.release_time = Number(claim.value.release_time);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nsfw) {
|
if (nsfw) {
|
||||||
|
@ -346,23 +352,25 @@ export const doCheckPendingPublishes = () => (dispatch: Dispatch, getState: GetS
|
||||||
|
|
||||||
const checkFileList = () => {
|
const checkFileList = () => {
|
||||||
Lbry.claim_list().then(claims => {
|
Lbry.claim_list().then(claims => {
|
||||||
claims.forEach(claim => {
|
if (claims) {
|
||||||
// If it's confirmed, check if it was pending previously
|
claims.forEach(claim => {
|
||||||
if (claim.confirmations > 0 && pendingById[claim.claim_id]) {
|
// If it's confirmed, check if it was pending previously
|
||||||
delete pendingById[claim.claim_id];
|
if (claim.confirmations > 0 && pendingById[claim.claim_id]) {
|
||||||
|
delete pendingById[claim.claim_id];
|
||||||
|
|
||||||
// If it's confirmed, check if we should notify the user
|
// If it's confirmed, check if we should notify the user
|
||||||
if (selectosNotificationsEnabled(getState())) {
|
if (selectosNotificationsEnabled(getState())) {
|
||||||
const notif = new window.Notification('LBRY Publish Complete', {
|
const notif = new window.Notification('LBRY Publish Complete', {
|
||||||
body: `${claim.value.title} has been published to lbry://${claim.name}. Click here to view it`,
|
body: `${claim.value.title} has been published to lbry://${claim.name}. Click here to view it`,
|
||||||
silent: false,
|
silent: false,
|
||||||
});
|
});
|
||||||
notif.onclick = () => {
|
notif.onclick = () => {
|
||||||
dispatch(push(formatLbryUriForWeb(claim.permanent_url)));
|
dispatch(push(formatLbryUriForWeb(claim.permanent_url)));
|
||||||
};
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.FETCH_CLAIM_LIST_MINE_COMPLETED,
|
type: ACTIONS.FETCH_CLAIM_LIST_MINE_COMPLETED,
|
||||||
|
|
|
@ -27,6 +27,7 @@ type PublishState = {
|
||||||
bidError: ?string,
|
bidError: ?string,
|
||||||
otherLicenseDescription: string,
|
otherLicenseDescription: string,
|
||||||
licenseUrl: string,
|
licenseUrl: string,
|
||||||
|
tags: Array<string>,
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultState: PublishState = {
|
const defaultState: PublishState = {
|
||||||
|
@ -53,6 +54,7 @@ const defaultState: PublishState = {
|
||||||
licenseType: 'None',
|
licenseType: 'None',
|
||||||
otherLicenseDescription: 'All rights reserved',
|
otherLicenseDescription: 'All rights reserved',
|
||||||
licenseUrl: '',
|
licenseUrl: '',
|
||||||
|
tags: [],
|
||||||
publishing: false,
|
publishing: false,
|
||||||
publishSuccess: false,
|
publishSuccess: false,
|
||||||
publishError: undefined,
|
publishError: undefined,
|
||||||
|
|
|
@ -18,6 +18,12 @@ export const selectPublishFormValues = createSelector(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const makeSelectPublishFormValue = item =>
|
||||||
|
createSelector(
|
||||||
|
selectState,
|
||||||
|
state => state[item]
|
||||||
|
);
|
||||||
|
|
||||||
// Is the current uri the same as the uri they clicked "edit" on
|
// Is the current uri the same as the uri they clicked "edit" on
|
||||||
export const selectIsStillEditing = createSelector(
|
export const selectIsStillEditing = createSelector(
|
||||||
selectPublishFormValues,
|
selectPublishFormValues,
|
||||||
|
|
|
@ -46,6 +46,5 @@
|
||||||
@import 'component/tags';
|
@import 'component/tags';
|
||||||
@import 'component/time';
|
@import 'component/time';
|
||||||
@import 'component/toggle';
|
@import 'component/toggle';
|
||||||
@import 'component/tooltip';
|
|
||||||
@import 'component/wunderbar';
|
@import 'component/wunderbar';
|
||||||
@import 'component/yrbl';
|
@import 'component/yrbl';
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
.file-list__header {
|
.claim-list__header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-height: 4.5rem;
|
min-height: 4.5rem;
|
||||||
|
@ -31,12 +31,13 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-list__header--small {
|
.claim-list__header--small {
|
||||||
height: 3rem;
|
height: 3rem;
|
||||||
|
min-height: 3rem;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-list__dropdown {
|
.claim-list__dropdown {
|
||||||
background-position: 95% center;
|
background-position: 95% center;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-size: 1.2rem;
|
background-size: 1.2rem;
|
||||||
|
@ -50,8 +51,8 @@
|
||||||
background-color: lighten($lbry-black, 10%);
|
background-color: lighten($lbry-black, 10%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-list__header,
|
.claim-list__header,
|
||||||
.file-list__dropdown {
|
.claim-list__dropdown {
|
||||||
background-color: lighten($lbry-black, 10%);
|
background-color: lighten($lbry-black, 10%);
|
||||||
|
|
||||||
[data-mode='dark'] & {
|
[data-mode='dark'] & {
|
||||||
|
@ -59,17 +60,17 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-list__header-text {
|
.claim-list__header-text {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-list__header-text,
|
.claim-list__header-text,
|
||||||
.file-list__dropdown {
|
.claim-list__dropdown {
|
||||||
font-size: 1.3rem;
|
font-size: 1.3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-list__alt-controls {
|
.claim-list__alt-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
@ -80,7 +81,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-list__item {
|
.claim-list__item {
|
||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
font-size: 1.3rem;
|
font-size: 1.3rem;
|
||||||
|
@ -103,12 +104,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-list__item--injected,
|
.claim-list__item--injected,
|
||||||
.file-list__item {
|
.claim-list__item + .claim-list__item {
|
||||||
border-bottom: 1px solid rgba($lbry-teal-5, 0.1);
|
border-top: 1px solid rgba($lbry-teal-5, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-list__item--large {
|
.claim-list__item--large {
|
||||||
@include mediaThumbHoverZoom;
|
@include mediaThumbHoverZoom;
|
||||||
font-size: 1.6rem;
|
font-size: 1.6rem;
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
|
@ -124,36 +125,49 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-list__item-metadata {
|
.claim-list__pending {
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.6;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $lbry-white;
|
||||||
|
|
||||||
|
[data-mode='dark'] & {
|
||||||
|
background-color: lighten($lbry-black, 5%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.claim-list__item-metadata {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-list__item-info {
|
.claim-list__item-info {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-list__item-info,
|
.claim-list__item-info,
|
||||||
.file-list__item-properties {
|
.claim-list__item-properties {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-list__item-properties {
|
.claim-list__item-properties {
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-list__item-title {
|
.claim-list__item-title {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-list__item-tags {
|
.claim-list__item-tags {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-list__meta {
|
.claim-list__meta {
|
||||||
padding: var(--spacing-medium);
|
padding: var(--spacing-medium);
|
||||||
background-color: lighten($lbry-teal-5, 55%);
|
background-color: lighten($lbry-teal-5, 55%);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,36 @@
|
||||||
@import '~@lbry/components/sass/form/_index.scss';
|
@import '~@lbry/components/sass/form/_index.scss';
|
||||||
|
|
||||||
|
// replace this
|
||||||
|
form {
|
||||||
|
// setting the font size here sizes everything within
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-bottom: var(--spacing-s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
select {
|
||||||
|
height: var(--spacing-l);
|
||||||
|
border: 1px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
checkbox-element,
|
||||||
|
radio-element,
|
||||||
|
select {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
&::placeholder {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// lbry/components overrides and minor styles
|
// lbry/components overrides and minor styles
|
||||||
// Some items have very specific styling
|
// Some items have very specific styling
|
||||||
// This is because many styles inside `lbry/components/sass/form/` are very specific
|
// This is because many styles inside `lbry/components/sass/form/` are very specific
|
||||||
// As styles become hardened here, they _should_ slowly move over to that repo
|
// As styles become hardened here, they _should_ slowly move over to that repo
|
||||||
|
|
||||||
input,
|
|
||||||
textarea,
|
|
||||||
select {
|
|
||||||
border-radius: var(--input-border-radius);
|
|
||||||
}
|
|
||||||
|
|
||||||
input-submit {
|
input-submit {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
@ -21,7 +41,8 @@ input[type='number'] {
|
||||||
|
|
||||||
input[type='text'],
|
input[type='text'],
|
||||||
input[type='number'],
|
input[type='number'],
|
||||||
select {
|
select,
|
||||||
|
textarea {
|
||||||
padding-bottom: 0.1em;
|
padding-bottom: 0.1em;
|
||||||
|
|
||||||
[data-mode='dark'] & {
|
[data-mode='dark'] & {
|
||||||
|
@ -31,6 +52,14 @@ select {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
border-color: lighten($lbry-black, 20%);
|
||||||
|
border-radius: var(--input-border-radius);
|
||||||
|
border-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
fieldset-section {
|
fieldset-section {
|
||||||
label {
|
label {
|
||||||
width: auto;
|
width: auto;
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
padding-top: var(--header-height);
|
padding-top: var(--header-height);
|
||||||
padding-left: var(--spacing-large);
|
padding-left: var(--spacing-large);
|
||||||
padding-right: var(--spacing-large);
|
padding-right: var(--spacing-large);
|
||||||
|
padding-bottom: var(--spacing-large);
|
||||||
background-color: mix($lbry-white, $lbry-gray-1, 70%);
|
background-color: mix($lbry-white, $lbry-gray-1, 70%);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
.placeholder {
|
.placeholder {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
&.file-list__item-title {
|
&.claim-list__item-title {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 3rem;
|
height: 3rem;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,152 +0,0 @@
|
||||||
.tooltip {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.tooltip__body {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
.tooltip__body {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip__body {
|
|
||||||
font-size: 1rem;
|
|
||||||
color: $lbry-black;
|
|
||||||
font-weight: 400;
|
|
||||||
padding: var(--spacing-miniscule);
|
|
||||||
position: absolute;
|
|
||||||
text-align: center;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
width: 200px;
|
|
||||||
background-color: $lbry-white;
|
|
||||||
border: 1px solid $lbry-gray-1;
|
|
||||||
box-shadow: 0 2px 5px rgba($lbry-black, 0.15);
|
|
||||||
|
|
||||||
&.tooltip__body--short {
|
|
||||||
width: 110px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
|
|
||||||
border-style: solid;
|
|
||||||
border-width: 5px;
|
|
||||||
content: ' ';
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$tooltip-border: $lbry-gray-5;
|
|
||||||
&.tooltip--bottom .tooltip__body::after {
|
|
||||||
border-color: transparent transparent $tooltip-border transparent;
|
|
||||||
}
|
|
||||||
&.tooltip--left .tooltip__body::after {
|
|
||||||
border-color: transparent transparent transparent $tooltip-border;
|
|
||||||
}
|
|
||||||
&.tooltip--right .tooltip__body::after {
|
|
||||||
border-color: transparent $tooltip-border transparent transparent;
|
|
||||||
}
|
|
||||||
&.tooltip--top .tooltip__body::after {
|
|
||||||
border-color: $tooltip-border transparent transparent transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
html[data-mode='dark'] & {
|
|
||||||
.tooltip__body {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
$tooltip-border: $lbry-white;
|
|
||||||
|
|
||||||
&.tooltip--bottom .tooltip__body::after {
|
|
||||||
border-color: transparent transparent $tooltip-border transparent;
|
|
||||||
}
|
|
||||||
&.tooltip--left .tooltip__body::after {
|
|
||||||
border-color: transparent transparent transparent $tooltip-border;
|
|
||||||
}
|
|
||||||
&.tooltip--right .tooltip__body::after {
|
|
||||||
border-color: transparent $tooltip-border transparent transparent;
|
|
||||||
}
|
|
||||||
&.tooltip--top .tooltip__body::after {
|
|
||||||
border-color: $tooltip-border transparent transparent transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip--always-visible {
|
|
||||||
.tooltip__body {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip--label {
|
|
||||||
// When there is a label for the tooltip and not just using a button or icon
|
|
||||||
font-size: 14px;
|
|
||||||
padding-left: $spacing-vertical * 1/3;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip--on-component,
|
|
||||||
.tooltip--icon {
|
|
||||||
.tooltip__body {
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip--left .tooltip__body {
|
|
||||||
top: -5px;
|
|
||||||
right: 105%;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
top: 17px;
|
|
||||||
left: 100%;
|
|
||||||
margin-top: -5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip--right .tooltip__body {
|
|
||||||
margin-top: -28px;
|
|
||||||
margin-left: 110%;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
top: 14px;
|
|
||||||
right: 100%; // To the left of the tooltip
|
|
||||||
margin-top: -5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip--bottom .tooltip__body {
|
|
||||||
top: 90%;
|
|
||||||
left: 50%;
|
|
||||||
margin-left: -100px;
|
|
||||||
|
|
||||||
&.tooltip__body--short {
|
|
||||||
margin-left: -56px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
bottom: 100%;
|
|
||||||
left: 50%;
|
|
||||||
margin-left: -5px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip--top .tooltip__body {
|
|
||||||
bottom: 120%;
|
|
||||||
left: 50%;
|
|
||||||
margin-left: -100px;
|
|
||||||
|
|
||||||
&.tooltip__body--short {
|
|
||||||
margin-left: -56px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
top: 100%;
|
|
||||||
left: 50%;
|
|
||||||
margin-left: -5px;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -91,7 +91,14 @@
|
||||||
.icon {
|
.icon {
|
||||||
margin-right: var(--spacing-small);
|
margin-right: var(--spacing-small);
|
||||||
margin-bottom: 0.2rem;
|
margin-bottom: 0.2rem;
|
||||||
|
|
||||||
stroke: $lbry-gray-5;
|
stroke: $lbry-gray-5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-mode='dark'] & {
|
||||||
|
color: $lbry-gray-2;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
stroke: $lbry-gray-2;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -294,4 +294,4 @@
|
||||||
"Post": "Post",
|
"Post": "Post",
|
||||||
"Channel": "Channel",
|
"Channel": "Channel",
|
||||||
"No modifier provided after separator %s.": "No modifier provided after separator %s."
|
"No modifier provided after separator %s.": "No modifier provided after separator %s."
|
||||||
}
|
}
|
||||||
|
|
11
yarn.lock
11
yarn.lock
|
@ -993,7 +993,7 @@
|
||||||
integrity sha512-JZshEuGsLvi6fUIJ7Unx12yNeM5SmqWjber2MLr9tfwf1hpNv73EiPBOIJyV0DjW7GXzjcOEvwnqysm59s2s/A==
|
integrity sha512-JZshEuGsLvi6fUIJ7Unx12yNeM5SmqWjber2MLr9tfwf1hpNv73EiPBOIJyV0DjW7GXzjcOEvwnqysm59s2s/A==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@reach/component-component" "^0.1.3"
|
"@reach/component-component" "^0.1.3"
|
||||||
|
|
||||||
"@samverschueren/stream-to-observable@^0.3.0":
|
"@samverschueren/stream-to-observable@^0.3.0":
|
||||||
version "0.3.0"
|
version "0.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz#ecdf48d532c58ea477acfcab80348424f8d0662f"
|
resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz#ecdf48d532c58ea477acfcab80348424f8d0662f"
|
||||||
|
@ -6641,9 +6641,9 @@ lazy-val@^1.0.3, lazy-val@^1.0.4:
|
||||||
yargs "^13.2.2"
|
yargs "^13.2.2"
|
||||||
zstd-codec "^0.1.1"
|
zstd-codec "^0.1.1"
|
||||||
|
|
||||||
lbry-redux@lbryio/lbry-redux#141593500693a93db74c62ef5a9fe67b43896603:
|
lbry-redux@lbryio/lbry-redux#b3bf3f6d53410ff1c5415b51ca425341e364959f:
|
||||||
version "0.0.1"
|
version "0.0.1"
|
||||||
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/141593500693a93db74c62ef5a9fe67b43896603"
|
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/b3bf3f6d53410ff1c5415b51ca425341e364959f"
|
||||||
dependencies:
|
dependencies:
|
||||||
proxy-polyfill "0.1.6"
|
proxy-polyfill "0.1.6"
|
||||||
reselect "^3.0.0"
|
reselect "^3.0.0"
|
||||||
|
@ -9459,11 +9459,6 @@ react-dom@^16.8.2, react-dom@^16.8.6:
|
||||||
prop-types "^15.6.2"
|
prop-types "^15.6.2"
|
||||||
scheduler "^0.13.6"
|
scheduler "^0.13.6"
|
||||||
|
|
||||||
react-feather@^1.0.8:
|
|
||||||
version "1.1.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/react-feather/-/react-feather-1.1.6.tgz#2a547e3d5cd5e383d3da0128d593cbdb3c1b32f7"
|
|
||||||
integrity sha512-iCofWhTjX+vQwvDmg7o6vg0XrUg1c41yBDZG+l83nz1FiCsleJoUgd3O+kHpOeWMXuPrRIFfCixvcqyOLGOgIg==
|
|
||||||
|
|
||||||
react-ga@^2.5.7:
|
react-ga@^2.5.7:
|
||||||
version "2.5.7"
|
version "2.5.7"
|
||||||
resolved "https://registry.yarnpkg.com/react-ga/-/react-ga-2.5.7.tgz#1c80a289004bf84f84c26d46f3a6a6513081bf2e"
|
resolved "https://registry.yarnpkg.com/react-ga/-/react-ga-2.5.7.tgz#1c80a289004bf84f84c26d46f3a6a6513081bf2e"
|
||||||
|
|
Loading…
Reference in a new issue