From 11a954a442e619cf90ffefbb4431987594930d93 Mon Sep 17 00:00:00 2001 From: bill bittner Date: Thu, 11 Jan 2018 12:51:38 -0800 Subject: [PATCH] got publish submission working --- public/assets/js/publishFileFunctions.js | 96 ---------------- public/assets/js/validationFunctions.js | 96 ---------------- react/actions/index.js | 14 ++- react/components/Preview.jsx | 1 - react/containers/PublishForm.jsx | 123 +++++++++++++++++++-- react/containers/PublishThumbnailInput.jsx | 2 +- react/reducers/index.js | 8 +- views/partials/navBar.handlebars | 6 +- 8 files changed, 137 insertions(+), 209 deletions(-) diff --git a/public/assets/js/publishFileFunctions.js b/public/assets/js/publishFileFunctions.js index 4ce63d0c..425d3094 100644 --- a/public/assets/js/publishFileFunctions.js +++ b/public/assets/js/publishFileFunctions.js @@ -9,103 +9,7 @@ const publishFileFunctions = { } 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 - 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 (){ console.log('starting upload'); // hide the publish tool diff --git a/public/assets/js/validationFunctions.js b/public/assets/js/validationFunctions.js index d9a9c9d0..97f46862 100644 --- a/public/assets/js/validationFunctions.js +++ b/public/assets/js/validationFunctions.js @@ -1,53 +1,5 @@ // validation function which checks the proposed file's type, size, and name 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) { name = name.substring(name.indexOf('@') + 1); // ensure a name was entered @@ -117,54 +69,6 @@ const validationFunctions = { name = `@${name}`; 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 validateNewChannelSubmission: function (userName, password) { const channelName = `@${userName}`; diff --git a/react/actions/index.js b/react/actions/index.js index 4bed823d..45926bd1 100644 --- a/react/actions/index.js +++ b/react/actions/index.js @@ -5,6 +5,7 @@ export const METADATA_UPDATE = 'METADATA_UPDATE'; export const CLAIM_UPDATE = 'CLAIM_UPDATE'; export const CHANNEL_UPDATE = 'CHANNEL_UPDATE'; export const SET_PUBLISH_IN_CHANNEL = 'SET_PUBLISH_IN_CHANNEL'; +export const PUBLISH_STATUS_UPDATE = 'PUBLISH_STATUS_UPDATE'; // export action creators export function selectFile (file) { @@ -44,9 +45,16 @@ export function updateLoggedInChannel (name, shortId, longId) { }; }; -export function setPublishInChannel (value) { +export function setPublishInChannel (channel) { return { type: SET_PUBLISH_IN_CHANNEL, - value, + channel, }; -} +}; + +export function updatePublishStatus (status) { + return { + type: PUBLISH_STATUS_UPDATE, + status, + }; +}; diff --git a/react/components/Preview.jsx b/react/components/Preview.jsx index e6a8248f..23bd19e2 100644 --- a/react/components/Preview.jsx +++ b/react/components/Preview.jsx @@ -18,7 +18,6 @@ class Preview extends React.Component { this.previewFile(newProps.file); } previewFile (file) { - console.log('previewFile', file) const that = this; if (file.type !== 'video/mp4') { const previewReader = new FileReader(); diff --git a/react/containers/PublishForm.jsx b/react/containers/PublishForm.jsx index 8c211313..81b9451c 100644 --- a/react/containers/PublishForm.jsx +++ b/react/containers/PublishForm.jsx @@ -6,18 +6,19 @@ import PublishUrlInput from './PublishUrlInput.jsx'; import PublishThumbnailInput from './PublishThumbnailInput.jsx'; import PublishMetadataInputs from './PublishMetadataInputs.jsx'; import AnonymousOrChannelSelect from './AnonymousOrChannelSelect.jsx'; - -import { selectFile, clearFile, updateLoggedInChannel } from '../actions/index'; import { connect } from 'react-redux'; import { getCookie } from '../utils/cookies.js'; +import { selectFile, clearFile, updateLoggedInChannel, updatePublishStatus } from '../actions'; class PublishForm extends React.Component { constructor (props) { super(props); // set defaults this.state = { - error: null, + publishRequestError: null, }; + this.validatePublishRequest = this.validatePublishRequest.bind(this); + this.makePublishRequest = this.makePublishRequest.bind(this); this.publish = this.publish.bind(this); } componentWillMount () { @@ -28,8 +29,102 @@ class PublishForm extends React.Component { console.log(`channel cookies found: ${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 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 () { return ( @@ -61,14 +156,18 @@ class PublishForm extends React.Component { - { (this.props.fileType === 'video/mp4') && } + { (this.props.file.type === 'video/mp4') && ( +
+ +
+ )}
- +
@@ -89,8 +188,15 @@ class PublishForm extends React.Component { const mapStateToProps = state => { return { - fileType: state.file.type, - claim : state.claim, + file : state.file, + 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) => { dispatch(updateLoggedInChannel(name, shortId, longId)); }, + onPublishStatusChange: (status) => { + dispatch(updatePublishStatus(status)); + }, }; }; diff --git a/react/containers/PublishThumbnailInput.jsx b/react/containers/PublishThumbnailInput.jsx index 410385ff..ee29a8db 100644 --- a/react/containers/PublishThumbnailInput.jsx +++ b/react/containers/PublishThumbnailInput.jsx @@ -56,7 +56,7 @@ class ThumbnailInput extends React.Component { } render () { return ( -
+
diff --git a/react/reducers/index.js b/react/reducers/index.js index 5a5ecf01..a13bb633 100644 --- a/react/reducers/index.js +++ b/react/reducers/index.js @@ -1,5 +1,5 @@ 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, } from '../actions'; @@ -55,8 +55,12 @@ export default function (state = initialState, action) { }); case SET_PUBLISH_IN_CHANNEL: return Object.assign({}, state, { - publishInChannel: action.value, + publishInChannel: action.channel, }); + case PUBLISH_STATUS_UPDATE: + return Object.assign({}, state, { + publishStatus: action.status, + }) default: return state; } diff --git a/views/partials/navBar.handlebars b/views/partials/navBar.handlebars index 1693cf23..e4d2dad6 100644 --- a/views/partials/navBar.handlebars +++ b/views/partials/navBar.handlebars @@ -24,8 +24,8 @@ Open-source, decentralized image and video sharing.