Merge pull request #343 from lbryio/react-router

React router
This commit is contained in:
Bill Bittner 2018-02-14 23:02:16 -08:00 committed by GitHub
commit 16cb329e53
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
111 changed files with 2043 additions and 1580 deletions

View file

@ -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)

View file

@ -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);
})

View file

@ -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;
},

View file

@ -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 = {};

View file

@ -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));
}

View file

@ -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,
};
},
};

View file

@ -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');
},
};

View file

@ -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},

View file

@ -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({

View file

@ -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",

View file

@ -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

View file

@ -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();
})
};
};

View file

@ -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);
}
})
}

View file

@ -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');
}

View file

@ -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');
}
})
}

View file

@ -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);
};
};

View file

@ -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 = "&#x2714";
},
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();
});
}
};

View file

@ -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,
},
};
};

View file

@ -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
View 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
View 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
View 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
View 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);
}

View file

@ -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')
)

View 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;

View 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);

View 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;

View 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);

View 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;

View 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;

View 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);

View file

@ -0,0 +1,11 @@
import React from 'react';
const AssetTitle = ({ title }) => {
return (
<div>
<span className="text--large">{title}</span>
</div>
);
};
export default AssetTitle;

View 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;

View 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;

View file

@ -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>
);
};

View file

@ -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;

View file

@ -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"

View 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;

View file

@ -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) &&

View 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);

View 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;

View 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);

View 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;

View 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);

View 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;

View file

@ -0,0 +1,4 @@
export const LOCAL_CHECK = 'LOCAL_CHECK';
export const UNAVAILABLE = 'UNAVAILABLE';
export const ERROR = 'ERROR';
export const AVAILABLE = 'AVAILABLE';

View 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';

View file

@ -0,0 +1,3 @@
export const CHANNEL = 'CHANNEL';
export const ASSET_LITE = 'ASSET_LITE';
export const ASSET_DETAILS = 'ASSET_DETAILS';

View 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);

View 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;

View file

@ -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 () {

View file

@ -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});
}
});
}

View file

@ -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 {

View 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);

View 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);

View file

@ -17,6 +17,9 @@ const mapDispatchToProps = dispatch => {
dispatch(updateLoggedInChannel(name, shortId, longId));
dispatch(updateSelectedChannel(name));
},
onChannelLogout: () => {
dispatch(updateLoggedInChannel(null, null, null));
},
};
};

View file

@ -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);

View file

@ -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);

View file

@ -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>
);
}

View file

@ -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 () {

View file

@ -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 () {

View 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);

View 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
View 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')
);

View file

@ -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;

View file

@ -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,
});

View file

@ -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
View 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
View 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
View 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
View 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
View 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);
};

View 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
View 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
View 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
View 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,
};
},
};

View file

@ -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);
});
}

View file

@ -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);
});
});
};

View file

@ -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'});
}
});
};

View file

@ -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');
});
};

View file

@ -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');
});
};

View file

@ -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');
});
}
});
};

View file

@ -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;

View file

@ -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;

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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}}

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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&lt;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>

View file

@ -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='&lt;video width="100%" controls poster="{{claimInfo.thumbnail}}" src="{{claimInfo.host}}/{{claimInfo.claimId}}/{{claimInfo.name}}.{{claimInfo.fileExt}}"/>&lt;/video>'/>
{{else}}
<input type="text" id="embed-text" class="input-disabled input-text--full-width" readonly onclick="select()" spellcheck="false" value='&lt;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>

View file

@ -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>

View file

@ -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>

View file

@ -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