Progress towards working publish

This commit is contained in:
6ea86b96 2017-06-18 00:59:18 +07:00
parent df954882bc
commit 8325828f6e
11 changed files with 1399 additions and 159 deletions

View file

@ -15,6 +15,7 @@ import { selectBadgeNumber } from "selectors/app";
import { selectTotalDownloadProgress } from "selectors/file_info";
import setBadge from "util/setBadge";
import setProgressBar from "util/setProgressBar";
import { doFileList } from "actions/file_info";
import batchActions from "util/batchActions";
const { ipcRenderer } = require("electron");
@ -356,3 +357,92 @@ export function doFetchChannelListMine() {
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 => {
resolve(err);
}
);
});
};
}
export function doPublish(params) {
return function(dispatch, getState) {
let uri;
const { name, channel_name } = params;
if (channel_name) {
uri = lbryuri.build({ name: channel_name, path: name }, false);
} else {
uri = lbryuri.build({ name: name }, false);
}
const pendingPublish = {
name,
channel_name,
claim_id: "pending_claim_" + uri,
txid: "pending_" + uri,
nout: 0,
outpoint: "pending_" + uri + ":0",
time: Date.now(),
};
dispatch({
type: types.PUBLISH_STARTED,
data: {
params,
pendingPublish,
},
});
return new Promise((resolve, reject) => {
const success = claim => {
claim.name = params.name;
claim.channel_name = params.channel_name;
dispatch({
type: types.PUBLISH_COMPLETED,
data: {
claim,
uri,
pendingPublish,
},
});
dispatch(doFileList());
resolve(claim);
};
const failure = error => {
dispatch({
type: types.PUBLISH_FAILED,
data: {
error,
params,
uri,
pendingPublish,
},
});
reject(error);
};
lbry.publish(params).then(success, failure);
});
};
}

View file

@ -42,6 +42,40 @@ export class TruncatedText extends React.PureComponent {
}
}
export 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 class BusyMessage extends React.PureComponent {
static propTypes = {
message: React.PropTypes.string,

View file

@ -68,6 +68,11 @@ export const FETCH_CHANNEL_LIST_MINE_STARTED =
"FETCH_CHANNEL_LIST_MINE_STARTED";
export const FETCH_CHANNEL_LIST_MINE_COMPLETED =
"FETCH_CHANNEL_LIST_MINE_COMPLETED";
export const CREATE_CHANNEL_STARTED = "CREATE_CHANNEL_STARTED";
export const CREATE_CHANNEL_COMPLETED = "CREATE_CHANNEL_COMPLETED";
export const PUBLISH_STARTED = "PUBLISH_STARTED";
export const PUBLISH_COMPLETED = "PUBLISH_COMPLETED";
export const PUBLISH_FAILED = "PUBLISH_FAILED";
// Search
export const SEARCH_STARTED = "SEARCH_STARTED";

View file

@ -203,6 +203,8 @@ lbryuri.build = function(uriObj, includeProto = true, allowExtraProps = false) {
/* Takes a parseable LBRY URI and converts it to standard, canonical format (currently this just
* consists of adding the lbry:// prefix if needed) */
lbryuri.normalize = function(uri) {
if (uri.match(/pending_claim/)) return uri;
const { name, path, bidPosition, claimSequence, claimId } = lbryuri.parse(
uri
);

View file

@ -13,6 +13,8 @@ import {
doFetchClaimListMine,
doFetchChannelListMine,
doResolveUri,
doCreateChannel,
doPublish,
} from "actions/content";
import rewards from "rewards";
import PublishPage from "./view";
@ -33,6 +35,8 @@ const perform = dispatch => ({
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);

View file

@ -125,16 +125,11 @@ class PublishPage extends React.PureComponent {
publishArgs.file_path = this.refs.file.getValue();
}
lbry.publishDeprecated(
publishArgs,
message => {
this.handlePublishStarted();
},
null,
error => {
this.handlePublishError(error);
}
);
const success = claim => {};
const failure = error => this.handlePublishError(error);
this.handlePublishStarted();
this.props.publish(publishArgs).then(success, failure);
};
if (this.state.isFee) {
@ -216,6 +211,10 @@ class PublishPage extends React.PureComponent {
handleNameChange(event) {
var rawName = event.target.value;
this.nameChanged(rawName);
}
nameChanged(rawName) {
if (!rawName) {
this.setState({
rawName: "",
@ -233,15 +232,26 @@ class PublishPage extends React.PureComponent {
return;
}
let channel = "";
if (this.state.channel !== "anonymous") channel = this.state.channel;
const name = rawName.toLowerCase();
const uri = lbryuri.normalize(name);
const uri = lbryuri.build({ contentName: name, channelName: channel });
this.setState({
rawName: rawName,
name: name,
uri,
});
this.props.resolveUri(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,
});
}
handleBidChange(event) {
@ -302,40 +312,12 @@ class PublishPage extends React.PureComponent {
});
}
handleChannelChange(event) {
const channel = event.target.value;
handleChannelChange(channelName) {
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,
channel: channelName,
});
const nameChanged = () => this.nameChanged(this.state.rawName);
setTimeout(nameChanged.bind(this), 500, { once: true });
}
handleTOSChange(event) {
@ -413,19 +395,26 @@ class PublishPage extends React.PureComponent {
getNameBidHelpText() {
if (
this.state.uri &&
this.props.resolvingUris.indexOf(this.state.uri) !== -1
this.props.resolvingUris.indexOf(this.state.uri) !== -1 &&
this.claim() === undefined
) {
return <BusyMessage />;
} 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()) {
return __(
"You have already used this URL. Publishing to it again will update your previous publish."
} else if (this.myClaimExists() && !this.state.prefillDone) {
return (
<Notice>
{__("You already have a claim with this name.")}{" "}
<Link
label={__("Use data from my existing claim")}
onClick={() => this.handlePrefillClicked()}
/>
</Notice>
);
} else if (this.state.topClaimValue) {
if (this.state.topClaimValue === 1) {
} else if (this.claim()) {
if (this.topClaimValue() === 1) {
return (
<span>
{__(
@ -439,7 +428,7 @@ class PublishPage extends React.PureComponent {
<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.topClaimValue(),
this.state.name
)}
</span>
@ -709,77 +698,11 @@ class PublishPage extends React.PureComponent {
</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">
{this.props.fetchingChannels
? <BusyMessage message="Fetching identities" />
: <FormRow
type="select"
tabIndex="1"
onChange={event => {
this.handleChannelChange(event);
}}
value={this.state.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>}
</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>
<ChannelSection
{...this.props}
handleChannelChange={this.handleChannelChange.bind(this)}
channel={this.state.channel}
/>
<section className="card">
<div className="card__title-primary">
@ -795,7 +718,9 @@ class PublishPage extends React.PureComponent {
</div>
<div className="card__content">
<FormRow
prefix="lbry://"
prefix={`lbry://${this.state.channel === "anonymous"
? ""
: `${this.state.channel}/`}`}
type="text"
ref="name"
placeholder="myname"
@ -907,4 +832,177 @@ class PublishPage extends React.PureComponent {
}
}
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);
}).bind(this);
const failure = (err => {
this.setState({
creatingChannel: false,
});
this.refs.newChannelName.showError(
__("Unable to create channel due to an internal error.")
);
}).bind(this);
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 = [];
if (channels.length > 0) {
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" />
);
}
} else if (fetchingChannels) {
channelContent.push(
<BusyMessage message="Loading 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 PublishPage;

View file

@ -15,7 +15,13 @@ reducers[types.RESOLVE_URI_COMPLETED] = function(state, action) {
byUri[uri] = claim.claim_id;
} else if (claim === undefined && certificate !== undefined) {
byId[certificate.claim_id] = certificate;
byUri[uri] = certificate.claim_id;
// Don't point URI at the channel certificate unless it actually is
// a channel URI. This is brittle.
if (!uri.split(certificate.name)[1].match(/\//)) {
byUri[uri] = certificate.claim_id;
} else {
byUri[uri] = null;
}
} else {
byUri[uri] = null;
}
@ -108,6 +114,34 @@ 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,
});
};
reducers[types.PUBLISH_COMPLETED] = function(state, action) {
const { claim } = action.data;
const byId = Object.assign({}, state.byId);
const myClaims = new Set(state.myClaims);
byId[claim.claim_id] = claim;
myClaims.add(claim.claim_id);
return Object.assign({}, state, {
byId,
myClaims,
});
};
export default function reducer(state = defaultState, action) {
const handler = reducers[action.type];
if (handler) return handler(state, action);

View file

@ -12,8 +12,9 @@ reducers[types.FILE_LIST_STARTED] = function(state, action) {
reducers[types.FILE_LIST_COMPLETED] = function(state, action) {
const { fileInfos } = action.data;
const newByOutpoint = Object.assign({}, state.byOutpoint);
const pendingByOutpoint = Object.assign({}, state.pendingByOutpoint);
fileInfos.forEach(fileInfo => {
const { outpoint } = fileInfo;
@ -23,6 +24,7 @@ reducers[types.FILE_LIST_COMPLETED] = function(state, action) {
return Object.assign({}, state, {
isFileListPending: false,
byOutpoint: newByOutpoint,
pendingByOutpoint,
});
};
@ -136,6 +138,61 @@ reducers[types.LOADING_VIDEO_FAILED] = function(state, action) {
});
};
reducers[types.PUBLISH_STARTED] = function(state, action) {
const { pendingPublish } = action.data;
const pendingByOutpoint = Object.assign({}, state.pendingByOutpoint);
pendingByOutpoint[pendingPublish.outpoint] = pendingPublish;
return Object.assign({}, state, {
pendingByOutpoint,
});
};
reducers[types.PUBLISH_COMPLETED] = function(state, action) {
const { pendingPublish } = action.data;
const pendingByOutpoint = Object.assign({}, state.pendingByOutpoint);
delete pendingByOutpoint[pendingPublish.outpoint];
return Object.assign({}, state, {
pendingByOutpoint,
});
};
reducers[types.PUBLISH_FAILED] = function(state, action) {
const { pendingPublish } = action.data;
const pendingByOutpoint = Object.assign({}, state.pendingByOutpoint);
delete pendingByOutpoint[pendingPublish.outpoint];
return Object.assign({}, state, {
pendingByOutpoint,
});
};
// reducers[types.PUBLISH_COMPLETED] = function(state, action) {
// const { claim } = action.data;
// const uri = lbryuri.build({
// txid: claim.txId
// })
// const newPendingPublish = {
// name,
// channel_name,
// claim_id: "pending_claim_" + uri,
// txid: "pending_" + uri,
// nout: 0,
// outpoint: "pending_" + uri + ":0",
// time: Date.now(),
// };
// const fileInfos = Object.assign({}, state.fileInfos)
// fileInfos[newPendingPublish.outpoint] = newPendingPublish
// return Object.assign({}, state, {
// fileInfos,
// })
// }
export default function reducer(state = defaultState, action) {
const handler = reducers[action.type];
if (handler) return handler(state, action);

View file

@ -69,6 +69,11 @@ export const makeSelectLoadingForUri = () => {
return createSelector(selectLoadingForUri, loading => !!loading);
};
export const selectFileInfosPendingPublish = createSelector(
_selectState,
state => Object.values(state.pendingByOutpoint || {})
);
export const selectFileInfosDownloaded = createSelector(
selectFileInfosByOutpoint,
selectMyClaimsOutpoints,
@ -87,24 +92,17 @@ export const selectFileInfosDownloaded = createSelector(
}
);
export const selectFileInfosPendingPublish = createSelector(
_selectState,
state => {
return lbry.getPendingPublishes();
}
);
export const selectFileInfosPublished = createSelector(
selectFileInfosByOutpoint,
selectFileInfosPendingPublish,
selectMyClaimsOutpoints,
(byOutpoint, pendingFileInfos, outpoints) => {
selectFileInfosPendingPublish,
(byOutpoint, outpoints, pendingPublish) => {
const fileInfos = [];
outpoints.forEach(outpoint => {
const fileInfo = byOutpoint[outpoint];
if (fileInfo) fileInfos.push(fileInfo);
});
return [...fileInfos, ...pendingFileInfos];
return fileInfos;
}
);
@ -133,7 +131,6 @@ export const selectFileInfosByUri = createSelector(
if (fileInfo) fileInfos[uri] = fileInfo;
}
});
return fileInfos;
}
);

View file

@ -92,12 +92,16 @@ const saveClaimsFilter = createFilter("claims", [
"myClaims",
"myChannelClaims",
]);
const saveFileInfosFilter = createFilter("fileInfo", [
"fileInfos",
"pendingByOutpoint",
]);
const persistOptions = {
whitelist: ["claims"],
whitelist: ["claims", "fileInfo"],
// Order is important. Needs to be compressed last or other transforms can't
// read the data
transforms: [saveClaimsFilter, compressor],
transforms: [saveClaimsFilter, saveFileInfosFilter, compressor],
debounce: 1000,
storage: localForage,
};

File diff suppressed because it is too large Load diff