Merge pull request #346 from lbryio/publishing2

Publishing Take 2
This commit is contained in:
Jeremy Kauffman 2017-07-13 10:19:51 -04:00 committed by GitHub
commit 2dbca1533f
33 changed files with 2479 additions and 1123 deletions

1
.gitignore vendored
View file

@ -26,3 +26,4 @@ build/daemon.zip
.vimrc .vimrc
package-lock.json package-lock.json
ui/yarn.lock

View file

@ -10,14 +10,18 @@ Web UI version numbers should always match the corresponding version of LBRY App
### Added ### Added
* Added option to release claim when deleting a file * Added option to release claim when deleting a file
* Added transition to card hovers to smooth animation * Added transition to card hovers to smooth animation
* Support markdown makeup in claim description
*
### Changed ### Changed
* * Publishes now uses claims rather than files
* *
### Fixed ### Fixed
* Fixed bug with download notice when switching window focus * 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 ### Deprecated
* *

View file

@ -15,6 +15,7 @@ import { selectBadgeNumber } from "selectors/app";
import { selectTotalDownloadProgress } from "selectors/file_info"; import { selectTotalDownloadProgress } from "selectors/file_info";
import setBadge from "util/setBadge"; import setBadge from "util/setBadge";
import setProgressBar from "util/setProgressBar"; import setProgressBar from "util/setProgressBar";
import { doFileList } from "actions/file_info";
import batchActions from "util/batchActions"; import batchActions from "util/batchActions";
const { ipcRenderer } = require("electron"); 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);
});
};
}

View file

@ -3,11 +3,11 @@ import lbry from "lbry";
import { doFetchClaimListMine } from "actions/content"; import { doFetchClaimListMine } from "actions/content";
import { import {
selectClaimsByUri, selectClaimsByUri,
selectClaimListMineIsPending, selectIsFetchingClaimListMine,
selectMyClaimsOutpoints, selectMyClaimsOutpoints,
} from "selectors/claims"; } from "selectors/claims";
import { import {
selectFileListIsPending, selectIsFetchingFileList,
selectFileInfosByOutpoint, selectFileInfosByOutpoint,
selectUrisLoading, selectUrisLoading,
} from "selectors/file_info"; } from "selectors/file_info";
@ -48,16 +48,16 @@ export function doFetchFileInfo(uri) {
export function doFileList() { export function doFileList() {
return function(dispatch, getState) { return function(dispatch, getState) {
const state = getState(); const state = getState();
const isPending = selectFileListIsPending(state); const isFetching = selectIsFetchingFileList(state);
if (!isPending) { if (!isFetching) {
dispatch({ dispatch({
type: types.FILE_LIST_STARTED, type: types.FILE_LIST_STARTED,
}); });
lbry.file_list().then(fileInfos => { lbry.file_list().then(fileInfos => {
dispatch({ dispatch({
type: types.FILE_LIST_COMPLETED, type: types.FILE_LIST_SUCCEEDED,
data: { data: {
fileInfos, fileInfos,
}, },
@ -102,14 +102,12 @@ export function doDeleteFile(outpoint, deleteFromComputer, abandonClaim) {
}, },
}); });
const success = () => { const success = dispatch({
dispatch({ type: types.ABANDON_CLAIM_SUCCEEDED,
type: types.ABANDON_CLAIM_COMPLETED, data: {
data: { claimId: fileInfo.claim_id,
claimId: fileInfo.claim_id, },
}, });
});
};
lbry.claim_abandon({ claim_id: fileInfo.claim_id }).then(success); lbry.claim_abandon({ claim_id: fileInfo.claim_id }).then(success);
} }
} }
@ -128,10 +126,10 @@ export function doDeleteFile(outpoint, deleteFromComputer, abandonClaim) {
export function doFetchFileInfosAndPublishedClaims() { export function doFetchFileInfosAndPublishedClaims() {
return function(dispatch, getState) { return function(dispatch, getState) {
const state = getState(), const state = getState(),
isClaimListMinePending = selectClaimListMineIsPending(state), isFetchingClaimListMine = selectIsFetchingClaimListMine(state),
isFileInfoListPending = selectFileListIsPending(state); isFetchingFileInfo = selectIsFetchingFileList(state);
dispatch(doFetchClaimListMine()); if (!isFetchingClaimListMine) dispatch(doFetchClaimListMine());
dispatch(doFileList()); if (!isFetchingFileInfo) dispatch(doFileList());
}; };
} }

View file

@ -1,10 +1,11 @@
import React from "react"; import React from "react";
import lbryuri from "lbryuri.js"; import lbryuri from "lbryuri.js";
import Link from "component/link"; import Link from "component/link";
import { TruncatedText, Icon } from "component/common"; import { Thumbnail, TruncatedText, Icon } from "component/common";
import FilePrice from "component/filePrice"; import FilePrice from "component/filePrice";
import UriIndicator from "component/uriIndicator"; import UriIndicator from "component/uriIndicator";
import NsfwOverlay from "component/nsfwOverlay"; import NsfwOverlay from "component/nsfwOverlay";
import TruncatedMarkdown from "component/truncatedMarkdown";
class FileCard extends React.PureComponent { class FileCard extends React.PureComponent {
constructor(props) { constructor(props) {
@ -94,7 +95,7 @@ class FileCard extends React.PureComponent {
style={{ backgroundImage: "url('" + metadata.thumbnail + "')" }} style={{ backgroundImage: "url('" + metadata.thumbnail + "')" }}
/>} />}
<div className="card__content card__subtext card__subtext--two-lines"> <div className="card__content card__subtext card__subtext--two-lines">
<TruncatedText lines={2}>{description}</TruncatedText> <TruncatedMarkdown lines={2}>{description}</TruncatedMarkdown>
</div> </div>
</Link> </Link>
</div> </div>

View file

@ -67,7 +67,9 @@ class FileList extends React.PureComponent {
const content = []; const content = [];
this._sortFunctions[sortBy](fileInfos).forEach(fileInfo => { this._sortFunctions[sortBy](fileInfos).forEach(fileInfo => {
let uriParams = {}; let uriParams = {
claimId: fileInfo.claim_id,
};
if (fileInfo.channel_name) { if (fileInfo.channel_name) {
uriParams.channelName = fileInfo.channel_name; uriParams.channelName = fileInfo.channel_name;
uriParams.contentName = fileInfo.name; uriParams.contentName = fileInfo.name;
@ -79,7 +81,7 @@ class FileList extends React.PureComponent {
content.push( content.push(
<FileTile <FileTile
key={uri} key={fileInfo.outpoint || fileInfo.claim_id}
uri={uri} uri={uri}
hidePrice={true} hidePrice={true}
showEmpty={this.props.fileTileShowEmpty} showEmpty={this.props.fileTileShowEmpty}
@ -94,7 +96,6 @@ class FileList extends React.PureComponent {
<FormField type="select" onChange={this.handleSortChanged.bind(this)}> <FormField type="select" onChange={this.handleSortChanged.bind(this)}>
<option value="date">{__("Date")}</option> <option value="date">{__("Date")}</option>
<option value="title">{__("Title")}</option> <option value="title">{__("Title")}</option>
<option value="filename">{__("File name")}</option>
</FormField> </FormField>
</span> </span>
{content} {content}

View file

@ -64,7 +64,7 @@ class FileTile extends React.PureComponent {
const isClaimable = lbryuri.isClaimable(uri); const isClaimable = lbryuri.isClaimable(uri);
const title = isClaimed && metadata && metadata.title const title = isClaimed && metadata && metadata.title
? metadata.title ? metadata.title
: uri; : lbryuri.parse(uri).contentName;
const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw; const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw;
let onClick = () => navigate("/show", { uri }); let onClick = () => navigate("/show", { uri });

View file

@ -1,8 +1,9 @@
import React from "react"; import React from "react";
import FileSelector from "./file-selector.js"; 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"], formFieldFileSelectorTypes = ["file", "directory"],
formFieldNestedLabelTypes = ["radio", "checkbox"]; formFieldNestedLabelTypes = ["radio", "checkbox"];
@ -24,6 +25,7 @@ export class FormField extends React.PureComponent {
this._fieldRequiredText = __("This field is required"); this._fieldRequiredText = __("This field is required");
this._type = null; this._type = null;
this._element = null; this._element = null;
this._extraElementProps = {};
this.state = { this.state = {
isError: null, isError: null,
@ -38,6 +40,12 @@ export class FormField extends React.PureComponent {
} else if (this.props.type == "text-number") { } else if (this.props.type == "text-number") {
this._element = "input"; this._element = "input";
this._type = "text"; 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)) { } else if (formFieldFileSelectorTypes.includes(this.props.type)) {
this._element = "input"; this._element = "input";
this._type = "hidden"; this._type = "hidden";
@ -81,6 +89,8 @@ export class FormField extends React.PureComponent {
getValue() { getValue() {
if (this.props.type == "checkbox") { if (this.props.type == "checkbox") {
return this.refs.field.checked; return this.refs.field.checked;
} else if (this.props.type == "SimpleMDE") {
return this.refs.field.simplemde.value();
} else { } else {
return this.refs.field.value; return this.refs.field.value;
} }
@ -90,6 +100,10 @@ export class FormField extends React.PureComponent {
return this.refs.field.options[this.refs.field.selectedIndex]; return this.refs.field.options[this.refs.field.selectedIndex];
} }
getOptions() {
return this.refs.field.options;
}
render() { render() {
// Pass all unhandled props to the field element // Pass all unhandled props to the field element
const otherProps = Object.assign({}, this.props), const otherProps = Object.assign({}, this.props),
@ -106,7 +120,6 @@ export class FormField extends React.PureComponent {
delete otherProps.className; delete otherProps.className;
delete otherProps.postfix; delete otherProps.postfix;
delete otherProps.prefix; delete otherProps.prefix;
const element = ( const element = (
<this._element <this._element
id={elementId} id={elementId}
@ -122,6 +135,7 @@ export class FormField extends React.PureComponent {
(isError ? "form-field__input--error" : "") (isError ? "form-field__input--error" : "")
} }
{...otherProps} {...otherProps}
{...this._extraElementProps}
> >
{this.props.children} {this.props.children}
</this._element> </this._element>
@ -220,6 +234,10 @@ export class FormRow extends React.PureComponent {
return this.refs.field.getSelectedElement(); return this.refs.field.getSelectedElement();
} }
getOptions() {
return this.refs.field.getOptions();
}
focus() { focus() {
this.refs.field.focus(); this.refs.field.focus();
} }

View file

@ -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;

View file

@ -0,0 +1,5 @@
import React from "react";
import { connect } from "react-redux";
import PublishForm from "./view";
export default connect()(PublishForm);

View 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;

View 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;

View file

@ -0,0 +1,5 @@
import React from "react";
import { connect } from "react-redux";
import TruncatedMarkdown from "./view";
export default connect()(TruncatedMarkdown);

View 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;

View file

@ -47,7 +47,7 @@ export const FETCH_CLAIM_LIST_MINE_STARTED = "FETCH_CLAIM_LIST_MINE_STARTED";
export const FETCH_CLAIM_LIST_MINE_COMPLETED = export const FETCH_CLAIM_LIST_MINE_COMPLETED =
"FETCH_CLAIM_LIST_MINE_COMPLETED"; "FETCH_CLAIM_LIST_MINE_COMPLETED";
export const FILE_LIST_STARTED = "FILE_LIST_STARTED"; 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_STARTED = "FETCH_FILE_INFO_STARTED";
export const FETCH_FILE_INFO_COMPLETED = "FETCH_FILE_INFO_COMPLETED"; export const FETCH_FILE_INFO_COMPLETED = "FETCH_FILE_INFO_COMPLETED";
export const FETCH_COST_INFO_STARTED = "FETCH_COST_INFO_STARTED"; 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 FETCH_AVAILABILITY_COMPLETED = "FETCH_AVAILABILITY_COMPLETED";
export const FILE_DELETE = "FILE_DELETE"; export const FILE_DELETE = "FILE_DELETE";
export const ABANDON_CLAIM_STARTED = "ABANDON_CLAIM_STARTED"; 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 // Search
export const SEARCH_STARTED = "SEARCH_STARTED"; export const SEARCH_STARTED = "SEARCH_STARTED";

View file

@ -223,45 +223,38 @@ lbry.publishDeprecated = function(
) { ) {
lbry.publish(params).then( lbry.publish(params).then(
result => { result => {
if (returnedPending) { if (returnPendingTimeout) clearTimeout(returnPendingTimeout);
return;
}
clearTimeout(returnPendingTimeout);
publishedCallback(result); publishedCallback(result);
}, },
err => { err => {
if (returnedPending) { if (returnPendingTimeout) clearTimeout(returnPendingTimeout);
return;
}
clearTimeout(returnPendingTimeout);
errorCallback(err); errorCallback(err);
} }
); );
let returnedPending = false;
// Give a short grace period in case publish() returns right away or (more likely) gives an error // Give a short grace period in case publish() returns right away or (more likely) gives an error
const returnPendingTimeout = setTimeout(() => { const returnPendingTimeout = setTimeout(
returnedPending = true; () => {
if (publishedCallback) {
savePendingPublish({
name: params.name,
channel_name: params.channel_name,
});
publishedCallback(true);
}
if (publishedCallback) { if (fileListedCallback) {
savePendingPublish({ const { name, channel_name } = params;
name: params.name, savePendingPublish({
channel_name: params.channel_name, name: params.name,
}); channel_name: params.channel_name,
publishedCallback(true); });
} fileListedCallback(true);
}
if (fileListedCallback) { },
const { name, channel_name } = params; 2000,
savePendingPublish({ { once: true }
name: params.name, );
channel_name: params.channel_name,
});
fileListedCallback(true);
}
}, 2000);
}; };
lbry.getClientSettings = function() { lbry.getClientSettings = function() {

View file

@ -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 /* Takes a parseable LBRY URI and converts it to standard, canonical format (currently this just
* consists of adding the lbry:// prefix if needed) */ * consists of adding the lbry:// prefix if needed) */
lbryuri.normalize = function(uri) { lbryuri.normalize = function(uri) {
if (uri.match(/pending_claim/)) return uri;
const { name, path, bidPosition, claimSequence, claimId } = lbryuri.parse( const { name, path, bidPosition, claimSequence, claimId } = lbryuri.parse(
uri uri
); );

View file

@ -3,15 +3,22 @@ import { connect } from "react-redux";
import { doFetchFileInfosAndPublishedClaims } from "actions/file_info"; import { doFetchFileInfosAndPublishedClaims } from "actions/file_info";
import { import {
selectFileInfosDownloaded, selectFileInfosDownloaded,
selectFileListDownloadedOrPublishedIsPending, selectIsFetchingFileListDownloadedOrPublished,
} from "selectors/file_info"; } from "selectors/file_info";
import {
selectMyClaimsWithoutChannels,
selectIsFetchingClaimListMine,
} from "selectors/claims";
import { doFetchClaimListMine } from "actions/content";
import { doNavigate } from "actions/app"; import { doNavigate } from "actions/app";
import { doCancelAllResolvingUris } from "actions/content"; import { doCancelAllResolvingUris } from "actions/content";
import FileListDownloaded from "./view"; import FileListDownloaded from "./view";
const select = state => ({ const select = state => ({
fileInfos: selectFileInfosDownloaded(state), fileInfos: selectFileInfosDownloaded(state),
isPending: selectFileListDownloadedOrPublishedIsPending(state), isFetching: selectIsFetchingFileListDownloadedOrPublished(state),
claims: selectMyClaimsWithoutChannels(state),
isFetchingClaims: selectIsFetchingClaimListMine(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({
@ -19,6 +26,7 @@ const perform = dispatch => ({
fetchFileInfosDownloaded: () => fetchFileInfosDownloaded: () =>
dispatch(doFetchFileInfosAndPublishedClaims()), dispatch(doFetchFileInfosAndPublishedClaims()),
cancelResolvingUris: () => dispatch(doCancelAllResolvingUris()), cancelResolvingUris: () => dispatch(doCancelAllResolvingUris()),
fetchClaims: () => dispatch(doFetchClaimListMine()),
}); });
export default connect(select, perform)(FileListDownloaded); export default connect(select, perform)(FileListDownloaded);

View file

@ -12,7 +12,8 @@ import SubHeader from "component/subHeader";
class FileListDownloaded extends React.PureComponent { class FileListDownloaded extends React.PureComponent {
componentWillMount() { componentWillMount() {
if (!this.props.isPending) this.props.fetchFileInfosDownloaded(); if (!this.props.isFetchingClaims) this.props.fetchClaims();
if (!this.props.isFetching) this.props.fetchFileInfosDownloaded();
} }
componentWillUnmount() { componentWillUnmount() {
@ -20,13 +21,13 @@ class FileListDownloaded extends React.PureComponent {
} }
render() { render() {
const { fileInfos, isPending, navigate } = this.props; const { fileInfos, isFetching, navigate } = this.props;
let content; let content;
if (fileInfos && fileInfos.length > 0) { if (fileInfos && fileInfos.length > 0) {
content = <FileList fileInfos={fileInfos} fetching={isPending} />; content = <FileList fileInfos={fileInfos} fetching={isFetching} />;
} else { } else {
if (isPending) { if (isFetching) {
content = <BusyMessage message={__("Loading")} />; content = <BusyMessage message={__("Loading")} />;
} else { } else {
content = ( content = (

View file

@ -1,24 +1,24 @@
import React from "react"; import React from "react";
import rewards from "rewards"; import rewards from "rewards";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { doFetchFileInfosAndPublishedClaims } from "actions/file_info"; import { doFetchClaimListMine } from "actions/content";
import { import {
selectFileInfosPublished, selectMyClaimsWithoutChannels,
selectFileListDownloadedOrPublishedIsPending, selectIsFetchingClaimListMine,
} from "selectors/file_info"; } from "selectors/claims";
import { doClaimRewardType } from "actions/rewards"; import { doClaimRewardType } from "actions/rewards";
import { doNavigate } from "actions/app"; import { doNavigate } from "actions/app";
import { doCancelAllResolvingUris } from "actions/content"; import { doCancelAllResolvingUris } from "actions/content";
import FileListPublished from "./view"; import FileListPublished from "./view";
const select = state => ({ const select = state => ({
fileInfos: selectFileInfosPublished(state), claims: selectMyClaimsWithoutChannels(state),
isPending: selectFileListDownloadedOrPublishedIsPending(state), isFetching: selectIsFetchingClaimListMine(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({
navigate: path => dispatch(doNavigate(path)), navigate: path => dispatch(doNavigate(path)),
fetchFileListPublished: () => dispatch(doFetchFileInfosAndPublishedClaims()), fetchClaims: () => dispatch(doFetchClaimListMine()),
claimFirstPublishReward: () => claimFirstPublishReward: () =>
dispatch(doClaimRewardType(rewards.TYPE_FIRST_PUBLISH)), dispatch(doClaimRewardType(rewards.TYPE_FIRST_PUBLISH)),
cancelResolvingUris: () => dispatch(doCancelAllResolvingUris()), cancelResolvingUris: () => dispatch(doCancelAllResolvingUris()),

View file

@ -12,11 +12,11 @@ import SubHeader from "component/subHeader";
class FileListPublished extends React.PureComponent { class FileListPublished extends React.PureComponent {
componentWillMount() { componentWillMount() {
if (!this.props.isPending) this.props.fetchFileListPublished(); if (!this.props.isFetching) this.props.fetchClaims();
} }
componentDidUpdate() { componentDidUpdate() {
if (this.props.fileInfos.length > 0) this.props.claimFirstPublishReward(); // if (this.props.claims.length > 0) this.props.fetchClaims();
} }
componentWillUnmount() { componentWillUnmount() {
@ -24,20 +24,20 @@ class FileListPublished extends React.PureComponent {
} }
render() { render() {
const { fileInfos, isPending, navigate } = this.props; const { claims, isFetching, navigate } = this.props;
let content; let content;
if (fileInfos && fileInfos.length > 0) { if (claims && claims.length > 0) {
content = ( content = (
<FileList <FileList
fileInfos={fileInfos} fileInfos={claims}
fetching={isPending} fetching={isFetching}
fileTileShowEmpty={FileTile.SHOW_EMPTY_PENDING} fileTileShowEmpty={FileTile.SHOW_EMPTY_PENDING}
/> />
); );
} else { } else {
if (isPending) { if (isFetching) {
content = <BusyMessage message={__("Loading")} />; content = <BusyMessage message={__("Loading")} />;
} else { } else {
content = ( content = (

View file

@ -1,4 +1,5 @@
import React from "react"; import React from "react";
import ReactMarkdown from "react-markdown";
import lbry from "lbry.js"; import lbry from "lbry.js";
import lbryuri from "lbryuri.js"; import lbryuri from "lbryuri.js";
import Video from "component/video"; import Video from "component/video";
@ -119,7 +120,11 @@ class FilePage extends React.PureComponent {
</div> </div>
</div> </div>
<div className="card__content card__subtext card__subtext card__subtext--allow-newlines"> <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>
</div> </div>
{metadata {metadata

View file

@ -2,13 +2,29 @@ import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { doNavigate, doHistoryBack } from "actions/app"; import { doNavigate, doHistoryBack } from "actions/app";
import { doClaimRewardType } from "actions/rewards"; import { doClaimRewardType } from "actions/rewards";
import { selectMyClaims } from "selectors/claims"; import {
import { doFetchClaimListMine } from "actions/content"; 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 rewards from "rewards";
import PublishPage from "./view"; import PublishPage from "./view";
const select = state => ({ const select = state => ({
myClaims: selectMyClaims(state), myClaims: selectMyClaims(state),
fetchingChannels: selectFetchingMyChannels(state),
channels: selectMyChannelClaims(state),
claimsByUri: selectClaimsByUri(state),
resolvingUris: selectResolvingUris(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({
@ -17,6 +33,10 @@ const perform = dispatch => ({
fetchClaimListMine: () => dispatch(doFetchClaimListMine()), fetchClaimListMine: () => dispatch(doFetchClaimListMine()),
claimFirstChannelReward: () => claimFirstChannelReward: () =>
dispatch(doClaimRewardType(rewards.TYPE_FIRST_CHANNEL)), 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); export default connect(select, perform)(PublishPage);

View file

@ -1,922 +1,8 @@
import React from "react"; import React from "react";
import lbry from "lbry"; import PublishForm from "component/publishForm";
import lbryuri from "lbryuri";
import { FormField, FormRow } from "component/form.js";
import Link from "component/link";
import rewards from "rewards";
import Modal from "component/modal";
class PublishPage extends React.PureComponent { const PublishPage = props => {
constructor(props) { return <PublishForm {...props} />;
super(props); };
this._requiredFields = ["meta_title", "name", "bid", "tos_agree"];
this.state = {
channels: null,
rawName: "",
name: "",
bid: 10,
hasFile: false,
feeAmount: "",
feeCurrency: "USD",
channel: "anonymous",
newChannelName: "@",
newChannelBid: 10,
nameResolved: null,
myClaimExists: null,
topClaimValue: 0.0,
myClaimValue: 0.0,
myClaimMetadata: null,
copyrightNotice: "",
otherLicenseDescription: "",
otherLicenseUrl: "",
uploadProgress: 0.0,
uploaded: false,
errorMessage: null,
submitting: false,
creatingChannel: false,
modal: null,
};
}
_updateChannelList(channel) {
// Calls API to update displayed list of channels. If a channel name is provided, will select
// that channel at the same time (used immediately after creating a channel)
lbry.channel_list_mine().then(channels => {
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; export default PublishPage;

View file

@ -24,7 +24,7 @@ class ShowPage extends React.PureComponent {
let innerContent = ""; let innerContent = "";
if (isResolvingUri && !claim) { if ((isResolvingUri && !claim) || !claim) {
innerContent = ( innerContent = (
<section className="card"> <section className="card">
<div className="card__inner"> <div className="card__inner">

View file

@ -15,7 +15,13 @@ reducers[types.RESOLVE_URI_COMPLETED] = function(state, action) {
byUri[uri] = claim.claim_id; byUri[uri] = claim.claim_id;
} else if (claim === undefined && certificate !== undefined) { } else if (claim === undefined && certificate !== undefined) {
byId[certificate.claim_id] = certificate; byId[certificate.claim_id] = certificate;
byUri[uri] = certificate.claim_id; // 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 { } else {
byUri[uri] = null; byUri[uri] = null;
} }
@ -28,43 +34,72 @@ reducers[types.RESOLVE_URI_COMPLETED] = function(state, action) {
reducers[types.FETCH_CLAIM_LIST_MINE_STARTED] = function(state, action) { reducers[types.FETCH_CLAIM_LIST_MINE_STARTED] = function(state, action) {
return Object.assign({}, state, { return Object.assign({}, state, {
isClaimListMinePending: true, isFetchingClaimListMine: true,
}); });
}; };
reducers[types.FETCH_CLAIM_LIST_MINE_COMPLETED] = function(state, action) { reducers[types.FETCH_CLAIM_LIST_MINE_COMPLETED] = function(state, action) {
const { claims } = action.data; const { claims } = action.data;
const myClaims = new Set(state.myClaims);
const byUri = Object.assign({}, state.claimsByUri); const byUri = Object.assign({}, state.claimsByUri);
const byId = Object.assign({}, state.byId); 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 => { claims.forEach(claim => {
myClaims.add(claim.claim_id);
byId[claim.claim_id] = claim; 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, { return Object.assign({}, state, {
isClaimListMinePending: false, isFetchingClaimListMine: false,
myClaims: myClaims, myClaims: myClaims,
byId, byId,
pendingById,
}); });
}; };
// reducers[types.FETCH_CHANNEL_CLAIMS_STARTED] = function(state, action) { reducers[types.FETCH_CHANNEL_LIST_MINE_STARTED] = function(state, action) {
// const { return Object.assign({}, state, { fetchingMyChannels: true });
// uri, };
// } = action.data
// reducers[types.FETCH_CHANNEL_LIST_MINE_COMPLETED] = function(state, action) {
// const newClaims = Object.assign({}, state.claimsByChannel) const { claims } = action.data;
// const myChannelClaims = new Set(state.myChannelClaims);
// if (claims !== undefined) { const byId = Object.assign({}, state.byId);
// newClaims[uri] = claims
// } claims.forEach(claim => {
// myChannelClaims.add(claim.claim_id);
// return Object.assign({}, state, { byId[claims.claim_id] = claim;
// claimsByChannel: newClaims });
// })
// } return Object.assign({}, state, {
byId,
fetchingMyChannels: false,
myChannelClaims,
});
};
reducers[types.FETCH_CHANNEL_CLAIMS_COMPLETED] = function(state, action) { reducers[types.FETCH_CHANNEL_CLAIMS_COMPLETED] = function(state, action) {
const { uri, claims } = action.data; 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 { claimId } = action.data;
const myClaims = new Set(state.myClaims); const myClaims = new Set(state.myClaims);
const byId = Object.assign({}, state.byId); 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) { export default function reducer(state = defaultState, action) {
const handler = reducers[action.type]; const handler = reducers[action.type];
if (handler) return handler(state, action); if (handler) return handler(state, action);

View file

@ -6,14 +6,15 @@ const defaultState = {};
reducers[types.FILE_LIST_STARTED] = function(state, action) { reducers[types.FILE_LIST_STARTED] = function(state, action) {
return Object.assign({}, state, { 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 { fileInfos } = action.data;
const newByOutpoint = Object.assign({}, state.byOutpoint); const newByOutpoint = Object.assign({}, state.byOutpoint);
const pendingByOutpoint = Object.assign({}, state.pendingByOutpoint);
fileInfos.forEach(fileInfo => { fileInfos.forEach(fileInfo => {
const { outpoint } = fileInfo; const { outpoint } = fileInfo;
@ -21,8 +22,9 @@ reducers[types.FILE_LIST_COMPLETED] = function(state, action) {
}); });
return Object.assign({}, state, { return Object.assign({}, state, {
isFileListPending: false, isFetchingFileList: false,
byOutpoint: newByOutpoint, byOutpoint: newByOutpoint,
pendingByOutpoint,
}); });
}; };

View file

@ -51,7 +51,7 @@ export const makeSelectClaimForUri = () => {
const selectClaimForUriIsMine = (state, props) => { const selectClaimForUriIsMine = (state, props) => {
const uri = lbryuri.normalize(props.uri); const uri = lbryuri.normalize(props.uri);
const claim = selectClaimsByUri(state)[uri]; const claim = selectClaimsByUri(state)[uri];
const myClaims = selectMyClaims(state); const myClaims = selectMyClaimsRaw(state);
return myClaims.has(claim.claim_id); return myClaims.has(claim.claim_id);
}; };
@ -100,27 +100,72 @@ export const makeSelectContentTypeForUri = () => {
); );
}; };
export const selectClaimListMineIsPending = createSelector( export const selectIsFetchingClaimListMine = createSelector(
_selectState, _selectState,
state => state.isClaimListMinePending state => !!state.isFetchingClaimListMine
); );
export const selectMyClaims = createSelector( export const selectMyClaimsRaw = createSelector(
_selectState, _selectState,
state => new Set(state.myClaims) 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( export const selectMyClaimsOutpoints = createSelector(
selectMyClaims, selectMyClaims,
selectClaimsById, myClaims => {
(claimIds, byId) => {
const outpoints = []; const outpoints = [];
claimIds.forEach(claimId => { myClaims.forEach(claim => outpoints.push(`${claim.txid}:${claim.nout}`));
const claim = byId[claimId];
if (claim) outpoints.push(`${claim.txid}:${claim.nout}`);
});
return outpoints; 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;
}
);

View file

@ -2,7 +2,8 @@ import lbry from "lbry";
import { createSelector } from "reselect"; import { createSelector } from "reselect";
import { import {
selectClaimsByUri, selectClaimsByUri,
selectClaimListMineIsPending, selectIsFetchingClaimListMine,
selectMyClaims,
selectMyClaimsOutpoints, selectMyClaimsOutpoints,
} from "selectors/claims"; } from "selectors/claims";
@ -13,16 +14,16 @@ export const selectFileInfosByOutpoint = createSelector(
state => state.byOutpoint || {} state => state.byOutpoint || {}
); );
export const selectFileListIsPending = createSelector( export const selectIsFetchingFileList = createSelector(
_selectState, _selectState,
state => state.isFileListPending state => !!state.isFetchingFileList
); );
export const selectFileListDownloadedOrPublishedIsPending = createSelector( export const selectIsFetchingFileListDownloadedOrPublished = createSelector(
selectFileListIsPending, selectIsFetchingFileList,
selectClaimListMineIsPending, selectIsFetchingClaimListMine,
(isFileListPending, isClaimListMinePending) => (isFetchingFileList, isFetchingClaimListMine) =>
isFileListPending || isClaimListMinePending isFetchingFileList || isFetchingClaimListMine
); );
export const selectFileInfoForUri = (state, props) => { export const selectFileInfoForUri = (state, props) => {
@ -69,42 +70,38 @@ export const makeSelectLoadingForUri = () => {
return createSelector(selectLoadingForUri, loading => !!loading); 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( export const selectFileInfosPendingPublish = createSelector(
_selectState, _selectState,
state => { state => Object.values(state.pendingByOutpoint || {})
return lbry.getPendingPublishes(); );
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( export const selectFileInfosPublished = createSelector(
selectFileInfosByOutpoint, selectFileInfosByOutpoint,
selectFileInfosPendingPublish,
selectMyClaimsOutpoints, selectMyClaimsOutpoints,
(byOutpoint, pendingFileInfos, outpoints) => { selectFileInfosPendingPublish,
(byOutpoint, outpoints, pendingPublish) => {
const fileInfos = []; const fileInfos = [];
outpoints.forEach(outpoint => { outpoints.forEach(outpoint => {
const fileInfo = byOutpoint[outpoint]; const fileInfo = byOutpoint[outpoint];
if (fileInfo) fileInfos.push(fileInfo); if (fileInfo) fileInfos.push(fileInfo);
}); });
return [...fileInfos, ...pendingFileInfos]; return [...fileInfos, ...pendingPublish];
} }
); );
@ -133,7 +130,6 @@ export const selectFileInfosByUri = createSelector(
if (fileInfo) fileInfos[uri] = fileInfo; if (fileInfo) fileInfos[uri] = fileInfo;
} }
}); });
return fileInfos; return fileInfos;
} }
); );

View file

@ -86,18 +86,14 @@ const createStoreWithMiddleware = redux.compose(
const reduxStore = createStoreWithMiddleware(enableBatching(reducers)); const reduxStore = createStoreWithMiddleware(enableBatching(reducers));
const compressor = createCompressor(); const compressor = createCompressor();
const saveClaimsFilter = createFilter("claims", [ const saveClaimsFilter = createFilter("claims", ["byId", "claimsByUri"]);
"byId",
"claimsByUri",
"myClaims",
]);
const persistOptions = { const persistOptions = {
whitelist: ["claims"], whitelist: ["claims"],
// Order is important. Needs to be compressed last or other transforms can't // Order is important. Needs to be compressed last or other transforms can't
// read the data // read the data
transforms: [saveClaimsFilter, compressor], transforms: [saveClaimsFilter, compressor],
debounce: 1000, debounce: 10000,
storage: localForage, storage: localForage,
}; };
window.cacheStore = persistStore(reduxStore, persistOptions); window.cacheStore = persistStore(reduxStore, persistOptions);

View file

@ -29,8 +29,10 @@
"rc-progress": "^2.0.6", "rc-progress": "^2.0.6",
"react": "^15.4.0", "react": "^15.4.0",
"react-dom": "^15.4.0", "react-dom": "^15.4.0",
"react-markdown": "^2.5.0",
"react-modal": "^1.5.2", "react-modal": "^1.5.2",
"react-redux": "^5.0.3", "react-redux": "^5.0.3",
"react-simplemde-editor": "^3.6.11",
"redux": "^3.6.0", "redux": "^3.6.0",
"redux-action-buffer": "^1.1.0", "redux-action-buffer": "^1.1.0",
"redux-logger": "^3.0.1", "redux-logger": "^3.0.1",
@ -52,6 +54,8 @@
"babel-preset-es2015": "^6.24.1", "babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1", "babel-preset-react": "^6.24.1",
"babel-preset-stage-2": "^6.18.0", "babel-preset-stage-2": "^6.18.0",
"electron-rebuild": "^1.5.11",
"css-loader": "^0.28.4",
"eslint": "^3.10.2", "eslint": "^3.10.2",
"eslint-config-airbnb": "^13.0.0", "eslint-config-airbnb": "^13.0.0",
"eslint-loader": "^1.6.1", "eslint-loader": "^1.6.1",
@ -64,6 +68,7 @@
"lint-staged": "^3.6.0", "lint-staged": "^3.6.0",
"node-loader": "^0.6.0", "node-loader": "^0.6.0",
"prettier": "^1.4.2", "prettier": "^1.4.2",
"style-loader": "^0.18.2",
"webpack": "^2.6.1", "webpack": "^2.6.1",
"webpack-dev-server": "^2.4.4", "webpack-dev-server": "^2.4.4",
"webpack-notifier": "^1.5.0", "webpack-notifier": "^1.5.0",

View file

@ -117,6 +117,9 @@ input[type="text"].input-copyable {
border: $width-input-border solid $color-form-border; border: $width-input-border solid $color-form-border;
} }
} }
.form-field--SimpleMDE {
display: block;
}
.form-field__label { .form-field__label {
&[for] { cursor: pointer; } &[for] { cursor: pointer; }
@ -163,4 +166,8 @@ input[type="text"].input-copyable {
} }
.form-field__helper { .form-field__helper {
color: $color-help; color: $color-help;
}
.form-field__input.form-field__input-SimpleMDE .CodeMirror-scroll {
height: auto;
} }

File diff suppressed because it is too large Load diff