lbry-desktop/src/renderer/component/publishForm/view.jsx

638 lines
20 KiB
React
Raw Normal View History

2018-03-26 23:32:43 +02:00
// @flow
import * as React from 'react';
2018-06-14 23:31:27 +02:00
import { isNameValid, buildURI, regexInvalidURI, THUMBNAIL_STATUSES } from 'lbry-redux';
2018-03-26 23:32:43 +02:00
import { Form, FormField, FormRow, FormFieldPrice, Submit } from 'component/common/form';
import Button from 'component/button';
import ChannelSection from 'component/selectChannel';
import classnames from 'classnames';
import type { PublishParams, UpdatePublishFormData } from 'redux/reducers/publish';
import FileSelector from 'component/common/file-selector';
2018-04-02 18:39:00 +02:00
import SelectThumbnail from 'component/selectThumbnail';
2018-03-26 23:32:43 +02:00
import { COPYRIGHT, OTHER } from 'constants/licenses';
2018-03-30 07:37:09 +02:00
import { CHANNEL_NEW, CHANNEL_ANONYMOUS, MINIMUM_PUBLISH_BID } from 'constants/claim';
2018-03-26 23:32:43 +02:00
import * as icons from 'constants/icons';
2018-05-25 20:05:30 +02:00
import type { Claim } from 'types/claim';
2018-03-30 07:37:09 +02:00
import BidHelpText from './internal/bid-help-text';
import LicenseType from './internal/license-type';
2018-03-26 23:32:43 +02:00
type Props = {
publish: PublishParams => void,
filePath: ?string,
bid: ?number,
editingURI: ?string,
title: ?string,
thumbnail: ?string,
uploadThumbnailStatus: ?string,
2018-06-08 06:05:45 +02:00
thumbnailPath: ?string,
2018-03-26 23:32:43 +02:00
description: ?string,
language: string,
nsfw: boolean,
contentIsFree: boolean,
price: {
amount: number,
currency: string,
},
channel: string,
name: ?string,
tosAccepted: boolean,
updatePublishForm: UpdatePublishFormData => void,
bid: number,
nameError: ?string,
isResolvingUri: boolean,
winningBidForClaimUri: number,
2018-05-25 20:05:30 +02:00
myClaimForUri: ?Claim,
2018-03-26 23:32:43 +02:00
licenseType: string,
otherLicenseDescription: ?string,
licenseUrl: ?string,
copyrightNotice: ?string,
uri: ?string,
bidError: ?string,
publishing: boolean,
balance: number,
2018-06-12 07:11:17 +02:00
isStillEditing: boolean,
2018-03-26 23:32:43 +02:00
clearPublish: () => void,
resolveUri: string => void,
scrollToTop: () => void,
2018-07-30 20:24:40 +02:00
prepareEdit: ({ }) => void,
resetThumbnailStatus: () => void,
2018-03-26 23:32:43 +02:00
};
class PublishForm extends React.PureComponent<Props> {
constructor(props: Props) {
super(props);
2018-03-26 23:32:43 +02:00
(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);
}
componentWillMount() {
2018-07-18 17:46:21 +02:00
const { thumbnail } = this.props;
2018-07-17 19:43:43 +02:00
if (!thumbnail) {
2018-06-13 06:19:39 +02:00
this.props.resetThumbnailStatus();
}
}
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;
2018-03-26 23:32:43 +02:00
let uri;
try {
uri = buildURI({ contentName: name, channelName });
} catch (e) {
// something wrong with channel or name
}
if (uri) {
resolveUri(uri);
return uri;
2017-08-26 04:09:56 +02:00
}
return '';
2017-08-26 04:09:56 +02:00
}
2018-03-26 23:32:43 +02:00
handleFileChange(filePath: string, fileName: string) {
const { updatePublishForm, channel, name } = this.props;
const newFileParams: {
filePath: string,
name?: string,
uri?: string,
} = { filePath };
if (!name) {
const parsedFileName = fileName.replace(regexInvalidURI, '');
const uri = this.getNewUri(parsedFileName, channel);
newFileParams.name = parsedFileName;
newFileParams.uri = uri;
2018-03-26 23:32:43 +02:00
}
updatePublishForm(newFileParams);
}
2018-03-26 23:32:43 +02:00
handleNameChange(name: ?string) {
const { channel, updatePublishForm } = this.props;
2018-03-26 23:32:43 +02:00
if (!name) {
updatePublishForm({ name, nameError: undefined });
return;
}
2018-03-26 23:32:43 +02:00
if (!isNameValid(name, false)) {
updatePublishForm({
name,
nameError: __('LBRY names must contain only letters, numbers and dashes.'),
});
return;
}
2018-03-26 23:32:43 +02:00
const uri = this.getNewUri(name, channel);
updatePublishForm({
name,
uri,
2018-03-26 23:32:43 +02:00
nameError: undefined,
});
}
2018-03-26 23:32:43 +02:00
handleChannelChange(channelName: string) {
2018-05-25 20:05:30 +02:00
const { name, updatePublishForm } = this.props;
const form = { channel: channelName };
2018-05-25 20:05:30 +02:00
2018-03-26 23:32:43 +02:00
if (name) {
form.uri = this.getNewUri(name, channelName);
}
updatePublishForm(form);
}
2018-03-26 23:32:43 +02:00
handleBidChange(bid: number) {
const { balance, updatePublishForm, myClaimForUri } = this.props;
let previousBidAmount = 0;
if (myClaimForUri) {
previousBidAmount = myClaimForUri.amount;
}
const totalAvailableBidAmount = previousBidAmount + balance;
2018-03-26 23:32:43 +02:00
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');
2018-03-26 23:32:43 +02:00
} else if (bid <= MINIMUM_PUBLISH_BID) {
bidError = __('Your deposit must be higher');
2018-03-26 23:32:43 +02:00
}
2018-03-26 23:32:43 +02:00
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,
copyrightNotice,
licenseType,
licenseUrl,
otherLicenseDescription,
2018-04-04 01:46:03 +02:00
myClaimForUri,
publish,
} = this.props;
let publishingLicense;
switch (licenseType) {
case COPYRIGHT:
publishingLicense = copyrightNotice;
break;
case OTHER:
publishingLicense = otherLicenseDescription;
break;
default:
publishingLicense = licenseType;
}
const publishingLicenseUrl = licenseType === COPYRIGHT ? '' : licenseUrl;
const publishParams = {
filePath,
bid: this.props.bid,
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,
copyrightNotice,
name: this.props.name,
contentIsFree: this.props.contentIsFree,
price: this.props.price,
uri: this.props.uri,
channel: this.props.channel,
isStillEditing: this.props.isStillEditing,
};
2018-04-04 01:46:03 +02:00
// Editing a claim
if (!filePath && myClaimForUri) {
const { source } = myClaimForUri.value.stream;
publishParams.sources = source;
2018-04-04 01:46:03 +02:00
}
publish(publishParams);
}
2018-03-26 23:32:43 +02:00
checkIsFormValid() {
const {
name,
nameError,
title,
bid,
bidError,
tosAccepted,
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 &&
tosAccepted &&
!(uploadThumbnailStatus === THUMBNAIL_STATUSES.IN_PROGRESS);
return editingURI && !filePath ? isStillEditing && formValidLessFile : formValidLessFile;
}
2018-03-26 23:32:43 +02:00
renderFormErrors() {
const {
name,
nameError,
title,
bid,
bidError,
tosAccepted,
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
2018-03-26 23:32:43 +02:00
return (
!isFormValid && (
<div className="card__content card__subtitle form-field__error">
{!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 bid amount is required')}</div>}
{!!bid && bidError && <div>{bidError}</div>}
{uploadThumbnailStatus === THUMBNAIL_STATUSES.IN_PROGRESS
&& <div>{__('Please wait for thumbnail to finish uploading')}</div>}
{!tosAccepted && <div>{__('You must agree to the terms of service')}</div>}
{!!editingURI &&
!isStillEditing &&
!filePath && <div>{__('You need to reselect a file after changing the LBRY URL')}</div>}
</div>
)
2018-03-26 23:32:43 +02:00
);
}
render() {
2018-03-26 23:32:43 +02:00
const {
filePath,
editingURI,
title,
thumbnail,
uploadThumbnailStatus,
2018-03-26 23:32:43 +02:00
description,
language,
nsfw,
contentIsFree,
price,
channel,
name,
tosAccepted,
updatePublishForm,
bid,
nameError,
isResolvingUri,
winningBidForClaimUri,
myClaimForUri,
licenseType,
otherLicenseDescription,
licenseUrl,
copyrightNotice,
uri,
bidError,
publishing,
clearPublish,
2018-06-08 06:05:45 +02:00
thumbnailPath,
resetThumbnailStatus,
2018-06-12 07:11:17 +02:00
isStillEditing,
2018-03-26 23:32:43 +02:00
} = this.props;
const formDisabled = (!filePath && !editingURI) || publishing;
const formValid = this.checkIsFormValid();
let submitLabel;
if (isStillEditing) {
submitLabel = !publishing ? __('Edit') : __('Editing...');
} else {
submitLabel = !publishing ? __('Publish') : __('Publishing...');
2017-09-05 03:03:48 +02:00
}
return (
2018-03-26 23:32:43 +02:00
<Form onSubmit={this.handlePublish}>
2018-06-28 22:13:59 +02:00
<section className={classnames('card card--section', { 'card--disabled': publishing })}>
2018-03-26 23:32:43 +02:00
<div className="card__title">{__('Content')}</div>
<div className="card__subtitle">
{isStillEditing ? __('Editing a claim') : __('What are you publishing?')}
2018-07-30 20:24:40 +02:00
{' '}{__(
2018-07-30 21:24:00 +02:00
'Read our'
2018-07-30 20:24:40 +02:00
)}{' '}
<Button button="link" label={__('FAQ')} href="https://lbry.io/faq/how-to-publish" />{' '}
{__(
'to learn more.'
)}
2018-03-26 23:32:43 +02:00
</div>
{(filePath || !!editingURI) && (
<div className="card-media__internal-links">
<Button
button="inverse"
icon={icons.CLOSE}
label={__('Clear')}
onClick={clearPublish}
/>
</div>
2018-03-26 23:32:43 +02:00
)}
2018-07-18 17:46:21 +02:00
<div className="card__content">
<FileSelector currentPath={filePath} onFileChosen={this.handleFileChange} />
{!!isStillEditing &&
name && (
<p className="card__content card__subtitle">
{__("If you don't choose a file, the file from your existing claim")}
{` "${name}" `}
{__('will be used.')}
</p>
)}
</div>
2018-03-26 23:32:43 +02:00
</section>
<div className={classnames({ 'card--disabled': formDisabled })}>
<section className="card card--section">
<FormRow>
<FormField
stretch
type="text"
name="content_title"
label={__('Title')}
placeholder={__('Titular Title')}
disabled={formDisabled}
value={title}
onChange={e => updatePublishForm({ title: e.target.value })}
/>
</FormRow>
<FormRow padded>
<FormField
stretch
type="markdown"
name="content_description"
label={__('Description')}
placeholder={__('Description of your content')}
value={description}
disabled={formDisabled}
onChange={text => updatePublishForm({ description: text })}
/>
</FormRow>
</section>
2018-06-08 06:05:45 +02:00
<section className="card card--section">
<div className="card__title">{__('Thumbnail')}</div>
<div className="card__subtitle">
2018-06-14 23:31:27 +02:00
{uploadThumbnailStatus === THUMBNAIL_STATUSES.API_DOWN ? (
2018-06-26 19:25:54 +02:00
__('Enter a URL for your thumbnail.')
) : (
2018-07-30 20:24:40 +02:00
<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>
)}
2018-06-08 06:05:45 +02:00
</div>
<SelectThumbnail
thumbnailPath={thumbnailPath}
thumbnail={thumbnail}
uploadThumbnailStatus={uploadThumbnailStatus}
updatePublishForm={updatePublishForm}
formDisabled={formDisabled}
resetThumbnailStatus={resetThumbnailStatus}
/>
</section>
2018-03-26 23:32:43 +02:00
<section className="card card--section">
<div className="card__title">{__('Price')}</div>
<div className="card__subtitle">{__('How much will this content cost?')}</div>
<div className="card__content">
2018-03-26 23:32:43 +02:00
<FormField
type="radio"
2018-03-26 23:32:43 +02:00
name="content_free"
postfix={__('Free')}
checked={contentIsFree}
disabled={formDisabled}
onChange={() => updatePublishForm({ contentIsFree: true })}
/>
<FormField
type="radio"
2018-03-26 23:32:43 +02:00
name="content_cost"
postfix={__('Choose price')}
checked={!contentIsFree}
disabled={formDisabled}
onChange={() => updatePublishForm({ contentIsFree: false })}
/>
2018-04-12 01:41:53 +02:00
{!contentIsFree && (
<FormFieldPrice
name="content_cost_amount"
min="0"
price={price}
onChange={newPrice => updatePublishForm({ price: newPrice })}
/>
)}
2018-03-26 23:32:43 +02:00
{price.currency !== 'LBC' && (
<p className="form-field__help">
2017-11-21 20:51:12 +01:00
{__(
'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.'
2017-11-21 20:51:12 +01:00
)}
2018-03-26 23:32:43 +02:00
</p>
)}
2017-10-14 21:41:04 +02:00
</div>
</section>
2018-03-26 23:32:43 +02:00
<section className="card card--section">
<div className="card__title">{__('Anonymous or under a channel?')}</div>
<p className="card__subtitle">
{__('This is a username or handle that your content can be found under.')}{' '}
{__('Ex. @Marvel, @TheBeatles, @BooksByJoe')}
</p>
<ChannelSection channel={channel} onChannelChange={this.handleChannelChange} />
</section>
2017-11-21 20:51:12 +01:00
2018-03-26 23:32:43 +02:00
<section className="card card--section">
<div className="card__title">{__('Where can people find this content?')}</div>
<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.io/faq/naming" />
</p>
<div className="card__content">
<FormRow>
<FormField
stretch
prefix={`lbry://${
!channel || channel === CHANNEL_ANONYMOUS || channel === CHANNEL_NEW
? ''
: `${channel}/`
2018-07-30 20:24:40 +02:00
}`}
2017-11-21 20:51:12 +01:00
type="text"
2018-03-26 23:32:43 +02:00
name="content_name"
placeholder="myname"
value={name}
onChange={event => this.handleNameChange(event.target.value)}
error={nameError}
helper={
<BidHelpText
2018-06-12 07:11:17 +02:00
isStillEditing={isStillEditing}
uri={uri}
2018-03-26 23:32:43 +02:00
editingURI={editingURI}
isResolvingUri={isResolvingUri}
winningBidForClaimUri={winningBidForClaimUri}
myClaimForUri={myClaimForUri}
2018-03-26 23:32:43 +02:00
onEditMyClaim={this.editExistingClaim}
/>
}
2017-11-21 20:51:12 +01:00
/>
2018-03-26 23:32:43 +02:00
</FormRow>
</div>
<div className={classnames('card__content', { 'card--disabled': !name })}>
<FormField
className="input--price-amount"
type="number"
name="content_bid"
step="any"
label={__('Deposit')}
postfix="LBC"
value={bid}
error={bidError}
min="0"
disabled={!name}
onChange={event => this.handleBidChange(parseFloat(event.target.value))}
helper={__('This LBC remains yours and the deposit can be undone at any time.')}
placeholder={winningBidForClaimUri ? winningBidForClaimUri + 0.1 : 0.1}
/>
</div>
</section>
2018-03-26 23:32:43 +02:00
<section className="card card--section">
<FormRow>
<FormField
type="checkbox"
name="content_is_mature"
postfix={__('Mature audiences only')}
checked={nsfw}
onChange={event => updatePublishForm({ nsfw: event.target.checked })}
/>
2018-03-26 23:32:43 +02:00
</FormRow>
<FormRow padded>
<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>
</FormField>
</FormRow>
<LicenseType
licenseType={licenseType}
otherLicenseDescription={otherLicenseDescription}
licenseUrl={licenseUrl}
copyrightNotice={copyrightNotice}
handleLicenseChange={(newLicenseType, newLicenseUrl) =>
updatePublishForm({
licenseType: newLicenseType,
licenseUrl: newLicenseUrl,
})
}
handleLicenseDescriptionChange={event =>
updatePublishForm({
otherLicenseDescription: event.target.value,
})
}
handleLicenseUrlChange={event =>
updatePublishForm({ licenseUrl: event.target.value })
}
handleCopyrightNoticeChange={event =>
updatePublishForm({ copyrightNotice: event.target.value })
}
/>
</section>
2018-03-26 23:32:43 +02:00
<section className="card card--section">
<div className="card__title">{__('Terms of Service')}</div>
<div className="card__content">
2018-03-26 23:32:43 +02:00
<FormField
name="lbry_tos"
type="checkbox"
checked={tosAccepted}
postfix={
<span>
{__('I agree to the')}{' '}
2018-03-26 23:32:43 +02:00
<Button
button="link"
href="https://www.lbry.io/termsofservice"
label={__('LBRY terms of service')}
/>
</span>
}
2018-03-26 23:32:43 +02:00
onChange={event => updatePublishForm({ tosAccepted: event.target.checked })}
/>
</div>
</section>
2018-03-26 23:32:43 +02:00
<div className="card__actions">
<Submit
label={submitLabel}
disabled={
2018-06-14 23:31:27 +02:00
formDisabled ||
!formValid ||
uploadThumbnailStatus === THUMBNAIL_STATUSES.IN_PROGRESS
}
/>
2018-03-26 23:32:43 +02:00
<Button button="alt" onClick={this.handleCancelPublish} label={__('Cancel')} />
</div>
2018-03-26 23:32:43 +02:00
{!formDisabled && !formValid && this.renderFormErrors()}
</div>
</Form>
);
}
}
export default PublishForm;