lbry-desktop/ui/js/page/publish/view.jsx

593 lines
22 KiB
JavaScript

import React from 'react';
import lbry from 'lbry';
import lbryuri from 'lbryuri'
import {FormField, FormRow} from 'component/form.js';
import Link from 'component/link';
import rewards from 'rewards';
import Modal from 'component/modal';
class PublishPage extends React.Component {
constructor(props) {
super(props);
this._requiredFields = ['meta_title', 'name', 'bid', 'tos_agree'];
this.state = {
channels: null,
rawName: '',
name: '',
bid: 10,
hasFile: false,
feeAmount: '',
feeCurrency: 'USD',
channel: 'anonymous',
newChannelName: '@',
newChannelBid: 10,
nameResolved: null,
myClaimExists: null,
topClaimValue: 0.0,
myClaimValue: 0.0,
myClaimMetadata: null,
copyrightNotice: '',
otherLicenseDescription: '',
otherLicenseUrl: '',
uploadProgress: 0.0,
uploaded: false,
errorMessage: null,
submitting: false,
creatingChannel: false,
modal: null,
};
}
_updateChannelList(channel) {
// Calls API to update displayed list of channels. If a channel name is provided, will select
// that channel at the same time (used immediately after creating a channel)
lbry.channel_list_mine().then((channels) => {
rewards.claimReward(rewards.TYPE_FIRST_CHANNEL).then(() => {}, () => {})
this.setState({
channels: channels,
... channel ? {channel} : {}
});
});
}
handleSubmit(event) {
if (typeof event !== 'undefined') {
event.preventDefault();
}
this.setState({
submitting: true,
});
let checkFields = this._requiredFields;
if (!this.state.myClaimExists) {
checkFields.unshift('file');
}
let missingFieldFound = false;
for (let 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;
}
if (this.state.nameIsMine) {
// Pre-populate with existing metadata
var metadata = Object.assign({}, this.state.myClaimMetadata);
if (this.refs.file.getValue() !== '') {
delete metadata.sources;
}
} else {
var metadata = {};
}
for (let metaField of ['title', 'description', 'thumbnail', 'license', 'license_url', 'language']) {
var value = this.refs['meta_' + metaField].getValue();
if (value !== '') {
metadata[metaField] = value;
}
}
metadata.nsfw = parseInt(this.refs.meta_nsfw.getValue()) === 1;
const licenseUrl = this.refs.meta_license_url.getValue();
if (licenseUrl) {
metadata.license_url = licenseUrl;
}
var doPublish = () => {
var publishArgs = {
name: this.state.name,
bid: parseFloat(this.state.bid),
metadata: metadata,
... this.state.channel != 'new' && this.state.channel != 'anonymous' ? {channel_name: this.state.channel} : {},
};
if (this.refs.file.getValue() !== '') {
publishArgs.file_path = this.refs.file.getValue();
}
lbry.publish(publishArgs, (message) => {
this.handlePublishStarted();
}, null, (error) => {
this.handlePublishError(error);
});
};
if (this.state.isFee) {
lbry.wallet_unused_address().then((address) => {
metadata.fee = {};
metadata.fee[this.state.feeCurrency] = {
amount: parseFloat(this.state.feeAmount),
address: 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,
});
}
handleNameChange(event) {
var rawName = event.target.value;
if (!rawName) {
this.setState({
rawName: '',
name: '',
nameResolved: false,
});
return;
}
if (!lbryuri.isValidName(rawName, false)) {
this.refs.name.showError(__("LBRY names must contain only letters, numbers and dashes."));
return;
}
const name = rawName.toLowerCase();
this.setState({
rawName: rawName,
name: name,
nameResolved: null,
myClaimExists: null,
});
const myClaimInfo = Object.values(this.props.myClaims).find(claim => claim.name === name)
this.setState({
myClaimExists: !!myClaimInfo,
});
lbry.resolve({uri: name}).then((claimInfo) => {
if (name != this.state.name) {
return;
}
if (!claimInfo) {
this.setState({
nameResolved: false,
});
} else {
const topClaimIsMine = myClaimInfo && myClaimInfo.amount >= claimInfo.amount;
const newState = {
nameResolved: true,
topClaimValue: parseFloat(claimInfo.amount),
myClaimExists: !!myClaimInfo,
myClaimValue: myClaimInfo ? parseFloat(myClaimInfo.amount) : null,
myClaimMetadata: myClaimInfo ? myClaimInfo.value : null,
topClaimIsMine: topClaimIsMine,
};
if (topClaimIsMine) {
newState.bid = myClaimInfo.amount;
} else if (this.state.myClaimMetadata) {
// Just changed away from a name we have a claim on, so clear pre-fill
newState.bid = '';
}
this.setState(newState);
}
}, () => { // Assume an error means the name is available
this.setState({
name: name,
nameResolved: false,
myClaimExists: false,
});
});
}
handleBidChange(event) {
this.setState({
bid: event.target.value,
});
}
handleFeeAmountChange(event) {
this.setState({
feeAmount: event.target.value,
});
}
handleFeeCurrencyChange(event) {
this.setState({
feeCurrency: event.target.value,
});
}
handleFeePrefChange(feeEnabled) {
this.setState({
isFee: feeEnabled
});
}
handleLicenseChange(event) {
var licenseType = event.target.options[event.target.selectedIndex].getAttribute('data-license-type');
var newState = {
copyrightChosen: licenseType == 'copyright',
otherLicenseChosen: licenseType == 'other',
};
if (licenseType == 'copyright') {
newState.copyrightNotice = __("All rights reserved.")
}
this.setState(newState);
}
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(event) {
const channel = event.target.value;
this.setState({
channel: channel,
});
}
handleNewChannelNameChange(event) {
const newChannelName = (event.target.value.startsWith('@') ? event.target.value : '@' + event.target.value);
if (newChannelName.length > 1 && !lbryuri.isValidName(newChannelName.substr(1), false)) {
this.refs.newChannelName.showError(__("LBRY channel names must contain only letters, numbers and dashes."));
return;
} else {
this.refs.newChannelName.clearError()
}
this.setState({
newChannelName: newChannelName,
});
}
handleNewChannelBidChange(event) {
this.setState({
newChannelBid: event.target.value,
});
}
handleTOSChange(event) {
this.setState({
TOSAgreed: event.target.checked,
});
}
handleCreateChannelClick(event) {
if (this.state.newChannelName.length < 5) {
this.refs.newChannelName.showError(__("LBRY channel names must be at least 4 characters in length."));
return;
}
this.setState({
creatingChannel: true,
});
const newChannelName = this.state.newChannelName;
lbry.channel_new({channel_name: newChannelName, amount: parseInt(this.state.newChannelBid)}).then(() => {
setTimeout(() => {
this.setState({
creatingChannel: false,
});
this._updateChannelList(newChannelName);
}, 5000);
}, (error) => {
// TODO: better error handling
this.refs.newChannelName.showError(__("Unable to create channel due to an internal error."));
this.setState({
creatingChannel: false,
});
});
}
getLicenseUrl() {
if (!this.refs.meta_license) {
return '';
} else if (this.state.otherLicenseChosen) {
return this.state.otherLicenseUrl;
} else {
return this.refs.meta_license.getSelectedElement().getAttribute('data-url') || '' ;
}
}
componentWillMount() {
this._updateChannelList();
}
onFileChange() {
if (this.refs.file.getValue()) {
this.setState({ hasFile: true })
} else {
this.setState({ hasFile: false })
}
}
getNameBidHelpText() {
if (!this.state.name) {
return __("Select a URL for this publish.");
} else if (this.state.nameResolved === false) {
return __("This URL is unused.");
} else if (this.state.myClaimExists) {
return __("You have already used this URL. Publishing to it again will update your previous publish.")
} else if (this.state.topClaimValue) {
return <span>{__n("A deposit of at least \"%s\" credit is required to win \"%s\". However, you can still get a permanent URL for any amount."
, "A deposit of at least \"%s\" credits is required to win \"%s\". However, you can still get a permanent URL for any amount."
, this.state.topClaimValue /*pluralization param*/
, this.state.topClaimValue, this.state.name /*regular params*/
)}</span>
} else {
return '';
}
}
closeModal() {
this.setState({
modal: null,
});
}
render() {
if (this.state.channels === null) {
return null;
}
const lbcInputHelp = __("This LBC remains yours and the deposit can be undone at any time.");
return (
<main className="main--single-column">
<form onSubmit={(event) => { this.handleSubmit(event) }}>
<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" label="File" ref="file" type="file" onChange={(event) => { this.onFileChange(event) }}
helper={this.state.myClaimExists ? __("If you don't choose a file, the file from your existing claim will be used.") : null}/>
</div>
{ !this.state.hasFile ? '' :
<div>
<div className="card__content">
<FormRow label={__("Title")} type="text" ref="meta_title" name="title" placeholder={__("Title")} />
</div>
<div className="card__content">
<FormRow type="text" label={__("Thumbnail URL")} ref="meta_thumbnail" name="thumbnail" placeholder="http://spee.ch/mylogo" />
</div>
<div className="card__content">
<FormRow label={__("Description")} type="textarea" ref="meta_description" name="description" placeholder={__("Description of your content")} />
</div>
<div className="card__content">
<FormRow label={__("Language")} type="select" defaultValue="en" ref="meta_language" name="language">
<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>
</FormRow>
</div>
<div className="card__content">
<FormRow type="select" label={__("Maturity")} defaultValue="en" ref="meta_nsfw" name="nsfw">
{/* <option value=""></option> */}
<option value="0">{__("All Ages")}</option>
<option value="1">{__("Adults Only")}</option>
</FormRow>
</div>
</div>}
</section>
<section className="card">
<div className="card__title-primary">
<h4>{__("Access")}</h4>
<div className="card__subtitle">
{__("How much does this content cost?")}
</div>
</div>
<div className="card__content">
<div className="form-row__label-row">
<label className="form-row__label">{__("Price")}</label>
</div>
<FormRow label={__("Free")} type="radio" name="isFree" value="1" onChange={ () => { this.handleFeePrefChange(false) } } defaultChecked={!this.state.isFee} />
<FormField type="radio" name="isFree" label={!this.state.isFee ? __('Choose price...') : __('Price ') }
onChange={ () => { this.handleFeePrefChange(true) } } defaultChecked={this.state.isFee} />
<span className={!this.state.isFee ? 'hidden' : ''}>
<FormField type="number" className="form-field__input--inline" step="0.01" placeholder="1.00" onChange={(event) => this.handleFeeAmountChange(event)} /> <FormField type="select" onChange={(event) => { this.handleFeeCurrencyChange(event) }}>
<option value="USD">{__("US Dollars")}</option>
<option value="LBC">{__("LBRY credits")}</option>
</FormField>
</span>
{ this.state.isFee ?
<div className="form-field__helper">
{__("If you choose to price this content in dollars, the number of credits charged will be adjusted based on the value of LBRY credits at the time of purchase.")}
</div> : '' }
<FormRow label="License" type="select" ref="meta_license" name="license" onChange={(event) => { this.handleLicenseChange(event) }}>
<option></option>
<option>{__("Public Domain")}</option>
<option data-url="https://creativecommons.org/licenses/by/4.0/legalcode">{__("Creative Commons Attribution 4.0 International")}</option>
<option data-url="https://creativecommons.org/licenses/by-sa/4.0/legalcode">{__("Creative Commons Attribution-ShareAlike 4.0 International")}</option>
<option data-url="https://creativecommons.org/licenses/by-nd/4.0/legalcode">{__("Creative Commons Attribution-NoDerivatives 4.0 International")}</option>
<option data-url="https://creativecommons.org/licenses/by-nc/4.0/legalcode">{__("Creative Commons Attribution-NonCommercial 4.0 International")}</option>
<option data-url="https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode">{__("Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International")}</option>
<option data-url="https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode">{__("Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International")}</option>
<option data-license-type="copyright" {... this.state.copyrightChosen ? {value: this.state.copyrightNotice} : {}}>{__("Copyrighted...")}</option>
<option data-license-type="other" {... this.state.otherLicenseChosen ? {value: this.state.otherLicenseDescription} : {}}>{__("Other...")}</option>
</FormRow>
<FormField type="hidden" ref="meta_license_url" name="license_url" value={this.getLicenseUrl()} />
{this.state.copyrightChosen
? <FormRow label={__("Copyright notice")} type="text" name="copyright-notice"
value={this.state.copyrightNotice} onChange={(event) => { this.handleCopyrightNoticeChange(event) }} />
: null}
{this.state.otherLicenseChosen ?
<FormRow label={__("License description")} type="text" name="other-license-description" onChange={(event) => { this.handleOtherLicenseDescriptionChange() }} />
: null}
{this.state.otherLicenseChosen ?
<FormRow label={__("License URL")} type="text" name="other-license-url" onChange={(event) => { this.handleOtherLicenseUrlChange(event) }} />
: null}
</div>
</section>
<section className="card">
<div className="card__title-primary">
<h4>{__("Identity")}</h4>
<div className="card__subtitle">
{__("Who created this content?")}
</div>
</div>
<div className="card__content">
<FormRow type="select" tabIndex="1" onChange={(event) => { this.handleChannelChange(event) }} value={this.state.channel}>
<option key="anonymous" value="anonymous">{__("Anonymous")}</option>
{this.state.channels.map(({name}) => <option key={name} value={name}>{name}</option>)}
<option key="new" value="new">{__("New identity...")}</option>
</FormRow>
</div>
{this.state.channel == 'new' ?
<div className="card__content">
<FormRow label={__("Name")} type="text" onChange={(event) => { this.handleNewChannelNameChange(event) }} ref={newChannelName => { this.refs.newChannelName = newChannelName }}
value={this.state.newChannelName} />
<FormRow label={__("Deposit")}
postfix="LBC"
step="0.01"
type="number"
helper={lbcInputHelp}
onChange={(event) => { this.handleNewChannelBidChange(event) }}
value={this.state.newChannelBid} />
<div className="form-row-submit">
<Link button="primary" label={!this.state.creatingChannel ? __("Create identity") : __("Creating identity...")} onClick={(event) => { this.handleCreateChannelClick(event) }} disabled={this.state.creatingChannel} />
</div>
</div>
: null}
</section>
<section className="card">
<div className="card__title-primary">
<h4>{__("Address")}</h4>
<div className="card__subtitle">{__("Where should this content permanently reside?")} <Link label={__("Read more")} href="https://lbry.io/faq/naming" />.</div>
</div>
<div className="card__content">
<FormRow prefix="lbry://" type="text" ref="name" placeholder="myname" value={this.state.rawName} onChange={(event) => { this.handleNameChange(event) }}
helper={this.getNameBidHelpText()} />
</div>
{ this.state.rawName ?
<div className="card__content">
<FormRow ref="bid"
type="number"
step="0.01"
label={__("Deposit")}
postfix="LBC"
onChange={(event) => { this.handleBidChange(event) }}
value={this.state.bid}
placeholder={this.state.nameResolved ? this.state.topClaimValue + 10 : 100}
helper={lbcInputHelp} />
</div> : '' }
</section>
<section className="card">
<div className="card__title-primary">
<h4>{__("Terms of Service")}</h4>
</div>
<div className="card__content">
<FormRow label={
<span>{__("I agree to the")} <Link href="https://www.lbry.io/termsofservice" label={__("LBRY terms of service")} checked={this.state.TOSAgreed} /></span>
} type="checkbox" name="tos_agree" ref={(field) => { this.refs.tos_agree = field }} onChange={(event) => { this.handleTOSChange(event)}} />
</div>
</section>
<div className="card-series-submit">
<Link button="primary" label={!this.state.submitting ? __("Publish") : __("Publishing...")} onClick={(event) => { this.handleSubmit(event) }} disabled={this.state.submitting} />
<Link button="cancel" onClick={this.props.back} label={__("Cancel")} />
<input type="submit" className="hidden" />
</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")} <code>lbry://{this.state.name}</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 PublishPage;