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 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 (
+
+ );
+ case 'image/gif':
+ return (
+
+ );
+ case 'video/mp4':
+ return (
+
+
+ Your browser does not support the video
element.
+
+ );
+ 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}
+
+ }
+
+
+
+
+
+ { this.state.showDetails &&
+
+
+
+
+ Claim Name:
+
+ {name}
+
+
+
+
+ Claim Id:
+
+ {claimId}
+
+
+
+
+ File Type:
+
+ {contentType ? `${contentType}` : 'unknown'}
+
+
+
+
+
+ }
+
+ {this.state.showDetails ? 'less' : 'more'}
+
+
+ );
+ }
+};
+
+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 (
+
+ );
+ case 'image/gif':
+ return (
+
+ );
+ 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 (
+
+ );
+ }
+};
+
+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 (
-
+
{channelName}
View
Logout
@@ -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 (
+
+
+
+ );
+ }
+};
+
+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 (
+
+ );
+ }
+};
+
+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) &&
+ Previous Page
+ }
+ {(currentPage < totalPages) &&
+ Next Page
+ }
+
+
+ ) : (
+
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.
@@ -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]'}
+ {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 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}}
-
-
- {{#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 @@
-