commit
2dbca1533f
33 changed files with 2479 additions and 1123 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -26,3 +26,4 @@ build/daemon.zip
|
|||
.vimrc
|
||||
|
||||
package-lock.json
|
||||
ui/yarn.lock
|
||||
|
|
|
@ -10,14 +10,18 @@ Web UI version numbers should always match the corresponding version of LBRY App
|
|||
### Added
|
||||
* Added option to release claim when deleting a file
|
||||
* Added transition to card hovers to smooth animation
|
||||
* Support markdown makeup in claim description
|
||||
*
|
||||
|
||||
### Changed
|
||||
*
|
||||
* Publishes now uses claims rather than files
|
||||
*
|
||||
|
||||
### Fixed
|
||||
* Fixed bug with download notice when switching window focus
|
||||
*
|
||||
* Fixed newly published files appearing twice
|
||||
* Fixed unconfirmed published files missing channel name
|
||||
* Fixed old files from updated published claims appearing in downloaded list
|
||||
|
||||
### Deprecated
|
||||
*
|
||||
|
|
|
@ -15,6 +15,7 @@ import { selectBadgeNumber } from "selectors/app";
|
|||
import { selectTotalDownloadProgress } from "selectors/file_info";
|
||||
import setBadge from "util/setBadge";
|
||||
import setProgressBar from "util/setProgressBar";
|
||||
import { doFileList } from "actions/file_info";
|
||||
import batchActions from "util/batchActions";
|
||||
|
||||
const { ipcRenderer } = require("electron");
|
||||
|
@ -339,3 +340,68 @@ export function doFetchClaimListMine() {
|
|||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function doFetchChannelListMine() {
|
||||
return function(dispatch, getState) {
|
||||
dispatch({
|
||||
type: types.FETCH_CHANNEL_LIST_MINE_STARTED,
|
||||
});
|
||||
|
||||
const callback = channels => {
|
||||
dispatch({
|
||||
type: types.FETCH_CHANNEL_LIST_MINE_COMPLETED,
|
||||
data: { claims: channels },
|
||||
});
|
||||
};
|
||||
|
||||
lbry.channel_list_mine().then(callback);
|
||||
};
|
||||
}
|
||||
|
||||
export function doCreateChannel(name, amount) {
|
||||
return function(dispatch, getState) {
|
||||
dispatch({
|
||||
type: types.CREATE_CHANNEL_STARTED,
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
lbry
|
||||
.channel_new({
|
||||
channel_name: name,
|
||||
amount: parseFloat(amount),
|
||||
})
|
||||
.then(
|
||||
channelClaim => {
|
||||
channelClaim.name = name;
|
||||
dispatch({
|
||||
type: types.CREATE_CHANNEL_COMPLETED,
|
||||
data: { channelClaim },
|
||||
});
|
||||
resolve(channelClaim);
|
||||
},
|
||||
err => {
|
||||
reject(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function doPublish(params) {
|
||||
return function(dispatch, getState) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const success = claim => {
|
||||
resolve(claim);
|
||||
|
||||
if (claim === true) dispatch(doFetchClaimListMine());
|
||||
else
|
||||
setTimeout(() => dispatch(doFetchClaimListMine()), 20000, {
|
||||
once: true,
|
||||
});
|
||||
};
|
||||
const failure = err => reject(err);
|
||||
|
||||
lbry.publishDeprecated(params, null, success, failure);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -3,11 +3,11 @@ import lbry from "lbry";
|
|||
import { doFetchClaimListMine } from "actions/content";
|
||||
import {
|
||||
selectClaimsByUri,
|
||||
selectClaimListMineIsPending,
|
||||
selectIsFetchingClaimListMine,
|
||||
selectMyClaimsOutpoints,
|
||||
} from "selectors/claims";
|
||||
import {
|
||||
selectFileListIsPending,
|
||||
selectIsFetchingFileList,
|
||||
selectFileInfosByOutpoint,
|
||||
selectUrisLoading,
|
||||
} from "selectors/file_info";
|
||||
|
@ -48,16 +48,16 @@ export function doFetchFileInfo(uri) {
|
|||
export function doFileList() {
|
||||
return function(dispatch, getState) {
|
||||
const state = getState();
|
||||
const isPending = selectFileListIsPending(state);
|
||||
const isFetching = selectIsFetchingFileList(state);
|
||||
|
||||
if (!isPending) {
|
||||
if (!isFetching) {
|
||||
dispatch({
|
||||
type: types.FILE_LIST_STARTED,
|
||||
});
|
||||
|
||||
lbry.file_list().then(fileInfos => {
|
||||
dispatch({
|
||||
type: types.FILE_LIST_COMPLETED,
|
||||
type: types.FILE_LIST_SUCCEEDED,
|
||||
data: {
|
||||
fileInfos,
|
||||
},
|
||||
|
@ -102,14 +102,12 @@ export function doDeleteFile(outpoint, deleteFromComputer, abandonClaim) {
|
|||
},
|
||||
});
|
||||
|
||||
const success = () => {
|
||||
dispatch({
|
||||
type: types.ABANDON_CLAIM_COMPLETED,
|
||||
const success = dispatch({
|
||||
type: types.ABANDON_CLAIM_SUCCEEDED,
|
||||
data: {
|
||||
claimId: fileInfo.claim_id,
|
||||
},
|
||||
});
|
||||
};
|
||||
lbry.claim_abandon({ claim_id: fileInfo.claim_id }).then(success);
|
||||
}
|
||||
}
|
||||
|
@ -128,10 +126,10 @@ export function doDeleteFile(outpoint, deleteFromComputer, abandonClaim) {
|
|||
export function doFetchFileInfosAndPublishedClaims() {
|
||||
return function(dispatch, getState) {
|
||||
const state = getState(),
|
||||
isClaimListMinePending = selectClaimListMineIsPending(state),
|
||||
isFileInfoListPending = selectFileListIsPending(state);
|
||||
isFetchingClaimListMine = selectIsFetchingClaimListMine(state),
|
||||
isFetchingFileInfo = selectIsFetchingFileList(state);
|
||||
|
||||
dispatch(doFetchClaimListMine());
|
||||
dispatch(doFileList());
|
||||
if (!isFetchingClaimListMine) dispatch(doFetchClaimListMine());
|
||||
if (!isFetchingFileInfo) dispatch(doFileList());
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import React from "react";
|
||||
import lbryuri from "lbryuri.js";
|
||||
import Link from "component/link";
|
||||
import { TruncatedText, Icon } from "component/common";
|
||||
import { Thumbnail, TruncatedText, Icon } from "component/common";
|
||||
import FilePrice from "component/filePrice";
|
||||
import UriIndicator from "component/uriIndicator";
|
||||
import NsfwOverlay from "component/nsfwOverlay";
|
||||
import TruncatedMarkdown from "component/truncatedMarkdown";
|
||||
|
||||
class FileCard extends React.PureComponent {
|
||||
constructor(props) {
|
||||
|
@ -94,7 +95,7 @@ class FileCard extends React.PureComponent {
|
|||
style={{ backgroundImage: "url('" + metadata.thumbnail + "')" }}
|
||||
/>}
|
||||
<div className="card__content card__subtext card__subtext--two-lines">
|
||||
<TruncatedText lines={2}>{description}</TruncatedText>
|
||||
<TruncatedMarkdown lines={2}>{description}</TruncatedMarkdown>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
|
|
@ -67,7 +67,9 @@ class FileList extends React.PureComponent {
|
|||
const content = [];
|
||||
|
||||
this._sortFunctions[sortBy](fileInfos).forEach(fileInfo => {
|
||||
let uriParams = {};
|
||||
let uriParams = {
|
||||
claimId: fileInfo.claim_id,
|
||||
};
|
||||
if (fileInfo.channel_name) {
|
||||
uriParams.channelName = fileInfo.channel_name;
|
||||
uriParams.contentName = fileInfo.name;
|
||||
|
@ -79,7 +81,7 @@ class FileList extends React.PureComponent {
|
|||
|
||||
content.push(
|
||||
<FileTile
|
||||
key={uri}
|
||||
key={fileInfo.outpoint || fileInfo.claim_id}
|
||||
uri={uri}
|
||||
hidePrice={true}
|
||||
showEmpty={this.props.fileTileShowEmpty}
|
||||
|
@ -94,7 +96,6 @@ class FileList extends React.PureComponent {
|
|||
<FormField type="select" onChange={this.handleSortChanged.bind(this)}>
|
||||
<option value="date">{__("Date")}</option>
|
||||
<option value="title">{__("Title")}</option>
|
||||
<option value="filename">{__("File name")}</option>
|
||||
</FormField>
|
||||
</span>
|
||||
{content}
|
||||
|
|
|
@ -64,7 +64,7 @@ class FileTile extends React.PureComponent {
|
|||
const isClaimable = lbryuri.isClaimable(uri);
|
||||
const title = isClaimed && metadata && metadata.title
|
||||
? metadata.title
|
||||
: uri;
|
||||
: lbryuri.parse(uri).contentName;
|
||||
const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw;
|
||||
let onClick = () => navigate("/show", { uri });
|
||||
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import React from "react";
|
||||
import FileSelector from "./file-selector.js";
|
||||
import { Icon } from "./common.js";
|
||||
import SimpleMDE from "react-simplemde-editor";
|
||||
import style from "react-simplemde-editor/dist/simplemde.min.css";
|
||||
|
||||
var formFieldCounter = 0,
|
||||
let formFieldCounter = 0,
|
||||
formFieldFileSelectorTypes = ["file", "directory"],
|
||||
formFieldNestedLabelTypes = ["radio", "checkbox"];
|
||||
|
||||
|
@ -24,6 +25,7 @@ export class FormField extends React.PureComponent {
|
|||
this._fieldRequiredText = __("This field is required");
|
||||
this._type = null;
|
||||
this._element = null;
|
||||
this._extraElementProps = {};
|
||||
|
||||
this.state = {
|
||||
isError: null,
|
||||
|
@ -38,6 +40,12 @@ export class FormField extends React.PureComponent {
|
|||
} else if (this.props.type == "text-number") {
|
||||
this._element = "input";
|
||||
this._type = "text";
|
||||
} else if (this.props.type == "SimpleMDE") {
|
||||
this._element = SimpleMDE;
|
||||
this._type = "textarea";
|
||||
this._extraElementProps.options = {
|
||||
hideIcons: ["guide", "heading", "image", "fullscreen", "side-by-side"],
|
||||
};
|
||||
} else if (formFieldFileSelectorTypes.includes(this.props.type)) {
|
||||
this._element = "input";
|
||||
this._type = "hidden";
|
||||
|
@ -81,6 +89,8 @@ export class FormField extends React.PureComponent {
|
|||
getValue() {
|
||||
if (this.props.type == "checkbox") {
|
||||
return this.refs.field.checked;
|
||||
} else if (this.props.type == "SimpleMDE") {
|
||||
return this.refs.field.simplemde.value();
|
||||
} else {
|
||||
return this.refs.field.value;
|
||||
}
|
||||
|
@ -90,6 +100,10 @@ export class FormField extends React.PureComponent {
|
|||
return this.refs.field.options[this.refs.field.selectedIndex];
|
||||
}
|
||||
|
||||
getOptions() {
|
||||
return this.refs.field.options;
|
||||
}
|
||||
|
||||
render() {
|
||||
// Pass all unhandled props to the field element
|
||||
const otherProps = Object.assign({}, this.props),
|
||||
|
@ -106,7 +120,6 @@ export class FormField extends React.PureComponent {
|
|||
delete otherProps.className;
|
||||
delete otherProps.postfix;
|
||||
delete otherProps.prefix;
|
||||
|
||||
const element = (
|
||||
<this._element
|
||||
id={elementId}
|
||||
|
@ -122,6 +135,7 @@ export class FormField extends React.PureComponent {
|
|||
(isError ? "form-field__input--error" : "")
|
||||
}
|
||||
{...otherProps}
|
||||
{...this._extraElementProps}
|
||||
>
|
||||
{this.props.children}
|
||||
</this._element>
|
||||
|
@ -220,6 +234,10 @@ export class FormRow extends React.PureComponent {
|
|||
return this.refs.field.getSelectedElement();
|
||||
}
|
||||
|
||||
getOptions() {
|
||||
return this.refs.field.getOptions();
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.refs.field.focus();
|
||||
}
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
import React from "react";
|
||||
|
||||
export class Notice extends React.PureComponent {
|
||||
static propTypes = {
|
||||
isError: React.PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
isError: false,
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<section
|
||||
className={
|
||||
"notice " +
|
||||
(this.props.isError ? "notice--error " : "") +
|
||||
(this.props.className || "")
|
||||
}
|
||||
>
|
||||
{this.props.children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Notice;
|
5
ui/js/component/publishForm/index.js
Normal file
5
ui/js/component/publishForm/index.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import PublishForm from "./view";
|
||||
|
||||
export default connect()(PublishForm);
|
174
ui/js/component/publishForm/internal/channelSection.jsx
Normal file
174
ui/js/component/publishForm/internal/channelSection.jsx
Normal file
|
@ -0,0 +1,174 @@
|
|||
import React from "react";
|
||||
import lbryuri from "lbryuri";
|
||||
import { FormField, FormRow } from "component/form.js";
|
||||
import { BusyMessage } from "component/common";
|
||||
import Link from "component/link";
|
||||
|
||||
class ChannelSection extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
newChannelName: "@",
|
||||
newChannelBid: 10,
|
||||
addingChannel: false,
|
||||
};
|
||||
}
|
||||
|
||||
handleChannelChange(event) {
|
||||
const channel = event.target.value;
|
||||
if (channel === "new") this.setState({ addingChannel: true });
|
||||
else {
|
||||
this.setState({ addingChannel: false });
|
||||
this.props.handleChannelChange(event.target.value);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
handleNewChannelBidChange(event) {
|
||||
this.setState({
|
||||
newChannelBid: event.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
const amount = parseFloat(this.state.newChannelBid);
|
||||
this.setState({
|
||||
creatingChannel: true,
|
||||
});
|
||||
const success = () => {
|
||||
this.setState({
|
||||
creatingChannel: false,
|
||||
addingChannel: false,
|
||||
channel: newChannelName,
|
||||
});
|
||||
this.props.handleChannelChange(newChannelName);
|
||||
};
|
||||
const failure = err => {
|
||||
this.setState({
|
||||
creatingChannel: false,
|
||||
});
|
||||
this.refs.newChannelName.showError(
|
||||
__("Unable to create channel due to an internal error.")
|
||||
);
|
||||
};
|
||||
this.props.createChannel(newChannelName, amount).then(success, failure);
|
||||
}
|
||||
|
||||
render() {
|
||||
const lbcInputHelp = __(
|
||||
"This LBC remains yours and the deposit can be undone at any time."
|
||||
);
|
||||
|
||||
const { fetchingChannels, channels = [] } = this.props;
|
||||
|
||||
let channelContent = [];
|
||||
channelContent.push(
|
||||
<FormRow
|
||||
key="channel"
|
||||
type="select"
|
||||
tabIndex="1"
|
||||
onChange={this.handleChannelChange.bind(this)}
|
||||
value={this.props.channel}
|
||||
>
|
||||
<option key="anonymous" value="anonymous">
|
||||
{__("Anonymous")}
|
||||
</option>
|
||||
{this.props.channels.map(({ name }) =>
|
||||
<option key={name} value={name}>{name}</option>
|
||||
)}
|
||||
<option key="new" value="new">
|
||||
{__("New identity...")}
|
||||
</option>
|
||||
</FormRow>
|
||||
);
|
||||
if (fetchingChannels) {
|
||||
channelContent.push(
|
||||
<BusyMessage message="Updating channels" key="loading" />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<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">
|
||||
{channelContent}
|
||||
</div>
|
||||
{this.state.addingChannel &&
|
||||
<div className="card__content">
|
||||
<FormRow
|
||||
label={__("Name")}
|
||||
type="text"
|
||||
onChange={event => {
|
||||
this.handleNewChannelNameChange(event);
|
||||
}}
|
||||
value={this.state.newChannelName}
|
||||
/>
|
||||
<FormRow
|
||||
label={__("Deposit")}
|
||||
postfix="LBC"
|
||||
step="0.1"
|
||||
min="0"
|
||||
type="number"
|
||||
helper={lbcInputHelp}
|
||||
ref="newChannelName"
|
||||
onChange={this.handleNewChannelBidChange.bind(this)}
|
||||
value={this.state.newChannelBid}
|
||||
/>
|
||||
<div className="form-row-submit">
|
||||
<Link
|
||||
button="primary"
|
||||
label={
|
||||
!this.state.creatingChannel
|
||||
? __("Create identity")
|
||||
: __("Creating identity...")
|
||||
}
|
||||
onClick={this.handleCreateChannelClick.bind(this)}
|
||||
disabled={this.state.creatingChannel}
|
||||
/>
|
||||
</div>
|
||||
</div>}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ChannelSection;
|
921
ui/js/component/publishForm/view.jsx
Normal file
921
ui/js/component/publishForm/view.jsx
Normal file
|
@ -0,0 +1,921 @@
|
|||
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 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: "USD",
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
_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) {
|
||||
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.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,
|
||||
});
|
||||
}
|
||||
|
||||
handleFeeAmountChange(event) {
|
||||
this.setState({
|
||||
feeAmount: event.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
handleFeeCurrencyChange(event) {
|
||||
this.setState({
|
||||
feeCurrency: event.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
handleFeePrefChange(feeEnabled) {
|
||||
this.setState({
|
||||
isFee: feeEnabled,
|
||||
});
|
||||
}
|
||||
|
||||
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 });
|
||||
} else {
|
||||
this.setState({ hasFile: false });
|
||||
}
|
||||
}
|
||||
|
||||
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 (
|
||||
<span>
|
||||
{__("You already have a claim with this name.")}{" "}
|
||||
<Link
|
||||
label={__("Use data from my existing claim")}
|
||||
onClick={() => this.handlePrefillClicked()}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
} else if (this.claim()) {
|
||||
if (this.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.',
|
||||
this.state.name
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
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.',
|
||||
this.topClaimValue(),
|
||||
this.state.name
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
closeModal() {
|
||||
this.setState({
|
||||
modal: null,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
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.myClaimExists()
|
||||
? __(
|
||||
"If you don't choose a file, the file from your existing claim will be used."
|
||||
)
|
||||
: null
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{!this.state.hasFile && !this.myClaimExists()
|
||||
? null
|
||||
: <div>
|
||||
<div className="card__content">
|
||||
<FormRow
|
||||
label={__("Title")}
|
||||
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")}
|
||||
name="thumbnail"
|
||||
value={this.state.meta_thumbnail}
|
||||
placeholder="http://spee.ch/mylogo"
|
||||
onChange={event => {
|
||||
this.handleMetadataChange(event);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<FormRow
|
||||
label={__("Description")}
|
||||
type="SimpleMDE"
|
||||
ref="meta_description"
|
||||
name="description"
|
||||
value={this.state.meta_description}
|
||||
placeholder={__("Description of your content")}
|
||||
onChange={text => {
|
||||
this.handleDescriptionChanged(text);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<FormRow
|
||||
label={__("Language")}
|
||||
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>
|
||||
</FormRow>
|
||||
</div>
|
||||
<div className="card__content">
|
||||
<FormRow
|
||||
type="select"
|
||||
label={__("Maturity")}
|
||||
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>
|
||||
</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"
|
||||
min="0.01"
|
||||
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"
|
||||
value={this.state.licenseType}
|
||||
ref={row => {
|
||||
this._meta_license = row;
|
||||
}}
|
||||
onChange={event => {
|
||||
this.handleLicenseTypeChange(event);
|
||||
}}
|
||||
>
|
||||
<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"
|
||||
? <FormRow
|
||||
label={__("Copyright notice")}
|
||||
type="text"
|
||||
name="copyright-notice"
|
||||
value={this.state.copyrightNotice}
|
||||
onChange={event => {
|
||||
this.handleCopyrightNoticeChange(event);
|
||||
}}
|
||||
/>
|
||||
: null}
|
||||
|
||||
{this.state.licenseType == "other"
|
||||
? <FormRow
|
||||
label={__("License description")}
|
||||
type="text"
|
||||
name="other-license-description"
|
||||
value={this.state.otherLicenseDescription}
|
||||
onChange={event => {
|
||||
this.handleOtherLicenseDescriptionChange(event);
|
||||
}}
|
||||
/>
|
||||
: null}
|
||||
|
||||
{this.state.licenseType == "other"
|
||||
? <FormRow
|
||||
label={__("License URL")}
|
||||
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>{__("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://${this.state.channel === "anonymous"
|
||||
? ""
|
||||
: `${this.state.channel}/`}`}
|
||||
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.claim() ? this.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")}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
type="checkbox"
|
||||
checked={this.state.tosAgree}
|
||||
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 ||
|
||||
(this.state.uri &&
|
||||
this.props.resolvingUris.indexOf(this.state.uri) !== -1) ||
|
||||
(this.claim() &&
|
||||
!this.topClaimIsMine() &&
|
||||
this.state.bid <= this.topClaimValue())
|
||||
}
|
||||
/>
|
||||
<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>{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;
|
5
ui/js/component/truncatedMarkdown/index.js
Normal file
5
ui/js/component/truncatedMarkdown/index.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import TruncatedMarkdown from "./view";
|
||||
|
||||
export default connect()(TruncatedMarkdown);
|
39
ui/js/component/truncatedMarkdown/view.jsx
Normal file
39
ui/js/component/truncatedMarkdown/view.jsx
Normal file
|
@ -0,0 +1,39 @@
|
|||
import React from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import ReactDOMServer from "react-dom/server";
|
||||
|
||||
class TruncatedMarkdown extends React.PureComponent {
|
||||
static propTypes = {
|
||||
lines: React.PropTypes.number,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
lines: null,
|
||||
};
|
||||
|
||||
transformMarkdown(text) {
|
||||
// render markdown to html string then trim html tag
|
||||
let htmlString = ReactDOMServer.renderToStaticMarkup(
|
||||
<ReactMarkdown source={this.props.children} />
|
||||
);
|
||||
var txt = document.createElement("textarea");
|
||||
txt.innerHTML = htmlString;
|
||||
return txt.value.replace(/<(?:.|\n)*?>/gm, "");
|
||||
}
|
||||
|
||||
render() {
|
||||
let content = this.props.children && typeof this.props.children === "string"
|
||||
? this.transformMarkdown(this.props.children)
|
||||
: this.props.children;
|
||||
return (
|
||||
<span
|
||||
className="truncated-text"
|
||||
style={{ WebkitLineClamp: this.props.lines }}
|
||||
>
|
||||
{content}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TruncatedMarkdown;
|
|
@ -47,7 +47,7 @@ export const FETCH_CLAIM_LIST_MINE_STARTED = "FETCH_CLAIM_LIST_MINE_STARTED";
|
|||
export const FETCH_CLAIM_LIST_MINE_COMPLETED =
|
||||
"FETCH_CLAIM_LIST_MINE_COMPLETED";
|
||||
export const FILE_LIST_STARTED = "FILE_LIST_STARTED";
|
||||
export const FILE_LIST_COMPLETED = "FILE_LIST_COMPLETED";
|
||||
export const FILE_LIST_SUCCEEDED = "FILE_LIST_SUCCEEDED";
|
||||
export const FETCH_FILE_INFO_STARTED = "FETCH_FILE_INFO_STARTED";
|
||||
export const FETCH_FILE_INFO_COMPLETED = "FETCH_FILE_INFO_COMPLETED";
|
||||
export const FETCH_COST_INFO_STARTED = "FETCH_COST_INFO_STARTED";
|
||||
|
@ -63,7 +63,16 @@ export const FETCH_AVAILABILITY_STARTED = "FETCH_AVAILABILITY_STARTED";
|
|||
export const FETCH_AVAILABILITY_COMPLETED = "FETCH_AVAILABILITY_COMPLETED";
|
||||
export const FILE_DELETE = "FILE_DELETE";
|
||||
export const ABANDON_CLAIM_STARTED = "ABANDON_CLAIM_STARTED";
|
||||
export const ABANDON_CLAIM_COMPLETED = "ABANDON_CLAIM_COMPLETED";
|
||||
export const ABANDON_CLAIM_SUCCEEDED = "ABANDON_CLAIM_SUCCEEDED";
|
||||
export const FETCH_CHANNEL_LIST_MINE_STARTED =
|
||||
"FETCH_CHANNEL_LIST_MINE_STARTED";
|
||||
export const FETCH_CHANNEL_LIST_MINE_COMPLETED =
|
||||
"FETCH_CHANNEL_LIST_MINE_COMPLETED";
|
||||
export const CREATE_CHANNEL_STARTED = "CREATE_CHANNEL_STARTED";
|
||||
export const CREATE_CHANNEL_COMPLETED = "CREATE_CHANNEL_COMPLETED";
|
||||
export const PUBLISH_STARTED = "PUBLISH_STARTED";
|
||||
export const PUBLISH_COMPLETED = "PUBLISH_COMPLETED";
|
||||
export const PUBLISH_FAILED = "PUBLISH_FAILED";
|
||||
|
||||
// Search
|
||||
export const SEARCH_STARTED = "SEARCH_STARTED";
|
||||
|
|
|
@ -223,28 +223,18 @@ lbry.publishDeprecated = function(
|
|||
) {
|
||||
lbry.publish(params).then(
|
||||
result => {
|
||||
if (returnedPending) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(returnPendingTimeout);
|
||||
if (returnPendingTimeout) clearTimeout(returnPendingTimeout);
|
||||
publishedCallback(result);
|
||||
},
|
||||
err => {
|
||||
if (returnedPending) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(returnPendingTimeout);
|
||||
if (returnPendingTimeout) clearTimeout(returnPendingTimeout);
|
||||
errorCallback(err);
|
||||
}
|
||||
);
|
||||
|
||||
let returnedPending = false;
|
||||
// Give a short grace period in case publish() returns right away or (more likely) gives an error
|
||||
const returnPendingTimeout = setTimeout(() => {
|
||||
returnedPending = true;
|
||||
|
||||
const returnPendingTimeout = setTimeout(
|
||||
() => {
|
||||
if (publishedCallback) {
|
||||
savePendingPublish({
|
||||
name: params.name,
|
||||
|
@ -261,7 +251,10 @@ lbry.publishDeprecated = function(
|
|||
});
|
||||
fileListedCallback(true);
|
||||
}
|
||||
}, 2000);
|
||||
},
|
||||
2000,
|
||||
{ once: true }
|
||||
);
|
||||
};
|
||||
|
||||
lbry.getClientSettings = function() {
|
||||
|
|
|
@ -203,6 +203,8 @@ lbryuri.build = function(uriObj, includeProto = true, allowExtraProps = false) {
|
|||
/* Takes a parseable LBRY URI and converts it to standard, canonical format (currently this just
|
||||
* consists of adding the lbry:// prefix if needed) */
|
||||
lbryuri.normalize = function(uri) {
|
||||
if (uri.match(/pending_claim/)) return uri;
|
||||
|
||||
const { name, path, bidPosition, claimSequence, claimId } = lbryuri.parse(
|
||||
uri
|
||||
);
|
||||
|
|
|
@ -3,15 +3,22 @@ import { connect } from "react-redux";
|
|||
import { doFetchFileInfosAndPublishedClaims } from "actions/file_info";
|
||||
import {
|
||||
selectFileInfosDownloaded,
|
||||
selectFileListDownloadedOrPublishedIsPending,
|
||||
selectIsFetchingFileListDownloadedOrPublished,
|
||||
} from "selectors/file_info";
|
||||
import {
|
||||
selectMyClaimsWithoutChannels,
|
||||
selectIsFetchingClaimListMine,
|
||||
} from "selectors/claims";
|
||||
import { doFetchClaimListMine } from "actions/content";
|
||||
import { doNavigate } from "actions/app";
|
||||
import { doCancelAllResolvingUris } from "actions/content";
|
||||
import FileListDownloaded from "./view";
|
||||
|
||||
const select = state => ({
|
||||
fileInfos: selectFileInfosDownloaded(state),
|
||||
isPending: selectFileListDownloadedOrPublishedIsPending(state),
|
||||
isFetching: selectIsFetchingFileListDownloadedOrPublished(state),
|
||||
claims: selectMyClaimsWithoutChannels(state),
|
||||
isFetchingClaims: selectIsFetchingClaimListMine(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
|
@ -19,6 +26,7 @@ const perform = dispatch => ({
|
|||
fetchFileInfosDownloaded: () =>
|
||||
dispatch(doFetchFileInfosAndPublishedClaims()),
|
||||
cancelResolvingUris: () => dispatch(doCancelAllResolvingUris()),
|
||||
fetchClaims: () => dispatch(doFetchClaimListMine()),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(FileListDownloaded);
|
||||
|
|
|
@ -12,7 +12,8 @@ import SubHeader from "component/subHeader";
|
|||
|
||||
class FileListDownloaded extends React.PureComponent {
|
||||
componentWillMount() {
|
||||
if (!this.props.isPending) this.props.fetchFileInfosDownloaded();
|
||||
if (!this.props.isFetchingClaims) this.props.fetchClaims();
|
||||
if (!this.props.isFetching) this.props.fetchFileInfosDownloaded();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -20,13 +21,13 @@ class FileListDownloaded extends React.PureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { fileInfos, isPending, navigate } = this.props;
|
||||
const { fileInfos, isFetching, navigate } = this.props;
|
||||
|
||||
let content;
|
||||
if (fileInfos && fileInfos.length > 0) {
|
||||
content = <FileList fileInfos={fileInfos} fetching={isPending} />;
|
||||
content = <FileList fileInfos={fileInfos} fetching={isFetching} />;
|
||||
} else {
|
||||
if (isPending) {
|
||||
if (isFetching) {
|
||||
content = <BusyMessage message={__("Loading")} />;
|
||||
} else {
|
||||
content = (
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
import React from "react";
|
||||
import rewards from "rewards";
|
||||
import { connect } from "react-redux";
|
||||
import { doFetchFileInfosAndPublishedClaims } from "actions/file_info";
|
||||
import { doFetchClaimListMine } from "actions/content";
|
||||
import {
|
||||
selectFileInfosPublished,
|
||||
selectFileListDownloadedOrPublishedIsPending,
|
||||
} from "selectors/file_info";
|
||||
selectMyClaimsWithoutChannels,
|
||||
selectIsFetchingClaimListMine,
|
||||
} from "selectors/claims";
|
||||
import { doClaimRewardType } from "actions/rewards";
|
||||
import { doNavigate } from "actions/app";
|
||||
import { doCancelAllResolvingUris } from "actions/content";
|
||||
import FileListPublished from "./view";
|
||||
|
||||
const select = state => ({
|
||||
fileInfos: selectFileInfosPublished(state),
|
||||
isPending: selectFileListDownloadedOrPublishedIsPending(state),
|
||||
claims: selectMyClaimsWithoutChannels(state),
|
||||
isFetching: selectIsFetchingClaimListMine(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
navigate: path => dispatch(doNavigate(path)),
|
||||
fetchFileListPublished: () => dispatch(doFetchFileInfosAndPublishedClaims()),
|
||||
fetchClaims: () => dispatch(doFetchClaimListMine()),
|
||||
claimFirstPublishReward: () =>
|
||||
dispatch(doClaimRewardType(rewards.TYPE_FIRST_PUBLISH)),
|
||||
cancelResolvingUris: () => dispatch(doCancelAllResolvingUris()),
|
||||
|
|
|
@ -12,11 +12,11 @@ import SubHeader from "component/subHeader";
|
|||
|
||||
class FileListPublished extends React.PureComponent {
|
||||
componentWillMount() {
|
||||
if (!this.props.isPending) this.props.fetchFileListPublished();
|
||||
if (!this.props.isFetching) this.props.fetchClaims();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.props.fileInfos.length > 0) this.props.claimFirstPublishReward();
|
||||
// if (this.props.claims.length > 0) this.props.fetchClaims();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -24,20 +24,20 @@ class FileListPublished extends React.PureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { fileInfos, isPending, navigate } = this.props;
|
||||
const { claims, isFetching, navigate } = this.props;
|
||||
|
||||
let content;
|
||||
|
||||
if (fileInfos && fileInfos.length > 0) {
|
||||
if (claims && claims.length > 0) {
|
||||
content = (
|
||||
<FileList
|
||||
fileInfos={fileInfos}
|
||||
fetching={isPending}
|
||||
fileInfos={claims}
|
||||
fetching={isFetching}
|
||||
fileTileShowEmpty={FileTile.SHOW_EMPTY_PENDING}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
if (isPending) {
|
||||
if (isFetching) {
|
||||
content = <BusyMessage message={__("Loading")} />;
|
||||
} else {
|
||||
content = (
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import lbry from "lbry.js";
|
||||
import lbryuri from "lbryuri.js";
|
||||
import Video from "component/video";
|
||||
|
@ -119,7 +120,11 @@ class FilePage extends React.PureComponent {
|
|||
</div>
|
||||
</div>
|
||||
<div className="card__content card__subtext card__subtext card__subtext--allow-newlines">
|
||||
{metadata && metadata.description}
|
||||
<ReactMarkdown
|
||||
source={(metadata && metadata.description) || ""}
|
||||
escapeHtml={true}
|
||||
disallowedTypes={["Heading", "HtmlInline", "HtmlBlock"]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{metadata
|
||||
|
|
|
@ -2,13 +2,29 @@ import React from "react";
|
|||
import { connect } from "react-redux";
|
||||
import { doNavigate, doHistoryBack } from "actions/app";
|
||||
import { doClaimRewardType } from "actions/rewards";
|
||||
import { selectMyClaims } from "selectors/claims";
|
||||
import { doFetchClaimListMine } from "actions/content";
|
||||
import {
|
||||
selectMyClaims,
|
||||
selectFetchingMyChannels,
|
||||
selectMyChannelClaims,
|
||||
selectClaimsByUri,
|
||||
} from "selectors/claims";
|
||||
import { selectResolvingUris } from "selectors/content";
|
||||
import {
|
||||
doFetchClaimListMine,
|
||||
doFetchChannelListMine,
|
||||
doResolveUri,
|
||||
doCreateChannel,
|
||||
doPublish,
|
||||
} from "actions/content";
|
||||
import rewards from "rewards";
|
||||
import PublishPage from "./view";
|
||||
|
||||
const select = state => ({
|
||||
myClaims: selectMyClaims(state),
|
||||
fetchingChannels: selectFetchingMyChannels(state),
|
||||
channels: selectMyChannelClaims(state),
|
||||
claimsByUri: selectClaimsByUri(state),
|
||||
resolvingUris: selectResolvingUris(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
|
@ -17,6 +33,10 @@ const perform = dispatch => ({
|
|||
fetchClaimListMine: () => dispatch(doFetchClaimListMine()),
|
||||
claimFirstChannelReward: () =>
|
||||
dispatch(doClaimRewardType(rewards.TYPE_FIRST_CHANNEL)),
|
||||
fetchChannelListMine: () => dispatch(doFetchChannelListMine()),
|
||||
resolveUri: uri => dispatch(doResolveUri(uri)),
|
||||
createChannel: (name, amount) => dispatch(doCreateChannel(name, amount)),
|
||||
publish: params => dispatch(doPublish(params)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(PublishPage);
|
||||
|
|
|
@ -1,922 +1,8 @@
|
|||
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";
|
||||
import PublishForm from "component/publishForm";
|
||||
|
||||
class PublishPage extends React.PureComponent {
|
||||
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,
|
||||
const PublishPage = props => {
|
||||
return <PublishForm {...props} />;
|
||||
};
|
||||
}
|
||||
|
||||
_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 => {
|
||||
this.props.claimFirstChannelReward();
|
||||
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.publishDeprecated(
|
||||
publishArgs,
|
||||
message => {
|
||||
this.handlePublishStarted();
|
||||
},
|
||||
null,
|
||||
error => {
|
||||
this.handlePublishError(error);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
if (this.state.isFee) {
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
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: 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,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
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.props.fetchClaimListMine();
|
||||
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) {
|
||||
if (this.state.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.',
|
||||
this.state.name
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
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.',
|
||||
this.state.topClaimValue,
|
||||
this.state.name
|
||||
)}
|
||||
</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"
|
||||
min="0.01"
|
||||
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>{__("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"
|
||||
min="0"
|
||||
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;
|
||||
|
|
|
@ -24,7 +24,7 @@ class ShowPage extends React.PureComponent {
|
|||
|
||||
let innerContent = "";
|
||||
|
||||
if (isResolvingUri && !claim) {
|
||||
if ((isResolvingUri && !claim) || !claim) {
|
||||
innerContent = (
|
||||
<section className="card">
|
||||
<div className="card__inner">
|
||||
|
|
|
@ -15,10 +15,16 @@ reducers[types.RESOLVE_URI_COMPLETED] = function(state, action) {
|
|||
byUri[uri] = claim.claim_id;
|
||||
} else if (claim === undefined && certificate !== undefined) {
|
||||
byId[certificate.claim_id] = certificate;
|
||||
// Don't point URI at the channel certificate unless it actually is
|
||||
// a channel URI. This is brittle.
|
||||
if (!uri.split(certificate.name)[1].match(/\//)) {
|
||||
byUri[uri] = certificate.claim_id;
|
||||
} else {
|
||||
byUri[uri] = null;
|
||||
}
|
||||
} else {
|
||||
byUri[uri] = null;
|
||||
}
|
||||
|
||||
return Object.assign({}, state, {
|
||||
byId,
|
||||
|
@ -28,43 +34,72 @@ reducers[types.RESOLVE_URI_COMPLETED] = function(state, action) {
|
|||
|
||||
reducers[types.FETCH_CLAIM_LIST_MINE_STARTED] = function(state, action) {
|
||||
return Object.assign({}, state, {
|
||||
isClaimListMinePending: true,
|
||||
isFetchingClaimListMine: true,
|
||||
});
|
||||
};
|
||||
|
||||
reducers[types.FETCH_CLAIM_LIST_MINE_COMPLETED] = function(state, action) {
|
||||
const { claims } = action.data;
|
||||
const myClaims = new Set(state.myClaims);
|
||||
const byUri = Object.assign({}, state.claimsByUri);
|
||||
const byId = Object.assign({}, state.byId);
|
||||
const pendingById = Object.assign({}, state.pendingById);
|
||||
const abandoningById = Object.assign({}, state.abandoningById);
|
||||
const myClaims = new Set(
|
||||
claims
|
||||
.map(claim => claim.claim_id)
|
||||
.filter(claimId => Object.keys(abandoningById).indexOf(claimId) === -1)
|
||||
);
|
||||
|
||||
claims.forEach(claim => {
|
||||
myClaims.add(claim.claim_id);
|
||||
byId[claim.claim_id] = claim;
|
||||
|
||||
const pending = Object.values(pendingById).find(pendingClaim => {
|
||||
return (
|
||||
pendingClaim.name == claim.name &&
|
||||
pendingClaim.channel_name == claim.channel_name
|
||||
);
|
||||
});
|
||||
|
||||
if (pending) {
|
||||
delete pendingById[pending.claim_id];
|
||||
}
|
||||
});
|
||||
|
||||
// Remove old timed out pending publishes
|
||||
const old = Object.values(pendingById)
|
||||
.filter(pendingClaim => Date.now() - pendingClaim.time >= 20 * 60 * 1000)
|
||||
.forEach(pendingClaim => {
|
||||
delete pendingById[pendingClaim.claim_id];
|
||||
});
|
||||
|
||||
return Object.assign({}, state, {
|
||||
isClaimListMinePending: false,
|
||||
isFetchingClaimListMine: false,
|
||||
myClaims: myClaims,
|
||||
byId,
|
||||
pendingById,
|
||||
});
|
||||
};
|
||||
|
||||
// reducers[types.FETCH_CHANNEL_CLAIMS_STARTED] = function(state, action) {
|
||||
// const {
|
||||
// uri,
|
||||
// } = action.data
|
||||
//
|
||||
// const newClaims = Object.assign({}, state.claimsByChannel)
|
||||
//
|
||||
// if (claims !== undefined) {
|
||||
// newClaims[uri] = claims
|
||||
// }
|
||||
//
|
||||
// return Object.assign({}, state, {
|
||||
// claimsByChannel: newClaims
|
||||
// })
|
||||
// }
|
||||
reducers[types.FETCH_CHANNEL_LIST_MINE_STARTED] = function(state, action) {
|
||||
return Object.assign({}, state, { fetchingMyChannels: true });
|
||||
};
|
||||
|
||||
reducers[types.FETCH_CHANNEL_LIST_MINE_COMPLETED] = function(state, action) {
|
||||
const { claims } = action.data;
|
||||
const myChannelClaims = new Set(state.myChannelClaims);
|
||||
const byId = Object.assign({}, state.byId);
|
||||
|
||||
claims.forEach(claim => {
|
||||
myChannelClaims.add(claim.claim_id);
|
||||
byId[claims.claim_id] = claim;
|
||||
});
|
||||
|
||||
return Object.assign({}, state, {
|
||||
byId,
|
||||
fetchingMyChannels: false,
|
||||
myChannelClaims,
|
||||
});
|
||||
};
|
||||
|
||||
reducers[types.FETCH_CHANNEL_CLAIMS_COMPLETED] = function(state, action) {
|
||||
const { uri, claims } = action.data;
|
||||
|
@ -80,7 +115,18 @@ reducers[types.FETCH_CHANNEL_CLAIMS_COMPLETED] = function(state, action) {
|
|||
});
|
||||
};
|
||||
|
||||
reducers[types.ABANDON_CLAIM_COMPLETED] = function(state, action) {
|
||||
reducers[types.ABANDON_CLAIM_STARTED] = function(state, action) {
|
||||
const { claimId } = action.data;
|
||||
const abandoningById = Object.assign({}, state.abandoningById);
|
||||
|
||||
abandoningById[claimId] = true;
|
||||
|
||||
return Object.assign({}, state, {
|
||||
abandoningById,
|
||||
});
|
||||
};
|
||||
|
||||
reducers[types.ABANDON_CLAIM_SUCCEEDED] = function(state, action) {
|
||||
const { claimId } = action.data;
|
||||
const myClaims = new Set(state.myClaims);
|
||||
const byId = Object.assign({}, state.byId);
|
||||
|
@ -103,6 +149,20 @@ reducers[types.ABANDON_CLAIM_COMPLETED] = function(state, action) {
|
|||
});
|
||||
};
|
||||
|
||||
reducers[types.CREATE_CHANNEL_COMPLETED] = function(state, action) {
|
||||
const { channelClaim } = action.data;
|
||||
const byId = Object.assign({}, state.byId);
|
||||
const myChannelClaims = new Set(state.myChannelClaims);
|
||||
|
||||
byId[channelClaim.claim_id] = channelClaim;
|
||||
myChannelClaims.add(channelClaim.claim_id);
|
||||
|
||||
return Object.assign({}, state, {
|
||||
byId,
|
||||
myChannelClaims,
|
||||
});
|
||||
};
|
||||
|
||||
export default function reducer(state = defaultState, action) {
|
||||
const handler = reducers[action.type];
|
||||
if (handler) return handler(state, action);
|
||||
|
|
|
@ -6,14 +6,15 @@ const defaultState = {};
|
|||
|
||||
reducers[types.FILE_LIST_STARTED] = function(state, action) {
|
||||
return Object.assign({}, state, {
|
||||
isFileListPending: true,
|
||||
isFetchingFileList: true,
|
||||
});
|
||||
};
|
||||
|
||||
reducers[types.FILE_LIST_COMPLETED] = function(state, action) {
|
||||
reducers[types.FILE_LIST_SUCCEEDED] = function(state, action) {
|
||||
const { fileInfos } = action.data;
|
||||
|
||||
const newByOutpoint = Object.assign({}, state.byOutpoint);
|
||||
const pendingByOutpoint = Object.assign({}, state.pendingByOutpoint);
|
||||
|
||||
fileInfos.forEach(fileInfo => {
|
||||
const { outpoint } = fileInfo;
|
||||
|
||||
|
@ -21,8 +22,9 @@ reducers[types.FILE_LIST_COMPLETED] = function(state, action) {
|
|||
});
|
||||
|
||||
return Object.assign({}, state, {
|
||||
isFileListPending: false,
|
||||
isFetchingFileList: false,
|
||||
byOutpoint: newByOutpoint,
|
||||
pendingByOutpoint,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@ export const makeSelectClaimForUri = () => {
|
|||
const selectClaimForUriIsMine = (state, props) => {
|
||||
const uri = lbryuri.normalize(props.uri);
|
||||
const claim = selectClaimsByUri(state)[uri];
|
||||
const myClaims = selectMyClaims(state);
|
||||
const myClaims = selectMyClaimsRaw(state);
|
||||
|
||||
return myClaims.has(claim.claim_id);
|
||||
};
|
||||
|
@ -100,27 +100,72 @@ export const makeSelectContentTypeForUri = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export const selectClaimListMineIsPending = createSelector(
|
||||
export const selectIsFetchingClaimListMine = createSelector(
|
||||
_selectState,
|
||||
state => state.isClaimListMinePending
|
||||
state => !!state.isFetchingClaimListMine
|
||||
);
|
||||
|
||||
export const selectMyClaims = createSelector(
|
||||
export const selectMyClaimsRaw = createSelector(
|
||||
_selectState,
|
||||
state => new Set(state.myClaims)
|
||||
);
|
||||
|
||||
export const selectAbandoningIds = createSelector(_selectState, state =>
|
||||
Object.keys(state.abandoningById || {})
|
||||
);
|
||||
|
||||
export const selectPendingClaims = createSelector(_selectState, state =>
|
||||
Object.values(state.pendingById || {})
|
||||
);
|
||||
|
||||
export const selectMyClaims = createSelector(
|
||||
selectMyClaimsRaw,
|
||||
selectClaimsById,
|
||||
selectAbandoningIds,
|
||||
selectPendingClaims,
|
||||
(myClaimIds, byId, abandoningIds, pendingClaims) => {
|
||||
const claims = [];
|
||||
|
||||
myClaimIds.forEach(id => {
|
||||
const claim = byId[id];
|
||||
|
||||
if (claim && abandoningIds.indexOf(id) == -1) claims.push(claim);
|
||||
});
|
||||
|
||||
return [...claims, ...pendingClaims];
|
||||
}
|
||||
);
|
||||
|
||||
export const selectMyClaimsWithoutChannels = createSelector(
|
||||
selectMyClaims,
|
||||
myClaims => myClaims.filter(claim => !claim.name.match(/^@/))
|
||||
);
|
||||
|
||||
export const selectMyClaimsOutpoints = createSelector(
|
||||
selectMyClaims,
|
||||
selectClaimsById,
|
||||
(claimIds, byId) => {
|
||||
myClaims => {
|
||||
const outpoints = [];
|
||||
|
||||
claimIds.forEach(claimId => {
|
||||
const claim = byId[claimId];
|
||||
if (claim) outpoints.push(`${claim.txid}:${claim.nout}`);
|
||||
});
|
||||
myClaims.forEach(claim => outpoints.push(`${claim.txid}:${claim.nout}`));
|
||||
|
||||
return outpoints;
|
||||
}
|
||||
);
|
||||
|
||||
export const selectFetchingMyChannels = createSelector(
|
||||
_selectState,
|
||||
state => !!state.fetchingMyChannels
|
||||
);
|
||||
|
||||
export const selectMyChannelClaims = createSelector(
|
||||
_selectState,
|
||||
selectClaimsById,
|
||||
(state, byId) => {
|
||||
const ids = state.myChannelClaims || [];
|
||||
const claims = [];
|
||||
|
||||
ids.forEach(id => claims.push(byId[id]));
|
||||
|
||||
return claims;
|
||||
}
|
||||
);
|
||||
|
|
|
@ -2,7 +2,8 @@ import lbry from "lbry";
|
|||
import { createSelector } from "reselect";
|
||||
import {
|
||||
selectClaimsByUri,
|
||||
selectClaimListMineIsPending,
|
||||
selectIsFetchingClaimListMine,
|
||||
selectMyClaims,
|
||||
selectMyClaimsOutpoints,
|
||||
} from "selectors/claims";
|
||||
|
||||
|
@ -13,16 +14,16 @@ export const selectFileInfosByOutpoint = createSelector(
|
|||
state => state.byOutpoint || {}
|
||||
);
|
||||
|
||||
export const selectFileListIsPending = createSelector(
|
||||
export const selectIsFetchingFileList = createSelector(
|
||||
_selectState,
|
||||
state => state.isFileListPending
|
||||
state => !!state.isFetchingFileList
|
||||
);
|
||||
|
||||
export const selectFileListDownloadedOrPublishedIsPending = createSelector(
|
||||
selectFileListIsPending,
|
||||
selectClaimListMineIsPending,
|
||||
(isFileListPending, isClaimListMinePending) =>
|
||||
isFileListPending || isClaimListMinePending
|
||||
export const selectIsFetchingFileListDownloadedOrPublished = createSelector(
|
||||
selectIsFetchingFileList,
|
||||
selectIsFetchingClaimListMine,
|
||||
(isFetchingFileList, isFetchingClaimListMine) =>
|
||||
isFetchingFileList || isFetchingClaimListMine
|
||||
);
|
||||
|
||||
export const selectFileInfoForUri = (state, props) => {
|
||||
|
@ -69,42 +70,38 @@ export const makeSelectLoadingForUri = () => {
|
|||
return createSelector(selectLoadingForUri, loading => !!loading);
|
||||
};
|
||||
|
||||
export const selectFileInfosDownloaded = createSelector(
|
||||
selectFileInfosByOutpoint,
|
||||
selectMyClaimsOutpoints,
|
||||
(byOutpoint, myClaimOutpoints) => {
|
||||
const fileInfoList = [];
|
||||
Object.values(byOutpoint).forEach(fileInfo => {
|
||||
if (
|
||||
fileInfo &&
|
||||
myClaimOutpoints.indexOf(fileInfo.outpoint) === -1 &&
|
||||
(fileInfo.completed || fileInfo.written_bytes)
|
||||
) {
|
||||
fileInfoList.push(fileInfo);
|
||||
}
|
||||
});
|
||||
return fileInfoList;
|
||||
}
|
||||
);
|
||||
|
||||
export const selectFileInfosPendingPublish = createSelector(
|
||||
_selectState,
|
||||
state => {
|
||||
return lbry.getPendingPublishes();
|
||||
state => Object.values(state.pendingByOutpoint || {})
|
||||
);
|
||||
|
||||
export const selectFileInfosDownloaded = createSelector(
|
||||
selectFileInfosByOutpoint,
|
||||
selectMyClaims,
|
||||
(byOutpoint, myClaims) => {
|
||||
return Object.values(byOutpoint).filter(fileInfo => {
|
||||
const myClaimIds = myClaims.map(claim => claim.claim_id);
|
||||
|
||||
return (
|
||||
fileInfo &&
|
||||
myClaimIds.indexOf(fileInfo.claim_id) === -1 &&
|
||||
(fileInfo.completed || fileInfo.written_bytes)
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export const selectFileInfosPublished = createSelector(
|
||||
selectFileInfosByOutpoint,
|
||||
selectFileInfosPendingPublish,
|
||||
selectMyClaimsOutpoints,
|
||||
(byOutpoint, pendingFileInfos, outpoints) => {
|
||||
selectFileInfosPendingPublish,
|
||||
(byOutpoint, outpoints, pendingPublish) => {
|
||||
const fileInfos = [];
|
||||
outpoints.forEach(outpoint => {
|
||||
const fileInfo = byOutpoint[outpoint];
|
||||
if (fileInfo) fileInfos.push(fileInfo);
|
||||
});
|
||||
return [...fileInfos, ...pendingFileInfos];
|
||||
return [...fileInfos, ...pendingPublish];
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -133,7 +130,6 @@ export const selectFileInfosByUri = createSelector(
|
|||
if (fileInfo) fileInfos[uri] = fileInfo;
|
||||
}
|
||||
});
|
||||
|
||||
return fileInfos;
|
||||
}
|
||||
);
|
||||
|
|
|
@ -86,18 +86,14 @@ const createStoreWithMiddleware = redux.compose(
|
|||
|
||||
const reduxStore = createStoreWithMiddleware(enableBatching(reducers));
|
||||
const compressor = createCompressor();
|
||||
const saveClaimsFilter = createFilter("claims", [
|
||||
"byId",
|
||||
"claimsByUri",
|
||||
"myClaims",
|
||||
]);
|
||||
const saveClaimsFilter = createFilter("claims", ["byId", "claimsByUri"]);
|
||||
|
||||
const persistOptions = {
|
||||
whitelist: ["claims"],
|
||||
// Order is important. Needs to be compressed last or other transforms can't
|
||||
// read the data
|
||||
transforms: [saveClaimsFilter, compressor],
|
||||
debounce: 1000,
|
||||
debounce: 10000,
|
||||
storage: localForage,
|
||||
};
|
||||
window.cacheStore = persistStore(reduxStore, persistOptions);
|
||||
|
|
|
@ -29,8 +29,10 @@
|
|||
"rc-progress": "^2.0.6",
|
||||
"react": "^15.4.0",
|
||||
"react-dom": "^15.4.0",
|
||||
"react-markdown": "^2.5.0",
|
||||
"react-modal": "^1.5.2",
|
||||
"react-redux": "^5.0.3",
|
||||
"react-simplemde-editor": "^3.6.11",
|
||||
"redux": "^3.6.0",
|
||||
"redux-action-buffer": "^1.1.0",
|
||||
"redux-logger": "^3.0.1",
|
||||
|
@ -52,6 +54,8 @@
|
|||
"babel-preset-es2015": "^6.24.1",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"babel-preset-stage-2": "^6.18.0",
|
||||
"electron-rebuild": "^1.5.11",
|
||||
"css-loader": "^0.28.4",
|
||||
"eslint": "^3.10.2",
|
||||
"eslint-config-airbnb": "^13.0.0",
|
||||
"eslint-loader": "^1.6.1",
|
||||
|
@ -64,6 +68,7 @@
|
|||
"lint-staged": "^3.6.0",
|
||||
"node-loader": "^0.6.0",
|
||||
"prettier": "^1.4.2",
|
||||
"style-loader": "^0.18.2",
|
||||
"webpack": "^2.6.1",
|
||||
"webpack-dev-server": "^2.4.4",
|
||||
"webpack-notifier": "^1.5.0",
|
||||
|
|
|
@ -117,6 +117,9 @@ input[type="text"].input-copyable {
|
|||
border: $width-input-border solid $color-form-border;
|
||||
}
|
||||
}
|
||||
.form-field--SimpleMDE {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-field__label {
|
||||
&[for] { cursor: pointer; }
|
||||
|
@ -164,3 +167,7 @@ input[type="text"].input-copyable {
|
|||
.form-field__helper {
|
||||
color: $color-help;
|
||||
}
|
||||
|
||||
.form-field__input.form-field__input-SimpleMDE .CodeMirror-scroll {
|
||||
height: auto;
|
||||
}
|
957
ui/yarn.lock
957
ui/yarn.lock
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue