diff --git a/README.md b/README.md index 9ac04df7..974834d1 100644 --- a/README.md +++ b/README.md @@ -29,20 +29,20 @@ spee.ch is a single-serving site that reads and publishes images and videos to a ## API #### GET -* /api/claim-resolve/:name - * example: `curl https://spee.ch/api/claim-resolve/doitlive` -* /api/claim-list/:name - * example: `curl https://spee.ch/api/claim-list/doitlive` -* /api/claim-is-available/:name ( +* /api/claim/resolve/:name/:claimId + * example: `curl https://spee.ch/api/claim/resolve/doitlive/xyz` +* /api/claim/list/:name + * example: `curl https://spee.ch/api/claim/list/doitlive` +* /api/claim/availability/:name ( * returns `true`/`false` for whether a name is available through spee.ch - * example: `curl https://spee.ch/api/claim-is-available/doitlive` -* /api/channel-is-available/:name ( + * example: `curl https://spee.ch/api/claim/availability/doitlive` +* /api/channel/availability/:name ( * returns `true`/`false` for whether a channel is available through spee.ch - * example: `curl https://spee.ch/api/channel-is-available/@CoolChannel` + * example: `curl https://spee.ch/api/channel/availability/@CoolChannel` #### POST -* /api/claim-publish - * example: `curl -X POST -F 'name=MyPictureName' -F 'file=@/path/to/myPicture.jpeg' https://spee.ch/api/claim-publish` +* /api/claim/publish + * example: `curl -X POST -F 'name=MyPictureName' -F 'file=@/path/to/myPicture.jpeg' https://spee.ch/api/claim/publish` * Parameters: * `name` * `file` (must be type .mp4, .jpeg, .jpg, .gif, or .png) diff --git a/controllers/serveController.js b/controllers/serveController.js index e2e9ff13..a1a8fbc8 100644 --- a/controllers/serveController.js +++ b/controllers/serveController.js @@ -1,6 +1,6 @@ const db = require('../models'); const logger = require('winston'); -const { returnPaginatedChannelViewData } = require('../helpers/channelPagination.js'); +const { returnPaginatedChannelClaims } = require('../helpers/channelPagination.js'); const NO_CHANNEL = 'NO_CHANNEL'; const NO_CLAIM = 'NO_CLAIM'; @@ -53,7 +53,7 @@ module.exports = { }); }); }, - getChannelViewData (channelName, channelClaimId, query) { + getChannelData (channelName, channelClaimId, page) { return new Promise((resolve, reject) => { // 1. get the long channel Id (make sure channel exists) db.Certificate.getLongChannelId(channelName, channelClaimId) @@ -62,14 +62,41 @@ module.exports = { return [null, null, null]; } // 2. get the short ID and all claims for that channel - return Promise.all([longChannelClaimId, db.Certificate.getShortChannelIdFromLongChannelId(longChannelClaimId, channelName), db.Claim.getAllChannelClaims(longChannelClaimId)]); + return Promise.all([longChannelClaimId, db.Certificate.getShortChannelIdFromLongChannelId(longChannelClaimId, channelName)]); }) - .then(([longChannelClaimId, shortChannelClaimId, channelClaimsArray]) => { + .then(([longChannelClaimId, shortChannelClaimId]) => { + if (!longChannelClaimId) { + return resolve(NO_CHANNEL); + } + // 3. return all the channel information + resolve({ + channelName, + longChannelClaimId, + shortChannelClaimId, + }); + }) + .catch(error => { + reject(error); + }); + }); + }, + getChannelClaims (channelName, channelClaimId, page) { + return new Promise((resolve, reject) => { + // 1. get the long channel Id (make sure channel exists) + db.Certificate.getLongChannelId(channelName, channelClaimId) + .then(longChannelClaimId => { + if (!longChannelClaimId) { + return [null, null, null]; + } + // 2. get the short ID and all claims for that channel + return Promise.all([longChannelClaimId, db.Claim.getAllChannelClaims(longChannelClaimId)]); + }) + .then(([longChannelClaimId, channelClaimsArray]) => { if (!longChannelClaimId) { return resolve(NO_CHANNEL); } // 3. format the data for the view, including pagination - let paginatedChannelViewData = returnPaginatedChannelViewData(channelName, longChannelClaimId, shortChannelClaimId, channelClaimsArray, query); + let paginatedChannelViewData = returnPaginatedChannelClaims(channelName, longChannelClaimId, channelClaimsArray, page); // 4. return all the channel information and contents resolve(paginatedChannelViewData); }) diff --git a/helpers/channelPagination.js b/helpers/channelPagination.js index d454a9ff..e76a1158 100644 --- a/helpers/channelPagination.js +++ b/helpers/channelPagination.js @@ -1,25 +1,24 @@ -const CLAIMS_PER_PAGE = 10; +const CLAIMS_PER_PAGE = 12; module.exports = { - returnPaginatedChannelViewData (channelName, longChannelClaimId, shortChannelClaimId, claims, query) { + returnPaginatedChannelClaims (channelName, longChannelClaimId, claims, page) { const totalPages = module.exports.determineTotalPages(claims); - const paginationPage = module.exports.getPageFromQuery(query); + const paginationPage = module.exports.getPageFromQuery(page); const viewData = { - channelName : channelName, - longChannelClaimId : longChannelClaimId, - shortChannelClaimId: shortChannelClaimId, - claims : module.exports.extractPageFromClaims(claims, paginationPage), - previousPage : module.exports.determinePreviousPage(paginationPage), - currentPage : paginationPage, - nextPage : module.exports.determineNextPage(totalPages, paginationPage), - totalPages : totalPages, - totalResults : module.exports.determineTotalClaims(claims), + channelName : channelName, + longChannelClaimId: longChannelClaimId, + claims : module.exports.extractPageFromClaims(claims, paginationPage), + previousPage : module.exports.determinePreviousPage(paginationPage), + currentPage : paginationPage, + nextPage : module.exports.determineNextPage(totalPages, paginationPage), + totalPages : totalPages, + totalResults : module.exports.determineTotalClaims(claims), }; return viewData; }, - getPageFromQuery (query) { - if (query.p) { - return parseInt(query.p); + getPageFromQuery (page) { + if (page) { + return parseInt(page); } return 1; }, @@ -30,7 +29,7 @@ module.exports = { // logger.debug('claims is array?', Array.isArray(claims)); // logger.debug(`pageNumber ${pageNumber} is number?`, Number.isInteger(pageNumber)); const claimStartIndex = (pageNumber - 1) * CLAIMS_PER_PAGE; - const claimEndIndex = claimStartIndex + 10; + const claimEndIndex = claimStartIndex + CLAIMS_PER_PAGE; const pageOfClaims = claims.slice(claimStartIndex, claimEndIndex); return pageOfClaims; }, diff --git a/helpers/errorHandlers.js b/helpers/errorHandlers.js index e170a9aa..1c7336dd 100644 --- a/helpers/errorHandlers.js +++ b/helpers/errorHandlers.js @@ -1,51 +1,30 @@ const logger = require('winston'); module.exports = { + handleErrorResponse: function (originalUrl, ip, error, res) { + logger.error(`Error on ${originalUrl}`, module.exports.useObjectPropertiesIfNoKeys(error)); + const [status, message] = module.exports.returnErrorMessageAndStatus(error); + res + .status(status) + .json(module.exports.createErrorResponsePayload(status, message)); + }, returnErrorMessageAndStatus: function (error) { let status, message; // check for daemon being turned off if (error.code === 'ECONNREFUSED') { status = 503; message = 'Connection refused. The daemon may not be running.'; - // check for errors from the daemon - } else if (error.response) { - status = error.response.status || 500; - if (error.response.data) { - if (error.response.data.message) { - message = error.response.data.message; - } else if (error.response.data.error) { - message = error.response.data.error.message; - } else { - message = error.response.data; - } - } else { - message = error.response; - } - // check for thrown errors - } else if (error.message) { - status = 400; - message = error.message; - // fallback for everything else + // fallback for everything else } else { status = 400; - message = error; - } + if (error.message) { + message = error.message; + } else { + message = error; + }; + }; return [status, message]; }, - handleRequestError: function (originalUrl, ip, error, res) { - logger.error(`Request Error on ${originalUrl}`, module.exports.useObjectPropertiesIfNoKeys(error)); - const [status, message] = module.exports.returnErrorMessageAndStatus(error); - res - .status(status) - .render('requestError', module.exports.createErrorResponsePayload(status, message)); - }, - handleApiError: function (originalUrl, ip, error, res) { - logger.error(`Api Error on ${originalUrl}`, module.exports.useObjectPropertiesIfNoKeys(error)); - const [status, message] = module.exports.returnErrorMessageAndStatus(error); - res - .status(status) - .json(module.exports.createErrorResponsePayload(status, message)); - }, useObjectPropertiesIfNoKeys: function (err) { if (Object.keys(err).length === 0) { let newErrorObject = {}; diff --git a/helpers/lbryApi.js b/helpers/lbryApi.js index 25ee96cd..ac4c3280 100644 --- a/helpers/lbryApi.js +++ b/helpers/lbryApi.js @@ -10,13 +10,13 @@ function handleLbrynetResponse ({ data }, resolve, reject) { // check for an error if (data.result.error) { logger.debug('Lbrynet api error:', data.result.error); - reject(data.result.error); + reject(new Error(data.result.error)); return; }; resolve(data.result); return; } - // fallback in case the just timed out + // fallback in case it just timed out reject(JSON.stringify(data)); } diff --git a/helpers/lbryUri.js b/helpers/lbryUri.js index 15ab0b86..f72a25ef 100644 --- a/helpers/lbryUri.js +++ b/helpers/lbryUri.js @@ -55,14 +55,14 @@ module.exports = { claimId, }; }, - parseName: function (name) { - logger.debug('parsing name:', name); + parseClaim: function (claim) { + logger.debug('parsing name:', claim); const componentsRegex = new RegExp( '([^:$#/.]*)' + // name (stops at the first modifier) '([:$#.]?)([^/]*)' // modifier separator, modifier (stops at the first path separator or end) ); const [proto, claimName, modifierSeperator, modifier] = componentsRegex - .exec(name) + .exec(claim) .map(match => match || null); logger.debug(`${proto}, ${claimName}, ${modifierSeperator}, ${modifier}`); @@ -75,7 +75,6 @@ module.exports = { throw new Error(`Invalid characters in claim name: ${nameBadChars.join(', ')}.`); } // Validate and process modifier - let isServeRequest = false; if (modifierSeperator) { if (!modifier) { throw new Error(`No file extension provided after separator ${modifierSeperator}.`); @@ -83,11 +82,29 @@ module.exports = { if (modifierSeperator !== '.') { throw new Error(`The ${modifierSeperator} modifier is not supported in the claim name`); } - isServeRequest = true; } + // return results return { claimName, - isServeRequest, + }; + }, + parseModifier: function (claim) { + logger.debug('parsing modifier:', claim); + const componentsRegex = new RegExp( + '([^:$#/.]*)' + // name (stops at the first modifier) + '([:$#.]?)([^/]*)' // modifier separator, modifier (stops at the first path separator or end) + ); + const [proto, claimName, modifierSeperator, modifier] = componentsRegex + .exec(claim) + .map(match => match || null); + logger.debug(`${proto}, ${claimName}, ${modifierSeperator}, ${modifier}`); + // Validate and process modifier + let hasFileExtension = false; + if (modifierSeperator) { + hasFileExtension = true; + } + return { + hasFileExtension, }; }, }; diff --git a/helpers/serveHelpers.js b/helpers/serveHelpers.js index 8bfa37f1..b3e1e61a 100644 --- a/helpers/serveHelpers.js +++ b/helpers/serveHelpers.js @@ -16,10 +16,10 @@ module.exports = { }, showFile (claimInfo, shortId, res) { logger.verbose(`showing claim: ${claimInfo.name}#${claimInfo.claimId}`); - res.status(200).render('show', { layout: 'show', claimInfo, shortId }); + res.status(200).render('index'); }, showFileLite (claimInfo, shortId, res) { logger.verbose(`showlite claim: ${claimInfo.name}#${claimInfo.claimId}`); - res.status(200).render('showLite', { layout: 'showlite', claimInfo, shortId }); + res.status(200).render('index'); }, }; diff --git a/models/certificate.js b/models/certificate.js index 50a24479..07c0cc76 100644 --- a/models/certificate.js +++ b/models/certificate.js @@ -100,7 +100,7 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => { }; Certificate.getShortChannelIdFromLongChannelId = function (longChannelId, channelName) { - logger.debug(`finding short channel id for ${channelName}:${longChannelId}`); + logger.debug(`getShortChannelIdFromLongChannelId ${channelName}:${longChannelId}`); return new Promise((resolve, reject) => { this .findAll({ @@ -122,6 +122,7 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => { }; Certificate.getLongChannelIdFromShortChannelId = function (channelName, channelClaimId) { + logger.debug(`getLongChannelIdFromShortChannelId(${channelName}, ${channelClaimId})`); return new Promise((resolve, reject) => { this .findAll({ @@ -170,6 +171,7 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => { }; Certificate.validateLongChannelId = function (name, claimId) { + logger.debug(`validateLongChannelId(${name}, ${claimId})`); return new Promise((resolve, reject) => { this.findOne({ where: {name, claimId}, diff --git a/models/claim.js b/models/claim.js index 6fc23de2..25c1a04f 100644 --- a/models/claim.js +++ b/models/claim.js @@ -342,6 +342,7 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => { }; Claim.resolveClaim = function (name, claimId) { + logger.debug(`Claim.resolveClaim: ${name} ${claimId}`); return new Promise((resolve, reject) => { this .findAll({ diff --git a/package.json b/package.json index e2b779a1..55bf9ddd 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,9 @@ "react": "^16.2.0", "react-dom": "^16.2.0", "react-redux": "^5.0.6", + "react-router-dom": "^4.2.2", "redux": "^3.7.2", + "redux-saga": "^0.16.0", "request": "^2.83.0", "request-promise": "^4.2.2", "sequelize": "^4.1.0", @@ -61,6 +63,7 @@ "devDependencies": { "babel-core": "^6.26.0", "babel-loader": "^7.1.2", + "babel-polyfill": "^6.26.0", "babel-preset-es2015": "^6.24.1", "babel-preset-react": "^6.24.1", "babel-preset-stage-2": "^6.24.1", diff --git a/public/assets/css/general.css b/public/assets/css/general.css index 1e6d5b86..3f6d439c 100644 --- a/public/assets/css/general.css +++ b/public/assets/css/general.css @@ -276,7 +276,7 @@ a, a:visited { vertical-align: top; } -.align-content-right { +.align-content-bottom { vertical-align: bottom; } @@ -407,12 +407,13 @@ button { cursor: pointer; } -.button--primary { +.button--primary, .button--primary:focus { border: 1px solid black; padding: 0.5em; margin: 0.5em 0.3em 0.5em 0.3em; color: black; background-color: white; + outline: 0px; } .button--primary:hover { @@ -422,9 +423,28 @@ button { } .button--primary:active{ - border: 1px solid #4156C5; - color: white; + border: 1px solid #ffffff; + color: #d0d0d0; + background-color: #ffffff; +} + +.button--secondary, .button--secondary:focus { + border: 0px; + border-bottom: 1px solid black; + padding: 0.5em; + margin: 0.5em 0.3em 0.5em 0.3em; + color: black; background-color: white; + outline: 0px; +} + +.button--secondary:hover { + border-bottom: 1px solid #9b9b9b; + color: #4156C5; +} + +.button--secondary:active { + color: #ffffff;; } .button--large{ @@ -495,7 +515,7 @@ table { padding: 1em; } -#asset-preview { +#dropzone-preview { display: block; width: 100%; } @@ -506,6 +526,20 @@ table { /* Assets */ +.asset-holder { + clear : both; + display: inline-block; + width : 31%; + padding: 0px; + margin : 1%; +} + +.asset-preview { + width : 100%; + padding: 0px; + margin : 0px +} + .asset { width: 100%; } diff --git a/public/assets/img/loading.gif b/public/assets/img/loading.gif deleted file mode 100644 index a9648d12..00000000 Binary files a/public/assets/img/loading.gif and /dev/null differ diff --git a/public/assets/js/assetConstructor.js b/public/assets/js/assetConstructor.js deleted file mode 100644 index f84d3344..00000000 --- a/public/assets/js/assetConstructor.js +++ /dev/null @@ -1,139 +0,0 @@ -const Asset = function () { - this.data = {}; - this.addPlayPauseToVideo = function () { - const that = this; - const video = document.getElementById('video-asset'); - if (video) { - // add event listener for click - video.addEventListener('click', ()=> { - that.playOrPause(video); - }); - // add event listener for space bar - document.body.onkeyup = (event) => { - if (event.keyCode == 32) { - that.playOrPause(video); - } - }; - } - }; - this.playOrPause = function(video){ - if (video.paused == true) { - video.play(); - } - else{ - video.pause(); - } - }; - this.showAsset = function () { - this.hideAssetStatus(); - this.showAssetHolder(); - if (!this.data.src) { - return console.log('error: src is not set') - } - if (!this.data.contentType) { - return console.log('error: contentType is not set') - } - if (this.data.contentType === 'video/mp4') { - this.showVideo(); - } else { - this.showImage(); - } - }; - this.showVideo = function () { - console.log('showing video', this.data.src); - const video = document.getElementById('video-asset'); - const source = document.createElement('source'); - source.setAttribute('src', this.data.src); - video.appendChild(source); - video.play(); - }; - this.showImage = function () { - console.log('showing image', this.data.src); - const asset = document.getElementById('image-asset'); - asset.setAttribute('src', this.data.src); - }; - this.hideAssetStatus = function () { - const assetStatus = document.getElementById('asset-status'); - assetStatus.hidden = true; - }; - this.showAssetHolder =function () { - const assetHolder = document.getElementById('asset-holder'); - assetHolder.hidden = false; - }; - this.showSearchMessage = function () { - const searchMessage = document.getElementById('searching-message'); - searchMessage.hidden = false; - }; - this.showFailureMessage = function (msg) { - console.log(msg); - const searchMessage = document.getElementById('searching-message'); - const failureMessage = document.getElementById('failure-message'); - const errorMessage = document.getElementById('error-message'); - searchMessage.hidden = true; - failureMessage.hidden = false; - errorMessage.innerText = msg; - }; - this.checkFileAndRenderAsset = function () { - const that = this; - this.isFileAvailable() - .then(isAvailable => { - if (!isAvailable) { - console.log('file is not yet available'); - that.showSearchMessage(); - return that.getAssetOnSpeech(); - } - }) - .then(() => { - that.showAsset(); - }) - .catch(error => { - that.showFailureMessage(error); - }) - }; - this.isFileAvailable = function () { - console.log(`checking if file is available for ${this.data.claimName}#${this.data.claimId}`) - const uri = `/api/file-is-available/${this.data.claimName}/${this.data.claimId}`; - const xhr = new XMLHttpRequest(); - return new Promise((resolve, reject) => { - xhr.open("GET", uri, true); - xhr.onreadystatechange = function() { - if (xhr.readyState == 4) { - const response = JSON.parse(xhr.response); - if (xhr.status == 200) { - console.log('isFileAvailable succeeded:', response); - if (response.message === true) { - resolve(true); - } else { - resolve(false); - } - } else { - console.log('isFileAvailable failed:', response); - reject('Well this sucks, but we can\'t seem to phone home'); - } - } - }; - xhr.send(); - }) - }; - this.getAssetOnSpeech = function() { - console.log(`getting claim for ${this.data.claimName}#${this.data.claimId}`) - const uri = `/api/claim-get/${this.data.claimName}/${this.data.claimId}`; - const xhr = new XMLHttpRequest(); - return new Promise((resolve, reject) => { - xhr.open("GET", uri, true); - xhr.onreadystatechange = function() { - if (xhr.readyState == 4) { - const response = JSON.parse(xhr.response); - if (xhr.status == 200) { - console.log('getAssetOnSpeech succeeded:', response) - resolve(true); - } else { - console.log('getAssetOnSpeech failed:', response); - reject(response.message); - } - } - }; - xhr.send(); - }) - }; -}; diff --git a/public/assets/js/createChannelFunctions.js b/public/assets/js/createChannelFunctions.js deleted file mode 100644 index 478cc2d8..00000000 --- a/public/assets/js/createChannelFunctions.js +++ /dev/null @@ -1,65 +0,0 @@ -// display the content that shows channel creation has started -function showChannelCreateInProgressDisplay () { - const publishChannelForm = document.getElementById('publish-channel-form'); - const inProgress = document.getElementById('channel-publish-in-progress'); - publishChannelForm.hidden = true; - inProgress.hidden = false; -} - -// display the content that shows channel creation is done -function showChannelCreateDoneDisplay() { - const inProgress = document.getElementById('channel-publish-in-progress'); - inProgress.hidden=true; - const done = document.getElementById('channel-publish-done'); - done.hidden = false; -} - -function showChannelCreationError(msg) { - const inProgress = document.getElementById('channel-publish-in-progress'); - inProgress.innerText = msg; -} - -function publishNewChannel (event) { - 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) - .then(() => { - showChannelCreateInProgressDisplay(); - // 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(signupResult => { - console.log('signup success:', signupResult); - showChannelCreateDoneDisplay(); - window.location = '/'; - }) - .catch(error => { - if (error.name === 'ChannelNameError' || error.name === 'ChannelPasswordError'){ - const channelNameErrorDisplayElement = document.getElementById('input-error-channel-name'); - 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!', error); - } - }) -} diff --git a/public/assets/js/generalFunctions.js b/public/assets/js/generalFunctions.js deleted file mode 100644 index 74497983..00000000 --- a/public/assets/js/generalFunctions.js +++ /dev/null @@ -1,56 +0,0 @@ -// Create new error objects, that prototypically inherit from the Error constructor -function FileError(message) { - this.name = 'FileError'; - this.message = message || 'Default Message'; - this.stack = (new Error()).stack; -} -FileError.prototype = Object.create(Error.prototype); -FileError.prototype.constructor = FileError; - -function NameError(message) { - this.name = 'NameError'; - this.message = message || 'Default Message'; - this.stack = (new Error()).stack; -} -NameError.prototype = Object.create(Error.prototype); -NameError.prototype.constructor = NameError; - -function ChannelNameError(message) { - this.name = 'ChannelNameError'; - this.message = message || 'Default Message'; - this.stack = (new Error()).stack; -} -ChannelNameError.prototype = Object.create(Error.prototype); -ChannelNameError.prototype.constructor = ChannelNameError; - -function ChannelPasswordError(message) { - this.name = 'ChannelPasswordError'; - this.message = message || 'Default Message'; - this.stack = (new Error()).stack; -} -ChannelPasswordError.prototype = Object.create(Error.prototype); -ChannelPasswordError.prototype.constructor = ChannelPasswordError; - -function AuthenticationError(message) { - this.name = 'AuthenticationError'; - this.message = message || 'Default Message'; - this.stack = (new Error()).stack; -} -AuthenticationError.prototype = Object.create(Error.prototype); -AuthenticationError.prototype.constructor = AuthenticationError; - -function showAssetDetails(event) { - var thisAssetHolder = document.getElementById(event.target.id); - var thisAssetImage = thisAssetHolder.firstElementChild; - var thisAssetDetails = thisAssetHolder.lastElementChild; - thisAssetImage.style.opacity = 0.2; - thisAssetDetails.setAttribute('class', 'grid-item-details flex-container--column flex-container--center-center'); -} - -function hideAssetDetails(event) { - var thisAssetHolder = document.getElementById(event.target.id); - var thisAssetImage = thisAssetHolder.firstElementChild; - var thisAssetDetails = thisAssetHolder.lastElementChild; - thisAssetImage.style.opacity = 1; - thisAssetDetails.setAttribute('class', 'hidden'); -} diff --git a/public/assets/js/loginFunctions.js b/public/assets/js/loginFunctions.js deleted file mode 100644 index b13710ab..00000000 --- a/public/assets/js/loginFunctions.js +++ /dev/null @@ -1,44 +0,0 @@ -function loginToChannel (event) { - 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) - .then(() => { - 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(function({success, message}) { - if (success) { - window.location = '/'; - } else { - throw new Error(message); - } - - }) - .catch(error => { - const loginErrorDisplayElement = document.getElementById('login-error-display-element'); - if (error.message){ - validationFunctions.showError(loginErrorDisplayElement, error.message); - } else { - validationFunctions.showError(loginErrorDisplayElement, 'There was an error logging into your channel'); - } - }) -} diff --git a/public/assets/js/progressBarConstructor.js b/public/assets/js/progressBarConstructor.js deleted file mode 100644 index 468cf178..00000000 --- a/public/assets/js/progressBarConstructor.js +++ /dev/null @@ -1,45 +0,0 @@ -const ProgressBar = function() { - this.data = { - x: 0, - adder: 1, - bars: [], - }; - this.barHolder = document.getElementById('bar-holder'); - this.createProgressBar = function (size) { - this.data['size'] = size; - for (var i = 0; i < size; i++) { - const bar = document.createElement('span'); - bar.innerText = '| '; - bar.setAttribute('class', 'progress-bar progress-bar--inactive'); - this.barHolder.appendChild(bar); - this.data.bars.push(bar); - } - }; - this.startProgressBar = function () { - this.updateInterval = setInterval(this.updateProgressBar.bind(this), 300); - }; - this.updateProgressBar = function () { - const x = this.data.x; - const adder = this.data.adder; - const size = this.data.size; - // update the appropriate bar - if (x > -1 && x < size){ - if (adder === 1){ - this.data.bars[x].setAttribute('class', 'progress-bar progress-bar--active'); - } else { - this.data.bars[x].setAttribute('class', 'progress-bar progress-bar--inactive'); - } - } - // update adder - if (x === size){ - this.data['adder'] = -1; - } else if ( x === -1){ - this.data['adder'] = 1; - } - // update x - this.data['x'] = x + adder; - }; - this.stopProgressBar = function () { - clearInterval(this.updateInterval); - }; -}; \ No newline at end of file diff --git a/public/assets/js/validationFunctions.js b/public/assets/js/validationFunctions.js deleted file mode 100644 index 07c9bad3..00000000 --- a/public/assets/js/validationFunctions.js +++ /dev/null @@ -1,132 +0,0 @@ -// validation function which checks the proposed file's type, size, and name -const validationFunctions = { - validateChannelName: function (name) { - name = name.substring(name.indexOf('@') + 1); - // ensure a name was entered - if (name.length < 1) { - throw new ChannelNameError("You must enter a name for your channel"); - } - // validate the characters in the 'name' field - const invalidCharacters = /[^A-Za-z0-9,-,@]/g.exec(name); - if (invalidCharacters) { - throw new ChannelNameError('"' + invalidCharacters + '" characters are not allowed'); - } - }, - validatePassword: function (password) { - if (password.length < 1) { - throw new ChannelPasswordError("You must enter a password for you channel"); - } - }, - // 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 fetch(url) - .then(function (response) { - return response.json(); - }) - .catch(error => { - console.log('isNameAvailable error', error); - throw error; - }) - }, - showError: function (errorDisplay, errorMsg) { - errorDisplay.hidden = false; - errorDisplay.innerText = errorMsg; - }, - hideError: function (errorDisplay) { - errorDisplay.hidden = true; - errorDisplay.innerText = ''; - }, - showSuccess: function (successElement) { - successElement.hidden = false; - successElement.innerHTML = "✔"; - }, - hideSuccess: function (successElement) { - successElement.hidden = true; - successElement.innerHTML = ""; - }, - 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 - that.validateChannelName(channelName); - // check to make sure it is available - 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); - } - }, - // validation function which checks all aspects of a new channel submission - validateNewChannelSubmission: function (userName, password) { - const channelName = `@${userName}`; - var that = this; - return new Promise(function (resolve, reject) { - // 1. validate name - try { - that.validateChannelName(channelName); - } catch (error) { - return reject(error); - } - // 2. validate password - try { - that.validatePassword(password); - } catch (error) { - return reject(error); - } - // 3. if all validation passes, check availability of the name - that.isChannelNameAvailable(channelName) - .then(function(isAvailable) { - if (isAvailable) { - resolve(); - } else { - reject(new ChannelNameError('Sorry, that name is already taken')); - } - }) - .catch(function(error) { - reject(error); - }); - }); - }, - // validation function which checks all aspects of a new channel login - validateNewChannelLogin: function (userName, password) { - const channelName = `@${userName}`; - var that = this; - return new Promise(function (resolve, reject) { - // 1. validate name - try { - that.validateChannelName(channelName); - } catch (error) { - return reject(error); - } - // 2. validate password - try { - that.validatePassword(password); - } catch (error) { - return reject(error); - } - resolve(); - }); - } -}; diff --git a/react/actions/channel.js b/react/actions/channel.js index a1e34e71..7c17ed94 100644 --- a/react/actions/channel.js +++ b/react/actions/channel.js @@ -5,8 +5,10 @@ import * as actions from 'constants/channel_action_types'; export function updateLoggedInChannel (name, shortId, longId) { return { type: actions.CHANNEL_UPDATE, - name, - shortId, - longId, + data: { + name, + shortId, + longId, + }, }; }; diff --git a/react/actions/publish.js b/react/actions/publish.js index 603aca4e..8afd3556 100644 --- a/react/actions/publish.js +++ b/react/actions/publish.js @@ -4,7 +4,7 @@ import * as actions from 'constants/publish_action_types'; export function selectFile (file) { return { type: actions.FILE_SELECTED, - file: file, + data: file, }; }; @@ -17,15 +17,17 @@ export function clearFile () { export function updateMetadata (name, value) { return { type: actions.METADATA_UPDATE, - name, - value, + data: { + name, + value, + }, }; }; export function updateClaim (value) { return { type: actions.CLAIM_UPDATE, - value, + data: value, }; }; @@ -39,29 +41,33 @@ export function setPublishInChannel (channel) { export function updatePublishStatus (status, message) { return { type: actions.PUBLISH_STATUS_UPDATE, - status, - message, + data: { + status, + message, + }, }; }; export function updateError (name, value) { return { type: actions.ERROR_UPDATE, - name, - value, + data: { + name, + value, + }, }; }; -export function updateSelectedChannel (value) { +export function updateSelectedChannel (channelName) { return { type: actions.SELECTED_CHANNEL_UPDATE, - value, + data: channelName, }; }; -export function toggleMetadataInputs (value) { +export function toggleMetadataInputs (showMetadataInputs) { return { type: actions.TOGGLE_METADATA_INPUTS, - value, + data: showMetadataInputs, }; }; diff --git a/react/actions/show.js b/react/actions/show.js new file mode 100644 index 00000000..3a23b66d --- /dev/null +++ b/react/actions/show.js @@ -0,0 +1,109 @@ +import * as actions from 'constants/show_action_types'; + +import { CHANNEL, ASSET_LITE, ASSET_DETAILS } from 'constants/show_request_types'; + +// basic request parsing +export function handleShowPageUri (params) { + return { + type: actions.HANDLE_SHOW_URI, + data: params, + }; +}; + +export function onRequestError (error) { + return { + type: actions.REQUEST_UPDATE_ERROR, + data: error, + }; +}; + +export function onNewChannelRequest (channelName, channelId) { + const requestType = CHANNEL; + const requestId = `cr#${channelName}#${channelId}`; + return { + type: actions.CHANNEL_REQUEST_NEW, + data: { requestType, requestId, channelName, channelId }, + }; +}; + +export function onNewAssetRequest (name, id, channelName, channelId, extension) { + const requestType = extension ? ASSET_LITE : ASSET_DETAILS; + const requestId = `ar#${name}#${id}#${channelName}#${channelId}`; + return { + type: actions.ASSET_REQUEST_NEW, + data: { + requestType, + requestId, + name, + modifier: { + id, + channel: { + name: channelName, + id : channelId, + }, + }, + }, + }; +}; + +export function addRequestToRequestList (id, error, key) { + return { + type: actions.REQUEST_LIST_ADD, + data: { id, error, key }, + }; +}; + +// asset actions + +export function addAssetToAssetList (id, error, name, claimId, shortId, claimData) { + return { + type: actions.ASSET_ADD, + data: { id, error, name, claimId, shortId, claimData }, + }; +} + +// channel actions + +export function addNewChannelToChannelList (id, name, shortId, longId, claimsData) { + return { + type: actions.CHANNEL_ADD, + data: { id, name, shortId, longId, claimsData }, + }; +}; + +export function onUpdateChannelClaims (channelKey, name, longId, page) { + return { + type: actions.CHANNEL_CLAIMS_UPDATE_ASYNC, + data: {channelKey, name, longId, page}, + }; +}; + +export function updateChannelClaims (channelListId, claimsData) { + return { + type: actions.CHANNEL_CLAIMS_UPDATE_SUCCESS, + data: {channelListId, claimsData}, + }; +}; + +// display a file + +export function fileRequested (name, claimId) { + return { + type: actions.FILE_REQUESTED, + data: { name, claimId }, + }; +}; + +export function updateFileAvailability (status) { + return { + type: actions.FILE_AVAILABILITY_UPDATE, + data: status, + }; +}; + +export function updateDisplayAssetError (error) { + return { + type: actions.DISPLAY_ASSET_ERROR, + data: error, + }; +}; diff --git a/react/api/assetApi.js b/react/api/assetApi.js new file mode 100644 index 00000000..6664a493 --- /dev/null +++ b/react/api/assetApi.js @@ -0,0 +1,39 @@ +import Request from 'utils/request'; + +export function getLongClaimId (name, modifier) { + // console.log('getting long claim id for asset:', name, modifier); + let body = {}; + // create request params + if (modifier) { + if (modifier.id) { + body['claimId'] = modifier.id; + } else { + body['channelName'] = modifier.channel.name; + body['channelClaimId'] = modifier.channel.id; + } + } + body['claimName'] = name; + const params = { + method : 'POST', + headers: new Headers({ + 'Content-Type': 'application/json', + }), + body: JSON.stringify(body), + } + // create url + const url = `/api/claim/long-id`; + // return the request promise + return Request(url, params); +}; + +export function getShortId (name, claimId) { + // console.log('getting short id for asset:', name, claimId); + const url = `/api/claim/short-id/${claimId}/${name}`; + return Request(url); +}; + +export function getClaimData (name, claimId) { + // console.log('getting claim data for asset:', name, claimId); + const url = `/api/claim/data/${name}/${claimId}`; + return Request(url); +}; diff --git a/react/api/channelApi.js b/react/api/channelApi.js new file mode 100644 index 00000000..d060fe2c --- /dev/null +++ b/react/api/channelApi.js @@ -0,0 +1,16 @@ +import Request from 'utils/request'; +import request from '../utils/request'; + +export function getChannelData (name, id) { + console.log('getting channel data for channel:', name, id); + if (!id) id = 'none'; + const url = `/api/channel/data/${name}/${id}`; + return request(url); +}; + +export function getChannelClaims (name, longId, page) { + console.log('getting channel claims for channel:', name, longId); + if (!page) page = 1; + const url = `/api/channel/claims/${name}/${longId}/${page}`; + return Request(url); +}; diff --git a/react/api/fileApi.js b/react/api/fileApi.js new file mode 100644 index 00000000..ecc4a652 --- /dev/null +++ b/react/api/fileApi.js @@ -0,0 +1,11 @@ +import Request from 'utils/request'; + +export function checkFileAvailability (name, claimId) { + const url = `/api/file/availability/${name}/${claimId}`; + return Request(url); +} + +export function triggerClaimGet (name, claimId) { + const url = `/api/claim/get/${name}/${claimId}`; + return Request(url); +} diff --git a/react/app.js b/react/app.js deleted file mode 100644 index d6af0313..00000000 --- a/react/app.js +++ /dev/null @@ -1,26 +0,0 @@ -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( - - - , - document.getElementById('react-nav-bar') -) - -ReactDOM.render( - - - , - document.getElementById('react-publish-tool') -) diff --git a/react/components/AboutPage/index.js b/react/components/AboutPage/index.js new file mode 100644 index 00000000..73ceb2be --- /dev/null +++ b/react/components/AboutPage/index.js @@ -0,0 +1,33 @@ +import React from 'react'; +import NavBar from 'containers/NavBar'; + +class AboutPage extends React.Component { + render () { + return ( +
+ +
+
+
+

Spee.ch is an open-source project. Please contribute to the existing site, or fork it and make your own.

+

TWITTER

+

GITHUB

+

DISCORD CHANNEL

+

DOCUMENTATION

+
+
+
+

Spee.ch is a media-hosting site that reads from and publishes content to the LBRY blockchain.

+

Spee.ch is a hosting service, but with the added benefit that it stores your content on a decentralized network of computers -- the LBRY network. This means that your images are stored in multiple locations without a single point of failure.

+

Contribute

+

If you have an idea for your own spee.ch-like site on top of LBRY, fork our github repo and go to town!

+

If you want to improve spee.ch, join our discord channel or solve one of our github issues.

+
+
+
+
+ ); + } +}; + +export default AboutPage; diff --git a/react/components/AssetDisplay/index.js b/react/components/AssetDisplay/index.js new file mode 100644 index 00000000..a401f98f --- /dev/null +++ b/react/components/AssetDisplay/index.js @@ -0,0 +1,28 @@ +import { connect } from 'react-redux'; +import View from './view'; +import { fileRequested } from 'actions/show'; +import { selectAsset } from 'selectors/show'; + +const mapStateToProps = ({ show }) => { + // select error and status + const error = show.displayAsset.error; + const status = show.displayAsset.status; + // select asset + const asset = selectAsset(show); + // return props + return { + error, + status, + asset, + }; +}; + +const mapDispatchToProps = dispatch => { + return { + onFileRequest: (name, claimId) => { + dispatch(fileRequested(name, claimId)); + }, + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(View); diff --git a/react/components/AssetDisplay/view.jsx b/react/components/AssetDisplay/view.jsx new file mode 100644 index 00000000..ecac5e6d --- /dev/null +++ b/react/components/AssetDisplay/view.jsx @@ -0,0 +1,74 @@ +import React from 'react'; +import ProgressBar from 'components/ProgressBar'; +import { LOCAL_CHECK, UNAVAILABLE, ERROR, AVAILABLE } from 'constants/asset_display_states'; + +class AssetDisplay extends React.Component { + componentDidMount () { + const { asset: { claimData: { name, claimId } } } = this.props; + this.props.onFileRequest(name, claimId); + } + render () { + console.log('rendering assetdisplay', this.props); + const { status, error, asset: { claimData: { name, claimId, contentType, fileExt, thumbnail } } } = this.props; + return ( +
+ {(status === LOCAL_CHECK) && +
+

Checking to see if Spee.ch has your asset locally...

+
+ } + {(status === UNAVAILABLE) && +
+

Sit tight, we're searching the LBRY blockchain for your asset!

+ +

Curious what magic is happening here? Learn more.

+
+ } + {(status === ERROR) && +
+

Unfortunately, we couldn't download your asset from LBRY. You can help us out by sharing the below error message in the LBRY discord.

+

{error}

+
+ } + {(status === AVAILABLE) && + (() => { + switch (contentType) { + case 'image/jpeg': + case 'image/jpg': + case 'image/png': + return ( + {name}/ + ); + case 'image/gif': + return ( + {name} + ); + case 'video/mp4': + return ( + + ); + default: + return ( +

Unsupported file type

+ ); + } + })() + } +
+ ); + } +}; + +export default AssetDisplay; diff --git a/react/components/AssetInfo/index.js b/react/components/AssetInfo/index.js new file mode 100644 index 00000000..32fc3f2f --- /dev/null +++ b/react/components/AssetInfo/index.js @@ -0,0 +1,14 @@ +import { connect } from 'react-redux'; +import View from './view'; +import { selectAsset } from 'selectors/show'; + +const mapStateToProps = ({ show }) => { + // select asset + const asset = selectAsset(show); + // return props + return { + asset, + }; +}; + +export default connect(mapStateToProps, null)(View); diff --git a/react/components/AssetInfo/view.jsx b/react/components/AssetInfo/view.jsx new file mode 100644 index 00000000..0f1f3efe --- /dev/null +++ b/react/components/AssetInfo/view.jsx @@ -0,0 +1,165 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +class AssetInfo extends React.Component { + constructor (props) { + super(props); + this.state = { + showDetails: false, + }; + this.toggleDetails = this.toggleDetails.bind(this); + this.copyToClipboard = this.copyToClipboard.bind(this); + } + toggleDetails () { + if (this.state.showDetails) { + return this.setState({showDetails: false}); + } + this.setState({showDetails: true}); + } + copyToClipboard (event) { + var elementToCopy = event.target.dataset.elementtocopy; + var element = document.getElementById(elementToCopy); + element.select(); + try { + document.execCommand('copy'); + } catch (err) { + this.setState({error: 'Oops, unable to copy'}); + } + } + render () { + const { asset: { shortId, claimData : { channelName, certificateId, description, name, claimId, fileExt, contentType, thumbnail, host } } } = this.props; + return ( +
+ {channelName && +
+
+ Channel: +
+
+ {channelName} +
+
+ } + + {description && +
+ {description} +
+ } + +
+ +
+
+ Embed: +
+
+
+
+ + {(contentType === 'video/mp4') ? ( + `}/> + ) : ( + `} + /> + )} +
+
+
+ +
+
+
+
+
+ +
+
+
+ Share: +
+
+
+ twitter + facebook + tumblr + reddit +
+
+
+
+ + { this.state.showDetails && +
+
+
+
+ Claim Name: +
+ {name} +
+
+
+
+ Claim Id: +
+ {claimId} +
+
+
+
+ File Type: +
+ {contentType ? `${contentType}` : 'unknown'} +
+
+
+
+
+ Report +
+
+
+ } +
+ +
+
+ ); + } +}; + +export default AssetInfo; diff --git a/react/components/AssetPreview/index.js b/react/components/AssetPreview/index.js new file mode 100644 index 00000000..0afe16d4 --- /dev/null +++ b/react/components/AssetPreview/index.js @@ -0,0 +1,39 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +const AssetPreview = ({ name, claimId, fileExt, contentType }) => { + const directSourceLink = `${claimId}/${name}.${fileExt}`; + const showUrlLink = `${claimId}/${name}`; + return ( +
+ + {(() => { + switch (contentType) { + case 'image/jpeg': + case 'image/jpg': + case 'image/png': + return ( + {name}/ + ); + case 'image/gif': + return ( + {name}/ + ); + case 'video/mp4': + return ( + + ); + default: + return ( +

unsupported file type

+ ); + } + })()} + +
+ ); +}; + +export default AssetPreview; diff --git a/react/components/AssetTitle/index.js b/react/components/AssetTitle/index.js new file mode 100644 index 00000000..537d6948 --- /dev/null +++ b/react/components/AssetTitle/index.js @@ -0,0 +1,14 @@ +import { connect } from 'react-redux'; +import View from './view'; +import { selectAsset } from 'selectors/show'; + +const mapStateToProps = ({ show }) => { + // select title + const { claimData: { title } } = selectAsset(show); + // return props + return { + title, + }; +}; + +export default connect(mapStateToProps, null)(View); diff --git a/react/components/AssetTitle/view.jsx b/react/components/AssetTitle/view.jsx new file mode 100644 index 00000000..38e59257 --- /dev/null +++ b/react/components/AssetTitle/view.jsx @@ -0,0 +1,11 @@ +import React from 'react'; + +const AssetTitle = ({ title }) => { + return ( +
+ {title} +
+ ); +}; + +export default AssetTitle; diff --git a/react/components/ErrorPage/index.js b/react/components/ErrorPage/index.js new file mode 100644 index 00000000..3e148105 --- /dev/null +++ b/react/components/ErrorPage/index.js @@ -0,0 +1,23 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import NavBar from 'containers/NavBar'; + +class ErrorPage extends React.Component { + render () { + const { error } = this.props; + return ( +
+ +
+

{error}

+
+
+ ); + } +}; + +ErrorPage.propTypes = { + error: PropTypes.string.isRequired, +} + +export default ErrorPage; diff --git a/react/components/FourOhFourPage/index.js b/react/components/FourOhFourPage/index.js new file mode 100644 index 00000000..6a7c80b9 --- /dev/null +++ b/react/components/FourOhFourPage/index.js @@ -0,0 +1,18 @@ +import React from 'react'; +import NavBar from 'containers/NavBar'; + +class FourOhForPage extends React.Component { + render () { + return ( +
+ +
+

404

+

That page does not exist

+
+
+ ); + } +}; + +export default FourOhForPage; diff --git a/react/components/Logo/index.jsx b/react/components/Logo/index.jsx index 7bc4dd36..e4b0149e 100644 --- a/react/components/Logo/index.jsx +++ b/react/components/Logo/index.jsx @@ -1,9 +1,10 @@ import React from 'react'; +import { Link } from 'react-router-dom'; function Logo () { return ( - + Logo Spee.ch logo @@ -20,7 +21,7 @@ function Logo () { - + ); }; diff --git a/react/components/NavBarChannelOptionsDropdown/index.jsx b/react/components/NavBarChannelOptionsDropdown/index.jsx index 0ea68531..08b285f0 100644 --- a/react/components/NavBarChannelOptionsDropdown/index.jsx +++ b/react/components/NavBarChannelOptionsDropdown/index.jsx @@ -1,8 +1,8 @@ import React from 'react'; -function NavBarChannelOptionsDropdown ({ channelName, handleSelection, VIEW, LOGOUT }) { +function NavBarChannelDropdown ({ channelName, handleSelection, defaultSelection, VIEW, LOGOUT }) { return ( - @@ -10,4 +10,4 @@ function NavBarChannelOptionsDropdown ({ channelName, handleSelection, VIEW, LOG ); }; -export default NavBarChannelOptionsDropdown; +export default NavBarChannelDropdown; diff --git a/react/components/Preview/index.jsx b/react/components/Preview/index.jsx index b67a8144..b0ab1a81 100644 --- a/react/components/Preview/index.jsx +++ b/react/components/Preview/index.jsx @@ -8,7 +8,6 @@ class Preview extends React.Component { imgSource : '', defaultThumbnail: '/assets/img/video_thumb_default.png', }; - this.previewFile = this.previewFile.bind(this); } componentDidMount () { this.previewFile(this.props.file); @@ -22,21 +21,20 @@ class Preview extends React.Component { } } 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}); + previewReader.onloadend = () => { + this.setState({imgSource: previewReader.result}); }; } else { - that.setState({imgSource: (this.props.thumbnail || this.state.defaultThumbnail)}); + this.setState({imgSource: (this.props.thumbnail || this.state.defaultThumbnail)}); } } render () { return ( publish preview + +
+ +
+ + ); + } +}; + +export default PublishPage; diff --git a/react/components/PublishStatus/index.jsx b/react/components/PublishStatus/index.jsx index 1ed8f8dc..e20de9d6 100644 --- a/react/components/PublishStatus/index.jsx +++ b/react/components/PublishStatus/index.jsx @@ -30,7 +30,7 @@ function PublishStatus ({ status, message }) { {(status === publishStates.SUCCESS) &&

Your publish is complete! You are being redirected to it now.

-

If you are not automatically redirected, click here.

+

If you are not automatically redirected, click here.

} {(status === publishStates.FAILED) && diff --git a/react/components/ShowAssetDetails/index.js b/react/components/ShowAssetDetails/index.js new file mode 100644 index 00000000..0af0073c --- /dev/null +++ b/react/components/ShowAssetDetails/index.js @@ -0,0 +1,21 @@ +import { connect } from 'react-redux'; +import View from './view'; + +const mapStateToProps = ({ show }) => { + // select request info + const requestId = show.request.id; + // select asset info + let asset; + const request = show.requestList[requestId] || null; + const assetList = show.assetList; + if (request && assetList) { + const assetKey = request.key; // note: just store this in the request + asset = assetList[assetKey] || null; + }; + // return props + return { + asset, + }; +}; + +export default connect(mapStateToProps, null)(View); diff --git a/react/components/ShowAssetDetails/view.jsx b/react/components/ShowAssetDetails/view.jsx new file mode 100644 index 00000000..42047032 --- /dev/null +++ b/react/components/ShowAssetDetails/view.jsx @@ -0,0 +1,39 @@ +import React from 'react'; +import NavBar from 'containers/NavBar'; +import ErrorPage from 'components/ErrorPage'; +import AssetTitle from 'components/AssetTitle'; +import AssetDisplay from 'components/AssetDisplay'; +import AssetInfo from 'components/AssetInfo'; + +class ShowAssetDetails extends React.Component { + render () { + const { asset } = this.props; + if (asset) { + return ( +
+ +
+
+ +
+
+
+ +
+
+
+ +
+
+
+ } +
+ ); + }; + return ( + + ); + } +}; + +export default ShowAssetDetails; diff --git a/react/components/ShowAssetLite/index.js b/react/components/ShowAssetLite/index.js new file mode 100644 index 00000000..0af0073c --- /dev/null +++ b/react/components/ShowAssetLite/index.js @@ -0,0 +1,21 @@ +import { connect } from 'react-redux'; +import View from './view'; + +const mapStateToProps = ({ show }) => { + // select request info + const requestId = show.request.id; + // select asset info + let asset; + const request = show.requestList[requestId] || null; + const assetList = show.assetList; + if (request && assetList) { + const assetKey = request.key; // note: just store this in the request + asset = assetList[assetKey] || null; + }; + // return props + return { + asset, + }; +}; + +export default connect(mapStateToProps, null)(View); diff --git a/react/components/ShowAssetLite/view.jsx b/react/components/ShowAssetLite/view.jsx new file mode 100644 index 00000000..e8567fbc --- /dev/null +++ b/react/components/ShowAssetLite/view.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import AssetDisplay from 'components/AssetDisplay'; + +class ShowLite extends React.Component { + render () { + const { asset } = this.props; + return ( +
+ { (asset) && +
+ + hosted via Spee.ch +
+ } +
+ ); + } +}; + +export default ShowLite; diff --git a/react/components/ShowChannel/index.js b/react/components/ShowChannel/index.js new file mode 100644 index 00000000..120f75b4 --- /dev/null +++ b/react/components/ShowChannel/index.js @@ -0,0 +1,20 @@ +import { connect } from 'react-redux'; +import View from './view'; + +const mapStateToProps = ({ show }) => { + // select request info + const requestId = show.request.id; + // select request + const previousRequest = show.requestList[requestId] || null; + // select channel + let channel; + if (previousRequest) { + const channelKey = previousRequest.key; + channel = show.channelList[channelKey] || null; + } + return { + channel, + }; +}; + +export default connect(mapStateToProps, null)(View); diff --git a/react/components/ShowChannel/view.jsx b/react/components/ShowChannel/view.jsx new file mode 100644 index 00000000..3867cde2 --- /dev/null +++ b/react/components/ShowChannel/view.jsx @@ -0,0 +1,33 @@ +import React from 'react'; +import ErrorPage from 'components/ErrorPage'; +import NavBar from 'containers/NavBar'; +import ChannelClaimsDisplay from 'containers/ChannelClaimsDisplay'; + +class ShowChannel extends React.Component { + render () { + const { channel } = this.props; + if (channel) { + const { name, longId, shortId } = channel; + return ( +
+ +
+
+

channel name: {name || 'loading...'}

+

full channel id: {longId || 'loading...'}

+

short channel id: {shortId || 'loading...'}

+
+
+ +
+
+
+ ); + }; + return ( + + ); + } +}; + +export default ShowChannel; diff --git a/react/constants/asset_display_states.js b/react/constants/asset_display_states.js new file mode 100644 index 00000000..910c7848 --- /dev/null +++ b/react/constants/asset_display_states.js @@ -0,0 +1,4 @@ +export const LOCAL_CHECK = 'LOCAL_CHECK'; +export const UNAVAILABLE = 'UNAVAILABLE'; +export const ERROR = 'ERROR'; +export const AVAILABLE = 'AVAILABLE'; diff --git a/react/constants/show_action_types.js b/react/constants/show_action_types.js new file mode 100644 index 00000000..5137d13b --- /dev/null +++ b/react/constants/show_action_types.js @@ -0,0 +1,20 @@ +// request actions +export const HANDLE_SHOW_URI = 'HANDLE_SHOW_URI'; +export const REQUEST_UPDATE_ERROR = 'REQUEST_UPDATE_ERROR'; +export const ASSET_REQUEST_NEW = 'ASSET_REQUEST_NEW'; +export const CHANNEL_REQUEST_NEW = 'CHANNEL_REQUEST_NEW'; +export const REQUEST_LIST_ADD = 'REQUEST_LIST_ADD'; + +// asset actions +export const ASSET_ADD = `ASSET_ADD`; + +// channel actions +export const CHANNEL_ADD = 'CHANNEL_ADD'; + +export const CHANNEL_CLAIMS_UPDATE_ASYNC = 'CHANNEL_CLAIMS_UPDATE_ASYNC'; +export const CHANNEL_CLAIMS_UPDATE_SUCCESS = 'CHANNEL_CLAIMS_UPDATE_SUCCESS'; + +// asset/file display actions +export const FILE_REQUESTED = 'FILE_REQUESTED'; +export const FILE_AVAILABILITY_UPDATE = 'FILE_AVAILABILITY_UPDATE'; +export const DISPLAY_ASSET_ERROR = 'DISPLAY_ASSET_ERROR'; diff --git a/react/constants/show_request_types.js b/react/constants/show_request_types.js new file mode 100644 index 00000000..d5fbed67 --- /dev/null +++ b/react/constants/show_request_types.js @@ -0,0 +1,3 @@ +export const CHANNEL = 'CHANNEL'; +export const ASSET_LITE = 'ASSET_LITE'; +export const ASSET_DETAILS = 'ASSET_DETAILS'; diff --git a/react/containers/ChannelClaimsDisplay/index.js b/react/containers/ChannelClaimsDisplay/index.js new file mode 100644 index 00000000..8a943f14 --- /dev/null +++ b/react/containers/ChannelClaimsDisplay/index.js @@ -0,0 +1,22 @@ +import { connect } from 'react-redux'; +import { onUpdateChannelClaims } from 'actions/show'; +import View from './view'; + +const mapStateToProps = ({ show }) => { + // select channel key + const request = show.requestList[show.request.id]; + const channelKey = request.key; + // select channel claims + const channel = show.channelList[channelKey] || null; + // return props + return { + channelKey, + channel, + }; +}; + +const mapDispatchToProps = { + onUpdateChannelClaims, +}; + +export default connect(mapStateToProps, mapDispatchToProps)(View); diff --git a/react/containers/ChannelClaimsDisplay/view.jsx b/react/containers/ChannelClaimsDisplay/view.jsx new file mode 100644 index 00000000..fd450268 --- /dev/null +++ b/react/containers/ChannelClaimsDisplay/view.jsx @@ -0,0 +1,54 @@ +import React from 'react'; +import AssetPreview from 'components/AssetPreview'; + +class ChannelClaimsDisplay extends React.Component { + constructor (props) { + super(props); + this.showNextResultsPage = this.showNextResultsPage.bind(this); + this.showPreviousResultsPage = this.showPreviousResultsPage.bind(this); + } + showPreviousResultsPage () { + const { channel: { claimsData: { currentPage } } } = this.props; + const previousPage = parseInt(currentPage) - 1; + this.showNewPage(previousPage); + } + showNextResultsPage () { + const { channel: { claimsData: { currentPage } } } = this.props; + const nextPage = parseInt(currentPage) + 1; + this.showNewPage(nextPage); + } + showNewPage (page) { + const { channelKey, channel: { name, longId } } = this.props; + this.props.onUpdateChannelClaims(channelKey, name, longId, page); + } + render () { + const { channel: { claimsData: { claims, currentPage, totalPages } } } = this.props; + return ( +
+ {(claims.length > 0) ? ( +
+ {claims.map((claim, index) => )} +
+ {(currentPage > 1) && + + } + {(currentPage < totalPages) && + + } +
+
+ ) : ( +

There are no claims in this channel

+ )} +
+ ); + } +}; + +export default ChannelClaimsDisplay; diff --git a/react/containers/ChannelCreateForm/view.jsx b/react/containers/ChannelCreateForm/view.jsx index 8d0924f5..69fed75d 100644 --- a/react/containers/ChannelCreateForm/view.jsx +++ b/react/containers/ChannelCreateForm/view.jsx @@ -11,13 +11,8 @@ class ChannelCreateForm extends React.Component { 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) { @@ -41,24 +36,23 @@ class ChannelCreateForm extends React.Component { this.setState({[name]: value}); } updateIsChannelAvailable (channel) { - const that = this; const channelWithAtSymbol = `@${channel}`; - request(`/api/channel-is-available/${channelWithAtSymbol}`) + request(`/api/channel/availability/${channelWithAtSymbol}`) .then(isAvailable => { if (isAvailable) { - that.setState({'error': null}); + this.setState({'error': null}); } else { - that.setState({'error': 'That channel has already been claimed'}); + this.setState({'error': 'That channel has already been claimed'}); } }) .catch((error) => { - that.setState({'error': error.message}); + this.setState({'error': error.message}); }); } checkIsChannelAvailable (channel) { const channelWithAtSymbol = `@${channel}`; return new Promise((resolve, reject) => { - request(`/api/channel-is-available/${channelWithAtSymbol}`) + request(`/api/channel/availability/${channelWithAtSymbol}`) .then(isAvailable => { console.log('checkIsChannelAvailable result:', isAvailable); if (!isAvailable) { @@ -105,21 +99,20 @@ class ChannelCreateForm extends React.Component { } createChannel (event) { event.preventDefault(); - const that = this; this.checkIsPasswordProvided() .then(() => { - return that.checkIsChannelAvailable(that.state.channel, that.state.password); + return this.checkIsChannelAvailable(this.state.channel, this.state.password); }) .then(() => { - that.setState({status: 'We are publishing your new channel. Sit tight...'}); - return that.makePublishChannelRequest(that.state.channel, that.state.password); + this.setState({status: 'We are publishing your new channel. Sit tight...'}); + return this.makePublishChannelRequest(this.state.channel, this.state.password); }) .then(result => { - that.setState({status: null}); - that.props.onChannelLogin(result.channelName, result.shortChannelId, result.channelClaimId); + this.setState({status: null}); + this.props.onChannelLogin(result.channelName, result.shortChannelId, result.channelClaimId); }) .catch((error) => { - that.setState({'error': error.message, status: null}); + this.setState({'error': error.message, status: null}); }); } render () { diff --git a/react/containers/ChannelLoginForm/view.jsx b/react/containers/ChannelLoginForm/view.jsx index dde7d46c..ad98f9a5 100644 --- a/react/containers/ChannelLoginForm/view.jsx +++ b/react/containers/ChannelLoginForm/view.jsx @@ -27,22 +27,21 @@ class ChannelLoginForm extends React.Component { }), 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); + this.props.onChannelLogin(channelName, shortChannelId, channelClaimId); } else { - that.setState({'error': message}); + this.setState({'error': message}); }; }) .catch(error => { console.log('login error', error); if (error.message) { - that.setState({'error': error.message}); + this.setState({'error': error.message}); } else { - that.setState({'error': error}); + this.setState({'error': error}); } }); } diff --git a/react/containers/ChannelSelect/index.js b/react/containers/ChannelSelect/index.js index b7766609..a401fe38 100644 --- a/react/containers/ChannelSelect/index.js +++ b/react/containers/ChannelSelect/index.js @@ -1,6 +1,6 @@ import {connect} from 'react-redux'; import {setPublishInChannel, updateSelectedChannel, updateError} from 'actions/publish'; -import View from './view.jsx'; +import View from './view'; const mapStateToProps = ({ channel, publish }) => { return { diff --git a/react/containers/LoginPage/index.js b/react/containers/LoginPage/index.js new file mode 100644 index 00000000..2e193098 --- /dev/null +++ b/react/containers/LoginPage/index.js @@ -0,0 +1,10 @@ +import {connect} from 'react-redux'; +import View from './view'; + +const mapStateToProps = ({ channel }) => { + return { + loggedInChannelName: channel.loggedInChannel.name, + }; +}; + +export default connect(mapStateToProps, null)(View); diff --git a/react/containers/LoginPage/view.jsx b/react/containers/LoginPage/view.jsx new file mode 100644 index 00000000..dea4f950 --- /dev/null +++ b/react/containers/LoginPage/view.jsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { withRouter } from 'react-router-dom'; +import NavBar from 'containers/NavBar'; +import ChannelLoginForm from 'containers/ChannelLoginForm'; +import ChannelCreateForm from 'containers/ChannelCreateForm'; + +class PublishPage extends React.Component { + componentWillReceiveProps (newProps) { + // re-route the user to the homepage if the user is logged in + if (newProps.loggedInChannelName !== this.props.loggedInChannelName) { + console.log('user logged into new channel:', newProps.loggedInChannelName); + this.props.history.push(`/`); + } + } + render () { + return ( +
+ +
+
+
+

Channels allow you to publish and group content under an identity. You can create a channel for yourself, or share one with like-minded friends. You can create 1 channel, or 100, so whether you're documenting important events, or making a public repository for cat gifs (password: '1234'), try creating a channel for it!

+
+
+
+

Log in to an existing channel:

+ +

Create a brand new channel:

+ +
+
+
+
+ ); + } +}; + +export default withRouter(PublishPage); diff --git a/react/containers/NavBar/index.js b/react/containers/NavBar/index.js index 82a6080d..80689d97 100644 --- a/react/containers/NavBar/index.js +++ b/react/containers/NavBar/index.js @@ -17,6 +17,9 @@ const mapDispatchToProps = dispatch => { dispatch(updateLoggedInChannel(name, shortId, longId)); dispatch(updateSelectedChannel(name)); }, + onChannelLogout: () => { + dispatch(updateLoggedInChannel(null, null, null)); + }, }; }; diff --git a/react/containers/NavBar/view.jsx b/react/containers/NavBar/view.jsx index 2ecf395f..f8546817 100644 --- a/react/containers/NavBar/view.jsx +++ b/react/containers/NavBar/view.jsx @@ -1,7 +1,8 @@ import React from 'react'; -import request from 'utils/request'; +import { NavLink, withRouter } from 'react-router-dom'; import Logo from 'components/Logo'; import NavBarChannelDropdown from 'components/NavBarChannelOptionsDropdown'; +import request from 'utils/request'; const VIEW = 'VIEW'; const LOGOUT = 'LOGOUT'; @@ -18,25 +19,24 @@ class NavBar extends React.Component { this.checkForLoggedInUser(); } checkForLoggedInUser () { - // check for whether a channel is already logged in - const params = { - credentials: 'include', - } + 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'); - } + .then(({ data }) => { + this.props.onChannelLogin(data.channelName, data.shortChannelId, data.channelClaimId); }) .catch(error => { - console.log('authenticate user errored:', error); + console.log('/user error:', error.message); }); } logoutUser () { - // send logout request to server - window.location.href = '/logout'; // NOTE: replace with a call to the server + const params = {credentials: 'include'}; + request('/logout', params) + .then(() => { + this.props.onChannelLogout(); + }) + .catch(error => { + console.log('/logout error', error.message); + }); } handleSelection (event) { console.log('handling selection', event); @@ -48,7 +48,7 @@ class NavBar extends React.Component { break; case VIEW: // redirect to channel page - window.location.href = `/${this.props.channelName}:${this.props.channelLongId}`; + this.props.history.push(`/${this.props.channelName}:${this.props.channelLongId}`); break; default: break; @@ -63,17 +63,18 @@ class NavBar extends React.Component { Open-source, decentralized image and video sharing.
- Publish - About + Publish + About { this.props.channelName ? ( ) : ( - Channel + Channel )}
@@ -82,4 +83,4 @@ class NavBar extends React.Component { } } -export default NavBar; +export default withRouter(NavBar); diff --git a/react/containers/PublishForm/view.jsx b/react/containers/PublishForm/view.jsx index 86b952bc..366e86ba 100644 --- a/react/containers/PublishForm/view.jsx +++ b/react/containers/PublishForm/view.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import { withRouter } from 'react-router-dom'; import Dropzone from 'containers/Dropzone'; import PublishTitleInput from 'containers/PublishTitleInput'; import PublishUrlInput from 'containers/PublishUrlInput'; @@ -10,9 +11,7 @@ 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.makePublishRequest = this.makePublishRequest.bind(this); this.publish = this.publish.bind(this); } validateChannelSelection () { @@ -49,37 +48,33 @@ class PublishForm extends React.Component { } makePublishRequest (file, metadata) { console.log('making publish request'); - const uri = '/api/claim-publish'; + 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('loadstart', () => { + this.props.onPublishStatusChange(publishStates.LOAD_START, 'upload started'); }); - xhr.upload.addEventListener('progress', function (e) { + xhr.upload.addEventListener('progress', (e) => { if (e.lengthComputable) { const percentage = Math.round((e.loaded * 100) / e.total); console.log('progress:', percentage); - that.props.onPublishStatusChange(publishStates.LOADING, `${percentage}%`); + this.props.onPublishStatusChange(publishStates.LOADING, `${percentage}%`); } }, false); - xhr.upload.addEventListener('load', function () { + xhr.upload.addEventListener('load', () => { console.log('loaded 100%'); - that.props.onPublishStatusChange(publishStates.PUBLISHING, null); + this.props.onPublishStatusChange(publishStates.PUBLISHING, null); }, false); xhr.open('POST', uri, true); - xhr.onreadystatechange = function () { + xhr.onreadystatechange = () => { 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.'); + const response = JSON.parse(xhr.response); + console.log('publish response:', response); + if ((xhr.status === 200) && response.success) { + this.props.history.push(`/${response.data.claimId}/${response.data.name}`); + this.props.onPublishStatusChange(publishStates.SUCCESS, response.data.url); } else { - that.props.onPublishStatusChange(publishStates.FAILED, JSON.parse(xhr.response).message); + this.props.onPublishStatusChange(publishStates.FAILED, response.message); } } }; @@ -107,7 +102,6 @@ class PublishForm extends React.Component { 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]); } } @@ -116,21 +110,20 @@ class PublishForm extends React.Component { publish () { console.log('publishing file'); // publish the asset - const that = this; this.validateChannelSelection() .then(() => { - return that.validatePublishParams(); + return this.validatePublishParams(); }) .then(() => { - const metadata = that.createMetadata(); + const metadata = this.createMetadata(); // publish the claim - return that.makePublishRequest(that.props.file, metadata); + return this.makePublishRequest(this.props.file, metadata); }) .then(() => { - that.props.onPublishStatusChange('publish request made'); + this.props.onPublishStatusChange('publish request made'); }) .catch((error) => { - that.props.onPublishSubmitError(error.message); + this.props.onPublishSubmitError(error.message); }); } render () { @@ -176,4 +169,4 @@ class PublishForm extends React.Component { } }; -export default PublishForm; +export default withRouter(PublishForm); diff --git a/react/containers/PublishMetadataInputs/view.jsx b/react/containers/PublishMetadataInputs/view.jsx index 8ef2d3c5..85bcb29e 100644 --- a/react/containers/PublishMetadataInputs/view.jsx +++ b/react/containers/PublishMetadataInputs/view.jsx @@ -65,7 +65,7 @@ class PublishMetadataInputs extends React.Component { )} - {this.props.showMetadataInputs ? '[less]' : '[more]'} + ); } diff --git a/react/containers/PublishThumbnailInput/view.jsx b/react/containers/PublishThumbnailInput/view.jsx index 2ddddb6e..139465b4 100644 --- a/react/containers/PublishThumbnailInput/view.jsx +++ b/react/containers/PublishThumbnailInput/view.jsx @@ -9,8 +9,6 @@ class PublishThumbnailInput extends React.Component { 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) { @@ -38,22 +36,21 @@ class PublishThumbnailInput extends React.Component { } 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}); + this.props.onThumbnailChange('thumbnail', imageUrl); + this.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'}); + this.props.onThumbnailChange('thumbnail', null); + this.setState({thumbnailError: 'That is an invalid image url'}); }); } else { - that.props.onThumbnailChange('thumbnail', null); - that.setState({thumbnailError: null}); + this.props.onThumbnailChange('thumbnail', null); + this.setState({thumbnailError: null}); } } render () { diff --git a/react/containers/PublishUrlInput/view.jsx b/react/containers/PublishUrlInput/view.jsx index 21e06420..38ed7c91 100644 --- a/react/containers/PublishUrlInput/view.jsx +++ b/react/containers/PublishUrlInput/view.jsx @@ -6,9 +6,6 @@ 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 === '') { @@ -40,18 +37,17 @@ class PublishUrlInput extends React.Component { this.props.onClaimChange(cleanClaimName); } checkClaimIsAvailable (claim) { - const that = this; - request(`/api/claim-is-available/${claim}`) + request(`/api/claim/availability/${claim}`) .then(isAvailable => { // console.log('checkClaimIsAvailable request response:', isAvailable); if (isAvailable) { - that.props.onUrlError(null); + this.props.onUrlError(null); } else { - that.props.onUrlError('That url has already been claimed'); + this.props.onUrlError('That url has already been claimed'); } }) .catch((error) => { - that.props.onUrlError(error.message); + this.props.onUrlError(error.message); }); } render () { diff --git a/react/containers/ShowPage/index.js b/react/containers/ShowPage/index.js new file mode 100644 index 00000000..077d00c2 --- /dev/null +++ b/react/containers/ShowPage/index.js @@ -0,0 +1,16 @@ +import { connect } from 'react-redux'; +import { handleShowPageUri } from 'actions/show'; +import View from './view'; + +const mapStateToProps = ({ show }) => { + return { + error : show.request.error, + requestType: show.request.type, + }; +}; + +const mapDispatchToProps = { + handleShowPageUri, +}; + +export default connect(mapStateToProps, mapDispatchToProps)(View); diff --git a/react/containers/ShowPage/view.jsx b/react/containers/ShowPage/view.jsx new file mode 100644 index 00000000..cf565f02 --- /dev/null +++ b/react/containers/ShowPage/view.jsx @@ -0,0 +1,38 @@ +import React from 'react'; +import ErrorPage from 'components/ErrorPage'; +import ShowAssetLite from 'components/ShowAssetLite'; +import ShowAssetDetails from 'components/ShowAssetDetails'; +import ShowChannel from 'components/ShowChannel'; + +import { CHANNEL, ASSET_LITE, ASSET_DETAILS } from 'constants/show_request_types'; + +class ShowPage extends React.Component { + componentDidMount () { + this.props.handleShowPageUri(this.props.match.params); + } + componentWillReceiveProps (nextProps) { + if (nextProps.match.params !== this.props.match.params) { + this.props.handleShowPageUri(nextProps.match.params); + } + } + render () { + const { error, requestType } = this.props; + if (error) { + return ( + + ); + } + switch (requestType) { + case CHANNEL: + return ; + case ASSET_LITE: + return ; + case ASSET_DETAILS: + return ; + default: + return

loading...

; + } + } +}; + +export default ShowPage; diff --git a/react/index.js b/react/index.js new file mode 100644 index 00000000..239d0072 --- /dev/null +++ b/react/index.js @@ -0,0 +1,24 @@ +import React from 'react'; +import { render } from 'react-dom'; +import { createStore, applyMiddleware, compose } from 'redux'; +import Reducer from 'reducers'; +import createSagaMiddleware from 'redux-saga'; +import rootSaga from 'sagas'; +import Root from './root'; + +const sagaMiddleware = createSagaMiddleware(); +const middleware = applyMiddleware(sagaMiddleware); + +const enhancer = window.__REDUX_DEVTOOLS_EXTENSION__ ? compose(middleware, window.__REDUX_DEVTOOLS_EXTENSION__()) : middleware; + +let store = createStore( + Reducer, + enhancer, +); + +sagaMiddleware.run(rootSaga); + +render( + , + document.getElementById('react-app') +); diff --git a/react/reducers/channel.js b/react/reducers/channel.js index fcf1f33c..a3b811c0 100644 --- a/react/reducers/channel.js +++ b/react/reducers/channel.js @@ -8,19 +8,11 @@ const initialState = { }, }; -/* -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, - }, + loggedInChannel: action.data, }); default: return state; diff --git a/react/reducers/index.js b/react/reducers/index.js index ee000700..c80273f3 100644 --- a/react/reducers/index.js +++ b/react/reducers/index.js @@ -1,8 +1,10 @@ import { combineReducers } from 'redux'; import PublishReducer from 'reducers/publish'; import ChannelReducer from 'reducers/channel'; +import ShowReducer from 'reducers/show'; export default combineReducers({ channel: ChannelReducer, publish: PublishReducer, + show : ShowReducer, }); diff --git a/react/reducers/publish.js b/react/reducers/publish.js index 1195a37d..c23e6e2b 100644 --- a/react/reducers/publish.js +++ b/react/reducers/publish.js @@ -26,27 +26,23 @@ const initialState = { }, }; -/* -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, + file: action.data, }); case actions.FILE_CLEAR: return initialState; case actions.METADATA_UPDATE: return Object.assign({}, state, { metadata: Object.assign({}, state.metadata, { - [action.name]: action.value, + [action.data.name]: action.data.value, }), }); case actions.CLAIM_UPDATE: return Object.assign({}, state, { - claim: action.value, + claim: action.data, }); case actions.SET_PUBLISH_IN_CHANNEL: return Object.assign({}, state, { @@ -54,24 +50,21 @@ export default function (state = initialState, action) { }); case actions.PUBLISH_STATUS_UPDATE: return Object.assign({}, state, { - status: Object.assign({}, state.status, { - status : action.status, - message: action.message, - }), + status: action.data, }); case actions.ERROR_UPDATE: return Object.assign({}, state, { error: Object.assign({}, state.error, { - [action.name]: action.value, + [action.data.name]: action.data.value, }), }); case actions.SELECTED_CHANNEL_UPDATE: return Object.assign({}, state, { - selectedChannel: action.value, + selectedChannel: action.data, }); case actions.TOGGLE_METADATA_INPUTS: return Object.assign({}, state, { - showMetadataInputs: action.value, + showMetadataInputs: action.data, }); default: return state; diff --git a/react/reducers/show.js b/react/reducers/show.js new file mode 100644 index 00000000..114fe06c --- /dev/null +++ b/react/reducers/show.js @@ -0,0 +1,96 @@ +import * as actions from 'constants/show_action_types'; +import { LOCAL_CHECK, ERROR } from 'constants/asset_display_states'; + +const initialState = { + request: { + error: null, + type : null, + id : null, + }, + requestList : {}, + channelList : {}, + assetList : {}, + displayAsset: { + error : null, + status: LOCAL_CHECK, + }, +}; + +export default function (state = initialState, action) { + switch (action.type) { + // handle request + case actions.REQUEST_UPDATE_ERROR: + return Object.assign({}, state, { + request: Object.assign({}, state.request, { + error: action.data, + }), + }); + case actions.CHANNEL_REQUEST_NEW: + case actions.ASSET_REQUEST_NEW: + return Object.assign({}, state, { + request: Object.assign({}, state.request, { + type: action.data.requestType, + id : action.data.requestId, + }), + }); + // store requests + case actions.REQUEST_LIST_ADD: + return Object.assign({}, state, { + requestList: Object.assign({}, state.requestList, { + [action.data.id]: { + error: action.data.error, + key : action.data.key, + }, + }), + }); + // asset data + case actions.ASSET_ADD: + return Object.assign({}, state, { + assetList: Object.assign({}, state.assetList, { + [action.data.id]: { + error : action.data.error, + name : action.data.name, + claimId : action.data.claimId, + shortId : action.data.shortId, + claimData: action.data.claimData, + }, + }), + }); + // channel data + case actions.CHANNEL_ADD: + return Object.assign({}, state, { + channelList: Object.assign({}, state.channelList, { + [action.data.id]: { + name : action.data.name, + longId : action.data.longId, + shortId : action.data.shortId, + claimsData: action.data.claimsData, + }, + }), + }); + case actions.CHANNEL_CLAIMS_UPDATE_SUCCESS: + return Object.assign({}, state, { + channelList: Object.assign({}, state.channelList, { + [action.data.channelListId]: Object.assign({}, state.channelList[action.data.channelListId], { + claimsData: action.data.claimsData, + }), + }), + }); + // display an asset + case actions.FILE_AVAILABILITY_UPDATE: + return Object.assign({}, state, { + displayAsset: Object.assign({}, state.displayAsset, { + status: action.data, + }), + }); + case actions.DISPLAY_ASSET_ERROR: + return Object.assign({}, state, { + displayAsset: Object.assign({}, state.displayAsset, { + error : action.data, + status: ERROR, + }), + }); + default: + return state; + } +} diff --git a/react/root.js b/react/root.js new file mode 100644 index 00000000..86c1f53d --- /dev/null +++ b/react/root.js @@ -0,0 +1,31 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Provider } from 'react-redux'; +import { BrowserRouter, Route, Switch } from 'react-router-dom'; + +import PublishPage from 'components/PublishPage'; +import AboutPage from 'components/AboutPage'; +import LoginPage from 'containers/LoginPage'; +import ShowPage from 'containers/ShowPage'; +import FourOhFourPage from 'components/FourOhFourPage'; + +const Root = ({ store }) => ( + + + + + + + + + + + + +); + +Root.propTypes = { + store: PropTypes.object.isRequired, +}; + +export default Root; diff --git a/react/sagas/file.js b/react/sagas/file.js new file mode 100644 index 00000000..66639b23 --- /dev/null +++ b/react/sagas/file.js @@ -0,0 +1,33 @@ +import { call, put, takeLatest } from 'redux-saga/effects'; +import * as actions from 'constants/show_action_types'; +import { updateFileAvailability, updateDisplayAssetError } from 'actions/show'; +import { UNAVAILABLE, AVAILABLE } from 'constants/asset_display_states'; +import { checkFileAvailability, triggerClaimGet } from 'api/fileApi'; + +function* retrieveFile (action) { + const name = action.data.name; + const claimId = action.data.claimId; + // see if the file is available + let isAvailable; + try { + ({ data: isAvailable } = yield call(checkFileAvailability, name, claimId)); + } catch (error) { + return yield put(updateDisplayAssetError(error.message)); + }; + if (isAvailable) { + yield put(updateDisplayAssetError(null)); + return yield put(updateFileAvailability(AVAILABLE)); + } + yield put(updateFileAvailability(UNAVAILABLE)); + // initiate get request for the file + try { + yield call(triggerClaimGet, name, claimId); + } catch (error) { + return yield put(updateDisplayAssetError(error.message)); + }; + yield put(updateFileAvailability(AVAILABLE)); +}; + +export function* watchFileIsRequested () { + yield takeLatest(actions.FILE_REQUESTED, retrieveFile); +}; diff --git a/react/sagas/index.js b/react/sagas/index.js new file mode 100644 index 00000000..e2b808c2 --- /dev/null +++ b/react/sagas/index.js @@ -0,0 +1,15 @@ +import { all } from 'redux-saga/effects'; +import { watchHandleShowPageUri } from './show_uri'; +import { watchNewAssetRequest } from './show_asset'; +import { watchNewChannelRequest, watchUpdateChannelClaims } from './show_channel'; +import { watchFileIsRequested } from './file'; + +export default function* rootSaga () { + yield all([ + watchHandleShowPageUri(), + watchNewAssetRequest(), + watchNewChannelRequest(), + watchUpdateChannelClaims(), + watchFileIsRequested(), + ]); +} diff --git a/react/sagas/show_asset.js b/react/sagas/show_asset.js new file mode 100644 index 00000000..62d9995f --- /dev/null +++ b/react/sagas/show_asset.js @@ -0,0 +1,57 @@ +import { call, put, select, takeLatest } from 'redux-saga/effects'; +import * as actions from 'constants/show_action_types'; +import { addRequestToRequestList, onRequestError, addAssetToAssetList } from 'actions/show'; +import { getLongClaimId, getShortId, getClaimData } from 'api/assetApi'; +import { selectShowState } from 'selectors/show'; + +function* newAssetRequest (action) { + const { requestId, name, modifier } = action.data; + const state = yield select(selectShowState); + // is this an existing request? + // If this uri is in the request list, it's already been fetched + if (state.requestList[requestId]) { + console.log('that request already exists in the request list!'); + return null; + } + // get long id && add request to request list + console.log(`getting asset long id ${name}`); + let longId; + try { + ({data: longId} = yield call(getLongClaimId, name, modifier)); + } catch (error) { + console.log('error:', error); + return yield put(onRequestError(error.message)); + } + const assetKey = `a#${name}#${longId}`; + yield put(addRequestToRequestList(requestId, null, assetKey)); + // is this an existing asset? + // If this asset is in the asset list, it's already been fetched + if (state.assetList[assetKey]) { + console.log('that asset already exists in the asset list!'); + return null; + } + // get short Id + console.log(`getting asset short id ${name} ${longId}`); + let shortId; + try { + ({data: shortId} = yield call(getShortId, name, longId)); + } catch (error) { + return yield put(onRequestError(error.message)); + } + // get asset claim data + console.log(`getting asset claim data ${name} ${longId}`); + let claimData; + try { + ({data: claimData} = yield call(getClaimData, name, longId)); + } catch (error) { + return yield put(onRequestError(error.message)); + } + // add asset to asset list + yield put(addAssetToAssetList(assetKey, null, name, longId, shortId, claimData)); + // clear any errors in request error + yield put(onRequestError(null)); +}; + +export function* watchNewAssetRequest () { + yield takeLatest(actions.ASSET_REQUEST_NEW, newAssetRequest); +}; diff --git a/react/sagas/show_channel.js b/react/sagas/show_channel.js new file mode 100644 index 00000000..34d7ecd3 --- /dev/null +++ b/react/sagas/show_channel.js @@ -0,0 +1,64 @@ +import {call, put, select, takeLatest} from 'redux-saga/effects'; +import * as actions from 'constants/show_action_types'; +import { addNewChannelToChannelList, addRequestToRequestList, onRequestError, updateChannelClaims } from 'actions/show'; +import { getChannelClaims, getChannelData } from 'api/channelApi'; +import { selectShowState } from 'selectors/show'; + +function* getNewChannelAndUpdateChannelList (action) { + const { requestId, channelName, channelId } = action.data; + const state = yield select(selectShowState); + // is this an existing request? + // If this uri is in the request list, it's already been fetched + if (state.requestList[requestId]) { + console.log('that request already exists in the request list!'); + return null; + } + // get channel long id + console.log('getting channel long id and short id'); + let longId, shortId; + try { + ({ data: {longChannelClaimId: longId, shortChannelClaimId: shortId} } = yield call(getChannelData, channelName, channelId)); + } catch (error) { + return yield put(onRequestError(error.message)); + } + // store the request in the channel requests list + const channelKey = `c#${channelName}#${longId}`; + yield put(addRequestToRequestList(requestId, null, channelKey)); + // is this an existing channel? + // If this channel is in the channel list, it's already been fetched + if (state.channelList[channelKey]) { + console.log('that channel already exists in the channel list!'); + return null; + } + // get channel claims data + console.log('getting channel claims data'); + let claimsData; + try { + ({ data: claimsData } = yield call(getChannelClaims, channelName, longId, 1)); + } catch (error) { + return yield put(onRequestError(error.message)); + } + // store the channel data in the channel list + yield put(addNewChannelToChannelList(channelKey, channelName, shortId, longId, claimsData)); + // clear any request errors + yield put(onRequestError(null)); +} + +export function* watchNewChannelRequest () { + yield takeLatest(actions.CHANNEL_REQUEST_NEW, getNewChannelAndUpdateChannelList); +}; + +function* getNewClaimsAndUpdateChannel (action) { + const { channelKey, name, longId, page } = action.data; + let claimsData; + try { + ({ data: claimsData } = yield call(getChannelClaims, name, longId, page)); + } catch (error) { + return yield put(onRequestError(error.message)); + } + yield put(updateChannelClaims(channelKey, claimsData)); +} + +export function* watchUpdateChannelClaims () { + yield takeLatest(actions.CHANNEL_CLAIMS_UPDATE_ASYNC, getNewClaimsAndUpdateChannel); +} diff --git a/react/sagas/show_uri.js b/react/sagas/show_uri.js new file mode 100644 index 00000000..0ac519ee --- /dev/null +++ b/react/sagas/show_uri.js @@ -0,0 +1,60 @@ +import { call, put, takeLatest } from 'redux-saga/effects'; +import * as actions from 'constants/show_action_types'; +import { onRequestError, onNewChannelRequest, onNewAssetRequest } from 'actions/show'; +import lbryUri from 'utils/lbryUri'; + +function* parseAndUpdateIdentifierAndClaim (modifier, claim) { + console.log('parseAndUpdateIdentifierAndClaim'); + // this is a request for an asset + // claim will be an asset claim + // the identifier could be a channel or a claim id + let isChannel, channelName, channelClaimId, claimId, claimName, extension; + try { + ({ isChannel, channelName, channelClaimId, claimId } = lbryUri.parseIdentifier(modifier)); + ({ claimName, extension } = lbryUri.parseClaim(claim)); + } catch (error) { + return yield put(onRequestError(error.message)); + } + // trigger an new action to update the store + if (isChannel) { + return yield put(onNewAssetRequest(claimName, null, channelName, channelClaimId, extension)); + }; + yield put(onNewAssetRequest(claimName, claimId, null, null, extension)); +} +function* parseAndUpdateClaimOnly (claim) { + console.log('parseAndUpdateIdentifierAndClaim'); + // this could be a request for an asset or a channel page + // claim could be an asset claim or a channel claim + let isChannel, channelName, channelClaimId; + try { + ({ isChannel, channelName, channelClaimId } = lbryUri.parseIdentifier(claim)); + } catch (error) { + return yield put(onRequestError(error.message)); + } + // trigger an new action to update the store + // return early if this request is for a channel + if (isChannel) { + return yield put(onNewChannelRequest(channelName, channelClaimId)); + } + // if not for a channel, parse the claim request + let claimName, extension; + try { + ({claimName, extension} = lbryUri.parseClaim(claim)); + } catch (error) { + return yield put(onRequestError(error.message)); + } + yield put(onNewAssetRequest(claimName, null, null, null, extension)); +} + +function* handleShowPageUri (action) { + console.log('handleShowPageUri'); + const { identifier, claim } = action.data; + if (identifier) { + return yield call(parseAndUpdateIdentifierAndClaim, identifier, claim); + } + yield call(parseAndUpdateClaimOnly, claim); +}; + +export function* watchHandleShowPageUri () { + yield takeLatest(actions.HANDLE_SHOW_URI, handleShowPageUri); +}; diff --git a/react/selectors/show.js b/react/selectors/show.js new file mode 100644 index 00000000..b3b5ba92 --- /dev/null +++ b/react/selectors/show.js @@ -0,0 +1,9 @@ +export const selectAsset = (show) => { + const request = show.requestList[show.request.id]; + const assetKey = request.key; + return show.assetList[assetKey]; +}; + +export const selectShowState = (state) => { + return state.show; +}; diff --git a/react/utils/lbryUri.js b/react/utils/lbryUri.js new file mode 100644 index 00000000..33ac7081 --- /dev/null +++ b/react/utils/lbryUri.js @@ -0,0 +1,85 @@ +module.exports = { + REGEXP_INVALID_CLAIM : /[^A-Za-z0-9-]/g, + REGEXP_INVALID_CHANNEL: /[^A-Za-z0-9-@]/g, + REGEXP_ADDRESS : /^b(?=[^0OIl]{32,33})[0-9A-Za-z]{32,33}$/, + CHANNEL_CHAR : '@', + parseIdentifier : function (identifier) { + const componentsRegex = new RegExp( + '([^:$#/]*)' + // value (stops at the first separator or end) + '([:$#]?)([^/]*)' // modifier separator, modifier (stops at the first path separator or end) + ); + const [proto, value, modifierSeperator, modifier] = componentsRegex + .exec(identifier) + .map(match => match || null); + + // Validate and process name + if (!value) { + throw new Error(`Check your URL. No channel name provided before "${modifierSeperator}"`); + } + const isChannel = value.startsWith(module.exports.CHANNEL_CHAR); + const channelName = isChannel ? value : null; + let claimId; + if (isChannel) { + if (!channelName) { + throw new Error('Check your URL. No channel name after "@".'); + } + const nameBadChars = (channelName).match(module.exports.REGEXP_INVALID_CHANNEL); + if (nameBadChars) { + throw new Error(`Check your URL. Invalid characters in channel name: "${nameBadChars.join(', ')}".`); + } + } else { + claimId = value; + } + + // Validate and process modifier + let channelClaimId; + if (modifierSeperator) { + if (!modifier) { + throw new Error(`Check your URL. No modifier provided after separator "${modifierSeperator}"`); + } + + if (modifierSeperator === ':') { + channelClaimId = modifier; + } else { + throw new Error(`Check your URL. The "${modifierSeperator}" modifier is not currently supported`); + } + } + return { + isChannel, + channelName, + channelClaimId: channelClaimId || null, + claimId : claimId || null, + }; + }, + parseClaim: function (name) { + const componentsRegex = new RegExp( + '([^:$#/.]*)' + // name (stops at the first extension) + '([:$#.]?)([^/]*)' // extension separator, extension (stops at the first path separator or end) + ); + const [proto, claimName, extensionSeperator, extension] = componentsRegex + .exec(name) + .map(match => match || null); + + // Validate and process name + if (!claimName) { + throw new Error('Check your URL. No claim name provided before "."'); + } + const nameBadChars = (claimName).match(module.exports.REGEXP_INVALID_CLAIM); + if (nameBadChars) { + throw new Error(`Check your URL. Invalid characters in claim name: "${nameBadChars.join(', ')}".`); + } + // Validate and process extension + if (extensionSeperator) { + if (!extension) { + throw new Error(`Check your URL. No file extension provided after separator "${extensionSeperator}".`); + } + if (extensionSeperator !== '.') { + throw new Error(`Check your URL. The "${extensionSeperator}" separator is not supported in the claim name.`); + } + } + return { + claimName, + extension: extension || null, + }; + }, +}; diff --git a/react/utils/request.js b/react/utils/request.js index 5d836585..81674721 100644 --- a/react/utils/request.js +++ b/react/utils/request.js @@ -13,18 +13,18 @@ function parseJSON (response) { } /** - * Checks if a network request came back fine, and throws an error if not + * Parses the status returned by a network request * * @param {object} response A response from a network request + * @param {object} response The parsed JSON from the network request * - * @return {object|undefined} Returns either the response, or throws an error + * @return {object | undefined} Returns object with status and statusText, or undefined */ -function checkStatus (response) { +function checkStatus (response, jsonResponse) { if (response.status >= 200 && response.status < 300) { - return response; + return jsonResponse; } - - const error = new Error(response.statusText); + const error = new Error(jsonResponse.message); error.response = response; throw error; } @@ -37,8 +37,13 @@ function checkStatus (response) { * * @return {object} The response data */ + export default function request (url, options) { return fetch(url, options) - .then(checkStatus) - .then(parseJSON); + .then(response => { + return Promise.all([response, parseJSON(response)]); + }) + .then(([response, jsonResponse]) => { + return checkStatus(response, jsonResponse); + }); } diff --git a/routes/api-routes.js b/routes/api-routes.js index 65057295..21e5b656 100644 --- a/routes/api-routes.js +++ b/routes/api-routes.js @@ -9,36 +9,79 @@ const { createPublishParams, parsePublishApiRequestBody, parsePublishApiRequestF const errorHandlers = require('../helpers/errorHandlers.js'); const { sendGoogleAnalyticsTiming } = require('../helpers/statsHelpers.js'); const { authenticateIfNoUserToken } = require('../auth/authentication.js'); +const { getChannelData, getChannelClaims, getClaimId } = require('../controllers/serveController.js'); + +const NO_CHANNEL = 'NO_CHANNEL'; +const NO_CLAIM = 'NO_CLAIM'; module.exports = (app) => { - // route to run a claim_list request on the daemon - app.get('/api/claim-list/:name', ({ ip, originalUrl, params }, res) => { - getClaimList(params.name) - .then(claimsList => { - res.status(200).json(claimsList); - }) - .catch(error => { - errorHandlers.handleApiError(originalUrl, ip, error, res); - }); - }); - // route to see if asset is available locally - app.get('/api/file-is-available/:name/:claimId', ({ ip, originalUrl, params }, res) => { - const name = params.name; - const claimId = params.claimId; - let isLocalFileAvailable = false; - db.File.findOne({where: {name, claimId}}) + // route to check whether site has published to a channel + app.get('/api/channel/availability/:name', ({ ip, originalUrl, params }, res) => { + checkChannelAvailability(params.name) .then(result => { - if (result) { - isLocalFileAvailable = true; + if (result === true) { + res.status(200).json(true); + } else { + res.status(200).json(false); } - res.status(200).json({status: 'success', message: isLocalFileAvailable}); }) .catch(error => { - errorHandlers.handleApiError(originalUrl, ip, error, res); + errorHandlers.handleErrorResponse(originalUrl, ip, error, res); + }); + }); + // route to get a short channel id from long channel Id + app.get('/api/channel/short-id/:longId/:name', ({ ip, originalUrl, params }, res) => { + db.Certificate.getShortChannelIdFromLongChannelId(params.longId, params.name) + .then(shortId => { + res.status(200).json(shortId); + }) + .catch(error => { + errorHandlers.handleErrorResponse(originalUrl, ip, error, res); + }); + }); + app.get('/api/channel/data/:channelName/:channelClaimId', ({ ip, originalUrl, body, params }, res) => { + const channelName = params.channelName; + let channelClaimId = params.channelClaimId; + if (channelClaimId === 'none') channelClaimId = null; + getChannelData(channelName, channelClaimId, 0) + .then(data => { + if (data === NO_CHANNEL) { + return res.status(404).json({success: false, message: 'No matching channel was found'}); + } + res.status(200).json({success: true, data}); + }) + .catch(error => { + errorHandlers.handleErrorResponse(originalUrl, ip, error, res); + }); + }); + app.get('/api/channel/claims/:channelName/:channelClaimId/:page', ({ ip, originalUrl, body, params }, res) => { + const channelName = params.channelName; + let channelClaimId = params.channelClaimId; + if (channelClaimId === 'none') channelClaimId = null; + const page = params.page; + getChannelClaims(channelName, channelClaimId, page) + .then(data => { + if (data === NO_CHANNEL) { + return res.status(404).json({success: false, message: 'No matching channel was found'}); + } + res.status(200).json({success: true, data}); + }) + .catch(error => { + errorHandlers.handleErrorResponse(originalUrl, ip, error, res); + }); + }); + // route to run a claim_list request on the daemon + app.get('/api/claim/list/:name', ({ ip, originalUrl, params }, res) => { + getClaimList(params.name) + .then(claimsList => { + res.status(200).json(claimsList); + }) + .catch(error => { + errorHandlers.handleErrorResponse(originalUrl, ip, error, res); }); }); // route to get an asset - app.get('/api/claim-get/:name/:claimId', ({ ip, originalUrl, params }, res) => { + app.get('/api/claim/get/:name/:claimId', ({ ip, originalUrl, params }, res) => { const name = params.name; const claimId = params.claimId; // resolve the claim @@ -57,30 +100,15 @@ module.exports = (app) => { return Promise.all([db.upsert(db.File, fileData, {name, claimId}, 'File'), getResult]); }) .then(([ fileRecord, {message, completed} ]) => { - res.status(200).json({ status: 'success', message, completed }); + res.status(200).json({ success: true, message, completed }); }) .catch(error => { - errorHandlers.handleApiError(originalUrl, ip, error, res); + errorHandlers.handleErrorResponse(originalUrl, ip, error, res); }); }); - // route to check whether this site published to a claim - app.get('/api/claim-is-available/:name', ({ params }, res) => { + app.get('/api/claim/availability/:name', ({ ip, originalUrl, params }, res) => { checkClaimNameAvailability(params.name) - .then(result => { - if (result === true) { - res.status(200).json(true); - } else { - res.status(200).json(false); - } - }) - .catch(error => { - res.status(500).json(error); - }); - }); - // route to check whether site has published to a channel - app.get('/api/channel-is-available/:name', ({ params }, res) => { - checkChannelAvailability(params.name) .then(result => { if (result === true) { res.status(200).json(true); @@ -89,21 +117,21 @@ module.exports = (app) => { } }) .catch(error => { - res.status(500).json(error); + errorHandlers.handleErrorResponse(originalUrl, ip, error, res); }); }); // route to run a resolve request on the daemon - app.get('/api/claim-resolve/:uri', ({ headers, ip, originalUrl, params }, res) => { - resolveUri(params.uri) - .then(resolvedUri => { - res.status(200).json(resolvedUri); - }) - .catch(error => { - errorHandlers.handleApiError(originalUrl, ip, error, res); - }); + app.get('/api/claim/resolve/:name/:claimId', ({ headers, ip, originalUrl, params }, res) => { + resolveUri(`${params.name}#${params.claimId}`) + .then(resolvedUri => { + res.status(200).json(resolvedUri); + }) + .catch(error => { + errorHandlers.handleErrorResponse(originalUrl, ip, error, res); + }); }); // route to run a publish request on the daemon - app.post('/api/claim-publish', multipartMiddleware, ({ body, files, headers, ip, originalUrl, user }, res) => { + app.post('/api/claim/publish', multipartMiddleware, ({ body, files, headers, ip, originalUrl, user }, res) => { logger.debug('api/claim-publish body:', body); logger.debug('api/claim-publish files:', files); // record the start time of the request and create variable for storing the action type @@ -119,70 +147,108 @@ module.exports = (app) => { ({fileName, filePath, fileType} = parsePublishApiRequestFiles(files)); ({channelName, channelPassword} = parsePublishApiChannel(body, user)); } catch (error) { - logger.debug('publish request rejected, insufficient request parameters', error); return res.status(400).json({success: false, message: error.message}); } // check channel authorization authenticateIfNoUserToken(channelName, channelPassword, user) - .then(authenticated => { - if (!authenticated) { - throw new Error('Authentication failed, you do not have access to that channel'); - } - // make sure the claim name is available - return checkClaimNameAvailability(name); - }) - .then(result => { - if (!result) { - throw new Error('That name is already claimed by another user.'); - } - // create publish parameters object - return createPublishParams(filePath, name, title, description, license, nsfw, thumbnail, channelName); - }) - .then(publishParams => { - // set the timing event type for reporting - timingActionType = returnPublishTimingActionType(publishParams.channel_name); - // publish the asset - return publish(publishParams, fileName, fileType); - }) - .then(result => { - res.status(200).json({ - success: true, - message: { - name, - url : `${site.host}/${result.claim_id}/${name}`, - lbryTx: result, - }, + .then(authenticated => { + if (!authenticated) { + throw new Error('Authentication failed, you do not have access to that channel'); + } + // make sure the claim name is available + return checkClaimNameAvailability(name); + }) + .then(result => { + if (!result) { + throw new Error('That name is already claimed by another user.'); + } + // create publish parameters object + return createPublishParams(filePath, name, title, description, license, nsfw, thumbnail, channelName); + }) + .then(publishParams => { + // set the timing event type for reporting + timingActionType = returnPublishTimingActionType(publishParams.channel_name); + // publish the asset + return publish(publishParams, fileName, fileType); + }) + .then(result => { + res.status(200).json({ + success: true, + message: 'publish completed successfully', + data : { + name, + claimId: result.claim_id, + url : `${site.host}/${result.claim_id}/${name}`, + lbryTx : result, + }, + }); + // log the publish end time + const publishEndTime = Date.now(); + logger.debug('publish request completed @', publishEndTime); + sendGoogleAnalyticsTiming(timingActionType, headers, ip, originalUrl, publishStartTime, publishEndTime); + }) + .catch(error => { + errorHandlers.handleErrorResponse(originalUrl, ip, error, res); }); - // log the publish end time - const publishEndTime = Date.now(); - logger.debug('publish request completed @', publishEndTime); - sendGoogleAnalyticsTiming(timingActionType, headers, ip, originalUrl, publishStartTime, publishEndTime); - }) - .catch(error => { - errorHandlers.handleApiError(originalUrl, ip, error, res); - }); }); // route to get a short claim id from long claim Id - app.get('/api/claim-shorten-id/:longId/:name', ({ params }, res) => { + app.get('/api/claim/short-id/:longId/:name', ({ ip, originalUrl, body, params }, res) => { db.Claim.getShortClaimIdFromLongClaimId(params.longId, params.name) .then(shortId => { - res.status(200).json(shortId); + res.status(200).json({success: true, data: shortId}); }) .catch(error => { - logger.error('api error getting short channel id', error); - res.status(400).json(error.message); + errorHandlers.handleErrorResponse(originalUrl, ip, error, res); }); }); - // route to get a short channel id from long channel Id - app.get('/api/channel-shorten-id/:longId/:name', ({ ip, originalUrl, params }, res) => { - db.Certificate.getShortChannelIdFromLongChannelId(params.longId, params.name) - .then(shortId => { - logger.debug('sending back short channel id', shortId); - res.status(200).json(shortId); + app.post('/api/claim/long-id', ({ ip, originalUrl, body, params }, res) => { + logger.debug('body:', body); + const channelName = body.channelName; + const channelClaimId = body.channelClaimId; + const claimName = body.claimName; + const claimId = body.claimId; + getClaimId(channelName, channelClaimId, claimName, claimId) + .then(result => { + if (result === NO_CHANNEL) { + return res.status(404).json({success: false, message: 'No matching channel could be found'}); + } + if (result === NO_CLAIM) { + return res.status(404).json({success: false, message: 'No matching claim id could be found'}); + } + res.status(200).json({success: true, data: result}); }) .catch(error => { - logger.error('api error getting short channel id', error); - errorHandlers.handleApiError(originalUrl, ip, error, res); + errorHandlers.handleErrorResponse(originalUrl, ip, error, res); + }); + }); + app.get('/api/claim/data/:claimName/:claimId', ({ ip, originalUrl, body, params }, res) => { + const claimName = params.claimName; + let claimId = params.claimId; + if (claimId === 'none') claimId = null; + db.Claim.resolveClaim(claimName, claimId) + .then(claimInfo => { + if (!claimInfo) { + return res.status(404).json({success: false, message: 'No claim could be found'}); + } + res.status(200).json({success: true, data: claimInfo}); + }) + .catch(error => { + errorHandlers.handleErrorResponse(originalUrl, ip, error, res); + }); + }); + // route to see if asset is available locally + app.get('/api/file/availability/:name/:claimId', ({ ip, originalUrl, params }, res) => { + const name = params.name; + const claimId = params.claimId; + db.File.findOne({where: {name, claimId}}) + .then(result => { + if (result) { + return res.status(200).json({success: true, data: true}); + } + res.status(200).json({success: true, data: false}); + }) + .catch(error => { + errorHandlers.handleErrorResponse(originalUrl, ip, error, res); }); }); }; diff --git a/routes/auth-routes.js b/routes/auth-routes.js index 62e89bf9..5c6013d7 100644 --- a/routes/auth-routes.js +++ b/routes/auth-routes.js @@ -20,7 +20,7 @@ module.exports = (app) => { return next(err); } if (!user) { - return res.status(200).json({ + return res.status(400).json({ success: false, message: info.message, }); @@ -39,12 +39,17 @@ module.exports = (app) => { }); })(req, res, next); }); + // route to log out + app.get('/logout', (req, res) => { + req.logout(); + res.status(200).json({success: true, message: 'you successfully logged out'}); + }); // 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}); + res.status(200).json({success: true, data: req.user}); } else { - res.status(200).json({success: false, message: 'user is not logged in'}); + res.status(401).json({success: false, message: 'user is not logged in'}); } }); }; diff --git a/routes/home-routes.js b/routes/home-routes.js index 0879194a..e58607fc 100644 --- a/routes/home-routes.js +++ b/routes/home-routes.js @@ -6,6 +6,6 @@ module.exports = app => { // a catch-all route if someone visits a page that does not exist app.use('*', ({ originalUrl, ip }, res) => { // send response - res.status(404).render('fourOhFour'); + res.status(404).render('404'); }); }; diff --git a/routes/page-routes.js b/routes/page-routes.js index 54317d00..fd15ca88 100644 --- a/routes/page-routes.js +++ b/routes/page-routes.js @@ -1,53 +1,24 @@ -const errorHandlers = require('../helpers/errorHandlers.js'); -const { getTrendingClaims, getRecentClaims } = require('../controllers/statsController.js'); const { site } = require('../config/speechConfig.js'); module.exports = (app) => { - // route to log out - app.get('/logout', (req, res) => { - req.logout(); - res.redirect('/'); - }); // route to display login page app.get('/login', (req, res) => { - if (req.user) { - res.status(200).redirect(`/${req.user.channelName}`); - } else { - res.status(200).render('login'); - } + res.status(200).render('index'); }); // route to show 'about' page app.get('/about', (req, res) => { - // get and render the content - res.status(200).render('about'); + res.status(200).render('index'); }); // route to display a list of the trending images app.get('/trending', (req, res) => { res.status(301).redirect('/popular'); }); app.get('/popular', ({ ip, originalUrl }, res) => { - const startDate = new Date(); - startDate.setDate(startDate.getDate() - 1); - const dateTime = startDate.toISOString().slice(0, 19).replace('T', ' '); - getTrendingClaims(dateTime) - .then(result => { - res.status(200).render('popular', { - trendingAssets: result, - }); - }) - .catch(error => { - errorHandlers.handleRequestError(originalUrl, ip, error, res); - }); + res.status(200).render('index'); }); // route to display a list of the trending images app.get('/new', ({ ip, originalUrl }, res) => { - getRecentClaims() - .then(result => { - res.status(200).render('new', { newClaims: result }); - }) - .catch(error => { - errorHandlers.handleRequestError(originalUrl, ip, error, res); - }); + res.status(200).render('index'); }); // route to send embedable video player (for twitter) app.get('/embed/:claimId/:name', ({ params }, res) => { @@ -57,9 +28,4 @@ module.exports = (app) => { // get and render the content res.status(200).render('embed', { layout: 'embed', host, claimId, name }); }); - // route to display all free public claims at a given name - app.get('/:name/all', (req, res) => { - // get and render the content - res.status(410).send('/:name/all is no longer supported'); - }); }; diff --git a/routes/serve-routes.js b/routes/serve-routes.js index 3baf1f6e..38c755ee 100644 --- a/routes/serve-routes.js +++ b/routes/serve-routes.js @@ -1,14 +1,11 @@ const logger = require('winston'); -const { getClaimId, getChannelViewData, getLocalFileRecord } = require('../controllers/serveController.js'); +const { getClaimId, getLocalFileRecord } = require('../controllers/serveController.js'); const serveHelpers = require('../helpers/serveHelpers.js'); -const { handleRequestError } = require('../helpers/errorHandlers.js'); -const { postToStats } = require('../helpers/statsHelpers.js'); -const db = require('../models'); +const { handleErrorResponse } = require('../helpers/errorHandlers.js'); const lbryUri = require('../helpers/lbryUri.js'); const SERVE = 'SERVE'; const SHOW = 'SHOW'; -const SHOWLITE = 'SHOWLITE'; const NO_CHANNEL = 'NO_CHANNEL'; const NO_CLAIM = 'NO_CLAIM'; const NO_FILE = 'NO_FILE'; @@ -25,25 +22,6 @@ function isValidShortIdOrClaimId (input) { return (isValidClaimId(input) || isValidShortId(input)); } -function sendChannelInfoAndContentToClient (channelPageData, res) { - if (channelPageData === NO_CHANNEL) { - res.status(200).render('noChannel'); - } else { - res.status(200).render('channel', channelPageData); - } -} - -function showChannelPageToClient (channelName, channelClaimId, originalUrl, ip, query, res) { - // 1. retrieve the channel contents - getChannelViewData(channelName, channelClaimId, query) - .then(channelViewData => { - sendChannelInfoAndContentToClient(channelViewData, res); - }) - .catch(error => { - handleRequestError(originalUrl, ip, error, res); - }); -} - function clientAcceptsHtml ({accept}) { return accept && accept.match(/text\/html/); } @@ -58,55 +36,29 @@ function clientWantsAsset ({accept, range}) { return imageIsWanted || videoIsWanted; } -function determineResponseType (isServeRequest, headers) { +function determineResponseType (hasFileExtension, headers) { let responseType; - if (isServeRequest) { - responseType = SERVE; - if (clientAcceptsHtml(headers)) { // this is in case a serve request comes from a browser - responseType = SHOWLITE; + if (hasFileExtension) { + responseType = SERVE; // assume a serve request if file extension is present + if (clientAcceptsHtml(headers)) { // if the request comes from a browser, change it to a show request + responseType = SHOW; } } else { responseType = SHOW; if (clientWantsAsset(headers) && requestIsFromBrowser(headers)) { // this is in case someone embeds a show url - logger.debug('Show request came from browser and wants an image/video; changing response to serve.'); + logger.debug('Show request came from browser but wants an image/video. Changing response to serve...'); responseType = SERVE; } } return responseType; } -function showAssetToClient (claimId, name, res) { - return Promise - .all([db.Claim.resolveClaim(name, claimId), db.Claim.getShortClaimIdFromLongClaimId(claimId, name)]) - .then(([claimInfo, shortClaimId]) => { - // logger.debug('claimInfo:', claimInfo); - // logger.debug('shortClaimId:', shortClaimId); - return serveHelpers.showFile(claimInfo, shortClaimId, res); - }) - .catch(error => { - throw error; - }); -} - -function showLiteAssetToClient (claimId, name, res) { - return Promise - .all([db.Claim.resolveClaim(name, claimId), db.Claim.getShortClaimIdFromLongClaimId(claimId, name)]) - .then(([claimInfo, shortClaimId]) => { - // logger.debug('claimInfo:', claimInfo); - // logger.debug('shortClaimId:', shortClaimId); - return serveHelpers.showFileLite(claimInfo, shortClaimId, res); - }) - .catch(error => { - throw error; - }); -} - function serveAssetToClient (claimId, name, res) { return getLocalFileRecord(claimId, name) .then(fileInfo => { // logger.debug('fileInfo:', fileInfo); if (fileInfo === NO_FILE) { - return res.status(307).redirect(`/api/claim-get/${name}/${claimId}`); + return res.status(307).redirect(`/api/claim/get/${name}/${claimId}`); } return serveHelpers.serveFile(fileInfo, claimId, name, res); }) @@ -115,19 +67,6 @@ function serveAssetToClient (claimId, name, res) { }); } -function showOrServeAsset (responseType, claimId, claimName, res) { - switch (responseType) { - case SHOW: - return showAssetToClient(claimId, claimName, res); - case SHOWLITE: - return showLiteAssetToClient(claimId, claimName, res); - case SERVE: - return serveAssetToClient(claimId, claimName, res); - default: - break; - } -} - function flipClaimNameAndIdForBackwardsCompatibility (identifier, name) { // this is a patch for backwards compatability with '/name/claim_id' url format if (isValidShortIdOrClaimId(name) && !isValidShortIdOrClaimId(identifier)) { @@ -147,70 +86,87 @@ function logRequestData (responseType, claimName, channelName, claimId) { module.exports = (app) => { // route to serve a specific asset using the channel or claim id - app.get('/:identifier/:name', ({ headers, ip, originalUrl, params }, res) => { - let isChannel, channelName, channelClaimId, claimId, claimName, isServeRequest; + app.get('/:identifier/:claim', ({ headers, ip, originalUrl, params }, res) => { + // decide if this is a show request + let hasFileExtension; + try { + ({ hasFileExtension } = lbryUri.parseModifier(params.claim)); + } catch (error) { + return res.status(400).json({success: false, message: error.message}); + } + let responseType = determineResponseType(hasFileExtension, headers); + if (responseType !== SERVE) { + return res.status(200).render('index'); + } + // parse the claim + let claimName; + try { + ({ claimName } = lbryUri.parseClaim(params.claim)); + } catch (error) { + return res.status(400).json({success: false, message: error.message}); + } + // parse the identifier + let isChannel, channelName, channelClaimId, claimId; try { ({ isChannel, channelName, channelClaimId, claimId } = lbryUri.parseIdentifier(params.identifier)); - ({ claimName, isServeRequest } = lbryUri.parseName(params.name)); } catch (error) { - return handleRequestError(originalUrl, ip, error, res); + return res.status(400).json({success: false, message: error.message}); } if (!isChannel) { [claimId, claimName] = flipClaimNameAndIdForBackwardsCompatibility(claimId, claimName); } - let responseType = determineResponseType(isServeRequest, headers); // log the request data for debugging logRequestData(responseType, claimName, channelName, claimId); - // get the claim Id and then serve/show the asset + // get the claim Id and then serve the asset getClaimId(channelName, channelClaimId, claimName, claimId) - .then(fullClaimId => { - if (fullClaimId === NO_CLAIM) { - return res.status(200).render('noClaim'); - } else if (fullClaimId === NO_CHANNEL) { - return res.status(200).render('noChannel'); - } - showOrServeAsset(responseType, fullClaimId, claimName, res); - postToStats(responseType, originalUrl, ip, claimName, fullClaimId, 'success'); - }) - .catch(error => { - handleRequestError(originalUrl, ip, error, res); - }); + .then(fullClaimId => { + if (fullClaimId === NO_CLAIM) { + return res.status(404).json({success: false, message: 'no claim id could be found'}); + } else if (fullClaimId === NO_CHANNEL) { + return res.status(404).json({success: false, message: 'no channel id could be found'}); + } + serveAssetToClient(fullClaimId, claimName, res); + // postToStats(responseType, originalUrl, ip, claimName, fullClaimId, 'success'); + }) + .catch(error => { + handleErrorResponse(originalUrl, ip, error, res); + // postToStats(responseType, originalUrl, ip, claimName, fullClaimId, 'fail'); + }); }); // route to serve the winning asset at a claim or a channel page - app.get('/:identifier', ({ headers, ip, originalUrl, params, query }, res) => { - let isChannel, channelName, channelClaimId; + app.get('/:claim', ({ headers, ip, originalUrl, params, query }, res) => { + // decide if this is a show request + let hasFileExtension; try { - ({ isChannel, channelName, channelClaimId } = lbryUri.parseIdentifier(params.identifier)); + ({ hasFileExtension } = lbryUri.parseModifier(params.claim)); } catch (error) { - return handleRequestError(originalUrl, ip, error, res); + return res.status(400).json({success: false, message: error.message}); } - if (isChannel) { - // log the request data for debugging - logRequestData(null, null, channelName, null); - // handle showing the channel page - showChannelPageToClient(channelName, channelClaimId, originalUrl, ip, query, res); - } else { - let claimName, isServeRequest; - try { - ({claimName, isServeRequest} = lbryUri.parseName(params.identifier)); - } catch (error) { - return handleRequestError(originalUrl, ip, error, res); - } - let responseType = determineResponseType(isServeRequest, headers); - // log the request data for debugging - logRequestData(responseType, claimName, null, null); - // get the claim Id and then serve/show the asset - getClaimId(null, null, claimName, null) - .then(fullClaimId => { - if (fullClaimId === NO_CLAIM) { - return res.status(200).render('noClaim'); - } - showOrServeAsset(responseType, fullClaimId, claimName, res); - postToStats(responseType, originalUrl, ip, claimName, fullClaimId, 'success'); - }) - .catch(error => { - handleRequestError(originalUrl, ip, error, res); - }); + let responseType = determineResponseType(hasFileExtension, headers); + if (responseType !== SERVE) { + return res.status(200).render('index'); } + // parse the claim + let claimName; + try { + ({claimName} = lbryUri.parseClaim(params.claim)); + } catch (error) { + return res.status(400).json({success: false, message: error.message}); + } + // log the request data for debugging + logRequestData(responseType, claimName, null, null); + // get the claim Id and then serve the asset + getClaimId(null, null, claimName, null) + .then(fullClaimId => { + if (fullClaimId === NO_CLAIM) { + return res.status(404).json({success: false, message: 'no claim id could be found'}); + } + serveAssetToClient(fullClaimId, claimName, res); + // postToStats(responseType, originalUrl, ip, claimName, fullClaimId, 'success'); + }) + .catch(error => { + handleErrorResponse(originalUrl, ip, error, res); + // postToStats(responseType, originalUrl, ip, claimName, fullClaimId, 'fail'); + }); }); }; diff --git a/task-scripts/update-password.js b/task-scripts/update-password.js index 6a5176d4..89cff11f 100644 --- a/task-scripts/update-password.js +++ b/task-scripts/update-password.js @@ -1,6 +1,6 @@ // load dependencies const logger = require('winston'); -const db = require('../models/index'); // require our models for syncing +const db = require('../models'); // require our models for syncing // configure logging const config = require('../config/speechConfig.js'); const { logLevel } = config.logging; diff --git a/test/end-to-end/end-to-end.tests.js b/test/end-to-end/end-to-end.tests.js index 4fb9c31c..09290733 100644 --- a/test/end-to-end/end-to-end.tests.js +++ b/test/end-to-end/end-to-end.tests.js @@ -84,8 +84,28 @@ describe('end-to-end', function () { }); }); + describe('channel data request from client', function () { + const url = '/@test'; + const urlWithShortClaimId = '/@test:3'; + const urlWithMediumClaimId = '/@test:3b5bc6b6819172c6'; + const urlWithLongClaimId = '/@test:3b5bc6b6819172c6e2f3f90aa855b14a956b4a82'; + + describe(url, function () { + it('should pass the tests I write here'); + }); + describe(urlWithShortClaimId, function () { + it('should pass the tests I write here'); + }); + describe(urlWithMediumClaimId, function () { + it('should pass the tests I write here'); + }); + describe(urlWithLongClaimId, function () { + it('should pass the tests I write here'); + }); + }); + describe('publish requests', function () { - const publishUrl = '/api/claim-publish'; + const publishUrl = '/api/claim/publish'; const filePath = './test/mock-data/bird.jpeg'; const fileName = 'byrd.jpeg'; const channelName = testChannel; diff --git a/views/fourOhFour.handlebars b/views/404.handlebars similarity index 60% rename from views/fourOhFour.handlebars rename to views/404.handlebars index a7f6f0ea..9cc0a1c4 100644 --- a/views/fourOhFour.handlebars +++ b/views/404.handlebars @@ -1,5 +1,4 @@ -{{> navBar}} -
+

404: Not Found

That page does not exist. Return home.

diff --git a/views/about.handlebars b/views/about.handlebars deleted file mode 100644 index 0274f1e9..00000000 --- a/views/about.handlebars +++ /dev/null @@ -1,21 +0,0 @@ -{{> navBar}} -
-
-
-

Spee.ch is an open-source project. Please contribute to the existing site, or fork it and make your own.

-

TWITTER

-

GITHUB

-

DISCORD CHANNEL

-

DOCUMENTATION

-
-
-
-

Spee.ch is a media-hosting site that reads from and publishes content to the LBRY blockchain.

-

Spee.ch is a hosting service, but with the added benefit that it stores your content on a decentralized network of computers -- the LBRY network. This means that your images are stored in multiple locations without a single point of failure.

-

Contribute

-

If you have an idea for your own spee.ch-like site on top of LBRY, fork our github repo and go to town!

-

If you want to improve spee.ch, join our discord channel or solve one of our github issues.

-
-
- -
diff --git a/views/channel.handlebars b/views/channel.handlebars deleted file mode 100644 index 2fcff401..00000000 --- a/views/channel.handlebars +++ /dev/null @@ -1,55 +0,0 @@ -{{> navBar}} -
-
- {{#ifConditional this.totalPages '===' 0}} -

There is no content in {{this.channelName}}:{{this.longChannelClaimId}} yet. Upload some!

- {{/ifConditional}} - {{#ifConditional this.totalPages '>=' 1}} -

Below are the contents for {{this.channelName}}:{{this.longChannelClaimId}}

-
- {{#each this.claims}} - {{> gridItem}} - {{/each}} -
- {{/ifConditional}} - {{#ifConditional this.totalPages '>' 1}} -
-
- First [1] -
- {{#if this.previousPage}} - Previous - {{else}} - Previous - {{/if}} - | - {{#if this.nextPage}} - Next - {{else}} - Next - {{/if}} -
-
- {{/ifConditional}} -
-
- - - - diff --git a/views/index.handlebars b/views/index.handlebars index 563fa20b..075fe851 100644 --- a/views/index.handlebars +++ b/views/index.handlebars @@ -1,6 +1,5 @@ -
-
+

loading...

{{> progressBar}} diff --git a/views/layouts/channel.handlebars b/views/layouts/channel.handlebars deleted file mode 100644 index 18a41951..00000000 --- a/views/layouts/channel.handlebars +++ /dev/null @@ -1,22 +0,0 @@ - - - - {{ placeCommonHeaderTags }} - - - - - - - - - - - - {{ googleAnalytics }} - - - - {{{ body }}} - - diff --git a/views/layouts/main.handlebars b/views/layouts/main.handlebars index fb262239..09068747 100644 --- a/views/layouts/main.handlebars +++ b/views/layouts/main.handlebars @@ -16,8 +16,6 @@ {{ googleAnalytics }} - - {{{ body }}} diff --git a/views/layouts/show.handlebars b/views/layouts/show.handlebars deleted file mode 100644 index 8ba33271..00000000 --- a/views/layouts/show.handlebars +++ /dev/null @@ -1,21 +0,0 @@ - - - - {{ placeCommonHeaderTags }} - - {{#unless claimInfo.nsfw}} - {{{addTwitterCard claimInfo }}} - {{{addOpenGraph claimInfo }}} - {{/unless}} - - - - {{ googleAnalytics }} - - - - - {{{ body }}} - - - diff --git a/views/login.handlebars b/views/login.handlebars deleted file mode 100644 index 17b028cc..00000000 --- a/views/login.handlebars +++ /dev/null @@ -1,16 +0,0 @@ -{{> navBar}} -
-
-
-

Channels allow you to publish and group content under an identity. You can create a channel for yourself, or share one with like-minded friends. You can create 1 channel, or 100, so whether you're documenting important events, or making a public repository for cat gifs (password: '1234'), try creating a channel for it!

-
-
-
-

Log in to an existing channel:

- {{>channelLoginForm}} -

Create a brand new channel:

- {{>channelCreationForm}} -
-
-
- diff --git a/views/noChannel.handlebars b/views/noChannel.handlebars deleted file mode 100644 index 19b6a9bc..00000000 --- a/views/noChannel.handlebars +++ /dev/null @@ -1,6 +0,0 @@ -{{> navBar}} -
-

No Channel

-

There are no published channels matching your url

-

If you think this message is an error, contact us in the LBRY Discord!

-
diff --git a/views/noClaim.handlebars b/views/noClaim.handlebars deleted file mode 100644 index f479c81c..00000000 --- a/views/noClaim.handlebars +++ /dev/null @@ -1,6 +0,0 @@ -{{> navBar}} -
-

No Claims

-

There are no free assets at that claim. You should publish one at spee.ch.

-

NOTE: it is possible your claim was published, but it is still being processed by the blockchain

-
diff --git a/views/partials/asset.handlebars b/views/partials/asset.handlebars deleted file mode 100644 index fefe944f..00000000 --- a/views/partials/asset.handlebars +++ /dev/null @@ -1,36 +0,0 @@ -
-
- - -
- -
- - diff --git a/views/partials/assetInfo.handlebars b/views/partials/assetInfo.handlebars deleted file mode 100644 index 0ecd3644..00000000 --- a/views/partials/assetInfo.handlebars +++ /dev/null @@ -1,134 +0,0 @@ -{{#if claimInfo.channelName}} -
-
- Channel: -
-
-{{/if}} - -{{#if claimInfo.description}} -
- {{claimInfo.description}} -
-{{/if}} - -
- -
-
- Embed: -
-
-
- - {{#ifConditional claimInfo.contentType '===' 'video/mp4'}} - - {{else}} - - {{/ifConditional}} -
- -
-
-
-
-
- -
-
-
- Share: -
- -
-
-
- - - - - -
- [more] -
- - diff --git a/views/partials/channelCreationForm.handlebars b/views/partials/channelCreationForm.handlebars deleted file mode 100644 index 1b28db91..00000000 --- a/views/partials/channelCreationForm.handlebars +++ /dev/null @@ -1,39 +0,0 @@ -
-

-
-
- -
-
- @ - - -
-
-
-
-
- -
-
- -
-
-
- -
- -
-
- - - - - - - diff --git a/views/partials/channelLoginForm.handlebars b/views/partials/channelLoginForm.handlebars deleted file mode 100644 index c4c0217f..00000000 --- a/views/partials/channelLoginForm.handlebars +++ /dev/null @@ -1,28 +0,0 @@ -
-

-
-
- -
-
- @ - -
-
-
-
-
- -
-
- -
-
-
- -
- -
-
- - diff --git a/views/partials/contentListItem.handlebars b/views/partials/contentListItem.handlebars deleted file mode 100644 index 378153fe..00000000 --- a/views/partials/contentListItem.handlebars +++ /dev/null @@ -1,15 +0,0 @@ - - diff --git a/views/partials/gridItem.handlebars b/views/partials/gridItem.handlebars deleted file mode 100644 index 45bd7bcd..00000000 --- a/views/partials/gridItem.handlebars +++ /dev/null @@ -1,11 +0,0 @@ -
- {{#ifConditional this.contentType '===' 'video/mp4'}} - - {{else}} - - {{/ifConditional}} - - -
diff --git a/views/partials/image.handlebars b/views/partials/image.handlebars deleted file mode 100644 index 1091a2e5..00000000 --- a/views/partials/image.handlebars +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/views/partials/navBar.handlebars b/views/partials/navBar.handlebars deleted file mode 100644 index 8460ee4a..00000000 --- a/views/partials/navBar.handlebars +++ /dev/null @@ -1,68 +0,0 @@ - - - diff --git a/views/partials/progressBar.handlebars b/views/partials/progressBar.handlebars index e9d2dae6..dd7dc827 100644 --- a/views/partials/progressBar.handlebars +++ b/views/partials/progressBar.handlebars @@ -1,8 +1,53 @@

- \ No newline at end of file + diff --git a/views/partials/video.handlebars b/views/partials/video.handlebars deleted file mode 100644 index d968c652..00000000 --- a/views/partials/video.handlebars +++ /dev/null @@ -1,10 +0,0 @@ - - - - \ No newline at end of file diff --git a/views/popular.handlebars b/views/popular.handlebars deleted file mode 100644 index 7f3072c2..00000000 --- a/views/popular.handlebars +++ /dev/null @@ -1,24 +0,0 @@ -{{> navBar}} -
-
- {{#each trendingAssets}} - {{> gridItem}} - {{/each}} -
-
- - - - diff --git a/views/requestError.handlebars b/views/requestError.handlebars deleted file mode 100644 index 45048624..00000000 --- a/views/requestError.handlebars +++ /dev/null @@ -1,7 +0,0 @@ -{{> navBar}} -
-

Error

-

Unfortnately, Spee.ch encountered an error. You can help us out, by reporting the below error message in the #speech channel on LBRY Discord!

-

status: {{status}}

-

message: {{message}}

-
diff --git a/views/show.handlebars b/views/show.handlebars deleted file mode 100644 index ac8066c8..00000000 --- a/views/show.handlebars +++ /dev/null @@ -1,18 +0,0 @@ -{{> navBar}} -
-
- - {{claimInfo.title}} -
-
- -
- {{> asset}} -
-
- -
- {{> assetInfo}} -
-
-
diff --git a/views/showLite.handlebars b/views/showLite.handlebars deleted file mode 100644 index 31b5ee78..00000000 --- a/views/showLite.handlebars +++ /dev/null @@ -1,3 +0,0 @@ -
- {{> asset }} -
\ No newline at end of file diff --git a/views/statistics.handlebars b/views/statistics.handlebars deleted file mode 100644 index 09c4747f..00000000 --- a/views/statistics.handlebars +++ /dev/null @@ -1,32 +0,0 @@ -
-

Site Statistics

-

Serve: {{ totals.totalServe }}

-

Publish: {{ totals.totalPublish }}

-

Show: {{ totals.totalShow }}

-

Percent Success: {{ percentSuccess}}%

- - - - - - - - - {{#each records}} - - - - - - - - {{/each}} - - - - - - - -
actionurlcountsuccessfailure
{{ this.action }}{{ this.url }}{{ this.count }}{{ this.success }}{{ this.failure }}
{{ totals.totalCount }}{{ totals.totalSuccess }}{{ totals.totalFailure }}
-
diff --git a/webpack.config.js b/webpack.config.js index ae082fa7..a0447013 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -3,7 +3,7 @@ const Path = require('path'); const REACT_ROOT = Path.resolve(__dirname, 'react/'); module.exports = { - entry : ['whatwg-fetch', './react/app.js'], + entry : ['babel-polyfill', 'whatwg-fetch', './react/index.js'], output: { path : Path.join(__dirname, '/public/bundle/'), filename: 'bundle.js',