import React from "react"; import lbry from "lbry"; import lbryuri from "lbryuri"; import FormField from "component/formField"; import { FormRow } from "component/form.js"; import Link from "component/link"; import FormFieldPrice from "component/formFieldPrice"; import Modal from "component/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."; this.state = { 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, }; } _updateChannelList(channel) { const { fetchingChannels, fetchChannelListMine } = this.props; if (!fetchingChannels) fetchChannelListMine(); } handleSubmit(event) { if (typeof event !== "undefined") { event.preventDefault(); } this.setState({ submitting: true, }); let checkFields = this._requiredFields; if (!this.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; } let metadata = {}; for (let 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); 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(); } 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: 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().amount); } myClaimExists() { const { myClaims } = this.props; const { name } = this.state; if (!name) return false; return !!myClaims.find(claim => claim.name === name); } 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; return myClaimInfo.amount >= claim.amount; } myClaimInfo() { const { name } = this.state; return Object.values(this.props.myClaims).find( claim => claim.name === name ); } handleNameChange(event) { var rawName = event.target.value; this.setState({ customUrl: Boolean(rawName.length), }); this.nameChanged(rawName); } nameChanged(rawName) { if (!rawName) { this.setState({ rawName: "", name: "", uri: "", prefillDone: false, }); return; } if (!lbryuri.isValidName(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 = lbryuri.build({ contentName: name, channelName: channel }); this.setState({ rawName: rawName, name: name, prefillDone: false, 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, }); } handlePrefillClicked() { const claimInfo = this.myClaimInfo(); const { license, licenseUrl, title, thumbnail, description, language, nsfw, } = claimInfo.value.stream.metadata; let newState = { meta_title: title, meta_thumbnail: thumbnail, meta_description: description, meta_language: language, meta_nsfw: nsfw, prefillDone: true, bid: claimInfo.amount, }; 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 (let 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({ bid: event.target.value, }); } handleFeeChange(newValue) { this.state.feeAmount = newValue.amount; this.state.feeCurrency = newValue.currency; } handleFeePrefChange(feeEnabled) { this.setState({ isFee: feeEnabled, feeAmount: this.state.feeAmount == "" ? "5.00" : 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({ channel: channelName, }); const nameChanged = () => this.nameChanged(this.state.rawName); setTimeout(nameChanged.bind(this), 500, { once: true }); } handleTOSChange(event) { this.setState({ tosAgree: 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: parseFloat(this.state.newChannelBid), }) .then( () => { setTimeout(() => { this.setState({ creatingChannel: false, }); this._updateChannelList(newChannelName); }, 10000); }, error => { // TODO: better error handling this.refs.newChannelName.showError( __("Unable to create channel due to an internal error.") ); this.setState({ creatingChannel: false, }); } ); } 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(); } onFileChange() { if (this.refs.file.getValue()) { this.setState({ hasFile: true }); if (!this.state.customUrl) { let 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(lbryuri.REGEXP_INVALID_URI, ""); return fileName; } getNameBidHelpText() { if (this.state.prefillDone) { return __("Existing claim data was prefilled"); } if ( this.state.uri && this.props.resolvingUris.indexOf(this.state.uri) !== -1 && this.claim() === undefined ) { return __("Checking..."); } else if (!this.state.name) { return __("Select a URL for this publish."); } else if (!this.claim()) { return __("This URL is unused."); } else if (this.myClaimExists() && !this.state.prefillDone) { return ( {__("You already have a claim with this name.")}{" "} this.handlePrefillClicked()} /> ); } else if (this.claim()) { if (this.topClaimValue() === 1) { return ( {__( 'A deposit of at least one credit is required to win "%s". However, you can still get a permanent URL for any amount.', this.state.name )} ); } else { return ( {__( 'A deposit of at least "%s" credits is required to win "%s". However, you can still get a permanent URL for any amount.', this.topClaimValue(), this.state.name )} ); } } else { return ""; } } closeModal() { this.setState({ modal: null, }); } render() { const lbcInputHelp = __( "This LBC remains yours and the deposit can be undone at any time." ); return (
{ this.handleSubmit(event); }} >

{__("Content")}

{__("What are you publishing?")}
{ this.onFileChange(event); }} helper={ this.myClaimExists() ? __( "If you don't choose a file, the file from your existing claim will be used." ) : null } />
{!this.state.hasFile && !this.myClaimExists() ? null :
{ this.handleMetadataChange(event); }} />
{ this.handleMetadataChange(event); }} />
{ this.handleDescriptionChanged(text); }} />
{ this.handleMetadataChange(event); }} >
{ this.handleMetadataChange(event); }} > {/* */}
}

{__("Access")}

{__("How much does this content cost?")}
this.handleFeePrefChange(false)} checked={!this.state.isFee} /> { this.handleFeePrefChange(true); }} checked={this.state.isFee} /> {/*min=0.01 caused weird interactions with step (e.g. down from 5 equals 4.91 rather than 4.9) */} this.handleFeeChange(val)} /> {/* && this.state.feeCurrency.toUpperCase() != "LBC" for some reason, react does not trigger a re-render on currency change (despite trigger a state change), so the above logic cannot be added to the below check */} {this.state.isFee ?
{__( "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." )}
: null} { this._meta_license = row; }} onChange={event => { this.handleLicenseTypeChange(event); }} > {this.state.licenseType == "copyright" ? { this.handleCopyrightNoticeChange(event); }} /> : null} {this.state.licenseType == "other" ? { this.handleOtherLicenseDescriptionChange(event); }} /> : null} {this.state.licenseType == "other" ? { this.handleOtherLicenseUrlChange(event); }} /> : null}

{__("Content URL")}

{__( "This is the exact address where people find your content (ex. lbry://myvideo)." )} {" "} .
{ this.handleNameChange(event); }} helper={this.getNameBidHelpText()} />
{this.state.rawName ?
{ this.handleBidChange(event); }} value={this.state.bid} placeholder={this.claim() ? this.topClaimValue() + 10 : 100} helper={lbcInputHelp} />
: ""}

{__("Terms of Service")}

{__("I agree to the")} {" "} } type="checkbox" checked={this.state.tosAgree} onChange={event => { this.handleTOSChange(event); }} />
{ this.handleSubmit(event); }} disabled={ this.state.submitting || (this.state.uri && this.props.resolvingUris.indexOf(this.state.uri) !== -1) || (this.claim() && !this.topClaimIsMine() && this.state.bid <= this.topClaimValue()) } />
{ this.handlePublishStartedConfirmed(event); }} >

{__("Your file has been published to LBRY at the address")} {" "}{this.state.uri}!

{__( '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.' )}

{ this.closeModal(event); }} > {__( "The following error occurred when attempting to publish your file" )}: {this.state.errorMessage}
); } } export default PublishForm;