cleanup and add tags to publish page

This commit is contained in:
Sean Yesmunt 2019-06-26 13:31:15 -04:00
parent f39ca4dd57
commit f9269ae969
59 changed files with 1265 additions and 913 deletions

View file

@ -48,4 +48,5 @@ declare type PublishParams = {
},
claim: StreamClaim,
nsfw: boolean,
tags: Array<Tag>,
};

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

@ -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" />

View file

@ -13,7 +13,7 @@ const BLUE_COLOR = '#49b2e2';
type Props = {
icon: string,
tooltip?: string, // tooltip direction
tooltip?: boolean,
iconColor?: string,
size?: number,
className?: string,

View file

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

View file

@ -3,10 +3,12 @@ import Button from 'component/button';
export default function UnsupportedOnWeb() {
return (
<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>
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>
)
);
}

View file

@ -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!')}

View file

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

View file

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

View file

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

View file

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

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

View file

@ -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,

View 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;

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

View 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;

View file

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

View file

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

View file

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

View file

@ -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,35 +52,49 @@ 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);
function PublishForm(props: Props) {
const {
thumbnail,
name,
channel,
editingURI,
resolveUri,
title,
bid,
uploadThumbnailStatus,
resetThumbnailStatus,
updatePublishForm,
filePath,
publishing,
clearPublish,
isStillEditing,
tags,
publish,
} = props;
const formDisabled = (!filePath && !editingURI) || publishing;
// If they are editing, they don't need a new file chosen
const formValidLessFile = name && title && bid && !(uploadThumbnailStatus === THUMBNAIL_STATUSES.IN_PROGRESS);
const formValid = editingURI && !filePath ? isStillEditing && formValidLessFile : formValidLessFile;
(this: any).handleFileChange = this.handleFileChange.bind(this);
(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);
let submitLabel;
if (isStillEditing) {
submitLabel = !publishing ? __('Edit') : __('Editing...');
} else {
submitLabel = !publishing ? __('Publish') : __('Publishing...');
}
componentDidMount() {
const { thumbnail, name, channel, editingURI } = this.props;
useEffect(() => {
if (!thumbnail) {
this.props.resetThumbnailStatus();
resetThumbnailStatus();
}
if (editingURI) {
this.getNewUri(name, channel);
}
}
}, [thumbnail, resetThumbnailStatus]);
getNewUri(name: string, channel: string) {
const { resolveUri } = this.props;
// 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;
@ -89,523 +102,78 @@ class PublishForm extends React.PureComponent<Props> {
let uri;
try {
uri = buildURI({ contentName: name, channelName });
} catch (e) {
// something wrong with channel or name
} 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) {
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;
updatePublishForm({ uri });
}
}, [name, channel, resolveUri, updatePublishForm]);
return '';
}
return (
<Fragment>
<UnsupportedOnWeb />
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() {
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>
)}
<PublishFile />
<div className={classnames({ 'card--disabled': formDisabled })}>
<PublishText disabled={formDisabled} />
<div className="card card--section">
{/* This should probably be PublishThumbnail */}
<SelectThumbnail />
</div>
)
);
}
render() {
const {
filePath,
editingURI,
title,
thumbnail,
uploadThumbnailStatus,
description,
language,
nsfw,
contentIsFree,
fee,
channel,
name,
updatePublishForm,
bid,
nameError,
isResolvingUri,
winningBidForClaimUri,
myClaimForUri,
licenseType,
otherLicenseDescription,
licenseUrl,
uri,
bidError,
publishing,
clearPublish,
thumbnailPath,
resetThumbnailStatus,
isStillEditing,
amountNeededForTakeover,
balance,
} = this.props;
const formDisabled = (!filePath && !editingURI) || publishing;
const formValid = this.checkIsFormValid();
let submitLabel;
if (isStillEditing) {
submitLabel = !publishing ? __('Edit') : __('Editing...');
} else {
submitLabel = !publishing ? __('Publish') : __('Publishing...');
}
const shortUri = buildURI({ contentName: name });
return (
<React.Fragment>
{IS_WEB && <UnsupportedOnWeb />}
<Form onSubmit={this.handlePublish}>
<section
className={classnames('card card--section', {
'card--disabled': IS_WEB || publishing || balance === 0,
})}
>
<header className="card__header">
<h2 className="card__title card__title--flex-between">
{__('Content')}
{(filePath || !!editingURI) && (
<Button button="inverse" icon={ICONS.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>
<div className="card__content">
<FileSelector currentPath={filePath} onFileChosen={this.handleFileChange} />
{!!isStillEditing && name && (
<p className="help">
{__("If you don't choose a file, the file from your existing claim")}
{` "${name}" `}
{__('will be used.')}
</p>
)}
</div>
</section>
<div className={classnames({ 'card--disabled': formDisabled })}>
<section className="card card--section">
<div className="card__content">
<FormField
type="text"
name="content_title"
label={__('Title')}
placeholder={__('Titular Title')}
disabled={formDisabled}
value={title}
onChange={e => updatePublishForm({ title: e.target.value })}
/>
<FormField
type="markdown"
name="content_description"
label={__('Description')}
placeholder={__('Description of your content')}
value={description}
disabled={formDisabled}
onChange={text => updatePublishForm({ description: text })}
/>
</div>
</section>
<section className="card card--section">
<header className="card__header">
<h2 className="card__title">{__('Thumbnail')}</h2>
<p className="card__subtitle">
{(uploadThumbnailStatus === undefined && __('You should reselect your file to choose a thumbnail')) ||
(uploadThumbnailStatus === THUMBNAIL_STATUSES.API_DOWN ? (
__('Enter a URL for your thumbnail.')
) : (
<React.Fragment>
{__('Upload your thumbnail (.png/.jpg/.jpeg/.gif) to')}{' '}
<Button button="link" label={__('spee.ch')} href="https://spee.ch/about" />.{' '}
{__('Recommended size: 800x450 (16:9)')}
</React.Fragment>
))}
</p>
</header>
<SelectThumbnail
filePath={filePath}
thumbnailPath={thumbnailPath}
thumbnail={thumbnail}
uploadThumbnailStatus={uploadThumbnailStatus}
updatePublishForm={updatePublishForm}
formDisabled={formDisabled}
resetThumbnailStatus={resetThumbnailStatus}
/>
</section>
<section className="card card--section">
<header className="card__header">
<h2 className="card__title">{__('Price')}</h2>
<p className="card__subtitle">{__('How much will this content cost?')}</p>
</header>
<div className="card__content">
<FormField
type="radio"
name="content_free"
label={__('Free')}
checked={contentIsFree}
disabled={formDisabled}
onChange={() => updatePublishForm({ contentIsFree: true })}
/>
<FormField
type="radio"
name="content_cost"
label={__('Choose price')}
checked={!contentIsFree}
disabled={formDisabled}
onChange={() => updatePublishForm({ contentIsFree: false })}
/>
{!contentIsFree && (
<FormFieldPrice
name="content_cost_amount"
min="0"
price={fee}
onChange={newFee => updatePublishForm({ fee: newFee })}
/>
)}
{fee && fee.currency !== 'LBC' && (
<p className="form-field__help">
{__(
'All content fees are charged in LBC. For non-LBC payment methods, the number of credits charged will be adjusted based on the value of LBRY credits at the time of purchase.'
)}
</p>
)}
</div>
</section>
<section className="card card--section">
<header className="card__header">
<h2 className="card__title">{__('Anonymous or under a channel?')}</h2>
<p className="card__subtitle">
{__('This is a username or handle that your content can be found under.')}{' '}
{__('Ex. @Marvel, @TheBeatles, @BooksByJoe')}
</p>
</header>
<div className="card__content">
<ChannelSection channel={channel} onChannelChange={this.handleChannelChange} />
</div>
</section>
<section className="card card--section">
<header className="card__header">
<h2 className="card__title">{__('Where can people find this content?')}</h2>
<p className="card__subtitle">
{__('The LBRY URL is the exact address where people find your content (ex. lbry://myvideo).')}{' '}
<Button button="link" label={__('Learn more')} href="https://lbry.com/faq/naming" />
</p>
</header>
<div className="card__content">
<fieldset-group class="fieldset-group--smushed fieldset-group--disabled-prefix">
<fieldset-section>
<label>{__('Name')}</label>
<span className="form-field__prefix">{`lbry://${
!channel || channel === CHANNEL_ANONYMOUS || channel === CHANNEL_NEW ? '' : `${channel}/`
}`}</span>
</fieldset-section>
<FormField
type="text"
name="content_name"
value={name}
onChange={event => this.handleNameChange(event.target.value)}
error={nameError}
/>
</fieldset-group>
<div className="form-field__help">
<NameHelpText
isStillEditing={isStillEditing}
uri={uri}
myClaimForUri={myClaimForUri}
onEditMyClaim={this.editExistingClaim}
/>
</div>
</div>
<div className={classnames('card__content', { 'card--disabled': !name })}>
<FormField
className="form-field--price-amount"
type="number"
name="content_bid"
step="any"
label={__('Deposit (LBC)')}
postfix="LBC"
value={bid}
error={bidError}
min="0"
disabled={!name}
onChange={event => this.handleBidChange(parseFloat(event.target.value))}
placeholder={winningBidForClaimUri ? winningBidForClaimUri + 0.1 : 0.1}
helper={
<BidHelpText
uri={shortUri}
isResolvingUri={isResolvingUri}
amountNeededForTakeover={amountNeededForTakeover}
/>
}
/>
</div>
</section>
<section className="card card--section">
<div className="card__content">
<FormField
type="checkbox"
name="content_is_mature"
label={__('Mature audiences only')}
checked={nsfw}
onChange={() => updatePublishForm({ nsfw: !nsfw })}
/>
<FormField
label={__('Language')}
type="select"
name="content_language"
value={language}
onChange={event => updatePublishForm({ language: event.target.value })}
>
<option value="en">{__('English')}</option>
<option value="zh">{__('Chinese')}</option>
<option value="fr">{__('French')}</option>
<option value="de">{__('German')}</option>
<option value="jp">{__('Japanese')}</option>
<option value="ru">{__('Russian')}</option>
<option value="es">{__('Spanish')}</option>
<option value="id">{__('Indonesian')}</option>
<option value="it">{__('Italian')}</option>
<option value="nl">{__('Dutch')}</option>
<option value="tr">{__('Turkish')}</option>
<option value="pl">{__('Polish')}</option>
<option value="ms">{__('Malay')}</option>
</FormField>
<LicenseType
licenseType={licenseType}
otherLicenseDescription={otherLicenseDescription}
licenseUrl={licenseUrl}
handleLicenseChange={(newLicenseType, newLicenseUrl) =>
updatePublishForm({
licenseType: newLicenseType,
licenseUrl: newLicenseUrl,
})
}
handleLicenseDescriptionChange={event =>
updatePublishForm({
otherLicenseDescription: event.target.value,
})
}
handleLicenseUrlChange={event => updatePublishForm({ licenseUrl: event.target.value })}
/>
</div>
</section>
<section className="card card--section">
<div className="card__actions">
<Submit
label={submitLabel}
disabled={formDisabled || !formValid || uploadThumbnailStatus === THUMBNAIL_STATUSES.IN_PROGRESS}
/>
<Button button="link" onClick={this.handleCancelPublish} label={__('Cancel')} />
</div>
<p className="help">
{__('By continuing, you accept the')}{' '}
<Button button="link" href="https://www.lbry.com/termsofservice" label={__('LBRY Terms of Service')} />.
</p>
</section>
<div 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 className="card card--section">
<div className="card__content">
<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>
</div>
</section>
{!formDisabled && !formValid && this.renderFormErrors()}
</Form>
</React.Fragment>
);
}
<PublishName disabled={formDisabled} />
<PublishPrice disabled={formDisabled} />
<PublishAdditionalOptions disabled={formDisabled} />
<section className="card card--section">
{!formDisabled && !formValid && <PublishFormErrors />}
<div className="card__actions">
<Button
button="primary"
onClick={publish}
label={submitLabel}
disabled={formDisabled || !formValid || uploadThumbnailStatus === THUMBNAIL_STATUSES.IN_PROGRESS}
/>
<Button button="link" onClick={clearPublish} label={__('Cancel')} />
</div>
<p className="help">
{__('By continuing, you accept the')}{' '}
<Button button="link" href="https://www.lbry.com/termsofservice" label={__('LBRY Terms of Service')} />.
</p>
</section>
</div>
</Fragment>
);
}
export default PublishForm;

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

View 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;

View 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;

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

View 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;

View 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;

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

View 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;

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

View 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;

View file

@ -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="/" />} />

View file

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

View file

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

View file

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

View file

@ -15,33 +15,48 @@ 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 (!unfollowedTags.includes(newTag)) {
doAddTag(newTag);
}
if (onSelect) {
onSelect({ name: newTag });
} else {
if (!unfollowedTags.includes(newTag)) {
doAddTag(newTag);
}
if (!followedTags.includes(newTag)) {
doToggleTagFollow(newTag);
if (!followedTags.includes(newTag)) {
doToggleTagFollow(newTag);
}
}
}
function handleTagClick(tag) {
if (onSelect) {
onSelect(tag);
} else {
doToggleTagFollow(tag);
}
}
@ -59,7 +74,7 @@ export default function TagSelect(props: Props) {
<ul className="tags">
{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>}

View file

@ -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">
<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>
{title !== false && (
<h2 className="card__title card__title--flex-between">
{title}
{showClose && !hasClosed && <Button button="close" icon={ICONS.REMOVE} onClick={handleClose} />}
</h2>
)}
<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>
)

View file

@ -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 {

View file

@ -20,3 +20,4 @@ export const SEARCH = 'search';
export const TRANSACTIONS = 'transactions';
export const TAGS = 'tags';
export const WALLET = 'wallet';
export const FOLLOWING = 'following';

View file

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

View file

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

View 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;

View file

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

View file

@ -1,3 +1,4 @@
// @flow
import React, { Fragment } from 'react';
import PublishForm from 'component/publishForm';
import Page from 'component/page';
@ -6,64 +7,67 @@ 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>
{balance === 0 && (
<Fragment>
<Yrbl
title={__("You can't publish things quite yet")}
subtitle={
<Fragment>
<p>
{__(
'LBRY uses a blockchain, which is a fancy way of saying that users (you) are in control of your data.'
)}
</p>
<p>
{__('Because of the blockchain, some actions require LBRY credits')} (
<LbcSymbol />
).
</p>
<p>
<LbcSymbol />{' '}
{__(
'allows you to do some neat things, like paying your favorite creators for their content. And no company can stop you.'
)}
</p>
</Fragment>
}
/>
<section className="card card--section">
<header className="card__header">
<h1 className="card__title">{__('LBRY Credits Required')}</h1>
</header>
<p className="card__subtitle">
{__(' There are a variety of ways to get credits, including more than')}{' '}
<CreditAmount inheritStyle amount={totalRewardRounded} />{' '}
{__('in free rewards for participating in the LBRY beta.')}
</p>
<div className="card__actions">
<Button button="link" navigate="/$/rewards" label={__('Checkout the rewards')} />
</div>
</section>
</Fragment>
)}
<PublishForm {...this.props} scrollToTop={this.scrollToTop} />
</Page>
);
}
return (
<Page>
{balance === 0 && (
<Fragment>
<Yrbl
title={__("You can't publish things quite yet")}
subtitle={
<Fragment>
<p>
{__(
'LBRY uses a blockchain, which is a fancy way of saying that users (you) are in control of your data.'
)}
</p>
<p>
{__('Because of the blockchain, some actions require LBRY credits')} (
<LbcSymbol />
).
</p>
<p>
<LbcSymbol />{' '}
{__(
'allows you to do some neat things, like paying your favorite creators for their content. And no company can stop you.'
)}
</p>
</Fragment>
}
/>
<section className="card card--section">
<header className="card__header">
<h1 className="card__title">{__('LBRY Credits Required')}</h1>
</header>
<p className="card__subtitle">
{__(' There are a variety of ways to get credits, including more than')}{' '}
<CreditAmount inheritStyle amount={totalRewardRounded} />{' '}
{__('in free rewards for participating in the LBRY beta.')}
</p>
<div className="card__actions">
<Button button="link" navigate="/$/rewards" label={__('Checkout the rewards')} />
</div>
</section>
</Fragment>
)}
<PublishForm scrollToTop={scrollToTop} />
</Page>
);
}
export default PublishPage;

View file

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

View file

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

View file

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

View file

@ -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,23 +352,25 @@ export const doCheckPendingPublishes = () => (dispatch: Dispatch, getState: GetS
const checkFileList = () => {
Lbry.claim_list().then(claims => {
claims.forEach(claim => {
// If it's confirmed, check if it was pending previously
if (claim.confirmations > 0 && pendingById[claim.claim_id]) {
delete pendingById[claim.claim_id];
if (claims) {
claims.forEach(claim => {
// If it's confirmed, check if it was pending previously
if (claim.confirmations > 0 && pendingById[claim.claim_id]) {
delete pendingById[claim.claim_id];
// If it's confirmed, check if we should notify the user
if (selectosNotificationsEnabled(getState())) {
const notif = new window.Notification('LBRY Publish Complete', {
body: `${claim.value.title} has been published to lbry://${claim.name}. Click here to view it`,
silent: false,
});
notif.onclick = () => {
dispatch(push(formatLbryUriForWeb(claim.permanent_url)));
};
// If it's confirmed, check if we should notify the user
if (selectosNotificationsEnabled(getState())) {
const notif = new window.Notification('LBRY Publish Complete', {
body: `${claim.value.title} has been published to lbry://${claim.name}. Click here to view it`,
silent: false,
});
notif.onclick = () => {
dispatch(push(formatLbryUriForWeb(claim.permanent_url)));
};
}
}
}
});
});
}
dispatch({
type: ACTIONS.FETCH_CLAIM_LIST_MINE_COMPLETED,

View file

@ -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,

View file

@ -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,

View file

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

View file

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

View file

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

View file

@ -9,7 +9,7 @@
.placeholder {
display: flex;
&.file-list__item-title {
&.claim-list__item-title {
width: 100%;
height: 3rem;
}