Merge pull request #323 from lbryio/react-upload

React/Redux - publish component
This commit is contained in:
Bill Bittner 2018-01-25 13:43:19 -08:00 committed by GitHub
commit 2f0df2df8f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
82 changed files with 21151 additions and 1019 deletions

View file

@ -9,7 +9,9 @@
"start": "node speech.js",
"lint": "eslint .",
"fix": "eslint . --fix",
"precommit": "eslint ."
"precommit": "eslint .",
"babel": "babel",
"webpack": "webpack"
},
"repository": {
"type": "git",
@ -41,16 +43,27 @@
"nodemon": "^1.11.0",
"passport": "^0.4.0",
"passport-local": "^1.0.0",
"prop-types": "^15.6.0",
"react": "^16.2.0",
"react-dom": "^16.2.0",
"react-redux": "^5.0.6",
"redux": "^3.7.2",
"request": "^2.83.0",
"request-promise": "^4.2.2",
"sequelize": "^4.1.0",
"sequelize-cli": "^3.0.0-3",
"sleep": "^5.1.1",
"universal-analytics": "^0.4.13",
"whatwg-fetch": "^2.0.3",
"winston": "^2.3.1",
"winston-slack-webhook": "billbitt/winston-slack-webhook"
},
"devDependencies": {
"babel-core": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-2": "^6.24.1",
"chai": "^4.1.2",
"chai-http": "^3.0.0",
"eslint": "3.19.0",
@ -61,6 +74,8 @@
"eslint-plugin-react": "6.10.3",
"eslint-plugin-standard": "3.0.1",
"husky": "^0.13.4",
"mocha": "^4.0.1"
"mocha": "^4.0.1",
"redux-devtools": "^3.4.1",
"webpack": "^3.10.0"
}
}

View file

@ -37,7 +37,7 @@ module.exports = new PassportLocalStrategy(
.then(user => {
if (!user) {
// logger.debug('no user found');
return done(null, false, {message: 'Incorrect username or password.'});
return done(null, false, {message: 'Incorrect username or password'});
}
user.comparePassword(password, (passwordErr, isMatch) => {
if (passwordErr) {
@ -46,7 +46,7 @@ module.exports = new PassportLocalStrategy(
}
if (!isMatch) {
// logger.debug('incorrect password');
return done(null, false, {message: 'Incorrect username or password.'});
return done(null, false, {message: 'Incorrect username or password'});
}
logger.debug('Password was a match, returning User');
return returnUserAndChannelInfo(user)

View file

@ -483,32 +483,31 @@ table {
cursor: pointer;
}
#primary-dropzone-instructions, #dropbzone-dragover {
z-index: -1;
}
.position-absolute {
#dropzone-text-holder {
position: absolute;
top: 0px;
left: 0px;
height: 100%;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
#asset-preview-holder {
position: relative;
#dropzone-dragover, #dropzone-instructions {
padding: 1em;
}
#asset-preview {
display: block;
padding: 0.5rem;
width: calc(100% - 1rem);
width: 100%;
}
.dim {
opacity: 0.2;
}
/* Assets */
.asset {
max-width: 100%;
width: 100%;
}
#show-body #asset-boilerpate {

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 KiB

View file

@ -1,4 +0,0 @@
function sendAuthRequest (channelName, password, url) {
const params = `username=${channelName}&password=${password}`;
return postRequest(url, params);
}

View file

@ -1 +0,0 @@
const EMAIL_FORMAT = 'ERROR_EMAIL_FORMAT';

View file

@ -2,13 +2,11 @@
function showChannelCreateInProgressDisplay () {
const publishChannelForm = document.getElementById('publish-channel-form');
const inProgress = document.getElementById('channel-publish-in-progress');
const channelProgressBar = document.getElementById('create-channel-progress-bar');
publishChannelForm.hidden = true;
inProgress.hidden = false;
createProgressBar(channelProgressBar, 12);
}
// display the content that shows channle creation is done
// display the content that shows channel creation is done
function showChannelCreateDoneDisplay() {
const inProgress = document.getElementById('channel-publish-in-progress');
inProgress.hidden=true;
@ -22,27 +20,38 @@ function showChannelCreationError(msg) {
}
function publishNewChannel (event) {
const userName = document.getElementById('new-channel-name').value;
const username = document.getElementById('new-channel-name').value;
const password = document.getElementById('new-channel-password').value;
// prevent default so this script can handle submission
event.preventDefault();
// validate submission
validationFunctions.validateNewChannelSubmission(userName, password)
validationFunctions.validateNewChannelSubmission(username, password)
.then(() => {
showChannelCreateInProgressDisplay();
return sendAuthRequest(userName, password, '/signup') // post the request
// return sendAuthRequest(userName, password, '/signup') // post the request
return fetch('/signup', {
method: 'POST',
body: JSON.stringify({username, password}),
headers: new Headers({
'Content-Type': 'application/json'
}),
credentials: 'include',
})
.then(function(response) {
if (response.ok){
return response.json();
} else {
throw response;
}
})
.catch(function(error) {
throw error;
})
})
.then(result => {
setUserCookies(result.channelName, result.channelClaimId, result.shortChannelId);
.then(signupResult => {
console.log('signup success:', signupResult);
showChannelCreateDoneDisplay();
// if user is on the home page, update the needed elements without reloading
if (window.location.pathname === '/') {
replaceChannelOptionInPublishChannelSelect(result.channelName);
replaceChannelOptionInNavBarChannelSelect(result.channelName);
// if user is not on home page, redirect to home page
} else {
window.location = '/';
}
window.location = '/';
})
.catch(error => {
if (error.name === 'ChannelNameError' || error.name === 'ChannelPasswordError'){
@ -50,7 +59,7 @@ function publishNewChannel (event) {
validationFunctions.showError(channelNameErrorDisplayElement, error.message);
} else {
console.log('signup failure:', error);
showChannelCreationError('Unfortunately, we encountered an error while creating your channel. Please let us know in slack!');
showChannelCreationError('Unfortunately, we encountered an error while creating your channel. Please let us know in slack!', error);
}
})
}

View file

@ -1,53 +0,0 @@
function drop_handler(event) {
event.preventDefault();
// if dropped items aren't files, reject them
var dt = event.dataTransfer;
if (dt.items) {
if (dt.items[0].kind == 'file') {
var droppedFile = dt.items[0].getAsFile();
publishFileFunctions.previewAndStageFile(droppedFile);
}
}
}
function dragover_handler(event) {
event.preventDefault();
}
function dragend_handler(event) {
var dt = event.dataTransfer;
if (dt.items) {
for (var i = 0; i < dt.items.length; i++) {
dt.items.remove(i);
}
} else {
event.dataTransfer.clearData();
}
}
function dragenter_handler(event) {
var thisDropzone = document.getElementById(event.target.id);
thisDropzone.setAttribute('class', 'dropzone dropzone--drag-over row row--margined row--padded row--tall flex-container--column flex-container--center-center');
thisDropzone.firstElementChild.setAttribute('class', 'hidden');
thisDropzone.lastElementChild.setAttribute('class', '');
}
function dragexit_handler(event) {
var thisDropzone = document.getElementById(event.target.id);
thisDropzone.setAttribute('class', 'dropzone row row--tall row--margined row--padded flex-container--column flex-container--center-center');
thisDropzone.firstElementChild.setAttribute('class', '');
thisDropzone.lastElementChild.setAttribute('class', 'hidden');
}
function preview_onmouseenter_handler () {
document.getElementById('asset-preview-dropzone-instructions').setAttribute('class', 'flex-container--column flex-container--center-center position-absolute');
document.getElementById('asset-preview').style.opacity = 0.2;
}
function preview_onmouseleave_handler () {
document.getElementById('asset-preview-dropzone-instructions').setAttribute('class', 'hidden');
document.getElementById('asset-preview').style.opacity = 1;
}

View file

@ -1,160 +1,3 @@
function getRequest (url) {
return new Promise((resolve, reject) => {
let xhttp = new XMLHttpRequest();
xhttp.open('GET', url, true);
xhttp.responseType = 'json';
xhttp.onreadystatechange = () => {
if (xhttp.readyState == 4 ) {
if ( xhttp.status == 200) {
resolve(xhttp.response);
} else if (xhttp.status == 401) {
reject('Wrong channel name or password');
} else {
reject('request failed with status:' + xhttp.status);
};
}
};
xhttp.send();
})
}
function postRequest (url, params) {
return new Promise((resolve, reject) => {
let xhttp = new XMLHttpRequest();
xhttp.open('POST', url, true);
xhttp.responseType = 'json';
xhttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhttp.onreadystatechange = () => {
if (xhttp.readyState == 4 ) {
if ( xhttp.status == 200) {
resolve(xhttp.response);
} else if (xhttp.status == 401) {
reject( new AuthenticationError('Wrong channel name or password'));
} else {
reject('request failed with status:' + xhttp.status);
};
}
};
xhttp.send(params);
})
}
function toggleSection(event){
event.preventDefault();
var dataSet = event.target.dataset;
var status = dataSet.open;
var masterElement = document.getElementById(event.target.id||event.srcElement.id);
var slaveElement = document.getElementById(dataSet.slaveelementid);
var closedLabel = dataSet.closedlabel;
var openLabel = dataSet.openlabel;
if (status === "false") {
slaveElement.hidden = false;
masterElement.innerText = openLabel;
masterElement.dataset.open = "true";
} else {
slaveElement.hidden = true;
masterElement.innerText = closedLabel;
masterElement.dataset.open = "false";
}
}
function createProgressBar(element, size){
var x = 0;
var adder = 1;
// create the bar holder & place it
var barHolder = document.createElement('p');
for (var i = 0; i < size; i++) {
const bar = document.createElement('span');
bar.innerText = '| ';
bar.setAttribute('class', 'progress-bar progress-bar--inactive');
barHolder.appendChild(bar);
}
element.appendChild(barHolder);
// get the bars
const bars = document.getElementsByClassName('progress-bar');
// function to update the bars' classes
function updateOneBar(){
// update the appropriate bar
if (x > -1 && x < size){
if (adder === 1){
bars[x].setAttribute('class', 'progress-bar progress-bar--active');
} else {
bars[x].setAttribute('class', 'progress-bar progress-bar--inactive');
}
}
// set x
if (x === size){
adder = -1;
} else if ( x === -1){
adder = 1;
}
// update the adder
x += adder;
};
// start updater
setInterval(updateOneBar, 300);
}
function setCookie(key, value) {
document.cookie = `${key}=${value}`;
}
function getCookie(cname) {
const name = cname + "=";
const decodedCookie = decodeURIComponent(document.cookie);
const ca = decodedCookie.split(';');
for(let i = 0; i <ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
function checkCookie() {
const channelName = getCookie("channel_name");
if (channelName != "") {
console.log(`cookie found for ${channelName}`);
} else {
console.log('no channel_name cookie found');
}
}
function clearCookie(name) {
document.cookie = `${name}=; expires=Thu, 01-Jan-1970 00:00:01 GMT;`;
}
function setUserCookies(channelName, channelClaimId, shortChannelId) {
setCookie('channel_name', channelName)
setCookie('channel_claim_id', channelClaimId);
setCookie('short_channel_id', shortChannelId);
}
function clearUserCookies() {
clearCookie('channel_name')
clearCookie('channel_claim_id');
clearCookie('short_channel_id');
}
function copyToClipboard(event){
var elementToCopy = event.target.dataset.elementtocopy;
var element = document.getElementById(elementToCopy);
var errorElement = 'input-error-copy-text' + elementToCopy;
element.select();
try {
document.execCommand('copy');
} catch (err) {
validationFunctions.showError(errorElement, 'Oops, unable to copy');
}
}
// Create new error objects, that prototypically inherit from the Error constructor
function FileError(message) {
this.name = 'FileError';

View file

@ -1,67 +1,41 @@
function replaceChannelOptionInPublishChannelSelect(loggedInChannel) {
// remove the old channel option
const oldChannel = document.getElementById('publish-channel-select-channel-option')
if (oldChannel){
oldChannel.parentNode.removeChild(oldChannel);
}
// create new channel option
const newChannelOption = document.createElement('option');
newChannelOption.setAttribute('value', loggedInChannel);
newChannelOption.setAttribute('id', 'publish-channel-select-channel-option');
newChannelOption.setAttribute('selected', '');
newChannelOption.innerText = loggedInChannel;
// add the new option
const channelSelect = document.getElementById('channel-name-select');
channelSelect.insertBefore(newChannelOption, channelSelect.firstChild);
// carry out channel selection
toggleSelectedChannel(loggedInChannel);
}
function replaceChannelOptionInNavBarChannelSelect (loggedInChannel) {
// remove the old channel option
const oldChannel = document.getElementById('nav-bar-channel-select-channel-option');
if (oldChannel){
oldChannel.parentNode.removeChild(oldChannel);
}
// create new channel option & select it
const newChannelOption = document.createElement('option');
newChannelOption.setAttribute('value', loggedInChannel);
newChannelOption.setAttribute('id', 'nav-bar-channel-select-channel-option');
newChannelOption.setAttribute('selected', '');
newChannelOption.innerText = loggedInChannel;
// add the new option
const channelSelect = document.getElementById('nav-bar-channel-select');
channelSelect.style.display = 'inline-block';
channelSelect.insertBefore(newChannelOption, channelSelect.firstChild);
// hide login
const navBarLoginLink = document.getElementById('nav-bar-login-link');
navBarLoginLink.style.display = 'none';
}
function loginToChannel (event) {
const userName = document.getElementById('channel-login-name-input').value;
const username = document.getElementById('channel-login-name-input').value;
const password = document.getElementById('channel-login-password-input').value;
// prevent default
event.preventDefault()
validationFunctions.validateNewChannelLogin(userName, password)
validationFunctions.validateNewChannelLogin(username, password)
.then(() => {
return sendAuthRequest(userName, password, '/login')
return fetch('/login', {
method: 'POST',
body: JSON.stringify({username, password}),
headers: new Headers({
'Content-Type': 'application/json'
}),
credentials: 'include',
})
.then(function(response) {
console.log(response);
if (response.ok){
return response.json();
} else {
throw response;
}
})
.catch(function(error) {
throw error;
})
})
.then(result => {
setUserCookies(result.channelName, result.channelClaimId, result.shortChannelId);
// if user is on the home page, update the needed elements without reloading
if (window.location.pathname === '/') {
replaceChannelOptionInPublishChannelSelect(result.channelName);
replaceChannelOptionInNavBarChannelSelect(result.channelName);
// if user is not on home page, redirect to home page
} else {
.then(function({success, message}) {
if (success) {
window.location = '/';
} else {
throw new Error(message);
}
})
.catch(error => {
const loginErrorDisplayElement = document.getElementById('login-error-display-element');
if (error.name){
if (error.message){
validationFunctions.showError(loginErrorDisplayElement, error.message);
} else {
validationFunctions.showError(loginErrorDisplayElement, 'There was an error logging into your channel');

View file

@ -1,15 +0,0 @@
function toggleNavBarSelection (value) {
const selectedOption = value;
if (selectedOption === 'LOGOUT') {
// remove session cookies
clearUserCookies();
// send logout request to server
window.location.href = '/logout';
} else if (selectedOption === 'VIEW') {
// get channel info
const channelName = getCookie('channel_name');
const channelClaimId = getCookie('channel_claim_id');
// redirect to channel page
window.location.href = `/${channelName}:${channelClaimId}`;
}
}

View file

@ -1,234 +0,0 @@
var stagedFiles = null;
const publishFileFunctions = {
triggerFileChooser: function (fileInputId) {
document.getElementById(fileInputId).click();
},
cancelPublish: function () {
window.location.href = '/';
},
previewAndStageFile: function (selectedFile) {
const fileSelectionInputError = document.getElementById('input-error-file-selection');
// When a file is selected for publish, validate that file and
// stage it so it will be ready when the publish button is clicked
try {
validationFunctions.validateFile(selectedFile); // validate the file's name, type, and size
} catch (error) {
validationFunctions.showError(fileSelectionInputError, error.message);
return;
}
// set image preview, if an image was provided
this.setImagePreview(selectedFile);
// hide the primary drop zone
this.hidePrimaryDropzone();
// set the name input value to the image name if none is set yet
this.updateClaimNameInputWithFileName(selectedFile);
// store the selected file for upload
stagedFiles = [selectedFile];
},
hidePrimaryDropzone: function () {
const primaryDropzone = document.getElementById('primary-dropzone');
const publishForm = document.getElementById('publish-form');
primaryDropzone.setAttribute('class', 'hidden');
publishForm.setAttribute('class', 'row')
},
updateClaimNameInputWithFileName: function (selectedFile) {
const nameInput = document.getElementById('claim-name-input');
if (nameInput.value === "") {
var filename = selectedFile.name.substring(0, selectedFile.name.indexOf('.'))
nameInput.value = validationFunctions.cleanseClaimName(filename);
validationFunctions.checkClaimName(nameInput.value);
}
},
setImagePreview: function (selectedFile) {
const assetPreview = document.getElementById('asset-preview-target');
const thumbnailInput = document.getElementById('claim-thumbnail-input');
const thumbnailInputTool = document.getElementById('publish-thumbnail');
if (selectedFile.type !== 'video/mp4') {
const previewReader = new FileReader();
if (selectedFile.type === 'image/gif') {
assetPreview.innerHTML = `<p>loading preview...</p>`
}
previewReader.readAsDataURL(selectedFile);
previewReader.onloadend = function () {
assetPreview.innerHTML = '<img id="asset-preview" src="' + previewReader.result + '" alt="image preview"/>';
};
// clear & hide the thumbnail selection input
thumbnailInput.value = '';
thumbnailInputTool.hidden = true;
} else {
assetPreview.innerHTML = `<img id="asset-preview" src="/assets/img/video_thumb_default.png"/>`;
// clear & show the thumbnail selection input
thumbnailInput.value = '';
thumbnailInputTool.hidden = false;
}
},
returnNullOrChannel: function () {
const channelRadio = document.getElementById('channel-radio');
if (channelRadio.checked) {
const channelInput = document.getElementById('channel-name-select');
return channelInput.value.trim();
}
return null;
},
createMetadata: function() {
const nameInput = document.getElementById('claim-name-input');
const titleInput = document.getElementById('publish-title');
const descriptionInput = document.getElementById('publish-description');
const licenseInput = document.getElementById('publish-license');
const nsfwInput = document.getElementById('publish-nsfw');
const thumbnailInput = document.getElementById('claim-thumbnail-input');
const channelName = this.returnNullOrChannel();
let metadata = {
name: nameInput.value.trim(),
title: titleInput.value.trim(),
description: descriptionInput.value.trim(),
license: licenseInput.value.trim(),
nsfw: nsfwInput.checked,
type: stagedFiles[0].type,
thumbnail: thumbnailInput.value.trim(),
};
if (channelName) {
metadata['channelName'] = channelName;
}
return metadata;
},
appendDataToFormData: function (file, metadata) {
var fd = new FormData();
fd.append('file', file)
for (var key in metadata) {
if (metadata.hasOwnProperty(key)) {
console.log(key, metadata[key]);
fd.append(key, metadata[key]);
}
}
return fd;
},
publishFile: function (file, metadata) {
var uri = "/api/claim-publish";
var xhr = new XMLHttpRequest();
var fd = this.appendDataToFormData(file, metadata);
var that = this;
xhr.upload.addEventListener("loadstart", function(e) {
that.showUploadStartedMessage();
})
xhr.upload.addEventListener("progress", function(e) {
if (e.lengthComputable) {
var percentage = Math.round((e.loaded * 100) / e.total);
console.log('progress:', percentage);
that.showUploadProgressMessage(percentage);
}
}, false);
xhr.upload.addEventListener("load", function(e){
console.log('loaded 100%');
that.showFilePublishUpdate("Your file has been loaded, and is now being published to the blockchain. Sit tight...")
}, false);
xhr.open("POST", uri, true);
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
console.log('publish response:', xhr.response)
if (xhr.status == 200) {
console.log('publish complete!');
that.showFilePublishComplete(JSON.parse(xhr.response).message);
} else if (xhr.status == 502){
that.showFilePublishFailure('Spee.ch was not able to get a response from the LBRY network.');
} else {
that.showFilePublishFailure(JSON.parse(xhr.response).message);
}
}
};
// Initiate a multipart/form-data upload
xhr.send(fd);
},
// Validate the publish submission and then trigger upload
publishStagedFile: function (event) {
event.preventDefault(); // prevent default so this script can handle submission
const metadata = this.createMetadata();
const that = this;
const fileSelectionInputError = document.getElementById('input-error-file-selection');
const claimNameError = document.getElementById('input-error-claim-name');
const channelSelectError = document.getElementById('input-error-channel-select');
const publishSubmitError = document.getElementById('input-error-publish-submit');
// validate, submit, and handle response
validationFunctions.validateFilePublishSubmission(stagedFiles, metadata)
.then( function() {
that.publishFile(stagedFiles[0], metadata);
})
.catch(error => {
if (error.name === 'FileError') {
validationFunctions.showError(fileSelectionInputError, error.message);
} else if (error.name === 'NameError') {
validationFunctions.showError(claimNameError, error.message);
} else if (error.name === 'ChannelNameError'){
console.log(error);
validationFunctions.showError(channelSelectError, error.message);
} else {
validationFunctions.showError(publishSubmitError, error.message);
}
return;
})
},
showUploadStartedMessage: function (){
console.log('starting upload');
// hide the publish tool
this.hidePublishTools();
// show the progress status and animation
this.showPublishStatus();
this.showPublishProgressBar();
},
showUploadProgressMessage: function (percentage){
this.updatePublishStatus('<p>File is loading to server</p>');
this.updateUploadPercent('<p class="blue">' + percentage + '% </p>');
},
showFilePublishUpdate: function (msg) {
this.updatePublishStatus('<p>' + msg + '</p>');
this.updateUploadPercent('<p>Curious what magic is happening here? <a class="link--primary" target="blank" href="https://lbry.io/faq/what-is-lbry">Learn more.</a></p>');
},
showFilePublishFailure: function (msg){
this.updatePublishStatus('<p>Something went wrong...</p><p><strong>' + msg + '</strong></p><p>For help, post the above error text in the #speech channel on the <a class="link--primary" href="https://discord.gg/YjYbwhS" target="_blank">lbry discord</a>');
this.hidePublishProgressBar();
this.hideUploadPercent();
},
showFilePublishComplete: function (msg) {
console.log('Publish complete!');
const showUrl = msg.lbryTx.claim_id + "/" + msg.name;
// update status
this.updatePublishStatus('<p>Your publish is complete! You are being redirected to it now.</p>');
this.updateUploadPercent('<p><a class="link--primary" target="_blank" href="' + showUrl + '">If you do not get redirected, click here.</a></p>')
// redirect the user
window.location.href = showUrl;
},
hidePublishTools: function () {
const publishFormWrapper = document.getElementById('publish-form');
publishFormWrapper.setAttribute('class', 'hidden');
},
// publish status functions
showPublishStatus: function () {
const publishStatus = document.getElementById('publish-status');
publishStatus.setAttribute('class', 'row row--tall flex-container--column flex-container--center-center');
},
updatePublishStatus: function (msg){
const publishUpdate = document.getElementById('publish-update');
publishUpdate.innerHTML = msg;
},
// progress bar functions
showPublishProgressBar: function (){
const publishProgressBar = document.getElementById('publish-progress-bar');
createProgressBar(publishProgressBar, 12);
},
hidePublishProgressBar: function (){
const publishProgressBar = document.getElementById('publish-progress-bar');
publishProgressBar.hidden = true;
},
// upload percent functions
updateUploadPercent: function (msg){
const uploadPercent = document.getElementById('upload-percent');
uploadPercent.innerHTML = msg;
},
hideUploadPercent: function (){
const uploadPercent = document.getElementById('upload-percent');
uploadPercent.hidden = true;
},
}

View file

@ -1,53 +1,5 @@
// validation function which checks the proposed file's type, size, and name
const validationFunctions = {
validateFile: function (file) {
if (!file) {
console.log('no file found');
throw new Error('no file provided');
}
if (/'/.test(file.name)) {
console.log('file name had apostrophe in it');
throw new Error('apostrophes are not allowed in the file name');
}
// validate size and type
switch (file.type) {
case 'image/jpeg':
case 'image/jpg':
case 'image/png':
if (file.size > 10000000) {
console.log('file was too big');
throw new Error('Sorry, images are limited to 10 megabytes.');
}
break;
case 'image/gif':
if (file.size > 50000000) {
console.log('file was too big');
throw new Error('Sorry, .gifs are limited to 50 megabytes.');
}
break;
case 'video/mp4':
if (file.size > 50000000) {
console.log('file was too big');
throw new Error('Sorry, videos are limited to 50 megabytes.');
}
break;
default:
console.log('file type is not supported');
throw new Error(file.type + ' is not a supported file type. Only, .jpeg, .png, .gif, and .mp4 files are currently supported.')
}
},
// validation function that checks to make sure the claim name is valid
validateClaimName: function (name) {
// ensure a name was entered
if (name.length < 1) {
throw new NameError("You must enter a name for your url");
}
// validate the characters in the 'name' field
const invalidCharacters = /[^A-Za-z0-9,-]/g.exec(name);
if (invalidCharacters) {
throw new NameError('"' + invalidCharacters + '" characters are not allowed');
}
},
validateChannelName: function (name) {
name = name.substring(name.indexOf('@') + 1);
// ensure a name was entered
@ -65,15 +17,21 @@ const validationFunctions = {
throw new ChannelPasswordError("You must enter a password for you channel");
}
},
cleanseClaimName: function (name) {
name = name.replace(/\s+/g, '-'); // replace spaces with dashes
name = name.replace(/[^A-Za-z0-9-]/g, ''); // remove all characters that are not A-Z, a-z, 0-9, or '-'
return name;
},
// validation functions to check claim & channel name eligibility as the inputs change
isChannelNameAvailable: function (name) {
return this.isNameAvailable(name, '/api/channel-is-available/');
},
isNameAvailable: function (name, apiUrl) {
console.log('isNameAvailable?', name);
const url = apiUrl + name;
return getRequest(url)
return fetch(url)
.then(function (response) {
return response.json();
})
.catch(error => {
console.log('isNameAvailable error', error);
throw error;
})
},
showError: function (errorDisplay, errorMsg) {
errorDisplay.hidden = false;
@ -91,90 +49,35 @@ const validationFunctions = {
successElement.hidden = true;
successElement.innerHTML = "";
},
checkAvailability: function (name, successDisplayElement, errorDisplayElement, validateName, errorMessage, apiUrl) {
checkChannelName: function (name) {
var successDisplayElement = document.getElementById('input-success-channel-name');
var errorDisplayElement = document.getElementById('input-error-channel-name');
var channelName = `@${name}`;
var that = this;
try {
// check to make sure the characters are valid
validateName(name);
that.validateChannelName(channelName);
// check to make sure it is available
that.isNameAvailable(name, apiUrl)
.then(function (result) {
if (result === true) {
that.hideError(errorDisplayElement);
that.showSuccess(successDisplayElement)
} else {
that.hideSuccess(successDisplayElement);
that.showError(errorDisplayElement, errorMessage);
}
})
.catch(error => {
that.hideSuccess(successDisplayElement);
that.showError(errorDisplayElement, error.message);
});
that.isChannelNameAvailable(channelName)
.then(function(isAvailable){
console.log('isChannelNameAvailable:', isAvailable);
if (isAvailable) {
that.hideError(errorDisplayElement);
that.showSuccess(successDisplayElement)
} else {
that.hideSuccess(successDisplayElement);
that.showError(errorDisplayElement, 'Sorry, that name is already taken');
}
})
.catch(error => {
that.hideSuccess(successDisplayElement);
that.showError(errorDisplayElement, error.message);
});
} catch (error) {
that.hideSuccess(successDisplayElement);
that.showError(errorDisplayElement, error.message);
}
},
checkClaimName: function (name) {
const successDisplayElement = document.getElementById('input-success-claim-name');
const errorDisplayElement = document.getElementById('input-error-claim-name');
this.checkAvailability(name, successDisplayElement, errorDisplayElement, this.validateClaimName, 'Sorry, that ending is already taken', '/api/claim-is-available/');
},
checkChannelName: function (name) {
const successDisplayElement = document.getElementById('input-success-channel-name');
const errorDisplayElement = document.getElementById('input-error-channel-name');
name = `@${name}`;
this.checkAvailability(name, successDisplayElement, errorDisplayElement, this.validateChannelName, 'Sorry, that name is already taken', '/api/channel-is-available/');
},
// validation function which checks all aspects of the publish submission
validateFilePublishSubmission: function (stagedFiles, metadata) {
const channelName = metadata.channelName;
const claimName = metadata.name;
var that = this;
return new Promise(function (resolve, reject) {
// 1. make sure 1 file was staged
if (!stagedFiles) {
reject(new FileError("Please select a file"));
return;
} else if (stagedFiles.length > 1) {
reject(new FileError("Only one file is allowed at a time"));
return;
}
// 2. validate the file's name, type, and size
try {
that.validateFile(stagedFiles[0]);
} catch (error) {
reject(error);
return;
}
// 3. validate that a channel was chosen
if (channelName === 'new' || channelName === 'login') {
reject(new ChannelNameError("Please log in to a channel"));
return;
}
;
// 4. validate the claim name
try {
that.validateClaimName(claimName);
} catch (error) {
reject(error);
return;
}
// if all validation passes, check availability of the name (note: do we need to re-validate channel name vs. credentials as well?)
return that.isNameAvailable(claimName, '/api/claim-is-available/')
.then(result => {
if (result) {
resolve();
} else {
reject(new NameError('Sorry, that ending is already taken'));
}
})
.catch(error => {
reject(error);
});
});
},
// validation function which checks all aspects of a new channel submission
validateNewChannelSubmission: function (userName, password) {
const channelName = `@${userName}`;
@ -193,16 +96,15 @@ const validationFunctions = {
return reject(error);
}
// 3. if all validation passes, check availability of the name
that.isNameAvailable(channelName, '/api/channel-is-available/') // validate the availability
.then(function(result) {
if (result) {
that.isChannelNameAvailable(channelName)
.then(function(isAvailable) {
if (isAvailable) {
resolve();
} else {
reject(new ChannelNameError('Sorry, that name is already taken'));
}
})
.catch(function(error) {
console.log('error evaluating channel name availability', error);
reject(error);
});
});

19012
public/bundle/bundle.js Normal file

File diff suppressed because it is too large Load diff

12
react/actions/channel.js Normal file
View file

@ -0,0 +1,12 @@
import * as actions from 'constants/channel_action_types';
// export action creators
export function updateLoggedInChannel (name, shortId, longId) {
return {
type: actions.CHANNEL_UPDATE,
name,
shortId,
longId,
};
};

67
react/actions/publish.js Normal file
View file

@ -0,0 +1,67 @@
import * as actions from 'constants/publish_action_types';
// export action creators
export function selectFile (file) {
return {
type: actions.FILE_SELECTED,
file: file,
};
};
export function clearFile () {
return {
type: actions.FILE_CLEAR,
};
};
export function updateMetadata (name, value) {
return {
type: actions.METADATA_UPDATE,
name,
value,
};
};
export function updateClaim (value) {
return {
type: actions.CLAIM_UPDATE,
value,
};
};
export function setPublishInChannel (channel) {
return {
type: actions.SET_PUBLISH_IN_CHANNEL,
channel,
};
};
export function updatePublishStatus (status, message) {
return {
type: actions.PUBLISH_STATUS_UPDATE,
status,
message,
};
};
export function updateError (name, value) {
return {
type: actions.ERROR_UPDATE,
name,
value,
};
};
export function updateSelectedChannel (value) {
return {
type: actions.SELECTED_CHANNEL_UPDATE,
value,
};
};
export function toggleMetadataInputs (value) {
return {
type: actions.TOGGLE_METADATA_INPUTS,
value,
};
};

26
react/app.js Normal file
View file

@ -0,0 +1,26 @@
import React from 'react';
import ReactDOM from 'react-dom';
import {Provider} from 'react-redux';
import {createStore} from 'redux';
import Reducer from 'reducers';
import Publish from 'containers/PublishTool';
import NavBar from 'containers/NavBar';
let store = createStore(
Reducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
ReactDOM.render(
<Provider store={store}>
<NavBar />
</Provider>,
document.getElementById('react-nav-bar')
)
ReactDOM.render(
<Provider store={store}>
<Publish />
</Provider>,
document.getElementById('react-publish-tool')
)

View file

@ -0,0 +1,7 @@
import React from 'react';
const ActiveStatusBar = () => {
return <span className="progress-bar progress-bar--active">| </span>;
}
export default ActiveStatusBar;

View file

@ -0,0 +1,36 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
class ExpandingTextarea extends Component {
componentDidMount () {
this.adjustTextarea({});
}
render () {
const { onChange, ...rest } = this.props;
return (
<textarea
{ ...rest }
ref={x => this.el = x}
onChange={ this._handleChange.bind(this) }
/>
);
}
_handleChange (event) {
const { onChange } = this.props;
if (onChange) onChange(event);
this.adjustTextarea(event);
}
adjustTextarea ({ target = this.el }) {
target.style.height = 0;
target.style.height = `${target.scrollHeight}px`;
}
}
ExpandingTextarea.propTypes = {
onChange: PropTypes.func,
};
export default ExpandingTextarea;

View file

@ -0,0 +1,7 @@
import React from 'react';
const InactiveStatusBar = () => {
return <span className="progress-bar progress-bar--inactive">| </span>;
}
export default InactiveStatusBar;

View file

@ -0,0 +1,28 @@
import React from 'react';
function Logo () {
return (
<svg version="1.1" id="Layer_1" x="0px" y="0px" height="24px" viewBox="0 0 80 31" enableBackground="new 0 0 80 31" className="nav-bar-logo">
<a href="/">
<title>Logo</title>
<desc>Spee.ch logo</desc>
<g id="About">
<g id="Publish-Form-V2-_x28_filled_x29_" transform="translate(-42.000000, -23.000000)">
<g id="Group-17" transform="translate(42.000000, 22.000000)">
<text transform="matrix(1 0 0 1 0 20)" fontSize="25" fontFamily="Roboto">Spee&lt;h</text>
<g id="Group-16" transform="translate(0.000000, 30.000000)">
<path id="Line-8" fill="none" stroke="#09F911" strokeWidth="1" strokeLinecap="square" d="M0.5,1.5h15"/>
<path id="Line-8-Copy" fill="none" stroke="#029D74" strokeWidth="1" strokeLinecap="square" d="M16.5,1.5h15"/>
<path id="Line-8-Copy-2" fill="none" stroke="#E35BD8" strokeWidth="1" strokeLinecap="square" d="M32.5,1.5h15"/>
<path id="Line-8-Copy-3" fill="none" stroke="#4156C5" strokeWidth="1" strokeLinecap="square" d="M48.5,1.5h15"/>
<path id="Line-8-Copy-4" fill="none" stroke="#635688" strokeWidth="1" strokeLinecap="square" d="M64.5,1.5h15"/>
</g>
</g>
</g>
</g>
</a>
</svg>
);
};
export default Logo;

View file

@ -0,0 +1,13 @@
import React from 'react';
function NavBarChannelOptionsDropdown ({ channelName, handleSelection, VIEW, LOGOUT }) {
return (
<select type="text" id="nav-bar-channel-select" className="select select--arrow link--nav" onChange={handleSelection}>
<option id="nav-bar-channel-select-channel-option">{channelName}</option>
<option value={VIEW}>View</option>
<option value={LOGOUT}>Logout</option>
</select>
);
};
export default NavBarChannelOptionsDropdown;

View file

@ -0,0 +1,54 @@
import React from 'react';
import PropTypes from 'prop-types';
class Preview extends React.Component {
constructor (props) {
super(props);
this.state = {
imgSource : '',
defaultThumbnail: '/assets/img/video_thumb_default.png',
};
this.previewFile = this.previewFile.bind(this);
}
componentDidMount () {
this.previewFile(this.props.file);
}
componentWillReceiveProps (newProps) {
if (newProps.file !== this.props.file) {
this.previewFile(newProps.file);
}
if (newProps.thumbnail !== this.props.thumbnail) {
this.setState({imgSource: (newProps.thumbnail || this.state.defaultThumbnail)});
}
}
previewFile (file) {
const that = this;
if (file.type !== 'video/mp4') {
const previewReader = new FileReader();
previewReader.readAsDataURL(file);
previewReader.onloadend = function () {
that.setState({imgSource: previewReader.result});
};
} else {
that.setState({imgSource: (this.props.thumbnail || this.state.defaultThumbnail)});
}
}
render () {
return (
<img
id="asset-preview"
src={this.state.imgSource}
className={this.props.dimPreview ? 'dim' : ''}
alt="publish preview"
/>
);
}
};
Preview.propTypes = {
dimPreview: PropTypes.bool.isRequired,
file : PropTypes.object.isRequired,
thumbnail : PropTypes.string,
};
export default Preview;

View file

@ -0,0 +1,76 @@
import React from 'react';
import PropTypes from 'prop-types';
import ActiveStatusBar from 'components/ActiveStatusBar';
import InactiveStatusBar from 'components/InactiveStatusBar';
class ProgressBar extends React.Component {
constructor (props) {
super(props);
this.state = {
bars : [],
index : 0,
incrementer: 1,
};
this.createBars = this.createBars.bind(this);
this.startProgressBar = this.startProgressBar.bind(this);
this.updateProgressBar = this.updateProgressBar.bind(this);
this.stopProgressBar = this.stopProgressBar.bind(this);
}
componentDidMount () {
this.createBars();
this.startProgressBar();
}
componentWillUnmount () {
this.stopProgressBar();
}
createBars () {
const bars = [];
for (let i = 0; i <= this.props.size; i++) {
bars.push({isActive: false});
}
this.setState({ bars });
}
startProgressBar () {
this.updateInterval = setInterval(this.updateProgressBar.bind(this), 300);
};
updateProgressBar () {
let index = this.state.index;
let incrementer = this.state.incrementer;
let bars = this.state.bars;
// flip incrementer if necessary, to stay in bounds
if ((index < 0) || (index > this.props.size)) {
incrementer = incrementer * -1;
index += incrementer;
}
// update the indexed bar
if (incrementer > 0) {
bars[index].isActive = true;
} else {
bars[index].isActive = false;
};
// increment index
index += incrementer;
// update state
this.setState({
bars,
incrementer,
index,
});
};
stopProgressBar () {
clearInterval(this.updateInterval);
};
render () {
return (
<div>
{this.state.bars.map((bar, index) => bar.isActive ? <ActiveStatusBar key={index} /> : <InactiveStatusBar key={index}/>)}
</div>
);
}
};
ProgressBar.propTypes = {
size: PropTypes.number.isRequired,
};
export default ProgressBar;

View file

@ -0,0 +1,52 @@
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 class="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;

View file

@ -0,0 +1,23 @@
import React from 'react';
import PropTypes from 'prop-types';
function UrlMiddle ({publishInChannel, selectedChannel, loggedInChannelName, loggedInChannelShortId}) {
if (publishInChannel) {
if (selectedChannel === loggedInChannelName) {
return <span id="url-channel" className="url-text--secondary">{loggedInChannelName}:{loggedInChannelShortId} /</span>;
}
return <span id="url-channel-placeholder" className="url-text--secondary tooltip">@channel<span
className="tooltip-text">Select a channel below</span> /</span>;
}
return (
<span id="url-no-channel-placeholder" className="url-text--secondary tooltip">xyz<span className="tooltip-text">This will be a random id</span> /</span>
);
}
UrlMiddle.propTypes = {
publishInChannel : PropTypes.bool.isRequired,
loggedInChannelName : PropTypes.string,
loggedInChannelShortId: PropTypes.string,
};
export default UrlMiddle;

View file

@ -0,0 +1 @@
export const CHANNEL_UPDATE = 'CHANNEL_UPDATE';

View file

@ -0,0 +1,9 @@
export const FILE_SELECTED = 'FILE_SELECTED';
export const FILE_CLEAR = 'FILE_CLEAR';
export const METADATA_UPDATE = 'METADATA_UPDATE';
export const CLAIM_UPDATE = 'CLAIM_UPDATE';
export const SET_PUBLISH_IN_CHANNEL = 'SET_PUBLISH_IN_CHANNEL';
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';

View file

@ -0,0 +1,2 @@
export const LOGIN = 'Existing';
export const CREATE = 'New';

View file

@ -0,0 +1,5 @@
export const LOAD_START = 'LOAD_START';
export const LOADING = 'LOADING';
export const PUBLISHING = 'PUBLISHING';
export const SUCCESS = 'SUCCESS';
export const FAILED = 'FAILED';

View file

@ -0,0 +1,15 @@
import { connect } from 'react-redux';
import { updateLoggedInChannel } from 'actions/channel';
import View from './view';
import {updateSelectedChannel} from '../../actions/publish';
const mapDispatchToProps = dispatch => {
return {
onChannelLogin: (name, shortId, longId) => {
dispatch(updateLoggedInChannel(name, shortId, longId));
dispatch(updateSelectedChannel(name));
},
};
};
export default connect(null, mapDispatchToProps)(View);

View file

@ -0,0 +1,168 @@
import React from 'react';
import ProgressBar from 'components/ProgressBar';
import request from 'utils/request';
class ChannelCreateForm extends React.Component {
constructor (props) {
super(props);
this.state = {
error : null,
channel : '',
password: '',
status : null,
};
this.cleanseChannelInput = this.cleanseChannelInput.bind(this);
this.handleChannelInput = this.handleChannelInput.bind(this);
this.handleInput = this.handleInput.bind(this);
this.updateIsChannelAvailable = this.updateIsChannelAvailable.bind(this);
this.checkIsChannelAvailable = this.checkIsChannelAvailable.bind(this);
this.checkIsPasswordProvided = this.checkIsPasswordProvided.bind(this);
this.makePublishChannelRequest = this.makePublishChannelRequest.bind(this);
this.createChannel = this.createChannel.bind(this);
}
cleanseChannelInput (input) {
input = input.replace(/\s+/g, '-'); // replace spaces with dashes
input = input.replace(/[^A-Za-z0-9-]/g, ''); // remove all characters that are not A-Z, a-z, 0-9, or '-'
return input;
}
handleChannelInput (event) {
let value = event.target.value;
value = this.cleanseChannelInput(value);
this.setState({channel: value});
if (value) {
this.updateIsChannelAvailable(value);
} else {
this.setState({error: 'Please enter a channel name'});
}
}
handleInput (event) {
const name = event.target.name;
const value = event.target.value;
this.setState({[name]: value});
}
updateIsChannelAvailable (channel) {
const that = this;
const channelWithAtSymbol = `@${channel}`;
request(`/api/channel-is-available/${channelWithAtSymbol}`)
.then(isAvailable => {
if (isAvailable) {
that.setState({'error': null});
} else {
that.setState({'error': 'That channel has already been claimed'});
}
})
.catch((error) => {
that.setState({'error': error.message});
});
}
checkIsChannelAvailable (channel) {
const channelWithAtSymbol = `@${channel}`;
return new Promise((resolve, reject) => {
request(`/api/channel-is-available/${channelWithAtSymbol}`)
.then(isAvailable => {
console.log('checkIsChannelAvailable result:', isAvailable);
if (!isAvailable) {
return reject(new Error('That channel has already been claimed'));
}
resolve();
})
.catch((error) => {
reject(error);
});
});
}
checkIsPasswordProvided () {
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();
});
}
makePublishChannelRequest (username, password) {
const params = {
method : 'POST',
body : JSON.stringify({username, password}),
headers: new Headers({
'Content-Type': 'application/json',
}),
credentials: 'include',
};
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!'));
});
});
}
createChannel (event) {
event.preventDefault();
const that = this;
this.checkIsPasswordProvided()
.then(() => {
return that.checkIsChannelAvailable(that.state.channel, that.state.password);
})
.then(() => {
that.setState({status: 'We are publishing your new channel. Sit tight...'});
return that.makePublishChannelRequest(that.state.channel, that.state.password);
})
.then(result => {
that.setState({status: null});
that.props.onChannelLogin(result.channelName, result.shortChannelId, result.channelClaimId);
})
.catch((error) => {
that.setState({'error': error.message, status: null});
});
}
render () {
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">
<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> }
</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>
</div>
</div>
<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}/>
</div>
)}
</div>
);
}
}
export default ChannelCreateForm;

View file

@ -0,0 +1,15 @@
import { connect } from 'react-redux';
import { updateLoggedInChannel } from 'actions/channel';
import View from './view';
import {updateSelectedChannel} from '../../actions/publish';
const mapDispatchToProps = dispatch => {
return {
onChannelLogin: (name, shortId, longId) => {
dispatch(updateLoggedInChannel(name, shortId, longId));
dispatch(updateSelectedChannel(name));
},
};
};
export default connect(null, mapDispatchToProps)(View);

View file

@ -0,0 +1,81 @@
import React from 'react';
import request from 'utils/request';
class ChannelLoginForm extends React.Component {
constructor (props) {
super(props);
this.state = {
error : null,
name : '',
password: '',
};
this.handleInput = this.handleInput.bind(this);
this.loginToChannel = this.loginToChannel.bind(this);
}
handleInput (event) {
const name = event.target.name;
const value = event.target.value;
this.setState({[name]: value});
}
loginToChannel (event) {
event.preventDefault();
const params = {
method : 'POST',
body : JSON.stringify({username: this.state.name, password: this.state.password}),
headers: new Headers({
'Content-Type': 'application/json',
}),
credentials: 'include',
}
const that = this;
request('login', params)
.then(({success, channelName, shortChannelId, channelClaimId, message}) => {
console.log('loginToChannel success:', success);
if (success) {
that.props.onChannelLogin(channelName, shortChannelId, channelClaimId);
} else {
that.setState({'error': message});
};
})
.catch(error => {
console.log('login error', error);
if (error.message) {
that.setState({'error': error.message});
} else {
that.setState({'error': error});
}
});
}
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}/>
</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>
</div>
</div>
<div className="row row--wide">
<button className="button--primary" onClick={this.loginToChannel}>Authenticate</button>
</div>
</form>
);
}
}
export default ChannelLoginForm;

View file

@ -0,0 +1,27 @@
import {connect} from 'react-redux';
import {setPublishInChannel, updateSelectedChannel, updateError} from 'actions/publish';
import View from './view.jsx';
const mapStateToProps = ({ channel, publish }) => {
return {
loggedInChannelName: channel.loggedInChannel.name,
publishInChannel : publish.publishInChannel,
selectedChannel : publish.selectedChannel,
channelError : publish.error.channel,
};
};
const mapDispatchToProps = dispatch => {
return {
onPublishInChannelChange: (value) => {
dispatch(updateError('channel', null));
dispatch(setPublishInChannel(value));
},
onChannelSelect: (value) => {
dispatch(updateError('channel', null));
dispatch(updateSelectedChannel(value));
},
};
}
export default connect(mapStateToProps, mapDispatchToProps)(View);

View file

@ -0,0 +1,58 @@
import React from 'react';
import ChannelLoginForm from 'containers/ChannelLoginForm';
import ChannelCreateForm from 'containers/ChannelCreateForm';
import * as states from 'constants/publish_channel_select_states';
class ChannelSelect extends React.Component {
constructor (props) {
super(props);
this.toggleAnonymousPublish = this.toggleAnonymousPublish.bind(this);
this.handleSelection = this.handleSelection.bind(this);
}
toggleAnonymousPublish (event) {
const value = event.target.value;
if (value === 'anonymous') {
this.props.onPublishInChannelChange(false);
} else {
this.props.onPublishInChannelChange(true);
}
}
handleSelection (event) {
const selectedOption = event.target.selectedOptions[0].value;
this.props.onChannelSelect(selectedOption);
}
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>
<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>
</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>
{ (this.props.selectedChannel === states.LOGIN) && <ChannelLoginForm /> }
{ (this.props.selectedChannel === states.CREATE) && <ChannelCreateForm /> }
</div>
)}
</div>
);
}
}
export default ChannelSelect;

View file

@ -0,0 +1,25 @@
import { connect } from 'react-redux';
import { selectFile, updateError } from 'actions/publish';
import View from './view';
const mapStateToProps = ({ publish }) => {
return {
file : publish.file,
thumbnail: publish.metadata.thumbnail,
fileError: publish.error.file,
};
};
const mapDispatchToProps = dispatch => {
return {
onFileSelect: (file) => {
dispatch(selectFile(file));
dispatch(updateError('publishSubmit', null));
},
onFileError: (value) => {
dispatch(updateError('file', value));
},
};
};
export default connect(mapStateToProps, mapDispatchToProps)(View);

View file

@ -0,0 +1,140 @@
import React from 'react';
import { validateFile } from 'utils/file';
import Preview from 'components/Preview';
class Dropzone extends React.Component {
constructor (props) {
super(props);
this.state = {
dragOver : false,
mouseOver : false,
dimPreview: false,
};
this.handleDrop = this.handleDrop.bind(this);
this.handleDragOver = this.handleDragOver.bind(this);
this.handleDragEnd = this.handleDragEnd.bind(this);
this.handleDragEnter = this.handleDragEnter.bind(this);
this.handleDragLeave = this.handleDragLeave.bind(this);
this.handleMouseEnter = this.handleMouseEnter.bind(this);
this.handleMouseLeave = this.handleMouseLeave.bind(this);
this.handleClick = this.handleClick.bind(this);
this.handleFileInput = this.handleFileInput.bind(this);
this.selectFile = this.selectFile.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') {
const droppedFile = dt.items[0].getAsFile();
this.selectFile(droppedFile);
}
}
}
handleDragOver (event) {
event.preventDefault();
}
handleDragEnd (event) {
var dt = event.dataTransfer;
if (dt.items) {
for (var i = 0; i < dt.items.length; i++) {
dt.items.remove(i);
}
} else {
event.dataTransfer.clearData();
}
}
handleDragEnter () {
this.setState({dragOver: true, dimPreview: true});
}
handleDragLeave () {
this.setState({dragOver: false, dimPreview: false});
}
handleMouseEnter () {
this.setState({mouseOver: true, dimPreview: true});
}
handleMouseLeave () {
this.setState({mouseOver: false, dimPreview: false});
}
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]);
}
selectFile (file) {
if (file) {
try {
validateFile(file); // validate the file's name, type, and size
} catch (error) {
return this.props.onFileError(error.message);
}
// stage it so it will be ready when the publish button is clicked
this.props.onFileError(null);
this.props.onFileSelect(file);
}
}
render () {
return (
<div className="row row--tall flex-container--column">
<form>
<input className="input-file" type="file" id="file_input" name="file_input" accept="video/*,image/*" onChange={this.handleFileInput} encType="multipart/form-data"/>
</form>
<div id="preview-dropzone" className={'row row--padded row--tall dropzone' + (this.state.dragOver ? ' dropzone--drag-over' : '')} onDrop={this.handleDrop} onDragOver={this.handleDragOver} onDragEnd={this.handleDragEnd} onDragEnter={this.handleDragEnter} onDragLeave={this.handleDragLeave} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} onClick={this.handleClick}>
{this.props.file ? (
<div>
<Preview
dimPreview={this.state.dimPreview}
file={this.props.file}
thumbnail={this.props.thumbnail}
/>
<div id="dropzone-text-holder" className={'flex-container--column flex-container--center-center'}>
{ this.state.dragOver ? (
<div id="dropzone-dragover">
<p className="blue">Drop it.</p>
</div>
) : (
null
)}
{ this.state.mouseOver ? (
<div id="dropzone-instructions">
<p className="info-message-placeholder info-message--failure" id="input-error-file-selection">{this.props.fileError}</p>
<p>Drag & drop image or video here to publish</p>
<p className="fine-print">OR</p>
<p className="blue--underlined">CHOOSE FILE</p>
</div>
) : (
null
)}
</div>
</div>
) : (
<div id="dropzone-text-holder" className={'flex-container--column flex-container--center-center'}>
{ this.state.dragOver ? (
<div id="dropzone-dragover">
<p className="blue">Drop it.</p>
</div>
) : (
<div id="dropzone-instructions">
<p className="info-message-placeholder info-message--failure" id="input-error-file-selection">{this.props.fileError}</p>
<p>Drag & drop image or video here to publish</p>
<p className="fine-print">OR</p>
<p className="blue--underlined">CHOOSE FILE</p>
</div>
)}
</div>
)}
</div>
</div>
);
}
};
export default Dropzone;

View file

@ -0,0 +1,23 @@
import { connect } from 'react-redux';
import { updateLoggedInChannel } from 'actions/channel';
import View from './view';
import {updateSelectedChannel} from '../../actions/publish';
const mapStateToProps = ({ channel }) => {
return {
channelName : channel.loggedInChannel.name,
channelShortId: channel.loggedInChannel.shortId,
channelLongId : channel.loggedInChannel.longId,
};
};
const mapDispatchToProps = dispatch => {
return {
onChannelLogin: (name, shortId, longId) => {
dispatch(updateLoggedInChannel(name, shortId, longId));
dispatch(updateSelectedChannel(name));
},
};
};
export default connect(mapStateToProps, mapDispatchToProps)(View);

View file

@ -0,0 +1,85 @@
import React from 'react';
import request from 'utils/request';
import Logo from 'components/Logo';
import NavBarChannelDropdown from 'components/NavBarChannelOptionsDropdown';
const VIEW = 'VIEW';
const LOGOUT = 'LOGOUT';
class NavBar extends React.Component {
constructor (props) {
super(props);
this.checkForLoggedInUser = this.checkForLoggedInUser.bind(this);
this.logoutUser = this.logoutUser.bind(this);
this.handleSelection = this.handleSelection.bind(this);
}
componentDidMount () {
// check to see if the user is already logged in
this.checkForLoggedInUser();
}
checkForLoggedInUser () {
// check for whether a channel is already logged in
const params = {
credentials: 'include',
}
request('/user', params)
.then(({success, message}) => {
if (success) {
this.props.onChannelLogin(message.channelName, message.shortChannelId, message.channelClaimId);
} else {
console.log('user was not logged in');
}
})
.catch(error => {
console.log('authenticate user errored:', error);
});
}
logoutUser () {
// send logout request to server
window.location.href = '/logout'; // NOTE: replace with a call to the server
}
handleSelection (event) {
console.log('handling selection', event);
const value = event.target.selectedOptions[0].value;
console.log('value', value);
switch (value) {
case LOGOUT:
this.logoutUser();
break;
case VIEW:
// redirect to channel page
window.location.href = `/${this.props.channelName}:${this.props.channelLongId}`;
break;
default:
break;
}
}
render () {
return (
<div className="row row--wide nav-bar">
<div className="row row--padded row--short flex-container--row flex-container--space-between-center">
<Logo />
<div className="nav-bar--center">
<span className="nav-bar-tagline">Open-source, decentralized image and video sharing.</span>
</div>
<div className="nav-bar--right">
<a className="nav-bar-link link--nav-active" href="/">Publish</a>
<a className="nav-bar-link link--nav" href="/about">About</a>
{ this.props.channelName ? (
<NavBarChannelDropdown
channelName={this.props.channelName}
handleSelection={this.handleSelection}
VIEW={VIEW}
LOGOUT={LOGOUT}
/>
) : (
<a id="nav-bar-login-link" className="nav-bar-link link--nav" href="/login">Channel</a>
)}
</div>
</div>
</div>
);
}
}
export default NavBar;

View file

@ -0,0 +1,47 @@
import {connect} from 'react-redux';
import {clearFile, selectFile, updateError, updatePublishStatus} from 'actions/publish';
import {updateLoggedInChannel} from 'actions/channel';
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 {
onFileSelect: (file) => {
dispatch(selectFile(file));
},
onFileClear: () => {
dispatch(clearFile());
},
onChannelLogin: (name, shortId, longId) => {
dispatch(updateLoggedInChannel(name, shortId, longId));
},
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);

View file

@ -0,0 +1,179 @@
import React from 'react';
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.validateChannelSelection = this.validateChannelSelection.bind(this);
this.validatePublishParams = this.validatePublishParams.bind(this);
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);
const that = this;
xhr.upload.addEventListener('loadstart', function () {
that.props.onPublishStatusChange(publishStates.LOAD_START, 'upload started');
});
xhr.upload.addEventListener('progress', function (e) {
if (e.lengthComputable) {
const percentage = Math.round((e.loaded * 100) / e.total);
console.log('progress:', percentage);
that.props.onPublishStatusChange(publishStates.LOADING, `${percentage}%`);
}
}, false);
xhr.upload.addEventListener('load', function () {
console.log('loaded 100%');
that.props.onPublishStatusChange(publishStates.PUBLISHING, null);
}, false);
xhr.open('POST', uri, true);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
console.log('publish response:', xhr.response);
if (xhr.status === 200) {
console.log('publish complete!');
const url = JSON.parse(xhr.response).message.url;
that.props.onPublishStatusChange(publishStates.SUCCESS, url);
window.location = url;
} else if (xhr.status === 502) {
that.props.onPublishStatusChange(publishStates.FAILED, 'Spee.ch was not able to get a response from the LBRY network.');
} else {
that.props.onPublishStatusChange(publishStates.FAILED, JSON.parse(xhr.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)) {
console.log('adding form data', key, metadata[key]);
fd.append(key, metadata[key]);
}
}
return fd;
}
publish () {
console.log('publishing file');
// publish the asset
const that = this;
this.validateChannelSelection()
.then(() => {
return that.validatePublishParams();
})
.then(() => {
const metadata = that.createMetadata();
// publish the claim
return that.makePublishRequest(that.props.file, metadata);
})
.then(() => {
that.props.onPublishStatusChange('publish request made');
})
.catch((error) => {
that.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 PublishForm;

View file

@ -0,0 +1,25 @@
import {connect} from 'react-redux';
import {updateMetadata, toggleMetadataInputs} from 'actions/publish';
import View from './view';
const mapStateToProps = ({ publish }) => {
return {
showMetadataInputs: publish.showMetadataInputs,
description : publish.metadata.description,
license : publish.metadata.license,
nsfw : publish.metadata.nsfw,
};
};
const mapDispatchToProps = dispatch => {
return {
onMetadataChange: (name, value) => {
dispatch(updateMetadata(name, value));
},
onToggleMetadataInputs: (value) => {
dispatch(toggleMetadataInputs(value));
},
};
};
export default connect(mapStateToProps, mapDispatchToProps)(View);

View file

@ -0,0 +1,74 @@
import React from 'react';
import ExpandingTextArea from 'components/ExpandingTextArea';
class PublishMetadataInputs extends React.Component {
constructor (props) {
super(props);
this.toggleShowInputs = this.toggleShowInputs.bind(this);
this.handleInput = this.handleInput.bind(this);
this.handleSelect = this.handleSelect.bind(this);
}
toggleShowInputs () {
this.props.onToggleMetadataInputs(!this.props.showMetadataInputs);
}
handleInput (event) {
const target = event.target;
const value = target.type === 'checkbox' ? target.checked : target.value;
const name = target.name;
this.props.onMetadataChange(name, value);
}
handleSelect (event) {
const name = event.target.name;
const selectedOption = event.target.selectedOptions[0].value;
this.props.onMetadataChange(name, selectedOption);
}
render () {
return (
<div id="publish-details" className="row row--padded row--no-top row--wide">
{this.props.showMetadataInputs && (
<div>
<div className="row row--no-top">
<div className="column column--3 column--med-10 align-content-top">
<label htmlFor="publish-license" className="label">Description:</label>
</div><div className="column column--7 column--sml-10">
<ExpandingTextArea
id="publish-description"
className="textarea textarea--primary textarea--full-width"
rows={1}
maxLength={2000}
style={{ maxHeight: 200 }}
name="description"
placeholder="Optional description"
value={this.props.description}
onChange={this.handleInput} />
</div>
</div>
<div className="row row--no-top">
<div className="column column--3 column--med-10">
<label htmlFor="publish-license" className="label">License:</label>
</div><div className="column column--7 column--sml-10">
<select type="text" name="license" id="publish-license" className="select select--primary" onChange={this.handleSelect}>
<option value=" ">Unspecified</option>
<option value="Public Domain">Public Domain</option>
<option value="Creative Commons">Creative Commons</option>
</select>
</div>
</div>
<div className="row row--no-top">
<div className="column column--3">
<label htmlFor="publish-nsfw" className="label">Mature:</label>
</div><div className="column column--7">
<input className="input-checkbox" type="checkbox" id="publish-nsfw" name="nsfw" value={this.props.nsfw} onChange={this.handleInput} />
</div>
</div>
</div>
)}
<a className="label link--primary" id="publish-details-toggle" href="#" onClick={this.toggleShowInputs}>{this.props.showMetadataInputs ? '[less]' : '[more]'}</a>
</div>
);
}
}
export default PublishMetadataInputs;

View file

@ -0,0 +1,19 @@
import {connect} from 'react-redux';
import {updateMetadata} from 'actions/publish';
import View from './view';
const mapStateToProps = ({ publish }) => {
return {
thumbnail: publish.metadata.thumbnail,
};
};
const mapDispatchToProps = dispatch => {
return {
onThumbnailChange: (name, value) => {
dispatch(updateMetadata(name, value));
},
};
};
export default connect(mapStateToProps, mapDispatchToProps)(View);

View file

@ -0,0 +1,83 @@
import React from 'react';
class PublishThumbnailInput extends React.Component {
constructor (props) {
super(props);
this.state = {
videoPreviewSrc: null,
thumbnailError : null,
thumbnailInput : '',
}
this.handleInput = this.handleInput.bind(this);
this.urlIsAnImage = this.urlIsAnImage.bind(this);
this.testImage = this.testImage.bind(this);
this.updateVideoThumb = this.updateVideoThumb.bind(this);
}
handleInput (event) {
const value = event.target.value;
this.setState({thumbnailInput: value});
}
urlIsAnImage (url) {
return (url.match(/\.(jpeg|jpg|gif|png)$/) != null);
}
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();
});
}
updateVideoThumb (event) {
const imageUrl = event.target.value;
const that = this;
if (this.urlIsAnImage(imageUrl)) {
this.testImage(imageUrl, 3000)
.then(() => {
console.log('thumbnail is a valid image');
that.props.onThumbnailChange('thumbnail', imageUrl);
that.setState({thumbnailError: null});
})
.catch(error => {
console.log('encountered an error loading thumbnail image url:', error);
that.props.onThumbnailChange('thumbnail', null);
that.setState({thumbnailError: 'That is an invalid image url'});
});
} else {
that.props.onThumbnailChange('thumbnail', null);
that.setState({thumbnailError: null});
}
}
render () {
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>
</div>
);
}
}
export default PublishThumbnailInput;

View file

@ -0,0 +1,19 @@
import {connect} from 'react-redux';
import {updateMetadata} from 'actions/publish';
import View from './view';
const mapStateToProps = ({ publish }) => {
return {
title: publish.metadata.title,
};
};
const mapDispatchToProps = dispatch => {
return {
onMetadataChange: (name, value) => {
dispatch(updateMetadata(name, value));
},
};
}
export default connect(mapStateToProps, mapDispatchToProps)(View);

View file

@ -0,0 +1,20 @@
import React from 'react';
class PublishTitleInput extends React.Component {
constructor (props) {
super(props);
this.handleInput = this.handleInput.bind(this);
}
handleInput (e) {
const name = e.target.name;
const value = e.target.value;
this.props.onMetadataChange(name, value);
}
render () {
return (
<input type="text" id="publish-title" className="input-text text--large input-text--full-width" name="title" placeholder="Give your post a title..." onChange={this.handleInput} value={this.props.title}/>
);
}
}
export default PublishTitleInput;

View file

@ -0,0 +1,12 @@
import {connect} from 'react-redux';
import View from './view';
const mapStateToProps = ({ publish }) => {
return {
file : publish.file,
status : publish.status.status,
message: publish.status.message,
};
};
export default connect(mapStateToProps, null)(View);

View file

@ -0,0 +1,25 @@
import React from 'react';
import Dropzone from 'containers/Dropzone';
import PublishForm from 'containers/PublishForm';
import PublishStatus from 'components/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}
/>
);
} else {
return <PublishForm />;
}
} else {
return <Dropzone />;
}
}
};
export default PublishTool;

View file

@ -0,0 +1,29 @@
import {updateClaim, updateError} from 'actions/publish';
import {connect} from 'react-redux';
import View from './view';
const mapStateToProps = ({ channel, publish }) => {
return {
loggedInChannelName : channel.loggedInChannel.name,
loggedInChannelShortId: channel.loggedInChannel.shortId,
fileName : publish.file.name,
publishInChannel : publish.publishInChannel,
selectedChannel : publish.selectedChannel,
claim : publish.claim,
urlError : publish.error.url,
};
};
const mapDispatchToProps = dispatch => {
return {
onClaimChange: (value) => {
dispatch(updateClaim(value));
dispatch(updateError('publishSubmit', null));
},
onUrlError: (value) => {
dispatch(updateError('url', value));
},
};
};
export default connect(mapStateToProps, mapDispatchToProps)(View);

View file

@ -0,0 +1,83 @@
import React from 'react';
import request from 'utils/request';
import UrlMiddle from 'components/PublishUrlMiddleDisplay';
class PublishUrlInput extends React.Component {
constructor (props) {
super(props);
this.handleInput = this.handleInput.bind(this);
this.cleanseInput = this.cleanseInput.bind(this);
this.setClaimNameFromFileName = this.setClaimNameFromFileName.bind(this);
this.checkClaimIsAvailable = this.checkClaimIsAvailable.bind(this);
}
componentDidMount () {
if (!this.props.claim || this.props.claim === '') {
this.setClaimNameFromFileName();
}
}
componentWillReceiveProps ({claim: newClaim}) {
if (newClaim) {
this.checkClaimIsAvailable(newClaim);
} else {
this.props.onUrlError('Please enter a URL');
}
}
handleInput (event) {
let value = event.target.value;
value = this.cleanseInput(value);
// update the state
this.props.onClaimChange(value);
}
cleanseInput (input) {
input = input.replace(/\s+/g, '-'); // replace spaces with dashes
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;
const fileNameWithoutEnding = fileName.substring(0, fileName.lastIndexOf('.'));
const cleanClaimName = this.cleanseInput(fileNameWithoutEnding);
this.props.onClaimChange(cleanClaimName);
}
checkClaimIsAvailable (claim) {
const that = this;
request(`/api/claim-is-available/${claim}`)
.then(isAvailable => {
// console.log('checkClaimIsAvailable request response:', isAvailable);
if (isAvailable) {
that.props.onUrlError(null);
} else {
that.props.onUrlError('That url has already been claimed');
}
})
.catch((error) => {
that.props.onUrlError(error.message);
});
}
render () {
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>
<UrlMiddle
publishInChannel={this.props.publishInChannel}
selectedChannel={this.props.selectedChannel}
loggedInChannelName={this.props.loggedInChannelName}
loggedInChannelShortId={this.props.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> }
</div>
</div>
);
}
}
export default PublishUrlInput;

28
react/reducers/channel.js Normal file
View file

@ -0,0 +1,28 @@
import * as actions from 'constants/channel_action_types';
const initialState = {
loggedInChannel: {
name : null,
shortId: null,
longId : null,
},
};
/*
Reducers describe how the application's state changes in response to actions
*/
export default function (state = initialState, action) {
switch (action.type) {
case actions.CHANNEL_UPDATE:
return Object.assign({}, state, {
loggedInChannel: {
name : action.name,
shortId: action.shortId,
longId : action.longId,
},
});
default:
return state;
}
}

8
react/reducers/index.js Normal file
View file

@ -0,0 +1,8 @@
import { combineReducers } from 'redux';
import PublishReducer from 'reducers/publish';
import ChannelReducer from 'reducers/channel';
export default combineReducers({
channel: ChannelReducer,
publish: PublishReducer,
});

79
react/reducers/publish.js Normal file
View file

@ -0,0 +1,79 @@
import * as actions from 'constants/publish_action_types';
import { LOGIN } from 'constants/publish_channel_select_states';
const initialState = {
publishInChannel : false,
selectedChannel : LOGIN,
showMetadataInputs: false,
status : {
status : null,
message: null,
},
error: {
file : null,
url : null,
channel : null,
publishSubmit: null,
},
file : null,
claim : '',
metadata: {
title : '',
thumbnail : '',
description: '',
license : '',
nsfw : false,
},
};
/*
Reducers describe how the application's state changes in response to actions
*/
export default function (state = initialState, action) {
switch (action.type) {
case actions.FILE_SELECTED:
return Object.assign({}, state, {
file: action.file,
});
case actions.FILE_CLEAR:
return initialState;
case actions.METADATA_UPDATE:
return Object.assign({}, state, {
metadata: Object.assign({}, state.metadata, {
[action.name]: action.value,
}),
});
case actions.CLAIM_UPDATE:
return Object.assign({}, state, {
claim: action.value,
});
case actions.SET_PUBLISH_IN_CHANNEL:
return Object.assign({}, state, {
publishInChannel: action.channel,
});
case actions.PUBLISH_STATUS_UPDATE:
return Object.assign({}, state, {
status: Object.assign({}, state.status, {
status : action.status,
message: action.message,
}),
});
case actions.ERROR_UPDATE:
return Object.assign({}, state, {
error: Object.assign({}, state.error, {
[action.name]: action.value,
}),
});
case actions.SELECTED_CHANNEL_UPDATE:
return Object.assign({}, state, {
selectedChannel: action.value,
});
case actions.TOGGLE_METADATA_INPUTS:
return Object.assign({}, state, {
showMetadataInputs: action.value,
});
default:
return state;
}
}

38
react/utils/file.js Normal file
View file

@ -0,0 +1,38 @@
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
switch (file.type) {
case 'image/jpeg':
case 'image/jpg':
case 'image/png':
if (file.size > 10000000) {
console.log('file was too big');
throw new Error('Sorry, images are limited to 10 megabytes.');
}
break;
case 'image/gif':
if (file.size > 50000000) {
console.log('file was too big');
throw new Error('Sorry, GIFs are limited to 50 megabytes.');
}
break;
case 'video/mp4':
if (file.size > 50000000) {
console.log('file was too big');
throw new Error('Sorry, videos are limited to 50 megabytes.');
}
break;
default:
console.log('file type is not supported');
throw new Error(file.type + ' is not a supported file type. Only, .jpeg, .png, .gif, and .mp4 files are currently supported.');
}
},
}

44
react/utils/request.js Normal file
View file

@ -0,0 +1,44 @@
/**
* Parses the JSON returned by a network request
*
* @param {object} response A response from a network request
*
* @return {object} The parsed JSON from the request
*/
function parseJSON (response) {
if (response.status === 204 || response.status === 205) {
return null;
}
return response.json();
}
/**
* Checks if a network request came back fine, and throws an error if not
*
* @param {object} response A response from a network request
*
* @return {object|undefined} Returns either the response, or throws an error
*/
function checkStatus (response) {
if (response.status >= 200 && response.status < 300) {
return response;
}
const error = new Error(response.statusText);
error.response = response;
throw error;
}
/**
* Requests a URL, returning a promise
*
* @param {string} url The URL we want to request
* @param {object} [options] The options we want to pass to "fetch"
*
* @return {object} The response data
*/
export default function request (url, options) {
return fetch(url, options)
.then(checkStatus)
.then(parseJSON);
}

View file

@ -71,7 +71,6 @@ module.exports = (app) => {
if (result === true) {
res.status(200).json(true);
} else {
// logger.debug(`Rejecting '${params.name}' because that name has already been claimed by this site`);
res.status(200).json(false);
}
})
@ -86,7 +85,6 @@ module.exports = (app) => {
if (result === true) {
res.status(200).json(true);
} else {
// logger.debug(`Rejecting '${params.name}' because that channel has already been claimed`);
res.status(200).json(false);
}
})

View file

@ -13,14 +13,38 @@ module.exports = (app) => {
});
});
// route for log in
app.post('/login', passport.authenticate('local-login'), (req, res) => {
// logger.debug('req.user:', req.user); // req.user contains the authenticated user's info
logger.debug('successful login');
res.status(200).json({
success : true,
channelName : req.user.channelName,
channelClaimId: req.user.channelClaimId,
shortChannelId: req.user.shortChannelId,
});
app.post('/login', (req, res, next) => {
passport.authenticate('local-login', (err, user, info) => {
logger.debug('info:', info);
if (err) {
return next(err);
}
if (!user) {
return res.status(200).json({
success: false,
message: info.message,
});
}
logger.debug('successful login');
req.logIn(user, (err) => {
if (err) {
return next(err);
}
return res.status(200).json({
success : true,
channelName : req.user.channelName,
channelClaimId: req.user.channelClaimId,
shortChannelId: req.user.shortChannelId,
});
});
})(req, res, next);
});
// see if user is authenticated, and return credentials if so
app.get('/user', (req, res) => {
if (req.user) {
res.status(200).json({success: true, message: req.user});
} else {
res.status(200).json({success: false, message: 'user is not logged in'});
}
});
};

View file

@ -1,3 +1,4 @@
{{> navBar}}
<div class="row row--padded">
<div class="column column--5 column--med-10 align-content-top">
<div class="column column--8 column--med-10">

View file

@ -1,3 +1,4 @@
{{> navBar}}
<div class="row row--padded">
<div class="row">
{{#ifConditional this.totalPages '===' 0}}

View file

@ -1,3 +1,4 @@
{{> navBar}}
<div class="row row--padded">
<h3>404: Not Found</h3>
<p>That page does not exist. Return <a class="link--primary" href="/">home</a>.</p>

View file

@ -1,52 +1,11 @@
<div id="react-nav-bar"></div>
<div class="row row--tall flex-container--column">
<form>
<input class="input-file" type="file" id="file_input" name="file_input" accept="video/*,image/*" onchange="publishFileFunctions.previewAndStageFile(event.target.files[0])" enctype="multipart/form-data"/>
</form>
<div id="primary-dropzone" class="dropzone row row--margined row--padded row--tall flex-container--column flex-container--center-center" ondrop="drop_handler(event);" ondragover="dragover_handler(event);" ondragend="dragend_handler(event)" ondragenter="dragenter_handler(event)" ondragleave="dragexit_handler(event)" onclick="publishFileFunctions.triggerFileChooser('file_input', event)">
<div id="primary-dropzone-instructions">
<p class="info-message-placeholder info-message--failure" id="input-error-file-selection" hidden="true"></p>
<p>Drag & drop image or video here to publish</p>
<p class="fine-print">OR</p>
<p class="blue--underlined">CHOOSE FILE</p>
</div>
<div id="dropbzone-dragover" class="hidden">
<p class="blue">Drop it.</p>
</div>
</div>
<div id="publish-form" class="hidden">
<div class="row row--padded row--no-bottom">
<div class="column column--10">
<!-- title input -->
<input type="text" id="publish-title" class="input-text text--large input-text--full-width" placeholder="Give your post a title...">
</div>
<div class="column column--5 column--sml-10" >
<!-- preview -->
<div class="row row--padded">
<div id="asset-preview-holder" class="dropzone" ondrop="drop_handler(event);" ondragover="dragover_handler(event);" ondragend="dragend_handler(event)" ondragenter="preview_onmouseenter_handler()" ondragleave="preview_onmouseleave_handler()" onmouseenter="preview_onmouseenter_handler()" onmouseleave="preview_onmouseleave_handler()" onclick="publishFileFunctions.triggerFileChooser('file_input', event)">
<div id="asset-preview-dropzone-instructions" class="hidden">
<p>Drag & drop image or video here</p>
<p class="fine-print">OR</p>
<p class="blue--underlined">CHOOSE FILE</p>
</div>
<div id="asset-preview-target"></div>
</div>
</div>
</div><div class="column column--5 column--sml-10 align-content-top">
<div id="publish-active-area" class="row row--padded">
{{> publishForm-Channel}}
{{> publishForm-Url}}
{{> publishForm-Thumbnail}}
{{> publishForm-Details}}
{{> publishForm-Submit}}
</div>
</div>
</div>
</div>
<div id="publish-status" class="hidden">
<div class="row row--margined">
<div id="publish-update" class="row align-content-center"></div>
<div id="publish-progress-bar" class="row align-content-center"></div>
<div id="upload-percent" class="row align-content-center"></div>
<div id="react-publish-tool" class="row row--padded row--tall flex-container--column">
<div class="row row--padded row--tall flex-container--column flex-container--center-center">
<p>loading...</p>
{{> progressBar}}
</div>
</div>
</div>
<script src="/bundle/bundle.js"></script>

View file

@ -17,8 +17,6 @@
</head>
<body id="channel-body">
<script src="/assets/js/generalFunctions.js"></script>
<script src="/assets/js/navBarFunctions.js"></script>
{{> navBar}}
{{{ body }}}
</body>
</html>

View file

@ -18,13 +18,6 @@
<body id="main-body">
<script src="/assets/js/generalFunctions.js"></script>
<script src="/assets/js/validationFunctions.js"></script>
<script src="/assets/js/publishFileFunctions.js"></script>
<script src="/assets/js/authFunctions.js"></script>
<script src="/assets/js/loginFunctions.js"></script>
<script src="/assets/js/dropzoneFunctions.js"></script>
<script src="/assets/js/createChannelFunctions.js"></script>
<script src="/assets/js/navBarFunctions.js"></script>
{{> navBar}}
{{{ body }}}
</body>
</html>

View file

@ -14,9 +14,7 @@
</head>
<body id="show-body">
<script src="/assets/js/generalFunctions.js"></script>
<script src="/assets/js/navBarFunctions.js"></script>
<script src="/assets/js/assetConstructor.js"></script>
{{> navBar}}
{{{ body }}}
</body>
</html>

View file

@ -1,3 +1,4 @@
{{> navBar}}
<div class="row row--padded">
<div class="column column--5 column--med-10 align-content-top">
<div class="column column--8 column--med-10">

View file

@ -1,3 +1,4 @@
{{> navBar}}
<div class="row row--padded">
<h3>No Channel</h3>
<p>There are no published channels matching your url</p>

View file

@ -1,3 +1,4 @@
{{> navBar}}
<div class="row row--padded">
<h3>No Claims</h3>
<p>There are no free assets at that claim. You should publish one at <a class="link--primary" href="/">spee.ch</a>.</p>

View file

@ -50,7 +50,7 @@
</div>
<div id="show-share-buttons">
<div class="row row--padded row--wide">
<div class="row row--padded row--no-bottom row--wide">
<div class="column column--2 column--med-10">
<span class="text">Share:</span>
</div><div class="column column--7 column--med-10">
@ -64,14 +64,10 @@
</div>
</div>
<div class="row row--wide">
<a class="text link--primary" id="show-details-toggle" href="#" onclick="toggleSection(event)" data-open="false" data-openlabel="[less]" data-closedlabel="[more]" data-slaveelementid="show-details">[more]</a>
</div>
<div id="show-details" class="row row--padded row--wide" hidden="true">
<div id="show-details" class="row row--padded row--no-bottom row--wide" hidden="true">
<div id="show-claim-name">
<div class="column column--2 column--med-10">
<span class="text">Name:</span>
<span class="text">Claim Name:</span>
</div><div class="column column--8 column--med-10">
{{claimInfo.name}}
</div>
@ -83,13 +79,6 @@
{{claimInfo.claimId}}
</div>
</div>
<div id="show-claim-id">
<div class="column column--2 column--med-10">
<span class="text">File Name:</span>
</div><div class="column column--8 column--med-10">
{{claimInfo.fileName}}
</div>
</div>
<div id="show-claim-id">
<div class="column column--2 column--med-10">
<span class="text">File Type:</span>
@ -102,3 +91,37 @@
</div>
</div>
</div>
<div class="row row--padded row--no-bottom row--wide">
<a class="text link--primary" id="show-details-toggle" href="#" onclick="toggleSection(event)" data-status="closed">[more]</a>
</div>
<script>
function toggleSection(event){
event.preventDefault();
var dataSet = event.target.dataset;
var status = dataSet.status;
var toggle = document.getElementById("show-details-toggle");
var details = document.getElementById("show-details");
if (status === "closed") {
details.hidden = false;
toggle.innerText = "[less]";
toggle.dataset.status = "open";
} else {
details.hidden = true;
toggle.innerText = "[more]";
toggle.dataset.status = "closed";
}
}
function copyToClipboard(event){
var elementToCopy = event.target.dataset.elementtocopy;
var element = document.getElementById(elementToCopy);
var errorElement = 'input-error-copy-text' + elementToCopy;
element.select();
try {
document.execCommand('copy');
} catch (err) {
validationFunctions.showError(errorElement, 'Oops, unable to copy');
}
}
</script>

View file

@ -29,9 +29,11 @@
<div id="channel-publish-in-progress" hidden="true">
<p>Creating your new channel. This may take a few seconds...</p>
<div id="create-channel-progress-bar"></div>
{{> progressBar}}
</div>
<div id="channel-publish-done" hidden="true">
<p>Your channel has been successfully created!</p>
</div>
<script src="/assets/js/createChannelFunctions.js"></script>

View file

@ -24,3 +24,5 @@
<button class="button--primary" onclick="loginToChannel(event)">Authenticate</button>
</div>
</form>
<script src="/assets/js/loginFunctions.js"></script>

View file

@ -24,15 +24,18 @@
<span class="nav-bar-tagline">Open-source, decentralized image and video sharing.</span>
</div>
<div class="nav-bar--right">
<a class="nav-bar-link link--nav" href="/">Upload</a>
<a class="nav-bar-link link--nav" href="/popular">Popular</a>
<a class="nav-bar-link link--nav" href="/">Publish</a>
<!--<a class="nav-bar-link link--nav" href="/popular">Popular</a>-->
<a class="nav-bar-link link--nav" href="/about">About</a>
<select type="text" id="nav-bar-channel-select" class="select select--arrow link--nav" onchange="toggleNavBarSelection(event.target.selectedOptions[0].value)" {{#unless user}}style="display:none"{{/unless}}>
<option id="nav-bar-channel-select-channel-option">@{{user.userName}}</option>
<option value="VIEW">View</option>
<option value="LOGOUT">Logout</option>
</select>
<a id="nav-bar-login-link" class="nav-bar-link link--nav" href="/login" {{#if user}}style="display:none"{{/if}}>Channel</a>
{{#if user }}
<select type="text" id="nav-bar-channel-select" class="select select--arrow link--nav" onchange="toggleNavBarSelection(event)">
<option id="nav-bar-channel-select-channel-option" >{{user.channelName}}</option>
<option value="VIEW" data-channelUrl="/{{user.channelName}}:{{user.channelClaimId}}">View</option>
<option value="LOGOUT">Logout</option>
</select>
{{else}}
<a id="nav-bar-login-link" class="nav-bar-link link--nav" href="/login">Channel</a>
{{/if}}
</div>
</div>
</div>
@ -48,4 +51,18 @@
link.setAttribute('class', 'select select--arrow link--nav-active');
}
}
// function to send user to their channel if selected
function toggleNavBarSelection (event) {
console.log('toggleNavBarSelection event', event);
const selectedOption = event.target.selectedOptions[0].value;
if (selectedOption === 'LOGOUT') {
// send logout request to server
window.location.href = '/logout';
} else if (selectedOption === 'VIEW') {
// redirect to channel page
const channelUrl = event.target.selectedOptions[0].dataset.channelurl;
console.log('url:', channelUrl);
window.location.href = channelUrl;
}
}
</script>

View file

@ -1,105 +0,0 @@
<!-- select whether to publish anonymously or in a channel -->
<div class="row row--padded row--short row--wide">
<div class="column column--10">
<form>
<div class="column column--3 column--med-10">
<input type="radio" name="anonymous-or-channel" id="anonymous-radio" class="input-radio" value="anonymous" {{#unless user}}checked {{/unless}} onchange="toggleChannel(event.target.value)"/>
<label class="label label--pointer" for="anonymous-radio">Anonymous</label>
</div><div class="column column--7 column--med-10">
<input type="radio" name="anonymous-or-channel" id="channel-radio" class="input-radio" value="in a channel" {{#if user}}checked {{/if}} onchange="toggleChannel(event.target.value)"/>
<label class="label label--pointer" for="channel-radio">In a channel</label>
</div>
</form>
</div>
</div>
<div id="channel-select-options" {{#unless user}}hidden="true"{{/unless}}>
<div class="row row--padded row--no-top row--no-bottom row--wide">
<!--error display-->
<p id="input-error-channel-select" class="info-message-placeholder info-message--failure"></p>
<!--channel login/create select-->
<div class="column column--3">
<label class="label" for="channel-name-select">Channel:</label>
</div><div class="column column--7">
<select type="text" id="channel-name-select" class="select select--arrow" onchange="toggleSelectedChannel(event.target.selectedOptions[0].value)">
{{#if user}}
<option value="{{user.channelName}}" id="publish-channel-select-channel-option">{{user.channelName}}</option>
{{/if}}
<option value="login">Existing</option>
<option value="new" >New</option>
</select>
</div>
</div>
<!-- log into an existing channel -->
<div id="channel-login-details" class="row row--padded row--short row--wide" {{#if user}}hidden="true"{{/if}}>
{{> channelLoginForm}}
</div>
<!-- create a channel -->
<div id="channel-create-details" class="row row--padded row--short row--wide" hidden="true">
{{> channelCreationForm}}
</div>
</div>
<script type="text/javascript">
// show or hide the channel selection tools
function toggleChannel (selectedOption) {
const channelSelectOptions = document.getElementById('channel-select-options');
// show/hide the login and new channel forms
if (selectedOption === 'anonymous') {
channelSelectOptions.hidden = true;
channelSelectOptions.hidden = true;
// update url
updateUrl(selectedOption);
} else if (selectedOption === 'in a channel') {
channelSelectOptions.hidden = false;
// update url
let selectedChannel = document.getElementById('channel-name-select').selectedOptions[0].value
toggleSelectedChannel(selectedChannel);
} else {
console.log('selected option was not recognized');
}
}
// show or hide the channel create/login tool
function toggleSelectedChannel (selectedChannel) {
const createChannelTool = document.getElementById('channel-create-details');
const loginToChannelTool = document.getElementById('channel-login-details');
// show/hide the login and new channel forms
if (selectedChannel === 'new') {
createChannelTool.hidden = false;
loginToChannelTool.hidden = true;
} else if (selectedChannel === 'login') {
loginToChannelTool.hidden = false;
createChannelTool.hidden = true;
} else {
// hide the login and new channel forms
loginToChannelTool.hidden = true;
createChannelTool.hidden = true;
validationFunctions.hideError(document.getElementById('input-error-channel-select'));
}
// update url
updateUrl(selectedChannel);
}
function updateUrl (selectedOption) {
const urlChannel = document.getElementById('url-channel');
const urlNoChannelPlaceholder = document.getElementById('url-no-channel-placeholder');
const urlChannelPlaceholder = document.getElementById('url-channel-placeholder');
if (selectedOption === 'new' || selectedOption === 'login' || selectedOption === ''){
urlChannel.hidden = true;
urlNoChannelPlaceholder.hidden = true;
urlChannelPlaceholder.hidden = false;
} else if (selectedOption === 'anonymous'){
urlChannel.hidden = true;
urlNoChannelPlaceholder.hidden = false;
urlChannelPlaceholder.hidden = true;
} else {
urlChannel.hidden = false;
// show channel and short id
const selectedChannel = getCookie('channel_name');
const shortChannelId = getCookie('short_channel_id');
urlChannel.innerText = `${selectedChannel}:${shortChannelId}`;
urlNoChannelPlaceholder.hidden = true;
urlChannelPlaceholder.hidden = true;
}
}
</script>

View file

@ -1,50 +0,0 @@
<div class="row row--padded row--no-top row--no-bottom row--wide">
<div class="column column--10">
<a class="label link--primary" id="publish-details-toggle" href="#" onclick="toggleSection(event)" data-open="false" data-openlabel="[less]" data-closedlabel="[more]" data-slaveelementid="publish-details">[more]</a>
</div>
</div>
<div id="publish-details" hidden="true" class="row row--padded row--wide">
<!-- description input -->
<div class="row row--no-top">
<div class="column column--3 column--med-10 align-content-top">
<label for="publish-license" class="label">Description:</label>
</div><div class="column column--7 column--sml-10">
<textarea rows="1" id="publish-description" class="textarea textarea--primary textarea--full-width" placeholder="Optional description"></textarea>
</div>
</div>
<div class="row row--no-top">
<div class="column column--3 column--med-10">
<label for="publish-license" class="label">License:</label>
</div><div class="column column--7 column--sml-10">
<select type="text" id="publish-license" class="select select--primary">
<option value=" ">Unspecified</option>
<option value="Public Domain">Public Domain</option>
<option value="Creative Commons">Creative Commons</option>
</select>
</div>
</div>
<div class="row row--no-top">
<div class="column column--3">
<label for="publish-nsfw" class="label">Mature:</label>
</div><div class="column column--7">
<input class="input-checkbox" type="checkbox" id="publish-nsfw">
</div>
</div>
</div>
<script type="text/javascript">
const textarea = document.getElementById('publish-description');
const limit = 200;
textarea.oninput = () => {
textarea.style.height = '';
textarea.style.height = Math.min(textarea.scrollHeight, limit) + 'px';
}
</script>

View file

@ -1,10 +0,0 @@
<div class="row row--padded row--wide">
<div class="input-error" id="input-error-publish-submit" hidden="true"></div>
<button id="publish-submit" class="button--primary button--large" onclick="publishFileFunctions.publishStagedFile(event)">Upload</button>
</div>
<div class="row row--short align-content-center">
<button class="button--cancel" onclick="publishFileFunctions.cancelPublish()">Cancel</button>
</div>
<div class="row row--short align-content-center">
<p class="fine-print">By clicking 'Upload', 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 class="link--primary" target="_blank" href="https://lbry.io/learn">Read more.</a></p>
</div>

View file

@ -1,56 +0,0 @@
<div class="row row--padded row--wide row--no-top" id="publish-thumbnail" hidden="true">
<div class="column column--3 column--sml-10">
<label class="label">Thumbnail:</label>
</div><div class="column column--6 column--sml-10">
<div class="input-text--primary">
<input type="text" id="claim-thumbnail-input" class="input-text input-text--full-width" placeholder="https://spee.ch/xyz/example.jpg" value="" oninput="updateVideoThumb(event)">
</div>
</div>
</div>
<script type="text/javascript">
function urlIsAnImage(url) {
return(url.match(/\.(jpeg|jpg|gif|png)$/) != null);
}
function testImage(url, timeoutT) {
return new Promise(function (resolve, reject) {
var timeout = timeoutT || 5000;
var timer, img = new Image();
img.onerror = img.onabort = function () {
clearTimeout(timer);
reject("error");
};
img.onload = function () {
clearTimeout(timer);
resolve("success");
};
timer = setTimeout(function () {
// reset .src to invalid URL so it stops previous
// loading, but doesn't trigger new load
img.src = "//!!!!/test.jpg";
reject("timeout");
}, timeout);
img.src = url;
});
}
function updateVideoThumb(event){
var videoPreview = document.getElementById('asset-preview');
var imageUrl = event.target.value;
if (urlIsAnImage(imageUrl)){
testImage(imageUrl, 3000)
.then(function(result) {
if (result === 'success'){
videoPreview.src = imageUrl;
} else if (result === 'timeout') {
console.log('could not resolve the provided thumbnail image url');
}
})
.catch(function(error) {
console.log('encountered an error loading thumbnail image url.')
})
}
}
</script>

View file

@ -1,16 +0,0 @@
<div class="row row--padded row--wide">
<!--error display-->
<p id="input-error-claim-name" class="info-message-placeholder info-message--failure" hidden="true"></p>
<!--url selection-->
<div class="column column--3 column--sml-10">
<label class="label">URL:</label>
</div><div class="column column--7 column--sml-10 input-text--primary span--relative">
<span class="url-text--secondary">spee.ch /</span>
<span id="url-channel" class="url-text--secondary" {{#if user}}{{else}}hidden="true"{{/if}}>{{user.channelName}}:{{user.shortChannelId}}</span>
<span id="url-no-channel-placeholder" class="url-text--secondary tooltip" {{#if user}}hidden="true"{{else}}{{/if}}>xyz<span class="tooltip-text">This will be a random id</span></span>
<span id="url-channel-placeholder" class="url-text--secondary tooltip" hidden="true">@channel<span class="tooltip-text">Select a channel above</span></span> /
<input type="text" id="claim-name-input" class="input-text" placeholder="your-url-here" oninput="validationFunctions.checkClaimName(event.target.value)">
<span id="input-success-claim-name" class="info-message--success span--absolute"></span>
</div>
</div>

View file

@ -1,3 +1,4 @@
{{> navBar}}
<div class="row row--padded">
<div class="grid">
{{#each trendingAssets}}

View file

@ -1,3 +1,4 @@
{{> navBar}}
<div class="row row--padded">
<h3>Error</h3>
<p>Unfortnately, Spee.ch encountered an error. You can help us out, by reporting the below error message in the #speech channel on <a class="link--primary" href="https://discord.gg/YjYbwhS" target="_blank">LBRY Discord</a>!</p>

View file

@ -1,3 +1,4 @@
{{> navBar}}
<div class="row row--tall row--padded">
<div class="column column--10">
<!-- title -->

32
webpack.config.js Normal file
View file

@ -0,0 +1,32 @@
const Path = require('path');
const REACT_ROOT = Path.resolve(__dirname, 'react/');
module.exports = {
entry : ['whatwg-fetch', './react/app.js'],
output: {
path : Path.join(__dirname, '/public/bundle/'),
filename: 'bundle.js',
},
watch : true,
module: {
loaders: [
{
test : /.jsx?$/,
loader : 'babel-loader',
exclude: /node_modules/,
query : {
presets: ['es2015', 'react', 'stage-2'],
},
},
],
},
resolve: {
modules: [
REACT_ROOT,
'node_modules',
__dirname,
],
extensions: ['.js', '.jsx', '.scss'],
},
};