got publish submission working

This commit is contained in:
bill bittner 2018-01-11 12:51:38 -08:00
parent 70a65a243a
commit 11a954a442
8 changed files with 137 additions and 209 deletions

View file

@ -9,103 +9,7 @@ const publishFileFunctions = {
} }
return null; return null;
}, },
createMetadata: function() {
const nameInput = document.getElementById('claim-name-input');
const titleInput = document.getElementById('publish-title');
const descriptionInput = document.getElementById('publish-description');
const licenseInput = document.getElementById('publish-license');
const nsfwInput = document.getElementById('publish-nsfw');
const thumbnailInput = document.getElementById('claim-thumbnail-input');
const channelName = this.returnNullOrChannel();
let metadata = {
name: nameInput.value.trim(),
title: titleInput.value.trim(),
description: descriptionInput.value.trim(),
license: licenseInput.value.trim(),
nsfw: nsfwInput.checked,
type: stagedFiles[0].type,
thumbnail: thumbnailInput.value.trim(),
};
if (channelName) {
metadata['channelName'] = channelName;
}
return metadata;
},
appendDataToFormData: function (file, metadata) {
var fd = new FormData();
fd.append('file', file)
for (var key in metadata) {
if (metadata.hasOwnProperty(key)) {
console.log(key, metadata[key]);
fd.append(key, metadata[key]);
}
}
return fd;
},
publishFile: function (file, metadata) {
var uri = "/api/claim-publish";
var xhr = new XMLHttpRequest();
var fd = this.appendDataToFormData(file, metadata);
var that = this;
xhr.upload.addEventListener("loadstart", function(e) {
that.showUploadStartedMessage();
})
xhr.upload.addEventListener("progress", function(e) {
if (e.lengthComputable) {
var percentage = Math.round((e.loaded * 100) / e.total);
console.log('progress:', percentage);
that.showUploadProgressMessage(percentage);
}
}, false);
xhr.upload.addEventListener("load", function(e){
console.log('loaded 100%');
that.showFilePublishUpdate("Your file has been loaded, and is now being published to the blockchain. Sit tight...")
}, false);
xhr.open("POST", uri, true);
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
console.log('publish response:', xhr.response)
if (xhr.status == 200) {
console.log('publish complete!');
that.showFilePublishComplete(JSON.parse(xhr.response).message);
} else if (xhr.status == 502){
that.showFilePublishFailure('Spee.ch was not able to get a response from the LBRY network.');
} else {
that.showFilePublishFailure(JSON.parse(xhr.response).message);
}
}
};
// Initiate a multipart/form-data upload
xhr.send(fd);
},
// Validate the publish submission and then trigger upload // Validate the publish submission and then trigger upload
publishStagedFile: function (event) {
event.preventDefault(); // prevent default so this script can handle submission
const metadata = this.createMetadata();
const that = this;
const fileSelectionInputError = document.getElementById('input-error-file-selection');
const claimNameError = document.getElementById('input-error-claim-name');
const channelSelectError = document.getElementById('input-error-channel-select');
const publishSubmitError = document.getElementById('input-error-publish-submit');
// validate, submit, and handle response
validationFunctions.validateFilePublishSubmission(stagedFiles, metadata)
.then( function() {
that.publishFile(stagedFiles[0], metadata);
})
.catch(error => {
if (error.name === 'FileError') {
validationFunctions.showError(fileSelectionInputError, error.message);
} else if (error.name === 'NameError') {
validationFunctions.showError(claimNameError, error.message);
} else if (error.name === 'ChannelNameError'){
console.log(error);
validationFunctions.showError(channelSelectError, error.message);
} else {
validationFunctions.showError(publishSubmitError, error.message);
}
return;
})
},
showUploadStartedMessage: function (){ showUploadStartedMessage: function (){
console.log('starting upload'); console.log('starting upload');
// hide the publish tool // hide the publish tool

View file

@ -1,53 +1,5 @@
// validation function which checks the proposed file's type, size, and name // validation function which checks the proposed file's type, size, and name
const validationFunctions = { const validationFunctions = {
validateFile: function (file) {
if (!file) {
console.log('no file found');
throw new Error('no file provided');
}
if (/'/.test(file.name)) {
console.log('file name had apostrophe in it');
throw new Error('apostrophes are not allowed in the file name');
}
// validate size and type
switch (file.type) {
case 'image/jpeg':
case 'image/jpg':
case 'image/png':
if (file.size > 10000000) {
console.log('file was too big');
throw new Error('Sorry, images are limited to 10 megabytes.');
}
break;
case 'image/gif':
if (file.size > 50000000) {
console.log('file was too big');
throw new Error('Sorry, .gifs are limited to 50 megabytes.');
}
break;
case 'video/mp4':
if (file.size > 50000000) {
console.log('file was too big');
throw new Error('Sorry, videos are limited to 50 megabytes.');
}
break;
default:
console.log('file type is not supported');
throw new Error(file.type + ' is not a supported file type. Only, .jpeg, .png, .gif, and .mp4 files are currently supported.')
}
},
// validation function that checks to make sure the claim name is valid
validateClaimName: function (name) {
// ensure a name was entered
if (name.length < 1) {
throw new NameError("You must enter a name for your url");
}
// validate the characters in the 'name' field
const invalidCharacters = /[^A-Za-z0-9,-]/g.exec(name);
if (invalidCharacters) {
throw new NameError('"' + invalidCharacters + '" characters are not allowed');
}
},
validateChannelName: function (name) { validateChannelName: function (name) {
name = name.substring(name.indexOf('@') + 1); name = name.substring(name.indexOf('@') + 1);
// ensure a name was entered // ensure a name was entered
@ -117,54 +69,6 @@ const validationFunctions = {
name = `@${name}`; name = `@${name}`;
this.checkAvailability(name, successDisplayElement, errorDisplayElement, this.validateChannelName, 'Sorry, that name is already taken', '/api/channel-is-available/'); this.checkAvailability(name, successDisplayElement, errorDisplayElement, this.validateChannelName, 'Sorry, that name is already taken', '/api/channel-is-available/');
}, },
// validation function which checks all aspects of the publish submission
validateFilePublishSubmission: function (stagedFiles, metadata) {
const channelName = metadata.channelName;
const claimName = metadata.name;
var that = this;
return new Promise(function (resolve, reject) {
// 1. make sure 1 file was staged
if (!stagedFiles) {
reject(new FileError("Please select a file"));
return;
} else if (stagedFiles.length > 1) {
reject(new FileError("Only one file is allowed at a time"));
return;
}
// 2. validate the file's name, type, and size
try {
that.validateFile(stagedFiles[0]);
} catch (error) {
reject(error);
return;
}
// 3. validate that a channel was chosen
if (channelName === 'new' || channelName === 'login') {
reject(new ChannelNameError("Please log in to a channel"));
return;
}
;
// 4. validate the claim name
try {
that.validateClaimName(claimName);
} catch (error) {
reject(error);
return;
}
// if all validation passes, check availability of the name (note: do we need to re-validate channel name vs. credentials as well?)
return that.isNameAvailable(claimName, '/api/claim-is-available/')
.then(result => {
if (result) {
resolve();
} else {
reject(new NameError('Sorry, that ending is already taken'));
}
})
.catch(error => {
reject(error);
});
});
},
// validation function which checks all aspects of a new channel submission // validation function which checks all aspects of a new channel submission
validateNewChannelSubmission: function (userName, password) { validateNewChannelSubmission: function (userName, password) {
const channelName = `@${userName}`; const channelName = `@${userName}`;

View file

@ -5,6 +5,7 @@ export const METADATA_UPDATE = 'METADATA_UPDATE';
export const CLAIM_UPDATE = 'CLAIM_UPDATE'; export const CLAIM_UPDATE = 'CLAIM_UPDATE';
export const CHANNEL_UPDATE = 'CHANNEL_UPDATE'; export const CHANNEL_UPDATE = 'CHANNEL_UPDATE';
export const SET_PUBLISH_IN_CHANNEL = 'SET_PUBLISH_IN_CHANNEL'; export const SET_PUBLISH_IN_CHANNEL = 'SET_PUBLISH_IN_CHANNEL';
export const PUBLISH_STATUS_UPDATE = 'PUBLISH_STATUS_UPDATE';
// export action creators // export action creators
export function selectFile (file) { export function selectFile (file) {
@ -44,9 +45,16 @@ export function updateLoggedInChannel (name, shortId, longId) {
}; };
}; };
export function setPublishInChannel (value) { export function setPublishInChannel (channel) {
return { return {
type: SET_PUBLISH_IN_CHANNEL, type: SET_PUBLISH_IN_CHANNEL,
value, channel,
}; };
} };
export function updatePublishStatus (status) {
return {
type: PUBLISH_STATUS_UPDATE,
status,
};
};

View file

@ -18,7 +18,6 @@ class Preview extends React.Component {
this.previewFile(newProps.file); this.previewFile(newProps.file);
} }
previewFile (file) { previewFile (file) {
console.log('previewFile', file)
const that = this; const that = this;
if (file.type !== 'video/mp4') { if (file.type !== 'video/mp4') {
const previewReader = new FileReader(); const previewReader = new FileReader();

View file

@ -6,18 +6,19 @@ import PublishUrlInput from './PublishUrlInput.jsx';
import PublishThumbnailInput from './PublishThumbnailInput.jsx'; import PublishThumbnailInput from './PublishThumbnailInput.jsx';
import PublishMetadataInputs from './PublishMetadataInputs.jsx'; import PublishMetadataInputs from './PublishMetadataInputs.jsx';
import AnonymousOrChannelSelect from './AnonymousOrChannelSelect.jsx'; import AnonymousOrChannelSelect from './AnonymousOrChannelSelect.jsx';
import { selectFile, clearFile, updateLoggedInChannel } from '../actions/index';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { getCookie } from '../utils/cookies.js'; import { getCookie } from '../utils/cookies.js';
import { selectFile, clearFile, updateLoggedInChannel, updatePublishStatus } from '../actions';
class PublishForm extends React.Component { class PublishForm extends React.Component {
constructor (props) { constructor (props) {
super(props); super(props);
// set defaults // set defaults
this.state = { this.state = {
error: null, publishRequestError: null,
}; };
this.validatePublishRequest = this.validatePublishRequest.bind(this);
this.makePublishRequest = this.makePublishRequest.bind(this);
this.publish = this.publish.bind(this); this.publish = this.publish.bind(this);
} }
componentWillMount () { componentWillMount () {
@ -28,8 +29,102 @@ class PublishForm extends React.Component {
console.log(`channel cookies found: ${loggedInChannelName} ${loggedInChannelShortId} ${loggedInChannelLongId}`); console.log(`channel cookies found: ${loggedInChannelName} ${loggedInChannelShortId} ${loggedInChannelLongId}`);
this.props.onChannelLogin(loggedInChannelName, loggedInChannelShortId, loggedInChannelLongId); this.props.onChannelLogin(loggedInChannelName, loggedInChannelShortId, loggedInChannelLongId);
} }
validatePublishRequest () {
// make sure all required data is provided
return new Promise((resolve, reject) => {
// is there a file?
if (!this.props.file) {
return reject(new Error('Please choose a file'));
}
// is there a claim chosen?
if (!this.props.claim) {
return reject(new Error('Please enter a claim name'));
}
// if publishInChannel is true, is a channel logged in (or selected)
if (this.props.publishInChannel && !this.props.loggedInChannel.name) {
return reject(new Error('Select Anonymous or log in to a channel'));
}
// tbd: is the claim available?
resolve();
});
}
makePublishRequest (file, metadata) {
const uri = '/api/claim-publish';
const xhr = new XMLHttpRequest();
const fd = this.appendDataToFormData(file, metadata);
const that = this;
xhr.upload.addEventListener('loadstart', function () {
that.props.onPublishStatusChange('upload started');
});
xhr.upload.addEventListener('progress', function (e) {
if (e.lengthComputable) {
const percentage = Math.round((e.loaded * 100) / e.total);
that.props.onPublishStatusChange(`upload progress: ${percentage}%`);
console.log('progress:', percentage);
}
}, false);
xhr.upload.addEventListener('load', function () {
console.log('loaded 100%');
that.props.onPublishStatusChange(`Upload complete. Your file is now being published on the blockchain...`);
}, false);
xhr.open('POST', uri, true);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
console.log('publish response:', xhr.response);
if (xhr.status === 200) {
console.log('publish complete!');
that.props.onPublishStatusChange(JSON.parse(xhr.response).message);
} else if (xhr.status === 502) {
that.props.onPublishStatusChange('Spee.ch was not able to get a response from the LBRY network.');
} else {
that.props.onPublishStatusChange(JSON.parse(xhr.response).message);
}
}
};
// Initiate a multipart/form-data upload
xhr.send(fd);
}
createMetadata () {
let metadata = {
name : this.props.claim,
title : this.props.title,
description: this.props.description,
license : this.props.license,
nsfw : this.props.nsfw,
type : this.props.file.type,
thumbnail : this.props.thumbnail,
};
if (this.props.publishInChannel) {
metadata['channelName'] = this.props.loggedInChannel.name;
}
return metadata;
}
appendDataToFormData (file, metadata) {
var fd = new FormData();
fd.append('file', file);
for (var key in metadata) {
if (metadata.hasOwnProperty(key)) {
console.log(key, metadata[key]);
fd.append(key, metadata[key]);
}
}
return fd;
}
publish () { publish () {
// publish the asset // publish the asset
const that = this;
this.validatePublishRequest()
.then(() => {
const metadata = that.createMetadata();
// publish the claim
return that.makePublishRequest(this.props.file, metadata);
})
.then(() => {
that.props.onPublishStatusChange('publish request made');
})
.catch((error) => {
that.setState({publishRequestError: error.message});
});
} }
render () { render () {
return ( return (
@ -61,14 +156,18 @@ class PublishForm extends React.Component {
<ChannelSelector /> <ChannelSelector />
</div> </div>
{ (this.props.fileType === 'video/mp4') && <PublishThumbnailInput /> } { (this.props.file.type === 'video/mp4') && (
<div className="row row--padded row--wide row--no-top">
<PublishThumbnailInput />
</div>
)}
<div className="row row--padded row--no-top row--no-bottom row--wide"> <div className="row row--padded row--no-top row--no-bottom row--wide">
<PublishMetadataInputs /> <PublishMetadataInputs />
</div> </div>
<div className="row row--padded row--wide"> <div className="row row--padded row--wide">
<div className="input-error" id="input-error-publish-submit" hidden="true">{this.state.error}</div> <div className="input-error" id="input-error-publish-submit" hidden="true">{this.state.publishRequestError}</div>
<button id="publish-submit" className="button--primary button--large" onClick={this.publish}>Publish</button> <button id="publish-submit" className="button--primary button--large" onClick={this.publish}>Publish</button>
</div> </div>
@ -89,8 +188,15 @@ class PublishForm extends React.Component {
const mapStateToProps = state => { const mapStateToProps = state => {
return { return {
fileType: state.file.type, file : state.file,
claim : state.claim, claim : state.claim,
title : state.metadata.title,
thumbnail : state.metadata.thumbnail,
description : state.metadata.description,
license : state.metadata.license,
nsfw : state.metadata.nsfw,
loggedInChannel : state.loggedInChannel,
publishInChannel: state.publishInChannel,
}; };
}; };
@ -105,6 +211,9 @@ const mapDispatchToProps = dispatch => {
onChannelLogin: (name, shortId, longId) => { onChannelLogin: (name, shortId, longId) => {
dispatch(updateLoggedInChannel(name, shortId, longId)); dispatch(updateLoggedInChannel(name, shortId, longId));
}, },
onPublishStatusChange: (status) => {
dispatch(updatePublishStatus(status));
},
}; };
}; };

View file

@ -56,7 +56,7 @@ class ThumbnailInput extends React.Component {
} }
render () { render () {
return ( return (
<div className="row row--padded row--wide row--no-top" id="publish-thumbnail"> <div>
<div className="column column--3 column--sml-10"> <div className="column column--3 column--sml-10">
<label className="label">Thumbnail:</label> <label className="label">Thumbnail:</label>
</div><div className="column column--6 column--sml-10"> </div><div className="column column--6 column--sml-10">

View file

@ -1,5 +1,5 @@
import { import {
CHANNEL_UPDATE, CLAIM_UPDATE, FILE_CLEAR, FILE_SELECTED, METADATA_UPDATE, CHANNEL_UPDATE, CLAIM_UPDATE, FILE_CLEAR, FILE_SELECTED, METADATA_UPDATE, PUBLISH_STATUS_UPDATE,
SET_PUBLISH_IN_CHANNEL, SET_PUBLISH_IN_CHANNEL,
} from '../actions'; } from '../actions';
@ -55,8 +55,12 @@ export default function (state = initialState, action) {
}); });
case SET_PUBLISH_IN_CHANNEL: case SET_PUBLISH_IN_CHANNEL:
return Object.assign({}, state, { return Object.assign({}, state, {
publishInChannel: action.value, publishInChannel: action.channel,
}); });
case PUBLISH_STATUS_UPDATE:
return Object.assign({}, state, {
publishStatus: action.status,
})
default: default:
return state; return state;
} }

View file

@ -24,8 +24,8 @@
<span class="nav-bar-tagline">Open-source, decentralized image and video sharing.</span> <span class="nav-bar-tagline">Open-source, decentralized image and video sharing.</span>
</div> </div>
<div class="nav-bar--right"> <div class="nav-bar--right">
<a class="nav-bar-link link--nav" href="/">Upload</a> <a class="nav-bar-link link--nav" href="/">Publish</a>
<a class="nav-bar-link link--nav" href="/popular">Popular</a> <!--<a class="nav-bar-link link--nav" href="/popular">Popular</a>-->
<a class="nav-bar-link link--nav" href="/about">About</a> <a class="nav-bar-link link--nav" href="/about">About</a>
<select type="text" id="nav-bar-channel-select" class="select select--arrow link--nav" onchange="toggleNavBarSelection(event.target.selectedOptions[0].value)" {{#unless user}}style="display:none"{{/unless}}> <select type="text" id="nav-bar-channel-select" class="select select--arrow link--nav" onchange="toggleNavBarSelection(event.target.selectedOptions[0].value)" {{#unless user}}style="display:none"{{/unless}}>
<option id="nav-bar-channel-select-channel-option">@{{user.userName}}</option> <option id="nav-bar-channel-select-channel-option">@{{user.userName}}</option>