lbry-desktop/src/ui/component/publishForm/view.jsx
Thomas Zarebczan 3d27b4f2d9 moar release
Fixes claim editing while keeping any existing tags + setting/clearing fee.
Fixes not being able to abandon claims if file was deleted

Beware of lint errors..some old and new. Everything works and we should clean them up later once tags are properly integrated.
2019-05-09 19:26:58 -04:00

607 lines
21 KiB
JavaScript

// @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 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';
type Props = {
publish: PublishParams => void,
filePath: ?string,
bid: ?number,
editingURI: ?string,
title: ?string,
thumbnail: ?string,
uploadThumbnailStatus: ?string,
thumbnailPath: ?string,
description: ?string,
language: string,
nsfw: boolean,
contentIsFree: boolean,
fee: {
amount: number,
currency: string,
},
channel: string,
name: ?string,
updatePublishForm: UpdatePublishFormData => void,
nameError: ?string,
isResolvingUri: boolean,
winningBidForClaimUri: number,
myClaimForUri: ?StreamClaim,
licenseType: string,
otherLicenseDescription: ?string,
licenseUrl: ?string,
uri: ?string,
bidError: ?string,
publishing: boolean,
balance: number,
isStillEditing: boolean,
clearPublish: () => void,
resolveUri: string => void,
scrollToTop: () => void,
prepareEdit: (claim: any, uri: string) => void,
resetThumbnailStatus: () => void,
amountNeededForTakeover: ?number,
};
class PublishForm extends React.PureComponent<Props> {
constructor(props: Props) {
super(props);
(this: any).handleFileChange = this.handleFileChange.bind(this);
(this: any).checkIsFormValid = this.checkIsFormValid.bind(this);
(this: any).renderFormErrors = this.renderFormErrors.bind(this);
(this: any).handlePublish = this.handlePublish.bind(this);
(this: any).handleCancelPublish = this.handleCancelPublish.bind(this);
(this: any).handleNameChange = this.handleNameChange.bind(this);
(this: any).handleChannelChange = this.handleChannelChange.bind(this);
(this: any).editExistingClaim = this.editExistingClaim.bind(this);
(this: any).getNewUri = this.getNewUri.bind(this);
}
componentDidMount() {
const { thumbnail } = this.props;
if (!thumbnail) {
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;
// We are only going to store the full uri, but we need to resolve the uri with and without the channel name
let uri;
try {
uri = buildURI({ contentName: name, channelName });
} catch (e) {
// something wrong with channel or name
}
if (uri) {
if (channelName) {
// resolve without the channel name so we know the winning bid for it
const uriLessChannel = buildURI({ contentName: name });
resolveUri(uriLessChannel);
}
resolveUri(uri);
return uri;
}
return '';
}
handleFileChange(filePath: string, fileName: string) {
const { updatePublishForm, channel, name } = this.props;
const newFileParams: UpdatePublishFormData = { filePath };
if (!name) {
const parsedFileName = fileName.replace(regexInvalidURI, '');
const uri = this.getNewUri(parsedFileName, channel);
newFileParams.name = parsedFileName;
newFileParams.uri = uri;
}
updatePublishForm(newFileParams);
}
handleNameChange(name: ?string) {
const { channel, updatePublishForm } = this.props;
if (!name) {
updatePublishForm({ name: '', nameError: __('A name is required.') });
return;
}
if (!isNameValid(name, false)) {
updatePublishForm({
name,
nameError: __('LBRY names must contain only letters, numbers and dashes.'),
});
return;
}
const uri = this.getNewUri(name, channel);
updatePublishForm({
name,
uri,
nameError: undefined,
});
}
handleChannelChange(channelName: string) {
const { name, updatePublishForm } = this.props;
const form: UpdatePublishFormData = { channel: channelName };
if (name) {
form.uri = this.getNewUri(name, channelName);
}
updatePublishForm(form);
}
handleBidChange(bid: number) {
const { balance, updatePublishForm, myClaimForUri } = this.props;
let previousBidAmount = 0;
if (myClaimForUri) {
previousBidAmount = 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,
feeAmount: this.props.fee.amount,
feeCurrency: this.props.fee.currency,
uri: this.props.uri || undefined,
channel: this.props.channel,
isStillEditing: this.props.isStillEditing,
claim: this.props.myClaimForUri,
};
publish(publishParams);
}
checkIsFormValid() {
const {
name,
nameError,
title,
bid,
bidError,
editingURI,
isStillEditing,
filePath,
uploadThumbnailStatus,
} = this.props;
// If they are editing, they don't need a new file chosen
const formValidLessFile =
name && !nameError && title && bid && !bidError && !(uploadThumbnailStatus === THUMBNAIL_STATUSES.IN_PROGRESS);
return editingURI && !filePath ? isStillEditing && formValidLessFile : formValidLessFile;
}
renderFormErrors() {
const {
name,
nameError,
title,
bid,
bidError,
editingURI,
filePath,
isStillEditing,
uploadThumbnailStatus,
} = this.props;
const isFormValid = this.checkIsFormValid();
// These are extra help
// If there is an error it will be presented as an inline error as well
return (
!isFormValid && (
<div className="card__content error-text">
{!title && <div>{__('A title is required')}</div>}
{!name && <div>{__('A URL is required')}</div>}
{name && nameError && <div>{__('The URL you created is not valid')}</div>}
{!bid && <div>{__('A deposit amount is required')}</div>}
{!!bid && bidError && <div>{bidError}</div>}
{uploadThumbnailStatus === THUMBNAIL_STATUSES.IN_PROGRESS && (
<div>{__('Please wait for thumbnail to finish uploading')}</div>
)}
{!!editingURI && !isStillEditing && !filePath && (
<div>{__('You need to reselect a file after changing the LBRY URL')}</div>
)}
</div>
)
);
}
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.CLOSE} label={__('Clear')} onClick={clearPublish} />
)}
</h2>
<p className="card__subtitle">
{isStillEditing ? __('You are currently editing a claim.') : __('What are you publishing?')}{' '}
{__('Read our')} <Button button="link" label={__('FAQ')} href="https://lbry.com/faq/how-to-publish" />{' '}
{__('to learn more.')}
</p>
</header>
<div className="card__content">
<FileSelector currentPath={filePath} onFileChosen={this.handleFileChange} />
{!!isStillEditing && name && (
<p className="help">
{__("If you don't choose a file, the file from your existing claim")}
{` "${name}" `}
{__('will be used.')}
</p>
)}
</div>
</section>
<div className={classnames({ 'card--disabled': formDisabled })}>
<section className="card card--section">
<div className="card__content">
<FormField
type="text"
name="content_title"
label={__('Title')}
placeholder={__('Titular Title')}
disabled={formDisabled}
value={title}
onChange={e => updatePublishForm({ title: e.target.value })}
/>
<FormField
type="markdown"
name="content_description"
label={__('Description')}
placeholder={__('Description of your content')}
value={description}
disabled={formDisabled}
onChange={text => updatePublishForm({ description: text })}
/>
</div>
</section>
<section className="card card--section">
<header className="card__header">
<h2 className="card__title">{__('Thumbnail')}</h2>
<p className="card__subtitle">
{uploadThumbnailStatus === 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
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.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>
{!formDisabled && !formValid && this.renderFormErrors()}
</Form>
</React.Fragment>
);
}
}
export default PublishForm;