Merge pull request #371 from lbryio/282-thumbnail-selector
282 thumbnail selector
This commit is contained in:
commit
be420b362a
51 changed files with 679 additions and 561 deletions
|
@ -28,6 +28,10 @@ module.exports = {
|
||||||
host : 'https://spee.ch',
|
host : 'https://spee.ch',
|
||||||
description: 'Open-source, decentralized image and video sharing.'
|
description: 'Open-source, decentralized image and video sharing.'
|
||||||
},
|
},
|
||||||
|
publish: {
|
||||||
|
thumbnailChannel : '@channelName', // create a channel to use for thumbnail images
|
||||||
|
thumbnailChannelId: 'xyz123...', // the channel_id (claim id) for the channel above
|
||||||
|
}
|
||||||
claim: {
|
claim: {
|
||||||
defaultTitle : 'Spee.ch',
|
defaultTitle : 'Spee.ch',
|
||||||
defaultThumbnail : 'https://spee.ch/assets/img/video_thumb_default.png',
|
defaultThumbnail : 'https://spee.ch/assets/img/video_thumb_default.png',
|
||||||
|
|
|
@ -39,7 +39,6 @@ module.exports = (req, res) => {
|
||||||
.run(saga)
|
.run(saga)
|
||||||
.done
|
.done
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log('preload sagas are done');
|
|
||||||
// render component to a string
|
// render component to a string
|
||||||
const html = renderToString(
|
const html = renderToString(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
|
@ -56,10 +55,7 @@ module.exports = (req, res) => {
|
||||||
|
|
||||||
// check for a redirect
|
// check for a redirect
|
||||||
if (context.url) {
|
if (context.url) {
|
||||||
console.log('REDIRECTING:', context.url);
|
|
||||||
return res.redirect(301, context.url);
|
return res.redirect(301, context.url);
|
||||||
} else {
|
|
||||||
console.log(`we're good, send the response`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the initial state from our Redux store
|
// get the initial state from our Redux store
|
||||||
|
|
|
@ -71,7 +71,6 @@ module.exports = {
|
||||||
},
|
},
|
||||||
resolveUri (uri) {
|
resolveUri (uri) {
|
||||||
logger.debug(`lbryApi >> Resolving URI for "${uri}"`);
|
logger.debug(`lbryApi >> Resolving URI for "${uri}"`);
|
||||||
// console.log('resolving uri', uri);
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
axios
|
axios
|
||||||
.post(lbryApiUri, {
|
.post(lbryApiUri, {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
const logger = require('winston');
|
const logger = require('winston');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const { site, wallet } = require('../config/speechConfig.js');
|
const { site, wallet, publish } = require('../config/speechConfig.js');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
parsePublishApiRequestBody ({name, nsfw, license, title, description, thumbnail}) {
|
parsePublishApiRequestBody ({name, nsfw, license, title, description, thumbnail}) {
|
||||||
|
@ -28,8 +28,7 @@ module.exports = {
|
||||||
thumbnail,
|
thumbnail,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
parsePublishApiRequestFiles ({file}) {
|
parsePublishApiRequestFiles ({file, thumbnail}) {
|
||||||
logger.debug('file', file);
|
|
||||||
// make sure a file was provided
|
// make sure a file was provided
|
||||||
if (!file) {
|
if (!file) {
|
||||||
throw new Error('no file with key of [file] found in request');
|
throw new Error('no file with key of [file] found in request');
|
||||||
|
@ -45,16 +44,18 @@ module.exports = {
|
||||||
}
|
}
|
||||||
// validate the file name
|
// validate the file name
|
||||||
if (/'/.test(file.name)) {
|
if (/'/.test(file.name)) {
|
||||||
logger.debug('publish > file validation > file name had apostrophe in it');
|
|
||||||
throw new Error('apostrophes are not allowed in the file name');
|
throw new Error('apostrophes are not allowed in the file name');
|
||||||
}
|
}
|
||||||
// validate the file
|
// validate the file
|
||||||
module.exports.validateFileTypeAndSize(file);
|
module.exports.validateFileTypeAndSize(file);
|
||||||
// return results
|
// return results
|
||||||
return {
|
return {
|
||||||
fileName: file.name,
|
fileName : file.name,
|
||||||
filePath: file.path,
|
filePath : file.path,
|
||||||
fileType: file.type,
|
fileType : file.type,
|
||||||
|
thumbnailFileName: (thumbnail ? thumbnail.name : null),
|
||||||
|
thumbnailFilePath: (thumbnail ? thumbnail.path : null),
|
||||||
|
thumbnailFileType: (thumbnail ? thumbnail.type : null),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
validateFileTypeAndSize (file) {
|
validateFileTypeAndSize (file) {
|
||||||
|
@ -116,11 +117,34 @@ module.exports = {
|
||||||
claim_address: wallet.lbryClaimAddress,
|
claim_address: wallet.lbryClaimAddress,
|
||||||
};
|
};
|
||||||
// add thumbnail to channel if video
|
// add thumbnail to channel if video
|
||||||
if (thumbnail !== null) {
|
if (thumbnail) {
|
||||||
publishParams['metadata']['thumbnail'] = thumbnail;
|
publishParams['metadata']['thumbnail'] = thumbnail;
|
||||||
}
|
}
|
||||||
return publishParams;
|
return publishParams;
|
||||||
},
|
},
|
||||||
|
createThumbnailPublishParams (thumbnailFilePath, claimName, license, nsfw) {
|
||||||
|
if (!thumbnailFilePath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.debug(`Creating Thumbnail Publish Parameters`);
|
||||||
|
// create the publish params
|
||||||
|
return {
|
||||||
|
name : `${claimName}-thumb`,
|
||||||
|
file_path: thumbnailFilePath,
|
||||||
|
bid : 0.01,
|
||||||
|
metadata : {
|
||||||
|
title : `${claimName} thumbnail`,
|
||||||
|
description: `a thumbnail for ${claimName}`,
|
||||||
|
author : site.title,
|
||||||
|
language : 'en',
|
||||||
|
license,
|
||||||
|
nsfw,
|
||||||
|
},
|
||||||
|
claim_address: wallet.lbryClaimAddress,
|
||||||
|
channel_name : publish.thumbnailChannel,
|
||||||
|
channel_id : publish.thumbnailChannelId,
|
||||||
|
};
|
||||||
|
},
|
||||||
deleteTemporaryFile (filePath) {
|
deleteTemporaryFile (filePath) {
|
||||||
fs.unlink(filePath, err => {
|
fs.unlink(filePath, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|
|
@ -283,11 +283,12 @@ a, a:visited {
|
||||||
|
|
||||||
/* ERROR MESSAGES */
|
/* ERROR MESSAGES */
|
||||||
|
|
||||||
.info-message--success, .info-message--failure {
|
.info-message, .info-message--success, .info-message--failure {
|
||||||
|
|
||||||
font-size: medium;
|
font-size: medium;
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
padding: 0.3em;
|
padding: 0.3em;
|
||||||
|
color: #9b9b9b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-message--success {
|
.info-message--success {
|
||||||
|
@ -542,7 +543,7 @@ table {
|
||||||
|
|
||||||
/* show */
|
/* show */
|
||||||
|
|
||||||
#video {
|
.video {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
width: calc(100% - 12px - 12px - 2px);
|
width: calc(100% - 12px - 12px - 2px);
|
||||||
|
@ -603,33 +604,6 @@ table {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- grid items ---- */
|
.slider {
|
||||||
|
width: 100%
|
||||||
.grid-item {
|
|
||||||
width: calc(33% - 2rem);
|
|
||||||
padding: 0px;
|
|
||||||
margin: 1rem;
|
|
||||||
float: left;
|
|
||||||
border: 0.5px solid white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-item-image {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-item-details {
|
|
||||||
position: absolute;
|
|
||||||
top: 0px;
|
|
||||||
left: 0px;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-item-details-text {
|
|
||||||
font-size: medium;
|
|
||||||
margin: 0px;
|
|
||||||
text-align: center;
|
|
||||||
padding: 1em 0px 1em 0px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -71,3 +71,17 @@ export function toggleMetadataInputs (showMetadataInputs) {
|
||||||
data: showMetadataInputs,
|
data: showMetadataInputs,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function onNewThumbnail (file) {
|
||||||
|
return {
|
||||||
|
type: actions.THUMBNAIL_NEW,
|
||||||
|
data: file,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function startPublish (history) {
|
||||||
|
return {
|
||||||
|
type: actions.PUBLISH_START,
|
||||||
|
data: { history },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ import Request from 'utils/request';
|
||||||
const { site: { host } } = require('../../config/speechConfig.js');
|
const { site: { host } } = require('../../config/speechConfig.js');
|
||||||
|
|
||||||
export function getLongClaimId (name, modifier) {
|
export function getLongClaimId (name, modifier) {
|
||||||
// console.log('getting long claim id for asset:', name, modifier);
|
|
||||||
let body = {};
|
let body = {};
|
||||||
// create request params
|
// create request params
|
||||||
if (modifier) {
|
if (modifier) {
|
||||||
|
@ -26,13 +25,11 @@ export function getLongClaimId (name, modifier) {
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getShortId (name, claimId) {
|
export function getShortId (name, claimId) {
|
||||||
// console.log('getting short id for asset:', name, claimId);
|
|
||||||
const url = `${host}/api/claim/short-id/${claimId}/${name}`;
|
const url = `${host}/api/claim/short-id/${claimId}/${name}`;
|
||||||
return Request(url);
|
return Request(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getClaimData (name, claimId) {
|
export function getClaimData (name, claimId) {
|
||||||
// console.log('getting claim data for asset:', name, claimId);
|
|
||||||
const url = `${host}/api/claim/data/${name}/${claimId}`;
|
const url = `${host}/api/claim/data/${name}/${claimId}`;
|
||||||
return Request(url);
|
return Request(url);
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,14 +2,12 @@ import Request from 'utils/request';
|
||||||
const { site: { host } } = require('../../config/speechConfig.js');
|
const { site: { host } } = require('../../config/speechConfig.js');
|
||||||
|
|
||||||
export function getChannelData (name, id) {
|
export function getChannelData (name, id) {
|
||||||
console.log('getting channel data for channel:', name, id);
|
|
||||||
if (!id) id = 'none';
|
if (!id) id = 'none';
|
||||||
const url = `${host}/api/channel/data/${name}/${id}`;
|
const url = `${host}/api/channel/data/${name}/${id}`;
|
||||||
return Request(url);
|
return Request(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getChannelClaims (name, longId, page) {
|
export function getChannelClaims (name, longId, page) {
|
||||||
console.log('getting channel claims for channel:', name, longId);
|
|
||||||
if (!page) page = 1;
|
if (!page) page = 1;
|
||||||
const url = `${host}/api/channel/claims/${name}/${longId}/${page}`;
|
const url = `${host}/api/channel/claims/${name}/${longId}/${page}`;
|
||||||
return Request(url);
|
return Request(url);
|
||||||
|
|
0
react/api/publishApi.js
Normal file
0
react/api/publishApi.js
Normal file
48
react/channels/publish.js
Normal file
48
react/channels/publish.js
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import {buffers, END, eventChannel} from 'redux-saga';
|
||||||
|
|
||||||
|
export const makePublishRequestChannel = (fd) => {
|
||||||
|
return eventChannel(emitter => {
|
||||||
|
const uri = '/api/claim/publish';
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
// add event listeners
|
||||||
|
const onLoadStart = () => {
|
||||||
|
emitter({loadStart: true});
|
||||||
|
};
|
||||||
|
const onProgress = (event) => {
|
||||||
|
if (event.lengthComputable) {
|
||||||
|
const percentage = Math.round((event.loaded * 100) / event.total);
|
||||||
|
emitter({progress: percentage});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const onLoad = () => {
|
||||||
|
emitter({load: true});
|
||||||
|
};
|
||||||
|
xhr.upload.addEventListener('loadstart', onLoadStart);
|
||||||
|
xhr.upload.addEventListener('progress', onProgress);
|
||||||
|
xhr.upload.addEventListener('load', onLoad);
|
||||||
|
// set state change handler
|
||||||
|
xhr.onreadystatechange = () => {
|
||||||
|
if (xhr.readyState === 4) {
|
||||||
|
const response = JSON.parse(xhr.response);
|
||||||
|
if ((xhr.status === 200) && response.success) {
|
||||||
|
emitter({success: response});
|
||||||
|
emitter(END);
|
||||||
|
} else {
|
||||||
|
emitter({error: new Error(response.message)});
|
||||||
|
emitter(END);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// open and send
|
||||||
|
xhr.open('POST', uri, true);
|
||||||
|
xhr.send(fd);
|
||||||
|
// clean up
|
||||||
|
return () => {
|
||||||
|
xhr.upload.removeEventListener('loadstart', onLoadStart);
|
||||||
|
xhr.upload.removeEventListener('progress', onProgress);
|
||||||
|
xhr.upload.removeEventListener('load', onLoad);
|
||||||
|
xhr.onreadystatechange = null;
|
||||||
|
xhr.abort();
|
||||||
|
};
|
||||||
|
}, buffers.sliding(2));
|
||||||
|
};
|
|
@ -51,7 +51,7 @@ class AssetDisplay extends React.Component {
|
||||||
);
|
);
|
||||||
case 'video/mp4':
|
case 'video/mp4':
|
||||||
return (
|
return (
|
||||||
<video id='video' className='asset' controls poster={thumbnail}>
|
<video className='asset video' controls poster={thumbnail}>
|
||||||
<source
|
<source
|
||||||
src={`/${claimId}/${name}.${fileExt}`}
|
src={`/${claimId}/${name}.${fileExt}`}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -24,7 +24,7 @@ const AssetPreview = ({ claimData: { name, claimId, fileExt, contentType, thumbn
|
||||||
case 'video/mp4':
|
case 'video/mp4':
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
className={'asset-preview'}
|
className={'asset-preview video'}
|
||||||
src={thumbnail || defaultThumbnail}
|
src={thumbnail || defaultThumbnail}
|
||||||
alt={name}
|
alt={name}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -10,34 +10,44 @@ class Preview extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
this.previewFile(this.props.file);
|
this.setPreviewImageSource(this.props.file);
|
||||||
}
|
}
|
||||||
componentWillReceiveProps (newProps) {
|
componentWillReceiveProps (newProps) {
|
||||||
if (newProps.file !== this.props.file) {
|
if (newProps.file !== this.props.file) {
|
||||||
this.previewFile(newProps.file);
|
this.setPreviewImageSource(newProps.file);
|
||||||
}
|
}
|
||||||
if (newProps.thumbnail !== this.props.thumbnail) {
|
if (newProps.thumbnail !== this.props.thumbnail) {
|
||||||
this.setState({imgSource: (newProps.thumbnail || this.state.defaultThumbnail)});
|
if (newProps.thumbnail) {
|
||||||
|
this.setPreviewImageSourceFromFile(newProps.thumbnail);
|
||||||
|
} else {
|
||||||
|
this.setState({imgSource: this.state.defaultThumbnail});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
previewFile (file) {
|
setPreviewImageSourceFromFile (file) {
|
||||||
|
const previewReader = new FileReader();
|
||||||
|
previewReader.readAsDataURL(file);
|
||||||
|
previewReader.onloadend = () => {
|
||||||
|
this.setState({imgSource: previewReader.result});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
setPreviewImageSource (file) {
|
||||||
if (file.type !== 'video/mp4') {
|
if (file.type !== 'video/mp4') {
|
||||||
const previewReader = new FileReader();
|
this.setPreviewImageSourceFromFile(file);
|
||||||
previewReader.readAsDataURL(file);
|
|
||||||
previewReader.onloadend = () => {
|
|
||||||
this.setState({imgSource: previewReader.result});
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
this.setState({imgSource: (this.props.thumbnail || this.state.defaultThumbnail)});
|
if (this.props.thumbnail) {
|
||||||
|
this.setPreviewImageSourceFromFile(this.props.thumbnail);
|
||||||
|
}
|
||||||
|
this.setState({imgSource: this.state.defaultThumbnail});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
id="dropzone-preview"
|
id='dropzone-preview'
|
||||||
src={this.state.imgSource}
|
src={this.state.imgSource}
|
||||||
className={this.props.dimPreview ? 'dim' : ''}
|
className={this.props.dimPreview ? 'dim' : ''}
|
||||||
alt="publish preview"
|
alt='publish preview'
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -46,7 +56,7 @@ class Preview extends React.Component {
|
||||||
Preview.propTypes = {
|
Preview.propTypes = {
|
||||||
dimPreview: PropTypes.bool.isRequired,
|
dimPreview: PropTypes.bool.isRequired,
|
||||||
file : PropTypes.object.isRequired,
|
file : PropTypes.object.isRequired,
|
||||||
thumbnail : PropTypes.string,
|
thumbnail : PropTypes.object,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Preview;
|
export default Preview;
|
||||||
|
|
|
@ -1,52 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import ProgressBar from 'components/ProgressBar';
|
|
||||||
import * as publishStates from 'constants/publish_claim_states';
|
|
||||||
|
|
||||||
function PublishStatus ({ status, message }) {
|
|
||||||
return (
|
|
||||||
<div className='row row--tall flex-container--column flex-container--center-center'>
|
|
||||||
{(status === publishStates.LOAD_START) &&
|
|
||||||
<div className='row align-content-center'>
|
|
||||||
<p>File is loading to server</p>
|
|
||||||
<p className='blue'>{message}</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
{(status === publishStates.LOADING) &&
|
|
||||||
<div>
|
|
||||||
<div className='row align-content-center'>
|
|
||||||
<p>File is loading to server</p>
|
|
||||||
<p className='blue'>{message}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
{(status === publishStates.PUBLISHING) &&
|
|
||||||
<div className='row align-content-center'>
|
|
||||||
<p>Upload complete. Your file is now being published on the blockchain...</p>
|
|
||||||
<ProgressBar size={12} />
|
|
||||||
<p>Curious what magic is happening here? <a className='link--primary' target='blank' href='https://lbry.io/faq/what-is-lbry'>Learn more.</a></p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
{(status === publishStates.SUCCESS) &&
|
|
||||||
<div className='row align-content-center'>
|
|
||||||
<p>Your publish is complete! You are being redirected to it now.</p>
|
|
||||||
<p>If you are not automatically redirected, <a className='link--primary' target='_blank' href={message}>click here.</a></p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
{(status === publishStates.FAILED) &&
|
|
||||||
<div className='row align-content-center'>
|
|
||||||
<p>Something went wrong...</p>
|
|
||||||
<p><strong>{message}</strong></p>
|
|
||||||
<p>For help, post the above error text in the #speech channel on the <a className='link--primary' href='https://discord.gg/YjYbwhS' target='_blank'>lbry discord</a></p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
PublishStatus.propTypes = {
|
|
||||||
status : PropTypes.string.isRequired,
|
|
||||||
message: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PublishStatus;
|
|
|
@ -7,3 +7,5 @@ export const PUBLISH_STATUS_UPDATE = 'PUBLISH_STATUS_UPDATE';
|
||||||
export const ERROR_UPDATE = 'ERROR_UPDATE';
|
export const ERROR_UPDATE = 'ERROR_UPDATE';
|
||||||
export const SELECTED_CHANNEL_UPDATE = 'SELECTED_CHANNEL_UPDATE';
|
export const SELECTED_CHANNEL_UPDATE = 'SELECTED_CHANNEL_UPDATE';
|
||||||
export const TOGGLE_METADATA_INPUTS = 'TOGGLE_METADATA_INPUTS';
|
export const TOGGLE_METADATA_INPUTS = 'TOGGLE_METADATA_INPUTS';
|
||||||
|
export const THUMBNAIL_NEW = 'THUMBNAIL_NEW';
|
||||||
|
export const PUBLISH_START = 'PUBLISH_START';
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { updateLoggedInChannel } from 'actions/channel';
|
import { updateLoggedInChannel } from 'actions/channel';
|
||||||
import View from './view';
|
import View from './view';
|
||||||
import {updateSelectedChannel} from '../../actions/publish';
|
import {updateSelectedChannel} from 'actions/publish';
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => {
|
const mapDispatchToProps = dispatch => {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -54,7 +54,6 @@ class ChannelCreateForm extends React.Component {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
request(`/api/channel/availability/${channelWithAtSymbol}`)
|
request(`/api/channel/availability/${channelWithAtSymbol}`)
|
||||||
.then(isAvailable => {
|
.then(isAvailable => {
|
||||||
console.log('checkIsChannelAvailable result:', isAvailable);
|
|
||||||
if (!isAvailable) {
|
if (!isAvailable) {
|
||||||
return reject(new Error('That channel has already been claimed'));
|
return reject(new Error('That channel has already been claimed'));
|
||||||
}
|
}
|
||||||
|
@ -69,10 +68,8 @@ class ChannelCreateForm extends React.Component {
|
||||||
const password = this.state.password;
|
const password = this.state.password;
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!password || password.length < 1) {
|
if (!password || password.length < 1) {
|
||||||
console.log('password not provided');
|
|
||||||
return reject(new Error('Please provide a password'));
|
return reject(new Error('Please provide a password'));
|
||||||
}
|
}
|
||||||
console.log('password provided');
|
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -88,12 +85,10 @@ class ChannelCreateForm extends React.Component {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
request('/signup', params)
|
request('/signup', params)
|
||||||
.then(result => {
|
.then(result => {
|
||||||
console.log('makePublishChannelRequest result:', result);
|
|
||||||
return resolve(result);
|
return resolve(result);
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.log('create channel request failed:', error);
|
reject(new Error(`Unfortunately, we encountered an error while creating your channel. Please let us know in Discord! ${error.message}`));
|
||||||
reject(new Error('Unfortunately, we encountered an error while creating your channel. Please let us know in Discord!'));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -119,38 +114,41 @@ class ChannelCreateForm extends React.Component {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{ !this.state.status ? (
|
{ !this.state.status ? (
|
||||||
<form id="publish-channel-form">
|
<form id='publish-channel-form'>
|
||||||
<p id="input-error-channel-name" className="info-message-placeholder info-message--failure">{this.state.error}</p>
|
<div className='row row--wide row--short'>
|
||||||
<div className="row row--wide row--short">
|
<div className='column column--3 column--sml-10'>
|
||||||
<div className="column column--3 column--sml-10">
|
<label className='label' htmlFor='new-channel-name'>Name:</label>
|
||||||
<label className="label" htmlFor="new-channel-name">Name:</label>
|
</div><div className='column column--6 column--sml-10'>
|
||||||
</div><div className="column column--6 column--sml-10">
|
<div className='input-text--primary flex-container--row flex-container--left-bottom span--relative'>
|
||||||
<div className="input-text--primary flex-container--row flex-container--left-bottom span--relative">
|
|
||||||
<span>@</span>
|
<span>@</span>
|
||||||
<input type="text" name="channel" id="new-channel-name" className="input-text" placeholder="exampleChannelName" value={this.state.channel} onChange={this.handleChannelInput} />
|
<input type='text' name='channel' id='new-channel-name' className='input-text' placeholder='exampleChannelName' value={this.state.channel} onChange={this.handleChannelInput} />
|
||||||
{ (this.state.channel && !this.state.error) && <span id="input-success-channel-name" className="info-message--success span--absolute">{'\u2713'}</span> }
|
{ (this.state.channel && !this.state.error) && <span id='input-success-channel-name' className='info-message--success span--absolute'>{'\u2713'}</span> }
|
||||||
{ this.state.error && <span id="input-success-channel-name" className="info-message--failure span--absolute">{'\u2716'}</span> }
|
{ this.state.error && <span id='input-success-channel-name' className='info-message--failure span--absolute'>{'\u2716'}</span> }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="row row--wide row--short">
|
<div className='row row--wide row--short'>
|
||||||
<div className="column column--3 column--sml-10">
|
<div className='column column--3 column--sml-10'>
|
||||||
<label className="label" htmlFor="new-channel-password">Password:</label>
|
<label className='label' htmlFor='new-channel-password'>Password:</label>
|
||||||
</div><div className="column column--6 column--sml-10">
|
</div><div className='column column--6 column--sml-10'>
|
||||||
<div className="input-text--primary">
|
<div className='input-text--primary'>
|
||||||
<input type="password" name="password" id="new-channel-password" className="input-text" placeholder="" value={this.state.password} onChange={this.handleInput} />
|
<input type='password' name='password' id='new-channel-password' className='input-text' placeholder='' value={this.state.password} onChange={this.handleInput} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{this.state.error ? (
|
||||||
<div className="row row--wide">
|
<p className='info-message--failure'>{this.state.error}</p>
|
||||||
<button className="button--primary" onClick={this.createChannel}>Create Channel</button>
|
) : (
|
||||||
|
<p className='info-message'>Choose a name and password for your channel</p>
|
||||||
|
)}
|
||||||
|
<div className='row row--wide'>
|
||||||
|
<button className='button--primary' onClick={this.createChannel}>Create Channel</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
<p className="fine-print">{this.state.status}</p>
|
<p className='fine-print'>{this.state.status}</p>
|
||||||
<ProgressBar size={12}/>
|
<ProgressBar size={12} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -26,10 +26,9 @@ class ChannelLoginForm extends React.Component {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
}),
|
}),
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
}
|
};
|
||||||
request('login', params)
|
request('login', params)
|
||||||
.then(({success, channelName, shortChannelId, channelClaimId, message}) => {
|
.then(({success, channelName, shortChannelId, channelClaimId, message}) => {
|
||||||
console.log('loginToChannel success:', success);
|
|
||||||
if (success) {
|
if (success) {
|
||||||
this.props.onChannelLogin(channelName, shortChannelId, channelClaimId);
|
this.props.onChannelLogin(channelName, shortChannelId, channelClaimId);
|
||||||
} else {
|
} else {
|
||||||
|
@ -37,7 +36,6 @@ class ChannelLoginForm extends React.Component {
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.log('login error', error);
|
|
||||||
if (error.message) {
|
if (error.message) {
|
||||||
this.setState({'error': error.message});
|
this.setState({'error': error.message});
|
||||||
} else {
|
} else {
|
||||||
|
@ -47,30 +45,33 @@ class ChannelLoginForm extends React.Component {
|
||||||
}
|
}
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<form id="channel-login-form">
|
<form id='channel-login-form'>
|
||||||
<p id="login-error-display-element" className="info-message-placeholder info-message--failure">{this.state.error}</p>
|
<div className='row row--wide row--short'>
|
||||||
<div className="row row--wide row--short">
|
<div className='column column--3 column--sml-10'>
|
||||||
<div className="column column--3 column--sml-10">
|
<label className='label' htmlFor='channel-login-name-input'>Name:</label>
|
||||||
<label className="label" htmlFor="channel-login-name-input">Name:</label>
|
</div><div className='column column--6 column--sml-10'>
|
||||||
</div><div className="column column--6 column--sml-10">
|
<div className='input-text--primary flex-container--row flex-container--left-bottom'>
|
||||||
<div className="input-text--primary flex-container--row flex-container--left-bottom">
|
<span>@</span>
|
||||||
<span>@</span>
|
<input type='text' id='channel-login-name-input' className='input-text' name='name' placeholder='Your Channel Name' value={this.state.channelName} onChange={this.handleInput} />
|
||||||
<input type="text" id="channel-login-name-input" className="input-text" name="name" placeholder="Your Channel Name" value={this.state.channelName} onChange={this.handleInput}/>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className='row row--wide row--short'>
|
||||||
<div className="row row--wide row--short">
|
<div className='column column--3 column--sml-10'>
|
||||||
<div className="column column--3 column--sml-10">
|
<label className='label' htmlFor='channel-login-password-input' >Password:</label>
|
||||||
<label className="label" htmlFor="channel-login-password-input" >Password:</label>
|
</div><div className='column column--6 column--sml-10'>
|
||||||
</div><div className="column column--6 column--sml-10">
|
<div className='input-text--primary'>
|
||||||
<div className="input-text--primary">
|
<input type='password' id='channel-login-password-input' name='password' className='input-text' placeholder='' value={this.state.channelPassword} onChange={this.handleInput} />
|
||||||
<input type="password" id="channel-login-password-input" name="password" className="input-text" placeholder="" value={this.state.channelPassword} onChange={this.handleInput}/>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{ this.state.error ? (
|
||||||
|
<p className='info-message--failure'>{this.state.error}</p>
|
||||||
<div className="row row--wide">
|
) : (
|
||||||
<button className="button--primary" onClick={this.loginToChannel}>Authenticate</button>
|
<p className='info-message'>Enter the name and password for your channel</p>
|
||||||
|
)}
|
||||||
|
<div className='row row--wide'>
|
||||||
|
<button className='button--primary' onClick={this.loginToChannel}>Authenticate</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|
|
@ -24,28 +24,32 @@ class ChannelSelect extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p id="input-error-channel-select" className="info-message-placeholder info-message--failure">{this.props.channelError}</p>
|
|
||||||
<form>
|
<form>
|
||||||
<div className="column column--3 column--med-10">
|
<div className='column column--3 column--med-10'>
|
||||||
<input type="radio" name="anonymous-or-channel" id="anonymous-radio" className="input-radio" value="anonymous" checked={!this.props.publishInChannel} onChange={this.toggleAnonymousPublish}/>
|
<input type='radio' name='anonymous-or-channel' id='anonymous-radio' className='input-radio' value='anonymous' checked={!this.props.publishInChannel} onChange={this.toggleAnonymousPublish} />
|
||||||
<label className="label label--pointer" htmlFor="anonymous-radio">Anonymous</label>
|
<label className='label label--pointer' htmlFor='anonymous-radio'>Anonymous</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="column column--7 column--med-10">
|
<div className='column column--7 column--med-10'>
|
||||||
<input type="radio" name="anonymous-or-channel" id="channel-radio" className="input-radio" value="in a channel" checked={this.props.publishInChannel} onChange={this.toggleAnonymousPublish}/>
|
<input type='radio' name='anonymous-or-channel' id='channel-radio' className='input-radio' value='in a channel' checked={this.props.publishInChannel} onChange={this.toggleAnonymousPublish} />
|
||||||
<label className="label label--pointer" htmlFor="channel-radio">In a channel</label>
|
<label className='label label--pointer' htmlFor='channel-radio'>In a channel</label>
|
||||||
</div>
|
</div>
|
||||||
|
{ this.props.channelError ? (
|
||||||
|
<p className='info-message--failure'>{this.props.channelError}</p>
|
||||||
|
) : (
|
||||||
|
<p className='info-message'>Publish anonymously or in a channel</p>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
{ this.props.publishInChannel && (
|
{ this.props.publishInChannel && (
|
||||||
<div>
|
<div>
|
||||||
<div className="column column--3">
|
<div className='column column--3'>
|
||||||
<label className="label" htmlFor="channel-name-select">Channel:</label>
|
<label className='label' htmlFor='channel-name-select'>Channel:</label>
|
||||||
</div><div className="column column--7">
|
</div><div className='column column--7'>
|
||||||
<select type="text" id="channel-name-select" className="select select--arrow" value={this.props.selectedChannel} onChange={this.handleSelection}>
|
<select type='text' id='channel-name-select' className='select select--arrow' value={this.props.selectedChannel} onChange={this.handleSelection}>
|
||||||
{ this.props.loggedInChannelName && <option value={this.props.loggedInChannelName} id="publish-channel-select-channel-option">{this.props.loggedInChannelName}</option> }
|
{ this.props.loggedInChannelName && <option value={this.props.loggedInChannelName} id='publish-channel-select-channel-option'>{this.props.loggedInChannelName}</option> }
|
||||||
<option value={states.LOGIN}>Existing</option>
|
<option value={states.LOGIN}>Existing</option>
|
||||||
<option value={states.CREATE}>New</option>
|
<option value={states.CREATE}>New</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
{ (this.props.selectedChannel === states.LOGIN) && <ChannelLoginForm /> }
|
{ (this.props.selectedChannel === states.LOGIN) && <ChannelLoginForm /> }
|
||||||
{ (this.props.selectedChannel === states.CREATE) && <ChannelCreateForm /> }
|
{ (this.props.selectedChannel === states.CREATE) && <ChannelCreateForm /> }
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,7 +5,7 @@ import View from './view';
|
||||||
const mapStateToProps = ({ publish }) => {
|
const mapStateToProps = ({ publish }) => {
|
||||||
return {
|
return {
|
||||||
file : publish.file,
|
file : publish.file,
|
||||||
thumbnail: publish.metadata.thumbnail,
|
thumbnail: publish.thumbnail,
|
||||||
fileError: publish.error.file,
|
fileError: publish.error.file,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -14,15 +14,11 @@ const mapDispatchToProps = dispatch => {
|
||||||
return {
|
return {
|
||||||
selectFile: (file) => {
|
selectFile: (file) => {
|
||||||
dispatch(selectFile(file));
|
dispatch(selectFile(file));
|
||||||
dispatch(updateError('publishSubmit', null));
|
|
||||||
},
|
},
|
||||||
setFileError: (value) => {
|
setFileError: (value) => {
|
||||||
dispatch(clearFile());
|
dispatch(clearFile());
|
||||||
dispatch(updateError('file', value));
|
dispatch(updateError('file', value));
|
||||||
},
|
},
|
||||||
clearFileError: () => {
|
|
||||||
dispatch(updateError('file', null));
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -19,18 +19,17 @@ class Dropzone extends React.Component {
|
||||||
this.handleMouseLeave = this.handleMouseLeave.bind(this);
|
this.handleMouseLeave = this.handleMouseLeave.bind(this);
|
||||||
this.handleClick = this.handleClick.bind(this);
|
this.handleClick = this.handleClick.bind(this);
|
||||||
this.handleFileInput = this.handleFileInput.bind(this);
|
this.handleFileInput = this.handleFileInput.bind(this);
|
||||||
this.selectFile = this.selectFile.bind(this);
|
this.chooseFile = this.chooseFile.bind(this);
|
||||||
}
|
}
|
||||||
handleDrop (event) {
|
handleDrop (event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.setState({dragOver: false});
|
this.setState({dragOver: false});
|
||||||
// if dropped items aren't files, reject them
|
// if dropped items aren't files, reject them
|
||||||
const dt = event.dataTransfer;
|
const dt = event.dataTransfer;
|
||||||
console.log('dt', dt);
|
|
||||||
if (dt.items) {
|
if (dt.items) {
|
||||||
if (dt.items[0].kind == 'file') {
|
if (dt.items[0].kind === 'file') {
|
||||||
const droppedFile = dt.items[0].getAsFile();
|
const droppedFile = dt.items[0].getAsFile();
|
||||||
this.selectFile(droppedFile);
|
this.chooseFile(droppedFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -61,15 +60,14 @@ class Dropzone extends React.Component {
|
||||||
}
|
}
|
||||||
handleClick (event) {
|
handleClick (event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
// trigger file input
|
|
||||||
document.getElementById('file_input').click();
|
document.getElementById('file_input').click();
|
||||||
}
|
}
|
||||||
handleFileInput (event) {
|
handleFileInput (event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const fileList = event.target.files;
|
const fileList = event.target.files;
|
||||||
this.selectFile(fileList[0]);
|
this.chooseFile(fileList[0]);
|
||||||
}
|
}
|
||||||
selectFile (file) {
|
chooseFile (file) {
|
||||||
if (file) {
|
if (file) {
|
||||||
try {
|
try {
|
||||||
validateFile(file); // validate the file's name, type, and size
|
validateFile(file); // validate the file's name, type, and size
|
||||||
|
@ -77,7 +75,6 @@ class Dropzone extends React.Component {
|
||||||
return this.props.setFileError(error.message);
|
return this.props.setFileError(error.message);
|
||||||
}
|
}
|
||||||
// stage it so it will be ready when the publish button is clicked
|
// stage it so it will be ready when the publish button is clicked
|
||||||
this.props.clearFileError(null);
|
|
||||||
this.props.selectFile(file);
|
this.props.selectFile(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,6 @@ class LoginPage extends React.Component {
|
||||||
componentWillReceiveProps (newProps) {
|
componentWillReceiveProps (newProps) {
|
||||||
// re-route the user to the homepage if the user is logged in
|
// re-route the user to the homepage if the user is logged in
|
||||||
if (newProps.loggedInChannelName !== this.props.loggedInChannelName) {
|
if (newProps.loggedInChannelName !== this.props.loggedInChannelName) {
|
||||||
console.log('user logged into new channel:', newProps.loggedInChannelName);
|
|
||||||
this.props.history.push(`/`);
|
this.props.history.push(`/`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,9 +39,7 @@ class NavBar extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
handleSelection (event) {
|
handleSelection (event) {
|
||||||
console.log('handling selection', event);
|
|
||||||
const value = event.target.selectedOptions[0].value;
|
const value = event.target.selectedOptions[0].value;
|
||||||
console.log('value', value);
|
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case LOGOUT:
|
case LOGOUT:
|
||||||
this.logoutUser();
|
this.logoutUser();
|
||||||
|
|
16
react/containers/PublishDetails/index.js
Normal file
16
react/containers/PublishDetails/index.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import {connect} from 'react-redux';
|
||||||
|
import {clearFile, startPublish} from 'actions/publish';
|
||||||
|
import View from './view';
|
||||||
|
|
||||||
|
const mapStateToProps = ({ channel, publish }) => {
|
||||||
|
return {
|
||||||
|
file: publish.file,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
clearFile,
|
||||||
|
startPublish,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(View);
|
63
react/containers/PublishDetails/view.jsx
Normal file
63
react/containers/PublishDetails/view.jsx
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { withRouter } from 'react-router-dom';
|
||||||
|
import Dropzone from 'containers/Dropzone';
|
||||||
|
import PublishTitleInput from 'containers/PublishTitleInput';
|
||||||
|
import PublishUrlInput from 'containers/PublishUrlInput';
|
||||||
|
import PublishThumbnailInput from 'containers/PublishThumbnailInput';
|
||||||
|
import PublishMetadataInputs from 'containers/PublishMetadataInputs';
|
||||||
|
import ChannelSelect from 'containers/ChannelSelect';
|
||||||
|
|
||||||
|
class PublishDetails extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
this.onPublishSubmit = this.onPublishSubmit.bind(this);
|
||||||
|
}
|
||||||
|
onPublishSubmit () {
|
||||||
|
this.props.startPublish(this.props.history);
|
||||||
|
}
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<div className='row row--no-bottom'>
|
||||||
|
<div className='column column--10'>
|
||||||
|
<PublishTitleInput />
|
||||||
|
</div>
|
||||||
|
{/* left column */}
|
||||||
|
<div className='column column--5 column--sml-10' >
|
||||||
|
<div className='row row--padded'>
|
||||||
|
<Dropzone />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* right column */}
|
||||||
|
<div className='column column--5 column--sml-10 align-content-top'>
|
||||||
|
<div id='publish-active-area' className='row row--padded'>
|
||||||
|
<div className='row row--padded row--no-top row--wide'>
|
||||||
|
<PublishUrlInput />
|
||||||
|
</div>
|
||||||
|
<div className='row row--padded row--no-top row--wide'>
|
||||||
|
<ChannelSelect />
|
||||||
|
</div>
|
||||||
|
{ (this.props.file.type === 'video/mp4') && (
|
||||||
|
<div className='row row--padded row--no-top row--wide '>
|
||||||
|
<PublishThumbnailInput />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className='row row--padded row--no-top row--no-bottom row--wide'>
|
||||||
|
<PublishMetadataInputs />
|
||||||
|
</div>
|
||||||
|
<div className='row row--wide align-content-center'>
|
||||||
|
<button id='publish-submit' className='button--primary button--large' onClick={this.onPublishSubmit}>Publish</button>
|
||||||
|
</div>
|
||||||
|
<div className='row row--padded row--no-bottom align-content-center'>
|
||||||
|
<button className='button--cancel' onClick={this.props.clearFile}>Cancel</button>
|
||||||
|
</div>
|
||||||
|
<div className='row row--short align-content-center'>
|
||||||
|
<p className='fine-print'>By clicking 'Publish', you affirm that you have the rights to publish this content to the LBRY network, and that you understand the properties of publishing it to a decentralized, user-controlled network. <a className='link--primary' target='_blank' href='https://lbry.io/learn'>Read more.</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withRouter(PublishDetails);
|
|
@ -1,40 +0,0 @@
|
||||||
import {connect} from 'react-redux';
|
|
||||||
import {clearFile, updateError, updatePublishStatus} from 'actions/publish';
|
|
||||||
import View from './view';
|
|
||||||
|
|
||||||
const mapStateToProps = ({ channel, publish }) => {
|
|
||||||
return {
|
|
||||||
loggedInChannel : channel.loggedInChannel,
|
|
||||||
file : publish.file,
|
|
||||||
claim : publish.claim,
|
|
||||||
title : publish.metadata.title,
|
|
||||||
thumbnail : publish.metadata.thumbnail,
|
|
||||||
description : publish.metadata.description,
|
|
||||||
license : publish.metadata.license,
|
|
||||||
nsfw : publish.metadata.nsfw,
|
|
||||||
publishInChannel : publish.publishInChannel,
|
|
||||||
selectedChannel : publish.selectedChannel,
|
|
||||||
fileError : publish.error.file,
|
|
||||||
urlError : publish.error.url,
|
|
||||||
publishSubmitError: publish.error.publishSubmit,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => {
|
|
||||||
return {
|
|
||||||
onFileClear: () => {
|
|
||||||
dispatch(clearFile());
|
|
||||||
},
|
|
||||||
onPublishStatusChange: (status, message) => {
|
|
||||||
dispatch(updatePublishStatus(status, message));
|
|
||||||
},
|
|
||||||
onChannelSelectionError: (value) => {
|
|
||||||
dispatch(updateError('channel', value));
|
|
||||||
},
|
|
||||||
onPublishSubmitError: (value) => {
|
|
||||||
dispatch(updateError('publishSubmit', value));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(View);
|
|
|
@ -1,169 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { withRouter } from 'react-router-dom';
|
|
||||||
import Dropzone from 'containers/Dropzone';
|
|
||||||
import PublishTitleInput from 'containers/PublishTitleInput';
|
|
||||||
import PublishUrlInput from 'containers/PublishUrlInput';
|
|
||||||
import PublishThumbnailInput from 'containers/PublishThumbnailInput';
|
|
||||||
import PublishMetadataInputs from 'containers/PublishMetadataInputs';
|
|
||||||
import ChannelSelect from 'containers/ChannelSelect';
|
|
||||||
import * as publishStates from 'constants/publish_claim_states';
|
|
||||||
|
|
||||||
class PublishForm extends React.Component {
|
|
||||||
constructor (props) {
|
|
||||||
super(props);
|
|
||||||
// this.makePublishRequest = this.makePublishRequest.bind(this);
|
|
||||||
this.publish = this.publish.bind(this);
|
|
||||||
}
|
|
||||||
validateChannelSelection () {
|
|
||||||
console.log('validating channel selection');
|
|
||||||
// make sure all required data is provided
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
// if publishInChannel is true, is a channel selected & logged in?
|
|
||||||
if (this.props.publishInChannel && (this.props.selectedChannel !== this.props.loggedInChannel.name)) {
|
|
||||||
// update state with error
|
|
||||||
this.props.onChannelSelectionError('Log in to a channel or select Anonymous"');
|
|
||||||
// reject this promise
|
|
||||||
return reject(new Error('Fix the channel'));
|
|
||||||
}
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
validatePublishParams () {
|
|
||||||
console.log('validating publish params');
|
|
||||||
// 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 URL'));
|
|
||||||
}
|
|
||||||
if (this.props.urlError) {
|
|
||||||
return reject(new Error('Fix the url'));
|
|
||||||
}
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
makePublishRequest (file, metadata) {
|
|
||||||
console.log('making publish request');
|
|
||||||
const uri = '/api/claim/publish';
|
|
||||||
const xhr = new XMLHttpRequest();
|
|
||||||
const fd = this.appendDataToFormData(file, metadata);
|
|
||||||
xhr.upload.addEventListener('loadstart', () => {
|
|
||||||
this.props.onPublishStatusChange(publishStates.LOAD_START, 'upload started');
|
|
||||||
});
|
|
||||||
xhr.upload.addEventListener('progress', (e) => {
|
|
||||||
if (e.lengthComputable) {
|
|
||||||
const percentage = Math.round((e.loaded * 100) / e.total);
|
|
||||||
console.log('progress:', percentage);
|
|
||||||
this.props.onPublishStatusChange(publishStates.LOADING, `${percentage}%`);
|
|
||||||
}
|
|
||||||
}, false);
|
|
||||||
xhr.upload.addEventListener('load', () => {
|
|
||||||
console.log('loaded 100%');
|
|
||||||
this.props.onPublishStatusChange(publishStates.PUBLISHING, null);
|
|
||||||
}, false);
|
|
||||||
xhr.open('POST', uri, true);
|
|
||||||
xhr.onreadystatechange = () => {
|
|
||||||
if (xhr.readyState === 4) {
|
|
||||||
const response = JSON.parse(xhr.response);
|
|
||||||
console.log('publish response:', response);
|
|
||||||
if ((xhr.status === 200) && response.success) {
|
|
||||||
this.props.history.push(`/${response.data.claimId}/${response.data.name}`);
|
|
||||||
this.props.onFileClear();
|
|
||||||
} else {
|
|
||||||
this.props.onPublishStatusChange(publishStates.FAILED, response.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// Initiate a multipart/form-data upload
|
|
||||||
xhr.send(fd);
|
|
||||||
}
|
|
||||||
createMetadata () {
|
|
||||||
console.log('creating metadata');
|
|
||||||
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.selectedChannel;
|
|
||||||
}
|
|
||||||
return metadata;
|
|
||||||
}
|
|
||||||
appendDataToFormData (file, metadata) {
|
|
||||||
var fd = new FormData();
|
|
||||||
fd.append('file', file);
|
|
||||||
for (var key in metadata) {
|
|
||||||
if (metadata.hasOwnProperty(key)) {
|
|
||||||
fd.append(key, metadata[key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fd;
|
|
||||||
}
|
|
||||||
publish () {
|
|
||||||
console.log('publishing file');
|
|
||||||
// publish the asset
|
|
||||||
this.validateChannelSelection()
|
|
||||||
.then(() => {
|
|
||||||
return this.validatePublishParams();
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
const metadata = this.createMetadata();
|
|
||||||
// publish the claim
|
|
||||||
return this.makePublishRequest(this.props.file, metadata);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
this.props.onPublishSubmitError(error.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
render () {
|
|
||||||
return (
|
|
||||||
<div className='row row--no-bottom'>
|
|
||||||
<div className='column column--10'>
|
|
||||||
<PublishTitleInput />
|
|
||||||
</div>
|
|
||||||
<div className='column column--5 column--sml-10' >
|
|
||||||
<div className='row row--padded'>
|
|
||||||
<Dropzone />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='column column--5 column--sml-10 align-content-top'>
|
|
||||||
<div id='publish-active-area' className='row row--padded'>
|
|
||||||
<div className='row row--padded row--no-top row--wide'>
|
|
||||||
<PublishUrlInput />
|
|
||||||
</div>
|
|
||||||
<div className='row row--padded row--no-top row--wide'>
|
|
||||||
<ChannelSelect />
|
|
||||||
</div>
|
|
||||||
{ (this.props.file.type === 'video/mp4') && (
|
|
||||||
<div className='row row--padded row--no-top row--wide '>
|
|
||||||
<PublishThumbnailInput />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className='row row--padded row--no-top row--no-bottom row--wide'>
|
|
||||||
<PublishMetadataInputs />
|
|
||||||
</div>
|
|
||||||
<div className='row row--wide align-content-center'>
|
|
||||||
<button id='publish-submit' className='button--primary button--large' onClick={this.publish}>Publish</button>
|
|
||||||
</div>
|
|
||||||
<div className='row row--padded row--no-bottom align-content-center'>
|
|
||||||
<button className='button--cancel' onClick={this.props.onFileClear}>Cancel</button>
|
|
||||||
</div>
|
|
||||||
<div className='row row--short align-content-center'>
|
|
||||||
<p className='fine-print'>By clicking 'Publish', you affirm that you have the rights to publish this content to the LBRY network, and that you understand the properties of publishing it to a decentralized, user-controlled network. <a className='link--primary' target='_blank' href='https://lbry.io/learn'>Read more.</a></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withRouter(PublishForm);
|
|
16
react/containers/PublishStatus/index.js
Normal file
16
react/containers/PublishStatus/index.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import {connect} from 'react-redux';
|
||||||
|
import {clearFile} from 'actions/publish';
|
||||||
|
import View from './view';
|
||||||
|
|
||||||
|
const mapStateToProps = ({ publish }) => {
|
||||||
|
return {
|
||||||
|
status : publish.status.status,
|
||||||
|
message: publish.status.message,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
clearFile,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(View);
|
50
react/containers/PublishStatus/view.jsx
Normal file
50
react/containers/PublishStatus/view.jsx
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import React from 'react';
|
||||||
|
import ProgressBar from 'components/ProgressBar';
|
||||||
|
import * as publishStates from 'constants/publish_claim_states';
|
||||||
|
|
||||||
|
class PublishStatus extends React.Component {
|
||||||
|
render () {
|
||||||
|
const { status, message, clearFile } = this.props;
|
||||||
|
return (
|
||||||
|
<div className='row row--tall flex-container--column flex-container--center-center'>
|
||||||
|
{status === publishStates.LOAD_START &&
|
||||||
|
<div className='row align-content-center'>
|
||||||
|
<p>File is loading to server</p>
|
||||||
|
<p className='blue'>0%</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{status === publishStates.LOADING &&
|
||||||
|
<div>
|
||||||
|
<div className='row align-content-center'>
|
||||||
|
<p>File is loading to server</p>
|
||||||
|
<p className='blue'>{message}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{status === publishStates.PUBLISHING &&
|
||||||
|
<div className='row align-content-center'>
|
||||||
|
<p>Upload complete. Your file is now being published on the blockchain...</p>
|
||||||
|
<ProgressBar size={12} />
|
||||||
|
<p>Curious what magic is happening here? <a className='link--primary' target='blank' href='https://lbry.io/faq/what-is-lbry'>Learn more.</a></p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{status === publishStates.SUCCESS &&
|
||||||
|
<div className='row align-content-center'>
|
||||||
|
<p>Your publish is complete! You are being redirected to it now.</p>
|
||||||
|
<p>If you are not automatically redirected, <a className='link--primary' target='_blank' href={message}>click here.</a></p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
{status === publishStates.FAILED &&
|
||||||
|
<div className='row align-content-center'>
|
||||||
|
<p>Something went wrong...</p>
|
||||||
|
<p><strong>{message}</strong></p>
|
||||||
|
<p>For help, post the above error text in the #speech channel on the <a className='link--primary' href='https://discord.gg/YjYbwhS' target='_blank'>lbry discord</a></p>
|
||||||
|
<button className='button--secondary' onClick={clearFile}>Reset</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PublishStatus;
|
|
@ -1,19 +1,15 @@
|
||||||
import {connect} from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import {updateMetadata} from 'actions/publish';
|
import { onNewThumbnail } from 'actions/publish';
|
||||||
import View from './view';
|
import View from './view';
|
||||||
|
|
||||||
const mapStateToProps = ({ publish }) => {
|
const mapStateToProps = ({ publish: { file } }) => {
|
||||||
return {
|
return {
|
||||||
thumbnail: publish.metadata.thumbnail,
|
file,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => {
|
const mapDispatchToProps = {
|
||||||
return {
|
onNewThumbnail,
|
||||||
onThumbnailChange: (name, value) => {
|
|
||||||
dispatch(updateMetadata(name, value));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(View);
|
export default connect(mapStateToProps, mapDispatchToProps)(View);
|
||||||
|
|
|
@ -1,77 +1,137 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
function dataURItoBlob(dataURI) {
|
||||||
|
// convert base64/URLEncoded data component to raw binary data held in a string
|
||||||
|
let byteString = atob(dataURI.split(',')[1]);
|
||||||
|
// separate out the mime component
|
||||||
|
let mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
|
||||||
|
// write the bytes of the string to a typed array
|
||||||
|
let ia = new Uint8Array(byteString.length);
|
||||||
|
for (let i = 0; i < byteString.length; i++) {
|
||||||
|
ia[i] = byteString.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return new Blob([ia], {type: mimeString});
|
||||||
|
}
|
||||||
|
|
||||||
class PublishThumbnailInput extends React.Component {
|
class PublishThumbnailInput extends React.Component {
|
||||||
constructor (props) {
|
constructor (props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
videoPreviewSrc: null,
|
videoSource : null,
|
||||||
thumbnailError : null,
|
error : null,
|
||||||
thumbnailInput : '',
|
sliderMinRange: 1,
|
||||||
}
|
sliderMaxRange: null,
|
||||||
this.handleInput = this.handleInput.bind(this);
|
sliderValue : null,
|
||||||
this.updateVideoThumb = this.updateVideoThumb.bind(this);
|
};
|
||||||
|
this.handleVideoLoadedData = this.handleVideoLoadedData.bind(this);
|
||||||
|
this.handleSliderChange = this.handleSliderChange.bind(this);
|
||||||
|
this.createThumbnail = this.createThumbnail.bind(this);
|
||||||
}
|
}
|
||||||
handleInput (event) {
|
componentDidMount () {
|
||||||
const value = event.target.value;
|
const { file } = this.props;
|
||||||
this.setState({thumbnailInput: value});
|
this.setVideoSource(file);
|
||||||
}
|
}
|
||||||
urlIsAnImage (url) {
|
componentWillReceiveProps (nextProps) {
|
||||||
return (url.match(/\.(jpeg|jpg|gif|png)$/) != null);
|
// if file changes
|
||||||
|
if (nextProps.file && nextProps.file !== this.props.file) {
|
||||||
|
const { file } = nextProps;
|
||||||
|
this.setVideoSource(file);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
testImage (url) {
|
setVideoSource (file) {
|
||||||
return new Promise((resolve, reject) => {
|
const previewReader = new FileReader();
|
||||||
const xhttp = new XMLHttpRequest();
|
previewReader.readAsDataURL(file);
|
||||||
xhttp.open('HEAD', url, true);
|
previewReader.onloadend = () => {
|
||||||
xhttp.onreadystatechange = () => {
|
const dataUri = previewReader.result;
|
||||||
if (xhttp.readyState === 4) {
|
const blob = dataURItoBlob(dataUri);
|
||||||
if (xhttp.status === 200) {
|
const videoSource = URL.createObjectURL(blob);
|
||||||
resolve();
|
this.setState({ videoSource });
|
||||||
} else {
|
};
|
||||||
reject();
|
}
|
||||||
}
|
handleVideoLoadedData (event) {
|
||||||
}
|
const duration = event.target.duration;
|
||||||
};
|
const totalMinutes = Math.floor(duration / 60);
|
||||||
xhttp.send();
|
const totalSeconds = Math.floor(duration % 60);
|
||||||
|
// set the slider
|
||||||
|
this.setState({
|
||||||
|
sliderMaxRange: duration * 100,
|
||||||
|
sliderValue : duration * 100 / 2,
|
||||||
|
totalMinutes,
|
||||||
|
totalSeconds,
|
||||||
});
|
});
|
||||||
|
// update the current time of the video
|
||||||
|
let video = document.getElementById('video-thumb-player');
|
||||||
|
video.currentTime = duration / 2;
|
||||||
}
|
}
|
||||||
updateVideoThumb (event) {
|
handleSliderChange (event) {
|
||||||
const imageUrl = event.target.value;
|
const value = parseInt(event.target.value);
|
||||||
if (this.urlIsAnImage(imageUrl)) {
|
// update the slider value
|
||||||
this.testImage(imageUrl, 3000)
|
this.setState({
|
||||||
.then(() => {
|
sliderValue: value,
|
||||||
console.log('thumbnail is a valid image');
|
});
|
||||||
this.props.onThumbnailChange('thumbnail', imageUrl);
|
// update the current time of the video
|
||||||
this.setState({thumbnailError: null});
|
let video = document.getElementById('video-thumb-player');
|
||||||
})
|
video.currentTime = value / 100;
|
||||||
.catch(error => {
|
}
|
||||||
console.log('encountered an error loading thumbnail image url:', error);
|
createThumbnail () {
|
||||||
this.props.onThumbnailChange('thumbnail', null);
|
// take a snapshot
|
||||||
this.setState({thumbnailError: 'That is an invalid image url'});
|
let video = document.getElementById('video-thumb-player');
|
||||||
});
|
let canvas = document.createElement('canvas');
|
||||||
} else {
|
canvas.width = video.videoWidth;
|
||||||
this.props.onThumbnailChange('thumbnail', null);
|
canvas.height = video.videoHeight;
|
||||||
this.setState({thumbnailError: null});
|
canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||||
|
const dataUrl = canvas.toDataURL();
|
||||||
|
const blob = dataURItoBlob(dataUrl);
|
||||||
|
const snapshot = new File([blob], `thumbnail.png`, {
|
||||||
|
type: 'image/png',
|
||||||
|
});
|
||||||
|
// set the thumbnail in redux store
|
||||||
|
if (snapshot) {
|
||||||
|
this.props.onNewThumbnail(snapshot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
render () {
|
render () {
|
||||||
|
const { error, videoSource, sliderMinRange, sliderMaxRange, sliderValue, totalMinutes, totalSeconds } = this.state;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="column column--3 column--sml-10">
|
<label className='label'>Thumbnail:</label>
|
||||||
<label className="label">Thumbnail:</label>
|
<video
|
||||||
</div><div className="column column--6 column--sml-10">
|
id='video-thumb-player'
|
||||||
<div className="input-text--primary">
|
preload='metadata'
|
||||||
<p className="info-message-placeholder info-message--failure">{this.state.thumbnailError}</p>
|
muted
|
||||||
<input
|
style={{display: 'none'}}
|
||||||
type="text" id="claim-thumbnail-input"
|
playsInline
|
||||||
className="input-text input-text--full-width"
|
onLoadedData={this.handleVideoLoadedData}
|
||||||
placeholder="https://spee.ch/xyz/example.jpg"
|
src={videoSource}
|
||||||
value={this.state.thumbnailInput}
|
onSeeked={this.createThumbnail}
|
||||||
onChange={ (event) => {
|
/>
|
||||||
this.handleInput(event);
|
{
|
||||||
this.updateVideoThumb(event);
|
sliderValue ? (
|
||||||
}} />
|
<div>
|
||||||
</div>
|
<div className='flex-container--row flex-container--space-between-center' style={{width: '100%'}}>
|
||||||
</div>
|
<span className='info-message'>0'00"</span>
|
||||||
|
<span className='info-message'>{totalMinutes}'{totalSeconds}"</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type='range'
|
||||||
|
min={sliderMinRange}
|
||||||
|
max={sliderMaxRange}
|
||||||
|
value={sliderValue}
|
||||||
|
className='slider'
|
||||||
|
onChange={this.handleSliderChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className='info-message' >loading... </p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{ error ? (
|
||||||
|
<p className='info-message--failure'>{error}</p>
|
||||||
|
) : (
|
||||||
|
<p className='info-message'>Use slider to set thumbnail</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,8 @@ import View from './view';
|
||||||
|
|
||||||
const mapStateToProps = ({ publish }) => {
|
const mapStateToProps = ({ publish }) => {
|
||||||
return {
|
return {
|
||||||
file : publish.file,
|
file : publish.file,
|
||||||
status : publish.status.status,
|
status: publish.status.status,
|
||||||
message: publish.status.message,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,17 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Dropzone from 'containers/Dropzone';
|
import Dropzone from 'containers/Dropzone';
|
||||||
import PublishForm from 'containers/PublishForm';
|
import PublishDetails from 'containers/PublishDetails';
|
||||||
import PublishStatus from 'components/PublishStatus';
|
import PublishStatus from 'containers/PublishStatus';
|
||||||
|
|
||||||
class PublishTool extends React.Component {
|
class PublishTool extends React.Component {
|
||||||
render () {
|
render () {
|
||||||
if (this.props.file) {
|
if (this.props.file) {
|
||||||
if (this.props.status) {
|
if (this.props.status) {
|
||||||
return (
|
return (
|
||||||
<PublishStatus
|
<PublishStatus />
|
||||||
status={this.props.status}
|
|
||||||
message={this.props.message}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return <PublishForm />;
|
return <PublishDetails />;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return <Dropzone />;
|
return <Dropzone />;
|
||||||
|
|
|
@ -8,15 +8,19 @@ class PublishUrlInput extends React.Component {
|
||||||
this.handleInput = this.handleInput.bind(this);
|
this.handleInput = this.handleInput.bind(this);
|
||||||
}
|
}
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
if (!this.props.claim || this.props.claim === '') {
|
const { claim, fileName } = this.props;
|
||||||
this.setClaimNameFromFileName();
|
if (!claim) {
|
||||||
|
this.setClaimName(fileName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
componentWillReceiveProps ({claim: newClaim}) {
|
componentWillReceiveProps ({ claim, fileName }) {
|
||||||
if (newClaim) {
|
// if a new file was chosen, update the claim name
|
||||||
this.checkClaimIsAvailable(newClaim);
|
if (fileName !== this.props.fileName) {
|
||||||
} else {
|
return this.setClaimName(fileName);
|
||||||
this.props.onUrlError('Please enter a URL');
|
}
|
||||||
|
// if the claim has updated, check its availability
|
||||||
|
if (claim !== this.props.claim) {
|
||||||
|
this.validateClaim(claim);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
handleInput (event) {
|
handleInput (event) {
|
||||||
|
@ -30,16 +34,17 @@ class PublishUrlInput extends React.Component {
|
||||||
input = input.replace(/[^A-Za-z0-9-]/g, ''); // remove all characters that are not A-Z, a-z, 0-9, or '-'
|
input = input.replace(/[^A-Za-z0-9-]/g, ''); // remove all characters that are not A-Z, a-z, 0-9, or '-'
|
||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
setClaimNameFromFileName () {
|
setClaimName (fileName) {
|
||||||
const fileName = this.props.fileName;
|
|
||||||
const fileNameWithoutEnding = fileName.substring(0, fileName.lastIndexOf('.'));
|
const fileNameWithoutEnding = fileName.substring(0, fileName.lastIndexOf('.'));
|
||||||
const cleanClaimName = this.cleanseInput(fileNameWithoutEnding);
|
const cleanClaimName = this.cleanseInput(fileNameWithoutEnding);
|
||||||
this.props.onClaimChange(cleanClaimName);
|
this.props.onClaimChange(cleanClaimName);
|
||||||
}
|
}
|
||||||
checkClaimIsAvailable (claim) {
|
validateClaim (claim) {
|
||||||
|
if (!claim) {
|
||||||
|
return this.props.onUrlError('Enter a url above');
|
||||||
|
}
|
||||||
request(`/api/claim/availability/${claim}`)
|
request(`/api/claim/availability/${claim}`)
|
||||||
.then(validatedClaimName => {
|
.then(() => {
|
||||||
console.log('api/claim/availability response:', validatedClaimName);
|
|
||||||
this.props.onUrlError(null);
|
this.props.onUrlError(null);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
@ -47,25 +52,27 @@ class PublishUrlInput extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
render () {
|
render () {
|
||||||
|
const { claim, loggedInChannelName, loggedInChannelShortId, publishInChannel, selectedChannel, urlError } = this.props;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className='column column--10 column--sml-10'>
|
||||||
<p id="input-error-claim-name" className="info-message-placeholder info-message--failure">{this.props.urlError}</p>
|
<div className='input-text--primary span--relative'>
|
||||||
<div className="column column--3 column--sml-10">
|
<span className='url-text--secondary'>spee.ch / </span>
|
||||||
<label className="label">URL:</label>
|
|
||||||
</div><div className="column column--7 column--sml-10 input-text--primary span--relative">
|
|
||||||
|
|
||||||
<span className="url-text--secondary">spee.ch / </span>
|
|
||||||
|
|
||||||
<UrlMiddle
|
<UrlMiddle
|
||||||
publishInChannel={this.props.publishInChannel}
|
publishInChannel={publishInChannel}
|
||||||
selectedChannel={this.props.selectedChannel}
|
selectedChannel={selectedChannel}
|
||||||
loggedInChannelName={this.props.loggedInChannelName}
|
loggedInChannelName={loggedInChannelName}
|
||||||
loggedInChannelShortId={this.props.loggedInChannelShortId}
|
loggedInChannelShortId={loggedInChannelShortId}
|
||||||
/>
|
/>
|
||||||
|
<input type='text' id='claim-name-input' className='input-text' name='claim' placeholder='your-url-here' onChange={this.handleInput} value={claim} />
|
||||||
<input type="text" id="claim-name-input" className="input-text" name='claim' placeholder="your-url-here" onChange={this.handleInput} value={this.props.claim}/>
|
{ (claim && !urlError) && <span id='input-success-claim-name' className='info-message--success span--absolute'>{'\u2713'}</span> }
|
||||||
{ (this.props.claim && !this.props.urlError) && <span id="input-success-claim-name" className="info-message--success span--absolute">{'\u2713'}</span> }
|
{ urlError && <span id='input-success-channel-name' className='info-message--failure span--absolute'>{'\u2716'}</span> }
|
||||||
{ this.props.urlError && <span id="input-success-channel-name" className="info-message--failure span--absolute">{'\u2716'}</span> }
|
</div>
|
||||||
|
<div>
|
||||||
|
{ urlError ? (
|
||||||
|
<p id='input-error-claim-name' className='info-message--failure'>{urlError}</p>
|
||||||
|
) : (
|
||||||
|
<p className='info-message'>Choose a custom url</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,9 +2,11 @@ import { combineReducers } from 'redux';
|
||||||
import PublishReducer from 'reducers/publish';
|
import PublishReducer from 'reducers/publish';
|
||||||
import ChannelReducer from 'reducers/channel';
|
import ChannelReducer from 'reducers/channel';
|
||||||
import ShowReducer from 'reducers/show';
|
import ShowReducer from 'reducers/show';
|
||||||
|
import SiteReducer from 'reducers/site';
|
||||||
|
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
channel: ChannelReducer,
|
channel: ChannelReducer,
|
||||||
publish: PublishReducer,
|
publish: PublishReducer,
|
||||||
show : ShowReducer,
|
show : ShowReducer,
|
||||||
|
site : SiteReducer,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import * as actions from 'constants/publish_action_types';
|
import * as actions from 'constants/publish_action_types';
|
||||||
import { LOGIN } from 'constants/publish_channel_select_states';
|
import { LOGIN } from 'constants/publish_channel_select_states';
|
||||||
|
const { publish } = require('../../config/speechConfig.js');
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
publishInChannel : false,
|
publishInChannel : false,
|
||||||
|
@ -19,17 +20,19 @@ const initialState = {
|
||||||
claim : '',
|
claim : '',
|
||||||
metadata: {
|
metadata: {
|
||||||
title : '',
|
title : '',
|
||||||
thumbnail : '',
|
|
||||||
description: '',
|
description: '',
|
||||||
license : '',
|
license : '',
|
||||||
nsfw : false,
|
nsfw : false,
|
||||||
},
|
},
|
||||||
|
thumbnailChannel : publish.thumbnailChannel,
|
||||||
|
thumbnailChannelId: publish.thumbnailChannelId,
|
||||||
|
thumbnail : null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function (state = initialState, action) {
|
export default function (state = initialState, action) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case actions.FILE_SELECTED:
|
case actions.FILE_SELECTED:
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, initialState, { // note: clears to initial state
|
||||||
file: action.data,
|
file: action.data,
|
||||||
});
|
});
|
||||||
case actions.FILE_CLEAR:
|
case actions.FILE_CLEAR:
|
||||||
|
@ -66,6 +69,10 @@ export default function (state = initialState, action) {
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
showMetadataInputs: action.data,
|
showMetadataInputs: action.data,
|
||||||
});
|
});
|
||||||
|
case actions.THUMBNAIL_NEW:
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
thumbnail: action.data,
|
||||||
|
});
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
12
react/reducers/site.js
Normal file
12
react/reducers/site.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
const { site } = require('../../config/speechConfig.js');
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
host: site.host,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function (state = initialState, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ import { watchHandleShowPageUri } from './show_uri';
|
||||||
import { watchNewAssetRequest } from './show_asset';
|
import { watchNewAssetRequest } from './show_asset';
|
||||||
import { watchNewChannelRequest, watchUpdateChannelClaims } from './show_channel';
|
import { watchNewChannelRequest, watchUpdateChannelClaims } from './show_channel';
|
||||||
import { watchFileIsRequested } from './file';
|
import { watchFileIsRequested } from './file';
|
||||||
|
import { watchPublishStart } from './publish';
|
||||||
|
|
||||||
export default function * rootSaga () {
|
export default function * rootSaga () {
|
||||||
yield all([
|
yield all([
|
||||||
|
@ -11,5 +12,6 @@ export default function * rootSaga () {
|
||||||
watchNewChannelRequest(),
|
watchNewChannelRequest(),
|
||||||
watchUpdateChannelClaims(),
|
watchUpdateChannelClaims(),
|
||||||
watchFileIsRequested(),
|
watchFileIsRequested(),
|
||||||
|
watchPublishStart(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
62
react/sagas/publish.js
Normal file
62
react/sagas/publish.js
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import { call, put, select, take, takeLatest } from 'redux-saga/effects';
|
||||||
|
import * as actions from 'constants/publish_action_types';
|
||||||
|
import * as publishStates from 'constants/publish_claim_states';
|
||||||
|
import { updateError, updatePublishStatus, clearFile } from 'actions/publish';
|
||||||
|
import { selectPublishState } from 'selectors/publish';
|
||||||
|
import { selectChannelState } from 'selectors/channel';
|
||||||
|
import { selectSiteState } from 'selectors/site';
|
||||||
|
import { validateChannelSelection, validatePublishParams } from 'utils/validate';
|
||||||
|
import { createPublishMetadata, createPublishFormData, createThumbnailUrl } from 'utils/publish';
|
||||||
|
import { makePublishRequestChannel } from 'channels/publish';
|
||||||
|
|
||||||
|
function * publishFile (action) {
|
||||||
|
const { history } = action.data;
|
||||||
|
const { publishInChannel, selectedChannel, file, claim, metadata, thumbnailChannel, thumbnailChannelId, thumbnail, error: { url: urlError } } = yield select(selectPublishState);
|
||||||
|
const { loggedInChannel } = yield select(selectChannelState);
|
||||||
|
const { host } = yield select(selectSiteState);
|
||||||
|
// validate the channel selection
|
||||||
|
try {
|
||||||
|
validateChannelSelection(publishInChannel, selectedChannel, loggedInChannel);
|
||||||
|
} catch (error) {
|
||||||
|
return yield put(updateError('channel', error.message));
|
||||||
|
};
|
||||||
|
// validate publish parameters
|
||||||
|
try {
|
||||||
|
validatePublishParams(file, claim, urlError);
|
||||||
|
} catch (error) {
|
||||||
|
return yield put(updateError('publishSubmit', error.message));
|
||||||
|
}
|
||||||
|
// create metadata
|
||||||
|
let publishMetadata = createPublishMetadata(claim, file, metadata, publishInChannel, selectedChannel);
|
||||||
|
if (thumbnail) {
|
||||||
|
// add thumbnail to publish metadata
|
||||||
|
publishMetadata['thumbnail'] = createThumbnailUrl(thumbnailChannel, thumbnailChannelId, claim, host);
|
||||||
|
}
|
||||||
|
// create form data for main publish
|
||||||
|
const publishFormData = createPublishFormData(file, thumbnail, publishMetadata);
|
||||||
|
// make the publish request
|
||||||
|
const publishChannel = yield call(makePublishRequestChannel, publishFormData);
|
||||||
|
while (true) {
|
||||||
|
const {loadStart, progress, load, success, error} = yield take(publishChannel);
|
||||||
|
if (error) {
|
||||||
|
return yield put(updatePublishStatus(publishStates.FAILED, error.message));
|
||||||
|
}
|
||||||
|
if (success) {
|
||||||
|
yield put(clearFile());
|
||||||
|
return history.push(`/${success.data.claimId}/${success.data.name}`);
|
||||||
|
}
|
||||||
|
if (loadStart) {
|
||||||
|
yield put(updatePublishStatus(publishStates.LOAD_START, null));
|
||||||
|
}
|
||||||
|
if (progress) {
|
||||||
|
yield put(updatePublishStatus(publishStates.LOADING, `${progress}%`));
|
||||||
|
}
|
||||||
|
if (load) {
|
||||||
|
yield put(updatePublishStatus(publishStates.PUBLISHING, null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export function * watchPublishStart () {
|
||||||
|
yield takeLatest(actions.PUBLISH_START, publishFile);
|
||||||
|
};
|
|
@ -12,16 +12,13 @@ export function * newAssetRequest (action) {
|
||||||
// If this uri is in the request list, it's already been fetched
|
// If this uri is in the request list, it's already been fetched
|
||||||
const state = yield select(selectShowState);
|
const state = yield select(selectShowState);
|
||||||
if (state.requestList[requestId]) {
|
if (state.requestList[requestId]) {
|
||||||
console.log('that request already exists in the request list!');
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// get long id && add request to request list
|
// get long id && add request to request list
|
||||||
console.log(`getting asset long id ${name}`);
|
|
||||||
let longId;
|
let longId;
|
||||||
try {
|
try {
|
||||||
({data: longId} = yield call(getLongClaimId, name, modifier));
|
({data: longId} = yield call(getLongClaimId, name, modifier));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('error:', error);
|
|
||||||
return yield put(onRequestError(error.message));
|
return yield put(onRequestError(error.message));
|
||||||
}
|
}
|
||||||
const assetKey = `a#${name}#${longId}`;
|
const assetKey = `a#${name}#${longId}`;
|
||||||
|
@ -29,11 +26,9 @@ export function * newAssetRequest (action) {
|
||||||
// is this an existing asset?
|
// is this an existing asset?
|
||||||
// If this asset is in the asset list, it's already been fetched
|
// If this asset is in the asset list, it's already been fetched
|
||||||
if (state.assetList[assetKey]) {
|
if (state.assetList[assetKey]) {
|
||||||
console.log('that asset already exists in the asset list!');
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// get short Id
|
// get short Id
|
||||||
console.log(`getting asset short id ${name} ${longId}`);
|
|
||||||
let shortId;
|
let shortId;
|
||||||
try {
|
try {
|
||||||
({data: shortId} = yield call(getShortId, name, longId));
|
({data: shortId} = yield call(getShortId, name, longId));
|
||||||
|
@ -41,7 +36,6 @@ export function * newAssetRequest (action) {
|
||||||
return yield put(onRequestError(error.message));
|
return yield put(onRequestError(error.message));
|
||||||
}
|
}
|
||||||
// get asset claim data
|
// get asset claim data
|
||||||
console.log(`getting asset claim data ${name} ${longId}`);
|
|
||||||
let claimData;
|
let claimData;
|
||||||
try {
|
try {
|
||||||
({data: claimData} = yield call(getClaimData, name, longId));
|
({data: claimData} = yield call(getClaimData, name, longId));
|
||||||
|
|
|
@ -12,11 +12,9 @@ export function * newChannelRequest (action) {
|
||||||
// If this uri is in the request list, it's already been fetched
|
// If this uri is in the request list, it's already been fetched
|
||||||
const state = yield select(selectShowState);
|
const state = yield select(selectShowState);
|
||||||
if (state.requestList[requestId]) {
|
if (state.requestList[requestId]) {
|
||||||
console.log('that request already exists in the request list!');
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// get channel long id
|
// get channel long id
|
||||||
console.log('getting channel long id and short id');
|
|
||||||
let longId, shortId;
|
let longId, shortId;
|
||||||
try {
|
try {
|
||||||
({ data: {longChannelClaimId: longId, shortChannelClaimId: shortId} } = yield call(getChannelData, channelName, channelId));
|
({ data: {longChannelClaimId: longId, shortChannelClaimId: shortId} } = yield call(getChannelData, channelName, channelId));
|
||||||
|
@ -29,11 +27,9 @@ export function * newChannelRequest (action) {
|
||||||
// is this an existing channel?
|
// is this an existing channel?
|
||||||
// If this channel is in the channel list, it's already been fetched
|
// If this channel is in the channel list, it's already been fetched
|
||||||
if (state.channelList[channelKey]) {
|
if (state.channelList[channelKey]) {
|
||||||
console.log('that channel already exists in the channel list!');
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// get channel claims data
|
// get channel claims data
|
||||||
console.log('getting channel claims data');
|
|
||||||
let claimsData;
|
let claimsData;
|
||||||
try {
|
try {
|
||||||
({ data: claimsData } = yield call(getChannelClaims, channelName, longId, 1));
|
({ data: claimsData } = yield call(getChannelClaims, channelName, longId, 1));
|
||||||
|
|
|
@ -6,7 +6,6 @@ import { newChannelRequest } from 'sagas/show_channel';
|
||||||
import lbryUri from 'utils/lbryUri';
|
import lbryUri from 'utils/lbryUri';
|
||||||
|
|
||||||
function * parseAndUpdateIdentifierAndClaim (modifier, claim) {
|
function * parseAndUpdateIdentifierAndClaim (modifier, claim) {
|
||||||
console.log('parseAndUpdateIdentifierAndClaim');
|
|
||||||
// this is a request for an asset
|
// this is a request for an asset
|
||||||
// claim will be an asset claim
|
// claim will be an asset claim
|
||||||
// the identifier could be a channel or a claim id
|
// the identifier could be a channel or a claim id
|
||||||
|
@ -24,7 +23,6 @@ function * parseAndUpdateIdentifierAndClaim (modifier, claim) {
|
||||||
yield call(newAssetRequest, onNewAssetRequest(claimName, claimId, null, null, extension));
|
yield call(newAssetRequest, onNewAssetRequest(claimName, claimId, null, null, extension));
|
||||||
}
|
}
|
||||||
function * parseAndUpdateClaimOnly (claim) {
|
function * parseAndUpdateClaimOnly (claim) {
|
||||||
console.log('parseAndUpdateIdentifierAndClaim');
|
|
||||||
// this could be a request for an asset or a channel page
|
// this could be a request for an asset or a channel page
|
||||||
// claim could be an asset claim or a channel claim
|
// claim could be an asset claim or a channel claim
|
||||||
let isChannel, channelName, channelClaimId;
|
let isChannel, channelName, channelClaimId;
|
||||||
|
@ -49,7 +47,6 @@ function * parseAndUpdateClaimOnly (claim) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function * handleShowPageUri (action) {
|
export function * handleShowPageUri (action) {
|
||||||
console.log('handleShowPageUri');
|
|
||||||
const { identifier, claim } = action.data;
|
const { identifier, claim } = action.data;
|
||||||
if (identifier) {
|
if (identifier) {
|
||||||
return yield call(parseAndUpdateIdentifierAndClaim, identifier, claim);
|
return yield call(parseAndUpdateIdentifierAndClaim, identifier, claim);
|
||||||
|
|
3
react/selectors/channel.js
Normal file
3
react/selectors/channel.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export const selectChannelState = (state) => {
|
||||||
|
return state.channel;
|
||||||
|
};
|
3
react/selectors/publish.js
Normal file
3
react/selectors/publish.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export const selectPublishState = (state) => {
|
||||||
|
return state.publish;
|
||||||
|
};
|
3
react/selectors/site.js
Normal file
3
react/selectors/site.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export const selectSiteState = (state) => {
|
||||||
|
return state.site;
|
||||||
|
};
|
|
@ -1,11 +1,9 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
validateFile (file) {
|
validateFile (file) {
|
||||||
if (!file) {
|
if (!file) {
|
||||||
console.log('no file found');
|
|
||||||
throw new Error('no file provided');
|
throw new Error('no file provided');
|
||||||
}
|
}
|
||||||
if (/'/.test(file.name)) {
|
if (/'/.test(file.name)) {
|
||||||
console.log('file name had apostrophe in it');
|
|
||||||
throw new Error('apostrophes are not allowed in the file name');
|
throw new Error('apostrophes are not allowed in the file name');
|
||||||
}
|
}
|
||||||
// validate size and type
|
// validate size and type
|
||||||
|
@ -14,24 +12,20 @@ module.exports = {
|
||||||
case 'image/jpg':
|
case 'image/jpg':
|
||||||
case 'image/png':
|
case 'image/png':
|
||||||
if (file.size > 10000000) {
|
if (file.size > 10000000) {
|
||||||
console.log('file was too big');
|
|
||||||
throw new Error('Sorry, images are limited to 10 megabytes.');
|
throw new Error('Sorry, images are limited to 10 megabytes.');
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'image/gif':
|
case 'image/gif':
|
||||||
if (file.size > 50000000) {
|
if (file.size > 30000000) {
|
||||||
console.log('file was too big');
|
throw new Error('Sorry, GIFs are limited to 30 megabytes.');
|
||||||
throw new Error('Sorry, GIFs are limited to 50 megabytes.');
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'video/mp4':
|
case 'video/mp4':
|
||||||
if (file.size > 50000000) {
|
if (file.size > 20000000) {
|
||||||
console.log('file was too big');
|
throw new Error('Sorry, videos are limited to 20 megabytes.');
|
||||||
throw new Error('Sorry, videos are limited to 50 megabytes.');
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
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.');
|
throw new Error(file.type + ' is not a supported file type. Only, .jpeg, .png, .gif, and .mp4 files are currently supported.');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
35
react/utils/publish.js
Normal file
35
react/utils/publish.js
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
export const createPublishMetadata = (claim, { type }, { title, description, license, nsfw }, publishInChannel, selectedChannel) => {
|
||||||
|
let metadata = {
|
||||||
|
name: claim,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
license,
|
||||||
|
nsfw,
|
||||||
|
type,
|
||||||
|
};
|
||||||
|
if (publishInChannel) {
|
||||||
|
metadata['channelName'] = selectedChannel;
|
||||||
|
}
|
||||||
|
return metadata;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createPublishFormData = (file, thumbnail, metadata) => {
|
||||||
|
let fd = new FormData();
|
||||||
|
// append file
|
||||||
|
fd.append('file', file);
|
||||||
|
// append thumbnail
|
||||||
|
if (thumbnail) {
|
||||||
|
fd.append('thumbnail', thumbnail);
|
||||||
|
}
|
||||||
|
// append metadata
|
||||||
|
for (let key in metadata) {
|
||||||
|
if (metadata.hasOwnProperty(key)) {
|
||||||
|
fd.append(key, metadata[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fd;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createThumbnailUrl = (channel, channelId, claim, host) => {
|
||||||
|
return `${host}/${channel}:${channelId}/${claim}-thumb.png`;
|
||||||
|
};
|
17
react/utils/validate.js
Normal file
17
react/utils/validate.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
export const validateChannelSelection = (publishInChannel, selectedChannel, loggedInChannel) => {
|
||||||
|
if (publishInChannel && (selectedChannel !== loggedInChannel.name)) {
|
||||||
|
throw new Error('Log in to a channel or select Anonymous');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validatePublishParams = (file, claim, urlError) => {
|
||||||
|
if (!file) {
|
||||||
|
throw new Error('Please choose a file');
|
||||||
|
}
|
||||||
|
if (!claim) {
|
||||||
|
throw new Error('Please enter a URL');
|
||||||
|
}
|
||||||
|
if (urlError) {
|
||||||
|
throw new Error('Fix the url');
|
||||||
|
}
|
||||||
|
};
|
|
@ -5,7 +5,7 @@ const multipartMiddleware = multipart({uploadDir: files.uploadDirectory});
|
||||||
const db = require('../models');
|
const db = require('../models');
|
||||||
const { claimNameIsAvailable, checkChannelAvailability, publish } = require('../controllers/publishController.js');
|
const { claimNameIsAvailable, checkChannelAvailability, publish } = require('../controllers/publishController.js');
|
||||||
const { getClaimList, resolveUri, getClaim } = require('../helpers/lbryApi.js');
|
const { getClaimList, resolveUri, getClaim } = require('../helpers/lbryApi.js');
|
||||||
const { createBasicPublishParams, parsePublishApiRequestBody, parsePublishApiRequestFiles, addGetResultsToFileData, createFileData } = require('../helpers/publishHelpers.js');
|
const { addGetResultsToFileData, createBasicPublishParams, createThumbnailPublishParams, parsePublishApiRequestBody, parsePublishApiRequestFiles, createFileData } = require('../helpers/publishHelpers.js');
|
||||||
const errorHandlers = require('../helpers/errorHandlers.js');
|
const errorHandlers = require('../helpers/errorHandlers.js');
|
||||||
const { sendGAAnonymousPublishTiming, sendGAChannelPublishTiming } = require('../helpers/googleAnalytics.js');
|
const { sendGAAnonymousPublishTiming, sendGAChannelPublishTiming } = require('../helpers/googleAnalytics.js');
|
||||||
const { authenticateUser } = require('../auth/authentication.js');
|
const { authenticateUser } = require('../auth/authentication.js');
|
||||||
|
@ -128,17 +128,17 @@ module.exports = (app) => {
|
||||||
});
|
});
|
||||||
// route to run a publish request on the daemon
|
// route to run a publish request on the daemon
|
||||||
app.post('/api/claim/publish', multipartMiddleware, ({ body, files, headers, ip, originalUrl, user }, res) => {
|
app.post('/api/claim/publish', multipartMiddleware, ({ body, files, headers, ip, originalUrl, user }, res) => {
|
||||||
logger.debug('api/claim-publish body:', body);
|
logger.debug('api/claim/publish req.body:', body);
|
||||||
logger.debug('api/claim-publish files:', files);
|
logger.debug('api/claim/publish req.files:', files);
|
||||||
// define variables
|
// define variables
|
||||||
let name, fileName, filePath, fileType, nsfw, license, title, description, thumbnail, channelName, channelId, channelPassword;
|
let name, fileName, filePath, fileType, thumbnailFileName, thumbnailFilePath, thumbnailFileType, nsfw, license, title, description, thumbnail, channelName, channelId, channelPassword;
|
||||||
// record the start time of the request
|
// record the start time of the request
|
||||||
const publishStartTime = Date.now();
|
const publishStartTime = Date.now();
|
||||||
// validate the body and files of the request
|
// validate the body and files of the request
|
||||||
try {
|
try {
|
||||||
// validateApiPublishRequest(body, files);
|
// validateApiPublishRequest(body, files);
|
||||||
({name, nsfw, license, title, description, thumbnail} = parsePublishApiRequestBody(body));
|
({name, nsfw, license, title, description, thumbnail} = parsePublishApiRequestBody(body));
|
||||||
({fileName, filePath, fileType} = parsePublishApiRequestFiles(files));
|
({fileName, filePath, fileType, thumbnailFileName, thumbnailFilePath, thumbnailFileType} = parsePublishApiRequestFiles(files));
|
||||||
({channelName, channelId, channelPassword} = body);
|
({channelName, channelId, channelPassword} = body);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return res.status(400).json({success: false, message: error.message});
|
return res.status(400).json({success: false, message: error.message});
|
||||||
|
@ -148,13 +148,18 @@ module.exports = (app) => {
|
||||||
authenticateUser(channelName, channelId, channelPassword, user),
|
authenticateUser(channelName, channelId, channelPassword, user),
|
||||||
claimNameIsAvailable(name),
|
claimNameIsAvailable(name),
|
||||||
createBasicPublishParams(filePath, name, title, description, license, nsfw, thumbnail),
|
createBasicPublishParams(filePath, name, title, description, license, nsfw, thumbnail),
|
||||||
|
createThumbnailPublishParams(thumbnailFilePath, name, license, nsfw),
|
||||||
])
|
])
|
||||||
.then(([{channelName, channelClaimId}, validatedClaimName, publishParams]) => {
|
.then(([{channelName, channelClaimId}, validatedClaimName, publishParams, thumbnailPublishParams]) => {
|
||||||
// add channel details to the publish params
|
// add channel details to the publish params
|
||||||
if (channelName && channelClaimId) {
|
if (channelName && channelClaimId) {
|
||||||
publishParams['channel_name'] = channelName;
|
publishParams['channel_name'] = channelName;
|
||||||
publishParams['channel_id'] = channelClaimId;
|
publishParams['channel_id'] = channelClaimId;
|
||||||
}
|
}
|
||||||
|
// publish the thumbnail
|
||||||
|
if (thumbnailPublishParams) {
|
||||||
|
publish(thumbnailPublishParams, thumbnailFileName, thumbnailFileType);
|
||||||
|
}
|
||||||
// publish the asset
|
// publish the asset
|
||||||
return publish(publishParams, fileName, fileType);
|
return publish(publishParams, fileName, fileType);
|
||||||
})
|
})
|
||||||
|
|
Loading…
Add table
Reference in a new issue