commit
16cb329e53
111 changed files with 2043 additions and 1580 deletions
20
README.md
20
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)
|
||||
|
|
|
@ -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);
|
||||
})
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
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,
|
||||
|
@ -17,9 +16,9 @@ module.exports = {
|
|||
};
|
||||
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;
|
||||
},
|
||||
|
|
|
@ -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
|
||||
} else {
|
||||
status = 400;
|
||||
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 = {};
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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');
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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%;
|
||||
}
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 9 KiB |
|
@ -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();
|
||||
})
|
||||
};
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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');
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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);
|
||||
};
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
};
|
|
@ -5,8 +5,10 @@ import * as actions from 'constants/channel_action_types';
|
|||
export function updateLoggedInChannel (name, shortId, longId) {
|
||||
return {
|
||||
type: actions.CHANNEL_UPDATE,
|
||||
data: {
|
||||
name,
|
||||
shortId,
|
||||
longId,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
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,
|
||||
data: {
|
||||
status,
|
||||
message,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export function updateError (name, value) {
|
||||
return {
|
||||
type: actions.ERROR_UPDATE,
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
|
109
react/actions/show.js
Normal file
109
react/actions/show.js
Normal file
|
@ -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,
|
||||
};
|
||||
};
|
39
react/api/assetApi.js
Normal file
39
react/api/assetApi.js
Normal file
|
@ -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);
|
||||
};
|
16
react/api/channelApi.js
Normal file
16
react/api/channelApi.js
Normal file
|
@ -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);
|
||||
};
|
11
react/api/fileApi.js
Normal file
11
react/api/fileApi.js
Normal file
|
@ -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);
|
||||
}
|
26
react/app.js
26
react/app.js
|
@ -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(
|
||||
<Provider store={store}>
|
||||
<NavBar />
|
||||
</Provider>,
|
||||
document.getElementById('react-nav-bar')
|
||||
)
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<Publish />
|
||||
</Provider>,
|
||||
document.getElementById('react-publish-tool')
|
||||
)
|
33
react/components/AboutPage/index.js
Normal file
33
react/components/AboutPage/index.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
import React from 'react';
|
||||
import NavBar from 'containers/NavBar';
|
||||
|
||||
class AboutPage extends React.Component {
|
||||
render () {
|
||||
return (
|
||||
<div>
|
||||
<NavBar/>
|
||||
<div className="row row--padded">
|
||||
<div className="column column--5 column--med-10 align-content-top">
|
||||
<div className="column column--8 column--med-10">
|
||||
<p className="pull-quote">Spee.ch is an open-source project. Please contribute to the existing site, or fork it and make your own.</p>
|
||||
<p><a className="link--primary" target="_blank" href="https://twitter.com/spee_ch">TWITTER</a></p>
|
||||
<p><a className="link--primary" target="_blank" href="https://github.com/lbryio/spee.ch">GITHUB</a></p>
|
||||
<p><a className="link--primary" target="_blank" href="https://discord.gg/YjYbwhS">DISCORD CHANNEL</a></p>
|
||||
<p><a className="link--primary" target="_blank" href="https://github.com/lbryio/spee.ch/blob/master/README.md">DOCUMENTATION</a></p>
|
||||
</div>
|
||||
</div><div className="column column--5 column--med-10 align-content-top">
|
||||
<div className="column column--8 column--med-10">
|
||||
<p>Spee.ch is a media-hosting site that reads from and publishes content to the <a className="link--primary" href="https://lbry.io">LBRY</a> blockchain.</p>
|
||||
<p>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.</p>
|
||||
<h3>Contribute</h3>
|
||||
<p>If you have an idea for your own spee.ch-like site on top of LBRY, fork our <a className="link--primary" href="https://github.com/lbryio/spee.ch">github repo</a> and go to town!</p>
|
||||
<p>If you want to improve spee.ch, join our <a className="link--primary" href="https://discord.gg/YjYbwhS">discord channel</a> or solve one of our <a className="link--primary" href="https://github.com/lbryio/spee.ch/issues">github issues</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default AboutPage;
|
28
react/components/AssetDisplay/index.js
Normal file
28
react/components/AssetDisplay/index.js
Normal file
|
@ -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);
|
74
react/components/AssetDisplay/view.jsx
Normal file
74
react/components/AssetDisplay/view.jsx
Normal file
|
@ -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 (
|
||||
<div id="asset-display-component">
|
||||
{(status === LOCAL_CHECK) &&
|
||||
<div>
|
||||
<p>Checking to see if Spee.ch has your asset locally...</p>
|
||||
</div>
|
||||
}
|
||||
{(status === UNAVAILABLE) &&
|
||||
<div>
|
||||
<p>Sit tight, we're searching the LBRY blockchain for your asset!</p>
|
||||
<ProgressBar size={12}/>
|
||||
<p>Curious what magic is happening here? <a className="link--primary" target="blank" href="https://lbry.io/faq/what-is-lbry">Learn more.</a></p>
|
||||
</div>
|
||||
}
|
||||
{(status === ERROR) &&
|
||||
<div>
|
||||
<p>Unfortunately, we couldn't download your asset from LBRY. You can help us out by sharing the below error message in the <a className="link--primary" href="https://discord.gg/YjYbwhS" target="_blank">LBRY discord</a>.</p>
|
||||
<i><p id="error-message">{error}</p></i>
|
||||
</div>
|
||||
}
|
||||
{(status === AVAILABLE) &&
|
||||
(() => {
|
||||
switch (contentType) {
|
||||
case 'image/jpeg':
|
||||
case 'image/jpg':
|
||||
case 'image/png':
|
||||
return (
|
||||
<img
|
||||
className="asset"
|
||||
src={`/${claimId}/${name}.${fileExt}`}
|
||||
alt={name}/>
|
||||
);
|
||||
case 'image/gif':
|
||||
return (
|
||||
<img
|
||||
className="asset"
|
||||
src={`/${claimId}/${name}.${fileExt}`}
|
||||
alt={name}
|
||||
/>
|
||||
);
|
||||
case 'video/mp4':
|
||||
return (
|
||||
<video id="video" className="asset" controls poster={thumbnail}>
|
||||
<source
|
||||
src={`/${claimId}/${name}.${fileExt}`}
|
||||
/>
|
||||
<p>Your browser does not support the <code>video</code> element.</p>
|
||||
</video>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<p>Unsupported file type</p>
|
||||
);
|
||||
}
|
||||
})()
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default AssetDisplay;
|
14
react/components/AssetInfo/index.js
Normal file
14
react/components/AssetInfo/index.js
Normal file
|
@ -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);
|
165
react/components/AssetInfo/view.jsx
Normal file
165
react/components/AssetInfo/view.jsx
Normal file
|
@ -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 (
|
||||
<div>
|
||||
{channelName &&
|
||||
<div className="row row--padded row--wide row--no-top">
|
||||
<div className="column column--2 column--med-10">
|
||||
<span className="text">Channel:</span>
|
||||
</div>
|
||||
<div className="column column--8 column--med-10">
|
||||
<span className="text"><Link to={`/${channelName}:${certificateId}`}>{channelName}</Link></span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
{description &&
|
||||
<div className="row row--padded row--wide row--no-top">
|
||||
<span className="text">{description}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className="row row--padded row--wide row--no-top">
|
||||
<div id="show-short-link">
|
||||
<div className="column column--2 column--med-10">
|
||||
<Link className="link--primary" to={`/${shortId}/${name}.${fileExt}`}><span
|
||||
className="text">Link:</span></Link>
|
||||
</div>
|
||||
<div className="column column--8 column--med-10">
|
||||
<div className="row row--short row--wide">
|
||||
<div className="column column--7">
|
||||
<div className="input-error" id="input-error-copy-short-link" hidden="true">error here</div>
|
||||
<input type="text" id="short-link" className="input-disabled input-text--full-width" readOnly
|
||||
spellCheck="false"
|
||||
value={`${host}/${shortId}/${name}.${fileExt}`}
|
||||
onClick={this.select}/>
|
||||
</div>
|
||||
<div className="column column--1"> </div>
|
||||
<div className="column column--2">
|
||||
<button className="button--primary" data-elementtocopy="short-link"
|
||||
onClick={this.copyToClipboard}>copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="show-embed-code">
|
||||
<div className="column column--2 column--med-10">
|
||||
<span className="text">Embed:</span>
|
||||
</div>
|
||||
<div className="column column--8 column--med-10">
|
||||
<div className="row row--short row--wide">
|
||||
<div className="column column--7">
|
||||
<div className="input-error" id="input-error-copy-embed-text" hidden="true">error here</div>
|
||||
{(contentType === 'video/mp4') ? (
|
||||
<input type="text" id="embed-text" className="input-disabled input-text--full-width" readOnly
|
||||
onClick={this.select} spellCheck="false"
|
||||
value={`<video width="100%" controls poster="${thumbnail}" src="${host}/${claimId}/${name}.${fileExt}"/></video>`}/>
|
||||
) : (
|
||||
<input type="text" id="embed-text" className="input-disabled input-text--full-width" readOnly
|
||||
onClick={this.select} spellCheck="false"
|
||||
value={`<img src="${host}/${claimId}/${name}.${fileExt}"/>`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="column column--1"> </div>
|
||||
<div className="column column--2">
|
||||
<button className="button--primary" data-elementtocopy="embed-text"
|
||||
onClick={this.copyToClipboard}>copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="show-share-buttons">
|
||||
<div className="row row--padded row--wide row--no-top">
|
||||
<div className="column column--2 column--med-10">
|
||||
<span className="text">Share:</span>
|
||||
</div>
|
||||
<div className="column column--7 column--med-10">
|
||||
<div
|
||||
className="row row--short row--wide flex-container--row flex-container--space-between-bottom flex-container--wrap">
|
||||
<a className="link--primary" target="_blank"
|
||||
href={`https://twitter.com/intent/tweet?text=${host}/${shortId}/${name}`}>twitter</a>
|
||||
<a className="link--primary" target="_blank"
|
||||
href={`https://www.facebook.com/sharer/sharer.php?u=${host}/${shortId}/${name}`}>facebook</a>
|
||||
<a className="link--primary" target="_blank"
|
||||
href={`http://tumblr.com/widgets/share/tool?canonicalUrl=${host}/${shortId}/${name}`}>tumblr</a>
|
||||
<a className="link--primary" target="_blank"
|
||||
href={`https://www.reddit.com/submit?url=${host}/${shortId}/${name}&title=${name}`}>reddit</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ this.state.showDetails &&
|
||||
<div>
|
||||
<div className="row--padded row--wide row--no-top">
|
||||
<div>
|
||||
<div className="column column--2 column--med-10">
|
||||
<span className="text">Claim Name:</span>
|
||||
</div><div className="column column--8 column--med-10">
|
||||
{name}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="column column--2 column--med-10">
|
||||
<span className="text">Claim Id:</span>
|
||||
</div><div className="column column--8 column--med-10">
|
||||
{claimId}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="column column--2 column--med-10">
|
||||
<span className="text">File Type:</span>
|
||||
</div><div className="column column--8 column--med-10">
|
||||
{contentType ? `${contentType}` : 'unknown'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row--padded row--wide row--no-top">
|
||||
<div className="column column--10">
|
||||
<a target="_blank" href="https://lbry.io/dmca">Report</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div className="row row--wide">
|
||||
<button className="button--secondary" onClick={this.toggleDetails}>{this.state.showDetails ? 'less' : 'more'}</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default AssetInfo;
|
39
react/components/AssetPreview/index.js
Normal file
39
react/components/AssetPreview/index.js
Normal file
|
@ -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 (
|
||||
<div className="asset-holder">
|
||||
<Link to={showUrlLink} >
|
||||
{(() => {
|
||||
switch (contentType) {
|
||||
case 'image/jpeg':
|
||||
case 'image/jpg':
|
||||
case 'image/png':
|
||||
return (
|
||||
<img className={'asset-preview'} src={directSourceLink} alt={name}/>
|
||||
);
|
||||
case 'image/gif':
|
||||
return (
|
||||
<img className={'asset-preview'} src={directSourceLink} alt={name}/>
|
||||
);
|
||||
case 'video/mp4':
|
||||
return (
|
||||
<video className={'asset-preview'}>
|
||||
<source src={directSourceLink} type={contentType}/>
|
||||
</video>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<p>unsupported file type</p>
|
||||
);
|
||||
}
|
||||
})()}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AssetPreview;
|
14
react/components/AssetTitle/index.js
Normal file
14
react/components/AssetTitle/index.js
Normal file
|
@ -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);
|
11
react/components/AssetTitle/view.jsx
Normal file
11
react/components/AssetTitle/view.jsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
import React from 'react';
|
||||
|
||||
const AssetTitle = ({ title }) => {
|
||||
return (
|
||||
<div>
|
||||
<span className="text--large">{title}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AssetTitle;
|
23
react/components/ErrorPage/index.js
Normal file
23
react/components/ErrorPage/index.js
Normal file
|
@ -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 (
|
||||
<div>
|
||||
<NavBar/>
|
||||
<div className="row row--padded">
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
ErrorPage.propTypes = {
|
||||
error: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
export default ErrorPage;
|
18
react/components/FourOhFourPage/index.js
Normal file
18
react/components/FourOhFourPage/index.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
import React from 'react';
|
||||
import NavBar from 'containers/NavBar';
|
||||
|
||||
class FourOhForPage extends React.Component {
|
||||
render () {
|
||||
return (
|
||||
<div>
|
||||
<NavBar/>
|
||||
<div className="row row--padded">
|
||||
<h2>404</h2>
|
||||
<p>That page does not exist</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default FourOhForPage;
|
|
@ -1,9 +1,10 @@
|
|||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
function Logo () {
|
||||
return (
|
||||
<svg version="1.1" id="Layer_1" x="0px" y="0px" height="24px" viewBox="0 0 80 31" enableBackground="new 0 0 80 31" className="nav-bar-logo">
|
||||
<a href="/">
|
||||
<Link to="/">
|
||||
<title>Logo</title>
|
||||
<desc>Spee.ch logo</desc>
|
||||
<g id="About">
|
||||
|
@ -20,7 +21,7 @@ function Logo () {
|
|||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</a>
|
||||
</Link>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import React from 'react';
|
||||
|
||||
function NavBarChannelOptionsDropdown ({ channelName, handleSelection, VIEW, LOGOUT }) {
|
||||
function NavBarChannelDropdown ({ channelName, handleSelection, defaultSelection, VIEW, LOGOUT }) {
|
||||
return (
|
||||
<select type="text" id="nav-bar-channel-select" className="select select--arrow link--nav" onChange={handleSelection}>
|
||||
<select type="text" id="nav-bar-channel-select" className="select select--arrow link--nav" onChange={handleSelection} value={defaultSelection}>
|
||||
<option id="nav-bar-channel-select-channel-option">{channelName}</option>
|
||||
<option value={VIEW}>View</option>
|
||||
<option value={LOGOUT}>Logout</option>
|
||||
|
@ -10,4 +10,4 @@ function NavBarChannelOptionsDropdown ({ channelName, handleSelection, VIEW, LOG
|
|||
);
|
||||
};
|
||||
|
||||
export default NavBarChannelOptionsDropdown;
|
||||
export default NavBarChannelDropdown;
|
||||
|
|
|
@ -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 (
|
||||
<img
|
||||
id="asset-preview"
|
||||
id="dropzone-preview"
|
||||
src={this.state.imgSource}
|
||||
className={this.props.dimPreview ? 'dim' : ''}
|
||||
alt="publish preview"
|
||||
|
|
18
react/components/PublishPage/index.js
Normal file
18
react/components/PublishPage/index.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
import React from 'react';
|
||||
import NavBar from 'containers/NavBar';
|
||||
import PublishTool from 'containers/PublishTool';
|
||||
|
||||
class PublishPage extends React.Component {
|
||||
render () {
|
||||
return (
|
||||
<div className={'row row--tall flex-container--column'}>
|
||||
<NavBar/>
|
||||
<div className={'row row--tall row--padded flex-container--column'}>
|
||||
<PublishTool/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default PublishPage;
|
|
@ -30,7 +30,7 @@ function PublishStatus ({ status, message }) {
|
|||
{(status === publishStates.SUCCESS) &&
|
||||
<div className="row align-content-center">
|
||||
<p>Your publish is complete! You are being redirected to it now.</p>
|
||||
<p>If you are not automatically redirected, <a class="link--primary" target="_blank" href={message}>click here.</a></p>
|
||||
<p>If you are not automatically redirected, <a className="link--primary" target="_blank" href={message}>click here.</a></p>
|
||||
</div>
|
||||
}
|
||||
{(status === publishStates.FAILED) &&
|
||||
|
|
21
react/components/ShowAssetDetails/index.js
Normal file
21
react/components/ShowAssetDetails/index.js
Normal file
|
@ -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);
|
39
react/components/ShowAssetDetails/view.jsx
Normal file
39
react/components/ShowAssetDetails/view.jsx
Normal file
|
@ -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 (
|
||||
<div>
|
||||
<NavBar/>
|
||||
<div className="row row--tall row--padded">
|
||||
<div className="column column--10">
|
||||
<AssetTitle />
|
||||
</div>
|
||||
<div className="column column--5 column--sml-10 align-content-top">
|
||||
<div className="row row--padded">
|
||||
<AssetDisplay />
|
||||
</div>
|
||||
</div><div className="column column--5 column--sml-10 align-content-top">
|
||||
<div className="row row--padded">
|
||||
<AssetInfo />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<ErrorPage error={'loading asset data...'}/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ShowAssetDetails;
|
21
react/components/ShowAssetLite/index.js
Normal file
21
react/components/ShowAssetLite/index.js
Normal file
|
@ -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);
|
21
react/components/ShowAssetLite/view.jsx
Normal file
21
react/components/ShowAssetLite/view.jsx
Normal file
|
@ -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 (
|
||||
<div className="row row--tall flex-container--column flex-container--center-center">
|
||||
{ (asset) &&
|
||||
<div>
|
||||
<AssetDisplay />
|
||||
<Link id="asset-boilerpate" className="link--primary fine-print" to={`/${asset.claimId}/${asset.name}`}>hosted via Spee.ch</Link>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ShowLite;
|
20
react/components/ShowChannel/index.js
Normal file
20
react/components/ShowChannel/index.js
Normal file
|
@ -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);
|
33
react/components/ShowChannel/view.jsx
Normal file
33
react/components/ShowChannel/view.jsx
Normal file
|
@ -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 (
|
||||
<div>
|
||||
<NavBar/>
|
||||
<div className="row row--tall row--padded">
|
||||
<div className="column column--10">
|
||||
<h2>channel name: {name || 'loading...'}</h2>
|
||||
<p className={'fine-print'}>full channel id: {longId || 'loading...'}</p>
|
||||
<p className={'fine-print'}>short channel id: {shortId || 'loading...'}</p>
|
||||
</div>
|
||||
<div className="column column--10">
|
||||
<ChannelClaimsDisplay />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<ErrorPage error={'loading channel data...'}/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ShowChannel;
|
4
react/constants/asset_display_states.js
Normal file
4
react/constants/asset_display_states.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
export const LOCAL_CHECK = 'LOCAL_CHECK';
|
||||
export const UNAVAILABLE = 'UNAVAILABLE';
|
||||
export const ERROR = 'ERROR';
|
||||
export const AVAILABLE = 'AVAILABLE';
|
20
react/constants/show_action_types.js
Normal file
20
react/constants/show_action_types.js
Normal file
|
@ -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';
|
3
react/constants/show_request_types.js
Normal file
3
react/constants/show_request_types.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
export const CHANNEL = 'CHANNEL';
|
||||
export const ASSET_LITE = 'ASSET_LITE';
|
||||
export const ASSET_DETAILS = 'ASSET_DETAILS';
|
22
react/containers/ChannelClaimsDisplay/index.js
Normal file
22
react/containers/ChannelClaimsDisplay/index.js
Normal file
|
@ -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);
|
54
react/containers/ChannelClaimsDisplay/view.jsx
Normal file
54
react/containers/ChannelClaimsDisplay/view.jsx
Normal file
|
@ -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 (
|
||||
<div className="row row--tall">
|
||||
{(claims.length > 0) ? (
|
||||
<div>
|
||||
{claims.map((claim, index) => <AssetPreview
|
||||
name={claim.name}
|
||||
claimId={claim.claimId}
|
||||
fileExt={claim.fileExt}
|
||||
contentType={claim.contentType}
|
||||
key={`${claim.name}-${index}`}
|
||||
/>)}
|
||||
<div>
|
||||
{(currentPage > 1) &&
|
||||
<button className={'button--secondary'} onClick={this.showPreviousResultsPage}>Previous Page</button>
|
||||
}
|
||||
{(currentPage < totalPages) &&
|
||||
<button className={'button--secondary'} onClick={this.showNextResultsPage}>Next Page</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p>There are no claims in this channel</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ChannelClaimsDisplay;
|
|
@ -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 () {
|
||||
|
|
|
@ -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});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
10
react/containers/LoginPage/index.js
Normal file
10
react/containers/LoginPage/index.js
Normal file
|
@ -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);
|
38
react/containers/LoginPage/view.jsx
Normal file
38
react/containers/LoginPage/view.jsx
Normal file
|
@ -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 (
|
||||
<div>
|
||||
<NavBar/>
|
||||
<div className="row row--padded">
|
||||
<div className="column column--5 column--med-10 align-content-top">
|
||||
<div className="column column--8 column--med-10">
|
||||
<p>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 <a className="link--primary" target="_blank" href="/@catalonia2017:43dcf47163caa21d8404d9fe9b30f78ef3e146a8">documenting important events</a>, or making a public repository for <a className="link--primary" target="_blank" href="/@catGifs">cat gifs</a> (password: '1234'), try creating a channel for it!</p>
|
||||
</div>
|
||||
</div><div className="column column--5 column--med-10 align-content-top">
|
||||
<div className="column column--8 column--med-10">
|
||||
<h3 className="h3--no-bottom">Log in to an existing channel:</h3>
|
||||
<ChannelLoginForm />
|
||||
<h3 className="h3--no-bottom">Create a brand new channel:</h3>
|
||||
<ChannelCreateForm />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default withRouter(PublishPage);
|
|
@ -17,6 +17,9 @@ const mapDispatchToProps = dispatch => {
|
|||
dispatch(updateLoggedInChannel(name, shortId, longId));
|
||||
dispatch(updateSelectedChannel(name));
|
||||
},
|
||||
onChannelLogout: () => {
|
||||
dispatch(updateLoggedInChannel(null, null, null));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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 {
|
|||
<span className="nav-bar-tagline">Open-source, decentralized image and video sharing.</span>
|
||||
</div>
|
||||
<div className="nav-bar--right">
|
||||
<a className="nav-bar-link link--nav-active" href="/">Publish</a>
|
||||
<a className="nav-bar-link link--nav" href="/about">About</a>
|
||||
<NavLink className="nav-bar-link link--nav" activeClassName="link--nav-active" to="/" exact={true}>Publish</NavLink>
|
||||
<NavLink className="nav-bar-link link--nav" activeClassName="link--nav-active" to="/about">About</NavLink>
|
||||
{ this.props.channelName ? (
|
||||
<NavBarChannelDropdown
|
||||
channelName={this.props.channelName}
|
||||
handleSelection={this.handleSelection}
|
||||
defaultSelection={this.props.channelName}
|
||||
VIEW={VIEW}
|
||||
LOGOUT={LOGOUT}
|
||||
/>
|
||||
) : (
|
||||
<a id="nav-bar-login-link" className="nav-bar-link link--nav" href="/login">Channel</a>
|
||||
<NavLink id="nav-bar-login-link" className="nav-bar-link link--nav" activeClassName="link--nav-active" to="/login">Channel</NavLink>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -82,4 +83,4 @@ class NavBar extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
export default NavBar;
|
||||
export default withRouter(NavBar);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -65,7 +65,7 @@ class PublishMetadataInputs extends React.Component {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
<a className="label link--primary" id="publish-details-toggle" href="#" onClick={this.toggleShowInputs}>{this.props.showMetadataInputs ? '[less]' : '[more]'}</a>
|
||||
<button className="button--secondary" onClick={this.toggleShowInputs}>{this.props.showMetadataInputs ? 'less' : 'more'}</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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 () {
|
||||
|
|
16
react/containers/ShowPage/index.js
Normal file
16
react/containers/ShowPage/index.js
Normal file
|
@ -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);
|
38
react/containers/ShowPage/view.jsx
Normal file
38
react/containers/ShowPage/view.jsx
Normal file
|
@ -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 (
|
||||
<ErrorPage error={error}/>
|
||||
);
|
||||
}
|
||||
switch (requestType) {
|
||||
case CHANNEL:
|
||||
return <ShowChannel />;
|
||||
case ASSET_LITE:
|
||||
return <ShowAssetLite />;
|
||||
case ASSET_DETAILS:
|
||||
return <ShowAssetDetails />;
|
||||
default:
|
||||
return <p>loading...</p>;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default ShowPage;
|
24
react/index.js
Normal file
24
react/index.js
Normal file
|
@ -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(
|
||||
<Root store={store} />,
|
||||
document.getElementById('react-app')
|
||||
);
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
96
react/reducers/show.js
Normal file
96
react/reducers/show.js
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
31
react/root.js
Normal file
31
react/root.js
Normal file
|
@ -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 }) => (
|
||||
<Provider store={store}>
|
||||
<BrowserRouter>
|
||||
<Switch>
|
||||
<Route exact path="/" component={PublishPage} />
|
||||
<Route exact path="/about" component={AboutPage} />
|
||||
<Route exact path="/login" component={LoginPage} />
|
||||
<Route exact path="/:identifier/:claim" component={ShowPage} />
|
||||
<Route exact path="/:claim" component={ShowPage} />
|
||||
<Route component={FourOhFourPage} />
|
||||
</Switch>
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
);
|
||||
|
||||
Root.propTypes = {
|
||||
store: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default Root;
|
33
react/sagas/file.js
Normal file
33
react/sagas/file.js
Normal file
|
@ -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);
|
||||
};
|
15
react/sagas/index.js
Normal file
15
react/sagas/index.js
Normal file
|
@ -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(),
|
||||
]);
|
||||
}
|
57
react/sagas/show_asset.js
Normal file
57
react/sagas/show_asset.js
Normal file
|
@ -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);
|
||||
};
|
64
react/sagas/show_channel.js
Normal file
64
react/sagas/show_channel.js
Normal file
|
@ -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);
|
||||
}
|
60
react/sagas/show_uri.js
Normal file
60
react/sagas/show_uri.js
Normal file
|
@ -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);
|
||||
};
|
9
react/selectors/show.js
Normal file
9
react/selectors/show.js
Normal file
|
@ -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;
|
||||
};
|
85
react/utils/lbryUri.js
Normal file
85
react/utils/lbryUri.js
Normal file
|
@ -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,
|
||||
};
|
||||
},
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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 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 === true) {
|
||||
res.status(200).json(true);
|
||||
} else {
|
||||
res.status(200).json(false);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
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) => {
|
||||
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}})
|
||||
.then(result => {
|
||||
if (result) {
|
||||
isLocalFileAvailable = true;
|
||||
}
|
||||
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 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,15 +100,14 @@ 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) {
|
||||
|
@ -75,35 +117,21 @@ module.exports = (app) => {
|
|||
}
|
||||
})
|
||||
.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);
|
||||
} else {
|
||||
res.status(200).json(false);
|
||||
}
|
||||
})
|
||||
.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)
|
||||
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.handleApiError(originalUrl, ip, error, res);
|
||||
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,7 +147,6 @@ 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
|
||||
|
@ -147,8 +174,10 @@ module.exports = (app) => {
|
|||
.then(result => {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: {
|
||||
message: 'publish completed successfully',
|
||||
data : {
|
||||
name,
|
||||
claimId: result.claim_id,
|
||||
url : `${site.host}/${result.claim_id}/${name}`,
|
||||
lbryTx : result,
|
||||
},
|
||||
|
@ -159,30 +188,67 @@ module.exports = (app) => {
|
|||
sendGoogleAnalyticsTiming(timingActionType, headers, ip, originalUrl, publishStartTime, publishEndTime);
|
||||
})
|
||||
.catch(error => {
|
||||
errorHandlers.handleApiError(originalUrl, ip, error, res);
|
||||
errorHandlers.handleErrorResponse(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);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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'});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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');
|
||||
return res.status(404).json({success: false, message: 'no claim id could be found'});
|
||||
} else if (fullClaimId === NO_CHANNEL) {
|
||||
return res.status(200).render('noChannel');
|
||||
return res.status(404).json({success: false, message: 'no channel id could be found'});
|
||||
}
|
||||
showOrServeAsset(responseType, fullClaimId, claimName, res);
|
||||
postToStats(responseType, originalUrl, ip, claimName, fullClaimId, 'success');
|
||||
serveAssetToClient(fullClaimId, claimName, res);
|
||||
// postToStats(responseType, originalUrl, ip, claimName, fullClaimId, 'success');
|
||||
})
|
||||
.catch(error => {
|
||||
handleRequestError(originalUrl, ip, error, res);
|
||||
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;
|
||||
let responseType = determineResponseType(hasFileExtension, headers);
|
||||
if (responseType !== SERVE) {
|
||||
return res.status(200).render('index');
|
||||
}
|
||||
// parse the claim
|
||||
let claimName;
|
||||
try {
|
||||
({claimName, isServeRequest} = lbryUri.parseName(params.identifier));
|
||||
({claimName} = lbryUri.parseClaim(params.claim));
|
||||
} catch (error) {
|
||||
return handleRequestError(originalUrl, ip, error, res);
|
||||
return res.status(400).json({success: false, message: error.message});
|
||||
}
|
||||
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
|
||||
// get the claim Id and then serve the asset
|
||||
getClaimId(null, null, claimName, null)
|
||||
.then(fullClaimId => {
|
||||
if (fullClaimId === NO_CLAIM) {
|
||||
return res.status(200).render('noClaim');
|
||||
return res.status(404).json({success: false, message: 'no claim id could be found'});
|
||||
}
|
||||
showOrServeAsset(responseType, fullClaimId, claimName, res);
|
||||
postToStats(responseType, originalUrl, ip, claimName, fullClaimId, 'success');
|
||||
serveAssetToClient(fullClaimId, claimName, res);
|
||||
// postToStats(responseType, originalUrl, ip, claimName, fullClaimId, 'success');
|
||||
})
|
||||
.catch(error => {
|
||||
handleRequestError(originalUrl, ip, error, res);
|
||||
handleErrorResponse(originalUrl, ip, error, res);
|
||||
// postToStats(responseType, originalUrl, ip, claimName, fullClaimId, 'fail');
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
{{> navBar}}
|
||||
<div class="row row--padded">
|
||||
<div class="row row--tall flex-container--column flex-container--center-center">
|
||||
<h3>404: Not Found</h3>
|
||||
<p>That page does not exist. Return <a class="link--primary" href="/">home</a>.</p>
|
||||
</div>
|
|
@ -1,21 +0,0 @@
|
|||
{{> navBar}}
|
||||
<div class="row row--padded">
|
||||
<div class="column column--5 column--med-10 align-content-top">
|
||||
<div class="column column--8 column--med-10">
|
||||
<p class="pull-quote">Spee.ch is an open-source project. Please contribute to the existing site, or fork it and make your own.</p>
|
||||
<p><a class="link--primary" target="_blank" href="https://twitter.com/spee_ch">TWITTER</a></p>
|
||||
<p><a class="link--primary" target="_blank" href="https://github.com/lbryio/spee.ch">GITHUB</a></p>
|
||||
<p><a class="link--primary" target="_blank" href="https://discord.gg/YjYbwhS">DISCORD CHANNEL</a></p>
|
||||
<p><a class="link--primary" target="_blank" href="https://github.com/lbryio/spee.ch/blob/master/README.md">DOCUMENTATION</a></p>
|
||||
</div>
|
||||
</div><div class="column column--5 column--med-10 align-content-top">
|
||||
<div class="column column--8 column--med-10">
|
||||
<p>Spee.ch is a media-hosting site that reads from and publishes content to the <a class="link--primary" href="https://lbry.io">LBRY</a> blockchain.</p>
|
||||
<p>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.</p>
|
||||
<h3>Contribute</h3>
|
||||
<p>If you have an idea for your own spee.ch-like site on top of LBRY, fork our <a class="link--primary" href="https://github.com/lbryio/spee.ch">github repo</a> and go to town!</p>
|
||||
<p>If you want to improve spee.ch, join our <a class="link--primary" href="https://discord.gg/YjYbwhS">discord channel</a> or solve one of our <a class="link--primary" href="https://github.com/lbryio/spee.ch/issues">github issues</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
|
@ -1,55 +0,0 @@
|
|||
{{> navBar}}
|
||||
<div class="row row--padded">
|
||||
<div class="row">
|
||||
{{#ifConditional this.totalPages '===' 0}}
|
||||
<p>There is no content in {{this.channelName}}:{{this.longChannelClaimId}} yet. Upload some!</p>
|
||||
{{/ifConditional}}
|
||||
{{#ifConditional this.totalPages '>=' 1}}
|
||||
<p>Below are the contents for {{this.channelName}}:{{this.longChannelClaimId}}</p>
|
||||
<div class="grid">
|
||||
{{#each this.claims}}
|
||||
{{> gridItem}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/ifConditional}}
|
||||
{{#ifConditional this.totalPages '>' 1}}
|
||||
<div class="row">
|
||||
<div class="column column--3 align-content--left">
|
||||
<a class="link--primary" href="/{{this.channelName}}:{{this.longChannelClaimId}}?p=1">First [1]</a>
|
||||
</div><div class="column column--4 align-content-center">
|
||||
{{#if this.previousPage}}
|
||||
<a class="link--primary" href="/{{this.channelName}}:{{this.longChannelClaimId}}?p={{this.previousPage}}">Previous</a>
|
||||
{{else}}
|
||||
<a disabled>Previous</a>
|
||||
{{/if}}
|
||||
|
|
||||
{{#if this.nextPage}}
|
||||
<a class="link--primary" href="/{{this.channelName}}:{{this.longChannelClaimId}}?p={{this.nextPage}}">Next</a>
|
||||
{{else}}
|
||||
<a disabled>Next</a>
|
||||
{{/if}}
|
||||
</div><div class="column column--3 align-content-right">
|
||||
<a class="link--primary" href="/{{this.channelName}}:{{this.longChannelClaimId}}?p={{this.totalPages}}">Last [{{this.totalPages}}]</a>
|
||||
</div>
|
||||
</div>
|
||||
{{/ifConditional}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/assets/vendors/masonry/masonry.pkgd.min.js"></script>
|
||||
<script src="/assets/vendors/imagesloaded/imagesloaded.pkgd.min.js"></script>
|
||||
<script>
|
||||
// init masonry with element
|
||||
var grid = document.querySelector('.grid');
|
||||
var msnry;
|
||||
|
||||
imagesLoaded( grid, function() {
|
||||
msnry = new Masonry( grid, {
|
||||
itemSelector: '.grid-item',
|
||||
columnWidth: 3,
|
||||
percentPosition: true
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
</script>
|
|
@ -1,6 +1,5 @@
|
|||
<div id="react-nav-bar"></div>
|
||||
<div class="row row--tall flex-container--column">
|
||||
<div id="react-publish-tool" class="row row--padded row--tall flex-container--column">
|
||||
<div id="react-app" class="row row--tall flex-container--column">
|
||||
<div class="row row--padded row--tall flex-container--column flex-container--center-center">
|
||||
<p>loading...</p>
|
||||
{{> progressBar}}
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb#">
|
||||
<head>
|
||||
{{ placeCommonHeaderTags }}
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:site" content="@spee_ch" />
|
||||
<meta property="og:title" content="{{this.channelName}} on Spee.ch" />
|
||||
<meta property="og:site_name" content="Spee.ch" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:image" content="https://spee.ch/assets/img/Speech_Logo_Main@OG-02.jpg" />
|
||||
<meta property="og:url" content="http://spee.ch/{{this.channelName}}:{{this.longChannelId}}" />
|
||||
<meta property="og:description" content="View images and videos from {{this.channelName}}" />
|
||||
<!--google font-->
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet">
|
||||
<!-- google analytics -->
|
||||
{{ googleAnalytics }}
|
||||
</head>
|
||||
<body id="channel-body">
|
||||
<script src="/assets/js/generalFunctions.js"></script>
|
||||
{{{ body }}}
|
||||
</body>
|
||||
</html>
|
|
@ -16,8 +16,6 @@
|
|||
{{ googleAnalytics }}
|
||||
</head>
|
||||
<body id="main-body">
|
||||
<script src="/assets/js/generalFunctions.js"></script>
|
||||
<script src="/assets/js/validationFunctions.js"></script>
|
||||
{{{ body }}}
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb# article: http://ogp.me/ns/article#">
|
||||
<head>
|
||||
{{ placeCommonHeaderTags }}
|
||||
<meta property="fb:app_id" content="1371961932852223">
|
||||
{{#unless claimInfo.nsfw}}
|
||||
{{{addTwitterCard claimInfo }}}
|
||||
{{{addOpenGraph claimInfo }}}
|
||||
{{/unless}}
|
||||
<!--google font-->
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet">
|
||||
<!-- google analytics -->
|
||||
{{ googleAnalytics }}
|
||||
</head>
|
||||
<body id="show-body">
|
||||
<script src="/assets/js/generalFunctions.js"></script>
|
||||
<script src="/assets/js/assetConstructor.js"></script>
|
||||
{{{ body }}}
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
{{> navBar}}
|
||||
<div class="row row--padded">
|
||||
<div class="column column--5 column--med-10 align-content-top">
|
||||
<div class="column column--8 column--med-10">
|
||||
<p>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 <a class="link--primary" target="_blank" href="/@catalonia2017:43dcf47163caa21d8404d9fe9b30f78ef3e146a8">documenting important events</a>, or making a public repository for <a class="link--primary" target="_blank" href="/@catGifs">cat gifs</a> (password: '1234'), try creating a channel for it!</p>
|
||||
</div>
|
||||
</div><div class="column column--5 column--med-10 align-content-top">
|
||||
<div class="column column--8 column--med-10">
|
||||
<h3 class="h3--no-bottom">Log in to an existing channel:</h3>
|
||||
{{>channelLoginForm}}
|
||||
<h3 class="h3--no-bottom">Create a brand new channel:</h3>
|
||||
{{>channelCreationForm}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
{{> navBar}}
|
||||
<div class="row row--padded">
|
||||
<h3>No Channel</h3>
|
||||
<p>There are no published channels matching your url</p>
|
||||
<p>If you think this message is an error, contact us in the <a class="link--primary" href="https://discord.gg/YjYbwhS" target="_blank">LBRY Discord!</a></p>
|
||||
</div>
|
|
@ -1,6 +0,0 @@
|
|||
{{> navBar}}
|
||||
<div class="row row--padded">
|
||||
<h3>No Claims</h3>
|
||||
<p>There are no free assets at that claim. You should publish one at <a class="link--primary" href="/">spee.ch</a>.</p>
|
||||
<p>NOTE: it is possible your claim was published, but it is still being processed by the blockchain</p>
|
||||
</div>
|
|
@ -1,36 +0,0 @@
|
|||
<div id="asset-display-component">
|
||||
<div id="asset-status">
|
||||
<div id="searching-message" hidden="true">
|
||||
<p>Sit tight, we're searching the LBRY blockchain for your asset!</p>
|
||||
{{> progressBar}}
|
||||
<p>Curious what magic is happening here? <a class="link--primary" target="blank" href="https://lbry.io/faq/what-is-lbry">Learn more.</a></p>
|
||||
</div>
|
||||
<div id="failure-message" hidden="true">
|
||||
<p>Unfortunately, we couldn't download your asset from LBRY. You can help us out by sharing the below error message in the <a class="link--primary" href="https://discord.gg/YjYbwhS" target="_blank">LBRY discord</a>.</p>
|
||||
<i><p id="error-message"></p></i>
|
||||
</div>
|
||||
</div>
|
||||
<div id="asset-holder" hidden="true">
|
||||
{{#ifConditional claimInfo.contentType '===' 'video/mp4'}}
|
||||
{{> video}}
|
||||
{{else}}
|
||||
{{> image}}
|
||||
{{/ifConditional}}
|
||||
<div>
|
||||
<a id="asset-boilerpate" class="link--primary fine-print" href="/{{claimInfo.claimId}}/{{claimInfo.name}}">hosted via Spee<h</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
const asset = new Asset();
|
||||
asset.data['src'] = '/{{claimInfo.claimId}}/{{claimInfo.name}}.{{claimInfo.fileExt}}';
|
||||
asset.data['claimName'] = '{{claimInfo.name}}';
|
||||
asset.data['claimId'] = '{{claimInfo.claimId}}';
|
||||
asset.data['fileExt'] = '{{claimInfo.fileExt}}';
|
||||
asset.data['contentType'] = '{{claimInfo.contentType}}';
|
||||
console.log('asset data:', asset.data);
|
||||
asset.checkFileAndRenderAsset();
|
||||
|
||||
</script>
|
|
@ -1,134 +0,0 @@
|
|||
{{#if claimInfo.channelName}}
|
||||
<div class="row row--padded row--wide row--no-top">
|
||||
<div class="column column--2 column--med-10">
|
||||
<span class="text">Channel:</span>
|
||||
</div><div class="column column--8 column--med-10">
|
||||
<span class="text"><a href="/{{claimInfo.channelName}}:{{claimInfo.certificateId}}">{{claimInfo.channelName}}</a></span>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if claimInfo.description}}
|
||||
<div class="row row--padded row--wide row--no-top">
|
||||
<span class="text">{{claimInfo.description}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="row row--padded row--wide row--no-top">
|
||||
<div id="show-short-link">
|
||||
<div class="column column--2 column--med-10">
|
||||
<a class="link--primary" href="/{{shortId}}/{{claimInfo.name}}.{{claimInfo.fileExt}}"><span class="text">Link:</span></a>
|
||||
</div><div class="column column--8 column--med-10">
|
||||
<div class="row row--short row--wide">
|
||||
<div class="column column--7">
|
||||
<div class="input-error" id="input-error-copy-short-link" hidden="true"></div>
|
||||
<input type="text" id="short-link" class="input-disabled input-text--full-width" readonly spellcheck="false" value="{{claimInfo.host}}/{{shortId}}/{{claimInfo.name}}.{{claimInfo.fileExt}}" onclick="select()"/>
|
||||
</div><div class="column column--1"></div><div class="column column--2">
|
||||
<button class="button--primary" data-elementtocopy="short-link" onclick="copyToClipboard(event)">copy</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="show-embed-code">
|
||||
<div class="column column--2 column--med-10">
|
||||
<span class="text">Embed:</span>
|
||||
</div><div class="column column--8 column--med-10">
|
||||
<div class="row row--short row--wide">
|
||||
<div class="column column--7">
|
||||
<div class="input-error" id="input-error-copy-embed-text" hidden="true"></div>
|
||||
{{#ifConditional claimInfo.contentType '===' 'video/mp4'}}
|
||||
<input type="text" id="embed-text" class="input-disabled input-text--full-width" readonly onclick="select()" spellcheck="false" value='<video width="100%" controls poster="{{claimInfo.thumbnail}}" src="{{claimInfo.host}}/{{claimInfo.claimId}}/{{claimInfo.name}}.{{claimInfo.fileExt}}"/></video>'/>
|
||||
{{else}}
|
||||
<input type="text" id="embed-text" class="input-disabled input-text--full-width" readonly onclick="select()" spellcheck="false" value='<img src="{{claimInfo.host}}/{{claimInfo.claimId}}/{{claimInfo.name}}.{{claimInfo.fileExt}}"/>'/>
|
||||
{{/ifConditional}}
|
||||
</div><div class="column column--1"></div><div class="column column--2">
|
||||
<button class="button--primary" data-elementtocopy="embed-text" onclick="copyToClipboard(event)">copy</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="show-share-buttons">
|
||||
<div class="row row--padded row--wide row--no-top">
|
||||
<div class="column column--2 column--med-10">
|
||||
<span class="text">Share:</span>
|
||||
</div><div class="column column--7 column--med-10">
|
||||
<div class="row row--short row--wide flex-container--row flex-container--space-between-bottom flex-container--wrap">
|
||||
<a class="link--primary" target="_blank" href="https://twitter.com/intent/tweet?text={{claimInfo.host}}/{{shortId}}/{{claimInfo.name}}">twitter</a>
|
||||
<a class="link--primary" target="_blank" href="https://www.facebook.com/sharer/sharer.php?u={{claimInfo.host}}/{{shortId}}/{{claimInfo.name}}">facebook</a>
|
||||
<a class="link--primary" target="_blank" href="http://tumblr.com/widgets/share/tool?canonicalUrl={{claimInfo.host}}/{{shortId}}/{{claimInfo.name}}">tumblr</a>
|
||||
<a class="link--primary" target="_blank" href="https://www.reddit.com/submit?url={{claimInfo.host}}/{{shortId}}/{{claimInfo.name}}&title={{claimInfo.name}}">reddit</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div id="show-details" class="row--padded row--wide row--no-top" hidden="true">
|
||||
<div id="show-claim-name">
|
||||
<div class="column column--2 column--med-10">
|
||||
<span class="text">Claim Name:</span>
|
||||
</div><div class="column column--8 column--med-10">
|
||||
{{claimInfo.name}}
|
||||
</div>
|
||||
</div>
|
||||
<div id="show-claim-id">
|
||||
<div class="column column--2 column--med-10">
|
||||
<span class="text">Claim Id:</span>
|
||||
</div><div class="column column--8 column--med-10">
|
||||
{{claimInfo.claimId}}
|
||||
</div>
|
||||
</div>
|
||||
<div id="show-claim-id">
|
||||
<div class="column column--2 column--med-10">
|
||||
<span class="text">File Type:</span>
|
||||
</div><div class="column column--8 column--med-10">
|
||||
{{#if claimInfo.contentType}}
|
||||
{{claimInfo.contentType}}
|
||||
{{else}}
|
||||
unknown
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<div id="show-claim-id">
|
||||
<div class="column column--10">
|
||||
<a target="_blank" href="https://lbry.io/dmca">Report</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row--wide">
|
||||
<a class="text link--primary" id="show-details-toggle" href="#" onclick="toggleSection(event)" data-status="closed">[more]</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleSection(event){
|
||||
event.preventDefault();
|
||||
var dataSet = event.target.dataset;
|
||||
var status = dataSet.status;
|
||||
var toggle = document.getElementById("show-details-toggle");
|
||||
var details = document.getElementById("show-details");
|
||||
if (status === "closed") {
|
||||
details.hidden = false;
|
||||
toggle.innerText = "[less]";
|
||||
toggle.dataset.status = "open";
|
||||
} else {
|
||||
details.hidden = true;
|
||||
toggle.innerText = "[more]";
|
||||
toggle.dataset.status = "closed";
|
||||
}
|
||||
}
|
||||
function copyToClipboard(event){
|
||||
var elementToCopy = event.target.dataset.elementtocopy;
|
||||
var element = document.getElementById(elementToCopy);
|
||||
var errorElement = 'input-error-copy-text' + elementToCopy;
|
||||
element.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
} catch (err) {
|
||||
validationFunctions.showError(errorElement, 'Oops, unable to copy');
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,39 +0,0 @@
|
|||
<form id="publish-channel-form">
|
||||
<p id="input-error-channel-name" class="info-message-placeholder info-message--failure"></p>
|
||||
<div class="row row--wide row--short">
|
||||
<div class="column column--3 column--sml-10">
|
||||
<label class="label" for="new-channel-name">Name:</label>
|
||||
</div><div class="column column--6 column--sml-10">
|
||||
<div class="input-text--primary flex-container--row flex-container--left-bottom">
|
||||
<span>@</span>
|
||||
<input type="text" name="new-channel-name" id="new-channel-name" class="input-text" placeholder="exampleChannelName" value="" oninput="validationFunctions.checkChannelName(event.target.value)">
|
||||
<span id="input-success-channel-name" class="info-message--success"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row row--wide row--short">
|
||||
<div class="column column--3 column--sml-10">
|
||||
<label class="label" for="new-channel-password">Password:</label>
|
||||
</div><div class="column column--6 column--sml-10">
|
||||
<div class="input-text--primary">
|
||||
<input type="password" name="new-channel-password" id="new-channel-password" class="input-text" placeholder="" value="" >
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row--wide">
|
||||
<button class="button--primary" onclick="publishNewChannel(event)">Create Channel</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
<div id="channel-publish-in-progress" hidden="true">
|
||||
<p>Creating your new channel. This may take a few seconds...</p>
|
||||
{{> progressBar}}
|
||||
</div>
|
||||
|
||||
<div id="channel-publish-done" hidden="true">
|
||||
<p>Your channel has been successfully created!</p>
|
||||
</div>
|
||||
|
||||
<script src="/assets/js/createChannelFunctions.js"></script>
|
|
@ -1,28 +0,0 @@
|
|||
<form id="channel-login-form">
|
||||
<p id="login-error-display-element" class="info-message-placeholder info-message--failure"></p>
|
||||
<div class="row row--wide row--short">
|
||||
<div class="column column--3 column--sml-10">
|
||||
<label class="label" for="channel-login-name-input">Name:</label>
|
||||
</div><div class="column column--6 column--sml-10">
|
||||
<div class="input-text--primary flex-container--row flex-container--left-bottom">
|
||||
<span>@</span>
|
||||
<input type="text" id="channel-login-name-input" class="input-text" placeholder="" value="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row row--wide row--short">
|
||||
<div class="column column--3 column--sml-10">
|
||||
<label class="label" for="channel-login-password-input" >Password:</label>
|
||||
</div><div class="column column--6 column--sml-10">
|
||||
<div class="input-text--primary">
|
||||
<input type="password" id="channel-login-password-input" class="input-text" placeholder="" value="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row row--wide">
|
||||
<button class="button--primary" onclick="loginToChannel(event)">Authenticate</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script src="/assets/js/loginFunctions.js"></script>
|
|
@ -1,15 +0,0 @@
|
|||
<div class='row row--wide'>
|
||||
<div class="column column--3 align-content-top">
|
||||
<a href="{{this.showUrlLong}}">
|
||||
{{#ifConditional this.contentType '===' 'video/mp4'}}
|
||||
<img class="content-list-item-asset" src="{{this.thumbnail}}"/>
|
||||
{{else}}
|
||||
<img class="content-list-item-asset" src="{{this.directUrlLong}}" />
|
||||
{{/ifConditional}}
|
||||
</a>
|
||||
</div><div class="column column--7 align-content-top">
|
||||
<p>{{this.title}}</p>
|
||||
<a class="link--primary" href="{{this.showUrlShort}}">spee.ch{{this.showUrlShort}}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue