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

872 lines
26 KiB
React
Raw Normal View History

import React from 'react';
import lbry from 'lbry';
import { isNameValid, buildURI, regexInvalidURI } from 'lbryURI';
import FormField from 'component/formField';
import { Form, FormRow, Submit } from 'component/form.js';
import Link from 'component/link';
import FormFieldPrice from 'component/formFieldPrice';
import Modal from 'modal/modal';
import { BusyMessage } from 'component/common';
import ChannelSection from './internal/channelSection';
class PublishForm extends React.PureComponent {
constructor(props) {
super(props);
this._requiredFields = ['name', 'bid', 'meta_title', 'tosAgree'];
this._defaultCopyrightNotice = 'All rights reserved.';
2017-08-30 15:02:40 +02:00
this._defaultPaidPrice = 0.01;
this.state = {
id: null,
uri: null,
rawName: '',
name: '',
bid: 10,
hasFile: false,
feeAmount: '',
feeCurrency: 'LBC',
channel: 'anonymous',
newChannelName: '@',
newChannelBid: 10,
meta_title: '',
meta_thumbnail: '',
meta_description: '',
meta_language: 'en',
meta_nsfw: '0',
licenseType: '',
copyrightNotice: this._defaultCopyrightNotice,
otherLicenseDescription: '',
otherLicenseUrl: '',
tosAgree: false,
prefillDone: false,
uploadProgress: 0.0,
uploaded: false,
errorMessage: null,
submitting: false,
creatingChannel: false,
modal: null,
isFee: false,
customUrl: false,
source: null,
mode: 'publish',
};
}
_updateChannelList(channel) {
const { fetchingChannels, fetchChannelListMine } = this.props;
if (!fetchingChannels) fetchChannelListMine();
}
handleSubmit() {
this.setState({
submitting: true,
});
const checkFields = this._requiredFields;
if (!this.myClaimExists()) {
checkFields.unshift('file');
}
let missingFieldFound = false;
for (const fieldName of checkFields) {
const field = this.refs[fieldName];
if (field) {
if (field.getValue() === '' || field.getValue() === false) {
field.showRequiredError();
if (!missingFieldFound) {
field.focus();
missingFieldFound = true;
}
} else {
field.clearError();
}
}
}
if (missingFieldFound) {
this.setState({
submitting: false,
});
return;
}
const metadata = {};
for (const metaField of ['title', 'description', 'thumbnail', 'language']) {
const value = this.state[`meta_${metaField}`];
if (value) {
metadata[metaField] = value;
}
}
metadata.license = this.getLicense();
metadata.licenseUrl = this.getLicenseUrl();
metadata.nsfw = !!parseInt(this.state.meta_nsfw);
const doPublish = () => {
const publishArgs = {
name: this.state.name,
bid: parseFloat(this.state.bid),
metadata,
...(this.state.channel != 'new' && this.state.channel != 'anonymous'
? { channel_name: this.state.channel }
: {}),
};
const { source } = this.state;
if (this.refs.file.getValue() !== '') {
publishArgs.file_path = this.refs.file.getValue();
} else if (source) {
publishArgs.sources = source;
}
const success = claim => {};
const failure = error => this.handlePublishError(error);
this.handlePublishStarted();
this.props.publish(publishArgs).then(success, failure);
};
if (this.state.isFee && parseFloat(this.state.feeAmount) > 0) {
lbry.wallet_unused_address().then(address => {
metadata.fee = {
currency: this.state.feeCurrency,
amount: parseFloat(this.state.feeAmount),
address,
};
doPublish();
});
} else {
doPublish();
}
}
handlePublishStarted() {
this.setState({
modal: 'publishStarted',
});
}
handlePublishStartedConfirmed() {
this.props.navigate('/published');
}
handlePublishError(error) {
this.setState({
submitting: false,
modal: 'error',
errorMessage: error.message,
});
}
claim() {
const { claimsByUri } = this.props;
const { uri } = this.state;
return claimsByUri[uri];
}
topClaimValue() {
if (!this.claim()) return null;
return parseFloat(this.claim().effective_amount);
}
myClaimExists() {
const { myClaims } = this.props;
const { name } = this.state;
if (!name) return false;
return !!myClaims.find(claim => claim.name === name);
}
2017-08-26 04:09:56 +02:00
handleEditClaim() {
const claimInfo = this.claim() || this.myClaimInfo();
2017-08-26 04:09:56 +02:00
if (claimInfo) {
this.handlePrefillClaim(claimInfo);
2017-08-26 04:09:56 +02:00
}
}
topClaimIsMine() {
const myClaimInfo = this.myClaimInfo();
const { claimsByUri } = this.props;
const { uri } = this.state;
if (!uri) return null;
const claim = claimsByUri[uri];
if (!claim) return true;
if (!myClaimInfo) return false;
2017-07-12 09:05:10 +02:00
return myClaimInfo.amount >= claim.amount;
}
myClaimInfo() {
const { id } = this.state;
return Object.values(this.props.myClaims).find(claim => claim.claim_id === id);
}
handleNameChange(event) {
const rawName = event.target.value;
this.setState({
customUrl: Boolean(rawName.length),
});
this.nameChanged(rawName);
}
nameChanged(rawName) {
if (!rawName) {
this.setState({
rawName: '',
name: '',
uri: '',
prefillDone: false,
mode: 'publish',
});
return;
}
if (!isNameValid(rawName, false)) {
this.refs.name.showError(__('LBRY names must contain only letters, numbers and dashes.'));
return;
}
let channel = '';
if (this.state.channel !== 'anonymous') channel = this.state.channel;
const name = rawName.toLowerCase();
const uri = buildURI({ contentName: name, channelName: channel });
this.setState({
rawName,
name,
prefillDone: false,
mode: 'publish',
uri,
});
if (this.resolveUriTimeout) {
clearTimeout(this.resolveUriTimeout);
this.resolveUriTimeout = undefined;
}
const resolve = () => this.props.resolveUri(uri);
this.resolveUriTimeout = setTimeout(resolve.bind(this), 500, {
once: true,
});
}
handlePrefillClaim(claimInfo) {
const { claim_id, name, channel_name, amount } = claimInfo;
const { source, metadata } = claimInfo.value.stream;
const { license, licenseUrl, title, thumbnail, description, language, nsfw } = metadata;
const newState = {
id: claim_id,
channel: channel_name || 'anonymous',
bid: amount,
meta_title: title,
meta_thumbnail: thumbnail,
meta_description: description,
meta_language: language,
meta_nsfw: nsfw,
mode: 'edit',
prefillDone: true,
rawName: name,
name,
source,
};
if (license == this._defaultCopyrightNotice) {
newState.licenseType = 'copyright';
newState.copyrightNotice = this._defaultCopyrightNotice;
} else {
// If the license URL or description matches one of the drop-down options, use that
let licenseType = 'other'; // Will be overridden if we find a match
for (const option of this._meta_license.getOptions()) {
if (option.getAttribute('data-url') === licenseUrl || option.text === license) {
licenseType = option.value;
}
}
if (licenseType == 'other') {
newState.otherLicenseDescription = license;
newState.otherLicenseUrl = licenseUrl;
}
newState.licenseType = licenseType;
}
this.setState(newState);
}
handleBidChange(event) {
this.setState({
2017-08-13 00:20:33 +02:00
bid: event.target.value,
});
}
handleFeeChange(newValue) {
this.setState({
feeAmount: newValue.amount,
feeCurrency: newValue.currency,
});
}
handleFeePrefChange(feeEnabled) {
this.setState({
isFee: feeEnabled,
feeAmount: this.state.feeAmount == '' ? this._defaultPaidPrice : this.state.feeAmount,
});
}
handleMetadataChange(event) {
/**
* This function is used for all metadata inputs that store the final value directly into state.
* The only exceptions are inputs related to license description and license URL, which require
* more complex logic and the final value is determined at submit time.
*/
this.setState({
[`meta_${event.target.name}`]: event.target.value,
});
}
handleDescriptionChanged(text) {
this.setState({
meta_description: text,
});
}
handleLicenseTypeChange(event) {
this.setState({
licenseType: event.target.value,
});
}
handleCopyrightNoticeChange(event) {
this.setState({
copyrightNotice: event.target.value,
});
}
handleOtherLicenseDescriptionChange(event) {
this.setState({
otherLicenseDescription: event.target.value,
});
}
handleOtherLicenseUrlChange(event) {
this.setState({
otherLicenseUrl: event.target.value,
});
}
handleChannelChange(channelName) {
this.setState({
mode: 'publish',
channel: channelName,
});
const nameChanged = () => this.nameChanged(this.state.rawName);
setTimeout(nameChanged.bind(this), 500, { once: true });
}
handleTOSChange(event) {
this.setState({
tosAgree: event.target.checked,
});
}
getLicense() {
switch (this.state.licenseType) {
case 'copyright':
return this.state.copyrightNotice;
case 'other':
return this.state.otherLicenseDescription;
default:
return this._meta_license.getSelectedElement().text;
}
}
getLicenseUrl() {
switch (this.state.licenseType) {
case 'copyright':
return '';
case 'other':
return this.state.otherLicenseUrl;
default:
return this._meta_license.getSelectedElement().getAttribute('data-url');
}
}
componentWillMount() {
this.props.fetchClaimListMine();
this._updateChannelList();
2017-08-26 04:09:56 +02:00
const { id } = this.props.params;
this.setState({ id });
2017-08-26 04:09:56 +02:00
}
componentDidMount() {
this.handleEditClaim();
}
onFileChange() {
const { mode } = this.state;
if (this.refs.file.getValue()) {
this.setState({ hasFile: true });
if (!this.state.customUrl && mode !== 'edit') {
const fileName = this._getFileName(this.refs.file.getValue());
this.nameChanged(fileName);
}
} else {
this.setState({ hasFile: false });
}
}
_getFileName(fileName) {
const path = require('path');
const extension = path.extname(fileName);
fileName = path.basename(fileName, extension);
fileName = fileName.replace(regexInvalidURI, '');
return fileName;
}
getNameBidHelpText() {
const { prefillDone, name, uri } = this.state;
const { resolvingUris } = this.props;
const claim = this.claim();
if (prefillDone) {
return __('Existing claim data was prefilled');
}
if (uri && resolvingUris.indexOf(uri) !== -1 && claim === undefined) {
return __('Checking...');
} else if (!name) {
return __('Select a URL for this publish.');
} else if (!claim) {
return __('This URL is unused.');
} else if (this.myClaimExists() && !prefillDone) {
return (
2017-07-12 09:05:10 +02:00
<span>
{__('You already have a claim with this name.')}{' '}
<Link label={__('Edit existing claim')} onClick={() => this.handleEditClaim()} />
2017-07-12 09:05:10 +02:00
</span>
);
} else if (claim) {
const topClaimValue = this.topClaimValue();
if (topClaimValue === 1) {
return (
<span>
{__(
'A deposit of at least one credit is required to win "%s". However, you can still get a permanent URL for any amount.',
name
)}
</span>
);
}
return (
<span>
{__(
'A deposit of at least "%s" credits is required to win "%s". However, you can still get a permanent URL for any amount.',
topClaimValue,
name
)}
</span>
);
}
return '';
}
closeModal() {
this.setState({
modal: null,
});
}
render() {
2017-09-05 03:03:48 +02:00
const { mode, submitting } = this.state;
const lbcInputHelp = __('This LBC remains yours and the deposit can be undone at any time.');
let submitLabel = !submitting ? __('Publish') : __('Publishing...');
2017-09-05 03:03:48 +02:00
if (mode === 'edit') {
submitLabel = !submitting ? __('Update') : __('Updating...');
2017-09-05 03:03:48 +02:00
}
return (
<main className="main--single-column">
<Form onSubmit={this.handleSubmit.bind(this)}>
<section className="card">
<div className="card__title-primary">
<h4>{__('Content')}</h4>
<div className="card__subtitle">{__('What are you publishing?')}</div>
</div>
<div className="card__content">
<FormRow
name="file"
ref="file"
type="file"
onChange={event => {
this.onFileChange(event);
}}
helper={
this.myClaimExists()
? __(
"If you don't choose a file, the file from your existing claim will be used."
)
: null
}
/>
</div>
2017-11-21 20:51:12 +01:00
{!this.state.hasFile && !this.myClaimExists() ? null : (
<div>
<div className="card__content">
<FormRow
ref="meta_title"
label={__('Title')}
2017-11-21 20:51:12 +01:00
type="text"
name="title"
value={this.state.meta_title}
placeholder="Titular Title"
onChange={event => {
this.handleMetadataChange(event);
}}
/>
</div>
<div className="card__content">
<FormRow
type="text"
label={__('Thumbnail URL')}
2017-11-21 20:51:12 +01:00
name="thumbnail"
value={this.state.meta_thumbnail}
placeholder="http://spee.ch/mylogo"
onChange={event => {
this.handleMetadataChange(event);
}}
/>
</div>
<div className="card__content">
<FormRow
type="SimpleMDE"
label={__('Description')}
2017-11-21 20:51:12 +01:00
ref="meta_description"
name="description"
value={this.state.meta_description}
placeholder={__('Description of your content')}
2017-11-21 20:51:12 +01:00
onChange={text => {
this.handleDescriptionChanged(text);
}}
/>
</div>
<div className="card__content">
<FormRow
label={__('Language')}
2017-11-21 20:51:12 +01:00
type="select"
value={this.state.meta_language}
name="language"
onChange={event => {
this.handleMetadataChange(event);
}}
>
<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>
2017-11-21 20:51:12 +01:00
</FormRow>
</div>
<div className="card__content">
<FormRow
type="select"
label={__('Maturity')}
2017-11-21 20:51:12 +01:00
value={this.state.meta_nsfw}
name="nsfw"
onChange={event => {
this.handleMetadataChange(event);
}}
>
{/* <option value=""></option> */}
<option value="0">{__('All Ages')}</option>
<option value="1">{__('Adults Only')}</option>
2017-11-21 20:51:12 +01:00
</FormRow>
</div>
</div>
)}
</section>
<section className="card">
<div className="card__title-primary">
<h4>{__('Price')}</h4>
<div className="card__subtitle">{__('How much does this content cost?')}</div>
</div>
<div className="card__content">
<FormRow
label={__('Free')}
type="radio"
name="isFree"
onChange={() => this.handleFeePrefChange(false)}
checked={!this.state.isFee}
/>
<FormField
type="radio"
name="isFree"
label={!this.state.isFee ? __('Choose price...') : __('Price ')}
onChange={() => {
this.handleFeePrefChange(true);
}}
checked={this.state.isFee}
/>
<span className={!this.state.isFee ? 'hidden' : ''}>
<FormFieldPrice
min="0"
defaultValue={{
amount: this._defaultPaidPrice,
currency: 'LBC',
}}
onChange={val => this.handleFeeChange(val)}
/>
</span>
{this.state.isFee && this.state.feeCurrency.toUpperCase() != 'LBC' ? (
2017-11-21 20:51:12 +01:00
<div className="form-field__helper">
{__(
'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
)}
</div>
) : null}
2017-10-14 21:41:04 +02:00
</div>
</section>
<section className="card">
<div className="card__title-primary">
<h4>{__('License')}</h4>
2017-10-14 21:41:04 +02:00
</div>
<div className="card__content">
<FormRow
type="select"
value={this.state.licenseType}
ref={row => {
this._meta_license = row;
}}
onChange={event => {
this.handleLicenseTypeChange(event);
}}
>
<option>{__('None')}</option>
<option value="publicDomain">{__('Public Domain')}</option>
<option
value="cc-by"
data-url="https://creativecommons.org/licenses/by/4.0/legalcode"
>
{__('Creative Commons Attribution 4.0 International')}
</option>
<option
value="cc-by-sa"
data-url="https://creativecommons.org/licenses/by-sa/4.0/legalcode"
>
{__('Creative Commons Attribution-ShareAlike 4.0 International')}
</option>
<option
value="cc-by-nd"
data-url="https://creativecommons.org/licenses/by-nd/4.0/legalcode"
>
{__('Creative Commons Attribution-NoDerivatives 4.0 International')}
</option>
<option
value="cc-by-nc"
data-url="https://creativecommons.org/licenses/by-nc/4.0/legalcode"
>
{__('Creative Commons Attribution-NonCommercial 4.0 International')}
</option>
<option
value="cc-by-nc-sa"
data-url="https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode"
>
{__('Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International')}
</option>
<option
value="cc-by-nc-nd"
data-url="https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode"
>
{__('Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International')}
</option>
<option value="copyright">{__('Copyrighted...')}</option>
<option value="other">{__('Other...')}</option>
</FormRow>
{this.state.licenseType == 'copyright' ? (
2017-11-21 20:51:12 +01:00
<FormRow
label={__('Copyright notice')}
2017-11-21 20:51:12 +01:00
type="text"
name="copyright-notice"
value={this.state.copyrightNotice}
onChange={event => {
this.handleCopyrightNoticeChange(event);
}}
/>
) : null}
{this.state.licenseType == 'other' ? (
2017-11-21 20:51:12 +01:00
<FormRow
label={__('License description')}
2017-11-21 20:51:12 +01:00
type="text"
name="other-license-description"
value={this.state.otherLicenseDescription}
onChange={event => {
this.handleOtherLicenseDescriptionChange(event);
}}
/>
) : null}
{this.state.licenseType == 'other' ? (
2017-11-21 20:51:12 +01:00
<FormRow
label={__('License URL')}
2017-11-21 20:51:12 +01:00
type="text"
name="other-license-url"
value={this.state.otherLicenseUrl}
onChange={event => {
this.handleOtherLicenseUrlChange(event);
}}
/>
) : null}
</div>
</section>
<ChannelSection
{...this.props}
handleChannelChange={this.handleChannelChange.bind(this)}
channel={this.state.channel}
/>
<section className="card">
<div className="card__title-primary">
<h4>{__('Content URL')}</h4>
<div className="card__subtitle">
2017-07-19 17:23:32 +02:00
{__(
'This is the exact address where people find your content (ex. lbry://myvideo).'
)}{' '}
<Link label={__('Learn more')} href="https://lbry.io/faq/naming" />.
</div>
</div>
<div className="card__content">
<FormRow
2017-11-21 20:51:12 +01:00
prefix={`lbry://${
this.state.channel === 'anonymous' ? '' : `${this.state.channel}/`
2017-11-21 20:51:12 +01:00
}`}
type="text"
ref="name"
placeholder="myname"
value={this.state.rawName}
onChange={event => {
this.handleNameChange(event);
}}
helper={this.getNameBidHelpText()}
/>
</div>
2017-11-21 20:51:12 +01:00
{this.state.rawName ? (
<div className="card__content">
<FormRow
ref="bid"
type="number"
step="any"
label={__('Deposit')}
2017-11-21 20:51:12 +01:00
postfix="LBC"
onChange={event => {
this.handleBidChange(event);
}}
value={this.state.bid}
placeholder={this.claim() ? this.topClaimValue() + 10 : 100}
helper={lbcInputHelp}
min="0"
/>
</div>
) : (
''
2017-11-21 20:51:12 +01:00
)}
</section>
<section className="card">
<div className="card__title-primary">
<h4>{__('Terms of Service')}</h4>
</div>
<div className="card__content">
<FormRow
ref="tosAgree"
label={
<span>
{__('I agree to the')}{' '}
<Link
href="https://www.lbry.io/termsofservice"
label={__('LBRY terms of service')}
/>
</span>
}
type="checkbox"
checked={this.state.tosAgree}
onChange={event => {
this.handleTOSChange(event);
}}
/>
</div>
</section>
<div className="card-series-submit">
<Submit
label={!this.state.submitting ? __('Publish') : __('Publishing...')}
2017-07-08 10:03:12 +02:00
disabled={
this.props.balance <= 0 ||
2017-07-08 10:03:12 +02:00
this.state.submitting ||
(this.state.uri && this.props.resolvingUris.indexOf(this.state.uri) !== -1) ||
(this.claim() && !this.topClaimIsMine() && this.state.bid <= this.topClaimValue())
2017-07-08 10:03:12 +02:00
}
/>
<Link button="cancel" onClick={this.props.back} label={__('Cancel')} />
</div>
</Form>
<Modal
isOpen={this.state.modal == 'publishStarted'}
contentLabel={__('File published')}
onConfirmed={event => {
this.handlePublishStartedConfirmed(event);
}}
>
<p>
{__('Your file has been published to LBRY at the address')}{' '}
2017-11-21 20:51:12 +01:00
<code>{this.state.uri}</code>!
</p>
<p>
{__(
'The file will take a few minutes to appear for other LBRY users. Until then it will be listed as "pending" under your published files.'
)}
</p>
</Modal>
<Modal
isOpen={this.state.modal == 'error'}
contentLabel={__('Error publishing file')}
onConfirmed={event => {
this.closeModal(event);
}}
>
{__('The following error occurred when attempting to publish your file')}:{' '}
{this.state.errorMessage}
</Modal>
</main>
);
}
}
export default PublishForm;