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
|
.vimrc
|
||||||
|
|
||||||
package-lock.json
|
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
|
||||||
* 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
|
||||||
*
|
*
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -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());
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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 });
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 =
|
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";
|
||||||
|
|
|
@ -223,28 +223,18 @@ 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) {
|
if (publishedCallback) {
|
||||||
savePendingPublish({
|
savePendingPublish({
|
||||||
name: params.name,
|
name: params.name,
|
||||||
|
@ -261,7 +251,10 @@ lbry.publishDeprecated = function(
|
||||||
});
|
});
|
||||||
fileListedCallback(true);
|
fileListedCallback(true);
|
||||||
}
|
}
|
||||||
}, 2000);
|
},
|
||||||
|
2000,
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
lbry.getClientSettings = function() {
|
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
|
/* 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
|
||||||
);
|
);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 = (
|
||||||
|
|
|
@ -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()),
|
||||||
|
|
|
@ -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 = (
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -15,10 +15,16 @@ 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;
|
||||||
|
// 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;
|
byUri[uri] = certificate.claim_id;
|
||||||
} else {
|
} else {
|
||||||
byUri[uri] = null;
|
byUri[uri] = null;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
byUri[uri] = null;
|
||||||
|
}
|
||||||
|
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
byId,
|
byId,
|
||||||
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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; }
|
||||||
|
@ -164,3 +167,7 @@ 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;
|
||||||
|
}
|
957
ui/yarn.lock
957
ui/yarn.lock
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue