cleanup and add tags to publish page
This commit is contained in:
parent
f39ca4dd57
commit
f9269ae969
59 changed files with 1265 additions and 913 deletions
1
flow-typed/publish.js
vendored
1
flow-typed/publish.js
vendored
|
@ -48,4 +48,5 @@ declare type PublishParams = {
|
|||
},
|
||||
claim: StreamClaim,
|
||||
nsfw: boolean,
|
||||
tags: Array<Tag>,
|
||||
};
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
// @flow
|
||||
import type { Node } from 'react';
|
||||
import React, { forwardRef } from 'react';
|
||||
import Icon from 'component/common/icon';
|
||||
import classnames from 'classnames';
|
||||
|
@ -14,13 +15,12 @@ type Props = {
|
|||
icon: ?string,
|
||||
iconRight: ?string,
|
||||
disabled: ?boolean,
|
||||
children: ?React.Node,
|
||||
children: ?Node,
|
||||
navigate: ?string,
|
||||
className: ?string,
|
||||
description: ?string,
|
||||
type: string,
|
||||
button: ?string, // primary, secondary, alt, link
|
||||
iconColor?: string,
|
||||
iconSize?: number,
|
||||
constrict: ?boolean, // to shorten the button and ellipsis, only use for links
|
||||
activeClass?: string,
|
||||
|
@ -31,7 +31,9 @@ type Props = {
|
|||
onMouseLeave: ?(any) => any,
|
||||
};
|
||||
|
||||
const Button = forwardRef((props: Props, ref) => {
|
||||
// use forwardRef to allow consumers to pass refs to the button content if they want to
|
||||
// flow requires forwardRef have default type arguments passed to it
|
||||
const Button = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||
const {
|
||||
type = 'button',
|
||||
onClick,
|
||||
|
@ -48,7 +50,6 @@ const Button = forwardRef((props: Props, ref) => {
|
|||
className,
|
||||
description,
|
||||
button,
|
||||
iconColor,
|
||||
iconSize,
|
||||
constrict,
|
||||
activeClass,
|
||||
|
@ -74,10 +75,10 @@ const Button = forwardRef((props: Props, ref) => {
|
|||
|
||||
const content = (
|
||||
<span className="button__content">
|
||||
{icon && <Icon icon={icon} iconColor={iconColor} size={iconSize} />}
|
||||
{icon && <Icon icon={icon} size={iconSize} />}
|
||||
{label && <span className="button__label">{label}</span>}
|
||||
{children && children}
|
||||
{iconRight && <Icon icon={iconRight} iconColor={iconColor} size={iconSize} />}
|
||||
{iconRight && <Icon icon={iconRight} size={iconSize} />}
|
||||
</span>
|
||||
);
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import type { Node } from 'react';
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import ClaimListItem from 'component/claimListItem';
|
||||
import ClaimPreview from 'component/claimPreview';
|
||||
import Spinner from 'component/spinner';
|
||||
import { FormField } from 'component/common/form';
|
||||
import usePersistedState from 'util/use-persisted-state';
|
||||
|
@ -36,10 +36,10 @@ export default function ClaimList(props: Props) {
|
|||
return (
|
||||
<section className={classnames('file-list')}>
|
||||
{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 || (
|
||||
<FormField
|
||||
className="file-list__dropdown"
|
||||
className="claim-list__dropdown"
|
||||
type="select"
|
||||
name="file_sort"
|
||||
value={currentSort}
|
||||
|
@ -50,16 +50,16 @@ export default function ClaimList(props: Props) {
|
|||
</FormField>
|
||||
)}
|
||||
{loading && <Spinner light type="small" />}
|
||||
<div className="file-list__alt-controls">{headerAltControls}</div>
|
||||
<div className="claim-list__alt-controls">{headerAltControls}</div>
|
||||
</div>
|
||||
)}
|
||||
{meta && <div className="file-list__meta">{meta}</div>}
|
||||
{meta && <div className="claim-list__meta">{meta}</div>}
|
||||
{hasUris && (
|
||||
<ul>
|
||||
{sortedUris.map((uri, index) => (
|
||||
<React.Fragment key={uri}>
|
||||
<ClaimListItem uri={uri} type={type} />
|
||||
{index === 4 && injectedItem && <li className="file-list__item--injected">{injectedItem}</li>}
|
||||
<ClaimPreview uri={uri} type={type} />
|
||||
{index === 4 && injectedItem && <li className="claim-list__item--injected">{injectedItem}</li>}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</ul>
|
||||
|
|
|
@ -70,7 +70,7 @@ function ClaimListDiscover(props: Props) {
|
|||
const header = (
|
||||
<h1 className="card__title--flex">
|
||||
<FormField
|
||||
className="file-list__dropdown"
|
||||
className="claim-list__dropdown"
|
||||
type="select"
|
||||
name="trending_sort"
|
||||
value={typeSort}
|
||||
|
@ -89,7 +89,7 @@ function ClaimListDiscover(props: Props) {
|
|||
<FormField
|
||||
type="select"
|
||||
name="trending_overview"
|
||||
className="file-list__dropdown"
|
||||
className="claim-list__dropdown"
|
||||
value={personalSort}
|
||||
onChange={e => setPersonalSort(e.target.value)}
|
||||
>
|
||||
|
@ -107,7 +107,7 @@ function ClaimListDiscover(props: Props) {
|
|||
<React.Fragment>
|
||||
{typeSort === 'top' && (
|
||||
<FormField
|
||||
className="file-list__dropdown"
|
||||
className="claim-list__dropdown"
|
||||
type="select"
|
||||
name="trending_time"
|
||||
value={timeSort}
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
} from 'lbry-redux';
|
||||
import { selectBlackListedOutpoints } from 'lbryinc';
|
||||
import { selectShowNsfw } from 'redux/selectors/settings';
|
||||
import ClaimListItem from './view';
|
||||
import ClaimPreview from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
pending: makeSelectClaimIsPending(props.uri)(state),
|
||||
|
@ -32,4 +32,4 @@ const perform = dispatch => ({
|
|||
export default connect(
|
||||
select,
|
||||
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);
|
|
@ -35,7 +35,7 @@ type Props = {
|
|||
}>,
|
||||
};
|
||||
|
||||
function ClaimListItem(props: Props) {
|
||||
function ClaimPreview(props: Props) {
|
||||
const {
|
||||
obscureNsfw,
|
||||
claimIsMine,
|
||||
|
@ -94,10 +94,10 @@ function ClaimListItem(props: Props) {
|
|||
|
||||
if (placeholder && !claim) {
|
||||
return (
|
||||
<li className="file-list__item" disabled>
|
||||
<li className="claim-list__item" disabled>
|
||||
<div className="placeholder media__thumb" />
|
||||
<div className="placeholder__wrapper">
|
||||
<div className="placeholder file-list__item-title" />
|
||||
<div className="placeholder claim-list__item-title" />
|
||||
<div className="placeholder media__subtitle" />
|
||||
</div>
|
||||
</li>
|
||||
|
@ -107,16 +107,17 @@ function ClaimListItem(props: Props) {
|
|||
return (
|
||||
<li
|
||||
role="link"
|
||||
onClick={onClick}
|
||||
onClick={pending ? undefined : onClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
className={classnames('file-list__item', {
|
||||
'file-list__item--large': type === 'large',
|
||||
className={classnames('claim-list__item', {
|
||||
'claim-list__item--large': type === 'large',
|
||||
'claim-list__pending': pending,
|
||||
})}
|
||||
>
|
||||
{isChannel ? <ChannelThumbnail uri={uri} /> : <CardMedia thumbnail={thumbnail} />}
|
||||
<div className="file-list__item-metadata">
|
||||
<div className="file-list__item-info">
|
||||
<div className="file-list__item-title">
|
||||
<div className="claim-list__item-metadata">
|
||||
<div className="claim-list__item-info">
|
||||
<div className="claim-list__item-title">
|
||||
<TruncatedText text={title || (claim && claim.name)} lines={1} />
|
||||
</div>
|
||||
{type !== 'small' && (
|
||||
|
@ -127,7 +128,7 @@ function ClaimListItem(props: Props) {
|
|||
)}
|
||||
</div>
|
||||
|
||||
<div className="file-list__item-properties">
|
||||
<div className="claim-list__item-properties">
|
||||
<div className="media__subtitle">
|
||||
<UriIndicator uri={uri} link />
|
||||
{pending && <div>Pending...</div>}
|
||||
|
@ -141,4 +142,4 @@ function ClaimListItem(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
export default withRouter(ClaimListItem);
|
||||
export default withRouter(ClaimPreview);
|
|
@ -1,13 +1,6 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
// @if TARGET='app'
|
||||
// $FlowFixMe
|
||||
import { remote } from 'electron';
|
||||
// @endif
|
||||
// @if TARGET='web'
|
||||
// $FlowFixMe
|
||||
import { remote } from 'web/stubs';
|
||||
// @endif
|
||||
import Button from 'component/button';
|
||||
import { FormField } from 'component/common/form';
|
||||
import path from 'path';
|
||||
|
@ -21,6 +14,8 @@ type Props = {
|
|||
type: string,
|
||||
currentPath: ?string,
|
||||
onFileChosen: (string, string) => void,
|
||||
label?: string,
|
||||
placeholder?: string,
|
||||
fileLabel?: string,
|
||||
directoryLabel?: string,
|
||||
filters?: FileFilters[],
|
||||
|
@ -83,13 +78,14 @@ class FileSelector extends React.PureComponent<Props> {
|
|||
input: ?HTMLInputElement;
|
||||
|
||||
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 (
|
||||
<React.Fragment>
|
||||
<FormField
|
||||
label={label}
|
||||
webkitdirectory="true"
|
||||
className="form-field--copyable"
|
||||
type="text"
|
||||
|
@ -98,8 +94,8 @@ class FileSelector extends React.PureComponent<Props> {
|
|||
if (this.fileInput) this.fileInput.current.select();
|
||||
}}
|
||||
readOnly="readonly"
|
||||
value={currentPath || __('No File Chosen')}
|
||||
inputButton={<Button button="primary" label={label} onClick={() => this.handleButtonClick()} />}
|
||||
value={currentPath || placeholder || __('Choose a file')}
|
||||
inputButton={<Button button="primary" label={buttonLabel} onClick={() => this.handleButtonClick()} />}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// @flow
|
||||
// A housing for all of our icons. Mostly taken fromhttps://github.com/feathericons/react-feather
|
||||
// A housing for all of our icons. Mostly taken from https://github.com/feathericons/react-feather
|
||||
import * as ICONS from 'constants/icons';
|
||||
import React, { forwardRef } from 'react';
|
||||
|
||||
|
@ -9,7 +9,8 @@ type IconProps = {
|
|||
};
|
||||
|
||||
// Returns a react component
|
||||
const buildIcon = (iconStrokes: React$Node, options?: {} = {}) =>
|
||||
// Icons with tooltips need to use this function so the ref can be properly forwarded
|
||||
const buildIcon = (iconStrokes: React$Node) =>
|
||||
forwardRef((props: IconProps, ref) => {
|
||||
const { size = 24, color = 'currentColor', ...otherProps } = props;
|
||||
return (
|
||||
|
@ -24,7 +25,6 @@ const buildIcon = (iconStrokes: React$Node, options?: {} = {}) =>
|
|||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
{...options}
|
||||
{...otherProps}
|
||||
>
|
||||
{iconStrokes}
|
||||
|
@ -34,7 +34,7 @@ const buildIcon = (iconStrokes: React$Node, options?: {} = {}) =>
|
|||
|
||||
export const icons = {
|
||||
// The LBRY icon is different from the base icon set so don't use buildIcon()
|
||||
[ICONS.LBRY]: (
|
||||
[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" />
|
||||
|
|
|
@ -13,7 +13,7 @@ const BLUE_COLOR = '#49b2e2';
|
|||
|
||||
type Props = {
|
||||
icon: string,
|
||||
tooltip?: string, // tooltip direction
|
||||
tooltip?: boolean,
|
||||
iconColor?: string,
|
||||
size?: number,
|
||||
className?: string,
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
// @flow
|
||||
import * as React from 'react';
|
||||
import type { Node } from 'react';
|
||||
import React from 'react';
|
||||
import ReachTooltip from '@reach/tooltip';
|
||||
import '@reach/tooltip/styles.css';
|
||||
|
||||
type Props = {
|
||||
label: string,
|
||||
children?: React.Node,
|
||||
label: string | Node,
|
||||
children?: Node,
|
||||
};
|
||||
|
||||
function Tooltip(props: Props) {
|
||||
|
|
|
@ -3,10 +3,12 @@ import Button from 'component/button';
|
|||
|
||||
export default function UnsupportedOnWeb() {
|
||||
return (
|
||||
IS_WEB && (
|
||||
<div className="card__content help help--warning">
|
||||
This page is not currently supported on the web.{' '}
|
||||
<Button button="link" label={__('Download the desktop app')} href="https://lbry.com/get" /> for full feature
|
||||
support.
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -68,7 +68,7 @@ class ErrorBoundary extends React.Component<Props, State> {
|
|||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div className="load-screen">
|
||||
<div className="main main--empty">
|
||||
<Yrbl
|
||||
type="sad"
|
||||
title={__('Aw shucks!')}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
// @flow
|
||||
import type { Node } from 'react';
|
||||
import * as MODALS from 'constants/modal_types';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
import Button from 'component/button';
|
||||
import Tooltip from 'component/common/tooltip';
|
||||
import { requestFullscreen, fullscreenElement } from 'util/full-screen';
|
||||
|
@ -16,7 +17,7 @@ type Props = {
|
|||
openModal: (id: string, { uri: string }) => void,
|
||||
claimIsMine: boolean,
|
||||
fileInfo: FileInfo,
|
||||
viewerContainer: React.Ref,
|
||||
viewerContainer: ?{ current: Node },
|
||||
showFullscreen: boolean,
|
||||
};
|
||||
|
||||
|
|
|
@ -57,7 +57,7 @@ class FileDownloadLink extends React.PureComponent<Props> {
|
|||
}
|
||||
|
||||
return (
|
||||
<ToolTip label={__('Download')}>
|
||||
<ToolTip label={__('Add to your library')}>
|
||||
<Button
|
||||
button="link"
|
||||
icon={ICONS.DOWNLOAD}
|
||||
|
|
|
@ -23,7 +23,7 @@ export default function FileProperties(props: Props) {
|
|||
<div className="file-properties">
|
||||
{isSubscribed && <Icon tooltip icon={icons.SUBSCRIPTION} />}
|
||||
{!claimIsMine && downloaded && <Icon tooltip icon={icons.DOWNLOAD} />}
|
||||
{isRewardContent && <Icon tooltip iconColor="red" icon={icons.FEATURED} />}
|
||||
{isRewardContent && <Icon tooltip icon={icons.FEATURED} />}
|
||||
<FilePrice hideFree uri={uri} />
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -4,6 +4,8 @@ import React from 'react';
|
|||
import LoadingScreen from 'component/common/loading-screen';
|
||||
import VideoViewer from 'component/viewers/videoViewer';
|
||||
|
||||
// Audio player on hold until the current player is dropped
|
||||
// This component is half working
|
||||
// const AudioViewer = React.lazy<*>(() =>
|
||||
// import(/* webpackChunkName: "audioViewer" */
|
||||
// 'component/viewers/audioViewer')
|
||||
|
|
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';
|
||||
|
||||
type Props = {
|
||||
licenseType: string,
|
||||
licenseType: ?string,
|
||||
licenseUrl: ?string,
|
||||
otherLicenseDescription: ?string,
|
||||
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);
|
54
src/ui/component/publishFile/view.jsx
Normal file
54
src/ui/component/publishFile/view.jsx
Normal file
|
@ -0,0 +1,54 @@
|
|||
// @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">
|
||||
{__("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
|
||||
import { COPYRIGHT, OTHER } from 'constants/licenses';
|
||||
import { CHANNEL_NEW, CHANNEL_ANONYMOUS, MINIMUM_PUBLISH_BID } from 'constants/claim';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import * as React from 'react';
|
||||
import { isNameValid, buildURI, regexInvalidURI, THUMBNAIL_STATUSES } from 'lbry-redux';
|
||||
import { Form, FormField, FormFieldPrice, Submit } from 'component/common/form';
|
||||
import React, { useEffect, Fragment } from 'react';
|
||||
import { CHANNEL_NEW, CHANNEL_ANONYMOUS } from 'constants/claim';
|
||||
import { buildURI, THUMBNAIL_STATUSES } from 'lbry-redux';
|
||||
import Button from 'component/button';
|
||||
import ChannelSection from 'component/selectChannel';
|
||||
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 BidHelpText from './internal/bid-help-text';
|
||||
import NameHelpText from './internal/name-help-text';
|
||||
import LicenseType from './internal/license-type';
|
||||
import TagSelect from 'component/tagsSelect';
|
||||
import PublishText from 'component/publishText';
|
||||
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 = {
|
||||
tags: Array<Tag>,
|
||||
publish: PublishParams => void,
|
||||
filePath: ?string,
|
||||
bid: ?number,
|
||||
|
@ -34,7 +35,6 @@ type Props = {
|
|||
},
|
||||
channel: string,
|
||||
name: ?string,
|
||||
updatePublishForm: UpdatePublishFormData => void,
|
||||
nameError: ?string,
|
||||
isResolvingUri: boolean,
|
||||
winningBidForClaimUri: number,
|
||||
|
@ -43,7 +43,6 @@ type Props = {
|
|||
otherLicenseDescription: ?string,
|
||||
licenseUrl: ?string,
|
||||
uri: ?string,
|
||||
bidError: ?string,
|
||||
publishing: boolean,
|
||||
balance: number,
|
||||
isStillEditing: boolean,
|
||||
|
@ -53,274 +52,33 @@ type Props = {
|
|||
prepareEdit: (claim: any, uri: string) => void,
|
||||
resetThumbnailStatus: () => void,
|
||||
amountNeededForTakeover: ?number,
|
||||
// Add back type
|
||||
updatePublishForm: any => void,
|
||||
};
|
||||
|
||||
class PublishForm extends React.PureComponent<Props> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
(this: any).handleFileChange = this.handleFileChange.bind(this);
|
||||
(this: any).checkIsFormValid = this.checkIsFormValid.bind(this);
|
||||
(this: any).renderFormErrors = this.renderFormErrors.bind(this);
|
||||
(this: any).handlePublish = this.handlePublish.bind(this);
|
||||
(this: any).handleCancelPublish = this.handleCancelPublish.bind(this);
|
||||
(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() {
|
||||
const { thumbnail, name, channel, editingURI } = this.props;
|
||||
if (!thumbnail) {
|
||||
this.props.resetThumbnailStatus();
|
||||
}
|
||||
if (editingURI) {
|
||||
this.getNewUri(name, channel);
|
||||
}
|
||||
}
|
||||
|
||||
getNewUri(name: string, channel: string) {
|
||||
const { resolveUri } = this.props;
|
||||
// If they are midway through a channel creation, treat it as anonymous until it completes
|
||||
const channelName = channel === CHANNEL_ANONYMOUS || channel === CHANNEL_NEW ? '' : channel;
|
||||
|
||||
// We are only going to store the full uri, but we need to resolve the uri with and without the channel name
|
||||
let uri;
|
||||
try {
|
||||
uri = buildURI({ contentName: name, channelName });
|
||||
} catch (e) {
|
||||
// something wrong with channel or name
|
||||
}
|
||||
|
||||
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);
|
||||
return uri;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
handleFileChange(filePath: string, fileName: string) {
|
||||
const { updatePublishForm, channel, name } = this.props;
|
||||
const newFileParams: UpdatePublishFormData = { filePath };
|
||||
|
||||
if (!name) {
|
||||
const parsedFileName = fileName.replace(regexInvalidURI, '');
|
||||
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() {
|
||||
function PublishForm(props: Props) {
|
||||
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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
filePath,
|
||||
editingURI,
|
||||
title,
|
||||
thumbnail,
|
||||
uploadThumbnailStatus,
|
||||
description,
|
||||
language,
|
||||
nsfw,
|
||||
contentIsFree,
|
||||
fee,
|
||||
channel,
|
||||
name,
|
||||
updatePublishForm,
|
||||
channel,
|
||||
editingURI,
|
||||
resolveUri,
|
||||
title,
|
||||
bid,
|
||||
nameError,
|
||||
isResolvingUri,
|
||||
winningBidForClaimUri,
|
||||
myClaimForUri,
|
||||
licenseType,
|
||||
otherLicenseDescription,
|
||||
licenseUrl,
|
||||
uri,
|
||||
bidError,
|
||||
uploadThumbnailStatus,
|
||||
resetThumbnailStatus,
|
||||
updatePublishForm,
|
||||
filePath,
|
||||
publishing,
|
||||
clearPublish,
|
||||
thumbnailPath,
|
||||
resetThumbnailStatus,
|
||||
isStillEditing,
|
||||
amountNeededForTakeover,
|
||||
balance,
|
||||
} = this.props;
|
||||
|
||||
tags,
|
||||
publish,
|
||||
} = props;
|
||||
const formDisabled = (!filePath && !editingURI) || publishing;
|
||||
const formValid = this.checkIsFormValid();
|
||||
// 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;
|
||||
|
||||
let submitLabel;
|
||||
if (isStillEditing) {
|
||||
|
@ -329,270 +87,84 @@ class PublishForm extends React.PureComponent<Props> {
|
|||
submitLabel = !publishing ? __('Publish') : __('Publishing...');
|
||||
}
|
||||
|
||||
const shortUri = buildURI({ contentName: name });
|
||||
useEffect(() => {
|
||||
if (!thumbnail) {
|
||||
resetThumbnailStatus();
|
||||
}
|
||||
}, [thumbnail, resetThumbnailStatus]);
|
||||
|
||||
// Every time the channel or name changes, resolve the uris to find winning bid amounts
|
||||
useEffect(() => {
|
||||
// If they are midway through a channel creation, treat it as anonymous until it completes
|
||||
const channelName = channel === CHANNEL_ANONYMOUS || channel === CHANNEL_NEW ? '' : channel;
|
||||
|
||||
// We are only going to store the full uri, but we need to resolve the uri with and without the channel name
|
||||
let uri;
|
||||
try {
|
||||
uri = buildURI({ contentName: name, channelName });
|
||||
} catch (e) {}
|
||||
|
||||
if (channelName) {
|
||||
// resolve without the channel name so we know the winning bid for it
|
||||
const uriLessChannel = buildURI({ contentName: name });
|
||||
resolveUri(uriLessChannel);
|
||||
}
|
||||
|
||||
if (uri) {
|
||||
resolveUri(uri);
|
||||
updatePublishForm({ uri });
|
||||
}
|
||||
}, [name, channel, resolveUri, updatePublishForm]);
|
||||
|
||||
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.REMOVE} 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>
|
||||
<Fragment>
|
||||
<UnsupportedOnWeb />
|
||||
|
||||
<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>
|
||||
<PublishFile />
|
||||
<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 })}
|
||||
<PublishText disabled={formDisabled} />
|
||||
<div className="card card--section">
|
||||
{/* This should probably be PublishThumbnail */}
|
||||
<SelectThumbnail />
|
||||
</div>
|
||||
<div className="card">
|
||||
<TagSelect
|
||||
title={false}
|
||||
help={__('The better your tags are, the easier it will be for people to discover your content.')}
|
||||
empty={__('No tags added')}
|
||||
onSelect={tag => updatePublishForm({ tags: [...tags, tag] })}
|
||||
onRemove={clickedTag => {
|
||||
const newTags = tags.slice().filter(tag => tag.name !== clickedTag.name);
|
||||
updatePublishForm({ tags: newTags });
|
||||
}}
|
||||
tagsChosen={tags}
|
||||
/>
|
||||
</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">
|
||||
<ChannelSection channel={channel} onChannelChange={channel => updatePublishForm({ channel })} />
|
||||
<p className="help">
|
||||
{__('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>
|
||||
<PublishName disabled={formDisabled} />
|
||||
<PublishPrice disabled={formDisabled} />
|
||||
<PublishAdditionalOptions disabled={formDisabled} />
|
||||
|
||||
<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 })}
|
||||
/>
|
||||
{!formDisabled && !formValid && <PublishFormErrors />}
|
||||
|
||||
<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
|
||||
<Button
|
||||
button="primary"
|
||||
onClick={publish}
|
||||
label={submitLabel}
|
||||
disabled={formDisabled || !formValid || uploadThumbnailStatus === THUMBNAIL_STATUSES.IN_PROGRESS}
|
||||
/>
|
||||
<Button button="link" onClick={this.handleCancelPublish} label={__('Cancel')} />
|
||||
<Button button="link" onClick={clearPublish} label={__('Cancel')} />
|
||||
</div>
|
||||
<p className="help">
|
||||
{__('By continuing, you accept the')}{' '}
|
||||
|
@ -600,12 +172,8 @@ class PublishForm extends React.PureComponent<Props> {
|
|||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{!formDisabled && !formValid && this.renderFormErrors()}
|
||||
</Form>
|
||||
</React.Fragment>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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>{__('You need to reselect a file after changing the LBRY URL')}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PublishFormErrors;
|
32
src/ui/component/publishName/bid-help-text.jsx
Normal file
32
src/ui/component/publishName/bid-help-text.jsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
// @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 {
|
||||
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;
|
46
src/ui/component/publishPrice/index.js
Normal file
46
src/ui/component/publishPrice/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),
|
||||
balance: selectBalance(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);
|
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 NavigationHistory from 'page/navigationHistory';
|
||||
import TagsPage from 'page/tags';
|
||||
import TagsEditPage from 'page/tagsEdit';
|
||||
import FollowingPage from 'page/following';
|
||||
|
||||
const Scroll = withRouter(function ScrollWrapper(props) {
|
||||
const { pathname } = props.location;
|
||||
|
@ -50,17 +50,17 @@ export default function AppRouter() {
|
|||
<Route path={`/$/${PAGES.REWARDS}`} exact component={RewardsPage} />
|
||||
<Route path={`/$/${PAGES.SEARCH}`} exact component={SearchPage} />
|
||||
<Route path={`/$/${PAGES.SETTINGS}`} exact component={SettingsPage} />
|
||||
<Route path={`/$/${PAGES.SUBSCRIPTIONS}`} exact component={SubscriptionsPage} />
|
||||
<Route path={`/$/${PAGES.TRANSACTIONS}`} exact component={TransactionHistoryPage} />
|
||||
<Route path={`/$/${PAGES.LIBRARY}`} exact component={UserHistoryPage} />
|
||||
<Route path={`/$/${PAGES.ACCOUNT}`} exact component={AccountPage} />
|
||||
<Route path={`/$/${PAGES.LIBRARY}/all`} exact component={NavigationHistory} />
|
||||
<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} />
|
||||
{/* 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/:claimId" exact component={ShowPage} />
|
||||
<Route path="/:claimName/:contentName" exact component={ShowPage} />
|
||||
|
||||
{/* Route not found. Mostly for people typing crazy urls into the url */}
|
||||
<Route render={() => <Redirect to="/" />} />
|
||||
|
|
|
@ -1,12 +1,47 @@
|
|||
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 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 => ({
|
||||
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)),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
select,
|
||||
perform
|
||||
)(SelectThumbnail);
|
||||
)(PublishPage);
|
||||
|
|
|
@ -122,7 +122,8 @@ class SelectThumbnail extends React.PureComponent<Props, State> {
|
|||
{status === THUMBNAIL_STATUSES.READY && (
|
||||
<FileSelector
|
||||
currentPath={thumbnailPath}
|
||||
fileLabel={__('Choose Thumbnail')}
|
||||
label={__('Thumbnail')}
|
||||
placeholder={__('Choose a thumbnail')}
|
||||
filters={filters}
|
||||
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>}
|
||||
<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 (.png/.jpg/.jpeg/.gif) to')}{' '}
|
||||
<Button button="link" label={__('spee.ch')} href="https://spee.ch/about" />.{' '}
|
||||
{__('Recommended size: 800x450 (16:9)')}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -34,23 +34,23 @@ function SideBar(props: Props) {
|
|||
...buildLink(null, __('Home'), ICONS.HOME),
|
||||
},
|
||||
{
|
||||
...buildLink(PAGES.SUBSCRIPTIONS, __('Subscriptions'), ICONS.SUBSCRIPTION),
|
||||
},
|
||||
{
|
||||
...buildLink(PAGES.PUBLISHED, __('Publishes'), ICONS.PUBLISHED),
|
||||
...buildLink(PAGES.FOLLOWING, __('Following'), ICONS.SUBSCRIPTION),
|
||||
},
|
||||
{
|
||||
...buildLink(PAGES.LIBRARY, __('Library'), ICONS.LIBRARY),
|
||||
},
|
||||
{
|
||||
...buildLink(PAGES.PUBLISHED, __('Publishes'), ICONS.PUBLISHED),
|
||||
},
|
||||
].map(renderLink)}
|
||||
|
||||
<li>
|
||||
<Button
|
||||
navigate="/$/tags/edit"
|
||||
navigate="/$/following/edit"
|
||||
icon={ICONS.EDIT}
|
||||
className="navigation__link"
|
||||
activeClass="navigation__link--active"
|
||||
label={__('Following')}
|
||||
label={__('Edit')}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
@ -15,27 +15,33 @@ type Props = {
|
|||
followedTags: Array<Tag>,
|
||||
doToggleTagFollow: string => void,
|
||||
doAddTag: string => void,
|
||||
onSelect?: Tag => void,
|
||||
};
|
||||
|
||||
export default function TagSelect(props: Props) {
|
||||
const { unfollowedTags, followedTags, doToggleTagFollow, doAddTag } = props;
|
||||
const { unfollowedTags, followedTags, doToggleTagFollow, doAddTag, onSelect } = props;
|
||||
const [newTag, setNewTag] = useState('');
|
||||
|
||||
let tags = unfollowedTags.slice();
|
||||
if (newTag) {
|
||||
tags = [{ name: newTag }, ...tags];
|
||||
tags.unshift({ name: newTag });
|
||||
}
|
||||
const suggestedTags = tags
|
||||
.filter(({ name }) => (newTag ? name.toLowerCase().includes(newTag.toLowerCase()) : true))
|
||||
.slice(0, 5);
|
||||
|
||||
const doesTagMatch = ({ name }) => (newTag ? name.toLowerCase().includes(newTag.toLowerCase()) : true);
|
||||
const suggestedTags = tags.filter(doesTagMatch).slice(0, 5);
|
||||
const suggestedTransitions = useTransition(suggestedTags, tag => tag.name, unfollowedTagsAnimation);
|
||||
|
||||
function onChange(e) {
|
||||
setNewTag(e.target.value);
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
setNewTag('');
|
||||
|
||||
if (onSelect) {
|
||||
onSelect({ name: newTag });
|
||||
} else {
|
||||
if (!unfollowedTags.includes(newTag)) {
|
||||
doAddTag(newTag);
|
||||
}
|
||||
|
@ -44,6 +50,15 @@ export default function TagSelect(props: Props) {
|
|||
doToggleTagFollow(newTag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleTagClick(tag) {
|
||||
if (onSelect) {
|
||||
onSelect(tag);
|
||||
} else {
|
||||
doToggleTagFollow(tag);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
@ -59,7 +74,7 @@ export default function TagSelect(props: Props) {
|
|||
<ul className="tags">
|
||||
{suggestedTransitions.map(({ item, key, 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>
|
||||
))}
|
||||
{!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';
|
||||
|
||||
type Props = {
|
||||
followedTags: Array<Tag>,
|
||||
showClose: boolean,
|
||||
title: string,
|
||||
doDeleteTag: string => void,
|
||||
followedTags: Array<Tag>,
|
||||
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 = {
|
||||
|
@ -21,23 +29,32 @@ const tagsAnimation = {
|
|||
};
|
||||
|
||||
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 tagsToDisplay = tagsChosen || followedTags;
|
||||
const transitions = useTransition(tagsToDisplay, tag => tag.name, tagsAnimation);
|
||||
|
||||
function handleClose() {
|
||||
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 (
|
||||
((showClose && !hasClosed) || !showClose) && (
|
||||
<div className="card--section">
|
||||
{title !== false && (
|
||||
<h2 className="card__title card__title--flex-between">
|
||||
{title}
|
||||
{showClose && !hasClosed && <Button button="close" icon={ICONS.REMOVE} onClick={handleClose} />}
|
||||
</h2>
|
||||
<p className="help">{__("The tags you follow will change what's trending for you.")}</p>
|
||||
)}
|
||||
|
||||
<div className="card__content">
|
||||
<ul className="tags--remove">
|
||||
|
@ -47,16 +64,19 @@ export default function TagSelect(props: Props) {
|
|||
name={item.name}
|
||||
type="remove"
|
||||
onClick={() => {
|
||||
doDeleteTag(item.name);
|
||||
handleTagClick(item);
|
||||
}}
|
||||
/>
|
||||
</animated.li>
|
||||
))}
|
||||
{!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>
|
||||
<TagsSearch />
|
||||
<TagsSearch onSelect={onSelect} />
|
||||
{help !== false && (
|
||||
<p className="help">{help || __("The tags you follow will change what's trending for you.")}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
import React from 'react';
|
||||
import Button from 'component/button';
|
||||
import { buildURI } from 'lbry-redux';
|
||||
import Tooltip from 'component/common/tooltip';
|
||||
import ClaimPreview from 'component/claimPreview';
|
||||
|
||||
type Props = {
|
||||
isResolvingUri: boolean,
|
||||
|
@ -61,7 +63,7 @@ class UriIndicator extends React.PureComponent<Props> {
|
|||
|
||||
return (
|
||||
<Button className="button--uri-indicator" navigate={channelLink}>
|
||||
{inner}
|
||||
<Tooltip label={<ClaimPreview uri={channelLink} type="small" />}>{inner}</Tooltip>
|
||||
</Button>
|
||||
);
|
||||
} else {
|
||||
|
|
|
@ -20,3 +20,4 @@ export const SEARCH = 'search';
|
|||
export const TRANSACTIONS = 'transactions';
|
||||
export const TAGS = 'tags';
|
||||
export const WALLET = 'wallet';
|
||||
export const FOLLOWING = 'following';
|
||||
|
|
|
@ -232,9 +232,9 @@ class FilePage extends React.Component<Props> {
|
|||
</div>
|
||||
|
||||
{claimIsMine && (
|
||||
<div className="media__subtext--large">
|
||||
<p>
|
||||
{viewCount} {viewCount !== 1 ? __('Views') : __('View')}
|
||||
</div>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectFollowedTags } from 'lbry-redux';
|
||||
import { selectSubscriptions } from 'redux/selectors/subscriptions';
|
||||
import TagsEdit from './view';
|
||||
|
||||
const select = state => ({
|
||||
followedTags: selectFollowedTags(state),
|
||||
subscribedChannels: selectSubscriptions(state),
|
||||
});
|
||||
|
||||
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 { 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 { selectBalance } from 'lbry-redux';
|
||||
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),
|
||||
balance: selectBalance(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
|
||||
null
|
||||
)(PublishPage);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
// @flow
|
||||
import React, { Fragment } from 'react';
|
||||
import PublishForm from 'component/publishForm';
|
||||
import Page from 'component/page';
|
||||
|
@ -6,17 +7,21 @@ import LbcSymbol from 'component/common/lbc-symbol';
|
|||
import CreditAmount from 'component/common/credit-amount';
|
||||
import Button from 'component/button';
|
||||
|
||||
class PublishPage extends React.PureComponent {
|
||||
scrollToTop = () => {
|
||||
type Props = {
|
||||
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');
|
||||
if (mainContent) {
|
||||
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>
|
||||
|
@ -60,10 +65,9 @@ class PublishPage extends React.PureComponent {
|
|||
</section>
|
||||
</Fragment>
|
||||
)}
|
||||
<PublishForm {...this.props} scrollToTop={this.scrollToTop} />
|
||||
<PublishForm scrollToTop={scrollToTop} />
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PublishPage;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import * as ICONS from 'constants/icons';
|
||||
import React, { useEffect, Fragment } from 'react';
|
||||
import { isURIValid, normalizeURI } from 'lbry-redux';
|
||||
import ClaimListItem from 'component/claimListItem';
|
||||
import ClaimPreview from 'component/claimPreview';
|
||||
import ClaimList from 'component/claimList';
|
||||
import Page from 'component/page';
|
||||
import SearchOptions from 'component/searchOptions';
|
||||
|
@ -29,7 +29,6 @@ export default function SearchPage(props: Props) {
|
|||
|
||||
useEffect(() => {
|
||||
if (urlQuery) {
|
||||
console.log('search', urlQuery);
|
||||
search(urlQuery);
|
||||
}
|
||||
}, [search, urlQuery]);
|
||||
|
@ -44,7 +43,7 @@ export default function SearchPage(props: Props) {
|
|||
<Button button="alt" navigate={uri} className="media__uri">
|
||||
{uri}
|
||||
</Button>
|
||||
<ClaimListItem uri={uri} type="large" />
|
||||
<ClaimPreview uri={uri} type="large" />
|
||||
</header>
|
||||
)}
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ export default function SubscriptionsPage(props: Props) {
|
|||
const viewingSuggestedSubs = urlParams.get('view');
|
||||
|
||||
function onClick() {
|
||||
let url = `/$/${PAGES.SUBSCRIPTIONS}`;
|
||||
let url = `/$/${PAGES.FOLLOWING}`;
|
||||
if (!viewingSuggestedSubs) {
|
||||
url += '?view=discover';
|
||||
}
|
||||
|
@ -54,6 +54,7 @@ export default function SubscriptionsPage(props: Props) {
|
|||
const ids = idString.split(',');
|
||||
const options = {
|
||||
channel_ids: ids,
|
||||
order_by: ['release_time'],
|
||||
};
|
||||
|
||||
doClaimSearch(20, options);
|
||||
|
@ -72,7 +73,8 @@ export default function SubscriptionsPage(props: Props) {
|
|||
onClick={() => onClick()}
|
||||
/>
|
||||
}
|
||||
uris={viewingSuggestedSubs ? suggestedSubscriptions.map(sub => sub.uri) : uris}
|
||||
// Fix the need to reverse this
|
||||
uris={viewingSuggestedSubs ? suggestedSubscriptions.map(sub => sub.uri) : uris.reverse()}
|
||||
/>
|
||||
</div>
|
||||
</Page>
|
||||
|
|
|
@ -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';
|
||||
import { doOpenModal } from 'redux/actions/app';
|
||||
import { selectosNotificationsEnabled } from 'redux/selectors/settings';
|
||||
import { selectMyClaimForUri, selectPublishFormValues } from 'redux/selectors/publish';
|
||||
import { push } from 'connected-react-router';
|
||||
import analytics from 'analytics';
|
||||
import { formatLbryUriForWeb } from 'util/uri';
|
||||
|
@ -146,7 +147,7 @@ export const doPrepareEdit = (claim: StreamClaim, uri: string, fileInfo: FileLis
|
|||
// use same values as default state
|
||||
// fee will be undefined for free content
|
||||
fee = {
|
||||
amount: 0,
|
||||
amount: '0',
|
||||
currency: 'LBC',
|
||||
},
|
||||
languages,
|
||||
|
@ -159,11 +160,11 @@ export const doPrepareEdit = (claim: StreamClaim, uri: string, fileInfo: FileLis
|
|||
const publishData: UpdatePublishFormData = {
|
||||
name,
|
||||
channel: channelName,
|
||||
bid: amount,
|
||||
bid: Number(amount),
|
||||
contentIsFree: !fee.amount,
|
||||
author,
|
||||
description,
|
||||
fee: { amount: fee.amount, currency: fee.currency },
|
||||
fee,
|
||||
languages,
|
||||
thumbnail: thumbnail ? thumbnail.url : null,
|
||||
title,
|
||||
|
@ -201,10 +202,13 @@ export const doPrepareEdit = (claim: StreamClaim, uri: string, fileInfo: FileLis
|
|||
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 });
|
||||
|
||||
const state = getState();
|
||||
const publishData = selectPublishFormValues(state);
|
||||
const myClaimForUri = selectMyClaimForUri(state);
|
||||
|
||||
const myChannels = selectMyChannelClaims(state);
|
||||
const myClaims = selectMyClaimsWithoutChannels(state);
|
||||
|
||||
|
@ -214,8 +218,9 @@ export const doPublish = (params: PublishParams) => (dispatch: Dispatch, getStat
|
|||
filePath,
|
||||
description,
|
||||
language,
|
||||
license,
|
||||
licenseUrl,
|
||||
licenseType,
|
||||
otherLicenseDescription,
|
||||
thumbnail,
|
||||
channel,
|
||||
title,
|
||||
|
@ -223,8 +228,19 @@ export const doPublish = (params: PublishParams) => (dispatch: Dispatch, getStat
|
|||
fee,
|
||||
uri,
|
||||
nsfw,
|
||||
claim,
|
||||
} = params;
|
||||
tags,
|
||||
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
|
||||
const namedChannelClaim = myChannels.find(myChannel => myChannel.name === channel);
|
||||
|
@ -244,29 +260,19 @@ export const doPublish = (params: PublishParams) => (dispatch: Dispatch, getStat
|
|||
fee_amount?: string,
|
||||
} = {
|
||||
name,
|
||||
bid: creditsToString(bid),
|
||||
title,
|
||||
license,
|
||||
languages: [language],
|
||||
description,
|
||||
tags: (claim && claim.value.tags) || [],
|
||||
locations: claim && claim.value.locations,
|
||||
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
|
||||
// Eventually we will allow users to enter their own tags on publish
|
||||
// `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 (myClaimForUri && myClaimForUri.value.release_time) {
|
||||
publishPayload.release_time = Number(myClaimForUri.value.release_time);
|
||||
}
|
||||
|
||||
if (nsfw) {
|
||||
|
@ -346,6 +352,7 @@ export const doCheckPendingPublishes = () => (dispatch: Dispatch, getState: GetS
|
|||
|
||||
const checkFileList = () => {
|
||||
Lbry.claim_list().then(claims => {
|
||||
if (claims) {
|
||||
claims.forEach(claim => {
|
||||
// If it's confirmed, check if it was pending previously
|
||||
if (claim.confirmations > 0 && pendingById[claim.claim_id]) {
|
||||
|
@ -363,6 +370,7 @@ export const doCheckPendingPublishes = () => (dispatch: Dispatch, getState: GetS
|
|||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: ACTIONS.FETCH_CLAIM_LIST_MINE_COMPLETED,
|
||||
|
|
|
@ -27,6 +27,7 @@ type PublishState = {
|
|||
bidError: ?string,
|
||||
otherLicenseDescription: string,
|
||||
licenseUrl: string,
|
||||
tags: Array<string>,
|
||||
};
|
||||
|
||||
const defaultState: PublishState = {
|
||||
|
@ -53,6 +54,7 @@ const defaultState: PublishState = {
|
|||
licenseType: 'None',
|
||||
otherLicenseDescription: 'All rights reserved',
|
||||
licenseUrl: '',
|
||||
tags: [],
|
||||
publishing: false,
|
||||
publishSuccess: false,
|
||||
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
|
||||
export const selectIsStillEditing = createSelector(
|
||||
selectPublishFormValues,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
.file-list__header {
|
||||
.claim-list__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 4.5rem;
|
||||
|
@ -31,12 +31,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
.file-list__header--small {
|
||||
.claim-list__header--small {
|
||||
height: 3rem;
|
||||
min-height: 3rem;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.file-list__dropdown {
|
||||
.claim-list__dropdown {
|
||||
background-position: 95% center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 1.2rem;
|
||||
|
@ -50,8 +51,8 @@
|
|||
background-color: lighten($lbry-black, 10%);
|
||||
}
|
||||
|
||||
.file-list__header,
|
||||
.file-list__dropdown {
|
||||
.claim-list__header,
|
||||
.claim-list__dropdown {
|
||||
background-color: lighten($lbry-black, 10%);
|
||||
|
||||
[data-mode='dark'] & {
|
||||
|
@ -59,17 +60,17 @@
|
|||
}
|
||||
}
|
||||
|
||||
.file-list__header-text {
|
||||
.claim-list__header-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.file-list__header-text,
|
||||
.file-list__dropdown {
|
||||
.claim-list__header-text,
|
||||
.claim-list__dropdown {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.file-list__alt-controls {
|
||||
.claim-list__alt-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
|
@ -80,7 +81,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.file-list__item {
|
||||
.claim-list__item {
|
||||
display: flex;
|
||||
position: relative;
|
||||
font-size: 1.3rem;
|
||||
|
@ -103,12 +104,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
.file-list__item--injected,
|
||||
.file-list__item {
|
||||
border-bottom: 1px solid rgba($lbry-teal-5, 0.1);
|
||||
.claim-list__item--injected,
|
||||
.claim-list__item + .claim-list__item {
|
||||
border-top: 1px solid rgba($lbry-teal-5, 0.1);
|
||||
}
|
||||
|
||||
.file-list__item--large {
|
||||
.claim-list__item--large {
|
||||
@include mediaThumbHoverZoom;
|
||||
font-size: 1.6rem;
|
||||
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;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.file-list__item-info {
|
||||
.claim-list__item-info {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.file-list__item-info,
|
||||
.file-list__item-properties {
|
||||
.claim-list__item-info,
|
||||
.claim-list__item-properties {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.file-list__item-properties {
|
||||
.claim-list__item-properties {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.file-list__item-title {
|
||||
.claim-list__item-title {
|
||||
font-weight: 600;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.file-list__item-tags {
|
||||
.claim-list__item-tags {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.file-list__meta {
|
||||
.claim-list__meta {
|
||||
padding: var(--spacing-medium);
|
||||
background-color: lighten($lbry-teal-5, 55%);
|
||||
}
|
||||
|
|
|
@ -1,16 +1,36 @@
|
|||
@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
|
||||
// Some items have very specific styling
|
||||
// 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
|
||||
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
border-radius: var(--input-border-radius);
|
||||
}
|
||||
|
||||
input-submit {
|
||||
align-items: center;
|
||||
}
|
||||
|
@ -21,7 +41,8 @@ input[type='number'] {
|
|||
|
||||
input[type='text'],
|
||||
input[type='number'],
|
||||
select {
|
||||
select,
|
||||
textarea {
|
||||
padding-bottom: 0.1em;
|
||||
|
||||
[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 {
|
||||
label {
|
||||
width: auto;
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
padding-top: var(--header-height);
|
||||
padding-left: var(--spacing-large);
|
||||
padding-right: var(--spacing-large);
|
||||
padding-bottom: var(--spacing-large);
|
||||
background-color: mix($lbry-white, $lbry-gray-1, 70%);
|
||||
display: flex;
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
.placeholder {
|
||||
display: flex;
|
||||
|
||||
&.file-list__item-title {
|
||||
&.claim-list__item-title {
|
||||
width: 100%;
|
||||
height: 3rem;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue