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',
|
||||
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: {
|
||||
defaultTitle : 'Spee.ch',
|
||||
defaultThumbnail : 'https://spee.ch/assets/img/video_thumb_default.png',
|
||||
|
|
|
@ -39,7 +39,6 @@ module.exports = (req, res) => {
|
|||
.run(saga)
|
||||
.done
|
||||
.then(() => {
|
||||
console.log('preload sagas are done');
|
||||
// render component to a string
|
||||
const html = renderToString(
|
||||
<Provider store={store}>
|
||||
|
@ -56,10 +55,7 @@ module.exports = (req, res) => {
|
|||
|
||||
// check for a redirect
|
||||
if (context.url) {
|
||||
console.log('REDIRECTING:', 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
|
||||
|
|
|
@ -71,7 +71,6 @@ module.exports = {
|
|||
},
|
||||
resolveUri (uri) {
|
||||
logger.debug(`lbryApi >> Resolving URI for "${uri}"`);
|
||||
// console.log('resolving uri', uri);
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.post(lbryApiUri, {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
const logger = require('winston');
|
||||
const fs = require('fs');
|
||||
const { site, wallet } = require('../config/speechConfig.js');
|
||||
const { site, wallet, publish } = require('../config/speechConfig.js');
|
||||
|
||||
module.exports = {
|
||||
parsePublishApiRequestBody ({name, nsfw, license, title, description, thumbnail}) {
|
||||
|
@ -28,8 +28,7 @@ module.exports = {
|
|||
thumbnail,
|
||||
};
|
||||
},
|
||||
parsePublishApiRequestFiles ({file}) {
|
||||
logger.debug('file', file);
|
||||
parsePublishApiRequestFiles ({file, thumbnail}) {
|
||||
// make sure a file was provided
|
||||
if (!file) {
|
||||
throw new Error('no file with key of [file] found in request');
|
||||
|
@ -45,16 +44,18 @@ module.exports = {
|
|||
}
|
||||
// validate the 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');
|
||||
}
|
||||
// validate the file
|
||||
module.exports.validateFileTypeAndSize(file);
|
||||
// return results
|
||||
return {
|
||||
fileName: file.name,
|
||||
filePath: file.path,
|
||||
fileType: file.type,
|
||||
fileName : file.name,
|
||||
filePath : file.path,
|
||||
fileType : file.type,
|
||||
thumbnailFileName: (thumbnail ? thumbnail.name : null),
|
||||
thumbnailFilePath: (thumbnail ? thumbnail.path : null),
|
||||
thumbnailFileType: (thumbnail ? thumbnail.type : null),
|
||||
};
|
||||
},
|
||||
validateFileTypeAndSize (file) {
|
||||
|
@ -116,11 +117,34 @@ module.exports = {
|
|||
claim_address: wallet.lbryClaimAddress,
|
||||
};
|
||||
// add thumbnail to channel if video
|
||||
if (thumbnail !== null) {
|
||||
if (thumbnail) {
|
||||
publishParams['metadata']['thumbnail'] = thumbnail;
|
||||
}
|
||||
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) {
|
||||
fs.unlink(filePath, err => {
|
||||
if (err) {
|
||||
|
|
|
@ -283,11 +283,12 @@ a, a:visited {
|
|||
|
||||
/* ERROR MESSAGES */
|
||||
|
||||
.info-message--success, .info-message--failure {
|
||||
.info-message, .info-message--success, .info-message--failure {
|
||||
|
||||
font-size: medium;
|
||||
margin: 0px;
|
||||
padding: 0.3em;
|
||||
color: #9b9b9b;
|
||||
}
|
||||
|
||||
.info-message--success {
|
||||
|
@ -542,7 +543,7 @@ table {
|
|||
|
||||
/* show */
|
||||
|
||||
#video {
|
||||
.video {
|
||||
cursor: pointer;
|
||||
background-color: #ffffff;
|
||||
width: calc(100% - 12px - 12px - 2px);
|
||||
|
@ -603,33 +604,6 @@ table {
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
/* ---- grid items ---- */
|
||||
|
||||
.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%;
|
||||
.slider {
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
||||
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');
|
||||
|
||||
export function getLongClaimId (name, modifier) {
|
||||
// console.log('getting long claim id for asset:', name, modifier);
|
||||
let body = {};
|
||||
// create request params
|
||||
if (modifier) {
|
||||
|
@ -26,13 +25,11 @@ export function getLongClaimId (name, modifier) {
|
|||
};
|
||||
|
||||
export function getShortId (name, claimId) {
|
||||
// console.log('getting short id for asset:', name, claimId);
|
||||
const url = `${host}/api/claim/short-id/${claimId}/${name}`;
|
||||
return Request(url);
|
||||
};
|
||||
|
||||
export function getClaimData (name, claimId) {
|
||||
// console.log('getting claim data for asset:', name, claimId);
|
||||
const url = `${host}/api/claim/data/${name}/${claimId}`;
|
||||
return Request(url);
|
||||
};
|
||||
|
|
|
@ -2,14 +2,12 @@ import Request from 'utils/request';
|
|||
const { site: { host } } = require('../../config/speechConfig.js');
|
||||
|
||||
export function getChannelData (name, id) {
|
||||
console.log('getting channel data for channel:', name, id);
|
||||
if (!id) id = 'none';
|
||||
const url = `${host}/api/channel/data/${name}/${id}`;
|
||||
return Request(url);
|
||||
};
|
||||
|
||||
export function getChannelClaims (name, longId, page) {
|
||||
console.log('getting channel claims for channel:', name, longId);
|
||||
if (!page) page = 1;
|
||||
const url = `${host}/api/channel/claims/${name}/${longId}/${page}`;
|
||||
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':
|
||||
return (
|
||||
<video id='video' className='asset' controls poster={thumbnail}>
|
||||
<video className='asset video' controls poster={thumbnail}>
|
||||
<source
|
||||
src={`/${claimId}/${name}.${fileExt}`}
|
||||
/>
|
||||
|
|
|
@ -24,7 +24,7 @@ const AssetPreview = ({ claimData: { name, claimId, fileExt, contentType, thumbn
|
|||
case 'video/mp4':
|
||||
return (
|
||||
<img
|
||||
className={'asset-preview'}
|
||||
className={'asset-preview video'}
|
||||
src={thumbnail || defaultThumbnail}
|
||||
alt={name}
|
||||
/>
|
||||
|
|
|
@ -10,34 +10,44 @@ class Preview extends React.Component {
|
|||
};
|
||||
}
|
||||
componentDidMount () {
|
||||
this.previewFile(this.props.file);
|
||||
this.setPreviewImageSource(this.props.file);
|
||||
}
|
||||
componentWillReceiveProps (newProps) {
|
||||
if (newProps.file !== this.props.file) {
|
||||
this.previewFile(newProps.file);
|
||||
this.setPreviewImageSource(newProps.file);
|
||||
}
|
||||
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') {
|
||||
const previewReader = new FileReader();
|
||||
previewReader.readAsDataURL(file);
|
||||
previewReader.onloadend = () => {
|
||||
this.setState({imgSource: previewReader.result});
|
||||
};
|
||||
this.setPreviewImageSourceFromFile(file);
|
||||
} 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 () {
|
||||
return (
|
||||
<img
|
||||
id="dropzone-preview"
|
||||
id='dropzone-preview'
|
||||
src={this.state.imgSource}
|
||||
className={this.props.dimPreview ? 'dim' : ''}
|
||||
alt="publish preview"
|
||||
alt='publish preview'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -46,7 +56,7 @@ class Preview extends React.Component {
|
|||
Preview.propTypes = {
|
||||
dimPreview: PropTypes.bool.isRequired,
|
||||
file : PropTypes.object.isRequired,
|
||||
thumbnail : PropTypes.string,
|
||||
thumbnail : PropTypes.object,
|
||||
};
|
||||
|
||||
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 SELECTED_CHANNEL_UPDATE = 'SELECTED_CHANNEL_UPDATE';
|
||||
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 { updateLoggedInChannel } from 'actions/channel';
|
||||
import View from './view';
|
||||
import {updateSelectedChannel} from '../../actions/publish';
|
||||
import {updateSelectedChannel} from 'actions/publish';
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
|
|
|
@ -54,7 +54,6 @@ class ChannelCreateForm extends React.Component {
|
|||
return new Promise((resolve, reject) => {
|
||||
request(`/api/channel/availability/${channelWithAtSymbol}`)
|
||||
.then(isAvailable => {
|
||||
console.log('checkIsChannelAvailable result:', isAvailable);
|
||||
if (!isAvailable) {
|
||||
return reject(new Error('That channel has already been claimed'));
|
||||
}
|
||||
|
@ -69,10 +68,8 @@ class ChannelCreateForm extends React.Component {
|
|||
const password = this.state.password;
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!password || password.length < 1) {
|
||||
console.log('password not provided');
|
||||
return reject(new Error('Please provide a password'));
|
||||
}
|
||||
console.log('password provided');
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
@ -88,12 +85,10 @@ class ChannelCreateForm extends React.Component {
|
|||
return new Promise((resolve, reject) => {
|
||||
request('/signup', params)
|
||||
.then(result => {
|
||||
console.log('makePublishChannelRequest result:', result);
|
||||
return resolve(result);
|
||||
})
|
||||
.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!'));
|
||||
reject(new Error(`Unfortunately, we encountered an error while creating your channel. Please let us know in Discord! ${error.message}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -119,38 +114,41 @@ class ChannelCreateForm extends React.Component {
|
|||
return (
|
||||
<div>
|
||||
{ !this.state.status ? (
|
||||
<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="column column--3 column--sml-10">
|
||||
<label className="label" htmlFor="new-channel-name">Name:</label>
|
||||
</div><div className="column column--6 column--sml-10">
|
||||
<div className="input-text--primary flex-container--row flex-container--left-bottom span--relative">
|
||||
<form id='publish-channel-form'>
|
||||
<div className='row row--wide row--short'>
|
||||
<div className='column column--3 column--sml-10'>
|
||||
<label className='label' htmlFor='new-channel-name'>Name:</label>
|
||||
</div><div className='column column--6 column--sml-10'>
|
||||
<div className='input-text--primary flex-container--row flex-container--left-bottom span--relative'>
|
||||
<span>@</span>
|
||||
<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.error && <span id="input-success-channel-name" className="info-message--failure span--absolute">{'\u2716'}</span> }
|
||||
<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.error && <span id='input-success-channel-name' className='info-message--failure span--absolute'>{'\u2716'}</span> }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row row--wide row--short">
|
||||
<div className="column column--3 column--sml-10">
|
||||
<label className="label" htmlFor="new-channel-password">Password:</label>
|
||||
</div><div className="column column--6 column--sml-10">
|
||||
<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} />
|
||||
<div className='row row--wide row--short'>
|
||||
<div className='column column--3 column--sml-10'>
|
||||
<label className='label' htmlFor='new-channel-password'>Password:</label>
|
||||
</div><div className='column column--6 column--sml-10'>
|
||||
<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} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row row--wide">
|
||||
<button className="button--primary" onClick={this.createChannel}>Create Channel</button>
|
||||
{this.state.error ? (
|
||||
<p className='info-message--failure'>{this.state.error}</p>
|
||||
) : (
|
||||
<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>
|
||||
</form>
|
||||
) : (
|
||||
<div>
|
||||
<p className="fine-print">{this.state.status}</p>
|
||||
<ProgressBar size={12}/>
|
||||
<p className='fine-print'>{this.state.status}</p>
|
||||
<ProgressBar size={12} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -26,10 +26,9 @@ class ChannelLoginForm extends React.Component {
|
|||
'Content-Type': 'application/json',
|
||||
}),
|
||||
credentials: 'include',
|
||||
}
|
||||
};
|
||||
request('login', params)
|
||||
.then(({success, channelName, shortChannelId, channelClaimId, message}) => {
|
||||
console.log('loginToChannel success:', success);
|
||||
if (success) {
|
||||
this.props.onChannelLogin(channelName, shortChannelId, channelClaimId);
|
||||
} else {
|
||||
|
@ -37,7 +36,6 @@ class ChannelLoginForm extends React.Component {
|
|||
};
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('login error', error);
|
||||
if (error.message) {
|
||||
this.setState({'error': error.message});
|
||||
} else {
|
||||
|
@ -47,30 +45,33 @@ class ChannelLoginForm extends React.Component {
|
|||
}
|
||||
render () {
|
||||
return (
|
||||
<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="column column--3 column--sml-10">
|
||||
<label className="label" htmlFor="channel-login-name-input">Name:</label>
|
||||
</div><div className="column column--6 column--sml-10">
|
||||
<div className="input-text--primary flex-container--row flex-container--left-bottom">
|
||||
<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}/>
|
||||
<form id='channel-login-form'>
|
||||
<div className='row row--wide row--short'>
|
||||
<div className='column column--3 column--sml-10'>
|
||||
<label className='label' htmlFor='channel-login-name-input'>Name:</label>
|
||||
</div><div className='column column--6 column--sml-10'>
|
||||
<div className='input-text--primary flex-container--row flex-container--left-bottom'>
|
||||
<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} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row row--wide row--short">
|
||||
<div className="column column--3 column--sml-10">
|
||||
<label className="label" htmlFor="channel-login-password-input" >Password:</label>
|
||||
</div><div className="column column--6 column--sml-10">
|
||||
<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}/>
|
||||
<div className='row row--wide row--short'>
|
||||
<div className='column column--3 column--sml-10'>
|
||||
<label className='label' htmlFor='channel-login-password-input' >Password:</label>
|
||||
</div><div className='column column--6 column--sml-10'>
|
||||
<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} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row row--wide">
|
||||
<button className="button--primary" onClick={this.loginToChannel}>Authenticate</button>
|
||||
{ this.state.error ? (
|
||||
<p className='info-message--failure'>{this.state.error}</p>
|
||||
) : (
|
||||
<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>
|
||||
</form>
|
||||
);
|
||||
|
|
|
@ -24,28 +24,32 @@ class ChannelSelect extends React.Component {
|
|||
render () {
|
||||
return (
|
||||
<div>
|
||||
<p id="input-error-channel-select" className="info-message-placeholder info-message--failure">{this.props.channelError}</p>
|
||||
<form>
|
||||
<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}/>
|
||||
<label className="label label--pointer" htmlFor="anonymous-radio">Anonymous</label>
|
||||
<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} />
|
||||
<label className='label label--pointer' htmlFor='anonymous-radio'>Anonymous</label>
|
||||
</div>
|
||||
<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}/>
|
||||
<label className="label label--pointer" htmlFor="channel-radio">In a channel</label>
|
||||
<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} />
|
||||
<label className='label label--pointer' htmlFor='channel-radio'>In a channel</label>
|
||||
</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>
|
||||
{ this.props.publishInChannel && (
|
||||
<div>
|
||||
<div className="column column--3">
|
||||
<label className="label" htmlFor="channel-name-select">Channel:</label>
|
||||
</div><div className="column column--7">
|
||||
<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> }
|
||||
<option value={states.LOGIN}>Existing</option>
|
||||
<option value={states.CREATE}>New</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className='column column--3'>
|
||||
<label className='label' htmlFor='channel-name-select'>Channel:</label>
|
||||
</div><div className='column column--7'>
|
||||
<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> }
|
||||
<option value={states.LOGIN}>Existing</option>
|
||||
<option value={states.CREATE}>New</option>
|
||||
</select>
|
||||
</div>
|
||||
{ (this.props.selectedChannel === states.LOGIN) && <ChannelLoginForm /> }
|
||||
{ (this.props.selectedChannel === states.CREATE) && <ChannelCreateForm /> }
|
||||
</div>
|
||||
|
|
|
@ -5,7 +5,7 @@ import View from './view';
|
|||
const mapStateToProps = ({ publish }) => {
|
||||
return {
|
||||
file : publish.file,
|
||||
thumbnail: publish.metadata.thumbnail,
|
||||
thumbnail: publish.thumbnail,
|
||||
fileError: publish.error.file,
|
||||
};
|
||||
};
|
||||
|
@ -14,15 +14,11 @@ const mapDispatchToProps = dispatch => {
|
|||
return {
|
||||
selectFile: (file) => {
|
||||
dispatch(selectFile(file));
|
||||
dispatch(updateError('publishSubmit', null));
|
||||
},
|
||||
setFileError: (value) => {
|
||||
dispatch(clearFile());
|
||||
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.handleClick = this.handleClick.bind(this);
|
||||
this.handleFileInput = this.handleFileInput.bind(this);
|
||||
this.selectFile = this.selectFile.bind(this);
|
||||
this.chooseFile = this.chooseFile.bind(this);
|
||||
}
|
||||
handleDrop (event) {
|
||||
event.preventDefault();
|
||||
this.setState({dragOver: false});
|
||||
// if dropped items aren't files, reject them
|
||||
const dt = event.dataTransfer;
|
||||
console.log('dt', dt);
|
||||
if (dt.items) {
|
||||
if (dt.items[0].kind == 'file') {
|
||||
if (dt.items[0].kind === 'file') {
|
||||
const droppedFile = dt.items[0].getAsFile();
|
||||
this.selectFile(droppedFile);
|
||||
this.chooseFile(droppedFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -61,15 +60,14 @@ class Dropzone extends React.Component {
|
|||
}
|
||||
handleClick (event) {
|
||||
event.preventDefault();
|
||||
// trigger file input
|
||||
document.getElementById('file_input').click();
|
||||
}
|
||||
handleFileInput (event) {
|
||||
event.preventDefault();
|
||||
const fileList = event.target.files;
|
||||
this.selectFile(fileList[0]);
|
||||
this.chooseFile(fileList[0]);
|
||||
}
|
||||
selectFile (file) {
|
||||
chooseFile (file) {
|
||||
if (file) {
|
||||
try {
|
||||
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);
|
||||
}
|
||||
// stage it so it will be ready when the publish button is clicked
|
||||
this.props.clearFileError(null);
|
||||
this.props.selectFile(file);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ class LoginPage extends React.Component {
|
|||
componentWillReceiveProps (newProps) {
|
||||
// re-route the user to the homepage if the user is logged in
|
||||
if (newProps.loggedInChannelName !== this.props.loggedInChannelName) {
|
||||
console.log('user logged into new channel:', newProps.loggedInChannelName);
|
||||
this.props.history.push(`/`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,9 +39,7 @@ class NavBar extends React.Component {
|
|||
});
|
||||
}
|
||||
handleSelection (event) {
|
||||
console.log('handling selection', event);
|
||||
const value = event.target.selectedOptions[0].value;
|
||||
console.log('value', value);
|
||||
switch (value) {
|
||||
case LOGOUT:
|
||||
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 {updateMetadata} from 'actions/publish';
|
||||
import { connect } from 'react-redux';
|
||||
import { onNewThumbnail } from 'actions/publish';
|
||||
import View from './view';
|
||||
|
||||
const mapStateToProps = ({ publish }) => {
|
||||
const mapStateToProps = ({ publish: { file } }) => {
|
||||
return {
|
||||
thumbnail: publish.metadata.thumbnail,
|
||||
file,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
onThumbnailChange: (name, value) => {
|
||||
dispatch(updateMetadata(name, value));
|
||||
},
|
||||
};
|
||||
const mapDispatchToProps = {
|
||||
onNewThumbnail,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(View);
|
||||
|
|
|
@ -1,77 +1,137 @@
|
|||
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 {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
videoPreviewSrc: null,
|
||||
thumbnailError : null,
|
||||
thumbnailInput : '',
|
||||
}
|
||||
this.handleInput = this.handleInput.bind(this);
|
||||
this.updateVideoThumb = this.updateVideoThumb.bind(this);
|
||||
videoSource : null,
|
||||
error : null,
|
||||
sliderMinRange: 1,
|
||||
sliderMaxRange: null,
|
||||
sliderValue : null,
|
||||
};
|
||||
this.handleVideoLoadedData = this.handleVideoLoadedData.bind(this);
|
||||
this.handleSliderChange = this.handleSliderChange.bind(this);
|
||||
this.createThumbnail = this.createThumbnail.bind(this);
|
||||
}
|
||||
handleInput (event) {
|
||||
const value = event.target.value;
|
||||
this.setState({thumbnailInput: value});
|
||||
componentDidMount () {
|
||||
const { file } = this.props;
|
||||
this.setVideoSource(file);
|
||||
}
|
||||
urlIsAnImage (url) {
|
||||
return (url.match(/\.(jpeg|jpg|gif|png)$/) != null);
|
||||
componentWillReceiveProps (nextProps) {
|
||||
// if file changes
|
||||
if (nextProps.file && nextProps.file !== this.props.file) {
|
||||
const { file } = nextProps;
|
||||
this.setVideoSource(file);
|
||||
};
|
||||
}
|
||||
testImage (url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhttp = new XMLHttpRequest();
|
||||
xhttp.open('HEAD', url, true);
|
||||
xhttp.onreadystatechange = () => {
|
||||
if (xhttp.readyState === 4) {
|
||||
if (xhttp.status === 200) {
|
||||
resolve();
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
}
|
||||
};
|
||||
xhttp.send();
|
||||
setVideoSource (file) {
|
||||
const previewReader = new FileReader();
|
||||
previewReader.readAsDataURL(file);
|
||||
previewReader.onloadend = () => {
|
||||
const dataUri = previewReader.result;
|
||||
const blob = dataURItoBlob(dataUri);
|
||||
const videoSource = URL.createObjectURL(blob);
|
||||
this.setState({ videoSource });
|
||||
};
|
||||
}
|
||||
handleVideoLoadedData (event) {
|
||||
const duration = event.target.duration;
|
||||
const totalMinutes = Math.floor(duration / 60);
|
||||
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) {
|
||||
const imageUrl = event.target.value;
|
||||
if (this.urlIsAnImage(imageUrl)) {
|
||||
this.testImage(imageUrl, 3000)
|
||||
.then(() => {
|
||||
console.log('thumbnail is a valid image');
|
||||
this.props.onThumbnailChange('thumbnail', imageUrl);
|
||||
this.setState({thumbnailError: null});
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('encountered an error loading thumbnail image url:', error);
|
||||
this.props.onThumbnailChange('thumbnail', null);
|
||||
this.setState({thumbnailError: 'That is an invalid image url'});
|
||||
});
|
||||
} else {
|
||||
this.props.onThumbnailChange('thumbnail', null);
|
||||
this.setState({thumbnailError: null});
|
||||
handleSliderChange (event) {
|
||||
const value = parseInt(event.target.value);
|
||||
// update the slider value
|
||||
this.setState({
|
||||
sliderValue: value,
|
||||
});
|
||||
// update the current time of the video
|
||||
let video = document.getElementById('video-thumb-player');
|
||||
video.currentTime = value / 100;
|
||||
}
|
||||
createThumbnail () {
|
||||
// take a snapshot
|
||||
let video = document.getElementById('video-thumb-player');
|
||||
let canvas = document.createElement('canvas');
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
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 () {
|
||||
const { error, videoSource, sliderMinRange, sliderMaxRange, sliderValue, totalMinutes, totalSeconds } = this.state;
|
||||
return (
|
||||
<div>
|
||||
<div className="column column--3 column--sml-10">
|
||||
<label className="label">Thumbnail:</label>
|
||||
</div><div className="column column--6 column--sml-10">
|
||||
<div className="input-text--primary">
|
||||
<p className="info-message-placeholder info-message--failure">{this.state.thumbnailError}</p>
|
||||
<input
|
||||
type="text" id="claim-thumbnail-input"
|
||||
className="input-text input-text--full-width"
|
||||
placeholder="https://spee.ch/xyz/example.jpg"
|
||||
value={this.state.thumbnailInput}
|
||||
onChange={ (event) => {
|
||||
this.handleInput(event);
|
||||
this.updateVideoThumb(event);
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
<label className='label'>Thumbnail:</label>
|
||||
<video
|
||||
id='video-thumb-player'
|
||||
preload='metadata'
|
||||
muted
|
||||
style={{display: 'none'}}
|
||||
playsInline
|
||||
onLoadedData={this.handleVideoLoadedData}
|
||||
src={videoSource}
|
||||
onSeeked={this.createThumbnail}
|
||||
/>
|
||||
{
|
||||
sliderValue ? (
|
||||
<div>
|
||||
<div className='flex-container--row flex-container--space-between-center' style={{width: '100%'}}>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,9 +3,8 @@ import View from './view';
|
|||
|
||||
const mapStateToProps = ({ publish }) => {
|
||||
return {
|
||||
file : publish.file,
|
||||
status : publish.status.status,
|
||||
message: publish.status.message,
|
||||
file : publish.file,
|
||||
status: publish.status.status,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -1,20 +1,17 @@
|
|||
import React from 'react';
|
||||
import Dropzone from 'containers/Dropzone';
|
||||
import PublishForm from 'containers/PublishForm';
|
||||
import PublishStatus from 'components/PublishStatus';
|
||||
import PublishDetails from 'containers/PublishDetails';
|
||||
import PublishStatus from 'containers/PublishStatus';
|
||||
|
||||
class PublishTool extends React.Component {
|
||||
render () {
|
||||
if (this.props.file) {
|
||||
if (this.props.status) {
|
||||
return (
|
||||
<PublishStatus
|
||||
status={this.props.status}
|
||||
message={this.props.message}
|
||||
/>
|
||||
<PublishStatus />
|
||||
);
|
||||
} else {
|
||||
return <PublishForm />;
|
||||
return <PublishDetails />;
|
||||
}
|
||||
} else {
|
||||
return <Dropzone />;
|
||||
|
|
|
@ -8,15 +8,19 @@ class PublishUrlInput extends React.Component {
|
|||
this.handleInput = this.handleInput.bind(this);
|
||||
}
|
||||
componentDidMount () {
|
||||
if (!this.props.claim || this.props.claim === '') {
|
||||
this.setClaimNameFromFileName();
|
||||
const { claim, fileName } = this.props;
|
||||
if (!claim) {
|
||||
this.setClaimName(fileName);
|
||||
}
|
||||
}
|
||||
componentWillReceiveProps ({claim: newClaim}) {
|
||||
if (newClaim) {
|
||||
this.checkClaimIsAvailable(newClaim);
|
||||
} else {
|
||||
this.props.onUrlError('Please enter a URL');
|
||||
componentWillReceiveProps ({ claim, fileName }) {
|
||||
// if a new file was chosen, update the claim name
|
||||
if (fileName !== this.props.fileName) {
|
||||
return this.setClaimName(fileName);
|
||||
}
|
||||
// if the claim has updated, check its availability
|
||||
if (claim !== this.props.claim) {
|
||||
this.validateClaim(claim);
|
||||
}
|
||||
}
|
||||
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 '-'
|
||||
return input;
|
||||
}
|
||||
setClaimNameFromFileName () {
|
||||
const fileName = this.props.fileName;
|
||||
setClaimName (fileName) {
|
||||
const fileNameWithoutEnding = fileName.substring(0, fileName.lastIndexOf('.'));
|
||||
const cleanClaimName = this.cleanseInput(fileNameWithoutEnding);
|
||||
this.props.onClaimChange(cleanClaimName);
|
||||
}
|
||||
checkClaimIsAvailable (claim) {
|
||||
validateClaim (claim) {
|
||||
if (!claim) {
|
||||
return this.props.onUrlError('Enter a url above');
|
||||
}
|
||||
request(`/api/claim/availability/${claim}`)
|
||||
.then(validatedClaimName => {
|
||||
console.log('api/claim/availability response:', validatedClaimName);
|
||||
.then(() => {
|
||||
this.props.onUrlError(null);
|
||||
})
|
||||
.catch((error) => {
|
||||
|
@ -47,25 +52,27 @@ class PublishUrlInput extends React.Component {
|
|||
});
|
||||
}
|
||||
render () {
|
||||
const { claim, loggedInChannelName, loggedInChannelShortId, publishInChannel, selectedChannel, urlError } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<p id="input-error-claim-name" className="info-message-placeholder info-message--failure">{this.props.urlError}</p>
|
||||
<div className="column column--3 column--sml-10">
|
||||
<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>
|
||||
|
||||
<div className='column column--10 column--sml-10'>
|
||||
<div className='input-text--primary span--relative'>
|
||||
<span className='url-text--secondary'>spee.ch / </span>
|
||||
<UrlMiddle
|
||||
publishInChannel={this.props.publishInChannel}
|
||||
selectedChannel={this.props.selectedChannel}
|
||||
loggedInChannelName={this.props.loggedInChannelName}
|
||||
loggedInChannelShortId={this.props.loggedInChannelShortId}
|
||||
publishInChannel={publishInChannel}
|
||||
selectedChannel={selectedChannel}
|
||||
loggedInChannelName={loggedInChannelName}
|
||||
loggedInChannelShortId={loggedInChannelShortId}
|
||||
/>
|
||||
|
||||
<input type="text" id="claim-name-input" className="input-text" name='claim' placeholder="your-url-here" onChange={this.handleInput} value={this.props.claim}/>
|
||||
{ (this.props.claim && !this.props.urlError) && <span id="input-success-claim-name" className="info-message--success span--absolute">{'\u2713'}</span> }
|
||||
{ this.props.urlError && <span id="input-success-channel-name" className="info-message--failure span--absolute">{'\u2716'}</span> }
|
||||
<input type='text' id='claim-name-input' className='input-text' name='claim' placeholder='your-url-here' onChange={this.handleInput} value={claim} />
|
||||
{ (claim && !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> }
|
||||
</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>
|
||||
);
|
||||
|
|
|
@ -2,9 +2,11 @@ import { combineReducers } from 'redux';
|
|||
import PublishReducer from 'reducers/publish';
|
||||
import ChannelReducer from 'reducers/channel';
|
||||
import ShowReducer from 'reducers/show';
|
||||
import SiteReducer from 'reducers/site';
|
||||
|
||||
export default combineReducers({
|
||||
channel: ChannelReducer,
|
||||
publish: PublishReducer,
|
||||
show : ShowReducer,
|
||||
site : SiteReducer,
|
||||
});
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import * as actions from 'constants/publish_action_types';
|
||||
import { LOGIN } from 'constants/publish_channel_select_states';
|
||||
const { publish } = require('../../config/speechConfig.js');
|
||||
|
||||
const initialState = {
|
||||
publishInChannel : false,
|
||||
|
@ -19,17 +20,19 @@ const initialState = {
|
|||
claim : '',
|
||||
metadata: {
|
||||
title : '',
|
||||
thumbnail : '',
|
||||
description: '',
|
||||
license : '',
|
||||
nsfw : false,
|
||||
},
|
||||
thumbnailChannel : publish.thumbnailChannel,
|
||||
thumbnailChannelId: publish.thumbnailChannelId,
|
||||
thumbnail : null,
|
||||
};
|
||||
|
||||
export default function (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.FILE_SELECTED:
|
||||
return Object.assign({}, state, {
|
||||
return Object.assign({}, initialState, { // note: clears to initial state
|
||||
file: action.data,
|
||||
});
|
||||
case actions.FILE_CLEAR:
|
||||
|
@ -66,6 +69,10 @@ export default function (state = initialState, action) {
|
|||
return Object.assign({}, state, {
|
||||
showMetadataInputs: action.data,
|
||||
});
|
||||
case actions.THUMBNAIL_NEW:
|
||||
return Object.assign({}, state, {
|
||||
thumbnail: action.data,
|
||||
});
|
||||
default:
|
||||
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 { watchNewChannelRequest, watchUpdateChannelClaims } from './show_channel';
|
||||
import { watchFileIsRequested } from './file';
|
||||
import { watchPublishStart } from './publish';
|
||||
|
||||
export default function * rootSaga () {
|
||||
yield all([
|
||||
|
@ -11,5 +12,6 @@ export default function * rootSaga () {
|
|||
watchNewChannelRequest(),
|
||||
watchUpdateChannelClaims(),
|
||||
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
|
||||
const state = yield select(selectShowState);
|
||||
if (state.requestList[requestId]) {
|
||||
console.log('that request already exists in the request list!');
|
||||
return null;
|
||||
}
|
||||
// get long id && add request to request list
|
||||
console.log(`getting asset long id ${name}`);
|
||||
let longId;
|
||||
try {
|
||||
({data: longId} = yield call(getLongClaimId, name, modifier));
|
||||
} catch (error) {
|
||||
console.log('error:', error);
|
||||
return yield put(onRequestError(error.message));
|
||||
}
|
||||
const assetKey = `a#${name}#${longId}`;
|
||||
|
@ -29,11 +26,9 @@ export function * newAssetRequest (action) {
|
|||
// is this an existing asset?
|
||||
// If this asset is in the asset list, it's already been fetched
|
||||
if (state.assetList[assetKey]) {
|
||||
console.log('that asset already exists in the asset list!');
|
||||
return null;
|
||||
}
|
||||
// get short Id
|
||||
console.log(`getting asset short id ${name} ${longId}`);
|
||||
let shortId;
|
||||
try {
|
||||
({data: shortId} = yield call(getShortId, name, longId));
|
||||
|
@ -41,7 +36,6 @@ export function * newAssetRequest (action) {
|
|||
return yield put(onRequestError(error.message));
|
||||
}
|
||||
// get asset claim data
|
||||
console.log(`getting asset claim data ${name} ${longId}`);
|
||||
let claimData;
|
||||
try {
|
||||
({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
|
||||
const state = yield select(selectShowState);
|
||||
if (state.requestList[requestId]) {
|
||||
console.log('that request already exists in the request list!');
|
||||
return null;
|
||||
}
|
||||
// get channel long id
|
||||
console.log('getting channel long id and short id');
|
||||
let longId, shortId;
|
||||
try {
|
||||
({ data: {longChannelClaimId: longId, shortChannelClaimId: shortId} } = yield call(getChannelData, channelName, channelId));
|
||||
|
@ -29,11 +27,9 @@ export function * newChannelRequest (action) {
|
|||
// is this an existing channel?
|
||||
// If this channel is in the channel list, it's already been fetched
|
||||
if (state.channelList[channelKey]) {
|
||||
console.log('that channel already exists in the channel list!');
|
||||
return null;
|
||||
}
|
||||
// get channel claims data
|
||||
console.log('getting channel claims data');
|
||||
let claimsData;
|
||||
try {
|
||||
({ data: claimsData } = yield call(getChannelClaims, channelName, longId, 1));
|
||||
|
|
|
@ -6,7 +6,6 @@ import { newChannelRequest } from 'sagas/show_channel';
|
|||
import lbryUri from 'utils/lbryUri';
|
||||
|
||||
function * parseAndUpdateIdentifierAndClaim (modifier, claim) {
|
||||
console.log('parseAndUpdateIdentifierAndClaim');
|
||||
// this is a request for an asset
|
||||
// claim will be an asset claim
|
||||
// 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));
|
||||
}
|
||||
function * parseAndUpdateClaimOnly (claim) {
|
||||
console.log('parseAndUpdateIdentifierAndClaim');
|
||||
// this could be a request for an asset or a channel page
|
||||
// claim could be an asset claim or a channel claim
|
||||
let isChannel, channelName, channelClaimId;
|
||||
|
@ -49,7 +47,6 @@ function * parseAndUpdateClaimOnly (claim) {
|
|||
}
|
||||
|
||||
export function * handleShowPageUri (action) {
|
||||
console.log('handleShowPageUri');
|
||||
const { identifier, claim } = action.data;
|
||||
if (identifier) {
|
||||
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 = {
|
||||
validateFile (file) {
|
||||
if (!file) {
|
||||
console.log('no file found');
|
||||
throw new Error('no file provided');
|
||||
}
|
||||
if (/'/.test(file.name)) {
|
||||
console.log('file name had apostrophe in it');
|
||||
throw new Error('apostrophes are not allowed in the file name');
|
||||
}
|
||||
// validate size and type
|
||||
|
@ -14,24 +12,20 @@ module.exports = {
|
|||
case 'image/jpg':
|
||||
case 'image/png':
|
||||
if (file.size > 10000000) {
|
||||
console.log('file was too big');
|
||||
throw new Error('Sorry, images are limited to 10 megabytes.');
|
||||
}
|
||||
break;
|
||||
case 'image/gif':
|
||||
if (file.size > 50000000) {
|
||||
console.log('file was too big');
|
||||
throw new Error('Sorry, GIFs are limited to 50 megabytes.');
|
||||
if (file.size > 30000000) {
|
||||
throw new Error('Sorry, GIFs are limited to 30 megabytes.');
|
||||
}
|
||||
break;
|
||||
case 'video/mp4':
|
||||
if (file.size > 50000000) {
|
||||
console.log('file was too big');
|
||||
throw new Error('Sorry, videos are limited to 50 megabytes.');
|
||||
if (file.size > 20000000) {
|
||||
throw new Error('Sorry, videos are limited to 20 megabytes.');
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.log('file type is not supported');
|
||||
throw new Error(file.type + ' is not a supported file type. Only, .jpeg, .png, .gif, and .mp4 files are currently supported.');
|
||||
}
|
||||
},
|
||||
|
|
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 { claimNameIsAvailable, checkChannelAvailability, publish } = require('../controllers/publishController.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 { sendGAAnonymousPublishTiming, sendGAChannelPublishTiming } = require('../helpers/googleAnalytics.js');
|
||||
const { authenticateUser } = require('../auth/authentication.js');
|
||||
|
@ -128,17 +128,17 @@ module.exports = (app) => {
|
|||
});
|
||||
// route to run a publish request on the daemon
|
||||
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 files:', files);
|
||||
logger.debug('api/claim/publish req.body:', body);
|
||||
logger.debug('api/claim/publish req.files:', files);
|
||||
// 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
|
||||
const publishStartTime = Date.now();
|
||||
// validate the body and files of the request
|
||||
try {
|
||||
// validateApiPublishRequest(body, files);
|
||||
({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);
|
||||
} catch (error) {
|
||||
return res.status(400).json({success: false, message: error.message});
|
||||
|
@ -148,13 +148,18 @@ module.exports = (app) => {
|
|||
authenticateUser(channelName, channelId, channelPassword, user),
|
||||
claimNameIsAvailable(name),
|
||||
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
|
||||
if (channelName && channelClaimId) {
|
||||
publishParams['channel_name'] = channelName;
|
||||
publishParams['channel_id'] = channelClaimId;
|
||||
}
|
||||
// publish the thumbnail
|
||||
if (thumbnailPublishParams) {
|
||||
publish(thumbnailPublishParams, thumbnailFileName, thumbnailFileType);
|
||||
}
|
||||
// publish the asset
|
||||
return publish(publishParams, fileName, fileType);
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue